diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index ddbfde1f7a5..00000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,72 +0,0 @@ -environment: - # This key is encrypted using secdev's appveyor private key, - # dissected only on master builds (not PRs) and is used during - # npcap OEM installation - npcap_oem_key: - secure: d120KTZBsVnzZ+pFPLPEOTOkyJxTVRjhbDJn9L+RYnM= - # Python versions that will be tested - # Note: it defines variables that can be used later - matrix: - - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.x" - PYTHON_ARCH: "64" - TOXENV: "py27-windows" - WINPCAP: "false" - UT_FLAGS: "" - - PYTHON: "C:\\Python37-x64" - PYTHON_VERSION: "3.7.x" - PYTHON_ARCH: "64" - TOXENV: "py37-windows" - WINPCAP: "false" - UT_FLAGS: "" - - PYTHON: "C:\\Python37-x64" - PYTHON_VERSION: "3.7.x" - PYTHON_ARCH: "64" - TOXENV: "py37-windows" - WINPCAP: "true" - UT_FLAGS: "-K tcpdump" - -# There is no build phase for Scapy -build: off - -install: - # Install the npcap, windump and wireshark suites - - ps: .\.config\appveyor\InstallNpcap.ps1 - - ps: .\.config\appveyor\InstallWindumpNpcap.ps1 - # Installs Wireshark 3.0 (and its dependencies) - # https://github.com/mkevenaar/chocolatey-packages/issues/16 - - choco install -n KB3033929 KB2919355 kb2999226 - - choco install -y wireshark - # Install Python modules - # https://github.com/tox-dev/tox/issues/791 - - "%PYTHON%\\python -m pip install virtualenv --upgrade" - - "%PYTHON%\\python -m pip install tox" - -# Compatibility run with Winpcap -# XXX Remove me when wireshark stops using it as default -for: - - - matrix: - only: - - WINPCAP: "true" - install: - # Install the winpcap and wireshark suites - - choco install -y winpcap - # See above for explanations - - choco install -n KB3033929 KB2919355 kb2999226 - - choco install -y wireshark - # Install Python modules - - "%PYTHON%\\python -m pip install virtualenv --upgrade" - - "%PYTHON%\\python -m pip install tox" - -test_script: - # Set environment variables - - set PYTHONPATH=%APPVEYOR_BUILD_FOLDER% - - set PATH=%APPVEYOR_BUILD_FOLDER%;C:\Program Files\Wireshark\;C:\Program Files\Windump\;%PATH% - - # Main unit tests - - "%PYTHON%\\python -m tox -- %UT_FLAGS%" - -after_test: - # Run codecov - - "%PYTHON%\\python -m tox -e codecov" diff --git a/.config/appveyor/InstallNpcap.ps1 b/.config/appveyor/InstallNpcap.ps1 deleted file mode 100644 index 23d774bbc91..00000000000 --- a/.config/appveyor/InstallNpcap.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -# Install Npcap on the machine. - -# Config: -$npcap_oem_file = "npcap-0.99-r9-oem.exe" - -# Note: because we need the /S option (silent), this script has two cases: -# - The script is runned from a master build, then use the secure variable 'npcap_oem_key' which will be available -# to decode the very recent npcap install oem file and use it -# - The script is runned from a PR, then use the provided archived 0.96 version, which is the last public one to -# provide support for the /S option - -Try -{ - # Check that the key is defined (build mode) - $user, $pass = (Get-ChildItem Env:npcap_oem_key).Value.replace("`"", "").split(",") - if(!$user -Or !$pass){ - Throw (New-Object System.Exception) - } - $file = $PSScriptRoot+"\"+$npcap_oem_file - # Download oem file using (super) secret credentials - $pair = "${user}:${pass}" - $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) - $basicAuthValue = "Basic $encodedCreds" - $headers = @{ Authorization = $basicAuthValue } - $secpasswd = ConvertTo-SecureString $pass -AsPlainText -Force - $credential = New-Object System.Management.Automation.PSCredential($user, $secpasswd) - Invoke-WebRequest -uri (-join("https://nmap.org/npcap/oem/dist/",$npcap_oem_file)) -OutFile $file -Headers $headers -Credential $credential -} -Catch -{ - $file = $PSScriptRoot+"\npcap-0.96.exe" - # Download the 0.96 file from nmap servers - wget "https://nmap.org/npcap/dist/npcap-0.96.exe" -UseBasicParsing -OutFile $file - # Now let's check its checksum - $_chksum = $(CertUtil -hashfile $file SHA256)[1] -replace " ","" - if ($_chksum -ne "83667e1306fdcf7f9967c10277b36b87e50ee8812e1ee2bb9443bdd065dc04a1"){ - echo "Checksums does NOT match !" - exit - } else { - echo "Checksums matches !" - } -} -echo "Installing:" -echo $file - -# Run installer -Start-Process $file -ArgumentList "/loopback_support=yes /S" -wait -echo "Npcap installation completed" diff --git a/.config/ci/install.ps1 b/.config/ci/install.ps1 new file mode 100644 index 00000000000..5f9e5b58443 --- /dev/null +++ b/.config/ci/install.ps1 @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# Install packages needed for the CI on Windows + +# Install npcap and windump +& "$PSScriptRoot\windows\InstallNpcap.ps1" +& "$PSScriptRoot\windows\InstallWindumpNpcap.ps1" + +# Install wireshark +choco install -y wireshark + +# Add to PATH +echo "C:\Program Files\Wireshark;C:\Program Files\Windump" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + +# Update pip & setuptools & wheel (tox uses those) +python -m pip install --upgrade pip setuptools wheel --ignore-installed + +# Make sure tox is installed and up to date +python -m pip install -U tox --ignore-installed diff --git a/.config/ci/install.sh b/.config/ci/install.sh index 8bc97753a0e..5bfeb12aeb2 100755 --- a/.config/ci/install.sh +++ b/.config/ci/install.sh @@ -1,36 +1,56 @@ #!/bin/bash + +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# Install packages needed for the CI on Linux/MacOS +# Usage: +# ./install.sh [install mode] + +# Detect install mode +if [[ "${1}" == "libpcap" ]] +then + SCAPY_USE_LIBPCAP="yes" + if [[ ! -z "$GITHUB_ACTIONS" ]] + then + echo "SCAPY_USE_LIBPCAP=yes" >> $GITHUB_ENV + fi +fi + # Install on osx -if [ "$OSTYPE" = "darwin"* ] || [ "$TRAVIS_OS_NAME" = "osx" ] +if [ "${OSTYPE:0:6}" = "darwin" ] then - if [ ! -z $SCAPY_USE_PCAPDNET ] + if [ ! -z $SCAPY_USE_LIBPCAP ] then brew update - brew install libdnet libpcap + brew install libpcap fi fi -# Install wireshark data, ifconfig & vcan -if [ "$OSTYPE" = "linux-gnu" ] || [ "$TRAVIS_OS_NAME" = "linux" ] -then - sudo apt-get update - sudo apt-get -qy install tshark net-tools - sudo apt-get -qy install can-utils build-essential linux-headers-$(uname -r) linux-modules-extra-$(uname -r); -fi +CUR=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) -# Make sure libpcap is installed -if [ ! -z $SCAPY_USE_PCAPDNET ] +# Install wireshark data, ifconfig, vcan, samba, openldap +if [ "$OSTYPE" = "linux-gnu" ] then - $SCAPY_SUDO apt-get -qy install libpcap-dev + sudo apt-get update + sudo apt-get -qy install tshark net-tools || exit 1 + sudo apt-get -qy install can-utils || exit 1 + sudo apt-get -qy install linux-modules-extra-$(uname -r) || exit 1 + sudo apt-get -qy install samba smbclient + sudo bash $CUR/openldap/install.sh + # Make sure libpcap is installed + if [ ! -z $SCAPY_USE_LIBPCAP ] + then + sudo apt-get -qy install libpcap-dev || exit 1 + fi fi # Update pip & setuptools (tox uses those) -python -m pip install --upgrade pip setuptools --ignore-installed +python -m pip install --upgrade pip setuptools wheel --ignore-installed # Make sure tox is installed and up to date python -m pip install -U tox --ignore-installed -# Make sure brotli is installed and up to date -python -m pip install -U brotli --ignore-installed - # Dump Environment (so that we can check PATH, UT_FLAGS, etc.) set diff --git a/.config/ci/openldap/config.ldif b/.config/ci/openldap/config.ldif new file mode 100644 index 00000000000..48df480744c --- /dev/null +++ b/.config/ci/openldap/config.ldif @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy + +# Contains the configuration of our OpenLDAP test server + +# Configure LDAPS +dn: cn=config +changetype: modify +add: olcTLSCACertificateFile +olcTLSCACertificateFile: {{CAFILE}} + +dn: cn=config +changetype: modify +replace: olcTLSCertificateKeyFile +olcTLSCertificateKeyFile: {{KEYFILE}} + +dn: cn=config +changetype: modify +replace: olcTLSCertificateFile +olcTLSCertificateFile: {{CRTFILE}} + +dn: cn=config +changetype: modify +add: olcTLSVerifyClient +olcTLSVerifyClient: never + +# Set channel bindings to 'tls-endpoint', like it would be on Windows +dn: cn=config +changetype: modify +replace: olcSaslCbinding +olcSaslCbinding: tls-endpoint diff --git a/.config/ci/openldap/install.sh b/.config/ci/openldap/install.sh new file mode 100755 index 00000000000..cbc8870fc8a --- /dev/null +++ b/.config/ci/openldap/install.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# Install an OpenLDAP test server + +# Pre-populate some setup questions +sudo debconf-set-selections <<< 'slapd slapd/password2 password Bonjour1' +sudo debconf-set-selections <<< 'slapd slapd/password1 password Bonjour1' +sudo debconf-set-selections <<< 'slapd slapd/domain string scapy.net' + +# Run setup +sudo apt-get -qy install slapd + +# Enable LDAPs +echo "Enabling HTTPS on slapd..." +sudo sed -i '/^SLAPD_SERVICES/ c\SLAPD_SERVICES="ldap:/// ldapi:/// ldaps://"' /etc/default/slapd +sudo systemctl restart slapd + +# Calculate the paths we're going to need. +CUR=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +PKIPATH=$(realpath "$CUR/../../../test/scapy/layers/tls/pki") +OLDAPPATH=$(mktemp -d -t scapy_openldap_XXXX) + +# Copy certificates to temp path +cp ${PKIPATH}/ca_cert.pem ${OLDAPPATH} +cp ${PKIPATH}/srv_cert.pem ${OLDAPPATH} +cp ${PKIPATH}/srv_key.pem ${OLDAPPATH} +chmod a+rx -R ${OLDAPPATH} + +# Copy config template and replace variables. +echo "Creating OpenLDAP config..." +openldap_conf=${OLDAPPATH}/openldap_config.ldif +cp $CUR/config.ldif $openldap_conf +sed -i "s@{{CAFILE}}@${OLDAPPATH}/ca_cert.pem@g" $openldap_conf +sed -i "s@{{CRTFILE}}@${OLDAPPATH}/srv_cert.pem@g" $openldap_conf +sed -i "s@{{KEYFILE}}@${OLDAPPATH}/srv_key.pem@g" $openldap_conf + +echo "Applying OpenLDAP config..." +sudo ldapmodify -Y EXTERNAL -H "ldapi:///" -w Bonjour1 -f $openldap_conf -c +echo "Adding initial dummy data..." +sudo ldapadd -D "cn=admin,dc=scapy,dc=net" -w Bonjour1 -H "ldapi:///" -f $CUR/testdata.ldif -c diff --git a/.config/ci/openldap/testdata.ldif b/.config/ci/openldap/testdata.ldif new file mode 100644 index 00000000000..63c150b4b64 --- /dev/null +++ b/.config/ci/openldap/testdata.ldif @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: OLDAP-2.8 +# This file is based on https://git.openldap.org/openldap/openldap/-/blob/master/tests/data/ppolicy.ldif?ref_type=heads +# (renamed to dc=scapy, dc=net) + +dn: dc=scapy, dc=net +objectClass: top +objectClass: organization +objectClass: dcObject +o: Scapy +dc: scapy + +dn: ou=People, dc=scapy, dc=net +objectClass: top +objectClass: organizationalUnit +ou: People + +dn: ou=Groups, dc=scapy, dc=net +objectClass: organizationalUnit +ou: Groups + +dn: cn=Policy Group, ou=Groups, dc=scapy, dc=net +objectClass: groupOfNames +cn: Policy Group +member: uid=nd, ou=People, dc=scapy, dc=net +owner: uid=ndadmin, ou=People, dc=scapy, dc=net + +dn: cn=Test Group, ou=Groups, dc=scapy, dc=net +objectClass: groupOfNames +cn: Policy Group +member: uid=another, ou=People, dc=scapy, dc=net + +dn: ou=Policies, dc=scapy, dc=net +objectClass: top +objectClass: organizationalUnit +ou: Policies + +dn: uid=nd, ou=People, dc=scapy, dc=net +objectClass: top +objectClass: person +objectClass: inetOrgPerson +cn: Neil Dunbar +uid: nd +sn: Dunbar +givenName: Neil +userPassword: testpassword + +dn: uid=ndadmin, ou=People, dc=scapy, dc=net +objectClass: top +objectClass: person +objectClass: inetOrgPerson +cn: Neil Dunbar (Admin) +uid: ndadmin +sn: Dunbar +givenName: Neil +userPassword: testpw + +dn: uid=another, ou=People, dc=scapy, dc=net +objectClass: top +objectClass: person +objectClass: inetOrgPerson +cn: Another Test +uid: another +sn: Test +givenName: Another +userPassword: testing + diff --git a/.config/ci/openssl.py b/.config/ci/openssl.py new file mode 100755 index 00000000000..58baa138c76 --- /dev/null +++ b/.config/ci/openssl.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Create a duplicate of the OpenSSL config to be able to use TLS < 1.2 +This returns the path to this new config file. +""" + +import os +import re +import subprocess +import tempfile + +# Get OpenSSL config file +OPENSSL_DIR = re.search( + b"OPENSSLDIR: \"(.*)\"", + subprocess.Popen( + ["openssl", "version", "-d"], + stdout=subprocess.PIPE + ).communicate()[0] +).group(1).decode() +OPENSSL_CONFIG = os.path.join(OPENSSL_DIR, 'openssl.cnf') + +# https://www.openssl.org/docs/manmaster/man5/config.html +DATA = b""" +openssl_conf = openssl_init + +[openssl_init] +ssl_conf = ssl_configuration + +[ssl_configuration] +system_default = tls_system_default + +[tls_system_default] +MinProtocol = TLSv1 +CipherString = DEFAULT:@SECLEVEL=0 +Options = UnsafeLegacyRenegotiation +""".strip() + +# Copy and edit +with tempfile.NamedTemporaryFile(suffix=".cnf", delete=False) as fd: + fd.write(DATA) + print(fd.name) diff --git a/.config/ci/test.ps1 b/.config/ci/test.ps1 new file mode 100644 index 00000000000..0e6fc39c87c --- /dev/null +++ b/.config/ci/test.ps1 @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# test.ps1 +# Usage: +# ./test.ps1 +# Examples: +# ./test.sh 3.13 + +if ($args.Count -eq 0) { + Write-Host "Usage: .\test.ps1 " + exit +} + +# Set TOXENV +$PY_VERSION = "py" + ($args[0] -replace '\.', '') +$env:TOXENV = $PY_VERSION + "-windows-root" + +if ($env:GITHUB_ACTIONS) { + # Due to a security policy, the firewall of the Azure runner + # (Standard_DS2_v2) that runs Github Actions on Linux blocks ICMP. + $env:UT_FLAGS += " -K icmp_firewall" +} + +# Launch Scapy unit tests +python -m tox -- @($env:UT_FLAGS.Trim() -split ' ') diff --git a/.config/ci/test.sh b/.config/ci/test.sh index beae06885c5..baa2e4e1b5f 100755 --- a/.config/ci/test.sh +++ b/.config/ci/test.sh @@ -1,37 +1,75 @@ #!/bin/bash +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + # test.sh # Usage: -# ./test.sh [tox version] [both/root/non_root (default root)] -# Example: +# ./test.sh [python version] [both/root/non_root (default root)] +# Examples: # ./test.sh 3.7 both +# ./test.sh 3.9 non_root -if [ "$OSTYPE" = "linux-gnu" ] || [ "$TRAVIS_OS_NAME" = "linux" ] +if [ "$OSTYPE" = "linux-gnu" ] then # Linux OSTOX="linux" - UT_FLAGS=" -K tshark" # TODO: also test as root ? - # check vcan - sudo modprobe -n -v vcan - if [[ $? -ne 0 ]] + UT_FLAGS+=" -K tshark" + if [ -z "$SIMPLE_TESTS" ] then - # The vcan module is currently unavailable on Travis-CI xenial builds + # check vcan + sudo modprobe -n -v vcan + if [[ $? -ne 0 ]] + then + # The vcan module is currently unavailable on xenial builds + UT_FLAGS+=" -K vcan_socket" + fi + else UT_FLAGS+=" -K vcan_socket" fi -elif [ "$OSTYPE" = "darwin"* ] || [ "$TRAVIS_OS_NAME" = "osx" ] +elif [[ "$OSTYPE" = "darwin"* ]] || [[ "$OSTYPE" = "FreeBSD" ]] || [[ "$OSTYPE" = *"bsd"* ]] then - OSTOX="osx" - UT_FLAGS=" -K tcpdump" + OSTOX="bsd" + # Travis CI in macOS 10.13+ can't load kexts. Need this for tuntaposx. + UT_FLAGS+=" -K tun -K tap" + if [[ "$OSTYPE" = "openbsd"* ]] + then + # Note: LibreSSL 3.6.* does not support X25519 according to + # the cryptogaphy module source code + UT_FLAGS+=" -K libressl" + fi +fi + +if [ ! -z "$GITHUB_ACTIONS" ] +then + # Due to a security policy, the firewall of the Azure runner + # (Standard_DS2_v2) that runs Github Actions on Linux blocks ICMP. + UT_FLAGS+=" -K icmp_firewall" fi # pypy if python --version 2>&1 | grep -q PyPy then UT_FLAGS+=" -K not_pypy" + # Code coverage with PyPy makes it very, very slow. Tests work + # but take around 30minutes, so we disable it. + export DISABLE_COVERAGE=" " +fi + +# macos -k scanner has glitchy coverage. skip it +if [ "$OSTOX" = "bsd" ] && [[ "$UT_FLAGS" = *"-k scanner"* ]]; then + export DISABLE_COVERAGE=" " +fi + +# libpcap +if [[ ! -z "$SCAPY_USE_LIBPCAP" ]]; then + UT_FLAGS+=" -K veth" fi # Create version tag (github actions) PY_VERSION="py${1//./}" +PY_VERSION=${PY_VERSION/pypypy/pypy} TESTVER="$PY_VERSION-$OSTOX" # Chose whether to run root or non_root @@ -51,29 +89,48 @@ if [ -z $TOXENV ] then case ${SCAPY_TOX_CHOSEN} in both) - TOXENV="${TESTVER}_non_root,${TESTVER}_root" + export TOXENV="${TESTVER}-non_root,${TESTVER}-root" ;; root) - TOXENV="${TESTVER}_root" + export TOXENV="${TESTVER}-root" ;; *) - TOXENV="${TESTVER}_non_root" + export TOXENV="${TESTVER}-non_root" ;; esac fi -# Dump vars (the others were already dumped in install.sh) +# Configure OpenSSL +export OPENSSL_CONF=$(${PYTHON:=python} `dirname $BASH_SOURCE`/openssl.py) + +# Dump vars (environment is already entirely dumped in install.sh) +echo OSTOX=$OSTOX echo UT_FLAGS=$UT_FLAGS echo TOXENV=$TOXENV +echo OPENSSL_CONF=$OPENSSL_CONF +echo OPENSSL_VER=$(openssl version) +echo COVERAGE=$([ -z "$DISABLE_COVERAGE" ] && echo "enabled" || echo "disabled") + +if [ "$OSTYPE" = "linux-gnu" ] +then + echo SMBCLIENT=$(smbclient -V) +fi # Launch Scapy unit tests -tox -- ${UT_FLAGS} +# export TOX_PARALLEL_NO_SPINNER=1 +tox -- ${UT_FLAGS} || exit 1 + +# Stop if NO_BASH_TESTS is set +if [ ! -z "$SIMPLE_TESTS" ] +then + exit $? +fi # Start Scapy in interactive mode TEMPFILE=$(mktemp) -cat << EOF > ${TEMPFILE} +cat < "${TEMPFILE}" print("Scapy on %s" % sys.version) sys.exit() EOF -./run_scapy -H -c ${TEMPFILE} -rm ${TEMPFILE} +echo "DEBUG: TEMPFILE=${TEMPFILE}" +./run_scapy -H -c "${TEMPFILE}" || exit 1 diff --git a/.config/ci/windows/InstallNpcap.ps1 b/.config/ci/windows/InstallNpcap.ps1 new file mode 100644 index 00000000000..347b7eb2420 --- /dev/null +++ b/.config/ci/windows/InstallNpcap.ps1 @@ -0,0 +1,78 @@ +# Install Npcap on the machine. + +# Config: +$npcap_oem_file = "npcap-1.60-oem.exe" +$npcap_oem_hash = "91e076eb9a197d55ca5e05b240e8049cd97ced3455eb7e7cb0f06066b423eb77" + +# Note: because we need the /S option (silent), this script has two cases: +# - The script is runned from a master build, then use the secure variable 'npcap_oem_key' which will be available +# to decode the very recent npcap install oem file and use it +# - The script is runned from a PR, then use the provided archived 0.96 version, which is the last public one to +# provide support for the /S option + +function checkTheSum($file, $hash) { + $_chksum = $(CertUtil -hashfile $file SHA256)[1] -replace " ","" + if ($_chksum -ne $hash){ + Write-Error "Checksums do NOT match !" + return 1, $file + } + return 0, $file +} + +function DownloadNPCAP_free { + $file = $PSScriptRoot+"\npcap-0.96.exe" + $hash = "83667e1306fdcf7f9967c10277b36b87e50ee8812e1ee2bb9443bdd065dc04a1" + # Download the 0.96 file from a copy :/ It was taken down from official servers. + Invoke-WebRequest "https://github.com/secdev/secdev.github.io/raw/refs/heads/master/public/ci/npcap-0.96.exe" -UseBasicParsing -OutFile $file + return checkTheSum $file $hash +} + +function DownloadNPCAP_oem { + # Unpack the key + $user, $pass = (Get-ChildItem Env:npcap_oem_key).Value.replace("`"", "").split(",") + if(!$user -Or !$pass){ + Throw (New-Object System.Exception) + } + $file = $PSScriptRoot+"\"+$npcap_oem_file + # Download oem file using (super) secret credentials + $pair = "${user}:${pass}" + $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) + $basicAuthValue = "Basic $encodedCreds" + $headers = @{ Authorization = $basicAuthValue } + $secpasswd = ConvertTo-SecureString $pass -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential($user, $secpasswd) + try { + Invoke-WebRequest -uri (-join("https://npcap.com/oem/dist/",$npcap_oem_file)) -OutFile $file -Headers $headers -Credential $credential + } catch [System.Net.WebException],[System.IO.IOException] { + Write-Error "Error while dowloading npcap oem!" + Write-Warning $Error[0] + return 1, $file + } + return checkTheSum $file $npcap_oem_hash +} + +if (Test-Path Env:npcap_oem_key){ # Key is here: on master + $success, $file = DownloadNPCAP_oem + if ($success -ne 0){ + $success, $file = DownloadNPCAP_free + } +} else { # No key: PRs + $success, $file = DownloadNPCAP_free +} + +if ($success -ne 0){ + Write-Error ('Npcap installation of '+$file+' arborted !') + exit 1 +} + +Write-Output ('Installing: ' + $file) + +# Run installer +$process = Start-Process $file -ArgumentList "/loopback_support=yes /winpcap_mode=no /S" -PassThru -Wait +if($process.ExitCode -eq 0) { + echo "Npcap installation completed !" + exit 0 +} else { + Write-Error "Npcap installation failed !" + exit 1 +} diff --git a/.config/appveyor/InstallWindumpNpcap.ps1 b/.config/ci/windows/InstallWindumpNpcap.ps1 similarity index 92% rename from .config/appveyor/InstallWindumpNpcap.ps1 rename to .config/ci/windows/InstallWindumpNpcap.ps1 index 76977f3710d..b6b8c940d80 100644 --- a/.config/appveyor/InstallWindumpNpcap.ps1 +++ b/.config/ci/windows/InstallWindumpNpcap.ps1 @@ -5,7 +5,7 @@ $checksum = "4253cbc494416c4917920e1f2424cdf039af8bc39f839a47aa4337bd28f4eb7e" ############ ############ # Download the file -wget $urlPath -UseBasicParsing -OutFile $PSScriptRoot"\npcap.zip" +Invoke-WebRequest $urlPath -UseBasicParsing -OutFile $PSScriptRoot"\npcap.zip" Add-Type -AssemblyName System.IO.Compression.FileSystem function Unzip { diff --git a/.config/ci/zipapp.sh b/.config/ci/zipapp.sh new file mode 100755 index 00000000000..42e9f566474 --- /dev/null +++ b/.config/ci/zipapp.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# Build a zipapp for Scapy + +DIR=$(realpath "$(dirname "$0")/../../") +cd $DIR + +if [ ! -e "pyproject.toml" ]; then + echo "zipapp.sh was not able to find scapy's root folder" + exit 1 +fi + +MODE="$1" +if [ -z "$MODE" ] || ( [ "$MODE" != "full" ] && [ "$MODE" != "simple" ] ); then + echo "Usage: zipapp.sh " + exit 1 +fi + +if [ -z "$PYTHON" ] +then + PYTHON=${PYTHON:-python3} +fi + +# Get Scapy version +SCPY_VERSION=$(python3 -c "print(__import__('scapy').__version__)") + +# Get temp directory +TMPFLD="$(mktemp -d)" +if [ -z "$TMPFLD" ] || [ ! -d "$TMPFLD" ]; then + echo "Error: 'mktemp -d' failed" + exit 1 +fi +ARCH="$TMPFLD/archive" +SCPY="$TMPFLD/scapy" +mkdir "$ARCH" +mkdir "$SCPY" + +# Create git archive +echo "> Creating git archive..." +git archive HEAD -o "$ARCH/scapy.tar.gz" + +# Unpack the archive to a temporary directory +if [ ! -e "$ARCH/scapy.tar.gz" ]; then + echo "ERROR: git archive failed" + exit 1 +fi +echo "> Unpacking..." +tar -xf "$ARCH/scapy.tar.gz" -C "$SCPY" + +# Remove unnecessary files +echo "> Stripping down..." +cd "$SCPY" && find . -not \( \ + -wholename "./scapy*" -o \ + -wholename "./pyproject.toml" -o \ + -wholename "./doc/scapy.1" -o \ + -wholename "./setup.py" -o \ + -wholename "./README.md" -o \ + -wholename "./LICENSE" \ +\) -delete +cd $DIR + +# Depending on the mode, install dependencies and get DEST file +if [ "$MODE" == "full" ]; then + echo "> Bundling dependencies..." + $PYTHON -m pip install --quiet --target "$SCPY" IPython + DEST="./dist/scapy-full-$SCPY_VERSION.pyz" +else + DEST="./dist/scapy-$SCPY_VERSION.pyz" +fi + +if [ ! -d "./dist" ]; then + mkdir dist +fi + +# Copy version +echo "$SCPY_VERSION" > "./dist/version" + +# Build the zipapp +echo "> Building zipapp..." +$PYTHON -m zipapp \ + -o "$DEST" \ + -p "/usr/bin/env python3" \ + -m "scapy.main:interact" \ + -c \ + "$SCPY" + +# Cleanup +rm -rf "$TMPFLD" + +echo "Success. zipapp avaiable at $DEST" +stat $DEST | head -n 2 diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index cc318d530cb..7ed3f0caa78 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -1,20 +1,48 @@ +abd aci ans +applikation archtypes ba +browseable +byteorder cace cas +ciph +componet +comversion +cros +delt doas doubleclick +ether eventtypes fo +funktion +gost +hart iff +implementors +inout +interaktive +joinin +merchantibility +microsof mitre nd +negociate +optiona ot +potatoe referer +requestor +ro ser +singl +slac +synching te +temporaere tim ue uint @@ -22,4 +50,4 @@ vas wan wanna webp -gost +widgits diff --git a/.config/mypy/mypy.ini b/.config/mypy/mypy.ini index efe200f0bf7..7b17578099d 100644 --- a/.config/mypy/mypy.ini +++ b/.config/mypy/mypy.ini @@ -1,5 +1,24 @@ [mypy] +# Internal Scapy modules that we ignore + +[mypy-scapy.libs.winpcapy] +ignore_errors = True +ignore_missing_imports = True + +[mypy-scapy.libs.rfc3961] +warn_return_any = False + +# Layers specific config + +[mypy-scapy.arch.*] +implicit_reexport = True + +[mypy-scapy.layers.*,scapy.contrib.*] +warn_return_any = False + +# External libraries that we ignore + [mypy-IPython] ignore_missing_imports = True @@ -9,13 +28,11 @@ ignore_missing_imports = True [mypy-traitlets.config.loader] ignore_missing_imports = True -[mypy-scapy.modules.six,scapy.modules.six.moves,scapy.libs.winpcapy] -ignore_errors = True -ignore_missing_imports = True - [mypy-pyx] ignore_missing_imports = True [mypy-matplotlib.lines] ignore_missing_imports = True +[mypy-prompt_toolkit.*] +ignore_missing_imports = True diff --git a/.config/mypy/mypy_check.py b/.config/mypy/mypy_check.py index cb313e6e69d..371d93c3487 100644 --- a/.config/mypy/mypy_check.py +++ b/.config/mypy/mypy_check.py @@ -1,8 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter """ Performs Static typing checks over Scapy's codebase @@ -23,28 +22,97 @@ from mypy.main import main as mypy_main +# Check platform arg + +PLATFORM = None + +if len(sys.argv) >= 2: + if len(sys.argv) > 2: + print("Usage: mypy_check.py [platform]") + sys.exit(1) + PLATFORM = sys.argv[1] + # Load files -with io.open("./.config/mypy/mypy_enabled.txt") as fd: +localdir = os.path.split(__file__)[0] + +with io.open(os.path.join(localdir, "mypy_enabled.txt")) as fd: FILES = [l.strip() for l in fd.readlines() if l.strip() and l[0] != "#"] if not FILES: print("No files specified. Arborting") - sys.exit(0) + sys.exit(1) # Generate mypy arguments ARGS = [ - "--py2", - "--follow-imports=skip", - "--config-file=" + os.path.abspath( - os.path.join( - os.path.split(__file__)[0], - "mypy.ini" - ) + # strictness: same as --strict minus --disallow-subclassing-any + "--warn-unused-configs", + "--disallow-any-generics", + "--disallow-untyped-calls", + "--disallow-untyped-defs", + "--disallow-incomplete-defs", + "--check-untyped-defs", + "--disallow-untyped-decorators", + "--no-implicit-optional", + "--warn-redundant-casts", + "--warn-unused-ignores", + "--warn-return-any", + "--no-implicit-reexport", + "--strict-equality", + "--ignore-missing-imports", + # config + "--follow-imports=skip", # Remove eventually + "--config-file=" + os.path.abspath(os.path.join(localdir, "mypy.ini")), + "--show-traceback", +] + (["--platform=" + PLATFORM] if PLATFORM else []) + +if PLATFORM.startswith("linux"): + ARGS.extend( + [ + "--always-true=LINUX", + "--always-false=OPENBSD", + "--always-false=FREEBSD", + "--always-false=NETBSD", + "--always-false=DARWIN", + "--always-false=WINDOWS", + "--always-false=BSD", + ] ) -] + [os.path.abspath(f) for f in FILES] + FILES = [x for x in FILES if not x.startswith("scapy/arch/windows")] +elif PLATFORM.startswith("win32"): + ARGS.extend( + [ + "--always-false=LINUX", + "--always-false=OPENBSD", + "--always-false=FREEBSD", + "--always-false=NETBSD", + "--always-false=DARWIN", + "--always-true=WINDOWS", + "--always-false=WINDOWS_XP", + "--always-false=BSD", + ] + ) + FILES = [ + x + for x in FILES + if ( + x + not in { + # Disabled on Windows + "scapy/arch/unix.py", + "scapy/arch/solaris.py", + "scapy/contrib/cansocket_native.py", + "scapy/contrib/isotp/isotp_native_socket.py", + } + ) + and not x.startswith("scapy/arch/bpf") + and not x.startswith("scapy/arch/linux") + ] +else: + raise ValueError("Unknown platform") # Run mypy over the files +ARGS += [os.path.abspath(f) for f in FILES] -mypy_main(None, sys.stdout, sys.stderr, ARGS) +mypy_main(args=ARGS) diff --git a/.config/mypy/mypy_deployment_stats.py b/.config/mypy/mypy_deployment_stats.py new file mode 100644 index 00000000000..4f54433237f --- /dev/null +++ b/.config/mypy/mypy_deployment_stats.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Generate MyPy deployment stats +""" + +import os +import io +import glob +from collections import defaultdict + +# Parse config file + +localdir = os.path.split(__file__)[0] +rootpath = os.path.abspath(os.path.join(localdir, '../../')) + +with io.open(os.path.join(localdir, "mypy_enabled.txt")) as fd: + FILES = [l.strip() for l in fd.readlines() if l.strip() and l[0] != "#"] + +# Scan Scapy + +ALL_FILES = [ + "".join(x.partition("scapy/")[2:]) for x in + glob.iglob(os.path.join(rootpath, 'scapy/**/*.py'), recursive=True) +] + +# Process + +REMAINING = defaultdict(list) +MODULES = defaultdict(lambda: (0, 0, 0, 0)) + +for f in ALL_FILES: + with open(os.path.join(rootpath, f)) as fd: + lines = len(fd.read().split("\n")) + parts = f.split("/") + if len(parts) > 2: + mod = parts[1] + else: + mod = "[core]" + e, l, t, a = MODULES[mod] + if f in FILES: + e += lines + t += 1 + else: + REMAINING[mod].append(f) + l += lines + a += 1 + MODULES[mod] = (e, l, t, a) + +ENABLED = sum(x[0] for x in MODULES.values()) +TOTAL = sum(x[1] for x in MODULES.values()) + +print("**MyPy Support: %.2f%%**" % (ENABLED / TOTAL * 100)) +print("| Module | Typed code (lines) | Typed files |") +print("| --- | --- | --- |") +for mod, dat in MODULES.items(): + print("|`%s` | %.2f%% | %s/%s |" % (mod, dat[0] / dat[1] * 100, dat[2], dat[3])) + +print() +COREMODS = REMAINING["[core]"] +if COREMODS: + print("Core modules still untypes:") + for mod in COREMODS: + print("- `%s`" % mod) + diff --git a/.config/mypy/mypy_enabled.txt b/.config/mypy/mypy_enabled.txt index 6ee1cd1f47e..a5001e3509f 100644 --- a/.config/mypy/mypy_enabled.txt +++ b/.config/mypy/mypy_enabled.txt @@ -4,7 +4,104 @@ # Style cheet: https://mypy.readthedocs.io/en/latest/cheat_sheet.html +# CORE scapy/__init__.py +scapy/__main__.py +scapy/all.py +scapy/ansmachine.py +scapy/arch/__init__.py +scapy/arch/bpf/__init__.py +scapy/arch/bpf/consts.py +scapy/arch/bpf/core.py +scapy/arch/bpf/supersocket.py +scapy/arch/common.py +scapy/arch/libpcap.py +scapy/arch/linux/__init__.py +scapy/arch/linux/rtnetlink.py +scapy/arch/solaris.py +scapy/arch/unix.py +scapy/arch/windows/__init__.py +scapy/arch/windows/native.py +scapy/arch/windows/structures.py +scapy/as_resolvers.py +scapy/asn1/__init__.py +scapy/asn1/asn1.py +scapy/asn1/ber.py +scapy/asn1/mib.py +scapy/asn1fields.py +scapy/asn1packet.py +scapy/automaton.py +scapy/autorun.py +scapy/base_classes.py +scapy/compat.py +scapy/config.py +scapy/consts.py +scapy/dadict.py +scapy/data.py +scapy/error.py +scapy/fields.py +scapy/interfaces.py scapy/main.py -scapy/contrib/http2.py +scapy/packet.py +scapy/pipetool.py scapy/plist.py +scapy/pton_ntop.py +scapy/route.py +scapy/route6.py +scapy/scapypipes.py +scapy/sendrecv.py +scapy/sessions.py +scapy/supersocket.py +scapy/themes.py +scapy/utils.py +scapy/utils6.py +scapy/volatile.py + +# LAYERS +scapy/layers/can.py +scapy/layers/l2.py + +# CONTRIB +scapy/contrib/automotive/bmw/hsfz.py +scapy/contrib/automotive/doip.py +scapy/contrib/automotive/ecu.py +scapy/contrib/automotive/gm/gmlan_ecu_states.py +scapy/contrib/automotive/gm/gmlan_logging.py +scapy/contrib/automotive/gm/gmlan_scanner.py +scapy/contrib/automotive/gm/gmlanutils.py +scapy/contrib/automotive/kwp.py +scapy/contrib/automotive/obd/scanner.py +scapy/contrib/automotive/scanner/configuration.py +scapy/contrib/automotive/scanner/enumerator.py +scapy/contrib/automotive/scanner/executor.py +scapy/contrib/automotive/scanner/graph.py +scapy/contrib/automotive/scanner/staged_test_case.py +scapy/contrib/automotive/scanner/test_case.py +scapy/contrib/automotive/uds_ecu_states.py +scapy/contrib/automotive/uds_logging.py +scapy/contrib/automotive/uds_scan.py +scapy/contrib/cansocket_native.py +scapy/contrib/cansocket_python_can.py +#scapy/contrib/http2.py # needs to be fixed +scapy/contrib/isotp/isotp_native_socket.py +scapy/contrib/isotp/isotp_packet.py +scapy/contrib/isotp/isotp_scanner.py +scapy/contrib/isotp/isotp_soft_socket.py +scapy/contrib/isotp/isotp_utils.py +scapy/contrib/roce.py +scapy/contrib/tcpao.py + +# LIBS +scapy/libs/__init__.py +scapy/libs/ethertypes.py +scapy/libs/extcap.py +scapy/libs/matplot.py +scapy/libs/rfc3961.py +scapy/libs/structures.py +scapy/libs/test_pyx.py + +# TEST +test/testsocket.py + +# TOOLS +scapy/tools/automotive/isotpscanner.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..d1ab8e752f9 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,51 @@ +# This file contains the list of commits that should be excluded from +# git blame. Read more informations on: +# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view + +# PEPin - https://github.com/secdev/scapy/issues/1277 +# E231 - missing whitespace after ',' +e7365b2baeded1a0e1e3b59bc0ad14a78d6e3086 +# E30* - Incorrect number of blank lines +b770bbc58c26437b354c0bd21dc4e2fcfa3abfdf +# E20* - Incorrect number of whitespace +6861a35d8ed4466df7b2ff82341e60caf9ff869a +# E12* - visual indent +275ad3246b5231bb046a66bcfdf3654d67fdea20 +# W29* - useless whitespaces +453f2592f7b6f2b8677619769f8427932894dc1c +# E251 - unexpected spaces around keyword / parameter equals +203254afd771b42ccf0fcca96ba92dc4075cfe4a +# E26 - comments +b7a3db73dfd17ec1e7bbace8d52464982bf8ea8d +# E1 - incorrect indentation +f2f1de742aa36167e2c86247a26ed5e7393366ea +# F821 - undefined name 'name' +f8525ea9f17cedf148febcab8d1dab51ddca9afe +# E2* - whitespaces errors +1c2fe99c131bb05e009896410766371a2f870175 +# E71* - tests syntax +927c157b58918d5fdce9714a3c35627339cc8657 +# F841 - local variable 'name' is assigned to but never used +dbe409531a22d1245cf4669f72a425b42c83b0db +# PEPin several fixes +93232490193ca2b59e3b1425131913d28f408f7a +# E501 - line too long (> 79 characters) +e89d8965748439adc253714316de7a9a35b8bd73 +# F601 - dictionary key repeated with different values +0fd7d76550e56831f887664202d743846d3619dd +# F811 - redefinition of unused variable/class/... +10454d1ca243d0fd8d2ab4a148d688e3ea916e49 +# E402 - module level import not at top of file +0f4a904d2801e8bbbc82880345ad453ceb6ee34f +# E722 - do not use bare except +a35575ff22da176a8b515405faea9a689462da0c +# E741 - ambiguous variable name 'l' +7c61676aef950ca268eac480902dd91cb0abe3a4 +# F405 - variable/function/... may be undefined, or defined from star +8773983edb0336db7aa84777dee2aa9892508418 +# F401 - 'module' imported but unused +a58e1b90a704c394216a0b5a864a50931754bdf7 +# W502 - line break before binary operator +9687222c3f0af6ef89ecfe15e5b983e1f7b5b31e +# E275 - Missing whitespace after keyword +08b1f9d67c8e716fd44036a027bdc90dcb9fcfdf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 70258bbec6e..ade501c1b5e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [gpotter2, guedou, p-l-] +github: [gpotter2, guedou, p-l-, polybassa] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 94f93347fb9..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,38 +0,0 @@ -#### Things to consider - -1. Please check that you are using the latest scapy version, e.g. installed via: - - `pip install --upgrade git+git://github.com/secdev/scapy` - -2. If you are here to ask a question - please check previous issues and online resources, and consider using gitter instead: - -3. Please understand that **this is not a forum** but an issue tracker. The following article explains why you should limit questions asked on Github issues: - -#### Brief description - - - - -#### Environment - -- Scapy version: `scapy version and/or commit-hash` -- Python version: `e.g. 3.5` -- Operating System: `e.g. Minix 3.4` - - - -#### How to reproduce - - - -#### Actual result - - - -#### Expected result - - - -#### Related resources - - diff --git a/.github/ISSUE_TEMPLATE/BUGS.yml b/.github/ISSUE_TEMPLATE/BUGS.yml new file mode 100644 index 00000000000..d70a370ff30 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUGS.yml @@ -0,0 +1,73 @@ +name: Bug Report +description: File a bug report +body: + - type: markdown + attributes: + value: | + ### Things to consider + 1. Please check that you are using the **latest Scapy version**, e.g. installed via: + `pip install --upgrade git+https://github.com/secdev/scapy.git` + 2. If you are here to ask a question - please check previous issues and online resources, and consider using Gitter instead: + 3. Please understand that **this is not a forum** but an issue tracker. The following article explains why you should limit questions asked on Github issues: + + ***All bug reports must have at least one reproducible example.*** This may be a code snippet, a pcap file (zipped).. + - type: textarea + id: description + attributes: + label: Brief description + description: | + Describe the main issue in one sentence + If possible, describe what components / protocols could be affected by the issue (e.g. wrpcap() + IPv6, it is likely this also affects XXX) + validations: + required: true + - type: input + id: scapy_ver + attributes: + label: Scapy version + description: Give the Scapy version or the commit hash + placeholder: 2.4.5 + validations: + required: true + - type: input + id: py_ver + attributes: + label: Python version + placeholder: "3.8" + validations: + required: true + - type: input + id: os + attributes: + label: Operating system + placeholder: Linux 5.10.46 + validations: + required: true + - type: textarea + id: add_os + attributes: + label: Additional environment information + description: If needed - further information to get a picture of your setup (e.g. a sketch of your network setup) + validations: + required: false + - type: textarea + id: reproduce + attributes: + label: How to reproduce + description: Step-by-step explanation or a short script, may reference section 'Related resources' + validations: + required: true + - type: textarea + id: result + attributes: + label: Actual result + description: Dump results that outline the issue, please format your code + - type: textarea + id: expected_result + attributes: + label: Expected result + description: Describe the expected result and outline the difference to the actual one, could also be a screen shot (e.g. wireshark) + - type: textarea + id: resources + attributes: + label: Related resources + description: Traces / sample pcaps (stripped to the relevant frames), related standards, RFCs or other resources diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..88315b98cc4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Ask a question + url: https://gitter.im/secdev/scapy + about: Please ask and answer questions on Gitter. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index be300774b6f..51acbea8a29 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,20 @@ - + -**Checklist:** +**Checklist :** - [ ] If you are new to Scapy: I have checked [CONTRIBUTING.md](https://github.com/secdev/scapy/blob/master/CONTRIBUTING.md) (esp. section submitting-pull-requests) - [ ] I squashed commits belonging together - [ ] I added unit tests or explained why they are not relevant -- [ ] I executed the regression tests for Python2 and Python3 (using `tox` or, `cd test && ./run_tests_py2, cd test && ./run_tests_py3`) +- [ ] I executed the regression tests (using `tox`) - [ ] If the PR is still not finished, please create a [Draft Pull Request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) +- [ ] This PR uses (partially) AI-generated code. If so: + - [ ] I ensured the generated code follows the internal concepts of scapy + - [ ] This PR has a test coverage > 90% + - [ ] I reviewed every generated line + - [ ] If this PR contains more than 500 lines of code (excluding unit tests) I considered splitting this PR. + - [ ] I considered interoperability tests with existing packages or utilities to ensure conformity of a newly generated protocol + +**I understand that failing to mention the use of AI may result in a ban. (We do not forbid it, but you must play fair. Be warned !)** diff --git a/.codecov.yml b/.github/codecov.yml similarity index 86% rename from .codecov.yml rename to .github/codecov.yml index 13cea13ba0e..cb9392d9391 100644 --- a/.codecov.yml +++ b/.github/codecov.yml @@ -16,7 +16,11 @@ coverage: round: down status: # Only consider changes to the whole project - project: true + project: + default: + target: auto + threshold: 0.5% + base: auto patch: false changes: false diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 00000000000..64d933590a8 --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,39 @@ +name: CIFuzz + +on: + push: + branches: [master] + +permissions: + contents: read + +jobs: + Fuzzing: + runs-on: ubuntu-latest + if: github.repository == 'secdev/scapy' + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'scapy' + language: python + dry-run: false + allowed-broken-targets-percentage: 0 + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'scapy' + language: python + dry-run: false + fuzz-seconds: 300 + - name: Upload Crash + uses: actions/upload-artifact@v4 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 0abfd7c97ca..5e8873e3142 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -1,16 +1,31 @@ name: Scapy unit tests -on: [push, pull_request] +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + +# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !contains(github.ref, 'master')}} + +permissions: + contents: read jobs: health: + name: Code health check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout Scapy + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: "3.12" - name: Install tox run: pip install tox - name: Run flake8 tests @@ -19,45 +34,177 @@ jobs: run: tox -e spell - name: Run twine check run: tox -e twine + - name: Run gitarchive check + run: tox -e gitarchive docs: - runs-on: ubuntu-latest + # 'runs-on' and 'python-version' should match the ones defined in .readthedocs.yml + name: Build doc + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - name: Checkout Scapy + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: "3.12" - name: Install tox run: pip install tox - name: Build docs run: tox -e docs + spdx: + name: Check SPDX identifiers + runs-on: ubuntu-latest + steps: + - name: Checkout Scapy + uses: actions/checkout@v4 + - name: Launch script + run: bash scapy/tools/check_spdx.sh mypy: + name: Type hints check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout Scapy + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: "3.12" - name: Install tox run: pip install tox - name: Run mypy run: tox -e mypy -# Github Actions block ICMP. We can still use it for non root tests + utscapy: + name: ${{ matrix.os }} ${{ matrix.installmode }} ${{ matrix.python }} ${{ matrix.mode }} ${{ matrix.flags }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + continue-on-error: ${{ matrix.allow-failure == 'true' }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python: ["3.8", "3.9", "3.10", "3.11", "3.12"] + mode: [non_root] + installmode: [''] + flags: [" -K scanner"] + allow-failure: ['false'] + include: + # Python 3.7 + - os: ubuntu-22.04 + python: "3.7" + mode: non_root + flags: " -K scanner" + # Linux root tests on last version + - os: ubuntu-latest + python: "3.13" + mode: root + flags: " -K scanner" + # PyPy tests: root only + - os: ubuntu-latest + python: "pypy3.11" + mode: root + flags: " -K scanner" + # Libpcap test + - os: ubuntu-latest + python: "3.13" + mode: root + installmode: 'libpcap' + flags: " -K scanner" + # macOS tests + - os: macos-14 + python: "3.13" + mode: both + flags: " -K scanner" + # windows tests + - os: windows-latest + python: "3.13" + mode: root + flags: " -K scanner" + # Scanner tests + - os: ubuntu-latest + python: "3.13" + mode: root + allow-failure: 'true' + flags: " -k scanner" + - os: ubuntu-latest + python: "pypy3.11" + mode: root + flags: " -k scanner" + - os: macos-14 + python: "3.13" + mode: both + allow-failure: 'true' + flags: " -k scanner" + - os: windows-latest + python: "3.13" + mode: both + allow-failure: 'true' + flags: " -k scanner" + steps: + - name: Checkout Scapy + uses: actions/checkout@v4 + # Codecov requires a fetch-depth > 1 + with: + fetch-depth: 2 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install Tox and any other packages (linux/osx) + run: ./.config/ci/install.sh ${{ matrix.installmode }} + if: ${{ ! contains(matrix.os, 'windows') }} + - name: Install Tox and any other packages (win) + run: ./.config/ci/install.ps1 + if: ${{ contains(matrix.os, 'windows') }} + - name: Run Tox (linux/osx) + run: ./.config/ci/test.sh ${{ matrix.python }} ${{ matrix.mode }} + env: + UT_FLAGS: ${{ matrix.flags }} + if: ${{ ! contains(matrix.os, 'windows') }} + - name: Run Tox (win) + run: ./.config/ci/test.ps1 ${{ matrix.python }} + env: + UT_FLAGS: ${{ matrix.flags }} + if: ${{ contains(matrix.os, 'windows') }} + - name: Codecov + uses: codecov/codecov-action@v5 + continue-on-error: true + with: + token: ${{ secrets.CODECOV_TOKEN }} -# utscapy: -# runs-on: ubuntu-latest -# strategy: -# matrix: -# python: [2.7, pypy2, pypy3, 3.5, 3.6, 3.7, 3.8] -# steps: -# - uses: actions/checkout@v2 -# - name: Setup Python -# uses: actions/setup-python@v1 -# with: -# python-version: ${{ matrix.python }} -# - name: Install Tox and any other packages -# run: ./.config/ghci/install.sh -# - name: Run Tox -# run: ./.config/ghci/test.sh ${{ matrix.python }} non_root + cryptography: + name: pyca/cryptography test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install tox + run: pip install tox + # pyca/cryptography's CI installs cryptography + # then runs the tests. We therefore didn't include it in tox + - name: Install cryptography + run: pip install cryptography + - name: Run tests + run: tox -e cryptography + + # CODE-QL + analyze: + name: CodeQL analysis + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: 'python' + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore index e90fba94c72..97e2a8a8be6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,16 @@ dist/ build/ MANIFEST *.egg-info/ -scapy/VERSION test/*.html +.coverage* +coverage.xml .tox +.ipynb_checkpoints .mypy_cache +.vscode +.DS_Store +[.]venv/ +__pycache__/ doc/scapy/_build doc/scapy/api +.idea \ No newline at end of file diff --git a/.packit.yml b/.packit.yml new file mode 100644 index 00000000000..ac6da5c6283 --- /dev/null +++ b/.packit.yml @@ -0,0 +1,54 @@ +--- +# Docs: https://packit.dev/docs + +specfile_path: .packit_rpm/scapy.spec +files_to_sync: + - .packit.yml + - src: .packit_rpm/scapy.spec + dest: scapy.spec +upstream_package_name: scapy +downstream_package_name: scapy +upstream_tag_template: "v{version}" +srpm_build_deps: [] + +actions: + post-upstream-clone: + # Use the Fedora Rawhide specfile + - "git clone https://src.fedoraproject.org/rpms/scapy .packit_rpm --depth=1" + # Drop the "sources" file so rebase-helper doesn't think we're a dist-git + - "rm -fv .packit_rpm/sources" + # Drop all downstream patches to prevent them from interfering with upstream builds + - "sed -ri '/^Patch[0-9]+\\:.+\\.patch/d' .packit_rpm/scapy.spec" + - "sed -i '/^%check$/apip3 install scapy-rpc\\nOPENSSL_ENABLE_SHA1_SIGNATURES=1 OPENSSL_CONF=$(python3 ./.config/ci/openssl.py) ./test/run_tests -c test/configs/linux.utsc -K ci_only -K netaccess -K scanner' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: can-utils' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: libpcap' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: openssl' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: tcpdump' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: wireshark' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-ipython' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-brotli' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-can' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-cbor2' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-coverage' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-cryptography' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-tkinter' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-zstandard' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: samba' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: samba-client' .packit_rpm/scapy.spec" + +jobs: +- job: copr_build + trigger: pull_request + manual_trigger: true + enable_net: true + targets: + - fedora-latest-stable-aarch64 + - fedora-latest-stable-i386 + - fedora-latest-stable-ppc64le + - fedora-latest-stable-s390x + - fedora-latest-stable-x86_64 + - fedora-rawhide-aarch64 + - fedora-rawhide-i386 + - fedora-rawhide-ppc64le + - fedora-rawhide-s390x + - fedora-rawhide-x86_64 diff --git a/.readthedocs.yml b/.readthedocs.yml index 64d61351d9a..b4732b29e04 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,21 +1,30 @@ # Readthedocs config file. # See https://docs.readthedocs.io/en/stable/config-file/v2.html#supported-settings -# Copied from https://github.com/pycontribs/jira/blob/master/.readthedocs.yml version: 2 +sphinx: + configuration: doc/scapy/conf.py + formats: - epub - pdf build: - image: latest + os: ubuntu-22.04 + tools: + python: "3.12" + # To show the correct Scapy version, we must unshallow + # https://docs.readthedocs.io/en/stable/build-customization.html#unshallow-git-clone + jobs: + post_checkout: + - git fetch --unshallow || true +# https://docs.readthedocs.io/en/stable/config-file/v2.html#python python: - version: 3.7 install: - method: pip path: . extra_requirements: - - docs + - doc diff --git a/.travis.yml b/.travis.yml index 3fa091bdd9b..433f41d9a14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +dist: bionic # OpenSSL 1.1.1 cache: directories: - $HOME/.cache/pip @@ -6,70 +7,13 @@ cache: jobs: include: - # Run as root - - os: linux - python: 2.7 - env: - - TOXENV=py27-linux_non_root,py27-linux_root,codecov - - - os: linux - python: pypy - env: - - TOXENV=pypy-linux_non_root,pypy-linux_root,codecov - - - os: linux - python: pypy3 - env: - - TOXENV=pypy3-linux_non_root,pypy3-linux_root,codecov - - - os: linux - python: 3.5 - env: - - TOXENV=py35-linux_root,codecov - - - os: linux - python: 3.6 - env: - - TOXENV=py36-linux_root,codecov - - - os: linux - python: 3.7 - env: - - TOXENV=py37-linux_root,codecov - - - os: linux - python: 3.8 - env: - - TOXENV=py38-linux_non_root,py38-linux_root,codecov - - - os: osx - language: generic - env: - - TOXENV=py27-bsd_non_root,py27-bsd_root,codecov - - os: osx - language: generic - env: - - TOXENV=py36-bsd_non_root,py36-bsd_root,codecov - + # run custom root tests + # isotp - os: linux python: 3.8 env: - TOXENV=py38-isotp_kernel_module,codecov - - os: linux - python: 3.8 - env: - - SCAPY_USE_PCAPDNET=yes TOXENV=py38-linux_root,codecov - - # Other root tests - # Test scapy against all warnings - - os: linux - python: 3.8 - env: - - SCAPY_PY_OPTS="-Werror -X tracemalloc=25" TOXENV=py38-linux_root - allow_failures: - - env: SCAPY_PY_OPTS="-Werror -X tracemalloc=25" TOXENV=py38-linux_root - install: - bash .config/ci/install.sh - python -c "from scapy.all import conf; print(repr(conf))" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3565a23acd..7c1efcd9064 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,9 +64,9 @@ of function calls, packet creations, etc.). is a nice read! - Avoid creating unnecessary `list` objects, particularly if they - can be huge (e.g., when possible, use `scapy.modules.six.range()` instead of - `range()`, `for line in fdesc` instead of `for line in - fdesc.readlines()`; more generally prefer generators over lists). + can be huge (e.g., when possible, use `for line in fdesc` instead of + `for line in fdesc.readlines()`; more generally prefer generators over + lists). ### Tests @@ -112,24 +112,29 @@ parsed from a string (during a network capture or a PCAP file read). Adding inefficient code here will have a disastrous effect on Scapy's performances. -### Python 2 and 3 compatibility - -The project aims to provide code that works both on Python 2 and Python 3. Therefore, some rules need to be applied to achieve compatibility: - -- byte-string must be defined as `b"\x00\x01\x02"` -- exceptions must comply with the new Python 3 format: `except SomeError as e:` -- lambdas must be written using a single argument when using tuples: use `lambda x, y: x + f(y)` instead of `lambda (x, y): x + f(y)`. -- use int instead of long -- use list comprehension instead of map() and filter() -- use scapy.modules.six.moves.range instead of xrange and range -- use scapy.modules.six.itervalues(dict) instead of dict.values() or dict.itervalues() -- use scapy.modules.six.string_types instead of basestring -- `__bool__ = __nonzero__` must be used when declaring `__nonzero__` methods -- `__next__ = next` must be used when declaring `next` methods in iterators -- `StopIteration` must NOT be used in generators (but it can still be used in iterators) -- `io.BytesIO` must be used instead of `StringIO` when using bytes -- `__cmp__` must not be used. -- UserDict should be imported via `six.UserDict` +### Logging + +Scapy has an internal logging system based on `logging`. + +In the past, Scapy was generally too verbose on packet dissection, +leading many new users to disable all logs, which makes it harder for them +to find real issues afterwards. You should comply with these guidelines to +make sure logging in Scapy remains helpful. + +- If you want the log message to only be displayed when using Scapy through + the interactive console, use `scapy.error.log_interactive`. You are free to + use any log level. +- Otherwise, always use `scapy.error.log_runtime`. + - On **packet dissection**, of *packet layers* + you should remain **AT OR BELOW the `logging.INFO` level**, unless the + issue is critical or tied to security. + For instance: "DNS Decompression loop detected !" is allowed as WARNING, + but "Could not dissect packet" or "Invalid value detected" are not. + - On **packet build** or **any command** or function that is called by the + user or the root program, you are **free and welcomed** to use the WARNING + or ERROR levels, to signal that a packet was wrongly built for instance. +- If you are working on Scapy's core, you may use: `scapy.error.log_loading` + only while Scapy is loading, to display import errors for instance. ### Code review diff --git a/MANIFEST.in b/MANIFEST.in index cdf9a3018f0..4ce295f4f62 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include MANIFEST.in include LICENSE include run_scapy -include scapy/VERSION +prune test diff --git a/README b/README deleted file mode 120000 index 42061c01a1c..00000000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -README.md \ No newline at end of file diff --git a/README.md b/README.md index 20db19ce3f4..0a9b17ae4b5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,11 @@ -

- -

- -# Scapy +# Scapy   Scapy -[![Travis Build Status](https://travis-ci.com/secdev/scapy.svg?branch=master)](https://travis-ci.com/secdev/scapy) -[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/os03daotfja0wtp7/branch/master?svg=true)](https://ci.appveyor.com/project/secdev/scapy/branch/master) +[![Scapy unit tests](https://github.com/secdev/scapy/actions/workflows/unittests.yml/badge.svg?branch=master&event=push)](https://github.com/secdev/scapy/actions/workflows/unittests.yml?query=event%3Apush) [![Codecov Status](https://codecov.io/gh/secdev/scapy/branch/master/graph/badge.svg)](https://codecov.io/gh/secdev/scapy) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/30ee6772bb264a689a2604f5cdb0437b)](https://www.codacy.com/app/secdev/scapy) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/30ee6772bb264a689a2604f5cdb0437b)](https://app.codacy.com/gh/secdev/scapy/dashboard) [![PyPI Version](https://img.shields.io/pypi/v/scapy.svg)](https://pypi.python.org/pypi/scapy/) -[![Python Versions](https://img.shields.io/pypi/pyversions/scapy.svg)](https://pypi.python.org/pypi/scapy/) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](LICENSE) [![Join the chat at https://gitter.im/secdev/scapy](https://badges.gitter.im/secdev/scapy.svg)](https://gitter.im/secdev/scapy) @@ -31,7 +25,7 @@ handle, like sending invalid frames, injecting your own 802.11 frames, combining techniques (VLAN hopping+ARP cache poisoning, VoIP decoding on WEP protected channel, ...), etc. -Scapy supports Python 2.7 and Python 3 (3.4 to 3.8). It's intended to +Scapy supports Python 3.7+. It's intended to be cross platform, and runs on many different platforms (Linux, OSX, \*BSD, and Windows). @@ -42,7 +36,7 @@ For further details, please head over to [Getting started with Scapy](https://sc ### Shell demo -![Scapy install demo](https://secdev.github.io/img/animation-scapy-install.svg) +![Scapy install demo](https://secdev.github.io/files/doc/animation-scapy-install.svg) Scapy can easily be used as an interactive shell to interact with the network. The following example shows how to send an ICMP Echo Request message to @@ -70,8 +64,7 @@ Other useful resources: - [Scapy in 20 minutes](https://github.com/secdev/scapy/blob/master/doc/notebooks/Scapy%20in%2015%20minutes.ipynb) - [Interactive tutorial](https://scapy.readthedocs.io/en/latest/usage.html#interactive-tutorial) (part of the documentation) -- [The quick demo: an interactive session](https://scapy.readthedocs.io/en/latest/introduction.html#quick-demo) -(some examples may be outdated) +- [The quick demo: an interactive session](https://scapy.readthedocs.io/en/latest/introduction.html#quick-demo) (some examples may be outdated) - [HTTP/2 notebook](https://github.com/secdev/scapy/blob/master/doc/notebooks/HTTP_2_Tuto.ipynb) - [TLS notebooks](https://github.com/secdev/scapy/blob/master/doc/notebooks/tls) @@ -97,6 +90,11 @@ follow the instructions to install them. +## License + +Scapy's code, tests and tools are licensed under GPL v2. +The documentation (everything unless marked otherwise in `doc/`, and except the logo) is licensed under CC BY-NC-SA 2.5. + ## Contributing Want to contribute? Great! Please take a few minutes to diff --git a/doc/LICENSE b/doc/LICENSE new file mode 100644 index 00000000000..d560622633d --- /dev/null +++ b/doc/LICENSE @@ -0,0 +1,55 @@ +Creative Commons Attribution-NonCommercial-ShareAlike 2.5 + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + + 1. Definitions + a. "Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with a number of other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License. + b. "Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License. + c. "Licensor" means the individual or entity that offers the Work under the terms of this License. + d. "Original Author" means the individual or entity who created the Work. + e. "Work" means the copyrightable work of authorship offered under the terms of this License. + f. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. + g. "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, Noncommercial, ShareAlike. + 2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws. + 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: + a. to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works; + b. to create and reproduce Derivative Works; + c. to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works; + d. to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works; + + The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved, including but not limited to the rights set forth in Sections 4(e) and 4(f). + 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: + a. You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any credit as required by clause 4(d), as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any credit as required by clause 4(d), as requested. + b. You may distribute, publicly display, publicly perform, or publicly digitally perform a Derivative Work only under the terms of this License, a later version of this License with the same License Elements as this License, or a Creative Commons iCommons license that contains the same License Elements as this License (e.g. Attribution-NonCommercial-ShareAlike 2.5 Japan). You must include a copy of, or the Uniform Resource Identifier for, this License or other license specified in the previous sentence with every copy or phonorecord of each Derivative Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Derivative Works that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder, and You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Derivative Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Derivative Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Derivative Work itself to be made subject to the terms of this License. + c. You may not exercise any of the rights granted to You in Section 3 above in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation. The exchange of the Work for other copyrighted works by means of digital file-sharing or otherwise shall not be considered to be intended for or directed toward commercial advantage or private monetary compensation, provided there is no payment of any monetary compensation in connection with the exchange of copyrighted works. + d. If you distribute, publicly display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or (ii) if the Original Author and/or Licensor designate another party or parties (e.g. a sponsor institute, publishing entity, journal) for attribution in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). Such credit may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear where any other comparable authorship credit appears and in a manner at least as prominent as such other comparable authorship credit. + e. For the avoidance of doubt, where the Work is a musical composition: + i. Performance Royalties Under Blanket Licenses. Licensor reserves the exclusive right to collect, whether individually or via a performance rights society (e.g. ASCAP, BMI, SESAC), royalties for the public performance or public digital performance (e.g. webcast) of the Work if that performance is primarily intended for or directed toward commercial advantage or private monetary compensation. + ii. Mechanical Rights and Statutory Royalties. Licensor reserves the exclusive right to collect, whether individually or via a music rights agency or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions), if Your distribution of such cover version is primarily intended for or directed toward commercial advantage or private monetary compensation. + f. Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor reserves the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions), if Your public digital performance is primarily intended for or directed toward commercial advantage or private monetary compensation. + 5. Representations, Warranties and Disclaimer + + UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + 7. Termination + a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works or Collective Works from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. + 8. Miscellaneous + a. Each time You distribute or publicly digitally perform the Work or a Collective Work, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. + b. Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. + +Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, neither party will use the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. + +Creative Commons may be contacted at http://creativecommons.org/. + diff --git a/doc/generate_docs.bat b/doc/generate_docs.bat deleted file mode 100644 index a9c7c98322e..00000000000 --- a/doc/generate_docs.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -cd .. -tox -e docs2 \ No newline at end of file diff --git a/doc/notebooks/Scapy in 15 minutes.ipynb b/doc/notebooks/Scapy in 15 minutes.ipynb index 7735524bafd..57befdb3ffb 100644 --- a/doc/notebooks/Scapy in 15 minutes.ipynb +++ b/doc/notebooks/Scapy in 15 minutes.ipynb @@ -20,7 +20,7 @@ "source": [ "[Scapy](http://www.secdev.org/projects/scapy) is a powerful Python-based interactive packet manipulation program and library. It can be used to forge or decode packets for a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more.\n", "\n", - "This iPython notebook provides a short tour of the main Scapy features. It assumes that you are familiar with networking terminology. All examples where built using the development version from [https://github.com/secdev/scapy](https://github.com/secdev/scapy), and tested on Linux. They should work as well on OS X, and other BSD.\n", + "This iPython notebook provides a short tour of the main Scapy features. It assumes that you are familiar with networking terminology. All examples were built using the development version from [https://github.com/secdev/scapy](https://github.com/secdev/scapy), and tested on Linux. They should work as well on OS X, and other BSD.\n", "\n", "The current documentation is available on [http://scapy.readthedocs.io/](http://scapy.readthedocs.io/) !" ] @@ -56,7 +56,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "2_ Adanced firewalking using IP options is sometimes useful to perform network enumeration. Here is more complicate one-liner:" + "2_ Advanced firewalking using IP options is sometimes useful to perform network enumeration. Here is a more complicated one-liner:" ] }, { @@ -89,7 +89,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Now that, we've got your attention, let's start the tutorial !" + "#### Now that we've got your attention, let's start the tutorial !" ] }, { @@ -103,7 +103,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The easiest way to try Scapy is to clone the github repository, then launch the `run_scapy` script as root. The following examples can be pasted on the Scapy prompt. There is no need to install any external Python modules." + "The easiest way to try Scapy is to clone the github repository, then launch the `run_scapy` script as root. The following examples can be pasted at the Scapy prompt. There is no need to install any external Python modules." ] }, { @@ -239,7 +239,7 @@ "source": [ "There are not many differences with the previous example. However, Scapy used the specific destination to perform some magic tricks !\n", "\n", - "Using internal mechanisms (such as DNS resolution, routing table and ARP resolution), Scapy has automatically set fields necessary to send the packet. This fields can of course be accessed and displayed." + "Using internal mechanisms (such as DNS resolution, routing table and ARP resolution), Scapy has automatically set fields necessary to send the packet. These fields can of course be accessed and displayed." ] }, { @@ -334,7 +334,7 @@ "source": [ "Currently, you know how to build packets with Scapy. The next step is to send them over the network !\n", "\n", - "The `sr1()` function sends a packet and return the corresponding answer. `srp1()` does the same for layer two packets, i.e. Ethernet. If you are only interested in sending packets `send()` is your friend.\n", + "The `sr1()` function sends a packet and returns the corresponding answer. `srp1()` does the same for layer two packets, i.e. Ethernet. If you are only interested in sending packets `send()` is your friend.\n", "\n", "As an example, we can use the DNS protocol to get www.example.com IPv4 address." ] @@ -485,7 +485,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Sniffing the network is a straightforward as sending and receiving packets. The `sniff()` function returns a list of Scapy packets, that can be manipulated as previously described." + "Sniffing the network is as straightforward as sending and receiving packets. The `sniff()` function returns a list of Scapy packets, that can be manipulated as previously described." ] }, { @@ -1136,7 +1136,7 @@ " rep /= Dot11Elt(ID=\"Rates\",info=b'\\x82\\x84\\x0b\\x16\\x96')\n", " rep /= Dot11Elt(ID=\"DSset\",info=chr(10))\n", "\n", - " OK,return rep\n", + " return rep\n", "\n", "# Start the answering machine\n", "#ProbeRequest_am()() # uncomment to test" diff --git a/doc/notebooks/tls/notebook3_tls_compromised.ipynb b/doc/notebooks/tls/notebook3_tls_compromised.ipynb index 8c0e5d3ba5d..6b9351d6544 100644 --- a/doc/notebooks/tls/notebook3_tls_compromised.ipynb +++ b/doc/notebooks/tls/notebook3_tls_compromised.ipynb @@ -4,119 +4,264 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# The lack of PFS: a danger to privacy" + "# The lack of PFS: a danger to privacy\n", + "\n", + "With TLS 1.2 and earlier, some cipher suites do not provide Perfect Forward Secrecy. Without this property, an attacker compromising the server private key can easily decrypt TLS traffic.\n", + "\n", + "In the following example, Scapy is used to decrypt a comunication made without PFS using the ciphersuite `TLS_RSA_WITH_AES_128_CBC_SHA`, giving the server private key stored in `raw_data/pki/srv_key.pem`." ] }, { + "metadata": {}, "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], "source": [ "from scapy.all import *\n", "load_layer('tls')" - ] + ], + "outputs": [], + "execution_count": null }, { + "metadata": {}, "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], "source": [ "record1_str = open('raw_data/tls_session_compromised/01_cli.raw', 'rb').read()\n", "record1 = TLS(record1_str)\n", "record1.msg[0].show()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false, "scrolled": true }, - "outputs": [], "source": [ "record2_str = open('raw_data/tls_session_compromised/02_srv.raw', 'rb').read()\n", "record2 = TLS(record2_str, tls_session=record1.tls_session.mirror())\n", "record2.msg[0].show()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "metadata": {}, "source": [ - "# Suppose we possess the private key of the server\n", - "# Try registering it to the session\n", - "#key = PrivKey('raw_data/pki/srv_key.pem')\n", - "#record2.tls_session.server_rsa_key = key" - ] + "# Supposing that the private key of the server was stolen,\n", + "# the traffic can be decoded by registering it to the Scapy TLS session\n", + "key = PrivKey('raw_data/pki/srv_key.pem')\n", + "record2.tls_session.server_rsa_key = key" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "metadata": {}, "source": [ "record3_str = open('raw_data/tls_session_compromised/03_cli.raw', 'rb').read()\n", "record3 = TLS(record3_str, tls_session=record2.tls_session.mirror())\n", "record3.show()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "metadata": {}, "source": [ "record4_str = open('raw_data/tls_session_compromised/04_srv.raw', 'rb').read()\n", "record4 = TLS(record4_str, tls_session=record3.tls_session.mirror())\n", "record4.show()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "metadata": {}, "source": [ + "# This is the first TLS Record containing user data. If decryption works,\n", + "# you should see the string \"To boldly go where no man has gone before...\" in plaintext.\n", "record5_str = open('raw_data/tls_session_compromised/05_cli.raw', 'rb').read()\n", "record5 = TLS(record5_str, tls_session=record4.tls_session.mirror())\n", "record5.show()" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Decrypting TLS Traffic Protected with PFS\n", + "\n", + "When PFS is in action, the only way to break TLS 1.2 is to possess decryption keys. They can be retrieved by dumping the process memory, or making the TLS library to write then into a [NSS Key Log](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format) (as allowed by OpenSSL, Chrome or Firefox).\n", + "\n", + "The data used in the following examples was retrieved the following commands:\n", + "```\n", + "cd doc/notebooks/tls/raw_data/\n", + "\n", + "# Start a TLS Server using the s_server\n", + "sudo openssl s_server -accept localhost:443 -cert pki/srv_cert.pem -key pki/srv_key.pem -WWW\n", + "\n", + "# Sniff the network and write packets to a file\n", + "sudo tcpdump -i lo -w tls_nss_example.pcap port 443\n", + "\n", + "# Connect to the server using TLS 1.2 and TLS 1.3, and write the keys to a file\n", + "echo -e \"GET /pki/srv_key.pem HTTP/1.0\\r\\n\" | openssl s_client -connect localhost:443 -keylogfile tls_nss_example.keys.txt -tls1_2 -ign_eof\n", + "echo -e \"GET /pki/srv_key.pem HTTP/1.0\\r\\n\" | openssl s_client -connect localhost:443 -keylogfile tls_nss_example.keys.txt -tls1_3 -ign_eof\n", + "```\n", + "\n", + "## Decrypt a PCAP files\n", + "\n", + "Scapy can parse NSS Key logs, and use the cryptographic material to decrypt TLS traffic from a pcap file." ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "load_layer(\"tls\")\n", + "\n", + "conf.tls_session_enable = True\n", + "conf.tls_nss_filename = \"raw_data/tls_nss_example.keys.txt\"\n", + "\n", + "packets = sniff(offline=\"raw_data/tls_nss_example.pcap\", session=TCPSession)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Display the TLS1.2 HTTP GET query\n", + "packets[9][TLS].show()" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Display the answer containing the secret\n", + "packets[10][TLS].show()" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Display the TLS1.3 HTTP GET query\n", + "packets[27][TLS13].show()" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Display the answer containing the secret\n", + "packets[28][TLS13].show()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Decrypt Manually\n", + "\n", + "Internally, the `conf.tls_session_enable` parameter makes Scapy follows TCP records, such as Client Hello or Server Hello, and updates `tlsSession` objects.\n", + "\n", + "Scapy inner behavior is illustrated by the following example." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Read packets from a pcap\n", + "load_layer(\"tls\")\n", + "\n", + "conf.tls_session_enable = False\n", + "packets = rdpcap(\"raw_data/tls_nss_example.pcap\")\n", + "\n", + "# Load the keys from a NSS Key Log\n", + "nss_keys = load_nss_keys(\"raw_data/tls_nss_example.keys.txt\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Parse the Client Hello message from its raw bytes. This configures a new tlsSession object\n", + "client_hello = TLS(raw(packets[3][TLS]))\n", + "\n", + "# Parse the Server Hello message, using the mirrored client_hello tlsSession object\n", + "server_hello = TLS(raw(packets[5][TLS]), tls_session=client_hello.tls_session.mirror())\n", + "\n", + "# Configure the TLS master secret retrieved from the NSS Key Log\n", + "server_hello.tls_session.master_secret = nss_keys[\"CLIENT_RANDOM\"][client_hello.tls_session.client_random]\n", + "server_hello.tls_session.compute_ms_and_derive_keys()\n", + "\n", + "# Parse remaining TLS messages\n", + "client_finished = TLS(raw(packets[7][TLS]), tls_session=server_hello.tls_session.mirror())\n", + "server_finished = TLS(raw(packets[8][TLS]), tls_session=client_finished.tls_session.mirror())" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Display the HTTP GET query\n", + "http_query = TLS(raw(packets[9][TLS]), tls_session=server_finished.tls_session.mirror())\n", + "http_query.show()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Display the answer containing the secret\n", + "http_response = TLS(raw(packets[10][TLS]), tls_session=http_query.tls_session.mirror())\n", + "http_response.show()" + ], + "outputs": [], + "execution_count": null } ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.13" + "pygments_lexer": "ipython3", + "version": "3.9.1" } }, "nbformat": 4, diff --git a/doc/notebooks/tls/notebook4_tls13.ipynb b/doc/notebooks/tls/notebook4_tls13.ipynb index 84a72211c32..da27d2e6b9b 100644 --- a/doc/notebooks/tls/notebook4_tls13.ipynb +++ b/doc/notebooks/tls/notebook4_tls13.ipynb @@ -10,16 +10,22 @@ "\"Handshake" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dissecting the handshake" + ] + }, { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from scapy.all import *\n", - "load_layer('tls')" + "load_layer('tls')\n", + "conf.logLevel = logging.INFO" ] }, { @@ -28,6 +34,7 @@ "metadata": {}, "outputs": [], "source": [ + "# ClientHello\n", "record1_str = open('raw_data/tls_session_13/01_cli.raw', 'rb').read()\n", "record1 = TLS(record1_str)\n", "sess = record1.tls_session\n", @@ -40,38 +47,42 @@ "metadata": {}, "outputs": [], "source": [ - "record2_str = open('raw_data/tls_session_13/02_srv.raw', 'rb').read()\n", - "record2 = TLS(record2_str, tls_session=sess.mirror())\n", - "record2.show()" + "# The PFS relies on the ECDH secret below being kept from observers, and deleted right after the key exchange\n", + "from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey\n", + "\n", + "# Used in records 2-6 + 8\n", + "x25519_client_privkey = open('raw_data/tls_session_13/cli_key.raw', 'rb').read()\n", + "sess.tls13_client_privshares[\"x25519\"] = X25519PrivateKey.from_private_bytes(x25519_client_privkey)\n", + "\n", + "# Used in records 7 + 9\n", + "x25519_server_privkey = open('raw_data/tls_session_13/srv_key.raw', 'rb').read()\n", + "sess.tls13_server_privshare[\"x25519\"] = X25519PrivateKey.from_private_bytes(x25519_server_privkey)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ - "record3_str = open('raw_data/tls_session_13/03_cli.raw', 'rb').read()\n", - "record3 = TLS(record3_str, tls_session=sess.mirror())\n", - "record3.show()" + "# ServerHello + ChangeCipherSpec (middlebox compatibility)\n", + "record2_str = open('raw_data/tls_session_13/02_srv.raw', 'rb').read()\n", + "record2 = TLS(record2_str, tls_session=sess.mirror())\n", + "record2.show()" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ - "# The PFS relies on the ECDH secret below being kept from observers, and deleted right after the key exchange\n", - "#from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateNumbers\n", - "#from cryptography.hazmat.backends import default_backend\n", - "#secp256r1_client_privkey = open('raw_data/tls_session_13/cli_key.raw', 'rb').read()\n", - "#pubnum = sess.tls13_client_pubshares[\"secp256r1\"].public_numbers()\n", - "#privnum = EllipticCurvePrivateNumbers(pkcs_os2ip(secp256r1_client_privkey), pubnum)\n", - "#privkey = privnum.private_key(default_backend())\n", - "#sess.tls13_client_privshares[\"secp256r1\"] = privkey" + "# Encrypted Extensions\n", + "record3_str = open('raw_data/tls_session_13/03_srv.raw', 'rb').read()\n", + "record3 = TLS(record3_str, tls_session=sess)\n", + "record3.show()" ] }, { @@ -82,8 +93,9 @@ }, "outputs": [], "source": [ + "# Certificate\n", "record4_str = open('raw_data/tls_session_13/04_srv.raw', 'rb').read()\n", - "record4 = TLS(record4_str, tls_session=sess.mirror())\n", + "record4 = TLS(record4_str, tls_session=sess)\n", "record4.show()" ] }, @@ -93,6 +105,7 @@ "metadata": {}, "outputs": [], "source": [ + "# Certificate verify\n", "record5_str = open('raw_data/tls_session_13/05_srv.raw', 'rb').read()\n", "record5 = TLS(record5_str, tls_session=sess)\n", "record5.show()" @@ -104,11 +117,57 @@ "metadata": {}, "outputs": [], "source": [ - "record6_str = open('raw_data/tls_session_13/06_cli.raw', 'rb').read()\n", - "record6 = TLS(record6_str, tls_session=sess.mirror())\n", + "# Finished\n", + "record6_str = open('raw_data/tls_session_13/06_srv.raw', 'rb').read()\n", + "record6 = TLS(record6_str, tls_session=sess)\n", "record6.show()" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Client ChangeCipherSpec (middlebox compatibility) + Finished\n", + "record7_str = open('raw_data/tls_session_13/07_cli.raw', 'rb').read()\n", + "record7 = TLS(record7_str, tls_session=sess.mirror())\n", + "record7.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dissecting some data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Client application data\n", + "record8_str = open('raw_data/tls_session_13/08_cli.raw', 'rb').read()\n", + "record8 = TLS(record8_str, tls_session=sess)\n", + "record8.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Server application data\n", + "record9_str = open('raw_data/tls_session_13/09_srv.raw', 'rb').read()\n", + "record9 = TLS(record9_str, tls_session=sess.mirror())\n", + "record9.show()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -122,21 +181,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.13" + "pygments_lexer": "ipython3", + "version": "3.10.5" } }, "nbformat": 4, diff --git a/doc/notebooks/tls/raw_data/README.md b/doc/notebooks/tls/raw_data/README.md new file mode 100644 index 00000000000..b089aaa045b --- /dev/null +++ b/doc/notebooks/tls/raw_data/README.md @@ -0,0 +1,3 @@ +This folder is used in the notebook and in some tests. + +Files in this folder are therefore cross licensed under both GPLv2 and CC-BY-NC-SA 2.5. diff --git a/doc/notebooks/tls/raw_data/tls_nss_example.keys.txt b/doc/notebooks/tls/raw_data/tls_nss_example.keys.txt new file mode 100644 index 00000000000..6cf32f6e662 --- /dev/null +++ b/doc/notebooks/tls/raw_data/tls_nss_example.keys.txt @@ -0,0 +1,7 @@ +# SSL/TLS secrets log file, generated by OpenSSL +CLIENT_RANDOM 216e876ea1a480c60145c4c80eb8d05c85b6806043105c391236cd4e88f79a21 54a828bfc25edf47070cd48b8253e8137e88082face8d7e96960756653b57f41bc6df3f45a5746bc9c6305ccd9b35ab8 +SERVER_HANDSHAKE_TRAFFIC_SECRET 74ef95570af6a305910ee6cb0f98fc5bcec0c5d5dffe5f293ae9a4d7ba2110f2 5f2fd60aecc80ee54d17d48ec58fcfccf6fe229e08055dba1a6a09297bea98fd1268bdd6fe19e15c76d7c152d17f7237 +EXPORTER_SECRET 74ef95570af6a305910ee6cb0f98fc5bcec0c5d5dffe5f293ae9a4d7ba2110f2 02aa67e90b524002f7eb00fcda23365ca6bfea5ad179d965264b5c1f6ff93483465b3c147c5070a90e47a406bd431152 +SERVER_TRAFFIC_SECRET_0 74ef95570af6a305910ee6cb0f98fc5bcec0c5d5dffe5f293ae9a4d7ba2110f2 c5f265aee5d17472c71fa889cfa351b12b9280bf74d16477161fd495c87432632908cae923e390d5d52a4719c2f896de +CLIENT_HANDSHAKE_TRAFFIC_SECRET 74ef95570af6a305910ee6cb0f98fc5bcec0c5d5dffe5f293ae9a4d7ba2110f2 bf58ee2a720cb26a594c0c7b714783a406f4daad18fbf7b7b3437bfe944d840cbc0e1843096e1c4ec92b68f230b22fa9 +CLIENT_TRAFFIC_SECRET_0 74ef95570af6a305910ee6cb0f98fc5bcec0c5d5dffe5f293ae9a4d7ba2110f2 7f3ac59f48dbe7f0fa66f92a0e691cf6ad4b84062e66b303f3149107c723ffb8424f8a3488072a8938d842b403e43229 diff --git a/doc/notebooks/tls/raw_data/tls_nss_example.pcap b/doc/notebooks/tls/raw_data/tls_nss_example.pcap new file mode 100644 index 00000000000..9268ae4866c Binary files /dev/null and b/doc/notebooks/tls/raw_data/tls_nss_example.pcap differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/01_cli.raw b/doc/notebooks/tls/raw_data/tls_session_13/01_cli.raw index 720269449ed..dae6d676159 100644 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/01_cli.raw and b/doc/notebooks/tls/raw_data/tls_session_13/01_cli.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/02_srv.raw b/doc/notebooks/tls/raw_data/tls_session_13/02_srv.raw index 3c119721eb9..a44e0450df8 100644 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/02_srv.raw and b/doc/notebooks/tls/raw_data/tls_session_13/02_srv.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/03_cli.raw b/doc/notebooks/tls/raw_data/tls_session_13/03_cli.raw deleted file mode 100644 index b526cd708ef..00000000000 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/03_cli.raw and /dev/null differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/03_srv.raw b/doc/notebooks/tls/raw_data/tls_session_13/03_srv.raw new file mode 100644 index 00000000000..c0ae12ac8bf Binary files /dev/null and b/doc/notebooks/tls/raw_data/tls_session_13/03_srv.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/04_srv.raw b/doc/notebooks/tls/raw_data/tls_session_13/04_srv.raw index 469e99d5d40..55358cc0b4b 100644 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/04_srv.raw and b/doc/notebooks/tls/raw_data/tls_session_13/04_srv.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/05_srv.raw b/doc/notebooks/tls/raw_data/tls_session_13/05_srv.raw index 03253fcc347..42a5a2311d2 100644 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/05_srv.raw and b/doc/notebooks/tls/raw_data/tls_session_13/05_srv.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/06_cli.raw b/doc/notebooks/tls/raw_data/tls_session_13/06_cli.raw deleted file mode 100644 index 127631c031d..00000000000 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/06_cli.raw and /dev/null differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/06_srv.raw b/doc/notebooks/tls/raw_data/tls_session_13/06_srv.raw new file mode 100644 index 00000000000..f889afc1455 Binary files /dev/null and b/doc/notebooks/tls/raw_data/tls_session_13/06_srv.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/07_cli.raw b/doc/notebooks/tls/raw_data/tls_session_13/07_cli.raw new file mode 100644 index 00000000000..f98f681c0aa Binary files /dev/null and b/doc/notebooks/tls/raw_data/tls_session_13/07_cli.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/07_srv.raw b/doc/notebooks/tls/raw_data/tls_session_13/07_srv.raw deleted file mode 100644 index 99f5f8dd12e..00000000000 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/07_srv.raw and /dev/null differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/08_cli.raw b/doc/notebooks/tls/raw_data/tls_session_13/08_cli.raw index 7d2a14f107e..179980edb3b 100644 Binary files a/doc/notebooks/tls/raw_data/tls_session_13/08_cli.raw and b/doc/notebooks/tls/raw_data/tls_session_13/08_cli.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/09_srv.raw b/doc/notebooks/tls/raw_data/tls_session_13/09_srv.raw new file mode 100644 index 00000000000..41d9c01d7bb Binary files /dev/null and b/doc/notebooks/tls/raw_data/tls_session_13/09_srv.raw differ diff --git a/doc/notebooks/tls/raw_data/tls_session_13/cli_key.raw b/doc/notebooks/tls/raw_data/tls_session_13/cli_key.raw index 46873d5457b..de91279c9fe 100644 --- a/doc/notebooks/tls/raw_data/tls_session_13/cli_key.raw +++ b/doc/notebooks/tls/raw_data/tls_session_13/cli_key.raw @@ -1 +1 @@ -úHÑSÉÿØÿ`¡6&]9Ÿ©ñ-vmB¦ÈN \ No newline at end of file + !"#$%&'()*+,-./0123456789:;<=>? \ No newline at end of file diff --git a/doc/notebooks/tls/raw_data/tls_session_13/srv_key.raw b/doc/notebooks/tls/raw_data/tls_session_13/srv_key.raw new file mode 100644 index 00000000000..643390219c4 --- /dev/null +++ b/doc/notebooks/tls/raw_data/tls_session_13/srv_key.raw @@ -0,0 +1 @@ +‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯ \ No newline at end of file diff --git a/doc/scapy.1 b/doc/scapy.1 index c385039a890..4811055d459 100644 --- a/doc/scapy.1 +++ b/doc/scapy.1 @@ -1,4 +1,5 @@ -.TH SCAPY 1 "May 8, 2018" +\" SPDX-License-Identifier: GPL-2.0-only +.TH SCAPY 1 "March 24, 2024" .SH NAME scapy \- Interactive packet manipulation tool .SH SYNOPSIS @@ -16,10 +17,7 @@ arping, tcpdump, tshark, p0f, ... .PP \fBScapy\fP uses the Python interpreter as a command board. That means that you can use directly Python language (assign variables, use loops, -define functions, etc.) If you give a file a parameter when you run -\fBScapy\fP, your session (variables, functions, instances, ...) will be saved -when you leave the interpreter and restored the next time you launch -\fBScapy\fP. +define functions, etc.) .PP The idea is simple. Those kinds of tools do two things : sending packets and receiving answers. That's what \fBScapy\fP does : you define a set of @@ -41,20 +39,20 @@ Options for Scapy are: \fB\-h\fR display usage .TP +\fB\-H\fR +header-less mode, also reduces verbosity. +.TP \fB\-d\fR increase log verbosity. Can be used many times. .TP -\fB\-s\fR FILE -use FILE to save/load session values (variables, functions, instances, ...) -.TP \fB\-p\fR PRESTART_FILE -use PRESTART_FILE instead of $HOME/.scapy_prestart.py as pre-startup file +use PRESTART_FILE instead of $HOME/.config/scapy/prestart.py as pre-startup file .TP \fB\-P\fR do not run prestart file .TP \fB\-c\fR STARTUP_FILE -use STARTUP_FILE instead of $HOME/.scapy_startup.py as startup file +use STARTUP_FILE instead of $HOME/.config/scapy/startup.py as startup file .TP \fB\-C\fR do not run startup file @@ -67,11 +65,6 @@ lists supported protocol layers. If a protocol layer is given as parameter, lists its fields and types of fields. If a string is given as parameter, it is used to filter the layers. .TP -\fBexplore()\fR -explores available protocols. -Allows to look for a layer or protocol through an interactive GUI. -If a Scapy module is given as parameter, explore this specific module. -.TP \fBlsc()\fR lists scapy's main user commands. .TP @@ -79,19 +72,22 @@ lists scapy's main user commands. this object contains the configuration. .SH FILES -\fB$HOME/.scapy_prestart.py\fR +\fB$HOME/.config/scapy/prestart.py\fR This file is run before Scapy core is loaded. Only the \fBconf\fP object -is available. This file can be used to manipulate \fBconf.load_layers\fP -list to choose which layers will be loaded: +is available. This file can be used to configure the CLI, configure +parameters such as the \fBconf.load_layers\fP list to choose which layers +will be loaded, or change the logging level (for instance): .nf +conf.interactive_shell = "bpython" +log_loading.setLevel(logging.WARNING) conf.load_layers.remove("bluetooth") conf.load_layers.append("new_layer") .fi -\fB$HOME/.scapy_startup.py\fR +\fB$HOME/.config/scapy/startup.py\fR This file is run after Scapy is loaded. It can be used to configure -some of the Scapy behaviors: +more of Scapy behaviors, like un-registering layers: .nf conf.prog.pdfreader = "xpdf" @@ -100,8 +96,8 @@ split_layers(UDP,DNS) .SH EXAMPLES -More verbose examples are available in the documentation -https://scapy.readthedocs.io/ +More verbose examples are available in the documentation at +\fIhttps://scapy.readthedocs.io/\fP. Just run \fBscapy\fP and try the following commands in the interpreter. .LP @@ -114,7 +110,7 @@ sr(IP(dst="172.16.1.1", ihl=2, options=["verb$2"], version=3)/ICMP(), timeout=2) Packet sniffing and dissection (with a bpf filter or tshark-like output): .nf a=sniff(filter="tcp port 110") -a=sniff(prn = lambda x: x.display) +a=sniff(prn = lambda x: x.show) .fi .LP @@ -200,7 +196,4 @@ BPF filters don't work on Point-to-point interfaces. .SH AUTHOR -Philippe Biondi -.PP -This manual page was written by Alberto Gonzalez Iniesta -and Philippe Biondi. +Philippe Biondi and the Scapy community. diff --git a/doc/scapy/_ext/linkcode_res.py b/doc/scapy/_ext/linkcode_res.py new file mode 100644 index 00000000000..fdcc6d013d2 --- /dev/null +++ b/doc/scapy/_ext/linkcode_res.py @@ -0,0 +1,100 @@ +import inspect +import os +import sys + +import scapy + +# -- Linkcode resolver ----------------------------------------------------- + +# This is HEAVILY inspired by numpy's +# https://github.com/numpy/numpy/blob/73fe877ff967f279d470b81ad447b9f3056c1335/doc/source/conf.py#L390 + +# Copyright (c) 2005-2020, NumPy Developers. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# * Neither the name of the NumPy Developers nor the names of any +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +def linkcode_resolve(domain, info): + """ + Determine the URL corresponding to Python object + """ + if domain != 'py': + return None + + modname = info['module'] + fullname = info['fullname'] + + submod = sys.modules.get(modname) + if submod is None: + return None + + obj = submod + for part in fullname.split('.'): + try: + obj = getattr(obj, part) + except Exception: + return None + + # strip decorators, which would resolve to the source of the decorator + # possibly an upstream bug in getsourcefile, bpo-1764286 + try: + unwrap = inspect.unwrap + except AttributeError: + pass + else: + obj = unwrap(obj) + + fn = None + lineno = None + + try: + fn = inspect.getsourcefile(obj) + except Exception: + fn = None + if not fn: + return None + + try: + source, lineno = inspect.getsourcelines(obj) + except Exception: + lineno = None + + fn = os.path.relpath(fn, start=os.path.dirname(scapy.__file__)) + + if lineno: + linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) + else: + linespec = "" + + if 'dev' in scapy.__version__: + return "https://github.com/secdev/scapy/blob/master/scapy/%s%s" % ( + fn, linespec) + else: + return "https://github.com/secdev/scapy/blob/v%s/scapy/%s%s" % ( + scapy.__version__, fn, linespec) diff --git a/doc/scapy/_ext/scapy_doc.py b/doc/scapy/_ext/scapy_doc.py index 1202184a2fd..678a9792fcc 100644 --- a/doc/scapy/_ext/scapy_doc.py +++ b/doc/scapy/_ext/scapy_doc.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter """ A Sphinx Extension for Scapy's doc preprocessing @@ -59,7 +59,7 @@ def get_fields_desc(obj): ( "**%s**" % fname, class_ref(cls) + ((" " + clsne) if clsne else ""), - "``%s``" % repr(dflt) + "``%s``" % dflt ) ) if output: @@ -127,7 +127,8 @@ def call_parent(): for line in tab(lines): self.add_line(line, sourcename) return - elif self.object_name in ["aliastypes"]: + elif (self.object_name in ["aliastypes"] or + self.object_name.startswith("class_")): # Ignore call_parent() return diff --git a/doc/scapy/_static/vethrelay.sh b/doc/scapy/_static/vethrelay.sh new file mode 100755 index 00000000000..0e62f7b4903 --- /dev/null +++ b/doc/scapy/_static/vethrelay.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Setup iptables for IP relay by creating an interface configured +# to be the destination of TPROXY rules. + +if [ "$EUID" -ne 0 ] + then echo "Please run as root" + exit +fi + +if [ "$1" != "setup" ] && [ "$1" != "unsetup" ]; then + echo -e "Usage: ./vethrelay \n" + exit 1 +fi + +IFACE="vethrelay" +IP="2.2.2.2" + +# Linux doc about TPROXY and example regarding this: +# https://www.kernel.org/doc/Documentation/networking/tproxy.txt +# https://powerdns.org/tproxydoc/tproxy.md.html + +function checkSetup() { + iptables -t mangle -n --list "DIVERT" >/dev/null 2>&1 + return $? +} + +if [ "$1" == "setup" ]; then + # Add "DIVERT" chain if it doesn't exist + checkSetup + if [ $? -eq 0 ]; then + echo "vethrelay already setup !" + exit 1 + fi + # Create an interface tcpreplay dedicated to relay + ip link add dev $IFACE type dummy + sysctl net.ipv6.conf.$IFACE.disable_ipv6=1 >/dev/null + ip link set dev $IFACE up + ip addr add dev $IFACE $IP/32 + # Create mangle "DIVERT" chain as an optimisation. -m socket matches + # packets from already established sockets. Those are marked as 1 then + # accepted directly. + iptables -t mangle -N DIVERT + iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT + iptables -t mangle -A DIVERT -j MARK --set-mark 1 + iptables -t mangle -A DIVERT -j ACCEPT + # Packets marked with 1 are routed through table 100 instead of the + # default routing table + ip rule add fwmark 1 lookup 100 + # In routing table 100, all IPs are local to 'vethrelay' + ip route add local 0.0.0.0/0 dev $IFACE table 100 + echo -e "\x1b[32mInterface $IFACE is now setup with IPv4: $IP !\x1b[0m\n" + echo -e "Add listening rules as follow:\n" + echo "# TPROXY incoming TCP packets on port 80 to $IFACE on port 8080" + echo "iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080 --on-ip $IP" + echo + echo "# Listen on wlp4s0 for incoming packets on port 80 (on the interface where it really comes from)" + echo "iptables -A INPUT -i wlp4s0 -p tcp --dport 80 -j ACCEPT" +elif [ "$1" == "unsetup" ]; then + checkSetup + if [ $? -ne 0 ]; then + echo "vethrelay not setup !" + exit 1 + fi + # Remove all setup rules + sudo ip rule del fwmark 1 lookup 100 + sudo ip route del local 0.0.0.0/0 dev $IFACE table 100 + sudo iptables -t mangle -D DIVERT -j ACCEPT + sudo iptables -t mangle -D DIVERT -j MARK --set-mark 1 + sudo iptables -t mangle -D PREROUTING -p tcp -m socket -j DIVERT + sudo iptables -t mangle -X DIVERT + sudo ip link del dev $IFACE + echo -e "\x1b[32mInterface $IFACE unsetup !\x1b[0m" +fi diff --git a/doc/scapy/_templates/README.md b/doc/scapy/_templates/README.md new file mode 100644 index 00000000000..1c5ee9fd0d6 --- /dev/null +++ b/doc/scapy/_templates/README.md @@ -0,0 +1,6 @@ +# Doc templates + +This folder contains templates used to generate Scapy's doc. It contains: + +- apidoc templates: inherited from + https://github.com/sphinx-doc/sphinx/blob/master/sphinx/templates/apidoc/ diff --git a/doc/scapy/_templates/module.rst_t b/doc/scapy/_templates/module.rst_t new file mode 100644 index 00000000000..d9a50e6b975 --- /dev/null +++ b/doc/scapy/_templates/module.rst_t @@ -0,0 +1,9 @@ +{%- if show_headings %} +{{- basename | e | heading }} + +{% endif -%} +.. automodule:: {{ qualname }} +{%- for option in automodule_options %} + :{{ option }}: +{%- endfor %} + diff --git a/doc/scapy/_templates/package.rst_t b/doc/scapy/_templates/package.rst_t new file mode 100644 index 00000000000..0e457d2358c --- /dev/null +++ b/doc/scapy/_templates/package.rst_t @@ -0,0 +1,55 @@ +{%- macro automodule(modname, options) -%} +.. automodule:: {{ modname }} +{%- for option in options %} + :{{ option }}: +{%- endfor %} +{%- endmacro %} + +{%- macro toctree(docnames) -%} +.. toctree:: + :maxdepth: {{ maxdepth }} + :titlesonly: +{% for docname in docnames %} + {{ docname }} +{%- endfor %} +{%- endmacro %} + +{%- if is_namespace %} +{{- [pkgname, "namespace"] | join(" ") | e | heading }} +{% else %} +{%- if pkgname == "scapy" %} +{{- "Scapy API reference" | e | heading }} +{% else %} +{{- [pkgname, "package"] | join(" ") | e | heading }} +{% endif %} +{% endif %} + +{%- if modulefirst and not is_namespace %} +{{ automodule(pkgname, automodule_options) }} +{% endif %} + +{%- if subpackages %} +Subpackages +----------- + +{{ toctree(subpackages) }} +{% endif %} + +{%- if submodules %} +Submodules +---------- +{% if separatemodules %} +{{ toctree(submodules) }} +{%- else %} +{%- for submodule in submodules %} +{% if show_headings %} +{{- [submodule, "module"] | join(" ") | e | heading(2) }} +{% endif %} +{{ automodule(submodule, automodule_options) }} +{% endfor %} +{%- endif %} +{% endif %} + +{%- if not modulefirst and not is_namespace %} +{{ automodule(pkgname, automodule_options) }} +{% endif %} diff --git a/doc/scapy/advanced_usage.rst b/doc/scapy/advanced_usage/asn1_snmp.rst similarity index 50% rename from doc/scapy/advanced_usage.rst rename to doc/scapy/advanced_usage/asn1_snmp.rst index c7e6ad012a2..feccf1f855d 100644 --- a/doc/scapy/advanced_usage.rst +++ b/doc/scapy/advanced_usage/asn1_snmp.rst @@ -1,7 +1,3 @@ -************** -Advanced usage -************** - ASN.1 and SNMP ============== @@ -486,745 +482,4 @@ or to resolve an OID:: It is even possible to graph it:: - >>> conf.mib._make_graph() - - - -Automata -======== - -Scapy enables to create easily network automata. Scapy does not stick to a specific model like Moore or Mealy automata. It provides a flexible way for you to choose your way to go. - -An automaton in Scapy is deterministic. It has different states. A start state and some end and error states. There are transitions from one state to another. Transitions can be transitions on a specific condition, transitions on the reception of a specific packet or transitions on a timeout. When a transition is taken, one or more actions can be run. An action can be bound to many transitions. Parameters can be passed from states to transitions, and from transitions to states and actions. - -From a programmer's point of view, states, transitions and actions are methods from an Automaton subclass. They are decorated to provide meta-information needed in order for the automaton to work. - -First example -------------- - -Let's begin with a simple example. I take the convention to write states with capitals, but anything valid with Python syntax would work as well. - -:: - - class HelloWorld(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - print "State=BEGIN" - - @ATMT.condition(BEGIN) - def wait_for_nothing(self): - print "Wait for nothing..." - raise self.END() - - @ATMT.action(wait_for_nothing) - def on_nothing(self): - print "Action on 'nothing' condition" - - @ATMT.state(final=1) - def END(self): - print "State=END" - -In this example, we can see 3 decorators: - -* ``ATMT.state`` that is used to indicate that a method is a state, and that can - have initial, final and error optional arguments set to non-zero for special states. -* ``ATMT.condition`` that indicate a method to be run when the automaton state - reaches the indicated state. The argument is the name of the method representing that state -* ``ATMT.action`` binds a method to a transition and is run when the transition is taken. - -Running this example gives the following result:: - - >>> a=HelloWorld() - >>> a.run() - State=BEGIN - Wait for nothing... - Action on 'nothing' condition - State=END - -This simple automaton can be described with the following graph: - -.. image:: graphics/ATMT_HelloWorld.* - -The graph can be automatically drawn from the code with:: - - >>> HelloWorld.graph() - -Changing states ---------------- - -The ``ATMT.state`` decorator transforms a method into a function that returns an exception. If you raise that exception, the automaton state will be changed. If the change occurs in a transition, actions bound to this transition will be called. The parameters given to the function replacing the method will be kept and finally delivered to the method. The exception has a method action_parameters that can be called before it is raised so that it will store parameters to be delivered to all actions bound to the current transition. - -As an example, let's consider the following state:: - - @ATMT.state() - def MY_STATE(self, param1, param2): - print "state=MY_STATE. param1=%r param2=%r" % (param1, param2) - -This state will be reached with the following code:: - - @ATMT.receive_condition(ANOTHER_STATE) - def received_ICMP(self, pkt): - if ICMP in pkt: - raise self.MY_STATE("got icmp", pkt[ICMP].type) - -Let's suppose we want to bind an action to this transition, that will also need some parameters:: - - @ATMT.action(received_ICMP) - def on_ICMP(self, icmp_type, icmp_code): - self.retaliate(icmp_type, icmp_code) - -The condition should become:: - - @ATMT.receive_condition(ANOTHER_STATE) - def received_ICMP(self, pkt): - if ICMP in pkt: - raise self.MY_STATE("got icmp", pkt[ICMP].type).action_parameters(pkt[ICMP].type, pkt[ICMP].code) - -Real example ------------- - -Here is a real example take from Scapy. It implements a TFTP client that can issue read requests. - -.. image:: graphics/ATMT_TFTP_read.* - -:: - - class TFTP_read(Automaton): - def parse_args(self, filename, server, sport = None, port=69, **kargs): - Automaton.parse_args(self, **kargs) - self.filename = filename - self.server = server - self.port = port - self.sport = sport - - def master_filter(self, pkt): - return ( IP in pkt and pkt[IP].src == self.server and UDP in pkt - and pkt[UDP].dport == self.my_tid - and (self.server_tid is None or pkt[UDP].sport == self.server_tid) ) - - # BEGIN - @ATMT.state(initial=1) - def BEGIN(self): - self.blocksize=512 - self.my_tid = self.sport or RandShort()._fix() - bind_bottom_up(UDP, TFTP, dport=self.my_tid) - self.server_tid = None - self.res = b"" - - self.l3 = IP(dst=self.server)/UDP(sport=self.my_tid, dport=self.port)/TFTP() - self.last_packet = self.l3/TFTP_RRQ(filename=self.filename, mode="octet") - self.send(self.last_packet) - self.awaiting=1 - - raise self.WAITING() - - # WAITING - @ATMT.state() - def WAITING(self): - pass - - @ATMT.receive_condition(WAITING) - def receive_data(self, pkt): - if TFTP_DATA in pkt and pkt[TFTP_DATA].block == self.awaiting: - if self.server_tid is None: - self.server_tid = pkt[UDP].sport - self.l3[UDP].dport = self.server_tid - raise self.RECEIVING(pkt) - @ATMT.action(receive_data) - def send_ack(self): - self.last_packet = self.l3 / TFTP_ACK(block = self.awaiting) - self.send(self.last_packet) - - @ATMT.receive_condition(WAITING, prio=1) - def receive_error(self, pkt): - if TFTP_ERROR in pkt: - raise self.ERROR(pkt) - - @ATMT.timeout(WAITING, 3) - def timeout_waiting(self): - raise self.WAITING() - @ATMT.action(timeout_waiting) - def retransmit_last_packet(self): - self.send(self.last_packet) - - # RECEIVED - @ATMT.state() - def RECEIVING(self, pkt): - recvd = pkt[Raw].load - self.res += recvd - self.awaiting += 1 - if len(recvd) == self.blocksize: - raise self.WAITING() - raise self.END() - - # ERROR - @ATMT.state(error=1) - def ERROR(self,pkt): - split_bottom_up(UDP, TFTP, dport=self.my_tid) - return pkt[TFTP_ERROR].summary() - - #END - @ATMT.state(final=1) - def END(self): - split_bottom_up(UDP, TFTP, dport=self.my_tid) - return self.res - -It can be run like this, for instance:: - - >>> TFTP_read("my_file", "192.168.1.128").run() - -Detailed documentation ----------------------- - -Decorators -^^^^^^^^^^ -Decorator for states -~~~~~~~~~~~~~~~~~~~~ - -States are methods decorated by the result of the ``ATMT.state`` function. It can take 3 optional parameters, ``initial``, ``final`` and ``error``, that, when set to ``True``, indicating that the state is an initial, final or error state. - -:: - - class Example(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - pass - - @ATMT.state() - def SOME_STATE(self): - pass - - @ATMT.state(final=1) - def END(self): - return "Result of the automaton: 42" - - @ATMT.state(error=1) - def ERROR(self): - return "Partial result, or explanation" - # [...] - -Decorators for transitions -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Transitions are methods decorated by the result of one of ``ATMT.condition``, ``ATMT.receive_condition``, ``ATMT.timeout``. They all take as argument the state method they are related to. ``ATMT.timeout`` also have a mandatory ``timeout`` parameter to provide the timeout value in seconds. ``ATMT.condition`` and ``ATMT.receive_condition`` have an optional ``prio`` parameter so that the order in which conditions are evaluated can be forced. The default priority is 0. Transitions with the same priority level are called in an undetermined order. - -When the automaton switches to a given state, the state's method is executed. Then transitions methods are called at specific moments until one triggers a new state (something like ``raise self.MY_NEW_STATE()``). First, right after the state's method returns, the ``ATMT.condition`` decorated methods are run by growing prio. Then each time a packet is received and accepted by the master filter all ``ATMT.receive_condition`` decorated hods are called by growing prio. When a timeout is reached since the time we entered into the current space, the corresponding ``ATMT.timeout`` decorated method is called. - -:: - - class Example(Automaton): - @ATMT.state() - def WAITING(self): - pass - - @ATMT.condition(WAITING) - def it_is_raining(self): - if not self.have_umbrella: - raise self.ERROR_WET() - - @ATMT.receive_condition(WAITING, prio=1) - def it_is_ICMP(self, pkt): - if ICMP in pkt: - raise self.RECEIVED_ICMP(pkt) - - @ATMT.receive_condition(WAITING, prio=2) - def it_is_IP(self, pkt): - if IP in pkt: - raise self.RECEIVED_IP(pkt) - - @ATMT.timeout(WAITING, 10.0) - def waiting_timeout(self): - raise self.ERROR_TIMEOUT() - -Decorator for actions -~~~~~~~~~~~~~~~~~~~~~ - -Actions are methods that are decorated by the return of ``ATMT.action`` function. This function takes the transition method it is bound to as first parameter and an optional priority ``prio`` as a second parameter. The default priority is 0. An action method can be decorated many times to be bound to many transitions. - -:: - - class Example(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - pass - - @ATMT.state(final=1) - def END(self): - pass - - @ATMT.condition(BEGIN, prio=1) - def maybe_go_to_end(self): - if random() > 0.5: - raise self.END() - @ATMT.condition(BEGIN, prio=2) - def certainly_go_to_end(self): - raise self.END() - - @ATMT.action(maybe_go_to_end) - def maybe_action(self): - print "We are lucky..." - @ATMT.action(certainly_go_to_end) - def certainly_action(self): - print "We are not lucky..." - @ATMT.action(maybe_go_to_end, prio=1) - @ATMT.action(certainly_go_to_end, prio=1) - def always_action(self): - print "This wasn't luck!..." - -The two possible outputs are:: - - >>> a=Example() - >>> a.run() - We are not lucky... - This wasn't luck!... - >>> a.run() - We are lucky... - This wasn't luck!... - -Methods to overload -^^^^^^^^^^^^^^^^^^^ - -Two methods are hooks to be overloaded: - -* The ``parse_args()`` method is called with arguments given at ``__init__()`` and ``run()``. Use that to parametrize the behavior of your automaton. - -* The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition. - -.. _pipetools: - -PipeTools -========= - -Pipetool is a smart piping system allowing to perform complex stream data management. There are various differences between PipeTools and Automatons: - -- PipeTools have no states: data is always sent following the same pattern -- PipeTools are not based on sockets but can handle more varied sources of data (and outputs) such as user input, pcap input (but also sniffing) -- PipeTools are not class-based, but rather implemented by manually linking all their parts. That has drawbacks but allows to dynamically add a Source, Drain while running, and set multiple drains for the same source - -.. note:: Pipetool default objects are located inside ``scapy.pipetool`` - -Class Types ------------ - -There are 3 different class of objects used for data management: - -- ``Sources`` -- ``Drains`` -- ``Sinks`` - -They are executed and handled by a ``PipeEngine`` object. - -When running, a pipetool engine waits for any available data from the Source, and send it in the Drains linked to it. -The data then goes from Drains to Drains until it arrives in a Sink, the final state of this data. - -Here is a basic demo of what the PipeTool system can do - -.. image:: graphics/pipetool_engine.png - -For instance, this engine was generated with this code: - ->>> s = CLIFeeder() ->>> s2 = CLIHighFeeder() ->>> d1 = Drain() ->>> d2 = TransformDrain(lambda x: x[::-1]) ->>> si1 = ConsoleSink() ->>> si2 = QueueSink() ->>> ->>> s > d1 ->>> d1 > si1 ->>> d1 > si2 ->>> ->>> s2 >> d1 ->>> d1 >> d2 ->>> d2 >> si1 ->>> ->>> p = PipeEngine() ->>> p.add(s) ->>> p.add(s2) ->>> p.graph(target="> the_above_image.png") - -Let's start our PipeEngine: - ->>> p.start() - -Now, let's play with it: - ->>> s.send("foo") ->'foo' ->>> s2.send("bar") ->>'rab' ->>> s.send("i like potato") ->'i like potato' ->>> print(si2.recv(), ":", si2.recv()) -foo : i like potato - -Let's study what happens here: - -- there are two canals in a PipeEngine, a lower one and a higher one. Some Sources write on the lower one, some on the higher one and some on both. -- most sources can be linked to any drain, on both lower and higher canals. The use of `>` indicates a link on the low canal, and `>>` on the higher one. -- when we send some data in `s`, which is on the lower canal, as shown above, it goes through the `Drain` then is sent to the `QueueSink` and to the `ConsoleSink` -- when we send some data in `s2`, it goes through the Drain, then the TransformDrain where the data is reversed (see the lambda), before being sent to `ConsoleSink` only. This explains why we only have the data of the lower sources inside the QueueSink: the higher one has not been linked. - -Most of the sinks receive from both lower and upper canals. This is verifiable using the `help(ConsoleSink)` - ->>> help(ConsoleSink) -Help on class ConsoleSink in module scapy.pipetool: -class ConsoleSink(Sink) - | Print messages on low and high entries - | +-------+ - | >>-|--. |->> - | | print | - | >-|--' |-> - | +-------+ - | - [...] - - -Sources -^^^^^^^ - -A Source is a class that generates some data. - -There are several source types integrated with Scapy, usable as-is, but you may -also create yours. - -Default Source classes -~~~~~~~~~~~~~~~~~~~~~~ - -For any of those class, have a look at ``help([theclass])`` to get more information or the required parameters. - -- CLIFeeder : a source especially used in interactive software. its ``send(data)`` generates the event data on the lower canal -- CLIHighFeeder : same than CLIFeeder, but writes on the higher canal -- PeriodicSource : Generate messages periodically on the low canal. -- AutoSource: the default source, that must be extended to create custom sources. - -Create a custom Source -~~~~~~~~~~~~~~~~~~~~~~ - -To create a custom source, one must extend the ``AutoSource`` class. - -Do NOT use the default ``Source`` class except if you are really sure of what you are doing: it is only used internally, and is missing some implementation. The ``AutoSource`` is made to be used. - - -To send data through it, the object must call its ``self._gen_data(msg)`` or ``self._gen_high_data(msg)`` functions, which send the data into the PipeEngine. - -The Source should also (if possible), set ``self.is_exhausted`` to ``True`` when empty, to allow the clean stop of the ``PipeEngine``. If the source is infinite, it will need a force-stop (see PipeEngine below) - -For instance, here is how CLIHighFeeder is implemented:: - - class CLIFeeder(CLIFeeder): - def send(self, msg): - self._gen_high_data(msg) - def close(self): - self.is_exhausted = True - -Drains -^^^^^^ - -Default Drain classes -~~~~~~~~~~~~~~~~~~~~~ - -Drains need to be linked on the entry that you are using. It can be either on the lower one (using ``>``) or the upper one (using ``>>``). -See the basic example above. - -- Drain : the most basic Drain possible. Will pass on both low and high entry if linked properly. -- TransformDrain : Apply a function to messages on low and high entry -- UpDrain : Repeat messages from low entry to high exit -- DownDrain : Repeat messages from high entry to low exit - -Create a custom Drain -~~~~~~~~~~~~~~~~~~~~~ - -To create a custom drain, one must extend the ``Drain`` class. - -A ``Drain`` object will receive data from the lower canal in its ``push`` method, and from the higher canal from its ``high_push`` method. - -To send the data back into the next linked Drain / Sink, it must call the ``self._send(msg)`` or ``self._high_send(msg)`` methods. - -For instance, here is how TransformDrain is implemented:: - - class TransformDrain(Drain): - def __init__(self, f, name=None): - Drain.__init__(self, name=name) - self.f = f - def push(self, msg): - self._send(self.f(msg)) - def high_push(self, msg): - self._high_send(self.f(msg)) - -Sinks -^^^^^ - -Sinks are destinations for messages. - -A :py:class:`Sink` receives data like a :py:class:`Drain`, but doesn't send any -messages after it. - -Messages on the low entry come from :py:meth:`~Sink.push`, and messages on the -high entry come from :py:meth:`~Sink.high_push`. - -Default Sink classes -~~~~~~~~~~~~~~~~~~~~ - -.. py:class:: Sink - - Does nothing; interface to extend for custom sinks. - - All sinks have the following constructor parameters: - - :param name: a human-readable name for the element - :type name: str - - All sinks should implement at least one of these methods: - - .. py:method:: push - - Called by :py:class:`PipeEngine` when there is a new message for the - low entry. - - :param msg: The message data - :returns: None - :rtype: None - - .. py:method:: high_push - - Called by :py:class:`PipeEngine` when there is a new message for the - high entry. - - :param msg: The message data - :returns: None - :rtype: None - -.. py:class:: ConsoleSink - - Prints messages on the low and high entries to ``stdout``. - -.. py:class:: RawConsoleSink - - Prints messages on the low and high entries, using :py:func:`os.write`. - - :param newlines: Include a new-line character after printing each packet. - Defaults to True. - :type newlines: bool - -.. py:class:: TermSink - - Prints messages on the low and high entries, on a separate terminal (xterm - or cmd). - - :param keepterm: Leaves the terminal window open after :py:meth:`~Pipe.stop` - is called. Defaults to True. - :type keepterm: bool - :param newlines: Include a new-line character after printing each packet. - Defaults to True. - :type newlines: bool - :param openearly: Automatically starts the terminal when the constructor is - called, rather than waiting for :py:meth:`~Pipe.start`. - Defaults to True. - :type openearly: bool - -.. py:class:: QueueSink - - Collects messages on the low and high entries into a :py:class:`Queue`. - - Messages are dequeued with :py:meth:`recv`. - - Both high and low entries share the same :py:class:`Queue`. - - .. py:method:: recv - - Reads the next message from the queue. - - If no message is available in the queue, returns None. - - :param block: Blocks execution until a packet is available in the queue. - Defaults to True. - :type block: bool - :param timeout: Controls how long to wait if ``block=True``. If None - (the default), this method will wait forever. If a - non-negative number, this is a number of seconds to - wait before giving up (and returning None). - :type timeout: None, int or float - -.. py:class:: WiresharkSink - - Streams :py:class:`Packet` from the low entry to Wireshark. - - Packets are written into a ``pcap`` stream (like :py:class:`WrpcapSink`), - and streamed to a new Wireshark process on its ``stdin``. - - Wireshark is run with the ``-ki -`` arguments, which cause it to treat - ``stdin`` as a capture device. Arguments in :py:attr:`args` will be - appended after this. - - Extends :py:mod:`WrpcapSink`. - - :param linktype: See :py:attr:`WrpcapSink.linktype`. - :type linktype: None or int - :param args: See :py:attr:`args`. - :type args: None or list[str] - - .. py:attribute:: args - - Additional arguments for the Wireshark process. - - This must be either ``None`` (the default), or a ``list`` of ``str``. - - This attribute has no effect after calling :py:meth:`PipeEngine.start`. - - See :manpage:`wireshark(1)` for more details. - -.. py:class:: WrpcapSink - - Writes :py:class:`Packet` on the low entry to a ``pcap`` file. - - Ignores all messages on the high entry. - - .. note:: - - Due to limitations of the ``pcap`` format, all packets **must** be of - the same link type. This class will not mutate packets to conform with - the expected link type. - - :param fname: Filename to write packets to. - :type fname: str - :param linktype: See :py:attr:`linktype`. - :type linktype: None or int - - .. py:attribute:: linktype - - Set an explicit link-type (``DLT_``) for packets. This must be an - ``int`` or ``None``. - - This is the same as the :py:func:`wrpcap` ``linktype`` parameter. - - If ``None`` (the default), the linktype will be auto-detected on the - first packet. This field will *not* be updated with the result of this - auto-detection. - - This attribute has no effect after calling :py:meth:`PipeEngine.start`. - - -Create a custom Sink -~~~~~~~~~~~~~~~~~~~~ - -To create a custom sink, one must extend :py:class:`Sink` and implement -:py:meth:`~Sink.push` and/or :py:meth:`~Sink.high_push`. - -This is a simplified version of :py:class:`ConsoleSink`: - -.. code-block:: python3 - - class ConsoleSink(Sink): - def push(self, msg): - print(">%r" % msg) - def high_push(self, msg): - print(">>%r" % msg) - -Link objects ------------- - -As shown in the example, most sources can be linked to any drain, on both low -and high entry. - -The use of ``>`` indicates a link on the low entry, and ``>>`` on the high -entry. - -For example, to link ``a``, ``b`` and ``c`` on the low entries: - -.. code-block:: pycon - - >>> a = CLIFeeder() - >>> b = Drain() - >>> c = ConsoleSink() - >>> a > b > c - >>> p = PipeEngine() - >>> p.add(a) - -This wouldn't link the high entries, so something like this would do nothing: - -.. code-block:: pycon - - >>> a2 = CLIHighFeeder() - >>> a2 >> b - >>> a2.send("hello") - -Because ``b`` (:py:class:`Drain`) and ``c`` (:py:class:`ConsoleSink`) are not -linked on the high entry. - -However, using a :py:class:`DownDrain` would bring the high messages from -:py:class:`CLIHighFeeder` to the lower channel: - -.. code-block:: pycon - - >>> a2 = CLIHighFeeder() - >>> b2 = DownDrain() - >>> a2 >> b2 - >>> b2 > b - >>> a2.send("hello") - -The PipeEngine class --------------------- - -The ``PipeEngine`` class is the core class of the Pipetool system. It must be initialized and passed the list of all Sources. - -There are two ways of passing sources: - -- during initialization: ``p = PipeEngine(source1, source2, ...)`` -- using the ``add(source)`` method - -A ``PipeEngine`` class must be started with ``.start()`` function. It may be force-stopped with the ``.stop()``, or cleanly stopped with ``.wait_and_stop()`` - -A clean stop only works if the Sources is exhausted (has no data to send left). - -It can be printed into a graph using ``.graph()`` methods. see ``help(do_graph)`` for the list of available keyword arguments. - -Scapy advanced PipeTool objects -------------------------------- - -.. note:: Unlike the previous objects, those are not located in ``scapy.pipetool`` but in ``scapy.scapypipes`` - -Now that you know the default PipeTool objects, here are some more advanced ones, based on packet functionalities. - -- SniffSource : Read packets from an interface and send them to low exit. -- RdpcapSource : Read packets from a PCAP file send them to low exit. -- InjectSink : Packets received on low input are injected (sent) to an interface -- WrpcapSink : Packets received on low input are written to PCAP file -- UDPDrain : UDP payloads received on high entry are sent over UDP (complicated, have a look at ``help(UDPDrain)``) -- FDSourceSink : Use a file descriptor as source and sink -- TCPConnectPipe : TCP connect to addr:port and use it as source and sink -- TCPListenPipe : TCP listen on [addr:]port and use the first connection as source and sink (complicated, have a look at ``help(TCPListenPipe)``) - -Triggering ----------- - -Some special sort of Drains exists: the Trigger Drains. - -Trigger Drains are special drains, that on receiving data not only pass it by but also send a "Trigger" input, that is received and handled by the next triggered drain (if it exists). - -For example, here is a basic TriggerDrain usage: - ->>> a = CLIFeeder() ->>> d = TriggerDrain(lambda msg: True) # Pass messages and trigger when a condition is met ->>> d2 = TriggeredValve() ->>> s = ConsoleSink() ->>> a > d > d2 > s ->>> d ^ d2 # Link the triggers ->>> p = PipeEngine(s) ->>> p.start() -INFO: Pipe engine thread started. ->>> ->>> a.send("this will be printed") ->'this will be printed' ->>> a.send("this won't, because the valve was switched") ->>> a.send("this will, because the valve was switched again") ->'this will, because the valve was switched again' ->>> p.stop() - -Several triggering Drains exist, they are pretty explicit. It is highly recommended to check the doc using ``help([the class])`` - -- TriggeredMessage : Send a preloaded message when triggered and trigger in chain -- TriggerDrain : Pass messages and trigger when a condition is met -- TriggeredValve : Let messages alternatively pass or not, changing on trigger -- TriggeredQueueingValve : Let messages alternatively pass or queued, changing on trigger -- TriggeredSwitch : Let messages alternatively high or low, changing on trigger + >>> conf.mib._make_graph() \ No newline at end of file diff --git a/doc/scapy/advanced_usage/automaton.rst b/doc/scapy/advanced_usage/automaton.rst new file mode 100644 index 00000000000..b8a7e984d70 --- /dev/null +++ b/doc/scapy/advanced_usage/automaton.rst @@ -0,0 +1,376 @@ +Automata +======== + +Scapy enables to create easily network automata. Scapy does not stick to a specific model like Moore or Mealy automata. It provides a flexible way for you to choose your way to go. + +An automaton in Scapy is deterministic. It has different states. A start state and some end and error states. There are transitions from one state to another. Transitions can be transitions on a specific condition, transitions on the reception of a specific packet or transitions on a timeout. When a transition is taken, one or more actions can be run. An action can be bound to many transitions. Parameters can be passed from states to transitions, and from transitions to states and actions. + +From a programmer's point of view, states, transitions and actions are methods from an Automaton subclass. They are decorated to provide meta-information needed in order for the automaton to work. + +First example +------------- + +Let's begin with a simple example. I take the convention to write states with capitals, but anything valid with Python syntax would work as well. + +:: + + class HelloWorld(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + print("State=BEGIN") + + @ATMT.condition(BEGIN) + def wait_for_nothing(self): + print("Wait for nothing...") + raise self.END() + + @ATMT.action(wait_for_nothing) + def on_nothing(self): + print("Action on 'nothing' condition") + + @ATMT.state(final=1) + def END(self): + print("State=END") + +In this example, we can see 3 decorators: + +* ``ATMT.state`` that is used to indicate that a method is a state, and that can + have initial, final, stop and error optional arguments set to non-zero for special states. +* ``ATMT.condition`` that indicate a method to be run when the automaton state + reaches the indicated state. The argument is the name of the method representing that state +* ``ATMT.action`` binds a method to a transition and is run when the transition is taken. + +Running this example gives the following result:: + + >>> a=HelloWorld() + >>> a.run() + State=BEGIN + Wait for nothing... + Action on 'nothing' condition + State=END + >>> a.destroy() + +This simple automaton can be described with the following graph: + +.. image:: ../graphics/ATMT_HelloWorld.* + +The graph can be automatically drawn from the code with:: + + >>> HelloWorld.graph() + +.. note:: An ``Automaton`` can be reset using ``restart()``. It is then possible to run it again. + +.. warning:: Remember to call ``destroy()`` once you're done using an Automaton. (especially on PyPy) + +Changing states +--------------- + +The ``ATMT.state`` decorator transforms a method into a function that returns an exception. If you raise that exception, the automaton state will be changed. If the change occurs in a transition, actions bound to this transition will be called. The parameters given to the function replacing the method will be kept and finally delivered to the method. The exception has a method action_parameters that can be called before it is raised so that it will store parameters to be delivered to all actions bound to the current transition. + +As an example, let's consider the following state:: + + @ATMT.state() + def MY_STATE(self, param1, param2): + print("state=MY_STATE. param1=%r param2=%r" % (param1, param2)) + +This state will be reached with the following code:: + + @ATMT.receive_condition(ANOTHER_STATE) + def received_ICMP(self, pkt): + if ICMP in pkt: + raise self.MY_STATE("got icmp", pkt[ICMP].type) + +Let's suppose we want to bind an action to this transition, that will also need some parameters:: + + @ATMT.action(received_ICMP) + def on_ICMP(self, icmp_type, icmp_code): + self.retaliate(icmp_type, icmp_code) + +The condition should become:: + + @ATMT.receive_condition(ANOTHER_STATE) + def received_ICMP(self, pkt): + if ICMP in pkt: + raise self.MY_STATE("got icmp", pkt[ICMP].type).action_parameters(pkt[ICMP].type, pkt[ICMP].code) + +Real example +------------ + +Here is a real example take from Scapy. It implements a TFTP client that can issue read requests. + +.. image:: ../graphics/ATMT_TFTP_read.* + +:: + + class TFTP_read(Automaton): + def parse_args(self, filename, server, sport = None, port=69, **kargs): + Automaton.parse_args(self, **kargs) + self.filename = filename + self.server = server + self.port = port + self.sport = sport + + def master_filter(self, pkt): + return ( IP in pkt and pkt[IP].src == self.server and UDP in pkt + and pkt[UDP].dport == self.my_tid + and (self.server_tid is None or pkt[UDP].sport == self.server_tid) ) + + # BEGIN + @ATMT.state(initial=1) + def BEGIN(self): + self.blocksize=512 + self.my_tid = self.sport or RandShort()._fix() + bind_bottom_up(UDP, TFTP, dport=self.my_tid) + self.server_tid = None + self.res = b"" + + self.l3 = IP(dst=self.server)/UDP(sport=self.my_tid, dport=self.port)/TFTP() + self.last_packet = self.l3/TFTP_RRQ(filename=self.filename, mode="octet") + self.send(self.last_packet) + self.awaiting=1 + + raise self.WAITING() + + # WAITING + @ATMT.state() + def WAITING(self): + pass + + @ATMT.receive_condition(WAITING) + def receive_data(self, pkt): + if TFTP_DATA in pkt and pkt[TFTP_DATA].block == self.awaiting: + if self.server_tid is None: + self.server_tid = pkt[UDP].sport + self.l3[UDP].dport = self.server_tid + raise self.RECEIVING(pkt) + @ATMT.action(receive_data) + def send_ack(self): + self.last_packet = self.l3 / TFTP_ACK(block = self.awaiting) + self.send(self.last_packet) + + @ATMT.receive_condition(WAITING, prio=1) + def receive_error(self, pkt): + if TFTP_ERROR in pkt: + raise self.ERROR(pkt) + + @ATMT.timeout(WAITING, 3) + def timeout_waiting(self): + raise self.WAITING() + @ATMT.action(timeout_waiting) + def retransmit_last_packet(self): + self.send(self.last_packet) + + # RECEIVED + @ATMT.state() + def RECEIVING(self, pkt): + recvd = pkt[Raw].load + self.res += recvd + self.awaiting += 1 + if len(recvd) == self.blocksize: + raise self.WAITING() + raise self.END() + + # ERROR + @ATMT.state(error=1) + def ERROR(self,pkt): + split_bottom_up(UDP, TFTP, dport=self.my_tid) + return pkt[TFTP_ERROR].summary() + + #END + @ATMT.state(final=1) + def END(self): + split_bottom_up(UDP, TFTP, dport=self.my_tid) + return self.res + +It can be run like this, for instance:: + + >>> atmt = TFTP_read("my_file", "192.168.1.128") + >>> atmt.run() + >>> atmt.destroy() + +Detailed documentation +---------------------- + +Decorators +^^^^^^^^^^ +Decorator for states +~~~~~~~~~~~~~~~~~~~~ + +States are methods decorated by the result of the ``ATMT.state`` function. It can take 4 optional parameters, ``initial``, ``final``, ``stop`` and ``error``, that, when set to ``True``, indicating that the state is an initial, final, stop or error state. + +.. note:: The ``initial`` state is called while starting the automata. The ``final`` step will tell the automata has reached its end. If you call ``atmt.stop()``, the automata will move to the ``stop`` step whatever its current state is. The ``error`` state will mark the automata as errored. If no ``stop`` state is specified, calling ``stop`` and ``forcestop`` will be equivalent. + +:: + + class Example(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + pass + + @ATMT.state() + def SOME_STATE(self): + pass + + @ATMT.state(final=1) + def END(self): + return "Result of the automaton: 42" + + @ATMT.state(stop=1) + def STOP(self): + print("SHUTTING DOWN...") + # e.g. close sockets... + + @ATMT.condition(STOP) + def is_stopping(self): + raise self.END() + + @ATMT.state(error=1) + def ERROR(self): + return "Partial result, or explanation" + # [...] + +Take for instance the TCP client: + +.. image:: ../graphics/ATMT_TCP_client.svg + +The ``START`` event is ``initial=1``, the ``STOP`` event is ``stop=1`` and the ``CLOSED`` event is ``final=1``. + +Decorators for transitions +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Transitions are methods decorated by the result of one of ``ATMT.condition``, ``ATMT.receive_condition``, ``ATMT.eof``, ``ATMT.timeout``, ``ATMT.timer``. They all take as argument the state method they are related to. ``ATMT.timeout`` and ``ATMT.timer`` also have a mandatory ``timeout`` parameter to provide the timeout value in seconds. The difference between ``ATMT.timeout`` and ``ATMT.timer`` is that ``ATMT.timeout`` gets triggered only once. ``ATMT.timer`` get reloaded automatically, which is useful for sending keep-alive packets. ``ATMT.condition`` and ``ATMT.receive_condition`` have an optional ``prio`` parameter so that the order in which conditions are evaluated can be forced. The default priority is 0. Transitions with the same priority level are called in an undetermined order. + +When the automaton switches to a given state, the state's method is executed. Then transitions methods are called at specific moments until one triggers a new state (something like ``raise self.MY_NEW_STATE()``). First, right after the state's method returns, the ``ATMT.condition`` decorated methods are run by growing prio. Then each time a packet is received and accepted by the master filter all ``ATMT.receive_condition`` decorated hods are called by growing prio. When a timeout is reached since the time we entered into the current space, the corresponding ``ATMT.timeout`` decorated method is called. If the socket raises an ``EOFError`` (closed) during a state, the ``ATMT.EOF`` transition is called. Otherwise it raises an exception and the automaton exits. + +:: + + class Example(Automaton): + @ATMT.state() + def WAITING(self): + pass + + @ATMT.condition(WAITING) + def it_is_raining(self): + if not self.have_umbrella: + raise self.ERROR_WET() + + @ATMT.receive_condition(WAITING, prio=1) + def it_is_ICMP(self, pkt): + if ICMP in pkt: + raise self.RECEIVED_ICMP(pkt) + + @ATMT.receive_condition(WAITING, prio=2) + def it_is_IP(self, pkt): + if IP in pkt: + raise self.RECEIVED_IP(pkt) + + @ATMT.timeout(WAITING, 10.0) + def waiting_timeout(self): + raise self.ERROR_TIMEOUT() + +Decorator for actions +~~~~~~~~~~~~~~~~~~~~~ + +Actions are methods that are decorated by the return of ``ATMT.action`` function. This function takes the transition method it is bound to as first parameter and an optional priority ``prio`` as a second parameter. The default priority is 0. An action method can be decorated many times to be bound to many transitions. + +:: + + from random import random + + class Example(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + pass + + @ATMT.state(final=1) + def END(self): + pass + + @ATMT.condition(BEGIN, prio=1) + def maybe_go_to_end(self): + if random() > 0.5: + raise self.END() + + @ATMT.condition(BEGIN, prio=2) + def certainly_go_to_end(self): + raise self.END() + + @ATMT.action(maybe_go_to_end) + def maybe_action(self): + print("We are lucky...") + + @ATMT.action(certainly_go_to_end) + def certainly_action(self): + print("We are not lucky...") + + @ATMT.action(maybe_go_to_end, prio=1) + @ATMT.action(certainly_go_to_end, prio=1) + def always_action(self): + print("This wasn't luck!...") + +The two possible outputs are:: + + >>> a=Example() + >>> a.run() + We are not lucky... + This wasn't luck!... + >>> a.run() + We are lucky... + This wasn't luck!... + >>> a.destroy() + + +.. note:: If you want to pass a parameter to an action, you can use the ``action_parameters`` function while raising the next state. + +In the following example, the ``send_copy`` action takes a parameter passed by ``is_fin``:: + + class Example(Automaton): + @ATMT.state() + def WAITING(self): + pass + + @ATMT.state() + def FIN_RECEIVED(self): + pass + + @ATMT.receive_condition(WAITING) + def is_fin(self, pkt): + if pkt[TCP].flags.F: + raise self.FIN_RECEIVED().action_parameters(pkt) + + @ATMT.action(is_fin) + def send_copy(self, pkt): + send(pkt) + + +Methods to overload +^^^^^^^^^^^^^^^^^^^ + +Two methods are hooks to be overloaded: + +* The ``parse_args()`` method is called with arguments given at ``__init__()`` and ``run()``. Use that to parametrize the behavior of your automaton. + +* The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition. + +Timer configuration +^^^^^^^^^^^^^^^^^^^ + +Some protocols allow timer configuration. In order to configure timeout values during class initialization one may use ``timer_by_name()`` method, which returns ``Timer`` object associated with the given function name:: + + class Example(Automaton): + def __init__(self, *args, **kwargs): + super(Example, self).__init__(*args, **kwargs) + timer = self.timer_by_name("waiting_timeout") + timer.set(1) + + @ATMT.state(initial=1) + def WAITING(self): + pass + + @ATMT.state(final=1) + def END(self): + pass + + @ATMT.timeout(WAITING, 10.0) + def waiting_timeout(self): + raise self.END() \ No newline at end of file diff --git a/doc/scapy/advanced_usage/cbor.rst b/doc/scapy/advanced_usage/cbor.rst new file mode 100644 index 00000000000..90f668e3a40 --- /dev/null +++ b/doc/scapy/advanced_usage/cbor.rst @@ -0,0 +1,167 @@ +CBOR +==== + +What is CBOR? +------------- + +.. note:: + + This section provides a practical introduction to CBOR from Scapy's perspective. For the complete specification, see RFC 8949. + +CBOR (Concise Binary Object Representation) is a data format whose goal is to provide a compact, self-describing binary data interchange format based on the JSON data model. It is defined in RFC 8949 and is designed to be small in code size, reasonably small in message size, and extensible without the need for version negotiation. + +CBOR provides basic data types including: + +* **Unsigned integers** (major type 0): Non-negative integers +* **Negative integers** (major type 1): Negative integers +* **Byte strings** (major type 2): Raw binary data +* **Text strings** (major type 3): UTF-8 encoded strings +* **Arrays** (major type 4): Ordered sequences of values +* **Maps** (major type 5): Unordered key-value pairs +* **Semantic tags** (major type 6): Tagged values with additional semantics +* **Simple values and floats** (major type 7): Booleans, null, undefined, and floating-point numbers + +Each CBOR data item begins with an initial byte that encodes the major type (in the top 3 bits) and additional information (in the low 5 bits). This design allows for compact encoding while maintaining self-describing properties. + +Scapy and CBOR +-------------- + + +Creating CBOR objects +^^^^^^^^^^^^^^^^^^^^^ + +CBOR objects can be easily created and composed:: + + >>> from scapy.cbor import CBOR_UNSIGNED_INTEGER, CBOR_TEXT_STRING, CBOR_BYTE_STRING, CBOR_ARRAY + >>> # Create basic types + >>> num = CBOR_UNSIGNED_INTEGER(42) + >>> text = CBOR_TEXT_STRING("Hello, CBOR!") + >>> data = CBOR_BYTE_STRING(b'\x01\x02\x03') + >>> + >>> # Create collections + >>> arr = CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1), + ... CBOR_UNSIGNED_INTEGER(2), + ... CBOR_TEXT_STRING("three")]) + >>> arr + , , ]]> + >>> + >>> # Create maps + >>> from scapy.cbor.cborcodec import CBORcodec_MAP + >>> mapping = {"name": "Alice", "age": 30, "active": True} + +Encoding and decoding +^^^^^^^^^^^^^^^^^^^^^ + +CBOR objects are encoded using their ``.enc()`` method. All codecs are referenced in the ``CBOR_Codecs`` object. The default codec is ``CBOR_Codecs.CBOR``:: + + >>> num = CBOR_UNSIGNED_INTEGER(42) + >>> encoded = bytes(num) + >>> encoded.hex() + '182a' + >>> + >>> # Decode back + >>> decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) + >>> decoded.val + 42 + >>> isinstance(decoded, CBOR_UNSIGNED_INTEGER) + True + +Encoding collections:: + + >>> from scapy.cbor import CBORcodec_ARRAY, CBORcodec_MAP + >>> # Encode an array + >>> encoded = CBORcodec_ARRAY.enc([1, 2, 3, 4, 5]) + >>> encoded.hex() + '850102030405' + >>> + >>> # Decode the array + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> [item.val for item in decoded.val] + [1, 2, 3, 4, 5] + >>> + >>> # Encode a map + >>> encoded = CBORcodec_MAP.enc({"x": 100, "y": 200}) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> isinstance(decoded, CBOR_MAP) + True + +Working with different types +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CBOR supports various data types:: + + >>> # Booleans + >>> true_val = CBOR_TRUE() + >>> false_val = CBOR_FALSE() + >>> bytes(true_val).hex() + 'f5' + >>> bytes(false_val).hex() + 'f4' + >>> + >>> # Null and undefined + >>> null_val = CBOR_NULL() + >>> undef_val = CBOR_UNDEFINED() + >>> bytes(null_val).hex() + 'f6' + >>> bytes(undef_val).hex() + 'f7' + >>> + >>> # Floating point + >>> float_val = CBOR_FLOAT(3.14159) + >>> bytes(float_val).hex() + 'fb400921f9f01b866e' + >>> + >>> # Negative integers + >>> neg = CBOR_NEGATIVE_INTEGER(-100) + >>> bytes(neg).hex() + '3863' + +Complex structures +^^^^^^^^^^^^^^^^^^ + +CBOR supports nested structures:: + + >>> # Nested arrays + >>> nested = CBORcodec_ARRAY.enc([1, [2, 3], [4, [5, 6]]]) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(nested) + >>> isinstance(decoded, CBOR_ARRAY) + True + >>> + >>> # Complex maps with mixed types + >>> data = { + ... "name": "Bob", + ... "age": 25, + ... "active": True, + ... "tags": ["user", "admin"] + ... } + >>> encoded = CBORcodec_MAP.enc(data) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> len(decoded.val) + 4 + +Semantic tags +^^^^^^^^^^^^^ + +CBOR supports semantic tags (major type 6) for providing additional meaning to data items:: + + >>> # Tag 1 is for Unix epoch timestamps + >>> import time + >>> timestamp = int(time.time()) + >>> tagged = CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(timestamp))) + >>> encoded = bytes(tagged) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> decoded.val[0] # Tag number + 1 + + +Error handling +^^^^^^^^^^^^^^ + +Scapy provides safe decoding with error handling:: + + >>> # Safe decoding returns error objects for invalid data + >>> invalid_data = b'\xff\xff\xff' + >>> obj, remainder = CBOR_Codecs.CBOR.safedec(invalid_data) + >>> isinstance(obj, CBOR_DECODING_ERROR) + True + diff --git a/doc/scapy/advanced_usage/fwdmachine.rst b/doc/scapy/advanced_usage/fwdmachine.rst new file mode 100644 index 00000000000..24b5bcd3b41 --- /dev/null +++ b/doc/scapy/advanced_usage/fwdmachine.rst @@ -0,0 +1,132 @@ +****************** +Forwarding Machine +****************** + +Scapy's ``ForwardMachine`` is a utility that allows to create a server that forwards packets to another server, with the ability +to modify them on-the-fly. This is similar to a "proxy", but works on the layer 4 (rather than 5+). The ``ForwardMachine`` was initially designed to be used with TPROXY, +a linux feature that allows to bind a socket that receives *packets to any IP destination* (usually, a socket only receives packets whose destination is local), but it also work as a standalone server (that binds a normal socket). + +A ``ForwardMachine`` is expected to be used over a normal Python socket, of any kind, and needs to extended with two +functions: ``xfrmcs`` and ``xfrmsc``. The first one is called whenever data is received from the client side (client-to-server, "cs"), the other when the data +is received from the server (server-to-client, "sc") + +``ForwardMachine`` can be used in two modes: + +- **TPROXY**, acts as a transparent proxy that intercepts one or many connections towards multiple servers +- **SERVER**, acts like a glorified socat that accepts connections towards the local server + +Basic usage +___________ + +Here's an example of a ``ForwardMachine`` over TPROXY that does nothing. Packets for all destinations are handled, and forwarded to their +initial destinations afterwards. More details on how to setup TPROXY are provided below. + +.. code:: python + + from scapy.fwdmachine import ForwardMachine + from scapy.layers.http import HTTP + + class NOPFwdMachine(ForwardMachine): + def xfrmcs(self, pkt, ctx): + pkt.show() # we print the client->server packets + raise self.FORWARD() + + def xfrmsc(self, pkt, ctx): + pkt.show() # we print the server->client packets + raise self.FORWARD() + + # Run it + NOPFwdMachine( + mode=ForwardMachine.MODE.TPROXY, + port=80, + cls=HTTP, # we specify the class of the payload we are receiving + ).run() + +The callback classes use **Operations** to tell the ``ForwardMachine`` what to do with the incoming data. + +.. figure:: ../graphics/fwdmachine.svg + :align: center + + The main operations available in a Forwarding machine, in this case in ``xfrmcs``. + +There are currently 5 operations available: + +- **FORWARD**: forward the received payload to the destination intended by the peer; +- **FORWARD_REPLACE**: forward a modified payload to the intended destination; +- **DROP**: drop the received payload; +- **ANSWER**: answer the peer directly with a payload, without forwarding its original payload to the other peer; +- **REDIRECT_TO**: (client-side only) redirects the connection of the client towards a new remote peer. + +The ``ctx`` attribute in the callbacks contains context relative to the current client. It can also be use to +store additional data specific to the session. + +If we were to use this machine in SERVER mode, we would call it like: + +.. code:: python + + NOPFwdMachine( + mode=ForwardMachine.MODE.SERVER, + port=12345, + bind_address="0.0.0.0", # the address we bind on + remote_address="192.168.0.1", # the server to redirect this to by default + cls=conf.raw_layer, # Default Raw layer: we don't know the type of data + ).run() + +TLS support +___________ + +``ForwardMachine`` has support for TLS through the ``ssl=True`` argument. When TLS is enabled, the SNI (Server Name Indication) is +properly forwarded to the remote peer, and can be accessed through the ``ctx.tls_sni_name`` attribute in the callbacks. + +**By default, a ForwardMachine generates self-signed certificates** that copy the attributes from the certificate of the remote +server. This behavior can be changed by specifying a certificate (which will be served by the TLS stack). + +We can run the same ForwardMachine as from the previous example, this time with self-signed TLS. + +.. code:: python + + # Run it + NOPFwdMachine( + mode=ForwardMachine.MODE.SERVER, + port=443, + cls=HTTP, + ssl=True, + ).run() + +Configuring TPROXY +__________________ + +TPROXY is a special socket mode that allows to bind a socket that listens for traffic that isn't directed at a local address. This is typically used by "transparent TLS proxies" to achieve their functionality, and is expected to be setup on a linux router. + +The ``ForwardingMachine`` supports TPROXY, which allows to intercept and modify all the traffic by many clients to many destinations, for instance on a specific port. This is much more versatile that a classic bind + socket, which would typically forward multiple clients to a single destination. + +Here are the steps: + +- Setup an interface that one can redirect traffic to, and that has TPROXY support. +- Bind the ``ForwardingMachine`` on that interface. +- Redirect some traffic to that interface, using ``iptables`` or ``nftables``, based on some arbitrary criteria. + +For ease of use, a script ``vethrelay.sh`` is provided to setup a veth (virtual ethernet) interface that can be used to bind the ``ForwardingMachine`` on. This script is available at https://github.com/secdev/scapy/blob/master/doc/scapy/_static/vethrelay.sh + +.. code:: bash + + ./vethrelay.sh setup + Interface vethrelay is now setup with IPv4: 2.2.2.2 ! + + Add listening rules as follow: + + # TPROXY incoming TCP packets on port 80 to vethrelay on port 8080 + iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080 --on-ip 2.2.2.2 + + # Listen on wlp4s0 for incoming packets on port 80 (on the interface where it really comes from) + iptables -A INPUT -i wlp4s0 -p tcp --dport 80 -j ACCEPT + +As the instructions say, to have traffic to anything on the port 80 go through the ``ForwardingMachine``, one can run the commands listed above assuming that the machine is started as such: + +.. code:: python + + NOPFwdMachine( + mode=ForwardMachine.MODE.TPROXY, + port=8080, + cls=HTTP, + ).run() diff --git a/doc/scapy/advanced_usage/index.rst b/doc/scapy/advanced_usage/index.rst new file mode 100644 index 00000000000..0e617423fc6 --- /dev/null +++ b/doc/scapy/advanced_usage/index.rst @@ -0,0 +1,10 @@ +.. Advanced usage documentation + +Advanced usage +============== + +.. toctree:: + :glob: + :titlesonly: + + * \ No newline at end of file diff --git a/doc/scapy/advanced_usage/pipetools.rst b/doc/scapy/advanced_usage/pipetools.rst new file mode 100644 index 00000000000..dbc7b6ce23a --- /dev/null +++ b/doc/scapy/advanced_usage/pipetools.rst @@ -0,0 +1,347 @@ +.. _pipetools: + +PipeTools +========= + +Scapy's ``pipetool`` is a smart piping system allowing to perform complex stream data management. + +The goal is to create a sequence of steps with one or several inputs and one or several outputs, with a bunch of blocks in between. +PipeTools can handle varied sources of data (and outputs) such as user input, pcap input, sniffing, wireshark... +A pipe system is implemented by manually linking all its parts. It is possible to dynamically add an element while running or set multiple drains for the same source. + +.. note:: Pipetool default objects are located inside ``scapy.pipetool`` + +Demo: sniff, anonymize, send to Wireshark +----------------------------------------- + +The following code will sniff packets on the default interface, anonymize the source and destination IP addresses and pipe it all into Wireshark. Useful when posting online examples, for instance. + +.. code-block:: python3 + + source = SniffSource(iface=conf.iface) + wire = WiresharkSink() + def transf(pkt): + if not pkt or IP not in pkt: + return pkt + pkt[IP].src = "1.1.1.1" + pkt[IP].dst = "2.2.2.2" + return pkt + + source > TransformDrain(transf) > wire + p = PipeEngine(source) + p.start() + p.wait_and_stop() + +The engine is pretty straightforward: + +.. image:: ../graphics/pipetool_demo.svg + +Let's run it: + +.. image:: ../graphics/animations/pipetool_demo.gif + +Class Types +----------- + +There are 3 different class of objects used for data management: + +- ``Sources`` +- ``Drains`` +- ``Sinks`` + +They are executed and handled by a :class:`~scapy.pipetool.PipeEngine` object. + +When running, a pipetool engine waits for any available data from the Source, and send it in the Drains linked to it. +The data then goes from Drains to Drains until it arrives in a Sink, the final state of this data. + +Let's see with a basic demo how to build a pipetool system. + +.. image:: ../graphics/pipetool_engine.png + +For instance, this engine was generated with this code: + +.. code:: pycon + + >>> s = CLIFeeder() + >>> s2 = CLIHighFeeder() + >>> d1 = Drain() + >>> d2 = TransformDrain(lambda x: x[::-1]) + >>> si1 = ConsoleSink() + >>> si2 = QueueSink() + >>> + >>> s > d1 + >>> d1 > si1 + >>> d1 > si2 + >>> + >>> s2 >> d1 + >>> d1 >> d2 + >>> d2 >> si1 + >>> + >>> p = PipeEngine() + >>> p.add(s) + >>> p.add(s2) + >>> p.graph(target="> the_above_image.png") + +``start()`` is used to start the :class:`~scapy.pipetool.PipeEngine`: + +.. code:: pycon + + >>> p.start() + +Now, let's play with it by sending some input data + +.. code:: pycon + + >>> s.send("foo") + >'foo' + >>> s2.send("bar") + >>'rab' + >>> s.send("i like potato") + >'i like potato' + >>> print(si2.recv(), ":", si2.recv()) + foo : i like potato + +Let's study what happens here: + +- there are **two canals** in a :class:`~scapy.pipetool.PipeEngine`, a lower one and a higher one. Some Sources write on the lower one, some on the higher one and some on both. +- most sources can be linked to any drain, on both lower and higher canals. The use of ``>`` indicates a link on the low canal, and ``>>`` on the higher one. +- when we send some data in ``s``, which is on the lower canal, as shown above, it goes through the :class:`~scapy.pipetool.Drain` then is sent to the :class:`~.scapy.pipetool.QueueSink` and to the :class:`~scapy.pipetool.ConsoleSink` +- when we send some data in ``s2``, it goes through the Drain, then the TransformDrain where the data is reversed (see the lambda), before being sent to :class:`~scapy.pipetool.ConsoleSink` only. This explains why we only have the data of the lower sources inside the QueueSink: the higher one has not been linked. + +Most of the sinks receive from both lower and upper canals. This is verifiable using the `help(ConsoleSink)` + +.. code:: pycon + + >>> help(ConsoleSink) + Help on class ConsoleSink in module scapy.pipetool: + class ConsoleSink(Sink) + | Print messages on low and high entries + | +-------+ + | >>-|--. |->> + | | print | + | >-|--' |-> + | +-------+ + | + [...] + + +Sources +^^^^^^^ + +A Source is a class that generates some data. + +There are several source types integrated with Scapy, usable as-is, but you may +also create yours. + +Default Source classes +~~~~~~~~~~~~~~~~~~~~~~ + +For any of those class, have a look at ``help([theclass])`` to get more information or the required parameters. + +- :class:`~scapy.pipetool.CLIFeeder` : a source especially used in interactive software. its ``send(data)`` generates the event data on the lower canal +- :class:`~scapy.pipetool.CLIHighFeeder` : same than CLIFeeder, but writes on the higher canal +- :class:`~scapy.pipetool.PeriodicSource` : Generate messages periodically on the low canal. +- :class:`~scapy.pipetool.AutoSource`: the default source, that must be extended to create custom sources. + +Create a custom Source +~~~~~~~~~~~~~~~~~~~~~~ + +To create a custom source, one must extend the :class:`~scapy.pipetool.AutoSource` class. + +.. note:: + + Do NOT use the default :class:`~scapy.pipetool.Source` class except if you are really sure of what you are doing: it is only used internally, and is missing some implementation. The :class:`~scapy.pipetool.AutoSource` is made to be used. + + +To send data through it, the object must call its ``self._gen_data(msg)`` or ``self._gen_high_data(msg)`` functions, which send the data into the PipeEngine. + +The Source should also (if possible), set ``self.is_exhausted`` to ``True`` when empty, to allow the clean stop of the :class:`~scapy.pipetool.PipeEngine`. If the source is infinite, it will need a force-stop (see PipeEngine below) + +For instance, here is how :class:`~scapy.pipetool.CLIHighFeeder` is implemented: + +.. code:: python3 + + class CLIFeeder(CLIFeeder): + def send(self, msg): + self._gen_high_data(msg) + def close(self): + self.is_exhausted = True + +Drains +^^^^^^ + +Default Drain classes +~~~~~~~~~~~~~~~~~~~~~ + +Drains need to be linked on the entry that you are using. It can be either on the lower one (using ``>``) or the upper one (using ``>>``). +See the basic example above. + +- :class:`~scapy.pipetool.Drain` : the most basic Drain possible. Will pass on both low and high entry if linked properly. +- :class:`~scapy.pipetool.TransformDrain` : Apply a function to messages on low and high entry +- :class:`~scapy.pipetool.UpDrain` : Repeat messages from low entry to high exit +- :class:`~scapy.pipetool.DownDrain` : Repeat messages from high entry to low exit + +Create a custom Drain +~~~~~~~~~~~~~~~~~~~~~ + +To create a custom drain, one must extend the :class:`~scapy.pipetool.Drain` class. + +A :class:`~scapy.pipetool.Drain` object will receive data from the lower canal in its ``push`` method, and from the higher canal from its ``high_push`` method. + +To send the data back into the next linked Drain / Sink, it must call the ``self._send(msg)`` or ``self._high_send(msg)`` methods. + +For instance, here is how :class:`~scapy.pipetool.TransformDrain` is implemented:: + + class TransformDrain(Drain): + def __init__(self, f, name=None): + Drain.__init__(self, name=name) + self.f = f + def push(self, msg): + self._send(self.f(msg)) + def high_push(self, msg): + self._high_send(self.f(msg)) + +Sinks +^^^^^ + +Sinks are destinations for messages. + +A :py:class:`~scapy.pipetool.Sink` receives data like a :py:class:`~scapy.pipetool.Drain`, but doesn't send any +messages after it. + +Messages on the low entry come from :py:meth:`~scapy.pipetool.Sink.push`, and messages on the +high entry come from :py:meth:`~scapy.pipetool.Sink.high_push`. + +Default Sinks classes +~~~~~~~~~~~~~~~~~~~~~ + +- :class:`~scapy.pipetool.ConsoleSink` : Print messages on low and high entries to ``stdout`` +- :class:`~scapy.pipetool.RawConsoleSink` : Print messages on low and high entries, using os.write +- :class:`~scapy.pipetool.TermSink` : Prints messages on the low and high entries, on a separate terminal +- :class:`~scapy.pipetool.QueueSink` : Collects messages on the low and high entries into a :py:class:`Queue` + +Create a custom Sink +~~~~~~~~~~~~~~~~~~~~ + +To create a custom sink, one must extend :py:class:`~scapy.pipetool.Sink` and implement +:py:meth:`~scapy.pipetool.Sink.push` and/or :py:meth:`~scapy.pipetool.Sink.high_push`. + +This is a simplified version of :py:class:`~scapy.pipetool.ConsoleSink`: + +.. code-block:: python3 + + class ConsoleSink(Sink): + def push(self, msg): + print(">%r" % msg) + def high_push(self, msg): + print(">>%r" % msg) + +Link objects +------------ + +As shown in the example, most sources can be linked to any drain, on both low +and high entry. + +The use of ``>`` indicates a link on the low entry, and ``>>`` on the high +entry. + +For example, to link ``a``, ``b`` and ``c`` on the low entries: + +.. code-block:: pycon + + >>> a = CLIFeeder() + >>> b = Drain() + >>> c = ConsoleSink() + >>> a > b > c + >>> p = PipeEngine() + >>> p.add(a) + +This wouldn't link the high entries, so something like this would do nothing: + +.. code-block:: pycon + + >>> a2 = CLIHighFeeder() + >>> a2 >> b + >>> a2.send("hello") + +Because ``b`` (:py:class:`~scapy.pipetool.Drain`) and ``c`` (:py:class:`scapy.pipetool.ConsoleSink`) are not +linked on the high entry. + +However, using a :py:class:`~scapy.pipetool.DownDrain` would bring the high messages from +:py:class:`~scapy.pipetool.CLIHighFeeder` to the lower channel: + +.. code-block:: pycon + + >>> a2 = CLIHighFeeder() + >>> b2 = DownDrain() + >>> a2 >> b2 + >>> b2 > b + >>> a2.send("hello") + +The PipeEngine class +-------------------- + +The :class:`~scapy.pipetool.PipeEngine` class is the core class of the Pipetool system. It must be initialized and passed the list of all Sources. + +There are two ways of passing sources: + +- during initialization: ``p = PipeEngine(source1, source2, ...)`` +- using the ``add(source)`` method + +A :class:`~scapy.pipetool.PipeEngine` class must be started with ``.start()`` function. It may be force-stopped with the ``.stop()``, or cleanly stopped with ``.wait_and_stop()`` + +A clean stop only works if the Sources is exhausted (has no data to send left). + +It can be printed into a graph using ``.graph()`` methods. see ``help(do_graph)`` for the list of available keyword arguments. + +Scapy advanced PipeTool objects +------------------------------- + +.. note:: Unlike the previous objects, those are not located in ``scapy.pipetool`` but in ``scapy.scapypipes`` + +Now that you know the default PipeTool objects, here are some more advanced ones, based on packet functionalities. + +- :class:`~scapy.scapypipes.SniffSource` : Read packets from an interface and send them to low exit. +- :class:`~scapy.scapypipes.RdpcapSource` : Read packets from a PCAP file send them to low exit. +- :class:`~scapy.scapypipes.InjectSink` : Packets received on low input are injected (sent) to an interface +- :class:`~scapy.scapypipes.WrpcapSink` : Packets received on low input are written to PCAP file +- :class:`~scapy.scapypipes.UDPDrain` : UDP payloads received on high entry are sent over UDP (complicated, have a look at ``help(UDPDrain)``) +- :class:`~scapy.scapypipes.FDSourceSink` : Use a file descriptor as source and sink +- :class:`~scapy.scapypipes.TCPConnectPipe`: TCP connect to addr:port and use it as source and sink +- :class:`~scapy.scapypipes.TCPListenPipe` : TCP listen on [addr:]port and use the first connection as source and sink (complicated, have a look at ``help(TCPListenPipe)``) + +Triggering +---------- + +Some special sort of Drains exists: the Trigger Drains. + +Trigger Drains are special drains, that on receiving data not only pass it by but also send a "Trigger" input, that is received and handled by the next triggered drain (if it exists). + +For example, here is a basic :class:`~scapy.scapypipes.TriggerDrain` usage: + +.. code:: pycon + + >>> a = CLIFeeder() + >>> d = TriggerDrain(lambda msg: True) # Pass messages and trigger when a condition is met + >>> d2 = TriggeredValve() + >>> s = ConsoleSink() + >>> a > d > d2 > s + >>> d ^ d2 # Link the triggers + >>> p = PipeEngine(s) + >>> p.start() + INFO: Pipe engine thread started. + >>> + >>> a.send("this will be printed") + >'this will be printed' + >>> a.send("this won't, because the valve was switched") + >>> a.send("this will, because the valve was switched again") + >'this will, because the valve was switched again' + >>> p.stop() + +Several triggering Drains exist, they are pretty explicit. It is highly recommended to check the doc using ``help([the class])`` + +- :class:`~scapy.scapypipes.TriggeredMessage` : Send a preloaded message when triggered and trigger in chain +- :class:`~scapy.scapypipes.TriggerDrain` : Pass messages and trigger when a condition is met +- :class:`~scapy.scapypipes.TriggeredValve` : Let messages alternatively pass or not, changing on trigger +- :class:`~scapy.scapypipes.TriggeredQueueingValve` : Let messages alternatively pass or queued, changing on trigger +- :class:`~scapy.scapypipes.TriggeredSwitch` : Let messages alternatively high or low, changing on trigger diff --git a/doc/scapy/backmatter.rst b/doc/scapy/backmatter.rst index f316bb2a879..56f1c2ed175 100644 --- a/doc/scapy/backmatter.rst +++ b/doc/scapy/backmatter.rst @@ -1,10 +1,18 @@ -********* +******* Credits -********* +******* -- Philippe Biondi is Scapy's author. He has also written most of the documentation. -- Pierre Lalet, Gabriel Potter, Guillaume Valadon are the current most active maintainers and contributors. +The maintainers of Scapy are: + - Pierre Lalet + - Gabriel Potter (Lead maintainer) + - Guillaume Valadon + - Nils Weiss + +Former maintainers include: + - Philippe Biondi, who was Scapy's original author. + +Other documentation credits include: - Fred Raynal wrote the chapter on building and dissecting packets. - Peter Kacherginsky contributed several tutorial sections, one-liners and recipes. - Dirk Loss integrated and restructured the existing docs to make this book. diff --git a/doc/scapy/build_dissect.rst b/doc/scapy/build_dissect.rst index 1601332c14b..2f53c349933 100644 --- a/doc/scapy/build_dissect.rst +++ b/doc/scapy/build_dissect.rst @@ -2,7 +2,7 @@ Adding new protocols ******************** -Adding new protocol (or more correctly: a new *layer*) in Scapy is very easy. All the magic is in the fields. If the +Adding a new protocol (or more correctly: a new *layer*) in Scapy is very easy. All the magic is in the fields. If the fields you need are already there and the protocol is not too brain-damaged, this should be a matter of minutes. @@ -187,24 +187,24 @@ last byte. For instance, 0x123456 will be coded as 0xC8E856:: def vlenq2str(l): s = [] - s.append( hex(l & 0x7F) ) + s.append(l & 0x7F) l = l >> 7 - while l>0: - s.append( hex(0x80 | (l & 0x7F) ) ) + while l > 0: + s.append( 0x80 | (l & 0x7F) ) l = l >> 7 s.reverse() - return "".join(chr(int(x, 16)) for x in s) + return bytes(bytearray(s)) - def str2vlenq(s=""): + def str2vlenq(s=b""): i = l = 0 - while i>> f = FOO(data="A"*129) - >>> f.show() - ###[ FOO ]### - len= 0 - data= 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - -Here, ``len`` is not yet computed and only the default value are -displayed. This is the current internal representation of our + >>> f = FOO(data="A"*129) + >>> f.show() + ###[ FOO ]### + len= None + data= 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + +Here, ``len`` has yet to be computed and only the default value is +displayed. This is the current internal representation of our layer. Let's force the computation now:: >>> f.show2() @@ -259,7 +259,7 @@ layer. Let's force the computation now:: len= 129 data= 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' -The method ``show2()`` displays the fields with their values as they will +The method ``show2()`` displays the fields with their values as they will be sent to the network, but in a human readable way, so we see ``len=129``. Last but not least, let us look now at the machine representation:: @@ -649,7 +649,7 @@ look to its building process:: def post_build(self, p, pay): if self.len is None and pay: l = len(pay) - p = p[:1] + hex(l)[2:]+ p[2:] + p = p[:1] + struct.pack("!B", l) + p[2:] return p+pay When ``post_build()`` is called, ``p`` is the current layer, ``pay`` the payload, @@ -869,10 +869,12 @@ Legend: XShortField X3BytesField # three bytes as hex - LEX3BytesField # little endian three bytes as hex + XLE3BytesField # little endian three bytes as hex ThreeBytesField # three bytes as decimal LEThreeBytesField # little endian three bytes as decimal - + LE3BytesEnumField + XLE3BytesEnumField + IntField SignedIntField LEIntField @@ -1043,6 +1045,7 @@ Special # Wrapper to make field 'fld' only appear if # function 'cond' evals to True, e.g. # ConditionalField(XShortField("chksum",None),lambda pkt:pkt.chksumpresent==1) + # When hidden, it won't be built nor dissected and the stored value will be 'None' PadField(fld, align, padwith=None) @@ -1135,6 +1138,7 @@ Field naming convention ----------------------- The goal is to keep the writing of packets fluent and intuitive. The basic instructions are the following : +* Do not use any value from the ``Packet.__slots__``` list as a field name (such as name, time or original), as they are reserved for Scapy internals * Use inverted camel case and common abbreviations (e.g. len, src, dst, dstPort, srcIp). * Wherever it is either possible or relevant, prefer using the names from the specifications. This aims to help newcomers to easily forge packets. diff --git a/doc/scapy/conf.py b/doc/scapy/conf.py index 61611e36c0f..4f0603971db 100644 --- a/doc/scapy/conf.py +++ b/doc/scapy/conf.py @@ -28,7 +28,7 @@ # If your documentation needs a minimal Sphinx version, state it here. # -needs_sphinx = '2.2.0' +needs_sphinx = '3.0.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -36,6 +36,8 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', + 'sphinx.ext.todo', + 'sphinx.ext.linkcode', 'scapy_doc' ] @@ -45,6 +47,12 @@ 'undoc-members': True } +# Enable the todo module +todo_include_todos = True + +# Linkcode resolver +from linkcode_res import linkcode_resolve + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -60,7 +68,7 @@ # General information about the project. project = 'Scapy' year = datetime.datetime.now().year -copyright = '2008-%s Philippe Biondi and the Scapy community' % year +copyright = '2008-%s The Scapy community' % year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -76,7 +84,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -89,6 +97,12 @@ # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +# Enable codeauthor and sectionauthor directives +show_authors = True + +# Mock python-can +autodoc_mock_imports = ["can"] + # -- Options for HTML output ---------------------------------------------- @@ -154,7 +168,7 @@ # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Scapy.tex', 'Scapy Documentation', - 'Philippe Biondi and the Scapy community', 'manual'), + 'The Scapy community', 'manual'), ] @@ -164,7 +178,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'scapy', 'Scapy Documentation', - ['Philippe Biondi and the Scapy community'], 1) + ['The Scapy community'], 1) ] @@ -175,7 +189,9 @@ # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Scapy', 'Scapy Documentation', - 'Philippe Biondi and the Scapy community', 'Scapy', + 'The Scapy community', 'Scapy', '', 'Miscellaneous'), ] + +suppress_warnings = ["app.add_directive", "ref.python"] \ No newline at end of file diff --git a/doc/scapy/development.rst b/doc/scapy/development.rst index 657f6698d04..90df9807776 100644 --- a/doc/scapy/development.rst +++ b/doc/scapy/development.rst @@ -120,7 +120,7 @@ The generic format for a test campaign is shown in the following table:: * Comments for unit test 1 # Python statements follow a = 1 - print a + print(a) a == 1 @@ -196,7 +196,7 @@ Table 5 shows a simple test campaign with multiple tests set definitions. Additi = Unit Test 1 ~ test_set_1 simple a = 1 - print a + print(a) = Unit test 2 ~ test_set_1 simple @@ -234,7 +234,7 @@ Table 5 shows a simple test campaign with multiple tests set definitions. Additi = Unit Test 6 ~ test_set_2 hardest - print e + print(e) e == 1296 To see an example that is targeted to Scapy, go to http://www.secdev.org/projects/UTscapy. Cut and paste the example at the bottom of the page to the file ``demo_campaign.txt`` and run UTScapy against it:: @@ -254,6 +254,15 @@ all Scapy unit tests automatically without any external dependency:: tox -- -K vcan_socket -K tcpdump -K tshark -K nmap -K manufdb -K crypto +.. note:: This will trigger the unit tests on all available Python versions + unless you specify a `-e` option. See below + +For your convenience, and for package maintainers, we provide a util that +run tox on only a single (default Python) environment, again with no external +dependencies:: + + ./test/run_tests + VIM syntax highlighting for .uts files -------------------------------------- @@ -284,17 +293,47 @@ signing a commit, the maintainer that wishes to create a release must: Taking v2.4.3 as an example, the following commands can be used to sign and publish the release:: - git tag -s v2.4.3 -m "Release 2.4.3" - git tag v2.4.3 -v - git push --tags + $ git tag -s v2.4.3 -m "Release 2.4.3" + $ git tag v2.4.3 -v + $ git push --tags Release Candidates (RC) could also be done. For example, the first RC will be tagged v2.4.3rc1 and the message ``2.4.3 Release Candidate #1``. -Prior to uploading the release to PyPi, the ``author_email`` in ``setup.py`` -must be changed to the address of the maintainer performing the release. The -following commands can then be used:: +.. note:: + To add a signing key, configure to use a SSH one, then register it via:: + $ git config --global gpg.format ssh + $ git config --global user.signingkey ~/.ssh/examplekey.pub + +Prior to uploading the release to PyPi, the mail address of the maintainer +performing the release must be added next to his name in ``pyproject.toml``. +See `this `_ for details. + +The following commands can then be used:: + + $ pip install --upgrade build + $ SCAPY_VERSION=2.6.0rc1 python -m build + $ twine check dist/* + $ twine upload dist/* + +.. warning:: + Make sure that you don't have left-overs in your ``dist/`` folder ! There should only be the source and the wheel for the package. + Also check that the wheel ends in ``*-py3-none-any.whl`` ! + + +Packaging Scapy +=============== + +When packaging Scapy, you should build the source while setting the ``SCAPY_VERSION`` variable, in order to make sure that the version remains consistent. + +.. code:: bash + + $ SCAPY_VERSION=2.5.0 python3 -m build + ... + Successfully built scapy-2.5.0.tar.gz and scapy-2.5.0-py3-none-any.whl + +If you want to test Scapy while packaging it, you are encouraged to use the ``./run_tests`` script with no arguments. It will run a subset of the tests that don't use any external dependency, and will be easier to test. The only dependency is ``tox`` + +.. code:: bash - python3 setup.py sdist - twine check dist/scapy-2.4.3.tar.gz - twine upload dist/scapy-2.4.3.tar.gz + $ ./test/run_tests diff --git a/doc/scapy/extending.rst b/doc/scapy/extending.rst index 41ccb9654e8..569418e7f1d 100644 --- a/doc/scapy/extending.rst +++ b/doc/scapy/extending.rst @@ -22,6 +22,25 @@ This first example takes an IP or a name as first parameter, send an ICMP echo r if p: p.show() +Configuring Scapy's logger +-------------------------- + +Scapy configures a logger automatically using Python's ``logging`` module. This +logger is custom to support things like colors and frequency filters. By +default, it is set to ``WARNING`` (when not in interactive mode), but you can +change that using for instance:: + + import logging + logging.getLogger("scapy").setLevel(logging.CRITICAL) + +To disable almost all logs. (Scapy simply won't work properly if a CRITICAL +failure occurs) + +.. note:: On interactive mode, the default log level is ``INFO`` + +More examples +------------- + This is a more complex example which does an ARP ping and reports what it found with LaTeX formatting:: #! /usr/bin/env python @@ -29,22 +48,22 @@ This is a more complex example which does an ARP ping and reports what it found import sys if len(sys.argv) != 2: - print "Usage: arping2tex \n eg: arping2tex 192.168.1.0/24" + print("Usage: arping2tex \n eg: arping2tex 192.168.1.0/24") sys.exit(1) - from scapy.all import srp,Ether,ARP,conf - conf.verb=0 - ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=sys.argv[1]), - timeout=2) + from scapy.all import srp, Ether, ARP, conf + conf.verb = 0 + ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=sys.argv[1]), + timeout=2) - print r"\begin{tabular}{|l|l|}" - print r"\hline" - print r"MAC & IP\\" - print r"\hline" + print(r"\begin{tabular}{|l|l|}") + print(r"\hline") + print(r"MAC & IP\\") + print(r"\hline") for snd,rcv in ans: - print rcv.sprintf(r"%Ether.src% & %ARP.psrc%\\") - print r"\hline" - print r"\end{tabular}" + print(rcv.sprintf(r"%Ether.src% & %ARP.psrc%\\")) + print(r"\hline") + print(r"\end{tabular}") Here is another tool that will constantly monitor all interfaces on a machine and print all ARP request it sees, even on 802.11 frames from a Wi-Fi card in monitor mode. Note the store=0 parameter to sniff() to avoid storing all packets in memory for nothing:: @@ -73,7 +92,6 @@ Once you've done that, you can launch Scapy and import your file, but this is st import logging logger = logging.getLogger("scapy") logger.setLevel(logging.INFO) - logger.addHandler(logging.StreamHandler()) from scapy.all import * diff --git a/doc/scapy/graphics/ATMT_TCP_client.svg b/doc/scapy/graphics/ATMT_TCP_client.svg new file mode 100644 index 00000000000..a89ca986940 --- /dev/null +++ b/doc/scapy/graphics/ATMT_TCP_client.svg @@ -0,0 +1,132 @@ + + + + + + +Automaton_metaclass + + + +START + +START + + + +SYN_SENT + +SYN_SENT + + + +START->SYN_SENT + + +connect +>[send_syn] + + + +CLOSED + +CLOSED + + + +STOP + +STOP + + + +STOP_SENT_FIN_ACK + +STOP_SENT_FIN_ACK + + + +STOP->STOP_SENT_FIN_ACK + + +stop_requested +>[stop_send_finack] + + + +ESTABLISHED + +ESTABLISHED + + + +SYN_SENT->ESTABLISHED + + +synack_received +>[send_ack_of_synack] + + + +STOP_SENT_FIN_ACK->CLOSED + + +stop_fin_received +>[stop_send_ack] + + + +STOP_SENT_FIN_ACK->CLOSED + + +stop_ack_timeout/1.0s + + + +ESTABLISHED->CLOSED + + +reset_received + + + +ESTABLISHED->ESTABLISHED + + +incoming_data_received +>[receive_data] + + + +ESTABLISHED->ESTABLISHED + + +outgoing_data_received +>[send_data] + + + +LAST_ACK + +LAST_ACK + + + +ESTABLISHED->LAST_ACK + + +fin_received +>[send_finack] + + + +LAST_ACK->CLOSED + + +ack_of_fin_received + + + diff --git a/doc/scapy/graphics/animations/animation-scapy-demo.svg b/doc/scapy/graphics/animations/animation-scapy-demo.svg deleted file mode 100755 index 7e7268f74c6..00000000000 --- a/doc/scapy/graphics/animations/animation-scapy-demo.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - demo@scapy:~/github/scapy# demo@scapy:~/github/scapy# s demo@scapy:~/github/scapy# su demo@scapy:~/github/scapy# sud demo@scapy:~/github/scapy# sudo demo@scapy:~/github/scapy# sudo demo@scapy:~/github/scapy# sudo . demo@scapy:~/github/scapy# sudo ./ demo@scapy:~/github/scapy# sudo ./r demo@scapy:~/github/scapy# sudo ./ru demo@scapy:~/github/scapy# sudo ./run_scapy WARNING: No route found for IPv6 destination :: (no default route?) e apyyyyCY//////////YCa | >>> >>> e >>> ex >>> exp >>> expl >>> explo >>> explor >>> explore >>> explore( >>> explore() demo@scapy:~/github/scapy# sudo ./run_scapyINFO: Can't import matplotlib. Won't be able to plot.WARNING: No route found for IPv6 destination :: (no default route?)e aSPY//YASa apyyyyCY//////////YCa | sY//////YSpcs scpCY//Pp | Welcome to Scapy ayp ayyyyyyySCP//Pp syY//C | Version 2.4.2.dev228 AYAsAYYYYYYYY///Ps cY//S | pCCCCY//p cSSps y//Y | https://github.com/secdev/scapy SPPPP///a pP///AC//Y | A//A cyP////C | Have fun! p///Ac sC///a | P////YCpc A//A | We are in France, we say Skappee. scccccp///pSP///p p//Y | OK? Merci. sY/////////y caa S//P | -- Sebastien Chabal cayCyayP//Ya pY/Ya | sY/PsY////YCc aC//Yp sc sccaCY//PCypaapyCP//YSs spCPY//////YPSps ccaacs using IPython 7.2.0>>> explore() ┌────────────────────────| Scapy v2.4.2.dev228 |────────────────────────┠│ │ │ Chose the type of packets you want to explore: │ │ < Layers > < Contribs > < Cancel > │ └───────────────────────────────────────────────────────────────────────┘ │ │ < Layers > < Contribs > < Cancel > │ │ │ │ │ │ ( ) IPv4 (Internet Protoco │ ( ) Bluetooth 4LE layer │ ( ) Wireless MAC according to IEEE 802.15.4. │ │ ( ) IrDA infrared data co │ ( ) LLMNR (Link Local Multicast Node Resolution). │ (*) Packet class. Binding mechanism. fuzz() method. ^ │ └─────────────────────────────────────────────── │ (*) Packet class. Binding mechanism. fuzz() method. ^ │ │ ( ) ASN.1 Packet │ │ ( ) ASN.1 Packet │ │ ( ) Bluetooth layers, sockets and send/receive functions. │ │ ( ) Bluetooth layers, sockets and send/receive functions. │ │ ( ) Classes and functions for layer 2 protocols. │ │ ( ) Classes and functions for layer 2 protocols. │ │ ( ) IPv4 (Internet Protocol v4). │ │ ( ) IPv4 (Internet Protocol v4). │ │ (*) IPv4 (Internet Protocol v4). │ │ < Ok > < Cancel > │ ┌───────────────────────────────────────────| Scapy v2.4.2.dev228 |────────────────────────────────────────────┠│ Please select a layer among the following, to see all packets contained in it: │ │ ( ) IPv6 (Internet Protocol v6). │ │ ( ) Wireless LAN according to IEEE 802.11. │ │ ( ) Per-Packet Information (PPI) Protocol │ │ ( ) Bluetooth 4LE layer │ │ ( ) DHCP (Dynamic Host Configuration Protocol) and BOOTP │ │ ( ) DHCPv6: Dynamic Host Configuration Protocol for IPv6. [RFC 3315] │ │ ( ) DNS: Domain Name System. │ │ ( ) Extensible Authentication Protocol (EAP) │ │ ( ) GPRS (General Packet Radio Service) for mobile data communication. │ │ ( ) HSRP (Hot Standby Router Protocol): proprietary redundancy protocol for Cisco routers. # noqa: E501 │ │ ( ) IPsec layer │ │ ( ) IrDA infrared data communication. │ │ ( ) ISAKMP (Internet Security Association and Key Management Protocol). │ │ ( ) PPP (Point to Point Protocol) │ │ ( ) L2TP (Layer 2 Tunneling Protocol) for VPNs. │ │ ( ) LLMNR (Link Local Multicast Node Resolution). v │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ ( ) Packet class. Binding mechanism. fuzz() method. ^ │ │ (*) IPv4 (Internet Protocol v4). │ │ < Ok > < Cancel > │ │ ( ) ASN.1 Packet │ ( ) DHCPv6: Dynamic Host Configuration Protocol for IPv │ ( ) GPRS (General Packet Radio Service) for mobile data communication. │ < Ok > < Cancel > │ Packets contained in scapy.layers.inet: Class |Name--------------------------|------- --------------------------|-------------------------------------------ICMP |ICMPICMPerror |ICMP in ICMPIP |IPIPOption |IP OptionIPOption_Address_Extension|IP Option Address ExtensionIPOption_EOL |IP Option End of Options ListIPOption_LSRR |IP Option Loose Source and Record RouteIPOption_MTU_Probe |IP Option MTU ProbeIPOption_MTU_Reply |IP Option MTU ReplyIPOption_NOP |IP Option No OperationIPOption_RR |IP Option Record RouteIPOption_Router_Alert |IP Option Router AlertIPOption_SDBM |IP Option Selective Directed Broadcast ModeIPOption_SSRR |IP Option Strict Source and Record RouteIPOption_Security |IP Option SecurityIPOption_Stream_Id |IP Option Stream IDIPOption_Traceroute |IP Option TracerouteIPerror |IP in ICMPTCP |TCPTCPerror |TCP in ICMPUDP >>> l >>> ls >>> ls( >>> ls(I >>> ls(IP >>> ls(IP) UDP |UDPUDPerror |UDP in ICMP>>> ls(IP) >>> p >>> pk >>> pkt >>> pkt >>> pkt = >>> pkt = >>> pkt = I >>> pkt = IP >>> pkt = IP( >>> pkt = IP(d >>> pkt = IP(ds >>> pkt = IP(dst >>> pkt = IP(dst= >>> pkt = IP(dst=" >>> pkt = IP(dst="w >>> pkt = IP(dst="ww >>> pkt = IP(dst="www >>> pkt = IP(dst="www. >>> pkt = IP(dst="www.s >>> pkt = IP(dst="www.se >>> pkt = IP(dst="www.sec >>> pkt = IP(dst="www.secd >>> pkt = IP(dst="www.secde >>> pkt = IP(dst="www.secdev >>> pkt = IP(dst="www.secdev. >>> pkt = IP(dst="www.secdev.o >>> pkt = IP(dst="www.secdev.or >>> pkt = IP(dst="www.secdev.org >>> pkt = IP(dst="www.secdev.org" >>> pkt = IP(dst="www.secdev.org") >>> pkt = IP(dst="www.secdev.org")/ >>> pkt = IP(dst="www.secdev.org")/I >>> pkt = IP(dst="www.secdev.org")/IC >>> pkt = IP(dst="www.secdev.org")/ICM >>> pkt = IP(dst="www.secdev.org")/ICMP >>> pkt = IP(dst="www.secdev.org")/ICMP( version : BitField (4 bits) = (4) ihl : BitField (4 bits) = (None)tos : XByteField = (0)len : ShortField = (None)id : ShortField = (1)flags : FlagsField (3 bits) = (<Flag 0 ()>)frag : BitField (13 bits) = (0)ttl : ByteField = (64)proto : ByteEnumField = (0)chksum : XShortField = (None)src : SourceIPField = (None)dst : DestIPField = (None)options : PacketListField = ([])>>> pkt = IP(dst="www.secdev.org")/ICMP() >>> pkt. >>> pkt.s >>> pkt.sh >>> pkt.sho >>> pkt.show >>> pkt.show2 >>> pkt.show2( >>> pkt.show2() >>> pkt = IP(dst="www.secdev.org")/ICMP() >>> pkt.show2() >>> >>> s ###[ IP ]### version= 4 ihl= 5 tos= 0x0 len= 28 id= 1 flags= frag= 0 ttl= 64 proto= icmp chksum= 0x875a src= 212.83.148.19 dst= 217.25.178.5 \options\###[ ICMP ]### type= echo-request code= 0 chksum= 0xf7ff id= 0x0 seq= 0x0>>> sr >>> sr sr() sr1flood() srbt1() srloop() srp1() srpflood() sr1() srbt() srflood() srp() srp1flood() srploop() sr() sr1flood() srbt1() srloop() srp1() srpflood() >>> sr1 >>> sr1 function(x, promisc, filter, iface, nofilter, args, kargs) sr1() srbt() srflood() srp() srp1flood() srploop() >>> sr1( >>> sr1( >>> sr1(p >>> sr1(pk >>> sr1(pkt >>> sr1(pkt) .... ..... . >>> sr1(pkt) Begin emission: .....Finished sending 1 packets. .* Received 7 packets, got 1 answers, remaining 0 packets<IP version=4 ihl=5 tos=0x0 len=28 id=21006 flags= frag=0 ttl=60 proto=icmp chksum=0x394d src=217.25.178.5 dst=212.83.148.19 |<ICMP type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>> >>> _ >>> _. >>> _.s >>> _.sh >>> _.sho >>> _.show >>> _.show( x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>>>> _.show() >>> _.show() id= 21006 ttl= 60 chksum= 0x394d src= 217.25.178.5 dst= 212.83.148.19 type= echo-reply chksum= 0xffff###[ Padding ]### load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'demo@scapy:~/github/scapy# >>> demo@scapy:~/github/scapy# exit demo@scapy:~/github/scapy# exit - \ No newline at end of file diff --git a/doc/scapy/graphics/animations/animation-scapy-themes-demo.gif b/doc/scapy/graphics/animations/animation-scapy-themes-demo.gif deleted file mode 100755 index 5a7a8331db8..00000000000 Binary files a/doc/scapy/graphics/animations/animation-scapy-themes-demo.gif and /dev/null differ diff --git a/doc/scapy/graphics/animations/pipetool_demo.gif b/doc/scapy/graphics/animations/pipetool_demo.gif new file mode 100755 index 00000000000..ac43613eff4 Binary files /dev/null and b/doc/scapy/graphics/animations/pipetool_demo.gif differ diff --git a/doc/scapy/graphics/automotive/CAN-full-frame.jpg b/doc/scapy/graphics/automotive/CAN-full-frame.jpg new file mode 100644 index 00000000000..d726b3b5c7c Binary files /dev/null and b/doc/scapy/graphics/automotive/CAN-full-frame.jpg differ diff --git a/doc/scapy/graphics/automotive/DC-ZGW-CAN-Bus-.png b/doc/scapy/graphics/automotive/DC-ZGW-CAN-Bus-.png new file mode 100644 index 00000000000..ea778feda1a Binary files /dev/null and b/doc/scapy/graphics/automotive/DC-ZGW-CAN-Bus-.png differ diff --git a/doc/scapy/graphics/automotive/Simple-CAN-Bus-.png b/doc/scapy/graphics/automotive/Simple-CAN-Bus-.png new file mode 100644 index 00000000000..d767795911a Binary files /dev/null and b/doc/scapy/graphics/automotive/Simple-CAN-Bus-.png differ diff --git a/doc/scapy/graphics/automotive/XCP_ReferenceBook.png b/doc/scapy/graphics/automotive/XCP_ReferenceBook.png new file mode 100644 index 00000000000..e970660c73d Binary files /dev/null and b/doc/scapy/graphics/automotive/XCP_ReferenceBook.png differ diff --git a/doc/scapy/graphics/automotive/ZGW-CAN-Bus-.png b/doc/scapy/graphics/automotive/ZGW-CAN-Bus-.png new file mode 100644 index 00000000000..80a3e9c51c9 Binary files /dev/null and b/doc/scapy/graphics/automotive/ZGW-CAN-Bus-.png differ diff --git a/doc/scapy/graphics/automotive/autosar1.png b/doc/scapy/graphics/automotive/autosar1.png new file mode 100644 index 00000000000..eaf766cef69 Binary files /dev/null and b/doc/scapy/graphics/automotive/autosar1.png differ diff --git a/doc/scapy/graphics/automotive/autosar2.png b/doc/scapy/graphics/automotive/autosar2.png new file mode 100644 index 00000000000..5c0aee75292 Binary files /dev/null and b/doc/scapy/graphics/automotive/autosar2.png differ diff --git a/doc/scapy/graphics/automotive/autosar3.png b/doc/scapy/graphics/automotive/autosar3.png new file mode 100644 index 00000000000..9568a240fbd Binary files /dev/null and b/doc/scapy/graphics/automotive/autosar3.png differ diff --git a/doc/scapy/graphics/automotive/autosar4.png b/doc/scapy/graphics/automotive/autosar4.png new file mode 100644 index 00000000000..e236f6694e9 Binary files /dev/null and b/doc/scapy/graphics/automotive/autosar4.png differ diff --git a/doc/scapy/graphics/automotive/can-bus-states.png b/doc/scapy/graphics/automotive/can-bus-states.png new file mode 100644 index 00000000000..389a00766ff Binary files /dev/null and b/doc/scapy/graphics/automotive/can-bus-states.png differ diff --git a/doc/scapy/graphics/automotive/can-frame-socket-can.png b/doc/scapy/graphics/automotive/can-frame-socket-can.png new file mode 100644 index 00000000000..efd96e03147 Binary files /dev/null and b/doc/scapy/graphics/automotive/can-frame-socket-can.png differ diff --git a/doc/scapy/graphics/automotive/diag-stack.png b/doc/scapy/graphics/automotive/diag-stack.png new file mode 100644 index 00000000000..76a7b11635e Binary files /dev/null and b/doc/scapy/graphics/automotive/diag-stack.png differ diff --git a/doc/scapy/graphics/automotive/isotp-flow.png b/doc/scapy/graphics/automotive/isotp-flow.png new file mode 100644 index 00000000000..08e7a7b944f Binary files /dev/null and b/doc/scapy/graphics/automotive/isotp-flow.png differ diff --git a/doc/scapy/graphics/automotive/isotp-frames.png b/doc/scapy/graphics/automotive/isotp-frames.png new file mode 100644 index 00000000000..a724f95e5fd Binary files /dev/null and b/doc/scapy/graphics/automotive/isotp-frames.png differ diff --git a/doc/scapy/graphics/dcerpc/debug_eerr.png b/doc/scapy/graphics/dcerpc/debug_eerr.png new file mode 100644 index 00000000000..4f078c8b9f2 Binary files /dev/null and b/doc/scapy/graphics/dcerpc/debug_eerr.png differ diff --git a/doc/scapy/graphics/dcerpc/ndr_conformant_varying_array.png b/doc/scapy/graphics/dcerpc/ndr_conformant_varying_array.png new file mode 100644 index 00000000000..5480c2d0d1b Binary files /dev/null and b/doc/scapy/graphics/dcerpc/ndr_conformant_varying_array.png differ diff --git a/doc/scapy/graphics/dcerpc/ndr_full_pointer.png b/doc/scapy/graphics/dcerpc/ndr_full_pointer.png new file mode 100644 index 00000000000..de7fa1ed1a6 Binary files /dev/null and b/doc/scapy/graphics/dcerpc/ndr_full_pointer.png differ diff --git a/doc/scapy/graphics/fwdmachine.drawio b/doc/scapy/graphics/fwdmachine.drawio new file mode 100644 index 00000000000..bb9caacaadd --- /dev/null +++ b/doc/scapy/graphics/fwdmachine.drawio @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/scapy/graphics/fwdmachine.svg b/doc/scapy/graphics/fwdmachine.svg new file mode 100644 index 00000000000..e9d06349c43 --- /dev/null +++ b/doc/scapy/graphics/fwdmachine.svg @@ -0,0 +1,3 @@ + + +
Client
Client
Server
192.168.0.1
Server192.168.0.1
ForwardingMachine
Forwarding...
FORWARD
FORWARD
DROP
DROP
data
data
FORWARD_REPLACE
FORWARD_REPLACE
data
data
data
data
data
data
ANSWER
ANSWER
Other server
192.168.0.2
Other server192.168....
REDIRECT_TO
REDIRECT_TO
\ No newline at end of file diff --git a/doc/scapy/graphics/kerberos/as_req_fast.png b/doc/scapy/graphics/kerberos/as_req_fast.png new file mode 100644 index 00000000000..180deac7a86 Binary files /dev/null and b/doc/scapy/graphics/kerberos/as_req_fast.png differ diff --git a/doc/scapy/graphics/kerberos/kerberos_atmt.png b/doc/scapy/graphics/kerberos/kerberos_atmt.png new file mode 100644 index 00000000000..d7c5fcdaaea Binary files /dev/null and b/doc/scapy/graphics/kerberos/kerberos_atmt.png differ diff --git a/doc/scapy/graphics/kerberos/ticketer.png b/doc/scapy/graphics/kerberos/ticketer.png new file mode 100644 index 00000000000..ce619782948 Binary files /dev/null and b/doc/scapy/graphics/kerberos/ticketer.png differ diff --git a/doc/scapy/graphics/kerberos/wireshark_asreq.png b/doc/scapy/graphics/kerberos/wireshark_asreq.png new file mode 100644 index 00000000000..68e82760741 Binary files /dev/null and b/doc/scapy/graphics/kerberos/wireshark_asreq.png differ diff --git a/doc/scapy/graphics/pipetool_demo.svg b/doc/scapy/graphics/pipetool_demo.svg new file mode 100644 index 00000000000..04d84ee6538 --- /dev/null +++ b/doc/scapy/graphics/pipetool_demo.svg @@ -0,0 +1,43 @@ + + + + + + +pipe + + + +2518161510704 + +TransformDrain + + + +2518161510896 + +WiresharkSink + + + +2518161510704->2518161510896 + + + + + +2518161510992 + +SniffSource + + + +2518161510992->2518161510704 + + + + + diff --git a/doc/scapy/graphics/scapy_version_timeline.jpg b/doc/scapy/graphics/scapy_version_timeline.jpg deleted file mode 100644 index 840dbc04a1e..00000000000 Binary files a/doc/scapy/graphics/scapy_version_timeline.jpg and /dev/null differ diff --git a/doc/scapy/graphics/smb/smb_client.png b/doc/scapy/graphics/smb/smb_client.png new file mode 100644 index 00000000000..be7abf7c4b6 Binary files /dev/null and b/doc/scapy/graphics/smb/smb_client.png differ diff --git a/doc/scapy/graphics/smb/smb_server.png b/doc/scapy/graphics/smb/smb_server.png new file mode 100644 index 00000000000..8edb3c5bdcf Binary files /dev/null and b/doc/scapy/graphics/smb/smb_server.png differ diff --git a/doc/scapy/index.rst b/doc/scapy/index.rst index 8f8229396fa..a7ae7f7d082 100644 --- a/doc/scapy/index.rst +++ b/doc/scapy/index.rst @@ -13,7 +13,7 @@ Welcome to Scapy's documentation! :Release: |release| :Date: |today| -This document is under a `Creative Commons Attribution - Non-Commercial +Scapy's documentation is under a `Creative Commons Attribution - Non-Commercial - Share Alike 2.5 `_ license. .. toctree:: @@ -24,7 +24,7 @@ This document is under a `Creative Commons Attribution - Non-Commercial installation usage - advanced_usage + advanced_usage/index.rst routing .. toctree:: @@ -53,7 +53,8 @@ This document is under a `Creative Commons Attribution - Non-Commercial .. only:: html .. toctree:: - :maxdepth: 4 + :maxdepth: 1 + :titlesonly: :caption: API Reference api/scapy.rst diff --git a/doc/scapy/installation.rst b/doc/scapy/installation.rst index 1ab8f6cb4fe..695b0403a77 100644 --- a/doc/scapy/installation.rst +++ b/doc/scapy/installation.rst @@ -7,7 +7,7 @@ Download and Installation Overview ======== - 0. Install `Python 2.7.X or 3.4+ `_. + 0. Install `Python 3.7+ `_. 1. `Download and install Scapy. <#installing-scapy-v2-x>`_ 2. `Follow the platform-specific instructions (dependencies) <#platform-specific-instructions>`_. 3. (Optional): `Install additional software for special features <#optional-software-for-special-features>`_. @@ -18,12 +18,19 @@ Each of these steps can be done in a different way depending on your platform an Scapy versions ============== -.. image:: graphics/scapy_version_timeline.jpg - -.. note:: - - In Scapy v2 use ``from scapy.all import *`` instead of ``from scapy import *``. +.. note:: Scapy 2.5.0 was the last version to support Python 2.7 ! ++------------------+-------+-------+--------+ +| Scapy version | 2.3.3 | 2.5.0 | >2.5.0 | ++==================+=======+=======+========+ +| Python 2.2-2.6 | ✅ | ⌠| ⌠| ++------------------+-------+-------+--------+ +| Python 2.7 | ✅ | ✅ | ⌠| ++------------------+-------+-------+--------+ +| Python 3.4-3.6 | ⌠| ✅ | ⌠| ++------------------+-------+-------+--------+ +| Python 3.7-3.11 | ⌠| ✅ | ✅ | ++------------------+-------+-------+--------+ Installing Scapy v2.x ===================== @@ -47,19 +54,21 @@ Latest release Use pip:: -$ pip install --pre scapy[basic] +$ pip install scapy -In fact, since 2.4.3, Scapy comes in 3 bundles: +.. + !! COMMENTED UNTIL NEXT RELEASE !! + Scapy specifies ``optional-dependencies`` so that you can install its optional dependencies directly through pip: -+----------+------------------------------------------+---------------------------------------+ -| Bundle | Contains | Pip command | -+==========+==========================================+=======================================+ -| Default | Only Scapy | ``pip install scapy`` | -+----------+------------------------------------------+---------------------------------------+ -| Basic | Scapy & IPython. **Highly recommended** | ``pip install --pre scapy[basic]`` | -+----------+------------------------------------------+---------------------------------------+ -| Complete | Scapy & all its main dependencies | ``pip install --pre scapy[complete]`` | -+----------+------------------------------------------+---------------------------------------+ + +----------+------------------------------------------+-----------------------------+ + | Bundle | Contains | Pip command | + +==========+==========================================+=============================+ + | Default | Only Scapy | ``pip install scapy`` | + +----------+------------------------------------------+-----------------------------+ + | CLI | Scapy & IPython. **Highly recommended** | ``pip install scapy[cli]`` | + +----------+------------------------------------------+-----------------------------+ + | All | Scapy & all its optional dependencies | ``pip install scapy[all]`` | + +----------+------------------------------------------+-----------------------------+ Current development version @@ -68,34 +77,29 @@ Current development version .. index:: single: Git, repository -If you always want the latest version with all new features and bugfixes, use Scapy's Git repository: +If you always want the latest version of Scapy with all new the features and bugfixes (but slightly less stable), you can install Scapy from its Git repository. -1. `Install the Git version control system `_. +.. note:: If you don't want to clone Scapy, you can install the development version in one line using:: -2. Check out a clone of Scapy's repository:: + $ pip install https://github.com/secdev/scapy/archive/refs/heads/master.zip - $ git clone https://github.com/secdev/scapy.git +1. Check out a clone of Scapy's repository with `git `_:: -.. note:: - You can also download Scapy's `latest version `_ in a zip file:: - - $ wget --trust-server-names https://github.com/secdev/scapy/archive/master.zip # or wget -O master.zip https://github.com/secdev/scapy/archive/master.zip - $ unzip master.zip - $ cd master + $ git clone https://github.com/secdev/scapy.git + $ cd scapy -3. Install Scapy in the standard `distutils `_ way:: +2. Install Scapy using `pip `_:: - $ cd scapy - $ sudo python setup.py install + $ pip install . -If you used Git, you can always update to the latest version afterwards:: +3. If you used Git, you can always update to the latest version afterwards:: $ git pull - $ sudo python setup.py install + $ pip install . .. note:: - You can run scapy without installing it using the ``run_scapy`` (unix) or ``run_scapy.bat`` (Windows) script or running it directly from the executable zip file (see the previous section). + You can run scapy without installing it using the ``run_scapy`` (unix) or ``run_scapy.bat`` (Windows) script. Optional Dependencies ===================== @@ -118,7 +122,7 @@ Here are the topics involved and some examples that you can use to try if your i * 2D graphics. ``psdump()`` and ``pdfdump()`` need `PyX `_ which in turn needs a LaTeX distribution: `texlive (Unix) `_ or `MikTex (Windows) `_. - Note: PyX requires version <=0.12.1 on Python 2.7. This means that on Python 2.7, it needs to be installed via ``pip install pyx==0.12.1``. Otherwise ``pip install pyx`` + You can install pyx using ``pip install pyx`` .. code-block:: python @@ -129,13 +133,13 @@ Here are the topics involved and some examples that you can use to try if your i .. code-block:: python - >>> p=readpcap("myfile.pcap") + >>> p=rdpcap("myfile.pcap") >>> p.conversations(type="jpg", target="> test.jpg") .. note:: ``Graphviz`` and ``ImageMagick`` need to be installed separately, using your platform-specific package manager. -* 3D graphics. ``trace3D()`` needs `VPython-Jupyter `_. +* 3D graphics. ``trace3D()`` needs `VPython-Jupyter `_. VPython-Jupyter is installable via ``pip install vpython`` @@ -189,29 +193,29 @@ Linux native Scapy can run natively on Linux, without libpcap. -* Install `Python 2.7 or 3.4+ `_. -* Install `tcpdump `_ and make sure it is in the $PATH. (It's only used to compile BPF filters (``-ddd option``)) +* Install `Python 3.7+ `__. +* Install `libpcap `_. (By default it will only be used to compile BPF filters) * Make sure your kernel has Packet sockets selected (``CONFIG_PACKET``) * If your kernel is < 2.6, make sure that Socket filtering is selected ``CONFIG_FILTER``) Debian/Ubuntu/Fedora -------------------- -Make sure tcpdump is installed: +Make sure libpcap is installed: - Debian/Ubuntu: .. code-block:: text - $ sudo apt-get install tcpdump + $ sudo apt-get install libpcap-dev - Fedora: .. code-block:: text - $ yum install tcpdump + $ yum install libpcap-devel -Then install Scapy via ``pip`` or ``apt`` (bundled under ``python-scapy``) +Then install Scapy via ``pip`` or ``apt`` (bundled under ``python3-scapy``) All dependencies may be installed either via the platform-specific installer, or via PyPI. See `Optional Dependencies <#optional-dependencies>`_ for more information. @@ -264,7 +268,7 @@ In a similar manner, to install Scapy on OpenBSD 5.9+, you **may** want to insta .. code-block:: text - $ doas pkg_add libpcap tcpdump + $ doas pkg_add libpcap Then install Scapy via ``pip`` or ``pkg_add`` (bundled under ``python-scapy``) All dependencies may be installed either via the platform-specific installer, or via PyPI. See `Optional Dependencies <#optional-dependencies>`_ for more information. @@ -283,68 +287,28 @@ Solaris / SunOS requires ``libpcap`` (installed by default) to work. Windows ------- -.. sectionauthor:: Dirk Loss +You need to install Npcap in order to install Scapy on Windows (should also work with Winpcap, but unsupported nowadays): -Scapy is primarily being developed for Unix-like systems and works best on those platforms. But the latest version of Scapy supports Windows out-of-the-box. So you can use nearly all of Scapy's features on your Windows machine as well. + * Download link: `Npcap `_: `the latest version `_ + * During installation: + * we advise to turn **off** the ``Winpcap compatibility mode`` + * if you want to use your wifi card in monitor mode (if supported), make sure you enable the ``802.11`` option -.. image:: graphics/scapy-win-screenshot1.png - :scale: 80 - :align: center - -You need the following software in order to install Scapy on Windows: +Once that is done, you can `continue with Scapy's installation <#latest-release>`_. - * `Python `_: `Python 2.7.X or 3.4+ `_. After installation, add the Python installation directory and its \Scripts subdirectory to your PATH. Depending on your Python version, the defaults would be ``C:\Python27`` and ``C:\Python27\Scripts`` respectively. - * `Npcap `_: `the latest version `_. Default values are recommended. Scapy will also work with Winpcap. - * `Scapy `_: `latest development version `_ from the `Git repository `_. Unzip the archive, open a command prompt in that directory and run ``python setup.py install``. +You should then be able to open a ``cmd.exe`` and just call ``scapy``. If not, you probably haven't enabled the "Add Python to PATH" option when installing Python. You can follow the instructions `over here `_ to change that (or add it manually). -Just download the files and run the setup program. Choosing the default installation options should be safe. (In the case of ``Npcap``, Scapy **will work** with ``802.11`` option enabled. You might want to make sure that this is ticked when installing). +Screenshots +^^^^^^^^^^^ -After all packages are installed, open a command prompt (cmd.exe) and run Scapy by typing ``scapy``. If you have set the PATH correctly, this will find a little batch file in your ``C:\Python27\Scripts`` directory and instruct the Python interpreter to load Scapy. - -If really nothing seems to work, consider skipping the Windows version and using Scapy from a Linux Live CD -- either in a virtual machine on your Windows host or by booting from CDROM: An older version of Scapy is already included in grml and BackTrack for example. While using the Live CD you can easily upgrade to the latest Scapy version by using the `above installation methods <#installing-scapy-v2-x>`_. - -Screenshot -^^^^^^^^^^ +.. image:: graphics/scapy-win-screenshot1.png + :scale: 80 + :align: center .. image:: graphics/scapy-win-screenshot2.png :scale: 80 :align: center -Known bugs -^^^^^^^^^^ - -You may bump into the following bugs, which are platform-specific, if Scapy didn't manage work around them automatically: - - * You may not be able to capture WLAN traffic on Windows. Reasons are explained on the `Wireshark wiki `_ and in the `WinPcap FAQ `_. Try switching off promiscuous mode with ``conf.sniff_promisc=False``. - * Packets sometimes cannot be sent to localhost (or local IP addresses on your own host). - -Winpcap/Npcap conflicts -^^^^^^^^^^^^^^^^^^^^^^^ - -As ``Winpcap`` is becoming old, it's recommended to use ``Npcap`` instead. ``Npcap`` is part of the ``Nmap`` project. - -.. note:: - This does NOT apply for Windows XP, which isn't supported by ``Npcap``. - -1. If you get the message ``'Winpcap is installed over Npcap.'`` it means that you have installed both Winpcap and Npcap versions, which isn't recommended. - -You may first **uninstall winpcap from your Program Files**, then you will need to remove:: - - C:/Windows/System32/wpcap.dll - C:/Windows/System32/Packet.dll - -And if you are on an x64 machine:: - - C:/Windows/SysWOW64/wpcap.dll - C:/Windows/SysWOW64/Packet.dll - -To use ``Npcap`` instead, as those files are not removed by the ``Winpcap`` un-installer. - -2. If you get the message ``'The installed Windump version does not work with Npcap'`` it surely means that you have installed an old version of ``Windump``, made for ``Winpcap``. -Download the correct one on https://github.com/hsluoyz/WinDump/releases - -In some cases, it could also mean that you had installed ``Npcap`` and ``Winpcap``, and that ``Windump`` is using ``Winpcap``. Fully delete ``Winpcap`` using the above method to solve the problem. - Build the documentation offline =============================== diff --git a/doc/scapy/introduction.rst b/doc/scapy/introduction.rst index 525a50a0ed4..ab8b97cf1f6 100644 --- a/doc/scapy/introduction.rst +++ b/doc/scapy/introduction.rst @@ -7,7 +7,7 @@ Introduction About Scapy =========== -Scapy is a Python program that enables the user to send, sniff and dissect and forge network packets. This capability allows construction of tools that can probe, scan or attack networks. +Scapy is a Python program that enables the user to send, sniff, dissect and forge network packets. This capability allows construction of tools that can probe, scan or attack networks. In other words, Scapy is a powerful interactive packet manipulation program. It is able to forge or decode packets of a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more. Scapy can easily handle most classical tasks like scanning, tracerouting, probing, unit tests, attacks or network discovery. It can replace hping, arpspoof, arp-sk, arping, p0f and even some parts of Nmap, tcpdump, and tshark. @@ -16,38 +16,38 @@ In other words, Scapy is a powerful interactive packet manipulation program. It Scapy also performs very well on a lot of other specific tasks that most other tools can't handle, like sending invalid frames, injecting your own 802.11 frames, combining techniques (VLAN hopping+ARP cache poisoning, VOIP decoding on WEP encrypted channel, ...), etc. -The idea is simple. Scapy mainly does two things: sending packets and receiving answers. You define a set of packets, it sends them, receives answers, matches requests with answers and returns a list of packet couples (request, answer) and a list of unmatched packets. This has the big advantage over tools like Nmap or hping that an answer is not reduced to (open/closed/filtered), but is the whole packet. +The idea is simple. Scapy mainly does two things: sending packets and receiving answers. You define a set of packets, it sends them, receives answers, matches requests with answers and returns a list of packet couples (request, answer) and a list of unmatched packets. This has the big advantage over tools like Nmap or hping that an answer is not reduced to open, closed, or filtered, but is the whole packet. -On top of this can be build more high level functions, for example, one that does traceroutes and give as a result only the start TTL of the request and the source IP of the answer. One that pings a whole network and gives the list of machines answering. One that does a portscan and returns a LaTeX report. +On top of this can be built more high level functions. For example, one that does traceroutes and give as a result only the start TTL of the request and the source IP of the answer. One that pings a whole network and gives the list of machines answering. One that does a portscan and returns a LaTeX report. What makes Scapy so special =========================== -First, with most other networking tools, you won't build something the author did not imagine. These tools have been built for a specific goal and can't deviate much from it. For example, an ARP cache poisoning program won't let you use double 802.1q encapsulation. Or try to find a program that can send, say, an ICMP packet with padding (I said *padding*, not *payload*, see?). In fact, each time you have a new need, you have to build a new tool. +First, with most other networking tools, you won't build something the author didn't imagine. These tools have been built for a specific goal and can't deviate much from it. For example, an ARP cache poisoning program won't let you use double 802.1q encapsulation. Or try to find a program that can send, say, an ICMP packet with padding (I said *padding*, not *payload*, see?). In fact, each time you have a new need, you have to build a new tool. Second, they usually confuse decoding and interpreting. Machines are good at decoding and can help human beings with that. Interpretation is reserved for human beings. Some programs try to mimic this behavior. For instance they say "*this port is open*" instead of "*I received a SYN-ACK*". Sometimes they are right. Sometimes not. It's easier for beginners, but when you know what you're doing, you keep on trying to deduce what really happened from the program's interpretation to make your own, which is hard because you lost a big amount of information. And you often end up using ``tcpdump -xX`` to decode and interpret what the tool missed. -Third, even programs which only decode do not give you all the information they received. The network's vision they give you is the one their author thought was sufficient. But it is not complete, and you have a bias. For instance, do you know a tool that reports the Ethernet padding? +Third, even programs which only decode do not give you all the information they received. The vision of the network they give you is the one their author thought was sufficient. But it is not complete, and you have a bias. For instance, do you know a tool that reports the Ethernet padding? -Scapy tries to overcome those problems. It enables you to build exactly the packets you want. Even if I think stacking a 802.1q layer on top of TCP has no sense, it may have some for somebody else working on some product I don't know. Scapy has a flexible model that tries to avoid such arbitrary limits. You're free to put any value you want in any field you want and stack them like you want. You're an adult after all. +Scapy tries to overcome those problems. It enables you to build exactly the packets you want. Even if I think stacking an 802.1q layer on top of TCP has no sense, it may have some for somebody else working on some product I don't know. Scapy has a flexible model that tries to avoid such arbitrary limits. You're free to put any value you want in any field you want and stack them like you want. You're an adult after all. In fact, it's like building a new tool each time, but instead of dealing with a hundred line C program, you only write 2 lines of Scapy. -After a probe (scan, traceroute, etc.) Scapy always gives you the full decoded packets from the probe, before any interpretation. That means that you can probe once and interpret many times, ask for a traceroute and look at the padding for instance. +After a probe (scan, traceroute, etc.) Scapy always gives you the full decoded packets from the probe, before any interpretation. That means that you can probe once and interpret many times. Ask for a traceroute and look at the padding, for instance. Fast packet design ------------------ Other tools stick to the **program-that-you-run-from-a-shell** paradigm. The result is an awful syntax to describe a packet. For these tools, the solution adopted uses a higher but less powerful description, in the form of scenarios imagined by the tool's author. As an example, only the IP address must be given to a port scanner to trigger the **port scanning** scenario. Even if the scenario is tweaked a bit, you still are stuck to a port scan. -Scapy's paradigm is to propose a Domain Specific Language (DSL) that enables a powerful and fast description of any kind of packet. Using the Python syntax and a Python interpreter as the DSL syntax and interpreter has many advantages: there is no need to write a separate interpreter, users don't need to learn yet another language and they benefit from a complete, concise and very powerful language. +Scapy's paradigm is to propose a Domain Specific Language (DSL) that enables a powerful and fast description of any kind of packet. Using the Python syntax and a Python interpreter as the DSL syntax and interpreter has many advantages: there is no need to write a separate interpreter, users don't need to learn yet another language, and they benefit from a complete, concise, and very powerful language. -Scapy enables the user to describe a packet or set of packets as layers that are stacked one upon another. Fields of each layer have useful default values that can be overloaded. Scapy does not oblige the user to use predetermined methods or templates. This alleviates the requirement of writing a new tool each time a different scenario is required. In C, it may take an average of 60 lines to describe a packet. With Scapy, the packets to be sent may be described in only a single line with another line to print the result. 90\% of the network probing tools can be rewritten in 2 lines of Scapy. +Scapy enables the user to describe a packet or set of packets as layers that are stacked one upon another. Fields of each layer have useful default values that can be overloaded. Scapy does not oblige the user to use predetermined methods or templates. This alleviates the requirement of writing a new tool each time a different scenario is required. In C, it may take an average of 60 lines to describe a packet. With Scapy, the packets to be sent may be described in only a single line, with another line to print the result. 90\% of network probing tools can be rewritten in 2 lines of Scapy. Probe once, interpret many -------------------------- -Network discovery is blackbox testing. When probing a network, many stimuli are sent while only a few of them are answered. If the right stimuli are chosen, the desired information may be obtained by the responses or the lack of responses. Unlike many tools, Scapy gives all the information, i.e. all the stimuli sent and all the responses received. Examination of this data will give the user the desired information. When the dataset is small, the user can just dig for it. In other cases, the interpretation of the data will depend on the point of view taken. Most tools choose the viewpoint and discard all the data not related to that point of view. Because Scapy gives the complete raw data, that data may be used many times allowing the viewpoint to evolve during analysis. For example, a TCP port scan may be probed and the data visualized as the result of the port scan. The data could then also be visualized with respect to the TTL of response packet. A new probe need not be initiated to adjust the viewpoint of the data. +Network discovery is blackbox testing. When probing a network, many stimuli are sent, while only a few of them are answered. If the right stimuli are chosen, the desired information may be obtained by the responses or the lack of responses. Unlike many tools, Scapy gives all the information, i.e. all the stimuli sent and all the responses received. Examination of this data will give the user the desired information. When the dataset is small, the user can just dig for it. In other cases, the interpretation of the data will depend on the point of view taken. Most tools choose the viewpoint and discard all the data not related to that point of view. Because Scapy gives the complete raw data, that data may be used many times allowing the viewpoint to evolve during analysis. For example, a TCP port scan may be probed and the data visualized as the result of the port scan. The data could then also be visualized with respect to the TTL of the response packet. A new probe need not be initiated to adjust the viewpoint of the data. .. image:: graphics/scapy-concept.* :scale: 80 @@ -55,16 +55,13 @@ Network discovery is blackbox testing. When probing a network, many stimuli are Scapy decodes, it does not interpret ------------------------------------ -A common problem with network probing tools is they try to interpret the answers received instead of only decoding and giving facts. Reporting something like **Received a TCP Reset on port 80** is not subject to interpretation errors. Reporting **Port 80 is closed** is an interpretation that may be right most of the time but wrong in some specific contexts the tool's author did not imagine. For instance, some scanners tend to report a filtered TCP port when they receive an ICMP destination unreachable packet. This may be right, but in some cases, it means the packet was not filtered by the firewall but rather there was no host to forward the packet to. +A common problem with network probing tools is they try to interpret the answers received instead of only decoding and giving facts. Reporting something like **Received a TCP Reset on port 80** is not subject to interpretation errors. Reporting **Port 80 is closed** is an interpretation that may be right most of the time but wrong in some specific contexts the tool's author did not imagine. For instance, some scanners tend to report a filtered TCP port when they receive an ICMP destination unreachable packet. This may be right, but in some cases, it means the packet was not filtered by the firewall, but rather there was no host to forward the packet to. -Interpreting results can help users that don't know what a port scan is but it can also make more harm than good, as it injects bias into the results. What can tend to happen is that so that they can do the interpretation themselves, knowledgeable users will try to reverse engineer the tool's interpretation to derive the facts that triggered that interpretation. Unfortunately, much information is lost in this operation. +Interpreting results can help users that don't know what a port scan is, but it can also make more harm than good, as it injects bias into the results. What can tend to happen is that knowledgeable users will try to reverse engineer the tool's interpretation to derive the facts that triggered that interpretation, so that they can do the interpretation themselves. Unfortunately, much information is lost in this operation. Quick demo ========== -.. image:: graphics/animations/animation-scapy-demo.svg - :align: center - First, we play a bit and create four IP packets at once. Let's see how it works. We first instantiate the IP class. Then, we instantiate it again and we provide a destination that is worth four IP addresses (/30 gives the netmask). Using a Python idiom, we develop this implicit packet in a set of explicit packets. Then, we quit the interpreter. As we provided a session file, the variables we were working on are saved, then reloaded:: # ./run_scapy -s mysession diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index 2a880482bad..a9fa9d3e107 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1,147 +1,284 @@ -******************* -Automotive Security -******************* +.. note:: This document is under a `Creative Commons Attribution - Non-Commercial - Share Alike 2.5 `_ license. +################################# +Automotive-specific Documentation +################################# + +.. sectionauthor:: Nils Weiss + +******** Overview -======== +******** .. note:: - All automotive related features work best on Linux systems. CANSockets and ISOTPSockets in Scapy are based on Linux kernel modules. - The python-can project is used to support CAN and CANSockets on other systems, besides Linux. - This guide explains the hardware setup on a BeagleBone Black. The BeagleBone Black was chosen because of its two CAN interfaces on the main processor. - The presence of two CAN interfaces in one device gives the possibility of CAN MITM attacks and session hijacking. - The Cannelloni framework turns a single board computer into a CAN-to-UDP interface, which gives you the freedom to run Scapy - on a more powerful machine. + All automotive-related features work best on Linux systems. CANSockets and ISOTPSockets are based on Linux kernel modules. The python-can project is used to support CAN and CANSockets on a wider range of operating systems and CAN hardware interfaces. Protocols ---------- +========= -The following table should give a brief overview about all automotive capabilities +The following table should give a brief overview of all the automotive-related capabilities of Scapy. Most application layer protocols have many specialized ``Packet`` classes. -These special purpose classes are not part of this overview. Use the ``explore()`` +These special-purpose ``Packets`` are not part of this overview. Use the ``explore()`` function to get all information about one specific protocol. -+---------------------+----------------------+--------------------------------------------------------+ -| OSI Layer | Protocol | Scapy Implementations | -+=====================+======================+========================================================+ -| Application Layer | UDS (ISO 14229) | UDS, UDS_*, UDS_TesterPresentSender | -| +----------------------+--------------------------------------------------------+ -| | GMLAN | GMLAN, GMLAN_*, GMLAN_TesterPresentSender | -| +----------------------+--------------------------------------------------------+ -| | SOME/IP | SOMEIP, SD | -| +----------------------+--------------------------------------------------------+ -| | BMW ENET | ENET, ENETSocket | -| +----------------------+--------------------------------------------------------+ -| | OBD | OBD, OBD_S0X | -| +----------------------+--------------------------------------------------------+ -| | CCP | CCP, DTO, CRO | -+---------------------+----------------------+--------------------------------------------------------+ -| Transportation Layer| ISO-TP (ISO 15765-2) | ISOTPSocket, ISOTPNativeSocket, ISOTPSoftSocket | -| | | | -| | | ISOTPSniffer, ISOTPMessageBuilder, ISOTPSession | -| | | | -| | | ISOTPHeader, ISOTPHeaderEA, ISOTPScan | -| | | | -| | | ISOTP, ISOTP_SF, ISOTP_FF, ISOTP_CF, ISOTP_FC | -+---------------------+----------------------+--------------------------------------------------------+ -| Data Link Layer | CAN (ISO 11898) | CAN, CANSocket, rdcandump, CandumpReader | -+---------------------+----------------------+--------------------------------------------------------+ - - -CAN Layer -========= ++----------------------+----------------------+--------------------------------------------------------+ +| OSI Layer | Protocol | Scapy Implementations | ++======================+======================+========================================================+ +| Application Layer | UDS (ISO 14229) | UDS, UDS_*, UDS_TesterPresentSender | +| +----------------------+--------------------------------------------------------+ +| | GMLAN | GMLAN, GMLAN_*, GMLAN_[Utilities] | +| +----------------------+--------------------------------------------------------+ +| | SOME/IP | SOMEIP, SD | +| +----------------------+--------------------------------------------------------+ +| | BMW HSFZ | HSFZ, HSFZSocket, UDS_HSFZSocket | +| +----------------------+--------------------------------------------------------+ +| | OBD | OBD, OBD_S0[0-9A] | +| +----------------------+--------------------------------------------------------+ +| | CCP | CCP, DTO, CRO | +| +----------------------+--------------------------------------------------------+ +| | XCP | XCPOnCAN, XCPOnUDP, XCPOnTCP, CTORequest, CTOResponse, | +| | | DTO | ++----------------------+----------------------+--------------------------------------------------------+ +| Transportation Layer | ISO-TP (ISO 15765-2) | ISOTPSocket, ISOTPNativeSocket, ISOTPSoftSocket | +| | | | +| | | ISOTPSniffer, ISOTPMessageBuilder, ISOTPSession | +| | | | +| | | ISOTPHeader, ISOTPHeaderEA, isotp_scan | +| | | | +| | | ISOTP, ISOTP_SF, ISOTP_FF, ISOTP_CF, ISOTP_FC | ++----------------------+----------------------+--------------------------------------------------------+ +| Data Link Layer | CAN (ISO 11898) | CAN, CANSocket, rdcandump, CandumpReader | ++----------------------+----------------------+--------------------------------------------------------+ + + +******************** +Technical Background +******************** + +Parts this section were published in a study report [10]_. + +Physical Protocols +================== + +More than 20 different communication protocols exist for the vehicle’s internal wired communication. Most vehicles make use of five to ten different protocols for their internal communication. The decision which communication protocol is used from an Original Equipment Manufacturer (OEM) is usually made by the trade-off between the costs for communication technology and the final car price. The four major communication technologies for inter-ECU communication are Controller Area Network (CAN), FlexRay, Local Interconnect Network (LIN), and Automotive Ethernet. For security considerations, these are the most relevant protocols for wired communication in vehicles. + +LIN +--- +LIN is a single wire communication protocol for low data rates. Actuators and sensors of a vehicle exchange information with an ECU, acting as a LIN master. Software updates over LIN are possible, but the LIN slaves usually do not need software updates because of their limited functionality. + +CAN +--- +CAN is by far the most used communication technology for inter-ECU communication in vehicles. In older or cheaper vehicles, CAN is still the primary protocol for a vehicle’s backbone communication. Safety-critical communication during a vehicle’s operation, diagnostic information, and software updates are transferred between ECUs over CAN. The lack of security features in the protocol itself, combined with the general use, makes CAN the primary protocol for security investigations. + +FlexRay +------- +The FlexRay consortium designed FlexRay as a successor of CAN. Modern vehicles have higher demands on communication bandwidth. By design, FlexRay is a fast and reliable communication protocol for inter-ECU communication. FlexRay components are more expensive than CAN components, leading to a more selective use by OEMs. + +Automotive Ethernet +------------------- +Recent upper-class vehicles implement Automotive Ethernet, the new backbone technology for internal vehicle communication. The rapidly grown bandwidth demands already replace FlexRay. The primary reasons for these demands are driver-assistant and autonomous-driving features. Only the physical layer (layer 1) of the Open Systems Interconnection (OSI) model distinguishes Ethernet (IEEE 802.3) from Automotive Ethernet (BroadR-Reach). This design decision leads to multiple advantages. For example, communication stacks of operating systems can be used without modification and routing, filtering, and firewall systems. Automotive Ethernet components are already cheaper than FlexRay components, which will lead to vehicle topologies, where CAN and Automotive Ethernet are the most used communication protocols. -How-To +Topologies +========== + +Line-Bus -------- -Send and receive a message over Linux SocketCAN:: +.. _fig-line-bus: - load_layer('can') - load_contrib('cansocket') +.. figure:: ../graphics/automotive/Simple-CAN-Bus-.png - socket = CANSocket(iface='can0') - packet = CAN(identifier=0x123, data=b'01020304') + Line-Bus network topology - socket.send(packet) - rx_packet = socket.recv() +The first vehicles with CAN bus used a single network with a line-bus topology. Some lower-priced vehicles still use one or two shared CAN bus networks for their internal communication nowadays. The downside of this topology is its vulnerability and the lack of network separation. All ECUs of a vehicle are connected on a shared bus. Since CAN does not support security features from its protocol definition, any participant on this bus can communicate directly with all other participants, which allows an attacker to affect all ECUs, even safety-critical ones, by compromising one single ECU. The overall security level of this network is given from the security level of the weakest participant. - socket.sr1(packet, timeout=1) +Central Gateway +--------------- -Send a message over a Vector CAN-Interface:: +.. _fig-cgw: - import can - load_layer('can') - conf.contribs['CANSocket'] = {'use-python-can' : True} - load_contrib('cansocket') - from can.interfaces.vector import VectorBus +.. figure:: ../graphics/automotive/ZGW-CAN-Bus-.png - socket = CANSocket(iface=VectorBus(0, bitrate=1000000)) - packet = CAN(identifier=0x123, data=b'01020304') + Network topology with central GW ECU - socket.send(packet) - rx_packet = socket.recv() +The central Gateway (GW) topology can be found in higher-priced older cars and medium-priced to lower-priced recent cars. A centralized GW ECU separates domain-specific sub-networks. This allows an OEM to encapsulate all ECUs with remote attack surfaces in one sub-network. ECUs with safety-critical functionalities are located in an individual CAN network. Next to CAN, FlexRay might also be used as a communication protocol inside a separate network domain. The security of a safety-critical network in this topology depends mainly on the central GW ECU’s security. This architecture increases the overall security level of a vehicle through domain separation. After an attacker successfully exploited an ECU through an arbitrary attack surface, a second exploitable vulnerability or a logical bug is necessary to compromise a different domain, a safety-critical network, inside a vehicle. This second exploit or logical bug is necessary to overcome the network separation of the central GW ECU. - socket.sr1(packet) +Central Gateway and Domain Controller +------------------------------------- +.. _fig-dc: +.. figure:: ../graphics/automotive/DC-ZGW-CAN-Bus-.png -Tutorials ---------- + Network topology with Automotive-Ethernet backbone and DC -Linux SocketCAN -^^^^^^^^^^^^^^^ +A new topology with central GW and Domain Controllers (DCs) can be found in the latest higher-priced vehicles. The increasing demand for bandwidth in modern vehicles with autonomous driving and driver assistant features led to this topology. An Automotive Ethernet network is used as a communication backbone for the entire vehicle. Individual domains, connected through a DC with the central GW, form the vehicle’s backbone. The individual DCs can control and regulate the data communication between a domain and the vehicle’s backbone. This topology achieves a very-high security level through a strong network separation with individual DCs, acting as gateway and firewall, to the vehicle’s backbone network. OEMs have the advantage of dynamic information routing next to this security improvement, an enabler for Feature on Demand (FoD) services. -This subsection summarizes some basics about Linux SocketCAN. An excellent overview -from Oliver Hartkopp can be found here: https://wiki.automotivelinux.org/_media/agl-distro/agl2017-socketcan-print.pdf +Automotive Communication Protocols +================================== -Virtual CAN Setup -^^^^^^^^^^^^^^^^^ +This section provides an overview of relevant communication protocols for security evaluations in automotive networks. In contrast to section "Physical Protocols", this section focuses on properties for data communication. -Linux SocketCAN supports virtual CAN interfaces. These interfaces are an easy way -to do some first steps on a CAN-Bus without the requirement of special hardware. -Besides that, virtual CAN interfaces are heavily used in Scapy unit test for automotive -related contributions. +CAN +--- -Virtual CAN sockets require a special Linux kernel module. The following shell command loads the required module:: +The CAN communication technology was invented in 1983 as a message-based robust vehicle bus communication system. The Robert Bosch GmbH designed multiple communication features into the CAN standard to achieve a robust and computation efficient protocol for controller area networks. Remarkable for the communication behavior of CAN is the internal state machine for transmission errors. This state machine implements a fail silent behavior to protect a safety-critical network from babbling idiot nodes. If a specific limit of reception errors (REC) or transmission errors (TEC) occurred, the CAN driver changes its state from error-active to error-passive and finally to bus-off. - sudo modprobe vcan +.. _fig-can-bus-states: -In order to use a virtual CAN interface some additional commands for setup are required. -This snippet chooses the name ``vcan0`` for the virtual CAN interface. Any name can be chosen here:: +.. figure:: ../graphics/automotive/can-bus-states.png - sudo ip link add name vcan0 type vcan - sudo ip link set dev vcan0 up + CAN bus states on transmission errors. Receive Error Counter (REC), Transmit Error Counter (TEC) -The same commands can be executed from Scapy like this:: +In recent years, this protocol specification was abused for Denial of Service (DoS) attacks and information gathering attacks on the CAN network of a vehicle. Cho et al. demonstrated a DoS attack against CAN networks by abusing the bus-off state of ECUs [1]_. Injections of communication errors in CAN frames of one specific node caused a high transmission error count in the node under attack, forcing the attacked node to enter the bus-off state. In 2019 Kulandaivel et al. combined this attack with statistical analysis to achieve a fast and inexpensive network mapping in vehicular networks [2]_. They combined statistical analysis of the CAN network traffic before and after the bus-off attack was applied to a node. All missing CAN frames in the network traffic after an ECU was attacked could now be mapped to the ECU under attack, helping researchers identify the origin ECU of a CAN frame. Ken Tindell published a comprehensive summary of low level attacks on CANs in 2019 [3]_. - from scapy.layers.can import * - import os +.. _fig-can-full-frame: - bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" - os.system(bashCommand) +.. figure:: ../graphics/automotive/CAN-full-frame.jpg -If it's required, a CAN interface can be set into a ``listen-only`` or ``loopback`` mode with ``ip link set`` commands:: + Complete CAN data frame structure [9]_ - ip link set vcan0 type can help # shows additional information +The above figure shows a CAN frame and its fields as it is transferred over the network. For information exchange, only the fields arbitration, control, and data are relevant. These are the only fields to which a usual application software has access. All other fields are evaluated on a hardware-layer and, in most cases, are not forwarded to an application. The data field has a variable length and can hold up to eight bytes. The length of the data field is specified by the data length code inside the control field. Important variations of this example are CAN-frames with extended arbitration fields and the Controller Area Network Flexible Data-Rate (CAN FD) protocol. On Linux, every received CAN frame is passed to SocketCAN. SocketCAN allows the CAN handling via network sockets of the operating system. SocketCAN was created by Oliver Hartkopp and added to the Linux Kernel version 2.6.25 [4]_. Figure 2.7 shows the frame structure, how CAN frames are encoded if a user-land application receives data from a CAN socket. +.. _fig-can-socket-frame: -Linux can-utils -^^^^^^^^^^^^^^^ +.. figure:: ../graphics/automotive/can-frame-socket-can.png -As part of Linux SocketCAN, some very useful commandline tools are provided from Oliver Hartkopp: https://github.com/linux-can/can-utils + CAN frame defined by SocketCAN -The following example shows basic functions of Linux can-utils. These utilities are very handy for -quick checks, dumping, sending or logging of CAN messages from the command line. +The comparison of above figures clearly shows the loss of information during the CAN frame processing from a physical layer driver. Almost every CAN driver acts in the same way, whether an application code runs on a microcontroller or a Linux kernel. This also means that a standard application does not have access to the Cyclic Redundancy Check (CRC) field, the acknowledgment bit, or the end-of-frame field. + +Through the CAN communication in a vehicle or a separated domain, ECUs exchange sensor-data and control inputs; this data is mainly not secured and can be modified by assailants. Attackers can easily spoof sensor values on a CAN bus to trigger malicious reactions of other ECUs. Miller and Valasek described this spoofing attack during their studies on automotive networks [5]_. To prevent attacks on safety-critical data transferred over CAN, Automotive Open System Architecture (AUTOSAR) released a secure onboard communication specification [6]_. + +ISO-TP (ISO 15765-2) +-------------------- + +The CAN protocol supports only eight bytes of data. Use-cases like diagnostic operations or ECU programming require much higher payloads than the CAN protocol supports. For these purposes, the automotive industry standardized the Transport Layer (ISO-TP) (ISO 15765-2) protocol [7]_. ISO-TP is a transportation layer protocol on top of CAN. Payloads with up to 4095 bytes can be transferred between ISO-TP endpoints fragmented in CAN frames. The ISO-TP protocol handling requires four special frame types. + +.. _fig-isotp-flow: + +.. figure:: ../graphics/automotive/isotp-flow.png + + ISO-TP fragmented communication + +The different types of ISO-TP frames are shown in the following figure. The payload of a CAN frame gets replaced by one of the four ISO-TP frames. The individual ISO-TP frames have different purposes. A single frame can transfer between 1 and 7 bytes of ISO-TP message data. The len field of a Single Frame or a First Frame indicates the ISO-TP message length. Every message with more than 7 bytes of payload data must be fragmented into a First Frame, followed by multiple Consecutive Frames. This communication is illustrated in the above figure. After the First Frame is sent from a sender, the receiver has to communicate its reception capabilities through a Flow Control Frame to the sender. Only after this Flow Control Frame is received, the sender is allowed to communicate the Consecutive Frames according to the receiver’s capabilities. + +.. _fig-isotp-frames: + +.. figure:: ../graphics/automotive/isotp-frames.png + + ISO-TP frame types + +ISO-TP acts as a transport protocol with the support of directed communication through addressing mechanisms. In vehicles, ISO-TP is mainly used as a transport protocol for diagnostic communication. In rare cases, ISO-TP is also used to exchange larger data between ECUs of a vehicle. Security measures have to be applied to the application layer protocol transported through ISO-TP since ISO-TP has no capabilities to secure its transported data. + +DoIP +---- + +Diagnostic over IP (DoIP) was first implemented on automotive networks with a centralized gateway topology. A centralized GW functions as a DoIP endpoint that routes diagnostic messages to the desired network, allowing manufacturers to program or diagnose multiple ECUs in parallel. Since the Internet Protocol (IP) communication between a repair-shop tester and the GW is many times faster than the communication between the GW ECU and a target ECU connected over CAN, the remaining bandwidth of the IP communication can be used to start further DoIP connections to other ECUs in different CAN domains. DoIP is specified as part of AUTOSAR and in ISO 13400-2. Similar to ISO-TP, DoIP does not specify special security measures. The responsibility regarding secured communication is delegated to the application layer protocol. + +Diagnostic Protocols +-------------------- + +Two examples of diagnostic protocols are General Motor Local Area Network (GMLAN) and Unified Diagnostic Service (UDS) (ISO 14229-2). The General Motors Cooperation uses GMLAN. German OEMs mainly use UDS. Both protocols are very similar from a specification point of view, and both protocols use either ISO-TP or DoIP messages for a directed communication with a target ECU. Since different OEMs use UDS, every manufacturer adds its custom additions to the standard. Also, every manufacturer uses individual ISO-TP addressing for the directed communication with an ECU. GMLAN includes more precise definitions about ECU addressing and an ECUs internal behavior compared to UDS. + +UDS and GMLAN follow a tree-like message structure, where the first byte identifies the service. Every service is answered by a response. Two types of responses are defined in the standard. Negative responses are indicated through the service 0x7F. Positive responses are identified by the request service identifier incremented with 0x40. + +.. _fig-diag-stack: + +.. figure:: ../graphics/automotive/diag-stack.png + + Automotive Diagnostic Protocol Stack + +A clear separation between the transport and the application layer allows creating application layer tools for both network stacks. The figure above provides an overview of relevant protocols and the corresponding layers. UDS defines a clean separation between application and transport layer. On CAN based networks, ISO-TP is used for this purpose. The CAN protocol can be treated as the network access protocol. This allows to replace ISO-TP and CAN with DoIP or HSFZ and Ethernet. The GMLAN protocol combines transport and application layer specifications very similar to ISO-TP and UDS. Because of that similarity, identical application layer-specific scan techniques can be applied. To overcome the bandwidth limitations of CAN, the latest vehicle architectures use an Ethernet-based diagnostic protocol (DoIP, HSFZ) to communicate with a central gateway ECU. The central gateway ECU routes application layer packets from an Ethernet-based network to a CAN based vehicle internal network. In general, the diagnostic functions of all ECUs in a vehicle can be accessed from the OBD connector over UDSonCAN or UDSonIP. + +SOME/IP +------- + +Scalable service-Oriented MiddlewarE over IP (SOME/IP) defines a new philosophy of data communication in automotive networks. SOME/IP is used to exchange data between network domain controllers in the latest vehicle networks. SOME/IP supports subscription and notification mechanisms, allowing domain controllers to dynamically subscribe to data provided by another domain controller dependent on the vehicle’s state. SOME/IP transports data between domain controllers and the gateway that a vehicle needs during its regular operation. The use-cases of SOME/IP are similar to the use-cases of CAN communication. The main purpose is the information exchange of sensor and actuator data between ECUs. This usage emphasizes SOME/IP communication as a rewarding target for cyber-attacks. + +CCP/XCP +------- + +Universal Measurement and Calibration Protocol (XCP), the CAN Calibration Protocol (CCP) successor, is a calibration protocol for automotive systems, standardized by ASAM e.V. in 2003. The primary usage of XCP is during the testing and calibration phase of ECU or vehicle development. CCP is designed for use on CAN. No message in CCP exceeds the 8-byte limitation of CAN. To overcome this restriction, XCP was designed to aim for compatibility with a wide range of transport protocols. XCP can be used on top of CAN, CAN FD, Serial Peripheral Interface (SPI), Ethernet, Universal Serial Bus (USB), and FlexRay. The features of CCP and XCP are very similar; however, XCP has a larger functional scope and optimizations for data efficiency. + +Both protocols have a session-based communication procedure and support authentication through seed and key mechanisms between a master and multiple slave nodes. A master node is typically an engineering Personal Computer (PC). In vehicles, slave nodes are ECUs for configuration. XCP also supports simulation. A vehicle engineer can debug a MATLAB Simulink model through XCP. In this case, the simulated model acts as the XCP slave node. CCP and XCP can read and write to the memory of an ECU. Another main feature is data acquisition. Both protocols support a procedure that allows an engineer to configure a so-called data acquisition list with memory addresses of interest. All memory specified in such a list will be read periodically and be broadcast in a CCP or XCP Data Acquisition (DAQ) packet on the chosen communication channel. The following figure gives an overview of all supported communication and packet types in XCP. In the Command Transfer Object (CTO) area, all communication follows a request and response procedure always initiated by the XCP master. A Command Packet (CMD) can receive a Command Response Packet (RES), an Error (ERR) packet, an Event Packet (EV), or a Service Request Packet (SERV) as a response. After the configuration of a slave through CTO CMDs, a slave can listen for Stimulation (STIM) packets and periodically send configured DAQ packets. The resources section in the following figure indicates the possible attack surfaces of this protocol (Programming (PGM), Calibration (CAL), DAQ, STIM) which an attacker could abuse. It is crucial for a vehicle’s security and safety that such protocols, which have their use only during calibration and development of a vehicle, are disabled or removed before a vehicle is shipped to a customer. + +.. _fig-xcp-reference: + +.. figure:: ../graphics/automotive/XCP_ReferenceBook.png + + XCP communication model between XCP Master and XCP Slave. This model shows the communication direction for CTO/Data Transfer Object (DTO) packages [8]_. + +**References** + +.. [1] Kyong-Tak Cho and Kang G. Shin. Error handling of in-vehicle networks makes them vulnerable. In Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, CCS ’16, page 1044–1055, New York, NY, USA, 2016. Association for Computing Machinery. + +.. [2] Sekar Kulandaivel, Tushar Goyal, Arnav Kumar Agrawal, and Vyas Sekar. Canvas: Fast and inexpensive automotive network mapping. In 28th USENIX Security Symposium (USENIX Security 19), pages 389–405, Santa Clara, CA, August 2019. USENIX Association. + +.. [3] Ken Tindell. CAN Bus Security - Attacks on CAN bus and their mitigations, 2019. https://canislabs.com/wp-content/uploads/2020/12/2020-02-14-White-Paper-CAN-Security.pdf + +.. [4] Oliver Hartkopp. Readme file for the Controller Area Network Protocol Family (aka SocketCAN), 2020 (accessed January 29, 2020). https://www.kernel.org/doc/Documentation/networking/can.txt + +.. [5] Dr. Charlie Miller and Chris Valasek. Adventures in Automotive Networks and Control Units. DEF CON 21 Hacking Conference. Las Vegas, NV: DEF CON, August 2013. http://illmatics.com/car_hacking.pdf (accessed 2020-05-27) + +.. [6] AUTOSAR. Specification of Secure Onboard Communication, 2020 (accessed January 31, 2020). https://www.autosar.org/fileadmin/user_upload/standards/classic/4-3/AUTOSAR_SWS_SecureOnboardCommunication.pdf + +.. [7] ISO Central Secretary. Road vehicles – Diagnostic communication over Controller Area Network (DoCAN) – Part 2: Transport protocol and network layer services. Standard ISO 15765-2:2016, International Organization for Standardization, Geneva, CH, 2016. + +.. [8] Vector Informatik GmbH. XCP – The Standard Protocol for ECU Development. Vector Informatik GmbH, 2020 (accessed January 30, 2020). https://assets.vector.com/cms/content/application-areas/ecu-calibration/xcp/XCP_ReferenceBook_V3.0_EN.pdf + +.. [9] Pico Technology Ltd. Complete CAN data frame structure, 2020 (accessed February 14, 2020). https://www.picotech.com/images/uploads/library/topics/_med/CAN-full-frame.jpg + +.. [10] Nils Weiss. Security Testing in Safety-Critical Networks. PhD Study Report. http://www.kiv.zcu.cz/site/documents/verejne/vyzkum/publikace/technicke-zpravy/2020/Rigo_Weiss_2020_2.pdf + + +****** +Layers +****** + +.. note:: **ATTENTION**: Animations below might be outdated. + +CAN +=== + +How-To +------ + +Send and receive a message over Linux SocketCAN:: + + load_layer("can") + load_contrib('cansocket') + + socket = CANSocket(channel='can0') + packet = CAN(identifier=0x123, data=b'01020304') + + socket.send(packet) + rx_packet = socket.recv() + + socket.sr1(packet, timeout=1) + +Send and receive a message over a Vector CAN-Interface:: + + load_layer("can") + conf.contribs['CANSocket'] = {'use-python-can' : True} + load_contrib('cansocket') + + socket = CANSocket(bustype='vector', channel=0, bitrate=1000000) + packet = CAN(identifier=0x123, data=b'01020304') + + socket.send(packet) + rx_packet = socket.recv() + + socket.sr1(packet) -.. image:: ../graphics/animations/animation-cansend.svg CAN Frame -^^^^^^^^^ +--------- Basic information about CAN can be found here: https://en.wikipedia.org/wiki/CAN_bus -The following examples assume that CAN layer in your Scapy session is loaded. If it isn't, -the CAN layer can be loaded with this command in your Scapy session:: +The following examples assume that CAN layer in your Scapy session is loaded. +If it isn't, the CAN layer can be loaded with this command in your Scapy session:: >>> load_layer("can") @@ -163,8 +300,9 @@ Creation of an extended CAN frame:: .. image:: ../graphics/animations/animation-scapy-canframe.svg + CAN Frame in- and export -^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------ CAN Frames can be written to and read from ``pcap`` files:: @@ -183,9 +321,127 @@ This allows you to use ``sniff`` and other functions from Scapy:: .. image:: ../graphics/animations/animation-scapy-rdcandump.svg -Scapy CANSocket + +DBC File Format and CAN Signals +------------------------------- + +In order to support the DBC file format, ``SignalFields`` and the ``SignalPacket`` +classes were added to Scapy. ``SignalFields`` should only be used inside a ``SignalPacket``. +Multiplexer fields (MUX) can be created through ``ConditionalFields``. The following +example demonstrates the usage:: + + DBC Example: + + BO_ 4 muxTestFrame: 7 TEST_ECU + SG_ myMuxer M : 53|3@1+ (1,0) [0|0] "" CCL_TEST + SG_ muxSig4 m0 : 25|7@1- (1,0) [0|0] "" CCL_TEST + SG_ muxSig3 m0 : 16|9@1+ (1,0) [0|0] "" CCL_TEST + SG_ muxSig2 m0 : 15|8@0- (1,0) [0|0] "" CCL_TEST + SG_ muxSig1 m0 : 0|8@1- (1,0) [0|0] "" CCL_TEST + SG_ muxSig5 m1 : 22|7@1- (0.01,0) [0|0] "" CCL_TEST + SG_ muxSig6 m1 : 32|9@1+ (2,10) [0|0] "mV" CCL_TEST + SG_ muxSig7 m1 : 2|8@0- (0.5,0) [0|0] "" CCL_TEST + SG_ muxSig8 m1 : 0|6@1- (10,0) [0|0] "" CCL_TEST + SG_ muxSig9 : 40|8@1- (100,-5) [0|0] "V" CCL_TEST + + BO_ 3 testFrameFloat: 8 TEST_ECU + SG_ floatSignal2 : 32|32@1- (1,0) [0|0] "" CCL_TEST + SG_ floatSignal1 : 7|32@0- (1,0) [0|0] "" CCL_TEST + +Scapy implementation of this DBC description:: + + class muxTestFrame(SignalPacket): + fields_desc = [ + LEUnsignedSignalField("myMuxer", default=0, start=53, size=3), + ConditionalField(LESignedSignalField("muxSig4", default=0, start=25, size=7), lambda p: p.myMuxer == 0), + ConditionalField(LEUnsignedSignalField("muxSig3", default=0, start=16, size=9), lambda p: p.myMuxer == 0), + ConditionalField(BESignedSignalField("muxSig2", default=0, start=15, size=8), lambda p: p.myMuxer == 0), + ConditionalField(LESignedSignalField("muxSig1", default=0, start=0, size=8), lambda p: p.myMuxer == 0), + ConditionalField(LESignedSignalField("muxSig5", default=0, start=22, size=7, scaling=0.01), lambda p: p.myMuxer == 1), + ConditionalField(LEUnsignedSignalField("muxSig6", default=0, start=32, size=9, scaling=2, offset=10, unit="mV"), lambda p: p.myMuxer == 1), + ConditionalField(BESignedSignalField("muxSig7", default=0, start=2, size=8, scaling=0.5), lambda p: p.myMuxer == 1), + ConditionalField(LESignedSignalField("muxSig8", default=0, start=3, size=3, scaling=10), lambda p: p.myMuxer == 1), + LESignedSignalField("muxSig9", default=0, start=41, size=7, scaling=100, offset=-5, unit="V"), + ] + + class testFrameFloat(SignalPacket): + fields_desc = [ + LEFloatSignalField("floatSignal2", default=0, start=32), + BEFloatSignalField("floatSignal1", default=0, start=7) + ] + + bind_layers(SignalHeader, muxTestFrame, identifier=0x123) + bind_layers(SignalHeader, testFrameFloat, identifier=0x321) + + dbc_sock = CANSocket("can0", basecls=SignalHeader) + + pkt = SignalHeader()/testFrameFloat(floatSignal2=3.4) + + dbc_sock.send(pkt) + +This example uses the class ``SignalHeader`` as header. The payload is specified by individual ``SignalPackets``. +``bind_layers`` combines the header with the payload dependent on the CAN identifier. +If you want to directly receive ``SignalPackets`` from your ``CANSocket``, provide the parameter ``basecls`` to +the ``init`` function of your ``CANSocket``. + +Canmatrix supports the creation of Scapy files from DBC or AUTOSAR XML files https://github.com/ebroecker/canmatrix + + +CANSockets +========== + +Linux SocketCAN +--------------- + +This subsection summarizes some basics about Linux SocketCAN. An excellent overview +from Oliver Hartkopp can be found here: https://wiki.automotivelinux.org/_media/agl-distro/agl2017-socketcan-print.pdf + +Virtual CAN Setup +^^^^^^^^^^^^^^^^^ + +Linux SocketCAN supports virtual CAN interfaces. These interfaces are an easy way +to do some first steps on a CAN-Bus without the requirement of special hardware. +Besides that, virtual CAN interfaces are heavily used in Scapy unit tests for +automotive-related contributions. + +Virtual CAN sockets require a special Linux kernel module. The following shell command loads the required module:: + + sudo modprobe vcan + +In order to use a virtual CAN interface some additional commands for setup are required. +This snippet chooses the name ``vcan0`` for the virtual CAN interface. Any name can be chosen here:: + + sudo ip link add name vcan0 type vcan + sudo ip link set dev vcan0 up + +The same commands can be executed from Scapy like this:: + + from scapy.layers.can import * + import os + + bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" + os.system(bashCommand) + +If it's required, a CAN interface can be set into a ``listen-only`` or ``loopback`` mode with ``ip link set`` commands:: + + ip link set vcan0 type can help # shows additional information + + +Linux can-utils ^^^^^^^^^^^^^^^ +As part of Linux SocketCAN, some very useful command line tools are provided from +Oliver Hartkopp: https://github.com/linux-can/can-utils + +The following example shows the basic functions of Linux can-utils. These utilities +are very handy for quick checks, dumping, sending, or logging of CAN messages +from the command line. + +.. image:: ../graphics/animations/animation-cansend.svg + +Scapy CANSocket +--------------- + In Scapy, two kind of CANSockets are implemented. One implementation is called **Native CANSocket**, the other implementation is called **Python-can CANSocket**. @@ -211,30 +467,30 @@ Creating a simple native CANSocket:: load_contrib('cansocket') # Simple Socket - socket = CANSocket(iface="vcan0") + socket = CANSocket(channel="vcan0") Creating a native CANSocket only listen for messages with Id == 0x200:: - socket = CANSocket(iface="vcan0", can_filters=[{'can_id': 0x200, 'can_mask': 0x7FF}]) + socket = CANSocket(channel="vcan0", can_filters=[{'can_id': 0x200, 'can_mask': 0x7FF}]) Creating a native CANSocket only listen for messages with Id >= 0x200 and Id <= 0x2ff:: - socket = CANSocket(iface="vcan0", can_filters=[{'can_id': 0x200, 'can_mask': 0x700}]) + socket = CANSocket(channel="vcan0", can_filters=[{'can_id': 0x200, 'can_mask': 0x700}]) Creating a native CANSocket only listen for messages with Id != 0x200:: - socket = CANSocket(iface="vcan0", can_filters=[{'can_id': 0x200 | CAN_INV_FILTER, 'can_mask': 0x7FF}]) + socket = CANSocket(channel="vcan0", can_filters=[{'can_id': 0x200 | CAN_INV_FILTER, 'can_mask': 0x7FF}]) Creating a native CANSocket with multiple can_filters:: - socket = CANSocket(iface='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}, + socket = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}, {'can_id': 0x400, 'can_mask': 0x7ff}, {'can_id': 0x600, 'can_mask': 0x7ff}, {'can_id': 0x7ff, 'can_mask': 0x7ff}]) Creating a native CANSocket which also receives its own messages:: - socket = CANSocket(iface="vcan0", receive_own_messages=True) + socket = CANSocket(channel="vcan0", receive_own_messages=True) .. image:: ../graphics/animations/animation-scapy-native-cansocket.svg @@ -290,8 +546,8 @@ Import modules:: Create can sockets for attack:: - socket0 = CANSocket(iface='vcan0') - socket1 = CANSocket(iface='vcan1') + socket0 = CANSocket(channel='vcan0') + socket1 = CANSocket(channel='vcan1') Create a function to send packet with threading:: @@ -307,8 +563,8 @@ Create a function for forwarding or change packets:: Create a function to bridge and sniff between two sockets:: def bridge(): - bSocket0 = CANSocket(iface='vcan0') - bSocket1 = CANSocket(iface='vcan1') + bSocket0 = CANSocket(channel='vcan0') + bSocket1 = CANSocket(channel='vcan1') bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=1) bSocket0.close() bSocket1.close() @@ -325,110 +581,46 @@ Start the threads:: Sniff packets:: - packets = socket1.sniff(timeout=0.3) - -Close the sockets:: - - socket0.close() - socket1.close() - -.. image:: ../graphics/animations/animation-scapy-cansockets-mitm.svg -.. image:: ../graphics/animations/animation-scapy-cansockets-mitm2.svg - -DBC File Format and CAN Signals -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In order to support the DBC file format, ``SignalFields`` and the ``SignalPacket`` -classes were added to Scapy. ``SignalFields`` should only be used inside a ``SignalPacket``. -Multiplexer fields (MUX) can be created through ``ConditionalFields``. The following -example demonstrates the usage:: - - DBC Example: - - BO_ 4 muxTestFrame: 7 TEST_ECU - SG_ myMuxer M : 53|3@1+ (1,0) [0|0] "" CCL_TEST - SG_ muxSig4 m0 : 25|7@1- (1,0) [0|0] "" CCL_TEST - SG_ muxSig3 m0 : 16|9@1+ (1,0) [0|0] "" CCL_TEST - SG_ muxSig2 m0 : 15|8@0- (1,0) [0|0] "" CCL_TEST - SG_ muxSig1 m0 : 0|8@1- (1,0) [0|0] "" CCL_TEST - SG_ muxSig5 m1 : 22|7@1- (0.01,0) [0|0] "" CCL_TEST - SG_ muxSig6 m1 : 32|9@1+ (2,10) [0|0] "mV" CCL_TEST - SG_ muxSig7 m1 : 2|8@0- (0.5,0) [0|0] "" CCL_TEST - SG_ muxSig8 m1 : 0|6@1- (10,0) [0|0] "" CCL_TEST - SG_ muxSig9 : 40|8@1- (100,-5) [0|0] "V" CCL_TEST - - BO_ 3 testFrameFloat: 8 TEST_ECU - SG_ floatSignal2 : 32|32@1- (1,0) [0|0] "" CCL_TEST - SG_ floatSignal1 : 7|32@0- (1,0) [0|0] "" CCL_TEST - -Scapy implementation of this DBC description:: - - class muxTestFrame(SignalPacket): - fields_desc = [ - LEUnsignedSignalField("myMuxer", default=0, start=53, size=3), - ConditionalField(LESignedSignalField("muxSig4", default=0, start=25, size=7), lambda p: p.myMuxer == 0), - ConditionalField(LEUnsignedSignalField("muxSig3", default=0, start=16, size=9), lambda p: p.myMuxer == 0), - ConditionalField(BESignedSignalField("muxSig2", default=0, start=15, size=8), lambda p: p.myMuxer == 0), - ConditionalField(LESignedSignalField("muxSig1", default=0, start=0, size=8), lambda p: p.myMuxer == 0), - ConditionalField(LESignedSignalField("muxSig5", default=0, start=22, size=7, scaling=0.01), lambda p: p.myMuxer == 1), - ConditionalField(LEUnsignedSignalField("muxSig6", default=0, start=32, size=9, scaling=2, offset=10, unit="mV"), lambda p: p.myMuxer == 1), - ConditionalField(BESignedSignalField("muxSig7", default=0, start=2, size=8, scaling=0.5), lambda p: p.myMuxer == 1), - ConditionalField(LESignedSignalField("muxSig8", default=0, start=3, size=3, scaling=10), lambda p: p.myMuxer == 1), - LESignedSignalField("muxSig9", default=0, start=41, size=7, scaling=100, offset=-5, unit="V"), - ] - - class testFrameFloat(SignalPacket): - fields_desc = [ - LEFloatSignalField("floatSignal2", default=0, start=32), - BEFloatSignalField("floatSignal1", default=0, start=7) - ] - - bind_layers(SignalHeader, muxTestFrame, identifier=0x123) - bind_layers(SignalHeader, testFrameFloat, identifier=0x321) - - dbc_sock = CANSocket("can0", basecls=SignalHeader) - - pkt = SignalHeader()/testFrameFloat(floatSignal2=3.4) - - dbc_sock.send(pkt) - -This example uses the class ``SignalHeader`` as header. The payload is specified by individual ``SignalPackets``. -``bind_layers`` combines the header with the payload dependent on the CAN identifier. -If you want to directly receive ``SignalPackets`` from your ``CANSocket``, provide the parameter ``basecls`` to -the ``init`` function of your ``CANSocket``. + packets = socket1.sniff(timeout=0.3) -Canmatrix supports the creation of Scapy files from DBC or AUTOSAR XML files https://github.com/ebroecker/canmatrix +Close the sockets:: + + socket0.close() + socket1.close() +.. image:: ../graphics/animations/animation-scapy-cansockets-mitm.svg +.. image:: ../graphics/animations/animation-scapy-cansockets-mitm2.svg CAN Calibration Protocol (CCP) ============================== CCP is derived from CAN. The CAN-header is part of a CCP frame. CCP has two types of message objects. One is called Command Receive Object (CRO), the other is called -Data Transmission Object (DTO). Usually CROs are sent to an ECU, and DTOs are received -from an ECU. The information, if one DTO answers a CRO is implemented through a counter +Data Transmission Object (DTO). Usually CROs are sent to an Ecu, and DTOs are received +from an Ecu. The information, if one DTO answers a CRO is implemented through a counter field (ctr). If both objects have the same counter value, the payload of a DTO object can be interpreted from the command of the associated CRO object. Creating a CRO message:: + load_contrib('automotive.ccp') CCP(identifier=0x700)/CRO(ctr=1)/CONNECT(station_address=0x02) CCP(identifier=0x711)/CRO(ctr=2)/GET_SEED(resource=2) CCP(identifier=0x711)/CRO(ctr=3)/UNLOCK(key=b"123456") -If we aren't interested in the DTO of an ECU, we can just send a CRO message like this: +If we aren't interested in the DTO of an Ecu, we can just send a CRO message like this: Sending a CRO message:: pkt = CCP(identifier=0x700)/CRO(ctr=1)/CONNECT(station_address=0x02) - sock = CANSocket(iface=can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000)) + sock = CANSocket(bustype='socketcan', channel='vcan0') sock.send(pkt) -If we are interested in the DTO of an ECU, we need to set the basecls parameter of the +If we are interested in the DTO of an Ecu, we need to set the basecls parameter of the CANSocket to CCP and we need to use sr1: Sending a CRO message:: cro = CCP(identifier=0x700)/CRO(ctr=0x53)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12") - sock = CANSocket(iface=can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000), basecls=CCP) + sock = CANSocket(bustype='socketcan', channel='vcan0', basecls=CCP) dto = sock.sr1(cro) dto.show() ###[ CAN Calibration Protocol ]### @@ -447,11 +639,158 @@ Sending a CRO message:: Since sr1 calls the answers function, our payload of the DTO objects gets interpreted with the command of our CRO object. + +Universal calibration and measurement protocol (XCP) +==================================================== + +XCP is the successor of CCP. It is usable with several protocols. Scapy includes CAN, UDP and TCP. +XCP has two types of message types: Command Transfer Object (CTO) and Data Transmission Object (DTO). +CTOs send to an Ecu are requests (commands) and the Ecu has to reply with a positive response or an error. +Additionally, the Ecu can send a CTO to inform the master about an asynchronous event (EV) or request a service execution (SERV). +DTOs sent by the Ecu are called DAQ (Data AcQuisition) and include measured values. +DTOs received by the Ecu are used for a periodic stimulation and are called STIM (Stimulation). + + +Creating a CTO message:: + + CTORequest() / Connect() + CTORequest() / GetDaqResolutionInfo() + CTORequest() / GetSeed(mode=0x01, resource=0x00) + +To send the message over CAN a header has to be added:: + + pkt = XCPOnCAN(identifier=0x700) / CTORequest() / Connect() + sock = CANSocket(iface=can.interface.Bus(bustype='socketcan', channel='vcan0')) + sock.send(pkt) + +If we are interested in the response of an Ecu, we need to set the basecls parameter of the +CANSocket to XCPonCAN and we need to use sr1: +Sending a CTO message:: + + sock = CANSocket(bustype='socketcan', channel='vcan0', basecls=XCPonCAN) + dto = sock.sr1(pkt) + +Since sr1 calls the answers function, our payload of the XCP-response objects gets interpreted with the +command of our CTO object. Otherwise it could not be interpreted. +The first message should always be the "CONNECT" message, the response of the Ecu determines how the messages are read. E.g.: byte order. +Otherwise, one must set the address granularity, and max size of the DTOs and CTOs per hand in the contrib config:: + + conf.contribs['XCP']['Address_Granularity_Byte'] = 1 # Can be 1, 2 or 4 + conf.contribs['XCP']['MAX_CTO'] = 8 + conf.contribs['XCP']['MAX_DTO'] = 8 + +If you do not want this to be set after receiving the message you can also disable that feature:: + + conf.contribs['XCP']['allow_byte_order_change'] = False + conf.contribs['XCP']['allow_ag_change'] = False + conf.contribs['XCP']['allow_cto_and_dto_change'] = False + +To send a pkt over TCP or UDP another header must be used. +TCP:: + + prt1, prt2 = 12345, 54321 + XCPOnTCP(sport=prt1, dport=prt2) / CTORequest() / Connect() + +UDP:: + + XCPOnUDP(sport=prt1, dport=prt2) / CTORequest() / Connect() + + +XCPScanner +--------------- + +The XCPScanner is a utility to find the CAN identifiers of ECUs that support XCP. + +Commandline usage example:: + + python -m scapy.tools.automotive.xcpscanner -h + Finds XCP slaves using the "GetSlaveId"-message(Broadcast) or the "Connect"-message. + + positional arguments: + channel Linux SocketCAN interface name, e.g.: vcan0 + + optional arguments: + -h, --help show this help message and exit + --start START, -s START + Start identifier CAN (in hex). + The scan will test ids between --start and --end (inclusive) + Default: 0x00 + --end END, -e END End identifier CAN (in hex). + The scan will test ids between --start and --end (inclusive) + Default: 0x7ff + --sniff_time', '-t' Duration in milliseconds a sniff is waiting for a response. + Default: 100 + --broadcast, -b Use Broadcast-message GetSlaveId instead of default "Connect" + (GetSlaveId is an optional Message that is not always implemented) + --verbose VERBOSE, -v + Display information during scan + + Examples: + python3.6 -m scapy.tools.automotive.xcpscanner can0 + python3.6 -m scapy.tools.automotive.xcpscanner can0 -b 500 + python3.6 -m scapy.tools.automotive.xcpscanner can0 -s 50 -e 100 + python3.6 -m scapy.tools.automotive.xcpscanner can0 -b 500 -v + + +Interactive shell usage example:: + >>> conf.contribs['CANSocket'] = {'use-python-can': False} + >>> load_layer("can") + >>> load_contrib("automotive.xcp.xcp") + >>> sock = CANSocket("vcan0") + >>> sock.basecls = XCPOnCAN + >>> scanner = XCPOnCANScanner(sock) + >>> result = scanner.start_scan() + +The result includes the slave_id (the identifier of the Ecu that receives XCP messages), +and the response_id (the identifier that the Ecu will send XCP messages to). + ISOTP ===== +ISOTP message +------------- + +Creating an ISOTP message:: + + load_contrib('isotp') + ISOTP(tx_id=0x241, rx_id=0x641, data=b"\x3eabc") + +Creating an ISOTP message with extended addressing:: + + ISOTP(tx_id=0x241, rx_id=0x641, rx_ext_address=0x41, data=b"\x3eabc") + +Creating an ISOTP message with extended addressing:: + + ISOTP(tx_id=0x241, rx_id=0x641, rx_ext_address=0x41, ext_address=0x41, data=b"\x3eabc") + +Create CAN-frames from an ISOTP message:: + + ISOTP(tx_id=0x241, rx_id=0x641, rx_ext_address=0x41, ext_address=0x55, data=b"\x3eabc" * 10).fragment() + +Send ISOTP message over ISOTP socket:: + + isoTpSocket = ISOTPSocket('vcan0', tx_id=0x241, rx_id=0x641) + isoTpMessage = ISOTP('Message') + isoTpSocket.send(isoTpMessage) + +Sniff ISOTP message:: + + isoTpSocket = ISOTPSocket('vcan0', tx_id=0x641, rx_id=0x241) + packets = isoTpSocket.sniff(timeout=0.5) + +ISOTP Sockets +------------- + +Scapy provides two kinds of ISOTP-Sockets. One implementation, the ``ISOTPNativeSocket`` +is using the Linux kernel module from Hartkopp. The other implementation, the ``ISOTPSoftSocket`` +is completely implemented in Python. This implementation can be used on Linux, +Windows, and OSX. + +An ``ISOTPSocket`` will not respect ``tx_id, rx_id, rx_ext_address, ext_address`` of an ``ISOTP`` +message object. + System compatibilities ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ Dependent on your setup, different implementations have to be used. @@ -471,45 +810,60 @@ The class ``ISOTPSocket`` can be set to a ``ISOTPNativeSocket`` or a ``ISOTPSoft The decision is made dependent on the configuration ``conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': True}`` (to select ``ISOTPNativeSocket``) or ``conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False}`` (to select ``ISOTPSoftSocket``). This will allow you to write platform independent code. Apply this configuration before loading the ISOTP layer -with ``load_contrib("isotp")``. +with ``load_contrib('isotp')``. -Another remark in respect to ISOTPSocket compatibility. Always use with for socket creation. Example:: +Another remark in respect to ISOTPSocket compatibility. Always use ``with`` for +socket creation. This ensures that ``ISOTPSoftSocket`` objects will get closed +properly. +Example:: - with ISOTPSocket("vcan0", did=0x241, sid=0x641) as sock: + with ISOTPSocket("vcan0", rx_id=0x241, tx_id=0x641) as sock: sock.send(...) +ISOTPNativeSocket +^^^^^^^^^^^^^^^^^ +**Requires:** -ISOTP message -------------- +* Python3 +* Linux +* Hartkopp's Linux kernel module: ``https://github.com/hartkopp/can-isotp.git`` (merged into mainline Linux in 5.10) -Creating an ISOTP message:: +During pentests, the ISOTPNativeSockets has a better performance and +reliability, usually. If you are working on Linux, consider this implementation:: + conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': True} load_contrib('isotp') - ISOTP(src=0x241, dst=0x641, data=b"\x3eabc") - -Creating an ISOTP message with extended addressing:: - - ISOTP(src=0x241, dst=0x641, exdst=0x41, data=b"\x3eabc") + sock = ISOTPSocket("can0", tx_id=0x641, rx_id=0x241) -Creating an ISOTP message with extended addressing:: +Since this implementation is using a standard Linux socket, all Scapy functions +like ``sniff, sr, sr1, bridge_and_sniff`` work out of the box. - ISOTP(src=0x241, dst=0x641, exdst=0x41, exsrc=0x41, data=b"\x3eabc") +ISOTPSoftSocket +^^^^^^^^^^^^^^^ -Create CAN-frames from an ISOTP message:: +ISOTPSoftSockets can use any CANSocket. This gives the flexibility to use all +python-can interfaces. Additionally, these sockets work on Python2 and Python3. +Usage on Linux with native CANSockets:: - ISOTP(src=0x241, dst=0x641, exdst=0x41, exsrc=0x55, data=b"\x3eabc" * 10).fragment() + conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} + load_contrib('isotp') + with ISOTPSocket("can0", tx_id=0x641, rx_id=0x241) as sock: + sock.send(...) -Send ISOTP message over ISOTP socket:: +Usage with python-can CANSockets:: - isoTpSocket = ISOTPSocket('vcan0', sid=0x241, did=0x641) - isoTpMessage = ISOTP('Message') - isoTpSocket.send(isoTpMessage) + conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} + conf.contribs['CANSocket'] = {'use-python-can': True} + load_contrib('isotp') + with ISOTPSocket(CANSocket(bustype='socketcan', channel="can0"), tx_id=0x641, rx_id=0x241) as sock: + sock.send(...) -Sniff ISOTP message:: +This second example allows the usage of any ``python_can.interface`` object. - isoTpSocket = ISOTPSocket('vcan0', sid=0x641, did=0x241) - packets = isoTpSocket.sniff(timeout=0.5) +**Attention:** The internal implementation of ISOTPSoftSockets requires a background +thread. In order to be able to close this thread properly, we suggest the use of +Pythons ``with`` statement. ISOTP MITM attack with bridge and sniff --------------------------------------- @@ -522,15 +876,6 @@ Set up two vcans on Linux terminal:: sudo ip link set dev vcan0 up sudo ip link set dev vcan1 up -Set up ISOTP: - -First make sure you installed an iso-tp kernel module. - -When the vcan core module is loaded with "sudo modprobe vcan" the iso-tp module can be loaded to the kernel. - -Therefore navigate to isotp directory, and load module with "sudo insmod ./net/can/can-isotp.ko". (Tested on Kernel 4.9.135-1-MANJARO) - -Detailed instructions you find in https://github.com/hartkopp/can-isotp. Import modules:: @@ -541,8 +886,8 @@ Import modules:: Create to ISOTP sockets for attack:: - isoTpSocketVCan0 = ISOTPSocket('vcan0', sid=0x241, did=0x641) - isoTpSocketVCan1 = ISOTPSocket('vcan1', sid=0x641, did=0x241) + isoTpSocketVCan0 = ISOTPSocket('vcan0', tx_id=0x241, rx_id=0x641) + isoTpSocketVCan1 = ISOTPSocket('vcan1', tx_id=0x641, rx_id=0x241) Create function to send packet on vcan0 with threading:: @@ -559,8 +904,8 @@ Create function to forward packet:: Create function to bridge and sniff between two buses:: def bridge(): - bSocket0 = ISOTPSocket('vcan0', sid=0x641, did=0x241) - bSocket1 = ISOTPSocket('vcan1', sid=0x241, did=0x641) + bSocket0 = ISOTPSocket('vcan0', tx_id=0x641, rx_id=0x241) + bSocket1 = ISOTPSocket('vcan1', tx_id=0x241, rx_id=0x641) bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=1) bSocket0.close() bSocket1.close() @@ -570,7 +915,7 @@ Create threads for sending packet and to bridge and sniff:: threadBridge = threading.Thread(target=bridge) threadSender = threading.Thread(target=sendPacketWithISOTPSocket) -Start threads are based on Linux kernel modules. The python-can project is used to support CAN and CANSockets on other systems, besides Linux. This guide explains the hardware setup on a BeagleBone Black. The BeagleBone Black was chosen because of its two CAN interfaces on the main processor. The presence of two CAN interfaces in one device gives the possibility of CAN MITM attacks and session hijacking. The Cannelloni framework turns a BeagleBone Black into a CAN-to-UDP interface, which gives you the freedom to run Scapy on a more powerful machine.:: +Start threads:: threadBridge.start() threadSender.start() @@ -584,66 +929,11 @@ Close sockets:: isoTpSocketVCan0.close() isoTpSocketVCan1.close() -An ISOTPSocket will not respect ``src, dst, exdst, exsrc`` of an ISOTP message object. - -ISOTP Sockets -============= - -Scapy provides two kinds of ISOTP Sockets. One implementation, the ISOTPNativeSocket -is using the Linux kernel module from Hartkopp. The other implementation, the ISOTPSoftSocket -is completely implemented in Python. This implementation can be used on Linux, -Windows, and OSX. - -ISOTPNativeSocket ------------------ - -**Requires:** - -* Python3 -* Linux -* Hartkopp's Linux kernel module: ``https://github.com/hartkopp/can-isotp.git`` - -During pentests, the ISOTPNativeSockets has a better performance and -reliability, usually. If you are working on Linux, consider this implementation:: - - conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': True} - load_contrib('isotp') - sock = ISOTPSocket("can0", sid=0x641, did=0x241) - -Since this implementation is using a standard Linux socket, all Scapy functions -like ``sniff, sr, sr1, bridge_and_sniff`` work out of the box. - -ISOTPSoftSocket ---------------- - -ISOTPSoftSockets can use any CANSocket. This gives the flexibility to use all -python-can interfaces. Additionally, these sockets work on Python2 and Python3. -Usage on Linux with native CANSockets:: - - conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} - load_contrib('isotp') - with ISOTPSocket("can0", sid=0x641, did=0x241) as sock: - sock.send(...) - -Usage with python-can CANSockets:: - - conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} - conf.contribs['CANSocket'] = {'use-python-can': True} - load_contrib('isotp') - with ISOTPSocket(CANSocket(iface=python_can.interface.Bus(bustype='socketcan', channel="can0", bitrate=250000)), sid=0x641, did=0x241) as sock: - sock.send(...) - -This second example allows the usage of any ``python_can.interface`` object. - -**Attention:** The internal implementation of ISOTPSoftSockets requires a background -thread. In order to be able to close this thread properly, we suggest the use of -Pythons ``with`` statement. - -ISOTPScan and ISOTPScanner --------------------------- +isotp_scan and ISOTPScanner +--------------------------- -ISOTPScan is a utility function to find ISOTP-Endpoints on a CAN-Bus. +isotp_scan is a utility function to find ISOTP-Endpoints on a CAN-Bus. ISOTPScanner is a commandline-utility for the identical function. .. image:: ../graphics/animations/animation-scapy-isotpscan.svg @@ -699,7 +989,7 @@ Interactive shell usage example:: >>> conf.contribs['CANSocket'] = {'use-python-can': False} >>> load_contrib('cansocket') >>> load_contrib('isotp') - >>> socks = ISOTPScan(CANSocket("vcan0"), range(0x700, 0x7ff), can_interface="vcan0") + >>> socks = isotp_scan(CANSocket("vcan0"), range(0x700, 0x800), can_interface="vcan0") >>> socks [< at 0x7f98e27c8210>, < at 0x7f98f9079cd0>, @@ -708,13 +998,11 @@ Interactive shell usage example:: < at 0x7f98f912e950>, < at 0x7f98f906c0d0>] - - UDS === -The main usage of UDS is flashing and diagnostic of an ECU. UDS is an -application layer protocol and can be used as a DoIP or ENET payload or a UDS packet +The main usage of UDS is flashing and diagnostic of an Ecu. UDS is an +application layer protocol and can be used as a DoIP or HSFZ payload or a UDS packet can directly be sent over an ISOTPSocket. Every OEM has its own customization of UDS. This increases the difficulty of generic applications and OEM specific knowledge is required for penetration tests. RoutineControl jobs and ReadDataByIdentifier/WriteDataByIdentifier @@ -732,9 +1020,9 @@ Customization of UDS_RDBI, UDS_WDBI ----------------------------------- In real-world use-cases, the UDS layer is heavily customized. OEMs define their own substructure of packets. -Especially the packets ReadDataByIdentifier or WriteDataByIdentifier have a very OEM or even ECU specific +Especially the packets ReadDataByIdentifier or WriteDataByIdentifier have a very OEM or even Ecu specific substructure. Therefore a ``StrField`` ``dataRecord`` is not added to the ``field_desc``. -The intended usage is to create ECU or OEM specific description files, which extend the general UDS layer of +The intended usage is to create Ecu or OEM specific description files, which extend the general UDS layer of Scapy with further protocol implementations. Customization example:: @@ -768,10 +1056,11 @@ Customization example:: UDS_RDBI.dataIdentifiers[0x172b] = 'GatewayIP' -If one wants to work with this custom additions, these can be loaded at runtime to the Scapy interpreter:: +If one wants to work with this custom additions, these can be loaded at runtime +to the Scapy interpreter:: - >>> load_contrib("automotive.uds") - >>> load_contrib("automotive.OEM-XYZ.car-model-xyz") + >>> load_contrib('automotive.uds') + >>> load_contrib('automotive.OEM-XYZ.car-model-xyz') >>> pkt = UDS()/UDS_WDBI()/DBI_IP(IP='192.168.2.1', SUBNETMASK='255.255.255.0', DEFAULT_GATEWAY='192.168.2.1') @@ -792,8 +1081,101 @@ If one wants to work with this custom additions, these can be loaded at runtime .. image:: ../graphics/animations/animation-scapy-uds3.svg + +Single Layer Mode +----------------- + +UDS, KWP, OBD, and GMLAN all support a *single layer mode* that makes each +service packet a standalone ``Packet`` rather than a nested sublayer. + +**Default (multi-layer) mode** + +.. code-block:: python + + >>> pkt = UDS() / UDS_DSC(diagnosticSessionType=0x01) + >>> UDS(b'\x10\x01') + > + +**Single layer mode** + +To enable before loading a module:: + + >>> conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, + ... 'single_layer_mode': True} + >>> load_contrib('automotive.uds') + +To toggle at runtime after loading:: + + >>> conf.contribs['UDS']['single_layer_mode'] = True + >>> UDS(b'\x10\x01') + + >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + >>> conf.contribs['UDS']['single_layer_mode'] = False # revert to multi-layer mode + +The same ``single_layer_mode`` key works for all protocols: replace ``'UDS'`` +with ``'KWP'``, ``'OBD'``, or ``'GMLAN'`` as appropriate. + +Compatibility Mode +------------------ + +Scapy allows crafting packets freely, including stacking a service sub-packet +on top of the base protocol layer (e.g. ``UDS()/UDS_DSC()``). When both +``single_layer_mode`` *and* stacking are used together, the ``service`` byte +would normally appear twice in the resulting byte stream – once from the base +layer and once from the sub-packet's own ``service`` ConditionalField. + +The **compatibility mode** flag (``compatibility_mode``, default ``True``) +addresses this: when it is enabled and ``single_layer_mode`` is active, the +sub-packet's ``service`` field is automatically **suppressed** whenever the +immediate underlayer is already the matching base-protocol packet. + +.. list-table:: Behaviour matrix + :header-rows: 1 + :widths: 25 25 50 + + * - ``single_layer_mode`` + - ``compatibility_mode`` + - ``UDS()/UDS_DSC()`` byte layout + * - ``False`` + - any + - ``service`` (UDS) + ``diagnosticSessionType`` (UDS_DSC) + * - ``True`` + - ``True`` *(default)* + - ``service`` (UDS) + ``diagnosticSessionType`` (UDS_DSC) — duplicate suppressed + * - ``True`` + - ``False`` + - ``service`` (UDS) + ``service`` (UDS_DSC) + ``diagnosticSessionType`` (UDS_DSC) + +Example with compatibility mode on (default):: + + >>> conf.contribs['UDS']['single_layer_mode'] = True + >>> conf.contribs['UDS']['compatibility_mode'] = True # already the default + + >>> # Standalone sub-packet: service field IS present (no UDS underlayer) + >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + + >>> # Stacked: service field in UDS_DSC is suppressed (UDS is the underlayer) + >>> bytes(UDS() / UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + +Example with compatibility mode off:: + + >>> conf.contribs['UDS']['compatibility_mode'] = False + + >>> # Stacked: both UDS and UDS_DSC emit a service byte + >>> bytes(UDS() / UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x10\x01' + + >>> conf.contribs['UDS']['compatibility_mode'] = True # restore default + +The same ``compatibility_mode`` key works for all protocols: replace ``'UDS'`` +with ``'KWP'``, ``'OBD'``, or ``'GMLAN'`` as appropriate. + GMLAN ===== + GMLAN is very similar to UDS. It's GMs application layer protocol for flashing, calibration and diagnostic of their cars. Use the argument ``basecls=GMLAN`` on the ``init`` function of an ISOTPSocket. @@ -803,47 +1185,52 @@ Usage example: .. image:: ../graphics/animations/animation-scapy-gmlan.svg -ECU Utility examples +Ecu Utility examples ==================== -The ECU utility can be used to analyze the internal states of an ECU under investigation. +The Ecu utility can be used to analyze the internal states of an Ecu under investigation. This utility depends heavily on the support of the used protocol. ``UDS`` is supported. -Log all commands applied to an ECU +Log all commands applied to an Ecu ---------------------------------- -This example shows the logging mechanism of an ECU object. The log of an ECU is a dictionary of applied UDS commands. The key for this dictionary is the UDS service name. The value consists of a list of tuples, containing a timestamp and a log value +This example shows the logging mechanism of an Ecu object. The log of an Ecu +is a dictionary of applied UDS commands. The key for this dictionary is the +UDS service name. The value consists of a list of tuples, containing a timestamp +and a log value Usage example:: - ecu = ECU(verbose=False, store_supported_responses=False) + ecu = Ecu(verbose=False, store_supported_responses=False) ecu.update(PacketList(msgs)) print(ecu.log) timestamp, value = ecu.log["DiagnosticSessionControl"][0] -Trace all commands applied to an ECU +Trace all commands applied to an Ecu ------------------------------------ -This example shows the trace mechanism of an ECU object. Traces of the current state of the ECU object and the received message are printed on stdout. Some messages, depending on the protocol, will change the internal state of the ECU. +This example shows the trace mechanism of an Ecu object. Traces of the current +state of the Ecu object and the received message are printed on stdout. +Some messages, depending on the protocol, will change the internal state of the Ecu. Usage example:: - ecu = ECU(verbose=True, logging=False, store_supported_responses=False) + ecu = Ecu(verbose=True, logging=False, store_supported_responses=False) ecu.update(PacketList(msgs)) print(ecu.current_session) -Generate supported responses of an ECU +Generate supported responses of an Ecu -------------------------------------- -This example shows a mechanism to clone a real world ECU by analyzing a list of Packets. +This example shows a mechanism to clone a real world Ecu by analyzing a list of Packets. Usage example:: - ecu = ECU(verbose=False, logging=False, store_supported_responses=True) + ecu = Ecu(verbose=False, logging=False, store_supported_responses=True) ecu.update(PacketList(msgs)) supported_responses = ecu.supported_responses unanswered_packets = ecu.unanswered_packets @@ -855,15 +1242,18 @@ Usage example:: Analyze multiple UDS messages ----------------------------- -This example shows how to load ``UDS`` messages from a ``.pcap`` file containing ``CAN`` messages. A ``PcapReader`` object is used as socket and an ``ISOTPSession`` parses ``CAN`` frames to ``ISOTP`` frames which are then casted to ``UDS`` objects through the ``basecls`` parameter +This example shows how to load ``UDS`` messages from a ``.pcap`` file containing +``CAN`` messages. A ``PcapReader`` object is used as socket and an +``ISOTPSession`` parses ``CAN`` frames to ``ISOTP`` frames which are +then casted to ``UDS`` objects through the ``basecls`` parameter Usage example:: with PcapReader("test/contrib/automotive/ecu_trace.pcap") as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"use_ext_addr":False, "basecls":UDS}, count=50, opened_socket=sock) + udsmsgs = sniff(session=ISOTPSession(use_ext_addr=False, basecls=UDS), count=50, opened_socket=sock) - ecu = ECU() + ecu = Ecu() ecu.update(udsmsgs) print(ecu.log) print(ecu.supported_responses) @@ -871,17 +1261,21 @@ Usage example:: -Analyze on the fly with ECUSession +Analyze on the fly with EcuSession ---------------------------------- -This example shows the usage of an ECUSession in sniff. An ISOTPSocket or any socket like object which returns entire messages of the right protocol can be used. An ``ECUSession`` is used as supersession in an ``ISOTPSession``. To obtain the ``ECU`` object from an ``ECUSession``, the ``ECUSession`` has to be created outside of sniff. +This example shows the usage of an EcuSession in sniff. An ISOTPSocket or any +socket like object which returns entire messages of the right protocol can be +used. An ``EcuSession`` is used as supersession in an ``ISOTPSession``. +To obtain the ``Ecu`` object from an ``EcuSession``, the ``EcuSession`` +has to be created outside of sniff. Usage example:: - session = ECUSession() + session = EcuSession() with PcapReader("test/contrib/automotive/ecu_trace.pcap") as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "use_ext_addr":False, "basecls":UDS}, count=50, opened_socket=sock) + udsmsgs = sniff(session=ISOTPSession(use_ext_addr=False, basecls=UDS, supersession=session)), count=50, opened_socket=sock) ecu = session.ecu print(ecu.log) @@ -895,12 +1289,15 @@ SOME/IP and SOME/IP SD messages Creating a SOME/IP message -------------------------- -This example shows a SOME/IP message which requests a service 0x1234 with the method 0x421. Different types of SOME/IP messages follow the same procedure and their specifications can be seen here ``http://www.some-ip.com/papers/cache/AUTOSAR_TR_SomeIpExample_4.2.1.pdf``. +This example shows a SOME/IP message which requests a service 0x1234 with the +method 0x421. Different types of SOME/IP messages follow the same procedure +and their specifications can be seen here +``http://www.some-ip.com/papers/cache/AUTOSAR_TR_SomeIpExample_4.2.1.pdf``. Load the contribution:: - load_contrib("automotive.someip") + load_contrib('automotive.someip') Create UDP package:: @@ -937,7 +1334,7 @@ In this example a SOME/IP SD offer service message is shown with an IPv4 endpoin Load the contribution:: - load_contrib("automotive.someip") + load_contrib('automotive.someip') Create UDP package:: @@ -963,7 +1360,7 @@ Create the entry array input:: Create the options array input:: - oa = SDOption_IP4_Endpoint() + oa = SDOption_IP4_EndPoint() oa.addr = "192.168.0.13" oa.l4_proto = 0x11 oa.port = 30509 @@ -985,10 +1382,7 @@ Stack it and send it:: OBD === -OBD message ------------ - -OBD is implemented on top of ISOTP. Use an ISOTPSocket for the communication with an ECU. +OBD is implemented on top of ISOTP. Use an ISOTPSocket for the communication with an Ecu. You should set the parameters ``basecls=OBD`` and ``padding=True`` in your ISOTPSocket init call. OBD is split into different service groups. Here are some example requests: @@ -1010,8 +1404,9 @@ The response will contain a PacketListField, called `data_records`. This field c |###[ PID_00_PIDsSupported ]### | supported_pids= PID20+PID1F+PID1C+PID15+PID14+PID13+PID11+PID10+PID0F+PID0E+PID0D+PID0C+PID0B+PID0A+PID07+PID06+PID05+PID04+PID03+PID01 -Let's assume our ECU under test supports the pid 0x15:: - + +Let's assume our Ecu under test supports the pid 0x15:: + req = OBD()/OBD_S01(pid=[0x15]) resp = sock.sr1(req) resp.show() @@ -1036,7 +1431,7 @@ Service 08 supports Test Identifiers (tid). Service 09 supports Information Identifiers (iid). Examples: -^^^^^^^^^ +--------- Request supported Information Identifiers:: @@ -1061,236 +1456,456 @@ Request the Vehicle Identification Number (VIN):: .. image:: ../graphics/animations/animation-scapy-obd.svg -Test-Setup Tutorials -==================== +Message Authentication (AUTOSAR SecOC) +====================================== -Hardware Setup --------------- +AutoSAR SecOC is a security architecture protecting communication between ECUs in a vehicle from cyber-attacks. -Beagle Bone Black Operating System Setup -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- **Module**: AUTOSAR +- **Functions**: Provides message integrity and authentication +- **Protection**: Freshness value to counter replay attacks +- **Cryptography**: Supports asymmetric and symmetric methods +- **Key Distribution**: Not specified +- **Unique Identifiers**: Every PDU has a SecOCDataID + - **CAN Networks**: Uses CAN identifier + - **Ethernet Networks**: Uses PDU identifier or mappings to SecOCDataIDs -#. | **Download an Image** - | The latest Debian Linux image can be found at the website - | ``https://beagleboard.org/latest-images``. Choose the BeagleBone - Black IoT version and download it. +.. figure:: ../graphics/automotive/autosar1.png + :alt: Overview SecOC. Author: AUTOSAR - :: +Generation +---------- - wget https://debian.beagleboard.org/images/bone-debian-8.7\ - -iot-armhf-2017-03-19-4gb.img.xz +- Secured I-PDU includes freshness value and MAC +- Freshness value increments on every transmit or derived from a tick count +- MAC generation uses SecOCDataID, PDU, and freshness value +- In symmetric mode, MAC bits can be truncated, reducing security +Truncation +---------- - After the download, copy it to an SD-Card with minimum of 4 GB storage. +.. figure:: ../graphics/automotive/autosar2.png + :alt: Secured I-PDU contents with truncated Freshness Counter and truncated Authenticator. Author: AUTOSAR - :: +- MAC and freshness value are transferred in truncated format to save bandwidth - xzcat bone-debian-8.7-iot-armhf-2017-03-19-4gb.img.xz | \ - sudo dd of=/dev/xvdj +Verification +------------ +- Only LSBs of the freshness value are transmitted +- Compute full freshness value internally + - Overwrite LSBs of the last received value + - Increment MSBs if received LSBs are smaller than the last LSBs +- Calculate MAC from PDU and full freshness count +- Accept PDU if calculated and transmitted MACs match, otherwise reject -#. | **Enable WiFi** - | USB-WiFi dongles are well supported by Debian Linux. Login over SSH - on the BBB and add the WiFi network credentials to the file - ``/var/lib/connman/wifi.config``. If a USB-WiFi dongle is not - available, it is also possible to share the host's internet - connection with the Ethernet connection of the BBB emulated over - USB. A tutorial to share the host network connection can be found - on this page: - | ``https://elementztechblog.wordpress.com/2014/12/22/sharing-internet -using-network-over-usb-in-beaglebone-black/``. - | Login as root onto the BBB: +Profiles +-------- - :: +AutoSAR specifies three profiles for truncated freshness value and MAC sizes. All use CMAC with AES128: - ssh debian@192.168.7.2 - sudo su +- **Profile 1 (24Bit-CMAC-8Bit-FV)** + - Algorithm: CMAC/AES-128 + - Freshness value: 8 bits + - MAC: 24 bits +- **Profile 2 (24Bit-CMAC-No-FV)** + - Algorithm: CMAC/AES-128 + - Freshness value: 0 bits + - MAC: 24 bits + - No freshness values used - Provide the WiFi login credentials to connman: +- **Profile 3 (JASPAR)** + - Algorithm: CMAC/AES-128 + - Freshness value: 64 bits + - Truncated Freshness value: 4 bits + - MAC: 28 bits - :: +Freshness Value +--------------- - echo "[service_home] - Type = wifi - Name = ssid - Security = wpa - Passphrase = xxxxxxxxxxxxx" \ - > /var/lib/connman/wifi.config +Protects against replay attacks. AUTOSAR recommends a structure for the freshness value, commonly distributed via authenticated PDUs. +.. figure:: ../graphics/automotive/autosar3.png + :alt: Structure of FreshnessValue. Author: AUTOSAR - Restart the connman service: +Sync Message +------------ - :: +Synchronizes the 'Trip Counter' and 'Reset Counter' across all ECUs to maintain a consistent freshness value. - systemctl restart connman.service +- Sync message sent when 'Message Counter' overflows +- Security recommendation: Use broadcast or multicast to prevent DoS attacks +.. figure:: ../graphics/automotive/autosar4.png + :alt: Format of the synchronization message (TripResetSyncMsg). Author: AUTOSAR -Dual-CAN Setup -^^^^^^^^^^^^^^ +SecOC in Scapy +============== -#. | **Device tree setup** - | You'll need to follow this section only if you want to use two CAN - interfaces (DCAN0 and DCAN1). This will disable I2C2 from using pins - P9.19 and P9.20, which are needed by DCAN0. You only need to perform the - steps in this section once. +Scapy supports the dissection, building, verification, and authentication of SecOC messages sent via AUTOSAR PDUs or CANFD packets. The implementation is designed to be vendor-independent and easily customizable, addressing common challenges such as handling freshness values and differentiating between SecOC and non-SecOC PDUs. - | Warning: The configuration in this section will disable BBB capes from - working. Each cape has a small I2C EEPROM that stores info that the BBB - needs to know in order to communicate with the cape. Disable I2C2, and - the BBB has no way to talk to cape EEPROMs. Of course, if you don't use - capes then this is not a problem. +General Implementation Difficulties +----------------------------------- - | Acquire DTS sources that matches your kernel version. Go - `here `__ and switch over to the - branch that represents your kernel version. Download the entire branch - as a ZIP file. Extract it and do the following (version 4.1 shown as an - example): +Implementing SecOC in Scapy involves several challenges: - :: +- **Vendor-Specific Implementations**: Different Original Equipment Manufacturers (OEMs) define their own standards for implementing SecOC, requiring the Scapy implementation to be flexible and adaptable. +- **Freshness Value Tracking**: Freshness values need to be tracked accurately to ensure proper message authentication and to prevent replay attacks. +- **SecOCDataID Management**: The SecOCDataID, which uniquely identifies each PDU, must be known and managed correctly. +- **Mix of SecOC and Non-SecOC PDUs**: SecOC PDUs are mixed with non-SecOC PDUs, and the only difference is their identifier. Proper identification and handling are crucial for correct processing. - # cd ~/src/linux-4.1/arch/arm/boot/dts/include/ - # rm dt-bindings - # ln -s ../../../../../include/dt-bindings - # cd .. - Edit am335x-bone-common.dtsi and ensure the line with "//pinctrl-0 = <&i2c2_pins>;" is commented out. - Remove the complete &ocp section at the end of this file - # mv am335x-boneblack.dts am335x-boneblack.raw.dts - # cpp -nostdinc -I include -undef -x assembler-with-cpp am335x-boneblack.raw.dts > am335x-boneblack.dts - # dtc -W no-unit_address_vs_reg -O dtb -o am335x-boneblack.dtb -b 0 -@ am335x-boneblack.dts - # cp /boot/dtbs/am335x-boneblack.dtb /boot/dtbs/am335x-boneblack.orig.dtb - # cp am335x-boneblack.dtb /boot/dtbs/ - Reboot +Customization +------------- -#. **Overlay setup** - | This section describes how to build the device overlays for the two CAN devices (DCAN0 and DCAN1). You only need to perform the steps in this section once. - | Acquire BBB cape overlays, in one of two ways… +Scapy SecOC Packets provide three stub functions that need to be customized to handle SecOC properly: + +.. code-block:: python + + class My_SecOC_CANFD(SecOC_CANFD): + + def get_secoc_payload(self) -> bytes: + """ + This method retrieves the payload, including the SecOCDataID, + which is used for MAC computation. + """ + secoc_data_id = self.identifier # CANFD identifier + payload = self.pdu_payload + return bytes(secoc_data_id) + bytes(payload) + + def get_secoc_key(self) -> bytes: + """ + This method provides the secret key for the specified SecOCDataID. + """ + secoc_data_id = self.identifier + secoc_key = GLOBAL_KEYS[secoc_data_id] + return secoc_key + + def get_secoc_freshness_value(self) -> bytes: + """ + This method provides the full freshness value required for MAC computation. + """ + freshness_value = trip_count + reset_counter + message_count + self.tfv + return bytes(freshness_value) + +Preparation +----------- - :: +To properly dissect SecOC and non-SecOC AUTOSAR PDUs or CANFD frames, SecOC PDUs need to be registered. This registration informs the dissector whether to use SecOC variants or non-SecOC variants of the packet for dissection. - # apt-get install bb-cape-overlays - https://github.com/beagleboard/bb.org-overlays/ +.. code-block:: python - | Then do the following: + My_SecOC_CANFD.register_secoc_protected_pdu(pdu_id=0x123) + socket = CANSocket("vcan0", fd=True, basecls=My_SecOC_CANFD) - :: +The above code registers the PDU with identifier `0x123` as a SecOC_CANFD packet. All other packets will be interpreted as regular CANFD packets. - # cd ~/src/bb.org-overlays-master/src/arm - # ln -s ../../include - # mv BB-CAN1-00A0.dts BB-CAN1-00A0.raw.dts - # cp BB-CAN1-00A0.raw.dts BB-CAN0-00A0.raw.dts - Edit BB-CAN0-00A0.raw.dts and make relevant to CAN0. Example is shown below. - # cpp -nostdinc -I include -undef -x assembler-with-cpp BB-CAN0-00A0.raw.dts > BB-CAN0-00A0.dts - # cpp -nostdinc -I include -undef -x assembler-with-cpp BB-CAN1-00A0.raw.dts > BB-CAN1-00A0.dts - # dtc -W no-unit_address_vs_reg -O dtb -o BB-CAN0-00A0.dtbo -b 0 -@ BB-CAN0-00A0.dts - # dtc -W no-unit_address_vs_reg -O dtb -o BB-CAN1-00A0.dtbo -b 0 -@ BB-CAN1-00A0.dts - # cp *.dtbo /lib/firmware +Working with SecOC +------------------ +Once you have obtained a SecOC packet from a socket or a PCAP file, you can use the SecOC-related functions to handle authentication and verification. -#. | **CAN0 Example Overlay** - | Inside the DTS folder, create a file with the content of the - following listing. +.. code-block:: python - :: + # Suppose this is our SecOC packet + pkt: My_SecOC_CANFD - cd ~/bb.org-overlays/src/arm - cat < BB-CAN0-00A0.raw.dts - - /* - * Copyright (C) 2015 Robert Nelson - * - * Virtual cape for CAN0 on connector pins P9.19 P9.20 - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - */ - /dts-v1/; - /plugin/; - - #include - #include - - / { - compatible = "ti,beaglebone", "ti,beaglebone-black", "ti,beaglebone-green"; - - /* identification */ - part-number = "BB-CAN0"; - version = "00A0"; - - /* state the resources this cape uses */ - exclusive-use = - /* the pin header uses */ - "P9.19", /* can0_rx */ - "P9.20", /* can0_tx */ - /* the hardware ip uses */ - "dcan0"; - - fragment@0 { - target = <&am33xx_pinmux>; - __overlay__ { - bb_dcan0_pins: pinmux_dcan0_pins { - pinctrl-single,pins = < - BONE_P9_19 (PIN_INPUT_PULLUP | MUX_MODE2) /* uart1_txd.d_can0_rx */ - BONE_P9_20 (PIN_OUTPUT_PULLUP | MUX_MODE2) /* uart1_rxd.d_can0_tx */ - >; - }; - }; - }; - - fragment@1 { - target = <&dcan0>; - __overlay__ { - status = "okay"; - pinctrl-names = "default"; - pinctrl-0 = <&bb_dcan0_pins>; - }; - }; - }; - EOF - - -#. | **Test the Dual-CAN Setup** - | Do the following each time you need CAN, or automate these steps if you like. + # A call to secoc_authenticate will update the truncated freshness value and the truncated MAC of the packet + pkt.secoc_authenticate() - :: + # The truncated freshness value and MAC are now updated + print(pkt.tfv) # Updated truncated freshness value + print(pkt.tmac) # Updated truncated MAC - # echo BB-CAN0 > /sys/devices/platform/bone_capemgr/slots - # echo BB-CAN1 > /sys/devices/platform/bone_capemgr/slots - # modprobe can - # modprobe can-dev - # modprobe can-raw - # ip link set can0 up type can bitrate 50000 - # ip link set can1 up type can bitrate 50000 + # A call to secoc_verify will compute the MAC from the payload of the packet and the local freshness value, + # then compare it with the truncated MAC of the packet. + if pkt.secoc_verify(): + print("Message verified") - Check the output of the Capemanager if both CAN interfaces have been - loaded. - :: - cat /sys/devices/platform/bone_capemgr/slots +Simulating ECUs and Security Functions +======================================= + + +Modeling an ECU as an Automaton +------------------------------- + +To begin, we need to power cycle our simulated ECU by creating a simple automaton with two states: ON and OFF. +Before building the actual ECU automaton, we require a power supply interface. + +Power Supply +------------ + +The power supply object serves as the interface to power cycle our ECU automaton. It enables communication between the +automaton and the power supply to accurately simulate the ECU's power consumption. +For multiprocessing support, file descriptors and multiprocessing Values are used. Here’s how to set it up: + +.. code-block:: python + + import logging + import sys + from multiprocessing import Value, Pipe + from multiprocessing.sharedctypes import Synchronized + + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - 0: PF---- -1 - 1: PF---- -1 - 2: PF---- -1 - 3: PF---- -1 - 4: P-O-L- 0 Override Board Name,00A0,Override Manuf, BB-CAN0 - 5: P-O-L- 1 Override Board Name,00A0,Override Manuf, BB-CAN1 + class AutomatonPowerSupply(): + def __init__(self) -> None: + super().__init__() + self.logger = logging.getLogger("AutomatonPowerSupply") + self.logger.info("Init done") + self.voltage_on: Synchronized[int] = Value("i", 0) + self.current_noise: Synchronized[int] = Value("i", 0) + self.current_on: Synchronized[int] = Value("i", 0) + self.delay_off = 0.001 + self.delay_on = 0.001 + self.read_pipe, self.write_pipe = Pipe() + self.closed = False + def on(self) -> None: + self.logger.debug("ON") + with self.voltage_on.get_lock(): + self.voltage_on.value = 12 + self.write_pipe.send(b"1") - If something went wrong, ``dmesg`` provides kernel messages to analyse the root of failure. + def off(self) -> None: + self.logger.debug("OFF") + with self.voltage_on.get_lock(): + self.voltage_on.value = 0 + self.write_pipe.send(b"0") -#. | **References** + def close(self) -> None: + if self.closed: + return + self.closed = True + self.read_pipe.close() + self.write_pipe.close() - - `embedded-things.com: Enable CANbus on the Beaglebone - Black `__ - - `electronics.stackexchange.com: Beaglebone Black CAN bus - Setup `__ + def reset(self) -> None: + self.off() + time.sleep(self.delay_off) + self.on() + time.sleep(self.delay_on) -#. | **Acknowledgment** - | Thanks to Tom Haramori. Parts of this section are copied from his guide: https://github.com/haramori/rhme3/blob/master/Preparation/BBB_CAN_setup.md +This code establishes the power supply, enabling it to control the power state of the ECU automaton. +The `on`, `off`, and `reset` methods manage state transitions, while `Pipe` and `Value` ensure inter-process +communication and synchronization. This setup guarantees accurate modeling and control of the ECU's power +consumption within a multiprocessing environment. +ECU Automaton +------------- + +Now that we have a power supply, we can start modeling our ECU automaton, which can be turned on and off. + +.. code-block:: python + + from typing import Optional, List, IO, Type, Any + from scapy.automaton import Automaton, ATMT + + class EcuAutomaton(Automaton): + def __init__(self, *args: Any, power_supply: AutomatonPowerSupply, **kargs: Any) -> None: + self.power_supply = power_supply + super().__init__(*args, + external_fd={"power_supply_fd": self.power_supply.read_pipe.fileno()}, + **kargs) + + @ATMT.state(initial=1) # type: ignore + def ECU_OFF(self) -> None: + pass + + @ATMT.state() # type: ignore + def ECU_ON(self) -> None: + pass + + # ====== POWER HANDLING ========== + @ATMT.ioevent(ECU_OFF, name="power_supply_fd") # type: ignore + def event_voltage_changed_on(self, fd: IO[bytes]) -> None: + new_voltage = fd.read(1) + if new_voltage == b"1": + raise self.ECU_ON() + + @ATMT.ioevent(ECU_ON, name="power_supply_fd") # type: ignore + def event_voltage_changed_off(self, fd: IO[bytes]) -> None: + new_voltage = fd.read(1) + if new_voltage == b"0": + raise self.ECU_OFF() + + @ATMT.action(event_voltage_changed_on) # type: ignore + def action_consumption_on(self) -> None: + self.debug(1, "Consuming energy ON") + with self.power_supply.current_on.get_lock(): + self.power_supply.current_on.value = 1 + + @ATMT.action(event_voltage_changed_off) # type: ignore + def action_consumption_off(self) -> None: + self.debug(1, "Consuming energy OFF") + with self.power_supply.current_on.get_lock(): + self.power_supply.current_on.value = 0 + +This code defines an `EcuAutomaton` class that models an ECU with two states: ON and OFF. It uses Scapy's automaton +framework to handle the state transitions based on the power supply's status. The `event_voltage_changed_on` and +`event_voltage_changed_off` methods listen for voltage changes to switch states, while `action_consumption_on` and +`action_consumption_off` manage the power consumption behavior. This setup allows for a robust simulation of an ECU's +power cycling behavior. + +Let's give it a shot: + +.. code-block:: python + + import threading + import time + from scapy.contrib.cansocket import NativeCANSocket + from scapy.error import log_runtime + + ps = AutomatonPowerSupply() + cs = NativeCANSocket("vcan0") + automaton = EcuAutomaton(debug=1, power_supply=ps, sock=cs) + automaton.runbg() + ps.on() + time.sleep(0.1) + print(f"Current consumption {ps.current_on.value}") + ps.off() + time.sleep(0.1) + print(f"Current consumption {ps.current_on.value}") + + automaton.stop() + +This code sets up and tests our ECU automaton. We import the necessary modules and initialize the power supply and +CAN socket. We then create an instance of `EcuAutomaton` with debugging enabled, and run it in the background. + +We power on the ECU and wait a bit to let it stabilize. Then, we print the current consumption, turn off the power, +wait again, and print the current consumption once more. Finally, we stop the automaton. + +By running this code, you should see the current consumption values change as the ECU powers on and off, demonstrating +our automaton in action. + +Simulating UDS +-------------- + +Next up, we want to communicate with our automaton over UDS (Unified Diagnostic Services), aiming to implement +complex state machines like Security Access. Let's start with a simpler example. The following function allows +us to receive and send packets from the automaton's socket, as provided in the `init` function. + +.. code-block:: python + + class EcuAutomaton(Automaton): + + # Existing states and transitions + + @ATMT.receive_condition(ECU_ON) # type: ignore + def on_pkt_on_received_ON(self, pkt: Packet) -> None: + response = None + if pkt: + if response := self.get_default_uds_response(pkt): + self.my_send(response) + + def get_default_uds_response(self, pkt: Packet) -> Optional[Packet]: + service = bytes(pkt)[0] + length = len(pkt) + sub_function = bytes(pkt)[1] if length > 1 else None + match service, length, sub_function: + case 0x10, 2, 1: + return UDS() / UDS_DSCPR(b"\x01") + case 0x3E, 2, 0: + return UDS() / UDS_TPPR() + case 0x3E, 2, 0x80: + return None + case 0x3E, 2, _: + return UDS() / UDS_NR(requestServiceId=service, + negativeResponseCode="subFunctionNotSupported") + case 0x3E, _, _: + return UDS() / UDS_NR(requestServiceId=service, + negativeResponseCode="incorrectMessageLengthOrInvalidFormat") + case 0x27, _, _: + return UDS() / UDS_NR(requestServiceId=service, + negativeResponseCode="incorrectMessageLengthOrInvalidFormat") + case _: + return UDS() / UDS_NR(requestServiceId=service, negativeResponseCode="serviceNotSupported") + +By using Python's match-case operator, we can craft a very elegant UDS answering machine. ECUs are usually precise +with their negative response codes, and modeling this becomes straightforward with the match operator. For instance, +consider the TesterPresent case. If we receive the correct service, length, and sub-function, we respond positively. +If the sub-function is anything else, we fall through to the negative response case "subFunctionNotSupported". If the +length is incorrect, we return "incorrectMessageLengthOrInvalidFormat". Finally, if the service is unknown, the +function returns "serviceNotSupported". This approach allows us to handle UDS communication effectively and implement +the necessary logic for our ECU automaton. + +Full example: + +.. code-block:: python + + from typing import Optional, List, IO, Type, Any + from scapy.packet import Packet + from scapy.automaton import ATMT, Automaton + from scapy.contrib.automotive.uds import * + from scapy.contrib.isotp import * + + class EcuAutomaton(Automaton): + def __init__(self, *args: Any, power_supply: AutomatonPowerSupply, **kargs: Any) -> None: + self.power_supply = power_supply + super().__init__(*args, + external_fd={"power_supply_fd": self.power_supply.read_pipe.fileno()}, + **kargs) + + @ATMT.state(initial=1) # type: ignore + def ECU_OFF(self) -> None: + pass + + @ATMT.state() # type: ignore + def ECU_ON(self) -> None: + pass + + # ====== POWER HANDLING ========== + @ATMT.ioevent(ECU_OFF, name="power_supply_fd") # type: ignore + def event_voltage_changed_on(self, fd: IO[bytes]) -> None: + new_voltage = fd.read(1) + if new_voltage == b"1": + raise self.ECU_ON() + + @ATMT.ioevent(ECU_ON, name="power_supply_fd") # type: ignore + def event_voltage_changed_off(self, fd: IO[bytes]) -> None: + new_voltage = fd.read(1) + if new_voltage == b"0": + raise self.ECU_OFF() + + @ATMT.action(event_voltage_changed_on) # type: ignore + def action_consumption_on(self) -> None: + self.debug(1, "Consuming energy ON") + with self.power_supply.current_on.get_lock(): + self.power_supply.current_on.value = 1 + + @ATMT.action(event_voltage_changed_off) # type: ignore + def action_consumption_off(self) -> None: + self.debug(1, "Consuming energy OFF") + with self.power_supply.current_on.get_lock(): + self.power_supply.current_on.value = 0 + + @ATMT.receive_condition(ECU_ON) # type: ignore + def on_pkt_on_received(self, pkt: Packet) -> None: + if response := self.get_default_uds_response(pkt): + self.my_send(response) + + def get_default_uds_response(self, pkt: Packet) -> Optional[Packet]: + service = bytes(pkt)[0] + length = len(pkt) + sub_function = bytes(pkt)[1] if length else None + match service, length, sub_function: + case 0x10, 2, 1: + return UDS()/UDS_DSCPR(b"\x01") + case 0x3E, 2, 0: + return UDS() / UDS_TPPR() + case 0x3E, 2, 0x80: + return None + case 0x3E, 2, _: + return UDS() / UDS_NR(requestServiceId=service, + + + +Test-Setup Tutorials +==================== ISO-TP Kernel Module Installation --------------------------------- @@ -1300,16 +1915,16 @@ A Linux ISO-TP kernel module can be downloaded from this website: ``README.isotp`` in this repository provides all information and necessary steps for downloading and building this kernel module. The ISO-TP kernel module should also be added to the ``/etc/modules`` file, -to load this module automatically at system boot of the BBB. +to load this module automatically at system boot. CAN-Interface Setup ------------------- -As the final step to prepare the BBB's CAN interfaces for usage, these +As the final step to prepare CAN interfaces for usage, these interfaces have to be set up through some terminal commands. The bitrate can be chosen to fit the bitrate of a CAN bus under test. -:: +How-To:: ip link set can0 up type can bitrate 500000 ip link set can1 up type can bitrate 500000 @@ -1325,7 +1940,7 @@ To build a small test environment in which you can send SOME/IP messages to and #. | **Vsomeip setup** - Download the vsomeip library on the Rapsberry, apply the git patch so it can work with the newer boost libraries and then install it. + Download the vsomeip library on the Raspberry, apply the git patch so it can work with the newer boost libraries and then install it. :: @@ -1347,11 +1962,8 @@ To build a small test environment in which you can send SOME/IP messages to and -Software Setup --------------- - -Cannelloni Framework Installation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Cannelloni Framework +-------------------- The Cannelloni framework is a small application written in C++ to transfer CAN data over UDP. In this way, a researcher can map the CAN @@ -1363,7 +1975,7 @@ explains the installation and usage in detail. Cannelloni needs virtual CAN interfaces on the operator's machine. The next listing shows the setup of virtual CAN interfaces. -:: +How-To:: modprobe vcan diff --git a/doc/scapy/layers/bluetooth.rst b/doc/scapy/layers/bluetooth.rst index b66a55b9146..d1d9add131b 100644 --- a/doc/scapy/layers/bluetooth.rst +++ b/doc/scapy/layers/bluetooth.rst @@ -71,12 +71,28 @@ There are multiple protocols available for Bluetooth through ``AF_BLUETOOTH`` sockets: Host-controller interface (HCI) ``BTPROTO_HCI`` - Scapy class: ``BluetoothHCISocket`` - This is the "base" level interface for communicating with a Bluetooth controller. Everything is built on top of this, and this represents about as close to the physical layer as one can get with regular Bluetooth hardware. + Scapy class: ``BluetoothMonitorSocket`` + + Allows to capture all HCI transactions that are taking place over all HCI + interfaces (including in BlueZ core). It is intended to perform monitoring of + transactions, device attachment and removal, BlueZ logging... + + Scapy class: ``BluetoothUserSocket`` + + This socket interacts with a Bluetooth controller with complete and exclusive + control of de device. This means that BlueZ will not try to take control of + the interface and will not help you manage connections via this interface. + + Scapy class: ``BluetoothHCISocket`` + + Using HCI protocol, this socket interacts with a Bluetooth controller but + does not have exclusive control over it, allowing BlueZ and other + applications to still use the adapter to communicate with devices. + Logical Link Control and Adaptation Layer Protocol (L2CAP) ``BTPROTO_L2CAP`` Scapy class: ``BluetoothL2CAPSocket`` @@ -471,7 +487,7 @@ This example sets up a virtual iBeacon: # Beacon data consists of a UUID, and two 16-bit integers: "major" and # "minor". # - # iBeacon sits ontop of Apple's BLE protocol. + # iBeacon sits on top of Apple's BLE protocol. p = Apple_BLE_Submessage()/IBeacon_Data( uuid='fb0b57a2-8228-44cd-913a-94a122ba1206', major=1, minor=2) @@ -567,9 +583,11 @@ also advertised within their manufacturer-specific data field, including: * AirPods * `Handoff`__ * Nearby + * `Overflow area`__ __ https://en.wikipedia.org/wiki/AirDrop __ https://en.wikipedia.org/wiki/OS_X_Yosemite#Continuity +__ https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393252-startadvertising For compatibility with these other broadcasts, Apple BLE frames in Scapy are layered on top of ``Apple_BLE_Submessage`` and ``Apple_BLE_Frame``: @@ -636,3 +654,26 @@ Results in the output: | | len= 5 | |###[ Raw ]### | | load= '\x03\x18\xc0\xb5%' + + +Using Nordic Semiconductor's nRF Sniffer +======================================== + +Since **Scapy >2.5.0**, Scapy supports `Wireshark's extcap `_ interfaces. +You can therefore use your USB nordic bluetooth dongle, provided that you `have installed `_ the Wireshark module properly. + +.. code:: pycon + + >>> load_contrib("nrf_sniffer") + >>> load_extcap() + >>> conf.ifaces + Source Index Name Address + nrf_sniffer_ble 100 nRF Sniffer for Bluetooth LE /dev/ttyUSB0-None + [...] + >>> sniff(iface="/dev/ttyUSB0-None", prn=lambda x: x.summary()) + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_NONCONN_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_NONCONN_IND + NRFS2_PCAP / NRFS2_Packet / NRF2_Packet_Event / BTLE / BTLE_ADV / BTLE_ADV_IND diff --git a/doc/scapy/layers/dcerpc.rst b/doc/scapy/layers/dcerpc.rst new file mode 100644 index 00000000000..c2e4290756a --- /dev/null +++ b/doc/scapy/layers/dcerpc.rst @@ -0,0 +1,480 @@ +DCE/RPC & [MS-RPCE] +=================== + +.. note:: DCE/RPC per `DCE/RPC 1.1 `_ with the `[MS-RPCE] `_ additions. + +Scapy provides support for dissecting and building Microsoft's Windows DCE/RPC calls. + +Usage documentation +------------------- + +Terminology +~~~~~~~~~~~ + +- ``NDR`` (and ``NDR64``) are the transfer syntax used by DCE/RPC, i.e. how objects are marshalled and sent over the network +- ``IDL`` or Interface Definition Language is "a language for specifying operations (procedures or functions), parameters to these operations, and data types" in context of DCE/RPC + +NDR64 and endianness +~~~~~~~~~~~~~~~~~~~~ + +All packets built with NDR extend the ``NDRPacket`` class, which adds the arguments ``ndr64`` and ``ndrendian``. + +You can therefore specify while dissecting or building packets whether it uses NDR64 or not (**by default: no**), or its endian (**by default: little**) + +.. code:: python + + NetrServerReqChallenge_Request(b"\x00....", ndr64=True, ndrendian="big") + +Dissecting +~~~~~~~~~~ + +You can dissect a DCE/RPC packet like any other packet, by calling ``ThePacketClass()``. The only difference is, as mentioned above, that there are extra ``ndr64`` and ``ndrendian`` arguments. + +.. note:: + DCE/RPC is stateful, and requires the dissector to remember which interface is bound, how (negotiation), etc. + Scapy therefore provides a ``DceRpcSession`` session that remembers the context to properly dissect requests and responses. + +Here's an example where a pcap (included in the ``test/pcaps`` folder) containing a [MS-NRPC] exchange is dissected using Scapy: + +.. code:: python + + >>> load_layer("msrpce") + >>> bind_layers(TCP, DceRpc, sport=40564) # the DCE/RPC port + >>> bind_layers(TCP, DceRpc, dport=40564) + >>> pkts = sniff(offline='dcerpc_msnrpc.pcapng.gz', session=DceRpcSession) + >>> pkts[6][DceRpc5].show() + ###[ DCE/RPC v5 ]### + rpc_vers = 5 (connection-oriented) + rpc_vers_minor= 0 + ptype = request + pfc_flags = PFC_FIRST_FRAG+PFC_LAST_FRAG + endian = little + encoding = ASCII + float = IEEE + reserved1 = 0 + reserved2 = 0 + frag_len = 58 + auth_len = 0 + call_id = 1 + ###[ DCE/RPC v5 - Request ]### + alloc_hint= 0 + cont_id = 0 + opnum = 4 + ###[ NetrServerReqChallenge_Request ]### + PrimaryName= None + \ComputerName\ + |###[ NDRConformantArray ]### + | max_count = 5 + | \value \ + | |###[ NDRVaryingArray ]### + | | offset = 0 + | | actual_count= 5 + | | value = b'WIN1' + \ClientChallenge\ + |###[ PNETLOGON_CREDENTIAL ]### + | data = b'12345678' + + +Scapy has opted to not abstract any of the NDR fields (see `Design choices`_), allowing to keep access to all lengths, offsets, counts, etc... This allows to put wrong length values anywhere to test implementations. + +The catch is that accessing the value of a field is a bit tedious:: + + >>> pkts[6][DceRpc5].ComputerName.value[0].value + b'WIN1' + +Sometimes, you'll be glad to have access to the size of a ConformantArray. Most times, you won't. +All ``NDRPacket`` therefore include a ``valueof()`` function that goes through any array or pointer containers:: + + >>> pkts[6][NetrServerReqChallenge_Request].valueof("ComputerName") + b'WIN1' + +.. warning:: + + Note that ``DceRpc5`` packets are NOT ``NDRPacket``, so you need to call ``valueof()`` on the NDR payload itself. + +Building +~~~~~~~~ + +If you were to re-build the previous packet exactly as it was dissected, it would look something like this: + +.. code:: python + + >>> pkt = NetrServerReqChallenge_Request( + ... ComputerName=NDRConformantArray(max_count=5, value=[ + ... NDRVaryingArray(offset=0, actual_count=5, value=b'WIN1') + ... ]), + ... ClientChallenge=PNETLOGON_CREDENTIAL(data=b'12345678'), + ... PrimaryName=None + ... ) + +If you don't care about specifying ``max_count``, ``offset`` or ``actual_count`` manually, you can however also do the following: + +.. code:: python + + >>> pkt = NetrServerReqChallenge_Request( + ... ComputerName=b'WIN1', + ... ClientChallenge=PNETLOGON_CREDENTIAL(data=b'12345678'), + ... PrimaryName=None + ... ) + >>> pkt.show() + ###[ NetrServerReqChallenge_Request ]### + PrimaryName= None + \ComputerName\ + |###[ NDRConformantArray ]### + | max_count = None + | \value \ + | |###[ NDRVaryingArray ]### + | | offset = 0 + | | actual_count= None + | | value = 'WIN1' + \ClientChallenge\ + |###[ PNETLOGON_CREDENTIAL ]### + | data = '12345678' + + +And Scapy will automatically add the ``NDRConformantArray``, ``NDRVaryingArray``... in the middle. + +This applies to ``NDRPointers`` too ! Skipping it will add a default one with a referent id of ``0x20000``. Take ``RPC_UNICODE_STRING`` for instance: + +.. code:: python + + >>> RPC_UNICODE_STRING(Buffer=b"WIN").show2() + ###[ RPC_UNICODE_STRING ]### + Length = 6 + MaximumLength= 6 + \Buffer \ + |###[ NDRPointer ]### + | referent_id= 0x20000 + | \value \ + | |###[ NDRConformantArray ]### + | | max_count = 3 + | | \value \ + | | |###[ NDRVaryingArray ]### + | | | offset = 0 + | | | actual_count= 3 + | | | value = 'WIN' + + +Client +------ + +Scapy also includes a DCE/RPC client: :class:`~scapy.layers.msrpce.rpcclient.DCERPC_Client`. + +It provides a bunch of basic DCE/RPC features: + +- :func:`~scapy.layers.msrpce.rpcclient.DCERPC_Client.connect`: connect to a host +- :func:`~scapy.layers.msrpce.rpcclient.DCERPC_Client.bind`: bind to a DCE/RPC interface +- :func:`~scapy.layers.msrpce.rpcclient.DCERPC_Client.connect_and_bind`: connect to a host, use the endpoint mapper to find the interface then reconnect to the host on the matching address +- :func:`~scapy.layers.msrpce.rpcclient.DCERPC_Client.sr1_req`: send/receive a DCE/RPC request + +To be able to use an interface, it must have been imported. This makes it so that the :func:`~scapy.layers.dcerpc.register_dcerpc_interface` function is called, allowing the :class:`~scapy.layers.dcerpc.DceRpcSession` session to properly understand the bind/alter requests, and match the DCE/RPCs by opcodes. + +In the DCE/RPC world, there are several "Transports". A transport corresponds to the various ways of transporting DCE/RPC. You can have a look at the documentation over `[MS-RPCE] 2.1 `_. In Scapy, this is implemented in the :class:`~scapy.layers.dcerpc.DCERPC_Transport` enum, that currently contains: + +- :const:`~scapy.layers.dcerpc.DCERPC_Transport.NCACN_IP_TCP`: the interface is reached over IP/TCP, on a port that varies. This port can typically be queried using the endpoint mapper, a DCE/RPC service that is always on port 135. +- :const:`~scapy.layers.dcerpc.DCERPC_Transport.NCACN_NP`: the interface is reached over a named pipe over SMB. This named pipe is typically well-known, or can also be queried using the endpoint mapper (over SMB) on certain cases. + +Here's an example sending a ``ServerAlive`` over the ``IObjectExporter`` interface from `[MS-DCOM] `_. + +.. code-block:: python + + from scapy.layers.dcerpc import * + from scapy.layers.msrpce.all import * + + client = DCERPC_Client( + DCERPC_Transport.NCACN_IP_TCP, + ndr64=False, + ) + client.connect("192.168.0.100") + client.bind(find_dcerpc_interface("IObjectExporter")) + + req = ServerAlive_Request(ndr64=False) + resp = client.sr1_req(req) + resp.show() + +Here's the same example, but this time asking for :const:`~scapy.layers.dcerpc.RPC_C_AUTHN_LEVEL.PKT_PRIVACY` (encryption) using ``NTLMSSP``: + +.. code-block:: python + + from scapy.layers.ntlm import * + from scapy.layers.dcerpc import * + from scapy.layers.msrpce.all import * + + ssp = NTLMSSP( + UPN="Administrator", + PASSWORD="Password1", + ) + client = DCERPC_Client( + DCERPC_Transport.NCACN_IP_TCP, + auth_level=DCE_C_AUTHN_LEVEL.PKT_PRIVACY, + ssp=ssp, + ndr64=False, + ) + client.connect("192.168.0.100") + client.bind(find_dcerpc_interface("IObjectExporter")) + + req = ServerAlive_Request(ndr64=False) + resp = client.sr1_req(req) + resp.show() + +Again, but this time using :const:`~scapy.layers.dcerpc.RPC_C_AUTHN_LEVEL.PKT_INTEGRITY` (signing) using ``SPNEGOSSP[KerberosSSP]``: + +.. code-block:: python + + from scapy.layers.kerberos import * + from scapy.layers.spnego import * + from scapy.layers.dcerpc import * + from scapy.layers.msrpce.all import * + + ssp = SPNEGOSSP( + [ + KerberosSSP( + UPN="Administrator@domain.local", + PASSWORD="Password1", + SPN="host/dc1", + ) + ] + ) + client = DCERPC_Client( + DCERPC_Transport.NCACN_IP_TCP, + auth_level=DCE_C_AUTHN_LEVEL.PKT_INTEGRITY, + ssp=ssp, + ndr64=False, + ) + client.connect("192.168.0.100") + client.bind(find_dcerpc_interface("IObjectExporter")) + + req = ServerAlive_Request(ndr64=False) + resp = client.sr1_req(req) + resp.show() + +Here's a different example, this time connecting over :const:`~scapy.layers.dcerpc.DCERPC_Transport.NCACN_NP` to `[MS-SAMR] `_ to enumerate the domains a server is in: + +.. code-block:: python + + from scapy.layers.ntlm import NTLMSSP, MD4le + from scapy.layers.dcerpc import * + from scapy.layers.msrpce.all import * + + ssp = NTLMSSP( + UPN="User", + HASHNT=MD4le("Password"), + ) + client = DCERPC_Client( + DCERPC_Transport.NCACN_NP, + ssp=ssp, + ndr64=False, + ) + client.connect("192.168.0.100") + client.open_smbpipe("lsass") # open the \pipe\lsass pipe + client.bind(find_dcerpc_interface("samr")) + + # Get Server Handle: call [0] SamrConnect + serverHandle = client.sr1_req(SamrConnect_Request( + DesiredAccess=( + 0x00000010 # SAM_SERVER_ENUMERATE_DOMAINS + ) + )).ServerHandle + + # Enumerate domains: call [6] SamrEnumerateDomainsInSamServer + EnumerationContext = 0 + while True: + resp = client.sr1_req( + SamrEnumerateDomainsInSamServer_Request( + ServerHandle=serverHandle, + EnumerationContext=EnumerationContext, + ) + ) + # note: there are a lot of sub-structures + print(resp.valueof("Buffer").valueof("Buffer")[0].valueof("Name").valueof("Buffer").decode()) + EnumerationContext = resp.EnumerationContext # continue enumeration + if resp.status == 0: # no domain left to enumerate + break + + client.close() + +.. note:: As you can see, we used the :class:`~scapy.layers.ntlm.NTLMSSP` security provider in the above connection. + +There are extensions to the :class:`~scapy.layers.msrpce.rpcclient.DCERPC_Client` class: + +- the :class:`~scapy.layers.msrpce.msnrpc.NetlogonClient`, worth mentioning because it implements its own :class:`~scapy.layers.msrpce.msnrpc.NetlogonSSP`: + +.. code-block:: python + + from scapy.layers.msrpce.msnrpc import * + from scapy.layers.msrpce.raw.ms_nrpc import * + + client = NetlogonClient( + auth_level=DCE_C_AUTHN_LEVEL.PKT_PRIVACY, + computername="SERVER", + domainname="DOMAIN", + ) + client.connect_and_bind("192.168.0.100") + client.negotiate_sessionkey(bytes.fromhex("77777777777777777777777777777777")) + client.close() + +- the :class:`~scapy.layers.msrpce.msdcom.DCOM_Client`. More details are available in `DCOM `_ + +Server +------ + +It is also possible to create your own DCE/RPC server. This takes the form of creating a :class:`~scapy.layers.msrpce.rpcserver.DCERPC_Server` class, then serving it over a transport. + +This class contains a :func:`~scapy.layers.msrpce.rpcserver.DCERPC_Server.answer` function that is used to register a handler for a Request, such as for instance: + +.. code-block:: python + + from scapy.layers.dcerpc import * + from scapy.layers.msrpce.all import * + + class MyRPCServer(DCERPC_Server): + @DCERPC_Server.answer(NetrWkstaGetInfo_Request) + def handle_NetrWkstaGetInfo(self, req): + """ + NetrWkstaGetInfo [MS-SRVS] + "returns information about the configuration of a workstation." + """ + return NetrWkstaGetInfo_Response( + WkstaInfo=NDRUnion( + tag=100, + value=LPWKSTA_INFO_100( + wki100_platform_id=500, # NT + wki100_ver_major=5, + ), + ), + ndr64=self.ndr64, + ) + +Let's spawn this server, listening on the ``12345`` port using the :const:`~scapy.layers.dcerpc.DCERPC_Transport.NCACN_IP_TCP` transport. + +.. code-block:: python + + MyRPCServer.spawn( + DCERPC_Transport.NCACN_IP_TCP, + port=12345, + ) + + +Of course that also works over :const:`~scapy.layers.dcerpc.DCERPC_Transport.NCACN_NP`, with for instance a :class:`~scapy.layers.ntlm.NTLMSSP`: + +.. code-block:: python + + from scapy.layers.ntlm import NTLMSSP, MD4le + ssp = NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password"), + } + ) + + MyRPCServer.spawn( + DCERPC_Transport.NCACN_NP, + ssp=ssp, + iface="eth0", + port=445, + ndr64=True, + ) + + +To start an endpoint mapper (this should be a separate process from your RPC server), you can use the default :class:`~scapy.layers.msrpce.rpcserver.DCERPC_Server` as such: + +.. code-block:: python + + from scapy.layers.dcerpc import * + from scapy.layers.msrpce.all import * + + DCERPC_Server.spawn( + DCERPC_Transport.NCACN_IP_TCP, + iface="eth0", + port=135, + portmap={ + find_dcerpc_interface("wkssvc"): 12345, + }, + ndr64=True, + ) + + +Debugging with extended error information (eerr) +------------------------------------------------ + +To debug a RPC call, you can enable the forwarding of Extended Error Information in ``Computer Configuration > Administrative Templates > System > Remote Procedure Call`` on the remote computer. + +.. image:: ../graphics/dcerpc/debug_eerr.png + :align: center + +Once this is done, load EERR in Scapy (in your script) as such: + +.. code:: python + + from scapy.layers.msrpce.mseerr import * + +To enable parsing of the extended error information. Those information will provide a more in-depth stack trace of errors, if available. + +Passive sniffing +---------------- + +If you're doing passive sniffing of a DCE/RPC session, you can instruct Scapy to still use its DCE/RPC session in order to check the INTEGRITY and decrypt (if PRIVACY is used) the packets. + +.. code-block:: python + + from scapy.all import * + + # Bind DCE/RPC port + bind_bottom_up(TCP, DceRpc5, dport=12345) + bind_bottom_up(TCP, DceRpc5, dport=12345) + + # Enable passive DCE/RPC session + conf.dcerpc_session_enable = True + + # Define SSPs that can be used for decryption / verify + conf.winssps_passive = [ + SPNEGOSSP([ + NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password1!"), + }, + ), + ]) + ] + + # Sniff + pkts = sniff(offline="dcerpc_exchange.pcapng", session=TCPSession) + pkts.show() + + +.. warning:: NTLM, KerberosSSP and SPNEGOSSP are currently supported. NetlogonSSP is still unsupported. + + +Define custom packets +--------------------- + +TODO: Add documentation on how to define NDR packets. + +.. _Design choices: + +Design choices +-------------- + +NDR is a rather complex encoding. For instance, there are multiple types of arrays: + +- fixed arrays +- conformant arrays +- varying arrays +- conformant varying arrays + +All of which have slightly different representations on the network, but generally speaking it can look like this: + +.. image:: ../graphics/dcerpc/ndr_conformant_varying_array.png + :align: center + +Those lengths are mostly computable, but this raises the question of: **what should Scapy report to the user?**. + +Some implementations (like impacket's), have chosen to abstract the lengths, offsets, etc. and hide it to the user. This has the big advantage that it makes packets much easier to build, but has the inconvenience that it is in fact hiding part of the information contained in the packet, which really is against Scapy's philosophy. + +The same happens when encoding pointers, which looks something like this: + +.. image:: ../graphics/dcerpc/ndr_full_pointer.png + :align: center + +where it is tempting to hide the ``referent_id`` part, which is on Windows in most parts irrelevant. + +**In Scapy, you will find all the fields**. The pros are that it is exhaustive and doesn't hide any information, the cons is that you need to use the utils (``valueof()`` on dissection, implicit ``any2i`` on build) in order for it not to be a massive pain. diff --git a/doc/scapy/layers/dcom.rst b/doc/scapy/layers/dcom.rst new file mode 100644 index 00000000000..203270746bf --- /dev/null +++ b/doc/scapy/layers/dcom.rst @@ -0,0 +1,128 @@ +[MS-DCOM] +========= + +DCOM is a mechanism to manipulate COM objects remotely. It is in many ways just an extension over normal DCE/RPC, so understanding DCE/RPC concepts beforehand can be very useful. +Before reading this, have a look at Scapy's `DCE/RPC `_ documentation page. + +Terminology +----------- + +- In DCOM one instantiates 'classes' to get 'object references'. A class implements one or several 'interfaces', each of which has methods. +- ``CLSID``: the UIID of a **class**, used to instantiate it. This is typically chosen by whoever implements the COM object. +- ``IID``: the UIID of an **interface**, used to request an IPID. This is chosen by whoever defines the COM interface (mostly Microsoft). +- ``IPID``: a UIID that uniquely references an **interface on an object**. This allows to tell DCOM on which object to run the request we send. + +There are other IDs such as the OID (a 64bit number that uniquely references each object), and the OXID (a 64bit number that uniquely references each object exporter), but we won't get into the details. + +Per the spec, a DCOM client is supposed to keep track of the IPID, OID and OXID ids. In this regard, Scapy abstracts their usage. +On the other hand, the calling application is supposed to know the ``CLSID`` of the class it wishes to instantiate, and the various ``IID`` of the interfaces it wishes to use. + +General behavior of a DCOM client +--------------------------------- + +1. Setup the DCOM client (endpoint, SSP, etc.) +2. Get an object reference: Instantiate a class to get an object reference of the instance (``RemoteCreateInstance``), **OR**, get an object reference towards the class itself (``RemoteGetClassObject``). +3. Acquire the IPID of an interface of the object. +4. Call a method of that interface. +5. Release the reference counts on the interface (delete the IPID). + +Step 3 can be done manually through the ``AcquireInterface()`` method, but Scapy will also automatically call it if you try to use an interface that you haven't acquired on an object. + +Using the DCOM client +--------------------- + +General usage +~~~~~~~~~~~~~ + +1. Setup the DCOM client and connect to the object resolver (which is by default on port 135). + +.. code:: python + + from scapy.layers.msrpce.msdcom import DCOM_Client + from scapy.layers.ntlm import NTLMSSP + + client = DCOM_Client( + ssp=NTLMSSP(UPN="Administrator@domain.local", PASSWORD="Scapy1111@"), + ) + client.connect("server1.domain.local") + +.. note:: See the examples in `DCE/RPC `_ to connect with SPNEGO/Kerberos. + +2. Instantiate a class to get an object reference + +.. code:: python + + import uuid + from scapy.layers.dcerpc import find_com_interface + from scapy.layers.msrpce.raw.ms_pla import GetDataCollectorSets_Request + + CLSID_TraceSessionCollection = uuid.UUID("03837530-098b-11d8-9414-505054503030") + # The COM interface must have been compiled by scapy-rpc (midl-to-scapy) + IDataCollectorSetCollection = find_com_interface("IDataCollectorSetCollection") + + # Get new object reference + objref = client.RemoteCreateInstance( + # The CLSID we're instantiating + clsid=CLSID_TraceSessionCollection, + iids=[ + # An initial list of interfaces to ask for. There must be at least 1. + IDataCollectorSetCollection, + ] + ) + +3. Call a method on that object reference + +.. code:: python + + result = objref.sr1_req( + # The request message (here from [MS-PLA]) + pkt=GetDataCollectorSets_Request( + server=None, + filter=NDRPointer( + referent_id=0x72657355, + value=FLAGGED_WORD_BLOB( + cBytes=18, + asData=r"session\*".encode("utf-16le"), + ) + ), + ), + # The interface to send it on + iface=IDataCollectorSetCollection, + ) + +4. Release all the requested interfaces on the object reference + +.. code:: python + + objref.release() + +5. Close the client + +.. code:: python + + client.close() + + +Unmarshalling object references +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some methods return a reference to an object that is created by the remote server. On the network, +those are typically marshalled as a ``MInterfacePointer`` structure. Such a structure can be "unmarshalled" into a local object reference that can be used in Scapy to call methods on that object. + +.. code:: python + + # For instance, let's assume we're calling Next() of the IEnumVARIANT + resp = enum.sr1_req( + pkt=Next_Request(celt=1), + iface=IEnumVARIANT, + ) + + # Get the MInterfacePointer value + value = resp.valueof("rgVar")[0].valueof("_varUnion") + assert isinstance(value, MInterfacePointer) + + # Unmarshall it and acquire an initial interface on it. + objref = client.UnmarshallObjectReference( + value, + iid=IDataCollectorSet, + ) diff --git a/doc/scapy/layers/dotnet.rst b/doc/scapy/layers/dotnet.rst new file mode 100644 index 00000000000..fdfbbbe200a --- /dev/null +++ b/doc/scapy/layers/dotnet.rst @@ -0,0 +1,18 @@ +.NET Protocols +============== + +Scapy implements a few .NET specific protocols. Those protocols are a bit uncommon, but it can be useful to try to understand what's sent by .NET applications, or for more offensive purposes (issues with .NET deserialization for instance). + +.NET Remoting +------------- + +Implemented under ``ms_nrtp``, you can load it using:: + + from scapy.layers.ms_nrtp import * + +This supports: + +- The .NET remote protocol: ``NRTP*`` classes +- The .NET Binary Formatter: ``NRBF*`` classes + +For instance you can try to parse a .NET Remoting payload generated using ysoserial with the ``NRBF()`` to analyse what it's doing. diff --git a/doc/scapy/layers/gssapi.rst b/doc/scapy/layers/gssapi.rst new file mode 100644 index 00000000000..6322d2750c5 --- /dev/null +++ b/doc/scapy/layers/gssapi.rst @@ -0,0 +1,158 @@ +GSSAPI +====== + +Scapy provides access to various `Security Providers `_ following the GSSAPI model, but aiming at interacting with the Windows world. + +.. note:: + + The GSSAPI interfaces are based off the following documentations: + + - GSSAPI: `RFC4121 `_ / `RFC2743 `_ + - GSSAPI C bindings: `RFC2744 `_ + +Usage +----- + +.. _ssplist: + +The following SSPs are currently provided: + + - :class:`~scapy.layers.ntlm.NTLMSSP` + - :class:`~scapy.layers.kerberos.KerberosSSP` + - :class:`~scapy.layers.spnego.SPNEGOSSP` + - :class:`~scapy.layers.msrpce.msnrpc.NetlogonSSP` + - :class:`~scapy.arch.windows.sspi.WinSSP` (Windows only) + +Basically those are classes that implement two functions, trying to micmic the RFCs: + +- :func:`~scapy.layers.gssapi.SSP.GSS_Init_sec_context`: called by the client, passing it a ``Context`` and optionally a token +- :func:`~scapy.layers.gssapi.SSP.GSS_Accept_sec_context`: called by the server, passing it a ``Context`` and optionally a token + +They both return the updated Context, a token to optionally send to the server/client and a GSSAPI status code. + +.. note:: + + You can typically use it in :class:`~scapy.layers.smbclient.SMB_Client`, :class:`~scapy.layers.smbserver.SMB_Server`, :class:`~scapy.layers.msrpce.rpcclient.DCERPC_Client` or :class:`~scapy.layers.msrpce.rpcserver.DCERPC_Server`. + Have a look at `SMB `_ and `DCE/RPC `_ to get examples on how to use it. + +Let's implement our own client that uses one of those SSPs. + +Client +~~~~~~ + +.. _ntlm: + +First let's create the SSP. We'll take :class:`~scapy.layers.ntlm.NTLMSSP` as an example but the others would work just as well. + +.. code:: python + + from scapy.layers.ntlm import * + clissp = NTLMSSP( + UPN="Administrator@domain.local", + PASSWORD="Password1!", + ) + +Let's get the first token (in this case, the ntlm negotiate): + +.. code:: python + + # We start with a context = None and a val (server answer) = None + sspcontext, token, status = clissp.GSS_Init_sec_context(None, None) + # sspcontext will be passed to subsequent calls and stores information + # regarding this NTLM session, token is the NTLM_NEGOTIATE and status + # the state of the SSP + assert status == GSS_S_CONTINUE_NEEDED + +Send this token to the server, or use it as required, and get back the server's token. +You can then pass that token as the second parameter of :func:`~scapy.layers.gssapi.SSP.GSS_Init_sec_context`. +To give an example, this is what is done in the LDAP client: + +.. code:: python + + # Do we have a token to send to the server? + while token: + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(b""), + authentication=LDAP_Authentication_SaslCredentials( + mechanism=ASN1_STRING(b"SPNEGO"), + credentials=ASN1_STRING(bytes(token)), + ), + ) + ) + sspcontext, token, status = clissp.GSS_Init_sec_context( + self.sspcontext, GSSAPI_BLOB(resp.protocolOp.serverSaslCreds.val) + ) + +.. _spnego: + +If you want to use :class:`~scapy.layers.spnego.SPEGOSSP`, you could wrap the SSP as so: + +.. code:: python + + from scapy.layers.ntlm import * + from scapy.layers.spnegossp import SPNEGOSSP + clissp = SPNEGOSSP( + [ + NTLMSSP( + UPN="Administrator@domain.local", + PASSWORD="Password1!", + ), + KerberosSSP( + UPN="Administrator@domain.local", + PASSWORD="Password1!", + SPN="host/dc1.domain.local", + ), + ] + ) + +You can override the GSS-API ``req_flags`` when calling :func:`~scapy.layers.gssapi.SSP.GSS_Init_sec_context`, using values from :class:`~scapy.layers.gssapi.GSS_C_FLAGS`: + +.. code:: python + + sspcontext, token, status = clissp.GSS_Init_sec_context(None, None, req_flags=( + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG | + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG | + GSS_C_FLAGS.GSS_C_CONF_FLAG # Asking for CONFIDENTIALITY + )) + + +Server +~~~~~~ + +Implementing a server is very similar to a client but you'd use :func:`~scapy.layers.gssapi.SSP.GSS_Accept_sec_context` instead. +The client is properly authenticated when `status` is `GSS_S_COMPLETE`. + +Let's use :class:`~scapy.layers.ntlm.NTLMSSP` as an example of server-side SSP. + +.. code:: python + + from scapy.layers.ntlm import * + clissp = NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password1!"), + "User2": MD4le("Password2!"), + } + ) + +You'll find other examples of how to instantiate a SSP in the docstrings of each SSP. See `the list <#ssplist>`_ + +WinSSP +~~~~~~ + +WinSSP is a special SSP that is only available on Windows, which calls the actual Windows SSPs local to the machine it's running on. +It allows to use the implicit authentication of the logged-in user with Scapy and its various clients, and is also sometimes necessary if you get unexpected ACCESS_DENIED on loopback connections. + +For instance using SPNEGO: + +.. code:: python + + from scapy.arch.windows.sspi import * + clissp = WinSSP(Package="Negotiate") + +For instance using NTLM: + +.. code:: python + + from scapy.arch.windows.sspi import * + clissp = WinSSP(Package="NTLM") diff --git a/doc/scapy/layers/http.rst b/doc/scapy/layers/http.rst index bdaefa473e3..b1ce44c7a63 100644 --- a/doc/scapy/layers/http.rst +++ b/doc/scapy/layers/http.rst @@ -23,7 +23,7 @@ To summarize, the frames can be split in 3 different ways: - using ``Content-Length``: the header of the HTTP frame announces the total length of the frame - None of the above: the HTTP frame ends when the TCP stream ends / when a TCP push happens. -Moreover, each frame may be aditionnally compressed, depending on the algorithm specified in the HTTP header: +Moreover, each frame may be additionally compressed, depending on the algorithm specified in the HTTP header: - ``compress``: compressed using *LZW* - ``deflate``: compressed using *ZLIB* @@ -84,19 +84,98 @@ All common header fields should be supported. Use Scapy to send/receive HTTP 1.X __________________________________ -To handle this decompression, Scapy uses `Sessions classes <../usage.html#advanced-sniffing-sessions>`_, more specifically the ``TCPSession`` class. -You have several ways of using it: +Scapy uses `Sessions classes <../usage.html#advanced-sniffing-sessions>`_ (more specifically the ``TCPSession`` class), in order to dissect and reconstruct HTTP packets. +This handles Content-Length, chunks and/or compression. -+--------------------------------------------+-------------------------------------------+ -| ``sniff(session=TCPSession, [...])`` | ``TCP_client.tcplink(HTTP, host, 80)`` | -+============================================+===========================================+ -| | Perform decompression / defragmentation | | Acts as a TCP client: handles SYN/ACK, | -| | on all TCP streams simultaneously, but | | and all TCP actions, but only creates | -| | only acts passively. | | one stream. | -+--------------------------------------------+-------------------------------------------+ +Here are the main ways of using HTTP 1.X with Scapy: + +- :class:`~scapy.layers.http.HTTP_Client`: Automata that send HTTP requests. It supports the :func:`~scapy.layers.gssapi.SSP` mechanism to support authorization with NTLM, Kerberos, etc. +- :class:`~scapy.layers.http.HTTP_Server`: Automata to handle incoming HTTP requests. Also supports :func:`~scapy.layers.gssapi.SSP`. +- ``sniff(session=TCPSession, [...])``: Perform decompression / defragmentation on all TCP streams simultaneously, but only acts passively. +- ``TCP_client.tcplink(HTTP, host, 80)``: Acts as a raw TCP client, handles SYN/ACK, and all TCP actions, but only creates one stream. It however supports some specific features, such as changing the source IP. **Examples:** +- :class:`~scapy.layers.http.HTTP_Client`: + +Let's perform a very simple GET request to an HTTP server: + +.. code:: python + + from scapy.layers.http import * # or load_layer("http") + client = HTTP_Client() + resp = client.request("http://127.0.0.1:8080") + client.close() + +You can use the following shorthand to do the same very basic feature: :func:`~scapy.layers.http.http_request`, usable as so: + +.. code:: python + + load_layer("http") + http_request("www.google.com", "/") # first argument is Host, second is Path + +Let's do the same request, but this time to a server that requires NTLM authentication: + +.. code:: python + + from scapy.layers.http import * # or load_layer("http") + client = HTTP_Client( + HTTP_AUTH_MECHS.NTLM, + ssp=NTLMSSP(UPN="user", PASSWORD="password"), + ) + resp = client.request("http://127.0.0.1:8080") + client.close() + +- :class:`~scapy.layers.http.HTTP_Server`: + +Start an unauthenticated HTTP server automaton: + +.. code:: python + + from scapy.layers.http import * + from scapy.layers.ntlm import * + + class Custom_HTTP_Server(HTTP_Server): + def answer(self, pkt): + if pkt.Path == b"/": + return HTTPResponse() / ( + "

OK

" + ) + else: + return HTTPResponse( + Status_Code=b"404", + Reason_Phrase=b"Not Found", + ) / ( + "

404 - Not Found

" + ) + + server = Custom_HTTP_Server.spawn( + port=8080, + iface="eth0", + ) + +We could also have started the same server, but requiring **NTLM authorization using**: + +.. code:: python + + server = HTTP_Server.spawn( + port=8080, + iface="eth0", + mech=HTTP_AUTH_MECHS.NTLM, + ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")}), + ) + +Or **basic auth**: + +.. code:: python + + server = HTTP_Server.spawn( + port=8080, + iface="eth0", + mech=HTTP_AUTH_MECHS.BASIC, + BASIC_IDENTITIES={"user": MD4le("password")}, + ) + - ``TCP_client.tcplink``: Send an HTTPRequest to ``www.secdev.org`` and write the result in a file: @@ -112,26 +191,17 @@ Send an HTTPRequest to ``www.secdev.org`` and write the result in a file: Pragma=b'no-cache' ) a = TCP_client.tcplink(HTTP, "www.secdev.org", 80) - answser = a.sr1(req) + answer = a.sr1(req) a.close() with open("www.secdev.org.html", "wb") as file: - file.write(answser.load) + file.write(answer.load) ``TCP_client.tcplink`` makes it feel like it only received one packet, but in reality it was recombined in ``TCPSession``. If you performed a plain ``sniff()``, you would have seen those packets. -**This code is implemented in a utility function:** ``http_request()``, usable as so: - -.. code:: python - - load_layer("http") - http_request("www.google.com", "/", display=True) - -This will open the webpage in your default browser thanks to ``display=True``. - - ``sniff()``: -Dissect a pcap which contains a JPEG image that was sent over HTTP using chunks. +Dissect a pcap which contains a JPEG image that was sent over HTTP using chunks. This is able to reconstruct all HTTP streams in parallel. .. note:: @@ -149,4 +219,4 @@ Dissect a pcap which contains a JPEG image that was sent over HTTP using chunks. HTTP 2.X -------- -The HTTP 2 documentation is available as a Jupyther notebook over here: `HTTP 2 Tuto `_ \ No newline at end of file +The HTTP 2 documentation is available as a Jupyter notebook over here: `HTTP 2 Tuto `_ diff --git a/doc/scapy/layers/index.rst b/doc/scapy/layers/index.rst index 5508057d578..c6139faf189 100644 --- a/doc/scapy/layers/index.rst +++ b/doc/scapy/layers/index.rst @@ -1,6 +1,10 @@ .. Layer-specific documentation +Layers +====== + .. toctree:: :glob: + :titlesonly: * \ No newline at end of file diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst new file mode 100644 index 00000000000..f27577da5fd --- /dev/null +++ b/doc/scapy/layers/kerberos.rst @@ -0,0 +1,882 @@ +Kerberos +======== + +.. note:: Kerberos per `RFC4120 `_ + `RFC6113 `_ (FAST) + `[MS-KILE] `_ (Windows) + +Scapy's Kerberos implementation is accessed through two main components: + +- :class:`~scapy.modules.ticketer.Ticketer`: a module that allows manipulating Kerberos tickets; +- :class:`~scapy.layers.kerberos.KerberosSSP`: an implementation of a GSSAPI SSP for Kerberos, usable in any of Scapy's client that support GSSAPI, for both authentication and encryption. + +The general idea is that the first one allows to request tickets and perform almost all Kerberos related operations (S4U2Self, S4U2Proxy, FAST armoring, U2U, DMSA, etc.). The latter is used once a final Service Ticket is obtained, by other parts of Scapy, for instance `SMB `_, `LDAP `_ or `DCE/RPC `_. + +Ticketer module +~~~~~~~~~~~~~~~ + +The :class:`~scapy.modules.ticketer.Ticketer` module can be used both from the CLI or programmatically to perform operations on Kerberos tickets. To use it, you must first create an instance of a :class:`~scapy.modules.ticketer.Ticketer`, which acts as both a **ccache** (holds tickets) and a **keytab** (holds secrets). + +This section tries to give many usage examples, but isn't exhaustive. For more details regarding the parameters of each functions, it is encouraged to have a look at the docstrings of :class:`~scapy.layers.kerberos.KerberosClient`. + +- **Request TGT**: see the docstring of :func:`~scapy.layers.kerberos.krb_as_req` + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("Administrator@DOMAIN.LOCAL") + Enter password: ************ + >>> t.show() + Tickets: + 0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + Start time End time Renew until Auth time + 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 + + +- **Then request a ST, using the TGT**: see the docstring of :func:`~scapy.layers.kerberos.krb_tgs_req` + +.. code:: pycon + + >>> # The TGT we just got has an ID of 0 + >>> t.request_st(0, "host/dc1.domain.local") + >>> t.show() + Tickets: + 0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + Start time End time Renew until Auth time + 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 + + 1. Administrator@DOMAIN.LOCAL -> host/dc1.domain.local@DOMAIN.LOCAL + Start time End time Renew until Auth time + 31/08/23 11:39:07 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 + + +- **Use ticket as SSP**: the :func:`~scapy.modules.ticketer.Ticketer.ssp` function. + +.. code:: pycon + + >>> # We use ticket 1 from the above store. + >>> smbclient("dc1.domain.local", ssp=t.ssp(1)) + +- **Request a TGT using a hash**: see the docstring of :func:`~scapy.layers.kerberos.krb_as_req` + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import EncryptionType + >>> load_module("ticketer") + >>> t = Ticketer() + >>> # Using the HashNT + >>> t.request_tgt("Administrator@DOMAIN.LOCAL", key=Key(EncryptionType.RC4_HMAC, bytes.fromhex("2b576acbe6bcfda7294d6bd18041b8fe"))) + >>> # Using the AES-256-SHA1-96 Kerberos Key + >>> t.request_tgt("Administrator@domain.local", key=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, bytes.fromhex("63a2577d8bf6abeba0847cded36b9aed202c23750eb9c56b6155be1cc946bb1d"))) + +- **Request a TGT using PKINIT**: + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import EncryptionType + >>> load_module("ticketer") + >>> t = Ticketer() + >>> # If P12: + >>> t.request_tgt(p12="admin.pfx", realm="DOMAIN.LOCAL", ca="ca.pem") + >>> # One could also have used a different cert and key file: + >>> t.request_tgt(x509="admin.cert", x509key="admin.key", realm="DOMAIN.LOCAL", ca="ca.pem") + +- **Request a user TGT with Kerberos armoring (FAST)** + +The ``armor_with`` keyword allows to select a ticket to armor the request with. + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("Machine01$@DOMAIN.LOCAL", key=Key(EncryptionType.RC4_HMAC, bytes.fromhex("2b576acbe6bcfda7294d6bd18041b8fe"))) + >>> t.show() + Tickets: + 0. Machine01$@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + Start time End time Renew until Auth time + 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 + >>> t.request_tgt("Administrator@domain.local", armor_with=0) # Armor with ticket n°0 + +- **Renew a TGT or ST**: + +.. code:: + + >>> t.renew(0) # renew TGT + >>> t.renew(1) # renew ST. Works only with 'host/' SPNs + >>> t.renew(1, armor_with=0) # renew something with armoring + +- **Import tickets from a ccache**: + +.. note:: We first added a realm ``DOMAIN.LOCAL`` with a kdc to ``/etc/krb5.conf`` + +.. code:: pycon + + $ kinit Administrator@DOMAIN.LOCAL + Password for Administrator@DOMAIN.LOCAL: + $ scapy + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.open_ccache("/tmp/krb5cc_1000") + >>> t.show() + Tickets: + 1. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + Start time End time Renew until Auth time + 31/08/23 12:08:15 31/08/23 22:08:15 01/09/23 12:08:12 31/08/23 12:08:15 + +- **Export tickets into a ccache**: + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("Administrator@domain.local", password="ScapyScapy1") + >>> t.save_ccache("/tmp/krb5cc_1000") + >>> exit() + $ klist + Ticket cache: FILE:/tmp/krb5cc_1000 + Default principal: Administrator@DOMAIN.LOCAL + + Valid starting Expires Service principal + 08/31/2023 12:08:15 08/31/2023 23:08:15 krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + renew until 09/01/2023 12:08:12 + +- **Perform S4U2Self** + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("SERVER1$@domain.local", key=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, bytes.fromhex("63a2577d8bf6abeba0847cded36b9aed202c23750eb9c56b6155be1cc946bb1d"))) + >>> t.request_st(0, "host/SERVER1", for_user="Administrator@domain.local") + >>> t.show() + CCache tickets: + 0. SERVER1$@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + canonicalize+pre-authent+initial+renewable+forwardable + Start time End time Renew until Auth time + 15/04/25 20:15:17 16/04/25 06:10:22 16/04/25 06:10:22 15/04/25 20:15:17 + + 1. Administrator@domain.local -> host/SERVER1@DOMAIN.LOCAL + canonicalize+pre-authent+renewable+forwardable + Start time End time Renew until Auth time + 15/04/25 20:15:20 16/04/25 06:10:22 16/04/25 06:10:22 15/04/25 20:15:17 + +- **Request a ticket for a DMSA** + +For more information about DMSAs and how to create them, consult the `relevant Microsoft documentation `_. In this example we allowed ``SERVER1$`` to retrieve the managed password of ``dmsa_user$``. + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("SERVER1$@domain.local", key=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, bytes.fromhex("63a2577d8bf6abeba0847cded36b9aed202c23750eb9c56b6155be1cc946bb1d"))) + >>> t.request_st(0, "krbtgt/domain.local", for_user="dmsa_user$@domain.local", dmsa=True) + INFO: 3 DMSA keys found and imported ! + >>> t.show() + Keytab name: UNSAVED + Principal Timestamp KVNO Keytype + dmsa_user$@domain.local 22/05/25 22:03:58 1 AES256-CTS-HMAC-SHA1-96 + dmsa_user$@domain.local 22/05/25 22:03:58 2 AES128-CTS-HMAC-SHA1-96 + dmsa_user$@domain.local 22/05/25 22:03:58 3 RC4-HMAC + + CCache tickets: + 0. SERVER1$@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + canonicalize+pre-authent+initial+renewable+forwardable + Start time End time Renew until Auth time + 22/05/25 22:06:32 23/05/25 08:03:53 23/05/25 08:03:53 22/05/25 22:06:32 + + 1. dmsa_user$@domain.local -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + canonicalize+pre-authent+renewable+forwardable + Start time End time Renew until Auth time + 22/05/25 22:06:37 23/05/25 08:03:53 23/05/25 08:03:53 22/05/25 22:06:32 + +As you can see, DMSA keys were imported in the keytab. You can use those as detailed in some of the following sections. + +- **Load and use keytab for client** + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.open_keytab("test.keytab") + >>> t.show() + Keytab name: test.keytab + Principal Timestamp KVNO Keytype + Administrator@domain.local 14/04/25 21:47:59 0 AES128-CTS-HMAC-SHA1-96 + Administrator@domain.local 14/04/25 21:47:59 0 AES256-CTS-HMAC-SHA1-96 + Administrator@domain.local 14/04/25 21:47:59 0 RC4-HMAC + + No tickets in CCache. + >>> t.request_tgt("Administrator@domain.local") + +- **Load and use keytab for server:** + +.. code:: pycon + + >>> t.open_keytab("server1.keytab") + >>> t.show() + Keytab name: server1.keytab + Principal Timestamp KVNO Keytype + host/Server1.domain.local@DOMAIN.LOCAL 01/01/70 01:00:00 10 RC4-HMAC + + No tickets in CCache. + >>> ssp = t.ssp("host/Server11.domain.local@DOMAIN.LOCAL") + >>> # Example: start a SMB server + >>> smbserver(ssp=ssp) + +- **Create client keytab:** + +.. code:: pycon + + >>> t = Ticketer() + >>> t.add_cred("Administrator@domain.local", etypes="all") + Enter password: ************ + >>> t.show() + Keytab name: UNSAVED + Principal Timestamp KVNO Keytype + Administrator@domain.local 15/04/25 20:24:13 1 AES128-CTS-HMAC-SHA1-96 + Administrator@domain.local 15/04/25 20:24:13 2 AES256-CTS-HMAC-SHA1-96 + Administrator@domain.local 15/04/25 20:24:13 3 RC4-HMAC + + No tickets in CCache. + +- **Create server keytab:** + +The following is equivalent to Windows' ``ktpass.exe /out kt.keytab /mapuser WKS02$@domain.local /princ host/WKS02.domain.local@domain.local /pass ScapyIsNice``. + +.. code:: pycon + + >>> t = Ticketer() + >>> t.add_cred("host/WKS02.domain.local@domain.local", etypes="all", mapupn="WKS02$@domain.local", password="ScapyIsNice") + Enter password: ************ + >>> t.show() + Keytab name: UNSAVED + Principal Timestamp KVNO Keytype + host/WKS02$.domain.local@domain.local 25/02/26 15:40:27 1 AES256-CTS-HMAC-SHA1-96 + + No tickets in CCache. + >>> t.save_keytab("kt.keytab") + +- **Change password using kpasswd in 'set' mode:** + +.. code:: pycon + + >>> t = Ticketer() + >>> t.request_tgt("Administrator@domain.local") + Enter password: ************ + >>> t.kpasswdset(0, "SERVER1$@domain.local") + INFO: Using 'Set Password' mode. This only works with admin privileges. + Enter NEW password: *********** + +- **Craft tickets**: We can start by showing how to craft a **golden ticket**: + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.create_ticket() + User [User]: Administrator + Domain [DOM.LOCAL]: DOMAIN.LOCAL + Domain SID [S-1-5-21-1-2-3]: S-1-5-21-4239584752-1119503303-314831486 + Group IDs [513, 512, 520, 518, 519]: 512, 520, 513, 519, 518 + User ID [500]: 500 + Primary Group ID [513]: + Extra SIDs [] :S-1-18-1 + Expires in (h) [10]: + What key should we use (AES128-CTS-HMAC-SHA1-96/AES256-CTS-HMAC-SHA1-96/RC4-HMAC) ? [AES256-CTS-HMAC-SHA1-96]: + Enter the NT hash (AES-256) for this ticket (as hex): 6df5a9a90cb076f4d232a123d9c24f46ae11590a5430710bc1881dca337989ce + >>> t.show() + Tickets: + 0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + >>> t.save_ccache(fname="blob.ccache") + +- **Edit tickets with the GUI** + +Let's assume you've acquired the KRBTGT of a KDC, plus you've used ``kinit`` to get a ticket. +This ticket was saved to a ``.ccache`` file, that we'll know try to open. + +.. note:: + + You can get the demo ccache file using the following + + .. code:: + + cat < krb.ccache + BQQADAABAAj/////AAAAAAAAAAEAAAABAAAADERPTUFJTi5MT0NBTAAAAA1BZG1pbmlzdHJhdG9y + AAAAAQAAAAEAAAAMRE9NQUlOLkxPQ0FMAAAADUFkbWluaXN0cmF0b3IAAAACAAAAAgAAAAxET01B + SU4uTE9DQUwAAAAGa3JidGd0AAAADERPTUFJTi5MT0NBTAASAAAAIItCJqGQhmy+NFrl5miCPt1T + WcsAvUeaZCi8j+sbpVdSYzMy+mMzMvpjM7+aYzSEdwBQ4QAAAAAAAAAAAAAAAARIYYIERDCCBECg + AwIBBaEOGwxET01BSU4uTE9DQUyiITAfoAMCAQKhGDAWGwZrcmJ0Z3QbDERPTUFJTi5MT0NBTKOC + BAQwggQAoAMCARKhAwIBAqKCA/IEggPuZiwq78yj+MeN444a8dY7GN4BHYZNm+wS88EeILC73Ebm + 9cgxGzMbHMJ7Ixk+kPpHunqmpn+6WCah9HVOpQUO6rLgfQej7BApsqEeBYzjHkj03ivOAX6cKRXu + QP+g9xCVlwiChvopD+bKd3RlFixXV6Z8xTqOMgSEakypz/MMgHPR6ec1tesicX+Xd8Lzj7E9IElS + 2xXk8WDiZTX1lvPOZPmo2WARcY0EBWUNf3xyj4fdLQ4iDkYQNH+qikUJm2OjUfWtz8z2adm2ES4x + iBr4aVYSlKIetuKxZLjObGx7AyfsbHHCN4SwbBkDCj+BEZ83fLbwOVtUd7/7xcGiJk7Er3b0s5pO + L3Aw1IyOu8ryEgNuoKWr3V2pH83D+5cA1TefA/vJ/jpHB42uMLBaQY9G7p6iX1IOt+Z7U9lvf0hu + WHiyLqj0IVE3p9z39Lb1BGNxXZ08VE8pRCDtD3QmlV+gpSfvzoYmT3wpvfws7iw+sifrS3ZR64AI + 4OsmlEakVIgpawQn+CuVmtBwFGzYqa7Z7yNoFb0hSfP4bXMidYTylNyGz0p35O6r+Y9PNC2/xL60 + bYNLDDED2MWWTK1IUu7TZcqOUJN+IZdhItXN4Yxatt1VKMOmgMCiGXEXZt1bajwQOuZa1fVzoxVD + oOvO/eF0kGKVEDD2OQfN4JIBDCLJB2MkjJ9s0DpvCny5p7dEG8feTEDB10k3Ov7ll6Usnb51M9e6 + JKOibfKUdLk2Q+7Zf2uP/ROXaGmESEG902TyRU1uPOGuZ37AHFksJbUOEgMDJA3arILfqdY7HELC + ObeKbE67orZFi5JJMcUrIjucnP1s8PCD5iOeMHR/EwLei96U/odWteARj17WHczDhi3byT8QPDFg + rBWFjL4zBCDW4H4snyQsLK+PBNg/PNcfQEwdVoFMniqnh3Y6vClTNCmUh/RU5LTrXw58PPXjdzdK + z4J8n+JV4cfNsTEp7wfHMRZO5O7VA/c1gpqLfMLjcY2yPYWDj796Q4YaHI+JDkwzQ3tldJlGtG9s + /xdnFY9WhLA18uoIb3tWT2pXBQcUtMrVFltyvm96aCCy6fiTZQYUfmSnei+c+cE/5P1ZuDGRiYEB + BooAPm9/kYAGYWIE/0sYqb9JVJe6DfDfy7iaXmQ8YGN2ZzV/zx2XtCQkDqdfzw0muxWQVRB/gNG8 + aCyQV/IqPvX7D1CtswupdbJQadOTv36yUi8jCRKsHmS7qTyRqnYKuxIJuxMT443d68rDJdJ775nW + YEXAl5m3ECCkT2S7tZxAVEkwT9lbjWvcbRfkdsuhiPMK0Eu2yR2RsCiwlTmGkpqftCsh9zAoyLof + QWxwYwAAAAAAAAABAAAAAQAAAAxET01BSU4uTE9DQUwAAAANQWRtaW5pc3RyYXRvcgAAAAAAAAAD + AAAADFgtQ0FDSEVDT05GOgAAABVrcmI1X2NjYWNoZV9jb25mX2RhdGEAAAAHcGFfdHlwZQAAACBr + cmJ0Z3QvRE9NQUlOLkxPQ0FMQERPTUFJTi5MT0NBTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAATIAAAAA + EOF + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.open_ccache("krb.ccache") + >>> t.show() + Tickets: + 1. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + >>> t.edit_ticket(0) + Enter the NT hash (AES-256) for this ticket (as hex): 6df5a9a90cb076f4d232a123d9c24f46ae11590a5430710bc1881dca337989ce + >>> t.resign_ticket(0) + >>> t.save_ccache() + 1660 + >>> # Other stuff you can do + >>> tkt = t.dec_ticket(0) + >>> tkt + + >>> t.update_ticket(0, tkt) + +.. figure:: ../graphics/kerberos/ticketer.png + :align: center + + +.. note:: Remember to call ``resign_ticket`` to update the Server and KDC checksums in the PAC. + + +Cheat sheet +----------- + ++---------------------------------------+--------------------------------+ +| Command | Description | ++=======================================+================================+ +| ``load_module("ticketer")`` | Load ticketer++ | ++---------------------------------------+--------------------------------+ +| ``t = Ticketer()`` | Create a Ticketer object | ++---------------------------------------+--------------------------------+ +| ``t.open_ccache("/tmp/krb5cc_1000")`` | Open a ccache file | ++---------------------------------------+--------------------------------+ +| ``t.save_ccache()`` | Save a ccache file | ++---------------------------------------+--------------------------------+ +| ``t.show()`` | List the tickets | ++---------------------------------------+--------------------------------+ +| ``t.create_ticket()`` | Forge a ticket | ++---------------------------------------+--------------------------------+ +| ``dTkt = t.dec_ticket()`` | Decipher a ticket | ++---------------------------------------+--------------------------------+ +| ``t.update_ticket(, dTkt)`` | Re-inject a deciphered ticket | ++---------------------------------------+--------------------------------+ +| ``t.edit_ticket()`` | Edit a ticket (GUI) | ++---------------------------------------+--------------------------------+ +| ``t.resign_ticket()`` | Resign a ticket | ++---------------------------------------+--------------------------------+ +| ``t.request_tgt(upn, [...])`` | Request a TGT | ++---------------------------------------+--------------------------------+ +| ``t.request_st(i, spn, [...])`` | Request a ST using ticket i | ++---------------------------------------+--------------------------------+ +| ``t.renew(i, [...])`` | Renew a TGT/ST | ++---------------------------------------+--------------------------------+ +| ``t.remove_krb(i)`` | Remove a TGT/ST | ++---------------------------------------+--------------------------------+ +| ``t.set_primary(i)`` | Set the primary ticket | ++---------------------------------------+--------------------------------+ + +Other useful commands +--------------------- + +To change your own password, you can use the plain ``kpasswd`` command from ``scapy.layers.kerberos``. + +.. code:: pycon + + >>> kpasswd("User1@domain.local") + Enter password: ********** + Enter NEW password: ********* + +To change the password of someone else, you can also the following. You need to have the rights to do so. You can also use the method from Scapy's Ticketer. + +.. code:: pycon + + >>> kpasswd("Administrator@domain.local", "User1@domain.local") + Enter password: ********** + Enter NEW password: ********* + +Inner-workings +~~~~~~~~~~~~~~ + +Behind the scenes, Scapy includes a (tiny) kerberos client, that has basic functionalities such as: + +AS-REQ +------ + +.. note:: Full doc at :func:`~scapy.layers.kerberos.krb_as_req`. ``krb_as_req`` actually calls a Scapy automaton that has the following behavior: + + .. image:: ../graphics/kerberos/kerberos_atmt.png + :align: center + +.. code:: pycon + + >>> res = krb_as_req("user1@DOMAIN.LOCAL", password="Password1") + +This is what it looks like with wireshark: + +.. image:: ../graphics/kerberos/wireshark_asreq.png + :align: center + +The result is a named tuple with both the full AP-REP and the decrypted session key: + +.. code:: pycon + + >>> res.asrep.show() + ###[ KRB_AS_REP ]### + pvno = 0x5 + msgType = 'AS-REP' 0xb + \padata \ + |###[ PADATA ]### + | padataType= 'PA-ETYPE-INFO2' 0x13 + | \padataValue\ + | |###[ ETYPE_INFO2 ]### + | | \seq \ + | | |###[ ETYPE_INFO_ENTRY2 ]### + | | | etype = 'AES-256' 0x12 + | | | salt = + | | | s2kparams = None + crealm = + [...] + >>> res.sessionkey.toKey() + + +Some more examples: + +**Enforce RC4:** + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import EncryptionType + >>> res = krb_as_req("user1@DOMAIN.LOCAL", etypes=[EncryptionType.RC4_HMAC]) + +**Ask for a DES_CBC_MD5 sessionkey:** + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import EncryptionType + >>> res = krb_as_req("user1@DOMAIN.LOCAL", etypes=[EncryptionType.DES_CBC_MD5, EncryptionType.RC4_HMAC]) + +TGS-REQ +------- + +.. note:: Full doc at :func:`~scapy.layers.kerberos.krb_tgs_req`. ``krb_tgs_req`` actually calls a Scapy automaton. + +**Ask for a ST:** + +Let's reuse the TGT and session key we got in the AS-REQ: + +.. code:: pycon + + >>> krb_tgs_req("user1@DOMAIN.LOCAL", "host/DC1", sessionkey=res.sessionkey, ticket=res.asrep.ticket) + +.. note:: + + There is also a :func:`~scapy.layers.kerberos.krb_as_and_tgs` function that does an AS-REQ then a TGS-REQ:: + + >>> krb_as_and_tgs("user1@DOMAIN.LOCAL", "host/DC1", password="Password1") + +Other things you can do: + +**Renew a TGT:** + +.. code:: pycon + + >>> krb_tgs_req("user1@DOMAIN.LOCAL", "krbtgt/DOMAIN.LOCAL", sessionkey=res.sessionkey, ticket=res.asrep.ticket, renew=True) + +**Renew a ST:** + +.. note:: For some mysterious reason, this is rarely implemented in other tools. + +.. code:: pycon + + >>> res2 = krb_tgs_req("user1@DOMAIN.LOCAL", "host/DC1", sessionkey=res.sessionkey, ticket=res.asrep.ticket) + >>> krb_tgs_req("user1@DOMAIN.LOCAL", "host/DC1", sessionkey=res2.sessionkey, ticket=res2.tgsrep.ticket, renew=True) + + +KerberosSSP +~~~~~~~~~~~ + +For Kerberos, the Scapy SSP is implemented in :class:`~scapy.layers.kerberos.KerberosSSP`. +You can typically use it in :class:`~scapy.layers.smbclient.SMB_Client`, :class:`~scapy.layers.smbserver.SMB_Server`, :class:`~scapy.layers.msrpce.rpcclient.DCERPC_Client` or :class:`~scapy.layers.msrpce.rpcserver.DCERPC_Server`. + +.. note:: Remember that you can wrap it in a :class:`~scapy.layers.spnego.SPNEGOSSP` + +See `GSSAPI `_ for usage examples. + +Decrypt kerberos packets manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: This section is useful to understand the inner workings of Kerberos, but isn't necessary to use Scapy's implementation. + +Kerberos packets contain encrypted content, let's take the following packet: + +.. code:: pycon + + >>> pkt = Ether(b"RT\x00iX\x13RT\x00!l+\x08\x00E\x00\x01]\xa7\x18@\x00\x80\x06\xdc\x83\xc0\xa8z\x9c\xc0\xa8z\x11\xc2\t\x00XT\xf6\xab#\x92\xc2[\xd6P\x18 \x14\xb6\xe0\x00\x00\x00\x00\x011j\x82\x01-0\x82\x01)\xa1\x03\x02\x01\x05\xa2\x03\x02\x01\n\xa3c0a0L\xa1\x03\x02\x01\x02\xa2E\x04C0A\xa0\x03\x02\x01\x12\xa2:\x048HHM\xec\xb0\x1c\x9bb\xa1\xca\xbf\xbc?-\x1e\xd8Z\xa5\xe0\x93\xba\x83X\xa8\xce\xa3MC\x93\xaf\x93\xbf!\x1e'O\xa5\x8e\x81Hx\xdb\x9f\rz(\xd9Ns'f\r\xb4\xf3pK0\x11\xa1\x04\x02\x02\x00\x80\xa2\t\x04\x070\x05\xa0\x03\x01\x01\xff\xa4\x81\xb70\x81\xb4\xa0\x07\x03\x05\x00@\x81\x00\x10\xa1\x120\x10\xa0\x03\x02\x01\x01\xa1\t0\x07\x1b\x05win1$\xa2\x0e\x1b\x0cDOMAIN.LOCAL\xa3!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cDOMAIN.LOCAL\xa5\x11\x18\x0f20370913024805Z\xa6\x11\x18\x0f20370913024805Z\xa7\x06\x02\x04p\x1c\xc5\xd1\xa8\x150\x13\x02\x01\x12\x02\x01\x11\x02\x01\x17\x02\x01\x18\x02\x02\xffy\x02\x01\x03\xa9\x1d0\x1b0\x19\xa0\x03\x02\x01\x14\xa1\x12\x04\x10WIN1 ") + >>> pkt[TCP].payload.show() + ###[ KerberosTCPHeader ]### + len = 305 + ###[ Kerberos ]### + \root \ + |###[ KRB_AS_REQ ]### + | pvno = 0x5 + | msgType = 'AS-REQ' 0xa + | \padata \ + | |###[ PADATA ]### + | | padataType= 'PA-ENC-TIMESTAMP' 0x2 + | | \padataValue\ + | | |###[ EncryptedData ]### + | | | etype = 'AES-256' 0x12 + | | | kvno = None + | | | cipher = + | |###[ PADATA ]### + | | padataType= 'PA-PAC-REQUEST' 0x80 + | | \padataValue\ + | | |###[ PA_PAC_REQUEST ]### + | | | includePac= True + | \reqBody \ + | |###[ KRB_KDC_REQ_BODY ]### + | | kdcOptions= forwardable, renewable, canonicalize, renewable-ok + | | \cname \ + | | |###[ PrincipalName ]### + | | | nameType = 'NT-PRINCIPAL' 0x1 + | | | nameString= [] + | | realm = + | | \sname \ + | | |###[ PrincipalName ]### + | | | nameType = 'NT-SRV-INST' 0x2 + | | | nameString= [, ] + | | from = None + | | till = 2037-09-13 02:48:05 UTC + | | rtime = 2037-09-13 02:48:05 UTC + | | nonce = 0x701cc5d1 + | | etype = [0x12 , 0x11 , 0x17 , 0x18 , -0x87 , 0x3 ] + | | \addresses \ + | | |###[ HostAddress ]### + | | | addrType = 'NetBios' 0x14 + | | | address = + | | encAuthorizationData= None + | | additionalTickets= None + +You likely want to decrypt ``pkt.root.padata[0].padataValue`` which is an :class:`~scapy.layers.kerberos.EncryptedData` packet. To do so, we need the :class:`~scapy.libs.rfc3961.Key` class. + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import Key, EncryptionType + >>> enc = pkt[Kerberos].root.padata[0].padataValue + >>> k = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex("7fada4e566ae4fb270e2800a23ae87127a819d42e69b5e22de0ddc63da80096d")) + +The first parameter of the :class:`~scapy.libs.rfc3961.Key` constructor is a value from :class:`~scapy.libs.rfc3961.EncryptionType`, in this case ``EncryptionType.AES256_CTS_HMAC_SHA1_96``. This is the same value than ``enc.etype.val``, which allows to know which key to use. + +We can then proceed to perform the decryption: + +.. code:: pycon + + >>> enc.decrypt(k) + pausec=0x9a4db |> + +Compute Kerberos keys +~~~~~~~~~~~~~~~~~~~~~ + +.. note:: Encryption for Kerberos 5 is defined in `RFC3961 `_ + +You may want to compute a Kerberos key from a password + salt. There is an API for that described in RFC3961 as "string-to-key". Our implementation is a class method as follow: + +.. function:: Key.string_to_key(etype, string, salt, params=None) + + Compute the kerberos key for a certain encryption type. + + :param int etype: The EncryptionType to use. May be any value from :class:`~scapy.libs.rfc3961.EncryptionType` + :param bytes string: The "string" bytes to use. This is the user password in almost all well-used cases. They must be passed as bytes. + :param bytes salt: The salt bytes to use. What value to use depends if you are considering a MACHINE account or a USER account, for the latter, it's just ``the concatenation of the principal's realm and name components, in order, with no separators.`` (RFC4120 sect 4) + :param bytes params: The opaque "parameter" used by string-to-key. The RFC defines this field in a very general manner but it is basically only used in AES, in which it is the iteration count as a big-endian int (``struct.pack(">L", 4096)`` by default) + +Let's run a few examples: + +.. code:: pycon + + >>> # Get the AES256 key for User1@DOMAIN.LOCAL with "Password1" + >>> from scapy.libs.rfc3961 import Key, EncryptionType + >>> Key.string_to_key(EncryptionType.AES256_CTS_HMAC_SHA1_96, b"Password1", b"DOMAIN.LOCALUser1") + >>> print(_.key) + b'm\x07H\xc5F\xf4\xe9\x92\x05\xe7\x8f\x8d\xa7h\x1dN\xc5R\n\xe4\x81UCr\x0c*d|\x1a\xe8\x14\xc9' + +.. note:: The following example is from https://datatracker.ietf.org/doc/html/rfc3962#appendix-B + +.. code:: pycon + + >>> # Get the AES128 key for raeburn@ATHENA.MIT.EDU with "password", with an iteration count of 1200 + >>> k = Key.string_to_key(EncryptionType.AES128_CTS_HMAC_SHA1_96, b"password", b"ATHENA.MIT.EDUraeburn", struct.pack(">L", 1200)) + >>> print(k.key.hex()) + '4c01cd46d632d01e6dbe230a01ed642a' + + +Decrypt FAST manually +~~~~~~~~~~~~~~~~~~~~~ + +.. note:: This section is useful to understand the inner workings of Kerberos FAST, but FAST can simply be used in :class:`~scapy.modules.ticketer.Ticketer` through the ``armor_with`` parameter when performing either a ASREQ or TGSREQ. For more details related to how FAST works, have a look at `RFC6113 `_. + +Let's take a Kerberos AS-REQ packet with FAST armoring (RFC6113): + +.. figure:: ../graphics/kerberos/as_req_fast.png + :align: center + + FAST armoring in AS-REQ. Credit to `this paper by A. Bordes `_. + +.. code:: pycon + + >>> pkt = Ether(bytes.fromhex(b'52540013d0835254003ea3be08004502089636a1400080063ad3c0a87fd2c0a87fc8fecc0058eea93069573b278e50180402897400000000086a6a82086630820862a103020105a20302010aa38207a23082079e3082079aa10402020088a28207900482078ca082078830820784a082064a30820646a003020101a182063d048206396e82063530820631a003020105a10302010ea20703050000000000a38205796182057530820571a003020105a10c1b0a444f4d312e4c4f43414ca21f301da003020102a11630141b066b72627467741b0a444f4d312e4c4f43414ca382053930820535a003020112a103020102a282052704820523acc8b7671c0d50522f1a8d8452ce450aceb40fff0229e8ee546bccf1512e4877ef93dde465595260a6a5a8e85ea38600ce8dff7d510f3c744e2c43eb9d3187d638f716c29b6e7aa9eb407de28d0161f49013966eda0a161ff174dad42e7aa500cfe298541215448013ffe4883b6b1166f908f50de129487fe77fff874fd4102cdcce8db8dbeb8da02f08cc88b3790cdad5ec499959c7e79d6fef107d1e17ce80cc3df050b7e7a1c31f278e4fd4ea9523c950876f174be363234f8495b9550de1560ba17daeafbf133f78991053d929ad3fd668327d42288e6581671daaef908682ee282e17c31d8f8bb55d27fce155ee2e84a2ff8bc9600891be15e6ede3e1bbd2742a7af8b0a32c48973c9e3776a69647bab11592756c5a15b9101c392efa35d000abb3dabccd97e64426e3fd8d47e0e369c83b5391f38947d536d351c061081d654eef1a3861cdb2ea2bc48222b450d1b7d09c0670493bccc60dfcaa5cfe46fd50adf8e388204a4691dc5f0c3dbae0b4da6ac2dd781f149a444840aaa3a3c3befb5a5c04ee0405baed66afcf9b988d10ea14a955f43df79465e6fc02a12bce3870988950f1ab48e1a4f876f351671c5061e6399a63cb0479f7bd017dfd9bc5be192faf6d4f11e6ee6003933eeaf632f0056c4c1ccd183d7977cfca85419fe5b039674419d802068e792c9576ae2a88bfbeb1f59273226782c6efb288717d8f7a4bc3bf4c697fcac1adc1829f0a914f2559b278ccadd108eb87a11dacc88e4302e9af627474e57171192b94c6b358f8f98e308596215d2fb9d9c2b49c4cbedcb43fc231b86f0493d56b82962cf3383a84f8922c2b99f8fa8fdd85797b09a6e60f72007c0379988be2ff1cfc16f21300c1b4b784174005a9185f760e68ef94b9384eb24decee31b63d1b92278cd75b85d4d80c4e83306533a9d95aa6207cbfbeb0970a41c44aba59839f007923ecd8ff0de8314990a435dbea4dedbee16faf5ab2be9f96d691cfa983a6c843bd183f84c1b4998a3eaa907cae6b82b0ae8363f3edd8cb03d3c9c60ff55a84d8a292ea20555fbd6ce5ad4ad7a6b4bc5bff2e02c477a7a8a98d5a387d389caa172c400b151d95871b2aa16a040dc71a9be5f0774b06a5ca87674ccb4109a2c41db9e3160704218ad495d0751194fbef4becae4d7be24b9d968da592256a2b22cf724e989e71a60d0603b59bebd475285f793794b7a18af49a2b68670e3a6247c453274e35c863a16b5023c6c94659e25abb27c760f989ac0bbf9a5b125d0ea34fb03225cc93d5b8b6829e906883ee76cf8ee61dfacc488e8dc5cbc8ba9705a9e915a68f838232394f97fb1aac4a2a90fe17d46f9c51946a2bf9598df7f5b5e7ee692a78860eea3cef748a5be36529228e40b4aec83ebc8bb14176a4c565b06500e9517229b8340c55812101dbbc6bee693c35873082a5a1a53b35cf3509193d4dc5175c9360a00da71692ba205b3264aecc9ecc8bca31fec43efc8701423bb484f6f21699439dd30f71228f16eaab96b7de3547721d1635bbfe50678900ac378a4958b6c34964f3e0dc843880dbde57fb4a76ab85eba2b190bfdaefc7ba17e109f839493b0f2d6fc7ea17403bebe06f2809314ca514606f54668082364ed6752019f27e1df74f93fcf1c25630a29713a89d4a998c444bc91279c6fc66e0aa5dec72be316e1160cf9f90d5915c464b6bfec5216e901be4726db596a15745511c63736a69ac9ecb9e86601c631b4992653c320e6983562fa613134560cb606621e9661ac5961313ee70868ab48d6010173d8a96fffdb2baf4afe18c846d3fed6f30b9a809d72e647735fc536edec543abc232480d28660395a4819e30819ba003020112a281930481901273d5af61ad426d51d0757e897917caeb6fc1b6950554e8d750f95d27f444e3aaf7ae0bf4595b5e906d9682dbdeedcf6eb42a84ab8092997b783f57710127228165deeb2ce5e09e2ddc71555dc31970a8312d888b8ae766382098276d62b4bd76f34cbc889e24ad5405ec037ceb724fdb71fe247fe2a414a037ed33c796f4475fcfb5993eed147b6d63d740d58da5b0a1173015a003020110a10e040c75f02d8d2954e0ae1a9e0653a282011930820115a003020112a282010c04820108ae9bbc4629c80f4a383a69c4583824295c75f34b000b3fdbdaab073a042935e32c29e0ee2b2b446e4a6a2592362d0d593cddd74dacc24f16353776e1b5d192ad1cf5e63f66f40a134ecb87c077c30922bc0cab00ae23d187d56090d9098f843c54fabe7c012ff87e317dfe339c40911264609d489b041a4e9b52c0eb03ee88a393d17da92786bd1716b92eb0d7a5a24a64ade0870dea8a7e138acdf209ee277cb3fadeedab173fd64cc10a1004010774658b94852639bda10a5e8aff29174e3d2c7032c32631b074afdac0e6832bae74de9be19e522f63bc8499753a209291fee1861c29096cc8ee3cfda5be235b0aa95635916edcfcdaf90b896e2eaa5a57d5e4da0b00408f4201a481af3081aca00703050040810010a11a3018a003020101a111300f1b0d61646d2d302d66617374656e62a2061b04444f4d31a3193017a003020102a110300e1b066b72627467741b04444f4d31a511180f32303337303931333032343830355aa611180f32303337303931333032343830355aa70602043f58a7a0a81530130201120201110201170201180202ff79020103a91d301b3019a003020114a112041053525620202020202020202020202020')) + >>> pkt[TCP].payload.show() + ###[ KerberosTCPHeader ]### + len = 2154 + ###[ Kerberos ]### + \root \ + |###[ KRB_AS_REQ ]### + | pvno = 0x5 + | msgType = 'AS-REQ' 0xa + | \padata \ + | |###[ PADATA ]### + | | padataType= 'PA-FX-FAST' 0x88 + | | \padataValue\ + | | |###[ PA_FX_FAST_REQUEST ]### + | | | \armoredData\ + | | | |###[ KrbFastArmoredReq ]### + | | | | \armor \ + | | | | |###[ KrbFastArmor ]### + | | | | | armorType = 'FX_FAST_ARMOR_AP_REQUEST' 0x1 + | | | | | \armorValue\ + | | | | | |###[ KRB_AP_REQ ]### + | | | | | | pvno = 0x5 + | | | | | | msgType = 'AP-REQ' 0xe + | | | | | | apOptions = + | | | | | | \ticket \ + | | | | | | |###[ KRB_Ticket ]### + | | | | | | | tktVno = 0x5 + | | | | | | | realm = + | | | | | | | \sname \ + | | | | | | | |###[ PrincipalName ]### + | | | | | | | | nameType = 'NT-SRV-INST' 0x2 + | | | | | | | | nameString= [, ] + | | | | | | | \encPart \ + | | | | | | | |###[ EncryptedData ]### + | | | | | | | | etype = 'AES-256' 0x12 + | | | | | | | | kvno = 0x2 + | | | | | | | | cipher = \xea\xf62\xf0\x05lL\x1c\xcd\x18=yw\xcf\xca\x85A\x9f\xe5\xb09gD\x19\xd8\x02\x06\x8ey,\x95v\xae*\x88\xbf\xbe\xb1\xf5\x92s"g\x82\xc6\xef\xb2\x88q}\x8fzK\xc3\xbfLi\x7f\xca\xc1\xad\xc1\x82\x9f\n\x91O%Y\xb2x\xcc\xad\xd1\x08\xeb\x87\xa1\x1d\xac\xc8\x8eC\x02\xe9\xafbtt\xe5qq\x19+\x94\xc6\xb3X\xf8\xf9\x8e0\x85\x96!]/\xb9\xd9\xc2\xb4\x9cL\xbe\xdc\xb4?\xc21\xb8o\x04\x93\xd5k\x82\x96,\xf38:\x84\xf8\x92,+\x99\xf8\xfa\x8f\xdd\x85y{\t\xa6\xe6\x0fr\x00|\x03y\x98\x8b\xe2\xff\x1c\xfc\x16\xf2\x13\x00\xc1\xb4\xb7\x84\x17@\x05\xa9\x18_v\x0eh\xef\x94\xb98N\xb2M\xec\xee1\xb6=\x1b\x92\'\x8c\xd7[\x85\xd4\xd8\x0cN\x830e3\xa9\xd9Z\xa6 |\xbf\xbe\xb0\x97\nA\xc4J\xbaY\x83\x9f\x00y#\xec\xd8\xff\r\xe81I\x90\xa45\xdb\xeaM\xed\xbe\xe1o\xafZ\xb2\xbe\x9f\x96\xd6\x91\xcf\xa9\x83\xa6\xc8C\xbd\x18?\x84\xc1\xb4\x99\x8a>\xaa\x90|\xaek\x82\xb0\xae\x83c\xf3\xed\xd8\xcb\x03\xd3\xc9\xc6\x0f\xf5Z\x84\xd8\xa2\x92\xea U_\xbdl\xe5\xadJ\xd7\xa6\xb4\xbc[\xff.\x02\xc4w\xa7\xa8\xa9\x8dZ8}8\x9c\xaa\x17,@\x0b\x15\x1d\x95\x87\x1b*\xa1j\x04\r\xc7\x1a\x9b\xe5\xf0wK\x06\xa5\xca\x87gL\xcbA\t\xa2\xc4\x1d\xb9\xe3\x16\x07\x04!\x8a\xd4\x95\xd0u\x11\x94\xfb\xefK\xec\xaeM{\xe2K\x9d\x96\x8d\xa5\x92%j+"\xcfrN\x98\x9eq\xa6\r\x06\x03\xb5\x9b\xeb\xd4u(_y7\x94\xb7\xa1\x8a\xf4\x9a+hg\x0e:bG\xc4S\'N5\xc8c\xa1kP#\xc6\xc9FY\xe2Z\xbb\'\xc7`\xf9\x89\xac\x0b\xbf\x9a[\x12]\x0e\xa3O\xb02%\xcc\x93\xd5\xb8\xb6\x82\x9e\x90h\x83\xeev\xcf\x8e\xe6\x1d\xfa\xccH\x8e\x8d\xc5\xcb\xc8\xba\x97\x05\xa9\xe9\x15\xa6\x8f\x83\x8229O\x97\xfb\x1a\xacJ*\x90\xfe\x17\xd4o\x9cQ\x94j+\xf9Y\x8d\xf7\xf5\xb5\xe7\xeei*x\x86\x0e\xea<\xeft\x8a[\xe3e)"\x8e@\xb4\xae\xc8>\xbc\x8b\xb1Av\xa4\xc5e\xb0e\x00\xe9Qr)\xb84\x0cU\x81!\x01\xdb\xbck\xeei<5\x870\x82\xa5\xa1\xa5;5\xcf5\t\x19=M\xc5\x17\\\x93`\xa0\r\xa7\x16\x92\xba [2d\xae\xcc\x9e\xcc\x8b\xca1\xfe\xc4>\xfc\x87\x01B;\xb4\x84\xf6\xf2\x16\x99C\x9d\xd3\x0fq"\x8f\x16\xea\xab\x96\xb7\xde5Gr\x1d\x165\xbb\xfePg\x89\x00\xac7\x8aIX\xb6\xc3Id\xf3\xe0\xdc\x848\x80\xdb\xdeW\xfbJv\xab\x85\xeb\xa2\xb1\x90\xbf\xda\xef\xc7\xba\x17\xe1\t\xf89I;\x0f-o\xc7\xea\x17@;\xeb\xe0o(\t1L\xa5\x14`oTf\x80\x826N\xd6u \x19\xf2~\x1d\xf7O\x93\xfc\xf1\xc2V0\xa2\x97\x13\xa8\x9dJ\x99\x8cDK\xc9\x12y\xc6\xfcf\xe0\xaa]\xecr\xbe1n\x11`\xcf\x9f\x90\xd5\x91\\FKk\xfe\xc5!n\x90\x1b\xe4rm\xb5\x96\xa1WEQ\x1ccsji\xac\x9e\xcb\x9e\x86`\x1cc\x1bI\x92e<2\x0ei\x83V/\xa6\x13\x13E`\xcb`f!\xe9f\x1a\xc5\x96\x13\x13\xeep\x86\x8a\xb4\x8d`\x10\x17=\x8a\x96\xff\xfd\xb2\xba\xf4\xaf\xe1\x8c\x84m?\xedo0\xb9\xa8\t\xd7.dw5\xfcSn\xde\xc5C\xab\xc22H\r(f\x03\x95']> + | | | | | | \authenticator\ + | | | | | | |###[ EncryptedData ]### + | | | | | | | etype = 'AES-256' 0x12 + | | | | | | | kvno = None + | | | | | | | cipher = \xed\x14{mc\xd7@\xd5\x8d\xa5\xb0']> + | | | | checksumtype= 'HMAC-SHA1-96-AES256' 0x10 + | | | | checksum = + | | | | \encFastReq\ + | | | | |###[ EncryptedData ]### + | | | | | etype = 'AES-256' 0x12 + | | | | | kvno = None + | | | | | cipher = + | \reqBody \ + | |###[ KRB_KDC_REQ_BODY ]### + | | kdcOptions= forwardable, renewable, canonicalize, renewable-ok + | | \cname \ + | | |###[ PrincipalName ]### + | | | nameType = 'NT-PRINCIPAL' 0x1 + | | | nameString= [] + | | realm = + | | \sname \ + | | |###[ PrincipalName ]### + | | | nameType = 'NT-SRV-INST' 0x2 + | | | nameString= [, ] + | | from = None + | | till = 2037-09-13 02:48:05 UTC + | | rtime = 2037-09-13 02:48:05 UTC + | | nonce = 0x3f58a7a0 + | | etype = [0x12 , 0x11 , 0x17 , 0x18 , -0x87 , 0x3 ] + | | \addresses \ + | | |###[ HostAddress ]### + | | | addrType = 'NetBios' 0x14 + | | | address = + | | encAuthorizationData= None + | | additionalTickets= None + +There are 3 encrypted payloads: + +- ``pkt.root.padata[0].padataValue.armoredData.armor.armorValue.ticket.encPart``, encrypted using the KRBTGT +- ``pkt.root.padata[0].padataValue.armoredData.armor.armorValue.authenticator``, encrypted using the ticket session key (that the clients gets from the first AS-REQ, and that that is also included in tickets for the server to use) +- ``pkt.root.padata[0].padataValue.armoredData.encFastReq``, encrypted using using the armor key + +We have the krbtgt for this demo: + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import Key, EncryptionType + >>> krbtgt_hex = "ac67a63d7155791fe31dace230ab516e818c453dfdbd44cbe691b240725c4907" + >>> krbtgt = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex(krbtgt_hex)) + +We can therefore decrypt the first payload: + +.. code:: pycon + + >>> enc = pkt.root.padata[0].padataValue.armoredData.armor.armorValue.ticket.encPart + >>> encticketpart = enc.decrypt(krbtgt) + >>> encticketpart.show() + ###[ EncTicketPart ]### + flags = forwardable, renewable, initial, pre-authent + \key \ + |###[ EncryptionKey ]### + | keytype = 'AES-256' 0x12 + | keyvalue = B\xd8)m/G\x82B;\x9f+\x86\xcd\xcd\xf4\x05']> + crealm = + \cname \ + |###[ PrincipalName ]### + | nameType = 'NT-PRINCIPAL' 0x1 + | nameString= [] + \transited \ + |###[ TransitedEncoding ]### + | trType = 0x0 + | contents = + authtime = 2022-07-12 23:02:25 UTC + starttime = 2022-07-12 23:02:25 UTC + endtime = 2022-07-13 09:02:25 UTC + renewTill = 2022-07-19 23:02:25 UTC + addresses = None + [...] + +We can see the ticket session key in there, let's retrieve it and build a :class:`~scapy.libs.rfc3961.Key` object: + +.. note:: We use the ``.toKey()`` function in the ``EncryptedKey`` type which is a shorthand for ``Key(, key=)`` + +.. code:: pycon + + >>> ticket_session_key = encticketpart.key.toKey() + >>> ticket_session_key.key + b'\xe3\xa2\x0f\x8e\xb2\xe1*\xe0\x7f\x86\xcc\x88\xe6,\x08>B\xd8)m/G\x82B;\x9f+\x86\xcd\xcd\xf4\x05' + +We can now decrypt the second payload: + +.. code:: pycon + + >>> enc = pkt.root.padata[0].padataValue.armoredData.armor.armorValue.authenticator + >>> authenticator = enc.decrypt(ticket_session_key) + >>> authenticator.show() + ###[ KRB_Authenticator ]### + authenticatorPvno= 0x5 + crealm = + \cname \ + |###[ PrincipalName ]### + | nameType = 'NT-PRINCIPAL' 0x1 + | nameString= [] + checksumtype= 0x0 + checksum = + cusec = 0x3c + ctime = 2022-07-12 23:54:37 UTC + \subkey \ + |###[ EncryptionKey ]### + | keytype = 'AES-256' 0x12 + | keyvalue = + seqNumber = 0x0 + encAuthorizationData= None + +Again, we see inside this the subkey that is used to compute the armor key. We get it: + +.. code:: pycon + + >>> subkey = authenticator.subkey.toKey() + >>> subkey.key + b'%\xa4n\xe1\xd0\xf5\x8d\xc4\x8d\xecv\xe8\x9c\xd3\xc9\xee\x1bu\xc9\xa5\xa6\xf8\x83f\x98\xa1\xd9\xe7*I\x9b\xf8' + +Following `RFC6113 sect 5.4.1.1 `_, we can now compute the armor key using: + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import KRB_FX_CF2 + >>> armorkey = KRB_FX_CF2(subkey, ticket_session_key, b"subkeyarmor", b"ticketarmor") + >>> print(armorkey.key) + b'\x9f\x18L]I\x16\xd0\xe5\xa6\xd9\x92+\xbf\xbc\xe0\n\xd1\xcb6\xf3\xd1.C\xc2\xdcp\xf0H(\x99\x14\x80' + +That we can now use to decrypt the last payload: + +.. code:: pycon + + >>> enc = pkt.root.padata[0].padataValue.armoredData.encFastReq + >>> krbfastreq = enc.decrypt(armorkey) + >>> krbfastreq.show() + ###[ KrbFastReq ]### + fastOptions= + \padata \ + |###[ PADATA ]### + | padataType= 'PA-PAC-REQUEST' 0x80 + | \padataValue\ + | |###[ PA_PAC_REQUEST ]### + | | includePac= True + |###[ PADATA ]### + | padataType= 'PA-PAC-OPTIONS' 0xa7 + | \padataValue\ + | |###[ PA_PAC_OPTIONS ]### + | | options = Claims + \reqBody \ + |###[ KRB_KDC_REQ_BODY ]### + | kdcOptions= forwardable, renewable, canonicalize, renewable-ok + | \cname \ + | |###[ PrincipalName ]### + | | nameType = 'NT-PRINCIPAL' 0x1 + | | nameString= [] + | realm = + | \sname \ + | |###[ PrincipalName ]### + | | nameType = 'NT-SRV-INST' 0x2 + | | nameString= [, ] + | from = None + | till = 2037-09-13 02:48:05 UTC + | rtime = 2037-09-13 02:48:05 UTC + | nonce = 0x3f58a7a0 + | etype = [0x12 , 0x11 , 0x17 , 0x18 , -0x87 , 0x3 ] + | \addresses \ + | |###[ HostAddress ]### + | | addrType = 'NetBios' 0x14 + | | address = + | encAuthorizationData= None + | additionalTickets= None + +Manually using Kerberos encryption +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A :func:`~scapy.libs.rfc3961.Key.encrypt` function exists in the :class:`~scapy.libs.rfc3961.Key` object in order to do the opposite of :func:`~scapy.libs.rfc3961.Key.decrypt`. + + +For instance, during pre-authentication, encode ``PA-ENC-TIMESTAMP``: + +.. code:: pycon + + >>> from datetime import datetime + >>> from scapy.libs.rfc3961 import Key, EncryptionType + >>> # Create the PADATA layer with its EncryptedValue + >>> pkt = PADATA(padataType=0x2, padataValue=EncryptedData()) + >>> # Compute the key + >>> key = Key.string_to_key(EncryptionType.AES256_CTS_HMAC_SHA1_96, b"Password1", b"DOMAIN.LOCALUser1") + >>> now_time = datetime.now(timezone.utc).replace(microsecond=0) # Current time with no milliseconds + >>> # Encrypt + >>> pkt.padataValue.encrypt(key, PA_ENC_TS_ENC(patimestamp=ASN1_GENERALIZED_TIME(now_time))) + >>> pkt.show() + ###[ PADATA ]### + padataType= 2 + \padataValue\ + |###[ EncryptedData ]### + | etype = 18 + | kvno = 0x0 + | cipher = b"\xc1\x9a\xaf\x89V\x16\x82\xb6\x9a\xcb\x15[\xaf\xed\xd9\xfc\x04\xbf\x18\xd4&\x91\xb3\xcf~tEk,\x98m\xee\xa4O\x05=\x11b\xe05\xca\x92+80\x99\xb1'~\x8d\xdbtz\xa8" diff --git a/doc/scapy/layers/ldap.rst b/doc/scapy/layers/ldap.rst new file mode 100644 index 00000000000..c87a5e8dcd5 --- /dev/null +++ b/doc/scapy/layers/ldap.rst @@ -0,0 +1,280 @@ +LDAP +==== + +Scapy fully implements the LDAPv2 / LDAPv3 messages, in addition to a very basic :class:`~scapy.layers.ldap.LDAP_Client` class. + + +LDAP client usage +----------------- + +The general idea when using the :class:`~scapy.layers.ldap.LDAP_Client` class comes down to: + +- instantiating the class +- calling :func:`~scapy.layers.ldap.LDAP_Client.connect` with the IP (this is where to specify whether to use SSL or not) +- calling :func:`~scapy.layers.ldap.LDAP_Client.bind` (this is where to specify a SSP if authentication is desired) +- calling :func:`~scapy.layers.ldap.LDAP_Client.search` to search data. +- calling :func:`~scapy.layers.ldap.LDAP_Client.modify` to edit data attributes. + +The simplest, unauthenticated demo of the client would be something like: + +.. code:: pycon + + >>> client = LDAP_Client() + >>> client.connect("192.168.0.100") + >>> client.bind(LDAP_BIND_MECHS.NONE) + >>> client.sr1(LDAP_SearchRequest()).show() + ┃ Connecting to 192.168.0.100 on port 389... + └ Connected from ('192.168.0.102', 40228) + NONE bind succeeded ! + >> LDAP_SearchRequest + << LDAP_SearchResponseEntry + ###[ LDAP ]### + messageID = 0x1 + \protocolOp\ + |###[ LDAP_SearchResponseEntry ]### + | objectName= + | \attributes\ + | |###[ LDAP_PartialAttribute ]### + | | type = + | | \values \ + | | |###[ LDAP_AttributeValue ]### + | | | value = + | |###[ LDAP_PartialAttribute ]### + | | type = + | | \values \ + | | |###[ LDAP_AttributeValue ]### + | | | value = + | |###[ LDAP_PartialAttribute ]### + | | type = + | | \values \ + | | |###[ LDAP_AttributeValue ]### + | | | value = + [...] + +Connecting +~~~~~~~~~~ + +Let's first instantiate the :class:`~scapy.layers.ldap.LDAP_Client`, and connect to a server over the default port (389): + +.. code:: python + + client = LDAP_Client() + client.connect("192.168.0.100") + +It is also possible to use TLS when connecting to the server. + +.. code:: python + + client = LDAP_Client() + client.connect("192.168.0.100", use_ssl=True) + +In that case, the default port is 636. This can be changed using the ``port`` attribute. + +.. note:: + By default, the server certificate is NOT checked when using this mode, because the server certificate will likely be self-signed. + To actually use TLS securely, you should pass a ``sslcontext`` as shown below: + +.. code:: python + + import ssl + client = LDAP_Client() + sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + sslcontext.load_verify_locations('path/to/ca.crt') + client.connect("192.168.0.100", use_ssl=True, sspcontext=sslcontext) + +.. note:: If the client is too verbose, you can pass ``verb=False`` when instantiating :class:`~scapy.layers.ldap.LDAP_Client`. + +Binding +~~~~~~~ + +When binding, you must specify a *mechanism type*. This type comes from the :class:`~scapy.layers.ldap.LDAP_BIND_MECHS` enumeration, which contains: + +- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.NONE`: an unauthenticated bind. +- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SIMPLE`: the simple bind mechanism. Credentials are sent **in plaintext**. +- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SICILY`: a `Windows specific authentication mechanism specified in [MS-ADTS] `_ that only supports NTLM. +- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSSAPI`: the SASL authentication mechanism, as specified by `RFC 4422 `_. +- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSS_SPNEGO`: the SPNEGO authentication mechanism, another `Windows specific authentication mechanism specified in [MS-SPNG] `_. + +Depending on the server that you are talking to, some of those mechanisms might not be available. This is most notably the case of :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SICILY` and :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSS_SPNEGO` which are mostly Windows-specific. + +We'll now go over "how to bind" using each one of those mechanisms: + +**NONE (Unauthenticated):** + +.. code:: python + + client.bind(LDAP_BIND_MECHS.NONE) + +**SIMPLE:** + +.. code:: python + + client.bind( + LDAP_BIND_MECHS.SIMPLE, + simple_username="Administrator", + simple_password="Password1!", + ) + +**SICILY - NTLM:** + +.. code:: python + + ssp = NTLMSSP(UPN="Administrator", PASSWORD="Password1!") + client.bind( + LDAP_BIND_MECHS.SICILY, + ssp=ssp, + ) + +**SASL_GSSAPI - Kerberos:** + +.. code:: python + + ssp = KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!", + SPN="ldap/dc1.domain.local") + client.bind( + LDAP_BIND_MECHS.SASL_GSSAPI, + ssp=ssp, + ) + +**SASL_GSS_SPNEGO - NTLM / Kerberos:** + +.. code:: python + + ssp = SPNEGOSSP([ + NTLMSSP(UPN="Administrator", PASSWORD="Password1!"), + KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!", + SPN="ldap/dc1.domain.local"), + ]) + client.bind( + LDAP_BIND_MECHS.SASL_GSS_SPNEGO, + ssp=ssp, + ) + +Signing / Encryption +~~~~~~~~~~~~~~~~~~~~ + +Additionally, it is possible to enable signing or encryption of the LDAP data, when LDAPS is NOT in use. +This is done by setting ``sign`` and ``encrypt`` parameters of the :func:`~scapy.layers.ldap.LDAP_Client.bind` function. + +There are however a few caveats to note: + +- It's not possible to use those flags in ``NONE`` (duh) or ``SIMPLE`` mode. +- When using the :class:`~scapy.layers.ntlm.NTLMSSP` (in :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SICILY` or :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSS_SPNEGO` mode), it isn't possible to use ``sign`` without ``encrypt``, because Windows doesn't implement it. + +Querying +~~~~~~~~ + +Once the LDAP connection is bound, it becomes possible to perform requests. For instance, to query all the values of the root DSE: + +.. code:: python + + client.sr1(LDAP_SearchRequest()).show() + +We can also use the :func:`~scapy.layers.ldap.LDAP_Client.search` passing a base DN, a filter (as specified by RFC2254) and a scope.\\ + +The scope can be one of the following: + +- 0=baseObject: only the base DN's attributes are queried +- 1=singleLevel: the base DN's children are queried +- 2=wholeSubtree: the entire subtree under the base DN is included + +For instance, this corresponds to querying the DN ``CN=Users,DC=domain,DC=local`` with the filter ``(objectCategory=person)`` and asking for the attributes ``objectClass,name,description,canonicalName``: + +.. code:: python + + resp = client.search( + "CN=Users,DC=domain,DC=local", + "(objectCategory=person)", + ["objectClass", "name", "description", "canonicalName"], + scope=1, # children + ) + resp.show() + +To understand exactly what's going on, note that the previous call is exactly identical to the following: + +.. code:: python + + resp = client.sr1( + LDAP_SearchRequest( + filter=LDAP_Filter( + filter=LDAP_FilterEqual( + attributeType=ASN1_STRING(b'objectCategory'), + attributeValue=ASN1_STRING(b'person') + ) + ), + attributes=[ + LDAP_SearchRequestAttribute(type=ASN1_STRING(b'objectClass')), + LDAP_SearchRequestAttribute(type=ASN1_STRING(b'name')), + LDAP_SearchRequestAttribute(type=ASN1_STRING(b'description')), + LDAP_SearchRequestAttribute(type=ASN1_STRING(b'canonicalName')) + ], + baseObject=ASN1_STRING(b'CN=Users,DC=domain,DC=local'), + scope=ASN1_ENUMERATED(1), + derefAliases=ASN1_ENUMERATED(0), + sizeLimit=ASN1_INTEGER(1000), + timeLimit=ASN1_INTEGER(60), + attrsOnly=ASN1_BOOLEAN(0) + ) + ) + + +.. warning:: + Our RFC2254 parser currently does not support 'Extensible Match'. + +Modifying attributes +~~~~~~~~~~~~~~~~~~~~ + +It's also possible to change some attributes on an object. +The following issues a ``Modify Request`` that replaces the ``displayName`` attribute and adds a ``servicePrincipalName``: + +.. code:: python + + client.modify( + "CN=User1,CN=Users,DC=domain,DC=local", + changes=[ + LDAP_ModifyRequestChange( + operation="replace", + modification=LDAP_PartialAttribute( + type="displayName", + values=[ + LDAP_AttributeValue(value="Lord User the 1st") + ] + ) + ), + LDAP_ModifyRequestChange( + operation="add", + modification=LDAP_PartialAttribute( + type="servicePrincipalName", + values=[ + LDAP_AttributeValue(value="http/lorduser") + ] + ) + ) + ] + ) + +LDAPHero +-------- + +LDAPHero (LDAPéro in French) is a graphical wrapper around Scapy's :class:`~scapy.layers.ldap.LDAP_Client`, that works on all plateforms. +It can be used with: + +.. code:: python + + >>> load_module("ticketer") + >>> LDAPHero() + +It's possible to pass it a SSP, which will be used when clicking the "Bind" button: + +.. code:: python + + >>> LDAPHero(mech=LDAP_BIND_MECHS.SICILY, + ... ssp=NTLMSSP(UPN="Administrator@domain.local", PASSWORD="test")) + +You can use the same examples as in `Binding <#binding>`_. + +It's also possible to pass some connection parameters, for instance to connect to a specific host, you could use: + +.. code:: python + + >>> LDAPHero(host="192.168.0.100") diff --git a/doc/scapy/layers/netflow.rst b/doc/scapy/layers/netflow.rst index 23be7a5b106..5c297732093 100644 --- a/doc/scapy/layers/netflow.rst +++ b/doc/scapy/layers/netflow.rst @@ -45,15 +45,17 @@ Fortunately, Scapy knows how to detect the templates and will provide dissecting header = Ether()/IP()/UDP() netflow_header = NetflowHeader()/NetflowHeaderV9() - # Let's first build the template. Those need an ID > 255 + # Let's first build the template. Those need an ID > 255. + # The (full) list of possible fieldType is available in the + # NetflowV910TemplateFieldTypes list. You can also use the int value. flowset = NetflowFlowsetV9( templates=[NetflowTemplateV9( template_fields=[ - NetflowTemplateFieldV9(fieldType=1, fieldLength=1), # IN_BYTES - NetflowTemplateFieldV9(fieldType=2, fieldLength=4), # IN_PKTS - NetflowTemplateFieldV9(fieldType=4), # PROTOCOL - NetflowTemplateFieldV9(fieldType=8), # IPV4_SRC_ADDR - NetflowTemplateFieldV9(fieldType=12), # IPV4_DST_ADDR + NetflowTemplateFieldV9(fieldType="IN_BYTES", fieldLength=1), + NetflowTemplateFieldV9(fieldType="IN_PKTS", fieldLength=4), + NetflowTemplateFieldV9(fieldType="PROTOCOL"), + NetflowTemplateFieldV9(fieldType="IPV4_SRC_ADDR"), + NetflowTemplateFieldV9(fieldType="IPV4_DST_ADDR"), ], templateID=256, fieldCount=5) diff --git a/doc/scapy/layers/pnio.rst b/doc/scapy/layers/pnio.rst index b3b37486575..49b9441c0b1 100644 --- a/doc/scapy/layers/pnio.rst +++ b/doc/scapy/layers/pnio.rst @@ -7,263 +7,125 @@ PROFINET IO is an industrial protocol composed of different layers such as the R RTC data packet --------------- -The first thing to do when building the RTC ``data`` buffer is to instantiate each Scapy packet which represents a piece of data. Each one of them may require some specific piece of configuration, such as its length. All packets and their configuration are: +The first thing to do when building the RTC ``data`` buffer is to instantiate each Scapy packet which represents a piece of data. Some of the basic packets are: -* ``PNIORealTimeRawData``: a simple raw data like ``Raw`` +* ``ProfinetIO``: the building block for PROFINET packets. Can be layered on top of Ether() or UDP() - * ``length``: defines the length of the data +* ``PROFIsafe``: the PROFIsafe profile to perform functional safety -* ``Profisafe``: the PROFIsafe profile to perform functional safety +* ``PNIORealTime_IOxS``: either an IO Consumer or Provider Status byte - * ``length``: defines the length of the whole packet - * ``CRC``: defines the length of the CRC, either ``3`` or ``4`` +Instantiate the packets as follows:: -* ``PNIORealTimeIOxS``: either an IO Consumer or Provider Status byte - - * Doesn't require any configuration - -To instantiate one of these packets with its configuration, the ``config`` argument must be given. It is a ``dict()`` which contains all the required piece of configuration:: - - >>> load_contrib('pnio_rtc') - >>> raw(PNIORealTimeRawData(load='AAA', config={'length': 4})) - 'AAA\x00' - >>> raw(Profisafe(load='AAA', Control_Status=0x20, CRC=0x424242, config={'length': 8, 'CRC': 3})) - 'AAA\x00 BBB' - >>> hexdump(PNIORealTimeIOxS()) + >>> load_contrib('pnio') + >>> raw(ProfinetIO()/b'AAA') + b'\x00\x00AAA' + >>> raw(PROFIsafe.build_PROFIsafe_class(PROFIsafeControl, 4)(data = b'AAA', control=0x20, crc=0x424242)) + b'AAA\x00 BBB' + >>> hexdump(PNIORealTime_IOxS()) 0000 80 . RTC packet ---------- -Now that a data packet can be instantiated, a whole RTC packet may be built. ``PNIORealTime`` contains a field ``data`` which is a list of all data packets to add in the buffer, however, without the configuration, Scapy won't be +Now that a data packet can be instantiated, a whole RTC packet may be built. ``PNIORealTimeCyclicPDU`` contains a field ``data`` which is a list of all data packets to add in the buffer, however, without the configuration, Scapy won't be able to dissect it:: - >>> load_contrib("pnio_rtc") - >>> p=PNIORealTime(cycleCounter=1024, data=[ - ... PNIORealTimeIOxS(), - ... PNIORealTimeRawData(load='AAA', config={'length':4}) / PNIORealTimeIOxS(), - ... Profisafe(load='AAA', Control_Status=0x20, CRC=0x424242, config={'length': 8, 'CRC': 3}) / PNIORealTimeIOxS(), + >>> load_contrib('pnio') + >>> p=PNIORealTimeCyclicPDU(cycleCounter=1024, data=[ + ... PNIORealTime_IOxS(), + ... PNIORealTimeCyclicPDU.build_fixed_len_raw_type(4)(data = b'AAA') / PNIORealTime_IOxS(), + ... PROFIsafe.build_PROFIsafe_class(PROFIsafeControl, 4)(data = b'AAA', control=0x20, crc=0x424242)/PNIORealTime_IOxS(), ... ]) >>> p.show() - ###[ PROFINET Real-Time ]### - len= None - dataLen= None - \data\ - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0 - | extension= 0 - |###[ PNIO RTC Raw data ]### - | load= 'AAA' - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0 - | extension= 0 - |###[ PROFISafe ]### - | load= 'AAA' - | Control_Status= 0x20 - | CRC= 0x424242 - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0 - | extension= 0 - padding= '' - cycleCounter= 1024 - dataStatus= primary+validData+run+no_problem - transferStatus= 0 - - >>> p.show2() - ###[ PROFINET Real-Time ]### - len= 44 - dataLen= 15 - \data\ - |###[ PNIO RTC Raw data ]### - | load= '\x80AAA\x00\x80AAA\x00 BBB\x80' - padding= '' - cycleCounter= 1024 - dataStatus= primary+validData+run+no_problem - transferStatus= 0 - -For Scapy to be able to dissect it correctly, one must also configure the layer for it to know the location of each data in the buffer. This configuration is saved in the dictionary ``conf.contribs["PNIO_RTC"]`` which can be updated with the ``pnio_update_config`` method. Each item in the dictionary uses the tuple ``(Ether.src, Ether.dst)`` as key, to be able to separate the configuration of each communication. Each value is then a list of a tuple which describes a data packet. It is composed of the negative index, from the end of the data buffer, of the packet position, the class of the packet as the second item and the ``config`` dictionary to provide to the class as last. If we continue the previous example, here is the configuration to set:: - - >>> load_contrib("pnio") - >>> e=Ether(src='00:01:02:03:04:05', dst='06:07:08:09:0a:0b') / ProfinetIO() / p - >>> e.show2() - ###[ Ethernet ]### - dst= 06:07:08:09:0a:0b - src= 00:01:02:03:04:05 - type= 0x8892 - ###[ ProfinetIO ]### - frameID= RT_CLASS_1 - ###[ PROFINET Real-Time ]### - len= 44 - dataLen= 15 - \data\ - |###[ PNIO RTC Raw data ]### - | load= '\x80AAA\x00\x80AAA\x00 BBB\x80' - padding= '' + ###[ PROFINET Real-Time ]### + \data \ + |###[ PNIO RTC IOxS ]### + | dataState = good + | instance = subslot + | reserved = 0x0 + | extension = 0 + |###[ FixedLenRawPacketLen4 ]### + | data = 'AAA' + |###[ PNIO RTC IOxS ]### + | dataState = good + | instance = subslot + | reserved = 0x0 + | extension = 0 + |###[ PROFISafe Control Message with F_CRC_Seed=0 ]### + | dat( = 'AAA' + | control = Toggle_h + | crc = 0x424242 + |###[ PNIO RTC IOxS ]### + | dataState = good + | instance = subslot + | reserved = 0x0 + | extension = 0 + padding = '' cycleCounter= 1024 dataStatus= primary+validData+run+no_problem transferStatus= 0 - >>> pnio_update_config({('00:01:02:03:04:05', '06:07:08:09:0a:0b'): [ - ... (-9, Profisafe, {'length': 8, 'CRC': 3}), - ... (-9 - 5, PNIORealTimeRawData, {'length':4}), - ... ]}) - >>> e.show2() - ###[ Ethernet ]### - dst= 06:07:08:09:0a:0b - src= 00:01:02:03:04:05 - type= 0x8892 - ###[ ProfinetIO ]### - frameID= RT_CLASS_1 - ###[ PROFINET Real-Time ]### - len= 44 - dataLen= 15 - \data\ - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - |###[ PNIO RTC Raw data ]### - | load= 'AAA' - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - |###[ PROFISafe ]### - | load= 'AAA' - | Control_Status= 0x20 - | CRC= 0x424242L - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - padding= '' - cycleCounter= 1024 - dataStatus= primary+validData+run+no_problem - transferStatus= 0 - -If no data packets are configured for a given offset, it defaults to a ``PNIORealTimeIOxS``. However, this method is not very convenient for the user to configure the layer and it only affects the dissection of packets. In such cases, one may have access to several RTC packets, sniffed or retrieved from a PCAP file. Thus, ``PNIORealTime`` provides some methods to analyse a list of ``PNIORealTime`` packets and locate all data in it, based on simple heuristics. All of them take as first argument an iterable which contains the list of packets to analyse. -* ``PNIORealTime.find_data()`` analyses the data buffer and separate real data from IOxS. It returns a dict which can be provided to ``pnio_update_config``. -* ``PNIORealTime.find_profisafe()`` analyses the data buffer and find the PROFIsafe profiles among the real data. It returns a dict which can be provided to ``pnio_update_config``. -* ``PNIORealTime.analyse_data()`` executes both previous methods and update the configuration. **This is usually the method to call.** -* ``PNIORealTime.draw_entropy()`` will draw the entropy of each byte in the data buffer. It can be used to easily visualize PROFIsafe locations as entropy is the base of the decision algorithm of ``find_profisafe``. -:: +For Scapy to be able to dissect it correctly, one must also configure the layer for it to know the location of each data in the buffer. This configuration is saved in the dictionary ``conf.contribs["PNIO_RTC"]`` which can be updated with the ``conf.contribs["PNIO_RTC"].update`` method. Each item in the dictionary uses the tuple ``(Ether.src, Ether.dst, ProfinetIO.frameID)`` as key, to be able to separate the configuration of each communication. Each value is then a list of classes which describes a data packet. If we continue the previous example, here is the configuration to set:: - >>> load_contrib('pnio_rtc') - >>> t=rdpcap('/path/to/trace.pcap', 1024) - >>> PNIORealTime.analyse_data(t) - {('00:01:02:03:04:05', '06:07:08:09:0a:0b'): [(-19, , {'length': 1}), (-15, , {'CRC': 3, 'length': 6}), (-7, , {'CRC': 3, 'length': 5})]} - >>> t[100].show() + >>> e=Ether(src='00:01:02:03:04:05', dst='06:07:08:09:0a:0b') / ProfinetIO(frameID="RT_CLASS_1") / p + >>> e.show2() ###[ Ethernet ]### - dst= 06:07:08:09:0a:0b - src= 00:01:02:03:04:05 - type= n_802_1Q - ###[ 802.1Q ]### - prio= 6L - id= 0L - vlan= 0L - type= 0x8892 + dst = 06:07:08:09:0a:0b + src = 00:01:02:03:04:05 + type = 0x8892 ###[ ProfinetIO ]### - frameID= RT_CLASS_1 + frameID = RT_CLASS_1 (8000) ###[ PROFINET Real-Time ]### - len= 44 - dataLen= 22 - \data\ - |###[ PNIO RTC Raw data ]### - | load= '\x80\x80\x80\x80\x80\x80\x00\x80\x80\x80\x12:\x0e\x12\x80\x80\x00\x12\x8b\x97\xe3\x80' - padding= '' - cycleCounter= 6208 - dataStatus= primary+validData+run+no_problem - transferStatus= 0 - - >>> t[100].show2() + \data \ + |###[ PROFINET IO Real Time Cyclic Default Raw Data ]### + | data = '\\x80AAA\x00\\x80AAA\x00 BBB\\x80' + padding = '' + cycleCounter= 1024 + dataStatus= primary+validData+run+no_problem + transferStatus= 0 + >>> conf.contribs["PNIO_RTC"].update({('00:01:02:03:04:05', '06:07:08:09:0a:0b', 0x8000): [ + ... PNIORealTime_IOxS, + ... PNIORealTimeCyclicPDU.build_fixed_len_raw_type(4), + ... PNIORealTime_IOxS, + ... PROFIsafe.build_PROFIsafe_class(PROFIsafeControl, 4), + ... PNIORealTime_IOxS, + ... ]}) + >>> e.show2() ###[ Ethernet ]### - dst= 06:07:08:09:0a:0b - src= 00:01:02:03:04:05 - type= n_802_1Q - ###[ 802.1Q ]### - prio= 6L - id= 0L - vlan= 0L - type= 0x8892 + dst = 06:07:08:09:0a:0b + src = 00:01:02:03:04:05 + type = 0x8892 ###[ ProfinetIO ]### - frameID= RT_CLASS_1 + frameID = RT_CLASS_1 (8000) ###[ PROFINET Real-Time ]### - len= 44 - dataLen= 22 - \data\ - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - [...] - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - |###[ PNIO RTC Raw data ]### - | load= '' - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - [...] - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - |###[ PROFISafe ]### - | load= '' - | Control_Status= 0x12 - | CRC= 0x3a0e12L - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - |###[ PROFISafe ]### - | load= '' - | Control_Status= 0x12 - | CRC= 0x8b97e3L - |###[ PNIO RTC IOxS ]### - | dataState= good - | instance= subslot - | reserved= 0x0L - | extension= 0L - padding= '' - cycleCounter= 6208 - dataStatus= primary+validData+run+no_problem - transferStatus= 0 - -In addition, one can see, when displaying a ``PNIORealTime`` packet, the field ``len``. This is a computed field which is not added in the final packet build. It is mainly useful for dissection and reconstruction, but it can also be used to modify the behaviour of the packet. In fact, RTC packet must always be long enough for an Ethernet frame and to do so, a padding must be added right after the ``data`` buffer. The default behaviour is to add ``padding`` whose size is computed during the ``build`` process:: - - >>> raw(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()])) - '\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00' - -However, one can set ``len`` to modify this behaviour. ``len`` controls the length of the whole ``PNIORealTime`` packet. Then, to shorten the length of the padding, ``len`` can be set to a lower value:: + \data \ + |###[ PNIO RTC IOxS ]### + | dataState = good + | instance = subslot + | reserved = 0x0 + | extension = 0 + |###[ FixedLenRawPacketLen4 ]### + | data = 'AAA' + |###[ PNIO RTC IOxS ]### + | dataState = good + | instance = subslot + | reserved = 0x0 + | extension = 0 + |###[ PROFISafe Control Message with F_CRC_Seed=0 ]### + | data = 'AAA' + | control = Toggle_h + | crc = 0x424242 + |###[ PNIO RTC IOxS ]### + | dataState = good + | instance = subslot + | reserved = 0x0 + | extension = 0 + padding = '' + cycleCounter= 1024 + dataStatus= primary+validData+run+no_problem + transferStatus= 0 - >>> raw(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()], len=50)) - '\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00' - >>> raw(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()])) - '\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00' - >>> raw(PNIORealTime(cycleCounter=0x4242, data=[PNIORealTimeIOxS()], len=30)) - '\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BB5\x00' +If no data packets are configured for a given offset, it defaults to a ``PNIORealTimeCyclicDefaultRawData``. diff --git a/doc/scapy/layers/smb.rst b/doc/scapy/layers/smb.rst new file mode 100644 index 00000000000..6b86d80f8ff --- /dev/null +++ b/doc/scapy/layers/smb.rst @@ -0,0 +1,355 @@ +SMB +=== + +Scapy provides pretty good support for SMB 2/3 and very partial support of SMB1. + +You can use the :class:`~scapy.layers.smb2.SMB2_Header` to dissect or build SMB2/3, or :class:`~scapy.layers.smb.SMB_Header` for SMB1. + +.. _client: + +SMB 2/3 client +-------------- + +Scapy provides a small SMB 2/3 client Automaton: :class:`~scapy.layers.smbclient.SMB_Client` + +.. image:: ../graphics/smb/smb_client.png + :align: center + + +Scapy's SMB client stack is as follows: + +- the :class:`~scapy.layers.smbclient.SMB_Client` Automaton handles the logic to bind, negotiate and establish the SMB session (eventually using Security Providers). +- This Automaton is wrapped into a :class:`~scapy.layers.smbclient.SMB_SOCKET` object which provides access to basic SMB commands such as open, read, write, close, etc. +- This socket is wrapped into a :class:`~scapy.layers.smbclient.smbclient` class which provides a high-level SMB client, with functions such as ``ls``, ``cd``, ``get``, ``put``, etc. + +You can access any of the 3 layers depending on how low-level you want to get. +We'll skip over the lowest one in this documentation, as it not really usable as an API, but note that this is where to look if you want to change SMB negotiation or session setup .(people wanting to use this are welcomed to have a look at the ``scapy/layers/smbclient.py`` code). + +High-Level :class:`~scapy.layers.smbclient.smbclient` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +From the CLI +____________ + +Let's start by using :class:`~scapy.layers.smbclient.smbclient` from the Scapy CLI: + +.. code:: python + + >>> smbclient("server1.domain.local", "Administrator@domain.local") + Password: ************ + SMB authentication successful using SPNEGOSSP[KerberosSSP] ! + smb: \> shares + ShareName ShareType Comment + ADMIN$ DISKTREE Remote Admin + C$ DISKTREE Default share + IPC$ IPC Remote IPC + NETLOGON DISKTREE Logon server share + SYSVOL DISKTREE Logon server share + Users DISKTREE + common DISKTREE + smb: \> use c$ + smb: \> cd Program Files\Microsoft\ + smb: \Program Files\Microsoft> ls + FileName FileAttributes EndOfFile LastWriteTime + . DIRECTORY 0B Fri, 24 Feb 2023 17:00:27 (1677254427) + .. DIRECTORY 0B Fri, 24 Feb 2023 17:00:27 (1677254427) + EdgeUpdater DIRECTORY 0B Fri, 24 Feb 2023 17:00:27 (1677254427) + +.. note:: You can use ``help`` or ``?`` in the CLI to get the list of available commands. + +As you can see, the previous example used Kerberos to authenticate. +By default, the :class:`~scapy.layers.smbclient.smbclient` class will use a :class:`~scapy.layers.spnego.SPNEGOSSP` and provide ask for both ``NTLM`` and ``Kerberos``. but it is possible to have a greater control over this by providing your own ``ssp`` attribute. + +**smbclient using a** :class:`~scapy.layers.ntlm.NTLMSSP` + +.. code:: python + + >>> smbclient("server1.domain.local", ssp=NTLMSSP(UPN="Administrator", PASSWORD="password")) + +You might be wondering if you can pass the ``HashNT`` of the password of the user 'Administrator' directly. The answer is yes, you can 'pass the hash' directly: + +.. code:: python + + >>> smbclient("server1.domain.local", ssp=NTLMSSP(UPN="Administrator", HASHNT=bytes.fromhex("8846f7eaee8fb117ad06bdd830b7586c"))) + +**smbclient using a** :class:`~scapy.layers.ntlm.KerberosSSP` + +.. code:: python + + >>> smbclient("server1.domain.local", ssp=KerberosSSP(UPN="Administrator@domain.local", PASSWORD="password")) + +**smbclient using a** :class:`~scapy.layers.ntlm.KerberosSSP` **created by** `Ticketer++ `_: + +.. code:: python + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("Administrator@DOMAIN.LOCAL") + Enter password: ********** + >>> t.request_st(0, "host/server1.domain.local") + >>> smbclient("server1.domain.local", ssp=t.ssp(1)) + SMB authentication successful using KerberosSSP ! + +If you pay very close attention, you'll notice that in this case we aren't using the :class:`~scapy.layers.spnego.SPNEGOSSP` wrapper. You could have used ``ssp=SPNEGOSSP([t.ssp(1)])``. + +**smbclient forcing encryption**: + +.. code:: python + + >>> smbclient("server1.domain.local", "admin", REQUIRE_ENCRYPTION=True) + +.. note:: + + It is also possible to start the :class:`~scapy.layers.smbclient.smbclient` directly from the OS, using the following:: + + $ python3 -m scapy.layers.smbclient server1.domain.local Administrator@DOMAIN.LOCAL + + Use ``python3 -m scapy.layers.smbclient -h`` to see the list of available options. + + +Programmatically +________________ + +A cool feature of the :class:`~scapy.layers.smbclient.smbclient` is that all commands that you can call from the CLI, you can also call programmatically. + +Let's re-do the initial example programmatically, by turning off the CLI mode. Obviously prompting for passwords will not work so make sure the client has everything it needs for Session Setup. + +.. code:: python + + >>> from scapy.layers.smbclient import smbclient + >>> cli = smbclient("server1.domain.local", "Administrator@domain.local", password="password", cli=False) + >>> shares = cli.shares() + >>> shares + [('ADMIN$', 'DISKTREE', 'Remote Admin'), + ('C$', 'DISKTREE', 'Default share'), + ('common', 'DISKTREE', ''), + ('IPC$', 'IPC', 'Remote IPC'), + ('NETLOGON', 'DISKTREE', 'Logon server share '), + ('SYSVOL', 'DISKTREE', 'Logon server share '), + ('Users', 'DISKTREE', '')] + >>> cli.use('c$') + >>> cli.cd(r'Program Files\Microsoft') + >>> names = [x[0] for x in cli.ls()] + >>> names + ['.', '..', 'EdgeUpdater'] + +Mid-Level :class:`~scapy.layers.smbclient.SMB_SOCKET` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you know what you're doing, then the High-Level smbclient might not be enough for you. You can go a level lower using the :class:`~scapy.layers.smbclient.SMB_SOCKET`. +You can instantiate the object directly or via the :meth:`~scapy.layers.smbclient.SMB_SOCKET.from_tcpsock` helper. + +Let's write a script that connects to a share and list the files in the root folder. + +.. code:: python + + import socket + from scapy.layers.smbclient import SMB_SOCKET + from scapy.layers.spnego import SPNEGOSSP + from scapy.layers.ntlm import NTLMSSP, MD4le + from scapy.layers.kerberos import KerberosSSP + # Build SSP first. In SMB_SOCKET you have to do this yourself + password = "password" + ssp = SPNEGOSSP([ + NTLMSSP(UPN="Administrator", PASSWORD=password), + KerberosSSP( + UPN="Administrator@domain.local", + PASSWORD=password, + ) + ]) + # Connect to the server + sock = socket.socket() + sock.connect(("server1.domain.local", 445)) + smbsock = SMB_SOCKET.from_tcpsock(sock, ssp=ssp) + # Tree connect + tid = smbsock.tree_connect("C$") + smbsock.set_TID(tid) + # Open root folder and query files at root + fileid = smbsock.create_request('', type='folder') + files = smbsock.query_directory(fileid) + names = [x[0] for x in files] + # Close the handle + smbsock.close_request(fileid) + # Close the socket + smbsock.close() + +This has a lot more overhead so make sure you need it. + +Something hybrid that might be easier to use, is to access the underlying :class:`~scapy.layers.smbclient.SMB_SOCKET` in a higher-level :class:`~scapy.layers.smbclient.smbclient`: + +.. code:: python + + >>> from scapy.layers.smbclient import smbclient + >>> cli = smbclient("server1.domain.local", "Administrator@domain.local", password="password", cli=False) + >>> cli.use('c$') + >>> smbsock = cli.smbsock + >>> # Open root folder and query files at root + >>> fileid = smbsock.create_request('', type='folder') + >>> files = smbsock.query_directory(fileid) + >>> names = [x[0] for x in files] + +Low-Level :class:`~scapy.layers.smbclient.SMB_Client` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, it's also possible to call the underlying :attr:`~scapy.layers.smbclient.SMB_Client.smblink` socket directly. +Again, you can instantiate the object directly or via the :meth:`~scapy.layers.smbclient.SMB_Client.from_tcpsock` helper. + +.. code:: python + + >>> import socket + >>> from scapy.layers.smbclient import SMB_Client + >>> sock = socket.socket() + >>> sock.connect(("192.168.0.100", 445)) + >>> lowsmbsock = SMB_Client.from_tcpsock(sock, ssp=NTLMSSP(UPN="Administrator", PASSWORD="password")) + >>> resp = cli.sock.sr1(SMB2_Tree_Connect_Request(Path=r"\\server1\c$")) + +It's also accessible as the ``ins`` attribute of a ``SMB_SOCKET``, or the ``sock`` attribute of a ``smbclient``. + +.. code:: python + + >>> from scapy.layers.smbclient import smbclient + >>> cli = smbclient("server1.domain.local", "Administrator@domain.local", password="password", cli=False) + >>> lowsmbsock = cli.sock + >>> resp = cli.sock.sr1(SMB2_Tree_Connect_Request(Path=r"\\server1\c$")) + +.. _server: + +SMB 2/3 server +-------------- + +Scapy provides a SMB 2/3 server Automaton: :class:`~scapy.layers.smbserver.SMB_Server` + +.. image:: ../graphics/smb/smb_server.png + :align: center + +Once again, Scapy provides high level :class:`~scapy.layers.smbserver.smbserver` class that allows to spawn a SMB server. + +High-Level :class:`~scapy.layers.smbserver.smbserver` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`~scapy.layers.smbserver.smbserver` class allows to spawn a SMB server serving a selection of shares. +A share is identified by a ``name`` and a ``path`` (+ an optional description called ``remark``). + +**Start a SMB server with NTLM auth for 2 users:** + +.. code:: python + + smbserver( + shares=[SMBShare(name="Scapy", path="/tmp")], + iface="eth0", + ssp=NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password1"), + "Administrator": MD4le("Password2"), + }, + ) + ) + +**Start a SMB server with NTLM auth in an AD, using machine credentials:** + +.. note:: This requires an active account with ``WORKSTATION_TRUST_ACCOUNT`` in its ``userAccountControl``. (otherwise you might get ``STATUS_NO_TRUST_SAM_ACCOUNT``) + +.. code:: python + + smbserver(ssp=NTLMSSP_DOMAIN(UPN="Computer1@domain.local", HASHNT=bytes.fromhex("7facdc498ed1680c4fd1448319a8c04f"))) + +**Start a SMB server with Kerberos auth:** + +.. code:: python + + smbserver( + shares=[SMBShare(name="Scapy", path="/tmp")], + iface="eth0", + ssp=KerberosSSP( + KEY=Key( + EncryptionType.AES256_CTS_HMAC_SHA1_96, + key=bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000"), + ), + SPN="cifs/server.domain.local", + ), + ) + +**You can of course combine a NTLM and Kerberos server and provide them both over a** :class:`~scapy.layers.spnego.SPNEGOSSP`: + +.. code:: python + + smbserver( + shares=[SMBShare(name="Scapy", path="/tmp")], + iface="eth0", + ssp=SPNEGOSSP( + [ + KerberosSSP( + KEY=Key( + EncryptionType.AES256_CTS_HMAC_SHA1_96, + key=bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000"), + ), + SPN="cifs/server.domain.local", + ), + NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password1"), + "Administrator": MD4le("Password2"), + }, + ), + ] + ), + ) + + +.. note:: + By default, Scapy's SMB server is read-only. You can set ``readonly`` to ``False`` to disable it, as follows. + + +**Start a SMB server with NTLM in Read-Write mode** + +.. code:: python + + smbserver( + shares=[SMBShare(name="Scapy", path="/tmp")], + iface="eth0", + ssp=NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password1"), + "Administrator": MD4le("Password2"), + }, + ), + # Enable Read-Write + readonly=False, + ) + +**Start a SMB server requiring encryption (two methods)**: + +.. code:: python + + # Method 1: require encryption globally (available in SMB 3.0.0+) + >>> smbserver(..., REQUIRE_ENCRYPTION=True) + # Method 2: for a specific share (only available in SMB 3.1.1+) + >>> smbserver(..., shares=[SMBShare(name="Scapy", path="/tmp", encryptdata=True)]) + +.. note:: + + It is possible to start the :class:`~scapy.layers.smbserver.smbserver` (albeit only in unauthenticated mode) directly from the OS, using the following:: + + $ python3 -m scapy.layers.smbserver --port 12345 + + Use ``python3 -m scapy.layers.smbserver -h`` to see the list of available options. + + +Low-Level :class:`~scapy.layers.smbserver.SMB_Server` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change the functionality of the :class:`~scapy.layers.smbserver.SMB_Server`, you shall extend the server class (which is an automaton) and provide additional custom conditions (or overwrite existing ones). + +.. code:: python + + from scapy.layers.smbserver import SMB_Server + class MyCustomSMBServer(SMB_Server): + """ + Ridiculous demo SMB Server + + We overwrite the handler of "SMB Echo Request" to do some crazy stuff + """ + @ATMT.action(SMB_Server.receive_echo_request) + def send_echo_reply(self, pkt): + super(MyCustomSMBServer, self).send_echo_reply(pkt) # send echo response + print("WHAT? An ECHO REQUEST? You MUUUSST be a linux user then, since Windows NEEEVER sends those !") diff --git a/doc/scapy/layers/tcp.rst b/doc/scapy/layers/tcp.rst index 23bac6f5033..18972139ee1 100644 --- a/doc/scapy/layers/tcp.rst +++ b/doc/scapy/layers/tcp.rst @@ -61,6 +61,6 @@ Use external projects - `muXTCP`_ - Writing your own flexible Userland TCP/IP Stack - Ninja Style!!! - Integrating `pynids`_ -.. _Automata's documentation: ../advanced_usage#automata +.. _Automata's documentation: ../advanced_usage.html#automata .. _muXTCP: http://events.ccc.de/congress/2005/fahrplan/events/529.en.html .. _pynids: http://jon.oberheide.org/pynids/ diff --git a/doc/scapy/layers/tuntap.rst b/doc/scapy/layers/tuntap.rst new file mode 100644 index 00000000000..5926af2ebe7 --- /dev/null +++ b/doc/scapy/layers/tuntap.rst @@ -0,0 +1,222 @@ +******************** +TUN / TAP Interfaces +******************** + +.. note:: + + This module only works on BSD, Linux and macOS. + +TUN/TAP lets you create virtual network interfaces from userspace. There are two +types of devices: + +TUN devices + Operates at Layer 3 (:py:class:`IP`), and is generally limited to one + protocol. + +TAP devices + Operates at Layer 2 (:py:class:`Ether`), and allows you to use any Layer 3 + protocol (:py:class:`IP`, :py:class:`IPv6`, IPX, etc.) + +Requirements +============ + +FreeBSD + Requires the ``if_tap`` and ``if_tun`` kernel modules. + + See `tap(4)`__ and `tun(4)`__ manual pages for more information. + +Linux + Load the ``tun`` kernel module: + + .. code-block:: console + + # modprobe tun + + ``udev`` normally handles the creation of device nodes. + + See `networking/tuntap.txt`__ in the Linux kernel documentation for more + information. + +macOS + On macOS 10.14 and earlier, you need to install `tuntaposx`__. macOS + 10.14.5 and later will warn about the ``tuntaposx`` kexts not being + `notarised`__, but this works because it was built before 2019-04-07. + + On macOS 10.15 and later, you need to use a `notarized build`__ of + ``tuntaposx``. `Tunnelblick`__ (OpenVPN client) contains a notarized build + of ``tuntaposx`` `which can be extracted`__. + + .. note:: + + On macOS 10.13 and later, you need to `explicitly approve loading + each third-party kext for the first time`__. + +__ https://www.freebsd.org/cgi/man.cgi?query=tap&sektion=4 +__ https://www.freebsd.org/cgi/man.cgi?query=tun&sektion=4 +__ https://www.kernel.org/doc/Documentation/networking/tuntap.txt +__ http://tuntaposx.sourceforge.net/ +__ https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution?language=objc +__ https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution?language=objc +__ https://tunnelblick.net/downloads.html +__ https://sourceforge.net/p/tuntaposx/bugs/28/#ac64 +__ https://developer.apple.com/library/archive/technotes/tn2459/_index.html + + +Using TUN/TAP in Scapy +====================== + +.. tip:: + + Using TUN/TAP generally requires running Scapy (and these utilities) as + ``root``. + +:py:class:`TunTapInterface` lets you easily create a new device: + +.. code-block:: pycon3 + + >>> t = TunTapInterface('tun0') + +You'll then need to bring the interface up, and assign an IP address in another +terminal. + +Because TUN is a layer 3 connection, it acts as a point-to-point link. We'll +assign these parameters: + +* local address (for your machine): 192.0.2.1 +* remote address (for Scapy): 192.0.2.2 + +On Linux, you would use: + +.. code-block:: shell + + sudo ip link set tun0 up + sudo ip addr add 192.0.2.1 peer 192.0.2.2 dev tun0 + +On BSD and macOS, use: + +.. code-block:: shell + + sudo ifconfig tun0 up + sudo ifconfig tun0 192.0.2.1 192.0.2.2 + +Now, nothing will happen when you ping those addresses -- you'll need to make +Scapy respond to that traffic. + +:py:class:`TunTapInterface` works the same as a :py:class:`SuperSocket`, so lets +setup an :py:class:`AnsweringMachine` to respond to :py:class:`ICMP` +``echo-request``: + +.. code-block:: pycon3 + + >>> am = t.am(ICMPEcho_am) + >>> am() + +Now, you can ping Scapy in another terminal: + +.. code-block: console: + + $ ping -c 3 192.0.2.2 + PING 192.0.2.2 (192.0.2.2): 56 data bytes + 64 bytes from 192.0.2.2: icmp_seq=0 ttl=64 time=2.414 ms + 64 bytes from 192.0.2.2: icmp_seq=1 ttl=64 time=3.927 ms + 64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=5.740 ms + + --- 192.0.2.2 ping statistics --- + 3 packets transmitted, 3 packets received, 0.0% packet loss + round-trip min/avg/max/stddev = 2.414/4.027/5.740/1.360 ms + +You should see those packets show up in Scapy: + +.. code-block:: pycon3 + + >>> am() + Replying 192.0.2.1 to 192.0.2.2 + Replying 192.0.2.1 to 192.0.2.2 + Replying 192.0.2.1 to 192.0.2.2 + +You might have noticed that didn't configure Scapy with any IP address... and +there's a trick to this: :py:class:`ICMPEcho_am` swaps the ``source`` and +``destination`` fields of any :py:class:`Ether` and :py:class:`IP` headers on +the :py:class:`ICMP` packet that it receives. As a result, it actually responds +to *any* IP address. + +You can stop the :py:class:`ICMPEcho_am` AnsweringMachine with :kbd:`^C`. + +When you close Scapy, the ``tun0`` interface will automatically disappear. + +TunTapInterface reference +========================= + +.. py:class:: TunTapInterface(SimpleSocket) + + A socket to act as the remote side of a TUN/TAP interface. + + .. py:method:: __init__(iface: Text, [mode_tun], [strip_packet_info = True], [default_read_size = MTU]) + + :param Text iface: + The name of the interface to use, eg: ``tun0``. + + On BSD and macOS, this must start with either ``tun`` or ``tap``, + and have a corresponding :file:`/dev/` node (eg: :file:`/dev/tun0`). + + On Linux, this will be truncated to 16 bytes. + + :param bool mode_tun: + If True, create as TUN interface (layer 3). If False, creates a TAP + interface (layer 2). + + If not supplied, attempts to detect from the ``iface`` parameter. + + :param bool strip_packet_info: + If True (default), any :py:class:`TunPacketInfo` will be stripped + from the packet (so you get :py:class:`Ether` or :py:class:`IP`). + + Only Linux TUN interfaces have :py:class:`TunPacketInfo` available. + + This has no effect for interfaces that do not have + :py:class:`TunPacketInfo` available. + + :param int default_read_size: + Sets the default size that is read by + :py:meth:`SuperSocket.raw_recv` and :py:meth:`SuperSocket.recv`. + This defaults to :py:data:`scapy.data.MTU`. + + :py:class:`TunTapInterface` always adds overhead for + :py:class:`TunPacketInfo` headers, if required. + +.. py:class:: TunPacketInfo(Packet) + + Abstract class used to stack layer 3 protocols on a platform-specific + header. + + See :py:class:`LinuxTunPacketInfo` for an example. + + .. py:method:: guess_payload_class(payload) + + The default implementation expects the field ``proto`` to be declared, + with a value from :py:data:`scapy.data.ETHER_TYPES`. + +Linux-specific structures +------------------------- + +.. py:class:: LinuxTunPacketInfo(TunPacketInfo) + + Packet header used for Linux TUN packets. + + This is ``struct tun_pi``, declared in :file:`linux/if_tun.h`. + + .. py:attribute:: flags + + Flags to set on the packet. Only ``TUN_VNET_HDR`` is supported. + + .. py:attribute:: proto + + Layer 3 protocol number, per :py:data:`scapy.data.ETHER_TYPES`. + + Used by :py:meth:`TunTapPacketInfo.guess_payload_class`. + +.. py:class:: LinuxTunIfReq(Packet) + + Internal "packet" used for ``TUNSETIFF`` requests on Linux. + + This is ``struct ifreq``, declared in :file:`linux/if.h`. diff --git a/doc/scapy/routing.rst b/doc/scapy/routing.rst index 8e468696daa..2f9a9ec8010 100644 --- a/doc/scapy/routing.rst +++ b/doc/scapy/routing.rst @@ -1,25 +1,87 @@ -************* -Scapy routing -************* +******************* +Scapy network stack +******************* -Scapy needs to know many things related to the network configuration of your machine, to be able to route packets properly. For instance, the interface list, the IPv4 and IPv6 routes... +Scapy maintains its own network stack, which is independent from the one of your operating system. +It possesses its own *interfaces list*, *routing table*, *ARP cache*, *IPv6 neighbour* cache, *nameservers* config... and so on, all of which is configurable. -This means that Scapy has implemented bindings to get this information. Those bindings are OS specific. This will show you how to use it for a different usage. +Here are a few examples of where this is used:: + +- When you use ``sr()/send()``, Scapy will use internally its own routing table (``conf.route``) in order to find which interface to use, and eventually send an ARP request. +- When using ``dns_resolve()``, Scapy uses its own nameservers list (``conf.nameservers``) to perform the request +- etc. .. note:: - Scapy will have OS-specific functions underlying some high level functions. This page ONLY presents the cross platform ones + What's important to note is that Scapy initializes its own tables by querying the OS-specific ones. + It has therefore implemented bindings for Linux/Windows/BSD.. in order to retrieve such data, which may also be used as a high-level API, documented below. -List interfaces +Interfaces list --------------- -Use ``get_if_list()`` to get the interface list +Scapy stores its interfaces list in the :py:attr:`conf.ifaces ` object. +It provides a few utility functions such as :py:attr:`dev_from_networkname() `, :py:attr:`dev_from_name() ` or :py:attr:`dev_from_index() ` in order to access those. + +.. code-block:: pycon + + >>> conf.ifaces + Source Index Name MAC IPv4 IPv6 + sys 1 lo 00:00:00:00:00:00 127.0.0.1 ::1 + sys 2 eth0 Microsof:12:cb:ef 10.0.0.5 fe80::10a:2bef:dc12:afae + >>> conf.ifaces.dev_from_index(2) + + +You can also use the older ``get_if_list()`` function in order to only get the interface names. .. code-block:: pycon >>> get_if_list() ['lo', 'eth0'] +Extcap interfaces +~~~~~~~~~~~~~~~~~ + +Scapy supports sniffing on `Wireshark's extcap `_ interfaces. You can simply enable it using ``load_extcap()`` (from ``scapy.libs.extcap``). + +.. code-block:: pycon + + >>> load_extcap() + >>> conf.ifaces + Source Index Name Address + ciscodump 100 Cisco remote capture ciscodump + dpauxmon 100 DisplayPort AUX channel monitor capture dpauxmon + randpktdump 100 Random packet generator randpkt + sdjournal 100 systemd Journal Export sdjournal + sshdump 100 SSH remote capture sshdump + udpdump 100 UDP Listener remote capture udpdump + wifidump 100 Wi-Fi remote capture wifidump + Source Index Name MAC IPv4 IPv6 + sys 1 lo 00:00:00:00:00:00 127.0.0.1 ::1 + sys 2 eth0 Microsof:12:cb:ef 10.0.0.5 fe80::10a:2bef:dc12:afae + + +Here's an example of how to use `sshdump `_. As you can see you can pass arguments that are properly converted: + +.. code-block:: pycon + + >>> load_extcap() + >>> sniff( + ... iface="sshdump", + ... prn=lambda x: x.summary(), + ... remote_host="192.168.0.1", + ... remote_username="root", + ... remote_password="SCAPY", + ... ) + + +You can check the available options by using the following. + +.. code-block:: python + + >>> conf.ifaces.dev_from_networkname("sshdump").get_extcap_config() + +.. todo:: The sections below can be greatly improved. + IPv4 routes ----------- @@ -49,10 +111,10 @@ Get the route for a specific IP: :py:func:`conf.route.route() ` +Same as IPv4 but with :py:attr:`conf.route6 ` -Get router IP address ---------------------- +Get default gateway IP address +------------------------------ .. code-block:: pycon @@ -60,8 +122,8 @@ Get router IP address >>> gw '10.0.0.1' -Get local IP / IP of an interface ---------------------------------- +Get the IP of an interface +-------------------------- Use ``conf.iface`` @@ -72,8 +134,8 @@ Use ``conf.iface`` >>> ip '10.0.0.5' -Get local MAC / MAC of an interface ------------------------------------ +Get the MAC of an interface +--------------------------- .. code-block:: pycon @@ -82,8 +144,11 @@ Get local MAC / MAC of an interface >>> mac '54:3f:19:c9:38:6d' -Get MAC by IP -------------- +Get MAC address of the next hop to reach an IP +---------------------------------------------- + +This basically performs a cached ARP who-has when the IP is on the same local link, +returns the MAC of the gateway when it's not, and handle special cases like multicast. .. code-block:: pycon diff --git a/doc/scapy/sphinx_apidoc_postprocess.py b/doc/scapy/sphinx_apidoc_postprocess.py deleted file mode 100644 index 0bb37f274fa..00000000000 --- a/doc/scapy/sphinx_apidoc_postprocess.py +++ /dev/null @@ -1,55 +0,0 @@ -# This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license - -""" -Scapy's post process of sphinx-api command -""" - -import glob -import os -import sys - -files = list(glob.iglob('./api/*.rst')) -parents = set( - "%s.rst" % x for x in ( - f.rsplit('.', 1)[0] for f in ( - f[:-4] for f in files - ) - ) if x -) - -for f in files: - # Post process each file - _e = False - with open(f) as fd: - content = fd.readlines() - if f in parents: - # Process "parent files", i.e. with subfiles - # Remove sub categories (better indexation) - for name in ["Subpackages", "Submodules"]: - try: - sub = content.index(name+"\n") - except ValueError: - continue - del content[sub:sub+3] - _e = True - # Custom - if f.endswith("scapy.rst"): - content[0] = "Scapy API reference\n" - content[1] = "=" * (len(content[0]) - 1) + "\n" - for i, line in enumerate(content): - if "toctree" in line: - content[i] = line + " :titlesonly:\n" - _e = True - # File / module file - for name in ["package", "module"]: - if name in content[0]: - content[0] = content[0].replace(" " + name, "") - content[1] = "=" * (len(content[0]) - 1) + "\n" - _e = True - if _e: - print("Post-processed '%s'" % f) - with open(f, "w") as fd: - fd.writelines(content) diff --git a/doc/scapy/troubleshooting.rst b/doc/scapy/troubleshooting.rst index e094e6bd68a..4131ec280f6 100644 --- a/doc/scapy/troubleshooting.rst +++ b/doc/scapy/troubleshooting.rst @@ -8,14 +8,33 @@ FAQ I can't sniff/inject packets in monitor mode. --------------------------------------------- -The use monitor mode varies greatly depending on the platform. +The use monitor mode varies greatly depending on the platform, reasons are explained on the `Wireshark wiki `_: -- **Windows or *BSD or ``conf.use_pcap = True``** - ``libpcap`` must be called differently by Scapy in order for it to create the sockets in monitor mode. You will need to pass the ``monitor=True`` to any calls that open a socket (``send``, ``sniff``...) or to a Scapy socket that you create yourself (``conf.L2Socket``...) -- **Native Linux (with pcap disabled):** - You should set the interface in monitor mode on your own. Scapy provides utilitary functions: ``set_iface_monitor`` and ``get_iface_mode`` (linux only), that may be used (they do system calls to ``iwconfig`` and will restart the adapter). + *Unfortunately, changing the 802.11 capture modes is very platform/network adapter/driver/libpcap dependent, and might not be possible at all (Windows is very limited here).* -Note that many adapters do not support monitor mode, especially on Windows, or may incorrectly report the headers. See `the Wireshark doc about this `_ +Here is some guidance on how to properly use monitor mode with Scapy: + +- **Using Libpcap (or Npcap)**: + ``libpcap`` must be called differently by Scapy in order for it to create the sockets in monitor mode. You will need to pass the ``monitor=True`` to any calls that open a socket (``send``, ``sniff``...) or to a Scapy socket that you create yourself (``conf.L2Socket``...) + + **On Windows**, you additionally need to turn on monitor mode on the WiFi card, use:: + + # Of course, conf.iface can be replaced by any interfaces accessed through conf.ifaces + >>> conf.iface.setmonitor(True) + +- **Native Linux (with libpcap disabled):** + You should set the interface in monitor mode on your own. The easiest way to do that is to use ``airmon-ng``:: + + $ sudo airmon-ng start wlan0 + + You can also use:: + + $ iw dev wlan0 interface add mon0 type monitor + $ ifconfig mon0 up + + If you want to enable monitor mode manually, have a look at https://wiki.wireshark.org/CaptureSetup/WLAN#linux + +.. warning:: **If you are using Npcap:** please note that Npcap ``npcap-0.9983`` broke the 802.11 support until ``npcap-1.3.0``. Avoid using those versions. We make our best to make this work, if your adapter works with Wireshark for instance, but not with Scapy, feel free to report an issue. @@ -23,18 +42,89 @@ My TCP connections are reset by Scapy or by my kernel. ------------------------------------------------------ The kernel is not aware of what Scapy is doing behind his back. If Scapy sends a SYN, the target replies with a SYN-ACK and your kernel sees it, it will reply with a RST. To prevent this, use local firewall rules (e.g. NetFilter for Linux). Scapy does not mind about local firewalls. -I can't ping 127.0.0.1. Scapy does not work with 127.0.0.1 or on the loopback interface ---------------------------------------------------------------------------------------- +I can't ping 127.0.0.1 (or ::1). Scapy does not work with 127.0.0.1 (or ::1) on the loopback interface. +------------------------------------------------------------------------------------------------------- + +The loopback interface is a very special interface. Packets going through it are not really assembled and disassembled. The kernel routes the packet to its destination while it is still stored an internal structure. What you see with ```tcpdump -i lo``` is only a fake to make you think everything is normal. The kernel is not aware of what Scapy is doing behind his back, so what you see on the loopback interface is also a fake. Except this one did not come from a local structure. Thus the kernel will never receive it. -The loopback interface is a very special interface. Packets going through it are not really assembled and disassembled. The kernel routes the packet to its destination while it is still stored an internal structure. What you see with tcpdump -i lo is only a fake to make you think everything is normal. The kernel is not aware of what Scapy is doing behind his back, so what you see on the loopback interface is also a fake. Except this one did not come from a local structure. Thus the kernel will never receive it. +.. note:: Starting from Scapy > **2.5.0**, Scapy will automatically use ``L3RawSocket`` when necessary when using L3-functions (sr-like) on the loopback interface, when libpcap is not in use. -In order to speak to local applications, you need to build your packets one layer upper, using a PF_INET/SOCK_RAW socket instead of a PF_PACKET/SOCK_RAW (or its equivalent on other systems than Linux):: +**On Linux**, in order to speak to local IPv4 applications, you need to build your packets one layer upper, using a PF_INET/SOCK_RAW socket instead of a PF_PACKET/SOCK_RAW (or its equivalent on other systems than Linux):: >>> conf.L3socket - >>> conf.L3socket=L3RawSocket - >>> sr1(IP(dst="127.0.0.1")/ICMP()) + >>> conf.L3socket = L3RawSocket + >>> sr1(IP() / ICMP()) + > + +With IPv6, you can simply do:: + + # Layer 3 + >>> sr1(IPv6() / ICMPv6EchoRequest()) + > + + # Layer 2 + >>> srp1(Ether() / IPv6() / ICMPv6EchoRequest(), iface=conf.loopback_name) + >> + +.. warning:: + On Linux, libpcap does not support loopback IPv4 pings: + >>> conf.use_pcap = True + >>> sr1(IP() / ICMP()) + Begin emission: + Finished sending 1 packets. + ..................................... + + You can disable libpcap using ``conf.use_pcap = False`` or bypass it on layer 3 using ``conf.L3socket = L3RawSocket``. + +**On Windows, BSD, and macOS**, you must deactivate/configure the local firewall prior to using the following commands:: + + # Layer 3 + >>> sr1(IP() / ICMP()) > + >>> sr1(IPv6() / ICMPv6EchoRequest()) + > + + # Layer 2 + >>> srp1(Loopback() / IP() / ICMP(), iface=conf.loopback_name) + >> + >>> srp1(Loopback() / IPv6() / ICMPv6EchoRequest(), iface=conf.loopback_name) + >> + +Getting 'failed to set hardware filter to promiscuous mode' error +----------------------------------------------------------------- + +Disable promiscuous mode:: + + conf.sniff_promisc = False + +Scapy says there are 'Winpcap/Npcap conflicts' +---------------------------------------------- + +**On Windows**, as ``Winpcap`` is becoming old, it's recommended to use ``Npcap`` instead. ``Npcap`` is part of the ``Nmap`` project. + +.. note:: + This does NOT apply for Windows XP, which isn't supported by ``Npcap``. On XP, uninstall ``Npcap`` and keep ``Winpcap``. + +1. If you get the message ``'Winpcap is installed over Npcap.'`` it means that you have installed both Winpcap and Npcap versions, which isn't recommended. + +You may first **uninstall winpcap from your Program Files**, then you will need to remove some files that are not deleted by the ``Winpcap`` uninstaller:: + + C:/Windows/System32/wpcap.dll + C:/Windows/System32/Packet.dll + +And if you are on an x64 machine, additionally the 32-bit variants:: + + C:/Windows/SysWOW64/wpcap.dll + C:/Windows/SysWOW64/Packet.dll + +Once that is done, you'll be able to use ``Npcap`` properly. + +2. If you get the message ``'The installed Windump version does not work with Npcap'`` it means that you have probably installed an old version of ``Windump``, made for ``Winpcap``. +Download the one compatible with ``Npcap`` on https://github.com/hsluoyz/WinDump/releases + +In some cases, it could also mean that you had installed both ``Npcap`` and ``Winpcap``, and that the Npcap ``Windump`` is using ``Winpcap``. Fully delete ``Winpcap`` using the above method to solve the problem. + BPF filters do not work. I'm on a ppp link ------------------------------------------ diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index 40b5492b04f..1795d1058e2 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -8,7 +8,7 @@ Starting Scapy Scapy's interactive shell is run in a terminal session. Root privileges are needed to send the packets, so we're using ``sudo`` here:: - $ sudo ./scapy + $ sudo scapy -H Welcome to Scapy (2.4.0) >>> @@ -27,30 +27,13 @@ some features will not be available:: The basic features of sending and receiving packets should still work, though. - -Customizing the Terminal ------------------------- - -Before you actually start using Scapy, you may want to configure Scapy to properly render colors on your terminal. To do so, set ``conf.color_theme`` to one of of the following themes:: - - DefaultTheme, BrightTheme, RastaTheme, ColorOnBlackTheme, BlackAndWhite, HTMLTheme, LatexTheme - -For instance:: - - conf.color_theme = BrightTheme() - -.. image:: graphics/animations/animation-scapy-themes-demo.gif - :align: center - -Other parameters such as ``conf.prompt`` can also provide some customization. Note Scapy will update the shell automatically as soon as the ``conf`` values are changed. - - Interactive tutorial ==================== This section will show you several of Scapy's features with Python 2. Just open a Scapy session as shown above and try the examples yourself. +.. note:: You can configure the Scapy terminal by modifying the ``~/.config/scapy/prestart.py`` file. First steps ----------- @@ -174,6 +157,7 @@ pkt.decode_payload_as() changes the way the payload is decoded pkt.psdump() draws a PostScript diagram with explained dissection pkt.pdfdump() draws a PDF with explained dissection pkt.command() return a Scapy command that can generate the packet +pkt.json() return a JSON string representing the packet ======================= ==================================================== @@ -210,6 +194,17 @@ For the moment, we have only generated one packet. Let see how to specify sets o Some operations (like building the string from a packet) can't work on a set of packets. In these cases, if you forgot to unroll your set of packets, only the first element of the list you forgot to generate will be used to assemble the packet. +On the other hand, it is possible to move sets of packets into a `PacketList` object, which provides some operations on lists of packets. + +:: + + >>> p = PacketList(a) + >>> p + + >>> p = PacketList([p for p in a/c]) + >>> p + + =============== ==================================================== Command Effect =============== ==================================================== @@ -223,7 +218,7 @@ hexraw() returns a hexdump of the Raw layer of all packets padding() returns a hexdump of packets with padding nzpadding() returns a hexdump of packets with non-zero padding plot() plots a lambda function applied to the packet list -make table() displays a table according to a lambda function +make\_table() displays a table according to a lambda function =============== ==================================================== @@ -257,6 +252,31 @@ Now that we know how to manipulate packets. Let's see how to send them. The send Sent 1 packets. +.. _multicast: + +Multicast on layer 3: Scope Identifiers +--------------------------------------- + +.. index:: + single: Multicast + +.. note:: This feature is only available since Scapy 2.6.0. + +If you try to use multicast addresses (IPv4) or link-local addresses (IPv6), you'll notice that Scapy follows the routing table and takes the first entry. In order to specify which interface to use when looking through the routing table, Scapy supports scope identifiers (similar to RFC6874 but for both IPv6 and IPv4). + +.. code:: python + + >>> conf.checkIPaddr = False # answer IP will be != from the one we requested + # send on interface 'eth0' + >>> sr(IP(dst="224.0.0.1%eth0")/ICMP(), multi=True) + >>> sr(IPv6(dst="ff02::1%eth0")/ICMPv6EchoRequest(), multi=True) + +You can use both ``%eth0`` format or ``%15`` (the interface id) format. You can query those using ``conf.ifaces``. + +.. note:: + + Behind the scene, calling ``IP(dst="224.0.0.1%eth0")`` creates a ``ScopedIP`` object that contains ``224.0.0.1`` on the scope of the interface ``eth0``. If you are using an interface object (for instance ``conf.iface``), you can also craft that object. For instance:: + >>> pkt = IP(dst=ScopedIP("224.0.0.1", scope=conf.iface))/ICMP() Fuzzing ------- @@ -270,6 +290,19 @@ The function fuzz() is able to change any default value that is not to be calcul ................^C Sent 16 packets. +Injecting bytes +--------------- + +.. index:: + single: RawVal + +In a packet, each field has a specific type. For instance, the length field of the IP packet ``len`` expects an integer. More on that later. If you're developing a PoC, there are times where you'll want to inject some value that doesn't fit that type. This is possible using ``RawVal`` + +.. code:: + + >>> pkt = IP(len=RawVal(b"NotAnInteger"), src="127.0.0.1") + >>> bytes(pkt) + b'H\x00NotAnInt\x0f\xb3er\x00\x01\x00\x00@\x00\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00' Send and receive packets (sr) ----------------------------- @@ -384,7 +417,7 @@ The above will send a single SYN packet to Google's port 80 and will quit after From the above output, we can see Google returned “SA†or SYN-ACK flags indicating an open port. -Use either notations to scan ports 400 through 443 on the system: +Use either notations to scan ports 440 through 443 on the system: >>> sr(IP(dst="192.168.1.1")/TCP(sport=666,dport=(440,443),flags="S")) @@ -403,7 +436,7 @@ In order to quickly review responses simply request a summary of collected packe The above will display stimulus/response pairs for answered probes. We can display only the information we are interested in by using a simple loop: - >>> ans.summary( lambda(s,r): r.sprintf("%TCP.sport% \t %TCP.flags%") ) + >>> ans.summary( lambda s,r: r.sprintf("%TCP.sport% \t %TCP.flags%") ) 440 RA 441 RA 442 RA @@ -417,7 +450,7 @@ Even better, a table can be built using the ``make_table()`` function to display **.*.*..*.................. Received 362 packets, got 8 answers, remaining 1 packets >>> ans.make_table( - ... lambda(s,r): (s.dst, s.dport, + ... lambda s,r: (s.dst, s.dport, ... r.sprintf("{TCP:%TCP.flags%}{ICMP:%IP.src% - %ICMP.type%}"))) 66.35.250.150 192.168.1.1 216.109.112.135 22 66.35.250.150 - dest-unreach RA - @@ -428,17 +461,17 @@ The above example will even print the ICMP error type if the ICMP packet was rec For larger scans, we could be interested in displaying only certain responses. The example below will only display packets with the “SA†flag set:: - >>> ans.nsummary(lfilter = lambda (s,r): r.sprintf("%TCP.flags%") == "SA") + >>> ans.nsummary(lfilter = lambda s,r: r.sprintf("%TCP.flags%") == "SA") 0003 IP / TCP 192.168.1.100:ftp_data > 192.168.1.1:https S ======> IP / TCP 192.168.1.1:https > 192.168.1.100:ftp_data SA In case we want to do some expert analysis of responses, we can use the following command to indicate which ports are open:: - >>> ans.summary(lfilter = lambda (s,r): r.sprintf("%TCP.flags%") == "SA",prn=lambda(s,r):r.sprintf("%TCP.sport% is open")) + >>> ans.summary(lfilter = lambda s,r: r.sprintf("%TCP.flags%") == "SA",prn=lambda s,r: r.sprintf("%TCP.sport% is open")) https is open Again, for larger scans we can build a table of open ports:: - >>> ans.filter(lambda (s,r):TCP in r and r[TCP].flags&2).make_table(lambda (s,r): + >>> ans.filter(lambda s,r: TCP in r and r[TCP].flags&2).make_table(lambda s,r: ... (s.dst, s.dport, "X")) 66.35.250.150 192.168.1.1 216.109.112.135 80 X - X @@ -469,8 +502,8 @@ A TCP traceroute:: ***...... Received 33 packets, got 21 answers, remaining 1 packets >>> for snd,rcv in ans: - ... print snd.ttl, rcv.src, isinstance(rcv.payload, TCP) - ... + ... print(snd.ttl, rcv.src, isinstance(rcv.payload, TCP)) + ... 5 194.51.159.65 0 6 194.51.159.49 0 4 194.250.107.181 0 @@ -526,7 +559,7 @@ In this example, we used the `traceroute_map()` function to print the graphic. T It could have been done differently: >>> conf.geoip_city = "path/to/GeoLite2-City.mmdb" - >>> a = traceroute(["www.google.co.uk", "www.secdev.org"], verbose=0) + >>> a, _ = traceroute(["www.google.co.uk", "www.secdev.org"], verbose=0) >>> a.world_trace() or such as above: @@ -698,6 +731,8 @@ We can sniff and do passive OS fingerprinting:: The number before the OS guess is the accuracy of the guess. +.. note:: When sniffing on several interfaces (e.g. ``iface=["eth0", ...]``), you can check what interface a packet was sniffed on by using the ``sniffed_on`` attribute, as shown in one of the examples above. + Asynchronous Sniffing --------------------- @@ -758,9 +793,19 @@ Advanced Sniffing - Sniffing Sessions Scapy includes some basic Sessions, but it is possible to implement your own. Available by default: -- ``IPSession`` -> *defragment IP packets* on-the-flow, to make a stream usable by ``prn``. -- ``TCPSession`` -> *defragment certain TCP protocols**. Only **HTTP 1.0** currently uses this functionality. -- ``NetflowSession`` -> *resolve Netflow V9 packets* from their NetflowFlowset information objects +- :py:class:`~scapy.sessions.IPSession` -> *defragment IP packets* on-the-fly, to make a stream usable by ``prn``. +- :py:class:`~scapy.sessions.TCPSession` -> *defragment certain TCP protocols*. Currently supports: + - HTTP 1.0 + - TLS + - Kerberos + - LDAP + - SMB + - DCE/RPC + - Postgres + - DOIP + - and maybe other protocols if this page isn't up to date. +- :py:class:`~scapy.sessions.TLSSession` -> *matches TLS sessions* on the flow. +- :py:class:`~scapy.sessions.NetflowSession` -> *resolve Netflow V9 packets* from their NetflowFlowset information objects Those sessions can be used using the ``session=`` parameter of ``sniff()``. Examples:: @@ -770,7 +815,39 @@ Those sessions can be used using the ``session=`` parameter of ``sniff()``. Exam .. note:: To implement your own Session class, in order to support another flow-based protocol, start by copying a sample from `scapy/sessions.py `_ - Your custom ``Session`` class only needs to extend the ``DefaultSession`` class, and implement a ``on_packet_received`` function, such as in the example. + Your custom ``Session`` class only needs to extend the :py:class:`~scapy.sessions.DefaultSession` class, and implement a ``process`` or a ``recv`` function, such as in the examples. + + +.. warning:: + The inner workings of ``Session`` is currently UNSTABLE: custom Sessions may break in the future. + + +How to use TCPSession to defragment TCP packets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The layer on which the decompression is applied must be immediately following the TCP layer. You need to implement a class function called ``tcp_reassemble`` that accepts the binary data, a metadata dictionary as argument and returns, when full, a packet. Let's study the (pseudo) example of TLS: + +.. code:: + + class TLS(Packet): + [...] + + @classmethod + def tcp_reassemble(cls, data, metadata, session): + length = struct.unpack("!H", data[3:5])[0] + 5 + if len(data) == length: + return TLS(data) + + +In this example, we first get the total length of the TLS payload announced by the TLS header, and we compare it to the length of the data. When the data reaches this length, the packet is complete and can be returned. When implementing ``tcp_reassemble``, it's usually a matter of detecting when a packet isn't missing anything else. + +The ``data`` argument is bytes and the ``metadata`` argument is a dictionary which keys are as follow: + +- ``metadata["pay_class"]``: the TCP payload class (here TLS) +- ``metadata.get("tcp_psh", False)``: will be present if the PUSH flag is set +- ``metadata.get("tcp_end", False)``: will be present if the END or RESET flag is set + +If ``tcp_reassemble`` **returns any padding**, it will be kept for the next payload. Filters ------- @@ -914,59 +991,6 @@ We can reimport the produced binary string by selecting the appropriate first la \x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e \x1f !"#$%&\'()*+,-./01234567' |>>>> -Base64 -^^^^^^ - -Using the ``export_object()`` function, Scapy can export a base64 encoded Python data structure representing a packet:: - - >>> pkt - >>> - >>> export_object(pkt) - eNplVwd4FNcRPt2dTqdTQ0JUUYwN+CgS0gkJONFEs5WxFDB+CdiI8+pupVl0d7uzRUiYtcEGG4ST - OD1OnB6nN6c4cXrvwQmk2U5xA9tgO70XMm+1rA78qdzbfTP/lDfzz7tD4WwmU1C0YiaT2Gqjaiao - bMlhCrsUSYrYoKbmcxZFXSpPiohlZikm6ltb063ZdGpNOjWQ7mhPt62hChHJWTbFvb0O/u1MD2bT - WZXXVCmi9pihUqI3FHdEQslriiVfWFTVT9VYpog6Q7fsjG0qRWtQNwsW1fRTrUg4xZxq5pUx1aS6 - ... - -The output above can be reimported back into Scapy using ``import_object()``:: - - >>> new_pkt = import_object() - eNplVwd4FNcRPt2dTqdTQ0JUUYwN+CgS0gkJONFEs5WxFDB+CdiI8+pupVl0d7uzRUiYtcEGG4ST - OD1OnB6nN6c4cXrvwQmk2U5xA9tgO70XMm+1rA78qdzbfTP/lDfzz7tD4WwmU1C0YiaT2Gqjaiao - bMlhCrsUSYrYoKbmcxZFXSpPiohlZikm6ltb063ZdGpNOjWQ7mhPt62hChHJWTbFvb0O/u1MD2bT - WZXXVCmi9pihUqI3FHdEQslriiVfWFTVT9VYpog6Q7fsjG0qRWtQNwsW1fRTrUg4xZxq5pUx1aS6 - ... - >>> new_pkt - >>> - -Sessions -^^^^^^^^ - -At last Scapy is capable of saving all session variables using the ``save_session()`` function: - ->>> dir() -['__builtins__', 'conf', 'new_pkt', 'pkt', 'pkt_export', 'pkt_hex', 'pkt_raw', 'pkts'] ->>> save_session("session.scapy") - -Next time you start Scapy you can load the previous saved session using the ``load_session()`` command:: - - >>> dir() - ['__builtins__', 'conf'] - >>> load_session("session.scapy") - >>> dir() - ['__builtins__', 'conf', 'new_pkt', 'pkt', 'pkt_export', 'pkt_hex', 'pkt_raw', 'pkts'] - - Making tables ------------- @@ -979,7 +1003,7 @@ Here we can see a multi-parallel traceroute (Scapy already has a multi TCP trace >>> ans, unans = sr(IP(dst="www.test.fr/30", ttl=(1,6))/TCP()) Received 49 packets, got 24 answers, remaining 0 packets - >>> ans.make_table( lambda (s,r): (s.dst, s.ttl, r.src) ) + >>> ans.make_table( lambda s,r: (s.dst, s.ttl, r.src) ) 216.15.189.192 216.15.189.193 216.15.189.194 216.15.189.195 1 192.168.8.1 192.168.8.1 192.168.8.1 192.168.8.1 2 81.57.239.254 81.57.239.254 81.57.239.254 81.57.239.254 @@ -994,7 +1018,7 @@ Here is a more complex example to distinguish machines or their IP stacks from t >>> ans, unans = sr(IP(dst="172.20.80.192/28")/TCP(dport=[20,21,22,25,53,80])) Received 142 packets, got 25 answers, remaining 71 packets - >>> ans.make_table(lambda (s,r): (s.dst, s.dport, r.sprintf("%IP.id%"))) + >>> ans.make_table(lambda s,r: (s.dst, s.dport, r.sprintf("%IP.id%"))) 172.20.80.196 172.20.80.197 172.20.80.198 172.20.80.200 172.20.80.201 20 0 4203 7021 - 11562 21 0 4204 7022 - 11563 @@ -1044,7 +1068,7 @@ We can easily plot some harvested values using Matplotlib. (Make sure that you h For example, we can observe the IP ID patterns to know how many distinct IP stacks are used behind a load balancer:: >>> a, b = sr(IP(dst="www.target.com")/TCP(sport=[RandShort()]*1000)) - >>> a.plot(lambda x:x[1].id) + >>> a.plot(lambda q,r: r.id) [] .. image:: graphics/ipid.png @@ -1173,21 +1197,9 @@ Wireless frame injection single: FakeAP, Dot11, wireless, WLAN .. note:: - See the TroubleShooting section for more information on the usage of Monitor mode among Scapy. - -Provided that your wireless card and driver are correctly configured for frame injection - -:: - - $ iw dev wlan0 interface add mon0 type monitor - $ ifconfig mon0 up - -On Windows, if using Npcap, the equivalent would be to call:: + See the :doc:`TroubleShooting ` section for more information on the usage of Monitor mode among Scapy. - >>> # Of course, conf.iface can be replaced by any interfaces accessed through IFACES - ... conf.iface.setmonitor(True) - -you can have a kind of FakeAP:: +Provided that your wireless card and driver are correctly configured for frame injection, you can have a kind of FakeAP:: >>> sendp(RadioTap()/ Dot11(addr1="ff:ff:ff:ff:ff:ff", @@ -1249,15 +1261,15 @@ ARP Ping The fastest way to discover hosts on a local ethernet network is to use the ARP Ping method:: - >>> ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.0/24"),timeout=2) + >>> ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.0/24"), timeout=2) Answers can be reviewed with the following command:: - >>> ans.summary(lambda (s,r): r.sprintf("%Ether.src% %ARP.psrc%") ) + >>> ans.summary(lambda s,r: r.sprintf("%Ether.src% %ARP.psrc%") ) Scapy also includes a built-in arping() function which performs similar to the above two commands: - >>> arping("192.168.1.*") + >>> arping("192.168.1.0/24") ICMP Ping @@ -1265,11 +1277,11 @@ ICMP Ping Classical ICMP Ping can be emulated using the following command:: - >>> ans, unans = sr(IP(dst="192.168.1.1-254")/ICMP()) + >>> ans, unans = sr(IP(dst="192.168.1.0/24")/ICMP(), timeout=3) Information on live hosts can be collected with the following request:: - >>> ans.summary(lambda (s,r): r.sprintf("%IP.src% is alive") ) + >>> ans.summary(lambda s,r: r.sprintf("%IP.src% is alive") ) TCP Ping @@ -1277,11 +1289,11 @@ TCP Ping In cases where ICMP echo requests are blocked, we can still use various TCP Pings such as TCP SYN Ping below:: - >>> ans, unans = sr( IP(dst="192.168.1.*")/TCP(dport=80,flags="S") ) + >>> ans, unans = sr( IP(dst="192.168.1.0/24")/TCP(dport=80,flags="S") ) Any response to our probes will indicate a live host. We can collect results with the following command:: - >>> ans.summary( lambda(s,r) : r.sprintf("%IP.src% is alive") ) + >>> ans.summary( lambda s,r : r.sprintf("%IP.src% is alive") ) UDP Ping @@ -1291,9 +1303,9 @@ If all else fails there is always UDP Ping which will produce ICMP Port unreacha >>> ans, unans = sr( IP(dst="192.168.*.1-10")/UDP(dport=0) ) -Once again, results can be collected with this command: +Once again, results can be collected with this command:: - >>> ans.summary( lambda(s,r) : r.sprintf("%IP.src% is alive") ) + >>> ans.summary( lambda s,r : r.sprintf("%IP.src% is alive") ) DNS Requests @@ -1304,21 +1316,21 @@ DNS Requests This will perform a DNS request looking for IPv4 addresses >>> ans = sr1(IP(dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="secdev.org",qtype="A"))) - >>> ans.an.rdata + >>> ans.an[0].rdata '217.25.178.5' **SOA request:** >>> ans = sr1(IP(dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="secdev.org",qtype="SOA"))) - >>> ans.ns.mname + >>> ans.an[0].mname b'dns.ovh.net.' - >>> ans.ns.rname + >>> ans.an[0].rname b'tech.ovh.net.' **MX request:** >>> ans = sr1(IP(dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="google.com",qtype="MX"))) - >>> results = [x.exchange for x in ans.an.iterpayloads()] + >>> results = [x.exchange for x in ans.an] >>> results [b'alt1.aspmx.l.google.com.', b'alt4.aspmx.l.google.com.', @@ -1364,6 +1376,18 @@ ARP cache poisoning with double 802.1q encapsulation:: /ARP(op="who-has", psrc=gateway, pdst=client), inter=RandNum(10,40), loop=1 ) +ARP MitM +-------- +This poisons the cache of 2 machines, then answers all following ARP requests to put the host between. +Calling ctrl^C will restore the connection. + +:: + + $ sysctl net.ipv4.conf.virbr0.send_redirects=0 # virbr0 = interface + $ sysctl net.ipv4.ip_forward=1 + $ sudo scapy + >>> arp_mitm("192.168.122.156", "192.168.122.17") + TCP Port Scanning ----------------- @@ -1376,7 +1400,7 @@ Possible result visualization: open ports :: - >>> res.nsummary( lfilter=lambda (s,r): (r.haslayer(TCP) and (r.getlayer(TCP).flags & 2)) ) + >>> res.nsummary( lfilter=lambda s,r: (r.haslayer(TCP) and (r.getlayer(TCP).flags & 2)) ) IKE Scanning @@ -1385,16 +1409,81 @@ IKE Scanning We try to identify VPN concentrators by sending ISAKMP Security Association proposals and receiving the answers:: - >>> res, unans = sr( IP(dst="192.168.1.*")/UDP() + >>> res, unans = sr( IP(dst="192.168.1.0/24")/UDP() /ISAKMP(init_cookie=RandString(8), exch_type="identity prot.") /ISAKMP_payload_SA(prop=ISAKMP_payload_Proposal()) ) Visualizing the results in a list:: - >>> res.nsummary(prn=lambda (s,r): r.src, lfilter=lambda (s,r): r.haslayer(ISAKMP) ) - - + >>> res.nsummary(prn=lambda s,r: r.src, lfilter=lambda s,r: r.haslayer(ISAKMP) ) + + +DNS server +---------- + +By default, ``dnsd`` uses a joker (IPv4 only): it answers to all unknown servers with the joker. See :class:`~scapy.layers.dns.DNS_am`:: + + >>> dnsd(iface="tap0", match={"google.com": "1.1.1.1"}, joker="192.168.1.1") + +You can also use ``relay=True`` to replace the joker behavior with a forward to a server included in ``conf.nameservers``. + +mDNS server +------------ + +See :class:`~scapy.layers.dns.mDNS_am`:: + + >>> mdnsd(iface="eth0", joker="192.168.1.1") + +Note that ``mdnsd`` extends the ``dnsd`` API. + +LLMNR server +------------ + +See :class:`~scapy.layers.llmnr.LLMNR_am`:: + + >>> conf.iface = "tap0" + >>> llmnrd(iface="tap0", from_ip=Net("10.0.0.1/24")) + +Note that ``llmnrd`` extends the ``dnsd`` API. + +Netbios server +-------------- + +See :class:`~scapy.layers.netbios.NBNS_am`:: + + >>> nbnsd(iface="eth0") # With local IP + >>> nbnsd(iface="eth0", ip="192.168.122.17") # With some other IP + +Node status request (get NetbiosName from IP) +--------------------------------------------- + +.. code:: + + >>> sr1(IP(dst="192.168.122.17")/UDP()/NBNSHeader()/NBNSNodeStatusRequest()) + +NBNS Query Request (find by NetbiosName) +---------------------------------------- + +.. code:: + + >>> conf.checkIPaddr = False # Mandatory because we are using a broadcast destination and receiving unicast + >>> sr1(IP(dst="192.168.0.255")/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME="DC1")) + +mDNS Query Request +------------------ + +For instance, find all spotify connect devices. + +.. code:: + + >>> # For interface 'eth0' + >>> ans, _ = sr(IPv6(dst="ff02::fb%eth0")/UDP(sport=5353, dport=5353)/DNS(rd=0, qd=[DNSQR(qname='_spotify-connect._tcp.local', qtype="PTR")]), multi=True, timeout=2) + >>> ans.show() + +.. note:: + + As you can see, we used a scope identifier (``%eth0``) to specify on which interface we want to use the above multicast IP. Advanced traceroute ------------------- @@ -1408,7 +1497,7 @@ TCP SYN traceroute Results would be:: - >>> ans.summary( lambda(s,r) : r.sprintf("%IP.src%\t{ICMP:%ICMP.type%}\t{TCP:%TCP.flags%}")) + >>> ans.summary( lambda s,r: r.sprintf("%IP.src%\t{ICMP:%ICMP.type%}\t{TCP:%TCP.flags%}")) 192.168.1.1 time-exceeded 68.86.90.162 time-exceeded 4.79.43.134 time-exceeded @@ -1430,7 +1519,7 @@ NTP, etc.) to deserve an answer:: We can visualize the results as a list of routers:: - >>> res.make_table(lambda (s,r): (s.dst, s.ttl, r.src)) + >>> res.make_table(lambda s,r: (s.dst, s.ttl, r.src)) DNS traceroute @@ -1489,9 +1578,10 @@ Wireless sniffing The following command will display information similar to most wireless sniffers:: ->>> sniff(iface="ath0", monitor=True, prn=lambda x:x.sprintf("{Dot11Beacon:%Dot11.addr3%\t%Dot11Beacon.info%\t%PrismHeader.channel%\t%Dot11Beacon.cap%}")) +>>> sniff(iface="ath0", prn=lambda x:x.sprintf("{Dot11Beacon:%Dot11.addr3%\t%Dot11Beacon.info%\t%PrismHeader.channel%\t%Dot11Beacon.cap%}")) -Note the `monitor=True` argument, which only work from scapy>2.4.0 (2.4.0dev+), that is cross-platform. It will in work in most cases (Windows, OSX), but might require you to manually toggle monitor mode. +.. note:: + On Windows and OSX, you will need to also use `monitor=True`, which only works on scapy>2.4.0 (2.4.0dev+). This might require you to manually toggle monitor mode. The above command will produce output similar to the one below:: @@ -1538,7 +1628,7 @@ Solution Use Scapy to send a DHCP discover request and analyze the replies:: >>> conf.checkIPaddr = False - >>> fam,hw = get_if_raw_hwaddr(conf.iface) + >>> hw = get_if_hwaddr(conf.iface) >>> dhcp_discover = Ether(dst="ff:ff:ff:ff:ff:ff")/IP(src="0.0.0.0",dst="255.255.255.255")/UDP(sport=68,dport=67)/BOOTP(chaddr=hw)/DHCP(options=[("message-type","discover"),"end"]) >>> ans, unans = srp(dhcp_discover, multi=True) # Press CTRL-C after several seconds Begin emission: @@ -1554,7 +1644,7 @@ In this case we got 2 replies, so there were two active DHCP servers on the test We are only interested in the MAC and IP addresses of the replies: - >>> for p in ans: print p[1][Ether].src, p[1][IP].src + >>> for p in ans: print(p[1][Ether].src, p[1][IP].src) ... 00:de:ad:be:ef:00 192.168.1.1 00:11:11:22:22:33 192.168.1.11 @@ -1580,16 +1670,16 @@ Firewalking TTL decrementation after a filtering operation only not filtered packets generate an ICMP TTL exceeded - >>> ans, unans = sr(IP(dst="172.16.4.27", ttl=16)/TCP(dport=(1,1024))) - >>> for s,r in ans: - if r.haslayer(ICMP) and r.payload.type == 11: - print s.dport + >>> ans, unans = sr(IP(dst="172.16.4.27", ttl=16)/TCP(dport=(1,1024))) + >>> for s,r in ans: + ... if r.haslayer(ICMP) and r.payload.type == 11: + ... print(s.dport) Find subnets on a multi-NIC firewall only his own NIC’s IP are reachable with this TTL:: - >>> ans, unans = sr(IP(dst="172.16.5/24", ttl=15)/TCP()) - >>> for i in unans: print i.dst + >>> ans, unans = sr(IP(dst="172.16.5/24", ttl=15)/TCP()) + >>> for i in unans: print(i.dst) TCP Timestamp Filtering @@ -1689,6 +1779,67 @@ Discussion __ https://www.wireshark.org __ https://wiki.wireshark.org/ProtocolReference +Performance of Scapy +-------------------- + +Problem +^^^^^^^ + +Scapy dissects slowly and/or misses packets under heavy loads. + +.. note:: + + Please bear in mind that Scapy is not designed to be blazing fast, but rather easily hackable & extensible. The packet model makes it VERY easy to create new layers, compared to pretty much all other alternatives, but comes with a performance cost. Of course, we still do our best to make Scapy as fast as possible, but it's not the absolute main goal. + +Solution +^^^^^^^^ + +There are quite a few ways of speeding up scapy's dissection. You can use all of them + +- **Using a BPF filter**: The OS is faster than Scapy. If you make the OS filter the packets instead of Scapy, it will only handle a fraction of the load. Use the ``filter=`` argument of the :py:func:`~scapy.sendrecv.sniff` function. +- **By disabling layers you don't use**: If you are not using some layers, why dissect them? You can let Scapy know which layers to dissect and all the others will simply be parsed as ``Raw``. This comes with a great performance boost but requires you to know what you're doing. + +.. code:: python + + # Enable filtering: only Ether, IP and ICMP will be dissected + conf.layers.filter([Ether, IP, ICMP]) + # Disable filtering: restore everything to normal + conf.layers.unfilter() + +Very slow start because of big routes +------------------------------------- + +Problem +^^^^^^^ + +Scapy takes ages to start because you have very big routing tables. + +Solution +^^^^^^^^ + +Disable the auto-loading of the routing tables: + +**CLI:** in ``~/.config/scapy/prestart.py`` add: + +.. code:: python + + conf.route_autoload = False + conf.route6_autoload = False + +**Programmatically:** + +.. code:: python + + # Before any other Scapy import + from scapy.config import conf + conf.route_autoload = False + conf.route6_autoload = False + # Import Scapy here + from scapy.all import * + +At anytime, you can trigger the routes loading using ``conf.route.resync()`` or ``conf.route6.resync()``, or add the routes yourself `as shown here <#routing>`_. + + OS Fingerprinting ----------------- diff --git a/doc/scapy_version_timeline.ods b/doc/scapy_version_timeline.ods deleted file mode 100644 index 10e0bca6960..00000000000 Binary files a/doc/scapy_version_timeline.ods and /dev/null differ diff --git a/doc/vagrant_ci/Vagrantfile b/doc/vagrant_ci/Vagrantfile index 5c5de0e5bab..0ba833fd421 100644 --- a/doc/vagrant_ci/Vagrantfile +++ b/doc/vagrant_ci/Vagrantfile @@ -1,25 +1,30 @@ # -*- mode: ruby -*- # vi: set ft=ruby : +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license Vagrant.configure("2") do |config| + config.vm.provider "virtualbox" do |vb| + vb.memory = 1024 + vb.cpus = 2 + end + config.vm.define "openbsd" do |bsd| - bsd.vm.box = "generic/openbsd6" + bsd.vm.box = "generic/openbsd7" bsd.vm.provision "shell", path: "provision_openbsd.sh" end config.vm.define "freebsd" do |bsd| - bsd.vm.box = "freebsd/FreeBSD-11.3-RELEASE" + bsd.vm.box = "freebsd/FreeBSD-14.0-RELEASE" bsd.vm.provision "shell", path: "provision_freebsd.sh" end config.vm.define "netbsd" do |bsd| - bsd.vm.box = "generic/netbsd8" + bsd.vm.box = "generic/netbsd9" bsd.vm.provision "shell", path: "provision_netbsd.sh" end diff --git a/doc/vagrant_ci/provision_freebsd.sh b/doc/vagrant_ci/provision_freebsd.sh index 1471ac8d313..56a9b92203d 100644 --- a/doc/vagrant_ci/provision_freebsd.sh +++ b/doc/vagrant_ci/provision_freebsd.sh @@ -1,15 +1,19 @@ -#!/bin/bash +#!/usr/local/bin/bash +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license -sudo pkg install --yes git python2 python3 py27-virtualenv bash +PACKAGES="git python39 python311 py39-virtualenv py39-pip py39-sqlite3 py311-sqlite3 bash rust sudo" + +pkg update +pkg install --yes $PACKAGES bash git clone https://github.com/secdev/scapy cd scapy export PATH=/usr/local/bin/:$PATH -virtualenv-2.7 -p python2.7 venv +virtualenv-3.9 -p python3.9 venv source venv/bin/activate pip install tox +chown -R vagrant:vagrant /home/vagrant/scapy diff --git a/doc/vagrant_ci/provision_netbsd.sh b/doc/vagrant_ci/provision_netbsd.sh index 69b213126ae..a4d59bd27e4 100644 --- a/doc/vagrant_ci/provision_netbsd.sh +++ b/doc/vagrant_ci/provision_netbsd.sh @@ -1,19 +1,22 @@ #!/bin/bash +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license + +RELEASE="9.0_2022Q2" +PACKAGES="git python27 python39 py39-virtualenv py27-sqlite3 py39-sqlite3 py39-expat rust mozilla-rootcerts-openssl" sudo -s unset PROMPT_COMMAND export PATH="/sbin:/usr/pkg/sbin:/usr/pkg/bin:$PATH" -export PKG_PATH="http://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/amd64/8.0_2018Q4/All/" +export PKG_PATH="http://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/amd64/${RELEASE}/All/" pkg_delete curl -pkg_add git python27 python36 py27-virtualenv py36-expat -git -c http.sslVerify=false clone https://github.com/secdev/scapy +pkg_add -u $PACKAGES +git clone https://github.com/secdev/scapy cd scapy -virtualenv-2.7 venv +virtualenv-3.9 venv . venv/bin/activate pip install tox chown -R vagrant:vagrant ../scapy/ diff --git a/doc/vagrant_ci/provision_openbsd.sh b/doc/vagrant_ci/provision_openbsd.sh index dad61e40f49..1759249f391 100644 --- a/doc/vagrant_ci/provision_openbsd.sh +++ b/doc/vagrant_ci/provision_openbsd.sh @@ -1,17 +1,19 @@ #!/bin/bash +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license -sudo pkg_add git python-2.7.15p0 python-3.6.6p1 py-virtualenv +PACKAGES="git python3 py3-virtualenv py3-cryptography" + +sudo pkg_add $PACKAGES sudo mkdir -p /usr/local/test/ sudo chown -R vagrant:vagrant /usr/local/test/ cd /usr/local/test/ git clone https://github.com/secdev/scapy cd scapy -virtualenv venv +virtualenv --system-site-packages venv source venv/bin/activate pip install tox sudo chown -R vagrant:vagrant /usr/local/test/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..1d5ffabfc4f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[build-system] +requires = [ "setuptools>=62.0.0" ] +build-backend = "setuptools.build_meta" + +[project] +name = "scapy" +dynamic = [ "version", "readme" ] +authors = [ + { name="Philippe BIONDI" }, + { name="Gabriel POTTER" }, +] +maintainers = [ + { name="Pierre LALET" }, + { name="Gabriel POTTER" }, + { name="Guillaume VALADON" }, + { name="Nils WEISS" }, +] +license = "GPL-2.0-only" +requires-python = ">=3.7, <4" +description = "Scapy: interactive packet manipulation tool" +keywords = [ "network" ] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "Intended Audience :: System Administrators", + "Intended Audience :: Telecommunications Industry", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Security", + "Topic :: System :: Networking", + "Topic :: System :: Networking :: Monitoring", +] + +[project.urls] +Homepage = "https://scapy.net" +Download = "https://github.com/secdev/scapy/tarball/master" +Documentation = "https://scapy.readthedocs.io" +"Source Code" = "https://github.com/secdev/scapy" +Changelog = "https://github.com/secdev/scapy/releases" + +[project.scripts] +scapy = "scapy.main:interact" + +[project.optional-dependencies] +cli = [ "ipython" ] +all = [ + "ipython", + "pyx", + "cryptography>=2.0", + "matplotlib", +] +doc = [ + "cryptography>=2.0", + "sphinx>=7.0.0", + "sphinx_rtd_theme>=1.3.0", + "tox>=3.0.0", +] + +# setuptools specific + +[tool.setuptools.package-data] +"scapy" = ["py.typed"] + +[tool.setuptools.packages.find] +include = [ + "scapy*", +] +exclude = [ + "test*", + "doc*", +] + +[tool.setuptools.dynamic] +version = { attr="scapy.VERSION" } + +# coverage + +[tool.coverage.run] +concurrency = [ "thread", "multiprocessing" ] +source = [ "scapy" ] +omit = [ + # Scapy tools + "scapy/tools/", + # Scapy external modules + "scapy/libs/ethertypes.py", + "scapy/libs/manuf.py", + "scapy/libs/winpcapy.py", +] diff --git a/run_scapy b/run_scapy index 86efae75df4..87b6f50e91d 100755 --- a/run_scapy +++ b/run_scapy @@ -2,16 +2,12 @@ DIR=$(dirname "$0") if [ -z "$PYTHON" ] then - ARGS=$(getopt 23 "$*" 2> /dev/null) - for arg in $ARGS - do - case $arg - in - -2) PYTHON=python2; shift;; - -3) PYTHON=python3; shift;; - --) break;; - esac - done PYTHON=${PYTHON:-python3} fi -PYTHONPATH=$DIR exec "$PYTHON" -m scapy "$@" +$PYTHON --version > /dev/null 2>&1 +if [ ! $? -eq 0 ] +then + echo "WARNING: '$PYTHON' not found, using 'python' instead." + PYTHON=python +fi +PYTHONPATH=$DIR exec "$PYTHON" -m scapy $@ diff --git a/run_scapy.bat b/run_scapy.bat index 332df0201a8..d801bbdc0e3 100644 --- a/run_scapy.bat +++ b/run_scapy.bat @@ -1,8 +1,23 @@ @echo off +setlocal set PYTHONPATH=%~dp0 -IF "%PYTHON%" == "" set PYTHON=python3 +REM shift will not work with %* +set "_args=%*" +IF "%PYTHON%" == "" set PYTHON=py WHERE %PYTHON% >nul 2>&1 -IF %ERRORLEVEL% NEQ 0 set PYTHON=python -%PYTHON% -m scapy %* +IF %ERRORLEVEL% NEQ 0 set PYTHON= +IF "%1" == "-3" ( + if "%PYTHON%" == "py" ( + set "PYTHON=py -3" + ) else ( + set PYTHON=python3 + ) + set "_args=%_args:~3%" +) else ( + IF "%PYTHON%" == "" set PYTHON=python3 + WHERE %PYTHON% >nul 2>&1 + IF %ERRORLEVEL% NEQ 0 set PYTHON=python +) +%PYTHON% -m scapy %_args% title Scapy - dead PAUSE \ No newline at end of file diff --git a/run_scapy_py2.bat b/run_scapy_py2.bat deleted file mode 100644 index 857c39b1226..00000000000 --- a/run_scapy_py2.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -set PYTHONPATH=%~dp0 -set PYTHON=python -call run_scapy.bat \ No newline at end of file diff --git a/run_scapy_py3.bat b/run_scapy_py3.bat deleted file mode 100644 index 89490be5629..00000000000 --- a/run_scapy_py3.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -set PYTHONPATH=%~dp0 -set PYTHON=python3 -call run_scapy.bat \ No newline at end of file diff --git a/scapy/__init__.py b/scapy/__init__.py index d2a1e43c937..139ba7a9371 100644 --- a/scapy/__init__.py +++ b/scapy/__init__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Scapy: create, send, sniff, dissect and manipulate network packets. @@ -10,18 +10,77 @@ https://scapy.net """ +import datetime import os import re import subprocess -from scapy.compat import AnyStr - +__all__ = [ + "VERSION", + "__version__", +] _SCAPY_PKG_DIR = os.path.dirname(__file__) +def _parse_tag(tag): + # type: (str) -> str + """ + Parse a tag from ``git describe`` into a version. + + Example:: + + v2.3.2-346-g164a52c075c8 -> '2.3.2.post346' + """ + match = re.match('^v?(.+?)-(\\d+)-g[a-f0-9]+$', tag) + if match: + # remove the 'v' prefix and add a '.postN' suffix + return '%s.post%s' % (match.group(1), match.group(2)) + else: + match = re.match('^v?([\\d\\.]+(rc\\d+)?)$', tag) + if match: + # tagged release version + return '%s' % (match.group(1)) + else: + raise ValueError('tag has invalid format') + + +def _version_from_git_archive(): + # type: () -> str + """ + Rely on git archive "export-subst" git attribute. + See 'man gitattributes' for more details. + Note: describe is only supported with git >= 2.32.0, + and the `tags=true` option with git >= 2.35.0 but we + use it to workaround GH#3121. + """ + git_archive_id = '$Format:%ct %(describe:tags=true)$'.split() + tstamp = git_archive_id[0] + if len(git_archive_id) > 1: + tag = git_archive_id[1] + else: + # project is run in CI and has another %(describe) + tag = "" + + if "Format" in tstamp: + raise ValueError('not a git archive') + + if "describe" in tag: + # git is too old! + tag = "" + if tag: + # archived revision is tagged, use the tag + return _parse_tag(tag) + elif tstamp: + # archived revision is not tagged, use the commit date + d = datetime.datetime.fromtimestamp(int(tstamp), datetime.timezone.utc) + return d.strftime('%Y.%m.%d') + + raise ValueError("invalid git archive format") + + def _version_from_git_describe(): - # type: () -> AnyStr + # type: () -> str """ Read the version from ``git describe``. It returns the latest tag with an optional suffix if the current directory is not exactly on the tag. @@ -34,13 +93,13 @@ def _version_from_git_describe(): The tag prefix (``v``) and the git commit sha1 (``-g164a52c075c8``) are removed if present. - If the current directory is not exactly on the tag, a ``.devN`` suffix is + If the current directory is not exactly on the tag, a ``.postN`` suffix is appended where N is the number of commits made after the last tag. Example:: >>> _version_from_git_describe() - '2.3.2.dev346' + '2.3.2.post346' :raises CalledProcessError: if git is unavailable :return: Scapy's latest tag @@ -49,6 +108,7 @@ def _version_from_git_describe(): raise ValueError('not in scapy git repo') def _git(cmd): + # type: (str) -> str process = subprocess.Popen( cmd.split(), cwd=_SCAPY_PKG_DIR, @@ -61,18 +121,12 @@ def _git(cmd): else: raise subprocess.CalledProcessError(process.returncode, err) - tag = _git("git describe --always") + tag = _git("git describe --tags --always --long") if not tag.startswith("v"): # Upstream was not fetched commit = _git("git rev-list --tags --max-count=1") tag = _git("git describe --tags --always --long %s" % commit) - match = re.match('^v?(.+?)-(\\d+)-g[a-f0-9]+$', tag) - if match: - # remove the 'v' prefix and add a '.devN' suffix - return '%s.dev%s' % (match.group(1), match.group(2)) - else: - # just remove the 'v' prefix - return re.sub('^v', '', tag) + return _parse_tag(tag) def _version(): @@ -81,37 +135,52 @@ def _version(): :return: the Scapy version """ + # Method 0: from external packaging + try: + # possibly forced by external packaging + return os.environ['SCAPY_VERSION'] + except KeyError: + pass + + # Method 1: from the VERSION file, included in sdist and wheels version_file = os.path.join(_SCAPY_PKG_DIR, 'VERSION') try: - tag = _version_from_git_describe() - # successfully read the tag from git, write it in VERSION for - # installation and/or archive generation. - with open(version_file, 'w') as fdesc: - fdesc.write(tag) + # file generated when running sdist + with open(version_file, 'r') as fdsec: + tag = fdsec.read() return tag + except (FileNotFoundError, NotADirectoryError): + pass + + # Method 2: from the archive tag, exported when using git archives + try: + return _version_from_git_archive() + except ValueError: + pass + + # Method 3: from git itself, used when Scapy was cloned + try: + return _version_from_git_describe() except Exception: - # failed to read the tag from git, try to read it from a VERSION file - try: - with open(version_file, 'r') as fdsec: - tag = fdsec.read() - return tag - except Exception: - # Rely on git archive "export-subst" git attribute. - # See 'man gitattributes' for more details. - git_archive_id = '$Format:%h %d$' - sha1 = git_archive_id.strip().split()[0] - match = re.search('tag:(\\S+)', git_archive_id) - if match: - return "git-archive.dev" + match.group(1) - elif sha1: - return "git-archive.dev" + sha1 - else: - return 'unknown.version' + pass + + # Fallback + try: + # last resort, use the modification date of __init__.py + d = datetime.datetime.fromtimestamp( + os.path.getmtime(__file__), datetime.timezone.utc + ) + return d.strftime('%Y.%m.%d') + except Exception: + pass + + # all hope is lost + return '0.0.0' VERSION = __version__ = _version() -_tmp = re.search(r"[0-9.]+", VERSION) +_tmp = re.search(r"([0-9]|\.[0-9])+", VERSION) VERSION_MAIN = _tmp.group() if _tmp is not None else VERSION if __name__ == "__main__": diff --git a/scapy/__main__.py b/scapy/__main__.py index 5a08e1347c8..9d06883d742 100644 --- a/scapy/__main__.py +++ b/scapy/__main__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Scapy: create, send, sniff, dissect and manipulate network packets. diff --git a/scapy/all.py b/scapy/all.py index 4f438461402..1c67e8b06ad 100644 --- a/scapy/all.py +++ b/scapy/all.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Aggregate top level objects from all Scapy modules. @@ -14,6 +14,7 @@ from scapy.error import * from scapy.themes import * from scapy.arch import * +from scapy.interfaces import * from scapy.plist import * from scapy.fields import * diff --git a/scapy/ansmachine.py b/scapy/ansmachine.py index ef78c184491..3c5cb865d33 100644 --- a/scapy/ansmachine.py +++ b/scapy/ansmachine.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Answering machines. @@ -11,53 +11,99 @@ # Answering machines # ######################## -from __future__ import absolute_import -from __future__ import print_function -from scapy.sendrecv import send, sniff +import abc +import functools +import threading +import socket +import warnings + +from scapy.arch import get_if_addr from scapy.config import conf -from scapy.error import log_interactive -import scapy.modules.six as six +from scapy.sendrecv import sendp, sniff, AsyncSniffer +from scapy.packet import Packet +from scapy.plist import PacketList + +from typing import ( + Any, + Callable, + Dict, + Generic, + Optional, + Tuple, + Type, + TypeVar, + cast, +) + +_T = TypeVar("_T", Packet, PacketList) class ReferenceAM(type): - def __new__(cls, name, bases, dct): - obj = super(ReferenceAM, cls).__new__(cls, name, bases, dct) + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type['AnsweringMachine[_T]'] + obj = cast('Type[AnsweringMachine[_T]]', + super(ReferenceAM, cls).__new__(cls, name, bases, dct)) + try: + import inspect + obj.__signature__ = inspect.signature( # type: ignore + obj.parse_options + ) + except (ImportError, AttributeError): + pass if obj.function_name: - globals()[obj.function_name] = lambda obj=obj, *args, **kargs: obj(*args, **kargs)() # noqa: E501 + func = lambda obj=obj, *args, **kargs: obj(*args, **kargs)() # type: ignore # noqa: E501 + # Inject signature + func.__name__ = func.__qualname__ = obj.function_name + func.__doc__ = obj.__doc__ or obj.parse_options.__doc__ + try: + func.__signature__ = obj.__signature__ # type: ignore + except (AttributeError): + pass + globals()[obj.function_name] = func return obj -class AnsweringMachine(six.with_metaclass(ReferenceAM, object)): +class AnsweringMachine(Generic[_T], metaclass=ReferenceAM): function_name = "" - filter = None - sniff_options = {"store": 0} - sniff_options_list = ["store", "iface", "count", "promisc", "filter", "type", "prn", "stop_filter"] # noqa: E501 - send_options = {"verbose": 0} - send_options_list = ["iface", "inter", "loop", "verbose"] - send_function = staticmethod(send) + filter = None # type: Optional[str] + sniff_options = {"store": 0} # type: Dict[str, Any] + sniff_options_list = ["store", "iface", "count", "promisc", "filter", + "type", "prn", "stop_filter", "opened_socket"] + send_options = {"verbose": 0} # type: Dict[str, Any] + send_options_list = ["iface", "inter", "loop", "verbose", "socket"] + send_function = staticmethod(sendp) def __init__(self, **kargs): + # type: (Any) -> None self.mode = 0 + self.verbose = kargs.get("verbose", conf.verb >= 0) if self.filter: kargs.setdefault("filter", self.filter) kargs.setdefault("prn", self.reply) - self.optam1 = {} - self.optam2 = {} - self.optam0 = {} + self.optam1 = {} # type: Dict[str, Any] + self.optam2 = {} # type: Dict[str, Any] + self.optam0 = {} # type: Dict[str, Any] doptsend, doptsniff = self.parse_all_options(1, kargs) self.defoptsend = self.send_options.copy() self.defoptsend.update(doptsend) self.defoptsniff = self.sniff_options.copy() self.defoptsniff.update(doptsniff) - self.optsend, self.optsniff = [{}, {}] + self.optsend = {} # type: Dict[str, Any] + self.optsniff = {} # type: Dict[str, Any] def __getattr__(self, attr): + # type: (str) -> Any for dct in [self.optam2, self.optam1]: if attr in dct: return dct[attr] raise AttributeError(attr) def __setattr__(self, attr, val): + # type: (str, Any) -> None mode = self.__dict__.get("mode", 0) if mode == 0: self.__dict__[attr] = val @@ -65,11 +111,13 @@ def __setattr__(self, attr, val): [self.optam1, self.optam2][mode - 1][attr] = val def parse_options(self): + # type: () -> None pass def parse_all_options(self, mode, kargs): - sniffopt = {} - sendopt = {} + # type: (int, Any) -> Tuple[Dict[str, Any], Dict[str, Any]] + sniffopt = {} # type: Dict[str, Any] + sendopt = {} # type: Dict[str, Any] for k in list(kargs): # use list(): kargs is modified in the loop if k in self.sniff_options_list: sniffopt[k] = kargs[k] @@ -92,40 +140,153 @@ def parse_all_options(self, mode, kargs): return sendopt, sniffopt def is_request(self, req): + # type: (Packet) -> int return 1 + @abc.abstractmethod def make_reply(self, req): - return req + # type: (Packet) -> _T + pass - def send_reply(self, reply): - self.send_function(reply, **self.optsend) + def send_reply(self, reply, send_function=None): + # type: (_T, Optional[Callable[..., None]]) -> None + if send_function: + send_function(reply) + else: + self.send_function(reply, **self.optsend) def print_reply(self, req, reply): - print("%s ==> %s" % (req.summary(), reply.summary())) + # type: (Packet, _T) -> None + if isinstance(reply, PacketList): + print("%s ==> %s" % (req.summary(), + [res.summary() for res in reply])) + else: + print("%s ==> %s" % (req.summary(), reply.summary())) - def reply(self, pkt): + def reply(self, pkt, send_function=None, address=None): + # type: (Packet, Optional[Callable[..., None]], Optional[Any]) -> None if not self.is_request(pkt): return - reply = self.make_reply(pkt) - self.send_reply(reply) - if conf.verb >= 0: + if address: + # Only on AnsweringMachineTCP + reply = self.make_reply(pkt, address=address) # type: ignore + else: + reply = self.make_reply(pkt) + if not reply: + return + if send_function: + self.send_reply(reply, send_function=send_function) + else: + # Retro-compability. Remove this if eventually + self.send_reply(reply) + if self.verbose: self.print_reply(pkt, reply) def run(self, *args, **kargs): - log_interactive.warning("run() method deprecated. The instance is now callable") # noqa: E501 + # type: (Any, Any) -> None + warnings.warn( + "run() method deprecated. The instance is now callable", + DeprecationWarning + ) self(*args, **kargs) + def bg(self, *args, **kwargs): + # type: (Any, Any) -> AsyncSniffer + kwargs.setdefault("bg", True) + self(*args, **kwargs) + return self.sniffer + def __call__(self, *args, **kargs): + # type: (Any, Any) -> None + bg = kargs.pop("bg", False) optsend, optsniff = self.parse_all_options(2, kargs) self.optsend = self.defoptsend.copy() self.optsend.update(optsend) self.optsniff = self.defoptsniff.copy() self.optsniff.update(optsniff) - try: - self.sniff() - except KeyboardInterrupt: - print("Interrupted by user") + if bg: + self.sniff_bg() + else: + try: + self.sniff() + except KeyboardInterrupt: + print("Interrupted by user") def sniff(self): + # type: () -> None sniff(**self.optsniff) + + def sniff_bg(self): + # type: () -> None + self.sniffer = AsyncSniffer(**self.optsniff) + self.sniffer.start() + + +class AnsweringMachineTCP(AnsweringMachine[Packet]): + """ + An answering machine that use the classic socket.socket to + answer multiple TCP clients + """ + TYPE = socket.SOCK_STREAM + + def parse_options(self, port=80, cls=conf.raw_layer): + # type: (int, Type[Packet]) -> None + self.port = port + self.cls = cls + + def close(self): + # type: () -> None + pass + + def sniff(self): + # type: () -> None + from scapy.supersocket import StreamSocket + ssock = socket.socket(socket.AF_INET, self.TYPE) + try: + ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except OSError: + pass + ssock.bind( + (get_if_addr(self.optsniff.get("iface", conf.iface)), self.port)) + ssock.listen() + sniffers = [] + try: + while True: + clientsocket, address = ssock.accept() + print("%s connected" % repr(address)) + sock = StreamSocket(clientsocket, self.cls) + optsniff = self.optsniff.copy() + optsniff["prn"] = functools.partial(self.reply, + send_function=sock.send, + address=address) + del optsniff["iface"] + sniffer = AsyncSniffer(opened_socket=sock, **optsniff) + sniffer.start() + sniffers.append((sniffer, sock)) + finally: + for (sniffer, sock) in sniffers: + try: + sniffer.stop() + except Exception: + pass + sock.close() + self.close() + ssock.close() + + def sniff_bg(self): + # type: () -> None + self.sniffer = threading.Thread(target=self.sniff) # type: ignore + self.sniffer.start() + + def make_reply(self, req, address=None): + # type: (Packet, Optional[Any]) -> Packet + return req + + +class AnsweringMachineUDP(AnsweringMachineTCP): + """ + An answering machine that use the classic socket.socket to + answer multiple UDP clients + """ + TYPE = socket.SOCK_DGRAM diff --git a/scapy/arch/__init__.py b/scapy/arch/__init__.py index 344db1f2ae8..bea0a570e02 100644 --- a/scapy/arch/__init__.py +++ b/scapy/arch/__init__.py @@ -1,96 +1,173 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Operating system specific functionality. """ -from __future__ import absolute_import import socket +import sys -import scapy.consts -from scapy.consts import LINUX, SOLARIS, WINDOWS, BSD -from scapy.error import Scapy_Exception +from scapy.compat import orb from scapy.config import conf, _set_conf_sockets +from scapy.consts import LINUX, SOLARIS, WINDOWS, BSD +from scapy.data import ( + IPV6_ADDR_GLOBAL, + IPV6_ADDR_LOOPBACK, +) +from scapy.error import log_loading +from scapy.interfaces import ( + _GlobInterfaceType, + network_name, + resolve_iface, +) from scapy.pton_ntop import inet_pton, inet_ntop -from scapy.data import ARPHDR_ETHER, ARPHDR_LOOPBACK, IPV6_ADDR_GLOBAL -from scapy.compat import orb +from scapy.libs.extcap import load_extcap + +# Typing imports +from typing import ( + List, + Optional, + Tuple, + Union, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.interfaces import NetworkInterface + +# Note: the typing of this file is heavily ignored because MyPy doesn't allow +# to import the same function from different files. + +# This list only includes imports that are common across all platforms. +__all__ = [ # noqa: F405 + "get_if_addr", + "get_if_addr6", + "get_if_hwaddr", + "get_if_list", + "get_if_raw_addr", + "get_if_raw_addr6", + "get_working_if", + "in6_getifaddr", + "read_nameservers", + "read_routes", + "read_routes6", + "load_extcap", + "SIOCGIFHWADDR", +] + +# BACKWARD COMPATIBILITY +from scapy.interfaces import ( + get_if_list, + get_working_if, +) + + +# We build the utils functions BEFORE importing the underlying handlers +# because they might be themselves imported within the arch/ folder. def str2mac(s): + # Duplicated from scapy/utils.py for import reasons + # type: (bytes) -> str return ("%02x:" * 6)[:-1] % tuple(orb(x) for x in s) -if not WINDOWS: - if not conf.use_pcap: - from scapy.arch.bpf.core import get_if_raw_addr - - def get_if_addr(iff): - return inet_ntop(socket.AF_INET, get_if_raw_addr(iff)) + # type: (_GlobInterfaceType) -> str + """ + Returns the IPv4 of an interface or "0.0.0.0" if not available + """ + return inet_ntop(socket.AF_INET, get_if_raw_addr(iff)) # noqa: F405 def get_if_hwaddr(iff): - addrfamily, mac = get_if_raw_hwaddr(iff) # noqa: F405 - if addrfamily in [ARPHDR_ETHER, ARPHDR_LOOPBACK]: - return str2mac(mac) - else: - raise Scapy_Exception("Unsupported address family (%i) for interface [%s]" % (addrfamily, iff)) # noqa: E501 + # type: (_GlobInterfaceType) -> str + """ + Returns the MAC (hardware) address of an interface + """ + return resolve_iface(iff).mac or "00:00:00:00:00:00" + + +def get_if_addr6(niff): + # type: (_GlobInterfaceType) -> Optional[str] + """ + Returns the main global unicast address associated with provided + interface, in human readable form. If no global address is found, + None is returned. + """ + iff = network_name(niff) + scope = IPV6_ADDR_GLOBAL + if iff == conf.loopback_name: + scope = IPV6_ADDR_LOOPBACK + return next((x[0] for x in in6_getifaddr() + if x[2] == iff and x[1] == scope), None) + + +def get_if_raw_addr6(iff): + # type: (_GlobInterfaceType) -> Optional[bytes] + """ + Returns the main global unicast address associated with provided + interface, in network format. If no global address is found, None + is returned. + """ + ip6 = get_if_addr6(iff) + if ip6 is not None: + return inet_pton(socket.AF_INET6, ip6) + + return None # Next step is to import following architecture specific functions: -# def get_if_raw_hwaddr(iff) -# def get_if_raw_addr(iff): -# def get_if_list(): -# def get_working_if(): -# def attach_filter(s, filter, iface): -# def set_promisc(s,iff,val=1): -# def read_routes(): -# def read_routes6(): -# def get_if(iff,cmd): -# def get_if_index(iff): +# def attach_filter(s, filter, iface) +# def get_if_raw_addr(iff) +# def in6_getifaddr() +# def read_nameservers() +# def read_routes() +# def read_routes6() +# def set_promisc(s,iff,val=1) if LINUX: from scapy.arch.linux import * # noqa F403 elif BSD: - from scapy.arch.unix import read_routes, read_routes6, in6_getifaddr # noqa: F401, E501 from scapy.arch.bpf.core import * # noqa F403 if not conf.use_pcap: # Native - from scapy.arch.bpf.supersocket import * # noqa F403 + from scapy.arch.bpf.supersocket import * # noqa F403 conf.use_bpf = True + SIOCGIFHWADDR = 0 # mypy compat elif SOLARIS: from scapy.arch.solaris import * # noqa F403 elif WINDOWS: from scapy.arch.windows import * # noqa F403 from scapy.arch.windows.native import * # noqa F403 + from scapy.arch.windows.sspi import * # noqa F403 + SIOCGIFHWADDR = 0 # mypy compat +else: + log_loading.critical( + "Scapy currently does not support %s! I/O will NOT work!" % sys.platform + ) + SIOCGIFHWADDR = 0 # mypy compat -if conf.iface is None: - conf.iface = scapy.consts.LOOPBACK_INTERFACE + # DUMMYS + def get_if_raw_addr(iff: Union['NetworkInterface', str]) -> bytes: + return b"\0\0\0\0" -_set_conf_sockets() # Apply config + def in6_getifaddr() -> List[Tuple[str, int, str]]: + return [] + def read_nameservers() -> List[str]: + return [] -def get_if_addr6(iff): - """ - Returns the main global unicast address associated with provided - interface, in human readable form. If no global address is found, - None is returned. - """ - return next((x[0] for x in in6_getifaddr() - if x[2] == iff and x[1] == IPV6_ADDR_GLOBAL), None) + def read_routes() -> List[str]: + return [] + def read_routes6() -> List[str]: + return [] -def get_if_raw_addr6(iff): - """ - Returns the main global unicast address associated with provided - interface, in network format. If no global address is found, None - is returned. - """ - ip6 = get_if_addr6(iff) - if ip6 is not None: - return inet_pton(socket.AF_INET6, ip6) +if LINUX or BSD: + conf.load_layers.append("tuntap") - return None +_set_conf_sockets() # Apply config diff --git a/scapy/arch/bpf/__init__.py b/scapy/arch/bpf/__init__.py index 2560caa12f4..b1ca74078d0 100644 --- a/scapy/arch/bpf/__init__.py +++ b/scapy/arch/bpf/__init__.py @@ -1,4 +1,7 @@ -# Guillaume Valadon +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Guillaume Valadon """ Scapy BSD native support diff --git a/scapy/arch/bpf/consts.py b/scapy/arch/bpf/consts.py index 92697d6511a..df207a3397f 100644 --- a/scapy/arch/bpf/consts.py +++ b/scapy/arch/bpf/consts.py @@ -1,27 +1,64 @@ -# Guillaume Valadon +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Guillaume Valadon """ Scapy BSD native support - constants """ -from ctypes import sizeof +import ctypes from scapy.libs.structures import bpf_program from scapy.data import MTU +# Type hints +from typing import ( + Any, + Callable, +) SIOCGIFFLAGS = 0xc0206911 BPF_BUFFER_LENGTH = MTU +# From sys/ioccom.h + +IOCPARM_MASK = 0x1fff +IOC_VOID = 0x20000000 +IOC_OUT = 0x40000000 +IOC_IN = 0x80000000 +IOC_INOUT = IOC_IN | IOC_OUT + +_th = lambda x: x if isinstance(x, int) else ctypes.sizeof(x) # type: Callable[[Any], int] # noqa: E501 + + +def _IOC(inout, group, num, len): + # type: (int, str, int, Any) -> int + return (inout | + ((_th(len) & IOCPARM_MASK) << 16) | + (ord(group) << 8) | (num)) + + +_IO = lambda g, n: _IOC(IOC_VOID, g, n, 0) # type: Callable[[str, int], int] +_IOR = lambda g, n, t: _IOC(IOC_OUT, g, n, t) # type: Callable[[str, int, Any], int] +_IOW = lambda g, n, t: _IOC(IOC_IN, g, n, t) # type: Callable[[str, int, Any], int] +_IOWR = lambda g, n, t: _IOC(IOC_INOUT, g, n, t) # type: Callable[[str, int, Any], int] + +# Length of some structures +_bpf_stat = 8 +_ifreq = 32 + # From net/bpf.h -BIOCIMMEDIATE = 0x80044270 -BIOCGSTATS = 0x4008426f -BIOCPROMISC = 0x20004269 -BIOCSETIF = 0x8020426c -BIOCSBLEN = 0xc0044266 -BIOCGBLEN = 0x40044266 -BIOCSETF = 0x80004267 | ((sizeof(bpf_program) & 0x1fff) << 16) -BIOCSDLT = 0x80044278 -BIOCSHDRCMPLT = 0x80044275 -BIOCGDLT = 0x4004426a -DLT_IEEE802_11_RADIO = 127 +BIOCGBLEN = _IOR('B', 102, ctypes.c_uint) +BIOCSBLEN = _IOWR('B', 102, ctypes.c_uint) +BIOCSETF = _IOW('B', 103, bpf_program) +BIOCPROMISC = _IO('B', 105) +BIOCGDLT = _IOR('B', 106, ctypes.c_uint) +BIOCSETIF = _IOW('B', 108, 32) +BIOCGSTATS = _IOR('B', 111, _bpf_stat) +BIOCIMMEDIATE = _IOW('B', 112, ctypes.c_uint) +BIOCSHDRCMPLT = _IOW('B', 117, ctypes.c_uint) +BIOCSDLT = _IOW('B', 120, ctypes.c_uint) +BIOCSTSTAMP = _IOW('B', 132, ctypes.c_uint) + +BPF_T_NANOTIME = 0x0001 diff --git a/scapy/arch/bpf/core.py b/scapy/arch/bpf/core.py index 3dc91ce9bd0..b2424fe38f6 100644 --- a/scapy/arch/bpf/core.py +++ b/scapy/arch/bpf/core.py @@ -1,107 +1,52 @@ -# Guillaume Valadon +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Guillaume Valadon """ Scapy *BSD native support - core """ -from __future__ import absolute_import -from ctypes import cdll, cast, pointer -from ctypes import c_int, c_ulong, c_char_p -from ctypes.util import find_library import fcntl import os -import re import socket import struct -import subprocess -from scapy.arch.bpf.consts import BIOCSETF, SIOCGIFFLAGS, BIOCSETIF -from scapy.arch.common import get_if, compile_filter -from scapy.compat import plain_str +from scapy.arch.bpf.consts import BIOCSETF, BIOCSETIF +from scapy.arch.common import compile_filter, free_filter from scapy.config import conf -from scapy.consts import LOOPBACK_NAME -from scapy.data import ARPHDR_LOOPBACK, ARPHDR_ETHER -from scapy.error import Scapy_Exception, warning -from scapy.modules.six.moves import range - - -# ctypes definitions - -LIBC = cdll.LoadLibrary(find_library("libc")) -LIBC.ioctl.argtypes = [c_int, c_ulong, c_char_p] -LIBC.ioctl.restype = c_int - - -# Addresses manipulation functions - -def get_if_raw_addr(ifname): - """Returns the IPv4 address configured on 'ifname', packed with inet_pton.""" # noqa: E501 - - # Get ifconfig output - subproc = subprocess.Popen( - [conf.prog.ifconfig, ifname], - close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = subproc.communicate() - if subproc.returncode: - warning("Failed to execute ifconfig: (%s)", plain_str(stderr)) - return b"\0\0\0\0" - # Get IPv4 addresses - - addresses = [ - line for line in plain_str(stdout).splitlines() - if "inet " in line - ] - - if not addresses: - warning("No IPv4 address found on %s !", ifname) - return b"\0\0\0\0" - - # Pack the first address - address = addresses[0].split(' ')[1] - if '/' in address: # NetBSD 8.0 - address = address.split("/")[0] - return socket.inet_pton(socket.AF_INET, address) - - -def get_if_raw_hwaddr(ifname): - """Returns the packed MAC address configured on 'ifname'.""" - - NULL_MAC_ADDRESS = b'\x00' * 6 - - # Handle the loopback interface separately - if ifname == LOOPBACK_NAME: - return (ARPHDR_LOOPBACK, NULL_MAC_ADDRESS) - - # Get ifconfig output - subproc = subprocess.Popen( - [conf.prog.ifconfig, ifname], - close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = subproc.communicate() - if subproc.returncode: - raise Scapy_Exception("Failed to execute ifconfig: (%s)" % - (plain_str(stderr))) - - # Get MAC addresses - addresses = [ - line for line in plain_str(stdout).splitlines() if ( - "ether" in line or "lladdr" in line or "address" in line - ) - ] - if not addresses: - raise Scapy_Exception("No MAC address found on %s !" % ifname) - - # Pack and return the MAC address - mac = addresses[0].split(' ')[1] - mac = [chr(int(b, 16)) for b in mac.split(':')] - return (ARPHDR_ETHER, ''.join(mac)) - +from scapy.consts import LINUX +from scapy.error import Scapy_Exception +from scapy.interfaces import ( + InterfaceProvider, + NetworkInterface, + _GlobInterfaceType, +) + +# re-export +from scapy.arch.bpf.pfroute import ( # noqa F403 + read_routes, + read_routes6, + _get_if_list, +) +from scapy.arch.common import get_if_raw_addr, read_nameservers # noqa: F401 + +# Typing +from typing import ( + Dict, + List, + Tuple, +) + +if LINUX: + raise OSError("BPF conflicts with Linux") # BPF specific functions + def get_dev_bpf(): + # type: () -> Tuple[int, int] """Returns an opened BPF file object""" # Get the first available BPF handle @@ -109,104 +54,93 @@ def get_dev_bpf(): try: fd = os.open("/dev/bpf%i" % bpf, os.O_RDWR) return (fd, bpf) - except OSError: + except OSError as ex: + if ex.errno == 13: # Permission denied + raise Scapy_Exception( + ( + "Permission denied: could not open /dev/bpf%i. " + "Make sure to be running Scapy as root ! (sudo)" + ) + % bpf + ) continue raise Scapy_Exception("No /dev/bpf handle is available !") def attach_filter(fd, bpf_filter, iface): + # type: (int, str, _GlobInterfaceType) -> None """Attach a BPF filter to the BPF file descriptor""" bp = compile_filter(bpf_filter, iface) # Assign the BPF program to the interface - ret = LIBC.ioctl(c_int(fd), BIOCSETF, cast(pointer(bp), c_char_p)) + ret = fcntl.ioctl(fd, BIOCSETF, bp) if ret < 0: raise Scapy_Exception("Can't attach the BPF filter !") + # Free + free_filter(bp) -# Interface manipulation functions - -def get_if_list(): - """Returns a list containing all network interfaces.""" - - # Get ifconfig output - subproc = subprocess.Popen( - [conf.prog.ifconfig], - close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = subproc.communicate() - if subproc.returncode: - raise Scapy_Exception("Failed to execute ifconfig: (%s)" % - (plain_str(stderr))) +def in6_getifaddr(): + # type: () -> List[Tuple[str, int, str]] + """ + Returns a list of 3-tuples of the form (addr, scope, iface) where + 'addr' is the address of scope 'scope' associated to the interface + 'iface'. - interfaces = [ - line[:line.find(':')] for line in plain_str(stdout).splitlines() - if ": flags" in line.lower() + This is the list of all addresses of all interfaces available on + the system. + """ + ifaces = _get_if_list() + return [ + (ip["address"], ip["scope"], iface["name"]) + for iface in ifaces.values() + for ip in iface["ips"] + if ip["af_family"] == socket.AF_INET6 ] - return interfaces - -_IFNUM = re.compile(r"([0-9]*)([ab]?)$") +# Interface provider -def get_working_ifaces(): - """ - Returns an ordered list of interfaces that could be used with BPF. - Note: the order mimics pcap_findalldevs() behavior - """ - # Only root is allowed to perform the following ioctl() call - if os.getuid() != 0: - return [] +class BPFInterfaceProvider(InterfaceProvider): + name = "BPF" - # Test all network interfaces - interfaces = [] - for ifname in get_if_list(): - - # Unlike pcap_findalldevs(), we do not care of loopback interfaces. - if ifname == LOOPBACK_NAME: - continue - - # Get interface flags + def _is_valid(self, dev): + # type: (NetworkInterface) -> bool + if not dev.flags & 0x1: # not IFF_UP + return False + # Get a BPF handle try: - result = get_if(ifname, SIOCGIFFLAGS) - except IOError: - warning("ioctl(SIOCGIFFLAGS) failed on %s !", ifname) - continue - - # Convert flags - ifflags = struct.unpack("16xH14x", result)[0] - if ifflags & 0x1: # IFF_UP - - # Get a BPF handle fd = get_dev_bpf()[0] - if fd is None: - raise Scapy_Exception("No /dev/bpf are available !") - - # Check if the interface can be used - try: - fcntl.ioctl(fd, BIOCSETIF, struct.pack("16s16x", - ifname.encode())) - except IOError: - pass - else: - ifnum, ifab = _IFNUM.search(ifname).groups() - interfaces.append((ifname, int(ifnum) if ifnum else -1, ifab)) - finally: - # Close the file descriptor - os.close(fd) - - # Sort to mimic pcap_findalldevs() order - interfaces.sort(key=lambda elt: (elt[1], elt[2], elt[0])) - - return [iface[0] for iface in interfaces] - - -def get_working_if(): - """Returns the first interface than can be used with BPF""" - - ifaces = get_working_ifaces() - if not ifaces: - # A better interface will be selected later using the routing table - return LOOPBACK_NAME - return ifaces[0] + except Scapy_Exception: + return True # Can't check if available (non sudo?) + if fd is None: + raise Scapy_Exception("No /dev/bpf are available !") + # Check if the interface can be used + try: + fcntl.ioctl(fd, BIOCSETIF, struct.pack("16s16x", dev.network_name.encode())) + except IOError: + return False + else: + return True + finally: + # Close the file descriptor + os.close(fd) + + def load(self): + # type: () -> Dict[str, NetworkInterface] + data = {} + for iface in _get_if_list().values(): + if_data = iface.copy() + if_data.update( + { + "network_name": iface["name"], + "description": iface["name"], + "ips": [x["address"] for x in iface["ips"]], + } + ) + data[iface["name"]] = NetworkInterface(self, if_data) + return data + + +conf.ifaces.register_provider(BPFInterfaceProvider) diff --git a/scapy/arch/bpf/pfroute.py b/scapy/arch/bpf/pfroute.py new file mode 100644 index 00000000000..e81c2b504eb --- /dev/null +++ b/scapy/arch/bpf/pfroute.py @@ -0,0 +1,1257 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +This file implements the PF_ROUTE API that is used to read the network +configuration of the machine. +""" + +import ctypes +import ctypes.util +import socket +import struct + +from scapy.consts import ( + BIG_ENDIAN, + BSD, + DARWIN, + IS_64BITS, + NETBSD, + OPENBSD, +) +from scapy.config import conf +from scapy.error import log_runtime +from scapy.packet import ( + Packet, + bind_layers, +) +from scapy.utils import atol +from scapy.utils6 import in6_mask2cidr, in6_getscope + +from scapy.fields import ( + ByteEnumField, + ByteField, + ConditionalField, + Field, + FlagsField, + IP6Field, + IPField, + MACField, + MultipleTypeField, + PacketField, + PacketListField, + FieldListField, + PadField, + StrField, + StrFixedLenField, + StrLenField, + XStrLenField, +) +from scapy.pton_ntop import inet_pton + +# Typing imports +from typing import ( + Any, + Dict, + Optional, + List, + Tuple, + Type, +) + +# Missing attributes +if not hasattr(socket, "PF_ROUTE"): + socket.PF_ROUTE = 17 + +# ctypes definitions + +if BSD: # Can be imported for testing. + LIBC = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c")) + LIBC.sysctl.argtypes = [ + ctypes.POINTER(ctypes.c_int), + ctypes.c_uint, + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_size_t), + ctypes.c_void_p, + ctypes.c_size_t, + ] + LIBC.sysctl.restype = ctypes.c_int +else: + LIBC = None + +_bsd_iff_flags = [ + "UP", + "BROADCAST", + "DEBUG", + "LOOPBACK", + "POINTOPOINT", + "NEEDSEPOCH", # UNNUMBERED on NetBSD + "DRV_RUNNING", + "NOARP", + "PROMISC", + "ALLMULTI", + "DRV_OACTIVE", + "SIMPLEX", + "LINK0", + "LINK1", + "LINK2", + "MULTICAST", + "CANTCONFIG", + "PPROMISC", + "MONITOR", + "STATICARP", + "STICKYARP", + "DYING", + "RENAMING", + "SPARE", + "NETLINK_1", +] + +if NETBSD: + _RTM_TYPE = { + # man 4 route + 0x01: "RTM_ADD", + 0x02: "RTM_DELETE", + 0x03: "RTM_CHANGE", + 0x04: "RTM_GET", + 0x05: "RTM_LOSING", + 0x06: "RTM_REDIRECT", + 0x07: "RTM_MISS", + 0x08: "RTM_LOCK", + 0x09: "RTM_OLDADD", + 0x0A: "RTM_OLDDEL", + 0x0B: "RTM_RESOLVE", + 0x0C: "RTM_ONEWADDR", + 0x0D: "RTM_ODELADDR", + 0x0E: "RTM_OOIFINFO", + 0x0F: "RTM_OIFINFO", + 0x10: "RTM_IFANNOUNCE", + 0x11: "RTM_IEEE80211", + 0x12: "RTM_SETGATE", + 0x13: "RTM_LLINFO_UPD", + 0x14: "RTM_IFINFO", + 0x15: "RTM_OCHGADDR", + 0x16: "RTM_NEWADDR", + 0x17: "RTM_DELADDR", + 0x18: "RTM_CHGADDR", + } +elif OPENBSD: + _RTM_TYPE = { + # man 4 route + 0x01: "RTM_ADD", + 0x02: "RTM_DELETE", + 0x03: "RTM_CHANGE", + 0x04: "RTM_GET", + 0x05: "RTM_LOSING", + 0x06: "RTM_REDIRECT", + 0x07: "RTM_MISS", + 0x08: "RTM_LOCK", + 0x09: "RTM_OLDADD", + 0x0A: "RTM_OLDDEL", + 0x0B: "RTM_RESOLVE", + 0x0C: "RTM_NEWADDR", + 0x0D: "RTM_DELADDR", + 0x0E: "RTM_IFINFO", + 0x0F: "RTM_IFANNOUNCE", + 0x10: "RTM_DESYNC", + 0x11: "RTM_INVALIDATE", + } +elif DARWIN: + _RTM_TYPE = { + # man 4 route + 0x01: "RTM_ADD", + 0x02: "RTM_DELETE", + 0x03: "RTM_CHANGE", + 0x04: "RTM_GET", + 0x05: "RTM_LOSING", + 0x06: "RTM_REDIRECT", + 0x07: "RTM_MISS", + 0x08: "RTM_LOCK", + 0x09: "RTM_OLDADD", + 0x0A: "RTM_OLDDEL", + 0x0B: "RTM_RESOLVE", + 0x0C: "RTM_NEWADDR", + 0x0D: "RTM_DELADDR", + 0x0E: "RTM_IFINFO", + 0x0F: "RTM_NEWMADDR", + 0x10: "RTM_DELMADDR", + 0x12: "RTM_IFINFO2", + 0x13: "RTM_NEWMADDR2", + 0x14: "RTM_GET2", + } +else: # FreeBSD + _RTM_TYPE = { + # man 4 route + 0x01: "RTM_ADD", + 0x02: "RTM_DELETE", + 0x03: "RTM_CHANGE", + 0x04: "RTM_GET", + 0x05: "RTM_LOSING", + 0x06: "RTM_REDIRECT", + 0x07: "RTM_MISS", + 0x08: "RTM_LOCK", + 0x09: "RTM_OLDADD", + 0x0A: "RTM_OLDDEL", + 0x0B: "RTM_RESOLVE", + 0x0C: "RTM_NEWADDR", + 0x0D: "RTM_DELADDR", + 0x0E: "RTM_IFINFO", + 0x0F: "RTM_NEWMADDR", + 0x10: "RTM_DELMADDR", + 0x11: "RTM_IFANNOUNCE", + 0x12: "RTM_IEEE80211", + } + +_RTM_ADDRS = { + 0x01: "RTA_DST", + 0x02: "RTA_GATEWAY", + 0x04: "RTA_NETMASK", + 0x08: "RTA_GENMASK", + 0x10: "RTA_IFP", + 0x20: "RTA_IFA", + 0x40: "RTA_AUTHOR", + 0x80: "RTA_BRD", + 0x100: "RTA_SRC", + 0x200: "RTA_SRCMASK", + 0x400: "RTA_LABEL", + 0x800: "RTA_BFD", + 0x1000: "RTA_DNS", + 0x2000: "RTA_STATIC", + 0x4000: "RTA_SEARCH", +} + +_RTM_FLAGS = { + 0x01: "RTF_UP", + 0x02: "RTF_GATEWAY", + 0x04: "RTF_HOST", + 0x08: "RTF_REJECT", + 0x10: "RTF_DYNAMIC", + 0x20: "RTF_MODIFIED", + 0x40: "RTF_DONE", + 0x80: "RTF_MASK", # NetBSD + 0x100: "RTF_CONNECTED", # NetBSD + 0x200: "RTF_XRESOLVE", + 0x400: "RTF_LLDATA", + 0x800: "RTF_STATIC", + 0x1000: "RTF_BLACKHOLE", + 0x4000: "RTF_PROTO2", + 0x8000: "RTF_PROTO1", + **( + { + 0x10000: "RTF_PRCLONING", + 0x20000: "RTF_WASCLONED", + } + if DARWIN + else { + 0x10000: "RTF_SRC", # NetBSD + 0x20000: "RTF_ANNOUNCE", # NetBSD + } + ), + 0x40000: "RTF_PROTO3", + 0x80000: "RTF_FIXEDMTU", + 0x100000: "RTF_PINNED", + 0x200000: "RTF_LOCAL", + 0x400000: "RTF_BROADCAST", + 0x800000: "RTF_MULTICAST", + **( + { + 0x1000000: "RTF_IFSCOPE", + 0x2000000: "RTF_CONDEMNED", + 0x4000000: "RTF_IFREF", + 0x8000000: "RTF_PROXY", + 0x10000000: "RTF_ROUTER", + 0x20000000: "RTF_DEAD", + 0x40000000: "RTF_GLOBAL", + } + if DARWIN + else { + 0x1000000: "RTF_STICKY", + 0x4000000: "RTF_RNH_LOCKED", # deprecated + 0x8000000: "RTF_GWFLAG_COMPAT", + } + ), +} + +_IFCAP = { + 0x00000001: "IFCAP_CSUM_IPv4", + 0x00000002: "IFCAP_CSUM_TCPv4", + 0x00000004: "IFCAP_CSUM_UDPv4", + 0x00000010: "IFCAP_VLAN_MTU", + 0x00000020: "IFCAP_VLAN_HWTAGGING", + 0x00000080: "IFCAP_CSUM_TCPv6", + 0x00000100: "IFCAP_CSUM_UDPv6", + 0x00001000: "IFCAP_TSOv4", + 0x00002000: "IFCAP_TSOv6", + 0x00004000: "IFCAP_LRO", + 0x00008000: "IFCAP_WOL", +} + +# Common Header + + +class pfmsghdr(Packet): + fields_desc = [ + Field("rtm_msglen", 0, fmt="=H"), + ByteField("rtm_version", 5), + ByteEnumField("rtm_type", 0, _RTM_TYPE), + ] + ( + # It begins... the IFs apocalypse + [Field("rtm_hdrlen", 0, fmt="=H")] + if OPENBSD + else [] + ) + + if OPENBSD: + + def extract_padding(self, s: bytes) -> Tuple[bytes, Optional[bytes]]: + if self.rtm_msglen < 6: + return s, b"" + return s[: self.rtm_msglen - 6], s[self.rtm_msglen - 6 :] + + else: + + def extract_padding(self, s: bytes) -> Tuple[bytes, Optional[bytes]]: + if self.rtm_msglen < 4: + return s, b"" + return s[: self.rtm_msglen - 4], s[self.rtm_msglen - 4 :] + + +bind_layers(pfmsghdr, conf.raw_layer, rtm_msglen=0) # padding + + +# END + + +class sockaddr(Packet): + fields_desc = [ + # socket.h + ByteField("sa_len", 0), + ByteEnumField("sa_family", 0, socket.AddressFamily), + # sockaddr_in + ConditionalField( + Field("sin_port", 0, fmt="=H"), lambda pkt: pkt.sa_family == socket.AF_INET + ), + ConditionalField( + IPField("sin_addr", 0), lambda pkt: pkt.sa_family == socket.AF_INET + ), + ConditionalField( + StrFixedLenField("sin_zero", "", length=8), + lambda pkt: pkt.sa_family == socket.AF_INET and pkt.sa_len > 7, + ), + # sockaddr_in6 + ConditionalField( + Field("sin6_port", 0, fmt="=H"), + lambda pkt: pkt.sa_family == socket.AF_INET6, + ), + ConditionalField( + Field("sin6_flowinfo", 0, fmt="=I"), + lambda pkt: pkt.sa_family == socket.AF_INET6, + ), + ConditionalField( + IP6Field("sin6_addr", "::"), lambda pkt: pkt.sa_family == socket.AF_INET6 + ), + ConditionalField( + Field("sin6_scope_id", 0, fmt="=I"), + lambda pkt: pkt.sa_family == socket.AF_INET6, + ), + # sockaddr_dl + ConditionalField( + Field("sdl_index", 0, fmt="=H"), lambda pkt: pkt.sa_family == socket.AF_LINK + ), + ConditionalField( + Field("sdl_type", 0, fmt="=B"), lambda pkt: pkt.sa_family == socket.AF_LINK + ), + ConditionalField( + Field("sdl_nlen", 0, fmt="=B"), lambda pkt: pkt.sa_family == socket.AF_LINK + ), + ConditionalField( + Field("sdl_alen", 0, fmt="=B"), lambda pkt: pkt.sa_family == socket.AF_LINK + ), + ConditionalField( + Field("sdl_slen", 0, fmt="=B"), lambda pkt: pkt.sa_family == socket.AF_LINK + ), + ConditionalField( + StrLenField("sdl_iface", "", length_from=lambda pkt: pkt.sdl_nlen), + lambda pkt: pkt.sa_family == socket.AF_LINK, + ), + ConditionalField( + MultipleTypeField( + [(MACField("sdl_addr", None), lambda pkt: pkt.sdl_alen == 6)], + StrLenField("sdl_addr", "", length_from=lambda pkt: pkt.sdl_alen), + ), + lambda pkt: pkt.sa_family == socket.AF_LINK, + ), + ConditionalField( + StrLenField("sdl_sel", "", length_from=lambda pkt: pkt.sdl_slen), + lambda pkt: pkt.sa_family == socket.AF_LINK, + ), + ConditionalField( + XStrLenField( + "sdl_data", + "", + length_from=lambda pkt: max( + pkt.sa_len - pkt.sdl_nlen - pkt.sdl_alen - pkt.sdl_slen - 8, 0 + ), + ), + lambda pkt: pkt.sa_family == socket.AF_LINK, + ), + ConditionalField( + XStrLenField("sdl_pad", b"", length_from=lambda pkt: 16 - pkt.sa_len), + lambda pkt: pkt.sa_len < 16 and pkt.sa_family == socket.AF_LINK, + ), + # others + ConditionalField( + XStrLenField( + "sa_data", + "", + length_from=lambda pkt: pkt.sa_len - 2 if pkt.sa_len >= 2 else 0, + ), + lambda pkt: pkt.sa_family + not in [ + socket.AF_INET, + socket.AF_INET6, + socket.AF_LINK, + ], + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class SockAddrsField(FieldListField): + holds_packets = 1 + + def __init__(self, name): + if not IS_64BITS or DARWIN: + align = 4 + else: + align = 8 + super(SockAddrsField, self).__init__( + name, + [], + PadField(PacketField("", None, sockaddr), align), + ) + + +if OPENBSD: + + class if_data(Packet): + # net/if.h + fields_desc = [ + ByteField("ifi_type", 0), + ByteField("ifi_addrlen", 0), + ByteField("ifi_hdrlen", 0), + ByteField("ifi_link_state", 0), + Field("ifi_mtu", 0, fmt="=I"), + Field("ifi_metric", 0, fmt="=I"), + Field("ifi_rdomain", 0, fmt="=I"), + Field("ifi_baudrate", 0, fmt="=Q"), + Field("ifi_ipackets", 0, fmt="=Q"), + Field("ifi_ierrors", 0, fmt="=Q"), + Field("ifi_opackets", 0, fmt="=Q"), + Field("ifi_oerrors", 0, fmt="=Q"), + Field("ifi_collision", 0, fmt="=Q"), + Field("ifi_ibytes", 0, fmt="=Q"), + Field("ifi_obytes", 0, fmt="=Q"), + Field("ifi_imcasts", 0, fmt="=Q"), + Field("ifi_omcasts", 0, fmt="=Q"), + Field("ifi_iqdrops", 0, fmt="=Q"), + Field("ifi_oqdrops", 0, fmt="=Q"), + Field("ifi_noproto", 0, fmt="=Q"), + FlagsField( + "ifi_capabilities", + 0, + 32 if BIG_ENDIAN else -32, + _IFCAP, + ), + StrFixedLenField("ifi_lastchange", 0, + length=16 if IS_64BITS else 8), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + +elif NETBSD: + + class if_data(Packet): + # net/if.h + fields_desc = [ + ByteField("ifi_type", 0), + ByteField("ifi_addrlen", 0), + ByteField("ifi_hdrlen", 0), + Field("ifi_link_state", 0, fmt="=I"), + Field("ifi_mtu", 0, fmt="=Q"), + Field("ifi_metric", 0, fmt="=Q"), + Field("ifi_baudrate", 0, fmt="=Q"), + Field("ifi_ipackets", 0, fmt="=Q"), + Field("ifi_ierrors", 0, fmt="=Q"), + Field("ifi_opackets", 0, fmt="=Q"), + Field("ifi_oerrors", 0, fmt="=Q"), + Field("ifi_collision", 0, fmt="=Q"), + Field("ifi_ibytes", 0, fmt="=Q"), + Field("ifi_obytes", 0, fmt="=Q"), + Field("ifi_imcasts", 0, fmt="=Q"), + Field("ifi_omcasts", 0, fmt="=Q"), + Field("ifi_iqdrops", 0, fmt="=Q"), + Field("ifi_noproto", 0, fmt="=Q"), + StrFixedLenField("ifi_lastchange", 0, + length=16 if IS_64BITS else 8), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + +elif DARWIN: + + class if_data(Packet): + # if_var.h + fields_desc = [ + ByteField("ifi_type", 0), + ByteField("ifi_typelen", 0), + ByteField("ifi_physical", 0), + ByteField("ifi_addrlen", 0), + ByteField("ifi_hdrlen", 0), + ByteField("ifi_recvquota", 0), + ByteField("ifi_xmitquota", 0), + ByteField("ifi_unused", 0), + Field("ifi_mtu", 0, fmt="=I"), + Field("ifi_metric", 0, fmt="=I"), + Field("ifi_baudrate", 0, fmt="=I"), + Field("ifi_ipackets", 0, fmt="=I"), + Field("ifi_ierrors", 0, fmt="=I"), + Field("ifi_opackets", 0, fmt="=I"), + Field("ifi_oerrors", 0, fmt="=I"), + Field("ifi_collision", 0, fmt="=I"), + Field("ifi_ibytes", 0, fmt="=I"), + Field("ifi_obytes", 0, fmt="=I"), + Field("ifi_imcasts", 0, fmt="=I"), + Field("ifi_omcasts", 0, fmt="=I"), + Field("ifi_iqdrops", 0, fmt="=I"), + Field("ifi_noproto", 0, fmt="=I"), + Field("ifi_recvtiming", 0, fmt="=I"), + Field("ifi_xmittiming", 0, fmt="=I"), + Field("ifi_lastchange", 0, fmt="=Q"), + Field("ifi_unused2", 0, fmt="=I"), + Field("ifi_hwassist", 0, fmt="=I"), + Field("ifi_reserved", 0, fmt="=Q"), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + +else: + # FreeBSD + + class if_data(Packet): + # net/if.h + fields_desc = [ + ByteField("ifi_type", 0), + ByteField("ifi_physical", 0), + ByteField("ifi_addrlen", 0), + ByteField("ifi_hdrlen", 0), + ByteField("ifi_link_state", 0), + ByteField("ifi_vhid", 0), + Field("ifi_datalen", 0, fmt="=H"), + Field("ifi_mtu", 0, fmt="=I"), + Field("ifi_metric", 0, fmt="=I"), + Field("ifi_baudrate", 0, fmt="=Q"), + Field("ifi_ipackets", 0, fmt="=Q"), + Field("ifi_ierrors", 0, fmt="=Q"), + Field("ifi_opackets", 0, fmt="=Q"), + Field("ifi_oerrors", 0, fmt="=Q"), + Field("ifi_collision", 0, fmt="=Q"), + Field("ifi_ibytes", 0, fmt="=Q"), + Field("ifi_obytes", 0, fmt="=Q"), + Field("ifi_imcasts", 0, fmt="=Q"), + Field("ifi_omcasts", 0, fmt="=Q"), + Field("ifi_iqdrops", 0, fmt="=Q"), + Field("ifi_oqdrops", 0, fmt="=Q"), + Field("ifi_noproto", 0, fmt="=Q"), + Field("ifi_hwassist", 0, fmt="=Q"), + Field("tt", 0, fmt="=Q"), + StrFixedLenField("tv", 0, + length=16 if IS_64BITS else 8), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +if OPENBSD: + + class if_msghdr(Packet): + fields_desc = [ + Field("ifm_index", 0, fmt="=H"), + Field("ifm_tableid", 0, fmt="=H"), + Field("_ifm_pad", 0, fmt="=H"), + FlagsField( + "ifm_addrs", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_ADDRS, + ), + FlagsField( + "ifm_flags", + 0, + 32 if BIG_ENDIAN else -32, + _bsd_iff_flags, + ), + Field("ifm_xflags", 0, fmt="=I"), + PadField( + PacketField("ifm_data", [], if_data), + 8, + ), + SockAddrsField("addrs"), + ] + +else: + + class if_msghdr(Packet): + fields_desc = [ + FlagsField( + "ifm_addrs", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_ADDRS, + ), + FlagsField( + "ifm_flags", + 0, + 32 if BIG_ENDIAN else -32, + _bsd_iff_flags, + ), + Field("ifm_index", 0, fmt="=H"), + Field("_ifm_spare1", 0, fmt="=H"), + PadField( + PacketField("ifm_data", [], if_data), + 8, + ), + SockAddrsField("addrs"), + ] + + +bind_layers(pfmsghdr, if_msghdr, rtm_type=0x0E) +if NETBSD: + bind_layers(pfmsghdr, if_msghdr, rtm_type=0x14) + + +if OPENBSD: + + class ifa_msghdr(Packet): + fields_desc = if_msghdr.fields_desc[:5] + [ + Field("ifam_metric", 0, fmt="=I"), + SockAddrsField("addrs"), + ] + +elif NETBSD: + + class ifa_msghdr(Packet): + fields_desc = [ + Field("ifm_index", 0, fmt="=H"), + Field("_rtm_spare1", 0, fmt="=H"), + FlagsField( + "ifm_flags", + 0, + 32 if BIG_ENDIAN else -32, + _bsd_iff_flags, + ), + FlagsField( + "ifm_addrs", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_ADDRS, + ), + Field("ifam_pid", 0, fmt="=I"), + Field("ifam_addrflags", 0, fmt="=I"), + PadField( + Field("ifam_metric", 0, fmt="=I"), + 8, + ), + SockAddrsField("addrs"), + ] + +else: # FreeBSD, Darwin + + class ifa_msghdr(Packet): + fields_desc = if_msghdr.fields_desc[:4] + [ + Field("ifam_metric", 0, fmt="=I"), + SockAddrsField("addrs"), + ] + + +bind_layers(pfmsghdr, ifa_msghdr, rtm_type=0x0C) +bind_layers(pfmsghdr, ifa_msghdr, rtm_type=0x0D) +if NETBSD: + bind_layers(pfmsghdr, ifa_msghdr, rtm_type=0x16) + bind_layers(pfmsghdr, ifa_msghdr, rtm_type=0x17) + + +class ifma_msghdr(Packet): + fields_desc = if_msghdr.fields_desc[:4] + + +bind_layers(pfmsghdr, ifma_msghdr, rtm_type=0x0F) +bind_layers(pfmsghdr, ifma_msghdr, rtm_type=0x10) + + +class if_announcemsghdr(Packet): + fields_desc = [ + Field("ifan_index", 0, fmt="=H"), + StrField("ifan_name", ""), + Field("ifan_what", 0, fmt="=H"), + ] + + +bind_layers(pfmsghdr, ifma_msghdr, rtm_type=0x11) + + +if OPENBSD: + + class rt_metrics(Packet): + fields_desc = [ + Field("rmx_pksent", 0, fmt="=Q"), + Field("rmx_expire", 0, fmt="=q"), + Field("rmx_locks", 0, fmt="=I"), + Field("rmx_mtu", 0, fmt="=I"), + Field("rmx_refcnt", 0, fmt="=I"), + Field("rmx_hopcount", 0, fmt="=I"), + Field("rmx_recvpipe", 0, fmt="=I"), + Field("rmx_sendpipe", 0, fmt="=I"), + Field("rmx_sshthresh", 0, fmt="=I"), + Field("rmx_rtt", 0, fmt="=I"), + Field("rmx_rttvar", 0, fmt="=I"), + Field("rmx_pad", 0, fmt="=I"), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + +elif NETBSD: + + class rt_metrics(Packet): + fields_desc = [ + Field("rmx_locks", 0, fmt="=Q"), + Field("rmx_mtu", 0, fmt="=Q"), + Field("rmx_hopcount", 0, fmt="=Q"), + Field("rmx_recvpipe", 0, fmt="=Q"), + Field("rmx_sendpipe", 0, fmt="=Q"), + Field("rmx_sshthresh", 0, fmt="=Q"), + Field("rmx_rtt", 0, fmt="=Q"), + Field("rmx_rttvar", 0, fmt="=Q"), + Field("rmx_expire", 0, fmt="=Q"), + Field("rmx_pksent", 0, fmt="=Q"), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + +elif DARWIN: + + class rt_metrics(Packet): + fields_desc = [ + Field("rmx_locks", 0, fmt="=I"), + Field("rmx_mtu", 0, fmt="=I"), + Field("rmx_hopcount", 0, fmt="=I"), + Field("rmx_expire", 0, fmt="=i"), + Field("rmx_recvpipe", 0, fmt="=I"), + Field("rmx_sendpipe", 0, fmt="=I"), + Field("rmx_sshthresh", 0, fmt="=I"), + Field("rmx_rtt", 0, fmt="=I"), + Field("rmx_rttvar", 0, fmt="=I"), + Field("rmx_pksent", 0, fmt="=I"), + StrFixedLenField("rmx_filler", 0, + length=16 if IS_64BITS else 8), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + +else: + # FreeBSD + + class rt_metrics(Packet): + fields_desc = [ + Field("rmx_locks", 0, fmt="=Q"), + Field("rmx_mtu", 0, fmt="=Q"), + Field("rmx_hopcount", 0, fmt="=Q"), + Field("rmx_expire", 0, fmt="=Q"), + Field("rmx_recvpipe", 0, fmt="=Q"), + Field("rmx_sendpipe", 0, fmt="=Q"), + Field("rmx_sshthresh", 0, fmt="=Q"), + Field("rmx_rtt", 0, fmt="=Q"), + Field("rmx_rttvar", 0, fmt="=Q"), + Field("rmx_pksent", 0, fmt="=Q"), + Field("rmx_weight", 0, fmt="=Q"), + Field("rmx_nhidx", 0, fmt="=Q"), + StrFixedLenField("rmx_filler", 0, + length=16 if IS_64BITS else 8), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +if OPENBSD: + + class rt_msghdr(Packet): + fields_desc = [ + Field("rtm_index", 0, fmt="=H"), + Field("rtm_tableid", 0, fmt="=H"), + ByteField("rtm_priority", 0), + ByteField("rtm_mpls", 0), + FlagsField( + "rtm_addrs", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_ADDRS, + ), + FlagsField( + "rtm_flags", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_FLAGS, + ), + Field("rtm_fmask", 0, fmt="=I"), + Field("rtm_pid", 0, fmt="=I"), + Field("rtm_seq", 0, fmt="=I"), + Field("rtm_errno", 0, fmt="=I"), + Field("rtm_inits", 0, fmt="=I"), + PadField( + PacketField("rtm_rmx", rt_metrics(), rt_metrics), + 8, + ), + SockAddrsField("addrs"), + ] + +elif NETBSD: + + class rt_msghdr(Packet): + fields_desc = [ + Field("rtm_index", 0, fmt="=H"), + Field("_rtm_spare1", 0, fmt="=H"), + FlagsField( + "rtm_flags", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_FLAGS, + ), + FlagsField( + "rtm_addrs", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_ADDRS, + ), + Field("rtm_pid", 0, fmt="=I"), + Field("rtm_seq", 0, fmt="=I"), + Field("rtm_errno", 0, fmt="=I"), + Field("rtm_use", 0, fmt="=I"), + PadField( + Field("rtm_inits", 0, fmt="=I"), + 8, + ), + PadField( + PacketField("rtm_rmx", rt_metrics(), rt_metrics), + 8, + ), + SockAddrsField("addrs"), + ] + +elif DARWIN: + + class rt_msghdr(Packet): + # actually rt_msghdr2 (we need parentflags) + fields_desc = [ + Field("rtm_index", 0, fmt="=H"), + Field("_rtm_spare1", 0, fmt="=H"), + FlagsField( + "rtm_flags", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_FLAGS, + ), + FlagsField( + "rtm_addrs", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_ADDRS, + ), + Field("rtm_refcnt", 0, fmt="=I"), + FlagsField( + "rtm_parentflags", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_FLAGS, + ), + Field("rtm_reserved", 0, fmt="=I"), + Field("rtm_use", 0, fmt="=I"), + Field("rtm_inits", 0, fmt="=I"), + PadField( + PacketField("rtm_rmx", rt_metrics(), rt_metrics), + 4, + ), + SockAddrsField("addrs"), + ] + +else: + + class rt_msghdr(Packet): + fields_desc = [ + Field("rtm_index", 0, fmt="=H"), + Field("_rtm_spare1", 0, fmt="=H"), + FlagsField( + "rtm_flags", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_FLAGS, + ), + FlagsField( + "rtm_addrs", + 0, + 32 if BIG_ENDIAN else -32, + _RTM_ADDRS, + ), + Field("rtm_pid", 0, fmt="=I"), + Field("rtm_seq", 0, fmt="=I"), + Field("rtm_errno", 0, fmt="=I"), + Field("rtm_fmask", 0, fmt="=I"), + Field("rtm_inits", 0, fmt="=Q"), + PadField( + PacketField("rtm_rmx", rt_metrics(), rt_metrics), + 8, + ), + SockAddrsField("addrs"), + ] + + +bind_layers(pfmsghdr, rt_msghdr) # else + + +class pfmsghdrs(Packet): + fields_desc = [ + PacketListField( + "msgs", + [], + pfmsghdr, + # 65535 / len(pfmsghdr) + max_count=4096, + ), + ] + + +# Utils + +CTL_NET = 4 +if DARWIN: + NET_RT_DUMP = 7 # NET_RT_DUMP2 +else: + NET_RT_DUMP = 1 +NET_RT_TABLE = 5 +if NETBSD: + NET_RT_IFLIST = 6 +else: + NET_RT_IFLIST = 3 + + +def _sr1_bsdsysctl(mib) -> List[Packet]: + """ + Send / Receive a BSD sysctl + """ + # Request routes + # 1. estimate needed size + oldplen = ctypes.c_size_t() + r = LIBC.sysctl( + mib, + len(mib), + None, + ctypes.byref(oldplen), + None, + 0, + ) + if r != 0: + return None + # 2. ask for real + oldp = ctypes.create_string_buffer(oldplen.value) + r = LIBC.sysctl( + mib, + len(mib), + oldp, + ctypes.byref(oldplen), + None, + 0, + ) + if r != 0: + return None + # Parse response + return pfmsghdrs(bytes(oldp)) + + +def read_routes(): + """ + Read the IPv4 routes using PF_ROUTE + """ + mib = [ + CTL_NET, + socket.PF_ROUTE, + 0, + int(socket.AF_INET), + NET_RT_DUMP, + 0, + ] + if not NETBSD and not DARWIN: + # NetBSD / OSX is missing the fib + if OPENBSD: + fib = 0 # default table + else: # FreeBSD + fib = -1 # means 'all' + mib.append(fib) + mib = (ctypes.c_int * len(mib))(*mib) + resp = _sr1_bsdsysctl(mib) + if not resp: + return [] + ifaces = _get_if_list() + routes = [] + for msg in resp.msgs: + if msg.rtm_type != 0x4 and (not DARWIN or msg.rtm_type != 0x14): # RTM_GET(2) + continue + # Parse route. addrs contains what addresses are present + flags = msg.rtm_flags + if not flags.RTF_UP: + continue + if DARWIN and flags.RTF_WASCLONED and msg.rtm_parentflags.RTF_PRCLONING: + # OSX needs filtering + continue + addrs = msg.rtm_addrs + net = 0 + mask = 0xFFFFFFFF + gw = 0 + iface = "" + addr = "" + metric = 1 + i = 0 + try: + if addrs.RTA_DST: + net = atol(msg.addrs[i].sin_addr) + i += 1 + if addrs.RTA_GATEWAY: + if msg.addrs[i].sa_family == socket.AF_LINK: + gw = "0.0.0.0" + else: + gw = msg.addrs[i].sin_addr or "0.0.0.0" + i += 1 + if addrs.RTA_NETMASK: + nm = msg.addrs[i] + if nm.sa_family == socket.AF_INET: + mask = atol(nm.sin_addr) + elif nm.sa_family in [0x00, 0xFF]: # NetBSD + mask = struct.unpack(" Dict[int, Dict[str, Any]] + """ + Read the interfaces list using a PF_ROUTE socket. + """ + mib = (ctypes.c_int * 6)( + CTL_NET, + socket.PF_ROUTE, + 0, + int(socket.AF_UNSPEC), + NET_RT_IFLIST, + 0, + ) + resp = _sr1_bsdsysctl(mib) + if not resp: + return {} + lifips = {} + for msg in resp.msgs: + if msg.rtm_type not in [0x0C, 0x16]: # RTM_NEWADDR + continue + if not msg.ifm_addrs.RTA_IFA: + continue + ifindex = msg.ifm_index + addrindex = ( + msg.ifm_addrs.RTA_DST + + msg.ifm_addrs.RTA_GATEWAY + + msg.ifm_addrs.RTA_NETMASK + + msg.ifm_addrs.RTA_GENMASK + ) + addr = msg.addrs[addrindex] + if addr.sa_family not in [socket.AF_INET, socket.AF_INET6]: + continue + data = { + "af_family": addr.sa_family, + "index": ifindex, + "address": addr.sin_addr, + } + if addr.sa_family == socket.AF_INET: # ipv4 + data["address"] = addr.sin_addr + else: # ipv6 + data.update( + { + "address": addr.sin6_addr, + "scope": in6_getscope(addr.sin6_addr), + } + ) + lifips.setdefault(ifindex, list()).append(data) + interfaces = {} + for msg in resp.msgs: + if msg.rtm_type != 0xE and (not NETBSD or msg.rtm_type != 0x14): # RTM_IFINFO + continue + ifindex = msg.ifm_index + ifname = None + mac = "00:00:00:00:00:00" + itype = -1 + ifflags = msg.ifm_flags + ips = [] + for addr in msg.addrs: + if addr.sa_family == socket.AF_LINK: + ifname = addr.sdl_iface.decode() + itype = addr.sdl_type + if addr.sdl_addr: + mac = addr.sdl_addr + if ifname is not None: + if ifindex in lifips: + ips = lifips[ifindex] + interfaces[ifindex] = { + "name": ifname, + "index": ifindex, + "flags": ifflags, + "mac": mac, + "ips": ips, + "type": itype, + } + return interfaces diff --git a/scapy/arch/bpf/supersocket.py b/scapy/arch/bpf/supersocket.py index 4087899bb12..0b76644444b 100644 --- a/scapy/arch/bpf/supersocket.py +++ b/scapy/arch/bpf/supersocket.py @@ -1,99 +1,195 @@ -# Guillaume Valadon +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Guillaume Valadon """ Scapy *BSD native support - BPF sockets """ -from ctypes import c_long, sizeof +from select import select + +import abc +import ctypes import errno import fcntl import os import platform -from select import select import struct +import sys import time from scapy.arch.bpf.core import get_dev_bpf, attach_filter -from scapy.arch.bpf.consts import BIOCGBLEN, BIOCGDLT, BIOCGSTATS, \ - BIOCIMMEDIATE, BIOCPROMISC, BIOCSBLEN, BIOCSETIF, BIOCSHDRCMPLT, \ - BPF_BUFFER_LENGTH, BIOCSDLT, DLT_IEEE802_11_RADIO +from scapy.arch.bpf.consts import ( + BIOCGBLEN, + BIOCGDLT, + BIOCGSTATS, + BIOCIMMEDIATE, + BIOCPROMISC, + BIOCSBLEN, + BIOCSDLT, + BIOCSETIF, + BIOCSHDRCMPLT, + BIOCSTSTAMP, + BPF_BUFFER_LENGTH, + BPF_T_NANOTIME, +) from scapy.config import conf -from scapy.consts import FREEBSD, NETBSD, DARWIN -from scapy.data import ETH_P_ALL +from scapy.consts import DARWIN, FREEBSD, NETBSD +from scapy.data import ETH_P_ALL, DLT_IEEE802_11_RADIO from scapy.error import Scapy_Exception, warning +from scapy.interfaces import network_name, _GlobInterfaceType from scapy.supersocket import SuperSocket from scapy.compat import raw - -if FREEBSD: +# Typing +from typing import ( + Any, + List, + Optional, + Tuple, + Type, + TYPE_CHECKING, +) +if TYPE_CHECKING: + from scapy.packet import Packet + +# Structures & c types + +if FREEBSD or NETBSD: # On 32bit architectures long might be 32bit. - BPF_ALIGNMENT = sizeof(c_long) + BPF_ALIGNMENT = ctypes.sizeof(ctypes.c_long) +else: + # DARWIN, OPENBSD + BPF_ALIGNMENT = ctypes.sizeof(ctypes.c_int32) + +_NANOTIME = FREEBSD # Kinda disappointing availability TBH + +if _NANOTIME: + class bpf_timeval(ctypes.Structure): + # actually a bpf_timespec + _fields_ = [("tv_sec", ctypes.c_ulong), + ("tv_nsec", ctypes.c_ulong)] elif NETBSD: - BPF_ALIGNMENT = 8 # sizeof(long) + class bpf_timeval(ctypes.Structure): + _fields_ = [("tv_sec", ctypes.c_ulong), + ("tv_usec", ctypes.c_ulong)] else: - BPF_ALIGNMENT = 4 # sizeof(int32_t) + class bpf_timeval(ctypes.Structure): # type: ignore + _fields_ = [("tv_sec", ctypes.c_uint32), + ("tv_usec", ctypes.c_uint32)] + + +class bpf_hdr(ctypes.Structure): + # Also called bpf_xhdr on some OSes + _fields_ = [("bh_tstamp", bpf_timeval), + ("bh_caplen", ctypes.c_uint32), + ("bh_datalen", ctypes.c_uint32), + ("bh_hdrlen", ctypes.c_uint16)] +_bpf_hdr_len = ctypes.sizeof(bpf_hdr) + # SuperSockets definitions + class _L2bpfSocket(SuperSocket): """"Generic Scapy BPF Super Socket""" + __slots__ = ["bpf_fd"] desc = "read/write packets using BPF" nonblocking_socket = True - def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, - nofilter=0, monitor=False): - self.fd_flags = None - self.assigned_interface = None + def __init__(self, + iface=None, # type: Optional[_GlobInterfaceType] + type=ETH_P_ALL, # type: int + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + nofilter=0, # type: int + monitor=False, # type: bool + ): + if monitor: + raise Scapy_Exception( + "We do not natively support monitor mode on BPF. " + "Please turn on libpcap using conf.use_pcap = True" + ) + + self.fd_flags = None # type: Optional[int] + self.type = type + self.bpf_fd = -1 # SuperSocket mandatory variables if promisc is None: - self.promisc = conf.sniff_promisc - else: - self.promisc = promisc + promisc = conf.sniff_promisc + self.promisc = promisc - if iface is None: - self.iface = conf.iface - else: - self.iface = iface + self.iface = network_name(iface or conf.iface) # Get the BPF handle - (self.ins, self.dev_bpf) = get_dev_bpf() - self.outs = self.ins + self.bpf_fd, self.dev_bpf = get_dev_bpf() + if FREEBSD: + # Set the BPF timeval format. Availability issues here ! + try: + fcntl.ioctl( + self.bpf_fd, BIOCSTSTAMP, + struct.pack('I', BPF_T_NANOTIME) + ) + except IOError: + raise Scapy_Exception("BIOCSTSTAMP failed on /dev/bpf%i" % + self.dev_bpf) # Set the BPF buffer length try: - fcntl.ioctl(self.ins, BIOCSBLEN, struct.pack('I', BPF_BUFFER_LENGTH)) # noqa: E501 + fcntl.ioctl( + self.bpf_fd, BIOCSBLEN, + struct.pack('I', BPF_BUFFER_LENGTH) + ) except IOError: raise Scapy_Exception("BIOCSBLEN failed on /dev/bpf%i" % self.dev_bpf) # Assign the network interface to the BPF handle try: - fcntl.ioctl(self.ins, BIOCSETIF, struct.pack("16s16x", self.iface.encode())) # noqa: E501 + fcntl.ioctl( + self.bpf_fd, BIOCSETIF, + struct.pack("16s16x", self.iface.encode()) + ) except IOError: raise Scapy_Exception("BIOCSETIF failed on %s" % self.iface) - self.assigned_interface = self.iface # Set the interface into promiscuous if self.promisc: - self.set_promisc(1) + self.set_promisc(True) # Set the interface to monitor mode # Note: - trick from libpcap/pcap-bpf.c - monitor_mode() # - it only works on OS X 10.5 and later if DARWIN and monitor: - dlt_radiotap = struct.pack('I', DLT_IEEE802_11_RADIO) + # Convert macOS version to an integer try: - fcntl.ioctl(self.ins, BIOCSDLT, dlt_radiotap) - except IOError: - raise Scapy_Exception("Can't set %s into monitor mode!" % - self.iface) + tmp_mac_version = platform.mac_ver()[0].split(".") + tmp_mac_version = [int(num) for num in tmp_mac_version] + macos_version = tmp_mac_version[0] * 10000 + macos_version += tmp_mac_version[1] * 100 + tmp_mac_version[2] + except (IndexError, ValueError): + warning("Could not determine your macOS version!") + macos_version = sys.maxint + + # Disable 802.11 monitoring on macOS Catalina (aka 10.15) and upper + if macos_version < 101500: + dlt_radiotap = struct.pack('I', DLT_IEEE802_11_RADIO) + try: + fcntl.ioctl(self.bpf_fd, BIOCSDLT, dlt_radiotap) + except IOError: + raise Scapy_Exception("Can't set %s into monitor mode!" % + self.iface) + else: + warning("Scapy won't activate 802.11 monitoring, " + "as it will crash your macOS kernel!") # Don't block on read try: - fcntl.ioctl(self.ins, BIOCIMMEDIATE, struct.pack('I', 1)) + fcntl.ioctl(self.bpf_fd, BIOCIMMEDIATE, struct.pack('I', 1)) except IOError: raise Scapy_Exception("BIOCIMMEDIATE failed on /dev/bpf%i" % self.dev_bpf) @@ -101,12 +197,13 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Scapy will provide the link layer source address # Otherwise, it is written by the kernel try: - fcntl.ioctl(self.ins, BIOCSHDRCMPLT, struct.pack('i', 1)) + fcntl.ioctl(self.bpf_fd, BIOCSHDRCMPLT, struct.pack('i', 1)) except IOError: raise Scapy_Exception("BIOCSHDRCMPLT failed on /dev/bpf%i" % self.dev_bpf) # Configure the BPF filter + filter_attached = False if not nofilter: if conf.except_filter: if filter: @@ -115,23 +212,36 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, filter = "not (%s)" % conf.except_filter if filter is not None: try: - attach_filter(self.ins, filter, self.iface) - except ImportError as ex: - warning("Cannot set filter: %s" % ex) + attach_filter(self.bpf_fd, filter, self.iface) + filter_attached = True + except (ImportError, Scapy_Exception) as ex: + raise Scapy_Exception("Cannot set filter: %s" % ex) + if NETBSD and filter_attached is False: + # On NetBSD, a filter must be attached to an interface, otherwise + # no frame will be received by os.read(). When no filter has been + # configured, Scapy uses a simple tcpdump filter that does nothing + # more than ensuring the length frame is not null. + filter = "greater 0" + try: + attach_filter(self.bpf_fd, filter, self.iface) + except ImportError as ex: + warning("Cannot set filter: %s" % ex) # Set the guessed packet class self.guessed_cls = self.guess_cls() def set_promisc(self, value): + # type: (bool) -> None """Set the interface in promiscuous mode""" try: - fcntl.ioctl(self.ins, BIOCPROMISC, struct.pack('i', value)) + fcntl.ioctl(self.bpf_fd, BIOCPROMISC, struct.pack('i', value)) except IOError: raise Scapy_Exception("Cannot set promiscuous mode on interface " "(%s)!" % self.iface) def __del__(self): + # type: () -> None """Close the file descriptor on delete""" # When the socket is deleted on Scapy exits, __del__ is # sometimes called "too late", and self is None @@ -139,12 +249,13 @@ def __del__(self): self.close() def guess_cls(self): + # type: () -> type """Guess the packet class that must be used on the interface""" # Get the data link type try: - ret = fcntl.ioctl(self.ins, BIOCGDLT, struct.pack('I', 0)) - ret = struct.unpack('I', ret)[0] + ret = fcntl.ioctl(self.bpf_fd, BIOCGDLT, struct.pack('I', 0)) + linktype = struct.unpack('I', ret)[0] except IOError: cls = conf.default_l2 warning("BIOCGDLT failed: unable to guess type. Using %s !", @@ -153,18 +264,20 @@ def guess_cls(self): # Retrieve the corresponding class try: - return conf.l2types[ret] + return conf.l2types.num2layer[linktype] except KeyError: cls = conf.default_l2 - warning("Unable to guess type (type %i). Using %s", ret, cls.name) + warning("Unable to guess type (type %i). Using %s", linktype, cls.name) + return cls def set_nonblock(self, set_flag=True): + # type: (bool) -> None """Set the non blocking flag on the socket""" # Get the current flags if self.fd_flags is None: try: - self.fd_flags = fcntl.fcntl(self.ins, fcntl.F_GETFL) + self.fd_flags = fcntl.fcntl(self.bpf_fd, fcntl.F_GETFL) except IOError: warning("Cannot get flags on this file descriptor !") return @@ -176,50 +289,58 @@ def set_nonblock(self, set_flag=True): new_fd_flags = self.fd_flags & ~os.O_NONBLOCK try: - fcntl.fcntl(self.ins, fcntl.F_SETFL, new_fd_flags) + fcntl.fcntl(self.bpf_fd, fcntl.F_SETFL, new_fd_flags) self.fd_flags = new_fd_flags except Exception: warning("Can't set flags on this file descriptor !") def get_stats(self): + # type: () -> Tuple[Optional[int], Optional[int]] """Get received / dropped statistics""" try: - ret = fcntl.ioctl(self.ins, BIOCGSTATS, struct.pack("2I", 0, 0)) + ret = fcntl.ioctl(self.bpf_fd, BIOCGSTATS, struct.pack("2I", 0, 0)) return struct.unpack("2I", ret) except IOError: warning("Unable to get stats from BPF !") return (None, None) def get_blen(self): + # type: () -> Optional[int] """Get the BPF buffer length""" try: - ret = fcntl.ioctl(self.ins, BIOCGBLEN, struct.pack("I", 0)) - return struct.unpack("I", ret)[0] + ret = fcntl.ioctl(self.bpf_fd, BIOCGBLEN, struct.pack("I", 0)) + return struct.unpack("I", ret)[0] # type: ignore except IOError: warning("Unable to get the BPF buffer length") - return + return None def fileno(self): + # type: () -> int """Get the underlying file descriptor""" - return self.ins + return self.bpf_fd def close(self): + # type: () -> None """Close the Super Socket""" - if not self.closed and self.ins is not None: - os.close(self.ins) + if not self.closed and self.bpf_fd != -1: + os.close(self.bpf_fd) self.closed = True - self.ins = None + self.bpf_fd = -1 + @abc.abstractmethod def send(self, x): + # type: (Packet) -> int """Dummy send method""" raise Exception( "Can't send anything with %s" % self.__class__.__name__ ) + @abc.abstractmethod def recv_raw(self, x=BPF_BUFFER_LENGTH): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 """Dummy recv method""" raise Exception( "Can't recv anything with %s" % self.__class__.__name__ @@ -227,25 +348,29 @@ def recv_raw(self, x=BPF_BUFFER_LENGTH): @staticmethod def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] """This function is called during sendrecv() routine to select the available sockets. """ # sockets, None (means use the socket's recv() ) - return bpf_select(sockets, remain), None + return bpf_select(sockets, remain) class L2bpfListenSocket(_L2bpfSocket): """"Scapy L2 BPF Listen Super Socket""" def __init__(self, *args, **kwargs): - self.received_frames = [] + # type: (*Any, **Any) -> None + self.received_frames = [] # type: List[Tuple[Optional[type], Optional[bytes], Optional[float]]] # noqa: E501 super(L2bpfListenSocket, self).__init__(*args, **kwargs) def buffered_frames(self): + # type: () -> int """Return the number of frames in the buffer""" return len(self.received_frames) def get_frame(self): + # type: () -> Tuple[Optional[type], Optional[bytes], Optional[float]] """Get a frame or packet from the received list""" if self.received_frames: return self.received_frames.pop(0) @@ -254,61 +379,47 @@ def get_frame(self): @staticmethod def bpf_align(bh_h, bh_c): + # type: (int, int) -> int """Return the index to the end of the current packet""" # from return ((bh_h + bh_c) + (BPF_ALIGNMENT - 1)) & ~(BPF_ALIGNMENT - 1) def extract_frames(self, bpf_buffer): - """Extract all frames from the buffer and stored them in the received list.""" # noqa: E501 + # type: (bytes) -> None + """ + Extract all frames from the buffer and stored them in the received list + """ # Ensure that the BPF buffer contains at least the header len_bb = len(bpf_buffer) - if len_bb < 20: # Note: 20 == sizeof(struct bfp_hdr) + if len_bb < _bpf_hdr_len: return # Extract useful information from the BPF header - if FREEBSD: - # Unless we set BIOCSTSTAMP to something different than - # BPF_T_MICROTIME, we will get bpf_hdr on FreeBSD, which means - # that we'll get a struct timeval, which is time_t, suseconds_t. - # On i386 time_t is 32bit so the bh_tstamp will only be 8 bytes. - # We really want to set BIOCSTSTAMP to BPF_T_NANOTIME and be - # done with this and it always be 16? - if platform.machine() == "i386": - # struct bpf_hdr - bh_tstamp_offset = 8 - else: - # struct bpf_hdr (64bit time_t) or struct bpf_xhdr - bh_tstamp_offset = 16 - elif NETBSD: - # struct bpf_hdr or struct bpf_hdr32 - bh_tstamp_offset = 16 - else: - # struct bpf_hdr - bh_tstamp_offset = 8 - - # Parse the BPF header - bh_caplen = struct.unpack('I', bpf_buffer[bh_tstamp_offset:bh_tstamp_offset + 4])[0] # noqa: E501 - next_offset = bh_tstamp_offset + 4 - bh_datalen = struct.unpack('I', bpf_buffer[next_offset:next_offset + 4])[0] # noqa: E501 - next_offset += 4 - bh_hdrlen = struct.unpack('H', bpf_buffer[next_offset:next_offset + 2])[0] # noqa: E501 - if bh_datalen == 0: + bh_hdr = bpf_hdr.from_buffer_copy(bpf_buffer) + if bh_hdr.bh_datalen == 0: return # Get and store the Scapy object - frame_str = bpf_buffer[bh_hdrlen:bh_hdrlen + bh_caplen] + frame_str = bpf_buffer[ + bh_hdr.bh_hdrlen:bh_hdr.bh_hdrlen + bh_hdr.bh_caplen + ] + if _NANOTIME: + ts = bh_hdr.bh_tstamp.tv_sec + 1e-9 * bh_hdr.bh_tstamp.tv_nsec + else: + ts = bh_hdr.bh_tstamp.tv_sec + 1e-6 * bh_hdr.bh_tstamp.tv_usec self.received_frames.append( - (self.guessed_cls, frame_str, None) + (self.guessed_cls, frame_str, ts) ) # Extract the next frame - end = self.bpf_align(bh_hdrlen, bh_caplen) + end = self.bpf_align(bh_hdr.bh_hdrlen, bh_hdr.bh_caplen) if (len_bb - end) >= 20: self.extract_frames(bpf_buffer[end:]) def recv_raw(self, x=BPF_BUFFER_LENGTH): + # type: (int) -> Tuple[Optional[type], Optional[bytes], Optional[float]] """Receive a frame from the network""" x = min(x, BPF_BUFFER_LENGTH) @@ -319,7 +430,7 @@ def recv_raw(self, x=BPF_BUFFER_LENGTH): # Get data from BPF try: - bpf_buffer = os.read(self.ins, x) + bpf_buffer = os.read(self.bpf_fd, x) except EnvironmentError as exc: if exc.errno != errno.EAGAIN: warning("BPF recv_raw()", exc_info=True) @@ -334,10 +445,12 @@ class L2bpfSocket(L2bpfListenSocket): """"Scapy L2 BPF Super Socket""" def send(self, x): + # type: (Packet) -> int """Send a frame""" - return os.write(self.outs, raw(x)) + return os.write(self.bpf_fd, raw(x)) def nonblock_recv(self): + # type: () -> Optional[Packet] """Non blocking receive""" if self.buffered_frames(): @@ -353,60 +466,124 @@ def nonblock_recv(self): class L3bpfSocket(L2bpfSocket): - def recv(self, x=BPF_BUFFER_LENGTH): + def __init__(self, + iface=None, # type: Optional[_GlobInterfaceType] + type=ETH_P_ALL, # type: int + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + nofilter=0, # type: int + monitor=False, # type: bool + ): + super(L3bpfSocket, self).__init__( + iface=iface, + type=type, + promisc=promisc, + filter=filter, + nofilter=nofilter, + monitor=monitor, + ) + self.filter = filter + self.send_socks = {network_name(self.iface): self} + + def recv(self, x: int = BPF_BUFFER_LENGTH, **kwargs: Any) -> Optional['Packet']: """Receive on layer 3""" - r = SuperSocket.recv(self, x) + r = SuperSocket.recv(self, x, **kwargs) if r: r.payload.time = r.time return r.payload return r def send(self, pkt): + # type: (Packet) -> int """Send a packet""" + from scapy.layers.l2 import Loopback # Use the routing table to find the output interface iff = pkt.route()[0] if iff is None: - iff = conf.iface + iff = network_name(conf.iface) # Assign the network interface to the BPF handle - if self.assigned_interface != iff: - try: - fcntl.ioctl(self.outs, BIOCSETIF, struct.pack("16s16x", iff.encode())) # noqa: E501 - except IOError: - raise Scapy_Exception("BIOCSETIF failed on %s" % iff) - self.assigned_interface = iff + if iff not in self.send_socks: + self.send_socks[iff] = L3bpfSocket( + iface=iff, + type=self.type, + filter=self.filter, + promisc=self.promisc, + ) + fd = self.send_socks[iff] # Build the frame - frame = raw(self.guessed_cls() / pkt) + # + # LINKTYPE_NULL / DLT_NULL (Loopback) is a special case. From the + # bpf(4) man page (from macOS/Darwin, but also for BSD): + # + # "A packet can be sent out on the network by writing to a bpf file + # descriptor. [...] Currently only writes to Ethernets and SLIP links + # are supported." + # + # Headers are only mentioned for reads, not writes, and it has the + # name "NULL" and id=0. + # + # The _correct_ behaviour appears to be that one should add a BSD + # Loopback header to every sent packet. This is needed by FreeBSD's + # if_lo, and Darwin's if_lo & if_utun. + # + # tuntaposx appears to have interpreted "NULL" as "no headers". + # Thankfully its interfaces have a different name (tunX) to Darwin's + # if_utun interfaces (utunX). + # + # There might be other drivers which make the same mistake as + # tuntaposx, but these are typically provided with VPN software, and + # Apple are breaking these kexts in a future version of macOS... so + # the problem will eventually go away. They already don't work on Macs + # with Apple Silicon (M1). + if DARWIN and iff.startswith('tun') and self.guessed_cls == Loopback: + frame = pkt + elif FREEBSD and (iff.startswith('tun') or iff.startswith('tap')): + # On FreeBSD, the bpf manpage states that it is only possible + # to write packets to Ethernet and SLIP network interfaces + # using /dev/bpf + # + # Note: `open("/dev/tun0", "wb").write(raw(pkt())) should be + # used + warning("Cannot write to %s according to the documentation!", iff) + return + else: + frame = fd.guessed_cls() / pkt + pkt.sent_time = time.time() # Send the frame - L2bpfSocket.send(self, frame) - + return L2bpfSocket.send(fd, frame) -# Sockets manipulation functions + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + socks = [] # type: List[SuperSocket] + for sock in sockets: + if isinstance(sock, L3bpfSocket): + socks += sock.send_socks.values() + else: + socks.append(sock) + return L2bpfSocket.select(socks, remain=remain) -def isBPFSocket(obj): - """Return True is obj is a BPF Super Socket""" - return isinstance( - obj, - (L2bpfListenSocket, L2bpfListenSocket, L3bpfSocket) - ) +# Sockets manipulation functions def bpf_select(fds_list, timeout=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] """A call to recv() can return several frames. This functions hides the fact that some frames are read from the internal buffer.""" # Check file descriptors types - bpf_scks_buffered = list() + bpf_scks_buffered = list() # type: List[SuperSocket] select_fds = list() for tmp_fd in fds_list: # Specific BPF sockets: get buffers status - if isBPFSocket(tmp_fd) and tmp_fd.buffered_frames(): + if isinstance(tmp_fd, L2bpfListenSocket) and tmp_fd.buffered_frames(): bpf_scks_buffered.append(tmp_fd) continue diff --git a/scapy/arch/common.py b/scapy/arch/common.py index e9380d3554f..697b759db1e 100644 --- a/scapy/arch/common.py +++ b/scapy/arch/common.py @@ -1,72 +1,74 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Functions common to different architectures """ import ctypes +import re import socket -import struct -import sys -import time -from scapy.consts import WINDOWS -from scapy.config import conf -from scapy.data import MTU, ARPHRD_TO_DLT -from scapy.error import Scapy_Exception - -if not WINDOWS: - from fcntl import ioctl - -# UTILS - - -def get_if(iff, cmd): - """Ease SIOCGIF* ioctl calls""" - - sck = socket.socket() - try: - return ioctl(sck, cmd, struct.pack("16s16x", iff.encode("utf8"))) - finally: - sck.close() - - -def get_if_raw_hwaddr(iff): - """Get the raw MAC address of a local interface. - - This function uses SIOCGIFHWADDR calls, therefore only works - on some distros. - - :param iff: the network interface name as a string - :returns: the corresponding raw MAC address - """ - from scapy.arch import SIOCGIFHWADDR - return struct.unpack("16xh6s8x", get_if(iff, SIOCGIFHWADDR)) - -# SOCKET UTILS +from scapy.config import conf +from scapy.data import MTU, ARPHRD_TO_DLT, DLT_RAW_ALT, DLT_RAW +from scapy.error import Scapy_Exception, warning +from scapy.interfaces import network_name, resolve_iface, NetworkInterface +from scapy.libs.structures import bpf_program +from scapy.pton_ntop import inet_pton +from scapy.utils import decode_locale_str + +# Type imports +import scapy +from typing import ( + List, + Optional, + Union, +) + +# From if.h +_iff_flags = [ + "UP", + "BROADCAST", + "DEBUG", + "LOOPBACK", + "POINTTOPOINT", + "NOTRAILERS", + "RUNNING", + "NOARP", + "PROMISC", + "ALLMULTI", + "MASTER", + "SLAVE", + "MULTICAST", + "PORTSEL", + "AUTOMEDIA", + "DYNAMIC", + "LOWER_UP", + "DORMANT", + "ECHO" +] + + +def get_if_raw_addr(iff): + # type: (Union[NetworkInterface, str]) -> bytes + """Return the raw IPv4 address of interface""" + iff = resolve_iface(iff) + if not iff.ip: + return b"\x00" * 4 + return inet_pton(socket.AF_INET, iff.ip) -def _select_nonblock(sockets, remain=None): - """This function is called during sendrecv() routine to select - the available sockets. - """ - # pcap sockets aren't selectable, so we return all of them - # and ask the selecting functions to use nonblock_recv instead of recv - def _sleep_nonblock_recv(self): - res = self.nonblock_recv() - if res is None: - time.sleep(conf.recv_poll_rate) - return res - # we enforce remain=None: don't wait. - return sockets, _sleep_nonblock_recv # BPF HANDLERS -def compile_filter(filter_exp, iface=None, linktype=None, - promisc=False): +def compile_filter(filter_exp, # type: str + iface=None, # type: Optional[Union[str, 'scapy.interfaces.NetworkInterface']] # noqa: E501 + linktype=None, # type: Optional[int] + promisc=False # type: bool + ): + # type: (...) -> bpf_program """Asks libpcap to parse the filter, then build the matching BPF bytecode. @@ -81,7 +83,6 @@ def compile_filter(filter_exp, iface=None, linktype=None, pcap_compile_nopcap, pcap_close ) - from scapy.libs.structures import bpf_program except OSError: raise ImportError( "libpcap is not available. Cannot compile filter !" @@ -96,43 +97,59 @@ def compile_filter(filter_exp, iface=None, linktype=None, raise Scapy_Exception( "Please provide an interface or linktype!" ) - if WINDOWS: - iface = conf.iface.pcap_name - else: - iface = conf.iface + iface = conf.iface # Try to guess linktype to avoid requiring root try: - arphd = get_if_raw_hwaddr(iface)[0] + arphd = resolve_iface(iface).type linktype = ARPHRD_TO_DLT.get(arphd) except Exception: # Failed to use linktype: use the interface pass if linktype is not None: + # Some conversion aliases (e.g. linktype_to_dlt in libpcap) + if linktype == DLT_RAW_ALT: + linktype = DLT_RAW ret = pcap_compile_nopcap( - MTU, linktype, ctypes.byref(bpf), bpf_filter, 0, -1 + MTU, linktype, ctypes.byref(bpf), bpf_filter, 1, -1 ) elif iface: err = create_string_buffer(PCAP_ERRBUF_SIZE) - iface = create_string_buffer(iface.encode("utf8")) + iface_b = create_string_buffer(network_name(iface).encode("utf8")) pcap = pcap_open_live( - iface, MTU, promisc, 0, err + iface_b, MTU, promisc, 0, err ) - error = bytes(bytearray(err)).strip(b"\x00") + error = decode_locale_str(bytearray(err).strip(b"\x00")) if error: raise OSError(error) ret = pcap_compile( - pcap, ctypes.byref(bpf), bpf_filter, 0, -1 + pcap, ctypes.byref(bpf), bpf_filter, 1, -1 ) pcap_close(pcap) if ret == -1: raise Scapy_Exception( "Failed to compile filter expression %s (%s)" % (filter_exp, ret) ) - if conf.use_pypy and sys.pypy_version_info <= (7, 3, 0): - # PyPy < 7.3.0 has a broken behavior - # https://bitbucket.org/pypy/pypy/issues/3114 - return struct.pack( - 'HL', - bpf.bf_len, ctypes.addressof(bpf.bf_insns.contents) - ) return bpf + + +def free_filter(bp: bpf_program) -> None: + """ + Free a bpf_program created with compile_filter + """ + from scapy.libs.winpcapy import pcap_freecode + pcap_freecode(ctypes.byref(bp)) + + +####### +# DNS # +####### + +def read_nameservers() -> List[str]: + """Return the nameservers configured by the OS + """ + try: + with open('/etc/resolv.conf', 'r') as fd: + return re.findall(r"nameserver\s+([^\s]+)", fd.read()) + except FileNotFoundError: + warning("Could not retrieve the OS's nameserver !") + return [] diff --git a/scapy/arch/libpcap.py b/scapy/arch/libpcap.py new file mode 100644 index 00000000000..80816a9083e --- /dev/null +++ b/scapy/arch/libpcap.py @@ -0,0 +1,698 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Packet sending and receiving libpcap/WinPcap. +""" + +import os +import platform +import socket +import struct +import time + +from scapy.automaton import select_objects +from scapy.compat import raw, plain_str +from scapy.config import conf +from scapy.consts import WINDOWS, LINUX, BSD, SOLARIS +from scapy.data import ( + DLT_RAW_ALT, + DLT_RAW, + ETH_P_ALL, + MTU, +) +from scapy.error import ( + Scapy_Exception, + log_loading, + log_runtime, + warning, +) +from scapy.interfaces import ( + InterfaceProvider, + NetworkInterface, + _GlobInterfaceType, + network_name, +) +from scapy.packet import Packet +from scapy.pton_ntop import inet_ntop +from scapy.supersocket import SuperSocket +from scapy.utils import str2mac, decode_locale_str + +import scapy.consts + +from typing import ( + Any, + Dict, + List, + NoReturn, + Optional, + Tuple, + Type, + cast, +) + +if not scapy.consts.WINDOWS: + from fcntl import ioctl + +# AF_LINK is only available and provided on BSD (MAC) +# but because we use its value elsewhere, let's patch it. +if not hasattr(socket, "AF_LINK"): + socket.AF_LINK = 18 # type: ignore + +############ +# COMMON # +############ + +# From BSD net/bpf.h +# BIOCIMMEDIATE = 0x80044270 +BIOCIMMEDIATE = -2147204496 + +# https://github.com/the-tcpdump-group/libpcap/blob/master/pcap/pcap.h +PCAP_IF_UP = 0x00000002 # interface is up +_pcap_if_flags = [ + "LOOPBACK", + "UP", + "RUNNING", + "WIRELESS", + "OK", + "DISCONNECTED", + "NA" +] + + +class _L2libpcapSocket(SuperSocket): + __slots__ = ["pcap_fd", "lvl"] + + def __init__(self, fd): + # type: (_PcapWrapper_libpcap) -> None + self.pcap_fd = fd + ll = self.pcap_fd.datalink() + if ll in conf.l2types: + self.LL = conf.l2types[ll] + if ll in [ + DLT_RAW, + DLT_RAW_ALT, + ]: + self.lvl = 3 + else: + self.lvl = 2 + else: + self.LL = conf.default_l2 + warning( + "Unable to guess datalink type " + "(interface=%s linktype=%i). Using %s", + self.iface, ll, self.LL.name + ) + + def recv_raw(self, x=MTU): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + """ + Receives a packet, then returns a tuple containing + (cls, pkt_data, time) + """ + ts, pkt = self.pcap_fd.next() + if pkt is None: + return None, None, None + return self.LL, pkt, ts + + def nonblock_recv(self, x=MTU): + # type: (int) -> Optional[Packet] + """Receives and dissect a packet in non-blocking mode.""" + self.pcap_fd.setnonblock(True) + p = self.recv(x) + self.pcap_fd.setnonblock(False) + return p + + def fileno(self): + # type: () -> int + return self.pcap_fd.fileno() + + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + return select_objects(sockets, remain) + + def close(self): + # type: () -> None + if self.closed: + return + self.closed = True + if hasattr(self, "pcap_fd"): + # If failed to open, won't exist + self.pcap_fd.close() + + +########## +# PCAP # +########## + +if WINDOWS: + NPCAP_PATH = "" + +if conf.use_pcap: + if WINDOWS: + # Windows specific + NPCAP_PATH = os.environ["WINDIR"] + "\\System32\\Npcap" + from scapy.libs.winpcapy import pcap_setmintocopy, pcap_getevent + else: + from scapy.libs.winpcapy import pcap_get_selectable_fd + from ctypes import POINTER, byref, create_string_buffer, c_ubyte, cast as ccast + + # Part of the Winpcapy integration was inspired by phaethon/scapy + # but he destroyed the commit history, so there is no link to that + try: + from scapy.libs.winpcapy import ( + PCAP_ERRBUF_SIZE, + PCAP_ERROR, + PCAP_ERROR_NO_SUCH_DEVICE, + PCAP_ERROR_PERM_DENIED, + bpf_program, + pcap_close, + pcap_compile, + pcap_datalink, + pcap_findalldevs, + pcap_freealldevs, + pcap_geterr, + pcap_if_t, + pcap_lib_version, + pcap_next_ex, + pcap_open_live, + pcap_pkthdr, + pcap_setfilter, + pcap_setnonblock, + sockaddr_in, + sockaddr_in6, + ) + try: + from scapy.libs.winpcapy import pcap_inject + except ImportError: + # Fallback for Winpcap... (for how long?) + from scapy.libs.winpcapy import pcap_sendpacket as pcap_inject + + def load_winpcapy(): + # type: () -> None + """This functions calls libpcap ``pcap_findalldevs`` function, + and extracts and parse all the data scapy will need + to build the Interface List. + + The data will be stored in ``conf.cache_pcapiflist`` + """ + from scapy.fields import FlagValue + + err = create_string_buffer(PCAP_ERRBUF_SIZE) + devs = POINTER(pcap_if_t)() + if_list = {} + if pcap_findalldevs(byref(devs), err) < 0: + return + try: + p = devs + # Iterate through the different interfaces + while p: + name = plain_str(p.contents.name) # GUID + description = plain_str( + p.contents.description or "" + ) # DESC + flags = p.contents.flags # FLAGS + ips = [] + mac = "" + itype = -1 + a = p.contents.addresses + while a: + # IPv4 address + family = a.contents.addr.contents.sa_family + ap = a.contents.addr + if family == socket.AF_INET: + val = ccast(ap, POINTER(sockaddr_in)) + addr_raw = val.contents.sin_addr[:] + elif family == socket.AF_INET6: + val = ccast(ap, POINTER(sockaddr_in6)) + addr_raw = val.contents.sin6_addr[:] + elif family == socket.AF_LINK: + # Special case: MAC + # (AF_LINK is mostly BSD specific) + val = ap.contents.sa_data + mac = str2mac(bytes(bytearray(val[:6]))) + a = a.contents.next + continue + else: + # Unknown AF + a = a.contents.next + continue + addr = inet_ntop(family, bytes(bytearray(addr_raw))) + if addr != "0.0.0.0": + ips.append(addr) + a = a.contents.next + flags = FlagValue(flags, _pcap_if_flags) + if_list[name] = (description, ips, flags, mac, itype) + p = p.contents.next + conf.cache_pcapiflist = if_list + except Exception: + raise + finally: + pcap_freealldevs(devs) + except OSError: + conf.use_pcap = False + if WINDOWS: + if conf.interactive: + log_loading.critical( + "Npcap/Winpcap is not installed ! See " + "https://scapy.readthedocs.io/en/latest/installation.html#windows" # noqa: E501 + ) + else: + if conf.interactive: + log_loading.critical( + "Libpcap is not installed!" + ) + else: + if WINDOWS: + # Detect Pcap version: check for Npcap + version = pcap_lib_version() + if b"winpcap" in version.lower(): + if os.path.exists(NPCAP_PATH + "\\wpcap.dll"): + warning("Winpcap is installed over Npcap. " + "Will use Winpcap (see 'Winpcap/Npcap conflicts' " + "in Scapy's docs)") + elif platform.release() != "XP": + warning("WinPcap is now deprecated (not maintained). " + "Please use Npcap instead") + elif b"npcap" in version.lower(): + conf.use_npcap = True + conf.loopback_name = conf.loopback_name = "Npcap Loopback Adapter" # noqa: E501 + +if conf.use_pcap: + class _PcapWrapper_libpcap: # noqa: F811 + """Wrapper for the libpcap calls""" + + def __init__(self, + device, # type: _GlobInterfaceType + snaplen, # type: int + promisc, # type: bool + to_ms, # type: int + monitor=None, # type: Optional[bool] + ): + # type: (...) -> None + self.errbuf = create_string_buffer(PCAP_ERRBUF_SIZE) + self.iface = create_string_buffer( + network_name(device).encode("utf8") + ) + self.dtl = -1 + if not WINDOWS or conf.use_npcap: + from scapy.libs.winpcapy import pcap_create + self.pcap = pcap_create(self.iface, self.errbuf) + if not self.pcap: + error = decode_locale_str(bytearray(self.errbuf).strip(b"\x00")) + if error: + raise OSError(error) + # Non-winpcap functions + from scapy.libs.winpcapy import ( + pcap_set_snaplen, + pcap_set_promisc, + pcap_set_timeout, + pcap_set_rfmon, + pcap_activate, + pcap_statustostr, + pcap_geterr, + ) + if pcap_set_snaplen(self.pcap, snaplen) != 0: + error = decode_locale_str(bytearray(self.errbuf).strip(b"\x00")) + if error: + raise OSError(error) + log_runtime.error("Could not set snaplen") + if pcap_set_promisc(self.pcap, promisc) != 0: + error = decode_locale_str(bytearray(self.errbuf).strip(b"\x00")) + if error: + raise OSError(error) + log_runtime.error("Could not set promisc") + if pcap_set_timeout(self.pcap, to_ms) != 0: + error = decode_locale_str(bytearray(self.errbuf).strip(b"\x00")) + if error: + raise OSError(error) + log_runtime.error("Could not set timeout") + if monitor: + if pcap_set_rfmon(self.pcap, 1) != 0: + error = decode_locale_str(bytearray(self.errbuf).strip(b"\x00")) + if error: + raise OSError(error) + log_runtime.error("Could not set monitor mode") + status = pcap_activate(self.pcap) + # status == 0 means success + # status < 0 means error + # status > 0 means success, but with a warning + if status < 0: + # self.iface, and strings we get back from + # pcap_geterr() and pcap_statustostr(), have the + # type "bytes". + # + # decode_locale_str() turns them into strings. + iface = decode_locale_str( + bytearray(self.iface).strip(b"\x00") + ) + errstr = decode_locale_str( + bytearray(pcap_geterr(self.pcap)).strip(b"\x00") + ) + statusstr = decode_locale_str( + bytearray(pcap_statustostr(status)).strip(b"\x00") + ) + if status == PCAP_ERROR: + errmsg = errstr + elif status == PCAP_ERROR_NO_SUCH_DEVICE: + errmsg = "%s: %s\n(%s)" % (iface, statusstr, errstr) + elif status == PCAP_ERROR_PERM_DENIED and errstr != "": + errmsg = "%s: %s\n(%s)" % (iface, statusstr, errstr) + else: + errmsg = "%s: %s" % (iface, statusstr) + raise OSError(errmsg) + else: + if WINDOWS and monitor: + raise OSError("On Windows, this feature requires NPcap !") + self.pcap = pcap_open_live(self.iface, + snaplen, promisc, to_ms, + self.errbuf) + error = decode_locale_str(bytearray(self.errbuf).strip(b"\x00")) + if error: + raise OSError(error) + + if WINDOWS: + # On Windows, we need to cache whether there are still packets in the + # queue or not. When they aren't, then we select normally like on linux. + self.remaining = True + # Winpcap/Npcap exclusive: make every packet to be instantly + # returned, and not buffered within Winpcap/Npcap + pcap_setmintocopy(self.pcap, 0) + + self.header = POINTER(pcap_pkthdr)() + self.pkt_data = POINTER(c_ubyte)() + self.bpf_program = bpf_program() + + def next(self): + # type: () -> Tuple[Optional[float], Optional[bytes]] + """ + Returns the next packet as the tuple + (timestamp, raw_packet) + """ + c = pcap_next_ex( + self.pcap, + byref(self.header), + byref(self.pkt_data) + ) + if not c > 0: + self.remaining = False # we emptied the queue + return None, None + else: + self.remaining = True + ts = ( + self.header.contents.ts.tv_sec + + float(self.header.contents.ts.tv_usec) / 1e6 + ) + pkt = bytes(bytearray( + self.pkt_data[:self.header.contents.len] + )) + return ts, pkt + __next__ = next + + def datalink(self): + # type: () -> int + """Wrapper around pcap_datalink""" + if self.dtl == -1: + self.dtl = pcap_datalink(self.pcap) + return self.dtl + + def fileno(self): + # type: () -> int + if WINDOWS: + if self.remaining: + # Still packets in the queue. Don't select + return -1 + return cast(int, pcap_getevent(self.pcap)) + else: + # This does not exist under Windows + return cast(int, pcap_get_selectable_fd(self.pcap)) + + def setfilter(self, f): + # type: (str) -> None + filter_exp = create_string_buffer(f.encode("utf8")) + if pcap_compile(self.pcap, byref(self.bpf_program), filter_exp, 1, -1) >= 0: # noqa: E501 + if pcap_setfilter(self.pcap, byref(self.bpf_program)) >= 0: + # Success + return + errstr = decode_locale_str( + bytearray(pcap_geterr(self.pcap)).strip(b"\x00") + ) + raise Scapy_Exception("Cannot set filter: %s" % errstr) + + def setnonblock(self, i): + # type: (bool) -> None + pcap_setnonblock(self.pcap, i, self.errbuf) + + def send(self, x): + # type: (bytes) -> int + return pcap_inject(self.pcap, x, len(x)) # type: ignore + + def close(self): + # type: () -> None + pcap_close(self.pcap) + open_pcap = _PcapWrapper_libpcap + + class LibpcapProvider(InterfaceProvider): + """ + Load interfaces from Libpcap on non-Windows machines + """ + name = "libpcap" + libpcap = True + + def load(self): + # type: () -> Dict[str, NetworkInterface] + if not conf.use_pcap or WINDOWS: + return {} + if not conf.cache_pcapiflist: + load_winpcapy() + data = {} + i = 0 + for ifname, dat in conf.cache_pcapiflist.items(): + description, ips, flags, mac, itype = dat + i += 1 + if LINUX or BSD or SOLARIS and not mac: + from scapy.arch.unix import get_if_raw_hwaddr + try: + itype, _mac = get_if_raw_hwaddr(ifname) + mac = str2mac(_mac) + except Exception: + # There are at least 3 different possible exceptions + mac = "00:00:00:00:00:00" + if_data = { + 'name': ifname, + 'description': description or ifname, + 'network_name': ifname, + 'index': i, + 'mac': mac, + 'type': itype, + 'ips': ips, + 'flags': flags + } + data[ifname] = NetworkInterface(self, if_data) + return data + + def reload(self): + # type: () -> Dict[str, NetworkInterface] + if conf.use_pcap: + from scapy.arch.libpcap import load_winpcapy + load_winpcapy() + return self.load() + + if not WINDOWS: + conf.ifaces.register_provider(LibpcapProvider) + + # pcap sockets + + class L2pcapListenSocket(_L2libpcapSocket): + desc = "read packets at layer 2 using libpcap" + + def __init__(self, + iface=None, # type: Optional[_GlobInterfaceType] + type=ETH_P_ALL, # type: int + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + monitor=None, # type: Optional[bool] + ): + # type: (...) -> None + self.type = type + self.outs = None + if iface is None: + iface = conf.iface + self.iface = iface + if promisc is not None: + self.promisc = promisc + else: + self.promisc = conf.sniff_promisc + self.monitor = monitor + fd = open_pcap( + device=iface, + snaplen=MTU, + promisc=self.promisc, + to_ms=100, + monitor=self.monitor, + ) + super(L2pcapListenSocket, self).__init__(fd) + try: + if not WINDOWS: + ioctl( + self.pcap_fd.fileno(), + BIOCIMMEDIATE, + struct.pack("I", 1) + ) + except Exception: + pass + if type == ETH_P_ALL: # Do not apply any filter if Ethernet type is given # noqa: E501 + if conf.except_filter: + if filter: + filter = "(%s) and not (%s)" % (filter, conf.except_filter) # noqa: E501 + else: + filter = "not (%s)" % conf.except_filter + if filter: + self.pcap_fd.setfilter(filter) + + def send(self, x): + # type: (Packet) -> NoReturn + raise Scapy_Exception( + "Can't send anything with L2pcapListenSocket" + ) + + class L2pcapSocket(_L2libpcapSocket): + desc = "read/write packets at layer 2 using only libpcap" + + def __init__(self, + iface=None, # type: Optional[_GlobInterfaceType] + type=ETH_P_ALL, # type: int + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + nofilter=0, # type: int + monitor=None # type: Optional[bool] + ): + # type: (...) -> None + if iface is None: + iface = conf.iface + self.iface = iface + self.type = type + if promisc is not None: + self.promisc = promisc + else: + self.promisc = conf.sniff_promisc + self.monitor = monitor + fd = open_pcap( + device=iface, + snaplen=MTU, + promisc=self.promisc, + to_ms=100, + monitor=self.monitor, + ) + super(L2pcapSocket, self).__init__(fd) + try: + if not WINDOWS: + ioctl( + self.pcap_fd.fileno(), + BIOCIMMEDIATE, + struct.pack("I", 1) + ) + except Exception: + pass + if nofilter: + if type != ETH_P_ALL: + # PF_PACKET stuff. Need to emulate this for pcap + filter = "ether proto %i" % type + else: + filter = None + else: + if conf.except_filter: + if filter: + filter = "(%s) and not (%s)" % (filter, conf.except_filter) # noqa: E501 + else: + filter = "not (%s)" % conf.except_filter + if type != ETH_P_ALL: + # PF_PACKET stuff. Need to emulate this for pcap + if filter: + filter = "(ether proto %i) and (%s)" % (type, filter) + else: + filter = "ether proto %i" % type + self.filter = filter + if filter: + self.pcap_fd.setfilter(filter) + + def send(self, x): + # type: (Packet) -> int + sx = raw(x) + try: + x.sent_time = time.time() + except AttributeError: + pass + return self.pcap_fd.send(sx) + + class L3pcapSocket(L2pcapSocket): + desc = "read/write packets at layer 3 using only libpcap" + + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + super(L3pcapSocket, self).__init__(*args, **kwargs) + self.send_socks = {network_name(self.iface): self} + + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + r = L2pcapSocket.recv(self, x, **kwargs) + if r and self.lvl == 2: + r.payload.time = r.time + return r.payload + return r + + def send(self, x): + # type: (Packet) -> int + # Select the file descriptor to send the packet on. + iff = x.route()[0] + if iff is None: + iff = network_name(conf.iface) + type_x = type(x) + if iff not in self.send_socks: + self.send_socks[iff] = L3pcapSocket( + iface=iff, + type=self.type, + filter=self.filter, + promisc=self.promisc, + monitor=self.monitor, + ) + sock = self.send_socks[iff] + fd = sock.pcap_fd + if sock.lvl == 3: + if not issubclass(sock.LL, type_x): + warning("Incompatible L3 types detected using %s instead of %s !", + type_x, sock.LL) + sock.LL = type_x + if sock.lvl == 2: + sx = bytes(sock.LL() / x) + else: + sx = bytes(x) + # Now send. + try: + x.sent_time = time.time() + except AttributeError: + pass + return fd.send(sx) + + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + socks = [] # type: List[SuperSocket] + for sock in sockets: + if isinstance(sock, L3pcapSocket): + socks += sock.send_socks.values() + else: + socks.append(sock) + return L2pcapSocket.select(socks, remain=remain) + + def close(self): + # type: () -> None + if self.closed: + return + super(L3pcapSocket, self).close() + for fd in self.send_socks.values(): + if fd is not self: + fd.close() diff --git a/scapy/arch/linux.py b/scapy/arch/linux.py deleted file mode 100644 index 3f14e890651..00000000000 --- a/scapy/arch/linux.py +++ /dev/null @@ -1,637 +0,0 @@ -# This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license - -""" -Linux specific functions. -""" - -from __future__ import absolute_import - - -import array -from fcntl import ioctl -import os -from select import select -import socket -import struct -import time -import re - -import subprocess - -from scapy.compat import raw, plain_str -from scapy.consts import LOOPBACK_NAME, LINUX -import scapy.utils -import scapy.utils6 -from scapy.packet import Packet, Padding -from scapy.config import conf -from scapy.data import MTU, ETH_P_ALL, SOL_PACKET, SO_ATTACH_FILTER, \ - SO_TIMESTAMPNS -from scapy.supersocket import SuperSocket -from scapy.error import warning, Scapy_Exception, \ - ScapyInvalidPlatformException, log_runtime -from scapy.arch.common import get_if, compile_filter -import scapy.modules.six as six -from scapy.modules.six.moves import range - -from scapy.arch.common import get_if_raw_hwaddr # noqa: F401 - -# From bits/ioctls.h -SIOCGIFHWADDR = 0x8927 # Get hardware address -SIOCGIFADDR = 0x8915 # get PA address -SIOCGIFNETMASK = 0x891b # get network PA mask -SIOCGIFNAME = 0x8910 # get iface name -SIOCSIFLINK = 0x8911 # set iface channel -SIOCGIFCONF = 0x8912 # get iface list -SIOCGIFFLAGS = 0x8913 # get flags -SIOCSIFFLAGS = 0x8914 # set flags -SIOCGIFINDEX = 0x8933 # name -> if_index mapping -SIOCGIFCOUNT = 0x8938 # get number of devices -SIOCGSTAMP = 0x8906 # get packet timestamp (as a timeval) - -# From if.h -IFF_UP = 0x1 # Interface is up. -IFF_BROADCAST = 0x2 # Broadcast address valid. -IFF_DEBUG = 0x4 # Turn on debugging. -IFF_LOOPBACK = 0x8 # Is a loopback net. -IFF_POINTOPOINT = 0x10 # Interface is point-to-point link. -IFF_NOTRAILERS = 0x20 # Avoid use of trailers. -IFF_RUNNING = 0x40 # Resources allocated. -IFF_NOARP = 0x80 # No address resolution protocol. -IFF_PROMISC = 0x100 # Receive all packets. - -# From netpacket/packet.h -PACKET_ADD_MEMBERSHIP = 1 -PACKET_DROP_MEMBERSHIP = 2 -PACKET_RECV_OUTPUT = 3 -PACKET_RX_RING = 5 -PACKET_STATISTICS = 6 -PACKET_MR_MULTICAST = 0 -PACKET_MR_PROMISC = 1 -PACKET_MR_ALLMULTI = 2 - -# From net/route.h -RTF_UP = 0x0001 # Route usable -RTF_REJECT = 0x0200 - -# From if_packet.h -PACKET_HOST = 0 # To us -PACKET_BROADCAST = 1 # To all -PACKET_MULTICAST = 2 # To group -PACKET_OTHERHOST = 3 # To someone else -PACKET_OUTGOING = 4 # Outgoing of any type -PACKET_LOOPBACK = 5 # MC/BRD frame looped back -PACKET_USER = 6 # To user space -PACKET_KERNEL = 7 # To kernel space -PACKET_AUXDATA = 8 -PACKET_FASTROUTE = 6 # Fastrouted frame -# Unused, PACKET_FASTROUTE and PACKET_LOOPBACK are invisible to user space - -# Utils - - -def get_if_raw_addr(iff): - try: - return get_if(iff, SIOCGIFADDR)[20:24] - except IOError: - return b"\0\0\0\0" - - -def get_if_list(): - try: - f = open("/proc/net/dev", "rb") - except IOError: - try: - f.close() - except Exception: - pass - warning("Can't open /proc/net/dev !") - return [] - lst = [] - f.readline() - f.readline() - for line in f: - line = plain_str(line) - lst.append(line.split(":")[0].strip()) - f.close() - return lst - - -def get_working_if(): - """ - Return the name of the first network interfcace that is up. - """ - for i in get_if_list(): - if i == LOOPBACK_NAME: - continue - ifflags = struct.unpack("16xH14x", get_if(i, SIOCGIFFLAGS))[0] - if ifflags & IFF_UP: - return i - return LOOPBACK_NAME - - -def attach_filter(sock, bpf_filter, iface): - """ - Compile bpf filter and attach it to a socket - - :param sock: the python socket - :param bpf_filter: the bpf string filter to compile - :param iface: the interface used to compile - """ - bp = compile_filter(bpf_filter, iface) - sock.setsockopt(socket.SOL_SOCKET, SO_ATTACH_FILTER, bp) - - -def set_promisc(s, iff, val=1): - mreq = struct.pack("IHH8s", get_if_index(iff), PACKET_MR_PROMISC, 0, b"") - if val: - cmd = PACKET_ADD_MEMBERSHIP - else: - cmd = PACKET_DROP_MEMBERSHIP - s.setsockopt(SOL_PACKET, cmd, mreq) - - -def get_alias_address(iface_name, ip_mask, gw_str, metric): - """ - Get the correct source IP address of an interface alias - """ - - # Detect the architecture - if scapy.consts.IS_64BITS: - offset, name_len = 16, 40 - else: - offset, name_len = 32, 32 - - # Retrieve interfaces structures - sck = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - names = array.array('B', b'\0' * 4096) - ifreq = ioctl(sck.fileno(), SIOCGIFCONF, - struct.pack("iL", len(names), names.buffer_info()[0])) - - # Extract interfaces names - out = struct.unpack("iL", ifreq)[0] - names = names.tobytes() if six.PY3 else names.tostring() - names = [names[i:i + offset].split(b'\0', 1)[0] for i in range(0, out, name_len)] # noqa: E501 - - # Look for the IP address - for ifname in names: - # Only look for a matching interface name - if not ifname.decode("utf8").startswith(iface_name): - continue - - # Retrieve and convert addresses - ifreq = ioctl(sck, SIOCGIFADDR, struct.pack("16s16x", ifname)) - ifaddr = struct.unpack(">I", ifreq[20:24])[0] - ifreq = ioctl(sck, SIOCGIFNETMASK, struct.pack("16s16x", ifname)) - msk = struct.unpack(">I", ifreq[20:24])[0] - - # Get the full interface name - ifname = plain_str(ifname) - if ':' in ifname: - ifname = ifname[:ifname.index(':')] - else: - continue - - # Check if the source address is included in the network - if (ifaddr & msk) == ip_mask: - sck.close() - return (ifaddr & msk, msk, gw_str, ifname, - scapy.utils.ltoa(ifaddr), metric) - - sck.close() - return - - -def read_routes(): - try: - f = open("/proc/net/route", "rb") - except IOError: - warning("Can't open /proc/net/route !") - return [] - routes = [] - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - ifreq = ioctl(s, SIOCGIFADDR, struct.pack("16s16x", scapy.consts.LOOPBACK_NAME.encode("utf8"))) # noqa: E501 - addrfamily = struct.unpack("h", ifreq[16:18])[0] - if addrfamily == socket.AF_INET: - ifreq2 = ioctl(s, SIOCGIFNETMASK, struct.pack("16s16x", scapy.consts.LOOPBACK_NAME.encode("utf8"))) # noqa: E501 - msk = socket.ntohl(struct.unpack("I", ifreq2[20:24])[0]) - dst = socket.ntohl(struct.unpack("I", ifreq[20:24])[0]) & msk - ifaddr = scapy.utils.inet_ntoa(ifreq[20:24]) - routes.append((dst, msk, "0.0.0.0", scapy.consts.LOOPBACK_NAME, ifaddr, 1)) # noqa: E501 - else: - warning("Interface %s: unknown address family (%i)" % (scapy.consts.LOOPBACK_NAME, addrfamily)) # noqa: E501 - except IOError as err: - if err.errno == 99: - warning("Interface %s: no address assigned" % scapy.consts.LOOPBACK_NAME) # noqa: E501 - else: - warning("Interface %s: failed to get address config (%s)" % (scapy.consts.LOOPBACK_NAME, str(err))) # noqa: E501 - - for line in f.readlines()[1:]: - line = plain_str(line) - iff, dst, gw, flags, _, _, metric, msk, _, _, _ = line.split() - flags = int(flags, 16) - if flags & RTF_UP == 0: - continue - if flags & RTF_REJECT: - continue - try: - ifreq = ioctl(s, SIOCGIFADDR, struct.pack("16s16x", iff.encode("utf8"))) # noqa: E501 - except IOError: # interface is present in routing tables but does not have any assigned IP # noqa: E501 - ifaddr = "0.0.0.0" - ifaddr_int = 0 - else: - addrfamily = struct.unpack("h", ifreq[16:18])[0] - if addrfamily == socket.AF_INET: - ifaddr = scapy.utils.inet_ntoa(ifreq[20:24]) - ifaddr_int = struct.unpack("!I", ifreq[20:24])[0] - else: - warning("Interface %s: unknown address family (%i)", iff, addrfamily) # noqa: E501 - continue - - # Attempt to detect an interface alias based on addresses inconsistencies # noqa: E501 - dst_int = socket.htonl(int(dst, 16)) & 0xffffffff - msk_int = socket.htonl(int(msk, 16)) & 0xffffffff - gw_str = scapy.utils.inet_ntoa(struct.pack("I", int(gw, 16))) - metric = int(metric) - - if ifaddr_int & msk_int != dst_int: - tmp_route = get_alias_address(iff, dst_int, gw_str, metric) - if tmp_route: - routes.append(tmp_route) - else: - routes.append((dst_int, msk_int, gw_str, iff, ifaddr, metric)) - - else: - routes.append((dst_int, msk_int, gw_str, iff, ifaddr, metric)) - - f.close() - s.close() - return routes - -############ -# IPv6 # -############ - - -def in6_getifaddr(): - """ - Returns a list of 3-tuples of the form (addr, scope, iface) where - 'addr' is the address of scope 'scope' associated to the interface - 'iface'. - - This is the list of all addresses of all interfaces available on - the system. - """ - ret = [] - try: - fdesc = open("/proc/net/if_inet6", "rb") - except IOError: - return ret - for line in fdesc: - # addr, index, plen, scope, flags, ifname - tmp = plain_str(line).split() - addr = scapy.utils6.in6_ptop( - b':'.join( - struct.unpack('4s4s4s4s4s4s4s4s', tmp[0].encode()) - ).decode() - ) - # (addr, scope, iface) - ret.append((addr, int(tmp[3], 16), tmp[5])) - fdesc.close() - return ret - - -def read_routes6(): - try: - f = open("/proc/net/ipv6_route", "rb") - except IOError: - return [] - # 1. destination network - # 2. destination prefix length - # 3. source network displayed - # 4. source prefix length - # 5. next hop - # 6. metric - # 7. reference counter (?!?) - # 8. use counter (?!?) - # 9. flags - # 10. device name - routes = [] - - def proc2r(p): - ret = struct.unpack('4s4s4s4s4s4s4s4s', p) - ret = b':'.join(ret).decode() - return scapy.utils6.in6_ptop(ret) - - lifaddr = in6_getifaddr() - for line in f.readlines(): - d, dp, _, _, nh, metric, rc, us, fl, dev = line.split() - metric = int(metric, 16) - fl = int(fl, 16) - dev = plain_str(dev) - - if fl & RTF_UP == 0: - continue - if fl & RTF_REJECT: - continue - - d = proc2r(d) - dp = int(dp, 16) - nh = proc2r(nh) - - cset = [] # candidate set (possible source addresses) - if dev == LOOPBACK_NAME: - if d == '::': - continue - cset = ['::1'] - else: - devaddrs = (x for x in lifaddr if x[2] == dev) - cset = scapy.utils6.construct_source_candidate_set(d, dp, devaddrs) - - if len(cset) != 0: - routes.append((d, dp, nh, dev, cset, metric)) - f.close() - return routes - - -def get_if_index(iff): - return int(struct.unpack("I", get_if(iff, SIOCGIFINDEX)[16:20])[0]) - - -if os.uname()[4] in ['x86_64', 'aarch64']: - def get_last_packet_timestamp(sock): - ts = ioctl(sock, SIOCGSTAMP, "1234567890123456") - s, us = struct.unpack("QQ", ts) - return s + us / 1000000.0 -else: - def get_last_packet_timestamp(sock): - ts = ioctl(sock, SIOCGSTAMP, "12345678") - s, us = struct.unpack("II", ts) - return s + us / 1000000.0 - - -def _flush_fd(fd): - if hasattr(fd, 'fileno'): - fd = fd.fileno() - while True: - r, w, e = select([fd], [], [], 0) - if r: - os.read(fd, MTU) - else: - break - - -def get_iface_mode(iface): - """Return the interface mode. - params: - - iface: the iwconfig interface - """ - p = subprocess.Popen(["iwconfig", iface], stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - output, err = p.communicate() - match = re.search(br"mode:([a-zA-Z]*)", output.lower()) - if match: - return plain_str(match.group(1)) - return "unknown" - - -def set_iface_monitor(iface, monitor): - """Sets the monitor mode (or remove it) from an interface. - params: - - iface: the iwconfig interface - - monitor: True if the interface should be set in monitor mode, - False if it should be in managed mode - """ - mode = get_iface_mode(iface) - if mode == "unknown": - warning("Could not parse iwconfig !") - current_monitor = mode == "monitor" - if monitor == current_monitor: - # Already correct - return True - s_mode = "monitor" if monitor else "managed" - - def _check_call(commands): - p = subprocess.Popen(commands, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE) - stdout, stderr = p.communicate() - if p.returncode != 0: - warning("%s failed !" % " ".join(commands)) - return False - return True - if not _check_call(["ifconfig", iface, "down"]): - return False - if not _check_call(["iwconfig", iface, "mode", s_mode]): - return False - if not _check_call(["ifconfig", iface, "up"]): - return False - return True - - -class L2Socket(SuperSocket): - desc = "read/write packets at layer 2 using Linux PF_PACKET sockets" - - def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, - nofilter=0, monitor=None): - self.iface = conf.iface if iface is None else iface - self.type = type - self.promisc = conf.sniff_promisc if promisc is None else promisc - if monitor is not None: - warning( - "The monitor argument is ineffective on native linux sockets." - " Use set_iface_monitor instead." - ) - self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type)) # noqa: E501 - if not nofilter: - if conf.except_filter: - if filter: - filter = "(%s) and not (%s)" % (filter, conf.except_filter) - else: - filter = "not (%s)" % conf.except_filter - if filter is not None: - try: - attach_filter(self.ins, filter, iface) - except ImportError as ex: - warning("Cannot set filter: %s" % ex) - if self.promisc: - set_promisc(self.ins, self.iface) - self.ins.bind((self.iface, type)) - _flush_fd(self.ins) - self.ins.setsockopt( - socket.SOL_SOCKET, - socket.SO_RCVBUF, - conf.bufsize - ) - if not six.PY2: - # Receive Auxiliary Data (VLAN tags) - try: - self.ins.setsockopt(SOL_PACKET, PACKET_AUXDATA, 1) - self.ins.setsockopt( - socket.SOL_SOCKET, - SO_TIMESTAMPNS, - 1 - ) - self.auxdata_available = True - except OSError: - # Note: Auxiliary Data is only supported since - # Linux 2.6.21 - msg = "Your Linux Kernel does not support Auxiliary Data!" - log_runtime.info(msg) - if isinstance(self, L2ListenSocket): - self.outs = None - else: - self.outs = self.ins - self.outs.setsockopt( - socket.SOL_SOCKET, - socket.SO_SNDBUF, - conf.bufsize - ) - sa_ll = self.ins.getsockname() - if sa_ll[3] in conf.l2types: - self.LL = conf.l2types[sa_ll[3]] - self.lvl = 2 - elif sa_ll[1] in conf.l3types: - self.LL = conf.l3types[sa_ll[1]] - self.lvl = 3 - else: - self.LL = conf.default_l2 - self.lvl = 2 - warning("Unable to guess type (interface=%s protocol=%#x family=%i). Using %s", sa_ll[0], sa_ll[1], sa_ll[3], self.LL.name) # noqa: E501 - - def close(self): - if self.closed: - return - try: - if self.promisc and self.ins: - set_promisc(self.ins, self.iface, 0) - except (AttributeError, OSError): - pass - SuperSocket.close(self) - - def recv_raw(self, x=MTU): - """Receives a packet, then returns a tuple containing (cls, pkt_data, time)""" # noqa: E501 - pkt, sa_ll, ts = self._recv_raw(self.ins, x) - if self.outs and sa_ll[2] == socket.PACKET_OUTGOING: - return None, None, None - if ts is None: - ts = get_last_packet_timestamp(self.ins) - return self.LL, pkt, ts - - def send(self, x): - try: - return SuperSocket.send(self, x) - except socket.error as msg: - if msg.errno == 22 and len(x) < conf.min_pkt_size: - padding = b"\x00" * (conf.min_pkt_size - len(x)) - if isinstance(x, Packet): - return SuperSocket.send(self, x / Padding(load=padding)) - else: - return SuperSocket.send(self, raw(x) + padding) - raise - - -class L2ListenSocket(L2Socket): - desc = "read packets at layer 2 using Linux PF_PACKET sockets. Also receives the packets going OUT" # noqa: E501 - - def send(self, x): - raise Scapy_Exception("Can't send anything with L2ListenSocket") - - -class L3PacketSocket(L2Socket): - desc = "read/write packets at layer 3 using Linux PF_PACKET sockets" - - def recv(self, x=MTU): - pkt = SuperSocket.recv(self, x) - if pkt and self.lvl == 2: - pkt.payload.time = pkt.time - return pkt.payload - return pkt - - def send(self, x): - iff = x.route()[0] - if iff is None: - iff = conf.iface - sdto = (iff, self.type) - self.outs.bind(sdto) - sn = self.outs.getsockname() - ll = lambda x: x - if type(x) in conf.l3types: - sdto = (iff, conf.l3types[type(x)]) - if sn[3] in conf.l2types: - ll = lambda x: conf.l2types[sn[3]]() / x - sx = raw(ll(x)) - x.sent_time = time.time() - try: - self.outs.sendto(sx, sdto) - except socket.error as msg: - if msg.errno == 22 and len(sx) < conf.min_pkt_size: - self.outs.send(sx + b"\x00" * (conf.min_pkt_size - len(sx))) - elif conf.auto_fragment and msg.errno == 90: - for p in x.fragment(): - self.outs.sendto(raw(ll(p)), sdto) - else: - raise - - -class VEthPair(object): - """ - encapsulates a virtual Ethernet interface pair - """ - - def __init__(self, iface_name, peer_name): - - if not LINUX: - # ToDo: do we need a kernel version check here? - raise ScapyInvalidPlatformException( - 'Virtual Ethernet interface pair only available on Linux' - ) - - self.ifaces = [iface_name, peer_name] - - def iface(self): - return self.ifaces[0] - - def peer(self): - return self.ifaces[1] - - def setup(self): - """ - create veth pair links - :raises subprocess.CalledProcessError if operation fails - """ - subprocess.check_call(['ip', 'link', 'add', self.ifaces[0], 'type', 'veth', 'peer', 'name', self.ifaces[1]]) # noqa: E501 - - def destroy(self): - """ - remove veth pair links - :raises subprocess.CalledProcessError if operation fails - """ - subprocess.check_call(['ip', 'link', 'del', self.ifaces[0]]) - - def up(self): - """ - set veth pair links up - :raises subprocess.CalledProcessError if operation fails - """ - for idx in [0, 1]: - subprocess.check_call(["ip", "link", "set", self.ifaces[idx], "up"]) # noqa: E501 - - def down(self): - """ - set veth pair links down - :raises subprocess.CalledProcessError if operation fails - """ - for idx in [0, 1]: - subprocess.check_call(["ip", "link", "set", self.ifaces[idx], "down"]) # noqa: E501 - - def __enter__(self): - self.setup() - self.up() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.destroy() diff --git a/scapy/arch/linux/__init__.py b/scapy/arch/linux/__init__.py new file mode 100644 index 00000000000..29ad12cddc9 --- /dev/null +++ b/scapy/arch/linux/__init__.py @@ -0,0 +1,480 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi + +""" +Linux specific functions. +""" + + +from fcntl import ioctl +from select import select + +import ctypes +import os +import socket +import struct +import subprocess +import sys +import time + +from scapy.compat import raw +from scapy.consts import LINUX +from scapy.arch.common import compile_filter, free_filter +from scapy.config import conf +from scapy.data import MTU, ETH_P_ALL, SOL_PACKET, SO_ATTACH_FILTER, \ + SO_TIMESTAMPNS +from scapy.error import ( + ScapyInvalidPlatformException, + Scapy_Exception, + log_runtime, + warning, +) +from scapy.interfaces import ( + InterfaceProvider, + NetworkInterface, + _GlobInterfaceType, + network_name, + resolve_iface, +) +from scapy.libs.structures import sock_fprog +from scapy.packet import Packet, Padding +from scapy.supersocket import SuperSocket + +# re-export +from scapy.arch.common import get_if_raw_addr, read_nameservers # noqa: F401 +from scapy.arch.linux.rtnetlink import ( # noqa: F401 + read_routes, + read_routes6, + in6_getifaddr, + _get_if_list, +) + +# Typing imports +from typing import ( + Any, + Dict, + List, + NoReturn, + Optional, + Tuple, + Type, + Union, +) + +# From sockios.h +SIOCGIFHWADDR = 0x8927 # Get hardware address +SIOCGIFADDR = 0x8915 # get PA address +SIOCGIFNETMASK = 0x891b # get network PA mask +SIOCGIFNAME = 0x8910 # get iface name +SIOCSIFLINK = 0x8911 # set iface channel +SIOCGIFCONF = 0x8912 # get iface list +SIOCGIFFLAGS = 0x8913 # get flags +SIOCSIFFLAGS = 0x8914 # set flags +SIOCGIFINDEX = 0x8933 # name -> if_index mapping +SIOCGIFCOUNT = 0x8938 # get number of devices +SIOCGSTAMP = 0x8906 # get packet timestamp (as a timeval) + +# From if.h +IFF_UP = 0x1 # Interface is up. +IFF_BROADCAST = 0x2 # Broadcast address valid. +IFF_DEBUG = 0x4 # Turn on debugging. +IFF_LOOPBACK = 0x8 # Is a loopback net. +IFF_POINTOPOINT = 0x10 # Interface is point-to-point link. +IFF_NOTRAILERS = 0x20 # Avoid use of trailers. +IFF_RUNNING = 0x40 # Resources allocated. +IFF_NOARP = 0x80 # No address resolution protocol. +IFF_PROMISC = 0x100 # Receive all packets. + +# From netpacket/packet.h +PACKET_ADD_MEMBERSHIP = 1 +PACKET_DROP_MEMBERSHIP = 2 +PACKET_RECV_OUTPUT = 3 +PACKET_RX_RING = 5 +PACKET_STATISTICS = 6 +PACKET_MR_MULTICAST = 0 +PACKET_MR_PROMISC = 1 +PACKET_MR_ALLMULTI = 2 + +# From net/route.h +RTF_UP = 0x0001 # Route usable +RTF_REJECT = 0x0200 + +# From if_packet.h +PACKET_HOST = 0 # To us +PACKET_BROADCAST = 1 # To all +PACKET_MULTICAST = 2 # To group +PACKET_OTHERHOST = 3 # To someone else +PACKET_OUTGOING = 4 # Outgoing of any type +PACKET_LOOPBACK = 5 # MC/BRD frame looped back +PACKET_USER = 6 # To user space +PACKET_KERNEL = 7 # To kernel space +PACKET_AUXDATA = 8 +PACKET_FASTROUTE = 6 # Fastrouted frame +# Unused, PACKET_FASTROUTE and PACKET_LOOPBACK are invisible to user space + + +# Utils + +def attach_filter(sock, bpf_filter, iface): + # type: (socket.socket, str, _GlobInterfaceType) -> None + """ + Compile bpf filter and attach it to a socket + + :param sock: the python socket + :param bpf_filter: the bpf string filter to compile + :param iface: the interface used to compile + """ + bp = compile_filter(bpf_filter, iface) + if conf.use_pypy and sys.pypy_version_info <= (7, 3, 2): # type: ignore + # PyPy < 7.3.2 has a broken behavior + # https://foss.heptapod.net/pypy/pypy/-/issues/3298 + fp = struct.pack( + 'HL', + bp.bf_len, ctypes.addressof(bp.bf_insns.contents) + ) + else: + fp = sock_fprog(bp.bf_len, bp.bf_insns) # type: ignore + sock.setsockopt(socket.SOL_SOCKET, SO_ATTACH_FILTER, fp) + free_filter(bp) + + +def set_promisc(s, iff, val=1): + # type: (socket.socket, _GlobInterfaceType, int) -> None + _iff = resolve_iface(iff) + mreq = struct.pack("IHH8s", _iff.index, PACKET_MR_PROMISC, 0, b"") + if val: + cmd = PACKET_ADD_MEMBERSHIP + else: + cmd = PACKET_DROP_MEMBERSHIP + s.setsockopt(SOL_PACKET, cmd, mreq) + + +# Interface provider + + +class LinuxInterfaceProvider(InterfaceProvider): + name = "sys" + + def _is_valid(self, dev): + # type: (NetworkInterface) -> bool + return bool(dev.flags & IFF_UP) + + def load(self): + # type: () -> Dict[str, NetworkInterface] + data = {} + for iface in _get_if_list().values(): + if_data = iface.copy() + if_data.update({ + "network_name": iface["name"], + "description": iface["name"], + "ips": [x["address"] for x in iface["ips"]] + }) + data[iface["name"]] = NetworkInterface(self, if_data) + return data + + +conf.ifaces.register_provider(LinuxInterfaceProvider) + +if os.uname()[4] in ['x86_64', 'aarch64']: + def get_last_packet_timestamp(sock): + # type: (socket.socket) -> float + ts = ioctl(sock, SIOCGSTAMP, "1234567890123456") # type: ignore + s, us = struct.unpack("QQ", ts) # type: Tuple[int, int] + return s + us / 1000000.0 +else: + def get_last_packet_timestamp(sock): + # type: (socket.socket) -> float + ts = ioctl(sock, SIOCGSTAMP, "12345678") # type: ignore + s, us = struct.unpack("II", ts) # type: Tuple[int, int] + return s + us / 1000000.0 + + +def _flush_fd(fd): + # type: (int) -> None + while True: + r, w, e = select([fd], [], [], 0) + if r: + os.read(fd, MTU) + else: + break + + +class L2Socket(SuperSocket): + desc = "read/write packets at layer 2 using Linux PF_PACKET sockets" + + def __init__(self, + iface=None, # type: Optional[Union[str, NetworkInterface]] + type=ETH_P_ALL, # type: int + promisc=None, # type: Optional[Any] + filter=None, # type: Optional[Any] + nofilter=0, # type: int + monitor=None, # type: Optional[Any] + ): + # type: (...) -> None + self.iface = network_name(iface or conf.iface) + self.type = type + self.promisc = conf.sniff_promisc if promisc is None else promisc + self.ins = socket.socket( + socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type)) + self.ins.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0) + if not nofilter: + if conf.except_filter: + if filter: + filter = "(%s) and not (%s)" % (filter, conf.except_filter) + else: + filter = "not (%s)" % conf.except_filter + if filter is not None: + try: + attach_filter(self.ins, filter, self.iface) + except (ImportError, Scapy_Exception) as ex: + raise Scapy_Exception("Cannot set filter: %s" % ex) + if self.promisc: + set_promisc(self.ins, self.iface) + self.ins.bind((self.iface, type)) + _flush_fd(self.ins.fileno()) + self.ins.setsockopt( + socket.SOL_SOCKET, + socket.SO_RCVBUF, + conf.bufsize + ) + # Receive Auxiliary Data (VLAN tags) + try: + self.ins.setsockopt(SOL_PACKET, PACKET_AUXDATA, 1) + self.ins.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1) + self.auxdata_available = True + except OSError: + # Note: Auxiliary Data is only supported since + # Linux 2.6.21 + msg = "Your Linux Kernel does not support Auxiliary Data!" + log_runtime.info(msg) + if not isinstance(self, L2ListenSocket): + self.outs = self.ins # type: socket.socket + self.outs.setsockopt( + socket.SOL_SOCKET, + socket.SO_SNDBUF, + conf.bufsize + ) + else: + self.outs = None # type: ignore + sa_ll = self.ins.getsockname() + if sa_ll[3] in conf.l2types: + self.LL = conf.l2types.num2layer[sa_ll[3]] + self.lvl = 2 + elif sa_ll[1] in conf.l3types: + self.LL = conf.l3types.num2layer[sa_ll[1]] + self.lvl = 3 + else: + self.LL = conf.default_l2 + self.lvl = 2 + warning("Unable to guess type (interface=%s protocol=%#x family=%i). Using %s", sa_ll[0], sa_ll[1], sa_ll[3], self.LL.name) # noqa: E501 + + def close(self): + # type: () -> None + if self.closed: + return + try: + if self.promisc and getattr(self, "ins", None): + set_promisc(self.ins, self.iface, 0) + except (AttributeError, OSError, ValueError): + pass + SuperSocket.close(self) + + def recv_raw(self, x=MTU): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + """Receives a packet, then returns a tuple containing (cls, pkt_data, time)""" # noqa: E501 + pkt, sa_ll, ts = self._recv_raw(self.ins, x) + if self.outs and sa_ll[2] == socket.PACKET_OUTGOING: + return None, None, None + if ts is None: + ts = get_last_packet_timestamp(self.ins) + return self.LL, pkt, ts + + def send(self, x): + # type: (Packet) -> int + try: + return SuperSocket.send(self, x) + except socket.error as msg: + if msg.errno == 22 and len(x) < conf.min_pkt_size: + padding = b"\x00" * (conf.min_pkt_size - len(x)) + if isinstance(x, Packet): + return SuperSocket.send(self, x / Padding(load=padding)) + else: + return SuperSocket.send(self, raw(x) + padding) + raise + + +class L2ListenSocket(L2Socket): + desc = "read packets at layer 2 using Linux PF_PACKET sockets. Also receives the packets going OUT" # noqa: E501 + + def send(self, x): + # type: (Packet) -> NoReturn + raise Scapy_Exception("Can't send anything with L2ListenSocket") + + +class L3PacketSocket(L2Socket): + desc = "read/write packets at layer 3 using Linux PF_PACKET sockets" + + def __init__(self, + iface=None, # type: Optional[Union[str, NetworkInterface]] + type=ETH_P_ALL, # type: int + promisc=None, # type: Optional[Any] + filter=None, # type: Optional[Any] + nofilter=0, # type: int + monitor=None, # type: Optional[Any] + ): + self.send_socks = {} + super(L3PacketSocket, self).__init__( + iface=iface, + type=type, + promisc=promisc, + filter=filter, + nofilter=nofilter, + monitor=monitor, + ) + self.filter = filter + self.send_socks = {network_name(self.iface): self} + + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + pkt = SuperSocket.recv(self, x, **kwargs) + if pkt and self.lvl == 2: + pkt.payload.time = pkt.time + return pkt.payload + return pkt + + def send(self, x): + # type: (Packet) -> int + # Select the file descriptor to send the packet on. + iff = x.route()[0] + if iff is None: + iff = network_name(conf.iface) + type_x = type(x) + if iff not in self.send_socks: + self.send_socks[iff] = L3PacketSocket( + iface=iff, + type=conf.l3types.layer2num.get(type_x, self.type), + filter=self.filter, + promisc=self.promisc, + ) + sock = self.send_socks[iff] + fd = sock.outs + if sock.lvl == 3: + if not issubclass(sock.LL, type_x): + warning("Incompatible L3 types detected using %s instead of %s !", + type_x, sock.LL) + sock.LL = type_x + if sock.lvl == 2: + sx = bytes(sock.LL() / x) + else: + sx = bytes(x) + # Now send. + try: + x.sent_time = time.time() + except AttributeError: + pass + try: + return fd.send(sx) + except socket.error as msg: + if msg.errno == 22 and len(sx) < conf.min_pkt_size: + return fd.send( + sx + b"\x00" * (conf.min_pkt_size - len(sx)) + ) + elif conf.auto_fragment and msg.errno == 90: + i = 0 + for p in x.fragment(): + i += fd.send(bytes(self.LL() / p)) + return i + else: + raise + + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + socks = [] # type: List[SuperSocket] + for sock in sockets: + if isinstance(sock, L3PacketSocket): + socks += sock.send_socks.values() + else: + socks.append(sock) + return L2Socket.select(socks, remain=remain) + + def close(self): + # type: () -> None + if self.closed: + return + super(L3PacketSocket, self).close() + for fd in self.send_socks.values(): + if fd is not self: + fd.close() + + +class VEthPair(object): + """ + encapsulates a virtual Ethernet interface pair + """ + + def __init__(self, iface_name, peer_name): + # type: (str, str) -> None + if not LINUX: + # ToDo: do we need a kernel version check here? + raise ScapyInvalidPlatformException( + 'Virtual Ethernet interface pair only available on Linux' + ) + + self.ifaces = [iface_name, peer_name] + + def iface(self): + # type: () -> str + return self.ifaces[0] + + def peer(self): + # type: () -> str + return self.ifaces[1] + + def setup(self): + # type: () -> None + """ + create veth pair links + :raises subprocess.CalledProcessError if operation fails + """ + subprocess.check_call(['ip', 'link', 'add', self.ifaces[0], 'type', 'veth', 'peer', 'name', self.ifaces[1]]) # noqa: E501 + + def destroy(self): + # type: () -> None + """ + remove veth pair links + :raises subprocess.CalledProcessError if operation fails + """ + subprocess.check_call(['ip', 'link', 'del', self.ifaces[0]]) + + def up(self): + # type: () -> None + """ + set veth pair links up + :raises subprocess.CalledProcessError if operation fails + """ + for idx in [0, 1]: + subprocess.check_call(["ip", "link", "set", self.ifaces[idx], "up"]) # noqa: E501 + + def down(self): + # type: () -> None + """ + set veth pair links down + :raises subprocess.CalledProcessError if operation fails + """ + for idx in [0, 1]: + subprocess.check_call(["ip", "link", "set", self.ifaces[idx], "down"]) # noqa: E501 + + def __enter__(self): + # type: () -> VEthPair + self.setup() + self.up() + conf.ifaces.reload() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # type: (Any, Any, Any) -> None + self.destroy() + conf.ifaces.reload() diff --git a/scapy/arch/linux/rtnetlink.py b/scapy/arch/linux/rtnetlink.py new file mode 100644 index 00000000000..5e0e72d404b --- /dev/null +++ b/scapy/arch/linux/rtnetlink.py @@ -0,0 +1,988 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +This file implements the rtnetlink API that is used to read the network +configuration of the machine. +""" + +import socket +import struct +import time + +import scapy.utils6 + +from scapy.consts import BIG_ENDIAN +from scapy.config import conf +from scapy.error import log_loading +from scapy.packet import ( + Packet, + bind_layers, +) +from scapy.utils import atol, itom + +from scapy.fields import ( + ByteEnumField, + ByteField, + EnumField, + Field, + FieldLenField, + FlagsField, + IP6Field, + IPField, + LenField, + MACField, + MayEnd, + MultipleTypeField, + PacketListField, + PadField, + StrLenField, + XStrLenField, +) + +from scapy.arch.common import _iff_flags + +# Typing imports +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + Type, +) + +# from and + + +# Common header + + +class rtmsghdr(Packet): + fields_desc = [ + LenField("nlmsg_len", None, fmt="=L"), + EnumField( + "nlmsg_type", + 0, + { + # netlink.h + 3: "NLMSG_DONE", + # rtnetlink.h + 16: "RTM_NEWLINK", + 17: "RTM_DELLINK", + 18: "RTM_GETLINK", + 19: "RTM_SETLINK", + 20: "RTM_NEWADDR", + 21: "RTM_DELADDR", + 22: "RTM_GETADDR", + # 23: unused + 24: "RTM_NEWROUTE", + 25: "RTM_DELROUTE", + 26: "RTM_GETROUTE", + # 27: unused + }, + fmt="=H", + ), + FlagsField( + "nlmsg_flags", + 0, + 16 if BIG_ENDIAN else -16, + { + 0x01: "NLM_F_REQUEST", + 0x02: "NLM_F_MULTI", + 0x04: "NLM_F_ACK", + 0x08: "NLM_F_ECHO", + 0x10: "NLM_F_DUMP_INTR", + 0x20: "NLM_F_DUMP_FILTERED", + # GET modifiers + 0x100: "NLM_F_ROOT", + 0x200: "NLM_F_MATCH", + 0x400: "NLM_F_ATOMIC", + }, + ), + Field("nlmsg_seq", 0, fmt="=L"), + Field("nlmsg_pid", 0, fmt="=L"), + ] + + def post_build(self, pkt: bytes, pay: bytes) -> bytes: + pkt += pay + if self.nlmsg_len is None: + pkt = struct.pack("=L", len(pkt)) + pkt[4:] + return pkt + + def extract_padding(self, s: bytes) -> Tuple[bytes, Optional[bytes]]: + return s[: self.nlmsg_len - 16], s[self.nlmsg_len - 16 :] + + def answers(self, other: Packet) -> bool: + return bool(other.nlmsg_seq == self.nlmsg_seq) + + +# DONE + + +class nlmsgerr_rtattr(Packet): + fields_desc = [ + FieldLenField( + "rta_len", None, length_of="rta_data", fmt="=H", adjust=lambda _, x: x + 4 + ), + EnumField( + "rta_type", + 0, + {}, + fmt="=H", + ), + PadField( + MultipleTypeField( + [], + StrLenField( + "rta_data", + b"", + length_from=lambda pkt: pkt.rta_len - 4, + ), + ), + align=4, + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class nlmsgerr(Packet): + fields_desc = [ + MayEnd(Field("status", 0, fmt="=L")), + # Pay + PacketListField("data", [], nlmsgerr_rtattr), + ] + + +bind_layers(rtmsghdr, nlmsgerr, nlmsg_type=3) + + +# LINK messages + + +class ifla_af_spec_inet_rtattr(Packet): + fields_desc = [ + FieldLenField( + "rta_len", None, length_of="rta_data", fmt="=H", adjust=lambda _, x: x + 4 + ), + EnumField( + "rta_type", + 0, + { + 0x00: "IFLA_INET_UNSPEC", + 0x01: "IFLA_INET_CONF", + }, + fmt="=H", + ), + PadField( + MultipleTypeField( + [], + XStrLenField( + "rta_data", + b"", + length_from=lambda pkt: pkt.rta_len - 4, + ), + ), + align=4, + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class ifla_af_spec_inet6_rtattr(Packet): + fields_desc = [ + FieldLenField( + "rta_len", None, length_of="rta_data", fmt="=H", adjust=lambda _, x: x + 4 + ), + EnumField( + "rta_type", + 0, + { + 0x00: "IFLA_INET6_UNSPEC", + 0x01: "IFLA_INET6_FLAGS", + 0x02: "IFLA_INET6_CONF", + 0x03: "IFLA_INET6_STATS", + 0x04: "IFLA_INET6_MCAST", + 0x05: "IFLA_INET6_CACHEINFO", + 0x06: "IFLA_INET6_ICMP6STATS", + 0x07: "IFLA_INET6_TOKEN", + 0x08: "IFLA_INET6_ADDR_GEN_MODE", + 0x09: "IFLA_INET6_RA_MTU", + }, + fmt="=H", + ), + PadField( + MultipleTypeField( + [], + XStrLenField( + "rta_data", + b"", + length_from=lambda pkt: pkt.rta_len - 4, + ), + ), + align=4, + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class ifla_af_spec_rtattr(Packet): + fields_desc = [ + FieldLenField( + "rta_len", None, length_of="rta_data", fmt="=H", adjust=lambda _, x: x + 4 + ), + EnumField("rta_type", 0, socket.AddressFamily, fmt="=H"), + PadField( + MultipleTypeField( + [ + ( + # AF_INET + PacketListField( + "rta_data", + [], + ifla_af_spec_inet_rtattr, + length_from=lambda pkt: pkt.rta_len - 4, + ), + lambda pkt: pkt.rta_type == 2, + ), + ( + # AF_INET6 + PacketListField( + "rta_data", + [], + ifla_af_spec_inet6_rtattr, + length_from=lambda pkt: pkt.rta_len - 4, + ), + lambda pkt: pkt.rta_type == 10, + ), + ], + XStrLenField( + "rta_data", + b"", + length_from=lambda pkt: pkt.rta_len - 4, + ), + ), + align=4, + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class ifinfomsg_rtattr(Packet): + fields_desc = [ + FieldLenField( + "rta_len", None, length_of="rta_data", fmt="=H", adjust=lambda _, x: x + 4 + ), + EnumField( + "rta_type", + 0, + { + 0x00: "IFLA_UNSPEC", + 0x01: "IFLA_ADDRESS", + 0x02: "IFLA_BROADCAST", + 0x03: "IFLA_IFNAME", + 0x04: "IFLA_MTU", + 0x05: "IFLA_LINK", + 0x06: "IFLA_QDISC", + 0x07: "IFLA_STATS", + 0x08: "IFLA_COST", + 0x09: "IFLA_PRIORITY", + 0x0A: "IFLA_MASTER", + 0x0B: "IFLA_WIRELESS", + 0x0C: "IFLA_PROTINFO", + 0x0D: "IFLA_TXQLEN", + 0x0E: "IFLA_MAP", + 0x0F: "IFLA_WEIGHT", + 0x10: "IFLA_OPERSTATE", + 0x11: "IFLA_LINKMODE", + 0x12: "IFLA_LINKINFO", + 0x13: "IFLA_NET_NS_PID", + 0x14: "IFLA_IFALIAS", + 0x15: "IFLA_NUM_VS", + 0x16: "IFLA_VFINFO_LIST", + 0x17: "IFLA_STATS64", + 0x18: "IFLA_VF_PORTS", + 0x19: "IFLA_PORT_SELF", + 0x1A: "IFLA_AF_SPEC", + 0x1B: "IFLA_GROUP", + 0x1C: "IFLA_NET_NS_FD", + 0x1D: "IFLA_EXT_MASK", + 0x1E: "IFLA_PROMISCUITY", + 0x1F: "IFLA_NUM_TX_QUEUES", + 0x20: "IFLA_NUM_RX_QUEUES", + 0x21: "IFLA_CARRIER", + 0x22: "IFLA_PHYS_PORT_ID", + 0x23: "IFLA_CARRIER_CHANGES", + 0x24: "IFLA_PHYS_SWITCH_ID", + 0x25: "IFLA_LINK_NETNSID", + 0x26: "IFLA_PHYS_PORT_NAME", + 0x27: "IFLA_PROTO_DOWN", + 0x28: "IFLA_GSO_MAX_SEGS", + 0x29: "IFLA_GSO_MAX_SIZE", + 0x2A: "IFLA_PAD", + 0x2B: "IFLA_XDP", + 0x2C: "IFLA_EVENT", + 0x2D: "IFLA_NEW_NETNSID", + 0x2E: "IFLA_IF_NETNSID", + 0x2F: "IFLA_CARRIER_UP_COUNT", + 0x30: "IFLA_CARRIER_DOWN_COUNT", + 0x31: "IFLA_NEW_IFINDEX", + 0x32: "IFLA_MIN_MTU", + 0x33: "IFLA_MAX_MTU", + 0x34: "IFLA_PROP_LIST", + 0x35: "IFLA_ALT_IFNAME", + 0x36: "IFLA_PERM_ADDRESS", + 0x37: "IFLA_PROTO_DOWN_REASON", + 0x38: "IFLA_PARENT_DEV_NAME", + 0x39: "IFLA_PARENT_DEV_BUS_NAME", + 0x3A: "IFLA_GRO_MAX_SIZE", + 0x3B: "IFLA_TSO_MAX_SIZE", + 0x3C: "IFLA_TSO_MAX_SEGS", + 0x3D: "IFLA_ALLMULTI", + }, + fmt="=H", + ), + PadField( + MultipleTypeField( + [ + ( + # IFLA_ADDRESS + MACField("rta_data", "00:00:00:00:00:00"), + lambda pkt: pkt.rta_type in [0x01, 0x36], + ), + ( + # IFLA_IFNAME + StrLenField( + "rta_data", b"", length_from=lambda pkt: pkt.rta_len - 4 + ), + lambda pkt: pkt.rta_type in [0x03], + ), + ( + # IFLA_AF_SPEC + PacketListField( + "rta_data", + [], + ifla_af_spec_rtattr, + length_from=lambda pkt: pkt.rta_len - 4, + ), + lambda pkt: pkt.rta_type == 0x1A, + ), + ], + XStrLenField( + "rta_data", + b"", + length_from=lambda pkt: pkt.rta_len - 4, + ), + ), + align=4, + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class ifinfomsg(Packet): + fields_desc = [ + ByteEnumField("ifi_family", 0, socket.AddressFamily), + ByteField("res", 0), + Field("ifi_type", 0, fmt="=H"), + Field("ifi_index", 0, fmt="=i"), + FlagsField( + "ifi_flags", + 0, + 32 if BIG_ENDIAN else -32, + _iff_flags, + ), + Field("ifi_change", 0, fmt="=I"), + # Pay + PacketListField("data", [], ifinfomsg_rtattr), + ] + + +bind_layers(rtmsghdr, ifinfomsg, nlmsg_type=16) +bind_layers(rtmsghdr, ifinfomsg, nlmsg_type=17) +bind_layers(rtmsghdr, ifinfomsg, nlmsg_type=18) +bind_layers(rtmsghdr, ifinfomsg, nlmsg_type=19) + + +# ADDR messages + + +class ifaddrmsg_rtattr(Packet): + fields_desc = [ + FieldLenField( + "rta_len", None, length_of="rta_data", fmt="=H", adjust=lambda _, x: x + 4 + ), + EnumField( + "rta_type", + 0, + { + 0x00: "IFA_UNSPEC", + 0x01: "IFA_ADDRESS", + 0x02: "IFA_LOCAL", + 0x03: "IFA_LABEL", + 0x04: "IFA_BROADCAST", + 0x05: "IFA_ANYCAST", + 0x06: "IFA_CACHEINFO", + 0x07: "IFA_MULTICAST", + 0x08: "IFA_FLAGS", + 0x09: "IFA_RT_PRIORITY", + 0x0A: "IFA_TARGET_NETNSID", + 0x0B: "IFA_PROTO", + }, + fmt="=H", + ), + PadField( + MultipleTypeField( + [ + # IFA_ADDRESS, IFA_LOCAL, IFA_BROADCAST + ( + IPField("rta_data", "0.0.0.0"), + lambda pkt: pkt.parent + and pkt.parent.ifa_family == 2 + and pkt.rta_type in [0x01, 0x02, 0x04], + ), + ( + IP6Field("rta_data", "::"), + lambda pkt: pkt.parent + and pkt.parent.ifa_family == 10 + and pkt.rta_type in [0x01, 0x02, 0x04], + ), + ( + # IFA_LABEL + StrLenField( + "rta_data", b"", length_from=lambda pkt: pkt.rta_len - 4 + ), + lambda pkt: pkt.rta_type in [0x03], + ), + ], + XStrLenField( + "rta_data", + b"", + length_from=lambda pkt: pkt.rta_len - 4, + ), + ), + align=4, + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class ifaddrmsg(Packet): + fields_desc = [ + ByteEnumField("ifa_family", 0, socket.AddressFamily), + ByteField("ifa_prefixlen", 0), + FlagsField( + "ifa_flags", + 0, + -8, + { + 0x01: "IFA_F_SECONDARY", + 0x02: "IFA_F_NODAD", + 0x04: "IFA_F_OPTIMISTIC", + 0x08: "IFA_F_DADFAILED", + 0x10: "IFA_F_HOMEADDRESS", + 0x20: "IFA_F_DEPRECATED", + 0x40: "IFA_F_TENTATIVE", + 0x80: "IFA_F_PERMANENT", + }, + ), + ByteField("ifa_scope", 0), + Field("ifa_index", 0, fmt="=L"), + # Pay + PacketListField("data", [], ifaddrmsg_rtattr), + ] + + +bind_layers(rtmsghdr, ifaddrmsg, nlmsg_type=20) +bind_layers(rtmsghdr, ifaddrmsg, nlmsg_type=21) +bind_layers(rtmsghdr, ifaddrmsg, nlmsg_type=22) + + +# ROUTE messages + + +RT_CLASS = { + 0: "RT_TABLE_UNSPEC", + 252: "RT_TABLE_COMPAT", + 253: "RT_TABLE_DEFAULT", + 254: "RT_TABLE_MAIN", + 255: "RT_TABLE_LOCAL", +} + + +class rtmsg_rtattr(Packet): + fields_desc = [ + FieldLenField( + "rta_len", None, length_of="rta_data", fmt="=H", adjust=lambda _, x: x + 4 + ), + EnumField( + "rta_type", + 0, + { + 0x00: "RTA_UNSPEC", + 0x01: "RTA_DST", + 0x02: "RTS_SRC", + 0x03: "RTS_IIF", + 0x04: "RTS_OIF", + 0x05: "RTA_GATEWAY", + 0x06: "RTA_PRIORITY", + 0x07: "RTA_PREFSRC", + 0x08: "RTA_METRICS", + 0x09: "RTA_MULTIPATH", + 0x0B: "RTA_FLOW", + 0x0C: "RTA_CACHEINFO", + 0x0F: "RTA_TABLE", + 0x10: "RTA_MARK", + 0x11: "RTA_MFC_STATS", + 0x12: "RTA_VIA", + 0x13: "RTA_NEWDST", + 0x14: "RTA_PREF", + 0x15: "RTA_ENCAP_TYPE", + 0x16: "RTA_ENCAP", + 0x17: "RTA_EXPIRES", + 0x18: "RTA_PAD", + 0x19: "RTA_UID", + 0x1A: "RTA_TTL_PROPAGATE", + 0x1B: "RTA_IP_PROTO", + 0x1C: "RTA_SPORT", + 0x1D: "RTA_DPORT", + 0x1E: "RTA_NH_ID", + }, + fmt="=H", + ), + PadField( + MultipleTypeField( + [ + # RTA_DST, RTA_SRC, RTA_PREFSRC, RTA_GATEWAY + ( + IPField("rta_data", "0.0.0.0"), + lambda pkt: pkt.parent + and pkt.parent.rtm_family == 2 + and pkt.rta_type in [0x01, 0x02, 0x05, 0x07], + ), + ( + IP6Field("rta_data", "::"), + lambda pkt: pkt.parent + and pkt.parent.rtm_family == 10 + and pkt.rta_type in [0x01, 0x02, 0x05, 0x07], + ), + # RTS_OIF, RTA_PRIORITY + ( + Field("rta_data", 0, fmt="=I"), + lambda pkt: pkt.rta_type in [0x04, 0x06, 0x10], + ), + # RTA_TABLE + ( + EnumField("rta_data", 0, RT_CLASS, fmt="=I"), + lambda pkt: pkt.rta_type in [0x0F], + ), + ], + XStrLenField( + "rta_data", + b"", + length_from=lambda pkt: pkt.rta_len - 4, + ), + ), + align=4, + ), + ] + + def default_payload_class(self, payload: bytes) -> Type[Packet]: + return conf.padding_layer + + +class rtmsg(Packet): + fields_desc = [ + ByteEnumField("rtm_family", 0, socket.AddressFamily), + ByteField("rtm_dst_len", 0), + ByteField("rtm_src_len", 0), + ByteField("rtm_tos", 0), + ByteEnumField( + "rtm_table", + 0, + RT_CLASS, + ), + ByteEnumField( + "rtm_protocol", + 0, + { + 0x00: "RTPROT_UNSPEC", + 0x01: "RTPROT_REDIRECT", + 0x02: "RTPROT_KERNEL", + 0x03: "RTPROT_BOOT", + 0x04: "RTPROT_STATIC", + }, + ), + ByteEnumField( + "rtm_scope", + 0, + { + 0: "RT_SCOPE_UNIVERSE", + 200: "RT_SCOPE_SITE", + 253: "RT_SCOPE_LINK", + 254: "RT_SCOPE_HOST", + 255: "RT_SCOPE_NOWHERE", + }, + ), + ByteEnumField( + "rtm_type", + 0, + { + 0x00: "RTN_UNSPEC", + 0x01: "RTN_UNICAST", + 0x02: "RTN_LOCAL", + 0x03: "RTN_BROADCAST", + 0x04: "RTN_ANYCAST", + 0x05: "RTN_MULTICAST", + 0x06: "RTN_BLACKHOLE", + 0x07: "RTN_UNREACHABLE", + 0x08: "RTN_PROHIBIT", + 0x09: "RTN_THROW", + 0x0A: "RTN_NAT", + 0x0B: "RTN_XRESOLVE", + }, + ), + FlagsField( + "rtm_flags", + 0, + 32 if BIG_ENDIAN else -32, + { + 0x100: "RTM_F_NOTIFY", + 0x200: "RTM_F_CLONED", + 0x400: "RTM_F_EQUALIZE", + 0x800: "RTM_F_PREFIX", + 0x1000: "RTM_F_LOOKUP_TABLE", + 0x2000: "RTM_F_FIB_MATCH", + 0x4000: "RTM_F_OFFLOAD", + 0x8000: "RTM_F_TRAP", + 0x20000000: "RTM_F_OFFLOAD_FAILED", + }, + ), + # Pay + PacketListField("data", [], rtmsg_rtattr), + ] + + +bind_layers(rtmsghdr, rtmsg, nlmsg_type=24) +bind_layers(rtmsghdr, rtmsg, nlmsg_type=25) +bind_layers(rtmsghdr, rtmsg, nlmsg_type=26) + + +class rtmsghdrs(Packet): + fields_desc = [ + PacketListField( + "msgs", + [], + rtmsghdr, + # 65535 / len(rtmsghdr) + max_count=4096, + ), + ] + + +# Utils + + +SOL_NETLINK = 270 +NETLINK_EXT_ACK = 11 +NETLINK_GET_STRICT_CHK = 12 + + +def _sr1_rtrequest(pkt: Packet) -> List[Packet]: + """ + Send / Receive a rtnetlink request + """ + # Create socket + sock = socket.socket( + socket.AF_NETLINK, + socket.SOCK_RAW | socket.SOCK_CLOEXEC, + socket.NETLINK_ROUTE, + ) + # Configure socket + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 32768) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576) + try: + sock.setsockopt(SOL_NETLINK, NETLINK_EXT_ACK, 1) + except OSError: + # Linux 4.12+ only + pass + sock.bind((0, 0)) # bind to kernel + try: + sock.setsockopt(SOL_NETLINK, NETLINK_GET_STRICT_CHK, 1) + except OSError: + # Linux 4.20+ only + pass + # Request routes + sock.send(bytes(rtmsghdrs(msgs=[pkt]))) + results: List[Packet] = [] + try: + while True: + msgs = rtmsghdrs(sock.recv(65535)) + if not msgs: + log_loading.warning("Failed to read the routes using RTNETLINK !") + return [] + for msg in msgs.msgs: + # Keep going until we find the end of the MULTI format + if not msg.nlmsg_flags.NLM_F_MULTI or msg.nlmsg_type == 3: + if msg.nlmsg_type == 3 and nlmsgerr in msg and msg.status != 0: + # NLMSG_DONE with errors + if msg.data and msg.data[0].rta_type == 1: + log_loading.debug( + "Scapy RTNETLINK error on %s: '%s'. Please report !", + pkt.sprintf("%nlmsg_type%"), + msg.data[0].rta_data.decode(), + ) + return [] + return results + results.append(msg) + finally: + sock.close() + + +def _get_ips(af_family=socket.AF_UNSPEC): + # type: (socket.AddressFamily) -> Dict[int, List[Dict[str, Any]]] + """ + Return a mapping of all interfaces IP using a NETLINK socket. + """ + results = _sr1_rtrequest( + rtmsghdr( + nlmsg_type="RTM_GETADDR", + nlmsg_flags="NLM_F_REQUEST+NLM_F_ROOT+NLM_F_MATCH", + nlmsg_seq=int(time.time()), + ) + / ifaddrmsg( + ifa_family=af_family, + data=[], + ) + ) + ips: Dict[int, List[Dict[str, Any]]] = {} + for msg in results: + ifindex = msg.ifa_index + address = None + family = msg.ifa_family + local = None + for attr in msg.data: + if attr.rta_type == 0x01: # IFA_ADDRESS + address = attr.rta_data + elif attr.rta_type == 0x02: # IFA_LOCAL + local = attr.rta_data + # include/uapi/linux/if_addr.h: for point-to-point links, IFA_LOCAL is the local + # interface address and IFA_ADDRESS is the peer address + local_address = local if local is not None else address + if local_address is not None: + data = { + "af_family": family, + "index": ifindex, + "address": local_address, + } + if family == 10: # ipv6 + data["scope"] = scapy.utils6.in6_getscope(local_address) + ips.setdefault(ifindex, list()).append(data) + return ips + + +def _get_if_list(): + # type: () -> Dict[int, Dict[str, Any]] + """ + Read the interfaces list using a NETLINK socket. + """ + results = _sr1_rtrequest( + rtmsghdr( + nlmsg_type="RTM_GETLINK", + nlmsg_flags="NLM_F_REQUEST+NLM_F_ROOT+NLM_F_MATCH", + nlmsg_seq=int(time.time()), + ) + / ifinfomsg( + data=[], + ) + ) + lifips = _get_ips() + interfaces = {} + for msg in results: + ifindex = msg.ifi_index + ifname = None + mac = "00:00:00:00:00:00" + itype = msg.ifi_type + ifflags = msg.ifi_flags + ips = [] + for attr in msg.data: + if attr.rta_type == 0x01: # IFLA_ADDRESS + mac = attr.rta_data + elif attr.rta_type == 0x03: # IFLA_NAME + ifname = attr.rta_data[:-1].decode() + if ifname is not None: + if ifindex in lifips: + ips = lifips[ifindex] + interfaces[ifindex] = { + "name": ifname, + "index": ifindex, + "flags": ifflags, + "mac": mac, + "type": itype, + "ips": ips, + } + return interfaces + + +def in6_getifaddr(): + # type: () -> List[Tuple[str, int, str]] + """ + Returns a list of 3-tuples of the form (addr, scope, iface) where + 'addr' is the address of scope 'scope' associated to the interface + 'iface'. + + This is the list of all addresses of all interfaces available on + the system. + """ + ips = _get_ips(af_family=socket.AF_INET6) + ifaces = _get_if_list() + result = [] + for intip in ips.values(): + for ip in intip: + if ip["index"] in ifaces: + result.append((ip["address"], ip["scope"], ifaces[ip["index"]]["name"])) + return result + + +def _read_routes(af_family): + # type: (socket.AddressFamily) -> List[Packet] + """ + Read routes using a NETLINK socket. + """ + results = [] + for rttable in ["RT_TABLE_LOCAL", "RT_TABLE_MAIN"]: + results.extend( + _sr1_rtrequest( + rtmsghdr( + nlmsg_type="RTM_GETROUTE", + nlmsg_flags="NLM_F_REQUEST+NLM_F_ROOT+NLM_F_MATCH", + nlmsg_seq=int(time.time()), + ) + / rtmsg( + rtm_family=af_family, + data=[ + rtmsg_rtattr(rta_type="RTA_TABLE", rta_data=rttable), + ], + ) + ) + ) + return [msg for msg in results if msg.nlmsg_type == 24] # RTM_NEWROUTE + + +def read_routes(): + # type: () -> List[Tuple[int, int, str, str, str, int]] + """ + Read IPv4 routes for current process + """ + routes = [] + ifaces = _get_if_list() + results = _read_routes(socket.AF_INET) + for msg in results: + # Omit stupid answers (some OS conf appears to lead to this) + if msg.rtm_family != socket.AF_INET: + continue + # Process the RTM_NEWROUTE + net = 0 + mask = itom(msg.rtm_dst_len) + gw = "0.0.0.0" + iface = "" + addr = "0.0.0.0" + metric = 0 + for attr in msg.data: + if attr.rta_type == 0x01: # RTA_DST + net = atol(attr.rta_data) + elif attr.rta_type == 0x04: # RTS_OIF + index = attr.rta_data + if index in ifaces: + iface = ifaces[index]["name"] + else: + iface = str(index) + elif attr.rta_type == 0x05: # RTA_GATEWAY + gw = attr.rta_data + elif attr.rta_type == 0x06: # RTA_PRIORITY + metric = attr.rta_data + elif attr.rta_type == 0x07: # RTA_PREFSRC + addr = attr.rta_data + routes.append((net, mask, gw, iface, addr, metric)) + # Add multicast routes, as those are missing by default + for _iface in ifaces.values(): + if _iface['flags'].MULTICAST: + try: + addr = next( + x["address"] + for x in _iface["ips"] + if x["af_family"] == socket.AF_INET + ) + except StopIteration: + continue + routes.append(( + 0xe0000000, 0xf0000000, "0.0.0.0", _iface["name"], addr, 250 + )) + return routes + + +def read_routes6(): + # type: () -> List[Tuple[str, int, str, str, List[str], int]] + """ + Read IPv6 routes for current process + """ + routes = [] + ifaces = _get_if_list() + results = _read_routes(socket.AF_INET6) + lifaddr = _get_ips(af_family=socket.AF_INET6) + for msg in results: + # Omit stupid answers (some OS conf appears to lead to this) + if msg.rtm_family != socket.AF_INET6: + continue + # Process the RTM_NEWROUTE + prefix = "::" + plen = msg.rtm_dst_len + nh = "::" + index = 0 + iface = "" + metric = 0 + for attr in msg.data: + if attr.rta_type == 0x01: # RTA_DST + prefix = attr.rta_data + elif attr.rta_type == 0x04: # RTS_OIF + index = attr.rta_data + if index in ifaces: + iface = ifaces[index]["name"] + else: + iface = str(index) + elif attr.rta_type == 0x05: # RTA_GATEWAY + nh = attr.rta_data + elif attr.rta_type == 0x06: # RTA_PRIORITY + metric = attr.rta_data + devaddrs = ((x["address"], x["scope"], iface) for x in lifaddr.get(index, [])) + cset = scapy.utils6.construct_source_candidate_set(prefix, plen, devaddrs) + if cset: + routes.append((prefix, plen, nh, iface, cset, metric)) + # Add multicast routes, as those are missing by default + for _iface in ifaces.values(): + if _iface['flags'].MULTICAST: + addrs = [ + x["address"] + for x in _iface["ips"] + if x["af_family"] == socket.AF_INET6 + ] + if not addrs: + continue + routes.append(( + "ff00::", 8, "::", _iface["name"], addrs, 250 + )) + return routes diff --git a/scapy/arch/pcapdnet.py b/scapy/arch/pcapdnet.py deleted file mode 100644 index 41bdec7b0d4..00000000000 --- a/scapy/arch/pcapdnet.py +++ /dev/null @@ -1,394 +0,0 @@ -# This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license - -""" -Packet sending and receiving libpcap/WinPcap. -""" - -import os -import platform -import socket -import struct -import time - -from scapy.automaton import SelectableObject -from scapy.arch.common import _select_nonblock -from scapy.compat import raw, plain_str -from scapy.config import conf -from scapy.consts import WINDOWS -from scapy.data import MTU, ETH_P_ALL -from scapy.pton_ntop import inet_ntop -from scapy.supersocket import SuperSocket -from scapy.error import Scapy_Exception, log_loading, warning -import scapy.consts - -if not scapy.consts.WINDOWS: - from fcntl import ioctl - -############ -# COMMON # -############ - -# From BSD net/bpf.h -# BIOCIMMEDIATE = 0x80044270 -BIOCIMMEDIATE = -2147204496 - - -class _L2pcapdnetSocket(SuperSocket, SelectableObject): - nonblocking_socket = True - - def __init__(self): - SelectableObject.__init__(self) - self.cls = None - - def check_recv(self): - return True - - def recv_raw(self, x=MTU): - """Receives a packet, then returns a tuple containing (cls, pkt_data, time)""" # noqa: E501 - if self.cls is None: - ll = self.ins.datalink() - if ll in conf.l2types: - self.cls = conf.l2types[ll] - else: - self.cls = conf.default_l2 - warning( - "Unable to guess datalink type " - "(interface=%s linktype=%i). Using %s", - self.iface, ll, self.cls.name - ) - - ts, pkt = self.ins.next() - if pkt is None: - return None, None, None - return self.cls, pkt, ts - - def nonblock_recv(self): - """Receives and dissect a packet in non-blocking mode. - Note: on Windows, this won't do anything.""" - self.ins.setnonblock(1) - p = self.recv(MTU) - self.ins.setnonblock(0) - return p - - @staticmethod - def select(sockets, remain=None): - return _select_nonblock(sockets, remain=None) - -########## -# PCAP # -########## - - -if conf.use_pcap: - if WINDOWS: - # Windows specific - NPCAP_PATH = os.environ["WINDIR"] + "\\System32\\Npcap" - from scapy.libs.winpcapy import pcap_setmintocopy - else: - from scapy.libs.winpcapy import pcap_get_selectable_fd - from ctypes import POINTER, byref, create_string_buffer, c_ubyte, cast - - # Part of the Winpcapy integration was inspired by phaethon/scapy - # but he destroyed the commit history, so there is no link to that - try: - from scapy.libs.winpcapy import PCAP_ERRBUF_SIZE, pcap_if_t, \ - sockaddr_in, sockaddr_in6, pcap_findalldevs, pcap_freealldevs, \ - pcap_lib_version, pcap_close, \ - pcap_open_live, pcap_pkthdr, \ - pcap_next_ex, pcap_datalink, \ - pcap_compile, pcap_setfilter, pcap_setnonblock, pcap_sendpacket, \ - bpf_program - - def load_winpcapy(): - """This functions calls libpcap ``pcap_findalldevs`` function, - and extracts and parse all the data scapy will need - to build the Interface List. - - The date will be stored in ``conf.cache_iflist``, or accessible - with ``get_if_list()`` - """ - err = create_string_buffer(PCAP_ERRBUF_SIZE) - devs = POINTER(pcap_if_t)() - if_list = {} - if pcap_findalldevs(byref(devs), err) < 0: - return - try: - p = devs - # Iterate through the different interfaces - while p: - name = plain_str(p.contents.name) # GUID - description = plain_str(p.contents.description) # NAME - flags = p.contents.flags # FLAGS - ips = [] - a = p.contents.addresses - while a: - # IPv4 address - family = a.contents.addr.contents.sa_family - ap = a.contents.addr - if family == socket.AF_INET: - val = cast(ap, POINTER(sockaddr_in)) - val = val.contents.sin_addr[:] - elif family == socket.AF_INET6: - val = cast(ap, POINTER(sockaddr_in6)) - val = val.contents.sin6_addr[:] - else: - # Unknown address family - # (AF_LINK isn't a thing on Windows) - a = a.contents.next - continue - addr = inet_ntop(family, bytes(bytearray(val))) - if addr != "0.0.0.0": - ips.append(addr) - a = a.contents.next - if_list[name] = (description, ips, flags) - p = p.contents.next - conf.cache_iflist = if_list - except Exception: - raise - finally: - pcap_freealldevs(devs) - except OSError: - conf.use_pcap = False - if WINDOWS: - if conf.interactive: - log_loading.critical( - "Npcap/Winpcap is not installed ! See " - "https://scapy.readthedocs.io/en/latest/installation.html#windows" # noqa: E501 - ) - else: - if conf.interactive: - log_loading.critical( - "Libpcap is not installed!" - ) - else: - if WINDOWS: - # Detect Pcap version: check for Npcap - version = pcap_lib_version() - if b"winpcap" in version.lower(): - if os.path.exists(NPCAP_PATH + "\\wpcap.dll"): - warning("Winpcap is installed over Npcap. " - "Will use Winpcap (see 'Winpcap/Npcap conflicts' " - "in Scapy's docs)") - elif platform.release() != "XP": - warning("WinPcap is now deprecated (not maintained). " - "Please use Npcap instead") - elif b"npcap" in version.lower(): - conf.use_npcap = True - LOOPBACK_NAME = scapy.consts.LOOPBACK_NAME = "Npcap Loopback Adapter" # noqa: E501 - -if conf.use_pcap: - def get_if_list(): - """Returns all pcap names""" - if not conf.cache_iflist: - load_winpcapy() - return list(conf.cache_iflist) - - class _PcapWrapper_libpcap: # noqa: F811 - """Wrapper for the libpcap calls""" - - def __init__(self, device, snaplen, promisc, to_ms, monitor=None): - self.errbuf = create_string_buffer(PCAP_ERRBUF_SIZE) - self.iface = create_string_buffer(device.encode("utf8")) - self.dtl = None - if monitor: - if WINDOWS and not conf.use_npcap: - raise OSError("On Windows, this feature requires NPcap !") - # Npcap-only functions - from scapy.libs.winpcapy import pcap_create, \ - pcap_set_snaplen, pcap_set_promisc, \ - pcap_set_timeout, pcap_set_rfmon, pcap_activate - self.pcap = pcap_create(self.iface, self.errbuf) - pcap_set_snaplen(self.pcap, snaplen) - pcap_set_promisc(self.pcap, promisc) - pcap_set_timeout(self.pcap, to_ms) - if pcap_set_rfmon(self.pcap, 1) != 0: - warning("Could not set monitor mode") - if pcap_activate(self.pcap) != 0: - raise OSError("Could not activate the pcap handler") - else: - self.pcap = pcap_open_live(self.iface, - snaplen, promisc, to_ms, - self.errbuf) - error = bytes(bytearray(self.errbuf)).strip(b"\x00") - if error: - raise OSError(error) - - if WINDOWS: - # Winpcap/Npcap exclusive: make every packet to be instantly - # returned, and not buffered within Winpcap/Npcap - pcap_setmintocopy(self.pcap, 0) - - self.header = POINTER(pcap_pkthdr)() - self.pkt_data = POINTER(c_ubyte)() - self.bpf_program = bpf_program() - - def next(self): - """ - Returns the next packet as the tuple - (timestamp, raw_packet) - """ - c = pcap_next_ex( - self.pcap, - byref(self.header), - byref(self.pkt_data) - ) - if not c > 0: - return None, None - ts = self.header.contents.ts.tv_sec + float(self.header.contents.ts.tv_usec) / 1e6 # noqa: E501 - pkt = bytes(bytearray(self.pkt_data[:self.header.contents.len])) - return ts, pkt - __next__ = next - - def datalink(self): - """Wrapper around pcap_datalink""" - if self.dtl is None: - self.dtl = pcap_datalink(self.pcap) - return self.dtl - - def fileno(self): - if WINDOWS: - log_loading.error("Cannot get selectable PCAP fd on Windows") - return -1 - else: - # This does not exist under Windows - return pcap_get_selectable_fd(self.pcap) - - def setfilter(self, f): - filter_exp = create_string_buffer(f.encode("utf8")) - if pcap_compile(self.pcap, byref(self.bpf_program), filter_exp, 0, -1) == -1: # noqa: E501 - log_loading.error("Could not compile filter expression %s", f) - return False - else: - if pcap_setfilter(self.pcap, byref(self.bpf_program)) == -1: - log_loading.error("Could not install filter %s", f) - return False - return True - - def setnonblock(self, i): - pcap_setnonblock(self.pcap, i, self.errbuf) - - def send(self, x): - pcap_sendpacket(self.pcap, x, len(x)) - - def close(self): - pcap_close(self.pcap) - open_pcap = _PcapWrapper_libpcap - - # pcap sockets - - class L2pcapListenSocket(_L2pcapdnetSocket): - desc = "read packets at layer 2 using libpcap" - - def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, monitor=None): # noqa: E501 - super(L2pcapListenSocket, self).__init__() - self.type = type - self.outs = None - self.iface = iface - if iface is None: - iface = conf.iface - if promisc is None: - promisc = conf.sniff_promisc - self.promisc = promisc - # Note: Timeout with Winpcap/Npcap - # The 4th argument of open_pcap corresponds to timeout. In an ideal world, we would # noqa: E501 - # set it to 0 ==> blocking pcap_next_ex. - # However, the way it is handled is very poor, and result in a jerky packet stream. # noqa: E501 - # To fix this, we set 100 and the implementation under windows is slightly different, as # noqa: E501 - # everything is always received as non-blocking - self.ins = open_pcap(iface, MTU, self.promisc, 100, - monitor=monitor) - try: - ioctl(self.ins.fileno(), BIOCIMMEDIATE, struct.pack("I", 1)) - except Exception: - pass - if type == ETH_P_ALL: # Do not apply any filter if Ethernet type is given # noqa: E501 - if conf.except_filter: - if filter: - filter = "(%s) and not (%s)" % (filter, conf.except_filter) # noqa: E501 - else: - filter = "not (%s)" % conf.except_filter - if filter: - self.ins.setfilter(filter) - - def send(self, x): - raise Scapy_Exception("Can't send anything with L2pcapListenSocket") # noqa: E501 - - class L2pcapSocket(_L2pcapdnetSocket): - desc = "read/write packets at layer 2 using only libpcap" - - def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, nofilter=0, # noqa: E501 - monitor=None): - super(L2pcapSocket, self).__init__() - if iface is None: - iface = conf.iface - self.iface = iface - if promisc is None: - promisc = 0 - self.promisc = promisc - # See L2pcapListenSocket for infos about this line - self.ins = open_pcap(iface, MTU, self.promisc, 100, - monitor=monitor) - self.outs = self.ins - try: - ioctl(self.ins.fileno(), BIOCIMMEDIATE, struct.pack("I", 1)) - except Exception: - pass - if nofilter: - if type != ETH_P_ALL: # PF_PACKET stuff. Need to emulate this for pcap # noqa: E501 - filter = "ether proto %i" % type - else: - filter = None - else: - if conf.except_filter: - if filter: - filter = "(%s) and not (%s)" % (filter, conf.except_filter) # noqa: E501 - else: - filter = "not (%s)" % conf.except_filter - if type != ETH_P_ALL: # PF_PACKET stuff. Need to emulate this for pcap # noqa: E501 - if filter: - filter = "(ether proto %i) and (%s)" % (type, filter) - else: - filter = "ether proto %i" % type - if filter: - self.ins.setfilter(filter) - - def send(self, x): - sx = raw(x) - try: - x.sent_time = time.time() - except AttributeError: - pass - self.outs.send(sx) - - class L3pcapSocket(L2pcapSocket): - desc = "read/write packets at layer 3 using only libpcap" - - def recv(self, x=MTU): - r = L2pcapSocket.recv(self, x) - if r: - r.payload.time = r.time - return r.payload - return r - - def send(self, x): - # Makes send detects when it should add Loopback(), Dot11... instead of Ether() # noqa: E501 - ll = self.ins.datalink() - if ll in conf.l2types: - cls = conf.l2types[ll] - else: - cls = conf.default_l2 - warning("Unable to guess datalink type (interface=%s linktype=%i). Using %s", self.iface, ll, cls.name) # noqa: E501 - sx = raw(cls() / x) - try: - x.sent_time = time.time() - except AttributeError: - pass - self.outs.send(sx) -else: - # No libpcap installed - get_if_list = lambda: [] - if WINDOWS: - NPCAP_PATH = "" diff --git a/scapy/arch/solaris.py b/scapy/arch/solaris.py index b450c3d5dfb..cb30bf04e89 100644 --- a/scapy/arch/solaris.py +++ b/scapy/arch/solaris.py @@ -1,14 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Customization for the Solaris operation system. """ import socket -import scapy.consts from scapy.config import conf conf.use_pcap = True @@ -19,12 +18,15 @@ # From sys/sockio.h and net/if.h SIOCGIFHWADDR = 0xc02069b9 # Get hardware address -from scapy.arch.pcapdnet import * # noqa: F401, F403 -from scapy.arch.unix import * # noqa: F401, F403 -from scapy.arch.common import get_if_raw_hwaddr # noqa: F401, F403 +from scapy.arch.common import get_if_raw_addr # noqa: F401, F403, E402 +from scapy.arch.libpcap import * # noqa: F401, F403, E402 +from scapy.arch.unix import * # noqa: F401, F403, E402 + +from scapy.interfaces import NetworkInterface # noqa: E402 def get_working_if(): + # type: () -> NetworkInterface """Return an interface that works""" try: # return the interface associated with the route with smallest @@ -32,5 +34,5 @@ def get_working_if(): iface = min(conf.route.routes, key=lambda x: x[1])[3] except ValueError: # no route - iface = scapy.consts.LOOPBACK_INTERFACE - return iface + iface = conf.loopback_name + return conf.ifaces.dev_from_name(iface) diff --git a/scapy/arch/unix.py b/scapy/arch/unix.py index 244ddd07c71..672a98a6da7 100644 --- a/scapy/arch/unix.py +++ b/scapy/arch/unix.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Common customizations for all Unix-like operating systems other than Linux @@ -9,23 +9,70 @@ import os import socket +import struct +from fcntl import ioctl import scapy.config import scapy.utils -from scapy.arch import get_if_addr from scapy.config import conf -from scapy.consts import FREEBSD, NETBSD, OPENBSD, SOLARIS, LOOPBACK_NAME -from scapy.error import warning, log_interactive +from scapy.consts import FREEBSD, NETBSD, OPENBSD, SOLARIS +from scapy.error import log_runtime, warning from scapy.pton_ntop import inet_pton from scapy.utils6 import in6_getscope, construct_source_candidate_set from scapy.utils6 import in6_isvalid, in6_ismlladdr, in6_ismnladdr +# Typing imports +from typing import ( + List, + Optional, + Tuple, + Union, + cast, +) + + +def get_if(iff, cmd): + # type: (str, int) -> bytes + """Ease SIOCGIF* ioctl calls""" + + sck = socket.socket() + try: + return ioctl(sck, cmd, struct.pack("16s16x", iff.encode("utf8"))) + finally: + sck.close() + + +def get_if_raw_hwaddr(iff, # type: str + siocgifhwaddr=None, # type: Optional[int] + ): + # type: (...) -> Tuple[int, bytes] + """Get the raw MAC address of a local interface. + + This function uses SIOCGIFHWADDR calls, therefore only works + on some distros. + + :param iff: the network interface name as a string + :returns: the corresponding raw MAC address + """ + + if siocgifhwaddr is None: + from scapy.arch import SIOCGIFHWADDR + siocgifhwaddr = SIOCGIFHWADDR + return cast( + "Tuple[int, bytes]", + struct.unpack( + "16xH6s8x", + get_if(iff, siocgifhwaddr) + ) + ) + ################## # Routes stuff # ################## def _guess_iface_name(netif): + # type: (str) -> Optional[str] """ We attempt to guess the name of interfaces that are truncated from the output of ifconfig -l. @@ -42,6 +89,7 @@ def _guess_iface_name(netif): def read_routes(): + # type: () -> List[Tuple[int, int, str, str, str, int]] """Return a list of IPv4 routes than can be used by Scapy. This function parses netstat. @@ -49,7 +97,7 @@ def read_routes(): if SOLARIS: f = os.popen("netstat -rvn -f inet") elif FREEBSD: - f = os.popen("netstat -rnW") # -W to handle long interface names + f = os.popen("netstat -rnW -f inet") # -W to show long interface names else: f = os.popen("netstat -rn -f inet") ok = 0 @@ -57,8 +105,8 @@ def read_routes(): prio_present = False refs_present = False use_present = False - routes = [] - pending_if = [] + routes = [] # type: List[Tuple[int, int, str, str, str, int]] + pending_if = [] # type: List[Tuple[int, int, str]] for line in f.readlines(): if not line: break @@ -71,57 +119,60 @@ def read_routes(): mtu_present = "mtu" in line prio_present = "prio" in line refs_present = "ref" in line # There is no s on Solaris - use_present = "use" in line + use_present = "use" in line or "nhop" in line continue if not line: break rt = line.split() if SOLARIS: - dest, netmask, gw, netif = rt[:4] + dest_, netmask_, gw, netif = rt[:4] flg = rt[4 + mtu_present + refs_present] else: - dest, gw, flg = rt[:3] + dest_, gw, flg = rt[:3] locked = OPENBSD and rt[6] == "l" offset = mtu_present + prio_present + refs_present + locked offset += use_present netif = rt[3 + offset] if flg.find("lc") >= 0: continue - elif dest == "default": + elif dest_ == "default": dest = 0 netmask = 0 elif SOLARIS: - dest = scapy.utils.atol(dest) - netmask = scapy.utils.atol(netmask) + dest = scapy.utils.atol(dest_) + netmask = scapy.utils.atol(netmask_) else: - if "/" in dest: - dest, netmask = dest.split("/") - netmask = scapy.utils.itom(int(netmask)) + if "/" in dest_: + dest_, netmask_ = dest_.split("/") + netmask = scapy.utils.itom(int(netmask_)) else: - netmask = scapy.utils.itom((dest.count(".") + 1) * 8) - dest += ".0" * (3 - dest.count(".")) - dest = scapy.utils.atol(dest) + netmask = scapy.utils.itom((dest_.count(".") + 1) * 8) + dest_ += ".0" * (3 - dest_.count(".")) + dest = scapy.utils.atol(dest_) # XXX: TODO: add metrics for unix.py (use -e option on netstat) metric = 1 if "g" not in flg: gw = '0.0.0.0' if netif is not None: + from scapy.arch import get_if_addr try: ifaddr = get_if_addr(netif) - routes.append((dest, netmask, gw, netif, ifaddr, metric)) - except OSError as exc: - if exc.message == 'Device not configured': + if ifaddr == "0.0.0.0": # This means the interface name is probably truncated by # netstat -nr. We attempt to guess it's name and if not we # ignore it. guessed_netif = _guess_iface_name(netif) if guessed_netif is not None: ifaddr = get_if_addr(guessed_netif) - routes.append((dest, netmask, gw, guessed_netif, ifaddr, metric)) # noqa: E501 + netif = guessed_netif else: - warning("Could not guess partial interface name: %s", netif) # noqa: E501 - else: - raise + log_runtime.info( + "Could not guess partial interface name: %s", + netif + ) + routes.append((dest, netmask, gw, netif, ifaddr, metric)) + except OSError: + raise else: pending_if.append((dest, netmask, gw)) f.close() @@ -131,8 +182,8 @@ def read_routes(): # know their output interface for dest, netmask, gw in pending_if: gw_l = scapy.utils.atol(gw) - max_rtmask, gw_if, gw_if_addr, = 0, None, None - for rtdst, rtmask, _, rtif, rtaddr in routes[:]: + max_rtmask, gw_if, gw_if_addr = 0, None, None + for rtdst, rtmask, _, rtif, rtaddr, _ in routes[:]: if gw_l & rtmask == rtdst: if rtmask >= max_rtmask: max_rtmask = rtmask @@ -140,7 +191,7 @@ def read_routes(): gw_if_addr = rtaddr # XXX: TODO add metrics metric = 1 - if gw_if: + if gw_if and gw_if_addr: routes.append((dest, netmask, gw, gw_if, gw_if_addr, metric)) else: warning("Did not find output interface to reach gateway %s", gw) @@ -153,6 +204,7 @@ def read_routes(): def _in6_getifaddr(ifname): + # type: (str) -> List[Tuple[str, int, str]] """ Returns a list of IPv6 addresses configured on the interface ifname. """ @@ -161,7 +213,7 @@ def _in6_getifaddr(ifname): try: f = os.popen("%s %s" % (conf.prog.ifconfig, ifname)) except OSError: - log_interactive.warning("Failed to execute ifconfig.") + log_runtime.warning("Failed to execute ifconfig.") return [] # Iterate over lines and extract IPv6 addresses @@ -189,6 +241,7 @@ def _in6_getifaddr(ifname): def in6_getifaddr(): + # type: () -> List[Tuple[str, int, str]] """ Returns a list of 3-tuples of the form (addr, scope, iface) where 'addr' is the address of scope 'scope' associated to the interface @@ -207,21 +260,21 @@ def in6_getifaddr(): try: f = os.popen(cmd % conf.prog.ifconfig) except OSError: - log_interactive.warning("Failed to execute ifconfig.") + log_runtime.warning("Failed to execute ifconfig.") return [] # Get the list of network interfaces splitted_line = [] - for l in f: - if "flags" in l: - iface = l.split()[0].rstrip(':') + for line in f: + if "flags" in line: + iface = line.split()[0].rstrip(':') splitted_line.append(iface) else: # FreeBSD, NetBSD or Darwin try: f = os.popen("%s -l" % conf.prog.ifconfig) except OSError: - log_interactive.warning("Failed to execute ifconfig.") + log_runtime.warning("Failed to execute ifconfig.") return [] # Get the list of network interfaces @@ -235,6 +288,7 @@ def in6_getifaddr(): def read_routes6(): + # type: () -> List[Tuple[str, int, str, str, List[str], int]] """Return a list of IPv6 routes than can be used by Scapy. This function parses netstat. @@ -299,7 +353,7 @@ def read_routes6(): next_hop = "::" # Default prefix length - destination_plen = 128 + destination_plen = 128 # type: Union[int, str] # Extract network interface from the zone id if '%' in destination: @@ -339,7 +393,7 @@ def read_routes6(): # Note: multicast routing is handled in Route6.route() continue - if LOOPBACK_NAME in dev: + if conf.loopback_name in dev: # Handle ::1 separately cset = ["::1"] next_hop = "::" diff --git a/scapy/arch/windows/__init__.py b/scapy/arch/windows/__init__.py index 01b84f9d23e..81b7bed57fd 100755 --- a/scapy/arch/windows/__init__.py +++ b/scapy/arch/windows/__init__.py @@ -1,58 +1,101 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter """ Customizations needed to support Microsoft Windows. """ -from __future__ import absolute_import -from __future__ import print_function +from glob import glob import os +import platform as platform_lib import socket -import subprocess as sp -from glob import glob import struct +import subprocess as sp +import warnings + +import winreg -import scapy -import scapy.consts -from scapy.arch.windows.structures import _windows_title, \ - GetAdaptersAddresses, GetIpForwardTable, GetIpForwardTable2, \ - get_service_status +from scapy.arch.windows.structures import ( + _windows_title, + GetAdaptersAddresses, + GetIpForwardTable, + GetIpForwardTable2, + get_service_status, +) from scapy.consts import WINDOWS, WINDOWS_XP -from scapy.config import conf, ConfClass -from scapy.error import Scapy_Exception, log_loading, log_runtime, warning -from scapy.pton_ntop import inet_ntop, inet_pton -from scapy.utils import atol, itom, pretty_list, mac2str, str2mac +from scapy.config import conf, ProgPath +from scapy.error import ( + Scapy_Exception, + log_interactive, + log_loading, + log_runtime, + warning, +) +from scapy.interfaces import NetworkInterface, InterfaceProvider, \ + dev_from_index, resolve_iface, network_name +from scapy.pton_ntop import inet_ntop +from scapy.utils import atol, itom, str2mac from scapy.utils6 import construct_source_candidate_set, in6_getscope -from scapy.data import ARPHDR_ETHER, load_manuf -import scapy.modules.six as six -from scapy.modules.six.moves import input, winreg, UserDict from scapy.compat import plain_str from scapy.supersocket import SuperSocket +# re-export +from scapy.arch.common import get_if_raw_addr # noqa: F401 + +# Typing imports +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Tuple, + Type, + Union, + cast, + overload, +) +from scapy.compat import Literal + conf.use_pcap = True # These import must appear after setting conf.use_* variables -from scapy.arch import pcapdnet # noqa: E402 -from scapy.arch.pcapdnet import NPCAP_PATH, get_if_list # noqa: E402 +from scapy.arch import libpcap # noqa: E402 +from scapy.arch.libpcap import ( # noqa: E402 + NPCAP_PATH, + PCAP_IF_UP, +) + +# Detection happens after libpcap import (NPcap detection) +NPCAP_LOOPBACK_NAME = r"\Device\NPF_Loopback" +NPCAP_LOOPBACK_NAME_LEGACY = "Npcap Loopback Adapter" # before npcap 0.9983 +if conf.use_npcap: + conf.loopback_name = NPCAP_LOOPBACK_NAME +else: + try: + if float(platform_lib.release()) >= 8.1: + conf.loopback_name = "Microsoft KM-TEST Loopback Adapter" + else: + conf.loopback_name = "Microsoft Loopback Adapter" + except ValueError: + conf.loopback_name = "Microsoft Loopback Adapter" # hot-patching socket for missing variables on Windows if not hasattr(socket, 'IPPROTO_IPIP'): - socket.IPPROTO_IPIP = 4 + socket.IPPROTO_IPIP = 4 # type: ignore if not hasattr(socket, 'IP_RECVTTL'): - socket.IP_RECVTTL = 12 + socket.IP_RECVTTL = 12 # type: ignore if not hasattr(socket, 'IPV6_HDRINCL'): - socket.IPV6_HDRINCL = 36 -# https://bugs.python.org/issue29515 + socket.IPV6_HDRINCL = 36 # type: ignore +# https://github.com/python/cpython/issues/73701 if not hasattr(socket, 'IPPROTO_IPV6'): - socket.SOL_IPV6 = 41 + socket.IPPROTO_IPV6 = 41 if not hasattr(socket, 'SOL_IPV6'): - socket.SOL_IPV6 = socket.IPPROTO_IPV6 + socket.SOL_IPV6 = socket.IPPROTO_IPV6 # type: ignore if not hasattr(socket, 'IPPROTO_GRE'): - socket.IPPROTO_GRE = 47 + socket.IPPROTO_GRE = 47 # type: ignore if not hasattr(socket, 'IPPROTO_AH'): socket.IPPROTO_AH = 51 if not hasattr(socket, 'IPPROTO_ESP'): @@ -62,6 +105,7 @@ def _encapsulate_admin(cmd): + # type: (str) -> str """Encapsulate a command with an Administrator flag""" # To get admin access, we start a new powershell instance with admin # rights, which will execute the command. This needs to be done from a @@ -73,6 +117,7 @@ def _encapsulate_admin(cmd): def _get_npcap_config(param_key): + # type: (str) -> Optional[str] """ Get a Npcap parameter matching key in the registry. @@ -89,10 +134,11 @@ def _get_npcap_config(param_key): winreg.CloseKey(key) except WindowsError: return None - return dot11_adapters + return cast(str, dot11_adapters) def _where(filename, dirs=None, env="PATH"): + # type: (str, Optional[Any], str) -> str """Find file in current dir, in deep_lookup cache or in system path""" if dirs is None: dirs = [] @@ -111,6 +157,7 @@ def _where(filename, dirs=None, env="PATH"): def win_find_exe(filename, installsubdir=None, env="ProgramFiles"): + # type: (str, Optional[Any], str) -> str """Find executable in current dir, system path or in the given ProgramFiles subdir, and retuen its absolute path. """ @@ -125,19 +172,19 @@ def win_find_exe(filename, installsubdir=None, env="ProgramFiles"): path = None else: break - return path - + return path or "" -class WinProgPath(ConfClass): - _default = "" +class WinProgPath(ProgPath): def __init__(self): + # type: () -> None self._reload() def _reload(self): - self.pdfreader = None - self.psreader = None - self.svgreader = None + # type: () -> None + self.pdfreader = "" + self.psreader = "" + self.svgreader = "" # We try some magic to find the appropriate executables self.dot = win_find_exe("dot") self.tcpdump = win_find_exe("windump") @@ -147,37 +194,21 @@ def _reload(self): self.hexedit = win_find_exe("hexer") self.sox = win_find_exe("sox") self.wireshark = win_find_exe("wireshark", "wireshark") - self.usbpcapcmd = win_find_exe( - "USBPcapCMD", - installsubdir="USBPcap", - env="programfiles" - ) + self.extcap_folders = [ + os.path.join(os.environ.get("appdata", ""), "Wireshark", "extcap"), + os.path.join(os.environ.get("programfiles", ""), "Wireshark", "extcap"), + ] self.powershell = win_find_exe( "powershell", installsubdir="System32\\WindowsPowerShell\\v1.0", env="SystemRoot" ) - self.cscript = win_find_exe("cscript", installsubdir="System32", - env="SystemRoot") self.cmd = win_find_exe("cmd", installsubdir="System32", env="SystemRoot") - if self.wireshark: - try: - new_manuf = load_manuf( - os.path.sep.join( - self.wireshark.split(os.path.sep)[:-1] - ) + os.path.sep + "manuf" - ) - except (IOError, OSError): # FileNotFoundError not available on Py2 - using OSError # noqa: E501 - log_loading.warning("Wireshark is installed, but cannot read manuf !") # noqa: E501 - new_manuf = None - if new_manuf: - # Inject new ManufDB - conf.manufdb.__dict__.clear() - conf.manufdb.__dict__.update(new_manuf.__dict__) def _exec_cmd(command): + # type: (str) -> Tuple[bytes, int] """Call a CMD command and return the output and returncode""" proc = sp.Popen(command, stdout=sp.PIPE, @@ -190,6 +221,7 @@ def _exec_cmd(command): if conf.prog.tcpdump and conf.use_npcap: def test_windump_npcap(): + # type: () -> bool """Return whether windump version is correct or not""" try: p_test_windump = sp.Popen([conf.prog.tcpdump, "-help"], stdout=sp.PIPE, stderr=sp.STDOUT) # noqa: E501 @@ -201,48 +233,55 @@ def test_windump_npcap(): return False windump_ok = test_windump_npcap() if not windump_ok: - warning("The installed Windump version does not work with Npcap ! Refer to 'Winpcap/Npcap conflicts' in scapy's doc") # noqa: E501 + log_loading.warning( + "The installed Windump version does not work with Npcap! " + "Refer to 'Winpcap/Npcap conflicts' in scapy's installation doc" + ) del windump_ok def get_windows_if_list(extended=False): + # type: (bool) -> List[Dict[str, Any]] """Returns windows interfaces through GetAdaptersAddresses. params: - extended: include anycast and multicast IPv6 (default False)""" # Should work on Windows XP+ def _get_mac(x): + # type: (Dict[str, Any]) -> str size = x["physical_address_length"] if size != 6: return "" data = bytearray(x["physical_address"]) return str2mac(bytes(data)[:size]) + def _resolve_ips(y): + # type: (List[Dict[str, Any]]) -> List[str] + if not isinstance(y, list): + return [] + ips = [] + for ip in y: + addr = ip['address']['address'].contents + if addr.si_family == socket.AF_INET6: + ip_key = "Ipv6" + si_key = "sin6_addr" + else: + ip_key = "Ipv4" + si_key = "sin_addr" + data = getattr(addr, ip_key) + data = getattr(data, si_key) + data = bytes(bytearray(data.byte)) + # Build IP + if data: + ips.append(inet_ntop(addr.si_family, data)) + return ips + def _get_ips(x): + # type: (Dict[str, Any]) -> List[str] unicast = x['first_unicast_address'] anycast = x['first_anycast_address'] multicast = x['first_multicast_address'] - def _resolve_ips(y): - if not isinstance(y, list): - return [] - ips = [] - for ip in y: - addr = ip['address']['address'].contents - if addr.si_family == socket.AF_INET6: - ip_key = "Ipv6" - si_key = "sin6_addr" - else: - ip_key = "Ipv4" - si_key = "sin_addr" - data = getattr(addr, ip_key) - data = getattr(data, si_key) - data = bytes(bytearray(data.byte)) - # Build IP - if data: - ips.append(inet_ntop(addr.si_family, data)) - return ips - ips = [] ips.extend(_resolve_ips(unicast)) if extended: @@ -250,47 +289,24 @@ def _resolve_ips(y): ips.extend(_resolve_ips(multicast)) return ips - if six.PY2: - _str_decode = lambda x: x.encode('utf8', errors='ignore') - else: - _str_decode = plain_str return [ { - "name": _str_decode(x["friendly_name"]), - "win_index": x["interface_index"], - "description": _str_decode(x["description"]), - "guid": _str_decode(x["adapter_name"]), + "name": plain_str(x["friendly_name"]), + "index": x["interface_index"], + "description": plain_str(x["description"]), + "guid": plain_str(x["adapter_name"]), "mac": _get_mac(x), + "type": x["interface_type"], "ipv4_metric": 0 if WINDOWS_XP else x["ipv4_metric"], "ipv6_metric": 0 if WINDOWS_XP else x["ipv6_metric"], - "ips": _get_ips(x) + "ips": _get_ips(x), + "nameservers": _resolve_ips(x["first_dns_server_address"]) } for x in GetAdaptersAddresses() ] -def get_ips(v6=False): - """Returns all available IPs matching to interfaces, using the windows system. - Should only be used as a WinPcapy fallback.""" - res = {} - for iface in six.itervalues(IFACES): - ips = [] - for ip in iface.ips: - if v6 and ":" in ip: - ips.append(ip) - elif not v6 and ":" not in ip: - ips.append(ip) - res[iface] = ips - return res - - -def get_ip_from_name(ifname, v6=False): - """Backward compatibility: indirectly calls get_ips - Deprecated.""" - iface = IFACES.dev_from_name(ifname) - return get_ips(v6=v6).get(iface, [""])[0] - - def _pcapname_to_guid(pcap_name): + # type: (str) -> str """Converts a Winpcap/Npcap pcpaname to its guid counterpart. e.g. \\DEVICE\\NPF_{...} => {...} """ @@ -299,100 +315,59 @@ def _pcapname_to_guid(pcap_name): return pcap_name -class NetworkInterface(object): +class NetworkInterface_Win(NetworkInterface): """A network interface of your local host""" - def __init__(self, data=None): - self.name = None - self.ip = None - self.mac = None - self.pcap_name = None - self.description = None - self.invalid = False - self.raw80211 = None - self.cache_mode = None - self.ipv4_metric = None - self.ipv6_metric = None - self.ips = None - self.flags = None - if data is not None: - self.update(data) + def __init__(self, provider, data=None): + # type: (WindowsInterfacesProvider, Optional[Dict[str, Any]]) -> None + self.cache_mode = None # type: Optional[bool] + self.ipv4_metric = None # type: Optional[int] + self.ipv6_metric = None # type: Optional[int] + self.nameservers = [] # type: List[str] + self.guid = None # type: Optional[str] + self.raw80211 = None # type: Optional[bool] + super(NetworkInterface_Win, self).__init__(provider, data) def update(self, data): + # type: (Dict[str, Any]) -> None """Update info about a network interface according to a given dictionary. Such data is provided by get_windows_if_list """ - self.data = data - self.name = data['name'] - self.description = data['description'] - self.win_index = data['win_index'] + # Populated early because used below + self.network_name = data['network_name'] + # Windows specific self.guid = data['guid'] - self.mac = data['mac'] self.ipv4_metric = data['ipv4_metric'] self.ipv6_metric = data['ipv6_metric'] - self.ips = data['ips'] - if 'invalid' in data: - self.invalid = data['invalid'] - # Other attributes are optional - self._update_pcapdata() + self.nameservers = data['nameservers'] try: # Npcap loopback interface - if conf.use_npcap: - pcap_name_loopback = _get_npcap_config("LoopbackAdapter") - if pcap_name_loopback: # May not be defined - guid = _pcapname_to_guid(pcap_name_loopback) - if self.guid == guid: - # https://nmap.org/npcap/guide/npcap-devguide.html - self.mac = "00:00:00:00:00:00" - self.ip = "127.0.0.1" - return + if conf.use_npcap and self.network_name == conf.loopback_name: + # https://nmap.org/npcap/guide/npcap-devguide.html + data["mac"] = "00:00:00:00:00:00" + data["ip"] = "127.0.0.1" + data["ip6"] = "::1" + data["ips"] = ["127.0.0.1", "::1"] except KeyError: pass - - try: - self.ip = next(x for x in self.ips if ":" not in x) - except StopIteration: - pass - - try: - # Windows native loopback interface - if not self.ip and self.name == scapy.consts.LOOPBACK_NAME: - self.ip = "127.0.0.1" - except (KeyError, AttributeError, NameError) as e: - print(e) - - def _update_pcapdata(self): - # https://github.com/nmap/nmap/issues/1422 - # Lookup for the Winpcap/Npcap pcap_name according to the GUID - if self.is_invalid(): - return - for pcap_name, if_data in six.iteritems(conf.cache_iflist): - _, ips, flags = if_data - if pcap_name.endswith(self.guid): - self.pcap_name = pcap_name - self.flags = flags - self.ips.extend(x for x in ips if x not in self.ips) - return - # No matching pcap_name found: won't be able to sniff on it - self.invalid = True - - def is_invalid(self): - return self.invalid + super(NetworkInterface_Win, self).update(data) def _check_npcap_requirement(self): + # type: () -> None if not conf.use_npcap: raise OSError("This operation requires Npcap.") if self.raw80211 is None: - # The Dot11Adapters is not officially supported anymore. - # we just try/except, and check that it exists globally val = _get_npcap_config("Dot11Support") self.raw80211 = bool(int(val)) if val else False if not self.raw80211: - raise Scapy_Exception("This interface does not support raw 802.11") + raise Scapy_Exception("Npcap 802.11 support is NOT enabled !") def _npcap_set(self, key, val): + # type: (str, str) -> bool """Internal function. Set a [key] parameter to [value]""" + if self.guid is None: + raise OSError("Interface not setup") res, code = _exec_cmd(_encapsulate_admin( " ".join([_WlanHelper, self.guid[1:-1], key, val]) )) @@ -402,6 +377,9 @@ def _npcap_set(self, key, val): return True def _npcap_get(self, key): + # type: (str) -> str + if self.guid is None: + raise OSError("Interface not setup") res, code = _exec_cmd(" ".join([_WlanHelper, self.guid[1:-1], key])) _windows_title() # Reset title of the window if code != 0: @@ -409,12 +387,14 @@ def _npcap_get(self, key): return plain_str(res.strip()) def mode(self): + # type: () -> str """Get the interface operation mode. Only available with Npcap.""" self._check_npcap_requirement() return self._npcap_get("mode") def ismonitor(self): + # type: () -> bool """Returns True if the interface is in monitor mode. Only available with Npcap.""" if self.cache_mode is not None: @@ -427,6 +407,7 @@ def ismonitor(self): return False def setmonitor(self, enable=True): + # type: (bool) -> bool """Alias for setmode('monitor') or setmode('managed') Only available with Npcap""" # We must reset the monitor cache @@ -441,6 +422,7 @@ def setmonitor(self, enable=True): return tmp if enable else (not tmp) def availablemodes(self): + # type: () -> List[str] """Get all available interface modes. Only available with Npcap.""" # According to https://nmap.org/npcap/guide/npcap-devguide.html#npcap-feature-dot11 # noqa: E501 @@ -448,6 +430,7 @@ def availablemodes(self): return self._npcap_get("modes").split(",") def setmode(self, mode): + # type: (Union[str, int]) -> bool """Set the interface mode. It can be: - 0 or managed: Managed Mode (aka "Extensible Station Mode") - 1 or monitor: Monitor Mode (aka "Network Monitor Mode") @@ -474,6 +457,7 @@ def setmode(self, mode): return self._npcap_set("mode", m) def channel(self): + # type: () -> int """Get the channel of the interface. Only available with Npcap.""" # According to https://nmap.org/npcap/guide/npcap-devguide.html#npcap-feature-dot11 # noqa: E501 @@ -481,20 +465,23 @@ def channel(self): return int(self._npcap_get("channel")) def setchannel(self, channel): + # type: (int) -> bool """Set the channel of the interface (1-14): Only available with Npcap.""" # According to https://nmap.org/npcap/guide/npcap-devguide.html#npcap-feature-dot11 # noqa: E501 self._check_npcap_requirement() return self._npcap_set("channel", str(channel)) - def frequence(self): - """Get the frequence of the interface. + def frequency(self): + # type: () -> int + """Get the frequency of the interface. Only available with Npcap.""" # According to https://nmap.org/npcap/guide/npcap-devguide.html#npcap-feature-dot11 # noqa: E501 self._check_npcap_requirement() return int(self._npcap_get("freq")) - def setfrequence(self, freq): + def setfrequency(self, freq): + # type: (int) -> bool """Set the channel of the interface (1-14): Only available with Npcap.""" # According to https://nmap.org/npcap/guide/npcap-devguide.html#npcap-feature-dot11 # noqa: E501 @@ -502,6 +489,7 @@ def setfrequence(self, freq): return self._npcap_set("freq", str(freq)) def availablemodulations(self): + # type: () -> List[str] """Get all available 802.11 interface modulations. Only available with Npcap.""" # According to https://nmap.org/npcap/guide/npcap-devguide.html#npcap-feature-dot11 # noqa: E501 @@ -509,6 +497,7 @@ def availablemodulations(self): return self._npcap_get("modus").split(",") def modulation(self): + # type: () -> str """Get the 802.11 modulation of the interface. Only available with Npcap.""" # According to https://nmap.org/npcap/guide/npcap-devguide.html#npcap-feature-dot11 # noqa: E501 @@ -516,6 +505,7 @@ def modulation(self): return self._npcap_get("modu") def setmodulation(self, modu): + # type: (int) -> bool """Set the interface modulation. It can be: - 0: dsss - 1: fhss @@ -548,54 +538,21 @@ def setmodulation(self, modu): m = _modus.get(modu, "unknown") if isinstance(modu, int) else modu return self._npcap_set("modu", str(m)) - def __repr__(self): - return "<%s [%s] %s>" % (self.__class__.__name__, - self.description, - self.guid) - - -def get_if_raw_addr(iff): - """Return the raw IPv4 address of interface""" - if not iff.ip: - return None - return inet_pton(socket.AF_INET, iff.ip) - - -def pcap_service_name(): - """Return the pcap adapter service's name""" - return "npcap" if conf.use_npcap else "npf" - -def pcap_service_status(): - """Returns whether the windows pcap adapter is running or not""" - status = get_service_status(pcap_service_name()) - return status["dwCurrentState"] == 4 +class WindowsInterfacesProvider(InterfaceProvider): + name = "libpcap" + libpcap = True - -def _pcap_service_control(action, askadmin=True): - """Internal util to run pcap control command""" - command = action + ' ' + pcap_service_name() - res, code = _exec_cmd(_encapsulate_admin(command) if askadmin else command) - if code != 0: - warning(res.decode("utf8", errors="ignore")) - return (code == 0) - - -def pcap_service_start(askadmin=True): - """Starts the pcap adapter. Will ask for admin. Returns True if success""" - return _pcap_service_control('sc start', askadmin=askadmin) - - -def pcap_service_stop(askadmin=True): - """Stops the pcap adapter. Will ask for admin. Returns True if success""" - return _pcap_service_control('sc stop', askadmin=askadmin) - - -class NetworkInterfaceDict(UserDict): - """Store information about network interfaces and convert between names""" + def _is_valid(self, dev): + # type: (NetworkInterface) -> bool + # Winpcap (and old Npcap) have no support for PCAP_IF_UP :( + if dev.flags == 0: + return True + return bool(dev.flags & PCAP_IF_UP) @classmethod def _pcap_check(cls): + # type: () -> None """Performs checks/restart pcap adapter""" if not conf.use_pcap: # Winpcap/Npcap isn't installed @@ -604,13 +561,14 @@ def _pcap_check(cls): _detect = pcap_service_status() def _ask_user(): + # type: () -> bool if not conf.interactive: return False msg = "Do you want to start it ? (yes/no) [y]: " try: # Better IPython compatibility import IPython - return IPython.utils.io.ask_yes_no(msg, default='y') + return cast(bool, IPython.utils.io.ask_yes_no(msg, default='y')) except (NameError, ImportError): while True: _confir = input(msg) @@ -623,7 +581,7 @@ def _ask_user(): # No action needed return else: - warning( + log_interactive.warning( "Scapy has detected that your pcap service is not running !" ) if not conf.interactive or _ask_user(): @@ -631,173 +589,187 @@ def _ask_user(): if succeed: log_loading.info("Pcap service started !") return - warning("Could not start the pcap service ! " - "You probably won't be able to send packets. " - "Deactivating unneeded interfaces and restarting " - "Scapy might help. Check your winpcap/npcap installation " - "and access rights.") - - def load(self): - if not get_if_list(): + log_loading.warning( + "Could not start the pcap service! " + "You probably won't be able to send packets. " + "Check your winpcap/npcap installation " + "and access rights." + ) + + def load(self, NetworkInterface_Win=NetworkInterface_Win): + # type: (type) -> Dict[str, NetworkInterface] + results = {} + if not conf.cache_pcapiflist: # Try a restart - NetworkInterfaceDict._pcap_check() + WindowsInterfacesProvider._pcap_check() + legacy_npcap_guid = None + windows_interfaces = dict() for i in get_windows_if_list(): - try: - interface = NetworkInterface(i) - self.data[interface.guid] = interface - except KeyError: - pass - - # Remove invalid loopback interfaces (not usable) - for key, iface in self.data.copy().items(): - if iface.ip == "127.0.0.1" and iface.is_invalid(): - del self.data[key] + # Only consider interfaces with a GUID + if i['guid']: + if conf.use_npcap: + # Detect the legacy Loopback interface + if i['name'] == NPCAP_LOOPBACK_NAME_LEGACY: + # Legacy Npcap (<0.9983) + legacy_npcap_guid = i['guid'] + elif "Loopback" in i['name']: + # Newer Npcap + i['guid'] = conf.loopback_name + # Map interface + windows_interfaces[i['guid']] = i + + def iterinterfaces() -> Iterator[ + Tuple[str, Optional[str], List[str], int, str, Optional[Dict[str, Any]]] + ]: + if conf.use_pcap: + # We have a libpcap provider: enrich pcap interfaces with + # Windows data + for netw, if_data in conf.cache_pcapiflist.items(): + name, ips, flags, _, _ = if_data + guid = _pcapname_to_guid(netw) + if guid == legacy_npcap_guid: + # Legacy Npcap detected ! + conf.loopback_name = netw + data = windows_interfaces.get(guid, None) + yield netw, name, ips, flags, guid, data + else: + # We don't have a libpcap provider: only use Windows data + for guid, data in windows_interfaces.items(): + netw = r'\Device\NPF_' + guid if guid[0] != '\\' else guid + yield netw, None, [], 0, guid, data - # Replace LOOPBACK_INTERFACE - try: - scapy.consts.LOOPBACK_INTERFACE = self.dev_from_name( - scapy.consts.LOOPBACK_NAME, - ) - except ValueError: - pass - # Support non-windows cards (e.g. Napatech) index = 0 - for pcap_name, if_data in six.iteritems(conf.cache_iflist): - name, _, _ = if_data - guid = _pcapname_to_guid(pcap_name) - if guid not in self.data: + for netw, name, ips, flags, guid, data in iterinterfaces(): + if data: + # Exists in Windows registry + data['network_name'] = netw + data['ips'] = list(set(data['ips'] + ips)) + data['flags'] = flags + else: + # Only in [Wi]npcap index -= 1 - dummy_data = { + data = { 'name': name, - 'description': "[Unknown] %s" % name, - 'win_index': index, + 'description': name, + 'index': index, 'guid': guid, - 'invalid': False, - 'mac': 'ff:ff:ff:ff:ff:ff', + 'network_name': netw, + 'mac': '00:00:00:00:00:00', 'ipv4_metric': 0, 'ipv6_metric': 0, - 'ips': [] + 'ips': ips, + 'flags': flags, + 'nameservers': [], } - # No KeyError will happen here, as we get it from cache - self.data[guid] = NetworkInterface(dummy_data) - - def dev_from_name(self, name): - """Return the first pcap device name for a given Windows - device name. - """ - try: - return next(iface for iface in six.itervalues(self) - if (iface.name == name or iface.description == name)) - except (StopIteration, RuntimeError): - raise ValueError("Unknown network interface %r" % name) - - def dev_from_pcapname(self, pcap_name): - """Return Windows device name for given pcap device name.""" - try: - return next(iface for iface in six.itervalues(self) - if iface.pcap_name == pcap_name) - except (StopIteration, RuntimeError): - raise ValueError("Unknown pypcap network interface %r" % pcap_name) - - def dev_from_index(self, if_index): - """Return interface name from interface index""" - try: - if_index = int(if_index) # Backward compatibility - return next(iface for iface in six.itervalues(self) - if iface.win_index == if_index) - except (StopIteration, RuntimeError): - if str(if_index) == "1": - # Test if the loopback interface is set up - if isinstance(scapy.consts.LOOPBACK_INTERFACE, NetworkInterface): # noqa: E501 - return scapy.consts.LOOPBACK_INTERFACE - raise ValueError("Unknown network interface index %r" % if_index) + # No KeyError will happen here, as we get it from cache + results[netw] = NetworkInterface_Win(self, data) + return results def reload(self): + # type: () -> Dict[str, NetworkInterface] """Reload interface list""" self.restarted_adapter = False - self.data.clear() if conf.use_pcap: # Reload from Winpcapy - from scapy.arch.pcapdnet import load_winpcapy + from scapy.arch.libpcap import load_winpcapy load_winpcapy() - self.load() - # Reload conf.iface - conf.iface = get_working_if() - - def show(self, resolve_mac=True, print_result=True): - """Print list of available network interfaces in human readable form""" - res = [] - for iface_name in sorted(self.data): - dev = self.data[iface_name] - mac = dev.mac - if resolve_mac and conf.manufdb: - mac = conf.manufdb._resolve_MAC(mac) - validity_color = lambda x: conf.color_theme.red if x else \ - conf.color_theme.green - description = validity_color(dev.is_invalid())( - str(dev.description) - ) - index = str(dev.win_index) - res.append((index, description, str(dev.ip), mac)) + return self.load() - res = pretty_list(res, [("INDEX", "IFACE", "IP", "MAC")], sortBy=2) - if print_result: - print(res) + def _l3socket(self, dev, ipv6): + # type: (NetworkInterface, bool) -> Type[SuperSocket] + """Return L3 socket used by interfaces of this provider""" + if ipv6: + return conf.L3socket6 else: - return res + return conf.L3socket - def __repr__(self): - return self.show(print_result=False) +# Register provider +conf.ifaces.register_provider(WindowsInterfacesProvider) -IFACES = ifaces = NetworkInterfaceDict() -IFACES.load() +def get_ips(v6=False): + # type: (bool) -> Dict[NetworkInterface, List[str]] + """Returns all available IPs matching to interfaces, using the windows system. + Should only be used as a WinPcapy fallback. -def pcapname(dev): - """Get the device pcap name by device name or Scapy NetworkInterface + :param v6: IPv6 addresses + """ + res = {} + for iface in conf.ifaces.values(): + if v6: + res[iface] = iface.ips[6] + else: + res[iface] = iface.ips[4] + return res + +def get_ip_from_name(ifname, v6=False): + # type: (str, bool) -> str + """Backward compatibility: indirectly calls get_ips + Deprecated. """ - if isinstance(dev, NetworkInterface): - if dev.is_invalid(): - return None - return dev.pcap_name - try: - return IFACES.dev_from_name(dev).pcap_name - except ValueError: - return IFACES.dev_from_pcapname(dev).pcap_name + warnings.warn( + "get_ip_from_name is deprecated. Use the `ip` attribute of the iface " + "or use get_ips() to get all ips per interface.", + DeprecationWarning + ) + iface = conf.ifaces.dev_from_name(ifname) + return get_ips(v6=v6).get(iface, [""])[0] + + +def pcap_service_name(): + # type: () -> str + """Return the pcap adapter service's name""" + return "npcap" if conf.use_npcap else "npf" + + +def pcap_service_status(): + # type: () -> bool + """Returns whether the windows pcap adapter is running or not""" + status = get_service_status(pcap_service_name()) + return status["dwCurrentState"] == 4 -def dev_from_pcapname(pcap_name): - """Return Scapy device name for given pcap device name""" - return IFACES.dev_from_pcapname(pcap_name) +def _pcap_service_control(action, askadmin=True): + # type: (str, bool) -> bool + """Internal util to run pcap control command""" + command = action + ' ' + pcap_service_name() + res, code = _exec_cmd(_encapsulate_admin(command) if askadmin else command) + if code != 0: + warning(res.decode("utf8", errors="ignore")) + return (code == 0) -def dev_from_index(if_index): - """Return Windows adapter name for given Windows interface index""" - return IFACES.dev_from_index(if_index) +def pcap_service_start(askadmin=True): + # type: (bool) -> bool + """Starts the pcap adapter. Will ask for admin. Returns True if success""" + return _pcap_service_control('sc start', askadmin=askadmin) -def show_interfaces(resolve_mac=True): - """Print list of available network interfaces""" - return IFACES.show(resolve_mac) +def pcap_service_stop(askadmin=True): + # type: (bool) -> bool + """Stops the pcap adapter. Will ask for admin. Returns True if success""" + return _pcap_service_control('sc stop', askadmin=askadmin) if conf.use_pcap: - _orig_open_pcap = pcapdnet.open_pcap + _orig_open_pcap = libpcap.open_pcap - def open_pcap(iface, *args, **kargs): + def open_pcap(device, # type: Union[str, NetworkInterface] + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> libpcap._PcapWrapper_libpcap """open_pcap: Windows routine for creating a pcap from an interface. This function is also responsible for detecting monitor mode. """ - iface_pcap_name = pcapname(iface) - if not isinstance(iface, NetworkInterface) and \ - iface_pcap_name is not None: - iface = IFACES.dev_from_name(iface) - if iface is None or iface.is_invalid(): + iface = cast(NetworkInterface_Win, resolve_iface(device)) + iface_network_name = iface.network_name + if not iface: raise Scapy_Exception( - "Interface is invalid (no pcap match found) !" + "Interface is invalid (no pcap match found)!" ) # Only check monitor mode when manually specified. # Checking/setting for monitor mode will slow down the process, and the @@ -809,42 +781,60 @@ def open_pcap(iface, *args, **kargs): # The monitor param is specified, and not matching the current # interface state iface.setmonitor(kw_monitor) - return _orig_open_pcap(iface_pcap_name, *args, **kargs) - pcapdnet.open_pcap = open_pcap - -get_if_raw_hwaddr = pcapdnet.get_if_raw_hwaddr = lambda iface, *args, **kargs: ( # noqa: E501 - ARPHDR_ETHER, mac2str(IFACES.dev_from_pcapname(pcapname(iface)).mac) -) + return _orig_open_pcap(iface_network_name, *args, **kargs) + libpcap.open_pcap = open_pcap # type: ignore def _read_routes_c_v1(): + # type: () -> List[Tuple[int, int, str, str, str, int]] """Retrieve Windows routes through a GetIpForwardTable call. This is compatible with XP but won't get IPv6 routes.""" def _extract_ip(obj): + # type: (int) -> str return inet_ntop(socket.AF_INET, struct.pack(" int + if WINDOWS_XP: + return struct.unpack("I", ip))[0] + return ip routes = [] for route in GetIpForwardTable(): ifIndex = route['ForwardIfIndex'] - dest = route['ForwardDest'] - netmask = route['ForwardMask'] + dest = _proc(route['ForwardDest']) + netmask = _proc(route['ForwardMask']) nexthop = _extract_ip(route['ForwardNextHop']) metric = route['ForwardMetric1'] # Build route try: - iface = dev_from_index(ifIndex) - if iface.ip == "0.0.0.0": + iface = cast(NetworkInterface_Win, dev_from_index(ifIndex)) + if not iface.ip or iface.ip == "0.0.0.0": continue except ValueError: continue ip = iface.ip + netw = network_name(iface) # RouteMetric + InterfaceMetric metric = metric + iface.ipv4_metric - routes.append((dest, netmask, nexthop, iface, ip, metric)) + routes.append((dest, netmask, nexthop, netw, ip, metric)) return routes -def _read_routes_c(ipv6=False): +@overload +def _read_routes_c(ipv6): # noqa: F811 + # type: (Literal[True]) -> List[Tuple[str, int, str, str, List[str], int]] + pass + + +@overload +def _read_routes_c(ipv6=False): # noqa: F811 + # type: (Literal[False]) -> List[Tuple[int, int, str, str, str, int]] + pass + + +def _read_routes_c(ipv6=False): # noqa: F811 + # type: (bool) -> Union[List[Tuple[int, int, str, str, str, int]], List[Tuple[str, int, str, str, List[str], int]]] # noqa: E501 """Retrieve Windows routes through a GetIpForwardTable2 call. This is not available on Windows XP !""" @@ -852,58 +842,54 @@ def _read_routes_c(ipv6=False): sock_addr_name = 'Ipv6' if ipv6 else 'Ipv4' sin_addr_name = 'sin6_addr' if ipv6 else 'sin_addr' metric_name = 'ipv6_metric' if ipv6 else 'ipv4_metric' - ip_len = 16 if ipv6 else 4 if ipv6: lifaddr = in6_getifaddr() - routes = [] + routes = [] # type: List[Any] - def _extract_ip_netmask(obj): + def _extract_ip(obj): + # type: (Dict[str, Any]) -> str ip = obj[sock_addr_name][sin_addr_name] ip = bytes(bytearray(ip['byte'])) - # Extract netmask - netmask = (ip_len - (len(ip) - len(ip.rstrip(b"\x00")))) * 8 # Build IP - ip = inet_ntop(af, ip) - return ip, netmask + return inet_ntop(af, ip) for route in GetIpForwardTable2(af): # Extract data ifIndex = route['InterfaceIndex'] - _dest = route['DestinationPrefix'] - dest, netmask = _extract_ip_netmask(_dest['Prefix']) - nexthop, _ = _extract_ip_netmask(route['NextHop']) + dest = _extract_ip(route['DestinationPrefix']['Prefix']) + netmask = route['DestinationPrefix']['PrefixLength'] + nexthop = _extract_ip(route['NextHop']) metric = route['Metric'] # Build route try: iface = dev_from_index(ifIndex) - if iface.ip == "0.0.0.0": + if not iface.ip or iface.ip == "0.0.0.0": continue except ValueError: continue ip = iface.ip + netw = network_name(iface) # RouteMetric + InterfaceMetric metric = metric + getattr(iface, metric_name) if ipv6: _append_route6(routes, dest, netmask, nexthop, - iface, lifaddr, metric) + netw, lifaddr, metric) else: routes.append((atol(dest), itom(int(netmask)), - nexthop, iface, ip, metric)) + nexthop, netw, ip, metric)) return routes def read_routes(): + # type: () -> List[Tuple[int, int, str, str, str, int]] routes = [] try: if WINDOWS_XP: routes = _read_routes_c_v1() else: - routes = _read_routes_c(False) + routes = _read_routes_c(ipv6=False) except Exception as e: - warning("Error building scapy IPv4 routing table : %s", e) - else: - if not routes: - warning("No default IPv4 routes found. Your Windows release may no be supported and you have to enter your routes manually") # noqa: E501 + log_loading.warning("Error building scapy IPv4 routing table : %s", e) return routes @@ -913,25 +899,33 @@ def read_routes(): def in6_getifaddr(): + # type: () -> List[Tuple[str, int, str]] """ Returns all IPv6 addresses found on the computer """ - ifaddrs = [] + ifaddrs = [] # type: List[Tuple[str, int, str]] ip6s = get_ips(v6=True) - for iface in ip6s: - ips = ip6s[iface] + for iface, ips in ip6s.items(): for ip in ips: scope = in6_getscope(ip) - ifaddrs.append((ip, scope, iface)) + ifaddrs.append((ip, scope, iface.network_name)) # Appends Npcap loopback if available - if conf.use_npcap and scapy.consts.LOOPBACK_INTERFACE: - ifaddrs.append(("::1", 0, scapy.consts.LOOPBACK_INTERFACE)) + if conf.use_npcap and conf.loopback_name: + ifaddrs.append(("::1", 0, conf.loopback_name)) return ifaddrs -def _append_route6(routes, dpref, dp, nh, iface, lifaddr, metric): +def _append_route6(routes, # type: List[Tuple[str, int, str, str, List[str], int]] + dpref, # type: str + dp, # type: int + nh, # type: str + iface, # type: str + lifaddr, # type: List[Tuple[str, int, str]] + metric, # type: int + ): + # type: (...) -> None cset = [] # candidate set (possible source addresses) - if iface.name == scapy.consts.LOOPBACK_NAME: + if iface == conf.loopback_name: if dpref == '::': return cset = ['::1'] @@ -940,106 +934,75 @@ def _append_route6(routes, dpref, dp, nh, iface, lifaddr, metric): cset = construct_source_candidate_set(dpref, dp, devaddrs) if not cset: return - # APPEND (DESTINATION, NETMASK, NEXT HOP, IFACE, CANDIDATS, METRIC) + # APPEND (DESTINATION, NETMASK, NEXT HOP, IFACE, CANDIDATES, METRIC) routes.append((dpref, dp, nh, iface, cset, metric)) def read_routes6(): + # type: () -> List[Tuple[str, int, str, str, List[str], int]] routes6 = [] if WINDOWS_XP: return routes6 try: routes6 = _read_routes_c(ipv6=True) except Exception as e: - warning("Error building scapy IPv6 routing table : %s", e) + log_loading.warning("Error building scapy IPv6 routing table : %s", e) return routes6 -def get_working_if(): - """Return an interface that works""" - try: - # return the interface associated with the route with smallest - # mask (route by default if it exists) - iface = min(conf.route.routes, key=lambda x: x[1])[3] - except ValueError: - # no route - iface = scapy.consts.LOOPBACK_INTERFACE - if iface.is_invalid(): - # Backup mode: try them all - for iface in six.itervalues(IFACES): - if not iface.is_invalid(): - return iface - return None - return iface - - -def _get_valid_guid(): - if scapy.consts.LOOPBACK_INTERFACE: - return scapy.consts.LOOPBACK_INTERFACE.guid - else: - return next((i.guid for i in six.itervalues(IFACES) - if not i.is_invalid()), None) - - -def route_add_loopback(routes=None, ipv6=False, iflist=None): +def _route_add_loopback(routes=None, # type: Optional[List[Any]] + ipv6=False, # type: bool + iflist=None, # type: Optional[List[str]] + ): + # type: (...) -> None """Add a route to 127.0.0.1 and ::1 to simplify unit tests on Windows""" if not WINDOWS: - warning("Not available") + warning("Calling _route_add_loopback is only valid on Windows") return warning("This will completely mess up the routes. Testing purpose only !") - # Add only if some adpaters already exist + # Add only if some adapters already exist if ipv6: if not conf.route6.routes: return else: if not conf.route.routes: return - data = { - 'name': scapy.consts.LOOPBACK_NAME, - 'description': "Loopback", - 'win_index': -1, - 'guid': "{0XX00000-X000-0X0X-X00X-00XXXX000XXX}", - 'invalid': True, - 'mac': '00:00:00:00:00:00', - 'ipv4_metric': 0, - 'ipv6_metric': 0, - 'ips': ["127.0.0.1", "::"] - } - adapter = NetworkInterface() - adapter.pcap_name = "\\Device\\NPF_{0XX00000-X000-0X0X-X00X-00XXXX000XXX}" - adapter.update(data) - adapter.invalid = False - adapter.ip = "127.0.0.1" + conf.ifaces._add_fake_iface(conf.loopback_name) + adapter = conf.ifaces.dev_from_name(conf.loopback_name) if iflist: - iflist.append(adapter.pcap_name) + iflist.append(adapter.network_name) return - # Remove all LOOPBACK_NAME routes + # Remove all conf.loopback_name routes for route in list(conf.route.routes): iface = route[3] - if iface.name == scapy.consts.LOOPBACK_NAME: + if iface == conf.loopback_name: conf.route.routes.remove(route) - # Remove LOOPBACK_NAME interface - for devname, iface in list(IFACES.items()): - if iface.name == scapy.consts.LOOPBACK_NAME: - IFACES.pop(devname) + # Remove conf.loopback_name interface + for devname, ifname in list(conf.ifaces.items()): + if ifname == conf.loopback_name: + conf.ifaces.pop(devname) # Inject interface - IFACES["{0XX00000-X000-0X0X-X00X-00XXXX000XXX}"] = adapter - scapy.consts.LOOPBACK_INTERFACE = adapter + conf.ifaces[r"\Device\NPF_{0XX00000-X000-0X0X-X00X-00XXXX000XXX}"] = adapter + conf.loopback_name = adapter.network_name if isinstance(conf.iface, NetworkInterface): - if conf.iface.name == scapy.consts.LOOPBACK_NAME: + if conf.iface.network_name == conf.loopback_name: conf.iface = adapter - if isinstance(conf.iface6, NetworkInterface): - if conf.iface6.name == scapy.consts.LOOPBACK_NAME: - conf.iface6 = adapter - conf.netcache.arp_cache["127.0.0.1"] = "ff:ff:ff:ff:ff:ff" - conf.netcache.in6_neighbor["::1"] = "ff:ff:ff:ff:ff:ff" + conf.netcache.arp_cache["127.0.0.1"] = "ff:ff:ff:ff:ff:ff" # type: ignore + conf.netcache.in6_neighbor["::1"] = "ff:ff:ff:ff:ff:ff" # type: ignore # Build the packed network addresses loop_net = struct.unpack("!I", socket.inet_aton("127.0.0.0"))[0] loop_mask = struct.unpack("!I", socket.inet_aton("255.0.0.0"))[0] # Build the fake routes - loopback_route = (loop_net, loop_mask, "0.0.0.0", adapter, "127.0.0.1", 1) - loopback_route6 = ('::1', 128, '::', adapter, ["::1"], 1) - loopback_route6_custom = ("fe80::", 128, "::", adapter, ["::1"], 1) + loopback_route = ( + loop_net, + loop_mask, + "0.0.0.0", + adapter.network_name, + "127.0.0.1", + 1 + ) + loopback_route6 = ('::1', 128, '::', adapter.network_name, ["::1"], 1) + loopback_route6_custom = ("fe80::", 128, "::", adapter.network_name, ["::1"], 1) if routes is None: # Injection conf.route6.routes.append(loopback_route6) @@ -1060,8 +1023,24 @@ class _NotAvailableSocket(SuperSocket): desc = "wpcap.dll missing" def __init__(self, *args, **kargs): + # type: (*Any, **Any) -> None raise RuntimeError( "Sniffing and sending packets is not available at layer 2: " - "winpcap is not installed. You may use conf.L3socket or" + "winpcap is not installed. You may use conf.L3socket or " "conf.L3socket6 to access layer 3" ) + + +####### +# DNS # +####### + +def read_nameservers() -> List[str]: + """Return the nameservers configured by the OS (on the default interface) + """ + # Windows has support for different DNS servers on each network interface, + # but to be cross-platform we only return the servers for the default one. + if isinstance(conf.iface, NetworkInterface_Win): + return conf.iface.nameservers + else: + return [] diff --git a/scapy/arch/windows/native.py b/scapy/arch/windows/native.py index 18a3ea54f15..b2b457da615 100644 --- a/scapy/arch/windows/native.py +++ b/scapy/arch/windows/native.py @@ -1,126 +1,122 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter """ Native Microsoft Windows sockets (L3 only) -## Notice: ICMP packets - -DISCLAIMER: Please use Npcap/Winpcap to send/receive ICMP. It is going to work. -Below is some additional information, mainly implemented in a testing purpose. - -When in native mode, everything goes through the Windows kernel. -This firstly requires that the Firewall is open. Be sure it allows ICMPv4/6 -packets in and out. -Windows may drop packets that it finds wrong. for instance, answers to -ICMP packets with id=0 or seq=0 may be dropped. It means that sent packets -should (most of the time) be perfectly built. - -A perfectly built ICMP req packet on Windows means that its id is 1, its -checksum (IP and ICMP) are correctly built, but also that its seq number is -in the "allowed range". - In fact, every time an ICMP packet is sent on Windows, a global sequence -number is increased, which is only reset at boot time. The seq number of the -received ICMP packet must be in the range [current, current + 3] to be valid, -and received by the socket. The current number is quite hard to get, thus we -provide in this module the get_actual_icmp_seq() function. - -Example: - >>> conf.use_pcap = False - >>> a = conf.L3socket() - # This will (most likely) work: - >>> current = get_current_icmp_seq() - >>> a.sr(IP(dst="www.google.com", ttl=128)/ICMP(id=1, seq=current)) - # This won't: - >>> a.sr(IP(dst="www.google.com", ttl=128)/ICMP()) - -PS: on computers where the firewall isn't open, Windows temporarily opens it -when using the `ping` util from cmd.exe. One can first call a ping on cmd, -then do custom calls through the socket using get_current_icmp_seq(). See -the tests (windows.uts) for an example. +This uses Raw Sockets from winsock +https://learn.microsoft.com/en-us/windows/win32/winsock/tcp-ip-raw-sockets-2 + +.. note:: + + Don't use this module. + It is a proof of concept, and a worse-case-scenario failover, but you should + consider that raw sockets on Windows don't work and install Npcap to avoid using + it at all cost. """ import io -import os import socket -import subprocess +import struct import time -from scapy.automaton import SelectableObject -from scapy.arch.common import _select_nonblock -from scapy.arch.windows.structures import GetIcmpStatistics +from scapy.automaton import select_objects from scapy.compat import raw from scapy.config import conf from scapy.data import MTU -from scapy.error import Scapy_Exception, warning +from scapy.error import Scapy_Exception, log_runtime +from scapy.packet import Packet +from scapy.interfaces import resolve_iface, _GlobInterfaceType from scapy.supersocket import SuperSocket +# Typing imports +from typing import ( + Any, + List, + Optional, + Tuple, + Type, +) + # Watch out for import loops (inet...) -class L3WinSocket(SuperSocket, SelectableObject): +class L3WinSocket(SuperSocket): + """ + A L3 raw socket implementation native to Windows. + + Official "Windows Limitations" from MSDN: + - TCP data cannot be sent over raw sockets. + - UDP datagrams with an invalid source address cannot be sent over raw sockets. + - For IPv6 (address family of AF_INET6), an application receives everything + after the last IPv6 header in each received datagram [...]. The application + does not receive any IPv6 headers using a raw socket. + + Unofficial limitations: + - Turns out we actually don't see any incoming TCP data, only the outgoing. + We do properly see UDP, ICMP, etc. both ways though. + - To match IPv6 responses, one must use `conf.checkIPaddr = False` as we can't + get the real destination. + + **To overcome those limitations, install Npcap.** + """ desc = "a native Layer 3 (IPv4) raw socket under Windows" nonblocking_socket = True - __slots__ = ["promisc", "cls", "ipv6", "proto"] - - def __init__(self, iface=None, proto=socket.IPPROTO_IP, - ttl=128, ipv6=False, promisc=True, **kwargs): + __selectable_force_select__ = True # see automaton.py + __slots__ = ["promisc", "cls", "ipv6"] + + def __init__(self, + iface=None, # type: Optional[_GlobInterfaceType] + ttl=128, # type: int + ipv6=False, # type: bool + promisc=True, # type: bool + **kwargs # type: Any + ): + # type: (...) -> None from scapy.layers.inet import IP from scapy.layers.inet6 import IPv6 for kwarg in kwargs: - warning("Dropping unsupported option: %s" % kwarg) + log_runtime.warning("Dropping unsupported option: %s" % kwarg) + self.iface = iface and resolve_iface(iface) or conf.iface + if not self.iface.is_valid(): + log_runtime.warning("Interface is invalid. This will fail.") af = socket.AF_INET6 if ipv6 else socket.AF_INET - self.proto = proto - if ipv6: - from scapy.arch import get_if_addr6 - self.host_ip6 = get_if_addr6(conf.iface) or "::1" - if proto == socket.IPPROTO_IP: - # We'll restrict ourselves to UDP, as TCP isn't bindable - # on AF_INET6 - self.proto = socket.IPPROTO_UDP - # On Windows, with promisc=False, you won't get much self.ipv6 = ipv6 self.cls = IPv6 if ipv6 else IP + # Promisc + if promisc is None: + promisc = conf.sniff_promisc self.promisc = promisc # Notes: - # - IPPROTO_RAW only works to send packets. + # - IPPROTO_RAW is broken. We don't use it. # - IPPROTO_IPV6 exists in MSDN docs, but using it will result in # no packets being received. Same for its options (IPV6_HDRINCL...) # However, using IPPROTO_IP with AF_INET6 will still receive # the IPv6 packets try: - self.ins = socket.socket(af, - socket.SOCK_RAW, - self.proto) - self.outs = socket.socket(af, - socket.SOCK_RAW, - socket.IPPROTO_RAW) + # Listening on AF_INET6 IPPROTO_IPV6 is broken. Use IPPROTO_IP + self.outs = self.ins = socket.socket( + af, + socket.SOCK_RAW, + socket.IPPROTO_IP, + ) except OSError as e: - if e.errno == 10013: + if e.errno == 13: raise OSError("Windows native L3 Raw sockets are only " "usable as administrator ! " - "Install Winpcap/Npcap to workaround !") + "Please install Npcap to workaround !") raise self.ins.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.outs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.ins.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2**30) self.outs.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2**30) - # IOCTL Include IP headers - self.ins.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) - self.outs.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) # set TTL self.ins.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) - self.outs.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) - # Bind on all ports - iface = iface or conf.iface - host = iface.ip if iface.ip else socket.gethostname() - self.ins.bind((host, 0)) - self.ins.setblocking(False) # Get as much data as possible: reduce what is cropped if ipv6: + # IPV6_HDRINCL is broken. Use IP_HDRINCL even on IPv6 + self.outs.setsockopt(socket.IPPROTO_IPV6, socket.IP_HDRINCL, 1) try: # Not all Windows versions self.ins.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_RECVTCLASS, 1) @@ -129,29 +125,58 @@ def __init__(self, iface=None, proto=socket.IPPROTO_IP, except (OSError, socket.error): pass else: + # IOCTL Include IP headers + self.ins.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) try: # Not Windows XP self.ins.setsockopt(socket.IPPROTO_IP, socket.IP_RECVDSTADDR, 1) except (OSError, socket.error): pass try: # Windows 10+ recent builds only - self.ins.setsockopt(socket.IPPROTO_IP, socket.IP_RECVTTL, 1) + self.ins.setsockopt( + socket.IPPROTO_IP, + socket.IP_RECVTTL, # type: ignore + 1 + ) except (OSError, socket.error): pass + # Bind on all ports + if ipv6: + from scapy.arch import get_if_addr6 + host = get_if_addr6(self.iface) + else: + from scapy.arch import get_if_addr + host = get_if_addr(self.iface) + self.ins.bind((host or socket.gethostname(), 0)) + # self.ins.setblocking(False) + # Set promisc if promisc: # IOCTL Receive all packets self.ins.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) def send(self, x): + # type: (Packet) -> int data = raw(x) if self.cls not in x: raise Scapy_Exception("L3WinSocket can only send IP/IPv6 packets !" " Install Npcap/Winpcap to send more") + from scapy.layers.inet import TCP + if TCP in x: + raise Scapy_Exception( + "'TCP data cannot be sent over raw socket': " + "https://learn.microsoft.com/en-us/windows/win32/winsock/tcp-ip-raw-sockets-2" # noqa: E501 + ) + if not self.outs: + raise Scapy_Exception("Socket not created") dst_ip = str(x[self.cls].dst) - self.outs.sendto(data, (dst_ip, 0)) + return self.outs.sendto(data, (dst_ip, 0)) def nonblock_recv(self, x=MTU): - return self.recv() + # type: (int) -> Optional[Packet] + try: + return self.recv() + except IOError: + return None # https://docs.microsoft.com/en-us/windows/desktop/winsock/tcp-ip-raw-sockets-2 # noqa: E501 # - For IPv4 (address family of AF_INET), an application receives the IP @@ -163,57 +188,56 @@ def nonblock_recv(self, x=MTU): # not receive any IPv6 headers using a raw socket. def recv_raw(self, x=MTU): + # type: (int) -> Tuple[Type[Packet], bytes, float] try: data, address = self.ins.recvfrom(x) except io.BlockingIOError: - return None, None, None - from scapy.layers.inet import IP - from scapy.layers.inet6 import IPv6 + return None, None, None # type: ignore if self.ipv6: + from scapy.layers.inet6 import IPv6 # AF_INET6 does not return the IPv6 header. Let's build it # (host, port, flowinfo, scopeid) host, _, flowinfo, _ = address - header = raw(IPv6(src=host, - dst=self.host_ip6, - fl=flowinfo, - nh=self.proto, # fixed for AF_INET6 - plen=len(data))) + # We have to guess what the proto is. Ugly heuristics ahead :( + # Waiting for https://github.com/python/cpython/issues/80398 + if len(data) > 6 and struct.unpack("!H", data[4:6])[0] == len(data): + proto = socket.IPPROTO_UDP + elif data and data[0] in range(128, 138): # ugh + proto = socket.IPPROTO_ICMPV6 + else: + proto = socket.IPPROTO_TCP + header = raw( + IPv6( + src=host, + dst="::", + fl=flowinfo, + nh=proto or 0xFF, + plen=len(data) + ) + ) return IPv6, header + data, time.time() else: + from scapy.layers.inet import IP return IP, data, time.time() - def check_recv(self): - return True - def close(self): - if not self.closed and self.promisc: + # type: () -> None + if not self.closed and self.promisc and hasattr(self, 'ins'): self.ins.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF) super(L3WinSocket, self).close() @staticmethod def select(sockets, remain=None): - return _select_nonblock(sockets, remain=remain) + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + return select_objects(sockets, remain) class L3WinSocket6(L3WinSocket): desc = "a native Layer 3 (IPv6) raw socket under Windows" def __init__(self, **kwargs): - super(L3WinSocket6, self).__init__(ipv6=True, **kwargs) - - -def open_icmp_firewall(host): - """Temporarily open the ICMP firewall. Tricks Windows into allowing - ICMP packets for a short period of time (~ 1 minute)""" - # We call ping with a timeout of 1ms: will return instantly - with open(os.devnull, 'wb') as DEVNULL: - return subprocess.Popen("ping -4 -w 1 -n 1 %s" % host, - shell=True, - stdout=DEVNULL, - stderr=DEVNULL).wait() - - -def get_current_icmp_seq(): - """See help(scapy.arch.windows.native) for more information. - Returns the current ICMP seq number.""" - return GetIcmpStatistics()['stats']['icmpOutStats']['dwEchos'] + # type: (**Any) -> None + super(L3WinSocket6, self).__init__( + ipv6=True, + **kwargs, + ) diff --git a/scapy/arch/windows/sspi.py b/scapy/arch/windows/sspi.py new file mode 100644 index 00000000000..8567d534d80 --- /dev/null +++ b/scapy/arch/windows/sspi.py @@ -0,0 +1,1000 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +SSP for implicit authentication on Windows +""" + +import ctypes +import ctypes.wintypes +import enum + +from scapy.layers.gssapi import ( + GSS_C_FLAGS, + GSS_S_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GssChannelBindings, + GSSAPI_BLOB, + GSSAPI_BLOB_SIGNATURE, + SSP, + GSS_S_BAD_NAME, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_DEFECTIVE_CREDENTIAL, + GSS_S_DEFECTIVE_TOKEN, + GSS_S_FAILURE, + GSS_S_UNAUTHORIZED, + GSS_S_UNAVAILABLE, +) + +# Typing imports +from typing import ( + Optional, + List, +) + +# Windows bindings + +SECPKG_CRED_INBOUND = 0x00000001 +SECPKG_CRED_OUTBOUND = 0x00000002 +SECPKG_CRED_BOTH = 0x00000003 + +SECPKG_ATTR_SIZES = 0 +SECPKG_ATTR_SESSION_KEY = 9 +SECPKG_ATTR_SERVER_FLAGS = 14 + + +class SecPkgContext_SessionKey(ctypes.Structure): + _fields_ = [ + ("SessionKeyLength", ctypes.wintypes.ULONG), + ("SessionKey", ctypes.wintypes.LPBYTE), + ] + + +class SecPkgContext_Flags(ctypes.Structure): + _fields_ = [ + ("Flags", ctypes.wintypes.ULONG), + ] + + +class SecPkgContext_Sizes(ctypes.Structure): + _fields_ = [ + ("cbMaxToken", ctypes.wintypes.ULONG), + ("cbMaxSignature", ctypes.wintypes.ULONG), + ("cbBlockSize", ctypes.wintypes.ULONG), + ("cbSecurityTrailer", ctypes.wintypes.ULONG), + ] + + +class SEC_CHANNEL_BINDINGS(ctypes.Structure): + _fields_ = [ + ("dwInitiatorAddrType", ctypes.wintypes.ULONG), + ("cbInitiatorLength", ctypes.wintypes.ULONG), + ("dwInitiatorOffset", ctypes.wintypes.ULONG), + ("dwAcceptorAddrType", ctypes.wintypes.ULONG), + ("cbAcceptorLength", ctypes.wintypes.ULONG), + ("dwAcceptorOffset", ctypes.wintypes.ULONG), + ("cbApplicationDataLength", ctypes.wintypes.ULONG), + ("dwApplicationDataOffset", ctypes.wintypes.ULONG), + ] + + @classmethod + def from_GSS(cls, bindings: GssChannelBindings): + """ + Convert a GssChannelBindings to SecPkgContext_Bindings + """ + # Initialize structure + buffer = ctypes.create_string_buffer( + ctypes.sizeof(SEC_CHANNEL_BINDINGS) + + len(bindings.initiator_address.value) + + len(bindings.acceptor_address.value) + + len(bindings.application_data.value) + ) + Bindings = ctypes.cast( + ctypes.byref(buffer), + ctypes.POINTER(SEC_CHANNEL_BINDINGS), + ) + + # Populate values with the offsets and lengths + offset = ctypes.sizeof(SEC_CHANNEL_BINDINGS) + Bindings.contents.dwInitiatorAddrType = bindings.initiator_addrtype + if bindings.initiator_address.value: + lgth = len(bindings.initiator_address.value) + Bindings.contents.cbInitiatorLength = lgth + Bindings.contents.dwInitiatorOffset = offset + buffer[offset : offset + lgth] = bindings.initiator_address.value + offset += lgth + Bindings.contents.dwAcceptorAddrType = bindings.acceptor_addrtype + if bindings.acceptor_address.value: + lgth = len(bindings.acceptor_address.value) + Bindings.contents.cbAcceptorLength = lgth + Bindings.contents.dwAcceptorOffset = offset + buffer[offset : offset + lgth] = bindings.acceptor_address.value + offset += lgth + if bindings.application_data.value: + lgth = len(bindings.application_data.value) + Bindings.contents.cbApplicationDataLength = lgth + Bindings.contents.dwApplicationDataOffset = offset + buffer[offset : offset + lgth] = bindings.application_data.value + offset += lgth + + return buffer, offset + + +SECURITY_NETWORK_DREP = 0 + + +class SEC_CODES(enum.IntEnum): + """ + Windows sspi.h return codes + """ + + SEC_E_OK = 0x00000000 + SEC_I_CONTINUE_NEEDED = 0x00090312 + SEC_I_COMPLETE_AND_CONTINUE = 0x00090314 + SEC_E_INSUFFICIENT_MEMORY = 0x80090300 + SEC_E_INTERNAL_ERROR = 0x80090304 + SEC_E_INVALID_HANDLE = 0x80090301 + SEC_E_INVALID_TOKEN = 0x80090308 + SEC_E_LOGON_DENIED = 0x8009030C + SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311 + SEC_E_NO_CREDENTIALS = 0x8009030E + SEC_E_TARGET_UNKNOWN = 0x80090303 + SEC_E_UNSUPPORTED_FUNCTION = 0x80090302 + SEC_E_WRONG_PRINCIPAL = 0x80090322 + + @staticmethod + def to_GSS(code: int): + if code in _GSS_REG_TRANSLATION: + return _GSS_REG_TRANSLATION[code] + else: + return code + + +_GSS_REG_TRANSLATION = { + SEC_CODES.SEC_E_OK: GSS_S_COMPLETE, + SEC_CODES.SEC_I_CONTINUE_NEEDED: GSS_S_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE: GSS_S_CONTINUE_NEEDED, + SEC_CODES.SEC_E_INSUFFICIENT_MEMORY: GSS_S_FAILURE, + SEC_CODES.SEC_E_INTERNAL_ERROR: GSS_S_FAILURE, + SEC_CODES.SEC_E_INVALID_HANDLE: GSS_S_DEFECTIVE_CREDENTIAL, + SEC_CODES.SEC_E_INVALID_TOKEN: GSS_S_DEFECTIVE_TOKEN, + SEC_CODES.SEC_E_LOGON_DENIED: GSS_S_UNAUTHORIZED, + SEC_CODES.SEC_E_NO_AUTHENTICATING_AUTHORITY: GSS_S_UNAVAILABLE, + SEC_CODES.SEC_E_NO_CREDENTIALS: GSS_S_DEFECTIVE_CREDENTIAL, + SEC_CODES.SEC_E_TARGET_UNKNOWN: GSS_S_BAD_NAME, + SEC_CODES.SEC_E_UNSUPPORTED_FUNCTION: GSS_S_UNAVAILABLE, + SEC_CODES.SEC_E_WRONG_PRINCIPAL: GSS_S_BAD_NAME, +} + + +class SECURITY_INTEGER(ctypes.Structure): + _fields_ = [ + ("LowPart", ctypes.wintypes.ULONG), + ("HighPart", ctypes.wintypes.LONG), + ] + + +class SecHandle(ctypes.Structure): + _fields_ = [ + ("dwLower", ctypes.POINTER(ctypes.wintypes.ULONG)), + ("dwUpper", ctypes.POINTER(ctypes.wintypes.ULONG)), + ] + + +_winapi_AcquireCredentialsHandle = ctypes.windll.secur32.AcquireCredentialsHandleW +_winapi_AcquireCredentialsHandle.restype = ctypes.wintypes.DWORD +_winapi_AcquireCredentialsHandle.argtypes = [ + ctypes.wintypes.LPWSTR, # pszPrincipal + ctypes.wintypes.LPWSTR, # pszPackage + ctypes.wintypes.ULONG, # fCredentialUse + ctypes.c_void_p, # pvLogonID + ctypes.c_void_p, # pAuthData + ctypes.c_void_p, # pGetKeyFn + ctypes.c_void_p, # pvGetKeyArgument + ctypes.POINTER(SecHandle), # phCredential, + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + + +class SecBuffer(ctypes.Structure): + _fields_ = [ + ("cbBuffer", ctypes.wintypes.ULONG), + ("BufferType", ctypes.wintypes.ULONG), + ("pvBuffer", ctypes.c_void_p), + ] + + def GetData(self): + if self.cbBuffer == 0: + return b"" + buf = ctypes.cast( + self.pvBuffer, + ctypes.POINTER(ctypes.wintypes.BYTE * self.cbBuffer), + ) + return bytes(buf.contents) + + +SECBUFFER_VERSION = 0 +SECBUFFER_DATA = 1 +SECBUFFER_TOKEN = 2 +SECBUFFER_READONLY = 0x80000000 +SECBUFFER_CHANNEL_BINDINGS = 14 + + +class SecBufferDesc(ctypes.Structure): + _fields_ = [ + ("ulVersion", ctypes.wintypes.ULONG), + ("cBuffers", ctypes.wintypes.ULONG), + ("pBuffers", ctypes.POINTER(ctypes.POINTER(SecBuffer))), + ] + + @staticmethod + def Create(Buffers: List[SecBuffer]): + Buffers = ctypes.ARRAY(SecBuffer, len(Buffers))(*Buffers) + Output = SecBufferDesc( + SECBUFFER_VERSION, + len(Buffers), + ctypes.cast( + ctypes.byref(Buffers), ctypes.POINTER(ctypes.POINTER(SecBuffer)) + ), + ) + return Buffers, Output + + @staticmethod + def ParseBuffer(Buffers: ctypes.ARRAY, BufferType: int, cls): + for Buffer in Buffers: + if Buffer.BufferType == BufferType: + return cls(Buffer.GetData()) + return None + + +_winapi_InitializeSecurityContext = ctypes.windll.secur32.InitializeSecurityContextW +_winapi_InitializeSecurityContext.restype = ctypes.wintypes.DWORD +_winapi_InitializeSecurityContext.argtypes = [ + ctypes.POINTER(SecHandle), # phCredential + ctypes.POINTER(SecHandle), # phContext (NULL on first call) + ctypes.wintypes.LPCWSTR, # pszTargetName + ctypes.wintypes.ULONG, # fContextReq + ctypes.wintypes.ULONG, # Reserved1 (must be 0) + ctypes.wintypes.ULONG, # TargetDataRep (e.g. SECURITY_NATIVE_DREP) + ctypes.POINTER(SecBufferDesc), # pInput (can be NULL) + ctypes.wintypes.ULONG, # Reserved2 (must be 0) + ctypes.POINTER(SecHandle), # phNewContext + ctypes.POINTER(SecBufferDesc), # pOutput + ctypes.POINTER(ctypes.wintypes.ULONG), # pfContextAttr + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + +_winapi_AcceptSecurityContext = ctypes.windll.secur32.AcceptSecurityContext +_winapi_AcceptSecurityContext.restype = ctypes.wintypes.DWORD +_winapi_AcceptSecurityContext.argtypes = [ + ctypes.POINTER(SecHandle), # phCredential + ctypes.POINTER(SecHandle), # phContext (NULL on first call) + ctypes.POINTER(SecBufferDesc), # pInput + ctypes.wintypes.ULONG, # fContextReq + ctypes.wintypes.ULONG, # TargetDataRep (e.g. SECURITY_NATIVE_DREP) + ctypes.POINTER(SecHandle), # phNewContext + ctypes.POINTER(SecBufferDesc), # pOutput + ctypes.POINTER(ctypes.wintypes.ULONG), # pfContextAttr + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + +_winapi_MakeSignature = ctypes.windll.secur32.MakeSignature +_winapi_MakeSignature.restype = ctypes.wintypes.DWORD +_winapi_MakeSignature.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.wintypes.ULONG, # fQOP + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo +] + +_winapi_VerifySignature = ctypes.windll.secur32.VerifySignature +_winapi_VerifySignature.restype = ctypes.wintypes.DWORD +_winapi_VerifySignature.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo + ctypes.POINTER(ctypes.wintypes.ULONG), # pfQOP +] + +_winapi_DecryptMessage = ctypes.windll.secur32.DecryptMessage +_winapi_DecryptMessage.restype = ctypes.wintypes.DWORD +_winapi_DecryptMessage.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo + ctypes.POINTER(ctypes.wintypes.ULONG), # pfQOP +] + +_winapi_EncryptMessage = ctypes.windll.secur32.EncryptMessage +_winapi_EncryptMessage.restype = ctypes.wintypes.DWORD +_winapi_EncryptMessage.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.wintypes.ULONG, # fQOP + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo +] + +_winapi_DecryptMessage = ctypes.windll.secur32.DecryptMessage +_winapi_DecryptMessage.restype = ctypes.wintypes.DWORD +_winapi_DecryptMessage.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo + ctypes.POINTER(ctypes.wintypes.ULONG), # pfQOP +] + +_winapi_FreeContextBuffer = ctypes.windll.secur32.FreeContextBuffer +_winapi_FreeContextBuffer.restype = ctypes.wintypes.DWORD +_winapi_FreeContextBuffer.argtypes = [ctypes.c_void_p] + +_winapi_QueryContextAttributesW = ctypes.windll.secur32.QueryContextAttributesW +_winapi_QueryContextAttributesW.restype = ctypes.wintypes.DWORD +_winapi_QueryContextAttributesW.argtypes = [ + ctypes.POINTER(SecHandle), + ctypes.wintypes.ULONG, + ctypes.c_void_p, +] + +_winapi_SspiGetTargetHostName = ctypes.windll.secur32.SspiGetTargetHostName +_winapi_SspiGetTargetHostName.restype = ctypes.wintypes.DWORD +_winapi_SspiGetTargetHostName.argtypes = [ + ctypes.wintypes.LPCWSTR, + ctypes.POINTER(ctypes.wintypes.LPWSTR), +] + + +# Types + + +class ISC_REQ_FLAGS(enum.IntFlag): + """ + ISC_REQ Flags per sspi.h + """ + + ISC_REQ_DELEGATE = 0x00000001 + ISC_REQ_MUTUAL_AUTH = 0x00000002 + ISC_REQ_REPLAY_DETECT = 0x00000004 + ISC_REQ_SEQUENCE_DETECT = 0x00000008 + ISC_REQ_CONFIDENTIALITY = 0x00000010 + ISC_REQ_USE_SESSION_KEY = 0x00000020 + ISC_REQ_PROMPT_FOR_CREDS = 0x00000040 + ISC_REQ_USE_SUPPLIED_CREDS = 0x00000080 + ISC_REQ_ALLOCATE_MEMORY = 0x00000100 + ISC_REQ_USE_DCE_STYLE = 0x00000200 + ISC_REQ_DATAGRAM = 0x00000400 + ISC_REQ_CONNECTION = 0x00000800 + ISC_REQ_CALL_LEVEL = 0x00001000 + ISC_REQ_FRAGMENT_SUPPLIED = 0x00002000 + ISC_REQ_EXTENDED_ERROR = 0x00004000 + ISC_REQ_STREAM = 0x00008000 + ISC_REQ_INTEGRITY = 0x00010000 + ISC_REQ_IDENTIFY = 0x00020000 + ISC_REQ_NULL_SESSION = 0x00040000 + ISC_REQ_MANUAL_CRED_VALIDATION = 0x00080000 + ISC_REQ_RESERVED1 = 0x00100000 + ISC_REQ_FRAGMENT_TO_FIT = 0x00200000 + ISC_REQ_FORWARD_CREDENTIALS = 0x00400000 + ISC_REQ_NO_INTEGRITY = 0x00800000 + ISC_REQ_USE_HTTP_STYLE = 0x01000000 + ISC_REQ_UNVERIFIED_TARGET_NAME = 0x20000000 + ISC_REQ_CONFIDENTIALITY_ONLY = 0x40000000 + ISC_REQ_MESSAGES = 0x0000000100000000 + ISC_REQ_DEFERRED_CRED_VALIDATION = 0x0000000200000000 + ISC_REQ_NO_POST_HANDSHAKE_AUTH = 0x0000000400000000 + ISC_REQ_REUSE_SESSION_TICKETS = 0x0000000800000000 + ISC_REQ_EXPLICIT_SESSION = 0x0000001000000000 + + @staticmethod + def from_GSS(flags: GSS_C_FLAGS) -> "ISC_REQ_FLAGS": + """ + Convert GSS_C_FLAGS into ISC_REQ_FLAGS + """ + result = 0 + for gssf, iscf in _GSS_ISC_TRANSLATION.items(): + if flags & gssf: + result |= iscf + return ISC_REQ_FLAGS(result) + + @staticmethod + def to_GSS(flags: "ISC_REQ_FLAGS") -> GSS_C_FLAGS: + """ + Convert ISC_REQ_FLAGS into GSS_C_FLAGS + """ + result = 0 + for gssf, iscf in _GSS_ISC_TRANSLATION.items(): + if flags & iscf: + result |= gssf + return GSS_C_FLAGS(result) + + +_GSS_ISC_TRANSLATION = { + GSS_C_FLAGS.GSS_C_DELEG_FLAG: ISC_REQ_FLAGS.ISC_REQ_DELEGATE, + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG: ISC_REQ_FLAGS.ISC_REQ_MUTUAL_AUTH, + GSS_C_FLAGS.GSS_C_REPLAY_FLAG: ISC_REQ_FLAGS.ISC_REQ_REPLAY_DETECT, + GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG: ISC_REQ_FLAGS.ISC_REQ_SEQUENCE_DETECT, + GSS_C_FLAGS.GSS_C_CONF_FLAG: ISC_REQ_FLAGS.ISC_REQ_CONFIDENTIALITY, + GSS_C_FLAGS.GSS_C_INTEG_FLAG: ISC_REQ_FLAGS.ISC_REQ_INTEGRITY, + GSS_C_FLAGS.GSS_C_DCE_STYLE: ISC_REQ_FLAGS.ISC_REQ_USE_DCE_STYLE, + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG: ISC_REQ_FLAGS.ISC_REQ_IDENTIFY, + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG: ISC_REQ_FLAGS.ISC_REQ_EXTENDED_ERROR, +} + + +class ASC_REQ_FLAGS(enum.IntFlag): + ASC_REQ_DELEGATE = 0x00000001 + ASC_REQ_MUTUAL_AUTH = 0x00000002 + ASC_REQ_REPLAY_DETECT = 0x00000004 + ASC_REQ_SEQUENCE_DETECT = 0x00000008 + ASC_REQ_CONFIDENTIALITY = 0x00000010 + ASC_REQ_USE_SESSION_KEY = 0x00000020 + ASC_REQ_SESSION_TICKET = 0x00000040 + ASC_REQ_ALLOCATE_MEMORY = 0x00000100 + ASC_REQ_USE_DCE_STYLE = 0x00000200 + ASC_REQ_DATAGRAM = 0x00000400 + ASC_REQ_CONNECTION = 0x00000800 + ASC_REQ_CALL_LEVEL = 0x00001000 + ASC_REQ_FRAGMENT_SUPPLIED = 0x00002000 + ASC_REQ_EXTENDED_ERROR = 0x00008000 + ASC_REQ_STREAM = 0x00010000 + ASC_REQ_INTEGRITY = 0x00020000 + ASC_REQ_LICENSING = 0x00040000 + ASC_REQ_IDENTIFY = 0x00080000 + ASC_REQ_ALLOW_NULL_SESSION = 0x00100000 + ASC_REQ_ALLOW_NON_USER_LOGONS = 0x00200000 + ASC_REQ_ALLOW_CONTEXT_REPLAY = 0x00400000 + ASC_REQ_FRAGMENT_TO_FIT = 0x00800000 + ASC_REQ_NO_TOKEN = 0x01000000 + ASC_REQ_PROXY_BINDINGS = 0x04000000 + ASC_REQ_ALLOW_MISSING_BINDINGS = 0x10000000 + + @staticmethod + def from_GSS(flags: GSS_C_FLAGS) -> "ASC_REQ_FLAGS": + """ + Convert GSS_C_FLAGS into ASC_REQ_FLAGS + """ + result = 0 + for gssf, ascf in _GSS_ASC_TRANSLATION.items(): + if flags & gssf: + result |= ascf + return ASC_REQ_FLAGS(result) + + @staticmethod + def to_GSS(flags: "ASC_REQ_FLAGS") -> GSS_C_FLAGS: + """ + Convert ASC_REQ_FLAGS into GSS_C_FLAGS + """ + result = 0 + for gssf, ascf in _GSS_ASC_TRANSLATION.items(): + if flags & ascf: + result |= gssf + return GSS_C_FLAGS(result) + + +_GSS_ASC_TRANSLATION = { + GSS_C_FLAGS.GSS_C_DELEG_FLAG: ASC_REQ_FLAGS.ASC_REQ_DELEGATE, + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG: ASC_REQ_FLAGS.ASC_REQ_MUTUAL_AUTH, + GSS_C_FLAGS.GSS_C_REPLAY_FLAG: ASC_REQ_FLAGS.ASC_REQ_REPLAY_DETECT, + GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG: ASC_REQ_FLAGS.ASC_REQ_SEQUENCE_DETECT, + GSS_C_FLAGS.GSS_C_CONF_FLAG: ASC_REQ_FLAGS.ASC_REQ_CONFIDENTIALITY, + GSS_C_FLAGS.GSS_C_INTEG_FLAG: ASC_REQ_FLAGS.ASC_REQ_INTEGRITY, + GSS_C_FLAGS.GSS_C_DCE_STYLE: ASC_REQ_FLAGS.ASC_REQ_USE_DCE_STYLE, + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG: ASC_REQ_FLAGS.ASC_REQ_IDENTIFY, + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG: ASC_REQ_FLAGS.ASC_REQ_EXTENDED_ERROR, + GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS: ASC_REQ_FLAGS.ASC_REQ_ALLOW_MISSING_BINDINGS, # noqa: E501 +} + + +# The SSP + + +class WinSSP(SSP): + """ + Use a native Windows SSP through SSPI + + :param Package: the SSP to use + """ + + class STATE(SSP.STATE): + NEGOTIATING = 1 + COMPLETED = 2 + + class CONTEXT(SSP.CONTEXT): + __slots__ = [ + "state", + "Credential", + "Package", + "phContext", + "ptsExpiry", + "SessionKey", + "ServerHostname", + "SendSeqNum", + "RecvSeqNum", + "cbMaxSignature", + "cbSecurityTrailer", + ] + + def __init__( + self, + Package: str, + CredentialUse: int, + req_flags: Optional["GSS_C_FLAGS | GSS_S_FLAGS"] = None, + ): + self.Credential = SecHandle() + self.phContext = None + self.ptsExpiry = SECURITY_INTEGER() + self.Package = Package + self.state = WinSSP.STATE.NEGOTIATING + self.ServerHostname = None + + status = _winapi_AcquireCredentialsHandle( + None, + Package, + CredentialUse, + None, + None, + None, + None, + ctypes.byref(self.Credential), + ctypes.byref(self.ptsExpiry), + ) + if status != SEC_CODES.SEC_E_OK: + raise OSError(f"AcquireCredentialsHandle failed: {hex(status)}") + + super(WinSSP.CONTEXT, self).__init__( + req_flags=req_flags, + ) + + def QuerySessionKey(self): + """ + Query the session key + """ + Buffer = SecPkgContext_SessionKey() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SESSION_KEY, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + SessionKeyBuf = ctypes.cast( + Buffer.SessionKey, + ctypes.POINTER(ctypes.wintypes.BYTE * Buffer.SessionKeyLength), + ) + self.SessionKey = bytes(SessionKeyBuf.contents) + + def QueryNegotiatedFlags(self): + """ + Query the negotiated flags. + """ + Buffer = SecPkgContext_Flags() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SERVER_FLAGS, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + self.flags = ISC_REQ_FLAGS.to_GSS(Buffer.Flags) + + def QueryPkgContextSizes(self): + """ + Query the package context sizes + """ + Buffer = SecPkgContext_Sizes() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SIZES, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + self.cbMaxSignature = Buffer.cbMaxSignature + self.cbSecurityTrailer = Buffer.cbSecurityTrailer + + def __repr__(self): + return "[Native SSP: %s]" % self.Package + + def __init__(self, Package: str = "Negotiate"): + self.Package = Package + if self.Package == "Negotiate": + self.auth_type = 0x09 + elif self.Package == "NTLM": + self.auth_type = 0x0A + elif self.Package == "Kerberos": + self.auth_type = 0x10 + super(WinSSP, self).__init__() + + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + # Get context + if not Context: + Context = self.CONTEXT( + self.Package, + SECPKG_CRED_OUTBOUND, + req_flags=req_flags, + ) + + if Context.state == self.STATE.COMPLETED: + # SSPI and GSSAPI count completion differently, so we might + # be called one time for nothing. Return that we completed properly. + return Context, None, GSS_S_COMPLETE + + # Create and populate the input buffers + InputBuffers = [] + if input_token: + input_token = bytes(input_token) + InputBuffers.append( + SecBuffer( + len(input_token), + SECBUFFER_TOKEN, + ctypes.cast( + ctypes.create_string_buffer(input_token), ctypes.c_void_p + ), + ) + ) + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + chan_bindings, lgth = SEC_CHANNEL_BINDINGS.from_GSS(chan_bindings) + InputBuffers.append( + SecBuffer( + lgth, + SECBUFFER_CHANNEL_BINDINGS, + ctypes.cast(chan_bindings, ctypes.c_void_p), + ) + ) + if InputBuffers: + InputBuffers, Input = SecBufferDesc.Create(InputBuffers) + else: + Input = None + + # Create the output buffers (empty for now) + OutputBuffers, Output = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(0), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.c_void_p(), + ) + ] + ) + + # Prepare other arguments + phNewContext = Context.phContext or SecHandle() + pfContextAttr = ctypes.wintypes.ULONG() + if target_name: + TargetName = ctypes.cast( + ctypes.create_string_buffer( + target_name.encode("utf-16le") + b"\x00\x00" + ), + ctypes.wintypes.LPCWSTR, + ) + + HostName = ctypes.wintypes.LPWSTR() + status = _winapi_SspiGetTargetHostName(TargetName, ctypes.byref(HostName)) + if status == SEC_CODES.SEC_E_OK: + Context.ServerHostname = HostName.value + else: + TargetName = None + + # Call SSPI + status = _winapi_InitializeSecurityContext( + ctypes.byref(Context.Credential), + Context.phContext if Context.phContext else None, + TargetName, + ISC_REQ_FLAGS.from_GSS(Context.flags) + | ISC_REQ_FLAGS.ISC_REQ_ALLOCATE_MEMORY, + 0, + SECURITY_NETWORK_DREP, + Input and ctypes.byref(Input), + 0, + ctypes.byref(phNewContext), + ctypes.byref(Output), + ctypes.byref(pfContextAttr), + ctypes.byref(Context.ptsExpiry), + ) + + # Find the output token, if any + output_token = None + if status in [ + SEC_CODES.SEC_E_OK, + SEC_CODES.SEC_I_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE, + ]: + if Context.phContext is None: + Context.phContext = phNewContext + + # Extract output token + output_token = SecBufferDesc.ParseBuffer( + OutputBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB + ) + + # If we succeeded, query the session key + if status in [SEC_CODES.SEC_E_OK, SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE]: + Context.QuerySessionKey() + Context.QueryNegotiatedFlags() + Context.QueryPkgContextSizes() + Context.state = self.STATE.COMPLETED + + # Free things we did not create (won't be freed by GC) + for OutputBuffer in OutputBuffers: + if OutputBuffer.pvBuffer is not None: + _winapi_FreeContextBuffer(OutputBuffer.pvBuffer) + + return Context, output_token, SEC_CODES.to_GSS(status) + + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + # Get context + if not Context: + Context = self.CONTEXT( + self.Package, + SECPKG_CRED_INBOUND, + req_flags=req_flags, + ) + + # Create and populate the input buffers + InputBuffers = [] + if input_token: + input_token = bytes(input_token) + InputBuffers.append( + SecBuffer( + len(input_token), + SECBUFFER_TOKEN, + ctypes.cast( + ctypes.create_string_buffer(input_token), ctypes.c_void_p + ), + ) + ) + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + chan_bindings, lgth = SEC_CHANNEL_BINDINGS.from_GSS(chan_bindings) + InputBuffers.append( + SecBuffer( + lgth, + SECBUFFER_CHANNEL_BINDINGS, + ctypes.cast(chan_bindings, ctypes.c_void_p), + ) + ) + if InputBuffers: + InputBuffers, Input = SecBufferDesc.Create(InputBuffers) + else: + Input = None + + # Create the output buffers (empty for now) + OutputBuffers, Output = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(0), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.c_void_p(), + ) + ] + ) + + # Prepare other arguments + phNewContext = Context.phContext or SecHandle() + pfContextAttr = ctypes.wintypes.ULONG() + + # Call SSPI + status = _winapi_AcceptSecurityContext( + ctypes.byref(Context.Credential), + Context.phContext if Context.phContext else None, + Input and ctypes.byref(Input), + ASC_REQ_FLAGS.from_GSS(Context.flags) + | ASC_REQ_FLAGS.ASC_REQ_ALLOCATE_MEMORY, + SECURITY_NETWORK_DREP, + ctypes.byref(phNewContext), + ctypes.byref(Output), + ctypes.byref(pfContextAttr), + ctypes.byref(Context.ptsExpiry), + ) + + # Find the output token, if any + output_token = None + if status in [ + SEC_CODES.SEC_E_OK, + SEC_CODES.SEC_I_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE, + ]: + if Context.phContext is None: + Context.phContext = phNewContext + + # Extract output token + output_token = SecBufferDesc.ParseBuffer( + OutputBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB + ) + + # If we succeeded, query the session key + if status in [SEC_CODES.SEC_E_OK, SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE]: + Context.QuerySessionKey() + Context.QueryNegotiatedFlags() + Context.QueryPkgContextSizes() + Context.state = self.STATE.COMPLETED + + # Free things we did not create (won't be freed by GC) + for OutputBuffer in OutputBuffers: + if OutputBuffer.pvBuffer is not None: + _winapi_FreeContextBuffer(OutputBuffer.pvBuffer) + + return Context, output_token, SEC_CODES.to_GSS(status) + + def LegsAmount(self, Context: CONTEXT): + if self.Package == "NTLM": + return 3 + else: + return 2 + + def MaximumSignatureLength(self, Context: CONTEXT): + return Context.cbMaxSignature + + def GSS_GetMICEx(self, Context, msgs, qop_req=0): + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG(SECBUFFER_DATA | SECBUFFER_READONLY), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(Context.cbMaxSignature), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(Context.cbMaxSignature), + ctypes.c_void_p, + ), + ) + ] + ) + # Call MakeSignature + status = _winapi_MakeSignature( + Context.phContext, + ctypes.wintypes.ULONG(qop_req), + ctypes.byref(Message), + 0, + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"MakeSignature failed with: {hex(status)}") + # Extract output token + sig = SecBufferDesc.ParseBuffer( + MessageBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB_SIGNATURE + ) + return sig + + def GSS_VerifyMICEx(self, Context, msgs, signature): + fQOP = ctypes.wintypes.ULONG(0) + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG(SECBUFFER_DATA | SECBUFFER_READONLY), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(len(signature)), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(bytes(signature)), ctypes.c_void_p + ), + ) + ] + ) + # Call VerifySignature + status = _winapi_VerifySignature( + Context.phContext, + ctypes.byref(Message), + 0, + ctypes.byref(fQOP), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"VerifySignature failed with: {hex(status)}") + + def GSS_WrapEx(self, Context, msgs, qop_req=0): + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG( + SECBUFFER_DATA + | (SECBUFFER_READONLY if not x.conf_req_flag else 0) + ), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(Context.cbSecurityTrailer), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(Context.cbSecurityTrailer), + ctypes.c_void_p, + ), + ) + ] + ) + # Call EncryptMessage + status = _winapi_EncryptMessage( + Context.phContext, + ctypes.wintypes.ULONG(qop_req), + ctypes.byref(Message), + 0, + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"EncryptMessage failed with: {hex(status)}") + # Update messages + for i in range(len(msgs)): + msgs[i].data = MessageBuffers[i].GetData() + # Extract signature + sig = SecBufferDesc.ParseBuffer( + MessageBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB_SIGNATURE + ) + return ( + msgs, + sig, + ) + + def GSS_UnwrapEx(self, Context, msgs, signature): + fQOP = ctypes.wintypes.ULONG(0) + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG( + SECBUFFER_DATA + | (SECBUFFER_READONLY if not x.conf_req_flag else 0) + ), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(len(signature)), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(bytes(signature)), ctypes.c_void_p + ), + ) + ] + ) + # Call DecryptMessage + status = _winapi_DecryptMessage( + Context.phContext, + ctypes.byref(Message), + 0, + ctypes.byref(fQOP), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"DecryptMessage failed with: {hex(status)}") + # Update messages + for i in range(len(msgs)): + msgs[i].data = MessageBuffers[i].GetData() + return msgs diff --git a/scapy/arch/windows/structures.py b/scapy/arch/windows/structures.py index af8cfb11652..e84b407cf23 100644 --- a/scapy/arch/windows/structures.py +++ b/scapy/arch/windows/structures.py @@ -1,8 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter # flake8: noqa E266 # (We keep comment boxes, it's then one-line comments) @@ -13,13 +12,30 @@ import ctypes import ctypes.wintypes -from ctypes import Structure, POINTER, byref, create_string_buffer, WINFUNCTYPE +from ctypes import ( + POINTER, + Structure, + WINFUNCTYPE, + byref, + create_string_buffer, +) +from socket import AddressFamily from scapy.config import conf from scapy.consts import WINDOWS_XP +from scapy.data import MTU + +# Typing imports +from typing import ( + Any, + Dict, + IO, + List, + Optional, + Tuple, +) ANY_SIZE = 65500 # FIXME quite inefficient :/ -AF_UNSPEC = 0 NO_ERROR = 0x0 CHAR = ctypes.c_char @@ -29,6 +45,7 @@ ULONG = ctypes.wintypes.ULONG ULONGLONG = ctypes.c_ulonglong HANDLE = ctypes.wintypes.HANDLE +LPVOID = ctypes.wintypes.LPVOID LPWSTR = ctypes.wintypes.LPWSTR VOID = ctypes.c_void_p INT = ctypes.c_int @@ -47,6 +64,7 @@ def _resolve_list(list_obj): + # type: (Any) -> List[Dict[str, Any]] current = list_obj _list = [] while current and hasattr(current, "contents"): @@ -56,7 +74,8 @@ def _resolve_list(list_obj): def _struct_to_dict(struct_obj): - results = {} + # type: (Any) -> Dict[str, Any] + results = {} # type: Dict[str, Any] for fname, ctype in struct_obj.__class__._fields_: val = getattr(struct_obj, fname) if fname == "next": @@ -83,8 +102,11 @@ def _struct_to_dict(struct_obj): _winapi_SetConsoleTitle.argtypes = [LPWSTR] def _windows_title(title=None): - """Updates the terminal title with the default one or with `title` - if provided.""" + # type: (Optional[str]) -> None + """ + Updates the terminal title with the default one or with `title` + if provided. + """ if conf.interactive: _winapi_SetConsoleTitle(title or "Scapy v{}".format(conf.version)) @@ -119,6 +141,7 @@ class SERVICE_STATUS(Structure): QueryServiceStatus.argtypes = [SC_HANDLE, POINTER(SERVICE_STATUS)] def get_service_status(service): + # type: (str) -> Dict[str, int] """Returns content of QueryServiceStatus for a service""" SERVICE_QUERY_STATUS = 0x0004 schSCManager = OpenSCManagerW( @@ -182,51 +205,6 @@ class SOCKADDR_INET(ctypes.Union): ("Ipv6", sockaddr_in6), ("si_family", USHORT)] -############################## -######### ICMP stats ######### -############################## - - -class MIBICMPSTATS(Structure): - _fields_ = [("dwMsgs", DWORD), - ("dwErrors", DWORD), - ("dwDestUnreachs", DWORD), - ("dwTimeExcds", DWORD), - ("dwParmProbs", DWORD), - ("dwSrcQuenchs", DWORD), - ("dwRedirects", DWORD), - ("dwEchos", DWORD), - ("dwEchoReps", DWORD), - ("dwTimestamps", DWORD), - ("dwTimestampReps", DWORD), - ("dwAddrMasks", DWORD), - ("dwAddrMaskReps", DWORD)] - - -class MIBICMPINFO(Structure): - _fields_ = [("icmpInStats", MIBICMPSTATS), - ("icmpOutStats", MIBICMPSTATS)] - - -class MIB_ICMP(Structure): - _fields_ = [("stats", MIBICMPINFO)] - - -PMIB_ICMP = POINTER(MIB_ICMP) - -# Func - -_GetIcmpStatistics = WINFUNCTYPE(ULONG, PMIB_ICMP)( - ('GetIcmpStatistics', iphlpapi)) - - -def GetIcmpStatistics(): - """Return all Windows ICMP stats from iphlpapi""" - statistics = MIB_ICMP() - _GetIcmpStatistics(byref(statistics)) - results = _struct_to_dict(statistics) - del(statistics) - return results ############################## ##### Adapters Addresses ##### @@ -241,7 +219,8 @@ def GetIcmpStatistics(): MAX_ADAPTER_ADDRESS_LENGTH = 8 MAX_DHCPV6_DUID_LENGTH = 130 -GAA_FLAG_INCLUDE_PREFIX = ULONG(0x0010) +GAA_FLAG_INCLUDE_PREFIX = 0x0010 +GAA_FLAG_INCLUDE_ALL_INTERFACES = 0x0100 # for now, just use void * for pointers to unused structures PIP_ADAPTER_WINS_SERVER_ADDRESS_LH = VOID PIP_ADAPTER_GATEWAY_ADDRESS_LH = VOID @@ -431,11 +410,12 @@ class IP_ADAPTER_ADDRESSES(Structure): ('GetAdaptersAddresses', iphlpapi)) -def GetAdaptersAddresses(AF=AF_UNSPEC): +def GetAdaptersAddresses(AF=AddressFamily.AF_UNSPEC): + # type: (int) -> List[Dict[str, Any]] """Return all Windows Adapters addresses from iphlpapi""" # We get the size first size = ULONG() - flags = GAA_FLAG_INCLUDE_PREFIX + flags = ULONG(GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_ALL_INTERFACES) res = _GetAdaptersAddresses(AF, flags, None, None, byref(size)) @@ -452,7 +432,7 @@ def GetAdaptersAddresses(AF=AF_UNSPEC): if res != NO_ERROR: raise RuntimeError("Error retrieving table (%d)" % res) results = _resolve_list(AdapterAddresses) - del(AdapterAddresses) + del AdapterAddresses return results ############################## @@ -494,6 +474,7 @@ class MIB_IPFORWARDTABLE(Structure): def GetIpForwardTable(): + # type: () -> List[Dict[str, Any]] """Return all Windows routes (IPv4 only) from iphlpapi""" # We get the size first size = ULONG() @@ -511,7 +492,7 @@ def GetIpForwardTable(): results = [] for i in range(pIpForwardTable.contents.NumEntries): results.append(_struct_to_dict(pIpForwardTable.contents.Table[i])) - del(pIpForwardTable) + del pIpForwardTable return results ### V2 ### @@ -570,7 +551,8 @@ class MIB_IPFORWARD_TABLE2(Structure): ) -def GetIpForwardTable2(AF=AF_UNSPEC): +def GetIpForwardTable2(AF=AddressFamily.AF_UNSPEC): + # type: (AddressFamily) -> List[Dict[str, Any]] """Return all Windows routes (IPv4/IPv6) from iphlpapi""" if WINDOWS_XP: raise OSError("Not available on Windows XP !") @@ -583,3 +565,61 @@ def GetIpForwardTable2(AF=AF_UNSPEC): results.append(_struct_to_dict(table.contents.Table[i])) _FreeMibTable(table) return results + + +############## +#### FIFO #### +############## + +class _SECURITY_ATTRIBUTES(Structure): + _fields_ = [("nLength", DWORD), + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL)] + + +LPSECURITY_ATTRIBUTES = POINTER(_SECURITY_ATTRIBUTES) + + +def _get_win_fifo() -> Tuple[str, Any]: + """Create a windows fifo and returns the (client file, server fd) + """ + from scapy.volatile import RandString + f = r"\\.\pipe\scapy%s" % str(RandString(6)) + buffer = create_string_buffer(ctypes.sizeof(_SECURITY_ATTRIBUTES)) + sec = ctypes.cast(buffer, LPSECURITY_ATTRIBUTES) + sec.contents.nLength = ctypes.sizeof(_SECURITY_ATTRIBUTES) + res = ctypes.windll.kernel32.CreateNamedPipeA( + create_string_buffer(f.encode()), + 0x00000003 | 0x40000000, + 0, + 1, 65536, 65536, + 300, + sec, + ) + if res == -1: + raise OSError(ctypes.FormatError()) + return f, res + + +def _win_fifo_open(fd: Any) -> IO[bytes]: + """Connect NamedPipe and return a fake open() file + """ + ctypes.windll.kernel32.ConnectNamedPipe(fd, None) + + class _opened(IO[bytes]): + def read(self, x: int = MTU) -> bytes: + buf = ctypes.create_string_buffer(x) + res = ctypes.windll.kernel32.ReadFile( + fd, + buf, + x, + None, + None, + ) + if res == 0: + raise OSError(ctypes.FormatError()) + return buf.raw + def close(self) -> None: + # ignore failures + ctypes.windll.kernel32.CloseHandle(fd) + return _opened() # type: ignore diff --git a/scapy/as_resolvers.py b/scapy/as_resolvers.py index 50e827904ca..a9f9bb537f9 100644 --- a/scapy/as_resolvers.py +++ b/scapy/as_resolvers.py @@ -1,24 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Resolve Autonomous Systems (AS). """ -from __future__ import absolute_import import socket from scapy.config import conf from scapy.compat import plain_str +from typing import ( + Any, + Optional, + Tuple, + List, +) + class AS_resolver: server = None - options = "-k" + options = "-k" # type: Optional[str] def __init__(self, server=None, port=43, options=None): + # type: (Optional[str], int, Optional[str]) -> None if server is not None: self.server = server self.port = port @@ -26,6 +33,7 @@ def __init__(self, server=None, port=43, options=None): self.options = options def _start(self): + # type: () -> None self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.s.connect((self.server, self.port)) if self.options: @@ -33,9 +41,11 @@ def _start(self): self.s.recv(8192) def _stop(self): + # type: () -> None self.s.close() def _parse_whois(self, txt): + # type: (bytes) -> Tuple[Optional[str], str] asn, desc = None, b"" for line in txt.splitlines(): if not asn and line.startswith(b"origin:"): @@ -49,16 +59,23 @@ def _parse_whois(self, txt): return asn, plain_str(desc.strip()) def _resolve_one(self, ip): + # type: (str) -> Tuple[str, Optional[str], str] self.s.send(("%s\n" % ip).encode("utf8")) x = b"" while not (b"%" in x or b"source" in x): - x += self.s.recv(8192) + d = self.s.recv(8192) + if not d: + break + x += d asn, desc = self._parse_whois(x) return ip, asn, desc - def resolve(self, *ips): + def resolve(self, + *ips # type: str + ): + # type: (...) -> List[Tuple[str, Optional[str], str]] self._start() - ret = [] + ret = [] # type: List[Tuple[str, Optional[str], str]] for ip in ips: ip, asn, desc = self._resolve_one(ip) if asn is not None: @@ -81,7 +98,10 @@ class AS_resolver_cymru(AS_resolver): server = "whois.cymru.com" options = None - def resolve(self, *ips): + def resolve(self, + *ips # type: str + ): + # type: (...) -> List[Tuple[str, Optional[str], str]] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((self.server, self.port)) s.send( @@ -100,11 +120,11 @@ def resolve(self, *ips): return self.parse(r) def parse(self, data): + # type: (bytes) -> List[Tuple[str, Optional[str], str]] """Parse bulk cymru data""" - ASNlist = [] - for line in data.splitlines()[1:]: - line = plain_str(line) + ASNlist = [] # type: List[Tuple[str, Optional[str], str]] + for line in plain_str(data).splitlines()[1:]: if "|" not in line: continue asn, ip, desc = [elt.strip() for elt in line.split('|')] @@ -116,16 +136,17 @@ def parse(self, data): class AS_resolver_multi(AS_resolver): - resolvers_list = (AS_resolver_riswhois(), AS_resolver_radb(), - AS_resolver_cymru()) - resolvers_list = resolvers_list[1:] - def __init__(self, *reslist): + # type: (*AS_resolver) -> None AS_resolver.__init__(self) if reslist: self.resolvers_list = reslist + else: + self.resolvers_list = (AS_resolver_radb(), + AS_resolver_cymru()) def resolve(self, *ips): + # type: (*Any) -> List[Tuple[str, Optional[str], str]] todo = ips ret = [] for ASres in self.resolvers_list: @@ -133,7 +154,7 @@ def resolve(self, *ips): res = ASres.resolve(*todo) except socket.error: continue - todo = [ip for ip in todo if ip not in [r[0] for r in res]] + todo = tuple(ip for ip in todo if ip not in [r[0] for r in res]) ret += res if not todo: break diff --git a/scapy/asn1/__init__.py b/scapy/asn1/__init__.py index 31debcc2716..39c022f6767 100644 --- a/scapy/asn1/__init__.py +++ b/scapy/asn1/__init__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Package holding ASN.1 related modules. diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 325c9182021..bd58715e26d 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -1,53 +1,103 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Modified by Maxence Tury -# This program is published under a GPLv2 license +# Acknowledgment: Maxence Tury """ ASN.1 (Abstract Syntax Notation One) """ -from __future__ import absolute_import -from __future__ import print_function import random -from datetime import datetime +from datetime import datetime, timedelta, tzinfo from scapy.config import conf from scapy.error import Scapy_Exception, warning from scapy.volatile import RandField, RandIP, GeneralizedTime from scapy.utils import Enum_metaclass, EnumElement, binrepr -from scapy.compat import plain_str, chb, orb -import scapy.modules.six as six -from scapy.modules.six.moves import range +from scapy.compat import plain_str, bytes_encode, chb, orb +from typing import ( + Any, + AnyStr, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + Union, + cast, + TYPE_CHECKING, +) +from typing import ( + TypeVar, +) -class RandASN1Object(RandField): +if TYPE_CHECKING: + from scapy.asn1.ber import BERcodec_Object + +try: + from datetime import timezone +except ImportError: + # Python 2 compat - don't bother typing it + class UTC(tzinfo): + """UTC""" + + def utcoffset(self, dt): # type: ignore + return timedelta(0) + + def tzname(self, dt): # type: ignore + return "UTC" + + def dst(self, dt): # type: ignore + return None + + class timezone(tzinfo): # type: ignore + def __init__(self, delta): # type: ignore + self.delta = delta + + def utcoffset(self, dt): # type: ignore + return self.delta + + def tzname(self, dt): # type: ignore + return None + + def dst(self, dt): # type: ignore + return None + + timezone.utc = UTC() # type: ignore + + +class RandASN1Object(RandField["ASN1_Object[Any]"]): def __init__(self, objlist=None): - self.objlist = [ - x._asn1_obj - for x in six.itervalues(ASN1_Class_UNIVERSAL.__rdict__) - if hasattr(x, "_asn1_obj") - ] if objlist is None else objlist + # type: (Optional[List[Type[ASN1_Object[Any]]]]) -> None + if objlist: + self.objlist = objlist + else: + self.objlist = [ + x._asn1_obj + for x in ASN1_Class_UNIVERSAL.__rdict__.values() # type: ignore + if hasattr(x, "_asn1_obj") + ] self.chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" # noqa: E501 def _fix(self, n=0): + # type: (int) -> ASN1_Object[Any] o = random.choice(self.objlist) if issubclass(o, ASN1_INTEGER): return o(int(random.gauss(0, 1000))) elif issubclass(o, ASN1_IPADDRESS): - z = RandIP()._fix() - return o(z) - elif issubclass(o, ASN1_GENERALIZED_TIME) or issubclass(o, ASN1_UTC_TIME): # noqa: E501 - z = GeneralizedTime()._fix() - return o(z) + return o(RandIP()._fix()) + elif issubclass(o, ASN1_GENERALIZED_TIME) or issubclass(o, ASN1_UTC_TIME): + return o(GeneralizedTime()._fix()) elif issubclass(o, ASN1_STRING): - z = int(random.expovariate(0.05) + 1) - return o("".join(random.choice(self.chars) for _ in range(z))) + z1 = int(random.expovariate(0.05) + 1) + return o("".join(random.choice(self.chars) for _ in range(z1)).encode()) elif issubclass(o, ASN1_SEQUENCE) and (n < 10): - z = int(random.expovariate(0.08) + 1) + z2 = int(random.expovariate(0.08) + 1) return o([self.__class__(objlist=self.objlist)._fix(n + 1) - for _ in range(z)]) + for _ in range(z2)]) return ASN1_INTEGER(int(random.gauss(0, 1000))) @@ -73,57 +123,73 @@ class ASN1_BadTag_Decoding_Error(ASN1_Decoding_Error): class ASN1Codec(EnumElement): def register_stem(cls, stem): + # type: (Type[BERcodec_Object[Any]]) -> None cls._stem = stem def dec(cls, s, context=None): - return cls._stem.dec(s, context=context) + # type: (bytes, Optional[Type[ASN1_Class]]) -> ASN1_Object[Any] + return cls._stem.dec(s, context=context) # type: ignore def safedec(cls, s, context=None): - return cls._stem.safedec(s, context=context) + # type: (bytes, Optional[Type[ASN1_Class]]) -> ASN1_Object[Any] + return cls._stem.safedec(s, context=context) # type: ignore def get_stem(cls): - return cls.stem + # type: () -> type + return cls._stem class ASN1_Codecs_metaclass(Enum_metaclass): element_class = ASN1Codec -class ASN1_Codecs(six.with_metaclass(ASN1_Codecs_metaclass)): - BER = 1 - DER = 2 - PER = 3 - CER = 4 - LWER = 5 - BACnet = 6 - OER = 7 - SER = 8 - XER = 9 +class ASN1_Codecs(metaclass=ASN1_Codecs_metaclass): + BER = cast(ASN1Codec, 1) + DER = cast(ASN1Codec, 2) + PER = cast(ASN1Codec, 3) + CER = cast(ASN1Codec, 4) + LWER = cast(ASN1Codec, 5) + BACnet = cast(ASN1Codec, 6) + OER = cast(ASN1Codec, 7) + SER = cast(ASN1Codec, 8) + XER = cast(ASN1Codec, 9) class ASN1Tag(EnumElement): - def __init__(self, key, value, context=None, codec=None): + def __init__(self, + key, # type: str + value, # type: int + context=None, # type: Optional[Type[ASN1_Class]] + codec=None # type: Optional[Dict[ASN1Codec, Type[BERcodec_Object[Any]]]] # noqa: E501 + ): + # type: (...) -> None EnumElement.__init__(self, key, value) - self._context = context + # populated by the metaclass + self.context = context # type: Type[ASN1_Class] # type: ignore if codec is None: codec = {} self._codec = codec def clone(self): # not a real deep copy. self.codec is shared - return self.__class__(self._key, self._value, self._context, self._codec) # noqa: E501 + # type: () -> ASN1Tag + return self.__class__(self._key, self._value, self.context, self._codec) # noqa: E501 def register_asn1_object(self, asn1obj): + # type: (Type[ASN1_Object[Any]]) -> None self._asn1_obj = asn1obj def asn1_object(self, val): + # type: (Any) -> ASN1_Object[Any] if hasattr(self, "_asn1_obj"): return self._asn1_obj(val) raise ASN1_Error("%r does not have any assigned ASN1 object" % self) def register(self, codecnum, codec): + # type: (ASN1Codec, Type[BERcodec_Object[Any]]) -> None self._codec[codecnum] = codec def get_codec(self, codec): + # type: (Any) -> Type[BERcodec_Object[Any]] try: c = self._codec[codec] except KeyError: @@ -134,14 +200,20 @@ def get_codec(self, codec): class ASN1_Class_metaclass(Enum_metaclass): element_class = ASN1Tag - def __new__(cls, name, bases, dct): # XXX factorise a bit with Enum_metaclass.__new__() # noqa: E501 + # XXX factorise a bit with Enum_metaclass.__new__() + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[ASN1_Class] for b in bases: - for k, v in six.iteritems(b.__dict__): + for k, v in b.__dict__.items(): if k not in dct and isinstance(v, ASN1Tag): dct[k] = v.clone() rdict = {} - for k, v in six.iteritems(dct): + for k, v in dct.items(): if isinstance(v, int): v = ASN1Tag(k, v) dct[k] = v @@ -150,108 +222,148 @@ def __new__(cls, name, bases, dct): # XXX factorise a bit with Enum_metaclass._ rdict[v] = v dct["__rdict__"] = rdict - cls = type.__new__(cls, name, bases, dct) - for v in six.itervalues(cls.__dict__): + ncls = cast('Type[ASN1_Class]', + type.__new__(cls, name, bases, dct)) + for v in ncls.__dict__.values(): if isinstance(v, ASN1Tag): - v.context = cls # overwrite ASN1Tag contexts, even cloned ones - return cls + # overwrite ASN1Tag contexts, even cloned ones + v.context = ncls + return ncls -class ASN1_Class(six.with_metaclass(ASN1_Class_metaclass)): +class ASN1_Class(metaclass=ASN1_Class_metaclass): pass class ASN1_Class_UNIVERSAL(ASN1_Class): name = "UNIVERSAL" - ERROR = -3 - RAW = -2 - NONE = -1 - ANY = 0 - BOOLEAN = 1 - INTEGER = 2 - BIT_STRING = 3 - STRING = 4 - NULL = 5 - OID = 6 - OBJECT_DESCRIPTOR = 7 - EXTERNAL = 8 - REAL = 9 - ENUMERATED = 10 - EMBEDDED_PDF = 11 - UTF8_STRING = 12 - RELATIVE_OID = 13 - SEQUENCE = 16 | 0x20 # constructed encoding - SET = 17 | 0x20 # constructed encoding - NUMERIC_STRING = 18 - PRINTABLE_STRING = 19 - T61_STRING = 20 # aka TELETEX_STRING - VIDEOTEX_STRING = 21 - IA5_STRING = 22 - UTC_TIME = 23 - GENERALIZED_TIME = 24 - GRAPHIC_STRING = 25 - ISO646_STRING = 26 # aka VISIBLE_STRING - GENERAL_STRING = 27 - UNIVERSAL_STRING = 28 - CHAR_STRING = 29 - BMP_STRING = 30 - IPADDRESS = 0 | 0x40 # application-specific encoding - COUNTER32 = 1 | 0x40 # application-specific encoding - GAUGE32 = 2 | 0x40 # application-specific encoding - TIME_TICKS = 3 | 0x40 # application-specific encoding + # Those casts are made so that MyPy understands what the + # metaclass does in the background. + ERROR = cast(ASN1Tag, -3) + RAW = cast(ASN1Tag, -2) + NONE = cast(ASN1Tag, -1) + ANY = cast(ASN1Tag, 0) + BOOLEAN = cast(ASN1Tag, 1) + INTEGER = cast(ASN1Tag, 2) + BIT_STRING = cast(ASN1Tag, 3) + STRING = cast(ASN1Tag, 4) + NULL = cast(ASN1Tag, 5) + OID = cast(ASN1Tag, 6) + OBJECT_DESCRIPTOR = cast(ASN1Tag, 7) + EXTERNAL = cast(ASN1Tag, 8) + REAL = cast(ASN1Tag, 9) + ENUMERATED = cast(ASN1Tag, 10) + EMBEDDED_PDF = cast(ASN1Tag, 11) + UTF8_STRING = cast(ASN1Tag, 12) + RELATIVE_OID = cast(ASN1Tag, 13) + SEQUENCE = cast(ASN1Tag, 16 | 0x20) # constructed encoding + SET = cast(ASN1Tag, 17 | 0x20) # constructed encoding + NUMERIC_STRING = cast(ASN1Tag, 18) + PRINTABLE_STRING = cast(ASN1Tag, 19) + T61_STRING = cast(ASN1Tag, 20) # aka TELETEX_STRING + VIDEOTEX_STRING = cast(ASN1Tag, 21) + IA5_STRING = cast(ASN1Tag, 22) + UTC_TIME = cast(ASN1Tag, 23) + GENERALIZED_TIME = cast(ASN1Tag, 24) + GRAPHIC_STRING = cast(ASN1Tag, 25) + ISO646_STRING = cast(ASN1Tag, 26) # aka VISIBLE_STRING + GENERAL_STRING = cast(ASN1Tag, 27) + UNIVERSAL_STRING = cast(ASN1Tag, 28) + CHAR_STRING = cast(ASN1Tag, 29) + BMP_STRING = cast(ASN1Tag, 30) + IPADDRESS = cast(ASN1Tag, 0 | 0x40) # application-specific encoding + COUNTER32 = cast(ASN1Tag, 1 | 0x40) # application-specific encoding + COUNTER64 = cast(ASN1Tag, 6 | 0x40) # application-specific encoding + GAUGE32 = cast(ASN1Tag, 2 | 0x40) # application-specific encoding + TIME_TICKS = cast(ASN1Tag, 3 | 0x40) # application-specific encoding class ASN1_Object_metaclass(type): - def __new__(cls, name, bases, dct): - c = super(ASN1_Object_metaclass, cls).__new__(cls, name, bases, dct) + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[ASN1_Object[Any]] + c = cast( + 'Type[ASN1_Object[Any]]', + super(ASN1_Object_metaclass, cls).__new__(cls, name, bases, dct) + ) try: c.tag.register_asn1_object(c) except Exception: - warning("Error registering %r for %r" % (c.tag, c.codec)) + warning("Error registering %r" % c.tag) return c -class ASN1_Object(six.with_metaclass(ASN1_Object_metaclass)): +_K = TypeVar('_K') + + +class ASN1_Object(Generic[_K], metaclass=ASN1_Object_metaclass): tag = ASN1_Class_UNIVERSAL.ANY def __init__(self, val): + # type: (_K) -> None self.val = val def enc(self, codec): + # type: (Any) -> bytes return self.tag.get_codec(codec).enc(self.val) def __repr__(self): + # type: () -> str return "<%s[%r]>" % (self.__dict__.get("name", self.__class__.__name__), self.val) # noqa: E501 def __str__(self): - return self.enc(conf.ASN1_default_codec) + # type: () -> str + return plain_str(self.enc(conf.ASN1_default_codec)) def __bytes__(self): + # type: () -> bytes return self.enc(conf.ASN1_default_codec) def strshow(self, lvl=0): + # type: (int) -> str return (" " * lvl) + repr(self) + "\n" def show(self, lvl=0): + # type: (int) -> None print(self.strshow(lvl)) def __eq__(self, other): - return self.val == other + # type: (Any) -> bool + return bool(self.val == other) def __lt__(self, other): - return self.val < other + # type: (Any) -> bool + return bool(self.val < other) def __le__(self, other): - return self.val <= other + # type: (Any) -> bool + return bool(self.val <= other) def __gt__(self, other): - return self.val > other + # type: (Any) -> bool + return bool(self.val > other) def __ge__(self, other): - return self.val >= other + # type: (Any) -> bool + return bool(self.val >= other) def __ne__(self, other): - return self.val != other + # type: (Any) -> bool + return bool(self.val != other) + + def command(self, json=False): + # type: (bool) -> Union[Dict[str, str], str] + if json: + if isinstance(self.val, bytes): + val = self.val.decode("utf-8", errors="backslashreplace") + else: + val = repr(self.val) + return {"type": self.__class__.__name__, "value": val} + else: + return "%s(%s)" % (self.__class__.__name__, repr(self.val)) ####################### @@ -260,27 +372,38 @@ def __ne__(self, other): # on the whole, we order the classes by ASN1_Class_UNIVERSAL tag value -class ASN1_DECODING_ERROR(ASN1_Object): +class _ASN1_ERROR(ASN1_Object[Union[bytes, ASN1_Object[Any]]]): + pass + + +class ASN1_DECODING_ERROR(_ASN1_ERROR): tag = ASN1_Class_UNIVERSAL.ERROR def __init__(self, val, exc=None): + # type: (Union[bytes, ASN1_Object[Any]], Optional[Exception]) -> None ASN1_Object.__init__(self, val) self.exc = exc def __repr__(self): - return "<%s[%r]{{%r}}>" % (self.__dict__.get("name", self.__class__.__name__), # noqa: E501 - self.val, self.exc.args[0]) + # type: () -> str + return "<%s[%r]{{%r}}>" % ( + self.__dict__.get("name", self.__class__.__name__), + self.val, + self.exc and self.exc.args[0] or "" + ) def enc(self, codec): + # type: (Any) -> bytes if isinstance(self.val, ASN1_Object): return self.val.enc(codec) return self.val -class ASN1_force(ASN1_Object): +class ASN1_force(_ASN1_ERROR): tag = ASN1_Class_UNIVERSAL.RAW def enc(self, codec): + # type: (Any) -> bytes if isinstance(self.val, ASN1_Object): return self.val.enc(codec) return self.val @@ -290,10 +413,11 @@ class ASN1_BADTAG(ASN1_force): pass -class ASN1_INTEGER(ASN1_Object): +class ASN1_INTEGER(ASN1_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER def __repr__(self): + # type: () -> str h = hex(self.val) if h[-1] == "L": h = h[:-1] @@ -311,10 +435,11 @@ class ASN1_BOOLEAN(ASN1_INTEGER): # BER: 0 means False, anything else means True def __repr__(self): + # type: () -> str return '%s %s' % (not (self.val == 0), ASN1_Object.__repr__(self)) -class ASN1_BIT_STRING(ASN1_Object): +class ASN1_BIT_STRING(ASN1_Object[str]): """ ASN1_BIT_STRING values are bit strings like "011101". A zero-bit padded readable string is provided nonetheless, @@ -323,12 +448,14 @@ class ASN1_BIT_STRING(ASN1_Object): tag = ASN1_Class_UNIVERSAL.BIT_STRING def __init__(self, val, readable=False): + # type: (AnyStr, bool) -> None if not readable: - self.val = val + self.val = cast(str, val) # type: ignore else: - self.val_readable = val + self.val_readable = cast(bytes, val) # type: ignore def __setattr__(self, name, value): + # type: (str, Any) -> None if name == "val_readable": if isinstance(value, (str, bytes)): val = "".join(binrepr(orb(x)).zfill(8) for x in value) @@ -336,7 +463,7 @@ def __setattr__(self, name, value): warning("Invalid val: should be bytes") val = "" object.__setattr__(self, "val", val) - object.__setattr__(self, name, value) + object.__setattr__(self, name, bytes_encode(value)) object.__setattr__(self, "unused_bits", 0) elif name == "val": value = plain_str(value) @@ -365,42 +492,58 @@ def __setattr__(self, name, value): else: object.__setattr__(self, name, value) + def set(self, i, val): + # type: (int, str) -> None + """ + Sets bit 'i' to value 'val' (starting from 0) + """ + val = str(val) + assert val in ['0', '1'] + if len(self.val) < i: + self.val += "0" * (i - len(self.val)) + self.val = self.val[:i] + val + self.val[i + 1:] + def __repr__(self): + # type: () -> str s = self.val_readable if len(s) > 16: s = s[:10] + b"..." + s[-10:] v = self.val if len(v) > 20: v = v[:10] + "..." + v[-10:] - return "<%s[%s]=%s (%d unused bit%s)>" % ( + return "<%s[%s]=%r (%d unused bit%s)>" % ( self.__dict__.get("name", self.__class__.__name__), v, s, - self.unused_bits, - "s" if self.unused_bits > 1 else "" + self.unused_bits, # type: ignore + "s" if self.unused_bits > 1 else "" # type: ignore ) -class ASN1_STRING(ASN1_Object): +class ASN1_STRING(ASN1_Object[bytes]): tag = ASN1_Class_UNIVERSAL.STRING -class ASN1_NULL(ASN1_Object): +class ASN1_NULL(ASN1_Object[None]): tag = ASN1_Class_UNIVERSAL.NULL def __repr__(self): + # type: () -> str return ASN1_Object.__repr__(self) -class ASN1_OID(ASN1_Object): +class ASN1_OID(ASN1_Object[str]): tag = ASN1_Class_UNIVERSAL.OID def __init__(self, val): - val = conf.mib._oid(plain_str(val)) + # type: (str) -> None + val = plain_str(val) + val = conf.mib._oid(val) ASN1_Object.__init__(self, val) self.oidname = conf.mib._oidname(val) def __repr__(self): + # type: () -> str return "<%s[%r]>" % (self.__dict__.get("name", self.__class__.__name__), self.oidname) # noqa: E501 @@ -412,11 +555,11 @@ class ASN1_UTF8_STRING(ASN1_STRING): tag = ASN1_Class_UNIVERSAL.UTF8_STRING -class ASN1_NUMERIC_STRING(ASN1_STRING): +class ASN1_NUMERIC_STRING(ASN1_Object[str]): tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING -class ASN1_PRINTABLE_STRING(ASN1_STRING): +class ASN1_PRINTABLE_STRING(ASN1_Object[str]): tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING @@ -432,43 +575,128 @@ class ASN1_IA5_STRING(ASN1_STRING): tag = ASN1_Class_UNIVERSAL.IA5_STRING -class ASN1_UTC_TIME(ASN1_STRING): - tag = ASN1_Class_UNIVERSAL.UTC_TIME +class ASN1_GENERAL_STRING(ASN1_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class ASN1_GENERALIZED_TIME(ASN1_Object[str]): + """ + Improved version of ASN1_GENERALIZED_TIME, properly handling time zones and + all string representation formats defined by ASN.1. These are: + + 1. Local time only: YYYYMMDDHH[MM[SS[.fff]]] + 2. Universal time (UTC time) only: YYYYMMDDHH[MM[SS[.fff]]]Z + 3. Difference between local and UTC times: YYYYMMDDHH[MM[SS[.fff]]]+-HHMM + + It also handles ASN1_UTC_TIME, which allows: + + 1. Universal time (UTC time) only: YYMMDDHHMM[SS[.fff]]Z + 2. Difference between local and UTC times: YYMMDDHHMM[SS[.fff]]+-HHMM + + Note the differences: Year is only two digits, minutes are not optional and + there is no milliseconds. + """ + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + pretty_time = None def __init__(self, val): - ASN1_STRING.__init__(self, val) + # type: (Union[str, datetime]) -> None + if isinstance(val, datetime): + self.__setattr__("datetime", val) + else: + super(ASN1_GENERALIZED_TIME, self).__init__(val) def __setattr__(self, name, value): + # type: (str, Any) -> None if isinstance(value, bytes): value = plain_str(value) + if name == "val": + formats = { + 10: "%Y%m%d%H", + 12: "%Y%m%d%H%M", + 14: "%Y%m%d%H%M%S" + } + dt = None # type: Optional[datetime] + try: + if value[-1] == "Z": + str, ofs = value[:-1], value[-1:] + elif value[-5] in ("+", "-"): + str, ofs = value[:-5], value[-5:] + elif isinstance(self, ASN1_UTC_TIME): + raise ValueError() + else: + str, ofs = value, "" + + if isinstance(self, ASN1_UTC_TIME) and len(str) >= 10: + fmt = "%y" + formats[len(str) + 2][2:] + elif str[-4] == ".": + fmt = formats[len(str) - 4] + ".%f" + else: + fmt = formats[len(str)] + + dt = datetime.strptime(str, fmt) + if ofs == 'Z': + dt = dt.replace(tzinfo=timezone.utc) + elif ofs: + sign = -1 if ofs[0] == "-" else 1 + ofs = datetime.strptime(ofs[1:], "%H%M") + delta = timedelta(hours=ofs.hour * sign, + minutes=ofs.minute * sign) + dt = dt.replace(tzinfo=timezone(delta)) + except Exception: + dt = None + pretty_time = None - if isinstance(self, ASN1_GENERALIZED_TIME): - _len = 15 - self._format = "%Y%m%d%H%M%S" - else: - _len = 13 - self._format = "%y%m%d%H%M%S" - _nam = self.tag._asn1_obj.__name__[4:].lower() - if (isinstance(value, str) and - len(value) == _len and value[-1] == "Z"): - dt = datetime.strptime(value[:-1], self._format) - pretty_time = dt.strftime("%b %d %H:%M:%S %Y GMT") - else: + if dt is None: + _nam = self.tag._asn1_obj.__name__[5:] + _nam = _nam.lower().replace("_", " ") pretty_time = "%s [invalid %s]" % (value, _nam) + else: + pretty_time = dt.strftime("%Y-%m-%d %H:%M:%S") + if dt.microsecond: + pretty_time += dt.strftime(".%f")[:4] + if dt.tzinfo == timezone.utc: + pretty_time += dt.strftime(" UTC") + elif dt.tzinfo is not None: + if dt.tzinfo.utcoffset(dt) is not None: + pretty_time += dt.strftime(" %z") + ASN1_STRING.__setattr__(self, "pretty_time", pretty_time) + ASN1_STRING.__setattr__(self, "datetime", dt) ASN1_STRING.__setattr__(self, name, value) elif name == "pretty_time": print("Invalid operation: pretty_time rewriting is not supported.") + elif name == "datetime": + ASN1_STRING.__setattr__(self, name, value) + if isinstance(value, datetime): + yfmt = "%y" if isinstance(self, ASN1_UTC_TIME) else "%Y" + if value.microsecond: + str = value.strftime(yfmt + "%m%d%H%M%S.%f")[:-3] + else: + str = value.strftime(yfmt + "%m%d%H%M%S") + + if value.tzinfo == timezone.utc: + str = str + "Z" + else: + str = str + value.strftime("%z") # empty if naive + + ASN1_STRING.__setattr__(self, "val", str) + else: + ASN1_STRING.__setattr__(self, "val", None) else: ASN1_STRING.__setattr__(self, name, value) def __repr__(self): - return "%s %s" % (self.pretty_time, ASN1_STRING.__repr__(self)) + # type: () -> str + return "%s %s" % ( + self.pretty_time, + super(ASN1_GENERALIZED_TIME, self).__repr__() + ) -class ASN1_GENERALIZED_TIME(ASN1_UTC_TIME): - tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME +class ASN1_UTC_TIME(ASN1_GENERALIZED_TIME): + tag = ASN1_Class_UNIVERSAL.UTC_TIME class ASN1_ISO646_STRING(ASN1_STRING): @@ -482,11 +710,28 @@ class ASN1_UNIVERSAL_STRING(ASN1_STRING): class ASN1_BMP_STRING(ASN1_STRING): tag = ASN1_Class_UNIVERSAL.BMP_STRING + def __setattr__(self, name, value): + # type: (str, Any) -> None + if name == "val": + if isinstance(value, str): + value = value.encode("utf-16be") + object.__setattr__(self, name, value) + else: + object.__setattr__(self, name, value) + + def __repr__(self): + # type: () -> str + return "<%s[%r]>" % ( + self.__dict__.get("name", self.__class__.__name__), + self.val.decode("utf-16be"), + ) + -class ASN1_SEQUENCE(ASN1_Object): +class ASN1_SEQUENCE(ASN1_Object[List[Any]]): tag = ASN1_Class_UNIVERSAL.SEQUENCE def strshow(self, lvl=0): + # type: (int) -> str s = (" " * lvl) + ("# %s:" % self.__class__.__name__) + "\n" for o in self.val: s += o.strshow(lvl=lvl + 1) @@ -497,7 +742,7 @@ class ASN1_SET(ASN1_SEQUENCE): tag = ASN1_Class_UNIVERSAL.SET -class ASN1_IPADDRESS(ASN1_STRING): +class ASN1_IPADDRESS(ASN1_Object[str]): tag = ASN1_Class_UNIVERSAL.IPADDRESS @@ -505,6 +750,10 @@ class ASN1_COUNTER32(ASN1_INTEGER): tag = ASN1_Class_UNIVERSAL.COUNTER32 +class ASN1_COUNTER64(ASN1_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + class ASN1_GAUGE32(ASN1_INTEGER): tag = ASN1_Class_UNIVERSAL.GAUGE32 diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index d6c6b1498c1..23899274e4a 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -1,22 +1,47 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Modified by Maxence Tury +# Acknowledgment: Maxence Tury # Acknowledgment: Ralph Broenink -# This program is published under a GPLv2 license """ Basic Encoding Rules (BER) for ASN.1 """ -from __future__ import absolute_import +# Good read: https://luca.ntop.org/Teaching/Appunti/asn1.html + from scapy.error import warning from scapy.compat import chb, orb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa -from scapy.asn1.asn1 import ASN1_Decoding_Error, ASN1_Encoding_Error, \ - ASN1_BadTag_Decoding_Error, ASN1_Codecs, ASN1_Class_UNIVERSAL, \ - ASN1_Error, ASN1_DECODING_ERROR, ASN1_BADTAG -from scapy.modules import six +from scapy.asn1.asn1 import ( + ASN1Tag, + ASN1_BADTAG, + ASN1_BadTag_Decoding_Error, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_DECODING_ERROR, + ASN1_Decoding_Error, + ASN1_Encoding_Error, + ASN1_Error, + ASN1_Object, + _ASN1_ERROR, +) + +from typing import ( + Any, + AnyStr, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) ################## # BER encoding # @@ -31,14 +56,20 @@ class BER_Exception(Exception): class BER_Encoding_Error(ASN1_Encoding_Error): - def __init__(self, msg, encoded=None, remaining=None): + def __init__(self, + msg, # type: str + encoded=None, # type: Optional[Union[BERcodec_Object[Any], str]] # noqa: E501 + remaining=b"" # type: bytes + ): + # type: (...) -> None Exception.__init__(self, msg) self.remaining = remaining self.encoded = encoded def __str__(self): + # type: () -> str s = Exception.__str__(self) - if isinstance(self.encoded, BERcodec_Object): + if isinstance(self.encoded, ASN1_Object): s += "\n### Already encoded ###\n%s" % self.encoded.strshow() else: s += "\n### Already encoded ###\n%r" % self.encoded @@ -47,14 +78,20 @@ def __str__(self): class BER_Decoding_Error(ASN1_Decoding_Error): - def __init__(self, msg, decoded=None, remaining=None): + def __init__(self, + msg, # type: str + decoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None Exception.__init__(self, msg) self.remaining = remaining self.decoded = decoded def __str__(self): + # type: () -> str s = Exception.__str__(self) - if isinstance(self.decoded, BERcodec_Object): + if isinstance(self.decoded, ASN1_Object): s += "\n### Already decoded ###\n%s" % self.decoded.strshow() else: s += "\n### Already decoded ###\n%r" % self.decoded @@ -68,6 +105,10 @@ class BER_BadTag_Decoding_Error(BER_Decoding_Error, def BER_len_enc(ll, size=0): + # type: (int, Optional[int]) -> bytes + from scapy.config import conf + if size is None: + size = conf.ASN1_default_long_size if ll <= 127 and size == 0: return chb(ll) s = b"" @@ -84,6 +125,7 @@ def BER_len_enc(ll, size=0): def BER_len_dec(s): + # type: (bytes) -> Tuple[int, bytes] tmp_len = orb(s[0]) if not tmp_len & 0x80: return tmp_len, s[1:] @@ -102,7 +144,8 @@ def BER_len_dec(s): def BER_num_enc(ll, size=1): - x = [] + # type: (int, int) -> bytes + x = [] # type: List[int] while ll or size > 0: x.insert(0, ll & 0x7f) if len(x) > 1: @@ -113,6 +156,7 @@ def BER_num_enc(ll, size=1): def BER_num_dec(s, cls_id=0): + # type: (bytes, int) -> Tuple[int, bytes] if len(s) == 0: raise BER_Decoding_Error("BER_num_dec: got empty string", remaining=s) x = cls_id @@ -129,6 +173,7 @@ def BER_num_dec(s, cls_id=0): def BER_id_dec(s): + # type: (bytes) -> Tuple[int, bytes] # This returns the tag ALONG WITH THE PADDED CLASS+CONSTRUCTIVE INFO. # Let's recall that bits 8-7 from the first byte of the tag encode # the class information, while bit 6 means primitive or constructive. @@ -155,6 +200,7 @@ def BER_id_dec(s): def BER_id_enc(n): + # type: (int) -> bytes if n < 256: # low-tag-number return chb(n) @@ -170,25 +216,39 @@ def BER_id_enc(n): # The functions below provide implicit and explicit tagging support. -def BER_tagging_dec(s, hidden_tag=None, implicit_tag=None, - explicit_tag=None, safe=False): +def BER_tagging_dec(s, # type: bytes + hidden_tag=None, # type: Optional[int | ASN1Tag] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + safe=False, # type: Optional[bool] + _fname="", # type: str + ): + # type: (...) -> Tuple[Optional[int], bytes] # We output the 'real_tag' if it is different from the (im|ex)plicit_tag. + # 'hidden_tag' is the type tag that is implicited when 'implicit_tag' is used. real_tag = None if len(s) > 0: - err_msg = "BER_tagging_dec: observed tag does not match expected tag" + err_msg = ( + "BER_tagging_dec: observed tag 0x%.02x does not " + "match expected tag 0x%.02x (%s)" + ) if implicit_tag is not None: ber_id, s = BER_id_dec(s) if ber_id != implicit_tag: - if not safe: - raise BER_Decoding_Error(err_msg, remaining=s) + if not safe and ber_id != implicit_tag: + raise BER_Decoding_Error(err_msg % ( + ber_id, implicit_tag, _fname), + remaining=s) else: real_tag = ber_id - s = chb(hash(hidden_tag)) + s + s = chb(int(hidden_tag)) + s # type: ignore elif explicit_tag is not None: ber_id, s = BER_id_dec(s) if ber_id != explicit_tag: if not safe: - raise BER_Decoding_Error(err_msg, remaining=s) + raise BER_Decoding_Error( + err_msg % (ber_id, explicit_tag, _fname), + remaining=s) else: real_tag = ber_id l, s = BER_len_dec(s) @@ -196,6 +256,7 @@ def BER_tagging_dec(s, hidden_tag=None, implicit_tag=None, def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): + # type: (bytes, Optional[int], Optional[int]) -> bytes if len(s) > 0: if implicit_tag is not None: s = BER_id_enc(implicit_tag) + s[1:] @@ -207,8 +268,14 @@ def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): class BERcodec_metaclass(type): - def __new__(cls, name, bases, dct): - c = super(BERcodec_metaclass, cls).__new__(cls, name, bases, dct) + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[BERcodec_Object[Any]] + c = cast('Type[BERcodec_Object[Any]]', + super(BERcodec_metaclass, cls).__new__(cls, name, bases, dct)) try: c.tag.register(c.codec, c) except Exception: @@ -216,16 +283,21 @@ def __new__(cls, name, bases, dct): return c -class BERcodec_Object(six.with_metaclass(BERcodec_metaclass)): +_K = TypeVar('_K') + + +class BERcodec_Object(Generic[_K], metaclass=BERcodec_metaclass): codec = ASN1_Codecs.BER tag = ASN1_Class_UNIVERSAL.ANY @classmethod def asn1_object(cls, val): + # type: (_K) -> ASN1_Object[_K] return cls.tag.asn1_object(val) @classmethod def check_string(cls, s): + # type: (bytes) -> None if not s: raise BER_Decoding_Error( "%s: Got empty object while expecting tag %r" % @@ -234,6 +306,7 @@ def check_string(cls, s): @classmethod def check_type(cls, s): + # type: (bytes) -> bytes cls.check_string(s) tag, remainder = BER_id_dec(s) if not isinstance(tag, int) or cls.tag != tag: @@ -245,6 +318,7 @@ def check_type(cls, s): @classmethod def check_type_get_len(cls, s): + # type: (bytes) -> Tuple[int, bytes] s2 = cls.check_type(s) if not s2: raise BER_Decoding_Error("%s: No bytes while expecting a length" % @@ -253,6 +327,7 @@ def check_type_get_len(cls, s): @classmethod def check_type_check_len(cls, s): + # type: (bytes) -> Tuple[int, bytes, bytes] l, s3 = cls.check_type_get_len(s) if len(s3) < l: raise BER_Decoding_Error("%s: Got %i bytes while expecting %i" % @@ -260,48 +335,72 @@ def check_type_check_len(cls, s): return l, s3[:l], s3[l:] @classmethod - def do_dec(cls, s, context=None, safe=False): - if context is None: - context = cls.tag.context + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + if context is not None: + _context = context + else: + _context = cls.tag.context cls.check_string(s) p, remainder = BER_id_dec(s) - if p not in context: + if p not in _context: t = s if len(t) > 18: t = t[:15] + b"..." raise BER_Decoding_Error("Unknown prefix [%02x] for [%r]" % (p, t), remaining=s) - codec = context[p].get_codec(ASN1_Codecs.BER) + tag = _context[p] + codec = cast('Type[BERcodec_Object[_K]]', + tag.get_codec(ASN1_Codecs.BER)) if codec == BERcodec_Object: # Value type defined as Unknown l, s = BER_num_dec(remainder) return ASN1_BADTAG(s[:l]), s[l:] - return codec.dec(s, context, safe) + return codec.dec(s, _context, safe) @classmethod - def dec(cls, s, context=None, safe=False): + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] if not safe: return cls.do_dec(s, context, safe) try: return cls.do_dec(s, context, safe) except BER_BadTag_Decoding_Error as e: - o, remain = BERcodec_Object.dec(e.remaining, context, safe) + o, remain = BERcodec_Object.dec( + e.remaining, context, safe + ) # type: Tuple[ASN1_Object[Any], bytes] return ASN1_BADTAG(o), remain except BER_Decoding_Error as e: - return ASN1_DECODING_ERROR(s, exc=e), "" + return ASN1_DECODING_ERROR(s, exc=e), b"" except ASN1_Error as e: - return ASN1_DECODING_ERROR(s, exc=e), "" + return ASN1_DECODING_ERROR(s, exc=e), b"" @classmethod - def safedec(cls, s, context=None): + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] return cls.dec(s, context, safe=True) @classmethod - def enc(cls, s): - if isinstance(s, six.string_types + (bytes,)): - return BERcodec_STRING.enc(s) + def enc(cls, s, size_len=0): + # type: (_K, Optional[int]) -> bytes + if isinstance(s, (str, bytes)): + return BERcodec_STRING.enc(s, size_len=size_len) else: - return BERcodec_INTEGER.enc(int(s)) + try: + return BERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore + except TypeError: + raise TypeError("Trying to encode an invalid value !") ASN1_Codecs.BER.register_stem(BERcodec_Object) @@ -311,29 +410,35 @@ def enc(cls, s): # BERcodec objects # ########################## -class BERcodec_INTEGER(BERcodec_Object): +class BERcodec_INTEGER(BERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @classmethod - def enc(cls, i): - s = [] + def enc(cls, i, size_len=0): + # type: (int, Optional[int]) -> bytes + ls = [] while True: - s.append(i & 0xff) + ls.append(i & 0xff) if -127 <= i < 0: break if 128 <= i <= 255: - s.append(0) + ls.append(0) i >>= 8 if not i: break - s = [chb(hash(c)) for c in s] - s.append(BER_len_enc(len(s))) - s.append(chb(hash(cls.tag))) + s = [chb(int(c)) for c in ls] + s.append(BER_len_enc(len(s), size=size_len)) + s.append(chb(int(cls.tag))) s.reverse() return b"".join(s) @classmethod - def do_dec(cls, s, context=None, safe=False): + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] l, s, t = cls.check_type_check_len(s) x = 0 if s: @@ -349,11 +454,16 @@ class BERcodec_BOOLEAN(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.BOOLEAN -class BERcodec_BIT_STRING(BERcodec_Object): +class BERcodec_BIT_STRING(BERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.BIT_STRING @classmethod - def do_dec(cls, s, context=None, safe=False): + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[str], bytes] # /!\ the unused_bits information is lost after this decoding l, s, t = cls.check_type_check_len(s) if len(s) > 0: @@ -363,10 +473,10 @@ def do_dec(cls, s, context=None, safe=False): "BERcodec_BIT_STRING: too many unused_bits advertised", remaining=s ) - s = "".join(binrepr(orb(x)).zfill(8) for x in s[1:]) + fs = "".join(binrepr(orb(x)).zfill(8) for x in s[1:]) if unused_bits > 0: - s = s[:-unused_bits] - return cls.tag.asn1_object(s), t + fs = fs[:-unused_bits] + return cls.tag.asn1_object(fs), t else: raise BER_Decoding_Error( "BERcodec_BIT_STRING found no content " @@ -375,9 +485,10 @@ def do_dec(cls, s, context=None, safe=False): ) @classmethod - def enc(cls, s): + def enc(cls, _s, size_len=0): + # type: (AnyStr, Optional[int]) -> bytes # /!\ this is DER encoding (bit strings are only zero-bit padded) - s = bytes_encode(s) + s = bytes_encode(_s) if len(s) % 8 == 0: unused_bits = 0 else: @@ -386,20 +497,26 @@ def enc(cls, s): s = b"".join(chb(int(b"".join(chb(y) for y in x), 2)) for x in zip(*[iter(s)] * 8)) s = chb(unused_bits) + s - return chb(hash(cls.tag)) + BER_len_enc(len(s)) + s + return chb(int(cls.tag)) + BER_len_enc(len(s), size=size_len) + s -class BERcodec_STRING(BERcodec_Object): +class BERcodec_STRING(BERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.STRING @classmethod - def enc(cls, s): - s = bytes_encode(s) + def enc(cls, _s, size_len=0): + # type: (Union[str, bytes], Optional[int]) -> bytes + s = bytes_encode(_s) # Be sure we are encoding bytes - return chb(hash(cls.tag)) + BER_len_enc(len(s)) + s + return chb(int(cls.tag)) + BER_len_enc(len(s), size=size_len) + s @classmethod - def do_dec(cls, s, context=None, safe=False): + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] l, s, t = cls.check_type_check_len(s) return cls.tag.asn1_object(s), t @@ -408,31 +525,38 @@ class BERcodec_NULL(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def enc(cls, i): + def enc(cls, i, size_len=0): + # type: (int, Optional[int]) -> bytes if i == 0: - return chb(hash(cls.tag)) + b"\0" + return chb(int(cls.tag)) + b"\0" else: - return super(cls, cls).enc(i) + return super(cls, cls).enc(i, size_len=size_len) -class BERcodec_OID(BERcodec_Object): +class BERcodec_OID(BERcodec_Object[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, oid): - oid = bytes_encode(oid) + def enc(cls, _oid, size_len=0): + # type: (AnyStr, Optional[int]) -> bytes + oid = bytes_encode(_oid) if oid: lst = [int(x) for x in oid.strip(b".").split(b".")] else: lst = list() if len(lst) >= 2: lst[1] += 40 * lst[0] - del(lst[0]) + del lst[0] s = b"".join(BER_num_enc(k) for k in lst) - return chb(hash(cls.tag)) + BER_len_enc(len(s)) + s + return chb(int(cls.tag)) + BER_len_enc(len(s), size=size_len) + s @classmethod - def do_dec(cls, s, context=None, safe=False): + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[bytes], bytes] l, s, t = cls.check_type_check_len(s) lst = [] while s: @@ -475,6 +599,10 @@ class BERcodec_IA5_STRING(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IA5_STRING +class BERcodec_GENERAL_STRING(BERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + class BERcodec_UTC_TIME(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.UTC_TIME @@ -495,17 +623,25 @@ class BERcodec_BMP_STRING(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.BMP_STRING -class BERcodec_SEQUENCE(BERcodec_Object): +class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]]]): # noqa: E501 tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def enc(cls, ll): - if not isinstance(ll, bytes): - ll = b"".join(x.enc(cls.codec) for x in ll) - return chb(hash(cls.tag)) + BER_len_enc(len(ll)) + ll + def enc(cls, _ll, size_len=0): + # type: (Union[bytes, List[BERcodec_Object[Any]]], Optional[int]) -> bytes + if isinstance(_ll, bytes): + ll = _ll + else: + ll = b"".join(x.enc(cls.codec) for x in _ll) + return chb(int(cls.tag)) + BER_len_enc(len(ll), size=size_len) + ll @classmethod - def do_dec(cls, s, context=None, safe=False): + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] if context is None: context = cls.tag.context ll, st = cls.check_type_get_len(s) # we may have len(s) < ll @@ -513,7 +649,10 @@ def do_dec(cls, s, context=None, safe=False): obj = [] while s: try: - o, s = BERcodec_Object.dec(s, context, safe) + o, remain = BERcodec_Object.dec( + s, context, safe + ) # type: Tuple[ASN1_Object[Any], bytes] + s = remain except BER_Decoding_Error as err: err.remaining += t if err.decoded is not None: @@ -535,15 +674,17 @@ class BERcodec_IPADDRESS(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii): + def enc(cls, ipaddr_ascii, size_len=0): # type: ignore + # type: (str, Optional[int]) -> bytes try: s = inet_aton(ipaddr_ascii) except Exception: raise BER_Encoding_Error("IPv4 address could not be encoded") - return chb(hash(cls.tag)) + BER_len_enc(len(s)) + s + return chb(int(cls.tag)) + BER_len_enc(len(s), size=size_len) + s @classmethod def do_dec(cls, s, context=None, safe=False): + # type: (bytes, Optional[Any], bool) -> Tuple[ASN1_Object[str], bytes] l, s, t = cls.check_type_check_len(s) try: ipaddr_ascii = inet_ntoa(s) @@ -557,6 +698,10 @@ class BERcodec_COUNTER32(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.COUNTER32 +class BERcodec_COUNTER64(BERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + class BERcodec_GAUGE32(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.GAUGE32 diff --git a/scapy/asn1/mib.py b/scapy/asn1/mib.py index f9df6a3a32e..b164fcc9c44 100644 --- a/scapy/asn1/mib.py +++ b/scapy/asn1/mib.py @@ -1,39 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Modified by Maxence Tury -# This program is published under a GPLv2 license +# Acknowledgment: Maxence Tury """ Management Information Base (MIB) parsing """ -from __future__ import absolute_import import re from glob import glob from scapy.dadict import DADict, fixname from scapy.config import conf from scapy.utils import do_graph -import scapy.modules.six as six from scapy.compat import plain_str +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, +) + ################# # MIB parsing # ################# _mib_re_integer = re.compile(r"^[0-9]+$") _mib_re_both = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_-]*)\(([0-9]+)\)$") -_mib_re_oiddecl = re.compile(r"$\s*([a-zA-Z0-9_-]+)\s+OBJECT([^:\{\}]|\{[^:]+\})+::=\s*\{([^\}]+)\}", re.M) # noqa: E501 +_mib_re_oiddecl = re.compile( + r"$\s*([a-zA-Z0-9_-]+)\s+OBJECT[^:\{\}]+::=\s*\{([^\}]+)\}", re.M) _mib_re_strings = re.compile(r'"[^"]*"') _mib_re_comments = re.compile(r'--.*(\r|\n)') -class MIBDict(DADict): - def fixname(self, val): - # We overwrite DADict fixname method as we want to keep - in names - return val - +class MIBDict(DADict[str, str]): def _findroot(self, x): + # type: (str) -> Tuple[str, str, str] """Internal MIBDict function used to find a partial OID""" if x.startswith("."): x = x[1:] @@ -42,7 +46,7 @@ def _findroot(self, x): max = 0 root = "." root_key = "" - for k in six.iterkeys(self): + for k in self: if x.startswith(k + "."): if max < len(k): max = len(k) @@ -51,29 +55,32 @@ def _findroot(self, x): return root, root_key, x[max:-1] def _oidname(self, x): + # type: (str) -> str """Deduce the OID name from its OID ID""" root, _, remainder = self._findroot(x) return root + remainder def _oid(self, x): + # type: (str) -> str """Parse the OID id/OID generator, and return real OID""" xl = x.strip(".").split(".") p = len(xl) - 1 while p >= 0 and _mib_re_integer.match(xl[p]): p -= 1 - if p != 0 or xl[p] not in six.itervalues(self.__dict__): + if p != 0 or xl[p] not in self.d.values(): return x - xl[p] = next(k for k, v in six.iteritems(self.__dict__) if v == xl[p]) + xl[p] = next(k for k, v in self.d.items() if v == xl[p]) return ".".join(xl[p:]) def _make_graph(self, other_keys=None, **kargs): + # type: (Optional[Any], **Any) -> None if other_keys is None: other_keys = [] nodes = [(self[key], key) for key in self.iterkeys()] oids = set(self.iterkeys()) for k in other_keys: if k not in oids: - nodes.append(self.oidname(k), k) + nodes.append((self._oidname(k), k)) s = 'digraph "mib" {\n\trankdir=LR;\n\n' for k, o in nodes: s += '\t"%s" [ label="%s" ];\n' % (o, k) @@ -88,12 +95,27 @@ def _make_graph(self, other_keys=None, **kargs): do_graph(s, **kargs) -def _mib_register(ident, value, the_mib, unresolved): - """Internal function used to register an OID and its name in a MIBDict""" - if ident in the_mib or ident in unresolved: - return ident in the_mib +def _mib_register(ident, # type: str + value, # type: List[str] + the_mib, # type: Dict[str, List[str]] + unresolved, # type: Dict[str, List[str]] + alias, # type: Dict[str, str] + ): + # type: (...) -> bool + """ + Internal function used to register an OID and its name in a MIBDict + """ + if ident in the_mib: + # We have already resolved this one. Store the alias + alias[".".join(value)] = ident + return True + if ident in unresolved: + # We know we can't resolve this one + return False resval = [] not_resolved = 0 + # Resolve the OID + # (e.g. 2.basicConstraints.3 -> 2.2.5.29.19.3) for v in value: if _mib_re_integer.match(v): resval.append(v) @@ -102,25 +124,28 @@ def _mib_register(ident, value, the_mib, unresolved): if v not in the_mib: not_resolved = 1 if v in the_mib: - v = the_mib[v] + resval += the_mib[v] elif v in unresolved: - v = unresolved[v] - if isinstance(v, list): - resval += v + resval += unresolved[v] else: resval.append(v) if not_resolved: + # Unresolved unresolved[ident] = resval return False else: + # Fully resolved the_mib[ident] = resval keys = list(unresolved) i = 0 + # Go through the unresolved to update the ones that + # depended on the one we just did while i < len(keys): k = keys[i] - if _mib_register(k, unresolved[k], the_mib, {}): - del(unresolved[k]) - del(keys[i]) + if _mib_register(k, unresolved[k], the_mib, {}, alias): + # Now resolved: we can remove it from unresolved + del unresolved[k] + del keys[i] i = 0 else: i += 1 @@ -129,35 +154,51 @@ def _mib_register(ident, value, the_mib, unresolved): def load_mib(filenames): - """Load the conf.mib dict from a list of filenames""" + # type: (str) -> None + """ + Load the conf.mib dict from a list of filenames + """ the_mib = {'iso': ['1']} - unresolved = {} - for k in six.iterkeys(conf.mib): - _mib_register(conf.mib[k], k.split("."), the_mib, unresolved) + unresolved = {} # type: Dict[str, List[str]] + alias = {} # type: Dict[str, str] + # Export the current MIB to a working dictionary + for k in conf.mib: + _mib_register(conf.mib[k], k.split("."), the_mib, unresolved, alias) + # Read the files if isinstance(filenames, (str, bytes)): - filenames = [filenames] - for fnames in filenames: + files_list = [filenames] + else: + files_list = filenames + for fnames in files_list: for fname in glob(fnames): with open(fname) as f: text = f.read() - cleantext = " ".join(_mib_re_strings.split(" ".join(_mib_re_comments.split(text)))) # noqa: E501 + cleantext = " ".join( + _mib_re_strings.split(" ".join(_mib_re_comments.split(text))) + ) for m in _mib_re_oiddecl.finditer(cleantext): gr = m.groups() - ident, oid = gr[0], gr[-1] + ident, oid_s = gr[0], gr[-1] ident = fixname(ident) - oid = oid.split() - for i, elt in enumerate(oid): - m = _mib_re_both.match(elt) - if m: - oid[i] = m.groups()[1] - _mib_register(ident, oid, the_mib, unresolved) - + oid_l = oid_s.split() + for i, elt in enumerate(oid_l): + m2 = _mib_re_both.match(elt) + if m2: + oid_l[i] = m2.groups()[1] + _mib_register(ident, oid_l, the_mib, unresolved, alias) + + # Create the new MIB newmib = MIBDict(_name="MIB") - for oid, key in six.iteritems(the_mib): + # Add resolved values + for oid, key in the_mib.items(): newmib[".".join(key)] = oid - for oid, key in six.iteritems(unresolved): + # Add unresolved values + for oid, key in unresolved.items(): newmib[".".join(key)] = oid + # Add aliases + for key_s, oid in alias.items(): + newmib[key_s] = oid conf.mib = newmib @@ -169,6 +210,7 @@ def load_mib(filenames): # pkcs1 # pkcs1_oids = { + "1.2.840.113549.1.1": "pkcs1", "1.2.840.113549.1.1.1": "rsaEncryption", "1.2.840.113549.1.1.2": "md2WithRSAEncryption", "1.2.840.113549.1.1.3": "md4WithRSAEncryption", @@ -188,12 +230,79 @@ def load_mib(filenames): # secsig oiw # secsig_oids = { - "1.3.14.3.2.26": "sha1" + "1.3.14.3.2": "OIWSEC", + "1.3.14.3.2.2": "md4RSA", + "1.3.14.3.2.3": "md5RSA", + "1.3.14.3.2.4": "md4RSA2", + "1.3.14.3.2.6": "desECB", + "1.3.14.3.2.7": "desCBC", + "1.3.14.3.2.8": "desOFB", + "1.3.14.3.2.9": "desCFB", + "1.3.14.3.2.10": "desMAC", + "1.3.14.3.2.11": "rsaSign", + "1.3.14.3.2.12": "dsa", + "1.3.14.3.2.13": "shaDSA", + "1.3.14.3.2.14": "mdc2RSA", + "1.3.14.3.2.15": "shaRSA", + "1.3.14.3.2.16": "dhCommMod", + "1.3.14.3.2.17": "desEDE", + "1.3.14.3.2.18": "sha", + "1.3.14.3.2.19": "mdc2", + "1.3.14.3.2.20": "dsaComm", + "1.3.14.3.2.21": "dsaCommSHA", + "1.3.14.3.2.22": "rsaXchg", + "1.3.14.3.2.23": "keyHashSeal", + "1.3.14.3.2.24": "md2RSASign", + "1.3.14.3.2.25": "md5RSASign", + "1.3.14.3.2.26": "sha1", + "1.3.14.3.2.27": "dsaSHA1", + "1.3.14.3.2.28": "dsaCommSHA1", + "1.3.14.3.2.29": "sha1RSASign", +} + +# nist # + +nist_oids = { + "2.16.840.1.101.3.4.2.1": "sha256", + "2.16.840.1.101.3.4.2.2": "sha384", + "2.16.840.1.101.3.4.2.3": "sha512", + "2.16.840.1.101.3.4.2.4": "sha224", + "2.16.840.1.101.3.4.2.5": "sha512-224", + "2.16.840.1.101.3.4.2.6": "sba512-256", + "2.16.840.1.101.3.4.2.7": "sha3-224", + "2.16.840.1.101.3.4.2.8": "sha3-256", + "2.16.840.1.101.3.4.2.9": "sha3-384", + "2.16.840.1.101.3.4.2.10": "sha3-512", + "2.16.840.1.101.3.4.2.11": "shake128", + "2.16.840.1.101.3.4.2.12": "shake256", +} + +# thawte # + +thawte_oids = { + "1.3.101.112": "Ed25519", + "1.3.101.113": "Ed448", +} + +# pkcs3 # + +pkcs3_oids = { + "1.2.840.113549.1.3": "pkcs-3", + "1.2.840.113549.1.3.1": "dhKeyAgreement", +} + +# pkcs7 # + +pkcs7_oids = { + "1.2.840.113549.1.7": "pkcs-7", + "1.2.840.113549.1.7.2": "id-signedData", + "1.2.840.113549.1.7.3": "id-envelopedData", } # pkcs9 # pkcs9_oids = { + "1.2.840.113549.1.9": "pkcs-9", "1.2.840.113549.1.9.0": "modules", "1.2.840.113549.1.9.1": "emailAddress", "1.2.840.113549.1.9.2": "unstructuredName", @@ -220,6 +329,13 @@ def load_mib(filenames): "1.2.840.113549.1.9.52": "id-aa-CMSAlgorithmProtection" } +# enc algs # + +encAlgs_oids = { + "1.2.840.113549.3.4": "rc4", + "1.2.840.113549.3.7": "des-ede3-cbc", +} + # x509 # attributeType_oids = { @@ -320,20 +436,22 @@ def load_mib(filenames): "2.5.4.94": "epcInUrn", "2.5.4.95": "ldapUrl", "2.5.4.96": "ldapUrl", - "2.5.4.97": "organizationIdentifier" + "2.5.4.97": "organizationIdentifier", + # RFC 4519 + "0.9.2342.19200300.100.1.25": "dc", } certificateExtension_oids = { - "2.5.29.1": "authorityKeyIdentifier", + "2.5.29.1": "authorityKeyIdentifier(obsolete)", "2.5.29.2": "keyAttributes", - "2.5.29.3": "certificatePolicies", + "2.5.29.3": "certificatePolicies(obsolete)", "2.5.29.4": "keyUsageRestriction", "2.5.29.5": "policyMapping", "2.5.29.6": "subtreesConstraint", - "2.5.29.7": "subjectAltName", - "2.5.29.8": "issuerAltName", + "2.5.29.7": "subjectAltName(obsolete)", + "2.5.29.8": "issuerAltName(obsolete)", "2.5.29.9": "subjectDirectoryAttributes", - "2.5.29.10": "basicConstraints", + "2.5.29.10": "basicConstraints(obsolete)", "2.5.29.14": "subjectKeyIdentifier", "2.5.29.15": "keyUsage", "2.5.29.16": "privateKeyUsagePeriod", @@ -345,8 +463,8 @@ def load_mib(filenames): "2.5.29.22": "expirationDate", "2.5.29.23": "instructionCode", "2.5.29.24": "invalidityDate", - "2.5.29.25": "cRLDistributionPoints", - "2.5.29.26": "issuingDistributionPoint", + "2.5.29.25": "cRLDistributionPoints(obsolete)", + "2.5.29.26": "issuingDistributionPoint(obsolete)", "2.5.29.27": "deltaCRLIndicator", "2.5.29.28": "issuingDistributionPoint", "2.5.29.29": "certificateIssuer", @@ -354,7 +472,7 @@ def load_mib(filenames): "2.5.29.31": "cRLDistributionPoints", "2.5.29.32": "certificatePolicies", "2.5.29.33": "policyMappings", - "2.5.29.34": "policyConstraints", + "2.5.29.34": "policyConstraints(obsolete)", "2.5.29.35": "authorityKeyIdentifier", "2.5.29.36": "policyConstraints", "2.5.29.37": "extKeyUsage", @@ -389,7 +507,25 @@ def load_mib(filenames): "2.5.29.66": "id-ce-groupAC", "2.5.29.67": "id-ce-allowedAttAss", "2.5.29.68": "id-ce-attributeMappings", - "2.5.29.69": "id-ce-holderNameConstraints" + "2.5.29.69": "id-ce-holderNameConstraints", + # [MS-WCCE] + wincrypt.h + "1.3.6.1.4.1.311.2.1.14": "OID_CERT_EXTENSIONS", + "1.3.6.1.4.1.311.10.3.4": "OID_EFS_CRYPTO", + "1.3.6.1.4.1.311.13.2.1": "OID_ENROLLMENT_NAME_VALUE_PAIR", + "1.3.6.1.4.1.311.13.2.2": "OID_ENROLLMENT_CSP_PROVIDER", + "1.3.6.1.4.1.311.13.2.3": "OID_OS_VERSION", + "1.3.6.1.4.1.311.10.10.1": "OID_CMC_ADD_ATTRIBUTES", + "1.3.6.1.4.1.311.20.2": "ENROLL_CERTTYPE", + "1.3.6.1.4.1.311.21.10": "OID_APPLICATION_CERT_POLICIES", + "1.3.6.1.4.1.311.21.20": "OID_REQUEST_CLIENT_INFO", + "1.3.6.1.4.1.311.21.23": "OID_ENROLL_EK_INFO", + "1.3.6.1.4.1.311.21.24": "OID_ENROLL_ATTESTATION_STATEMENT", + "1.3.6.1.4.1.311.21.25": "OID_ENROLL_KSP_NAME", + "1.3.6.1.4.1.311.21.39": "OID_ENROLL_AIK_INFO", + "1.3.6.1.4.1.311.21.7": "OID_CERTIFICATE_TEMPLATE", + "1.3.6.1.4.1.311.25.1": "NTDS_REPLICATION", + "1.3.6.1.4.1.311.25.2": "NTDS_CA_SECURITY_EXT", + "1.3.6.1.4.1.311.25.2.1": "NTDS_OBJECTSID", } certExt_oids = { @@ -442,6 +578,15 @@ def load_mib(filenames): "1.3.6.1.5.5.7.3.22": "secureShellServer" } +certPkixCmc_oids = { + "1.3.6.1.5.5.7.7.8": "id-cmc-addExtensions", +} + +certPkixCct_oids = { + "1.3.6.1.5.5.7.12.2": "id-cct-PKIData", + "1.3.6.1.5.5.7.12.3": "id-cct-PKIResponse", +} + certPkixAd_oids = { "1.3.6.1.5.5.7.48.1": "ocsp", "1.3.6.1.5.5.7.48.2": "caIssuers", @@ -454,6 +599,15 @@ def load_mib(filenames): "1.3.6.1.5.5.7.48.1.1": "basic-response" } +certIpsec_oids = { + "1.3.6.1.5.5.8.2.1": "iKEEnd", + "1.3.6.1.5.5.8.2.2": "iKEIntermediate", +} + +certTransp_oids = { + '1.3.6.1.4.1.11129.2.4.2': "SignedCertificateTimestampList", +} + # ansi-x962 # x962KeyType_oids = { @@ -471,6 +625,12 @@ def load_mib(filenames): "1.2.840.10045.4.3.4": "ecdsa-with-SHA512" } +# ansi-x942 # + +x942KeyType_oids = { + "1.2.840.10046.2.1": "dhpublicnumber", # RFC3770 sect 4.1.1 +} + # elliptic curves # ansiX962Curve_oids = { @@ -519,7 +679,7 @@ def load_mib(filenames): # policies # certPolicy_oids = { - "anyPolicy": "2.5.29.32.0" + "2.5.29.32.0": "anyPolicy" } # from Chromium source code (ev_root_ca_metadata.cc) @@ -527,19 +687,19 @@ def load_mib(filenames): '1.2.392.200091.100.721.1': 'EV Security Communication RootCA1', '1.2.616.1.113527.2.5.1.1': 'EV Certum Trusted Network CA', '1.3.159.1.17.1': 'EV Actualis Authentication Root CA', - '1.3.6.1.4.1.13177.10.1.3.10': 'EV Autoridad de Certificacion Firmaprofesional CIF A62634068', # noqa: E501 + '1.3.6.1.4.1.13177.10.1.3.10': 'EV Autoridad de Certificacion Firmaprofesional CIF A62634068', '1.3.6.1.4.1.14370.1.6': 'EV GeoTrust Primary Certification Authority', '1.3.6.1.4.1.14777.6.1.1': 'EV Izenpe.com roots Business', '1.3.6.1.4.1.14777.6.1.2': 'EV Izenpe.com roots Government', - '1.3.6.1.4.1.17326.10.14.2.1.2': 'EV AC Camerfirma S.A. Chambers of Commerce Root - 2008', # noqa: E501 - '1.3.6.1.4.1.17326.10.14.2.2.2': 'EV AC Camerfirma S.A. Chambers of Commerce Root - 2008', # noqa: E501 - '1.3.6.1.4.1.17326.10.8.12.1.2': 'EV AC Camerfirma S.A. Global Chambersign Root - 2008', # noqa: E501 - '1.3.6.1.4.1.17326.10.8.12.2.2': 'EV AC Camerfirma S.A. Global Chambersign Root - 2008', # noqa: E501 - '1.3.6.1.4.1.22234.2.5.2.3.1': 'EV CertPlus Class 2 Primary CA (KEYNECTIS)', # noqa: E501 + '1.3.6.1.4.1.17326.10.14.2.1.2': 'EV AC Camerfirma S.A. Chambers of Commerce Root - 2008', + '1.3.6.1.4.1.17326.10.14.2.2.2': 'EV AC Camerfirma S.A. Chambers of Commerce Root - 2008', + '1.3.6.1.4.1.17326.10.8.12.1.2': 'EV AC Camerfirma S.A. Global Chambersign Root - 2008', + '1.3.6.1.4.1.17326.10.8.12.2.2': 'EV AC Camerfirma S.A. Global Chambersign Root - 2008', + '1.3.6.1.4.1.22234.2.5.2.3.1': 'EV CertPlus Class 2 Primary CA (KEYNECTIS)', '1.3.6.1.4.1.23223.1.1.1': 'EV StartCom Certification Authority', - '1.3.6.1.4.1.29836.1.10': 'EV China Internet Network Information Center EV Certificates Root', # noqa: E501 + '1.3.6.1.4.1.29836.1.10': 'EV China Internet Network Information Center EV Certificates Root', '1.3.6.1.4.1.311.60.2.1.1': 'jurisdictionOfIncorporationLocalityName', - '1.3.6.1.4.1.311.60.2.1.2': 'jurisdictionOfIncorporationStateOrProvinceName', # noqa: E501 + '1.3.6.1.4.1.311.60.2.1.2': 'jurisdictionOfIncorporationStateOrProvinceName', '1.3.6.1.4.1.311.60.2.1.3': 'jurisdictionOfIncorporationCountryName', '1.3.6.1.4.1.34697.2.1': 'EV AffirmTrust Commercial', '1.3.6.1.4.1.34697.2.2': 'EV AffirmTrust Networking', @@ -563,32 +723,72 @@ def load_mib(filenames): '2.16.840.1.113733.1.7.23.6': 'EV VeriSign Certification Authorities', '2.16.840.1.113733.1.7.48.1': 'EV thawte CAs', '2.16.840.1.114028.10.1.2': 'EV Entrust Certification Authority', - '2.16.840.1.114171.500.9': 'EV Wells Fargo WellsSecure Public Root Certification Authority', # noqa: E501 + '2.16.840.1.114171.500.9': 'EV Wells Fargo WellsSecure Public Root Certification Authority', '2.16.840.1.114404.1.1.2.4.1': 'EV XRamp Global Certification Authority', '2.16.840.1.114412.2.1': 'EV DigiCert High Assurance EV Root CA', - '2.16.840.1.114413.1.7.23.3': 'EV ValiCert Class 2 Policy Validation Authority', # noqa: E501 + '2.16.840.1.114413.1.7.23.3': 'EV ValiCert Class 2 Policy Validation Authority', '2.16.840.1.114414.1.7.23.3': 'EV Starfield Certificate Authority', - '2.16.840.1.114414.1.7.24.3': 'EV Starfield Service Certificate Authority' # noqa: E501 + '2.16.840.1.114414.1.7.24.3': 'EV Starfield Service Certificate Authority' +} + +# gssapi # + +gssapi_oids = { + '1.2.840.48018.1.2.2': 'MS KRB5 - Microsoft Kerberos 5', + '1.2.840.113554.1.2.2': 'Kerberos 5', + '1.2.840.113554.1.2.2.3': 'Kerberos 5 - User to User', + '1.3.6.1.5.2.5': 'Kerberos 5 - IAKERB', + '1.3.6.1.5.5.2': 'SPNEGO - Simple Protected Negotiation', + '1.3.6.1.4.1.311.2.2.10': 'NTLMSSP - Microsoft NTLM Security Support Provider', + '1.3.6.1.4.1.311.2.2.30': 'NEGOEX - SPNEGO Extended Negotiation Security Mechanism', +} + +# kerberos # + +kerberos_oids = { + "1.3.6.1.5.2.3.1": "id-pkinit-authData", + "1.3.6.1.5.2.3.2": "id-pkinit-DHKeyData", + "1.3.6.1.5.2.3.3": "id-pkinit-rkeyData", + "1.3.6.1.5.2.3.4": "id-pkinit-KPClientAuth", + "1.3.6.1.5.2.3.5": "id-pkinit-KPKdc", + # RFC8363 + "1.3.6.1.5.2.3.6": "id-pkinit-kdf", + "1.3.6.1.5.2.3.6.1": "id-pkinit-kdf-sha1", + "1.3.6.1.5.2.3.6.2": "id-pkinit-kdf-sha256", + "1.3.6.1.5.2.3.6.3": "id-pkinit-kdf-sha512", + "1.3.6.1.5.2.3.6.4": "id-pkinit-kdf-sha384", } x509_oids_sets = [ pkcs1_oids, secsig_oids, + nist_oids, + thawte_oids, + pkcs3_oids, + pkcs7_oids, pkcs9_oids, + encAlgs_oids, attributeType_oids, certificateExtension_oids, certExt_oids, + certPkixAd_oids, + certPkixKp_oids, + certPkixCmc_oids, + certPkixCct_oids, certPkixPe_oids, certPkixQt_oids, - certPkixKp_oids, - certPkixAd_oids, certPolicy_oids, + certIpsec_oids, + certTransp_oids, evPolicy_oids, x962KeyType_oids, x962Signature_oids, + x942KeyType_oids, ansiX962Curve_oids, - certicomCurve_oids + certicomCurve_oids, + gssapi_oids, + kerberos_oids, ] x509_oids = {} @@ -607,6 +807,7 @@ def load_mib(filenames): # of some algorithms from pkcs1_oids and x962Signature_oids. hash_by_oid = { + "1.2.840.113549.1.1.1": "sha1", "1.2.840.113549.1.1.2": "md2", "1.2.840.113549.1.1.3": "md4", "1.2.840.113549.1.1.4": "md5", diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index d3fdd7d6d2d..9a5284bddfc 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -1,27 +1,66 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Enhanced by Maxence Tury -# This program is published under a GPLv2 license +# Acknowledgment: Maxence Tury """ Classes that implement ASN.1 data structures. """ -from __future__ import absolute_import -from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_NULL, ASN1_Error, \ - ASN1_Object, ASN1_INTEGER -from scapy.asn1.ber import BER_tagging_dec, BER_Decoding_Error, BER_id_dec, \ - BER_tagging_enc -from scapy.volatile import RandInt, RandChoice, RandNum, RandString, RandOID, \ - GeneralizedTime -from scapy.compat import orb, raw +import copy + +from functools import reduce + +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_BOOLEAN, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Error, + ASN1_INTEGER, + ASN1_NULL, + ASN1_OID, + ASN1_Object, + ASN1_STRING, +) +from scapy.asn1.ber import ( + BER_Decoding_Error, + BER_id_dec, + BER_tagging_dec, + BER_tagging_enc, +) from scapy.base_classes import BasePacket -from scapy.utils import binrepr +from scapy.volatile import ( + GeneralizedTime, + RandChoice, + RandInt, + RandNum, + RandOID, + RandString, + RandField, +) + from scapy import packet -from functools import reduce -import scapy.modules.six as six -from scapy.modules.six.moves import range + +from typing import ( + Any, + AnyStr, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.asn1packet import ASN1_Packet class ASN1F_badsequence(Exception): @@ -36,42 +75,60 @@ class ASN1F_element(object): # Basic ASN1 Field # ########################## -class ASN1F_field(ASN1F_element): +_I = TypeVar('_I') # Internal storage +_A = TypeVar('_A') # ASN.1 object + + +class ASN1F_field(ASN1F_element, Generic[_I, _A]): holds_packets = 0 islist = 0 ASN1_tag = ASN1_Class_UNIVERSAL.ANY - context = ASN1_Class_UNIVERSAL - - def __init__(self, name, default, context=None, - implicit_tag=None, explicit_tag=None, - flexible_tag=False): - self.context = context + context = ASN1_Class_UNIVERSAL # type: Type[ASN1_Class] + + def __init__(self, + name, # type: str + default, # type: Optional[_A] + context=None, # type: Optional[Type[ASN1_Class]] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + flexible_tag=False, # type: Optional[bool] + size_len=None, # type: Optional[int] + ): + # type: (...) -> None + if context is not None: + self.context = context self.name = name if default is None: - self.default = None + self.default = default # type: Optional[_A] elif isinstance(default, ASN1_NULL): - self.default = default + self.default = default # type: ignore else: - self.default = self.ASN1_tag.asn1_object(default) + self.default = self.ASN1_tag.asn1_object(default) # type: ignore + self.size_len = size_len self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" raise ASN1_Error(err_msg) - self.implicit_tag = implicit_tag - self.explicit_tag = explicit_tag + self.implicit_tag = implicit_tag and int(implicit_tag) + self.explicit_tag = explicit_tag and int(explicit_tag) # network_tag gets useful for ASN1F_CHOICE - self.network_tag = implicit_tag or explicit_tag or self.ASN1_tag + self.network_tag = int(implicit_tag or explicit_tag or self.ASN1_tag) + self.owners = [] # type: List[Type[ASN1_Packet]] + + def register_owner(self, cls): + # type: (Type[ASN1_Packet]) -> None + self.owners.append(cls) def i2repr(self, pkt, x): + # type: (ASN1_Packet, _I) -> str return repr(x) def i2h(self, pkt, x): - return x - - def any2i(self, pkt, x): + # type: (ASN1_Packet, _I) -> Any return x def m2i(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[_A, bytes] """ The good thing about safedec is that it may still decode ASN1 even if there is a mismatch between the expected tag (self.ASN1_tag) @@ -87,7 +144,8 @@ def m2i(self, pkt, s): diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, - safe=self.flexible_tag) + safe=self.flexible_tag, + _fname=self.name) if diff_tag is not None: # this implies that flexible_tag was True if self.implicit_tag is not None: @@ -96,11 +154,12 @@ def m2i(self, pkt, s): self.explicit_tag = diff_tag codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) if self.flexible_tag: - return codec.safedec(s, context=self.context) + return codec.safedec(s, context=self.context) # type: ignore else: - return codec.dec(s, context=self.context) + return codec.dec(s, context=self.context) # type: ignore def i2m(self, pkt, x): + # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes if x is None: return b"" if isinstance(x, ASN1_Object): @@ -112,101 +171,143 @@ def i2m(self, pkt, x): else: raise ASN1_Error("Encoding Error: got %r instead of an %r for field [%s]" % (x, self.ASN1_tag, self.name)) # noqa: E501 else: - s = self.ASN1_tag.get_codec(pkt.ASN1_codec).enc(x) - return BER_tagging_enc(s, implicit_tag=self.implicit_tag, + s = self.ASN1_tag.get_codec(pkt.ASN1_codec).enc(x, size_len=self.size_len) + return BER_tagging_enc(s, + implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) - def extract_packet(self, cls, s): - if len(s) > 0: - try: - c = cls(s) - except ASN1F_badsequence: - c = packet.Raw(s) - cpad = c.getlayer(packet.Raw) - s = b"" - if cpad is not None: - s = cpad.load - del(cpad.underlayer.payload) - return c, s - else: - return None, s + def any2i(self, pkt, x): + # type: (ASN1_Packet, Any) -> _I + return cast(_I, x) + + def extract_packet(self, + cls, # type: Type[ASN1_Packet] + s, # type: bytes + _underlayer=None # type: Optional[ASN1_Packet] + ): + # type: (...) -> Tuple[ASN1_Packet, bytes] + try: + c = cls(s, _underlayer=_underlayer) + except ASN1F_badsequence: + c = packet.Raw(s, _underlayer=_underlayer) # type: ignore + cpad = c.getlayer(packet.Raw) + s = b"" + if cpad is not None: + s = cpad.load + if cpad.underlayer: + del cpad.underlayer.payload + return c, s def build(self, pkt): + # type: (ASN1_Packet) -> bytes return self.i2m(pkt, getattr(pkt, self.name)) def dissect(self, pkt, s): + # type: (ASN1_Packet, bytes) -> bytes v, s = self.m2i(pkt, s) self.set_val(pkt, v) return s def do_copy(self, x): - if hasattr(x, "copy"): - return x.copy() + # type: (Any) -> Any if isinstance(x, list): x = x[:] for i in range(len(x)): if isinstance(x[i], BasePacket): x[i] = x[i].copy() + return x + if hasattr(x, "copy"): + return x.copy() return x def set_val(self, pkt, val): + # type: (ASN1_Packet, Any) -> None setattr(pkt, self.name, val) def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool return getattr(pkt, self.name) is None def get_fields_list(self): + # type: () -> List[ASN1F_field[Any, Any]] return [self] def __str__(self): + # type: () -> str return repr(self) def randval(self): - return RandInt() + # type: () -> RandField[_I] + return cast(RandField[_I], RandInt()) + + def copy(self): + # type: () -> ASN1F_field[_I, _A] + return copy.copy(self) ############################ # Simple ASN1 Fields # ############################ -class ASN1F_BOOLEAN(ASN1F_field): +class ASN1F_BOOLEAN(ASN1F_field[bool, ASN1_BOOLEAN]): ASN1_tag = ASN1_Class_UNIVERSAL.BOOLEAN def randval(self): + # type: () -> RandChoice return RandChoice(True, False) -class ASN1F_INTEGER(ASN1F_field): +class ASN1F_INTEGER(ASN1F_field[int, ASN1_INTEGER]): ASN1_tag = ASN1_Class_UNIVERSAL.INTEGER def randval(self): + # type: () -> RandNum return RandNum(-2**64, 2**64 - 1) class ASN1F_enum_INTEGER(ASN1F_INTEGER): - def __init__(self, name, default, enum, context=None, - implicit_tag=None, explicit_tag=None): - ASN1F_INTEGER.__init__(self, name, default, context=context, - implicit_tag=implicit_tag, - explicit_tag=explicit_tag) - i2s = self.i2s = {} - s2i = self.s2i = {} + def __init__(self, + name, # type: str + default, # type: ASN1_INTEGER + enum, # type: Dict[int, str] + context=None, # type: Optional[Any] + implicit_tag=None, # type: Optional[Any] + explicit_tag=None, # type: Optional[Any] + ): + # type: (...) -> None + super(ASN1F_enum_INTEGER, self).__init__( + name, default, context=context, + implicit_tag=implicit_tag, + explicit_tag=explicit_tag + ) + i2s = self.i2s = {} # type: Dict[int, str] + s2i = self.s2i = {} # type: Dict[str, int] if isinstance(enum, list): keys = range(len(enum)) else: keys = list(enum) - if any(isinstance(x, six.string_types) for x in keys): - i2s, s2i = s2i, i2s + if any(isinstance(x, str) for x in keys): + i2s, s2i = s2i, i2s # type: ignore for k in keys: i2s[k] = enum[k] s2i[enum[k]] = k - def i2m(self, pkt, s): - if isinstance(s, str): - s = self.s2i.get(s) - return super(ASN1F_enum_INTEGER, self).i2m(pkt, s) - - def i2repr(self, pkt, x): + def i2m(self, + pkt, # type: ASN1_Packet + s, # type: Union[bytes, str, int, ASN1_INTEGER] + ): + # type: (...) -> bytes + if not isinstance(s, str): + vs = s + else: + vs = self.s2i[s] + return super(ASN1F_enum_INTEGER, self).i2m(pkt, vs) + + def i2repr(self, + pkt, # type: ASN1_Packet + x, # type: Union[str, int] + ): + # type: (...) -> str if x is not None and isinstance(x, ASN1_INTEGER): r = self.i2s.get(x.val) if r: @@ -214,25 +315,39 @@ def i2repr(self, pkt, x): return repr(x) -class ASN1F_BIT_STRING(ASN1F_field): +class ASN1F_BIT_STRING(ASN1F_field[str, ASN1_BIT_STRING]): ASN1_tag = ASN1_Class_UNIVERSAL.BIT_STRING - def __init__(self, name, default, default_readable=True, context=None, - implicit_tag=None, explicit_tag=None): - if default is not None and default_readable: - default = b"".join(binrepr(orb(x)).zfill(8).encode("utf8") for x in default) # noqa: E501 - ASN1F_field.__init__(self, name, default, context=context, - implicit_tag=implicit_tag, - explicit_tag=explicit_tag) + def __init__(self, + name, # type: str + default, # type: Optional[Union[ASN1_BIT_STRING, AnyStr]] + default_readable=True, # type: bool + context=None, # type: Optional[Any] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + ): + # type: (...) -> None + super(ASN1F_BIT_STRING, self).__init__( + name, None, context=context, + implicit_tag=implicit_tag, + explicit_tag=explicit_tag + ) + if isinstance(default, (bytes, str)): + self.default = ASN1_BIT_STRING(default, + readable=default_readable) + else: + self.default = default def randval(self): + # type: () -> RandString return RandString(RandNum(0, 1000)) -class ASN1F_STRING(ASN1F_field): +class ASN1F_STRING(ASN1F_field[str, ASN1_STRING]): ASN1_tag = ASN1_Class_UNIVERSAL.STRING def randval(self): + # type: () -> RandString return RandString(RandNum(0, 1000)) @@ -240,10 +355,11 @@ class ASN1F_NULL(ASN1F_INTEGER): ASN1_tag = ASN1_Class_UNIVERSAL.NULL -class ASN1F_OID(ASN1F_field): +class ASN1F_OID(ASN1F_field[str, ASN1_OID]): ASN1_tag = ASN1_Class_UNIVERSAL.OID def randval(self): + # type: () -> RandOID return RandOID() @@ -275,17 +391,23 @@ class ASN1F_IA5_STRING(ASN1F_STRING): ASN1_tag = ASN1_Class_UNIVERSAL.IA5_STRING +class ASN1F_GENERAL_STRING(ASN1F_STRING): + ASN1_tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + class ASN1F_UTC_TIME(ASN1F_STRING): ASN1_tag = ASN1_Class_UNIVERSAL.UTC_TIME - def randval(self): + def randval(self): # type: ignore + # type: () -> GeneralizedTime return GeneralizedTime() class ASN1F_GENERALIZED_TIME(ASN1F_STRING): ASN1_tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME - def randval(self): + def randval(self): # type: ignore + # type: () -> GeneralizedTime return GeneralizedTime() @@ -301,7 +423,7 @@ class ASN1F_BMP_STRING(ASN1F_STRING): ASN1_tag = ASN1_Class_UNIVERSAL.BMP_STRING -class ASN1F_SEQUENCE(ASN1F_field): +class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): # Here is how you could decode a SEQUENCE # with an unknown, private high-tag prefix : # class PrivSeq(ASN1_Packet): @@ -317,28 +439,30 @@ class ASN1F_SEQUENCE(ASN1F_field): holds_packets = 1 def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None name = "dummy_seq_name" default = [field.default for field in seq] - for kwarg in ["context", "implicit_tag", - "explicit_tag", "flexible_tag"]: - setattr(self, kwarg, kwargs.get(kwarg)) - ASN1F_field.__init__(self, name, default, context=self.context, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - flexible_tag=self.flexible_tag) + super(ASN1F_SEQUENCE, self).__init__( + name, default, **kwargs + ) self.seq = seq self.islist = len(seq) > 1 def __repr__(self): + # type: () -> str return "<%s%r>" % (self.__class__.__name__, self.seq) def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool return all(f.is_empty(pkt) for f in self.seq) def get_fields_list(self): - return reduce(lambda x, y: x + y.get_fields_list(), self.seq, []) + # type: () -> List[ASN1F_field[Any, Any]] + return reduce(lambda x, y: x + y.get_fields_list(), + self.seq, []) def m2i(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] """ ASN1F_SEQUENCE behaves transparently, with nested ASN1_objects being dissected one by one. Because we use obj.dissect (see loop below) @@ -350,7 +474,8 @@ def m2i(self, pkt, s): diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, - safe=self.flexible_tag) + safe=self.flexible_tag, + _fname=pkt.name) if diff_tag is not None: if self.implicit_tag is not None: self.implicit_tag = diff_tag @@ -372,34 +497,78 @@ def m2i(self, pkt, s): return [], remain def dissect(self, pkt, s): + # type: (Any, bytes) -> bytes _, x = self.m2i(pkt, s) return x def build(self, pkt): - s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") - return self.i2m(pkt, s) + # type: (ASN1_Packet) -> bytes + s = reduce(lambda x, y: x + y.build(pkt), + self.seq, b"") + return super(ASN1F_SEQUENCE, self).i2m(pkt, s) class ASN1F_SET(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_UNIVERSAL.SET -class ASN1F_SEQUENCE_OF(ASN1F_field): +_SEQ_T = Union[ + 'ASN1_Packet', + Type[ASN1F_field[Any, Any]], + 'ASN1F_PACKET', + ASN1F_field[Any, Any], +] + + +class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T], + List[ASN1_Object[Any]]]): + """ + Two types are allowed as cls: ASN1_Packet, ASN1F_field + """ ASN1_tag = ASN1_Class_UNIVERSAL.SEQUENCE - holds_packets = 1 islist = 1 - def __init__(self, name, default, cls, context=None, - implicit_tag=None, explicit_tag=None): - self.cls = cls - ASN1F_field.__init__(self, name, None, context=context, - implicit_tag=implicit_tag, explicit_tag=explicit_tag) # noqa: E501 + def __init__(self, + name, # type: str + default, # type: Any + cls, # type: _SEQ_T + context=None, # type: Optional[Any] + implicit_tag=None, # type: Optional[Any] + explicit_tag=None, # type: Optional[Any] + ): + # type: (...) -> None + if isinstance(cls, type) and issubclass(cls, ASN1F_field) or \ + isinstance(cls, ASN1F_field): + if isinstance(cls, type): + self.fld = cls(name, b"") + else: + self.fld = cls + self._extract_packet = lambda s, pkt: self.fld.m2i(pkt, s) + self.holds_packets = 0 + elif hasattr(cls, "ASN1_root") or callable(cls): + self.cls = cast("Type[ASN1_Packet]", cls) + self._extract_packet = lambda s, pkt: self.extract_packet( + self.cls, s, _underlayer=pkt) + self.holds_packets = 1 + else: + raise ValueError("cls should be an ASN1_Packet or ASN1_field") + super(ASN1F_SEQUENCE_OF, self).__init__( + name, None, context=context, + implicit_tag=implicit_tag, explicit_tag=explicit_tag + ) self.default = default - def is_empty(self, pkt): + def is_empty(self, + pkt, # type: ASN1_Packet + ): + # type: (...) -> bool return ASN1F_field.is_empty(self, pkt) - def m2i(self, pkt, s): + def m2i(self, + pkt, # type: ASN1_Packet + s, # type: bytes + ): + # type: (...) -> Tuple[List[Any], bytes] diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -413,26 +582,45 @@ def m2i(self, pkt, s): i, s, remain = codec.check_type_check_len(s) lst = [] while s: - c, s = self.extract_packet(self.cls, s) - lst.append(c) + c, s = self._extract_packet(s, pkt) # type: ignore + if c: + lst.append(c) if len(s) > 0: raise BER_Decoding_Error("unexpected remainder", remaining=s) return lst, remain def build(self, pkt): + # type: (ASN1_Packet) -> bytes val = getattr(pkt, self.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: # noqa: E501 - s = val + if isinstance(val, ASN1_Object) and \ + val.tag == ASN1_Class_UNIVERSAL.RAW: + s = cast(Union[List[_SEQ_T], bytes], val) elif val is None: s = b"" else: - s = b"".join(raw(i) for i in val) + s = b"".join(bytes(i) for i in val) return self.i2m(pkt, s) + def i2repr(self, pkt, x): + # type: (ASN1_Packet, _I) -> str + if self.holds_packets: + return super(ASN1F_SEQUENCE_OF, self).i2repr(pkt, x) # type: ignore + elif x is None: + return "[]" + else: + return "[%s]" % ", ".join( + self.fld.i2repr(pkt, x) for x in x # type: ignore + ) + def randval(self): - return packet.fuzz(self.cls()) + # type: () -> Any + if self.holds_packets: + return packet.fuzz(self.cls()) + else: + return self.fld.randval() def __repr__(self): + # type: () -> str return "<%s %s>" % (self.__class__.__name__, self.name) @@ -453,14 +641,20 @@ class ASN1F_TIME_TICKS(ASN1F_INTEGER): ############################# class ASN1F_optional(ASN1F_element): + """ + ASN.1 field that is optional. + """ def __init__(self, field): + # type: (ASN1F_field[Any, Any]) -> None field.flexible_tag = False self._field = field def __getattr__(self, attr): + # type: (str) -> Optional[Any] return getattr(self._field, attr) def m2i(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] try: return self._field.m2i(pkt, s) except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): @@ -468,6 +662,7 @@ def m2i(self, pkt, s): return None, s def dissect(self, pkt, s): + # type: (ASN1_Packet, bytes) -> bytes try: return self._field.dissect(pkt, s) except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): @@ -475,18 +670,38 @@ def dissect(self, pkt, s): return s def build(self, pkt): + # type: (ASN1_Packet) -> bytes if self._field.is_empty(pkt): return b"" return self._field.build(pkt) def any2i(self, pkt, x): + # type: (ASN1_Packet, Any) -> Any return self._field.any2i(pkt, x) def i2repr(self, pkt, x): + # type: (ASN1_Packet, Any) -> str return self._field.i2repr(pkt, x) -class ASN1F_CHOICE(ASN1F_field): +class ASN1F_omit(ASN1F_field[None, None]): + """ + ASN.1 field that is not specified. This is simply omitted on the network. + This is different from ASN1F_NULL which has a network representation. + """ + def m2i(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[None, bytes] + return None, s + + def i2m(self, pkt, x): + # type: (ASN1_Packet, Optional[bytes]) -> bytes + return b"" + + +_CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] + + +class ASN1F_CHOICE(ASN1F_field[_CHOICE_T, ASN1_Object[Any]]): """ Multiple types are allowed: ASN1_Packet, ASN1F_field and ASN1F_PACKET(), See layers/x509.py for examples. @@ -496,35 +711,45 @@ class ASN1F_CHOICE(ASN1F_field): ASN1_tag = ASN1_Class_UNIVERSAL.ANY def __init__(self, name, default, *args, **kwargs): + # type: (str, Any, *_CHOICE_T, **Any) -> None if "implicit_tag" in kwargs: err_msg = "ASN1F_CHOICE has been called with an implicit_tag" raise ASN1_Error(err_msg) self.implicit_tag = None for kwarg in ["context", "explicit_tag"]: setattr(self, kwarg, kwargs.get(kwarg)) - ASN1F_field.__init__(self, name, None, context=self.context, - explicit_tag=self.explicit_tag) + super(ASN1F_CHOICE, self).__init__( + name, None, context=self.context, + explicit_tag=self.explicit_tag + ) self.default = default self.current_choice = None - self.choices = {} + self.choices = {} # type: Dict[int, _CHOICE_T] self.pktchoices = {} for p in args: - if hasattr(p, "ASN1_root"): # should be ASN1_Packet + if hasattr(p, "ASN1_root"): + p = cast('ASN1_Packet', p) + # should be ASN1_Packet if hasattr(p.ASN1_root, "choices"): - for k, v in six.iteritems(p.ASN1_root.choices): - self.choices[k] = v # ASN1F_CHOICE recursion + root = cast(ASN1F_CHOICE, p.ASN1_root) + for k, v in root.choices.items(): + # ASN1F_CHOICE recursion + self.choices[k] = v else: self.choices[p.ASN1_root.network_tag] = p elif hasattr(p, "ASN1_tag"): - if isinstance(p, type): # should be ASN1F_field class - self.choices[p.ASN1_tag] = p - else: # should be ASN1F_PACKET instance + if isinstance(p, type): + # should be ASN1F_field class + self.choices[int(p.ASN1_tag)] = p + else: + # should be ASN1F_field instance self.choices[p.network_tag] = p self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") def m2i(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] """ First we have to retrieve the appropriate choice. Then we extract the field/packet, according to this choice. @@ -534,82 +759,138 @@ def m2i(self, pkt, s): _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag) tag, _ = BER_id_dec(s) - if tag not in self.choices: + if tag in self.choices: + choice = self.choices[tag] + else: if self.flexible_tag: choice = ASN1F_field else: - raise ASN1_Error("ASN1F_CHOICE: unexpected field") - else: - choice = self.choices[tag] + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + self.name, tag, list(self.choices.keys()) + ) + ) if hasattr(choice, "ASN1_root"): # we don't want to import ASN1_Packet in this module... - return self.extract_packet(choice, s) + return self.extract_packet(choice, s, _underlayer=pkt) # type: ignore elif isinstance(choice, type): - # XXX find a way not to instantiate the ASN1F_field return choice(self.name, b"").m2i(pkt, s) else: # XXX check properly if this is an ASN1F_PACKET return choice.m2i(pkt, s) def i2m(self, pkt, x): + # type: (ASN1_Packet, Any) -> bytes if x is None: s = b"" else: - s = raw(x) + s = bytes(x) if hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] - s = BER_tagging_enc(s, implicit_tag=imp, + s = BER_tagging_enc(s, + implicit_tag=imp, explicit_tag=exp) return BER_tagging_enc(s, explicit_tag=self.explicit_tag) def randval(self): + # type: () -> RandChoice randchoices = [] - for p in six.itervalues(self.choices): - if hasattr(p, "ASN1_root"): # should be ASN1_Packet class - randchoices.append(packet.fuzz(p())) + for p in self.choices.values(): + if hasattr(p, "ASN1_root"): + # should be ASN1_Packet class + randchoices.append(packet.fuzz(p())) # type: ignore elif hasattr(p, "ASN1_tag"): - if isinstance(p, type): # should be (basic) ASN1F_field class # noqa: E501 + if isinstance(p, type): + # should be (basic) ASN1F_field class randchoices.append(p("dummy", None).randval()) - else: # should be ASN1F_PACKET instance + else: + # should be ASN1F_PACKET instance randchoices.append(p.randval()) return RandChoice(*randchoices) -class ASN1F_PACKET(ASN1F_field): +class ASN1F_PACKET(ASN1F_field['ASN1_Packet', Optional['ASN1_Packet']]): holds_packets = 1 - def __init__(self, name, default, cls, context=None, - implicit_tag=None, explicit_tag=None): + def __init__(self, + name, # type: str + default, # type: Optional[ASN1_Packet] + cls, # type: Type[ASN1_Packet] + context=None, # type: Optional[Any] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + next_cls_cb=None, # type: Optional[Callable[[ASN1_Packet], Type[ASN1_Packet]]] # noqa: E501 + ): + # type: (...) -> None self.cls = cls - ASN1F_field.__init__(self, name, None, context=context, - implicit_tag=implicit_tag, explicit_tag=explicit_tag) # noqa: E501 - if cls.ASN1_root.ASN1_tag == ASN1_Class_UNIVERSAL.SEQUENCE: - if implicit_tag is None and explicit_tag is None: - self.network_tag = 16 | 0x20 + self.next_cls_cb = next_cls_cb + super(ASN1F_PACKET, self).__init__( + name, None, context=context, + implicit_tag=implicit_tag, explicit_tag=explicit_tag + ) + if implicit_tag is None and explicit_tag is None and cls is not None: + if cls.ASN1_root.ASN1_tag == ASN1_Class_UNIVERSAL.SEQUENCE: + self.network_tag = 16 | 0x20 # 16 + CONSTRUCTED self.default = default def m2i(self, pkt, s): - diff_tag, s = BER_tagging_dec(s, hidden_tag=self.cls.ASN1_root.ASN1_tag, # noqa: E501 + # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] + if self.next_cls_cb: + cls = self.next_cls_cb(pkt) or self.cls + else: + cls = self.cls + if not hasattr(cls, "ASN1_root"): + # A normal Packet (!= ASN1) + return self.extract_packet(cls, s, _underlayer=pkt) + diff_tag, s = BER_tagging_dec(s, hidden_tag=cls.ASN1_root.ASN1_tag, # noqa: E501 implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, - safe=self.flexible_tag) + safe=self.flexible_tag, + _fname=self.name) if diff_tag is not None: if self.implicit_tag is not None: self.implicit_tag = diff_tag elif self.explicit_tag is not None: self.explicit_tag = diff_tag - p, s = self.extract_packet(self.cls, s) - return p, s + if not s: + return None, s + return self.extract_packet(cls, s, _underlayer=pkt) - def i2m(self, pkt, x): + def i2m(self, + pkt, # type: ASN1_Packet + x # type: Union[bytes, ASN1_Packet, None, ASN1_Object[Optional[ASN1_Packet]]] # noqa: E501 + ): + # type: (...) -> bytes if x is None: s = b"" + elif isinstance(x, bytes): + s = x + elif isinstance(x, ASN1_Object): + if x.val: + s = bytes(x.val) + else: + s = b"" else: - s = raw(x) - return BER_tagging_enc(s, implicit_tag=self.implicit_tag, + s = bytes(x) + if not hasattr(x, "ASN1_root"): + # A normal Packet (!= ASN1) + return s + return BER_tagging_enc(s, + implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) - def randval(self): + def any2i(self, + pkt, # type: ASN1_Packet + x # type: Union[bytes, ASN1_Packet, None, ASN1_Object[Optional[ASN1_Packet]]] # noqa: E501 + ): + # type: (...) -> 'ASN1_Packet' + if hasattr(x, "add_underlayer"): + x.add_underlayer(pkt) # type: ignore + return super(ASN1F_PACKET, self).any2i(pkt, x) + + def randval(self): # type: ignore + # type: () -> ASN1_Packet return packet.fuzz(self.cls()) @@ -618,48 +899,138 @@ class ASN1F_BIT_STRING_ENCAPS(ASN1F_BIT_STRING): We may emulate simple string encapsulation with explicit_tag=0x04, but we need a specific class for bit strings because of unused bits, etc. """ - holds_packets = 1 + ASN1_tag = ASN1_Class_UNIVERSAL.BIT_STRING - def __init__(self, name, default, cls, context=None, - implicit_tag=None, explicit_tag=None): + def __init__(self, + name, # type: str + default, # type: Optional[ASN1_Packet] + cls, # type: Type[ASN1_Packet] + context=None, # type: Optional[Any] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + ): + # type: (...) -> None self.cls = cls - ASN1F_BIT_STRING.__init__(self, name, None, context=context, - implicit_tag=implicit_tag, - explicit_tag=explicit_tag) - self.default = default - - def m2i(self, pkt, s): - bit_string, remain = ASN1F_BIT_STRING.m2i(self, pkt, s) + super(ASN1F_BIT_STRING_ENCAPS, self).__init__( # type: ignore + name, + default and bytes(default), + context=context, + implicit_tag=implicit_tag, + explicit_tag=explicit_tag + ) + + def m2i(self, pkt, s): # type: ignore + # type: (ASN1_Packet, bytes) -> Tuple[Optional[ASN1_Packet], bytes] + bit_string, remain = super(ASN1F_BIT_STRING_ENCAPS, self).m2i(pkt, s) if len(bit_string.val) % 8 != 0: raise BER_Decoding_Error("wrong bit string", remaining=s) - p, s = self.extract_packet(self.cls, bit_string.val_readable) + if bit_string.val_readable: + p, s = self.extract_packet(self.cls, bit_string.val_readable, + _underlayer=pkt) + else: + return None, bit_string.val_readable if len(s) > 0: raise BER_Decoding_Error("unexpected remainder", remaining=s) return p, remain - def i2m(self, pkt, x): - s = b"" if x is None else raw(x) - s = b"".join(binrepr(orb(x)).zfill(8).encode("utf8") for x in s) - return ASN1F_BIT_STRING.i2m(self, pkt, s) + def i2m(self, pkt, x): # type: ignore + # type: (ASN1_Packet, Optional[ASN1_BIT_STRING]) -> bytes + if not isinstance(x, ASN1_BIT_STRING): + x = ASN1_BIT_STRING( + b"" if x is None else bytes(x), # type: ignore + readable=True, + ) + return super(ASN1F_BIT_STRING_ENCAPS, self).i2m(pkt, x) class ASN1F_FLAGS(ASN1F_BIT_STRING): - def __init__(self, name, default, mapping, context=None, - implicit_tag=None, explicit_tag=None): + def __init__(self, + name, # type: str + default, # type: Optional[str] + mapping, # type: List[str] + context=None, # type: Optional[Any] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[Any] + ): + # type: (...) -> None self.mapping = mapping - ASN1F_BIT_STRING.__init__(self, name, default, - default_readable=False, - context=context, - implicit_tag=implicit_tag, - explicit_tag=explicit_tag) + super(ASN1F_FLAGS, self).__init__( + name, default, + default_readable=False, + context=context, + implicit_tag=implicit_tag, + explicit_tag=explicit_tag + ) + + def any2i(self, pkt, x): + # type: (ASN1_Packet, Any) -> str + if isinstance(x, str): + if any(y not in ["0", "1"] for y in x): + # resolve the flags + value = ["0"] * len(self.mapping) + for i in x.split("+"): + value[self.mapping.index(i)] = "1" + x = "".join(value) + x = ASN1_BIT_STRING(x) + return super(ASN1F_FLAGS, self).any2i(pkt, x) def get_flags(self, pkt): + # type: (ASN1_Packet) -> List[str] fbytes = getattr(pkt, self.name).val return [self.mapping[i] for i, positional in enumerate(fbytes) if positional == '1' and i < len(self.mapping)] def i2repr(self, pkt, x): + # type: (ASN1_Packet, Any) -> str if x is not None: pretty_s = ", ".join(self.get_flags(pkt)) return pretty_s + " " + repr(x) return repr(x) + + +class ASN1F_STRING_PacketField(ASN1F_STRING): + """ + ASN1F_STRING that holds packets. + """ + holds_packets = 1 + + def i2m(self, pkt, val): + # type: (ASN1_Packet, Any) -> bytes + if hasattr(val, "ASN1_root"): + val = ASN1_STRING(bytes(val)) + return super(ASN1F_STRING_PacketField, self).i2m(pkt, val) + + def any2i(self, pkt, x): + # type: (ASN1_Packet, Any) -> Any + if hasattr(x, "add_underlayer"): + x.add_underlayer(pkt) + return super(ASN1F_STRING_PacketField, self).any2i(pkt, x) + + +class ASN1F_STRING_ENCAPS(ASN1F_STRING_PacketField): + """ + ASN1F_STRING that encapsulates a single ASN1 packet. + """ + + def __init__(self, + name, # type: str + default, # type: Optional[ASN1_Packet] + cls, # type: Type[ASN1_Packet] + context=None, # type: Optional[Any] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + ): + # type: (...) -> None + self.cls = cls + super(ASN1F_STRING_ENCAPS, self).__init__( + name, + default and bytes(default), # type: ignore + context=context, + implicit_tag=implicit_tag, + explicit_tag=explicit_tag + ) + + def m2i(self, pkt, s): # type: ignore + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Packet, bytes] + val = super(ASN1F_STRING_ENCAPS, self).m2i(pkt, s) + return self.cls(val[0].val, _underlayer=pkt), val[1] diff --git a/scapy/asn1packet.py b/scapy/asn1packet.py index 759ec58f686..058aecc0edb 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ ASN.1 Packet @@ -9,27 +9,47 @@ Packet holding data in Abstract Syntax Notation (ASN.1). """ -from __future__ import absolute_import from scapy.base_classes import Packet_metaclass from scapy.packet import Packet -import scapy.modules.six as six + +from typing import ( + Any, + Dict, + Tuple, + Type, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.asn1fields import ASN1F_field # noqa: F401 class ASN1Packet_metaclass(Packet_metaclass): - def __new__(cls, name, bases, dct): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[ASN1_Packet] if dct["ASN1_root"] is not None: dct["fields_desc"] = dct["ASN1_root"].get_fields_list() - return super(ASN1Packet_metaclass, cls).__new__(cls, name, bases, dct) + return cast( + 'Type[ASN1_Packet]', + super(ASN1Packet_metaclass, cls).__new__(cls, name, bases, dct), + ) -class ASN1_Packet(six.with_metaclass(ASN1Packet_metaclass, Packet)): - ASN1_root = None +class ASN1_Packet(Packet, metaclass=ASN1Packet_metaclass): + ASN1_root = cast('ASN1F_field[Any, Any]', None) ASN1_codec = None def self_build(self): + # type: () -> bytes if self.raw_packet_cache is not None: return self.raw_packet_cache return self.ASN1_root.build(self) def do_dissect(self, x): + # type: (bytes) -> bytes return self.ASN1_root.dissect(self, x) diff --git a/scapy/automaton.py b/scapy/automaton.py index d02330e0c7a..2aae9168725 100644 --- a/scapy/automaton.py +++ b/scapy/automaton.py @@ -1,291 +1,445 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# Copyright (C) Gabriel Potter """ Automata with states, transitions and actions. + +TODO: + - add documentation for ioevent, as_supersocket... """ -from __future__ import absolute_import -import types +import ctypes import itertools -import time +import logging import os +import random +import socket import sys +import threading +import time import traceback -from select import select +import types + +import select from collections import deque -import threading + from scapy.config import conf -from scapy.utils import do_graph -from scapy.error import log_interactive, warning -from scapy.plist import PacketList -from scapy.data import MTU -from scapy.supersocket import SuperSocket from scapy.consts import WINDOWS -import scapy.modules.six as six - -if WINDOWS: - from scapy.error import Scapy_Exception - recv_error = Scapy_Exception -else: - recv_error = () - -""" In Windows, select.select is not available for custom objects. Here's the implementation of scapy to re-create this functionality # noqa: E501 -# Passive way: using no-ressources locks - +---------+ +---------------+ +-------------------------+ # noqa: E501 - | Start +------------->Select_objects +----->+Linux: call select.select| # noqa: E501 - +---------+ |(select.select)| +-------------------------+ # noqa: E501 - +-------+-------+ - | - +----v----+ +--------+ - | Windows | |Time Out+----------------------------------+ # noqa: E501 - +----+----+ +----+---+ | # noqa: E501 - | ^ | # noqa: E501 - Event | | | # noqa: E501 - + | | | # noqa: E501 - | +-------v-------+ | | # noqa: E501 - | +------+Selectable Sel.+-----+-----------------+-----------+ | # noqa: E501 - | | +-------+-------+ | | | v +-----v-----+ # noqa: E501 -+-------v----------+ | | | | | Passive lock<-----+release_all<------+ # noqa: E501 -|Data added to list| +----v-----+ +-----v-----+ +----v-----+ v v + +-----------+ | # noqa: E501 -+--------+---------+ |Selectable| |Selectable | |Selectable| ............ | | # noqa: E501 - | +----+-----+ +-----------+ +----------+ | | # noqa: E501 - | v | | # noqa: E501 - v +----+------+ +------------------+ +-------------v-------------------+ | # noqa: E501 - +-----+------+ |wait_return+-->+ check_recv: | | | | # noqa: E501 - |call_release| +----+------+ |If data is in list| | END state: selectable returned | +---+--------+ # noqa: E501 - +-----+-------- v +-------+----------+ | | | exit door | # noqa: E501 - | else | +---------------------------------+ +---+--------+ # noqa: E501 - | + | | # noqa: E501 - | +----v-------+ | | # noqa: E501 - +--------->free -->Passive lock| | | # noqa: E501 - +----+-------+ | | # noqa: E501 - | | | # noqa: E501 - | v | # noqa: E501 - +------------------Selectable-Selector-is-advertised-that-the-selectable-is-readable---------+ -""" - - -class SelectableObject(object): - """DEV: to implement one of those, you need to add 2 things to your object: - - add "check_recv" function - - call "self.call_release" once you are ready to be read - - You can set the __selectable_force_select__ to True in the class, if you want to # noqa: E501 - force the handler to use fileno(). This may only be usable on sockets created using # noqa: E501 - the builtin socket API.""" - __selectable_force_select__ = False - - def __init__(self): - self.hooks = [] - - def check_recv(self): - """DEV: will be called only once (at beginning) to check if the object is ready.""" # noqa: E501 - raise OSError("This method must be overwritten.") - - def _wait_non_ressources(self, callback): - """This get started as a thread, and waits for the data lock to be freed then advertise itself to the SelectableSelector using the callback""" # noqa: E501 - self.trigger = threading.Lock() - self.was_ended = False - self.trigger.acquire() - self.trigger.acquire() - if not self.was_ended: - callback(self) - - def wait_return(self, callback): - """Entry point of SelectableObject: register the callback""" - if self.check_recv(): - return callback(self) - _t = threading.Thread(target=self._wait_non_ressources, args=(callback,)) # noqa: E501 - _t.setDaemon(True) - _t.start() - - def register_hook(self, hook): - """DEV: When call_release() will be called, the hook will also""" - self.hooks.append(hook) +from scapy.data import MTU +from scapy.error import log_runtime, warning +from scapy.interfaces import _GlobInterfaceType +from scapy.packet import Packet +from scapy.plist import PacketList +from scapy.supersocket import SuperSocket, StreamSocket +from scapy.utils import do_graph - def call_release(self, arborted=False): - """DEV: Must be call when the object becomes ready to read. - Relesases the lock of _wait_non_ressources""" - self.was_ended = arborted - try: - self.trigger.release() - except (threading.ThreadError, AttributeError): - pass - # Trigger hooks - for hook in self.hooks: - hook() +# Typing imports +from typing import ( + Any, + Callable, + Deque, + Dict, + Generic, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) +from scapy.compat import DecoratorCallable + + +# winsock.h +FD_READ = 0x00000001 -class SelectableSelector(object): +def select_objects(inputs, remain): + # type: (Iterable[Any], Union[float, int, None]) -> List[Any] """ - Select SelectableObject objects. + Select objects. Same than: + ``select.select(inputs, [], [], remain)`` - inputs: objects to process - remain: timeout. If 0, return []. - customTypes: types of the objects that have the check_recv function. - """ + But also works on Windows, only on objects whose fileno() returns + a Windows event. For simplicity, just use `ObjectPipe()` as a queue + that you can select on whatever the platform is. - def _release_all(self): - """Releases all locks to kill all threads""" - for i in self.inputs: - i.call_release(True) - self.available_lock.release() - - def _timeout_thread(self, remain): - """Timeout before releasing every thing, if nothing was returned""" - time.sleep(remain) - if not self._ended: - self._ended = True - self._release_all() - - def _exit_door(self, _input): - """This function is passed to each SelectableObject as a callback - The SelectableObjects have to call it once there are ready""" - self.results.append(_input) - if self._ended: - return - self._ended = True - self._release_all() - - def __init__(self, inputs, remain): - self.results = [] - self.inputs = list(inputs) - self.remain = remain - self.available_lock = threading.Lock() - self.available_lock.acquire() - self._ended = False - - def process(self): - """Entry point of SelectableSelector""" - if WINDOWS: - select_inputs = [] - for i in self.inputs: - if not isinstance(i, SelectableObject): - warning("Unknown ignored object type: %s", type(i)) - elif i.__selectable_force_select__: - # Then use select.select - select_inputs.append(i) - elif not self.remain and i.check_recv(): - self.results.append(i) - elif self.remain: - i.wait_return(self._exit_door) - if select_inputs: - # Use default select function - self.results.extend(select(select_inputs, [], [], self.remain)[0]) # noqa: E501 - if not self.remain: - return self.results - - threading.Thread(target=self._timeout_thread, args=(self.remain,)).start() # noqa: E501 - if not self._ended: - self.available_lock.acquire() - return self.results - else: - r, _, _ = select(self.inputs, [], [], self.remain) - return r + If you want an object to be always included in the output of + select_objects (i.e. it's not selectable), just make fileno() + return a strictly negative value. + Example: -def select_objects(inputs, remain): - """ - Select SelectableObject objects. Same than: - ``select.select([inputs], [], [], remain)`` - But also works on Windows, only on SelectableObject. + >>> a, b = ObjectPipe("a"), ObjectPipe("b") + >>> b.send("test") + >>> select_objects([a, b], 1) + [b] :param inputs: objects to process - :param remain: timeout. If 0, return []. + :param remain: timeout. If 0, poll. If None, block. """ - handler = SelectableSelector(inputs, remain) - return handler.process() - - -class ObjectPipe(SelectableObject): - read_allowed_exceptions = () - - def __init__(self): + if not WINDOWS: + return select.select(inputs, [], [], remain)[0] + inputs = list(inputs) + events = [] + created = [] + results = set() + for i in inputs: + if getattr(i, "__selectable_force_select__", False): + # Native socket.socket object. We would normally use select.select. + evt = ctypes.windll.ws2_32.WSACreateEvent() + created.append(evt) + res = ctypes.windll.ws2_32.WSAEventSelect( + ctypes.c_void_p(i.fileno()), + evt, + FD_READ + ) + if res == 0: + # Was a socket + events.append(evt) + else: + # Fallback to normal event + events.append(i.fileno()) + elif i.fileno() < 0: + # Special case: On Windows, we consider that an object that returns + # a negative fileno (impossible), is always readable. This is used + # in very few places but important (e.g. PcapReader), where we have + # no valid fileno (and will stop on EOFError). + results.add(i) + remain = 0 + else: + events.append(i.fileno()) + if events: + # 0xFFFFFFFF = INFINITE + remainms = int(remain * 1000 if remain is not None else 0xFFFFFFFF) + if len(events) == 1: + res = ctypes.windll.kernel32.WaitForSingleObject( + ctypes.c_void_p(events[0]), + remainms + ) + else: + # Sadly, the only way to emulate select() is to first check + # if any object is available using WaitForMultipleObjects + # then poll the others. + res = ctypes.windll.kernel32.WaitForMultipleObjects( + len(events), + (ctypes.c_void_p * len(events))( + *events + ), + False, + remainms + ) + if res != 0xFFFFFFFF and res != 0x00000102: # Failed or Timeout + results.add(inputs[res]) + if len(events) > 1: + # Now poll the others, if any + for i, evt in enumerate(events): + res = ctypes.windll.kernel32.WaitForSingleObject( + ctypes.c_void_p(evt), + 0 # poll: don't wait + ) + if res == 0: + results.add(inputs[i]) + # Cleanup created events, if any + for evt in created: + ctypes.windll.ws2_32.WSACloseEvent(evt) + return list(results) + + +_T = TypeVar("_T") + + +class ObjectPipe(Generic[_T]): + def __init__(self, name=None): + # type: (Optional[str]) -> None + self.name = name or "ObjectPipe" self.closed = False - self.rd, self.wr = os.pipe() - self.queue = deque() - SelectableObject.__init__(self) + self.__rd, self.__wr = os.pipe() + self.__queue = deque() # type: Deque[_T] + if WINDOWS: + self._wincreate() + + if WINDOWS: + def _wincreate(self): + # type: () -> None + self._fd = cast(int, ctypes.windll.kernel32.CreateEventA( + None, True, False, + ctypes.create_string_buffer(b"ObjectPipe %f" % random.random()) + )) + + def _winset(self): + # type: () -> None + if ctypes.windll.kernel32.SetEvent(ctypes.c_void_p(self._fd)) == 0: + warning(ctypes.FormatError(ctypes.GetLastError())) + + def _winreset(self): + # type: () -> None + if ctypes.windll.kernel32.ResetEvent(ctypes.c_void_p(self._fd)) == 0: + warning(ctypes.FormatError(ctypes.GetLastError())) + + def _winclose(self): + # type: () -> None + if ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self._fd)) == 0: + warning(ctypes.FormatError(ctypes.GetLastError())) def fileno(self): - return self.rd - - def check_recv(self): - return len(self.queue) > 0 + # type: () -> int + if WINDOWS: + return self._fd + return self.__rd def send(self, obj): - self.queue.append(obj) - os.write(self.wr, b"X") - self.call_release() + # type: (_T) -> int + self.__queue.append(obj) + if WINDOWS: + self._winset() + os.write(self.__wr, b"X") + return 1 def write(self, obj): + # type: (_T) -> None self.send(obj) - def recv(self, n=0): + def empty(self): + # type: () -> bool + return not bool(self.__queue) + + def flush(self): + # type: () -> None + pass + + def recv(self, n=0, options=socket.MsgFlag(0)): + # type: (Optional[int], socket.MsgFlag) -> Optional[_T] if self.closed: - if self.check_recv(): - return self.queue.popleft() + raise EOFError + if options & socket.MSG_PEEK: + if self.__queue: + return self.__queue[0] return None - os.read(self.rd, 1) - return self.queue.popleft() + os.read(self.__rd, 1) + elt = self.__queue.popleft() + if WINDOWS and not self.__queue: + self._winreset() + return elt def read(self, n=0): + # type: (Optional[int]) -> Optional[_T] return self.recv(n) + def clear(self): + # type: () -> None + if not self.closed: + while not self.empty(): + self.recv() + def close(self): + # type: () -> None if not self.closed: self.closed = True - os.close(self.rd) - os.close(self.wr) - self.queue.clear() + os.close(self.__rd) + os.close(self.__wr) + if WINDOWS: + try: + self._winclose() + except ImportError: + # Python is shutting down + pass + + def __repr__(self): + # type: () -> str + return "<%s at %s>" % (self.name, id(self)) + + def __del__(self): + # type: () -> None + self.close() @staticmethod def select(sockets, remain=conf.recv_poll_rate): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] # Only handle ObjectPipes results = [] for s in sockets: - if s.closed: + if s.closed: # allow read to trigger EOF results.append(s) if results: - return results, None - return select_objects(sockets, remain), None + return results + return select_objects(sockets, remain) class Message: + type = None # type: str + pkt = None # type: Packet + result = None # type: str + state = None # type: Message + exc_info = None # type: Union[Tuple[None, None, None], Tuple[BaseException, Exception, types.TracebackType]] # noqa: E501 + def __init__(self, **args): + # type: (Any) -> None self.__dict__.update(args) def __repr__(self): - return "" % " ".join("%s=%r" % (k, v) - for (k, v) in six.iteritems(self.__dict__) # noqa: E501 - if not k.startswith("_")) + # type: () -> str + return "" % " ".join( + "%s=%r" % (k, v) + for k, v in self.__dict__.items() + if not k.startswith("_") + ) + + +class Timer(): + def __init__(self, time, prio=0, autoreload=False): + # type: (Union[int, float], int, bool) -> None + self._timeout = float(time) # type: float + self._time = 0 # type: float + self._just_expired = True + self._expired = True + self._prio = prio + self._func = _StateWrapper() + self._autoreload = autoreload + + def get(self): + # type: () -> float + return self._timeout + + def set(self, val): + # type: (float) -> None + self._timeout = val + + def _reset(self): + # type: () -> None + self._time = self._timeout + self._expired = False + self._just_expired = False + + def _reset_just_expired(self): + # type: () -> None + self._just_expired = False + + def _running(self): + # type: () -> bool + return self._time > 0 + + def _remaining(self): + # type: () -> float + return max(self._time, 0) + + def _decrement(self, time): + # type: (float) -> None + self._time -= time + if self._time <= 0: + if not self._expired: + self._just_expired = True + if self._autoreload: + # take overshoot into account + self._time = self._timeout + self._time + else: + self._expired = True + self._time = 0 + + def __lt__(self, obj): + # type: (Timer) -> bool + return ((self._time < obj._time) if self._time != obj._time + else (self._prio < obj._prio)) + + def __gt__(self, obj): + # type: (Timer) -> bool + return ((self._time > obj._time) if self._time != obj._time + else (self._prio > obj._prio)) + + def __eq__(self, obj): + # type: (Any) -> bool + if not isinstance(obj, Timer): + raise NotImplementedError() + return (self._time == obj._time) and (self._prio == obj._prio) + + def __repr__(self): + # type: () -> str + return "" % (self._time, self._timeout) + + +class _TimerList(): + def __init__(self): + # type: () -> None + self.timers = [] # type: list[Timer] + + def add_timer(self, timer): + # type: (Timer) -> None + self.timers.append(timer) + + def reset(self): + # type: () -> None + for t in self.timers: + t._reset() + + def decrement(self, time): + # type: (float) -> None + for t in self.timers: + t._decrement(time) + + def expired(self): + # type: () -> list[Timer] + lst = [t for t in self.timers if t._just_expired] + lst.sort(key=lambda x: x._prio, reverse=True) + for t in lst: + t._reset_just_expired() + return lst + + def until_next(self): + # type: () -> Optional[float] + try: + return min([t._remaining() for t in self.timers if t._running()]) + except ValueError: + return None # None means blocking + + def count(self): + # type: () -> int + return len(self.timers) + + def __iter__(self): + # type: () -> Iterator[Timer] + return self.timers.__iter__() + + def __repr__(self): + # type: () -> str + return self.timers.__repr__() class _instance_state: def __init__(self, instance): + # type: (Any) -> None self.__self__ = instance.__self__ self.__func__ = instance.__func__ self.__self__.__class__ = instance.__self__.__class__ def __getattr__(self, attr): + # type: (str) -> Any return getattr(self.__func__, attr) def __call__(self, *args, **kargs): + # type: (Any, Any) -> Any return self.__func__(self.__self__, *args, **kargs) def breaks(self): + # type: () -> Any return self.__self__.add_breakpoints(self.__func__) def intercepts(self): + # type: () -> Any return self.__self__.add_interception_points(self.__func__) def unbreaks(self): + # type: () -> Any return self.__self__.remove_breakpoints(self.__func__) def unintercepts(self): + # type: () -> Any return self.__self__.remove_interception_points(self.__func__) @@ -293,20 +447,42 @@ def unintercepts(self): # Automata # ############## +class _StateWrapper: + __name__ = None # type: str + atmt_type = None # type: str + atmt_state = None # type: str + atmt_initial = None # type: int + atmt_final = None # type: int + atmt_stop = None # type: int + atmt_error = None # type: int + atmt_origfunc = None # type: _StateWrapper + atmt_prio = None # type: int + atmt_as_supersocket = None # type: Optional[str] + atmt_condname = None # type: str + atmt_ioname = None # type: str + atmt_timeout = None # type: Timer + atmt_cond = None # type: Dict[str, int] + __code__ = None # type: types.CodeType + __call__ = None # type: Callable[..., ATMT.NewStateRequested] + + class ATMT: STATE = "State" ACTION = "Action" CONDITION = "Condition" RECV = "Receive condition" TIMEOUT = "Timeout condition" + EOF = "EOF condition" IOEVENT = "I/O event" class NewStateRequested(Exception): def __init__(self, state_func, automaton, *args, **kargs): + # type: (Any, ATMT, Any, Any) -> None self.func = state_func self.state = state_func.atmt_state self.initial = state_func.atmt_initial self.error = state_func.atmt_error + self.stop = state_func.atmt_stop self.final = state_func.atmt_final Exception.__init__(self, "Request state [%s]" % self.state) self.automaton = automaton @@ -315,41 +491,56 @@ def __init__(self, state_func, automaton, *args, **kargs): self.action_parameters() # init action parameters def action_parameters(self, *args, **kargs): + # type: (Any, Any) -> ATMT.NewStateRequested self.action_args = args self.action_kargs = kargs return self def run(self): + # type: () -> Any return self.func(self.automaton, *self.args, **self.kargs) def __repr__(self): + # type: () -> str return "NewStateRequested(%s)" % self.state @staticmethod - def state(initial=0, final=0, error=0): + def state(initial=0, # type: int + final=0, # type: int + stop=0, # type: int + error=0 # type: int + ): + # type: (...) -> Callable[[DecoratorCallable], DecoratorCallable] def deco(f, initial=initial, final=final): + # type: (_StateWrapper, int, int) -> _StateWrapper f.atmt_type = ATMT.STATE f.atmt_state = f.__name__ f.atmt_initial = initial f.atmt_final = final + f.atmt_stop = stop f.atmt_error = error - def state_wrapper(self, *args, **kargs): + def _state_wrapper(self, *args, **kargs): + # type: (ATMT, Any, Any) -> ATMT.NewStateRequested return ATMT.NewStateRequested(f, self, *args, **kargs) + state_wrapper = cast(_StateWrapper, _state_wrapper) state_wrapper.__name__ = "%s_wrapper" % f.__name__ state_wrapper.atmt_type = ATMT.STATE state_wrapper.atmt_state = f.__name__ state_wrapper.atmt_initial = initial state_wrapper.atmt_final = final + state_wrapper.atmt_stop = stop state_wrapper.atmt_error = error state_wrapper.atmt_origfunc = f return state_wrapper - return deco + return deco # type: ignore @staticmethod def action(cond, prio=0): + # type: (Any, int) -> Callable[[_StateWrapper, _StateWrapper], _StateWrapper] # noqa: E501 def deco(f, cond=cond): + # type: (_StateWrapper, _StateWrapper) -> _StateWrapper if not hasattr(f, "atmt_type"): f.atmt_cond = {} f.atmt_type = ATMT.ACTION @@ -359,7 +550,9 @@ def deco(f, cond=cond): @staticmethod def condition(state, prio=0): + # type: (Any, int) -> Callable[[_StateWrapper, _StateWrapper], _StateWrapper] # noqa: E501 def deco(f, state=state): + # type: (_StateWrapper, _StateWrapper) -> Any f.atmt_type = ATMT.CONDITION f.atmt_state = state.atmt_state f.atmt_condname = f.__name__ @@ -369,7 +562,9 @@ def deco(f, state=state): @staticmethod def receive_condition(state, prio=0): + # type: (_StateWrapper, int) -> Callable[[_StateWrapper, _StateWrapper], _StateWrapper] # noqa: E501 def deco(f, state=state): + # type: (_StateWrapper, _StateWrapper) -> _StateWrapper f.atmt_type = ATMT.RECV f.atmt_state = state.atmt_state f.atmt_condname = f.__name__ @@ -378,8 +573,14 @@ def deco(f, state=state): return deco @staticmethod - def ioevent(state, name, prio=0, as_supersocket=None): + def ioevent(state, # type: _StateWrapper + name, # type: str + prio=0, # type: int + as_supersocket=None # type: Optional[str] + ): + # type: (...) -> Callable[[_StateWrapper, _StateWrapper], _StateWrapper] # noqa: E501 def deco(f, state=state): + # type: (_StateWrapper, _StateWrapper) -> _StateWrapper f.atmt_type = ATMT.IOEVENT f.atmt_state = state.atmt_state f.atmt_condname = f.__name__ @@ -391,10 +592,37 @@ def deco(f, state=state): @staticmethod def timeout(state, timeout): - def deco(f, state=state, timeout=timeout): + # type: (_StateWrapper, Union[int, float]) -> Callable[[_StateWrapper, _StateWrapper, Timer], _StateWrapper] # noqa: E501 + def deco(f, state=state, timeout=Timer(timeout)): + # type: (_StateWrapper, _StateWrapper, Timer) -> _StateWrapper f.atmt_type = ATMT.TIMEOUT f.atmt_state = state.atmt_state f.atmt_timeout = timeout + f.atmt_timeout._func = f + f.atmt_condname = f.__name__ + return f + return deco + + @staticmethod + def timer(state, timeout, prio=0): + # type: (_StateWrapper, Union[float, int], int) -> Callable[[_StateWrapper, _StateWrapper, Timer], _StateWrapper] # noqa: E501 + def deco(f, state=state, timeout=Timer(timeout, prio=prio, autoreload=True)): + # type: (_StateWrapper, _StateWrapper, Timer) -> _StateWrapper + f.atmt_type = ATMT.TIMEOUT + f.atmt_state = state.atmt_state + f.atmt_timeout = timeout + f.atmt_timeout._func = f + f.atmt_condname = f.__name__ + return f + return deco + + @staticmethod + def eof(state): + # type: (_StateWrapper) -> Callable[[_StateWrapper, _StateWrapper], _StateWrapper] # noqa: E501 + def deco(f, state=state): + # type: (_StateWrapper, _StateWrapper) -> _StateWrapper + f.atmt_type = ATMT.EOF + f.atmt_state = state.atmt_state f.atmt_condname = f.__name__ return f return deco @@ -405,6 +633,7 @@ class _ATMT_Command: NEXT = "NEXT" FREEZE = "FREEZE" STOP = "STOP" + FORCESTOP = "FORCESTOP" END = "END" EXCEPTION = "EXCEPTION" SINGLESTEP = "SINGLESTEP" @@ -415,56 +644,69 @@ class _ATMT_Command: REJECT = "REJECT" -class _ATMT_supersocket(SuperSocket, SelectableObject): - def __init__(self, name, ioevent, automaton, proto, *args, **kargs): - SelectableObject.__init__(self) +class _ATMT_supersocket(SuperSocket): + def __init__(self, + name, # type: str + ioevent, # type: str + automaton, # type: Type[Automaton] + proto, # type: Callable[[bytes], Any] + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> None self.name = name self.ioevent = ioevent self.proto = proto # write, read - self.spa, self.spb = ObjectPipe(), ObjectPipe() - # Register recv hook - self.spb.register_hook(self.call_release) + self.spa, self.spb = ObjectPipe[Any]("spa"), \ + ObjectPipe[Any]("spb") kargs["external_fd"] = {ioevent: (self.spa, self.spb)} + kargs["is_atmt_socket"] = True + kargs["atmt_socket"] = self.name self.atmt = automaton(*args, **kargs) self.atmt.runbg() - def fileno(self): - return self.spb.fileno() - def send(self, s): - if not isinstance(s, bytes): - s = bytes(s) + # type: (Any) -> int return self.spa.send(s) - def check_recv(self): - return self.spb.check_recv() + def fileno(self): + # type: () -> int + return self.spb.fileno() - def recv(self, n=MTU): + # note: _ATMT_supersocket may return bytes in certain cases, which + # is expected. We cheat on typing. + def recv(self, n=MTU, **kwargs): # type: ignore + # type: (int, **Any) -> Any r = self.spb.recv(n) - if self.proto is not None: - r = self.proto(r) + if self.proto is not None and r is not None: + r = self.proto(r, **kwargs) return r def close(self): + # type: () -> None if not self.closed: self.atmt.stop() + self.atmt.destroy() self.spa.close() self.spb.close() self.closed = True @staticmethod def select(sockets, remain=conf.recv_poll_rate): - return select_objects(sockets, remain), None + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + return select_objects(sockets, remain) class _ATMT_to_supersocket: def __init__(self, name, ioevent, automaton): + # type: (str, str, Type[Automaton]) -> None self.name = name self.ioevent = ioevent self.automaton = automaton def __call__(self, proto, *args, **kargs): + # type: (Callable[[bytes], Any], Any, Any) -> _ATMT_supersocket return _ATMT_supersocket( self.name, self.ioevent, self.automaton, proto, *args, **kargs @@ -473,15 +715,19 @@ def __call__(self, proto, *args, **kargs): class Automaton_metaclass(type): def __new__(cls, name, bases, dct): - cls = super(Automaton_metaclass, cls).__new__(cls, name, bases, dct) + # type: (str, Tuple[Any], Dict[str, Any]) -> Type[Automaton] + cls = super(Automaton_metaclass, cls).__new__( # type: ignore + cls, name, bases, dct + ) cls.states = {} - cls.state = None - cls.recv_conditions = {} - cls.conditions = {} - cls.ioevents = {} - cls.timeout = {} - cls.actions = {} - cls.initial_states = [] + cls.recv_conditions = {} # type: Dict[str, List[_StateWrapper]] + cls.conditions = {} # type: Dict[str, List[_StateWrapper]] + cls.ioevents = {} # type: Dict[str, List[_StateWrapper]] + cls.timeout = {} # type: Dict[str, _TimerList] + cls.eofs = {} # type: Dict[str, _StateWrapper] + cls.actions = {} # type: Dict[str, List[_StateWrapper]] + cls.initial_states = [] # type: List[_StateWrapper] + cls.stop_state = None # type: Optional[_StateWrapper] cls.ionames = [] cls.iosupersockets = [] @@ -490,12 +736,12 @@ def __new__(cls, name, bases, dct): while classes: c = classes.pop(0) # order is important to avoid breaking method overloading # noqa: E501 classes += list(c.__bases__) - for k, v in six.iteritems(c.__dict__): + for k, v in c.__dict__.items(): # type: ignore if k not in members: members[k] = v - decorated = [v for v in six.itervalues(members) - if isinstance(v, types.FunctionType) and hasattr(v, "atmt_type")] # noqa: E501 + decorated = [v for v in members.values() + if hasattr(v, "atmt_type")] for m in decorated: if m.atmt_type == ATMT.STATE: @@ -504,10 +750,14 @@ def __new__(cls, name, bases, dct): cls.recv_conditions[s] = [] cls.ioevents[s] = [] cls.conditions[s] = [] - cls.timeout[s] = [] + cls.timeout[s] = _TimerList() if m.atmt_initial: cls.initial_states.append(m) - elif m.atmt_type in [ATMT.CONDITION, ATMT.RECV, ATMT.TIMEOUT, ATMT.IOEVENT]: # noqa: E501 + if m.atmt_stop: + if cls.stop_state is not None: + raise ValueError("There can only be a single stop state !") + cls.stop_state = m + elif m.atmt_type in [ATMT.CONDITION, ATMT.RECV, ATMT.TIMEOUT, ATMT.IOEVENT, ATMT.EOF]: # noqa: E501 cls.actions[m.atmt_condname] = [] for m in decorated: @@ -515,156 +765,419 @@ def __new__(cls, name, bases, dct): cls.conditions[m.atmt_state].append(m) elif m.atmt_type == ATMT.RECV: cls.recv_conditions[m.atmt_state].append(m) + elif m.atmt_type == ATMT.EOF: + cls.eofs[m.atmt_state] = m elif m.atmt_type == ATMT.IOEVENT: cls.ioevents[m.atmt_state].append(m) cls.ionames.append(m.atmt_ioname) if m.atmt_as_supersocket is not None: cls.iosupersockets.append(m) elif m.atmt_type == ATMT.TIMEOUT: - cls.timeout[m.atmt_state].append((m.atmt_timeout, m)) + cls.timeout[m.atmt_state].add_timer(m.atmt_timeout) elif m.atmt_type == ATMT.ACTION: - for c in m.atmt_cond: - cls.actions[c].append(m) - - for v in six.itervalues(cls.timeout): - v.sort(key=lambda x: x[0]) - v.append((None, None)) - for v in itertools.chain(six.itervalues(cls.conditions), - six.itervalues(cls.recv_conditions), - six.itervalues(cls.ioevents)): + for co in m.atmt_cond: + cls.actions[co].append(m) + + for v in itertools.chain( + cls.conditions.values(), + cls.recv_conditions.values(), + cls.ioevents.values() + ): v.sort(key=lambda x: x.atmt_prio) - for condname, actlst in six.iteritems(cls.actions): + for condname, actlst in cls.actions.items(): actlst.sort(key=lambda x: x.atmt_cond[condname]) for ioev in cls.iosupersockets: - setattr(cls, ioev.atmt_as_supersocket, _ATMT_to_supersocket(ioev.atmt_as_supersocket, ioev.atmt_ioname, cls)) # noqa: E501 + setattr(cls, ioev.atmt_as_supersocket, + _ATMT_to_supersocket( + ioev.atmt_as_supersocket, + ioev.atmt_ioname, + cast(Type["Automaton"], cls))) + + # Inject signature + try: + import inspect + cls.__signature__ = inspect.signature(cls.parse_args) # type: ignore # noqa: E501 + except (ImportError, AttributeError): + pass - return cls + return cast(Type["Automaton"], cls) def build_graph(self): + # type: () -> str s = 'digraph "%s" {\n' % self.__class__.__name__ se = "" # Keep initial nodes at the beginning for better rendering - for st in six.itervalues(self.states): + for st in self.states.values(): if st.atmt_initial: se = ('\t"%s" [ style=filled, fillcolor=blue, shape=box, root=true];\n' % st.atmt_state) + se # noqa: E501 elif st.atmt_final: se += '\t"%s" [ style=filled, fillcolor=green, shape=octagon ];\n' % st.atmt_state # noqa: E501 elif st.atmt_error: se += '\t"%s" [ style=filled, fillcolor=red, shape=octagon ];\n' % st.atmt_state # noqa: E501 + elif st.atmt_stop: + se += '\t"%s" [ style=filled, fillcolor=orange, shape=box, root=true ];\n' % st.atmt_state # noqa: E501 s += se - for st in six.itervalues(self.states): - for n in st.atmt_origfunc.__code__.co_names + st.atmt_origfunc.__code__.co_consts: # noqa: E501 + for st in self.states.values(): + names = list( + st.atmt_origfunc.__code__.co_names + + st.atmt_origfunc.__code__.co_consts + ) + while names: + n = names.pop() if n in self.states: - s += '\t"%s" -> "%s" [ color=green ];\n' % (st.atmt_state, n) # noqa: E501 - - for c, k, v in ([("purple", k, v) for k, v in self.conditions.items()] + # noqa: E501 - [("red", k, v) for k, v in self.recv_conditions.items()] + # noqa: E501 - [("orange", k, v) for k, v in self.ioevents.items()]): + s += '\t"%s" -> "%s" [ color=green ];\n' % (st.atmt_state, n) + elif n in self.__dict__: + # function indirection + if callable(self.__dict__[n]): + names.extend(self.__dict__[n].__code__.co_names) + names.extend(self.__dict__[n].__code__.co_consts) + + for c, sty, k, v in ( + [("purple", "solid", k, v) for k, v in self.conditions.items()] + + [("red", "solid", k, v) for k, v in self.recv_conditions.items()] + + [("orange", "solid", k, v) for k, v in self.ioevents.items()] + + [("black", "dashed", k, [v]) for k, v in self.eofs.items()] + ): for f in v: - for n in f.__code__.co_names + f.__code__.co_consts: + names = list(f.__code__.co_names + f.__code__.co_consts) + while names: + n = names.pop() if n in self.states: line = f.atmt_condname for x in self.actions[f.atmt_condname]: line += "\\l>[%s]" % x.__name__ - s += '\t"%s" -> "%s" [label="%s", color=%s];\n' % (k, n, line, c) # noqa: E501 - for k, v in six.iteritems(self.timeout): - for t, f in v: - if f is None: - continue - for n in f.__code__.co_names + f.__code__.co_consts: + s += '\t"%s" -> "%s" [label="%s", color=%s, style=%s];\n' % ( + k, + n, + line, + c, + sty, + ) + elif n in self.__dict__: + # function indirection + if callable(self.__dict__[n]) and hasattr(self.__dict__[n], "__code__"): # noqa: E501 + names.extend(self.__dict__[n].__code__.co_names) + names.extend(self.__dict__[n].__code__.co_consts) + for k, timers in self.timeout.items(): + for timer in timers: + for n in (timer._func.__code__.co_names + + timer._func.__code__.co_consts): if n in self.states: - line = "%s/%.1fs" % (f.atmt_condname, t) - for x in self.actions[f.atmt_condname]: + line = "%s/%.1fs" % (timer._func.atmt_condname, + timer.get()) + for x in self.actions[timer._func.atmt_condname]: line += "\\l>[%s]" % x.__name__ s += '\t"%s" -> "%s" [label="%s",color=blue];\n' % (k, n, line) # noqa: E501 s += "}\n" return s def graph(self, **kargs): + # type: (Any) -> Optional[str] s = self.build_graph() return do_graph(s, **kargs) -class Automaton(six.with_metaclass(Automaton_metaclass)): - def parse_args(self, debug=0, store=1, **kargs): +class Automaton(metaclass=Automaton_metaclass): + states = {} # type: Dict[str, _StateWrapper] + state = None # type: ATMT.NewStateRequested + recv_conditions = {} # type: Dict[str, List[_StateWrapper]] + conditions = {} # type: Dict[str, List[_StateWrapper]] + eofs = {} # type: Dict[str, _StateWrapper] + ioevents = {} # type: Dict[str, List[_StateWrapper]] + timeout = {} # type: Dict[str, _TimerList] + actions = {} # type: Dict[str, List[_StateWrapper]] + initial_states = [] # type: List[_StateWrapper] + stop_state = None # type: Optional[_StateWrapper] + ionames = [] # type: List[str] + iosupersockets = [] # type: List[SuperSocket] + + # used for spawn() + pkt_cls = conf.raw_layer + socketcls = StreamSocket + + # Internals + def __init__(self, *args, **kargs): + # type: (Any, Any) -> None + external_fd = kargs.pop("external_fd", {}) + if "sock" in kargs: + # We use a bi-directional sock + self.sock = kargs["sock"] + else: + # Separate sockets + self.sock = None + self.send_sock_class = kargs.pop("ll", conf.L3socket) + self.recv_sock_class = kargs.pop("recvsock", conf.L2listen) + self.listen_sock = None # type: Optional[SuperSocket] + self.send_sock = None # type: Optional[SuperSocket] + self.is_atmt_socket = kargs.pop("is_atmt_socket", False) + self.atmt_socket = kargs.pop("atmt_socket", None) + self.started = threading.Lock() + self.threadid = None # type: Optional[int] + self.breakpointed = None + self.breakpoints = set() # type: Set[_StateWrapper] + self.interception_points = set() # type: Set[_StateWrapper] + self.intercepted_packet = None # type: Union[None, Packet] + self.debug_level = 0 + self.init_args = args + self.init_kargs = kargs + self.io = type.__new__(type, "IOnamespace", (), {}) + self.oi = type.__new__(type, "IOnamespace", (), {}) + self.cmdin = ObjectPipe[Message]("cmdin") + self.cmdout = ObjectPipe[Message]("cmdout") + self.ioin = {} + self.ioout = {} + self.packets = PacketList() # type: PacketList + self.atmt_session = kargs.pop("session", None) + for n in self.__class__.ionames: + extfd = external_fd.get(n) + if not isinstance(extfd, tuple): + extfd = (extfd, extfd) + ioin, ioout = extfd + if ioin is None: + ioin = ObjectPipe("ioin") + else: + ioin = self._IO_fdwrapper(ioin, None) + if ioout is None: + ioout = ObjectPipe("ioout") + else: + ioout = self._IO_fdwrapper(None, ioout) + + self.ioin[n] = ioin + self.ioout[n] = ioout + ioin.ioname = n + ioout.ioname = n + setattr(self.io, n, self._IO_mixer(ioout, ioin)) + setattr(self.oi, n, self._IO_mixer(ioin, ioout)) + + for stname in self.states: + setattr(self, stname, + _instance_state(getattr(self, stname))) + + self.start() + + def parse_args(self, debug=0, store=0, session=None, **kargs): + # type: (int, int, Any, Any) -> None self.debug_level = debug + if debug: + conf.logLevel = logging.DEBUG + self.atmt_session = session self.socket_kargs = kargs self.store_packets = store + @classmethod + def spawn(cls, + port: int, + iface: Optional[_GlobInterfaceType] = None, + local_ip: Optional[str] = None, + bg: bool = False, + **kwargs: Any) -> Optional[socket.socket]: + """ + Spawn a TCP server that listens for connections and start the automaton + for each new client. + + :param port: the port to listen to + :param bg: background mode? (default: False) + + Note that in background mode, you shall close the TCP server as such:: + + srv = MyAutomaton.spawn(8080, bg=True) + srv.shutdown(socket.SHUT_RDWR) # important + srv.close() + """ + from scapy.arch import get_if_addr + # create server sock and bind it + ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if local_ip is None: + local_ip = get_if_addr(iface or conf.iface) + try: + ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except OSError: + pass + ssock.bind((local_ip, port)) + ssock.listen(5) + clients = [] + if kwargs.get("verb", True): + print(conf.color_theme.green( + "Server %s started listening on %s" % ( + cls.__name__, + (local_ip, port), + ) + )) + + def _run() -> None: + # Wait for clients forever + try: + while True: + atmt_server = None + clientsocket, address = ssock.accept() + if kwargs.get("verb", True): + print(conf.color_theme.gold( + "\u2503 Connection received from %s" % repr(address) + )) + try: + # Start atmt class with socket + if cls.socketcls is not None: + sock = cls.socketcls(clientsocket, cls.pkt_cls) + else: + sock = clientsocket + atmt_server = cls( + sock=sock, + iface=iface, **kwargs + ) + except OSError: + if atmt_server is not None: + atmt_server.destroy() + if kwargs.get("verb", True): + print("X Connection aborted.") + if kwargs.get("debug", 0) > 0: + traceback.print_exc() + continue + clients.append((atmt_server, clientsocket)) + # start atmt + atmt_server.runbg() + # housekeeping + for atmt, clientsocket in clients: + if not atmt.isrunning(): + atmt.destroy() + except KeyboardInterrupt: + print("X Exiting.") + ssock.shutdown(socket.SHUT_RDWR) + except OSError: + print("X Server closed.") + if kwargs.get("debug", 0) > 0: + traceback.print_exc() + finally: + for atmt, clientsocket in clients: + try: + atmt.forcestop(wait=False) + atmt.destroy() + except Exception: + pass + try: + clientsocket.shutdown(socket.SHUT_RDWR) + clientsocket.close() + except Exception: + pass + ssock.close() + if bg: + # Background + threading.Thread(target=_run).start() + return ssock + else: + # Non-background + _run() + return None + def master_filter(self, pkt): + # type: (Packet) -> bool return True - def my_send(self, pkt): - self.send_sock.send(pkt) + def my_send(self, pkt, **kwargs): + # type: (Packet, **Any) -> None + if not self.send_sock: + raise ValueError("send_sock is None !") + self.send_sock.send(pkt, **kwargs) + + def update_sock(self, sock): + # type: (SuperSocket) -> None + """ + Update the socket used by the automata. + Typically used in an eof event to reconnect. + """ + self.sock = sock + if self.listen_sock is not None: + self.listen_sock = self.sock + if self.send_sock: + self.send_sock = self.sock + + def timer_by_name(self, name): + # type: (str) -> Optional[Timer] + for _, timers in self.timeout.items(): + for timer in timers: # type: Timer + if timer._func.atmt_condname == name: + return timer + return None # Utility classes and exceptions - class _IO_fdwrapper(SelectableObject): - def __init__(self, rd, wr): - if rd is not None and not isinstance(rd, (int, ObjectPipe)): - rd = rd.fileno() - if wr is not None and not isinstance(wr, (int, ObjectPipe)): - wr = wr.fileno() + class _IO_fdwrapper: + def __init__(self, + rd, # type: Union[int, ObjectPipe[bytes], None] + wr # type: Union[int, ObjectPipe[bytes], None] + ): + # type: (...) -> None self.rd = rd self.wr = wr - SelectableObject.__init__(self) + if isinstance(self.rd, socket.socket): + self.__selectable_force_select__ = True def fileno(self): - if isinstance(self.rd, ObjectPipe): + # type: () -> int + if isinstance(self.rd, int): + return self.rd + elif self.rd: return self.rd.fileno() - return self.rd - - def check_recv(self): - return self.rd.check_recv() + return 0 def read(self, n=65535): - if isinstance(self.rd, ObjectPipe): + # type: (int) -> Optional[bytes] + if isinstance(self.rd, int): + return os.read(self.rd, n) + elif self.rd: return self.rd.recv(n) - return os.read(self.rd, n) + return None def write(self, msg): - self.call_release() - if isinstance(self.wr, ObjectPipe): - self.wr.send(msg) - return - return os.write(self.wr, msg) + # type: (bytes) -> int + if isinstance(self.wr, int): + return os.write(self.wr, msg) + elif self.wr: + return self.wr.send(msg) + return 0 def recv(self, n=65535): + # type: (int) -> Optional[bytes] return self.read(n) def send(self, msg): + # type: (bytes) -> int return self.write(msg) - class _IO_mixer(SelectableObject): - def __init__(self, rd, wr): + class _IO_mixer: + def __init__(self, + rd, # type: ObjectPipe[Any] + wr, # type: ObjectPipe[Any] + ): + # type: (...) -> None self.rd = rd self.wr = wr - SelectableObject.__init__(self) def fileno(self): - if isinstance(self.rd, int): - return self.rd - return self.rd.fileno() - - def check_recv(self): - return self.rd.check_recv() + # type: () -> Any + if isinstance(self.rd, ObjectPipe): + return self.rd.fileno() + return self.rd def recv(self, n=None): + # type: (Optional[int]) -> Any return self.rd.recv(n) def read(self, n=None): + # type: (Optional[int]) -> Any return self.recv(n) def send(self, msg): - self.wr.send(msg) - return self.call_release() + # type: (str) -> int + return self.wr.send(msg) def write(self, msg): + # type: (str) -> int return self.send(msg) class AutomatonException(Exception): def __init__(self, msg, state=None, result=None): + # type: (str, Optional[Message], Optional[str]) -> None Exception.__init__(self, msg) self.state = state self.result = result @@ -689,7 +1202,8 @@ class Singlestep(AutomatonStopped): class InterceptionPoint(AutomatonStopped): def __init__(self, msg, state=None, result=None, packet=None): - Automaton.AutomatonStopped.__init__(self, msg, state=state, result=result) # noqa: E501 + # type: (str, Optional[Message], Optional[str], Optional[Packet]) -> None + Automaton.AutomatonStopped.__init__(self, msg, state=state, result=result) self.packet = packet class CommandMessage(AutomatonException): @@ -697,16 +1211,27 @@ class CommandMessage(AutomatonException): # Services def debug(self, lvl, msg): + # type: (int, str) -> None if self.debug_level >= lvl: - log_interactive.debug(msg) + log_runtime.debug(msg) + + def isrunning(self): + # type: () -> bool + return self.started.locked() - def send(self, pkt): + def send(self, pkt, **kwargs): + # type: (Packet, **Any) -> None if self.state.state in self.interception_points: self.debug(3, "INTERCEPT: packet intercepted: %s" % pkt.summary()) self.intercepted_packet = pkt - cmd = Message(type=_ATMT_Command.INTERCEPT, state=self.state, pkt=pkt) # noqa: E501 - self.cmdout.send(cmd) + self.cmdout.send( + Message(type=_ATMT_Command.INTERCEPT, + state=self.state, pkt=pkt) + ) cmd = self.cmdin.recv() + if not cmd: + self.debug(3, "CANCELLED") + return self.intercepted_packet = None if cmd.type == _ATMT_Command.REJECT: self.debug(3, "INTERCEPT: packet rejected") @@ -718,66 +1243,22 @@ def send(self, pkt): self.debug(3, "INTERCEPT: packet accepted") else: raise self.AutomatonError("INTERCEPT: unknown verdict: %r" % cmd.type) # noqa: E501 - self.my_send(pkt) + self.my_send(pkt, **kwargs) self.debug(3, "SENT : %s" % pkt.summary()) if self.store_packets: self.packets.append(pkt.copy()) - # Internals - def __init__(self, *args, **kargs): - external_fd = kargs.pop("external_fd", {}) - self.send_sock_class = kargs.pop("ll", conf.L3socket) - self.recv_sock_class = kargs.pop("recvsock", conf.L2listen) - self.started = threading.Lock() - self.threadid = None - self.breakpointed = None - self.breakpoints = set() - self.interception_points = set() - self.intercepted_packet = None - self.debug_level = 0 - self.init_args = args - self.init_kargs = kargs - self.io = type.__new__(type, "IOnamespace", (), {}) - self.oi = type.__new__(type, "IOnamespace", (), {}) - self.cmdin = ObjectPipe() - self.cmdout = ObjectPipe() - self.ioin = {} - self.ioout = {} - for n in self.ionames: - extfd = external_fd.get(n) - if not isinstance(extfd, tuple): - extfd = (extfd, extfd) - ioin, ioout = extfd - if ioin is None: - ioin = ObjectPipe() - elif not isinstance(ioin, SelectableObject): - ioin = self._IO_fdwrapper(ioin, None) - if ioout is None: - ioout = ObjectPipe() - elif not isinstance(ioout, SelectableObject): - ioout = self._IO_fdwrapper(None, ioout) - - self.ioin[n] = ioin - self.ioout[n] = ioout - ioin.ioname = n - ioout.ioname = n - setattr(self.io, n, self._IO_mixer(ioout, ioin)) - setattr(self.oi, n, self._IO_mixer(ioin, ioout)) - - for stname in self.states: - setattr(self, stname, - _instance_state(getattr(self, stname))) - - self.start() - def __iter__(self): + # type: () -> Automaton return self def __del__(self): - self.stop() + # type: () -> None + self.destroy() def _run_condition(self, cond, *args, **kargs): + # type: (_StateWrapper, Any, Any) -> None try: self.debug(5, "Trying %s [%s]" % (cond.atmt_type, cond.atmt_condname)) # noqa: E501 cond(self, *args, **kargs) @@ -797,15 +1278,24 @@ def _run_condition(self, cond, *args, **kargs): self.debug(2, "%s [%s] not taken" % (cond.atmt_type, cond.atmt_condname)) # noqa: E501 def _do_start(self, *args, **kargs): + # type: (Any, Any) -> None ready = threading.Event() - _t = threading.Thread(target=self._do_control, args=(ready,) + (args), kwargs=kargs) # noqa: E501 - _t.setDaemon(True) + _t = threading.Thread( + target=self._do_control, + args=(ready,) + (args), + kwargs=kargs, + name="scapy.automaton _do_start" + ) + _t.daemon = True _t.start() ready.wait() def _do_control(self, ready, *args, **kargs): + # type: (threading.Event, Any, Any) -> None with self.started: - self.threadid = threading.currentThread().ident + self.threadid = threading.current_thread().ident + if self.threadid is None: + self.threadid = 0 # Update default parameters a = args + self.init_args[len(args):] @@ -815,9 +1305,11 @@ def _do_control(self, ready, *args, **kargs): # Start the automaton self.state = self.initial_states[0](self) - self.send_sock = self.send_sock_class(**self.socket_kargs) - self.listen_sock = self.recv_sock_class(**self.socket_kargs) - self.packets = PacketList(name="session[%s]" % self.__class__.__name__) # noqa: E501 + self.send_sock = self.sock or self.send_sock_class(**self.socket_kargs) + if self.recv_conditions: + # Only start a receiving socket if we have at least one recv_conditions + self.listen_sock = self.sock or self.recv_sock_class(**self.socket_kargs) # noqa: E501 + self.packets = PacketList(name="session[%s]" % self.__class__.__name__) singlestep = True iterator = self._do_iter() @@ -827,6 +1319,8 @@ def _do_control(self, ready, *args, **kargs): try: while True: c = self.cmdin.recv() + if c is None: + return None self.debug(5, "Received command %s" % c.type) if c.type == _ATMT_Command.RUN: singlestep = False @@ -835,6 +1329,14 @@ def _do_control(self, ready, *args, **kargs): elif c.type == _ATMT_Command.FREEZE: continue elif c.type == _ATMT_Command.STOP: + if self.stop_state: + # There is a stop state + self.state = self.stop_state() + iterator = self._do_iter() + else: + # Act as FORCESTOP + break + elif c.type == _ATMT_Command.FORCESTOP: break while True: state = next(iterator) @@ -854,16 +1356,18 @@ def _do_control(self, ready, *args, **kargs): self.cmdout.send(c) except Exception as e: exc_info = sys.exc_info() - self.debug(3, "Transferring exception from tid=%i:\n%s" % (self.threadid, traceback.format_exception(*exc_info))) # noqa: E501 + self.debug(3, "Transferring exception from tid=%i:\n%s" % (self.threadid, "".join(traceback.format_exception(*exc_info)))) # noqa: E501 m = Message(type=_ATMT_Command.EXCEPTION, exception=e, exc_info=exc_info) # noqa: E501 self.cmdout.send(m) self.debug(3, "Stopping control thread (tid=%i)" % self.threadid) self.threadid = None - # Close sockets - self.listen_sock.close() - self.send_sock.close() + if self.listen_sock: + self.listen_sock.close() + if self.send_sock: + self.send_sock.close() def _do_iter(self): + # type: () -> Iterator[Union[Automaton.AutomatonException, Automaton.AutomatonStopped, ATMT.NewStateRequested, None]] # noqa: E501 while True: try: self.debug(1, "## state=[%s]" % self.state.state) @@ -887,40 +1391,43 @@ def _do_iter(self): elif not isinstance(state_output, list): state_output = state_output, - # Then check immediate conditions - for cond in self.conditions[self.state.state]: - self._run_condition(cond, *state_output) - - # If still there and no conditions left, we are stuck! - if (len(self.recv_conditions[self.state.state]) == 0 and - len(self.ioevents[self.state.state]) == 0 and - len(self.timeout[self.state.state]) == 1): - raise self.Stuck("stuck in [%s]" % self.state.state, - state=self.state.state, result=state_output) # noqa: E501 + timers = self.timeout[self.state.state] + # If there are commandMessage, we should skip immediate + # conditions. + if not select_objects([self.cmdin], 0): + # Then check immediate conditions + for cond in self.conditions[self.state.state]: + self._run_condition(cond, *state_output) + + # If still there and no conditions left, we are stuck! + if (len(self.recv_conditions[self.state.state]) == 0 and + len(self.ioevents[self.state.state]) == 0 and + timers.count() == 0): + raise self.Stuck("stuck in [%s]" % self.state.state, + state=self.state.state, + result=state_output) # Finally listen and pay attention to timeouts - expirations = iter(self.timeout[self.state.state]) - next_timeout, timeout_func = next(expirations) - t0 = time.time() + timers.reset() + time_previous = time.time() - fds = [self.cmdin] - if len(self.recv_conditions[self.state.state]) > 0: + fds = [self.cmdin] # type: List[Union[SuperSocket, ObjectPipe[Any]]] + select_func = select_objects + if self.listen_sock and self.recv_conditions[self.state.state]: fds.append(self.listen_sock) + select_func = self.listen_sock.select # type: ignore for ioev in self.ioevents[self.state.state]: fds.append(self.ioin[ioev.atmt_ioname]) while True: - t = time.time() - t0 - if next_timeout is not None: - if next_timeout <= t: - self._run_condition(timeout_func, *state_output) - next_timeout, timeout_func = next(expirations) - if next_timeout is None: - remain = None - else: - remain = next_timeout - t + time_current = time.time() + timers.decrement(time_current - time_previous) + time_previous = time_current + for timer in timers.expired(): + self._run_condition(timer._func, *state_output) + remain = timers.until_next() self.debug(5, "Select on %r" % fds) - r = select_objects(fds, remain) + r = select_func(fds, remain) self.debug(5, "Selected %r" % r) for fd in r: self.debug(5, "Looking at %r" % fd) @@ -928,17 +1435,34 @@ def _do_iter(self): yield self.CommandMessage("Received command message") # noqa: E501 elif fd == self.listen_sock: try: - pkt = self.listen_sock.recv(MTU) - except recv_error: - pass - else: - if pkt is not None: - if self.master_filter(pkt): - self.debug(3, "RECVD: %s" % pkt.summary()) # noqa: E501 - for rcvcond in self.recv_conditions[self.state.state]: # noqa: E501 - self._run_condition(rcvcond, pkt, *state_output) # noqa: E501 - else: - self.debug(4, "FILTR: %s" % pkt.summary()) # noqa: E501 + pkt = self.listen_sock.recv() + except EOFError: + # Socket was closed abruptly. This will likely only + # ever happen when a client socket is passed to the + # automaton (not the case when the automaton is + # listening on a promiscuous conf.L2sniff) + self.listen_sock.close() + # False so that it is still reset by update_sock + self.listen_sock = False # type: ignore + fds.remove(fd) + if self.state.state in self.eofs: + # There is an eof state + eof = self.eofs[self.state.state] + self.debug(2, "Condition EOF [%s] taken" % eof.__name__) # noqa: E501 + raise self.eofs[self.state.state](self) + else: + # There isn't. Therefore, it's a closing condition. + raise EOFError("Socket ended arbruptly.") + if self.atmt_session is not None: + # Apply session if provided + pkt = self.atmt_session.process(pkt) + if pkt is not None: + if self.master_filter(pkt): + self.debug(3, "RECVD: %s" % pkt.summary()) # noqa: E501 + for rcvcond in self.recv_conditions[self.state.state]: # noqa: E501 + self._run_condition(rcvcond, pkt, *state_output) # noqa: E501 + else: + self.debug(4, "FILTR: %s" % pkt.summary()) # noqa: E501 else: self.debug(3, "IOEVENT on %s" % fd.ioname) for ioevt in self.ioevents[self.state.state]: @@ -950,45 +1474,65 @@ def _do_iter(self): self.state = state_req yield state_req + def __repr__(self): + # type: () -> str + return "" % ( + self.__class__.__name__, + ["HALTED", "RUNNING"][self.isrunning()] + ) + # Public API def add_interception_points(self, *ipts): + # type: (Any) -> None for ipt in ipts: if hasattr(ipt, "atmt_state"): ipt = ipt.atmt_state self.interception_points.add(ipt) def remove_interception_points(self, *ipts): + # type: (Any) -> None for ipt in ipts: if hasattr(ipt, "atmt_state"): ipt = ipt.atmt_state self.interception_points.discard(ipt) def add_breakpoints(self, *bps): + # type: (Any) -> None for bp in bps: if hasattr(bp, "atmt_state"): bp = bp.atmt_state self.breakpoints.add(bp) def remove_breakpoints(self, *bps): + # type: (Any) -> None for bp in bps: if hasattr(bp, "atmt_state"): bp = bp.atmt_state self.breakpoints.discard(bp) def start(self, *args, **kargs): - if not self.started.locked(): - self._do_start(*args, **kargs) - - def run(self, resume=None, wait=True): + # type: (Any, Any) -> None + if self.isrunning(): + raise ValueError("Already started") + # Start the control thread + self._do_start(*args, **kargs) + + def run(self, + resume=None, # type: Optional[Message] + wait=True # type: Optional[bool] + ): + # type: (...) -> Any if resume is None: resume = Message(type=_ATMT_Command.RUN) self.cmdin.send(resume) if wait: try: c = self.cmdout.recv() + if c is None: + return None except KeyboardInterrupt: self.cmdin.send(Message(type=_ATMT_Command.FREEZE)) - return + return None if c.type == _ATMT_Command.END: return c.result elif c.type == _ATMT_Command.INTERCEPT: @@ -998,31 +1542,77 @@ def run(self, resume=None, wait=True): elif c.type == _ATMT_Command.BREAKPOINT: raise self.Breakpoint("breakpoint triggered on state [%s]" % c.state.state, state=c.state.state) # noqa: E501 elif c.type == _ATMT_Command.EXCEPTION: - six.reraise(c.exc_info[0], c.exc_info[1], c.exc_info[2]) + # this code comes from the `six` module (`.reraise()`) + # to raise an exception with specified exc_info. + value = c.exc_info[0]() if c.exc_info[1] is None else c.exc_info[1] # type: ignore # noqa: E501 + if value.__traceback__ is not c.exc_info[2]: + raise value.with_traceback(c.exc_info[2]) + raise value + return None def runbg(self, resume=None, wait=False): + # type: (Optional[Message], Optional[bool]) -> None self.run(resume, wait) - def next(self): + def __next__(self): + # type: () -> Any return self.run(resume=Message(type=_ATMT_Command.NEXT)) - __next__ = next - def stop(self): - self.cmdin.send(Message(type=_ATMT_Command.STOP)) - with self.started: - # Flush command pipes - while True: - r = select_objects([self.cmdin, self.cmdout], 0) - if not r: - break - for fd in r: - fd.recv() + def _flush_inout(self): + # type: () -> None + # Flush command pipes + for cmd in [self.cmdin, self.cmdout]: + cmd.clear() + + def destroy(self): + # type: () -> None + """ + Destroys a stopped Automaton: this cleanups all opened file descriptors. + Required on PyPy for instance where the garbage collector behaves differently. + """ + if not hasattr(self, "started"): + return # was never started. + if self.isrunning(): + raise ValueError("Can't close running Automaton ! Call stop() beforehand") + # Close command pipes + self.cmdin.close() + self.cmdout.close() + self._flush_inout() + # Close opened ioins/ioouts + for i in itertools.chain(self.ioin.values(), self.ioout.values()): + if isinstance(i, ObjectPipe): + i.close() + + def stop(self, wait=True): + # type: (bool) -> None + try: + self.cmdin.send(Message(type=_ATMT_Command.STOP)) + except OSError: + pass + if wait: + with self.started: + self._flush_inout() + + def forcestop(self, wait=True): + # type: (bool) -> None + try: + self.cmdin.send(Message(type=_ATMT_Command.FORCESTOP)) + except OSError: + pass + if wait: + with self.started: + self._flush_inout() def restart(self, *args, **kargs): + # type: (Any, Any) -> None self.stop() self.start(*args, **kargs) - def accept_packet(self, pkt=None, wait=False): + def accept_packet(self, + pkt=None, # type: Optional[Packet] + wait=False # type: Optional[bool] + ): + # type: (...) -> Any rsm = Message() if pkt is None: rsm.type = _ATMT_Command.ACCEPT @@ -1031,6 +1621,9 @@ def accept_packet(self, pkt=None, wait=False): rsm.pkt = pkt return self.run(resume=rsm, wait=wait) - def reject_packet(self, wait=False): + def reject_packet(self, + wait=False # type: Optional[bool] + ): + # type: (...) -> Any rsm = Message(type=_ATMT_Command.REJECT) return self.run(resume=rsm, wait=wait) diff --git a/scapy/autorun.py b/scapy/autorun.py index 06cc8e15ee8..a0ee9e7469e 100644 --- a/scapy/autorun.py +++ b/scapy/autorun.py @@ -1,21 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Run commands when the Scapy interpreter starts. """ -from __future__ import print_function +import builtins import code +from io import StringIO +import logging +from queue import Queue import sys -import importlib +import threading +import traceback + from scapy.config import conf from scapy.themes import NoTheme, DefaultTheme, HTMLTheme2, LatexTheme2 -from scapy.error import Scapy_Exception +from scapy.error import log_scapy, Scapy_Exception from scapy.utils import tex_escape -import scapy.modules.six as six + +from typing import ( + Any, + Optional, + TextIO, + Dict, + Tuple, +) ######################### @@ -26,53 +38,60 @@ class StopAutorun(Scapy_Exception): code_run = "" +class StopAutorunTimeout(StopAutorun): + pass + + class ScapyAutorunInterpreter(code.InteractiveInterpreter): def __init__(self, *args, **kargs): + # type: (*Any, **Any) -> None code.InteractiveInterpreter.__init__(self, *args, **kargs) - self.error = 0 - def showsyntaxerror(self, *args, **kargs): - self.error = 1 - return code.InteractiveInterpreter.showsyntaxerror(self, *args, **kargs) # noqa: E501 + def write(self, data): + # type: (str) -> None + pass - def showtraceback(self, *args, **kargs): - self.error = 1 - exc_type, exc_value, exc_tb = sys.exc_info() - if isinstance(exc_value, StopAutorun): - raise exc_value - return code.InteractiveInterpreter.showtraceback(self, *args, **kargs) - -def autorun_commands(cmds, my_globals=None, ignore_globals=None, verb=None): +def autorun_commands(_cmds, my_globals=None, verb=None): + # type: (str, Optional[Dict[str, Any]], Optional[int]) -> Any sv = conf.verb try: try: if my_globals is None: - my_globals = importlib.import_module(".all", "scapy").__dict__ - if ignore_globals: - for ig in ignore_globals: - my_globals.pop(ig, None) + from scapy.main import _scapy_builtins + my_globals = _scapy_builtins() + interp = ScapyAutorunInterpreter(locals=my_globals) + try: + del builtins.__dict__["scapy_session"]["_"] + except KeyError: + pass if verb is not None: conf.verb = verb - interp = ScapyAutorunInterpreter(my_globals) cmd = "" - cmds = cmds.splitlines() + cmds = _cmds.splitlines() cmds.append("") # ensure we finish multi-line commands cmds.reverse() - six.moves.builtins.__dict__["_"] = None while True: if cmd: sys.stderr.write(sys.__dict__.get("ps2", "... ")) else: - sys.stderr.write(str(sys.__dict__.get("ps1", sys.ps1))) + sys.stderr.write(sys.__dict__.get("ps1", ">>> ")) line = cmds.pop() print(line) cmd += "\n" + line + sys.last_value = None if interp.runsource(cmd): continue - if interp.error: - return 0 + if sys.last_value: # An error occurred + traceback.print_exception( + sys.last_type, + sys.last_value, + sys.last_traceback.tb_next, + file=sys.stdout, + ) + sys.last_value = None + return False cmd = "" if len(cmds) <= 1: break @@ -80,52 +99,95 @@ def autorun_commands(cmds, my_globals=None, ignore_globals=None, verb=None): pass finally: conf.verb = sv - return _ # noqa: F821 + try: + return builtins.__dict__["scapy_session"]["_"] + except KeyError: + return builtins.__dict__.get("_", None) + + +def autorun_commands_timeout(cmds, timeout=None, **kwargs): + # type: (str, Optional[int], **Any) -> Any + """ + Wraps autorun_commands with a timeout that raises StopAutorunTimeout + on expiration. + """ + if timeout is None: + return autorun_commands(cmds, **kwargs) + + q = Queue() # type: Queue[Any] + + def _runner(): + # type: () -> None + q.put(autorun_commands(cmds, **kwargs)) + th = threading.Thread(target=_runner) + th.daemon = True + th.start() + th.join(timeout) + if th.is_alive(): + raise StopAutorunTimeout + return q.get() -class StringWriter(object): +class StringWriter(StringIO): """Util to mock sys.stdout and sys.stderr, and store their output in a 's' var.""" def __init__(self, debug=None): + # type: (Optional[TextIO]) -> None self.s = "" self.debug = debug + super().__init__() def write(self, x): - if self.debug: + # type: (str) -> int + # Object can be in the middle of being destroyed. + if getattr(self, "debug", None) and self.debug: self.debug.write(x) - self.s += x + if getattr(self, "s", None) is not None: + self.s += x + return len(x) def flush(self): - if self.debug: + # type: () -> None + if getattr(self, "debug", None) and self.debug: self.debug.flush() def autorun_get_interactive_session(cmds, **kargs): + # type: (str, **Any) -> Tuple[str, Any] """Create an interactive session and execute the commands passed as "cmds" and return all output :param cmds: a list of commands to run + :param timeout: timeout in seconds :returns: (output, returned) contains both sys.stdout and sys.stderr logs """ - sstdout, sstderr = sys.stdout, sys.stderr + sstdout, sstderr, sexcepthook = sys.stdout, sys.stderr, sys.excepthook sw = StringWriter() + h_old = log_scapy.handlers[0] + log_scapy.removeHandler(h_old) + log_scapy.addHandler(logging.StreamHandler(stream=sw)) try: try: sys.stdout = sys.stderr = sw - res = autorun_commands(cmds, **kargs) + sys.excepthook = sys.__excepthook__ + res = autorun_commands_timeout(cmds, **kargs) except StopAutorun as e: e.code_run = sw.s raise finally: - sys.stdout, sys.stderr = sstdout, sstderr + sys.stdout, sys.stderr, sys.excepthook = sstdout, sstderr, sexcepthook + log_scapy.removeHandler(log_scapy.handlers[0]) + log_scapy.addHandler(h_old) return sw.s, res def autorun_get_interactive_live_session(cmds, **kargs): + # type: (str, **Any) -> Tuple[str, Any] """Create an interactive session and execute the commands passed as "cmds" and return all output :param cmds: a list of commands to run + :param timeout: timeout in seconds :returns: (output, returned) contains both sys.stdout and sys.stderr logs """ sstdout, sstderr = sys.stdout, sys.stderr @@ -133,7 +195,7 @@ def autorun_get_interactive_live_session(cmds, **kargs): try: try: sys.stdout = sys.stderr = sw - res = autorun_commands(cmds, **kargs) + res = autorun_commands_timeout(cmds, **kargs) except StopAutorun as e: e.code_run = sw.s raise @@ -143,6 +205,7 @@ def autorun_get_interactive_live_session(cmds, **kargs): def autorun_get_text_interactive_session(cmds, **kargs): + # type: (str, **Any) -> Tuple[str, Any] ct = conf.color_theme try: conf.color_theme = NoTheme() @@ -153,6 +216,7 @@ def autorun_get_text_interactive_session(cmds, **kargs): def autorun_get_live_interactive_session(cmds, **kargs): + # type: (str, **Any) -> Tuple[str, Any] ct = conf.color_theme try: conf.color_theme = DefaultTheme() @@ -163,6 +227,7 @@ def autorun_get_live_interactive_session(cmds, **kargs): def autorun_get_ansi_interactive_session(cmds, **kargs): + # type: (str, **Any) -> Tuple[str, Any] ct = conf.color_theme try: conf.color_theme = DefaultTheme() @@ -173,8 +238,12 @@ def autorun_get_ansi_interactive_session(cmds, **kargs): def autorun_get_html_interactive_session(cmds, **kargs): + # type: (str, **Any) -> Tuple[str, Any] ct = conf.color_theme - to_html = lambda s: s.replace("<", "<").replace(">", ">").replace("#[#", "<").replace("#]#", ">") # noqa: E501 + + def to_html(s): + # type: (str) -> str + return s.replace("<", "<").replace(">", ">").replace("#[#", "<").replace("#]#", ">") # noqa: E501 try: try: conf.color_theme = HTMLTheme2() @@ -189,8 +258,12 @@ def autorun_get_html_interactive_session(cmds, **kargs): def autorun_get_latex_interactive_session(cmds, **kargs): + # type: (str, **Any) -> Tuple[str, Any] ct = conf.color_theme - to_latex = lambda s: tex_escape(s).replace("@[@", "{").replace("@]@", "}").replace("@`@", "\\") # noqa: E501 + + def to_latex(s): + # type: (str) -> str + return tex_escape(s).replace("@[@", "{").replace("@]@", "}").replace("@`@", "\\") # noqa: E501 try: try: conf.color_theme = LatexTheme2() diff --git a/scapy/base_classes.py b/scapy/base_classes.py index 78dee68054a..6940223dc3e 100644 --- a/scapy/base_classes.py +++ b/scapy/base_classes.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Generators and packet meta classes. @@ -11,32 +11,62 @@ # Generators # ################ -from __future__ import absolute_import from functools import reduce +import abc import operator import os -import re import random +import re import socket +import struct import subprocess import types +import warnings +import scapy +from scapy.error import Scapy_Exception from scapy.consts import WINDOWS -from scapy.modules.six.moves import range - -class Gen(object): - __slots__ = [] +from typing import ( + Any, + Dict, + Generic, + Iterator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + try: + import pyx + except ImportError: + pass + from scapy.packet import Packet + +_T = TypeVar("_T") + + +class Gen(Generic[_T]): + __slots__ = [] # type: List[str] def __iter__(self): + # type: () -> Iterator[_T] return iter([]) def __iterlen__(self): + # type: () -> int return sum(1 for _ in iter(self)) def _get_values(value): + # type: (Any) -> Any """Generate a range object from (start, stop[, step]) tuples, or return value. @@ -50,18 +80,17 @@ def _get_values(value): return value -class SetGen(Gen): +class SetGen(Gen[_T]): def __init__(self, values, _iterpacket=1): + # type: (Any, int) -> None self._iterpacket = _iterpacket if isinstance(values, (list, BasePacketList)): self.values = [_get_values(val) for val in values] else: self.values = [_get_values(values)] - def transf(self, element): - return element - def __iter__(self): + # type: () -> Iterator[Any] for i in self.values: if (isinstance(i, Gen) and (self._iterpacket or not isinstance(i, BasePacket))) or ( @@ -71,91 +100,227 @@ def __iter__(self): else: yield i + def __len__(self): + # type: () -> int + return self.__iterlen__() + def __repr__(self): + # type: () -> str return "" % self.values -class Net(Gen): - """Generate a list of IPs from a network address or a name""" - name = "ip" - ip_regex = re.compile(r"^(\*|[0-2]?[0-9]?[0-9](-[0-2]?[0-9]?[0-9])?)\.(\*|[0-2]?[0-9]?[0-9](-[0-2]?[0-9]?[0-9])?)\.(\*|[0-2]?[0-9]?[0-9](-[0-2]?[0-9]?[0-9])?)\.(\*|[0-2]?[0-9]?[0-9](-[0-2]?[0-9]?[0-9])?)(/[0-3]?[0-9])?$") # noqa: E501 +class _ScopedIP(str): + """ + A str that also holds extra attributes. + """ + __slots__ = ["scope"] + + def __init__(self, _: str) -> None: + self.scope = None - @staticmethod - def _parse_digit(a, netmask): - netmask = min(8, max(netmask, 0)) - if a == "*": - a = (0, 256) - elif a.find("-") >= 0: - x, y = [int(d) for d in a.split('-')] - if x > y: - y = x - a = (x & (0xff << netmask), max(y, (x | (0xff >> (8 - netmask)))) + 1) # noqa: E501 - else: - a = (int(a) & (0xff << netmask), (int(a) | (0xff >> (8 - netmask))) + 1) # noqa: E501 - return a + def __repr__(self) -> str: + val = super(_ScopedIP, self).__repr__() + if self.scope is not None: + return "ScopedIP(%s, scope=%s)" % (val, repr(self.scope)) + return val + + +def ScopedIP(net: str, scope: Optional[Any] = None) -> _ScopedIP: + """ + An str that also holds extra attributes. + + Examples:: + + >>> ScopedIP("224.0.0.1%eth0") # interface 'eth0' + >>> ScopedIP("224.0.0.1%1") # interface index 1 + >>> ScopedIP("224.0.0.1", scope=conf.iface) + """ + if "%" in net: + try: + net, scope = net.split("%", 1) + except ValueError: + raise Scapy_Exception("Scope identifier can only be present once !") + if scope is not None: + from scapy.interfaces import resolve_iface, network_name, dev_from_index + try: + iface = dev_from_index(int(scope)) + except (ValueError, TypeError): + iface = resolve_iface(scope) + if not iface.is_valid(): + raise Scapy_Exception( + "RFC6874 scope identifier '%s' could not be resolved to a " + "valid interface !" % scope + ) + scope = network_name(iface) + x = _ScopedIP(net) + x.scope = scope + return x + + +class Net(Gen[str]): + """ + Network object from an IP address or hostname and mask + + Examples: + + - With mask:: + + >>> list(Net("192.168.0.1/24")) + ['192.168.0.0', '192.168.0.1', ..., '192.168.0.255'] + + - With 'end':: + + >>> list(Net("192.168.0.100", "192.168.0.200")) + ['192.168.0.100', '192.168.0.101', ..., '192.168.0.200'] + + - With 'scope' (for multicast):: + + >>> Net("224.0.0.1%lo") + >>> Net("224.0.0.1", scope=conf.iface) + """ + name = "Net" # type: str + family = socket.AF_INET # type: int + max_mask = 32 # type: int + + @classmethod + def name2addr(cls, name): + # type: (str) -> str + try: + return next( + addr_port[0] + for family, _, _, _, addr_port in + socket.getaddrinfo(name, None, cls.family) + if family == cls.family + ) + except socket.error: + if re.search("(^|\\.)[0-9]+-[0-9]+($|\\.)", name) is not None: + raise Scapy_Exception("Ranges are no longer accepted in %s()" % + cls.__name__) + raise @classmethod - def _parse_net(cls, net): - tmp = net.split('/') + ["32"] - if not cls.ip_regex.match(net): - tmp[0] = socket.gethostbyname(tmp[0]) - netmask = int(tmp[1]) - ret_list = [cls._parse_digit(x, y - netmask) for (x, y) in zip(tmp[0].split('.'), [8, 16, 24, 32])] # noqa: E501 - return ret_list, netmask - - def __init__(self, net): - self.repr = net - self.parsed, self.netmask = self._parse_net(net) + def ip2int(cls, addr): + # type: (str) -> int + return cast(int, struct.unpack( + "!I", socket.inet_aton(cls.name2addr(addr)) + )[0]) + + @staticmethod + def int2ip(val): + # type: (int) -> str + return socket.inet_ntoa(struct.pack('!I', val)) + + def __init__(self, net, stop=None, scope=None): + # type: (str, Optional[str], Optional[str]) -> None + if "*" in net: + raise Scapy_Exception("Wildcards are no longer accepted in %s()" % + self.__class__.__name__) + self.scope = None + if "%" in net: + net = ScopedIP(net) + if isinstance(net, _ScopedIP): + self.scope = net.scope + if stop is None: + try: + net, mask = net.split("/", 1) + except ValueError: + self.mask = self.max_mask # type: Union[None, int] + else: + self.mask = int(mask) + self.net = net # type: Union[None, str] + inv_mask = self.max_mask - self.mask + self.start = self.ip2int(net) >> inv_mask << inv_mask + self.count = 1 << inv_mask + self.stop = self.start + self.count - 1 + else: + self.start = self.ip2int(net) + self.stop = self.ip2int(stop) + self.count = self.stop - self.start + 1 + self.net = self.mask = None def __str__(self): - return next(self.__iter__(), None) + # type: () -> str + return next(iter(self), "") def __iter__(self): - for d in range(*self.parsed[3]): - for c in range(*self.parsed[2]): - for b in range(*self.parsed[1]): - for a in range(*self.parsed[0]): - yield "%i.%i.%i.%i" % (a, b, c, d) + # type: () -> Iterator[str] + # Python 2 won't handle huge (> sys.maxint) values in range() + for i in range(self.count): + yield ScopedIP( + self.int2ip(self.start + i), + scope=self.scope, + ) + + def __len__(self): + # type: () -> int + return self.count def __iterlen__(self): - return reduce(operator.mul, ((y - x) for (x, y) in self.parsed), 1) + # type: () -> int + # for compatibility + return len(self) def choice(self): - return ".".join(str(random.randint(v[0], v[1] - 1)) for v in self.parsed) # noqa: E501 + # type: () -> str + return ScopedIP( + self.int2ip(random.randint(self.start, self.stop)), + scope=self.scope, + ) def __repr__(self): - return "Net(%r)" % self.repr + # type: () -> str + scope_id_repr = "" + if self.scope: + scope_id_repr = ", scope=%s" % repr(self.scope) + if self.mask is not None: + return '%s("%s/%d"%s)' % ( + self.__class__.__name__, + self.net, + self.mask, + scope_id_repr, + ) + return '%s("%s", "%s"%s)' % ( + self.__class__.__name__, + self.int2ip(self.start), + self.int2ip(self.stop), + scope_id_repr, + ) def __eq__(self, other): - if not other: + # type: (Any) -> bool + if isinstance(other, str): + return self == self.__class__(other) + if not isinstance(other, Net): return False - if hasattr(other, "parsed"): - p2 = other.parsed - else: - p2, nm2 = self._parse_net(other) - return self.parsed == p2 + if self.family != other.family: + return False + return (self.start == other.start) and (self.stop == other.stop) def __ne__(self, other): + # type: (Any) -> bool # Python 2.7 compat return not self == other - __hash__ = None + def __hash__(self): + # type: () -> int + return hash(("scapy.Net", self.family, self.start, self.stop, self.scope)) def __contains__(self, other): - if hasattr(other, "parsed"): - p2 = other.parsed - else: - p2, nm2 = self._parse_net(other) - return all(a1 <= a2 and b1 >= b2 for (a1, b1), (a2, b2) in zip(self.parsed, p2)) # noqa: E501 - - def __rcontains__(self, other): - return self in self.__class__(other) + # type: (Any) -> bool + if isinstance(other, int): + return self.start <= other <= self.stop + if isinstance(other, str): + return self.__class__(other) in self + if type(other) is not self.__class__: + return False + return self.start <= other.start <= other.stop <= self.stop -class OID(Gen): +class OID(Gen[str]): name = "OID" def __init__(self, oid): + # type: (str) -> None self.oid = oid self.cmpt = [] fmt = [] @@ -168,9 +333,11 @@ def __init__(self, oid): self.fmt = ".".join(fmt) def __repr__(self): + # type: () -> str return "OID(%r)" % self.oid def __iter__(self): + # type: () -> Iterator[str] ii = [k[0] for k in self.cmpt] while True: yield self.fmt % tuple(ii) @@ -186,6 +353,7 @@ def __iter__(self): i += 1 def __iterlen__(self): + # type: () -> int return reduce(operator.mul, (max(y - x, 0) + 1 for (x, y) in self.cmpt), 1) # noqa: E501 @@ -194,30 +362,49 @@ def __iterlen__(self): ###################################### class Packet_metaclass(type): - def __new__(cls, name, bases, dct): + def __new__(cls: Type[_T], + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type['Packet'] if "fields_desc" in dct: # perform resolution of references to other packets # noqa: E501 - current_fld = dct["fields_desc"] - resolved_fld = [] - for f in current_fld: - if isinstance(f, Packet_metaclass): # reference to another fields_desc # noqa: E501 - for f2 in f.fields_desc: - resolved_fld.append(f2) + current_fld = dct["fields_desc"] # type: List[Union[scapy.fields.Field[Any, Any], Packet_metaclass]] # noqa: E501 + resolved_fld = [] # type: List[scapy.fields.Field[Any, Any]] + for fld_or_pkt in current_fld: + if isinstance(fld_or_pkt, Packet_metaclass): + # reference to another fields_desc + for pkt_fld in fld_or_pkt.fields_desc: + resolved_fld.append(pkt_fld) else: - resolved_fld.append(f) + resolved_fld.append(fld_or_pkt) else: # look for a fields_desc in parent classes - resolved_fld = None + resolved_fld = [] for b in bases: if hasattr(b, "fields_desc"): resolved_fld = b.fields_desc break if resolved_fld: # perform default value replacements - final_fld = [] + final_fld = [] # type: List[scapy.fields.Field[Any, Any]] + names = [] for f in resolved_fld: + if f.name in names: + war_msg = ( + "Packet '%s' has a duplicated '%s' field ! " + "If you are using several ConditionalFields, have " + "a look at MultipleTypeField instead ! This will " + "become a SyntaxError in a future version of " + "Scapy !" % ( + name, f.name + ) + ) + warnings.warn(war_msg, SyntaxWarning) + names.append(f.name) if f.name in dct: f = f.copy() f.default = dct[f.name] - del(dct[f.name]) + del dct[f.name] final_fld.append(f) dct["fields_desc"] = final_fld @@ -228,32 +415,55 @@ def __new__(cls, name, bases, dct): dct["_%s" % attr] = dct.pop(attr) except KeyError: pass - newcls = super(Packet_metaclass, cls).__new__(cls, name, bases, dct) - newcls.__all_slots__ = set( + # Build and inject signature + try: + # Py3 only + import inspect + dct["__signature__"] = inspect.Signature([ + inspect.Parameter("_pkt", inspect.Parameter.POSITIONAL_ONLY), + ] + [ + inspect.Parameter(f.name, + inspect.Parameter.KEYWORD_ONLY, + default=f.default) + for f in dct["fields_desc"] + ]) + except (ImportError, AttributeError, KeyError): + pass + newcls = cast(Type['Packet'], type.__new__(cls, name, bases, dct)) + # Note: below can't be typed because we use attributes + # created dynamically.. + newcls.__all_slots__ = set( # type: ignore attr for cls in newcls.__mro__ if hasattr(cls, "__slots__") for attr in cls.__slots__ ) - newcls.aliastypes = [newcls] + getattr(newcls, "aliastypes", []) + newcls.aliastypes = ( # type: ignore + [newcls] + getattr(newcls, "aliastypes", []) + ) if hasattr(newcls, "register_variant"): newcls.register_variant() - for f in newcls.fields_desc: - if hasattr(f, "register_owner"): - f.register_owner(newcls) + for _f in newcls.fields_desc: + if hasattr(_f, "register_owner"): + _f.register_owner(newcls) if newcls.__name__[0] != "_": from scapy import config config.conf.layers.register(newcls) return newcls def __getattr__(self, attr): + # type: (str) -> Any for k in self.fields_desc: if k.name == attr: return k raise AttributeError(attr) - def __call__(cls, *args, **kargs): + def __call__(cls, + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> 'Packet' if "dispatch_hook" in cls.__dict__: try: cls = cls.dispatch_hook(*args, **kargs) @@ -262,32 +472,53 @@ def __call__(cls, *args, **kargs): if config.conf.debug_dissector: raise cls = config.conf.raw_layer - i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) + i = cls.__new__( + cls, # type: ignore + cls.__name__, + cls.__bases__, + cls.__dict__ # type: ignore + ) i.__init__(*args, **kargs) - return i + return i # type: ignore + +# Note: see compat.py for an explanation class Field_metaclass(type): - def __new__(cls, name, bases, dct): + def __new__(cls: Type[_T], + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[_T] dct.setdefault("__slots__", []) - newcls = super(Field_metaclass, cls).__new__(cls, name, bases, dct) - return newcls + newcls = type.__new__(cls, name, bases, dct) + return newcls # type: ignore -class BasePacket(Gen): - __slots__ = [] +PacketList_metaclass = Field_metaclass + + +class BasePacket(Gen['Packet']): + __slots__ = [] # type: List[str] ############################# # Packet list base class # ############################# -class BasePacketList(object): - __slots__ = [] +class BasePacketList(Gen[_T]): + __slots__ = [] # type: List[str] class _CanvasDumpExtended(object): + @abc.abstractmethod + def canvas_dump(self, layer_shift=0, rebuild=1): + # type: (int, int) -> pyx.canvas.canvas + pass + def psdump(self, filename=None, **kargs): + # type: (Optional[str], **Any) -> None """ psdump(filename=None, layer_shift=0, rebuild=1) @@ -302,7 +533,7 @@ def psdump(self, filename=None, **kargs): if filename is None: fname = get_temp_file(autoext=kargs.get("suffix", ".eps")) canvas.writeEPSfile(fname) - if WINDOWS and conf.prog.psreader is None: + if WINDOWS and not conf.prog.psreader: os.startfile(fname) else: with ContextManagerSubprocess(conf.prog.psreader): @@ -312,6 +543,7 @@ def psdump(self, filename=None, **kargs): print() def pdfdump(self, filename=None, **kargs): + # type: (Optional[str], **Any) -> None """ pdfdump(filename=None, layer_shift=0, rebuild=1) @@ -326,7 +558,7 @@ def pdfdump(self, filename=None, **kargs): if filename is None: fname = get_temp_file(autoext=kargs.get("suffix", ".pdf")) canvas.writePDFfile(fname) - if WINDOWS and conf.prog.pdfreader is None: + if WINDOWS and not conf.prog.pdfreader: os.startfile(fname) else: with ContextManagerSubprocess(conf.prog.pdfreader): @@ -336,6 +568,7 @@ def pdfdump(self, filename=None, **kargs): print() def svgdump(self, filename=None, **kargs): + # type: (Optional[str], **Any) -> None """ svgdump(filename=None, layer_shift=0, rebuild=1) @@ -350,7 +583,7 @@ def svgdump(self, filename=None, **kargs): if filename is None: fname = get_temp_file(autoext=kargs.get("suffix", ".svg")) canvas.writeSVGfile(fname) - if WINDOWS and conf.prog.svgreader is None: + if WINDOWS and not conf.prog.svgreader: os.startfile(fname) else: with ContextManagerSubprocess(conf.prog.svgreader): diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py new file mode 100644 index 00000000000..dcec5d8ed5d --- /dev/null +++ b/scapy/cbor/__init__.py @@ -0,0 +1,125 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Package holding CBOR (Concise Binary Object Representation) related modules. +Follows the same paradigm as ASN.1 implementation. +""" + +from scapy.cbor.cbor import ( + CBOR_Error, + CBOR_Encoding_Error, + CBOR_Decoding_Error, + CBOR_BadTag_Decoding_Error, + CBOR_Codecs, + CBOR_MajorTypes, + CBOR_Object, + CBOR_UNSIGNED_INTEGER, + CBOR_NEGATIVE_INTEGER, + CBOR_BYTE_STRING, + CBOR_TEXT_STRING, + CBOR_ARRAY, + CBOR_MAP, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_FALSE, + CBOR_TRUE, + CBOR_NULL, + CBOR_UNDEFINED, + CBOR_FLOAT, + CBOR_DECODING_ERROR, + RandCBORObject, +) + +from scapy.cbor.cborcodec import ( + CBORcodec_Object, + CBORcodec_UNSIGNED_INTEGER, + CBORcodec_NEGATIVE_INTEGER, + CBORcodec_BYTE_STRING, + CBORcodec_TEXT_STRING, + CBORcodec_ARRAY, + CBORcodec_MAP, + CBORcodec_SEMANTIC_TAG, + CBORcodec_SIMPLE_AND_FLOAT, +) + +from scapy.cbor.cborfields import ( + CBORF_element, + CBORF_field, + CBORF_UNSIGNED_INTEGER, + CBORF_NEGATIVE_INTEGER, + CBORF_INTEGER, + CBORF_BYTE_STRING, + CBORF_TEXT_STRING, + CBORF_BOOLEAN, + CBORF_NULL, + CBORF_UNDEFINED, + CBORF_FLOAT, + CBORF_ARRAY, + CBORF_ARRAY_OF, + CBORF_MAP, + CBORF_SEMANTIC_TAG, + CBORF_optional, + CBORF_PACKET, +) + +__all__ = [ + # Exceptions + "CBOR_Error", + "CBOR_Encoding_Error", + "CBOR_Decoding_Error", + "CBOR_BadTag_Decoding_Error", + # Codecs + "CBOR_Codecs", + "CBOR_MajorTypes", + # Objects + "CBOR_Object", + "CBOR_UNSIGNED_INTEGER", + "CBOR_NEGATIVE_INTEGER", + "CBOR_BYTE_STRING", + "CBOR_TEXT_STRING", + "CBOR_ARRAY", + "CBOR_MAP", + "CBOR_SEMANTIC_TAG", + "CBOR_SIMPLE_VALUE", + "CBOR_FALSE", + "CBOR_TRUE", + "CBOR_NULL", + "CBOR_UNDEFINED", + "CBOR_FLOAT", + "CBOR_DECODING_ERROR", + # Random/Fuzzing + "RandCBORObject", + # Codec classes + "CBORcodec_Object", + "CBORcodec_UNSIGNED_INTEGER", + "CBORcodec_NEGATIVE_INTEGER", + "CBORcodec_BYTE_STRING", + "CBORcodec_TEXT_STRING", + "CBORcodec_ARRAY", + "CBORcodec_MAP", + "CBORcodec_SEMANTIC_TAG", + "CBORcodec_SIMPLE_AND_FLOAT", + # Field base classes + "CBORF_element", + "CBORF_field", + # Scalar fields + "CBORF_UNSIGNED_INTEGER", + "CBORF_NEGATIVE_INTEGER", + "CBORF_INTEGER", + "CBORF_BYTE_STRING", + "CBORF_TEXT_STRING", + "CBORF_BOOLEAN", + "CBORF_NULL", + "CBORF_UNDEFINED", + "CBORF_FLOAT", + # Structured fields + "CBORF_ARRAY", + "CBORF_ARRAY_OF", + "CBORF_MAP", + "CBORF_SEMANTIC_TAG", + # Complex fields + "CBORF_optional", + "CBORF_PACKET", +] diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py new file mode 100644 index 00000000000..5588dba664d --- /dev/null +++ b/scapy/cbor/cbor.py @@ -0,0 +1,489 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +CBOR (Concise Binary Object Representation) - RFC 8949 +Following the ASN.1 paradigm +""" + +import random +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) + +from scapy.compat import plain_str +from scapy.error import Scapy_Exception, log_runtime +from scapy.utils import Enum_metaclass, EnumElement +from scapy.volatile import RandField + +if TYPE_CHECKING: + from scapy.cbor import CBORcodec_Object + + +class RandCBORObject(RandField["CBOR_Object[Any]"]): + """Random CBOR object generator for fuzzing""" + + def __init__(self, objlist=None): + # type: (Optional[List[Type[CBOR_Object[Any]]]]) -> None + if objlist: + self.objlist = objlist + else: + # Default list will be populated lazily to avoid forward reference + self.objlist = None # type: ignore + self.chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" # noqa: E501 + + def _get_objlist(self): + # type: () -> List[Type[CBOR_Object[Any]]] + """Get the list of CBOR object types (lazy initialization)""" + if self.objlist is None: + # Import here to avoid circular dependency + self.objlist = [ + CBOR_UNSIGNED_INTEGER, + CBOR_NEGATIVE_INTEGER, + CBOR_BYTE_STRING, + CBOR_TEXT_STRING, + CBOR_ARRAY, + CBOR_MAP, + CBOR_FALSE, + CBOR_TRUE, + CBOR_NULL, + CBOR_UNDEFINED, + CBOR_FLOAT, + ] + return self.objlist + + def _fix(self, n=0): + # type: (int) -> CBOR_Object[Any] + objlist = self._get_objlist() + + # If we're at max recursion depth and have arrays/maps in objlist, + # filter them out to avoid infinite recursion + if n >= 10: + objlist = [o for o in objlist if o not in [CBOR_ARRAY, CBOR_MAP]] + if not objlist: + # Fallback to a simple type + return CBOR_UNSIGNED_INTEGER( + abs(int(random.gauss(1000, 2000)))) + + o = random.choice(objlist) + + if o == CBOR_UNSIGNED_INTEGER: + # Random unsigned integer using gaussian distribution + return o(abs(int(random.gauss(1000, 2000)))) + elif o == CBOR_NEGATIVE_INTEGER: + # Random negative integer - ensure it's always negative + return o(-abs(int(random.gauss(1000, 2000))) - 1) + elif o == CBOR_BYTE_STRING: + # Random byte string with exponential length + length = int(random.expovariate(0.05) + 1) + return o(bytes(random.randint(0, 255) for _ in range(length))) + elif o == CBOR_TEXT_STRING: + # Random text string with exponential length + length = int(random.expovariate(0.05) + 1) + return o( + "".join(random.choice(self.chars) for _ in range(length))) + elif o == CBOR_ARRAY: + # Random array with random elements (limit recursion depth) + # Use smaller size and limit depth more aggressively for performance + size = min(int(random.expovariate(0.2) + 1), 3) # Smaller arrays + + # Get child objlist - use simple types if current list only has + # recursive types + child_objlist = self._get_objlist() + non_recursive = [ + t for t in child_objlist if t not in [CBOR_ARRAY, CBOR_MAP]] + + # If objlist only contains recursive types or we're deep, use simple + # types for children + if not non_recursive or n >= 3: + child_objlist = [ + CBOR_UNSIGNED_INTEGER, CBOR_TEXT_STRING, CBOR_NULL] + + return o([self.__class__(objlist=child_objlist)._fix(n + 1) + for _ in range(size)]) + elif o == CBOR_MAP: + # Random map with random key-value pairs (limit recursion depth) + # CBOR maps use raw Python values as keys, CBOR objects as values + # Use smaller size and limit depth more aggressively for + # performance + size = min(int(random.expovariate(0.2) + 1), 3) # Smaller maps + + # Get child objlist - use simple types if current list only has + # recursive types + child_objlist = self._get_objlist() + non_recursive = [ + t for t in child_objlist if t not in [CBOR_ARRAY, CBOR_MAP]] + + # If objlist only contains recursive types or we're deep, + # use simple types for children + if not non_recursive or n >= 3: + child_objlist = [ + CBOR_UNSIGNED_INTEGER, CBOR_TEXT_STRING, CBOR_NULL] + + map_dict = {} + for _ in range(size): + # Use simple hashable types for keys (int or str) + if random.choice([True, False]): + key = abs(int(random.gauss(100, 200))) + else: + key_len = int(random.expovariate(0.1) + 1) + key = "".join(random.choice(self.chars) for _ in range(key_len)) # noqa: E501 + val_obj = self.__class__(objlist=child_objlist)._fix(n + 1) + map_dict[key] = val_obj + return o(map_dict) + elif o == CBOR_FALSE: + return o() + elif o == CBOR_TRUE: + return o() + elif o == CBOR_NULL: + return o() + elif o == CBOR_UNDEFINED: + return o() + elif o == CBOR_FLOAT: + # Random float with gaussian distribution + return o(random.gauss(0, 1000.0)) + + # Default fallback to unsigned integer + return CBOR_UNSIGNED_INTEGER( + abs(int(random.gauss(1000, 2000)))) + + +############## +# CBOR # +############## + + +class CBOR_Error(Scapy_Exception): + pass + + +class CBOR_Encoding_Error(CBOR_Error): + pass + + +class CBOR_Decoding_Error(CBOR_Error): + pass + + +class CBOR_BadTag_Decoding_Error(CBOR_Decoding_Error): + pass + + +class CBORCodec(EnumElement): + def register_stem(cls, stem): + # type: (Type[CBORcodec_Object[Any]]) -> None + cls._stem = stem + + def dec(cls, s, context=None): + # type: (bytes, Optional[Any]) -> CBOR_Object[Any] + return cls._stem.dec(s, context=context) # type: ignore + + def safedec(cls, s, context=None): + # type: (bytes, Optional[Any]) -> CBOR_Object[Any] + return cls._stem.safedec(s, context=context) # type: ignore + + def get_stem(cls): + # type: () -> type + return cls._stem + + +class CBOR_Codecs_metaclass(Enum_metaclass): + element_class = CBORCodec + + +class CBOR_Codecs(metaclass=CBOR_Codecs_metaclass): + CBOR = cast(CBORCodec, 1) + + +class CBORTag(EnumElement): + """Represents a CBOR major type""" + + def __init__(self, + key, # type: str + value, # type: int + codec=None # type: Optional[Dict[CBORCodec, Type[CBORcodec_Object[Any]]]] # noqa: E501 + ): + # type: (...) -> None + EnumElement.__init__(self, key, value) + if codec is None: + codec = {} + self._codec = codec + + def clone(self): + # type: () -> CBORTag + return self.__class__(self._key, self._value, self._codec) + + def register_cbor_object(self, cborobj): + # type: (Type[CBOR_Object[Any]]) -> None + self._cbor_obj = cborobj + + def cbor_object(self, val): + # type: (Any) -> CBOR_Object[Any] + if hasattr(self, "_cbor_obj"): + return self._cbor_obj(val) + raise CBOR_Error("%r does not have any assigned CBOR object" % self) + + def register(self, codecnum, codec): + # type: (CBORCodec, Type[CBORcodec_Object[Any]]) -> None + self._codec[codecnum] = codec + + def get_codec(self, codec): + # type: (Any) -> Type[CBORcodec_Object[Any]] + try: + c = self._codec[codec] + except KeyError: + raise CBOR_Error("Codec %r not found for tag %r" % (codec, self)) + return c + + +class CBOR_MajorTypes_metaclass(Enum_metaclass): + element_class = CBORTag + + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBOR_MajorTypes] + rdict = {} + for k, v in dct.items(): + if isinstance(v, int): + v = CBORTag(k, v) + dct[k] = v + rdict[v] = v + elif isinstance(v, CBORTag): + rdict[v] = v + dct["__rdict__"] = rdict + + ncls = cast('Type[CBOR_MajorTypes]', + type.__new__(cls, name, bases, dct)) + return ncls + + +class CBOR_MajorTypes(metaclass=CBOR_MajorTypes_metaclass): + """CBOR Major Types (RFC 8949)""" + name = "CBOR_MAJOR_TYPES" + # CBOR major types (3-bit value in the high-order 3 bits) + UNSIGNED_INTEGER = cast(CBORTag, 0) + NEGATIVE_INTEGER = cast(CBORTag, 1) + BYTE_STRING = cast(CBORTag, 2) + TEXT_STRING = cast(CBORTag, 3) + ARRAY = cast(CBORTag, 4) + MAP = cast(CBORTag, 5) + TAG = cast(CBORTag, 6) + SIMPLE_AND_FLOAT = cast(CBORTag, 7) + + +class CBOR_Object_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBOR_Object[Any]] + c = cast( + 'Type[CBOR_Object[Any]]', + super(CBOR_Object_metaclass, cls).__new__(cls, name, bases, dct) + ) + try: + c.tag.register_cbor_object(c) + except Exception: + # Some objects may not have tags yet + log_runtime.warning("Failed to register CBOR object %r" % c) + return c + + +_K = TypeVar('_K') + + +class CBOR_Object(Generic[_K], metaclass=CBOR_Object_metaclass): + """Base class for CBOR value objects""" + tag = None # type: ignore # Subclasses must define their own tag + + def __init__(self, val): + # type: (_K) -> None + self.val = val + + def enc(self, codec=None): + # type: (Any) -> bytes + if codec is None: + codec = CBOR_Codecs.CBOR + if self.tag is None: + raise CBOR_Error("Cannot encode object without a tag") + # Pass self instead of self.val for special handling + return self.tag.get_codec(codec).enc(self) + + def __repr__(self): + # type: () -> str + return "<%s[%r]>" % (self.__class__.__name__, self.val) + + def __str__(self): + # type: () -> str + return plain_str(self.enc()) + + def __bytes__(self): + # type: () -> bytes + return self.enc() + + def strshow(self, lvl=0): + # type: (int) -> str + return (" " * lvl) + repr(self) + "\n" + + def show(self, lvl=0): + # type: (int) -> None + print(self.strshow(lvl)) + + def __eq__(self, other): + # type: (Any) -> bool + return bool(self.val == other) + + +####################### +# CBOR objects # +####################### + + +class CBOR_UNSIGNED_INTEGER(CBOR_Object[int]): + """CBOR unsigned integer (major type 0)""" + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + +class CBOR_NEGATIVE_INTEGER(CBOR_Object[int]): + """CBOR negative integer (major type 1)""" + tag = CBOR_MajorTypes.NEGATIVE_INTEGER + + +class CBOR_BYTE_STRING(CBOR_Object[bytes]): + """CBOR byte string (major type 2)""" + tag = CBOR_MajorTypes.BYTE_STRING + + +class CBOR_TEXT_STRING(CBOR_Object[str]): + """CBOR text string (major type 3)""" + tag = CBOR_MajorTypes.TEXT_STRING + + +class CBOR_ARRAY(CBOR_Object[List[Any]]): + """CBOR array (major type 4)""" + tag = CBOR_MajorTypes.ARRAY + + def strshow(self, lvl=0): + # type: (int) -> str + s = (" " * lvl) + ("# CBOR_ARRAY:") + "\n" + for o in self.val: + if hasattr(o, 'strshow'): + s += o.strshow(lvl=lvl + 1) + else: + s += (" " * (lvl + 1)) + repr(o) + "\n" + return s + + +class CBOR_MAP(CBOR_Object[Dict[Any, Any]]): + """CBOR map (major type 5)""" + tag = CBOR_MajorTypes.MAP + + def strshow(self, lvl=0): + # type: (int) -> str + s = (" " * lvl) + ("# CBOR_MAP:") + "\n" + for k, v in self.val.items(): + s += (" " * (lvl + 1)) + "Key: " + if hasattr(k, 'strshow'): + s += k.strshow(0).strip() + "\n" + else: + s += repr(k) + "\n" + s += (" " * (lvl + 1)) + "Value: " + if hasattr(v, 'strshow'): + s += v.strshow(0).strip() + "\n" + else: + s += repr(v) + "\n" + return s + + +class CBOR_SEMANTIC_TAG(CBOR_Object[Tuple[int, Any]]): + """CBOR semantic tag (major type 6)""" + tag = CBOR_MajorTypes.TAG + + +class CBOR_SIMPLE_VALUE(CBOR_Object[int]): + """CBOR simple value (major type 7)""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + +class CBOR_FALSE(CBOR_Object[bool]): + """CBOR false value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_FALSE, self).__init__(False) + + +class CBOR_TRUE(CBOR_Object[bool]): + """CBOR true value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_TRUE, self).__init__(True) + + +class CBOR_NULL(CBOR_Object[None]): + """CBOR null value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_NULL, self).__init__(None) + + +class CBOR_UNDEFINED(CBOR_Object[None]): + """CBOR undefined value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_UNDEFINED, self).__init__(None) + + +class CBOR_FLOAT(CBOR_Object[float]): + """CBOR floating-point number (major type 7)""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + +class _CBOR_ERROR(CBOR_Object[Union[bytes, CBOR_Object[Any]]]): + """CBOR decoding error wrapper""" + tag = None # type: ignore # Error objects don't have a CBOR tag + + +class CBOR_DECODING_ERROR(_CBOR_ERROR): + """CBOR decoding error object""" + + def __init__(self, val, exc=None): + # type: (Union[bytes, CBOR_Object[Any]], Optional[Exception]) -> None + CBOR_Object.__init__(self, val) + self.exc = exc + + def __repr__(self): + # type: () -> str + return "<%s[%r]{{%r}}>" % ( + self.__class__.__name__, + self.val, + self.exc and self.exc.args[0] or "" + ) + + def enc(self, codec=None): + # type: (Any) -> bytes + if isinstance(self.val, CBOR_Object): + return self.val.enc(codec) + return self.val # type: ignore diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py new file mode 100644 index 00000000000..b49b9d38b30 --- /dev/null +++ b/scapy/cbor/cborcodec.py @@ -0,0 +1,706 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +CBOR Codec Implementation - RFC 8949 +Following the BER paradigm for ASN.1 +""" + +import struct +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +from scapy.cbor.cbor import ( + CBOR_Codecs, + CBOR_DECODING_ERROR, + CBOR_Decoding_Error, + CBOR_Encoding_Error, + CBOR_Error, + CBOR_MajorTypes, + CBOR_Object, + _CBOR_ERROR, +) +from scapy.compat import chb, orb +from scapy.error import log_runtime + + +################## +# CBOR encoding # +################## + + +class CBOR_Exception(Exception): + pass + + +class CBOR_Codec_Encoding_Error(CBOR_Encoding_Error): + def __init__(self, + msg, # type: str + encoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.encoded = encoded + + +class CBOR_Codec_Decoding_Error(CBOR_Decoding_Error): + def __init__(self, + msg, # type: str + decoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.decoded = decoded + + +def CBOR_encode_head(major_type, value): + # type: (int, int) -> bytes + """ + Encode CBOR initial byte and additional info. + Format: 3 bits major type + 5 bits additional info + """ + if value < 24: + # Value fits in 5 bits + return chb((major_type << 5) | value) + elif value < 256: + # 1-byte value follows + return chb((major_type << 5) | 24) + chb(value) + elif value < 65536: + # 2-byte value follows + return chb((major_type << 5) | 25) + struct.pack(">H", value) + elif value < 4294967296: + # 4-byte value follows + return chb((major_type << 5) | 26) + struct.pack(">I", value) + else: + # 8-byte value follows + return chb((major_type << 5) | 27) + struct.pack(">Q", value) + + +def CBOR_decode_head(s): + # type: (bytes) -> Tuple[int, int, bytes] + """ + Decode CBOR initial byte and additional info. + Returns: (major_type, value, remaining_bytes) + """ + if not s: + raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) + + initial_byte = orb(s[0]) + major_type = initial_byte >> 5 + additional_info = initial_byte & 0x1f + + if additional_info < 24: + # Value is in the additional info + return major_type, additional_info, s[1:] + elif additional_info == 24: + # 1-byte value follows + if len(s) < 2: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 1-byte value", remaining=s) + return major_type, orb(s[1]), s[2:] + elif additional_info == 25: + # 2-byte value follows + if len(s) < 3: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 2-byte value", remaining=s) + value = struct.unpack(">H", s[1:3])[0] + return major_type, value, s[3:] + elif additional_info == 26: + # 4-byte value follows + if len(s) < 5: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 4-byte value", remaining=s) + value = struct.unpack(">I", s[1:5])[0] + return major_type, value, s[5:] + elif additional_info == 27: + # 8-byte value follows + if len(s) < 9: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 8-byte value", remaining=s) + value = struct.unpack(">Q", s[1:9])[0] + return major_type, value, s[9:] + else: + raise CBOR_Codec_Decoding_Error( + "Invalid additional info: %d" % additional_info, remaining=s) + + +# [ CBOR codec classes ] # + + +class CBORcodec_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBORcodec_Object[Any]] + c = cast('Type[CBORcodec_Object[Any]]', + super(CBORcodec_metaclass, cls).__new__(cls, name, bases, dct)) + try: + c.tag.register(c.codec, c) + except Exception: + log_runtime.error("Failed to register codec for tag") + return c + + +_K = TypeVar('_K') + + +class CBORcodec_Object(Generic[_K], metaclass=CBORcodec_metaclass): + """Base CBOR codec class""" + codec = CBOR_Codecs.CBOR + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + @classmethod + def cbor_object(cls, val): + # type: (_K) -> CBOR_Object[_K] + return cls.tag.cbor_object(val) + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + if not s: + raise CBOR_Codec_Decoding_Error( + "%s: Got empty object while expecting tag %r" % + (cls.__name__, cls.tag), remaining=s + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Any], bytes] + """Decode CBOR data using automatic dispatch based on major type.""" + return _decode_cbor_item(s, safe=safe) + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[Union[_CBOR_ERROR, CBOR_Object[_K]], bytes] + if not safe: + return cls.do_dec(s, context, safe) + try: + return cls.do_dec(s, context, safe) + except CBOR_Codec_Decoding_Error as e: + return CBOR_DECODING_ERROR(s, exc=e), b"" + except CBOR_Error as e: + return CBOR_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + ): + # type: (...) -> Tuple[Union[_CBOR_ERROR, CBOR_Object[_K]], bytes] + return cls.dec(s, context, safe=True) + + @classmethod + def enc(cls, s): + # type: (_K) -> bytes + raise NotImplementedError("Subclasses must implement enc") + + +CBOR_Codecs.CBOR.register_stem(CBORcodec_Object) + + +########################## +# CBORcodec objects # +########################## + + +class CBORcodec_UNSIGNED_INTEGER(CBORcodec_Object[int]): + """CBOR unsigned integer codec (major type 0)""" + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + @classmethod + def enc(cls, obj): + # type: (Union[int, CBOR_Object[int]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + i = obj.val if isinstance(obj, CBOR_Object) else obj + if i < 0: + raise CBOR_Codec_Encoding_Error( + "Cannot encode negative value as unsigned integer. " + "Use CBOR_NEGATIVE_INTEGER for negative values.") + return CBOR_encode_head(0, i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[int], bytes] + cls.check_string(s) + major_type, value, remainder = CBOR_decode_head(s) + if major_type != 0: + raise CBOR_Codec_Decoding_Error( + "Expected major type 0 (unsigned integer), got %d" % major_type, + remaining=s) + return cls.cbor_object(value), remainder + + +class CBORcodec_NEGATIVE_INTEGER(CBORcodec_Object[int]): + """CBOR negative integer codec (major type 1)""" + tag = CBOR_MajorTypes.NEGATIVE_INTEGER + + @classmethod + def enc(cls, obj): + # type: (Union[int, CBOR_Object[int]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + i = obj.val if isinstance(obj, CBOR_Object) else obj + if i >= 0: + raise CBOR_Codec_Encoding_Error( + "Cannot encode non-negative value as negative integer. " + "Use CBOR_UNSIGNED_INTEGER for non-negative values.") + # CBOR negative integer: -1 - n + return CBOR_encode_head(1, -1 - i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[int], bytes] + cls.check_string(s) + major_type, value, remainder = CBOR_decode_head(s) + if major_type != 1: + raise CBOR_Codec_Decoding_Error( + "Expected major type 1 (negative integer), got %d" % major_type, + remaining=s) + # Decode: -1 - n + return cls.cbor_object(-1 - value), remainder + + +class CBORcodec_BYTE_STRING(CBORcodec_Object[bytes]): + """CBOR byte string codec (major type 2)""" + tag = CBOR_MajorTypes.BYTE_STRING + + @classmethod + def enc(cls, obj): + # type: (Union[bytes, CBOR_Object[bytes]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + data = obj.val if isinstance(obj, CBOR_Object) else obj + if not isinstance(data, bytes): + data = bytes(data) + return CBOR_encode_head(2, len(data)) + data + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[bytes], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 2: + raise CBOR_Codec_Decoding_Error( + "Expected major type 2 (byte string), got %d" % major_type, + remaining=s) + if len(remainder) < length: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for byte string: expected %d, got %d" % + (length, len(remainder)), remaining=s) + return cls.cbor_object(remainder[:length]), remainder[length:] + + +class CBORcodec_TEXT_STRING(CBORcodec_Object[str]): + """CBOR text string codec (major type 3)""" + tag = CBOR_MajorTypes.TEXT_STRING + + @classmethod + def enc(cls, obj): + # type: (Union[str, CBOR_Object[str]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + text = obj.val if isinstance(obj, CBOR_Object) else obj + if isinstance(text, str): + text_bytes = text.encode('utf-8') + else: + text_bytes = bytes(text) + return CBOR_encode_head(3, len(text_bytes)) + text_bytes + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[str], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 3: + raise CBOR_Codec_Decoding_Error( + "Expected major type 3 (text string), got %d" % major_type, + remaining=s) + if len(remainder) < length: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for text string: expected %d, got %d" % + (length, len(remainder)), remaining=s) + try: + text = remainder[:length].decode('utf-8') + except UnicodeDecodeError as e: + raise CBOR_Codec_Decoding_Error( + "Invalid UTF-8 in text string: %s" % str(e), remaining=s) + return cls.cbor_object(text), remainder[length:] + + +class CBORcodec_ARRAY(CBORcodec_Object[List[Any]]): + """CBOR array codec (major type 4)""" + tag = CBOR_MajorTypes.ARRAY + + @classmethod + def enc(cls, obj): + # type: (Union[List[Any], CBOR_Object[List[Any]]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + array = obj.val if isinstance(obj, CBOR_Object) else obj + result = CBOR_encode_head(4, len(array)) + for item in array: + result += CBORcodec_Object.encode_cbor_item(item) + return result + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[List[Any]], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 4: + raise CBOR_Codec_Decoding_Error( + "Expected major type 4 (array), got %d" % major_type, + remaining=s) + + items = [] + for _ in range(length): + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough items in array", remaining=s) + item, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + items.append(item) + + return cls.cbor_object(items), remainder + + +class CBORcodec_MAP(CBORcodec_Object[Dict[Any, Any]]): + """CBOR map codec (major type 5)""" + tag = CBOR_MajorTypes.MAP + + @classmethod + def enc(cls, obj): + # type: (Union[Dict[Any, Any], CBOR_Object[Dict[Any, Any]]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + mapping = obj.val if isinstance(obj, CBOR_Object) else obj + result = CBOR_encode_head(5, len(mapping)) + for key, value in mapping.items(): + result += CBORcodec_Object.encode_cbor_item(key) + result += CBORcodec_Object.encode_cbor_item(value) + return result + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Dict[Any, Any]], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 5: + raise CBOR_Codec_Decoding_Error( + "Expected major type 5 (map), got %d" % major_type, + remaining=s) + + mapping = {} + for _ in range(length): + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough key-value pairs in map", remaining=s) + key, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Map key without value", remaining=s) + value, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + # Convert key to hashable type if it's a CBOR object + if isinstance(key, CBOR_Object): + key_val = key.val + else: + key_val = key + mapping[key_val] = value + + return cls.cbor_object(mapping), remainder + + +class CBORcodec_SEMANTIC_TAG(CBORcodec_Object[Tuple[int, Any]]): + """CBOR semantic tag codec (major type 6)""" + tag = CBOR_MajorTypes.TAG + + @classmethod + def enc(cls, obj): + # type: (Union[Tuple[int, Any], CBOR_Object[Tuple[int, Any]]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + tagged_item = obj.val if isinstance(obj, CBOR_Object) else obj + tag_num, item = tagged_item + result = CBOR_encode_head(6, tag_num) + result += CBORcodec_Object.encode_cbor_item(item) + return result + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Tuple[int, Any]], bytes] + cls.check_string(s) + major_type, tag_num, remainder = CBOR_decode_head(s) + if major_type != 6: + raise CBOR_Codec_Decoding_Error( + "Expected major type 6 (tag), got %d" % major_type, + remaining=s) + + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Tag without following item", remaining=s) + + item, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + return cls.cbor_object((tag_num, item)), remainder + + +class CBORcodec_SIMPLE_AND_FLOAT(CBORcodec_Object[Union[int, float, bool, None]]): + """CBOR simple values and floats codec (major type 7)""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + @classmethod + def enc(cls, obj): + # type: (Union[int, float, bool, None, CBOR_Object[Any]]) -> bytes + from scapy.cbor.cbor import ( + CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, CBOR_Object + ) + + # Check if obj is a CBOR object instance (for special cases like UNDEFINED) + if isinstance(obj, CBOR_UNDEFINED): + return chb(0xf7) # undefined + elif isinstance(obj, CBOR_NULL): + return chb(0xf6) # null + elif isinstance(obj, CBOR_TRUE): + return chb(0xf5) # true + elif isinstance(obj, CBOR_FALSE): + return chb(0xf4) # false + elif isinstance(obj, CBOR_Object): + # For other CBOR objects, use their val attribute + val = obj.val + else: + val = obj + + if val is False: + return chb(0xf4) # false + elif val is True: + return chb(0xf5) # true + elif val is None: + return chb(0xf6) # null + elif isinstance(val, float): + # Encode as double precision (8 bytes) + return chb(0xfb) + struct.pack(">d", val) + elif isinstance(val, int) and 0 <= val <= 23: + # Simple value 0-23 + return CBOR_encode_head(7, val) + else: + raise CBOR_Codec_Encoding_Error( + "Cannot encode value as simple/float: %r" % val) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Any], bytes] + from scapy.cbor.cbor import ( + CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, + CBOR_FLOAT, CBOR_SIMPLE_VALUE + ) + + cls.check_string(s) + + # For major type 7, we need special handling because additional_info + # encodes different things (simple values vs float sizes) + initial_byte = orb(s[0]) + major_type = initial_byte >> 5 + additional_info = initial_byte & 0x1f + + if major_type != 7: + raise CBOR_Codec_Decoding_Error( + "Expected major type 7 (simple/float), got %d" % major_type, + remaining=s) + + # Check for special simple values (encoded directly in additional_info) + if additional_info == 20: + return CBOR_FALSE(), s[1:] + elif additional_info == 21: + return CBOR_TRUE(), s[1:] + elif additional_info == 22: + return CBOR_NULL(), s[1:] + elif additional_info == 23: + return CBOR_UNDEFINED(), s[1:] + elif additional_info == 25: + # Half precision float (2 bytes) - IEEE 754 binary16 + if len(s) < 3: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for half float", remaining=s) + half_bytes = s[1:3] + remainder = s[3:] + # Convert IEEE 754 binary16 to binary64 (double) + half_int = struct.unpack(">H", half_bytes)[0] + sign = (half_int >> 15) & 0x1 + exponent = (half_int >> 10) & 0x1f + fraction = half_int & 0x3ff + + # Handle special cases + if exponent == 0: + if fraction == 0: + # Zero + float_val = -0.0 if sign else 0.0 + else: + # Subnormal number + float_val = ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) + elif exponent == 31: + if fraction == 0: + # Infinity + float_val = float('-inf') if sign else float('inf') + else: + # NaN + float_val = float('nan') + else: + # Normalized number + float_val = ( + ((-1) ** sign) * + (1 + fraction / 1024.0) * + (2 ** (exponent - 15))) + + return CBOR_FLOAT(float_val), remainder + elif additional_info == 26: + # Single precision float (4 bytes) + if len(s) < 5: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for single float", remaining=s) + float_val = struct.unpack(">f", s[1:5])[0] + return CBOR_FLOAT(float_val), s[5:] + elif additional_info == 27: + # Double precision float (8 bytes) + if len(s) < 9: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for double float", remaining=s) + float_val = struct.unpack(">d", s[1:9])[0] + return CBOR_FLOAT(float_val), s[9:] + elif additional_info < 24: + # Simple value 0-23 + return CBOR_SIMPLE_VALUE(additional_info), s[1:] + else: + # additional_info 24 means 1-byte simple value follows + if additional_info == 24: + if len(s) < 2: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for simple value", remaining=s) + return CBOR_SIMPLE_VALUE(orb(s[1])), s[2:] + else: + raise CBOR_Codec_Decoding_Error( + "Invalid additional info for major type 7: %d" % additional_info, + remaining=s) + + +# Helper methods for encoding/decoding arbitrary CBOR items + + +def _encode_cbor_item(item): + # type: (Any) -> bytes + """Encode a Python value to CBOR bytes""" + from scapy.cbor.cbor import CBOR_Object + + if isinstance(item, CBOR_Object): + return item.enc() + elif isinstance(item, bool): + # Must check bool before int (bool is subclass of int) + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif isinstance(item, int): + if item >= 0: + return CBORcodec_UNSIGNED_INTEGER.enc(item) + else: + return CBORcodec_NEGATIVE_INTEGER.enc(item) + elif isinstance(item, bytes): + return CBORcodec_BYTE_STRING.enc(item) + elif isinstance(item, str): + return CBORcodec_TEXT_STRING.enc(item) + elif isinstance(item, list): + return CBORcodec_ARRAY.enc(item) + elif isinstance(item, dict): + return CBORcodec_MAP.enc(item) + elif isinstance(item, float): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif item is None: + return CBORcodec_SIMPLE_AND_FLOAT.enc(None) + else: + raise CBOR_Codec_Encoding_Error( + "Cannot encode type: %s" % type(item)) + + +def _decode_cbor_item(s, safe=False): + # type: (bytes, bool) -> Tuple[CBOR_Object[Any], bytes] + """Decode CBOR bytes to a CBOR_Object""" + if not s: + raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) + + initial_byte = orb(s[0]) + major_type = initial_byte >> 5 + + # Dispatch to appropriate codec based on major type + if major_type == 0: + return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=safe) + elif major_type == 1: + return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=safe) + elif major_type == 2: + return CBORcodec_BYTE_STRING.dec(s, safe=safe) + elif major_type == 3: + return CBORcodec_TEXT_STRING.dec(s, safe=safe) + elif major_type == 4: + return CBORcodec_ARRAY.dec(s, safe=safe) + elif major_type == 5: + return CBORcodec_MAP.dec(s, safe=safe) + elif major_type == 6: + return CBORcodec_SEMANTIC_TAG.dec(s, safe=safe) + elif major_type == 7: + return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=safe) + else: + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, remaining=s) + + +# Add helper methods to CBORcodec_Object +CBORcodec_Object.encode_cbor_item = staticmethod(_encode_cbor_item) +CBORcodec_Object.decode_cbor_item = staticmethod(_decode_cbor_item) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py new file mode 100644 index 00000000000..536424728ec --- /dev/null +++ b/scapy/cbor/cborfields.py @@ -0,0 +1,904 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Classes that implement CBOR (Concise Binary Object Representation) data +structures as packet fields. Modelled after scapy/asn1fields.py. +""" + +import copy + +from functools import reduce + +from scapy.cbor.cbor import ( + CBOR_Decoding_Error, + CBOR_Error, + CBOR_MajorTypes, + CBOR_Object, + CBOR_UNSIGNED_INTEGER, + CBOR_NEGATIVE_INTEGER, + CBOR_BYTE_STRING, + CBOR_TEXT_STRING, + CBOR_SEMANTIC_TAG, + CBOR_FALSE, + CBOR_TRUE, + CBOR_NULL, + CBOR_UNDEFINED, + CBOR_FLOAT, +) +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + CBOR_decode_head, + CBOR_encode_head, + CBORcodec_Object, + CBORcodec_UNSIGNED_INTEGER, + CBORcodec_NEGATIVE_INTEGER, + CBORcodec_BYTE_STRING, + CBORcodec_TEXT_STRING, + CBORcodec_SIMPLE_AND_FLOAT, +) +from scapy.base_classes import BasePacket +from scapy.volatile import ( + RandChoice, + RandFloat, + RandNum, + RandString, + RandField, +) + +from scapy import packet + +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.cborpacket import CBOR_Packet # noqa: F401 + + +class CBORF_badsequence(Exception): + pass + + +class CBORF_element(object): + pass + + +########################## +# Basic CBOR Field # +########################## + +_I = TypeVar('_I') # Internal storage +_A = TypeVar('_A') # CBOR object + + +class CBORF_field(CBORF_element, Generic[_I, _A]): + holds_packets = 0 + islist = 0 + CBOR_tag = None # type: Optional[Any] + + def __init__(self, + name, # type: str + default, # type: Optional[_A] + ): + # type: (...) -> None + self.name = name + if default is None: + self.default = default # type: Optional[_A] + else: + self.default = self._wrap(default) + self.owners = [] # type: List[Type[CBOR_Packet]] + + def _wrap(self, val): + # type: (Any) -> _A + """Return a CBOR object wrapping *val*. + + The base implementation is a pass-through cast; subclasses override + this to convert a raw Python value to the appropriate CBOR object + type (e.g. :class:`~scapy.cbor.cbor.CBOR_UNSIGNED_INTEGER`). + """ + return cast(_A, val) + + def register_owner(self, cls): + # type: (Type[CBOR_Packet]) -> None + self.owners.append(cls) + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, _I) -> str + return repr(x) + + def i2h(self, pkt, x): + # type: (CBOR_Packet, _I) -> Any + return x + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[_A, bytes] + raise NotImplementedError("Subclasses must implement m2i") + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Union[bytes, _I, _A]) -> bytes + if x is None: + return b"" + if isinstance(x, CBOR_Object): + return x.enc() + return self._encode(x) + + def _encode(self, x): + # type: (Any) -> bytes + """Encode a raw Python value to CBOR bytes.""" + raise NotImplementedError("Subclasses must implement _encode") + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> _I + return cast(_I, x) + + def extract_packet(self, + cls, # type: Type[CBOR_Packet] + s, # type: bytes + _underlayer=None, # type: Optional[CBOR_Packet] + ): + # type: (...) -> Tuple[CBOR_Packet, bytes] + try: + c = cls(s, _underlayer=_underlayer) + except CBORF_badsequence: + c = packet.Raw(s, _underlayer=_underlayer) # type: ignore + cpad = c.getlayer(packet.Raw) + s = b"" + if cpad is not None: + s = cpad.load + if cpad.underlayer: + del cpad.underlayer.payload + return c, s + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + return self.i2m(pkt, getattr(pkt, self.name)) + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + v, s = self.m2i(pkt, s) + self.set_val(pkt, v) + return s + + def do_copy(self, x): + # type: (Any) -> Any + if isinstance(x, list): + x = x[:] + for i in range(len(x)): + if isinstance(x[i], BasePacket): + x[i] = x[i].copy() + return x + if hasattr(x, "copy"): + return x.copy() + return x + + def set_val(self, pkt, val): + # type: (CBOR_Packet, Any) -> None + setattr(pkt, self.name, val) + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return getattr(pkt, self.name) is None + + def get_fields_list(self): + # type: () -> List[CBORF_field[Any, Any]] + return [self] + + def __str__(self): + # type: () -> str + return repr(self) + + def randval(self): + # type: () -> RandField[_I] + return cast(RandField[_I], RandNum(0, 2 ** 32)) + + def copy(self): + # type: () -> CBORF_field[_I, _A] + return copy.copy(self) + + +############################# +# Simple CBOR Fields # +############################# + +class CBORF_UNSIGNED_INTEGER(CBORF_field[int, CBOR_UNSIGNED_INTEGER]): + """CBOR unsigned integer field (major type 0).""" + CBOR_tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + def _wrap(self, val): + # type: (Any) -> CBOR_UNSIGNED_INTEGER + if isinstance(val, CBOR_UNSIGNED_INTEGER): + return val + return CBOR_UNSIGNED_INTEGER(int(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_UNSIGNED_INTEGER, bytes] + return CBORcodec_UNSIGNED_INTEGER.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_UNSIGNED_INTEGER.enc( + x if isinstance(x, CBOR_Object) else CBOR_UNSIGNED_INTEGER(int(x)) + ) + + def randval(self): + # type: () -> RandNum + return RandNum(0, 2 ** 64 - 1) + + +class CBORF_NEGATIVE_INTEGER(CBORF_field[int, CBOR_NEGATIVE_INTEGER]): + """CBOR negative integer field (major type 1).""" + CBOR_tag = CBOR_MajorTypes.NEGATIVE_INTEGER + + def _wrap(self, val): + # type: (Any) -> CBOR_NEGATIVE_INTEGER + if isinstance(val, CBOR_NEGATIVE_INTEGER): + return val + return CBOR_NEGATIVE_INTEGER(int(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_NEGATIVE_INTEGER, bytes] + return CBORcodec_NEGATIVE_INTEGER.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_NEGATIVE_INTEGER.enc( + x if isinstance(x, CBOR_Object) else CBOR_NEGATIVE_INTEGER(int(x)) + ) + + def randval(self): + # type: () -> RandNum + return RandNum(-2 ** 64, -1) + + +class CBORF_INTEGER(CBORF_field[int, + Union[CBOR_UNSIGNED_INTEGER, + CBOR_NEGATIVE_INTEGER]]): + """CBOR integer field handling both positive and negative values.""" + + def _wrap(self, val): + # type: (Any) -> Union[CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER] + if isinstance(val, (CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER)): + return val + i = int(val) + if i >= 0: + return CBOR_UNSIGNED_INTEGER(i) + return CBOR_NEGATIVE_INTEGER(i) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Union[CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER], bytes] # noqa: E501 + if not s: + raise CBOR_Decoding_Error("Empty CBOR data") + major_type = (s[0] >> 5) & 0x7 + if major_type == 0: + return CBORcodec_UNSIGNED_INTEGER.dec(s) # type: ignore + elif major_type == 1: + return CBORcodec_NEGATIVE_INTEGER.dec(s) # type: ignore + raise CBOR_Decoding_Error( + "Expected integer (major type 0 or 1), got %d" % major_type) + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, CBOR_Object): + return x.enc() + i = int(x) + if i >= 0: + return CBORcodec_UNSIGNED_INTEGER.enc(CBOR_UNSIGNED_INTEGER(i)) + return CBORcodec_NEGATIVE_INTEGER.enc(CBOR_NEGATIVE_INTEGER(i)) + + def randval(self): + # type: () -> RandNum + return RandNum(-2 ** 64, 2 ** 64 - 1) + + +class CBORF_BYTE_STRING(CBORF_field[bytes, CBOR_BYTE_STRING]): + """CBOR byte string field (major type 2).""" + CBOR_tag = CBOR_MajorTypes.BYTE_STRING + + def _wrap(self, val): + # type: (Any) -> CBOR_BYTE_STRING + if isinstance(val, CBOR_BYTE_STRING): + return val + return CBOR_BYTE_STRING(bytes(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_BYTE_STRING, bytes] + return CBORcodec_BYTE_STRING.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_BYTE_STRING.enc( + x if isinstance(x, CBOR_Object) else CBOR_BYTE_STRING(bytes(x)) + ) + + def randval(self): + # type: () -> RandString + return RandString(RandNum(0, 1000)) + + +class CBORF_TEXT_STRING(CBORF_field[str, CBOR_TEXT_STRING]): + """CBOR text string field (major type 3).""" + CBOR_tag = CBOR_MajorTypes.TEXT_STRING + + def _wrap(self, val): + # type: (Any) -> CBOR_TEXT_STRING + if isinstance(val, CBOR_TEXT_STRING): + return val + return CBOR_TEXT_STRING(str(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_TEXT_STRING, bytes] + return CBORcodec_TEXT_STRING.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_TEXT_STRING.enc( + x if isinstance(x, CBOR_Object) else CBOR_TEXT_STRING(str(x)) + ) + + def randval(self): + # type: () -> RandString + return RandString(RandNum(0, 1000)) + + +class CBORF_BOOLEAN(CBORF_field[bool, Union[CBOR_FALSE, CBOR_TRUE]]): + """CBOR boolean field (major type 7, simple values 20/21).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def _wrap(self, val): + # type: (Any) -> Union[CBOR_FALSE, CBOR_TRUE] + if isinstance(val, (CBOR_FALSE, CBOR_TRUE)): + return val + return CBOR_TRUE() if val else CBOR_FALSE() + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Union[CBOR_FALSE, CBOR_TRUE], bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, (CBOR_FALSE, CBOR_TRUE)): + raise CBOR_Decoding_Error( + "Expected boolean (CBOR_FALSE or CBOR_TRUE), got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, (CBOR_FALSE, CBOR_TRUE)): + return x.enc() + return CBORcodec_SIMPLE_AND_FLOAT.enc( + CBOR_TRUE() if x else CBOR_FALSE() + ) + + def randval(self): + # type: () -> RandChoice + return RandChoice(True, False) + + +class CBORF_NULL(CBORF_field[None, CBOR_NULL]): + """CBOR null field (major type 7, simple value 22).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self, + name, # type: str + default=None, # type: None + ): + # type: (...) -> None + super(CBORF_NULL, self).__init__(name, None) + + def _wrap(self, val): + # type: (Any) -> CBOR_NULL + return CBOR_NULL() + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_NULL, bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, CBOR_NULL): + raise CBOR_Decoding_Error( + "Expected null, got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + return CBOR_NULL().enc() + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return False + + +class CBORF_UNDEFINED(CBORF_field[None, CBOR_UNDEFINED]): + """CBOR undefined field (major type 7, simple value 23).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self, + name, # type: str + default=None, # type: None + ): + # type: (...) -> None + super(CBORF_UNDEFINED, self).__init__(name, None) + + def _wrap(self, val): + # type: (Any) -> CBOR_UNDEFINED + return CBOR_UNDEFINED() + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_UNDEFINED, bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, CBOR_UNDEFINED): + raise CBOR_Decoding_Error( + "Expected undefined, got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + return CBOR_UNDEFINED().enc() + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return False + + +class CBORF_FLOAT(CBORF_field[float, CBOR_FLOAT]): + """CBOR float field (major type 7, double precision).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def _wrap(self, val): + # type: (Any) -> CBOR_FLOAT + if isinstance(val, CBOR_FLOAT): + return val + return CBOR_FLOAT(float(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_FLOAT, bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, CBOR_FLOAT): + raise CBOR_Decoding_Error( + "Expected float, got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, CBOR_FLOAT): + return x.enc() + return CBORcodec_SIMPLE_AND_FLOAT.enc(CBOR_FLOAT(float(x))) + + def randval(self): + # type: () -> RandFloat + return RandFloat(0, 2 ** 32) + + +############################## +# Structured CBOR Fields # +############################## + +class CBORF_ARRAY(CBORF_field[List[Any], List[Any]]): + """ + CBOR array with a fixed sequence of named, typed fields (major type 4). + Analogous to ASN1F_SEQUENCE: each positional element corresponds to a + specific CBORF_field. The CBOR array count must match the number of + declared fields. + + Example:: + + class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("name", ""), + ) + """ + CBOR_tag = CBOR_MajorTypes.ARRAY + holds_packets = 1 + + def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None + # The array itself is a structural field without its own named slot on + # the packet; a placeholder name is used so the base class __init__ + # stays happy. Individual element fields are the ones that carry names. + name = "_cbor_array" + default = [field.default for field in seq] + super(CBORF_ARRAY, self).__init__(name, None) + self.default = default + self.seq = seq + self.islist = len(seq) > 1 + + def __repr__(self): + # type: () -> str + return "<%s%r>" % (self.__class__.__name__, self.seq) + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return all(f.is_empty(pkt) for f in self.seq) + + def get_fields_list(self): + # type: () -> List[CBORF_field[Any, Any]] + return reduce(lambda x, y: x + y.get_fields_list(), + self.seq, []) + + def m2i(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + """ + Decode a CBOR array. Each element is decoded by its corresponding + field in ``self.seq``. The decoded values are set directly on the + packet by each field's ``dissect`` call, so this method returns an + empty list (which is discarded by ``dissect``). + """ + try: + major_type, count, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 4: + raise CBOR_Decoding_Error( + "Expected major type 4 (array), got %d" % major_type) + if count != len(self.seq): + raise CBOR_Decoding_Error( + "Array length mismatch: expected %d, got %d" % + (len(self.seq), count)) + for obj in self.seq: + try: + s = obj.dissect(pkt, s) + except CBORF_badsequence: + break + return [], s + + def dissect(self, pkt, s): + # type: (Any, bytes) -> bytes + _, x = self.m2i(pkt, s) + return x + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + items = b"".join(obj.build(pkt) for obj in self.seq) + return CBOR_encode_head(4, len(self.seq)) + items + + +_ARRAY_T = Union[ + 'CBOR_Packet', + Type[CBORF_field[Any, Any]], + 'CBORF_PACKET', + CBORF_field[Any, Any], +] + + +class CBORF_ARRAY_OF(CBORF_field[List[_ARRAY_T], List[CBOR_Object[Any]]]): + """ + CBOR array of homogeneous elements (major type 4). + Analogous to ASN1F_SEQUENCE_OF: variable-length array where every + element shares the same type, specified by ``cls``. + + ``cls`` may be a :class:`CBORF_field` class/instance (leaf type) or a + :class:`CBOR_Packet` subclass (structured type). + """ + CBOR_tag = CBOR_MajorTypes.ARRAY + islist = 1 + + def __init__(self, + name, # type: str + default, # type: Any + cls, # type: _ARRAY_T + ): + # type: (...) -> None + if isinstance(cls, type) and issubclass(cls, CBORF_field) or \ + isinstance(cls, CBORF_field): + if isinstance(cls, type): + self.fld = cls("_item", None) # type: ignore + else: + self.fld = cls + self._extract_item = lambda s, pkt: self.fld.m2i(pkt, s) + self.holds_packets = 0 + elif hasattr(cls, "CBOR_root") or callable(cls): + self.cls = cast("Type[CBOR_Packet]", cls) + self._extract_item = lambda s, pkt: self.extract_packet( + self.cls, s, _underlayer=pkt) + self.holds_packets = 1 + else: + raise ValueError("cls must be a CBORF_field or CBOR_Packet") + super(CBORF_ARRAY_OF, self).__init__(name, None) + self.default = default + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return CBORF_field.is_empty(self, pkt) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[List[Any], bytes] + try: + major_type, count, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 4: + raise CBOR_Decoding_Error( + "Expected major type 4 (array), got %d" % major_type) + lst = [] + for _ in range(count): + c, s = self._extract_item(s, pkt) # type: ignore + if c is not None: + lst.append(c) + return lst, s + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + val = getattr(pkt, self.name) + if val is None: + val = [] + items = b"".join(bytes(item) for item in val) + return CBOR_encode_head(4, len(val)) + items + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + if self.holds_packets: + return repr(x) + elif x is None: + return "[]" + else: + return "[%s]" % ", ".join( + self.fld.i2repr(pkt, item) for item in x # type: ignore + ) + + def __repr__(self): + # type: () -> str + return "<%s %s>" % (self.__class__.__name__, self.name) + + +class CBORF_MAP(CBORF_field[Dict[str, Any], Dict[str, Any]]): + """ + CBOR map with a fixed set of named, typed fields (major type 5). + + Each field in ``seq`` represents one key-value pair. The key is the + field's ``name`` encoded as a CBOR text string. The value is encoded + and decoded by the corresponding :class:`CBORF_field`. + + Example:: + + class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("name", ""), + ) + """ + CBOR_tag = CBOR_MajorTypes.MAP + holds_packets = 1 + + def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None + # The map itself is a structural field without its own named slot on + # the packet; a placeholder name is used so the base class __init__ + # stays happy. Individual value fields are the ones that carry names + # (which also serve as the CBOR text-string keys in the wire encoding). + name = "_cbor_map" + default = {field.name: field.default for field in seq} + super(CBORF_MAP, self).__init__(name, None) + self.default = default + self.seq = seq + self.islist = 1 + + def __repr__(self): + # type: () -> str + return "<%s%r>" % (self.__class__.__name__, self.seq) + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return all(f.is_empty(pkt) for f in self.seq) + + def get_fields_list(self): + # type: () -> List[CBORF_field[Any, Any]] + return reduce(lambda x, y: x + y.get_fields_list(), + self.seq, []) + + def m2i(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + """ + Decode a CBOR map. Keys are decoded as CBOR items and matched to + fields by name. Values are decoded by the matching field. Unknown + keys are silently skipped. + """ + try: + major_type, count, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 5: + raise CBOR_Decoding_Error( + "Expected major type 5 (map), got %d" % major_type) + # Build a lookup from field name to field object. + field_map = {f.name: f for f in self.seq} + for _ in range(count): + # Decode the key (any CBOR type; convert to str for lookup). + key_obj, s = CBORcodec_Object.decode_cbor_item(s) + if isinstance(key_obj, CBOR_Object): + key = str(key_obj.val) + else: + key = str(key_obj) + fld = field_map.get(key) + if fld is not None: + s = fld.dissect(pkt, s) + else: + # Skip unknown value. + _unknown, s = CBORcodec_Object.decode_cbor_item(s) + return [], s + + def dissect(self, pkt, s): + # type: (Any, bytes) -> bytes + _, x = self.m2i(pkt, s) + return x + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + result = CBOR_encode_head(5, len(self.seq)) + for fld in self.seq: + # Encode key as a CBOR text string. + result += CBORcodec_TEXT_STRING.enc(CBOR_TEXT_STRING(fld.name)) + result += fld.build(pkt) + return result + + +class CBORF_SEMANTIC_TAG(CBORF_field[Tuple[int, Any], + CBOR_SEMANTIC_TAG]): + """ + CBOR semantic tag field (major type 6). + + Wraps an ``inner_field`` with the given numeric ``tag_num``. The inner + field handles encoding and decoding of the tagged value. The outer field + (named ``name``) stores the :class:`~scapy.cbor.cbor.CBOR_SEMANTIC_TAG` + wrapper (tag number + ``None`` placeholder), while the inner field stores + its value under its own name on the packet. + + Example:: + + class TimestampPkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + "tag_info", None, 1, CBORF_INTEGER("ts", 0) + ) + """ + CBOR_tag = CBOR_MajorTypes.TAG + + def __init__(self, + name, # type: str + default, # type: Any + tag_num, # type: int + inner_field, # type: CBORF_field[Any, Any] + ): + # type: (...) -> None + self.tag_num = tag_num + self.inner_field = inner_field + super(CBORF_SEMANTIC_TAG, self).__init__(name, default) + + def _wrap(self, val): + # type: (Any) -> CBOR_SEMANTIC_TAG + if isinstance(val, CBOR_SEMANTIC_TAG): + return val + return CBOR_SEMANTIC_TAG((self.tag_num, val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_SEMANTIC_TAG, bytes] + try: + major_type, tag_num, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 6: + raise CBOR_Decoding_Error( + "Expected major type 6 (semantic tag), got %d" % major_type) + return CBOR_SEMANTIC_TAG((tag_num, None)), s # type: ignore + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + tag_obj, s = self.m2i(pkt, s) + self.set_val(pkt, tag_obj) + # Dissect the tagged content using the inner field. + return self.inner_field.dissect(pkt, s) + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + inner_bytes = self.inner_field.build(pkt) + return CBOR_encode_head(6, self.tag_num) + inner_bytes + + def get_fields_list(self): + # type: () -> List[CBORF_field[Any, Any]] + return [self] + self.inner_field.get_fields_list() + + +############################## +# Complex CBOR Fields # +############################## + +class CBORF_optional(CBORF_element): + """ + Wrapper making a :class:`CBORF_field` optional. + + During decoding, if the next CBOR item does not match the expected major + type, the field value is set to ``None`` and the stream is left unchanged. + """ + + def __init__(self, field): + # type: (CBORF_field[Any, Any]) -> None + self._field = field + + def __getattr__(self, attr): + # type: (str) -> Optional[Any] + return getattr(self._field, attr) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] + try: + return self._field.m2i(pkt, s) + except (CBOR_Error, CBORF_badsequence, + CBOR_Codec_Decoding_Error): + return None, s + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + try: + return self._field.dissect(pkt, s) + except (CBOR_Error, CBORF_badsequence, + CBOR_Codec_Decoding_Error): + self._field.set_val(pkt, None) + return s + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + if self._field.is_empty(pkt): + return b"" + return self._field.build(pkt) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> Any + return self._field.any2i(pkt, x) + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + return self._field.i2repr(pkt, x) + + +class CBORF_PACKET(CBORF_field['CBOR_Packet', Optional['CBOR_Packet']]): + """ + CBOR field that encapsulates a nested :class:`CBOR_Packet`. + + The nested packet is encoded as-is (its ``CBOR_root.build()`` output) + and decoded by instantiating ``cls`` from the current byte stream. + """ + holds_packets = 1 + + def __init__(self, + name, # type: str + default, # type: Optional[CBOR_Packet] + cls, # type: Type[CBOR_Packet] + ): + # type: (...) -> None + self.cls = cls + super(CBORF_PACKET, self).__init__(name, None) + self.default = default + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] + return self.extract_packet(self.cls, s, _underlayer=pkt) + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, bytes): + return x + return bytes(x) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> CBOR_Packet + if hasattr(x, "add_underlayer"): + x.add_underlayer(pkt) + return super(CBORF_PACKET, self).any2i(pkt, x) # type: ignore + + def randval(self): # type: ignore + # type: () -> CBOR_Packet + return packet.fuzz(self.cls()) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py new file mode 100644 index 00000000000..eb12bedaea9 --- /dev/null +++ b/scapy/cborpacket.py @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +CBOR Packet + +Packet holding data encoded in Concise Binary Object Representation (CBOR). +Modelled after scapy/asn1packet.py. +""" + +from scapy.base_classes import Packet_metaclass +from scapy.packet import Packet + +from typing import ( + Any, + Dict, + Tuple, + Type, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.cbor.cborfields import CBORF_field # noqa: F401 + + +class CBORPacket_metaclass(Packet_metaclass): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBOR_Packet] + if dct.get("CBOR_root") is not None: + dct["fields_desc"] = dct["CBOR_root"].get_fields_list() + return cast( + 'Type[CBOR_Packet]', + super(CBORPacket_metaclass, cls).__new__(cls, name, bases, dct), + ) + + +class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): + CBOR_root = cast('CBORF_field[Any, Any]', None) + + def self_build(self): + # type: () -> bytes + """Build this CBOR packet to wire bytes using CBOR_root. + + Returns the raw packet cache when already built, otherwise delegates + to CBOR_root.build() which encodes all fields according to the CBOR + schema defined for this packet. + """ + if self.raw_packet_cache is not None: + return self.raw_packet_cache + return self.CBOR_root.build(self) + + def do_dissect(self, x): + # type: (bytes) -> bytes + """Dissect CBOR-encoded bytes into packet fields. + + Delegates to CBOR_root.dissect() which reads CBOR items from *x*, + populates each field on the packet, and returns any unconsumed bytes. + """ + return self.CBOR_root.dissect(self, x) diff --git a/scapy/compat.py b/scapy/compat.py index ffc73eacc78..d8e5c050750 100644 --- a/scapy/compat.py +++ b/scapy/compat.py @@ -1,149 +1,203 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information """ -Python 2 and 3 link classes. +Compatibility module to various older versions of Python """ -from __future__ import absolute_import import base64 import binascii -import gzip +import enum import struct +import sys + +from typing import ( + Any, + AnyStr, + Callable, + Optional, + TypeVar, + TYPE_CHECKING, + Union, +) + +# Very important: will issue typing errors otherwise +__all__ = [ + # typing + 'DecoratorCallable', + 'Literal', + 'Protocol', + 'Self', + 'UserDict', + # compat + 'base64_bytes', + 'bytes_base64', + 'bytes_encode', + 'bytes_hex', + 'chb', + 'hex_bytes', + 'orb', + 'plain_str', + 'raw', + 'StrEnum', +] + +# Typing compatibility + +# Note: +# supporting typing on multiple python versions is a nightmare. +# we provide a FakeType class to be able to use types added on +# later Python versions (since we run mypy on 3.14), on older +# ones. + + +# Import or create fake types + +def _FakeType(name, cls=object): + # type: (str, Optional[type]) -> Any + class _FT(object): + def __init__(self, name): + # type: (str) -> None + self.name = name + + # make the objects subscriptable indefinitely + def __getitem__(self, item): # type: ignore + return cls + + def __call__(self, *args, **kargs): + # type: (*Any, **Any) -> Any + if isinstance(args[0], str): + self.name = args[0] + return self + + def __repr__(self): + # type: () -> str + return "" % self.name + return _FT(name) + + +# Python 3.8 Only +if sys.version_info >= (3, 8): + from typing import Literal + from typing import Protocol +else: + Literal = _FakeType("Literal") + + class Protocol: + pass + + +# Python 3.9 Only +if sys.version_info >= (3, 9): + from collections import UserDict +else: + from collections import UserDict as _UserDict + UserDict = _FakeType("_UserDict", _UserDict) + + +# Python 3.11 Only +if sys.version_info >= (3, 11): + from typing import Self +else: + Self = _FakeType("Self") + + +# Python 3.11 Only +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + class StrEnum(str, enum.Enum): + pass -import scapy.modules.six as six ########### # Python3 # ########### +# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators +DecoratorCallable = TypeVar("DecoratorCallable", bound=Callable[..., Any]) + + +# This is ugly, but we don't want to move raw() out of compat.py +# and it makes it much clearer +if TYPE_CHECKING: + from scapy.packet import Packet -def lambda_tuple_converter(func): + +def raw(x): + # type: (Packet) -> bytes """ - Converts a Python 2 function as - lambda (x,y): x + y - In the Python 3 format: - lambda x,y : x + y + Builds a packet and returns its bytes representation. + This function is and will always be cross-version compatible """ - if func is not None and func.__code__.co_argcount == 1: - return lambda *args: func(args[0] if len(args) == 1 else args) - else: - return func - - -if six.PY2: - bytes_encode = plain_str = str - chb = lambda x: x if isinstance(x, str) else chr(x) - orb = ord - - def raw(x): - """Builds a packet and returns its bytes representation. - This function is and always be cross-version compatible""" - if hasattr(x, "__bytes__"): - return x.__bytes__() - return bytes(x) -else: - def raw(x): - """Builds a packet and returns its bytes representation. - This function is and always be cross-version compatible""" - return bytes(x) - - def bytes_encode(x): - """Ensure that the given object is bytes. - If the parameter is a packet, raw() should be preferred. - """ - if isinstance(x, str): - return x.encode() - return bytes(x) - - def plain_str(x): - """Convert basic byte objects to str""" - if isinstance(x, bytes): - return x.decode(errors="ignore") - return str(x) - - def chb(x): - """Same than chr() but encode as bytes.""" - return struct.pack("!B", x) - - def orb(x): - """Return ord(x) when not already an int.""" - if isinstance(x, int): - return x - return ord(x) + return bytes(x) + + +def bytes_encode(x): + # type: (Any) -> bytes + """Ensure that the given object is bytes. If the parameter is a + packet, raw() should be preferred. + + """ + if isinstance(x, str): + return x.encode() + return bytes(x) + + +def plain_str(x): + # type: (Any) -> str + """Convert basic byte objects to str""" + if isinstance(x, bytes): + return x.decode(errors="backslashreplace") + return str(x) + + +def chb(x): + # type: (int) -> bytes + """Same than chr() but encode as bytes.""" + return struct.pack("!B", x) + + +def orb(x): + # type: (Union[int, str, bytes]) -> int + """Return ord(x) when not already an int.""" + if isinstance(x, int): + return x + return ord(x) def bytes_hex(x): + # type: (AnyStr) -> bytes """Hexify a str or a bytes object""" return binascii.b2a_hex(bytes_encode(x)) def hex_bytes(x): + # type: (AnyStr) -> bytes """De-hexify a str or a byte object""" return binascii.a2b_hex(bytes_encode(x)) +def int_bytes(x, size): + # type: (int, int) -> bytes + """Convert an int to an arbitrary sized bytes string""" + return x.to_bytes(size, byteorder='big') + + +def bytes_int(x): + # type: (bytes) -> int + """Convert an arbitrary sized bytes string to an int""" + return int.from_bytes(x, "big") + + def base64_bytes(x): + # type: (AnyStr) -> bytes """Turn base64 into bytes""" - if six.PY2: - return base64.decodestring(x) return base64.decodebytes(bytes_encode(x)) def bytes_base64(x): + # type: (AnyStr) -> bytes """Turn bytes into base64""" - if six.PY2: - return base64.encodestring(x).replace('\n', '') return base64.encodebytes(bytes_encode(x)).replace(b'\n', b'') - - -if six.PY2: - from StringIO import StringIO - - def gzip_decompress(x): - """Decompress using gzip""" - with gzip.GzipFile(fileobj=StringIO(x), mode='rb') as fdesc: - return fdesc.read() - - def gzip_compress(x): - """Compress using gzip""" - buf = StringIO() - with gzip.GzipFile(fileobj=buf, mode='wb') as fdesc: - fdesc.write(x) - return buf.getvalue() -else: - gzip_decompress = gzip.decompress - gzip_compress = gzip.compress - -# Typing compatibility - -try: - # Only required if using mypy-lang for static typing - from typing import Optional, List, Union, Callable, Any, AnyStr, Tuple, \ - Sized, Dict, Pattern, cast -except ImportError: - # Let's make some fake ones. - - def cast(_type, obj): - return obj - - class _FakeType(object): - # make the objects subscriptable indefinetly - def __getitem__(self, item): - return _FakeType() - - Optional = _FakeType() - Union = _FakeType() - Callable = _FakeType() - List = _FakeType() - Dict = _FakeType() - Any = _FakeType() - AnyStr = _FakeType() - Tuple = _FakeType() - Pattern = _FakeType() - - class Sized(object): - pass diff --git a/scapy/config.py b/scapy/config.py index 0fa4bb14edf..cae73d73f1a 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -1,26 +1,70 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Implementation of the configuration object. """ -from __future__ import absolute_import -from __future__ import print_function +import atexit +import copy import functools import os +import pathlib import re -import time import socket import sys +import time +import warnings + +from dataclasses import dataclass +from enum import Enum + +import importlib +import importlib.abc +import importlib.util -from scapy import VERSION, base_classes +import scapy +from scapy import VERSION +from scapy.base_classes import BasePacket from scapy.consts import DARWIN, WINDOWS, LINUX, BSD, SOLARIS -from scapy.error import log_scapy, warning, ScapyInvalidPlatformException -from scapy.modules import six -from scapy.themes import NoTheme, apply_ipython_style +from scapy.error import ( + log_loading, + log_scapy, + ScapyInvalidPlatformException, + warning, +) +from scapy.themes import ColorTheme, NoTheme, apply_ipython_style + +# Typing imports +from typing import ( + cast, + Any, + Callable, + Dict, + Iterator, + List, + NoReturn, + Optional, + Set, + Tuple, + Type, + Union, + overload, + TYPE_CHECKING, +) +from types import ModuleType +from scapy.compat import DecoratorCallable + +if TYPE_CHECKING: + # Do not import at runtime + import scapy.as_resolvers + from scapy.modules.nmap import NmapKnowledgeBase + from scapy.packet import Packet + from scapy.supersocket import SuperSocket # noqa: F401 + import scapy.asn1.asn1 + import scapy.asn1.mib ############ # Config # @@ -29,16 +73,19 @@ class ConfClass(object): def configure(self, cnf): + # type: (ConfClass) -> None self.__dict__ = cnf.__dict__.copy() def __repr__(self): + # type: () -> str return str(self) def __str__(self): + # type: () -> str s = "" - keys = self.__class__.__dict__.copy() - keys.update(self.__dict__) - keys = sorted(keys) + dkeys = self.__class__.__dict__.copy() + dkeys.update(self.__dict__) + keys = sorted(dkeys) for i in keys: if i[0] != "_": r = repr(getattr(self, i)) @@ -51,8 +98,14 @@ def __str__(self): class Interceptor(object): - def __init__(self, name=None, default=None, - hook=None, args=None, kargs=None): + def __init__(self, + name, # type: str + default, # type: Any + hook, # type: Callable[..., Any] + args=None, # type: Optional[List[Any]] + kargs=None # type: Optional[Dict[str, Any]] + ): + # type: (...) -> None self.name = name self.intname = "_intercepted_%s" % name self.default = default @@ -61,21 +114,26 @@ def __init__(self, name=None, default=None, self.kargs = kargs if kargs is not None else {} def __get__(self, obj, typ=None): + # type: (Conf, Optional[type]) -> Any if not hasattr(obj, self.intname): setattr(obj, self.intname, self.default) return getattr(obj, self.intname) @staticmethod def set_from_hook(obj, name, val): + # type: (Conf, str, bool) -> None int_name = "_intercepted_%s" % name setattr(obj, int_name, val) def __set__(self, obj, val): + # type: (Conf, Any) -> None + old = getattr(obj, self.intname, self.default) + val = self.hook(self.name, val, old, *self.args, **self.kargs) setattr(obj, self.intname, val) - self.hook(self.name, val, *self.args, **self.kargs) def _readonly(name): + # type: (str) -> NoReturn default = Conf.__dict__[name].default Interceptor.set_from_hook(conf, name, default) raise ValueError("Read-only value !") @@ -89,46 +147,58 @@ def _readonly(name): class ProgPath(ConfClass): - universal_open = "open" if DARWIN else "xdg-open" - pdfreader = universal_open - psreader = universal_open - svgreader = universal_open - dot = "dot" - display = "display" - tcpdump = "tcpdump" - tcpreplay = "tcpreplay" - hexedit = "hexer" - tshark = "tshark" - wireshark = "wireshark" - ifconfig = "ifconfig" + _default: str = "" + universal_open: str = "open" if DARWIN else "xdg-open" + pdfreader: str = universal_open + psreader: str = universal_open + svgreader: str = universal_open + dot: str = "dot" + display: str = "display" + tcpdump: str = "tcpdump" + tcpreplay: str = "tcpreplay" + hexedit: str = "hexer" + tshark: str = "tshark" + wireshark: str = "wireshark" + ifconfig: str = "ifconfig" + extcap_folders: List[str] = [ + os.path.join(os.path.expanduser("~"), ".config", "wireshark", "extcap"), + "/usr/lib/x86_64-linux-gnu/wireshark/extcap", + ] class ConfigFieldList: def __init__(self): - self.fields = set() - self.layers = set() + # type: () -> None + self.fields = set() # type: Set[Any] + self.layers = set() # type: Set[Any] @staticmethod def _is_field(f): + # type: (Any) -> bool return hasattr(f, "owners") def _recalc_layer_list(self): + # type: () -> None self.layers = {owner for f in self.fields for owner in f.owners} def add(self, *flds): + # type: (*Any) -> None self.fields |= {f for f in flds if self._is_field(f)} self._recalc_layer_list() def remove(self, *flds): + # type: (*Any) -> None self.fields -= set(flds) self._recalc_layer_list() def __contains__(self, elt): - if isinstance(elt, base_classes.Packet_metaclass): + # type: (Any) -> bool + if isinstance(elt, BasePacket): return elt in self.layers return elt in self.fields def __repr__(self): + # type: () -> str return "<%s [%s]>" % (self.__class__.__name__, " ".join(str(x) for x in self.fields)) # noqa: E501 @@ -142,42 +212,65 @@ class Resolve(ConfigFieldList): class Num2Layer: def __init__(self): - self.num2layer = {} - self.layer2num = {} + # type: () -> None + self.num2layer = {} # type: Dict[int, Type[Packet]] + self.layer2num = {} # type: Dict[Type[Packet], int] def register(self, num, layer): + # type: (int, Type[Packet]) -> None self.register_num2layer(num, layer) self.register_layer2num(num, layer) def register_num2layer(self, num, layer): + # type: (int, Type[Packet]) -> None self.num2layer[num] = layer def register_layer2num(self, num, layer): + # type: (int, Type[Packet]) -> None self.layer2num[layer] = num + @overload def __getitem__(self, item): - if isinstance(item, base_classes.Packet_metaclass): + # type: (Type[Packet]) -> int + pass + + @overload + def __getitem__(self, item): # noqa: F811 + # type: (int) -> Type[Packet] + pass + + def __getitem__(self, item): # noqa: F811 + # type: (Union[int, Type[Packet]]) -> Union[int, Type[Packet]] + if isinstance(item, int): + return self.num2layer[item] + else: return self.layer2num[item] - return self.num2layer[item] def __contains__(self, item): - if isinstance(item, base_classes.Packet_metaclass): + # type: (Union[int, Type[Packet]]) -> bool + if isinstance(item, int): + return item in self.num2layer + else: return item in self.layer2num - return item in self.num2layer - def get(self, item, default=None): + def get(self, + item, # type: Union[int, Type[Packet]] + default=None, # type: Optional[Type[Packet]] + ): + # type: (...) -> Optional[Union[int, Type[Packet]]] return self[item] if item in self else default def __repr__(self): + # type: () -> str lst = [] - for num, layer in six.iteritems(self.num2layer): + for num, layer in self.num2layer.items(): if layer in self.layer2num and self.layer2num[layer] == num: dir = "<->" else: dir = " ->" lst.append((num, "%#6x %s %-20s (%s)" % (num, dir, layer.__name__, layer._name))) - for layer, num in six.iteritems(self.layer2num): + for layer, num in self.layer2num.items(): if num not in self.num2layer or self.num2layer[num] != layer: lst.append((num, "%#6x <- %-20s (%s)" % (num, layer.__name__, layer._name))) @@ -185,71 +278,130 @@ def __repr__(self): return "\n".join(y for x, y in lst) -class LayersList(list): - +class LayersList(List[Type['scapy.packet.Packet']]): def __init__(self): + # type: () -> None list.__init__(self) - self.ldict = {} + self.ldict = {} # type: Dict[str, List[Type[Packet]]] + self.filtered = False + self._backup_dict = {} # type: Dict[Type[Packet], List[Tuple[Dict[str, Any], Type[Packet]]]] # noqa: E501 def __repr__(self): - return "\n".join("%-20s: %s" % (l.__name__, l.name) for l in self) + # type: () -> str + return "\n".join("%-20s: %s" % (layer.__name__, layer.name) + for layer in self) def register(self, layer): + # type: (Type[Packet]) -> None self.append(layer) + + # Skip arch* modules + if layer.__module__.startswith("scapy.arch."): + return + + # Register in module if layer.__module__ not in self.ldict: self.ldict[layer.__module__] = [] self.ldict[layer.__module__].append(layer) def layers(self): + # type: () -> List[Tuple[str, str]] result = [] # This import may feel useless, but it is required for the eval below import scapy # noqa: F401 + try: + import builtins # noqa: F401 + except ImportError: + import __builtin__ # noqa: F401 for lay in self.ldict: - doc = eval(lay).__doc__ + try: + doc = eval(lay).__doc__ + except AttributeError: + continue result.append((lay, doc.strip().split("\n")[0] if doc else lay)) return result - -class CommandsList(list): + def filter(self, items): + # type: (List[Type[Packet]]) -> None + """Disable dissection of unused layers to speed up dissection""" + if self.filtered: + raise ValueError("Already filtered. Please disable it first") + for lay in self.ldict.values(): + for cls in lay: + if cls not in self._backup_dict: + self._backup_dict[cls] = cls.payload_guess[:] + cls.payload_guess = [ + y for y in cls.payload_guess if y[1] in items + ] + self.filtered = True + + def unfilter(self): + # type: () -> None + """Re-enable dissection for all layers""" + if not self.filtered: + raise ValueError("Not filtered. Please filter first") + for lay in self.ldict.values(): + for cls in lay: + cls.payload_guess = self._backup_dict[cls] + self._backup_dict.clear() + self.filtered = False + + +class CommandsList(List[Callable[..., Any]]): def __repr__(self): + # type: () -> str s = [] - for l in sorted(self, key=lambda x: x.__name__): - doc = l.__doc__.split("\n")[0] if l.__doc__ else "--" - s.append("%-20s: %s" % (l.__name__, doc)) + for li in sorted(self, key=lambda x: x.__name__): + doc = li.__doc__ if li.__doc__ else "--" + doc = doc.lstrip().split('\n', 1)[0] + s.append("%-22s: %s" % (li.__name__, doc)) return "\n".join(s) def register(self, cmd): + # type: (DecoratorCallable) -> DecoratorCallable self.append(cmd) return cmd # return cmd so that method can be used as a decorator def lsc(): + # type: () -> None """Displays Scapy's default commands""" print(repr(conf.commands)) -class CacheInstance(dict, object): - __slots__ = ["timeout", "name", "_timetable", "__dict__"] +class CacheInstance(Dict[str, Any]): + __slots__ = ["timeout", "name", "_timetable"] def __init__(self, name="noname", timeout=None): + # type: (str, Optional[int]) -> None self.timeout = timeout self.name = name - self._timetable = {} + self._timetable = {} # type: Dict[str, float] def flush(self): - self.__init__(name=self.name, timeout=self.timeout) + # type: () -> None + self._timetable.clear() + self.clear() def __getitem__(self, item): + # type: (str) -> Any if item in self.__slots__: return object.__getattribute__(self, item) - val = dict.__getitem__(self, item) + if not self.__contains__(item): + raise KeyError(item) + return super(CacheInstance, self).__getitem__(item) + + def __contains__(self, item): + if not super(CacheInstance, self).__contains__(item): + return False if self.timeout is not None: t = self._timetable[item] if time.time() - t > self.timeout: - raise KeyError(item) - return val + return False + return True def get(self, item, default=None): + # type: (str, Optional[Any]) -> Any # overloading this method is needed to force the dict to go through # the timetable check try: @@ -258,13 +410,18 @@ def get(self, item, default=None): return default def __setitem__(self, item, v): + # type: (str, str) -> None if item in self.__slots__: return object.__setattr__(self, item, v) self._timetable[item] = time.time() - dict.__setitem__(self, item, v) - - def update(self, other): - for key, value in six.iteritems(other): + super(CacheInstance, self).__setitem__(item, v) + + def update(self, + other, # type: Any + **kwargs # type: Any + ): + # type: (...) -> None + for key, value in other.items(): # We only update an element from `other` either if it does # not exist in `self` or if the entry in `self` is older. if key not in self or self._timetable[key] < other._timetable[key]: @@ -272,78 +429,101 @@ def update(self, other): self._timetable[key] = other._timetable[key] def iteritems(self): + # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return six.iteritems(self.__dict__) + return super(CacheInstance, self).items() t0 = time.time() - return ((k, v) for (k, v) in six.iteritems(self.__dict__) if t0 - self._timetable[k] < self.timeout) # noqa: E501 + return ( + (k, v) + for (k, v) in super(CacheInstance, self).items() + if t0 - self._timetable[k] < self.timeout + ) def iterkeys(self): + # type: () -> Iterator[str] if self.timeout is None: - return six.iterkeys(self.__dict__) + return super(CacheInstance, self).keys() t0 = time.time() - return (k for k in six.iterkeys(self.__dict__) if t0 - self._timetable[k] < self.timeout) # noqa: E501 + return ( + k + for k in super(CacheInstance, self).keys() + if t0 - self._timetable[k] < self.timeout + ) def __iter__(self): - return six.iterkeys(self.__dict__) + # type: () -> Iterator[str] + return self.iterkeys() def itervalues(self): + # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return six.itervalues(self.__dict__) + return super(CacheInstance, self).values() t0 = time.time() - return (v for (k, v) in six.iteritems(self.__dict__) if t0 - self._timetable[k] < self.timeout) # noqa: E501 + return ( + v + for (k, v) in super(CacheInstance, self).items() + if t0 - self._timetable[k] < self.timeout + ) def items(self): - if self.timeout is None: - return dict.items(self) - t0 = time.time() - return [(k, v) for (k, v) in six.iteritems(self.__dict__) if t0 - self._timetable[k] < self.timeout] # noqa: E501 + # type: () -> Any + return list(self.iteritems()) def keys(self): - if self.timeout is None: - return dict.keys(self) - t0 = time.time() - return [k for k in six.iterkeys(self.__dict__) if t0 - self._timetable[k] < self.timeout] # noqa: E501 + # type: () -> Any + return list(self.iterkeys()) def values(self): - if self.timeout is None: - return list(six.itervalues(self)) - t0 = time.time() - return [v for (k, v) in six.iteritems(self.__dict__) if t0 - self._timetable[k] < self.timeout] # noqa: E501 + # type: () -> Any + return list(self.itervalues()) def __len__(self): + # type: () -> int if self.timeout is None: - return dict.__len__(self) + return super(CacheInstance, self).__len__() return len(self.keys()) def summary(self): + # type: () -> str return "%s: %i valid items. Timeout=%rs" % (self.name, len(self), self.timeout) # noqa: E501 def __repr__(self): + # type: () -> str s = [] if self: - mk = max(len(k) for k in six.iterkeys(self.__dict__)) + mk = max(len(k) for k in self) fmt = "%%-%is %%s" % (mk + 1) - for item in six.iteritems(self.__dict__): + for item in self.items(): s.append(fmt % item) return "\n".join(s) + def copy(self): + # type: () -> CacheInstance + return copy.copy(self) + class NetCache: def __init__(self): - self._caches_list = [] + # type: () -> None + self._caches_list = [] # type: List[CacheInstance] def add_cache(self, cache): + # type: (CacheInstance) -> None self._caches_list.append(cache) setattr(self, cache.name, cache) def new_cache(self, name, timeout=None): + # type: (str, Optional[int]) -> CacheInstance c = CacheInstance(name=name, timeout=timeout) self.add_cache(c) + return c def __delattr__(self, attr): + # type: (str) -> NoReturn raise AttributeError("Cannot delete attributes") def update(self, other): + # type: (NetCache) -> None for co in other._caches_list: if hasattr(self, co.name): getattr(self, co.name).update(co) @@ -351,14 +531,201 @@ def update(self, other): self.add_cache(co.copy()) def flush(self): + # type: () -> None for c in self._caches_list: c.flush() def __repr__(self): + # type: () -> str return "\n".join(c.summary() for c in self._caches_list) +class ScapyExt: + __slots__ = ["specs", "name", "version", "bash_completions"] + + class MODE(Enum): + LAYERS = "layers" + CONTRIB = "contrib" + MODULES = "modules" + + @dataclass + class ScapyExtSpec: + fullname: str + mode: 'ScapyExt.MODE' + spec: Any + default: bool + + def __init__(self): + self.specs: Dict[str, 'ScapyExt.ScapyExtSpec'] = {} + self.bash_completions = {} + + def config(self, name, version): + self.name = name + self.version = version + + def register(self, name, mode, path, default=None): + assert mode in self.MODE, "mode must be one of ScapyExt.MODE !" + fullname = f"scapy.{mode.value}.{name}" + spec = importlib.util.spec_from_file_location( + fullname, + str(path), + ) + spec = self.ScapyExtSpec( + fullname=fullname, + mode=mode, + spec=spec, + default=default or False, + ) + if default is None: + spec.default = bool(importlib.util.find_spec(spec.fullname)) + self.specs[fullname] = spec + + def register_bashcompletion(self, script: pathlib.Path): + self.bash_completions[script.name] = script + + def __repr__(self): + return "" % ( + self.name, + self.version, + len(self.specs), + ) + + +class ExtsManager(importlib.abc.MetaPathFinder): + __slots__ = ["exts", "all_specs"] + + GPLV2_LICENCES = [ + "GPL-2.0-only", + "GPL-2.0-or-later", + ] + + def __init__(self): + self.exts: List[ScapyExt] = [] + self.all_specs: Dict[str, ScapyExt.ScapyExtSpec] = {} + self._loaded: List[str] = [] + # Add to meta_path as we are an import provider + if self not in sys.meta_path: + sys.meta_path.append(self) + + def find_spec(self, fullname, path, target=None): + if fullname in self.all_specs: + return self.all_specs[fullname].spec + + def invalidate_caches(self): + pass + + def _register_spec(self, spec): + # Register to known specs + self.all_specs[spec.fullname] = spec + + # If default=True, inject it in the currently loaded modules + if spec.default: + loader = importlib.util.LazyLoader(spec.spec.loader) + spec.spec.loader = loader + module = importlib.util.module_from_spec(spec.spec) + sys.modules[spec.fullname] = module + loader.exec_module(module) + + def load(self, extension: str): + """ + Load a scapy extension. + + :param extension: the name of the extension, as installed. + """ + if extension in self._loaded: + return + + try: + import importlib.metadata + except ImportError: + log_loading.warning( + "'%s' not loaded. " + "Scapy extensions require at least Python 3.8+ !" % extension + ) + return + + # Get extension distribution + try: + distr = importlib.metadata.distribution(extension) + except importlib.metadata.PackageNotFoundError: + log_loading.warning("The extension '%s' was not found !" % extension) + return + + # Check the classifiers + if ( + distr.metadata.get('License-Expression', None) not in self.GPLV2_LICENCES + and distr.metadata.get('License', None) not in self.GPLV2_LICENCES + ): + log_loading.warning( + "'%s' has no GPLv2 classifier therefore cannot be loaded." % extension + ) + return + + # Create the extension + ext = ScapyExt() + + # Get the top-level declared "import packages" + # HACK: not available nicely in importlib :/ + packages = distr.read_text("top_level.txt").split() + + for package in packages: + scapy_ext = importlib.import_module(package) + + # We initialize the plugin by calling it's 'scapy_ext' function + try: + scapy_ext_func = scapy_ext.scapy_ext + except AttributeError: + log_loading.warning( + "'%s' does not look like a Scapy plugin !" % extension + ) + return + try: + scapy_ext_func(ext) + except Exception as ex: + log_loading.warning( + "'%s' failed during initialization with %s" % ( + extension, + ex + ) + ) + return + + # Register all the specs provided by this extension + for spec in ext.specs.values(): + self._register_spec(spec) + + # Add to the extension list + self.exts.append(ext) + self._loaded.append(extension) + + # If there are bash autocompletions, add them + if ext.bash_completions: + from scapy.main import _add_bash_autocompletion + + for name, script in ext.bash_completions.items(): + _add_bash_autocompletion(name, script) + + def loadall(self) -> None: + """ + Load all extensions registered in conf. + """ + for extension in conf.load_extensions: + self.load(extension) + + def __repr__(self): + from scapy.utils import pretty_list + return pretty_list( + [ + (x.name, x.version, [y.fullname for y in x.specs.values()]) + for x in self.exts + ], + [("Name", "Version", "Specs")], + sortBy=0, + ) + + def _version_checker(module, minver): + # type: (ModuleType, Tuple[int, ...]) -> bool """Checks that module has a higher version that minver. params: @@ -367,30 +734,42 @@ def _version_checker(module, minver): """ # We could use LooseVersion, but distutils imports imp which is deprecated version_regexp = r'[a-z]?((?:\d|\.)+\d+)(?:\.dev[0-9]+)?' - version_tags = re.match(version_regexp, module.__version__) - if not version_tags: + version_tags_r = re.match( + version_regexp, + getattr(module, "__version__", "") + ) + if not version_tags_r: return False - version_tags = version_tags.group(1).split(".") - version_tags = tuple(int(x) for x in version_tags) - return version_tags >= minver + version_tags_i = version_tags_r.group(1).split(".") + version_tags = tuple(int(x) for x in version_tags_i) + return bool(version_tags >= minver) def isCryptographyValid(): + # type: () -> bool """ - Check if the cryptography library is present, and if it is recent enough - for most usages in scapy (v1.7 or later). + Check if the cryptography module >= 2.0.0 is present. This is the minimum + version for most usages in Scapy. """ + # Check import try: import cryptography except ImportError: return False - return _version_checker(cryptography, (1, 7)) + + # Check minimum version + return _version_checker(cryptography, (2, 0, 0)) def isCryptographyAdvanced(): + # type: () -> bool """ - Check if the cryptography library is present, and if it supports X25519, - ChaCha20Poly1305 and such (v2.0 or later). + Check if the cryptography module is present, and if it supports X25519, + ChaCha20Poly1305 and such. + + Notes: + - cryptography >= 2.0 is required + - OpenSSL >= 1.1.0 is required """ try: from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey # noqa: E501 @@ -401,7 +780,25 @@ def isCryptographyAdvanced(): return True +def isCryptographyBackendCompatible() -> bool: + """ + Check if the cryptography backend is compatible + """ + # Check for LibreSSL + try: + from cryptography.hazmat.backends import default_backend + if "LibreSSL" in default_backend().openssl_version_text(): + # BUG: LibreSSL - https://marc.info/?l=libressl&m=173846028619304&w=2 + # It takes 5 whole minutes to import RFC3526's modp parameters. This is + # not okay. + return False + return True + except Exception: + return True + + def isPyPy(): + # type: () -> bool """Returns either scapy is running under PyPy or not""" try: import __pypy__ # noqa: F401 @@ -410,23 +807,28 @@ def isPyPy(): return False -def _prompt_changer(attr, val): +def _prompt_changer(attr, val, old): + # type: (str, Any, Any) -> Any """Change the current prompt theme""" + Interceptor.set_from_hook(conf, attr, val) try: sys.ps1 = conf.color_theme.prompt(conf.prompt) except Exception: pass try: - apply_ipython_style(get_ipython()) + apply_ipython_style( + get_ipython() # type: ignore + ) except NameError: pass + return getattr(conf, attr, old) def _set_conf_sockets(): + # type: () -> None """Populate the conf.L2Socket and conf.L3Socket according to the various use_* parameters """ - from scapy.main import _load if conf.use_bpf and not BSD: Interceptor.set_from_hook(conf, "use_bpf", False) raise ScapyInvalidPlatformException("BSD-like (OSX, *BSD...) only !") @@ -438,56 +840,57 @@ def _set_conf_sockets(): # we are already in an Interceptor hook, use Interceptor.set_from_hook if conf.use_pcap: try: - from scapy.arch.pcapdnet import L2pcapListenSocket, L2pcapSocket, \ + from scapy.arch.libpcap import L2pcapListenSocket, L2pcapSocket, \ L3pcapSocket except (OSError, ImportError): - warning("No libpcap provider available ! pcap won't be used") + log_loading.warning("No libpcap provider available ! pcap won't be used") Interceptor.set_from_hook(conf, "use_pcap", False) else: conf.L3socket = L3pcapSocket - conf.L3socket6 = functools.partial(L3pcapSocket, filter="ip6") + conf.L3socket6 = functools.partial( + L3pcapSocket, filter="ip6") conf.L2socket = L2pcapSocket conf.L2listen = L2pcapListenSocket - # Update globals - _load("scapy.arch.pcapdnet") - return - if conf.use_bpf: + elif conf.use_bpf: from scapy.arch.bpf.supersocket import L2bpfListenSocket, \ L2bpfSocket, L3bpfSocket conf.L3socket = L3bpfSocket - conf.L3socket6 = functools.partial(L3bpfSocket, filter="ip6") + conf.L3socket6 = functools.partial( + L3bpfSocket, filter="ip6") conf.L2socket = L2bpfSocket conf.L2listen = L2bpfListenSocket - # Update globals - _load("scapy.arch.bpf") - return - if LINUX: + elif LINUX: from scapy.arch.linux import L3PacketSocket, L2Socket, L2ListenSocket conf.L3socket = L3PacketSocket - conf.L3socket6 = functools.partial(L3PacketSocket, filter="ip6") + conf.L3socket6 = cast( + "Type[SuperSocket]", + functools.partial( + L3PacketSocket, + filter="ip6" + ) + ) conf.L2socket = L2Socket conf.L2listen = L2ListenSocket - # Update globals - _load("scapy.arch.linux") - return - if WINDOWS: + elif WINDOWS: from scapy.arch.windows import _NotAvailableSocket from scapy.arch.windows.native import L3WinSocket, L3WinSocket6 conf.L3socket = L3WinSocket conf.L3socket6 = L3WinSocket6 conf.L2socket = _NotAvailableSocket conf.L2listen = _NotAvailableSocket - # No need to update globals on Windows - return - from scapy.supersocket import L3RawSocket - from scapy.layers.inet6 import L3RawSocket6 - conf.L3socket = L3RawSocket - conf.L3socket6 = L3RawSocket6 + else: + from scapy.supersocket import L3RawSocket, L3RawSocket6 + conf.L3socket = L3RawSocket + conf.L3socket6 = L3RawSocket6 + # Reload the interfaces + conf.ifaces.reload() -def _socket_changer(attr, val): +def _socket_changer(attr, val, old): + # type: (str, bool, bool) -> Any if not isinstance(val, bool): raise TypeError("This argument should be a boolean") + Interceptor.set_from_hook(conf, attr, val) dependencies = { # Things that will be turned off "use_pcap": ["use_bpf"], "use_bpf": ["use_pcap"], @@ -504,147 +907,287 @@ def _socket_changer(attr, val): Interceptor.set_from_hook(conf, key, value) if isinstance(e, ScapyInvalidPlatformException): raise + return getattr(conf, attr) -def _loglevel_changer(attr, val): +def _loglevel_changer(attr, val, old): + # type: (str, int, int) -> int """Handle a change of conf.logLevel""" log_scapy.setLevel(val) + return val + + +def _iface_changer(attr, val, old): + # type: (str, Any, Any) -> 'scapy.interfaces.NetworkInterface' + """Resolves the interface in conf.iface""" + if isinstance(val, str): + from scapy.interfaces import resolve_iface + iface = resolve_iface(val) + if old and iface.dummy: + warning( + "This interface is not specified in any provider ! " + "See conf.ifaces output" + ) + return iface + return val + + +def _reset_tls_nss_keys(attr, val, old): + # type: (str, Any, Any) -> Any + """Reset conf.tls_nss_keys when conf.tls_nss_filename changes""" + conf.tls_nss_keys = None + return val class Conf(ConfClass): """ This object contains the configuration of Scapy. - - Attributes: - session: filename where the session will be saved - interactive_shell : can be "ipython", "python" or "auto". Default: Auto - stealth: if 1, prevents any unwanted packet to go out (ARP, DNS, ...) - checkIPID: if 0, doesn't check that IPID matches between IP sent and - ICMP IP citation received - if 1, checks that they either are equal or byte swapped - equals (bug in some IP stacks) - if 2, strictly checks that they are equals - checkIPsrc: if 1, checks IP src in IP and ICMP IP citation match - (bug in some NAT stacks) - checkIPinIP: if True, checks that IP-in-IP layers match. If False, do - not check IP layers that encapsulates another IP layer - check_TCPerror_seqack: if 1, also check that TCP seq and ack match the - ones in ICMP citation - iff: selects the default output interface for srp() and sendp(). - verb: level of verbosity, from 0 (almost mute) to 3 (verbose) - promisc: default mode for listening socket (to get answers if you - spoof on a lan) - sniff_promisc: default mode for sniff() - filter: bpf filter added to every sniffing socket to exclude traffic - from analysis - histfile: history file - padding: includes padding in disassembled packets - except_filter : BPF filter for packets to ignore - debug_match: when 1, store received packet that are not matched into - `debug.recv` - route: holds the Scapy routing table and provides methods to - manipulate it - warning_threshold : how much time between warnings from the same place - ASN1_default_codec: Codec used by default for ASN1 objects - mib: holds MIB direct access dictionary - resolve: holds list of fields for which resolution should be done - noenum: holds list of enum fields for which conversion to string - should NOT be done - AS_resolver: choose the AS resolver class to use - extensions_paths: path or list of paths where extensions are to be - looked for - contribs: a dict which can be used by contrib layers to store local - configuration - debug_tls: When 1, print some TLS session secrets - when they are computed. - recv_poll_rate: how often to check for new packets. Defaults to 0.05s. - raise_no_dst_mac: When True, raise exception if no dst MAC found - otherwise broadcast. Default is False. """ - version = ReadOnlyAttribute("version", VERSION) - session = "" + version: str = ReadOnlyAttribute("version", VERSION) + session: str = "" #: filename where the session will be saved interactive = False - interactive_shell = "" + #: can be "ipython", "bpython", "ptpython", "ptipython", "python" or "auto". + #: Default: Auto + interactive_shell = "auto" + #: Configuration for "ipython" to use jedi (disabled by default) + ipython_use_jedi = False + #: if 1, prevents any unwanted packet to go out (ARP, DNS, ...) stealth = "not implemented" - iface = None - iface6 = None - layers = LayersList() - commands = CommandsList() + #: selects the default output interface for srp() and sendp(). + iface = Interceptor("iface", None, _iface_changer) # type: 'scapy.interfaces.NetworkInterface' # noqa: E501 + layers: LayersList = LayersList() + commands = CommandsList() # type: CommandsList + #: Codec used by default for ASN1 objects + ASN1_default_codec = None # type: 'scapy.asn1.asn1.ASN1Codec' + #: Default size for ASN1 objects + ASN1_default_long_size = 0 + #: choose the AS resolver class to use + AS_resolver = None # type: scapy.as_resolvers.AS_resolver dot15d4_protocol = None # Used in dot15d4.py - logLevel = Interceptor("logLevel", log_scapy.level, _loglevel_changer) + logLevel: int = Interceptor("logLevel", log_scapy.level, _loglevel_changer) + #: if 0, doesn't check that IPID matches between IP sent and + #: ICMP IP citation received + #: if 1, checks that they either are equal or byte swapped + #: equals (bug in some IP stacks) + #: if 2, strictly checks that they are equals checkIPID = False + #: if 1, checks IP src in IP and ICMP IP citation match + #: (bug in some NAT stacks) checkIPsrc = True checkIPaddr = True + #: if True, checks that IP-in-IP layers match. If False, do + #: not check IP layers that encapsulates another IP layer checkIPinIP = True + #: if 1, also check that TCP seq and ack match the + #: ones in ICMP citation check_TCPerror_seqack = False - verb = 2 - prompt = Interceptor("prompt", ">>> ", _prompt_changer) - promisc = True - sniff_promisc = 1 - raw_layer = None - raw_summary = False - default_l2 = None - l2types = Num2Layer() - l3types = Num2Layer() - L3socket = None - L3socket6 = None - L2socket = None - L2listen = None - BTsocket = None - USBsocket = None + verb = 2 #: level of verbosity, from 0 (almost mute) to 3 (verbose) + prompt: str = Interceptor("prompt", ">>> ", _prompt_changer) + #: default mode for the promiscuous mode of a socket (to get answers if you + #: spoof on a lan) + sniff_promisc = True # type: bool + raw_layer = None # type: Type[Packet] + raw_summary = False # type: Union[bool, Callable[[bytes], Any]] + padding_layer = None # type: Type[Packet] + default_l2 = None # type: Type[Packet] + l2types: Num2Layer = Num2Layer() + l3types: Num2Layer = Num2Layer() + L3socket = None # type: Type[scapy.supersocket.SuperSocket] + L3socket6 = None # type: Type[scapy.supersocket.SuperSocket] + L2socket = None # type: Type[scapy.supersocket.SuperSocket] + L2listen = None # type: Type[scapy.supersocket.SuperSocket] + BTsocket = None # type: Type[scapy.supersocket.SuperSocket] min_pkt_size = 60 + #: holds MIB direct access dictionary + mib = None # type: 'scapy.asn1.mib.MIBDict' bufsize = 2**16 - histfile = os.getenv('SCAPY_HISTFILE', - os.path.join(os.path.expanduser("~"), - ".scapy_history")) + #: history file + histfile: str = os.getenv( + 'SCAPY_HISTFILE', + os.path.join( + os.path.expanduser("~"), + ".config", "scapy", "history" + ) + ) + #: includes padding in disassembled packets padding = 1 + #: BPF filter for packets to ignore except_filter = "" + #: bpf filter added to every sniffing socket to exclude traffic + #: from analysis + filter = "" + #: when 1, store received packet that are not matched into `debug.recv` debug_match = False + #: When 1, print some TLS session secrets when they are computed, and + #: warn about the session recognition. debug_tls = False wepkey = "" - cache_iflist = {} - route = None # Filed by route.py - route6 = None # Filed by route6.py + #: holds the Scapy interface list and manager + ifaces = None # type: 'scapy.interfaces.NetworkInterfaceDict' + #: holds the cache of interfaces loaded from Libpcap + cache_pcapiflist = {} # type: Dict[str, Tuple[str, List[str], Any, str, int]] + # `neighbor` will be filed by scapy.layers.l2 + neighbor = None # type: 'scapy.layers.l2.Neighbor' + #: holds the name servers IP/hosts used for custom DNS resolution + nameservers = None # type: str + #: automatically load IPv4 routes on startup. Disable this if your + #: routing table is too big. + route_autoload = True + #: automatically load IPv6 routes on startup. Disable this if your + #: routing table is too big. + route6_autoload = True + #: holds the Scapy IPv4 routing table and provides methods to + #: manipulate it + route = None # type: 'scapy.route.Route' + # `route` will be filed by route.py + #: holds the Scapy IPv6 routing table and provides methods to + #: manipulate it + route6 = None # type: 'scapy.route6.Route6' + manufdb = None # type: 'scapy.data.ManufDA' + ethertypes = None # type: 'scapy.data.EtherDA' + protocols = None # type: 'scapy.dadict.DADict[int, str]' + services_udp = None # type: 'scapy.dadict.DADict[int, str]' + services_tcp = None # type: 'scapy.dadict.DADict[int, str]' + services_sctp = None # type: 'scapy.dadict.DADict[int, str]' + # 'route6' will be filed by route6.py + teredoPrefix = "" # type: str + teredoServerPort = None # type: int auto_fragment = True + #: raise exception when a packet dissector raises an exception debug_dissector = False - color_theme = Interceptor("color_theme", NoTheme(), _prompt_changer) + color_theme: ColorTheme = Interceptor("color_theme", NoTheme(), _prompt_changer) + #: how much time between warnings from the same place warning_threshold = 5 - prog = ProgPath() - resolve = Resolve() - noenum = Resolve() - emph = Emphasize() - use_pypy = ReadOnlyAttribute("use_pypy", isPyPy()) - use_pcap = Interceptor( + prog: ProgPath = ProgPath() + #: holds list of fields for which resolution should be done + resolve: Resolve = Resolve() + #: holds list of enum fields for which conversion to string + #: should NOT be done + noenum: Resolve = Resolve() + emph: Emphasize = Emphasize() + #: read only attribute to show if PyPy is in use + use_pypy: bool = ReadOnlyAttribute("use_pypy", isPyPy()) + #: use libpcap integration or not. Changing this value will update + #: the conf.L[2/3] sockets + use_pcap: bool = Interceptor( "use_pcap", - os.getenv("SCAPY_USE_PCAPDNET", "").lower().startswith("y"), + os.getenv("SCAPY_USE_LIBPCAP", "").lower().startswith("y"), _socket_changer ) - use_bpf = Interceptor("use_bpf", False, _socket_changer) + use_bpf: bool = Interceptor("use_bpf", False, _socket_changer) use_npcap = False - ipv6_enabled = socket.has_ipv6 - extensions_paths = "." - stats_classic_protocols = [] - stats_dot11_protocols = [] - temp_files = [] - netcache = NetCache() + ipv6_enabled: bool = socket.has_ipv6 + stats_classic_protocols = [] # type: List[Type[Packet]] + stats_dot11_protocols = [] # type: List[Type[Packet]] + temp_files = [] # type: List[str] + #: netcache holds time-based caches for net operations + netcache: NetCache = NetCache() geoip_city = None - # can, tls, http are not loaded by default - load_layers = ['bluetooth', 'bluetooth4LE', 'dhcp', 'dhcp6', 'dns', - 'dot11', 'dot15d4', 'eap', 'gprs', 'hsrp', 'inet', - 'inet6', 'ipsec', 'ir', 'isakmp', 'l2', 'l2tp', - 'llmnr', 'lltd', 'mgcp', 'mobileip', 'netbios', - 'netflow', 'ntp', 'ppi', 'ppp', 'pptp', 'radius', 'rip', - 'rtp', 'sctp', 'sixlowpan', 'skinny', 'smb', 'snmp', - 'tftp', 'vrrp', 'vxlan', 'x509', 'zigbee'] - contribs = dict() + #: Scapy extensions that are loaded automatically on load + load_extensions: List[str] = [] + # can, tls, http and a few others are not loaded by default + load_layers: List[str] = [ + 'bluetooth', + 'bluetooth4LE', + 'dcerpc', + 'dhcp', + 'dhcp6', + 'dns', + 'dot11', + 'dot15d4', + 'eap', + 'gprs', + 'gssapi', + 'hsrp', + 'inet', + 'inet6', + 'ipsec', + 'ir', + 'isakmp', + 'kerberos', + 'l2', + 'l2tp', + 'ldap', + 'llmnr', + 'lltd', + 'mgcp', + 'msrpce.rpcclient', + 'msrpce.rpcserver', + 'mobileip', + 'netbios', + 'netflow', + 'ntlm', + 'ntp', + 'ppi', + 'ppp', + 'pptp', + 'radius', + 'rip', + 'rtp', + 'sctp', + 'sixlowpan', + 'skinny', + 'smb', + 'smb2', + 'smbclient', + 'smbserver', + 'snmp', + 'spnego', + 'tftp', + 'vrrp', + 'vxlan', + 'x509', + 'zigbee' + ] + #: a dict which can be used by contrib layers to store local + #: configuration + contribs = dict() # type: Dict[str, Any] + exts: ExtsManager = ExtsManager() crypto_valid = isCryptographyValid() crypto_valid_advanced = isCryptographyAdvanced() - fancy_prompt = True + #: controls whether or not to display the fancy banner + fancy_banner = True + #: controls whether tables (conf.iface, conf.route...) should be cropped + #: to fit the terminal auto_crop_tables = True + #: how often to check for new packets. + #: Defaults to 0.05s. recv_poll_rate = 0.05 + #: When True, raise exception if no dst MAC found otherwise broadcast. + #: Default is False. raise_no_dst_mac = False - - def __getattr__(self, attr): + loopback_name: str = "lo" if LINUX else "lo0" + nmap_base = "" # type: str + nmap_kdb = None # type: Optional[NmapKnowledgeBase] + #: a safety mechanism: the maximum amount of items included in a PacketListField + #: or a FieldListField + max_list_count = 100 + #: When the TLS module is loaded (not by default), the following turns on sessions + tls_session_enable = False + #: Filename containing NSS Keys Log + tls_nss_filename = Interceptor( + "tls_nss_filename", + None, + _reset_tls_nss_keys + ) + #: Dictionary containing parsed NSS Keys + tls_nss_keys: Dict[str, bytes] = None + #: Whether to use NDR64 by default instead of NDR 32 + ndr64: bool = True + #: When TCPSession is used, parse DCE/RPC sessions automatically. + #: This should be used for passive sniffing. + dcerpc_session_enable = False + #: If a capture is missing the first DCE/RPC binding message, we might incorrectly + #: assume that header signing isn't used. This forces it on. + dcerpc_force_header_signing = False + #: Windows SSPs for sniffing. This is used with + #: dcerpc_session_enable + winssps_passive = [] + #: Disables auto-stripping of StrFixedLenField for debugging purposes + debug_strfixedlenfield = False + + def __getattribute__(self, attr): + # type: (str) -> Any # Those are loaded on runtime to avoid import loops if attr == "manufdb": from scapy.data import MANUFDB @@ -661,26 +1204,61 @@ def __getattr__(self, attr): if attr == "services_tcp": from scapy.data import TCP_SERVICES return TCP_SERVICES + if attr == "services_sctp": + from scapy.data import SCTP_SERVICES + return SCTP_SERVICES + if attr == "iface6": + warnings.warn( + "conf.iface6 is deprecated in favor of conf.iface", + DeprecationWarning + ) + attr = "iface" return object.__getattribute__(self, attr) if not Conf.ipv6_enabled: log_scapy.warning("IPv6 support disabled in Python. Cannot load Scapy IPv6 layers.") # noqa: E501 - for m in ["inet6", "dhcp6"]: + for m in ["inet6", "dhcp6", "sixlowpan"]: if m in Conf.load_layers: Conf.load_layers.remove(m) -conf = Conf() +conf = Conf() # type: Conf + + +if not isCryptographyBackendCompatible(): + conf.crypto_valid = False + conf.crypto_valid_advanced = False + log_scapy.error( + "Scapy does not support LibreSSL as a backend to cryptography ! " + "See https://cryptography.io/en/latest/installation/#static-wheels " + "for instructions on how to recompile cryptography with another " + "backend." + ) def crypto_validator(func): + # type: (DecoratorCallable) -> DecoratorCallable """ This a decorator to be used for any method relying on the cryptography library. # noqa: E501 Its behaviour depends on the 'crypto_valid' attribute of the global 'conf'. """ def func_in(*args, **kwargs): + # type: (*Any, **Any) -> Any if not conf.crypto_valid: raise ImportError("Cannot execute crypto-related method! " - "Please install python-cryptography v1.7 or later.") # noqa: E501 + "Please install python-cryptography v2.0 or later.") # noqa: E501 return func(*args, **kwargs) return func_in + + +def scapy_delete_temp_files(): + # type: () -> None + for f in conf.temp_files: + try: + os.unlink(f) + except Exception: + pass + del conf.temp_files[:] + + +atexit.register(scapy_delete_temp_files) diff --git a/scapy/consts.py b/scapy/consts.py index 952d34ed057..ed8ce90c723 100644 --- a/scapy/consts.py +++ b/scapy/consts.py @@ -1,12 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license -import os -from sys import platform, maxsize +""" +This file contains constants +""" + +from sys import byteorder, platform, maxsize import platform as platform_lib +__all__ = [ + "LINUX", + "OPENBSD", + "FREEBSD", + "NETBSD", + "DARWIN", + "SOLARIS", + "WINDOWS", + "WINDOWS_XP", + "BSD", + "IS_64BITS", + "BIG_ENDIAN", +] + LINUX = platform.startswith("linux") OPENBSD = platform.startswith("openbsd") FREEBSD = "freebsd" in platform @@ -18,18 +35,5 @@ BSD = DARWIN or FREEBSD or OPENBSD or NETBSD # See https://docs.python.org/3/library/platform.html#cross-platform IS_64BITS = maxsize > 2**32 - -if WINDOWS: - try: - if float(platform_lib.release()) >= 8.1: - LOOPBACK_NAME = "Microsoft KM-TEST Loopback Adapter" - else: - LOOPBACK_NAME = "Microsoft Loopback Adapter" - except ValueError: - LOOPBACK_NAME = "Microsoft Loopback Adapter" - # Will be different on Windows - LOOPBACK_INTERFACE = None -else: - uname = os.uname() - LOOPBACK_NAME = "lo" if LINUX else "lo0" - LOOPBACK_INTERFACE = LOOPBACK_NAME +BIG_ENDIAN = byteorder == 'big' +# LOOPBACK_NAME moved to conf.loopback_name diff --git a/scapy/contrib/__init__.py b/scapy/contrib/__init__.py index 496a2f92cc1..8c54a8489ae 100644 --- a/scapy/contrib/__init__.py +++ b/scapy/contrib/__init__.py @@ -1,8 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Package of contrib modules that have to be loaded explicitly. """ + +# Make sure config is loaded +import scapy.config # noqa: F401 diff --git a/scapy/contrib/altbeacon.py b/scapy/contrib/altbeacon.py index 75ce3aeaf5d..b263872b0bb 100644 --- a/scapy/contrib/altbeacon.py +++ b/scapy/contrib/altbeacon.py @@ -1,10 +1,7 @@ -# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*- -# altbeacon.py - protocol handlers for AltBeacon -# +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Michael Farrell -# This program is published under a GPLv2 (or later) license # # scapy.contrib.description = AltBeacon BLE proximity beacon # scapy.contrib.status = loads @@ -14,8 +11,13 @@ The AltBeacon specification can be found at: https://github.com/AltBeacon/spec """ -from scapy.fields import ByteField, ShortField, SignedByteField, \ - StrFixedLenField +from scapy.fields import ( + ByteField, + MayEnd, + ShortField, + SignedByteField, + StrFixedLenField, +) from scapy.layers.bluetooth import EIR_Hdr, EIR_Manufacturer_Specific_Data, \ UUIDField, LowEnergyBeaconHelper from scapy.packet import Packet @@ -57,7 +59,7 @@ class AltBeacon(Packet, LowEnergyBeaconHelper): ShortField("id2", None), ShortField("id3", None), - SignedByteField("tx_power", None), + MayEnd(SignedByteField("tx_power", None)), ByteField("mfg_reserved", None), ] diff --git a/scapy/contrib/aoe.py b/scapy/contrib/aoe.py index d4fccac9ef8..bf608ada0f8 100644 --- a/scapy/contrib/aoe.py +++ b/scapy/contrib/aoe.py @@ -1,6 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2018 antoine.torre -## -# This program is published under a GPLv2 license # scapy.contrib.description = ATA Over Internet diff --git a/scapy/contrib/automotive/__init__.py b/scapy/contrib/automotive/__init__.py index d992d4cd522..ccec664f283 100644 --- a/scapy/contrib/automotive/__init__.py +++ b/scapy/contrib/automotive/__init__.py @@ -1,10 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip """ Package of contrib automotive modules that have to be loaded explicitly. """ + +import logging + +log_automotive = logging.getLogger("scapy.contrib.automotive") + +log_automotive.setLevel(logging.INFO) diff --git a/scapy/contrib/automotive/autosar/__init__.py b/scapy/contrib/automotive/autosar/__init__.py new file mode 100644 index 00000000000..b9fa5216c34 --- /dev/null +++ b/scapy/contrib/automotive/autosar/__init__.py @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Damian Zaręba + +# scapy.contrib.status = skip + +""" +Package of contrib automotive AUTOSAR modules +that have to be loaded explicitly. +""" diff --git a/scapy/contrib/automotive/autosar/pdu.py b/scapy/contrib/automotive/autosar/pdu.py new file mode 100644 index 00000000000..03ec3d3bcc6 --- /dev/null +++ b/scapy/contrib/automotive/autosar/pdu.py @@ -0,0 +1,46 @@ +#! /usr/bin/env python + +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Damian Zaręba + +# scapy.contrib.description = AUTOSAR PDU packets handling package. +# scapy.contrib.status = loads +from typing import Tuple, Optional +from scapy.layers.inet import UDP +from scapy.fields import XIntField, PacketListField, LenField +from scapy.packet import Packet, bind_bottom_up + + +class PDU(Packet): + """ + Single PDU Packet inside PDUTransport list. + Contains ID and payload length, and later - raw load. + It's free to interpret using bind_layers/bind_bottom_up method + + Based off this document: + + https://www.autosar.org/fileadmin/standards/classic/22-11/AUTOSAR_SWS_IPDUMultiplexer.pdf # noqa: E501 + """ + name = 'PDU' + fields_desc = [ + XIntField('pdu_id', 0), + LenField('pdu_payload_len', None, fmt="I")] + + def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + return s[:self.pdu_payload_len], s[self.pdu_payload_len:] + + +class PDUTransport(Packet): + """ + Packet representing PDUTransport containing multiple PDUs + """ + name = 'PDUTransport' + fields_desc = [ + PacketListField("pdus", [PDU()], PDU) + ] + + +bind_bottom_up(UDP, PDUTransport, dport=60000) diff --git a/scapy/contrib/automotive/autosar/secoc.py b/scapy/contrib/automotive/autosar/secoc.py new file mode 100644 index 00000000000..d83949e268e --- /dev/null +++ b/scapy/contrib/automotive/autosar/secoc.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = AUTOSAR Secure On-Board Communication +# scapy.contrib.status = library + +""" +SecOC +""" +from scapy.config import conf +from scapy.error import log_loading + +if conf.crypto_valid: + from cryptography.hazmat.primitives import cmac + from cryptography.hazmat.primitives.ciphers import algorithms +else: + log_loading.info("Can't import python-cryptography v2.0+. " + "Disabled SecOC calculate_cmac.") + +from scapy.config import conf +from scapy.fields import PacketLenField +from scapy.packet import Packet, Raw + +# Typing imports +from typing import ( + Callable, + Dict, + Optional, + Set, + Type, +) + + +class SecOCMixin: + + pdu_payload_cls_by_identifier: Dict[int, Type[Packet]] = dict() + secoc_protected_pdus_by_identifier: Set[int] = set() + + def secoc_authenticate(self) -> None: + raise NotImplementedError + + def secoc_verify(self) -> bool: + raise NotImplementedError + + def get_secoc_payload(self) -> bytes: + """Override this method for customization + """ + raise NotImplementedError + + def get_secoc_key(self) -> bytes: + """Override this method for customization + """ + return b"\x00" * 16 + + def get_secoc_freshness_value(self) -> bytes: + """Override this method for customization + """ + return b"\x00" * 4 + + def get_message_authentication_code(self): + payload = self.get_secoc_payload() + key = self.get_secoc_key() + freshness_value = self.get_secoc_freshness_value() + return self.calculate_cmac(key, payload, freshness_value) + + @staticmethod + def calculate_cmac(key: bytes, payload: bytes, freshness_value: bytes) -> bytes: + c = cmac.CMAC(algorithms.AES128(key)) + c.update(payload + freshness_value) + return c.finalize() + + @classmethod + def register_secoc_protected_pdu(cls, + pdu_id: int, + pdu_payload_cls: Type[Packet] = Raw + ) -> None: + cls.secoc_protected_pdus_by_identifier.add(pdu_id) + cls.pdu_payload_cls_by_identifier[pdu_id] = pdu_payload_cls + + @classmethod + def unregister_secoc_protected_pdu(cls, pdu_id: int) -> None: + cls.secoc_protected_pdus_by_identifier.remove(pdu_id) + del cls.pdu_payload_cls_by_identifier[pdu_id] + + +class PduPayloadField(PacketLenField): + __slots__ = ["guess_pkt_cls"] + + def __init__(self, + name, # type: str + default, # type: Packet + guess_pkt_cls, # type: Callable[[Packet, bytes], Packet] # noqa: E501 + length_from=None # type: Optional[Callable[[Packet], int]] # noqa: E501 + ): + # type: (...) -> None + super(PacketLenField, self).__init__(name, default, Raw) + self.length_from = length_from or (lambda x: 0) + self.guess_pkt_cls = guess_pkt_cls + + def m2i(self, pkt, m): # type: ignore + # type: (Optional[Packet], bytes) -> Packet + return self.guess_pkt_cls(pkt, m) diff --git a/scapy/contrib/automotive/autosar/secoc_canfd.py b/scapy/contrib/automotive/autosar/secoc_canfd.py new file mode 100644 index 00000000000..1514b17f35b --- /dev/null +++ b/scapy/contrib/automotive/autosar/secoc_canfd.py @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = AUTOSAR Secure On-Board Communication PDUs +# scapy.contrib.status = loads + +""" +SecOC PDU +""" +import struct + +from scapy.config import conf +from scapy.contrib.automotive.autosar.secoc import SecOCMixin, PduPayloadField +from scapy.base_classes import Packet_metaclass +from scapy.fields import (XByteField, FieldLenField, XStrFixedLenField, + FlagsField, XBitField, ShortField) +from scapy.layers.can import CANFD +from scapy.packet import Raw, Packet + +# Typing imports +from typing import ( + Any, + Optional, + Tuple, +) + + +class SecOC_CANFD(CANFD, SecOCMixin): + name = 'SecOC_CANFD' + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + FieldLenField('length', None, length_of='pdu_payload', + fmt='B', adjust=lambda pkt, x: x + 4), + FlagsField('fd_flags', 4, 8, [ + 'bit_rate_switch', 'error_state_indicator', 'fd_frame']), + ShortField('reserved', 0), + PduPayloadField('pdu_payload', + Raw(), + guess_pkt_cls=lambda pkt, data: SecOC_CANFD.get_pdu_payload_cls(pkt, data), # noqa: E501 + length_from=lambda pkt: pkt.length - 4), + XByteField("tfv", 0), # truncated freshness value + XStrFixedLenField("tmac", None, length=3)] # truncated message authentication code # noqa: E501 + + def secoc_authenticate(self) -> None: + self.tfv = struct.unpack(">B", self.get_secoc_freshness_value()[-1:])[0] + self.tmac = self.get_message_authentication_code()[0:3] + + def secoc_verify(self) -> bool: + return self.get_message_authentication_code()[0:3] == self.tmac + + def get_secoc_payload(self) -> bytes: + """Override this method for customization + """ + return bytes(self.pdu_payload) + + @classmethod + def dispatch_hook(cls, s=None, *_args, **_kwds): + # type: (Optional[bytes], Any, Any) -> Packet_metaclass + """dispatch_hook determines if PDU is protected by SecOC. + If PDU is protected, SecOC_PDU will be returned, otherwise AutoSAR PDU + will be returned. + """ + if s is None: + return SecOC_CANFD + if len(s) < 4: + return Raw + identifier = struct.unpack('>I', s[0:4])[0] & 0x1FFFFFFF + if identifier in cls.secoc_protected_pdus_by_identifier: + return SecOC_CANFD + else: + return CANFD + + @classmethod + def get_pdu_payload_cls(cls, + pkt: Packet, + data: bytes + ) -> Packet: + try: + klass = cls.pdu_payload_cls_by_identifier[pkt.identifier] + except KeyError: + klass = conf.raw_layer + return klass(data, _parent=pkt) + + def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + return b"", s diff --git a/scapy/contrib/automotive/autosar/secoc_pdu.py b/scapy/contrib/automotive/autosar/secoc_pdu.py new file mode 100644 index 00000000000..169f0bda08c --- /dev/null +++ b/scapy/contrib/automotive/autosar/secoc_pdu.py @@ -0,0 +1,109 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = AUTOSAR Secure On-Board Communication PDUs +# scapy.contrib.status = loads + +""" +SecOC PDU +""" +import struct + +from scapy.config import conf +from scapy.contrib.automotive.autosar.secoc import SecOCMixin, PduPayloadField +from scapy.base_classes import Packet_metaclass +from scapy.contrib.automotive.autosar.pdu import PDU +from scapy.fields import (XByteField, XIntField, PacketListField, + FieldLenField, XStrFixedLenField) +from scapy.packet import Packet, Raw + +# Typing imports +from typing import ( + Any, + Optional, + Tuple, + Type, +) + + +class SecOC_PDU(Packet, SecOCMixin): + name = 'SecOC_PDU' + fields_desc = [ + XIntField('pdu_id', 0), + FieldLenField('pdu_payload_len', None, + fmt="I", + length_of="pdu_payload", + adjust=lambda pkt, x: x + 4), + PduPayloadField('pdu_payload', + Raw(), + guess_pkt_cls=lambda pkt, data: SecOC_PDU.get_pdu_payload_cls(pkt, data), # noqa: E501 + length_from=lambda pkt: pkt.pdu_payload_len - 4), + XByteField("tfv", 0), # truncated freshness value + XStrFixedLenField("tmac", None, length=3)] # truncated message authentication code # noqa: E501 + + def secoc_authenticate(self) -> None: + self.tfv = struct.unpack(">B", self.get_secoc_freshness_value()[-1:])[0] + self.tmac = self.get_message_authentication_code()[0:3] + + def secoc_verify(self) -> bool: + return self.get_message_authentication_code()[0:3] == self.tmac + + def get_secoc_payload(self) -> bytes: + """Override this method for customization + """ + return self.pdu_payload + + @classmethod + def dispatch_hook(cls, s=None, *_args, **_kwds): + # type: (Optional[bytes], Any, Any) -> Packet_metaclass + """dispatch_hook determines if PDU is protected by SecOC. + If PDU is protected, SecOC_PDU will be returned, otherwise AutoSAR PDU + will be returned. + """ + if s is None: + return SecOC_PDU + if len(s) < 4: + return Raw + identifier = struct.unpack('>I', s[0:4])[0] + if identifier in cls.secoc_protected_pdus_by_identifier: + return SecOC_PDU + else: + return PDU + + @classmethod + def get_pdu_payload_cls(cls, + pkt: Packet, + data: bytes + ) -> Packet: + try: + klass = cls.pdu_payload_cls_by_identifier[pkt.pdu_id] + except KeyError: + klass = conf.raw_layer + return klass(data, _parent=pkt) + + def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + return b"", s + + +class SecOC_PDUTransport(Packet): + """ + Packet representing SecOC_PDUTransport containing multiple PDUs + """ + + name = 'SecOC_PDUTransport' + fields_desc = [ + PacketListField("pdus", [SecOC_PDU()], pkt_cls=SecOC_PDU) + ] + + @staticmethod + def register_secoc_protected_pdu(pdu_id: int, + pdu_payload_cls: Type[Packet] = Raw + ) -> None: + SecOC_PDU.register_secoc_protected_pdu(pdu_id, pdu_payload_cls) + + @staticmethod + def unregister_secoc_protected_pdu(pdu_id: int) -> None: + SecOC_PDU.unregister_secoc_protected_pdu(pdu_id) diff --git a/scapy/contrib/automotive/bmw/__init__.py b/scapy/contrib/automotive/bmw/__init__.py index f06000ab15a..618bfe6c8d2 100644 --- a/scapy/contrib/automotive/bmw/__init__.py +++ b/scapy/contrib/automotive/bmw/__init__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/bmw/definitions.py b/scapy/contrib/automotive/bmw/definitions.py index 73452935408..fe75c27cf5c 100644 --- a/scapy/contrib/automotive/bmw/definitions.py +++ b/scapy/contrib/automotive/bmw/definitions.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.description = BMW specific definitions for UDS # scapy.contrib.status = loads @@ -9,11 +9,12 @@ from scapy.packet import Packet, bind_layers from scapy.fields import ByteField, ShortField, ByteEnumField, X3BytesField, \ - StrField, StrFixedLenField, LEIntField, LEThreeBytesField, PacketListField + StrField, StrFixedLenField, LEThreeBytesField, \ + PacketListField, IntField, IPField, ThreeBytesField, ShortEnumField, \ + XStrFixedLenField from scapy.contrib.automotive.uds import UDS, UDS_RDBI, UDS_DSC, UDS_IOCBI, \ UDS_RC, UDS_RD, UDS_RSDBI, UDS_RDBIPR - BMW_specific_enum = { 0: "requestIdentifiedBCDDTCAndStatus", 1: "requestSupportedBCDDTCAndStatus", @@ -246,15 +247,65 @@ class UDS2S_REQ(Packet): class SVK_DateField(LEThreeBytesField): def i2repr(self, pkt, x): x = self.addfield(pkt, b"", x) - return "%02X.%02X.20%02X" % (x[0], x[1], x[2]) + return "%02X.%02X.20%02X" % (x[2], x[1], x[0]) class SVK_Entry(Packet): + process_classes = { + 0x01: "HWEL", + 0x02: "HWAP", + 0x03: "HWFR", + 0x05: "CAFD", + 0x06: "BTLD", + 0x08: "SWFL", + 0x09: "SWFF", + 0x0A: "SWPF", + 0x0B: "ONPS", + 0x0F: "FAFP", + 0x1A: "TLRT", + 0x1B: "TPRG", + 0x07: "FLSL", + 0x0C: "IBAD", + 0x10: "FCFA", + 0x1C: "BLUP", + 0x1D: "FLUP", + 0xC0: "SWUP", + 0xC1: "SWIP", + 0xA0: "ENTD", + 0xA1: "NAVD", + 0xA2: "FCFN", + 0x04: "GWTB", + 0x0D: "SWFK", + } + """ + HWEL - Hardware (Elektronik) - Hardware (Electronics) + HWAP - Hardwareauspraegung - Hardware Configuration + HWFR - Hardwarefarbe - Hardware Color + CAFD - Codierdaten - Coding Data + BTLD - Bootloader - Bootloader + SWFL - Software ECU Speicherimage - Software ECU Storage Image + SWFF - Flash File Software - Flash File Software + SWPF - Pruefsoftware - Testing Software + ONPS - Onboard Programmiersystem - Onboard Programming System + FAFP - FA2FP - FA2FP + TLRT - Temporaere Loeschroutine - Temporary Deletion Routine + TPRG - Temporaere Programmierroutine - Temporary Programming Routine + FLSL - Flashloader Slave - Flashloader Slave + IBAD - Interaktive Betriebsanleitung Daten - Interactive Operating Manual Data + FCFA - Freischaltcode Fahrzeug-Auftrag - Vehicle Order Unlock Code + BLUP - Bootloader-Update Applikation - Bootloader Update Application + FLUP - Flashloader-Update Applikation - Flashloader Update Application + SWUP - Software-Update Package - Software Update Package + SWIP - Index Software-Update Package - Software Update Package Index + ENTD - Entertainment Daten - Entertainment Data + NAVD - Navigation Daten - Navigation Data + FCFN - Freischaltcode Funktion - Function Unlock Code + GWTB - Gateway-Tabelle - Gateway Table + SWFK - BEGU: Detaillierung auf SWE-Ebene - BEGU: Detailing at SWE Level + """ fields_desc = [ - ByteEnumField("processClass", 0, {1: "HWEL", 2: "HWAP", 4: "GWTB", - 5: "CAFD", 6: "BTLD", 7: "FLSL", - 8: "SWFL"}), - StrFixedLenField("svk_id", b"", length=4), + ByteEnumField("processClass", 0, process_classes), + XStrFixedLenField("svk_id", b"", length=4), ByteField("mainVersion", 0), ByteField("subVersion", 0), ByteField("patchVersion", 0)] @@ -270,18 +321,108 @@ class SVK(Packet): 3: "software entry incompatible to hardware entry", 4: "software entry incompatible with other software entry"} + @staticmethod + def get_length(p: Packet): + return len(p.original) - (8 * p.entries_count + 7) + fields_desc = [ ByteEnumField("prog_status1", 0, prog_status_enum), ByteEnumField("prog_status2", 0, prog_status_enum), ShortField("entries_count", 0), - SVK_DateField("prog_date", b'\x00\x00\x00'), - ByteField("pad1", 0), - LEIntField("prog_milage", 0), - StrFixedLenField("pad2", 0, length=5), - PacketListField("entries", [], cls=SVK_Entry, + SVK_DateField("prog_date", 0), + StrFixedLenField("pad", b'\x00', length_from=get_length), + PacketListField("entries", [], SVK_Entry, count_from=lambda x: x.entries_count)] +class DIAG_SESSION_RESP(Packet): + fields_desc = [ + ByteField('DIAG_SESSION_VALUE', 0), + StrField('DIAG_SESSION_TEXT', '') + ] + + +class IP_CONFIG_RESP(Packet): + fields_desc = [ + ByteField('ADDRESS_FORMAT_ID', 0), + IPField('IP', '192.168.0.10'), + IPField('SUBNETMASK', '255.255.255.0'), + IPField('DEFAULT_GATEWAY', '192.168.0.1') + ] + + +bind_layers(UDS_RDBIPR, IP_CONFIG_RESP, dataIdentifier=0x172a) +bind_layers(UDS_RDBIPR, DIAG_SESSION_RESP, dataIdentifier=0xf186) + + +class DEV_JOB(Packet): + identifiers = { + 0x51F1: "ControlReciprocalMonitor", + 0xCADD: "EnableDebugCan", + 0xDEAD: "LockJtag1", + 0xDEAE: "LockJtag2", + 0xDEAF: "UnlockJtag", + 0xF510: "ControlFuSiIO", + 0xFF00: "ReadTransportMessageStatus", + 0xFF10: "ControlEthernetActivation", + 0xFF51: "ControlPwfMaster", + 0xFF66: "ControlWebsite", + 0xFF77: "ControlIdleMessage", + 0xFFB0: "ReadManufacturerData", + 0xFFB1: "ReadBuildNumber", + 0xFFD0: "ReadFzmSentryStates", + 0xFFD1: "ReadFzmSlaveStates", + 0xFFD2: "ReadFzmMasterState", + 0xFFD3: "ControlLifecycle", + 0xFFD5: "IsCertificateValid", + 0xFFFA: "SetDiagRouting", + 0xFFFF: "ReadMemory"} + fields_desc = [ + ShortEnumField('identifier', 0xffff, identifiers) + ] + + +class DEV_JOB_PR(Packet): + fields_desc = [ + ShortEnumField('identifier', 0xffff, DEV_JOB.identifiers) + ] + + def answers(self, other): + return isinstance(other, DEV_JOB) and \ + self.identifier == other.identifier + + +UDS.services[0xBF] = "DevelopmentJob" +UDS.services[0xFF] = "DevelopmentJobPositiveResponse" +bind_layers(UDS, DEV_JOB, service=0xBF) +bind_layers(UDS, DEV_JOB_PR, service=0xFF) + + +class READ_MEM(Packet): + fields_desc = [ + IntField('read_addr', 0), + IntField('read_length', 0) + ] + + +class READ_MEM_PR(Packet): + fields_desc = [ + StrField('data', ''), + ] + + +class WEBSERVER(Packet): + fields_desc = [ + ByteField('enable', 1), + ThreeBytesField('password', 0x10203) + ] + + +bind_layers(DEV_JOB, WEBSERVER, identifier=0xff66) +bind_layers(DEV_JOB_PR, WEBSERVER, identifier=0xff66) +bind_layers(DEV_JOB, READ_MEM, identifier=0xffff) +bind_layers(DEV_JOB_PR, READ_MEM_PR, identifier=0xffff) + bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf101) bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf102) bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf103) @@ -347,11 +488,10 @@ class SVK(Packet): bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf13f) bind_layers(UDS_RDBIPR, SVK, dataIdentifier=0xf140) - UDS_RDBI.dataIdentifiers[0x0014] = "RDBCI_IS_LESEN_DETAIL_REQ" UDS_RDBI.dataIdentifiers[0x0015] = "RDBCI_HS_LESEN_DETAIL_REQ" UDS_RDBI.dataIdentifiers[0x0e80] = "AirbagLock" -UDS_RDBI.dataIdentifiers[0x1000] = "testStamp" +UDS_RDBI.dataIdentifiers[0x1000] = "TestStamp" UDS_RDBI.dataIdentifiers[0x1001] = "CBSdata" UDS_RDBI.dataIdentifiers[0x1002] = "smallUserInformationField" UDS_RDBI.dataIdentifiers[0x1003] = "smallUserInformationField" @@ -361,8 +501,8 @@ class SVK(Packet): UDS_RDBI.dataIdentifiers[0x1007] = "smallUserInformationField" UDS_RDBI.dataIdentifiers[0x1008] = "smallUserInformationFieldBMWfast" UDS_RDBI.dataIdentifiers[0x1009] = "vehicleProductionDate" -UDS_RDBI.dataIdentifiers[0x100a] = "energySavingState" # or EnergyMode -UDS_RDBI.dataIdentifiers[0x100b] = "Istep" # or I-Stufe +UDS_RDBI.dataIdentifiers[0x100A] = "EnergyMode" +UDS_RDBI.dataIdentifiers[0x100B] = "VcmIntegrationStep" UDS_RDBI.dataIdentifiers[0x100d] = "gatewayTableVersionNumber" UDS_RDBI.dataIdentifiers[0x100e] = "ExtendedMode" UDS_RDBI.dataIdentifiers[0x1010] = "fullVehicleIdentificationNumber" @@ -637,10 +777,15 @@ class SVK(Packet): UDS_RDBI.dataIdentifiers[0x16fd] = "SubbusMemberSerialNumber" UDS_RDBI.dataIdentifiers[0x16fe] = "SubbusMemberSerialNumber" UDS_RDBI.dataIdentifiers[0x16ff] = "SubbusMemberSerialNumber" -UDS_RDBI.dataIdentifiers[0x171f] = "Certificate" -UDS_RDBI.dataIdentifiers[0x172a] = "IPIdent" -UDS_RDBI.dataIdentifiers[0x1734] = "IndividualDataIDTable" -UDS_RDBI.dataIdentifiers[0x1735] = "StatusLifeCycle" +UDS_RDBI.dataIdentifiers[0x1701] = "SysTime" +UDS_RDBI.dataIdentifiers[0x170C] = "BoardPowerSupply" +UDS_RDBI.dataIdentifiers[0x171F] = "Certificate" +UDS_RDBI.dataIdentifiers[0x1720] = "SCVersion" +UDS_RDBI.dataIdentifiers[0x1723] = "ActiveResponseDTCs" +UDS_RDBI.dataIdentifiers[0x1724] = "LockableDTCs" +UDS_RDBI.dataIdentifiers[0x172A] = "IPConfiguration" +UDS_RDBI.dataIdentifiers[0x172B] = "MACAddress" +UDS_RDBI.dataIdentifiers[0x1735] = "LifecycleMode" UDS_RDBI.dataIdentifiers[0x2000] = "dtcShadowMemory" UDS_RDBI.dataIdentifiers[0x2001] = "dtcShadowMemoryEntry" UDS_RDBI.dataIdentifiers[0x2002] = "dtcShadowMemoryEntry" @@ -1409,7 +1554,7 @@ class SVK(Packet): UDS_RDBI.dataIdentifiers[0x22fd] = "afterSalesServiceData_2200_22FF" UDS_RDBI.dataIdentifiers[0x22fe] = "afterSalesServiceData_2200_22FF" UDS_RDBI.dataIdentifiers[0x22ff] = "afterSalesServiceData_2200_22FF" -UDS_RDBI.dataIdentifiers[0x2300] = "operatingData" # or RDBCI_BETRIEBSDATEN_LESEN_REQ # noqa E501 +UDS_RDBI.dataIdentifiers[0x2300] = "operatingData" # or RDBCI_BETRIEBSDATEN_LESEN_REQ # noqa E501 UDS_RDBI.dataIdentifiers[0x2301] = "additionalOperatingData 2301-23FF" UDS_RDBI.dataIdentifiers[0x2302] = "additionalOperatingData 2301-23FF" UDS_RDBI.dataIdentifiers[0x2303] = "additionalOperatingData 2301-23FF" @@ -1730,57 +1875,69 @@ class SVK(Packet): UDS_RDBI.dataIdentifiers[0x243e] = "additionalPersonalizationDataDriver3" UDS_RDBI.dataIdentifiers[0x243f] = "additionalPersonalizationDataDriver3" UDS_RDBI.dataIdentifiers[0x2500] = "programmReferenzBackup/vehicleManufacturerECUHW_NrBackup" # noqa E501 -UDS_RDBI.dataIdentifiers[0x2501] = "eraseTime, signatureTime, resetTime, authentificationTime" # or memorySegmentationTable # noqa E501 -UDS_RDBI.dataIdentifiers[0x2502] = "hardwareReferenz" # or programmingCounter # noqa E501 -UDS_RDBI.dataIdentifiers[0x2503] = "programmingCounter-MaxValue" # or programmReferenz # noqa E501 -UDS_RDBI.dataIdentifiers[0x2504] = "flashTimingParameter" # or datenReferenz # noqa E501 -UDS_RDBI.dataIdentifiers[0x2505] = "maximumBlocklength" -UDS_RDBI.dataIdentifiers[0x2506] = "ReadMemoryAddress" # or maximaleBlockLaenge # noqa E501 +UDS_RDBI.dataIdentifiers[0x2501] = "MemorySegmentationTable" +UDS_RDBI.dataIdentifiers[0x2502] = "ProgrammingCounter" +UDS_RDBI.dataIdentifiers[0x2503] = "ProgrammingCounterMax" +UDS_RDBI.dataIdentifiers[0x2504] = "FlashTimings" +UDS_RDBI.dataIdentifiers[0x2505] = "MaxBlocklength" +UDS_RDBI.dataIdentifiers[0x2506] = "ReadMemoryAddress" # or maximaleBlockLaenge # noqa E501 UDS_RDBI.dataIdentifiers[0x2507] = "EcuSupportsDeleteSwe" UDS_RDBI.dataIdentifiers[0x2508] = "GWRoutingStatus" UDS_RDBI.dataIdentifiers[0x2509] = "RoutingTable" +UDS_RDBI.dataIdentifiers[0x2530] = "SubnetStatus" UDS_RDBI.dataIdentifiers[0x2541] = "STATUS_CALCVN" -UDS_RDBI.dataIdentifiers[0x3000] = "RDBI_CD_REQ" # or WDBI_CD_REQ +UDS_RDBI.dataIdentifiers[0x3000] = "RDBI_CD_REQ" # or WDBI_CD_REQ UDS_RDBI.dataIdentifiers[0x300a] = "Codier-VIN" UDS_RDBI.dataIdentifiers[0x37fe] = "Codierpruefstempel" UDS_RDBI.dataIdentifiers[0x3f00] = "SVT-Ist" UDS_RDBI.dataIdentifiers[0x3f01] = "SVT-Soll" -UDS_RDBI.dataIdentifiers[0x3f02] = "SGListeSecurity" -UDS_RDBI.dataIdentifiers[0x3f03] = "SG-Liste SWT" -UDS_RDBI.dataIdentifiers[0x3f04] = "Zeitstempel" -UDS_RDBI.dataIdentifiers[0x3f05] = "Liste aller Seriennummern" -UDS_RDBI.dataIdentifiers[0x3f06] = "FA" -UDS_RDBI.dataIdentifiers[0x3f07] = "SGListeKomplett" -UDS_RDBI.dataIdentifiers[0x3f08] = "SGListeAktivesMelden" -UDS_RDBI.dataIdentifiers[0x3f09] = "FP" -UDS_RDBI.dataIdentifiers[0x3f0a] = "SGListeDifferentiellProg" -UDS_RDBI.dataIdentifiers[0x3f0b] = "SGListeNGSC" -UDS_RDBI.dataIdentifiers[0x3f0c] = "SGListeCodierrelevantesSG" -UDS_RDBI.dataIdentifiers[0x3f0d] = "SGListeFlashfaehigesSG" -UDS_RDBI.dataIdentifiers[0x3f0e] = "SGListeK_CAN" -UDS_RDBI.dataIdentifiers[0x3f0f] = "SGListeBody_CAN" -UDS_RDBI.dataIdentifiers[0x3f10] = "SGListeI_CAN" -UDS_RDBI.dataIdentifiers[0x3f11] = "SGListeMOST" -UDS_RDBI.dataIdentifiers[0x3f12] = "SGListeFA_CAN" -UDS_RDBI.dataIdentifiers[0x3f13] = "SGListeFlexRay" -UDS_RDBI.dataIdentifiers[0x3f14] = "SGListeA_CAN" -UDS_RDBI.dataIdentifiers[0x3f15] = "SGListeISO14229" -UDS_RDBI.dataIdentifiers[0x3f16] = "SGListeS_CAN" -UDS_RDBI.dataIdentifiers[0x3f17] = "SGListeEthernet" -UDS_RDBI.dataIdentifiers[0x3f18] = "SGListeD_CAN" -UDS_RDBI.dataIdentifiers[0x3f19] = "Identifikation VCM" -UDS_RDBI.dataIdentifiers[0x3f1a] = "SVT-Version" +UDS_RDBI.dataIdentifiers[0x3F02] = "VcmEcuListSecurity" +UDS_RDBI.dataIdentifiers[0x3F03] = "VcmEcuListSwt" +UDS_RDBI.dataIdentifiers[0x3F04] = "VcmNotificationTimeStamp" +UDS_RDBI.dataIdentifiers[0x3F05] = "VcmSerialNumberReferenceList" +UDS_RDBI.dataIdentifiers[0x3F06] = "VcmVehicleOrder" +UDS_RDBI.dataIdentifiers[0x3F07] = "VcmEcuListAll" +UDS_RDBI.dataIdentifiers[0x3F08] = "VcmEcuListActiveResponse" +UDS_RDBI.dataIdentifiers[0x3F09] = "VcmVehicleProfile" +UDS_RDBI.dataIdentifiers[0x3F0A] = "VcmEcuListDiffProg" +UDS_RDBI.dataIdentifiers[0x3F0B] = "VcmEcuListNgsc" +UDS_RDBI.dataIdentifiers[0x3F0C] = "VcmEcuListCodingRelevant" +UDS_RDBI.dataIdentifiers[0x3F0D] = "VcmEcuListFlashable" +UDS_RDBI.dataIdentifiers[0x3F0E] = "VcmEcuListKCan" +UDS_RDBI.dataIdentifiers[0x3F0F] = "VcmEcuListBodyCan" +UDS_RDBI.dataIdentifiers[0x3F10] = "VcmEcuListSFCan" +UDS_RDBI.dataIdentifiers[0x3F11] = "VcmEcuListMost" +UDS_RDBI.dataIdentifiers[0x3F12] = "VcmEcuListFaCan" +UDS_RDBI.dataIdentifiers[0x3F13] = "VcmEcuListFlexray" +UDS_RDBI.dataIdentifiers[0x3F14] = "VcmEcuListACan" +UDS_RDBI.dataIdentifiers[0x3F15] = "VcmEcuListIso14229" +UDS_RDBI.dataIdentifiers[0x3F16] = "VcmEcuListSCan" +UDS_RDBI.dataIdentifiers[0x3F17] = "VcmEcuListEthernet" +UDS_RDBI.dataIdentifiers[0x3F18] = "VcmEcuListDCan" +UDS_RDBI.dataIdentifiers[0x3F19] = "VcmVcmIdentification" +UDS_RDBI.dataIdentifiers[0x3F1A] = "VcmSvtVersion" UDS_RDBI.dataIdentifiers[0x3f1b] = "vehicleOrder_3F00_3FFE" UDS_RDBI.dataIdentifiers[0x3f1c] = "FA_Teil1" UDS_RDBI.dataIdentifiers[0x3f1d] = "FA_Teil2" UDS_RDBI.dataIdentifiers[0x3fff] = "changeIndexOfCodingData" +UDS_RDBI.dataIdentifiers[0x4000] = "GWTableVersion" +UDS_RDBI.dataIdentifiers[0x4001] = "WakeupSource" +UDS_RDBI.dataIdentifiers[0x4020] = "StatusLearnFlexray" +UDS_RDBI.dataIdentifiers[0x4021] = "StatusFlexrayPath" +UDS_RDBI.dataIdentifiers[0x4030] = "EthernetRegisters" +UDS_RDBI.dataIdentifiers[0x4031] = "EthernetStatusInformation" UDS_RDBI.dataIdentifiers[0x403c] = "STATUS_CALCVN_EA" +UDS_RDBI.dataIdentifiers[0x4040] = "DemLockingMasterState" +UDS_RDBI.dataIdentifiers[0x4050] = "AmbiguousRoutings" UDS_RDBI.dataIdentifiers[0x4080] = "AirbagLock_NEU" +UDS_RDBI.dataIdentifiers[0x4140] = "BodyComConfig" UDS_RDBI.dataIdentifiers[0x4ab4] = "Betriebsstundenzaehler" UDS_RDBI.dataIdentifiers[0x5fc2] = "WDBI_DME_ABGLEICH_PROG_REQ" -UDS_RDBI.dataIdentifiers[0xd114] = "Gesamtweg-Streckenzähler Offset" +UDS_RDBI.dataIdentifiers[0xd114] = "Gesamtweg-Streckenzaehler Offset" UDS_RDBI.dataIdentifiers[0xd387] = "STATUS_DIEBSTAHLSCHUTZ" UDS_RDBI.dataIdentifiers[0xdb9c] = "InitStatusEngineAngle" +UDS_RDBI.dataIdentifiers[0xEFE9] = "WakeupRegistry" +UDS_RDBI.dataIdentifiers[0xEFE8] = "ClearWakeupRegistry" UDS_RDBI.dataIdentifiers[0xf000] = "networkConfigurationDataForTractorTrailerApplication" # noqa E501 UDS_RDBI.dataIdentifiers[0xf001] = "networkConfigurationDataForTractorTrailerApplication" # noqa E501 UDS_RDBI.dataIdentifiers[0xf002] = "networkConfigurationDataForTractorTrailerApplication" # noqa E501 @@ -2038,9 +2195,9 @@ class SVK(Packet): UDS_RDBI.dataIdentifiers[0xf0fe] = "networkConfigurationData" UDS_RDBI.dataIdentifiers[0xf0ff] = "networkConfigurationData" UDS_RDBI.dataIdentifiers[0xf100] = "activeSessionState" -UDS_RDBI.dataIdentifiers[0xf101] = "SVK_Aktuell" -UDS_RDBI.dataIdentifiers[0xf102] = "SVK_SystemSupplier" -UDS_RDBI.dataIdentifiers[0xf103] = "SVK_Werk" +UDS_RDBI.dataIdentifiers[0xF101] = "SVKCurrent" +UDS_RDBI.dataIdentifiers[0xF102] = "SVKSystemSupplier" +UDS_RDBI.dataIdentifiers[0xF103] = "SVKFactory" UDS_RDBI.dataIdentifiers[0xf104] = "SVK_Backup_01" UDS_RDBI.dataIdentifiers[0xf105] = "SVK_Backup_02" UDS_RDBI.dataIdentifiers[0xf106] = "SVK_Backup_03" @@ -4725,7 +4882,7 @@ class SVK(Packet): UDS_RC.routineControlIdentifiers[0x0205] = "readSWEDevelopmentInfo" UDS_RC.routineControlIdentifiers[0x0206] = "checkProgrammingPower" UDS_RC.routineControlIdentifiers[0x0207] = "VCM_Generiere_SVT" -UDS_RC.routineControlIdentifiers[0x020b] = "Steuergerätetausch" +UDS_RC.routineControlIdentifiers[0x020b] = "Steuergeraetetausch" UDS_RC.routineControlIdentifiers[0x020c] = "KeyExchange" UDS_RC.routineControlIdentifiers[0x020d] = "FingerprintExchange" UDS_RC.routineControlIdentifiers[0x020e] = "InternalAuthentication" @@ -4743,6 +4900,9 @@ class SVK(Packet): UDS_RC.routineControlIdentifiers[0x0233] = "GetParameterN11" UDS_RC.routineControlIdentifiers[0x0234] = "ExternerInit" UDS_RC.routineControlIdentifiers[0x02a5] = "RequestListEntry" +UDS_RC.routineControlIdentifiers[0x0303] = "DiagLoopbackStart" +UDS_RC.routineControlIdentifiers[0x0304] = "DTC" +UDS_RC.routineControlIdentifiers[0x0305] = "STEUERN_DM_FSS_MASTER" UDS_RC.routineControlIdentifiers[0x0f01] = "codingChecksum" UDS_RC.routineControlIdentifiers[0x0f02] = "clearMemory" UDS_RC.routineControlIdentifiers[0x0f04] = "selfTest" @@ -4753,7 +4913,7 @@ class SVK(Packet): UDS_RC.routineControlIdentifiers[0x0f09] = "checkSignature" UDS_RC.routineControlIdentifiers[0x0f0a] = "checkProgrammingStatus" UDS_RC.routineControlIdentifiers[0x0f0b] = "ExecuteDiagnosticService" -UDS_RC.routineControlIdentifiers[0x0f0c] = "SetEnergyMode" # or controlEnergySavingMode # noqa E501 +UDS_RC.routineControlIdentifiers[0x0f0c] = "SetEnergyMode" # or controlEnergySavingMode # noqa E501 UDS_RC.routineControlIdentifiers[0x0f0d] = "resetSystemFaultMessage" UDS_RC.routineControlIdentifiers[0x0f0e] = "timeControlledPowerDown" UDS_RC.routineControlIdentifiers[0x0f0f] = "disableCommunicationOverGateway" @@ -4773,8 +4933,10 @@ class SVK(Packet): UDS_RC.routineControlIdentifiers[0x1042] = "EthernetARLTable" UDS_RC.routineControlIdentifiers[0x1045] = "EthernetIPConfiguration" UDS_RC.routineControlIdentifiers[0x104e] = "EthernetARLTableExtended" +UDS_RC.routineControlIdentifiers[0x4000] = "Diagnosemaster" UDS_RC.routineControlIdentifiers[0x4001] = "SetGWRouting" UDS_RC.routineControlIdentifiers[0x4002] = "HDDDownload" +UDS_RC.routineControlIdentifiers[0x4004] = "KeepBussesAlive" UDS_RC.routineControlIdentifiers[0x4007] = "updateMode" UDS_RC.routineControlIdentifiers[0x4008] = "httpUpdate" UDS_RC.routineControlIdentifiers[0x7000] = "ProcessingApplicationData" @@ -5295,7 +5457,30 @@ class SVK(Packet): UDS_RC.routineControlIdentifiers[0xe1ff] = "OBDTestIDs" UDS_RC.routineControlIdentifiers[0xf013] = "DeactivateSegeln" UDS_RC.routineControlIdentifiers[0xf043] = "RequestDeactivateMontagemodus" -UDS_RC.routineControlIdentifiers[0xf760] = "ResetActivationline" +UDS_RC.routineControlIdentifiers[0xF720] = "ControlSniffingHuPort" +UDS_RC.routineControlIdentifiers[0xF759] = "ControlHeadUnitActivationLine" +UDS_RC.routineControlIdentifiers[0xF760] = "ResetHeadUnitActivationLine" +UDS_RC.routineControlIdentifiers[0xF761] = "ClearFilterCAN" +UDS_RC.routineControlIdentifiers[0xF762] = "SetFilterCAN" +UDS_RC.routineControlIdentifiers[0xF764] = "MessageLogging" +UDS_RC.routineControlIdentifiers[0xF765] = "ReceiveCANFrame" +UDS_RC.routineControlIdentifiers[0xF766] = "SendCANFrame" +UDS_RC.routineControlIdentifiers[0xF767] = "ReceiveFlexrayFrame" +UDS_RC.routineControlIdentifiers[0xF768] = "SendFlexrayFrame" +UDS_RC.routineControlIdentifiers[0xF769] = "SetFilterFlexray" +UDS_RC.routineControlIdentifiers[0xF770] = "ClearFilterFlexray" +UDS_RC.routineControlIdentifiers[0xF774] = "GetStatusLogging" +UDS_RC.routineControlIdentifiers[0xF776] = "MessageTunnelDeauthenticator" +UDS_RC.routineControlIdentifiers[0xF777] = "ControlTransDiagSend" +UDS_RC.routineControlIdentifiers[0xF778] = "ClearFilterAll" +UDS_RC.routineControlIdentifiers[0xF779] = "GetFilterCAN" +UDS_RC.routineControlIdentifiers[0xF77B] = "SteuernFlexrayAutoDetectDisable" +UDS_RC.routineControlIdentifiers[0xF77C] = "SteuernFlexrayPath" +UDS_RC.routineControlIdentifiers[0xF77D] = "SteuernResetLernFlexray" +UDS_RC.routineControlIdentifiers[0xF77F] = "SteuernLernFlexray" +UDS_RC.routineControlIdentifiers[0xF780] = "ClearFilterLIN" +UDS_RC.routineControlIdentifiers[0xF781] = "GetFilterLIN" +UDS_RC.routineControlIdentifiers[0xF782] = "SetFilterLIN" UDS_RC.routineControlIdentifiers[0xff00] = "eraseMemory" UDS_RC.routineControlIdentifiers[0xff01] = "checkProgrammingDependencies" diff --git a/scapy/contrib/automotive/bmw/enet.py b/scapy/contrib/automotive/bmw/enet.py deleted file mode 100644 index ebc5e356519..00000000000 --- a/scapy/contrib/automotive/bmw/enet.py +++ /dev/null @@ -1,95 +0,0 @@ -# This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Nils Weiss -# This program is published under a GPLv2 license - -# scapy.contrib.description = ENET - BMW diagnostic protocol over Ethernet -# scapy.contrib.status = loads - -import struct -import socket -from scapy.packet import Packet, bind_layers, bind_bottom_up -from scapy.fields import IntField, ShortEnumField, XByteField -from scapy.layers.inet import TCP -from scapy.supersocket import StreamSocket -from scapy.contrib.automotive.uds import UDS -from scapy.contrib.isotp import ISOTP -from scapy.error import Scapy_Exception -from scapy.data import MTU - - -""" -BMW specific diagnostic over IP protocol implementation ENET -""" - -# #########################ENET################################### - - -class ENET(Packet): - name = 'ENET' - fields_desc = [ - IntField('length', None), - ShortEnumField('type', 1, {0x01: "message", - 0x02: "echo"}), - XByteField('src', 0), - XByteField('dst', 0), - ] - - def hashret(self): - hdr_hash = struct.pack("B", self.src ^ self.dst) - pay_hash = self.payload.hashret() - return hdr_hash + pay_hash - - def answers(self, other): - if other.__class__ == self.__class__: - return self.payload.answers(other.payload) - return 0 - - def extract_padding(self, s): - return s[:self.length - 2], s[self.length - 2:] - - def post_build(self, pkt, pay): - """ - This will set the LenField 'length' to the correct value. - """ - if self.length is None: - pkt = struct.pack("!I", len(pay) + 2) + pkt[4:] - return pkt + pay - - -bind_bottom_up(TCP, ENET, sport=6801) -bind_bottom_up(TCP, ENET, dport=6801) -bind_layers(TCP, ENET, sport=6801, dport=6801) -bind_layers(ENET, UDS) - - -# ########################ENETSocket################################### - - -class ENETSocket(StreamSocket): - def __init__(self, ip='127.0.0.1', port=6801): - self.ip = ip - self.port = port - s = socket.socket() - s.connect((self.ip, self.port)) - StreamSocket.__init__(self, s, ENET) - - -class ISOTP_ENETSocket(ENETSocket): - def __init__(self, src, dst, ip='127.0.0.1', port=6801, basecls=ISOTP): - super(ISOTP_ENETSocket, self).__init__(ip, port) - self.src = src - self.dst = dst - self.basecls = ENET - self.outputcls = basecls - - def send(self, x): - if not isinstance(x, ISOTP): - raise Scapy_Exception("Please provide a packet class based on " - "ISOTP") - super(ISOTP_ENETSocket, self).send( - ENET(src=self.src, dst=self.dst) / x) - - def recv(self, x=MTU): - pkt = super(ISOTP_ENETSocket, self).recv(x) - return self.outputcls(bytes(pkt[1])) diff --git a/scapy/contrib/automotive/bmw/enumerator.py b/scapy/contrib/automotive/bmw/enumerator.py new file mode 100644 index 00000000000..e19aad16c8e --- /dev/null +++ b/scapy/contrib/automotive/bmw/enumerator.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = BMW specific enumerators +# scapy.contrib.status = loads + + +from scapy.packet import Packet +from scapy.contrib.automotive.scanner.enumerator import _AutomotiveTestCaseScanResult # noqa: E501 +from scapy.contrib.automotive.uds import UDS +from scapy.contrib.automotive.bmw.definitions import DEV_JOB +from scapy.contrib.automotive.uds_scan import UDS_Enumerator + +from typing import ( + Any, + Iterable, +) + + +class BMW_DevJobEnumerator(UDS_Enumerator): + _description = "Available DevelopmentJobs by Identifier " \ + "and negative response per state" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x10000)) + return (UDS() / DEV_JOB(identifier=x) for x in scan_range) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x: %s" % \ + (tup[1].identifier, tup[1].sprintf("%DEV_JOB.identifier%")) diff --git a/scapy/contrib/automotive/bmw/hsfz.py b/scapy/contrib/automotive/bmw/hsfz.py new file mode 100644 index 00000000000..3316d7d136f --- /dev/null +++ b/scapy/contrib/automotive/bmw/hsfz.py @@ -0,0 +1,240 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = HSFZ - BMW High-Speed-Fahrzeug-Zugang +# scapy.contrib.status = loads +import logging +import socket +import struct +import time +from typing import ( + Any, + Optional, + Tuple, + Type, + Iterable, + List, + Union, +) + +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.uds import UDS, UDS_TP +from scapy.data import MTU +from scapy.fields import (IntField, ShortEnumField, XByteField, + ConditionalField, StrFixedLenField) +from scapy.layers.inet import TCP, UDP +from scapy.packet import Packet, bind_layers, bind_bottom_up +from scapy.supersocket import StreamSocket + +""" +BMW HSFZ (High-Speed-Fahrzeug-Zugang / High-Speed-Car-Access). +BMW specific diagnostic over IP protocol implementation. +The physical interface for this connection is called ENET. +""" + + +# #########################HSFZ################################### + + +class HSFZ(Packet): + control_words = { + 0x01: "diagnostic_req_res", + 0x02: "acknowledge_transfer", + 0x10: "terminal15", + 0x11: "vehicle_ident_data", + 0x12: "alive_check", + 0x13: "status_data_inquiry", + 0x40: "incorrect_tester_address", + 0x41: "incorrect_control_word", + 0x42: "incorrect_format", + 0x43: "incorrect_dest_address", + 0x44: "message_too_large", + 0x45: "diag_app_not_ready", + 0xFF: "out_of_memory" + } + name = 'HSFZ' + fields_desc = [ + IntField('length', None), + ShortEnumField('control', 1, control_words), + ConditionalField( + XByteField('source', 0), lambda p: p._has_srctgt_addrs()), + ConditionalField( + XByteField('target', 0), lambda p: p._has_srctgt_addrs()), + ConditionalField( + XByteField('expected', 0), lambda p: p._has_exprecv_addrs()), + ConditionalField( + XByteField('received', 0), lambda p: p._has_exprecv_addrs()), + ConditionalField( + StrFixedLenField("identification_string", + None, None, lambda p: p.length), + lambda p: p._hasidstring()) + ] + + def _has_srctgt_addrs(self): + # type: () -> bool + # Address present in diagnostic_req_res, acknowledge_transfer, + # and two byte length alive_check frames. + return self.control == 0x01 or \ + self.control == 0x02 or \ + (self.control == 0x12 and self.length == 2) + + def _has_exprecv_addrs(self): + # type: () -> bool + # Address present in incorrect_tester_address frames. + return self.control == 0x40 + + def _hasidstring(self): + # type: () -> bool + # ID string is present in some vehicle_ident_data frames and in + # long alive_check grames. + return (self.control == 0x11 and self.length != 0) or \ + (self.control == 0x12 and self.length > 2) + + def hashret(self): + # type: () -> bytes + hdr_hash = struct.pack("B", self.source ^ self.target) + pay_hash = self.payload.hashret() + return hdr_hash + pay_hash + + def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, bytes] + return s[:self.length - 2], s[self.length - 2:] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + """ + This will set the LenField 'length' to the correct value. + """ + if self.length is None: + pkt = struct.pack("!I", len(pay) + 2) + pkt[4:] + return pkt + pay + + +bind_bottom_up(TCP, HSFZ, sport=6801) +bind_bottom_up(TCP, HSFZ, dport=6801) +bind_layers(TCP, HSFZ, sport=6801, dport=6801) + +bind_bottom_up(UDP, HSFZ, sport=6811) +bind_bottom_up(UDP, HSFZ, dport=6811) +bind_layers(UDP, HSFZ, sport=6811, dport=6811) + +bind_layers(HSFZ, UDS) + + +# ########################HSFZSocket################################### + + +class HSFZSocket(StreamSocket): + def __init__(self, ip='127.0.0.1', port=6801): + # type: (str, int) -> None + self.ip = ip + self.port = port + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.connect((self.ip, self.port)) + StreamSocket.__init__(self, s, HSFZ) + self.buffer = b"" + + def recv(self, x=MTU, **kwargs): + # type: (Optional[int], **Any) -> Optional[Packet] + if self.buffer: + len_data = self.buffer[:4] + else: + len_data = self.ins.recv(4, socket.MSG_PEEK) + if len(len_data) != 4: + return None + + len_int = struct.unpack(">I", len_data)[0] + len_int += 6 + self.buffer += self.ins.recv(len_int - len(self.buffer)) + + if len(self.buffer) != len_int: + return None + + pkt = self.basecls(self.buffer, **kwargs) # type: Packet + self.buffer = b"" + return pkt + + +class UDS_HSFZSocket(HSFZSocket): + def __init__(self, source, target, ip='127.0.0.1', port=6801, basecls=UDS): + # type: (int, int, str, int, Type[Packet]) -> None + super(UDS_HSFZSocket, self).__init__(ip, port) + self.source = source + self.target = target + self.basecls = HSFZ + self.outputcls = basecls + + def send(self, x): + # type: (Packet) -> int + try: + x.sent_time = time.time() + except AttributeError: + pass + + try: + return super(UDS_HSFZSocket, self).send( + HSFZ(source=self.source, target=self.target) / x) + except Exception as e: + # Workaround: + # This catch block is currently necessary to detect errors + # during send. In automotive application it's not uncommon that + # a destination socket goes down. If any function based on + # SndRcvHandler is used, all exceptions are silently handled + # in the send part. This means, a caller of the SndRcvHandler + # can not detect if an error occurred. This workaround closes + # the socket if a send error was detected. + log_automotive.exception("Exception: %s", e) + self.close() + return 0 + + def recv(self, x=MTU, **kwargs): + # type: (Optional[int], **Any) -> Optional[Packet] + pkt = super(UDS_HSFZSocket, self).recv(x) + if pkt and pkt.control == 1: + return self.outputcls(bytes(pkt.payload), **kwargs) + else: + return pkt + + +def hsfz_scan(ip, # type: str + scan_range=range(0x100), # type: Iterable[int] + source=0xf4, # type: int + timeout=0.1, # type: Union[int, float] + verbose=True # type: bool + ): + # type: (...) -> List[UDS_HSFZSocket] + """ + Helper function to scan for HSFZ endpoints. + + Example: + >>> sockets = hsfz_scan("192.168.0.42") + + :param ip: IPv4 address of target to scan + :param scan_range: Range for HSFZ destination address + :param source: HSFZ source address, used during the scan + :param timeout: Timeout for each request + :param verbose: Show information during scan, if True + :return: A list of open UDS_HSFZSockets + """ + if verbose: + log_automotive.setLevel(logging.DEBUG) + results = list() + for i in scan_range: + with UDS_HSFZSocket(source, i, ip) as sock: + try: + resp = sock.sr1(UDS() / UDS_TP(), + timeout=timeout, + verbose=False) + if resp: + results.append((i, resp)) + if resp: + log_automotive.debug( + "Found endpoint %s, source=0x%x, target=0x%x" % (ip, source, i)) + except Exception as e: + log_automotive.exception( + "Error %s at destination address 0x%x" % (e, i)) + return [UDS_HSFZSocket(0xf4, target, ip) for target, _ in results] diff --git a/scapy/contrib/automotive/ccp.py b/scapy/contrib/automotive/ccp.py index 2f9135448b6..6ec7eb31475 100644 --- a/scapy/contrib/automotive/ccp.py +++ b/scapy/contrib/automotive/ccp.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.description = CAN Calibration Protocol (CCP) # scapy.contrib.status = loads @@ -530,6 +530,10 @@ def __init__(self, *args, **kwargs): del kwargs["payload_cls"] Packet.__init__(self, *args, **kwargs) + def __eq__(self, other): + return super(DTO, self).__eq__(other) and \ + self.payload_cls == other.payload_cls + def guess_payload_class(self, payload): return self.payload_cls diff --git a/scapy/contrib/automotive/doip.py b/scapy/contrib/automotive/doip.py new file mode 100644 index 00000000000..acd58a811ba --- /dev/null +++ b/scapy/contrib/automotive/doip.py @@ -0,0 +1,525 @@ +#! /usr/bin/env python + +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = Diagnostic over IP (DoIP) / ISO 13400 +# scapy.contrib.status = loads + +import socket +import ssl +import struct +import time +from typing import ( + Any, + Union, + Tuple, + Optional, + Dict, + Type, +) + +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.uds import UDS +from scapy.data import MTU +from scapy.fields import ( + ByteEnumField, + ConditionalField, + IntField, + MayEnd, + StrFixedLenField, + XByteEnumField, + XByteField, + XIntField, + XShortEnumField, + XShortField, + XStrField, +) +from scapy.layers.inet import TCP, UDP +from scapy.packet import Packet, bind_layers, bind_bottom_up +from scapy.supersocket import SSLStreamSocket + + +# ISO 13400-2 sect 9.2 + + +class DoIP(Packet): + """ + Implementation of the DoIP (ISO 13400) protocol. DoIP packets can be sent + via UDP and TCP. Depending on the payload type, the correct connection + need to be chosen: + + +--------------+--------------------------------------------------------------+-----------------+ + | Payload Type | Payload Type Name | Connection Kind | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0000 | Generic DoIP header negative acknowledge | UDP / TCP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0001 | Vehicle Identification request message | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0002 | Vehicle identification request message with EID | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0003 | Vehicle identification request message with VIN | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0004 | Vehicle announcement message/vehicle identification response | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0005 | Routing activation request | TCP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0006 | Routing activation response | TCP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0007 | Alive Check request | TCP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x0008 | Alive Check response | TCP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x4001 | IP entity status request | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x4002 | DoIP entity status response | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x4003 | Diagnostic power mode information request | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x4004 | Diagnostic power mode information response | UDP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x8001 | Diagnostic message | TCP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x8002 | Diagnostic message positive acknowledgement | TCP | + +--------------+--------------------------------------------------------------+-----------------+ + | 0x8003 | Diagnostic message negative acknowledgement | TCP | + +--------------+--------------------------------------------------------------+-----------------+ + + Example with UDP: + >>> socket = L3RawSocket(iface="eth0") + >>> resp = socket.sr1(IP(dst="169.254.117.238")/UDP(dport=13400)/DoIP(payload_type=1)) + + Example with TCP: + >>> socket = DoIPSocket("169.254.117.238") + >>> pkt = DoIP(payload_type=0x8001, source_address=0xe80, target_address=0x1000) / UDS() / UDS_RDBI(identifiers=[0x1000]) + >>> resp = socket.sr1(pkt, timeout=1) + + Example with UDS: + >>> socket = UDS_DoIPSocket("169.254.117.238") + >>> pkt = UDS() / UDS_RDBI(identifiers=[0x1000]) + >>> resp = socket.sr1(pkt, timeout=1) + """ # noqa: E501 + payload_types = { + 0x0000: "Generic DoIP header NACK", + 0x0001: "Vehicle identification request", + 0x0002: "Vehicle identification request with EID", + 0x0003: "Vehicle identification request with VIN", + 0x0004: "Vehicle announcement message/vehicle identification response message", # noqa: E501 + 0x0005: "Routing activation request", + 0x0006: "Routing activation response", + 0x0007: "Alive check request", + 0x0008: "Alive check response", + 0x4001: "DoIP entity status request", + 0x4002: "DoIP entity status response", + 0x4003: "Diagnostic power mode information request", + 0x4004: "Diagnostic power mode information response", + 0x8001: "Diagnostic message", + 0x8002: "Diagnostic message ACK", + 0x8003: "Diagnostic message NACK"} + name = 'DoIP' + fields_desc = [ + XByteEnumField("protocol_version", 0x02, { + 0x01: "ISO13400_2010", 0x02: "ISO13400_2012", + 0x03: "ISO13400_2019", 0x04: "ISO13400_2019_AMD1"}), + XByteEnumField("inverse_version", 0xFD, { + 0xFE: "ISO13400_2010", 0xFD: "ISO13400_2012", + 0xFC: "ISO13400_2019", 0xFB: "ISO13400_2019_AMD1"}), + XShortEnumField("payload_type", 0, payload_types), + IntField("payload_length", None), + ConditionalField(ByteEnumField("nack", 0, { + 0: "Incorrect pattern format", 1: "Unknown payload type", + 2: "Message too large", 3: "Out of memory", + 4: "Invalid payload length" + }), lambda p: p.payload_type in [0x0]), + ConditionalField(StrFixedLenField("vin", b"", 17), + lambda p: p.payload_type in [3, 4]), + ConditionalField(XShortField("logical_address", 0), + lambda p: p.payload_type in [4]), + ConditionalField(StrFixedLenField("eid", b"", 6), + lambda p: p.payload_type in [2, 4]), + ConditionalField(StrFixedLenField("gid", b"", 6), + lambda p: p.payload_type in [4]), + ConditionalField(MayEnd(XByteEnumField("further_action", 0, { + 0x00: "No further action required", + 0x01: "Reserved by ISO 13400", 0x02: "Reserved by ISO 13400", + 0x03: "Reserved by ISO 13400", 0x04: "Reserved by ISO 13400", + 0x05: "Reserved by ISO 13400", 0x06: "Reserved by ISO 13400", + 0x07: "Reserved by ISO 13400", 0x08: "Reserved by ISO 13400", + 0x09: "Reserved by ISO 13400", 0x0a: "Reserved by ISO 13400", + 0x0b: "Reserved by ISO 13400", 0x0c: "Reserved by ISO 13400", + 0x0d: "Reserved by ISO 13400", 0x0e: "Reserved by ISO 13400", + 0x0f: "Reserved by ISO 13400", + 0x10: "Routing activation required to initiate central security", + })), lambda p: p.payload_type in [4]), + # VIN/GID sync. status is marked as optional, so the packet MayEnd + # on further_action + ConditionalField(XByteEnumField("vin_gid_status", 0, { + 0x00: "VIN and/or GID are synchronized", + 0x01: "Reserved by ISO 13400", 0x02: "Reserved by ISO 13400", + 0x03: "Reserved by ISO 13400", 0x04: "Reserved by ISO 13400", + 0x05: "Reserved by ISO 13400", 0x06: "Reserved by ISO 13400", + 0x07: "Reserved by ISO 13400", 0x08: "Reserved by ISO 13400", + 0x09: "Reserved by ISO 13400", 0x0a: "Reserved by ISO 13400", + 0x0b: "Reserved by ISO 13400", 0x0c: "Reserved by ISO 13400", + 0x0d: "Reserved by ISO 13400", 0x0e: "Reserved by ISO 13400", + 0x0f: "Reserved by ISO 13400", + 0x10: "Incomplete: VIN and GID are NOT synchronized" + }), lambda p: p.payload_type in [4]), + ConditionalField(XShortField("source_address", 0), + lambda p: p.payload_type in [5, 8, 0x8001, 0x8002, 0x8003]), # noqa: E501 + ConditionalField(XByteEnumField("activation_type", 0, { + 0: "Default", 1: "WWH-OBD", 0xe0: "Central security", + 0x16: "Default", 0x116: "Diagnostic", 0xe016: "Central security" + }), lambda p: p.payload_type in [5]), + ConditionalField(XShortField("logical_address_tester", 0), + lambda p: p.payload_type in [6]), + ConditionalField(XShortField("logical_address_doip_entity", 0), + lambda p: p.payload_type in [6]), + ConditionalField(XByteEnumField("routing_activation_response", 0, { + 0x00: "Routing activation denied due to unknown source address.", + 0x01: "Routing activation denied because all concurrently supported TCP_DATA sockets are registered and active.", # noqa: E501 + 0x02: "Routing activation denied because an SA different from the table connection entry was received on the already activated TCP_DATA socket.", # noqa: E501 + 0x03: "Routing activation denied because the SA is already registered and active on a different TCP_DATA socket.", # noqa: E501 + 0x04: "Routing activation denied due to missing authentication.", + 0x05: "Routing activation denied due to rejected confirmation.", + 0x06: "Routing activation denied due to unsupported routing activation type.", # noqa: E501 + 0x07: "Routing activation denied because the specified activation type requires a secure TLS TCP_DATA socket.", # noqa: E501 + 0x08: "Reserved by ISO 13400.", + 0x09: "Reserved by ISO 13400.", 0x0a: "Reserved by ISO 13400.", + 0x0b: "Reserved by ISO 13400.", 0x0c: "Reserved by ISO 13400.", + 0x0d: "Reserved by ISO 13400.", 0x0e: "Reserved by ISO 13400.", + 0x0f: "Reserved by ISO 13400.", + 0x10: "Routing successfully activated.", + 0x11: "Routing will be activated; confirmation required." + }), lambda p: p.payload_type in [6]), + ConditionalField(XIntField("reserved_iso", 0), + lambda p: p.payload_type in [5, 6]), + ConditionalField(XStrField("reserved_oem", b""), + lambda p: p.payload_type in [5, 6]), + ConditionalField(XByteEnumField("diagnostic_power_mode", 0, { + 0: "not ready", 1: "ready", 2: "not supported" + }), lambda p: p.payload_type in [0x4004]), + ConditionalField(ByteEnumField("node_type", 0, { + 0: "DoIP gateway", 1: "DoIP node" + }), lambda p: p.payload_type in [0x4002]), + ConditionalField(XByteField("max_open_sockets", 1), + lambda p: p.payload_type in [0x4002]), + ConditionalField(XByteField("cur_open_sockets", 0), + lambda p: p.payload_type in [0x4002]), + ConditionalField(IntField("max_data_size", 0), + lambda p: p.payload_type in [0x4002]), + ConditionalField(XShortField("target_address", 0), + lambda p: p.payload_type in [0x8001, 0x8002, 0x8003]), # noqa: E501 + ConditionalField(XByteEnumField("ack_code", 0, {0: "ACK"}), + lambda p: p.payload_type in [0x8002]), + ConditionalField(ByteEnumField("nack_code", 0, { + 0x00: "Reserved by ISO 13400", 0x01: "Reserved by ISO 13400", + 0x02: "Invalid source address", 0x03: "Unknown target address", + 0x04: "Diagnostic message too large", 0x05: "Out of memory", + 0x06: "Target unreachable", 0x07: "Unknown network", + 0x08: "Transport protocol error" + }), lambda p: p.payload_type in [0x8003]), + ConditionalField(XStrField("previous_msg", b""), + lambda p: p.payload_type in [0x8002, 0x8003]) + ] + + def answers(self, other): + # type: (Packet) -> int + """DEV: true if self is an answer from other""" + if isinstance(other, type(self)): + if self.payload_type == 0: + return 1 + + matches = [(4, 1), (4, 2), (4, 3), (6, 5), (8, 7), + (0x4002, 0x4001), (0x4004, 0x4003), + (0x8001, 0x8001), (0x8003, 0x8001)] + if (self.payload_type, other.payload_type) in matches: + if self.payload_type == 0x8001: + return self.payload.answers(other.payload) + return 1 + return 0 + + def hashret(self): + # type: () -> bytes + payload_type_mapping = { + 0x0000: b"\x01", + 0x0001: b"\x01", + 0x0002: b"\x01", + 0x0003: b"\x01", + 0x0004: b"\x01", + 0x0005: b"\x02", + 0x0006: b"\x02", + 0x0007: b"\x03", + 0x0008: b"\x03", + 0x4001: b"\x04", + 0x4002: b"\x04", + 0x4003: b"\x05", + 0x4004: b"\x05", + 0x8001: b"\x06", + 0x8002: b"\x06", + 0x8003: b"\x06", + } + + return payload_type_mapping.get(self.payload_type, b"\xff") + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + """ + This will set the Field 'payload_length' to the correct value. + """ + if self.payload_length is None: + pkt = pkt[:4] + struct.pack( + "!I", len(pay) + len(pkt) - 8) + pkt[8:] + return pkt + pay + + def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + if self.payload_type == 0x8001: + return s[:self.payload_length - 4], s[self.payload_length - 4:] + else: + return b"", s + + @classmethod + def tcp_reassemble(cls, data, metadata, session): + # type: (bytes, Dict[str, Any], Dict[str, Any]) -> Optional[Packet] + length = struct.unpack("!I", data[4:8])[0] + 8 + if len(data) >= length: + return DoIP(data) + return None + + +bind_bottom_up(UDP, DoIP, sport=13400) +bind_bottom_up(UDP, DoIP, dport=13400) +bind_layers(UDP, DoIP, sport=13400, dport=13400) + +bind_layers(TCP, DoIP, sport=13400) +bind_layers(TCP, DoIP, dport=13400) + +bind_layers(DoIP, UDS, payload_type=0x8001) + + +class DoIPSSLStreamSocket(SSLStreamSocket): + """Custom SSLStreamSocket for DoIP communication. + """ + + def __init__(self, sock, basecls=None): + # type: (socket.socket, Optional[Type[Packet]]) -> None + super(DoIPSSLStreamSocket, self).__init__(sock, basecls or DoIP) + self.buffer = b"" + + def recv(self, x=MTU, **kwargs): + # type: (Optional[int], **Any) -> Optional[Packet] + if len(self.buffer) < 8: + self.buffer += self.ins.recv(8) + if len(self.buffer) < 8: + return None + len_data = self.buffer[:8] + + len_int = struct.unpack(">I", len_data[4:8])[0] + len_int += 8 + + self.buffer += self.ins.recv(len_int - len(self.buffer)) + if len(self.buffer) < len_int: + return None + pktbuf = self.buffer[:len_int] + self.buffer = self.buffer[len_int:] + + pkt = self.basecls(pktbuf, **kwargs) # type: Packet + return pkt + + +class DoIPSocket(DoIPSSLStreamSocket): + """Socket for DoIP communication. This sockets automatically + sends a routing activation request as soon as a TCP or TLS connection is + established. + + :param ip: IP address of destination + :param port: destination port, usually 13400 + :param tls_port: destination port for TLS connection, usually 3496 + :param activate_routing: If true, routing activation request is + automatically sent + :param source_address: DoIP source address + :param target_address: DoIP target address, this is automatically + determined if routing activation request is sent + :param activation_type: This allows to set a different activation type for + the routing activation request + :param reserved_oem: Optional parameter to set value for reserved_oem field + of routing activation request + :param force_tls: Skip establishing of a TCP connection and directly try to + connect via SSL/TLS + :param context: Optional ssl.SSLContext object for initialization of ssl socket + connections. + :param doip_version: DoIP protocol version to use, default is 2 (ISO 13400-2012) + :param enforce_doip_version: If true, the protocol_version field in each DoIP + packet to be sent, is always set to the value of + doip_version. + + Example: + >>> socket = DoIPSocket("169.254.0.131") + >>> pkt = DoIP(payload_type=0x8001, source_address=0xe80, target_address=0x1000) / UDS() / UDS_RDBI(identifiers=[0x1000]) + >>> resp = socket.sr1(pkt, timeout=1) + """ # noqa: E501 + + def __init__(self, + ip='127.0.0.1', # type: str + port=13400, # type: int + tls_port=3496, # type: int + activate_routing=True, # type: bool + source_address=0xe80, # type: int + target_address=0, # type: int + activation_type=0, # type: int + reserved_oem=b"", # type: bytes + force_tls=False, # type: bool + context=None, # type: Optional[ssl.SSLContext] + doip_version=2, # type: int + enforce_doip_version=False, # type: bool + ): # type: (...) -> None + self.ip = ip + self.port = port + self.tls_port = tls_port + self.activate_routing = activate_routing + self.source_address = source_address + self.target_address = target_address + self.activation_type = activation_type + self.reserved_oem = reserved_oem + self.force_tls = force_tls + self.context = context + self.doip_version = doip_version + self.enforce_doip_version = enforce_doip_version + try: + self._init_socket() + except Exception: + self.close() + raise + + def _init_socket(self): + # type: () -> None + connected = False + addrinfo = socket.getaddrinfo(self.ip, self.port, proto=socket.IPPROTO_TCP) + sock_family = addrinfo[0][0] + + s = socket.socket(sock_family, socket.SOCK_STREAM) + s.settimeout(5) + s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + if not self.force_tls: + s.connect(addrinfo[0][-1]) + connected = True + DoIPSSLStreamSocket.__init__(self, s) + + if not self.activate_routing: + return + + activation_return = self._activate_routing() + else: + # Let's overwrite activation_return to force TLS Connection + activation_return = 0x07 + + if activation_return == 0x10: + # Routing successfully activated. + return + elif activation_return == 0x07: + # Routing activation denied because the specified activation + # type requires a secure TLS TCP_DATA socket. + if self.context is None: + raise ValueError("SSLContext 'context' can not be None") + if connected: + s.close() + s = socket.socket(sock_family, socket.SOCK_STREAM) + s.settimeout(5) + s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + ss = self.context.wrap_socket(s) + addrinfo = socket.getaddrinfo( + self.ip, self.tls_port, proto=socket.IPPROTO_TCP) + ss.connect(addrinfo[0][-1]) + DoIPSSLStreamSocket.__init__(self, ss) + + if not self.activate_routing: + return + + activation_return = self._activate_routing() + if activation_return == 0x10: + # Routing successfully activated. + return + else: + raise Exception( + "DoIPSocket activate_routing failed with " + "routing_activation_response 0x%x" % activation_return) + + elif activation_return == -1: + raise Exception("DoIPSocket._activate_routing failed") + else: + raise Exception( + "DoIPSocket activate_routing failed with " + "routing_activation_response 0x%x!" % activation_return) + + def _activate_routing(self): # type: (...) -> int + resp = self.sr1( + DoIP(payload_type=0x5, activation_type=self.activation_type, + source_address=self.source_address, reserved_oem=self.reserved_oem), + verbose=False, timeout=1) + if resp and resp.payload_type == 0x6 and \ + resp.routing_activation_response == 0x10: + self.target_address = ( + self.target_address or resp.logical_address_doip_entity) + log_automotive.info( + "Routing activation successful! Target address set to: 0x%x", + self.target_address) + else: + log_automotive.error( + "Routing activation failed! Response: %s", repr(resp)) + + if resp and resp.payload_type == 0x6: + return resp.routing_activation_response + else: + return -1 + + def send(self, x): # type: (Packet) -> int + if self.enforce_doip_version and isinstance(x, DoIP): + x[DoIP].protocol_version = self.doip_version + x[DoIP].inverse_version = 0xFF - self.doip_version + return super().send(x) + + +class UDS_DoIPSocket(DoIPSocket): + """ + Application-Layer socket for DoIP endpoints. This socket takes care about + the encapsulation of UDS packets into DoIP packets. + + Example: + >>> socket = UDS_DoIPSocket("169.254.117.238") + >>> pkt = UDS() / UDS_RDBI(identifiers=[0x1000]) + >>> resp = socket.sr1(pkt, timeout=1) + """ + + def send(self, x): + # type: (Union[Packet, bytes]) -> int + if isinstance(x, UDS): + pkt = DoIP(payload_type=0x8001, + source_address=self.source_address, + target_address=self.target_address + ) / x + else: + pkt = x + + try: + x.sent_time = time.time() # type: ignore + except AttributeError: + pass + + return super().send(pkt) + + def recv(self, x=MTU, **kwargs): + # type: (Optional[int], **Any) -> Optional[Packet] + pkt = super().recv(x, **kwargs) + if pkt and pkt.payload_type == 0x8001: + return pkt.payload + else: + return pkt + + pass diff --git a/scapy/contrib/automotive/ecu.py b/scapy/contrib/automotive/ecu.py index bca8c8ea188..7458468c95b 100644 --- a/scapy/contrib/automotive/ecu.py +++ b/scapy/contrib/automotive/ecu.py @@ -1,273 +1,632 @@ -#! /usr/bin/env python - +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license -# scapy.contrib.description = Helper class for tracking ECU states (ECU) +# scapy.contrib.description = Helper class for tracking Ecu states (Ecu) # scapy.contrib.status = loads import time import random +import copy +import itertools from collections import defaultdict +from types import GeneratorType +from threading import Lock +from scapy.compat import orb from scapy.packet import Raw, Packet from scapy.plist import PacketList -from scapy.error import Scapy_Exception from scapy.sessions import DefaultSession from scapy.ansmachine import AnsweringMachine +from scapy.supersocket import SuperSocket +from scapy.error import Scapy_Exception + +# Typing imports +from typing import ( + Any, + Union, + Iterable, + Callable, + List, + Optional, + Tuple, + Type, + cast, + Dict, +) + + +__all__ = ["EcuState", "Ecu", "EcuResponse", "EcuSession", + "EcuAnsweringMachine"] + + +class EcuState(object): + """ + Stores the state of an Ecu. The state is defined by a protocol, for + example UDS or GMLAN. + A EcuState supports comparison and serialization (command()). + """ + __slots__ = ["__dict__", "__cache__"] + + def __init__(self, **kwargs): + # type: (Any) -> None + self.__cache__ = None # type: Optional[Tuple[List[EcuState], List[Any]]] # noqa: E501 + for k, v in kwargs.items(): + if isinstance(v, GeneratorType): + v = list(v) + self.__setitem__(k, v) + + def _expand(self): + # type: () -> List[EcuState] + values = list(self.__dict__.values()) + keys = list(self.__dict__.keys()) + if self.__cache__ is None or self.__cache__[1] != values: + expanded = list() + for x in itertools.product(*[self._flatten(v) for v in values]): + kwargs = {} + for i, k in enumerate(keys): + if x[i] is None: + continue + kwargs[k] = x[i] + expanded.append(EcuState(**kwargs)) + self.__cache__ = (expanded, values) + return self.__cache__[0] + + @staticmethod + def _flatten(x): + # type: (Any) -> List[Any] + if isinstance(x, (str, bytes)): + return [x] + elif hasattr(x, "__iter__") and hasattr(x, "__len__") and len(x) == 1: + return list(*x) + elif not hasattr(x, "__iter__"): + return [x] + flattened = list() + for y in x: + if hasattr(x, "__iter__"): + flattened += EcuState._flatten(y) + else: + flattened += [y] + return flattened + + def __delitem__(self, key): + # type: (str) -> None + self.__cache__ = None + del self.__dict__[key] + + def __len__(self): + # type: () -> int + return len(self.__dict__.keys()) + + def __getitem__(self, item): + # type: (str) -> Any + return self.__dict__[item] + + def __setitem__(self, key, value): + # type: (str, Any) -> None + self.__cache__ = None + self.__dict__[key] = value + + def __repr__(self): + # type: () -> str + return "".join(str(k) + str(v) for k, v in + sorted(self.__dict__.items(), key=lambda t: t[0])) + + def __eq__(self, other): + # type: (object) -> bool + other = cast(EcuState, other) + if len(self.__dict__) != len(other.__dict__): + return False + try: + return all(self.__dict__[k] == other.__dict__[k] + for k in self.__dict__.keys()) + except KeyError: + return False + + def __contains__(self, item): + # type: (EcuState) -> bool + if not isinstance(item, EcuState): + return False + return all(s in self._expand() for s in item._expand()) + + def __ne__(self, other): + # type: (object) -> bool + return not other == self + + def __lt__(self, other): + # type: (EcuState) -> bool + if self == other: + return False + + if len(self) < len(other): + return True + + if len(self) > len(other): + return False + + common = set(self.__dict__.keys()).intersection( + set(other.__dict__.keys())) -__all__ = ["ECU", "ECUResponse", "ECUSession", "ECU_am"] - - -class ECU(object): - """A ECU object can be used to - - track the states of an ECU. - - to log all modification to an ECU - - to extract supported responses of a real ECU - - Usage: - >>> print("This ecu logs, tracks and creates supported responses") - >>> my_virtual_ecu = ECU() - >>> my_virtual_ecu.update(PacketList([...])) - >>> my_virtual_ecu.supported_responses - >>> print("Another ecu just tracks") - >>> my_tracking_ecu = ECU(logging=False, store_supported_responses=False) # noqa: E501 - >>> my_tracking_ecu.update(PacketList([...])) - >>> print("Another ecu just logs all modifications to it") - >>> my_logging_ecu = ECU(verbose=False, store_supported_responses=False) # noqa: E501 - >>> my_logging_ecu.update(PacketList([...])) - >>> my_logging_ecu.log - >>> print("Another ecu just creates supported responses") - >>> my_response_ecu = ECU(verbose=False, logging=False) - >>> my_response_ecu.update(PacketList([...])) - >>> my_response_ecu.supported_responses - """ - def __init__(self, init_session=None, init_security_level=None, - init_communication_control=None, logging=True, verbose=True, - store_supported_responses=True): + for k in sorted(common): + if not isinstance(other.__dict__[k], type(self.__dict__[k])): + raise TypeError( + "Can't compare %s with %s for the EcuState element %s" % + (type(self.__dict__[k]), type(other.__dict__[k]), k)) + if self.__dict__[k] < other.__dict__[k]: + return True + if self.__dict__[k] > other.__dict__[k]: + return False + + if len(common) < len(self.__dict__): + self_diffs = set(self.__dict__.keys()).difference( + set(other.__dict__.keys())) + other_diffs = set(other.__dict__.keys()).difference( + set(self.__dict__.keys())) + + for s, o in zip(self_diffs, other_diffs): + if s < o: + return True + + return False + + raise TypeError("EcuStates should be identical. Something bad happen. " + "self: %s other: %s" % (self.__dict__, other.__dict__)) + + def __hash__(self): + # type: () -> int + return hash(repr(self)) + + def reset(self): + # type: () -> None + self.__cache__ = None + keys = list(self.__dict__.keys()) + for k in keys: + del self.__dict__[k] + + def command(self): + # type: () -> str + return "EcuState(" + ", ".join( + ["%s=%s" % (k, repr(v)) for k, v in sorted( + self.__dict__.items(), key=lambda t: t[0])]) + ")" + + @staticmethod + def extend_pkt_with_modifier(cls): + # type: (Type[Packet]) -> Callable[[Callable[[Packet, Packet, EcuState], None]], None] # noqa: E501 + """ + Decorator to add a function as 'modify_ecu_state' method to a given + class. This allows dynamic modifications and additions to a protocol. + :param cls: A packet class to be modified + :return: Decorator function + """ + if len(cls.fields_desc) == 0: + raise Scapy_Exception("Packets without fields can't be extended.") + + if hasattr(cls, "modify_ecu_state"): + raise Scapy_Exception( + "Class already extended. Can't override existing method.") + + def decorator_function(f): + # type: (Callable[[Packet, Packet, EcuState], None]) -> None + setattr(cls, "modify_ecu_state", f) + + return decorator_function + + @staticmethod + def is_modifier_pkt(pkt): + # type: (Packet) -> bool """ - Initialize an ECU object - - :param init_session: An initial session - :param init_security_level: An initial security level - :param init_communication_control: An initial communication control - setting - :param logging: Turn logging on or off. Default is on. - :param verbose: Turn tracking on or off. Default is on. - :param store_supported_responses: Turn creation of supported responses - on or off. Default is on. + Helper function to determine if a Packet contains a layer that + modifies the EcuState. + :param pkt: Packet to be analyzed + :return: True if pkt contains layer that implements modify_ecu_state """ - self.current_session = init_session or 1 - self.current_security_level = init_security_level or 0 - self.communication_control = init_communication_control or 0 + return any(hasattr(layer, "modify_ecu_state") + for layer in pkt.layers()) + + @staticmethod + def get_modified_ecu_state(response, request, state, modify_in_place=False): # noqa: E501 + # type: (Packet, Packet, EcuState, bool) -> EcuState + """ + Helper function to get a modified EcuState from a Packet and a + previous EcuState. An EcuState is always modified after a response + Packet is received. In some protocols, the belonging request packet + is necessary to determine the precise state of the Ecu + + :param response: Response packet that supports `modify_ecu_state` + :param request: Belonging request of the response that modifies Ecu + :param state: The previous/current EcuState + :param modify_in_place: If True, the given EcuState will be modified + :return: The modified EcuState or a modified copy + """ + if modify_in_place: + new_state = state + else: + new_state = copy.copy(state) + + for layer in response.layers(): + if not hasattr(layer, "modify_ecu_state"): + continue + try: + layer.modify_ecu_state(response, request, new_state) + except TypeError: + layer.modify_ecu_state.im_func(response, request, new_state) + return new_state + + +class Ecu(object): + """An Ecu object can be used to + * track the states of an Ecu. + * to log all modification to an Ecu. + * to extract supported responses of a real Ecu. + + Example: + >>> print("This ecu logs, tracks and creates supported responses") + >>> my_virtual_ecu = Ecu() + >>> my_virtual_ecu.update(PacketList([...])) + >>> my_virtual_ecu.supported_responses + >>> print("Another ecu just tracks") + >>> my_tracking_ecu = Ecu(logging=False, store_supported_responses=False) + >>> my_tracking_ecu.update(PacketList([...])) + >>> print("Another ecu just logs all modifications to it") + >>> my_logging_ecu = Ecu(verbose=False, store_supported_responses=False) + >>> my_logging_ecu.update(PacketList([...])) + >>> my_logging_ecu.log + >>> print("Another ecu just creates supported responses") + >>> my_response_ecu = Ecu(verbose=False, logging=False) + >>> my_response_ecu.update(PacketList([...])) + >>> my_response_ecu.supported_responses + + Parameters to initialize an Ecu object + + :param logging: Turn logging on or off. Default is on. + :param verbose: Turn tracking on or off. Default is on. + :param store_supported_responses: Create a list of supported responses if True. + :param lookahead: Configuration for lookahead when computing supported responses + """ # noqa: E501 + def __init__(self, logging=True, verbose=True, + store_supported_responses=True, lookahead=10): + # type: (bool, bool, bool, int) -> None + self.state = EcuState() self.verbose = verbose self.logging = logging self.store_supported_responses = store_supported_responses - self.log = defaultdict(list) - self._supported_responses = list() - self._unanswered_packets = PacketList() + self.lookahead = lookahead + self.log = defaultdict(list) # type: Dict[str, List[Any]] + self.__supported_responses = list() # type: List[EcuResponse] + self.__unanswered_packets = PacketList() def reset(self): - self.current_session = 1 - self.current_security_level = 0 - self.communication_control = 0 + # type: () -> None + """ + Resets the internal state to a default EcuState. + """ + self.state = EcuState(session=1) def update(self, p): + # type: (Union[Packet, PacketList]) -> None + """ + Processes a Packet or a list of Packets, according to the chosen + configuration. + :param p: Packet or list of Packets + """ if isinstance(p, PacketList): for pkt in p: - self._update(pkt) + self.update(pkt) elif not isinstance(p, Packet): - raise Scapy_Exception("Provide a Packet object for an update") + raise TypeError("Provide a Packet object for an update") else: - self._update(p) + self.__update(p) - def _update(self, pkt): + def __update(self, pkt): + # type: (Packet) -> None + """ + Processes a Packet according to the chosen configuration. + :param pkt: Packet to be processed + """ if self.verbose: print(repr(self), repr(pkt)) - if self.store_supported_responses: - self._update_supported_responses(pkt) if self.logging: - self._update_log(pkt) - self._update_internal_state(pkt) - - def _update_log(self, pkt): - for l in pkt.layers(): - if hasattr(l, "get_log"): - log_key, log_value = l.get_log(pkt) - self.log[log_key].append((pkt.time, log_value)) - - def _update_internal_state(self, pkt): - for l in pkt.layers(): - if hasattr(l, "modifies_ecu_state"): - l.modifies_ecu_state(pkt, self) - - def _update_supported_responses(self, pkt): - self._unanswered_packets += PacketList([pkt]) - answered, unanswered = self._unanswered_packets.sr() - for _, resp in answered: - ecu_resp = ECUResponse(session=self.current_session, - security_level=self.current_security_level, - responses=resp) - - if ecu_resp not in self._supported_responses: - if self.verbose: - print("[+] ", repr(ecu_resp)) - self._supported_responses.append(ecu_resp) - else: - if self.verbose: - print("[-] ", repr(ecu_resp)) - self._unanswered_packets = unanswered + self.__update_log(pkt) + self.__update_supported_responses(pkt) + + def __update_log(self, pkt): + # type: (Packet) -> None + """ + Checks if a packet or a layer of this packet supports the function + `get_log`. If `get_log` is supported, this function will be executed + and the returned log information is stored in the intern log of this + Ecu object. + :param pkt: A Packet to be processed for log information. + """ + for layer in pkt.layers(): + if not hasattr(layer, "get_log"): + continue + try: + log_key, log_value = layer.get_log(pkt) + except TypeError: + log_key, log_value = layer.get_log.im_func(pkt) + + self.log[log_key].append((pkt.time, log_value)) + + def __update_supported_responses(self, pkt): + # type: (Packet) -> None + """ + Stores a given packet as supported response, if a matching request + packet is found in a list of the latest unanswered packets. For + performance improvements, this list of unanswered packets only contains + a fixed number of packets, defined by the `lookahead` parameter of + this Ecu. + :param pkt: Packet to be processed. + """ + self.__unanswered_packets.append(pkt) + reduced_plist = self.__unanswered_packets[-self.lookahead:] + answered, unanswered = reduced_plist.sr(lookahead=self.lookahead) + self.__unanswered_packets = unanswered + + for req, resp in answered: + added = False + current_state = copy.copy(self.state) + EcuState.get_modified_ecu_state(resp, req, self.state, True) + + if not self.store_supported_responses: + continue + + for sup_resp in self.__supported_responses: + if resp == sup_resp.key_response: + if sup_resp.states is not None and \ + self.state not in sup_resp.states: + sup_resp.states.append(current_state) + added = True + break + + if added: + continue + + ecu_resp = EcuResponse(current_state, responses=resp) + if self.verbose: + print("[+] ", repr(ecu_resp)) + self.__supported_responses.append(ecu_resp) + + @staticmethod + def sort_key_func(resp): + # type: (EcuResponse) -> Tuple[bool, int, int, int] + """ + This sorts responses in the following order: + 1. Positive responses first + 2. Lower ServiceIDs first + 3. Less supported states first + 4. Longer (more specific) responses first + :param resp: EcuResponse to be sorted + :return: Tuple as sort key + """ + first_layer = cast(Packet, resp.key_response[0]) # type: ignore + service = orb(bytes(first_layer)[0]) + return (service == 0x7f, + service, + 0xffffffff - len(resp.states or []), + 0xffffffff - len(resp.key_response)) @property def supported_responses(self): - # This sorts responses in the following order: - # 1. Positive responses first - # 2. Lower ServiceID first - # 3. Longer (more specific) responses first - self._supported_responses.sort( - key=lambda x: (x.responses[0].service == 0x7f, - x.responses[0].service, - 0xffffffff - len(x.responses[0]))) - return self._supported_responses + # type: () -> List[EcuResponse] + """ + Returns a sorted list of supported responses. The sort is done in a way + to provide the best possible results, if this list of supported + responses is used to simulate an real world Ecu with the + EcuAnsweringMachine object. + :return: A sorted list of EcuResponse objects + """ + self.__supported_responses.sort(key=self.sort_key_func) + return self.__supported_responses @property def unanswered_packets(self): - return self._unanswered_packets + # type: () -> PacketList + """ + A list of all unanswered packets, which were processed by this Ecu + object. + :return: PacketList of unanswered packets + """ + return self.__unanswered_packets def __repr__(self): - return "ses: %03d sec: %03d cc: %d" % (self.current_session, - self.current_security_level, - self.communication_control) + # type: () -> str + return repr(self.state) + @staticmethod + def extend_pkt_with_logging(cls): + # type: (Type[Packet]) -> Callable[[Callable[[Packet], Tuple[str, Any]]], None] # noqa: E501 + """ + Decorator to add a function as 'get_log' method to a given + class. This allows dynamic modifications and additions to a protocol. + :param cls: A packet class to be modified + :return: Decorator function + """ -class ECUSession(DefaultSession): - """Tracks modification to an ECU 'on-the-flow'. + def decorator_function(f): + # type: (Callable[[Packet], Tuple[str, Any]]) -> None + setattr(cls, "get_log", f) - Usage: - >>> sniff(session=ECUSession) + return decorator_function + + +class EcuSession(DefaultSession): """ + Tracks modification to an Ecu object 'on-the-flow'. + + The parameters for the internal Ecu object are obtained from the kwargs + dict. + `logging`: Turn logging on or off. Default is on. + `verbose`: Turn tracking on or off. Default is on. + `store_supported_responses`: Create a list of supported responses, if True. + + Example: + >>> sniff(session=EcuSession) + + """ def __init__(self, *args, **kwargs): - DefaultSession.__init__(self, *args, **kwargs) - self.ecu = ECU(init_session=kwargs.pop("init_session", None), - init_security_level=kwargs.pop("init_security_level", None), # noqa: E501 - init_communication_control=kwargs.pop("init_communication_control", None), # noqa: E501 - logging=kwargs.pop("logging", True), + # type: (Any, Any) -> None + self.ecu = Ecu(logging=kwargs.pop("logging", True), verbose=kwargs.pop("verbose", True), store_supported_responses=kwargs.pop("store_supported_responses", True)) # noqa: E501 + super(EcuSession, self).__init__(*args, **kwargs) - def on_packet_received(self, pkt): + def process(self, pkt: Packet) -> Optional[Packet]: if not pkt: - return - if isinstance(pkt, list): - for p in pkt: - ECUSession.on_packet_received(self, p) - return + return None self.ecu.update(pkt) - DefaultSession.on_packet_received(self, pkt) - - -class ECUResponse: - """Encapsulates a response and the according ECU state. - A list of this objects can be used to configure a ECU Answering Machine. - This is useful, if you want to clone the behaviour of a real ECU on a bus. + return pkt + + +class EcuResponse: + """Encapsulates responses and the according EcuStates. + A list of this objects can be used to configure an EcuAnsweringMachine. + This is useful, if you want to clone the behaviour of a real Ecu. + + Example: + >>> EcuResponse(EcuState(session=2, security_level=2), responses=UDS()/UDS_RDBIPR(dataIdentifier=2)/Raw(b"deadbeef1")) + >>> EcuResponse([EcuState(session=range(2, 5), security_level=2), EcuState(session=3, security_level=5)], responses=UDS()/UDS_RDBIPR(dataIdentifier=9)/Raw(b"deadbeef4")) + + Initialize an EcuResponse capsule + + :param state: EcuState or list of EcuStates in which this response + is allowed to be sent. If no state provided, the response + packet will always be send. + :param responses: A Packet or a list of Packet objects. By default the + last packet is asked if it answers an incoming + packet. This allows to send for example + `requestCorrectlyReceived-ResponsePending` packets. + :param answers: Optional argument to provide a custom answer here: + `lambda resp, req: return resp.answers(req)` + This allows the modification of a response depending + on a request. Custom SecurityAccess mechanisms can + be implemented in this way or generic NegativeResponse + messages which answers to everything can be realized + in this way. + """ # noqa: E501 + def __init__(self, state=None, responses=Raw(b"\x7f\x10"), answers=None): + # type: (Optional[Union[EcuState, Iterable[EcuState]]], Union[Iterable[Packet], PacketList, Packet], Optional[Callable[[Packet, Packet], bool]]) -> None # noqa: E501 + if state is None: + self.__states = None # type: Optional[List[EcuState]] + else: + if hasattr(state, "__iter__"): + state = cast(List[EcuState], state) + self.__states = state + else: + self.__states = [state] - Usage: - >>> print("Generates a ECUResponse which answers on UDS()/UDS_RDBI(identifiers=[2]) if ECU is in session 2 and has security_level 2") # noqa: E501 - >>> ECUResponse(session=2, security_level=2, responses=UDS()/UDS_RDBIPR(dataIdentifier=2)/Raw(b"deadbeef1")) # noqa: E501 - >>> print("Further examples") - >>> ECUResponse(session=range(3,5), security_level=[3,4], responses=UDS()/UDS_RDBIPR(dataIdentifier=3)/Raw(b"deadbeef2")) # noqa: E501 - >>> ECUResponse(session=[5,6,7], security_level=range(5,7), responses=UDS()/UDS_RDBIPR(dataIdentifier=5)/Raw(b"deadbeef3")) # noqa: E501 - >>> ECUResponse(session=lambda x: 8 < x <= 10, security_level=lambda x: x > 10, responses=UDS()/UDS_RDBIPR(dataIdentifier=9)/Raw(b"deadbeef4")) # noqa: E501 - """ - def __init__(self, session=1, security_level=0, - responses=Raw(b"\x7f\x10"), - answers=None): - """ - Initialize an ECUResponse capsule - - :param session: Defines the session in which this response is valid. - A integer, a callable or any iterable object can be - provided. - :param security_level: Defines the security_level in which this - response is valid. A integer, a callable or any - iterable object can be provided. - :param responses: A Packet or a list of Packet objects. By default the - last packet is asked if it answers a incoming packet. - This allows to send for example - `requestCorrectlyReceived-ResponsePending` packets. - :param answers: Optional argument to provide a custom answer here: - `lambda resp, req: return resp.answers(req)` - This allows the modification of a response depending - on a request. Custom SecurityAccess mechanisms can - be implemented in this way or generic NegativeResponse - messages which answers to everything can be realized - in this way. - """ - self.__session = session \ - if hasattr(session, "__iter__") or callable(session) else [session] - self.__security_level = security_level \ - if hasattr(security_level, "__iter__") or callable(security_level)\ - else [security_level] if isinstance(responses, PacketList): - self.responses = responses + self.__responses = responses # type: PacketList elif isinstance(responses, Packet): - self.responses = PacketList([responses]) + self.__responses = PacketList([responses]) elif hasattr(responses, "__iter__"): - self.responses = PacketList(responses) + responses = cast(List[Packet], responses) + self.__responses = PacketList(responses) else: - self.responses = PacketList([responses]) + raise TypeError( + "Can't handle type %s as response" % type(responses)) self.__custom_answers = answers - def in_correct_session(self, current_session): - if callable(self.__session): - return self.__session(current_session) - else: - return current_session in self.__session + @property + def states(self): + # type: () -> Optional[List[EcuState]] + return self.__states - def has_security_access(self, current_security_level): - if callable(self.__security_level): - return self.__security_level(current_security_level) + @property + def responses(self): + # type: () -> PacketList + return self.__responses + + @property + def key_response(self): + # type: () -> Packet + pkt = self.__responses[-1] # type: Packet + return pkt + + def supports_state(self, state): + # type: (EcuState) -> bool + if self.__states is None or len(self.__states) == 0: + return True else: - return current_security_level in self.__security_level + return any(s == state or state in s for s in self.__states) def answers(self, other): + # type: (Packet) -> Union[int, bool] if self.__custom_answers is not None: - return self.__custom_answers(self.responses[-1], other) + return self.__custom_answers(self.key_response, other) else: - return self.responses[-1].answers(other) + return self.key_response.answers(other) def __repr__(self): - return "session=%s, security_level=%s, responses=%s" % \ - (self.__session, self.__security_level, - [resp.summary() for resp in self.responses]) + # type: () -> str + return "%s, responses=%s" % \ + (repr(self.__states), + [resp.summary() for resp in self.__responses]) def __eq__(self, other): - return \ - self.__class__ == other.__class__ and \ - self.__session == other.__session and \ - self.__security_level == other.__security_level and \ + # type: (object) -> bool + other = cast(EcuResponse, other) + + responses_equal = \ len(self.responses) == len(other.responses) and \ all(bytes(x) == bytes(y) for x, y in zip(self.responses, other.responses)) + if self.__states is None: + return responses_equal + else: + return any(other.supports_state(s) for s in self.__states) and \ + responses_equal def __ne__(self, other): + # type: (object) -> bool # Python 2.7 compat return not self == other - __hash__ = None + def command(self): + # type: () -> str + if self.__states is not None: + return "EcuResponse(%s, responses=%s)" % ( + "[" + ", ".join(s.command() for s in self.__states) + "]", + "[" + ", ".join(p.command() for p in self.__responses) + "]") + else: + return "EcuResponse(responses=%s)" % "[" + ", ".join( + p.command() for p in self.__responses) + "]" + + __hash__ = None # type: ignore -class ECU_am(AnsweringMachine): +class EcuAnsweringMachine(AnsweringMachine[PacketList]): """AnsweringMachine which emulates the basic behaviour of a real world ECU. - Provide a list of ``ECUResponse`` objects to configure the behaviour of this + Provide a list of ``EcuResponse`` objects to configure the behaviour of a AnsweringMachine. - :param supported_responses: List of ``ECUResponse`` objects to define + Usage: + >>> resp = EcuResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10)) + >>> sock = ISOTPSocket(can_iface, tx_id=0x700, rx_id=0x600, basecls=UDS) + >>> answering_machine = EcuAnsweringMachine(supported_responses=[resp], main_socket=sock, basecls=UDS) + >>> sim = threading.Thread(target=answering_machine, kwargs={'count': 4, 'timeout':5}) + >>> sim.start() + """ # noqa: E501 + function_name = "EcuAnsweringMachine" + sniff_options_list = ["store", "opened_socket", "count", "filter", "prn", + "stop_filter", "timeout"] + + def parse_options( + self, + supported_responses=None, # type: Optional[List[EcuResponse]] + main_socket=None, # type: Optional[SuperSocket] + broadcast_socket=None, # type: Optional[SuperSocket] + basecls=Raw, # type: Type[Packet] + timeout=None, # type: Optional[Union[int, float]] + initial_ecu_state=None # type: Optional[EcuState] + ): + # type: (...) -> None + """ + :param supported_responses: List of ``EcuResponse`` objects to define the behaviour. The default response is ``generalReject``. :param main_socket: Defines the object of the socket to send @@ -276,68 +635,85 @@ class ECU_am(AnsweringMachine): Listen-only, responds with the main_socket. `None` to disable broadcast capabilities. :param basecls: Provide a basecls of the used protocol - - Usage: - >>> resp = ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10)) # noqa: E501 - >>> sock = ISOTPSocket(can_iface, sid=0x700, did=0x600, basecls=UDS) # noqa: E501 - >>> answering_machine = ECU_am(supported_responses=[resp], main_socket=sock, basecls=UDS) # noqa: E501 - >>> sim = threading.Thread(target=answering_machine, kwargs={'count': 4, 'timeout':5}) # noqa: E501 - >>> sim.start() - """ - function_name = "ECU_am" - sniff_options_list = ["store", "opened_socket", "count", "filter", "prn", "stop_filter", "timeout"] # noqa: E501 - - def parse_options(self, supported_responses=None, - main_socket=None, broadcast_socket=None, basecls=Raw, - timeout=None): - self.main_socket = main_socket - self.sockets = [self.main_socket] + :param timeout: Specifies the timeout for sniffing in seconds. + """ + self._main_socket = main_socket # type: Optional[SuperSocket] + self._sockets = [self._main_socket] if broadcast_socket is not None: - self.sockets.append(broadcast_socket) + self._sockets.append(broadcast_socket) + + self._initial_ecu_state = initial_ecu_state or EcuState(session=1) + self._ecu_state_mutex = Lock() + self._ecu_state = copy.copy(self._initial_ecu_state) - self.ecu_state = ECU(logging=False, verbose=False, - store_supported_responses=False) - self.basecls = basecls - self.supported_responses = supported_responses + self._basecls = basecls # type: Type[Packet] + self._supported_responses = supported_responses self.sniff_options["timeout"] = timeout - self.sniff_options["opened_socket"] = self.sockets + self.sniff_options["opened_socket"] = self._sockets - def is_request(self, req): - return req.__class__ == self.basecls + @property + def state(self): + # type: () -> EcuState + return self._ecu_state - def print_reply(self, req, reply): - print("%s ==> %s" % (req.summary(), [res.summary() for res in reply])) + def reset_state(self): + # type: () -> None + with self._ecu_state_mutex: + self._ecu_state = copy.copy(self._initial_ecu_state) - def make_reply(self, req): - if self.supported_responses is not None: - for resp in self.supported_responses: - if not isinstance(resp, ECUResponse): - raise Scapy_Exception("Unsupported type for response. " - "Please use `ECUResponse` objects. ") + def is_request(self, req): + # type: (Packet) -> bool + return isinstance(req, self._basecls) - if not resp.in_correct_session(self.ecu_state.current_session): - continue + def make_reply(self, req): + # type: (Packet) -> PacketList + """ + Checks if a given request can be answered by the internal list of + EcuResponses. First, it's evaluated if the internal EcuState of this + AnsweringMachine is supported by an EcuResponse, next it's evaluated if + a request answers the key_response of this EcuResponse object. The + first fitting EcuResponse is used. If this EcuResponse modified the + EcuState, the internal EcuState of this AnsweringMachine is updated, + and the list of response Packets of the selected EcuResponse is + returned. If no EcuResponse if found, a PacketList with a generic + NegativeResponse is returned. + :param req: A request packet + :return: A list of response packets + """ + if self._supported_responses is not None: + for resp in self._supported_responses: + if not isinstance(resp, EcuResponse): + raise TypeError("Unsupported type for response. " + "Please use `EcuResponse` objects.") - if not resp.has_security_access( - self.ecu_state.current_security_level): - continue + with self._ecu_state_mutex: + if not resp.supports_state(self._ecu_state): + continue - if not resp.answers(req): - continue + if not resp.answers(req): + continue - for r in resp.responses: - for l in r.layers(): - if hasattr(l, "modifies_ecu_state"): - l.modifies_ecu_state(r, self.ecu_state) + EcuState.get_modified_ecu_state( + resp.key_response, req, self._ecu_state, True) - return resp.responses + return resp.responses - return PacketList([self.basecls(b"\x7f" + bytes(req)[0:1] + b"\x10")]) + return PacketList([self._basecls( + b"\x7f" + bytes(req)[0:1] + b"\x10")]) - def send_reply(self, reply): + def send_reply(self, reply, send_function=None): + # type: (PacketList, Optional[Any]) -> None + """ + Sends all Packets of a EcuResponse object. This allows to send multiple + packets up on a request. If the list contains more than one packet, + a random time between each packet is waited until the next packet will + be sent. + :param reply: List of packets to be sent. + """ for p in reply: if len(reply) > 1: time.sleep(random.uniform(0.01, 0.5)) - self.main_socket.send(p) + if self._main_socket: + self._main_socket.send(p) diff --git a/scapy/contrib/automotive/gm/__init__.py b/scapy/contrib/automotive/gm/__init__.py index 3e4219ec566..b502719fb27 100644 --- a/scapy/contrib/automotive/gm/__init__.py +++ b/scapy/contrib/automotive/gm/__init__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 70513ba2b3c..62c88b208d7 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -1,22 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss # Copyright (C) Enrico Pozzobon -# This program is published under a GPLv2 license # scapy.contrib.description = General Motors Local Area Network (GMLAN) # scapy.contrib.status = loads import struct -from scapy.fields import ObservableDict, XByteEnumField, ByteEnumField, \ - ConditionalField, XByteField, StrField, XShortEnumField, XShortField, \ - X3BytesField, XIntField, ShortField, PacketField, PacketListField, \ - FieldListField + +from scapy.contrib.automotive import log_automotive +from scapy.fields import ( + ByteEnumField, + ConditionalField, + FieldListField, + MayEnd, + MultipleTypeField, + ObservableDict, + PacketField, + PacketListField, + ShortField, + StrField, + StrFixedLenField, + X3BytesField, + XByteEnumField, + XByteField, + XIntField, + XShortEnumField, + XShortField, +) from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf -from scapy.error import warning, log_loading from scapy.contrib.isotp import ISOTP +from scapy.compat import orb +from typing import ( # noqa: F401 + Dict, + Type, +) """ GMLAN @@ -26,26 +47,55 @@ if conf.contribs['GMLAN']['treat-response-pending-as-answer']: pass except KeyError: - log_loading.info("Specify \"conf.contribs['GMLAN'] = " - "{'treat-response-pending-as-answer': True}\" to treat " - "a negative response 'RequestCorrectlyReceived-" - "ResponsePending' as answer of a request. \n" - "The default value is False.") - conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False} + # log_automotive.info("Specify \"conf.contribs['GMLAN'] = " + # "{'treat-response-pending-as-answer': True}\" to treat " + # "a negative response 'RequestCorrectlyReceived-" + # "ResponsePending' as answer of a request. \n" + # "The default value is False.") + conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None +def _gmlan_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['GMLAN']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`GMLAN` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`GMLAN` packet, preventing a duplicate + service byte when sub-packets are stacked (``GMLAN()/GMLAN_IDO()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + """ + if not conf.contribs['GMLAN'].get('single_layer_mode', False): + return False + if conf.contribs['GMLAN'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, GMLAN) + return True + + class GMLAN(ISOTP): @staticmethod def determine_len(x): if conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] is None: - warning("Define conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme']! " # noqa: E501 - "Assign either 2,3 or 4") + log_automotive.warning( + "Define conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme']! " + "Assign either 2,3 or 4") if conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] \ not in [2, 3, 4]: - warning("Define conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme']! " # noqa: E501 - "Assign either 2,3 or 4") + log_automotive.warning( + "Define conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme']! " + "Assign either 2,3 or 4") return conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] == x services = ObservableDict( @@ -96,7 +146,7 @@ def determine_len(x): ] def answers(self, other): - if other.__class__ != self.__class__: + if not isinstance(other, type(self)): return False if self.service == 0x7f: return self.payload.answers(other) @@ -110,21 +160,19 @@ def answers(self, other): def hashret(self): if self.service == 0x7f: - return struct.pack('B', self.requestServiceId) + return struct.pack('B', self.requestServiceId & ~0x40) return struct.pack('B', self.service & ~0x40) - @staticmethod - def modifies_ecu_state(pkt, ecu): - if pkt.service == 0x50: - ecu.current_session = 3 - elif pkt.service == 0x60: - ecu.current_session = 1 - ecu.communication_control = 0 - ecu.current_security_level = 0 - elif pkt.service == 0x68: - ecu.communication_control = 1 - elif pkt.service == 0xe5: - ecu.current_session = 2 + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct GMLAN service class in single layer mode.""" + if conf.contribs['GMLAN'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls # ########################IDO################################### @@ -135,16 +183,13 @@ class GMLAN_IDO(Packet): 0x04: 'wakeUpLinks'} name = 'InitiateDiagnosticOperation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x10, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_IDO.subfunction%") - bind_layers(GMLAN, GMLAN_IDO, service=0x10) +GMLAN._service_cls[0x10] = GMLAN_IDO # ########################RFRD################################### @@ -167,37 +212,31 @@ class GMLAN_RFRD(Packet): 0x02: 'readFailureRecordParameters'} name = 'ReadFailureRecordData' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x12, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), - ConditionalField(PacketField("dtc", b'', GMLAN_DTC), + ConditionalField(PacketField("dtc", None, GMLAN_DTC), lambda pkt: pkt.subfunction == 0x02) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RFRD.subfunction%") - bind_layers(GMLAN, GMLAN_RFRD, service=0x12) +GMLAN._service_cls[0x12] = GMLAN_RFRD class GMLAN_RFRDPR(Packet): name = 'ReadFailureRecordDataPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x52, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, GMLAN_RFRD.subfunctions) ] def answers(self, other): - return other.__class__ == GMLAN_RFRD and \ + return isinstance(other, GMLAN_RFRD) and \ other.subfunction == self.subfunction - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RFRDPR.subfunction%") - bind_layers(GMLAN, GMLAN_RFRDPR, service=0x52) +GMLAN._service_cls[0x52] = GMLAN_RFRDPR class GMLAN_RFRDPR_RFRI(Packet): @@ -219,7 +258,7 @@ class GMLAN_RFRDPR_RFRI(Packet): class GMLAN_RFRDPR_RFRP(Packet): name = 'ReadFailureRecordDataPositiveResponse_readFailureRecordParameters' fields_desc = [ - PacketField("dtc", b'', GMLAN_DTC) + PacketField("dtc", None, GMLAN_DTC) ] @@ -315,36 +354,29 @@ class GMLAN_RDBI(Packet): name = 'ReadDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x1a, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, dataIdentifiers) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RDBI.dataIdentifier%") - -bind_layers(GMLAN, GMLAN_RDBI, service=0x1A) +bind_layers(GMLAN, GMLAN_RDBI, service=0x1a) +GMLAN._service_cls[0x1a] = GMLAN_RDBI class GMLAN_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x5a, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.sprintf("%GMLAN_RDBIPR.dataIdentifier%"), - bytes(pkt[1].payload)) - def answers(self, other): - return other.__class__ == GMLAN_RDBI and \ + return isinstance(other, GMLAN_RDBI) and \ other.dataIdentifier == self.dataIdentifier -bind_layers(GMLAN, GMLAN_RDBIPR, service=0x5A) +bind_layers(GMLAN, GMLAN_RDBIPR, service=0x5a) +GMLAN._service_cls[0x5a] = GMLAN_RDBIPR # ########################RDBI################################### @@ -356,37 +388,31 @@ class GMLAN_RDBPI(Packet): }) name = 'ReadDataByParameterIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x22, GMLAN.services), _gmlan_slm), FieldListField("identifiers", [], XShortEnumField('parameterIdentifier', 0, dataIdentifiers)) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RDBPI.identifiers%") - bind_layers(GMLAN, GMLAN_RDBPI, service=0x22) +GMLAN._service_cls[0x22] = GMLAN_RDBPI class GMLAN_RDBPIPR(Packet): name = 'ReadDataByParameterIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x62, GMLAN.services), _gmlan_slm), XShortEnumField('parameterIdentifier', 0, GMLAN_RDBPI.dataIdentifiers), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RDBPIPR.parameterIdentifier%") - def answers(self, other): - return other.__class__ == GMLAN_RDBPI and \ + return isinstance(other, GMLAN_RDBPI) and \ self.parameterIdentifier in other.identifiers bind_layers(GMLAN, GMLAN_RDBPIPR, service=0x62) +GMLAN._service_cls[0x62] = GMLAN_RDBPIPR # ########################RDBPKTI################################### @@ -401,66 +427,65 @@ class GMLAN_RDBPKTI(Packet): } fields_desc = [ + ConditionalField(XByteEnumField('service', 0xaa, GMLAN.services), _gmlan_slm), XByteEnumField('subfunction', 0, subfunctions), ConditionalField(FieldListField('request_DPIDs', [], XByteField("", 0)), lambda pkt: pkt.subfunction > 0x0) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RDBPKTI.subfunction%") - -bind_layers(GMLAN, GMLAN_RDBPKTI, service=0xAA) +bind_layers(GMLAN, GMLAN_RDBPKTI, service=0xaa) +GMLAN._service_cls[0xaa] = GMLAN_RDBPKTI # ########################RMBA################################### class GMLAN_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ - ConditionalField(XShortField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(2)), - ConditionalField(X3BytesField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(3)), - ConditionalField(XIntField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(4)), + ConditionalField(XByteEnumField('service', 0x23, GMLAN.services), _gmlan_slm), + MultipleTypeField( + [ + (XShortField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(2)), + (X3BytesField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(3)), + (XIntField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(4)) + ], + XIntField('memoryAddress', 0)), XShortField('memorySize', 0), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RMBA.memoryAddress%") - bind_layers(GMLAN, GMLAN_RMBA, service=0x23) +GMLAN._service_cls[0x23] = GMLAN_RMBA class GMLAN_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ - ConditionalField(XShortField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(2)), - ConditionalField(X3BytesField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(3)), - ConditionalField(XIntField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(4)), - StrField('dataRecord', None, fmt="B") + ConditionalField(XByteEnumField('service', 0x63, GMLAN.services), _gmlan_slm), + MultipleTypeField( + [ + (XShortField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(2)), + (X3BytesField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(3)), + (XIntField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(4)) + ], + XIntField('memoryAddress', 0)), + StrField('dataRecord', b"", fmt="B") ] def answers(self, other): - return other.__class__ == GMLAN_RMBA and \ + return isinstance(other, GMLAN_RMBA) and \ other.memoryAddress == self.memoryAddress - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.sprintf("%GMLAN_RMBAPR.memoryAddress%"), pkt.dataRecord) - bind_layers(GMLAN, GMLAN_RMBAPR, service=0x63) +GMLAN._service_cls[0x63] = GMLAN_RMBAPR # ########################SA################################### @@ -482,151 +507,126 @@ class GMLAN_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x27, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), - ConditionalField(XShortField('securityKey', B""), + ConditionalField(XShortField('securityKey', 0), lambda pkt: pkt.subfunction % 2 == 0) ] - @staticmethod - def get_log(pkt): - if pkt.subfunction % 2 == 1: - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.subfunction, None) - else: - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.subfunction, pkt.securityKey) - bind_layers(GMLAN, GMLAN_SA, service=0x27) +GMLAN._service_cls[0x27] = GMLAN_SA class GMLAN_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x67, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, GMLAN_SA.subfunctions), - ConditionalField(XShortField('securitySeed', B""), + ConditionalField(XShortField('securitySeed', 0), lambda pkt: pkt.subfunction % 2 == 1), ] def answers(self, other): - return other.__class__ == GMLAN_SA \ + return isinstance(other, GMLAN_SA) \ and other.subfunction == self.subfunction - @staticmethod - def get_log(pkt): - if pkt.subfunction % 2 == 0: - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.subfunction, None) - else: - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.subfunction, pkt.securitySeed) - - @staticmethod - def modifies_ecu_state(pkt, ecu): - if pkt.subfunction % 2 == 0: - ecu.current_security_level = pkt.subfunction - bind_layers(GMLAN, GMLAN_SAPR, service=0x67) +GMLAN._service_cls[0x67] = GMLAN_SAPR # ########################DDM################################### class GMLAN_DDM(Packet): name = 'DynamicallyDefineMessage' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2c, GMLAN.services), _gmlan_slm), XByteField('DPIDIdentifier', 0), StrField('PIDData', b'\x00\x00') ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.sprintf("%GMLAN_DDM.DPIDIdentifier%"), pkt.PIDData) - -bind_layers(GMLAN, GMLAN_DDM, service=0x2C) +bind_layers(GMLAN, GMLAN_DDM, service=0x2c) +GMLAN._service_cls[0x2c] = GMLAN_DDM class GMLAN_DDMPR(Packet): name = 'DynamicallyDefineMessagePositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6c, GMLAN.services), _gmlan_slm), XByteField('DPIDIdentifier', 0) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_DDMPR.DPIDIdentifier%") - def answers(self, other): - return other.__class__ == GMLAN_DDM \ + return isinstance(other, GMLAN_DDM) \ and other.DPIDIdentifier == self.DPIDIdentifier -bind_layers(GMLAN, GMLAN_DDMPR, service=0x6C) +bind_layers(GMLAN, GMLAN_DDMPR, service=0x6c) +GMLAN._service_cls[0x6c] = GMLAN_DDMPR # ########################DPBA################################### class GMLAN_DPBA(Packet): name = 'DefinePIDByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2d, GMLAN.services), _gmlan_slm), XShortField('parameterIdentifier', 0), - ConditionalField(XShortField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(2)), - ConditionalField(X3BytesField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(3)), - ConditionalField(XIntField('memoryAddress', 0), - lambda pkt: GMLAN.determine_len(4)), + MultipleTypeField( + [ + (XShortField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(2)), + (X3BytesField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(3)), + (XIntField('memoryAddress', 0), + lambda pkt: GMLAN.determine_len(4)) + ], + XIntField('memoryAddress', 0)), XByteField('memorySize', 0), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.parameterIdentifier, pkt.memoryAddress, pkt.memorySize) - -bind_layers(GMLAN, GMLAN_DPBA, service=0x2D) +bind_layers(GMLAN, GMLAN_DPBA, service=0x2d) +GMLAN._service_cls[0x2d] = GMLAN_DPBA class GMLAN_DPBAPR(Packet): name = 'DefinePIDByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6d, GMLAN.services), _gmlan_slm), XShortField('parameterIdentifier', 0), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), pkt.parameterIdentifier - def answers(self, other): - return other.__class__ == GMLAN_DPBA \ + return isinstance(other, GMLAN_DPBA) \ and other.parameterIdentifier == self.parameterIdentifier -bind_layers(GMLAN, GMLAN_DPBA, service=0x6D) +bind_layers(GMLAN, GMLAN_DPBAPR, service=0x6d) +GMLAN._service_cls[0x6d] = GMLAN_DPBAPR # ########################RD################################### class GMLAN_RD(Packet): name = 'RequestDownload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x34, GMLAN.services), _gmlan_slm), XByteField('dataFormatIdentifier', 0), - ConditionalField(XShortField('memorySize', 0), - lambda pkt: GMLAN.determine_len(2)), - ConditionalField(X3BytesField('memorySize', 0), - lambda pkt: GMLAN.determine_len(3)), - ConditionalField(XIntField('memorySize', 0), - lambda pkt: GMLAN.determine_len(4)), + MultipleTypeField( + [ + (XShortField('memorySize', 0), + lambda pkt: GMLAN.determine_len(2)), + (X3BytesField('memorySize', 0), + lambda pkt: GMLAN.determine_len(3)), + (XIntField('memorySize', 0), + lambda pkt: GMLAN.determine_len(4)) + ], + XIntField('memorySize', 0)) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.dataFormatIdentifier, pkt.memorySize) - bind_layers(GMLAN, GMLAN_RD, service=0x34) +GMLAN._service_cls[0x34] = GMLAN_RD # ########################TD################################### @@ -637,60 +637,54 @@ class GMLAN_TD(Packet): } name = 'TransferData' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x36, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), - ConditionalField(XShortField('startingAddress', 0), - lambda pkt: GMLAN.determine_len(2)), - ConditionalField(X3BytesField('startingAddress', 0), - lambda pkt: GMLAN.determine_len(3)), - ConditionalField(XIntField('startingAddress', 0), - lambda pkt: GMLAN.determine_len(4)), - StrField("dataRecord", None) + MultipleTypeField( + [ + (XShortField('startingAddress', 0), + lambda pkt: GMLAN.determine_len(2)), + (X3BytesField('startingAddress', 0), + lambda pkt: GMLAN.determine_len(3)), + (XIntField('startingAddress', 0), + lambda pkt: GMLAN.determine_len(4)) + ], + XIntField('startingAddress', 0)), + StrField("dataRecord", b"") ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.sprintf("%GMLAN_TD.subfunction%"), pkt.startingAddress, - pkt.dataRecord) - bind_layers(GMLAN, GMLAN_TD, service=0x36) +GMLAN._service_cls[0x36] = GMLAN_TD # ########################WDBI################################### class GMLAN_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3b, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), - StrField("dataRecord", b'\x00') + StrField("dataRecord", b'') ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.sprintf("%GMLAN_WDBI.dataIdentifier%"), pkt.dataRecord) - -bind_layers(GMLAN, GMLAN_WDBI, service=0x3B) +bind_layers(GMLAN, GMLAN_WDBI, service=0x3b) +GMLAN._service_cls[0x3b] = GMLAN_WDBI class GMLAN_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7b, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_WDBIPR.dataIdentifier%") - def answers(self, other): - return other.__class__ == GMLAN_WDBI \ + return isinstance(other, GMLAN_WDBI) \ and other.dataIdentifier == self.dataIdentifier -bind_layers(GMLAN, GMLAN_WDBIPR, service=0x7B) +bind_layers(GMLAN, GMLAN_WDBIPR, service=0x7b) +GMLAN._service_cls[0x7b] = GMLAN_WDBIPR # ########################RPSPR################################### @@ -709,16 +703,13 @@ class GMLAN_RPSPR(Packet): } name = 'ReportProgrammedStatePositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xe2, GMLAN.services), _gmlan_slm), ByteEnumField('programmedState', 0, programmedStates), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RPSPR.programmedState%") - -bind_layers(GMLAN, GMLAN_RPSPR, service=0xE2) +bind_layers(GMLAN, GMLAN_RPSPR, service=0xe2) +GMLAN._service_cls[0xe2] = GMLAN_RPSPR # ########################PM################################### @@ -730,16 +721,13 @@ class GMLAN_PM(Packet): } name = 'ProgrammingMode' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xa5, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_PM.subfunction%") - -bind_layers(GMLAN, GMLAN_PM, service=0xA5) +bind_layers(GMLAN, GMLAN_PM, service=0xa5) +GMLAN._service_cls[0xa5] = GMLAN_PM # ########################RDI################################### @@ -751,16 +739,13 @@ class GMLAN_RDI(Packet): } name = 'ReadDiagnosticInformation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xa9, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - pkt.sprintf("%GMLAN_RDI.subfunction%") - -bind_layers(GMLAN, GMLAN_RDI, service=0xA9) +bind_layers(GMLAN, GMLAN_RDI, service=0xa9) +GMLAN._service_cls[0xa9] = GMLAN_RDI class GMLAN_RDI_BN(Packet): @@ -793,9 +778,41 @@ class GMLAN_RDI_BC(Packet): bind_layers(GMLAN_RDI, GMLAN_RDI_BC, subfunction=0x82) + + # TODO:This function receive single frame responses... (Implement GMLAN Socket) +# ########################DC################################### +class GMLAN_DC(Packet): + name = 'DeviceControl' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0xae, GMLAN.services), _gmlan_slm), + XByteField('CPIDNumber', 0), + StrFixedLenField('CPIDControlBytes', b"", 5) + ] + + +bind_layers(GMLAN, GMLAN_DC, service=0xae) +GMLAN._service_cls[0xae] = GMLAN_DC + + +class GMLAN_DCPR(Packet): + name = 'DeviceControlPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0xee, GMLAN.services), _gmlan_slm), + XByteField('CPIDNumber', 0) + ] + + def answers(self, other): + return isinstance(other, GMLAN_DC) \ + and other.CPIDNumber == self.CPIDNumber + + +bind_layers(GMLAN, GMLAN_DCPR, service=0xee) +GMLAN._service_cls[0xee] = GMLAN_DCPR + + # ########################NRC################################### class GMLAN_NR(Packet): negativeResponseCodes = { @@ -816,17 +833,13 @@ class GMLAN_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7f, GMLAN.services), _gmlan_slm), XByteEnumField('requestServiceId', 0, GMLAN.services), - ByteEnumField('returnCode', 0, negativeResponseCodes), + MayEnd(ByteEnumField('returnCode', 0, negativeResponseCodes)), + # XXX Is this MayEnd correct? Why is the field below also 0xe3 ? ShortField('deviceControlLimitExceeded', 0) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%GMLAN.service%"), \ - (pkt.sprintf("%GMLAN_NR.requestServiceId%"), - pkt.sprintf("%GMLAN_NR.returnCode%")) - def answers(self, other): return self.requestServiceId == other.service and \ (self.returnCode != 0x78 or @@ -834,3 +847,4 @@ def answers(self, other): bind_layers(GMLAN, GMLAN_NR, service=0x7f) +GMLAN._service_cls[0x7f] = GMLAN_NR diff --git a/scapy/contrib/automotive/gm/gmlan_ecu_states.py b/scapy/contrib/automotive/gm/gmlan_ecu_states.py new file mode 100644 index 00000000000..4e118512c4d --- /dev/null +++ b/scapy/contrib/automotive/gm/gmlan_ecu_states.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = GMLAN EcuState modifications +# scapy.contrib.status = library +from scapy.packet import Packet +from scapy.contrib.automotive.ecu import EcuState +from scapy.contrib.automotive.gm.gmlan import GMLAN, GMLAN_SAPR + +__all__ = ["GMLAN_modify_ecu_state", "GMLAN_SAPR_modify_ecu_state"] + + +@EcuState.extend_pkt_with_modifier(GMLAN) +def GMLAN_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + if self.service == 0x50: + state.session = 3 # type: ignore + elif self.service == 0x60: + state.reset() + state.session = 1 # type: ignore + elif self.service == 0x68: + state.communication_control = 1 # type: ignore + elif self.service == 0xe5: + state.session = 2 # type: ignore + elif self.service == 0x74 and len(req) > 3: + state.request_download = 1 # type: ignore + elif self.service == 0x7e: + state.tp = 1 # type: ignore + + +@EcuState.extend_pkt_with_modifier(GMLAN_SAPR) +def GMLAN_SAPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + if self.subfunction % 2 == 0 and self.subfunction > 0 and len(req) >= 3: + state.security_level = self.subfunction # type: ignore + elif self.subfunction % 2 == 1 and \ + self.subfunction > 0 and \ + len(req) >= 3 and not any(self.securitySeed): + state.security_level = self.securityAccessType + 1 # type: ignore diff --git a/scapy/contrib/automotive/gm/gmlan_logging.py b/scapy/contrib/automotive/gm/gmlan_logging.py new file mode 100644 index 00000000000..33fc79c1073 --- /dev/null +++ b/scapy/contrib/automotive/gm/gmlan_logging.py @@ -0,0 +1,214 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = GMLAN Ecu logging additions +# scapy.contrib.status = library + + +from scapy.contrib.automotive.gm.gmlan import GMLAN_SA, GMLAN_IDO, GMLAN_DC, \ + GMLAN_NR, GMLAN_RD, GMLAN_TD, GMLAN_DCPR, GMLAN_DPBA, GMLAN_DPBAPR, \ + GMLAN_RPSPR, GMLAN_RDI, GMLAN_WDBI, GMLAN_WDBIPR, GMLAN_PM, GMLAN_SAPR, \ + GMLAN_RDBI, GMLAN_RDBIPR, GMLAN_RDBPI, GMLAN_RDBPIPR, GMLAN_RDBPKTI, \ + GMLAN_RFRD, GMLAN_RFRDPR, GMLAN_RMBA, GMLAN_RMBAPR, GMLAN_DDM, GMLAN_DDMPR +from scapy.packet import Packet +from scapy.contrib.automotive.ecu import Ecu + +# Typing imports +from typing import ( + Any, + Tuple, +) + + +@Ecu.extend_pkt_with_logging(GMLAN_IDO) +def GMLAN_IDO_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_IDO.subfunction%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RFRD) +def GMLAN_RFRD_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RFRD.subfunction%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RFRDPR) +def GMLAN_RFRDPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RFRDPR.subfunction%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RDBI) +def GMLAN_RDBI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RDBI.dataIdentifier%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RDBIPR) +def GMLAN_RDBIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.sprintf("%GMLAN_RDBIPR.dataIdentifier%"), + bytes(self.load)) + + +@Ecu.extend_pkt_with_logging(GMLAN_RDBPI) +def GMLAN_RDBPI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RDBPI.identifiers%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RDBPIPR) +def GMLAN_RDBPIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RDBPIPR.parameterIdentifier%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RDBPKTI) +def GMLAN_RDBPKTI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RDBPKTI.subfunction%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RMBA) +def GMLAN_RMBA_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RMBA.memoryAddress%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RMBAPR) +def GMLAN_RMBAPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.sprintf("%GMLAN_RMBAPR.memoryAddress%"), self.dataRecord) + + +@Ecu.extend_pkt_with_logging(GMLAN_SA) +def GMLAN_SA_get_log(self): + # type: (Packet) -> Tuple[str, Any] + if self.subfunction % 2 == 1: + return self.sprintf("%GMLAN.service%"), \ + (self.subfunction, None) + else: + return self.sprintf("%GMLAN.service%"), \ + (self.subfunction, self.securityKey) + + +@Ecu.extend_pkt_with_logging(GMLAN_SAPR) +def GMLAN_SAPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + if self.subfunction % 2 == 0: + return self.sprintf("%GMLAN.service%"), \ + (self.subfunction, None) + else: + return self.sprintf("%GMLAN.service%"), \ + (self.subfunction, self.securitySeed) + + +@Ecu.extend_pkt_with_logging(GMLAN_DDM) +def GMLAN_DDM_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.sprintf("%GMLAN_DDM.DPIDIdentifier%"), self.PIDData) + + +@Ecu.extend_pkt_with_logging(GMLAN_DDMPR) +def GMLAN_DDMPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_DDMPR.DPIDIdentifier%") + + +@Ecu.extend_pkt_with_logging(GMLAN_DPBA) +def GMLAN_DPBA_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.parameterIdentifier, self.memoryAddress, self.memorySize) + + +@Ecu.extend_pkt_with_logging(GMLAN_DPBAPR) +def GMLAN_DPBAPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), self.parameterIdentifier + + +@Ecu.extend_pkt_with_logging(GMLAN_RD) +def GMLAN_RD_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.dataFormatIdentifier, self.memorySize) + + +@Ecu.extend_pkt_with_logging(GMLAN_TD) +def GMLAN_TD_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.sprintf("%GMLAN_TD.subfunction%"), self.startingAddress, + self.dataRecord) + + +@Ecu.extend_pkt_with_logging(GMLAN_WDBI) +def GMLAN_WDBI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.sprintf("%GMLAN_WDBI.dataIdentifier%"), self.dataRecord) + + +@Ecu.extend_pkt_with_logging(GMLAN_WDBIPR) +def GMLAN_WDBIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_WDBIPR.dataIdentifier%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RPSPR) +def GMLAN_RPSPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RPSPR.programmedState%") + + +@Ecu.extend_pkt_with_logging(GMLAN_PM) +def GMLAN_PM_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_PM.subfunction%") + + +@Ecu.extend_pkt_with_logging(GMLAN_RDI) +def GMLAN_RDI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_RDI.subfunction%") + + +@Ecu.extend_pkt_with_logging(GMLAN_DC) +def GMLAN_DC_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_DC.CPIDNumber%") + + +@Ecu.extend_pkt_with_logging(GMLAN_DCPR) +def GMLAN_DCPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + self.sprintf("%GMLAN_DCPR.CPIDNumber%") + + +@Ecu.extend_pkt_with_logging(GMLAN_NR) +def GMLAN_NR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%GMLAN.service%"), \ + (self.sprintf("%GMLAN_NR.requestServiceId%"), + self.sprintf("%GMLAN_NR.returnCode%")) diff --git a/scapy/contrib/automotive/gm/gmlan_scanner.py b/scapy/contrib/automotive/gm/gmlan_scanner.py new file mode 100644 index 00000000000..46db962a41b --- /dev/null +++ b/scapy/contrib/automotive/gm/gmlan_scanner.py @@ -0,0 +1,757 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = GMLAN AutomotiveTestCaseExecutor Utilities +# scapy.contrib.status = loads + +import abc +import random +import time +import copy + +from collections import defaultdict + +from scapy.compat import orb +from scapy.contrib.automotive import log_automotive +from scapy.packet import Packet +from scapy.config import conf +from scapy.supersocket import SuperSocket +from scapy.error import Scapy_Exception +from scapy.contrib.automotive.gm.gmlanutils import GMLAN_InitDiagnostics, \ + GMLAN_TesterPresentSender +from scapy.contrib.automotive.gm.gmlan import GMLAN, GMLAN_SA, GMLAN_RD, \ + GMLAN_TD, GMLAN_RMBA, GMLAN_RDBI, GMLAN_RDBPI, GMLAN_IDO, \ + GMLAN_NR, GMLAN_WDBI, GMLAN_DC, GMLAN_PM +from scapy.contrib.automotive.ecu import EcuState + +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCaseABC, \ + _SocketUnion, _TransitionTuple, StateGenerator +from scapy.contrib.automotive.scanner.enumerator import ServiceEnumerator, \ + _AutomotiveTestCaseScanResult, StateGeneratingServiceEnumerator +from scapy.contrib.automotive.scanner.configuration import \ + AutomotiveTestCaseExecutorConfiguration +from scapy.contrib.automotive.scanner.graph import _Edge +from scapy.contrib.automotive.scanner.staged_test_case import \ + StagedAutomotiveTestCase +from scapy.contrib.automotive.scanner.executor import \ + AutomotiveTestCaseExecutor + +# TODO: Refactor this import +from scapy.contrib.automotive.gm.gmlan_ecu_states import * # noqa: F401, F403 + +# Typing imports +from typing import ( + Optional, + List, + Type, + Any, + Tuple, + Iterable, + Dict, + cast, + Callable, +) + +__all__ = ["GMLAN_Scanner", "GMLAN_ServiceEnumerator", "GMLAN_RDBIEnumerator", + "GMLAN_RDBPIEnumerator", "GMLAN_RMBAEnumerator", + "GMLAN_TPEnumerator", "GMLAN_IDOEnumerator", "GMLAN_PMEnumerator", + "GMLAN_RDEnumerator", "GMLAN_TDEnumerator", "GMLAN_WDBIEnumerator", + "GMLAN_SAEnumerator", "GMLAN_WDBISelectiveEnumerator", + "GMLAN_DCEnumerator"] + + +class GMLAN_Enumerator(ServiceEnumerator, metaclass=abc.ABCMeta): + """ + Abstract base class for GMLAN service enumerators. This class + implements GMLAN specific functions. + """ + @staticmethod + def _get_negative_response_code(resp): + # type: (Packet) -> int + return resp.returnCode + + @staticmethod + def _get_negative_response_desc(nrc): + # type: (int) -> str + return GMLAN_NR(returnCode=nrc).sprintf("%GMLAN_NR.returnCode%") + + @staticmethod + def _get_negative_response_label(response): + # type: (Packet) -> str + return response.sprintf("NR: %GMLAN_NR.returnCode%") + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2]) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + raise NotImplementedError("Overwrite this method") + + +class GMLAN_ServiceEnumerator(GMLAN_Enumerator, StateGeneratingServiceEnumerator): # noqa: E501 + """ + This enumerator scans for all services identifiers of GMLAN. During this + scan, corrupted packets might be sent to an ECU and mainly negative + responses will be received. + """ + _description = "Available services and negative response per state" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + services = set(x & ~0x40 for x in range(0x100)) + services.remove(0x10) # Remove InitiateDiagnosticOperation service + services.remove(0x3E) # Remove TesterPresent service + services.remove(0xa5) # Remove ProgrammingMode service + services.remove(0x34) # Remove RequestDownload + return (GMLAN(service=x) for x in services) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % ( + tup[1].service, tup[1].sprintf("%GMLAN.service%")) + + +class GMLAN_TPEnumerator(GMLAN_Enumerator, StateGeneratingServiceEnumerator): + """ + Performs a check if TesterPresent is available. If a positive response is + received, a new system state is generated and returned. + """ + _description = "TesterPresent supported" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return [GMLAN(service=0x3E)] + + @staticmethod + def enter(socket, # type: _SocketUnion + configuration, # type: AutomotiveTestCaseExecutorConfiguration + kwargs # type: Dict[str, Any] + ): + # type: (...) -> bool + if configuration.unittest: + configuration["tps"] = None + socket.sr1(GMLAN(service=0x3E), timeout=0.1, verbose=False) + return True + + GMLAN_TPEnumerator.cleanup(socket, configuration) + configuration["tps"] = GMLAN_TesterPresentSender( + cast(SuperSocket, socket)) + configuration["tps"].start() + return True + + @staticmethod + def cleanup(_, configuration): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration) -> bool + try: + if configuration["tps"]: + configuration["tps"].stop() + configuration["tps"] = None + except KeyError: + pass + return True + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + return self.enter, {"desc": "TP"}, self.cleanup + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "TesterPresent:" + + +class GMLAN_IDOEnumerator(GMLAN_Enumerator, StateGeneratingServiceEnumerator): + _description = "InitiateDiagnosticOperation supported" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return [GMLAN() / GMLAN_IDO(subfunction=2)] + + @staticmethod + def enter_diagnostic_session(socket): + # type: (_SocketUnion) -> bool + ans = socket.sr1( + GMLAN() / GMLAN_IDO(subfunction=2), timeout=5, verbose=False) + if ans is not None and ans.service == 0x7f: + log_automotive.debug( + "InitiateDiagnosticOperation received negative response!\n" + "%s", repr(ans)) + return ans is not None and ans.service != 0x7f + + def get_new_edge(self, socket, config): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration) -> Optional[_Edge] # noqa: E501 + edge = super(GMLAN_IDOEnumerator, self).get_new_edge(socket, config) + if edge: + state, new_state = edge + new_state.tp = 1 # type: ignore + return state, new_state + return None + + @staticmethod + def enter_state_with_tp(sock, conf, kwargs): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration, Dict[str, Any]) -> bool # noqa: E501 + GMLAN_TPEnumerator.enter(sock, conf, kwargs) + if GMLAN_IDOEnumerator.enter_diagnostic_session(sock): + return True + else: + GMLAN_TPEnumerator.cleanup(sock, conf) + return False + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + return self.enter_state_with_tp, {"desc": "IDO_TP"}, GMLAN_TPEnumerator.cleanup # noqa: E501 + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "InitiateDiagnosticOperation:" + + +class GMLAN_RDBIEnumerator(GMLAN_Enumerator): + _description = "Readable data identifier per state" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) + return (GMLAN() / GMLAN_RDBI(dataIdentifier=x) for x in scan_range) + + @staticmethod + def print_information(resp): + # type: (Packet) -> str + load = bytes(resp)[2:] if len(resp) > 3 else b"No data available" + return "PR: %r" % ((load[:17] + b"...") if len(load) > 20 else load) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x: %s" % (tup[1].dataIdentifier, + tup[1].sprintf("%GMLAN_RDBI.dataIdentifier%")) + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], self.print_information) + + +class GMLAN_WDBIEnumerator(GMLAN_Enumerator): + _description = "Writeable data identifier per state" + _supported_kwargs = copy.copy(GMLAN_Enumerator._supported_kwargs) + _supported_kwargs.update({ + 'rdbi_enumerator': (GMLAN_RDBIEnumerator, None) + }) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param rdbi_enumerator: Specifies an instance of a GMLAN_RDBIEnumerator + which is used to extract possible data + identifiers. + :type rdbi_enumerator: GMLAN_RDBIEnumerator""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(GMLAN_WDBIEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) + rdbi_enumerator = kwargs.pop("rdbi_enumerator", None) + if rdbi_enumerator is None: + return (GMLAN() / GMLAN_WDBI(dataIdentifier=x) for x in scan_range) + elif isinstance(rdbi_enumerator, GMLAN_RDBIEnumerator): + return (GMLAN() / GMLAN_WDBI(dataIdentifier=t.resp.dataIdentifier, + dataRecord=bytes(t.resp)[2:]) + for t in rdbi_enumerator.filtered_results + if t.resp.service != 0x7f and len(bytes(t.resp)) >= 2) + else: + raise Scapy_Exception("rdbi_enumerator has to be an instance " + "of GMLAN_RDBIEnumerator") + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % (tup[1].dataIdentifier, + tup[1].sprintf("%GMLAN_WDBI.dataIdentifier%")) + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], "PR: Writeable") + + +class GMLAN_WDBISelectiveEnumerator(StagedAutomotiveTestCase): + @staticmethod + def __connector_rdbi_to_wdbi(rdbi, _): + # type: (AutomotiveTestCaseABC, AutomotiveTestCaseABC) -> Dict[str, Any] # noqa: E501 + return {"rdbi_enumerator": rdbi} + + def __init__(self): + # type: () -> None + super(GMLAN_WDBISelectiveEnumerator, self).__init__( + [GMLAN_RDBIEnumerator(), GMLAN_WDBIEnumerator()], + [None, self.__connector_rdbi_to_wdbi]) + + +class GMLAN_SAEnumerator(GMLAN_Enumerator, StateGenerator): + _description = "SecurityAccess supported" + _transition_function_args = dict() # type: Dict[_Edge, Tuple[int, Optional[Callable[[int], int]]]] # noqa: E501 + _supported_kwargs = copy.copy(GMLAN_Enumerator._supported_kwargs) + _supported_kwargs.update({ + 'keyfunction': (None, None) + }) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param keyfunction: Specifies a function to generate the key from a + given seed. + :type keyfunction: Callable[[int], int]""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(GMLAN_SAEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(1, 10, 2)) + return (GMLAN() / GMLAN_SA(subfunction=x) for x in scan_range) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Subfunction %02d" % tup[1].subfunction + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], lambda r: "PR: %s" % r.securitySeed) + + def pre_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + if cast(ServiceEnumerator, self)._retry_pkt[state] and \ + not global_configuration.unittest: + # this is a retry execute. Wait much longer than usual because + # a required time delay not expired could have been received + # on the previous attempt + time.sleep(11) + + def _evaluate_retry(self, + state, # type: EcuState + request, # type: Packet + response, # type: Packet + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool + + if super(GMLAN_SAEnumerator, self)._evaluate_retry( + state, request, response, **kwargs): + return True + + if response.service == 0x7f and \ + self._get_negative_response_code(response) in [0x22, 0x37]: + log_automotive.debug( + "Retry %s because requiredTimeDelayNotExpired or " + "requestSequenceError received", + repr(request)) + return super(GMLAN_SAEnumerator, self)._populate_retry( + state, request) + return False + + def _evaluate_response(self, + state, # type: EcuState + request, # type: Packet + response, # type: Optional[Packet] + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool + if super(GMLAN_SAEnumerator, self)._evaluate_response( + state, request, response, **kwargs): + return True + + if response is not None and \ + response.service == 0x67 and response.subfunction % 2 == 1: + log_automotive.debug("Seed received. Leave scan to try a key") + return True + return False + + @staticmethod + def get_seed_pkt(sock, level=1): + # type: (_SocketUnion, int) -> Optional[Packet] + req = GMLAN() / GMLAN_SA(subfunction=level) + for _ in range(10): + seed = sock.sr1(req, timeout=5, verbose=False) + if seed is None: + return None + elif seed.service == 0x7f and \ + GMLAN_Enumerator._get_negative_response_code(seed) != 0x37: + log_automotive.info( + "Security access no seed! NR: %s", repr(seed)) + return None + + elif seed.service == 0x7f and \ + GMLAN_Enumerator._get_negative_response_code(seed) == 0x37: + log_automotive.info("Security access retry to get seed") + time.sleep(10) + continue + else: + return seed + return None + + @staticmethod + def evaluate_security_access_response(res, seed, key): + # type: (Optional[Packet], Packet, Optional[Packet]) -> bool + if res is None or res.service == 0x7f: + log_automotive.debug(repr(seed)) + log_automotive.debug(repr(key)) + log_automotive.debug(repr(res)) + log_automotive.info("Security access error!") + return False + else: + log_automotive.info("Security access granted!") + return True + + @staticmethod + def get_key_pkt(seed, keyfunction, level=1): + # type: (Packet, Callable[[int], int], int) -> Optional[Packet] + try: + s = seed.securitySeed + except AttributeError: + return None + + return cast(Packet, GMLAN() / GMLAN_SA(subfunction=level + 1, + securityKey=keyfunction(s))) + + @staticmethod + def get_security_access(sock, level=1, seed_pkt=None, keyfunction=None): + # type: (_SocketUnion, int, Optional[Packet], Optional[Callable[[int], int]]) -> bool # noqa: E501 + log_automotive.info( + "Try bootloader security access for level %d" % level) + if seed_pkt is None: + seed_pkt = GMLAN_SAEnumerator.get_seed_pkt(sock, level) + if not seed_pkt: + return False + + if keyfunction is None: + return False + + key_pkt = GMLAN_SAEnumerator.get_key_pkt(seed_pkt, keyfunction, level) + if key_pkt is None: + return False + + res = sock.sr1(key_pkt, timeout=5, verbose=False) + return GMLAN_SAEnumerator.evaluate_security_access_response( + res, seed_pkt, key_pkt) + + @staticmethod + def transition_function(sock, _, kwargs): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration, Dict[str, Any]) -> bool # noqa: E501 + return GMLAN_SAEnumerator.get_security_access( + sock, level=kwargs["sec_level"], keyfunction=kwargs["keyfunction"]) + + def get_new_edge(self, socket, config): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration) -> Optional[_Edge] # noqa: E501 + last_resp = self._results[-1].resp + last_state = self._results[-1].state + + if last_resp is None or last_resp.service == 0x7f: + return None + + try: + if last_resp.service != 0x67 or \ + last_resp.subfunction % 2 != 1: + return None + + seed = last_resp + sec_lvl = seed.subfunction + kf = config[self.__class__.__name__].get("keyfunction", None) + + if self.get_security_access(socket, level=sec_lvl, + seed_pkt=seed, keyfunction=kf): + log_automotive.debug("Security Access found.") + # create edge + new_state = copy.copy(last_state) + new_state.security_level = seed.subfunction + 1 # type: ignore # noqa: E501 + if last_state == new_state: + return None + edge = (last_state, new_state) + self._transition_function_args[edge] = (sec_lvl, kf) + return edge + except AttributeError: + pass + + return None + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + return self.transition_function, { + "sec_level": self._transition_function_args[edge][0], + "keyfunction": self._transition_function_args[edge][1], + "desc": "SA=%d" % self._transition_function_args[edge][0]}, None + + +class GMLAN_RDEnumerator(GMLAN_Enumerator, StateGeneratingServiceEnumerator): + _description = "RequestDownload supported" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return [GMLAN() / GMLAN_RD(memorySize=0x10)] + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "RequestDownload:" + + +class GMLAN_PMEnumerator(GMLAN_Enumerator, StateGeneratingServiceEnumerator): + _description = "ProgrammingMode supported" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + raise NotImplementedError() + + def execute(self, socket, state, timeout=1, execution_time=1200, **kwargs): + # type: (_SocketUnion, EcuState, int, int, Any) -> None + supported = GMLAN_InitDiagnostics( + cast(SuperSocket, socket), timeout=20, + unittest=kwargs.get("unittest", False)) + # TODO: Refactor result storage + if supported: + self._store_result( + state, GMLAN() / GMLAN_PM(), GMLAN(service=0xE5)) + else: + self._store_result( + state, GMLAN() / GMLAN_PM(), + GMLAN() / GMLAN_NR(returnCode=0x11, requestServiceId=0xA5)) + + self._state_completed[state] = True + + def get_new_edge(self, socket, config): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration) -> Optional[_Edge] # noqa: E501 + edge = super(GMLAN_PMEnumerator, self).get_new_edge(socket, config) + if edge: + state, new_state = edge + new_state.tp = 1 # type: ignore + new_state.communication_control = 1 # type: ignore + return state, new_state + return None + + @staticmethod + def enter_state_with_tp(sock, conf, kwargs): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration, Dict[str, Any]) -> bool # noqa: E501 + GMLAN_TPEnumerator.enter(sock, conf, kwargs) + res = GMLAN_InitDiagnostics(cast(SuperSocket, sock), timeout=20, + unittest=conf.unittest) + if not res: + GMLAN_TPEnumerator.cleanup(sock, conf) + return False + else: + return True + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + return self.enter_state_with_tp, {"desc": "PM_TP"}, \ + GMLAN_TPEnumerator.cleanup + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "ProgrammingMode:" + + +class GMLAN_RDBPIEnumerator(GMLAN_Enumerator): + _description = "Readable parameter identifier per state" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x10000)) + return (GMLAN() / GMLAN_RDBPI(identifiers=[x]) for x in scan_range) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x: %s" % ( + tup[1].identifiers[0], + tup[1].sprintf("%GMLAN_RDBPI.identifiers%")[1:-1]) + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], GMLAN_RDBIEnumerator.print_information) + + +class GMLAN_RMBAEnumerator(GMLAN_Enumerator): + _description = "Readable Memory Addresses and negative response per state" + + _supported_kwargs = copy.copy(GMLAN_Enumerator._supported_kwargs) + _supported_kwargs.update({ + 'probe_width': (int, lambda x: x >= 0), + 'random_probes_len': (int, lambda x: x >= 0), + 'sequential_probes_len': (int, lambda x: x >= 0) + }) + + _supported_kwargs_doc = GMLAN_Enumerator._supported_kwargs_doc + """ + :param int probe_width: Memory size of a probe. + :param int random_probes_len: Number of probes. + :param int sequential_probes_len: Size of a memory block during + sequential probing.""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(GMLAN_RMBAEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def __init__(self): + # type: () -> None + super(GMLAN_RMBAEnumerator, self).__init__() + self.random_probe_finished = defaultdict(bool) # type: Dict[EcuState, bool] # noqa: E501 + self.points_of_interest = defaultdict(list) # type: Dict[EcuState, List[Tuple[int, bool]]] # noqa: E501 + self.probe_width = 0x10 # defines the memorySize of a request + self.highest_possible_addr = \ + 2 ** (8 * conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme']) - 1 + self.random_probes_len = \ + min(10 ** conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'], + 0x5000) + self.sequential_probes_len = \ + 10 ** (conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme']) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + self.probe_width = kwargs.pop("probe_width", self.probe_width) + self.random_probes_len = \ + kwargs.pop("random_probes_len", self.random_probes_len) + self.sequential_probes_len = \ + kwargs.pop("sequential_probes_len", self.sequential_probes_len) + addresses = random.sample( + range(0, self.highest_possible_addr, self.probe_width), + self.random_probes_len) + scan_range = kwargs.pop("scan_range", addresses) + return (GMLAN() / GMLAN_RMBA(memoryAddress=x, + memorySize=self.probe_width) + for x in scan_range) + + def post_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + if not self._state_completed[state]: + return + + if not self.random_probe_finished[state]: + log_automotive.info("Random memory probing finished") + self.random_probe_finished[state] = True + for tup in [t for t in self.results_with_positive_response + if t.state == state]: + self.points_of_interest[state].append( + (tup.req.memoryAddress, True)) + self.points_of_interest[state].append( + (tup.req.memoryAddress, False)) + + if not len(self.points_of_interest[state]): + return + + log_automotive.info( + "Create %d memory points for sequential probing" % + len(self.points_of_interest[state])) + + tested_addrs = [tup.req.memoryAddress for tup in self.results] + pos_addrs = [tup.req.memoryAddress for tup in + self.results_with_positive_response if tup.state == state] + + new_requests = list() + new_points_of_interest = list() + + for poi, upward in self.points_of_interest[state]: + if poi not in pos_addrs: + continue + temp_new_requests = list() + for i in range( + self.probe_width, + self.sequential_probes_len + self.probe_width, + self.probe_width): + if upward: + new_addr = min(poi + i, self.highest_possible_addr) + else: + new_addr = max(poi - i, 0) + + if new_addr not in tested_addrs: + pkt = GMLAN() / GMLAN_RMBA(memoryAddress=new_addr, + memorySize=self.probe_width) + temp_new_requests.append(pkt) + + if len(temp_new_requests): + new_points_of_interest.append( + (temp_new_requests[-1].memoryAddress, upward)) + new_requests += temp_new_requests + + self.points_of_interest[state] = list() + + if len(new_requests): + self._state_completed[state] = False + self._request_iterators[state] = new_requests + self.points_of_interest[state] = new_points_of_interest + log_automotive.info( + "Created %d pkts for sequential probing" % + len(new_requests)) + + def show(self, dump=False, filtered=True, verbose=False): + # type: (bool, bool, bool) -> Optional[str] + s = super(GMLAN_RMBAEnumerator, self).show(dump, filtered, verbose) + try: + from intelhex import IntelHex + + ih = IntelHex() + for tup in self.results_with_positive_response: + for i, b in enumerate(tup.resp.dataRecord): + ih[tup.req.memoryAddress + i] = orb(b) + + ih.tofile("RMBA_dump.hex", format="hex") + except ImportError: + log_automotive.warning( + "Install 'intelhex' to create a hex file of the memory") + + if dump and s is not None: + return s + "\n" + else: + print(s) + return None + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x" % tup[1].memoryAddress + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], lambda r: "PR: %s" % r.dataRecord) + + +class GMLAN_TDEnumerator(GMLAN_Enumerator): + _description = "Transfer Data support and negative response per state" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x1ff)) + temp = conf.contribs["GMLAN"]['GMLAN_ECU_AddressingScheme'] + # Shift operations to eliminate addresses not aligned to 4 + max_addr = (2 ** (temp * 8) - 1) >> 2 + addresses = (random.randint(0, max_addr) << 2 for _ in scan_range) + return (GMLAN() / GMLAN_TD(subfunction=0, startingAddress=x) + for x in addresses) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x" % tup[1].startingAddress + + +class GMLAN_DCEnumerator(GMLAN_Enumerator): + _description = "DeviceControl supported per state" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) + return (GMLAN() / GMLAN_DC(CPIDNumber=x) for x in scan_range) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % \ + (tup[1].CPIDNumber, tup[1].sprintf("%GMLAN_DC.CPIDNumber%")) + + +# ########################## GMLAN SCANNER ################################### + +class GMLAN_Scanner(AutomotiveTestCaseExecutor): + @property + def default_test_case_clss(self): + # type: () -> List[Type[AutomotiveTestCaseABC]] + return [GMLAN_ServiceEnumerator, GMLAN_TPEnumerator, + GMLAN_IDOEnumerator, GMLAN_PMEnumerator, + GMLAN_RDEnumerator, GMLAN_SAEnumerator, GMLAN_TDEnumerator, + GMLAN_RMBAEnumerator, + GMLAN_WDBISelectiveEnumerator, GMLAN_DCEnumerator] diff --git a/scapy/contrib/automotive/gm/gmlanutils.py b/scapy/contrib/automotive/gm/gmlanutils.py index 66ae9919bc6..370e644ba68 100644 --- a/scapy/contrib/automotive/gm/gmlanutils.py +++ b/scapy/contrib/automotive/gm/gmlanutils.py @@ -1,84 +1,104 @@ -#! /usr/bin/env python - +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Markus Schroetter +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license +# Copyright (C) Markus Schroetter # scapy.contrib.description = GMLAN Utilities # scapy.contrib.status = loads import time + +from scapy.contrib.automotive import log_automotive + from scapy.contrib.automotive.gm.gmlan import GMLAN, GMLAN_SA, GMLAN_RD, \ GMLAN_TD, GMLAN_PM, GMLAN_RMBA from scapy.config import conf +from scapy.packet import Packet +from scapy.supersocket import SuperSocket from scapy.contrib.isotp import ISOTPSocket -from scapy.error import warning, log_loading from scapy.utils import PeriodicSenderThread +from typing import ( + Optional, + cast, + Callable, +) __all__ = ["GMLAN_TesterPresentSender", "GMLAN_InitDiagnostics", "GMLAN_GetSecurityAccess", "GMLAN_RequestDownload", "GMLAN_TransferData", "GMLAN_TransferPayload", "GMLAN_ReadMemoryByAddress", "GMLAN_BroadcastSocket"] -log_loading.info("\"conf.contribs['GMLAN']" - "['treat-response-pending-as-answer']\" set to True). This " - "is required by the GMLAN-Utils module to operate " - "correctly.") +log_automotive.info("\"conf.contribs['GMLAN']" + "['treat-response-pending-as-answer']\" set to True). This " + "is required by the GMLAN-Utils module to operate " + "correctly.") try: conf.contribs['GMLAN']['treat-response-pending-as-answer'] = False except KeyError: conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False} -class GMLAN_TesterPresentSender(PeriodicSenderThread): - def __init__(self, sock, pkt=GMLAN(service="TesterPresent"), interval=2): - """ Thread to send TesterPresent messages packets periodically - - Args: - sock: socket where packet is sent periodically - pkt: packet to send - interval: interval between two packets - """ - PeriodicSenderThread.__init__(self, sock, pkt, interval) - - -def _check_response(resp, verbose): +# Helper function +def _check_response(resp): + # type: (Optional[Packet]) -> bool if resp is None: - if verbose: - print("Timeout.") + log_automotive.debug("Timeout.") return False - if verbose: - resp.show() - return resp.sprintf("%GMLAN.service%") != "NegativeResponse" - + log_automotive.debug("%s", repr(resp)) + return resp.service != 0x7f # NegativeResponse -def _send_and_check_response(sock, req, timeout, verbose): - if verbose: - print("Sending %s" % repr(req)) - resp = sock.sr1(req, timeout=timeout, verbose=0) - return _check_response(resp, verbose) +class GMLAN_TesterPresentSender(PeriodicSenderThread): -def GMLAN_InitDiagnostics(sock, broadcastsocket=None, timeout=None, - verbose=None, retry=0): - """Send messages to put an ECU into an diagnostic/programming state. + def __init__(self, sock, pkt=GMLAN(service="TesterPresent"), interval=2): + # type: (SuperSocket, Packet, int) -> None + """ Thread to send GMLAN TesterPresent packets periodically - Args: - sock: socket to send the message on. - broadcast: socket for broadcasting. If provided some message will be - sent as broadcast. Recommended when used on a network with - several ECUs. - timeout: timeout for sending, receiving or sniffing packages. - verbose: set verbosity level - retry: number of retries in case of failure. + :param sock: socket where packet is sent periodically + :param pkt: packet to send + :param interval: interval between two packets + """ + PeriodicSenderThread.__init__(self, sock, pkt, interval) - Returns true on success. + def run(self): + # type: () -> None + while not self._stopped.is_set() and not self._socket.closed: + for p in self._pkts: + self._socket.sr1(p, verbose=False, timeout=0.1) + self._stopped.wait(timeout=self._interval) + if self._stopped.is_set() or self._socket.closed: + break + + +def GMLAN_InitDiagnostics( + sock, # type: SuperSocket + broadcast_socket=None, # type: Optional[SuperSocket] + timeout=1, # type: int + retry=0, # type: int + unittest=False # type: bool +): + # type: (...) -> bool + """ Send messages to put an ECU into diagnostic/programming state. + + :param sock: socket for communication. + :param broadcast_socket: socket for broadcasting. If provided some message + will be sent as broadcast. Recommended when used + on a network with several ECUs. + :param timeout: timeout for sending, receiving or sniffing packages. + :param retry: number of retries in case of failure. + :param unittest: disable delays + :return: True on success else False """ - if verbose is None: - verbose = conf.verb + + # Helper function + def _send_and_check_response(sock, req, timeout): + # type: (SuperSocket, Packet, int) -> bool + log_automotive.debug("Sending %s", repr(req)) + resp = sock.sr1(req, timeout=timeout, verbose=False) + return _check_response(resp) + retry = abs(retry) while retry >= 0: @@ -86,164 +106,166 @@ def GMLAN_InitDiagnostics(sock, broadcastsocket=None, timeout=None, # DisableNormalCommunication p = GMLAN(service="DisableNormalCommunication") - if broadcastsocket is None: - if not _send_and_check_response(sock, p, timeout, verbose): + if broadcast_socket is None: + if not _send_and_check_response(sock, p, timeout): continue else: - if verbose: - print("Sending %s as broadcast" % repr(p)) - broadcastsocket.send(p) - time.sleep(0.05) + log_automotive.debug("Sending %s as broadcast", repr(p)) + broadcast_socket.send(p) + + if not unittest: + time.sleep(0.05) # ReportProgrammedState p = GMLAN(service="ReportProgrammingState") - if not _send_and_check_response(sock, p, timeout, verbose): + if not _send_and_check_response(sock, p, timeout): continue # ProgrammingMode requestProgramming p = GMLAN() / GMLAN_PM(subfunction="requestProgrammingMode") - if not _send_and_check_response(sock, p, timeout, verbose): + if not _send_and_check_response(sock, p, timeout): continue - time.sleep(0.05) + + if not unittest: + time.sleep(0.05) # InitiateProgramming enableProgramming # No response expected p = GMLAN() / GMLAN_PM(subfunction="enableProgrammingMode") - if verbose: - print("Sending %s" % repr(p)) - sock.send(p) - time.sleep(0.05) + log_automotive.debug("Sending %s", repr(p)) + sock.sr1(p, timeout=0.001, verbose=False) return True return False -def GMLAN_GetSecurityAccess(sock, keyFunction, level=1, timeout=None, - verbose=None, retry=0): - """Authenticate on ECU. Implements Seey-Key procedure. - - Args: - sock: socket to send the message on. - keyFunction: function implementing the key algorithm. - level: level of access - timeout: timeout for sending, receiving or sniffing packages. - verbose: set verbosity level - retry: number of retries in case of failure. - - Returns true on success. +def GMLAN_GetSecurityAccess( + sock, # type: SuperSocket + key_function, # type: Callable[[int], int] + level=1, # type: int + timeout=None, # type: Optional[int] + retry=0, # type: int + unittest=False # type: bool +): + # type: (...) -> bool + """ Authenticate on ECU. Implements Seey-Key procedure. + + :param sock: socket to send the message on. + :param key_function: function implementing the key algorithm. + :param level: level of access + :param timeout: timeout for sending, receiving or sniffing packages. + :param retry: number of retries in case of failure. + :param unittest: disable internal delays + :return: True on success. """ - if verbose is None: - verbose = conf.verb retry = abs(retry) + if key_function is None: + return False + if level % 2 == 0: - warning("Parameter Error: Level must be an odd number.") + log_automotive.warning("Parameter Error: Level must be an odd number.") return False while retry >= 0: retry -= 1 request = GMLAN() / GMLAN_SA(subfunction=level) - if verbose: - print("Requesting seed..") - resp = sock.sr1(request, timeout=timeout, verbose=0) - if not _check_response(resp, verbose): - if verbose: - print("Negative Response.") + log_automotive.debug("Requesting seed..") + resp = sock.sr1(request, timeout=timeout, verbose=False) + if not _check_response(resp): + if resp is not None and resp.returnCode == 0x37 and retry: + log_automotive.debug("RequiredTimeDelayNotExpired. Wait 10s.") + if not unittest: + time.sleep(10) + log_automotive.debug("Negative Response.") continue - seed = resp.securitySeed + seed = cast(Packet, resp).securitySeed if seed == 0: - if verbose: - print("ECU security already unlocked. (seed is 0x0000)") + log_automotive.debug("ECU security already unlocked. (seed is 0x0000)") return True keypkt = GMLAN() / GMLAN_SA(subfunction=level + 1, - securityKey=keyFunction(seed)) - if verbose: - print("Responding with key..") - resp = sock.sr1(keypkt, timeout=timeout, verbose=0) + securityKey=key_function(seed)) + log_automotive.debug("Responding with key..") + resp = sock.sr1(keypkt, timeout=timeout, verbose=False) if resp is None: - if verbose: - print("Timeout.") + log_automotive.debug("Timeout.") continue - if verbose: - resp.show() - if resp.sprintf("%GMLAN.service%") == "SecurityAccessPositiveResponse": # noqa: E501 - if verbose: - print("SecurityAccess granted.") + log_automotive.debug("%s", repr(resp)) + if resp.service == 0x67: + log_automotive.debug("SecurityAccess granted.") return True # Invalid Key - elif resp.sprintf("%GMLAN.service%") == "NegativeResponse" and \ - resp.sprintf("%GMLAN.returnCode%") == "InvalidKey": - if verbose: - print("Key invalid") + elif resp.service == 0x7f and resp.returnCode == 0x35: + log_automotive.debug("Key invalid") continue return False -def GMLAN_RequestDownload(sock, length, timeout=None, verbose=None, retry=0): - """Send RequestDownload message. +def GMLAN_RequestDownload(sock, length, timeout=None, retry=0): + # type: (SuperSocket, int, Optional[int], int) -> bool + """ Send RequestDownload message. - Usually used before calling TransferData. + Usually used before calling TransferData. - Args: - sock: socket to send the message on. - length: value for the message's parameter 'unCompressedMemorySize'. - timeout: timeout for sending, receiving or sniffing packages. - verbose: set verbosity level. - retry: number of retries in case of failure. - - Returns true on success. + :param sock: socket to send the message on. + :param length: value for the message's parameter 'unCompressedMemorySize'. + :param timeout: timeout for sending, receiving or sniffing packages. + :param retry: number of retries in case of failure. + :return: True on success """ - if verbose is None: - verbose = conf.verb retry = abs(retry) while retry >= 0: # RequestDownload pkt = GMLAN() / GMLAN_RD(memorySize=length) - resp = sock.sr1(pkt, timeout=timeout, verbose=0) - if _check_response(resp, verbose): + resp = sock.sr1(pkt, timeout=timeout, verbose=False) + if _check_response(resp): return True retry -= 1 - if retry >= 0 and verbose: - print("Retrying..") + if retry >= 0: + log_automotive.debug("Retrying..") return False -def GMLAN_TransferData(sock, addr, payload, maxmsglen=None, timeout=None, - verbose=None, retry=0): - """Send TransferData message. +def GMLAN_TransferData( + sock, # type: SuperSocket + addr, # type: int + payload, # type: bytes + maxmsglen=None, # type: Optional[int] + timeout=None, # type: Optional[int] + retry=0 # type: int +): + # type: (...) -> bool + """ Send TransferData message. Usually used after calling RequestDownload. - Args: - sock: socket to send the message on. - addr: destination memory address on the ECU. - payload: data to be sent. - maxmsglen: maximum length of a single iso-tp message. (default: - maximum length) - timeout: timeout for sending, receiving or sniffing packages. - verbose: set verbosity level. - retry: number of retries in case of failure. - - Returns true on success. + :param sock: socket to send the message on. + :param addr: destination memory address on the ECU. + :param payload: data to be sent. + :param maxmsglen: maximum length of a single iso-tp message. + default: maximum length + :param timeout: timeout for sending, receiving or sniffing packages. + :param retry: number of retries in case of failure. + :return: True on success. """ - if verbose is None: - verbose = conf.verb retry = abs(retry) startretry = retry scheme = conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] - if addr < 0 or addr >= 2**(8 * scheme): - warning("Error: Invalid address " + hex(addr) + " for scheme " + - str(scheme)) + if addr < 0 or addr >= 2 ** (8 * scheme): + log_automotive.warning("Error: Invalid address %s for scheme %s", + hex(addr), str(scheme)) return False # max size of dataRecord according to gmlan protocol if maxmsglen is None or maxmsglen <= 0 or maxmsglen > (4093 - scheme): maxmsglen = (4093 - scheme) + maxmsglen = cast(int, maxmsglen) + for i in range(0, len(payload), maxmsglen): retry = startretry while True: @@ -253,87 +275,97 @@ def GMLAN_TransferData(sock, addr, payload, maxmsglen=None, timeout=None, transdata = payload[i:] pkt = GMLAN() / GMLAN_TD(startingAddress=addr + i, dataRecord=transdata) - resp = sock.sr1(pkt, timeout=timeout, verbose=0) - if _check_response(resp, verbose): + resp = sock.sr1(pkt, timeout=timeout, verbose=False) + if _check_response(resp): break retry -= 1 if retry >= 0: - if verbose: - print("Retrying..") + log_automotive.debug("Retrying..") else: return False return True -def GMLAN_TransferPayload(sock, addr, payload, maxmsglen=None, timeout=None, - verbose=None, retry=0): - """Send data by using GMLAN services. - - Args: - sock: socket to send the data on. - addr: destination memory address on the ECU. - payload: data to be sent. - maxmsglen: maximum length of a single iso-tp message. (default: - maximum length) - timeout: timeout for sending, receiving or sniffing packages. - verbose: set verbosity level. - retry: number of retries in case of failure. - - Returns true on success. +def GMLAN_TransferPayload( + sock, # type: SuperSocket + addr, # type: int + payload, # type: bytes + maxmsglen=None, # type: Optional[int] + timeout=None, # type: Optional[int] + retry=0 # type: int +): + # type: (...) -> bool + """ Send data by using GMLAN services. + + :param sock: socket to send the data on. + :param addr: destination memory address on the ECU. + :param payload: data to be sent. + :param maxmsglen: maximum length of a single iso-tp message. + default: maximum length + :param timeout: timeout for sending, receiving or sniffing packages. + :param retry: number of retries in case of failure. + :return: True on success. """ if not GMLAN_RequestDownload(sock, len(payload), timeout=timeout, - verbose=verbose, retry=retry): + retry=retry): return False if not GMLAN_TransferData(sock, addr, payload, maxmsglen=maxmsglen, - timeout=timeout, verbose=verbose, retry=retry): + timeout=timeout, retry=retry): return False return True -def GMLAN_ReadMemoryByAddress(sock, addr, length, timeout=None, - verbose=None, retry=0): - """Read data from ECU memory. - - Args: - sock: socket to send the data on. - addr: source memory address on the ECU. - length: bytes to read - timeout: timeout for sending, receiving or sniffing packages. - verbose: set verbosity level. - retry: number of retries in case of failure. - - Returns the bytes read. +def GMLAN_ReadMemoryByAddress( + sock, # type: SuperSocket + addr, # type: int + length, # type: int + timeout=None, # type: Optional[int] + retry=0 # type: int +): + # type: (...) -> Optional[bytes] + """ Read data from ECU memory. + + :param sock: socket to send the data on. + :param addr: source memory address on the ECU. + :param length: bytes to read. + :param timeout: timeout for sending, receiving or sniffing packages. + :param retry: number of retries in case of failure. + :return: bytes red or None """ - if verbose is None: - verbose = conf.verb retry = abs(retry) scheme = conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] - if addr < 0 or addr >= 2**(8 * scheme): - warning("Error: Invalid address " + hex(addr) + " for scheme " + - str(scheme)) + if addr < 0 or addr >= 2 ** (8 * scheme): + log_automotive.warning("Error: Invalid address %s for scheme %s", + hex(addr), str(scheme)) return None # max size of dataRecord according to gmlan protocol if length <= 0 or length > (4094 - scheme): - warning("Error: Invalid length " + hex(length) + " for scheme " + - str(scheme) + ". Choose between 0x1 and " + hex(4094 - scheme)) + log_automotive.warning("Error: Invalid length %s for scheme %s. " + "Choose between 0x1 and %s", + hex(length), str(scheme), hex(4094 - scheme)) return None while retry >= 0: # RequestDownload pkt = GMLAN() / GMLAN_RMBA(memoryAddress=addr, memorySize=length) - resp = sock.sr1(pkt, timeout=timeout, verbose=0) - if _check_response(resp, verbose): - return resp.dataRecord + resp = sock.sr1(pkt, timeout=timeout, verbose=False) + if _check_response(resp): + return cast(Packet, resp).dataRecord retry -= 1 - if retry >= 0 and verbose: - print("Retrying..") + if retry >= 0: + log_automotive.debug("Retrying..") return None def GMLAN_BroadcastSocket(interface): - """Returns a GMLAN broadcast socket using interface.""" - return ISOTPSocket(interface, sid=0x101, did=0x0, basecls=GMLAN, - extended_addr=0xfe) + # type: (str) -> SuperSocket + """ Returns a GMLAN broadcast socket using interface. + + :param interface: interface name + :return: ISOTPSocket configured as GMLAN Broadcast Socket + """ + return ISOTPSocket(interface, tx_id=0x101, rx_id=0x0, basecls=GMLAN, + ext_address=0xfe, padding=True) diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py new file mode 100644 index 00000000000..019319ee10f --- /dev/null +++ b/scapy/contrib/automotive/kwp.py @@ -0,0 +1,1167 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = Keyword Protocol 2000 (KWP2000) / ISO 14230 +# scapy.contrib.status = loads + +import struct + +from scapy.fields import ( + BitField, + ByteEnumField, + ByteField, + ConditionalField, + MayEnd, + ObservableDict, + StrField, + X3BytesField, + XByteEnumField, + XByteField, + XShortEnumField, +) +from scapy.packet import Packet, NoPayload, bind_layers +from scapy.config import conf +from scapy.error import log_loading +from scapy.utils import PeriodicSenderThread +from scapy.plist import _PacketIterable # noqa: F401 +from scapy.contrib.isotp import ISOTP +from scapy.compat import orb + +from typing import ( # noqa: F401 + Any, + Dict, + Type, +) + + +try: + if conf.contribs['KWP']['treat-response-pending-as-answer']: + pass +except KeyError: + log_loading.info("Specify \"conf.contribs['KWP'] = " + "{'treat-response-pending-as-answer': True}\" to treat " + "a negative response 'requestCorrectlyReceived-" + "ResponsePending' as answer of a request. \n" + "The default value is False.") + conf.contribs['KWP'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} + + +def _kwp_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['KWP']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`KWP` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`KWP` packet, preventing a duplicate + service byte when sub-packets are stacked (``KWP()/KWP_SDS()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + """ + if not conf.contribs['KWP'].get('single_layer_mode', False): + return False + if conf.contribs['KWP'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, KWP) + return True + + +class KWP(ISOTP): + services = ObservableDict( + {0x10: 'StartDiagnosticSession', + 0x11: 'ECUReset', + 0x14: 'ClearDiagnosticInformation', + 0x17: 'ReadStatusOfDiagnosticTroubleCodes', + 0x18: 'ReadDiagnosticTroubleCodesByStatus', + 0x1A: 'ReadECUIdentification', + 0x21: 'ReadDataByLocalIdentifier', + 0x22: 'ReadDataByIdentifier', + 0x23: 'ReadMemoryByAddress', + 0x27: 'SecurityAccess', + 0x28: 'DisableNormalMessageTransmission', + 0x29: 'EnableNormalMessageTransmission', + 0x2C: 'DynamicallyDefineLocalIdentifier', + 0x2E: 'WriteDataByIdentifier', + 0x30: 'InputOutputControlByLocalIdentifier', + 0x31: 'StartRoutineByLocalIdentifier', + 0x32: 'StopRoutineByLocalIdentifier', + 0x33: 'RequestRoutineResultsByLocalIdentifier', + 0x34: 'RequestDownload', + 0x35: 'RequestUpload', + 0x36: 'TransferData', + 0x37: 'RequestTransferExit', + 0x3B: 'WriteDataByLocalIdentifier', + 0x3D: 'WriteMemoryByAddress', + 0x3E: 'TesterPresent', + 0x85: 'ControlDTCSetting', + 0x86: 'ResponseOnEvent', + 0x50: 'StartDiagnosticSessionPositiveResponse', + 0x51: 'ECUResetPositiveResponse', + 0x54: 'ClearDiagnosticInformationPositiveResponse', + 0x57: 'ReadStatusOfDiagnosticTroubleCodesPositiveResponse', + 0x58: 'ReadDiagnosticTroubleCodesByStatusPositiveResponse', + 0x5A: 'ReadECUIdentificationPositiveResponse', + 0x61: 'ReadDataByLocalIdentifierPositiveResponse', + 0x62: 'ReadDataByIdentifierPositiveResponse', + 0x63: 'ReadMemoryByAddressPositiveResponse', + 0x67: 'SecurityAccessPositiveResponse', + 0x68: 'DisableNormalMessageTransmissionPositiveResponse', + 0x69: 'EnableNormalMessageTransmissionPositiveResponse', + 0x6C: 'DynamicallyDefineLocalIdentifierPositiveResponse', + 0x6E: 'WriteDataByIdentifierPositiveResponse', + 0x70: 'InputOutputControlByLocalIdentifierPositiveResponse', + 0x71: 'StartRoutineByLocalIdentifierPositiveResponse', + 0x72: 'StopRoutineByLocalIdentifierPositiveResponse', + 0x73: 'RequestRoutineResultsByLocalIdentifierPositiveResponse', + 0x74: 'RequestDownloadPositiveResponse', + 0x75: 'RequestUploadPositiveResponse', + 0x76: 'TransferDataPositiveResponse', + 0x77: 'RequestTransferExitPositiveResponse', + 0x7B: 'WriteDataByLocalIdentifierPositiveResponse', + 0x7D: 'WriteMemoryByAddressPositiveResponse', + 0x7E: 'TesterPresentPositiveResponse', + 0xC5: 'ControlDTCSettingPositiveResponse', + 0xC6: 'ResponseOnEventPositiveResponse', + 0x7f: 'NegativeResponse'}) # type: Dict[int, str] + name = 'KWP' + fields_desc = [ + XByteEnumField('service', 0, services) + ] + + def answers(self, other): + # type: (Packet) -> bool + if not isinstance(other, type(self)): + return False + if self.service == 0x7f: + return bool(self.payload.answers(other)) + if self.service == (other.service + 0x40): + if isinstance(self.payload, NoPayload) or \ + isinstance(other.payload, NoPayload): + return len(self) <= len(other) + else: + return bool(self.payload.answers(other.payload)) + return False + + def hashret(self): + # type: () -> bytes + if self.service == 0x7f: + return struct.pack('B', self.requestServiceId & ~0x40) + else: + return struct.pack('B', self.service & ~0x40) + + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (bytes, Any, Any) -> type + """Dispatch to the correct KWP service class in single layer mode.""" + if conf.contribs['KWP'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + + +# ########################SDS################################### +class KWP_SDS(Packet): + diagnosticSessionTypes = ObservableDict({ + 0x81: 'defaultSession', + 0x85: 'programmingSession', + 0x89: 'standBySession', + 0x90: 'EcuPassiveSession', + 0x92: 'extendedDiagnosticSession'}) + name = 'StartDiagnosticSession' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x10, KWP.services), _kwp_slm), + ByteEnumField('diagnosticSession', 0, diagnosticSessionTypes) + ] + + +bind_layers(KWP, KWP_SDS, service=0x10) +KWP._service_cls[0x10] = KWP_SDS + + +class KWP_SDSPR(Packet): + name = 'StartDiagnosticSessionPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x50, KWP.services), _kwp_slm), + ByteEnumField('diagnosticSession', 0, + KWP_SDS.diagnosticSessionTypes), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_SDS) and \ + other.diagnosticSession == self.diagnosticSession + + +bind_layers(KWP, KWP_SDSPR, service=0x50) +KWP._service_cls[0x50] = KWP_SDSPR + + +# ######################### KWP_ER ################################### +class KWP_ER(Packet): + resetModes = { + 0x00: 'reserved', + 0x01: 'powerOnReset', + 0x82: 'nonvolatileMemoryReset'} + name = 'ECUReset' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x11, KWP.services), _kwp_slm), + ByteEnumField('resetMode', 0, resetModes) + ] + + +bind_layers(KWP, KWP_ER, service=0x11) +KWP._service_cls[0x11] = KWP_ER + + +class KWP_ERPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x51, KWP.services), _kwp_slm), + ] + name = 'ECUResetPositiveResponse' + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_ER) + + +bind_layers(KWP, KWP_ERPR, service=0x51) +KWP._service_cls[0x51] = KWP_ERPR + + +# ######################### KWP_SA ################################### +class KWP_SA(Packet): + name = 'SecurityAccess' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x27, KWP.services), _kwp_slm), + ByteField('accessMode', 0), + ConditionalField(StrField('key', b""), + lambda pkt: pkt.accessMode % 2 == 0) + ] + + +bind_layers(KWP, KWP_SA, service=0x27) +KWP._service_cls[0x27] = KWP_SA + + +class KWP_SAPR(Packet): + name = 'SecurityAccessPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x67, KWP.services), _kwp_slm), + ByteField('accessMode', 0), + ConditionalField(StrField('seed', b""), + lambda pkt: pkt.accessMode % 2 == 1), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_SA) \ + and other.accessMode == self.accessMode + + +bind_layers(KWP, KWP_SAPR, service=0x67) +KWP._service_cls[0x67] = KWP_SAPR + + +# ######################### KWP_IOCBLI ################################### +class KWP_IOCBLI(Packet): + name = 'InputOutputControlByLocalIdentifier' + inputOutputControlParameters = { + 0x00: "Return Control to ECU", + 0x01: "Report Current State", + 0x04: "Reset to Default", + 0x05: "Freeze Current State", + 0x07: "Short Term Adjustment", + 0x08: "Long Term Adjustment" + } + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x30, KWP.services), _kwp_slm), + XByteField('localIdentifier', 0), + XByteEnumField('inputOutputControlParameter', 0, + inputOutputControlParameters), + StrField('controlState', b"", fmt="B") + ] + + +bind_layers(KWP, KWP_IOCBLI, service=0x30) +KWP._service_cls[0x30] = KWP_IOCBLI + + +class KWP_IOCBLIPR(Packet): + name = 'InputOutputControlByLocalIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x70, KWP.services), _kwp_slm), + XByteField('localIdentifier', 0), + XByteEnumField('inputOutputControlParameter', 0, + KWP_IOCBLI.inputOutputControlParameters), + StrField('controlState', b"", fmt="B") + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_IOCBLI) \ + and other.localIdentifier == self.localIdentifier + + +bind_layers(KWP, KWP_IOCBLIPR, service=0x70) +KWP._service_cls[0x70] = KWP_IOCBLIPR + + +# ######################### KWP_DNMT ################################### +class KWP_DNMT(Packet): + responseTypes = { + 0x01: 'responseRequired', + 0x02: 'noResponse', + } + name = 'DisableNormalMessageTransmission' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x28, KWP.services), _kwp_slm), + ByteEnumField('responseRequired', 0, responseTypes) + ] + + +bind_layers(KWP, KWP_DNMT, service=0x28) +KWP._service_cls[0x28] = KWP_DNMT + + +class KWP_DNMTPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x68, KWP.services), _kwp_slm), + ] + name = 'DisableNormalMessageTransmissionPositiveResponse' + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_DNMT) + + +bind_layers(KWP, KWP_DNMTPR, service=0x68) +KWP._service_cls[0x68] = KWP_DNMTPR + + +# ######################### KWP_ENMT ################################### +class KWP_ENMT(Packet): + responseTypes = { + 0x01: 'responseRequired', + 0x02: 'noResponse', + } + name = 'EnableNormalMessageTransmission' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x29, KWP.services), _kwp_slm), + ByteEnumField('responseRequired', 1, responseTypes) + ] + + +bind_layers(KWP, KWP_ENMT, service=0x29) +KWP._service_cls[0x29] = KWP_ENMT + + +class KWP_ENMTPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x69, KWP.services), _kwp_slm), + ] + name = 'EnableNormalMessageTransmissionPositiveResponse' + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_DNMT) + + +bind_layers(KWP, KWP_ENMTPR, service=0x69) +KWP._service_cls[0x69] = KWP_ENMTPR + + +# ######################### KWP_TP ################################### +class KWP_TP(Packet): + responseTypes = { + 0x01: 'responseRequired', + 0x02: 'noResponse', + } + name = 'TesterPresent' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3e, KWP.services), _kwp_slm), + ByteEnumField('responseRequired', 1, responseTypes) + ] + + +bind_layers(KWP, KWP_TP, service=0x3e) +KWP._service_cls[0x3e] = KWP_TP + + +class KWP_TPPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7e, KWP.services), _kwp_slm), + ] + name = 'TesterPresentPositiveResponse' + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_TP) + + +bind_layers(KWP, KWP_TPPR, service=0x7e) +KWP._service_cls[0x7e] = KWP_TPPR + + +# ######################### KWP_CDTCS ################################### +class KWP_CDTCS(Packet): + responseTypes = { + 0x01: 'responseRequired', + 0x02: 'noResponse', + } + DTCGroups = { + 0x0000: 'allPowertrainDTCs', + 0x4000: 'allChassisDTCs', + 0x8000: 'allBodyDTCs', + 0xC000: 'allNetworkDTCs', + 0xFF00: 'allDTCs' + } + DTCSettingModes = { + 0: 'Reserved', + 1: 'on', + 2: 'off' + } + name = 'ControlDTCSetting' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x85, KWP.services), _kwp_slm), + ByteEnumField('responseRequired', 1, responseTypes), + XShortEnumField('groupOfDTC', 0, DTCGroups), + ByteEnumField('DTCSettingMode', 0, DTCSettingModes), + ] + + +bind_layers(KWP, KWP_CDTCS, service=0x85) +KWP._service_cls[0x85] = KWP_CDTCS + + +class KWP_CDTCSPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc5, KWP.services), _kwp_slm), + ] + name = 'ControlDTCSettingPositiveResponse' + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_CDTCS) + + +bind_layers(KWP, KWP_CDTCSPR, service=0xc5) +KWP._service_cls[0xc5] = KWP_CDTCSPR + + +# ######################### KWP_ROE ################################### +class KWP_ROE(Packet): + responseTypes = { + 0x01: 'responseRequired', + 0x02: 'noResponse', + } + eventWindowTimes = { + 0x00: 'reserved', + 0x01: 'testerPresentRequired', + 0x02: 'infiniteTimeToResponse', + 0x80: 'noEventWindow' + } + eventTypes = { + 0x80: 'reportActivatedEvents', + 0x81: 'stopResponseOnEvent', + 0x82: 'onNewDTC', + 0x83: 'onTimerInterrupt', + 0x84: 'onChangeOfRecordValue', + 0xA0: 'onComparisonOfValues' + } + name = 'ResponseOnEvent' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x86, KWP.services), _kwp_slm), + ByteEnumField('responseRequired', 1, responseTypes), + ByteEnumField('eventWindowTime', 0, eventWindowTimes), + MayEnd(ByteEnumField('eventType', 0, eventTypes)), + # XXX Is this MayEnd correct? + ByteField('eventParameter', 0), + ByteEnumField('serviceToRespond', 0, KWP.services), + ByteField('serviceParameter', 0) + ] + + +bind_layers(KWP, KWP_ROE, service=0x86) +KWP._service_cls[0x86] = KWP_ROE + + +class KWP_ROEPR(Packet): + name = 'ResponseOnEventPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc6, KWP.services), _kwp_slm), + ByteField("numberOfActivatedEvents", 0), + MayEnd(ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes)), + # XXX Is this MayEnd correct? + ByteEnumField('eventType', 0, KWP_ROE.eventTypes), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_ROE) \ + and other.eventType == self.eventType + + +bind_layers(KWP, KWP_ROEPR, service=0xc6) +KWP._service_cls[0xc6] = KWP_ROEPR + + +# ######################### KWP_RDBLI ################################### +class KWP_RDBLI(Packet): + localIdentifiers = ObservableDict({ + 0xE0: "Development Data", + 0xE1: "ECU Serial Number", + 0xE2: "DBCom Data", + 0xE3: "Operating System Version", + 0xE4: "Ecu Reprogramming Identification", + 0xE5: "Vehicle Information", + 0xE6: "Flash Info 1", + 0xE7: "Flash Info 2", + 0xE8: "System Diagnostic general parameter data", + 0xE9: "System Diagnostic global parameter data", + 0xEA: "Ecu Configuration", + 0xEB: "Diagnostic Protocol Information" + }) + name = 'ReadDataByLocalIdentifier' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x21, KWP.services), _kwp_slm), + XByteEnumField('recordLocalIdentifier', 0, localIdentifiers) + ] + + +bind_layers(KWP, KWP_RDBLI, service=0x21) +KWP._service_cls[0x21] = KWP_RDBLI + + +class KWP_RDBLIPR(Packet): + name = 'ReadDataByLocalIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x61, KWP.services), _kwp_slm), + XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RDBLI) \ + and self.recordLocalIdentifier == other.recordLocalIdentifier + + +bind_layers(KWP, KWP_RDBLIPR, service=0x61) +KWP._service_cls[0x61] = KWP_RDBLIPR + + +# ######################### KWP_WDBLI ################################### +class KWP_WDBLI(Packet): + name = 'WriteDataByLocalIdentifier' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3b, KWP.services), _kwp_slm), + XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) + ] + + +bind_layers(KWP, KWP_WDBLI, service=0x3b) +KWP._service_cls[0x3b] = KWP_WDBLI + + +class KWP_WDBLIPR(Packet): + name = 'WriteDataByLocalIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7b, KWP.services), _kwp_slm), + XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_WDBLI) \ + and self.recordLocalIdentifier == other.recordLocalIdentifier + + +bind_layers(KWP, KWP_WDBLIPR, service=0x7b) +KWP._service_cls[0x7b] = KWP_WDBLIPR + + +# ######################### KWP_RDBI ################################### +class KWP_RDBI(Packet): + dataIdentifiers = ObservableDict() + name = 'ReadDataByIdentifier' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x22, KWP.services), _kwp_slm), + XShortEnumField('identifier', 0, dataIdentifiers) + ] + + +bind_layers(KWP, KWP_RDBI, service=0x22) +KWP._service_cls[0x22] = KWP_RDBI + + +class KWP_RDBIPR(Packet): + name = 'ReadDataByIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x62, KWP.services), _kwp_slm), + XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RDBI) \ + and self.identifier == other.identifier + + +bind_layers(KWP, KWP_RDBIPR, service=0x62) +KWP._service_cls[0x62] = KWP_RDBIPR + + +# ######################### KWP_RMBA ################################### +class KWP_RMBA(Packet): + name = 'ReadMemoryByAddress' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x23, KWP.services), _kwp_slm), + X3BytesField('memoryAddress', 0), + ByteField('memorySize', 0) + ] + + +bind_layers(KWP, KWP_RMBA, service=0x23) +KWP._service_cls[0x23] = KWP_RMBA + + +class KWP_RMBAPR(Packet): + name = 'ReadMemoryByAddressPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x63, KWP.services), _kwp_slm), + StrField('dataRecord', b"", fmt="B") + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RMBA) + + +bind_layers(KWP, KWP_RMBAPR, service=0x63) +KWP._service_cls[0x63] = KWP_RMBAPR + + +# ######################### KWP_DDLI ################################### +# TODO: Implement correct interpretation here, +# instead of using just the dataRecord +class KWP_DDLI(Packet): + name = 'DynamicallyDefineLocalIdentifier' + definitionModes = {0x1: "defineByLocalIdentifier", + 0x2: "defineByMemoryAddress", + 0x3: "defineByIdentifier", + 0x4: "clearDynamicallyDefinedLocalIdentifier"} + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2c, KWP.services), _kwp_slm), + XByteField('dynamicallyDefineLocalIdentifier', 0), + ByteEnumField('definitionMode', 0, definitionModes), + StrField('dataRecord', b"", fmt="B") + ] + + +bind_layers(KWP, KWP_DDLI, service=0x2c) +KWP._service_cls[0x2c] = KWP_DDLI + + +class KWP_DDLIPR(Packet): + name = 'DynamicallyDefineLocalIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6c, KWP.services), _kwp_slm), + XByteField('dynamicallyDefineLocalIdentifier', 0) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_DDLI) and \ + other.dynamicallyDefineLocalIdentifier == self.dynamicallyDefineLocalIdentifier # noqa: E501 + + +bind_layers(KWP, KWP_DDLIPR, service=0x6c) +KWP._service_cls[0x6c] = KWP_DDLIPR + + +# ######################### KWP_WDBI ################################### +class KWP_WDBI(Packet): + name = 'WriteDataByIdentifier' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2e, KWP.services), _kwp_slm), + XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers) + ] + + +bind_layers(KWP, KWP_WDBI, service=0x2e) +KWP._service_cls[0x2e] = KWP_WDBI + + +class KWP_WDBIPR(Packet): + name = 'WriteDataByIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6e, KWP.services), _kwp_slm), + XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_WDBI) \ + and other.identifier == self.identifier + + +bind_layers(KWP, KWP_WDBIPR, service=0x6e) +KWP._service_cls[0x6e] = KWP_WDBIPR + + +# ######################### KWP_WMBA ################################### +class KWP_WMBA(Packet): + name = 'WriteMemoryByAddress' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3d, KWP.services), _kwp_slm), + X3BytesField('memoryAddress', 0), + ByteField('memorySize', 0), + StrField('dataRecord', b'', fmt="B") + ] + + +bind_layers(KWP, KWP_WMBA, service=0x3d) +KWP._service_cls[0x3d] = KWP_WMBA + + +class KWP_WMBAPR(Packet): + name = 'WriteMemoryByAddressPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7d, KWP.services), _kwp_slm), + X3BytesField('memoryAddress', 0) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_WMBA) and \ + other.memoryAddress == self.memoryAddress + + +bind_layers(KWP, KWP_WMBAPR, service=0x7d) +KWP._service_cls[0x7d] = KWP_WMBAPR + + +# ######################### KWP_CDI ################################### +class KWP_CDI(Packet): + DTCGroups = { + 0x0000: 'allPowertrainDTCs', + 0x4000: 'allChassisDTCs', + 0x8000: 'allBodyDTCs', + 0xC000: 'allNetworkDTCs', + 0xFF00: 'allDTCs' + } + name = 'ClearDiagnosticInformation' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x14, KWP.services), _kwp_slm), + XShortEnumField('groupOfDTC', 0, DTCGroups) + ] + + +bind_layers(KWP, KWP_CDI, service=0x14) +KWP._service_cls[0x14] = KWP_CDI + + +class KWP_CDIPR(Packet): + name = 'ClearDiagnosticInformationPositiveResponse' + + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x54, KWP.services), _kwp_slm), + XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_CDI) and \ + self.groupOfDTC == other.groupOfDTC + + +bind_layers(KWP, KWP_CDIPR, service=0x54) +KWP._service_cls[0x54] = KWP_CDIPR + + +# ######################### KWP_RSODTC ################################### +class KWP_RSODTC(Packet): + name = 'ReadStatusOfDiagnosticTroubleCodes' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x17, KWP.services), _kwp_slm), + XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) + ] + + +bind_layers(KWP, KWP_RSODTC, service=0x17) +KWP._service_cls[0x17] = KWP_RSODTC + + +class KWP_RSODTCPR(Packet): + name = 'ReadStatusOfDiagnosticTroubleCodesPositiveResponse' + + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x57, KWP.services), _kwp_slm), + ByteField('numberOfDTC', 0), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RSODTC) + + +bind_layers(KWP, KWP_RSODTCPR, service=0x57) +KWP._service_cls[0x57] = KWP_RSODTCPR + + +# ######################### KWP_RECUI ################################### +class KWP_RECUI(Packet): + name = 'ReadECUIdentification' + localIdentifiers = ObservableDict({ + 0x86: "DCS ECU Identification", + 0x87: "DCX / MMC ECU Identification", + 0x88: "VIN (Original)", + 0x89: "Diagnostic Variant Code", + 0x90: "VIN (Current)", + 0x96: "Calibration Identification", + 0x97: "Calibration Verification Number", + 0x9A: "ECU Code Fingerprint", + 0x98: "ECU Data Fingerprint", + 0x9C: "ECU Code Software Identification", + 0x9D: "ECU Data Software Identification", + 0x9E: "ECU Boot Software Identification", + 0x9F: "ECU Boot Fingerprint" + }) + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x1a, KWP.services), _kwp_slm), + XByteEnumField('localIdentifier', 0, localIdentifiers) + ] + + +bind_layers(KWP, KWP_RECUI, service=0x1a) +KWP._service_cls[0x1a] = KWP_RECUI + + +class KWP_RECUIPR(Packet): + name = 'ReadECUIdentificationPositiveResponse' + + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x5a, KWP.services), _kwp_slm), + XByteEnumField('localIdentifier', 0, KWP_RECUI.localIdentifiers) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RECUI) and \ + self.localIdentifier == other.localIdentifier + + +bind_layers(KWP, KWP_RECUIPR, service=0x5a) +KWP._service_cls[0x5a] = KWP_RECUIPR + + +# ######################### KWP_SRBLI ################################### +class KWP_SRBLI(Packet): + routineLocalIdentifiers = ObservableDict({ + 0xE0: "FlashEraseRoutine", + 0xE1: "FlashCheckRoutine", + 0xE2: "Tell-TaleRetentionStack", + 0xE3: "RequestDTCsFromShadowErrorMemory", + 0xE4: "RequestEnvironmentDataFromShadowErrorMemory", + 0xE5: "RequestEventInformation", + 0xE6: "RequestEventEnvironmentData", + 0xE7: "RequestSoftwareModuleInformation", + 0xE8: "ClearTell-TaleRetentionStack", + 0xE9: "ClearEventInformation" + }) + name = 'StartRoutineByLocalIdentifier' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x31, KWP.services), _kwp_slm), + XByteEnumField('routineLocalIdentifier', 0, routineLocalIdentifiers) + ] + + +bind_layers(KWP, KWP_SRBLI, service=0x31) +KWP._service_cls[0x31] = KWP_SRBLI + + +class KWP_SRBLIPR(Packet): + name = 'StartRoutineByLocalIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x71, KWP.services), _kwp_slm), + XByteEnumField('routineLocalIdentifier', 0, + KWP_SRBLI.routineLocalIdentifiers) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_SRBLI) \ + and other.routineLocalIdentifier == self.routineLocalIdentifier + + +bind_layers(KWP, KWP_SRBLIPR, service=0x71) +KWP._service_cls[0x71] = KWP_SRBLIPR + + +# ######################### KWP_STRBLI ################################### +class KWP_STRBLI(Packet): + name = 'StopRoutineByLocalIdentifier' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x32, KWP.services), _kwp_slm), + XByteEnumField('routineLocalIdentifier', 0, + KWP_SRBLI.routineLocalIdentifiers) + ] + + +bind_layers(KWP, KWP_STRBLI, service=0x32) +KWP._service_cls[0x32] = KWP_STRBLI + + +class KWP_STRBLIPR(Packet): + name = 'StopRoutineByLocalIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x72, KWP.services), _kwp_slm), + XByteEnumField('routineLocalIdentifier', 0, + KWP_SRBLI.routineLocalIdentifiers) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_STRBLI) \ + and other.routineLocalIdentifier == self.routineLocalIdentifier + + +bind_layers(KWP, KWP_STRBLIPR, service=0x72) +KWP._service_cls[0x72] = KWP_STRBLIPR + + +# ######################### KWP_RRRBLI ################################### +class KWP_RRRBLI(Packet): + name = 'RequestRoutineResultsByLocalIdentifier' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x33, KWP.services), _kwp_slm), + XByteEnumField('routineLocalIdentifier', 0, + KWP_SRBLI.routineLocalIdentifiers) + ] + + +bind_layers(KWP, KWP_RRRBLI, service=0x33) +KWP._service_cls[0x33] = KWP_RRRBLI + + +class KWP_RRRBLIPR(Packet): + name = 'RequestRoutineResultsByLocalIdentifierPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x73, KWP.services), _kwp_slm), + XByteEnumField('routineLocalIdentifier', 0, + KWP_SRBLI.routineLocalIdentifiers) + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RRRBLI) \ + and other.routineLocalIdentifier == self.routineLocalIdentifier + + +bind_layers(KWP, KWP_RRRBLIPR, service=0x73) +KWP._service_cls[0x73] = KWP_RRRBLIPR + + +# ######################### KWP_RD ################################### +class KWP_RD(Packet): + name = 'RequestDownload' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x34, KWP.services), _kwp_slm), + X3BytesField('memoryAddress', 0), + BitField('compression', 0, 4), + BitField('encryption', 0, 4), + X3BytesField('uncompressedMemorySize', 0) + ] + + +bind_layers(KWP, KWP_RD, service=0x34) +KWP._service_cls[0x34] = KWP_RD + + +class KWP_RDPR(Packet): + name = 'RequestDownloadPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x74, KWP.services), _kwp_slm), + StrField('maxNumberOfBlockLength', b"", fmt="B"), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RD) + + +bind_layers(KWP, KWP_RDPR, service=0x74) +KWP._service_cls[0x74] = KWP_RDPR + + +# ######################### KWP_RU ################################### +class KWP_RU(Packet): + name = 'RequestUpload' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x35, KWP.services), _kwp_slm), + X3BytesField('memoryAddress', 0), + BitField('compression', 0, 4), + BitField('encryption', 0, 4), + X3BytesField('uncompressedMemorySize', 0) + ] + + +bind_layers(KWP, KWP_RU, service=0x35) +KWP._service_cls[0x35] = KWP_RU + + +class KWP_RUPR(Packet): + name = 'RequestUploadPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x75, KWP.services), _kwp_slm), + StrField('maxNumberOfBlockLength', b"", fmt="B"), + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RU) + + +bind_layers(KWP, KWP_RUPR, service=0x75) +KWP._service_cls[0x75] = KWP_RUPR + + +# ######################### KWP_TD ################################### +class KWP_TD(Packet): + name = 'TransferData' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x36, KWP.services), _kwp_slm), + ByteField('blockSequenceCounter', 0), + StrField('transferDataRequestParameter', b"", fmt="B") + ] + + +bind_layers(KWP, KWP_TD, service=0x36) +KWP._service_cls[0x36] = KWP_TD + + +class KWP_TDPR(Packet): + name = 'TransferDataPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x76, KWP.services), _kwp_slm), + ByteField('blockSequenceCounter', 0), + StrField('transferDataRequestParameter', b"", fmt="B") + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_TD) \ + and other.blockSequenceCounter == self.blockSequenceCounter + + +bind_layers(KWP, KWP_TDPR, service=0x76) +KWP._service_cls[0x76] = KWP_TDPR + + +# ######################### KWP_RTE ################################### +class KWP_RTE(Packet): + name = 'RequestTransferExit' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x37, KWP.services), _kwp_slm), + StrField('transferDataRequestParameter', b"", fmt="B") + ] + + +bind_layers(KWP, KWP_RTE, service=0x37) +KWP._service_cls[0x37] = KWP_RTE + + +class KWP_RTEPR(Packet): + name = 'RequestTransferExitPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x77, KWP.services), _kwp_slm), + StrField('transferDataRequestParameter', b"", fmt="B") + ] + + def answers(self, other): + # type: (Packet) -> int + return isinstance(other, KWP_RTE) + + +bind_layers(KWP, KWP_RTEPR, service=0x77) +KWP._service_cls[0x77] = KWP_RTEPR + + +# ######################### KWP_NR ################################### +class KWP_NR(Packet): + negativeResponseCodes = { + 0x00: 'positiveResponse', + 0x10: 'generalReject', + 0x11: 'serviceNotSupported', + 0x12: 'subFunctionNotSupported-InvalidFormat', + 0x21: 'busyRepeatRequest', + 0x22: 'conditionsNotCorrect-RequestSequenceError', + 0x23: 'routineNotComplete', + 0x31: 'requestOutOfRange', + 0x33: 'securityAccessDenied-SecurityAccessRequested', + 0x35: 'invalidKey', + 0x36: 'exceedNumberOfAttempts', + 0x37: 'requiredTimeDelayNotExpired', + 0x40: 'downloadNotAccepted', + 0x50: 'uploadNotAccepted', + 0x71: 'transferSuspended', + 0x78: 'requestCorrectlyReceived-ResponsePending', + 0x80: 'subFunctionNotSupportedInActiveDiagnosticSession', + 0x9A: 'dataDecompressionFailed', + 0x9B: 'dataDecryptionFailed', + 0xA0: 'EcuNotResponding', + 0xA1: 'EcuAddressUnknown' + } + name = 'NegativeResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7f, KWP.services), _kwp_slm), + MayEnd(XByteEnumField('requestServiceId', 0, KWP.services)), + # XXX Is this MayEnd correct? + ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) + ] + + def answers(self, other): + # type: (Packet) -> int + return self.requestServiceId == other.service and \ + (self.negativeResponseCode != 0x78 or + conf.contribs['KWP']['treat-response-pending-as-answer']) + + +bind_layers(KWP, KWP_NR, service=0x7f) +KWP._service_cls[0x7f] = KWP_NR + + +# ################################################################## +# ######################## UTILS ################################### +# ################################################################## + +class KWP_TesterPresentSender(PeriodicSenderThread): + def __init__(self, sock, pkt=KWP() / KWP_TP(responseRequired=0x02), + interval=2): + # type: (Any, _PacketIterable, float) -> None + """ Thread that sends TesterPresent packets periodically + + :param sock: socket where packet is sent periodically + :param pkt: packet to send + :param interval: interval between two packets + """ + PeriodicSenderThread.__init__(self, sock, pkt, interval) + + def run(self): + # type: () -> None + while not self._stopped.is_set(): + for p in self._pkts: + self._socket.sr1(p, timeout=0.3, verbose=False) + self._stopped.wait(timeout=self._interval) + if self._stopped.is_set() or self._socket.closed: + break diff --git a/scapy/contrib/automotive/obd/__init__.py b/scapy/contrib/automotive/obd/__init__.py index 9d7ebf0032f..c07294b07d7 100644 --- a/scapy/contrib/automotive/obd/__init__.py +++ b/scapy/contrib/automotive/obd/__init__.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/iid/__init__.py b/scapy/contrib/automotive/obd/iid/__init__.py index 9d7ebf0032f..c07294b07d7 100644 --- a/scapy/contrib/automotive/obd/iid/__init__.py +++ b/scapy/contrib/automotive/obd/iid/__init__.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/iid/iids.py b/scapy/contrib/automotive/obd/iid/iids.py index 0e820f81554..5c85c2c4834 100644 --- a/scapy/contrib/automotive/obd/iid/iids.py +++ b/scapy/contrib/automotive/obd/iid/iids.py @@ -1,16 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip -from scapy.fields import FieldLenField, FieldListField, StrFixedLenField, \ - ByteField, ShortField, FlagsField, XByteField, PacketListField +from scapy.fields import ( + ConditionalField, FieldLenField, FieldListField, StrFixedLenField, + ByteField, ShortField, FlagsField, XByteEnumField, XByteField, + PacketListField +) from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S09 +from scapy.contrib.automotive.obd.services import OBD_S09, _OBD_SERVICES, _obd_slm # See https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_09 @@ -26,11 +29,12 @@ class OBD_S09_PR_Record(Packet): class OBD_S09_PR(Packet): name = "Infotype IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x49, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S09_PR_Record) ] def answers(self, other): - return other.__class__ == OBD_S09 \ + return isinstance(other, OBD_S09) \ and all(r.iid in other.iid for r in self.data_records) diff --git a/scapy/contrib/automotive/obd/mid/__init__.py b/scapy/contrib/automotive/obd/mid/__init__.py index 9d7ebf0032f..c07294b07d7 100644 --- a/scapy/contrib/automotive/obd/mid/__init__.py +++ b/scapy/contrib/automotive/obd/mid/__init__.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/mid/mids.py b/scapy/contrib/automotive/obd/mid/mids.py index 634805abf23..0acd4a4df1c 100644 --- a/scapy/contrib/automotive/obd/mid/mids.py +++ b/scapy/contrib/automotive/obd/mid/mids.py @@ -1,16 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip -from scapy.fields import FlagsField, ScalingField, ByteEnumField, \ - MultipleTypeField, ShortField, ShortEnumField, PacketListField +from scapy.fields import ( + ConditionalField, FlagsField, ScalingField, ByteEnumField, + XByteEnumField, MultipleTypeField, ShortField, ShortEnumField, + PacketListField +) from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S06 +from scapy.contrib.automotive.obd.services import OBD_S06, _OBD_SERVICES, _obd_slm def _unit_and_scaling_fields(name): @@ -457,11 +460,12 @@ class OBD_S06_PR_Record(Packet): class OBD_S06_PR(Packet): name = "On-Board monitoring IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x46, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S06_PR_Record) ] def answers(self, other): - return other.__class__ == OBD_S06 \ + return isinstance(other, OBD_S06) \ and all(r.mid in other.mid for r in self.data_records) diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index 0983b6303da..005264f3559 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.description = On Board Diagnostic Protocol (OBD-II) # scapy.contrib.status = loads @@ -14,47 +14,34 @@ from scapy.contrib.automotive.obd.pid.pids import * from scapy.contrib.automotive.obd.tid.tids import * from scapy.contrib.automotive.obd.services import * -from scapy.packet import bind_layers, NoPayload -from scapy.error import log_loading +from scapy.contrib.automotive.obd.services import _OBD_SERVICES +from scapy.packet import NoPayload, bind_layers from scapy.config import conf from scapy.fields import XByteEnumField from scapy.contrib.isotp import ISOTP +from scapy.compat import orb + +from typing import ( # noqa: F401 + Dict, + Type, +) try: if conf.contribs['OBD']['treat-response-pending-as-answer']: pass except KeyError: - log_loading.info("Specify \"conf.contribs['OBD'] = " - "{'treat-response-pending-as-answer': True}\" to treat " - "a negative response 'requestCorrectlyReceived-" - "ResponsePending' as answer of a request. \n" - "The default value is False.") - conf.contribs['OBD'] = {'treat-response-pending-as-answer': False} + # log_automotive.info("Specify \"conf.contribs['OBD'] = " + # "{'treat-response-pending-as-answer': True}\" to treat " + # "a negative response 'requestCorrectlyReceived-" + # "ResponsePending' as answer of a request. \n" + # "The default value is False.") + conf.contribs['OBD'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} class OBD(ISOTP): - services = { - 0x01: 'CurrentPowertrainDiagnosticDataRequest', - 0x02: 'PowertrainFreezeFrameDataRequest', - 0x03: 'EmissionRelatedDiagnosticTroubleCodesRequest', - 0x04: 'ClearResetDiagnosticTroubleCodesRequest', - 0x05: 'OxygenSensorMonitoringTestResultsRequest', - 0x06: 'OnBoardMonitoringTestResultsRequest', - 0x07: 'PendingEmissionRelatedDiagnosticTroubleCodesRequest', - 0x08: 'ControlOperationRequest', - 0x09: 'VehicleInformationRequest', - 0x0A: 'PermanentDiagnosticTroubleCodesRequest', - 0x41: 'CurrentPowertrainDiagnosticDataResponse', - 0x42: 'PowertrainFreezeFrameDataResponse', - 0x43: 'EmissionRelatedDiagnosticTroubleCodesResponse', - 0x44: 'ClearResetDiagnosticTroubleCodesResponse', - 0x45: 'OxygenSensorMonitoringTestResultsResponse', - 0x46: 'OnBoardMonitoringTestResultsResponse', - 0x47: 'PendingEmissionRelatedDiagnosticTroubleCodesResponse', - 0x48: 'ControlOperationResponse', - 0x49: 'VehicleInformationResponse', - 0x4A: 'PermanentDiagnosticTroubleCodesResponse', - 0x7f: 'NegativeResponse'} + services = _OBD_SERVICES name = "On-board diagnostics" @@ -80,26 +67,71 @@ def answers(self, other): return self.payload.answers(other.payload) return False + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct OBD service class in single layer mode.""" + if conf.contribs['OBD'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls -# Service Bindings bind_layers(OBD, OBD_S01, service=0x01) +OBD._service_cls[0x01] = OBD_S01 + bind_layers(OBD, OBD_S02, service=0x02) +OBD._service_cls[0x02] = OBD_S02 + bind_layers(OBD, OBD_S03, service=0x03) +OBD._service_cls[0x03] = OBD_S03 + bind_layers(OBD, OBD_S04, service=0x04) +OBD._service_cls[0x04] = OBD_S04 + bind_layers(OBD, OBD_S06, service=0x06) +OBD._service_cls[0x06] = OBD_S06 + bind_layers(OBD, OBD_S07, service=0x07) +OBD._service_cls[0x07] = OBD_S07 + bind_layers(OBD, OBD_S08, service=0x08) +OBD._service_cls[0x08] = OBD_S08 + bind_layers(OBD, OBD_S09, service=0x09) +OBD._service_cls[0x09] = OBD_S09 + bind_layers(OBD, OBD_S0A, service=0x0A) +OBD._service_cls[0x0A] = OBD_S0A bind_layers(OBD, OBD_S01_PR, service=0x41) +OBD._service_cls[0x41] = OBD_S01_PR + bind_layers(OBD, OBD_S02_PR, service=0x42) +OBD._service_cls[0x42] = OBD_S02_PR + bind_layers(OBD, OBD_S03_PR, service=0x43) +OBD._service_cls[0x43] = OBD_S03_PR + bind_layers(OBD, OBD_S04_PR, service=0x44) +OBD._service_cls[0x44] = OBD_S04_PR + bind_layers(OBD, OBD_S06_PR, service=0x46) +OBD._service_cls[0x46] = OBD_S06_PR + bind_layers(OBD, OBD_S07_PR, service=0x47) +OBD._service_cls[0x47] = OBD_S07_PR + bind_layers(OBD, OBD_S08_PR, service=0x48) +OBD._service_cls[0x48] = OBD_S08_PR + bind_layers(OBD, OBD_S09_PR, service=0x49) +OBD._service_cls[0x49] = OBD_S09_PR + bind_layers(OBD, OBD_S0A_PR, service=0x4A) +OBD._service_cls[0x4A] = OBD_S0A_PR + bind_layers(OBD, OBD_NR, service=0x7F) +OBD._service_cls[0x7F] = OBD_NR diff --git a/scapy/contrib/automotive/obd/packet.py b/scapy/contrib/automotive/obd/packet.py index be958298a01..f08d4691901 100644 --- a/scapy/contrib/automotive/obd/packet.py +++ b/scapy/contrib/automotive/obd/packet.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/pid/__init__.py b/scapy/contrib/automotive/obd/pid/__init__.py index 9d7ebf0032f..c07294b07d7 100644 --- a/scapy/contrib/automotive/obd/pid/__init__.py +++ b/scapy/contrib/automotive/obd/pid/__init__.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/pid/pids.py b/scapy/contrib/automotive/obd/pid/pids.py index cfdebf31c8c..9ae039a52e1 100644 --- a/scapy/contrib/automotive/obd/pid/pids.py +++ b/scapy/contrib/automotive/obd/pid/pids.py @@ -1,15 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip from scapy.packet import Packet, bind_layers -from scapy.fields import PacketListField +from scapy.fields import ConditionalField, PacketListField, XByteEnumField -from scapy.contrib.automotive.obd.services import OBD_S01, OBD_S02 +from scapy.contrib.automotive.obd.services import ( + OBD_S01, OBD_S02, _OBD_SERVICES, _obd_slm +) from scapy.contrib.automotive.obd.pid.pids_00_1F import * from scapy.contrib.automotive.obd.pid.pids_20_3F import * from scapy.contrib.automotive.obd.pid.pids_40_5F import * @@ -27,11 +29,12 @@ class OBD_S01_PR_Record(Packet): class OBD_S01_PR(Packet): name = "Parameter IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x41, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S01_PR_Record) ] def answers(self, other): - return other.__class__ == OBD_S01 \ + return isinstance(other, OBD_S01) \ and all(r.pid in other.pid for r in self.data_records) @@ -45,11 +48,12 @@ class OBD_S02_PR_Record(Packet): class OBD_S02_PR(Packet): name = "Parameter IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x42, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S02_PR_Record) ] def answers(self, other): - return other.__class__ == OBD_S02 \ + return isinstance(other, OBD_S02) \ and all(r.pid in [o.pid for o in other.requests] for r in self.data_records) diff --git a/scapy/contrib/automotive/obd/pid/pids_00_1F.py b/scapy/contrib/automotive/obd/pid/pids_00_1F.py index 3b28c0868a5..829aac6e5ec 100644 --- a/scapy/contrib/automotive/obd/pid/pids_00_1F.py +++ b/scapy/contrib/automotive/obd/pid/pids_00_1F.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip @@ -19,7 +19,7 @@ class OBD_PID00(OBD_Packet): name = "PID_00_PIDsSupported" fields_desc = [ - FlagsField('supported_pids', b'', 32, [ + FlagsField('supported_pids', 0, 32, [ 'PID20', 'PID1F', 'PID1E', @@ -109,7 +109,7 @@ class OBD_PID01(OBD_Packet): class OBD_PID02(OBD_Packet): name = "PID_02_FreezeDtc" fields_desc = [ - PacketField('dtc', b'', OBD_DTC) + PacketField('dtc', None, OBD_DTC) ] @@ -250,7 +250,7 @@ class OBD_PID12(OBD_Packet): class OBD_PID13(OBD_Packet): name = "PID_13_OxygenSensorsPresent" fields_desc = [ - FlagsField('sensors_present', b'', 8, [ + FlagsField('sensors_present', 0, 8, [ 'Bank1Sensor1', 'Bank1Sensor2', 'Bank1Sensor3', diff --git a/scapy/contrib/automotive/obd/pid/pids_20_3F.py b/scapy/contrib/automotive/obd/pid/pids_20_3F.py index f382df5f286..b48e04e18af 100644 --- a/scapy/contrib/automotive/obd/pid/pids_20_3F.py +++ b/scapy/contrib/automotive/obd/pid/pids_20_3F.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/pid/pids_40_5F.py b/scapy/contrib/automotive/obd/pid/pids_40_5F.py index e23f9f2d5f2..116ead96209 100644 --- a/scapy/contrib/automotive/obd/pid/pids_40_5F.py +++ b/scapy/contrib/automotive/obd/pid/pids_40_5F.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/pid/pids_60_7F.py b/scapy/contrib/automotive/obd/pid/pids_60_7F.py index b8e65a3cdff..68e2acdedae 100644 --- a/scapy/contrib/automotive/obd/pid/pids_60_7F.py +++ b/scapy/contrib/automotive/obd/pid/pids_60_7F.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/pid/pids_80_9F.py b/scapy/contrib/automotive/obd/pid/pids_80_9F.py index 99647e430ce..7b9ace0beb8 100644 --- a/scapy/contrib/automotive/obd/pid/pids_80_9F.py +++ b/scapy/contrib/automotive/obd/pid/pids_80_9F.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/pid/pids_A0_C0.py b/scapy/contrib/automotive/obd/pid/pids_A0_C0.py index f4e075d2683..f7a58c572b6 100644 --- a/scapy/contrib/automotive/obd/pid/pids_A0_C0.py +++ b/scapy/contrib/automotive/obd/pid/pids_A0_C0.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/scanner.py b/scapy/contrib/automotive/obd/scanner.py index 1f01a9a7737..00884e5813d 100644 --- a/scapy/contrib/automotive/obd/scanner.py +++ b/scapy/contrib/automotive/obd/scanner.py @@ -1,231 +1,301 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Friedrich Feigel -# This program is published under a GPLv2 license +# Copyright (C) Nils Weiss # scapy.contrib.description = OnBoardDiagnosticScanner # scapy.contrib.status = loads -# XXX TODO This file contains illegal E501 issues D: +import copy -from scapy.compat import chb from scapy.contrib.automotive.obd.obd import OBD, OBD_S03, OBD_S07, OBD_S0A, \ - OBD_S01, OBD_S06, OBD_S08, OBD_S09 - - -def _supported_id_numbers(socket, timeout, service_class, id_name, verbose): - """ Check which Parameter IDs are supported by the vehicle - - Args: - socket: is the ISOTPSocket, over which the OBD-Services communicate. - the id 0x7df acts as a broadcast address for all obd-supporting ECUs. - timeout: only required for the OBD Simulator, since it might tell it - supports a PID, while it actually doesn't and won't respond to this PID. - If this happens with a real ECU, it is an implementation error. - service_class: specifies, which OBD-Service should be queried. - id_name: describes the car domain (e.g.: mid = IDs in Motor Domain). - verbose: specifies, whether the sr1()-method gives feedback or not. - - This method sends a query message via a ISOTPSocket, which will be responded by the ECUs with - a message containing Bits, representing whether a PID is supported by the vehicle's protocol implementation or not. - The first Message has the PID 0x00 and contains 32 Bits, which indicate by their index and value, which PIDs are - supported. - If the PID 0x20 is supported, that means, there are more supported PIDs within the next 32 PIDs, which will result - in a new query message being sent, that contains the next 32 Bits. - There is a maximum of 256 possible PIDs. - The supported PIDs will be returned as set. + OBD_S01, OBD_S06, OBD_S08, OBD_S09, OBD_NR, OBD_S02, OBD_S02_Record +from scapy.config import conf +from scapy.packet import Packet +from scapy.themes import BlackAndWhite + +from scapy.contrib.automotive.scanner.enumerator import ServiceEnumerator, \ + _AutomotiveTestCaseScanResult, _AutomotiveTestCaseFilteredScanResult +from scapy.contrib.automotive.scanner.executor import \ + AutomotiveTestCaseExecutor +from scapy.contrib.automotive.ecu import EcuState +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCaseABC, \ + _SocketUnion + +# Typing imports +from typing import ( + List, + Type, + Any, + Iterable, +) + + +class OBD_Enumerator(ServiceEnumerator): + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'full_scan': (bool, None), + }) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param bool full_scan: Specifies if the entire scan range is tested, or + if the bitmask with supported identifiers is + queried and only supported identifiers + are scanned.""" + + @staticmethod + def _get_negative_response_code(resp): + # type: (Packet) -> int + return resp.response_code + + @staticmethod + def _get_negative_response_desc(nrc): + # type: (int) -> str + return OBD_NR(response_code=nrc).sprintf("%OBD_NR.response_code%") + + @staticmethod + def _get_negative_response_label(response): + # type: (Packet) -> str + return response.sprintf("NR: %OBD_NR.response_code%") + + @property + def filtered_results(self): + # type: () -> List[_AutomotiveTestCaseFilteredScanResult] + return self.results_with_positive_response + + +class OBD_Service_Enumerator(OBD_Enumerator): """ - - supported_id_numbers = set() - supported_prop = 'supported_' + id_name + 's' - - # ID 0x00 requests the first range of supported IDs in OBD - supported_ids_req = OBD() / service_class(b'\x00') - - while supported_ids_req is not None: - resp = socket.sr1(supported_ids_req, timeout=timeout, verbose=verbose) - - # If None, the device did not respond. - # Usually only occurs, if device is off. - if resp is None or resp.service == 0x7f: - break - - supported_ids_req = None - - all_supported_in_range = getattr(resp.data_records[0], supported_prop) - - for supported in all_supported_in_range: - id_number = int(supported[-2:], 16) - supported_id_numbers.add(id_number) - - # send a new query if the next PID range is supported - if id_number % 0x20 == 0: - supported_ids_req = OBD() / service_class(chb(id_number)) - - return supported_id_numbers - - -def _scan_id_service(socket, timeout, service_class, id_numbers, verbose): - """ Queries certain PIDs and stores their return value - - Args: - socket: is the ISOTPSocket, over which the OBD-Services communicate. - the id 0x7df acts as a broadcast address for all obd-supporting ECUs. - timeout: only required for the OBD Simulator, since it might tell it - supports a PID, while it actually doesn't and won't respond to this PID. - If this happens with a real ECU, it is an implementation error. - service_class: specifies, which OBD-Service should be queried. - id_numbers: a set of PIDs, which should be queried by the method. - verbose: specifies, whether the sr1()-method gives feedback or not. - - This method queries the specified id_numbers and stores their responses in a dictionary, which is then returned. - """ - - data = dict() - - for id_number in id_numbers: - id_byte = chb(id_number) - # assemble request packet - pkt = OBD() / service_class(id_byte) - resp = socket.sr1(pkt, timeout=timeout, verbose=verbose) - - if resp is not None: - data[id_number] = bytes(resp) - return data - - -def _scan_dtc_service(socket, timeout, service_class, verbose): - """ Queries Diagnostic Trouble Code Parameters and stores their return value - - Args: - socket: is the ISOTPSocket, over which the OBD-Services communicate. - the id 0x7df acts as a broadcast address for all obd-supporting ECUs. - timeout: only required for the OBD Simulator, since it might tell it - supports a PID, while it actually doesn't and won't respond to this PID. - If this happens with a real ECU, it is an implementation error. - service_class: specifies, which OBD-Service should be queried. - verbose: specifies, whether the sr1()-method gives feedback or not. - - This method queries the specified Diagnostic Trouble Code Parameters and stores their responses in a dictionary, - which is then returned. - """ - - req = OBD() / service_class() - resp = socket.sr1(req, timeout=timeout, verbose=verbose) - if resp is not None: - return bytes(resp) - - -def obd_scan(socket, timeout=0.1, supported_ids=False, - unsupported_ids=False, verbose=False): - """ Scans for all accessible information of each commonly used OBD service classes and prints the results - - Args: - socket: is the ISOTPSocket, over which the OBD-Services communicate. - the id 0x7df acts as a broadcast address for all obd-supporting ECUs. - timeout: only required for the OBD Simulator, since it might tell it - supports a PID, while it actually doesn't and won't respond to this PID. - If this happens with a real ECU, it is an implementation error. - supported_ids: specifies, whether to check for supported Parameter IDs. - The OBD-Protocol offers querying, which PIDs the implemented ECUs support. - unsupported_ids: specifies, whether to check for unsupported or hidden Parameter IDs. - There is a possibility of PIDs answering, which are addressed directly, but which are - not listed in the supported query response. We call these PIDs unsupported PIDs, because - they are seemingly unsupported. - verbose: specifies, whether the sr1()-method gives feedback or not and turns. - - This method queries the Diagnostic Trouble Code Parameters and if selected, supported and/or unsupported PIDS and - prints the results. + Base class for OBD_Service_Enumerators """ - dtc = dict() - supported = dict() - unsupported = dict() - - if verbose: - print("\nStarting OBD-Scan...") - - print("\nScanning Diagnostic Trouble Codes:") - # Emission-related DTCs - dtc[3] = _scan_dtc_service(socket, timeout, OBD_S03, verbose) - # Emission-related DTCs detected during current or last completed driving - # cycle - dtc[7] = _scan_dtc_service(socket, timeout, OBD_S07, verbose) - # Permanent DTCs - dtc[10] = _scan_dtc_service(socket, timeout, OBD_S0A, verbose) - print("Service 3:") - print(dtc[3]) - print("Service 7:") - print(dtc[7]) - print("Service 10:") - print(dtc[10]) - - if not supported_ids and not unsupported_ids: - return dtc - - # Powertrain - supported_ids_s01 = _supported_id_numbers( - socket, timeout, OBD_S01, 'pid', verbose) - # On-board monitoring test results for non-continuously monitored systems - supported_ids_s06 = _supported_id_numbers( - socket, timeout, OBD_S06, 'mid', verbose) - # Control of on-board system, test or component - supported_ids_s08 = _supported_id_numbers( - socket, timeout, OBD_S08, 'tid', verbose) - # On-board monitoring test results for non-continuously monitored systems - supported_ids_s09 = _supported_id_numbers( - socket, timeout, OBD_S09, 'iid', verbose) - - if supported_ids: - print("\nScanning supported Parameter IDs") - supported[1] = _scan_id_service( - socket, timeout, OBD_S01, supported_ids_s01, verbose) - supported[6] = _scan_id_service( - socket, timeout, OBD_S06, supported_ids_s06, verbose) - supported[8] = _scan_id_service( - socket, timeout, OBD_S08, supported_ids_s08, verbose) - supported[9] = _scan_id_service( - socket, timeout, OBD_S09, supported_ids_s09, verbose) - print("\nSupported PIDs of Service 1:") - print(supported[1]) - print("Supported PIDs of Service 6:") - print(supported[6]) - print("Supported PIDs of Service 8:") - print(supported[8]) - print("Supported PIDs of Service 9:") - - # this option will slow down the test a lot, since it tests for seemingly unsupported ids - # the chances of those actually responding will be small, so a lot of - # timeouts can be expected - if unsupported_ids: - # the complete id range is from 1 to 255 - all_ids_set = set(range(1, 256)) - # the unsupported id ranges are obtained by creating the compliment set - # excluding 0 - unsupported_ids_s01 = all_ids_set - supported_ids_s01 - unsupported_ids_s06 = all_ids_set - supported_ids_s06 - unsupported_ids_s08 = all_ids_set - supported_ids_s08 - unsupported_ids_s09 = all_ids_set - supported_ids_s09 - - print("\nScanning unsupported Parameter IDs") - if verbose: - print("This may take a while...") - unsupported[1] = _scan_id_service( - socket, timeout, OBD_S01, unsupported_ids_s01, verbose) - unsupported[6] = _scan_id_service( - socket, timeout, OBD_S06, unsupported_ids_s06, verbose) - unsupported[8] = _scan_id_service( - socket, timeout, OBD_S08, unsupported_ids_s08, verbose) - unsupported[9] = _scan_id_service( - socket, timeout, OBD_S09, unsupported_ids_s09, verbose) - print("\nUnsupported PIDs of Service 1:") - print(unsupported[1]) - print("Unsupported PIDs of Service 6:") - print(unsupported[6]) - print("unsupported PIDs of Service 8:") - print(unsupported[8]) - print("Unsupported PIDs of Service 9:") - print(unsupported[9]) - - return dtc, supported, unsupported + def get_supported(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> List[int] + super(OBD_Service_Enumerator, self).execute( + socket, state, scan_range=range(0, 0xff, 0x20), + exit_scan_on_first_negative_response=True, **kwargs) + + supported = list() + for _, _, r, _, _ in self.results_with_positive_response: + dr = r.data_records[0] + key = next(iter((dr.lastlayer().fields.keys()))) + try: + supported += [int(i[-2:], 16) for i in + getattr(dr, key, ["xxx00"])] + except TypeError: + pass + return list(set([i for i in supported if i % 0x20])) + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + full_scan = kwargs.pop("full_scan", False) # type: bool + if full_scan: + super(OBD_Service_Enumerator, self).execute(socket, state, **kwargs) + else: + supported_pids = self.get_supported(socket, state, **kwargs) + del self._request_iterators[state] + super(OBD_Service_Enumerator, self).execute( + socket, state, scan_range=supported_pids, **kwargs) + + execute.__doc__ = OBD_Enumerator._supported_kwargs_doc + + @staticmethod + def print_payload(resp): + # type: (Packet) -> str + backup_ct = conf.color_theme + conf.color_theme = BlackAndWhite() + load = repr(resp.data_records[0].lastlayer()) + conf.color_theme = backup_ct + return load + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], self.print_payload) + + +class OBD_DTC_Enumerator(OBD_Enumerator): + @staticmethod + def print_payload(resp): + # type: (Packet) -> str + backup_ct = conf.color_theme + conf.color_theme = BlackAndWhite() + load = repr(resp.dtcs) + conf.color_theme = backup_ct + return load + + +class OBD_S03_Enumerator(OBD_DTC_Enumerator): + _description = "Available DTCs in OBD service 03" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return [OBD() / OBD_S03()] + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 03" + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + resp = tup[2] + if resp is None: + return "Timeout" + else: + return "NR" if resp.service == 0x7f else "%d DTCs" % resp.count + + +class OBD_S07_Enumerator(OBD_DTC_Enumerator): + _description = "Available DTCs in OBD service 07" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return [OBD() / OBD_S07()] + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 07" + + +class OBD_S0A_Enumerator(OBD_DTC_Enumerator): + _description = "Available DTCs in OBD service 10" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return [OBD() / OBD_S0A()] + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 0A" + + +class OBD_S01_Enumerator(OBD_Service_Enumerator): + """OBD_S01_Enumerator""" + + _description = "Available data in OBD service 01" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) # type: Iterable[int] # noqa: E501 + return (OBD() / OBD_S01(pid=[x]) for x in scan_range) + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 01" + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + resp = tup[2] + if resp is None: + return "Timeout" + else: + return "NR" if resp.service == 0x7f else \ + "%s" % resp.data_records[0].lastlayer().name + + +class OBD_S02_Enumerator(OBD_Service_Enumerator): + _description = "Available data in OBD service 02" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) # type: Iterable[int] # noqa: E501 + return (OBD() / OBD_S02(requests=[OBD_S02_Record(pid=[x])]) + for x in scan_range) + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 02" + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + resp = tup[2] + if resp is None: + return "Timeout" + else: + return "NR" if resp.service == 0x7f else \ + "%s" % resp.data_records[0].lastlayer().name + + +class OBD_S06_Enumerator(OBD_Service_Enumerator): + _description = "Available data in OBD service 06" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) # type: Iterable[int] # noqa: E501 + return (OBD() / OBD_S06(mid=[x]) for x in scan_range) + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 06" + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + req = tup[1] + resp = tup[2] + if resp is None: + return "Timeout" + else: + return "NR" if resp.service == 0x7f else \ + "0x%02x %s" % ( + req.mid[0], + resp.data_records[0].sprintf("%OBD_S06_PR_Record.mid%")) + + +class OBD_S08_Enumerator(OBD_Service_Enumerator): + _description = "Available data in OBD service 08" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) # type: Iterable[int] # noqa: E501 + return (OBD() / OBD_S08(tid=[x]) for x in scan_range) + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 08" + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + resp = tup[2] + if resp is None: + return "Timeout" + else: + return "NR" if resp.service == 0x7f else "0x%02x %s" % ( + tup[1].tid[0], resp.data_records[0].lastlayer().name) + + +class OBD_S09_Enumerator(OBD_Service_Enumerator): + _description = "Available data in OBD service 09" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x100)) # type: Iterable[int] # noqa: E501 + return (OBD() / OBD_S09(iid=[x]) for x in scan_range) + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "Service 09" + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + resp = tup[2] + if resp is None: + return "Timeout" + else: + return "NR" if resp.service == 0x7f else \ + "0x%02x %s" % (tup[1].iid[0], + resp.data_records[0].lastlayer().name) + + +class OBD_Scanner(AutomotiveTestCaseExecutor): + @property + def enumerators(self): + # type: () -> List[AutomotiveTestCaseABC] + return self.configuration.test_cases + + @property + def default_test_case_clss(self): + # type: () -> List[Type[AutomotiveTestCaseABC]] + return [OBD_S01_Enumerator, OBD_S02_Enumerator, OBD_S06_Enumerator, + OBD_S08_Enumerator, OBD_S09_Enumerator, OBD_S03_Enumerator, + OBD_S07_Enumerator, OBD_S0A_Enumerator] diff --git a/scapy/contrib/automotive/obd/services.py b/scapy/contrib/automotive/obd/services.py index df166f6421d..57303bc13fb 100644 --- a/scapy/contrib/automotive/obd/services.py +++ b/scapy/contrib/automotive/obd/services.py @@ -1,17 +1,74 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip from scapy.fields import ByteField, XByteField, BitEnumField, \ - PacketListField, XBitField, XByteEnumField, FieldListField, FieldLenField + PacketListField, XBitField, XByteEnumField, FieldListField, \ + FieldLenField, ConditionalField from scapy.packet import Packet from scapy.contrib.automotive.obd.packet import OBD_Packet from scapy.config import conf +_OBD_SERVICES = { + 0x01: 'CurrentPowertrainDiagnosticDataRequest', + 0x02: 'PowertrainFreezeFrameDataRequest', + 0x03: 'EmissionRelatedDiagnosticTroubleCodesRequest', + 0x04: 'ClearResetDiagnosticTroubleCodesRequest', + 0x05: 'OxygenSensorMonitoringTestResultsRequest', + 0x06: 'OnBoardMonitoringTestResultsRequest', + 0x07: 'PendingEmissionRelatedDiagnosticTroubleCodesRequest', + 0x08: 'ControlOperationRequest', + 0x09: 'VehicleInformationRequest', + 0x0A: 'PermanentDiagnosticTroubleCodesRequest', + 0x41: 'CurrentPowertrainDiagnosticDataResponse', + 0x42: 'PowertrainFreezeFrameDataResponse', + 0x43: 'EmissionRelatedDiagnosticTroubleCodesResponse', + 0x44: 'ClearResetDiagnosticTroubleCodesResponse', + 0x45: 'OxygenSensorMonitoringTestResultsResponse', + 0x46: 'OnBoardMonitoringTestResultsResponse', + 0x47: 'PendingEmissionRelatedDiagnosticTroubleCodesResponse', + 0x48: 'ControlOperationResponse', + 0x49: 'VehicleInformationResponse', + 0x4A: 'PermanentDiagnosticTroubleCodesResponse', + 0x7f: 'NegativeResponse', +} + + +def _obd_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['OBD']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`OBD` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already an :class:`OBD` packet, preventing a duplicate + service byte when sub-packets are stacked (``OBD()/OBD_S01()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + + .. note:: + OBD service classes live in ``services.py`` which is imported by + ``obd.py``. To avoid a circular import the underlayer class is + identified by its class name (``'OBD'``) rather than by an + ``isinstance`` check. + """ + if not conf.contribs['OBD'].get('single_layer_mode', False): + return False + if conf.contribs['OBD'].get('compatibility_mode', True): + ul = pkt.underlayer + return ul is None or type(ul).__name__ != 'OBD' + return True + class OBD_DTC(OBD_Packet): name = "DiagnosticTroubleCode" @@ -45,6 +102,7 @@ class OBD_NR(Packet): } fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7F, _OBD_SERVICES), _obd_slm), XByteField('request_service_id', 0), XByteEnumField('response_code', 0, responses) ] @@ -58,6 +116,7 @@ def answers(self, other): class OBD_S01(Packet): name = "S1_CurrentData" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x01, _OBD_SERVICES), _obd_slm), FieldListField("pid", [], XByteField('', 0)) ] @@ -72,61 +131,78 @@ class OBD_S02_Record(OBD_Packet): class OBD_S02(Packet): name = "S2_FreezeFrameData" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x02, _OBD_SERVICES), _obd_slm), PacketListField("requests", [], OBD_S02_Record) ] class OBD_S03(Packet): name = "S3_RequestDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x03, _OBD_SERVICES), _obd_slm), + ] class OBD_S03_PR(Packet): name = "S3_ResponseDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x43, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] def answers(self, other): - return other.__class__ == OBD_S03 + return isinstance(other, OBD_S03) class OBD_S04(Packet): name = "S4_ClearDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x04, _OBD_SERVICES), _obd_slm), + ] class OBD_S04_PR(Packet): name = "S4_ClearDTCsPositiveResponse" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x44, _OBD_SERVICES), _obd_slm), + ] def answers(self, other): - return other.__class__ == OBD_S04 + return isinstance(other, OBD_S04) class OBD_S06(Packet): name = "S6_OnBoardDiagnosticMonitoring" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x06, _OBD_SERVICES), _obd_slm), FieldListField("mid", [], XByteField('', 0)) ] class OBD_S07(Packet): name = "S7_RequestPendingDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x07, _OBD_SERVICES), _obd_slm), + ] class OBD_S07_PR(Packet): name = "S7_ResponsePendingDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x47, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] def answers(self, other): - return other.__class__ == OBD_S07 + return isinstance(other, OBD_S07) class OBD_S08(Packet): name = "S8_RequestControlOfSystem" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x08, _OBD_SERVICES), _obd_slm), FieldListField("tid", [], XByteField('', 0)) ] @@ -134,20 +210,25 @@ class OBD_S08(Packet): class OBD_S09(Packet): name = "S9_VehicleInformation" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x09, _OBD_SERVICES), _obd_slm), FieldListField("iid", [], XByteField('', 0)) ] class OBD_S0A(Packet): name = "S0A_RequestPermanentDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x0A, _OBD_SERVICES), _obd_slm), + ] class OBD_S0A_PR(Packet): name = "S0A_ResponsePermanentDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x4A, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] def answers(self, other): - return other.__class__ == OBD_S0A + return isinstance(other, OBD_S0A) diff --git a/scapy/contrib/automotive/obd/tid/__init__.py b/scapy/contrib/automotive/obd/tid/__init__.py index 9d7ebf0032f..c07294b07d7 100644 --- a/scapy/contrib/automotive/obd/tid/__init__.py +++ b/scapy/contrib/automotive/obd/tid/__init__.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/obd/tid/tids.py b/scapy/contrib/automotive/obd/tid/tids.py index fd3a24dbcde..cf92b7acd01 100644 --- a/scapy/contrib/automotive/obd/tid/tids.py +++ b/scapy/contrib/automotive/obd/tid/tids.py @@ -1,15 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip -from scapy.fields import FlagsField, ByteField, ScalingField, PacketListField +from scapy.fields import ( + ConditionalField, FlagsField, ByteField, ScalingField, PacketListField, + XByteEnumField +) from scapy.packet import bind_layers, Packet from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S08 +from scapy.contrib.automotive.obd.services import OBD_S08, _OBD_SERVICES, _obd_slm class _OBD_TID_Voltage(OBD_Packet): @@ -132,11 +135,12 @@ class OBD_S08_PR_Record(Packet): class OBD_S08_PR(Packet): name = "Control Operation IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x48, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S08_PR_Record) ] def answers(self, other): - return other.__class__ == OBD_S08 \ + return isinstance(other, OBD_S08) \ and all(r.tid in other.tid for r in self.data_records) diff --git a/scapy/contrib/automotive/scanner/__init__.py b/scapy/contrib/automotive/scanner/__init__.py new file mode 100644 index 00000000000..8be8d76679b --- /dev/null +++ b/scapy/contrib/automotive/scanner/__init__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = Automotive Scanner Library +# scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/scanner/configuration.py b/scapy/contrib/automotive/scanner/configuration.py new file mode 100644 index 00000000000..f7342bc11b8 --- /dev/null +++ b/scapy/contrib/automotive/scanner/configuration.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = AutomotiveTestCaseExecutorConfiguration +# scapy.contrib.status = library + +import inspect +from threading import Event + +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.scanner.graph import Graph +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCaseABC +from scapy.contrib.automotive.scanner.staged_test_case import StagedAutomotiveTestCase # noqa: E501 + +# Typing imports +from typing import ( + Any, + Union, + List, + Type, + Set, + cast, +) + + +class AutomotiveTestCaseExecutorConfiguration(object): + """ + Configuration storage for AutomotiveTestCaseExecutor. + + The following keywords are used in the AutomotiveTestCaseExecutor: + verbose: Enables verbose output and logging + debug: Will raise Exceptions on internal errors + + :param test_cases: List of AutomotiveTestCase classes or instances. + Classes will get instantiated in this initializer. + :param kwargs: Configuration for every AutomotiveTestCase in test_cases + and for the AutomotiveTestCaseExecutor. TestCase local + configuration and global configuration for all TestCase + objects are possible. All keyword arguments given will + be stored for every TestCase. To define a local + configuration for one TestCase only, the keyword + arguments need to be provided in a dictionary. + To assign a configuration dictionary to a TestCase, the + keyword need to identify the TestCase by the following + pattern. + ``MyTestCase_kwargs={"someConfig": 42}`` + The keyword is composed from the TestCase class name and + the postfix '_kwargs'. + + Example: + >>> config = AutomotiveTestCaseExecutorConfiguration([MyTestCase], global_config=42, MyTestCase_kwargs={"localConfig": 1337}) # noqa: E501 + """ + def __setitem__(self, key, value): + # type: (Any, Any) -> None + self.__dict__[key] = value + + def __getitem__(self, key): + # type: (Any) -> Any + return self.__dict__[key] + + def _generate_test_case_config(self, test_case_cls): + # type: (Type[AutomotiveTestCaseABC]) -> None + # try to get config from kwargs + if test_case_cls in self.test_case_clss: + return + + self.test_case_clss.add(test_case_cls) + + kwargs_name = test_case_cls.__name__ + "_kwargs" + self.__setattr__(test_case_cls.__name__, self.global_kwargs.pop( + kwargs_name, dict())) + + # apply global config + val = self.__getattribute__(test_case_cls.__name__) + for kwargs_key, kwargs_val in self.global_kwargs.items(): + if "_kwargs" in kwargs_key: + continue + if kwargs_key not in val.keys(): + val[kwargs_key] = kwargs_val + self.__setattr__(test_case_cls.__name__, val) + + def add_test_case(self, test_case): + # type: (Union[AutomotiveTestCaseABC, Type[AutomotiveTestCaseABC], StagedAutomotiveTestCase, Type[StagedAutomotiveTestCase]]) -> None # noqa: E501 + if inspect.isclass(test_case): + test_case_class = cast(Union[Type[AutomotiveTestCaseABC], + Type[StagedAutomotiveTestCase]], + test_case) + if issubclass(test_case_class, StagedAutomotiveTestCase): + self.add_test_case(test_case_class()) # type: ignore + elif issubclass(test_case_class, AutomotiveTestCaseABC): + self.add_test_case(test_case_class()) + else: + raise TypeError( + "Provided class is not in " + "Union[Type[AutomotiveTestCaseABC], " + "Type[StagedAutomotiveTestCase]]") + + elif isinstance(test_case, AutomotiveTestCaseABC): + self.test_cases.append(test_case) + self._generate_test_case_config(test_case.__class__) + if isinstance(test_case, StagedAutomotiveTestCase): + self.stages.append(test_case) + for tc in test_case.test_cases: + self.staged_test_cases.append(tc) + self._generate_test_case_config(tc.__class__) + else: + raise TypeError( + "Provided instance or class of " + "StagedAutomotiveTestCase or AutomotiveTestCaseABC") + + def __init__(self, test_cases, **kwargs): + # type: (Union[List[Union[AutomotiveTestCaseABC, Type[AutomotiveTestCaseABC]]], List[Type[AutomotiveTestCaseABC]]], Any) -> None # noqa: E501 + self.verbose = kwargs.get("verbose", False) + self.debug = kwargs.get("debug", False) + self.unittest = kwargs.pop("unittest", False) + self.delay_enter_state = kwargs.pop("delay_enter_state", 0) + self.state_graph = Graph() + self.test_cases = list() # type: List[AutomotiveTestCaseABC] + self.stages = list() # type: List[StagedAutomotiveTestCase] + self.staged_test_cases = list() # type: List[AutomotiveTestCaseABC] + self.test_case_clss = set() # type: Set[Type[AutomotiveTestCaseABC]] + self.stop_event = Event() + self.global_kwargs = kwargs + self.global_kwargs["stop_event"] = self.stop_event + + for tc in test_cases: + self.add_test_case(tc) + + log_automotive.debug("The following configuration was created") + log_automotive.debug(self.__dict__) + + def __reduce__(self): # type: ignore + f, t, d = super(AutomotiveTestCaseExecutorConfiguration, self).__reduce__() # type: ignore # noqa: E501 + + try: + del d["tps"] + except KeyError: + pass + + try: + del d["stop_event"] + except KeyError: + pass + + try: + del d["global_kwargs"]["stop_event"] + except KeyError: + pass + + for tc in d["test_cases"]: + try: + del d[tc.__class__.__name__]["stop_event"] + except KeyError: + pass + + for tc in d["staged_test_cases"]: + try: + del d[tc.__class__.__name__]["stop_event"] + except KeyError: + pass + + try: + del d["global_kwargs"]["stop_event"] + except KeyError: + pass + + return f, t, d diff --git a/scapy/contrib/automotive/scanner/enumerator.py b/scapy/contrib/automotive/scanner/enumerator.py new file mode 100644 index 00000000000..c778b56f53b --- /dev/null +++ b/scapy/contrib/automotive/scanner/enumerator.py @@ -0,0 +1,862 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = ServiceEnumerator definitions +# scapy.contrib.status = library + + +import abc +import threading +import time +import copy +from collections import defaultdict, OrderedDict +from itertools import chain +from typing import NamedTuple + +from scapy.compat import orb +from scapy.contrib.automotive import log_automotive +from scapy.error import Scapy_Exception +from scapy.utils import make_lined_table, EDecimal, PeriodicSenderThread +from scapy.packet import Packet +from scapy.contrib.automotive.ecu import EcuState, EcuResponse +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCase, \ + StateGenerator, _SocketUnion, _TransitionTuple +from scapy.contrib.automotive.scanner.configuration import \ + AutomotiveTestCaseExecutorConfiguration +from scapy.contrib.automotive.scanner.graph import _Edge + +# Typing imports +from typing import ( + Any, + Union, + List, + Optional, + Iterable, + Dict, + Tuple, + Set, + Callable, + cast, +) + +# Definition outside the class ServiceEnumerator to allow pickling +_AutomotiveTestCaseScanResult = NamedTuple( + "_AutomotiveTestCaseScanResult", + [("state", EcuState), + ("req", Packet), + ("resp", Optional[Packet]), + ("req_ts", Union[EDecimal, float]), + ("resp_ts", Optional[Union[EDecimal, float]])]) + +_AutomotiveTestCaseFilteredScanResult = NamedTuple( + "_AutomotiveTestCaseFilteredScanResult", + [("state", EcuState), + ("req", Packet), + ("resp", Packet), + ("req_ts", Union[EDecimal, float]), + ("resp_ts", Union[EDecimal, float])]) + + +class ServiceEnumerator(AutomotiveTestCase, metaclass=abc.ABCMeta): + """ + Base class for ServiceEnumerators of automotive diagnostic protocols + """ + + _supported_kwargs = copy.copy(AutomotiveTestCase._supported_kwargs) + _supported_kwargs.update({ + 'timeout': ((int, float), lambda x: x > 0), + 'count': (int, lambda x: x >= 0), + 'execution_time': (int, None), + 'state_allow_list': ((list, EcuState), None), + 'state_block_list': ((list, EcuState), None), + 'retry_if_none_received': (bool, None), + 'exit_if_no_answer_received': (bool, None), + 'exit_if_service_not_supported': (bool, None), + 'exit_scan_on_first_negative_response': (bool, None), + 'retry_if_busy_returncode': (bool, None), + 'stop_event': (threading.Event, None), + 'debug': (bool, None), + 'scan_range': ((list, tuple, range), None), + 'unittest': (bool, None), + 'disable_tps_while_sending': (bool, None), + 'inter': ((int, float), lambda x: x >= 0), + }) + + _supported_kwargs_doc = AutomotiveTestCase._supported_kwargs_doc + """ + :param timeout: Timeout until a response will arrive after a request + :type timeout: integer or float + :param integer count: Number of request to be sent in one execution + :param int execution_time: Time in seconds until the execution of + this enumerator is stopped. + :param state_allow_list: List of EcuState objects or EcuState object + in which the the execution of this enumerator + is allowed. If provided, other states will not + be executed. + :type state_allow_list: EcuState or list + :param state_block_list: List of EcuState objects or EcuState object + in which the the execution of this enumerator + is blocked. + :type state_block_list: EcuState or list + :param bool retry_if_none_received: Specifies if a request will be send + again, if None was received + (usually because of a timeout). + :param bool exit_if_no_answer_received: Specifies to finish the + execution of this enumerator + once None is received. + :param bool exit_if_service_not_supported: Specifies to finish the + execution of this + enumerator, once the + negative return code + 'serviceNotSupported' is + received. + :param bool exit_scan_on_first_negative_response: Specifies to finish + the execution once a + negative response is + received. + :param bool retry_if_busy_returncode: Specifies to retry a request, if + the 'busyRepeatRequest' negative + response code is received. + :param bool debug: Enables debug functions during execute. + :param Event stop_event: Signals immediate stop of the execution. + :param scan_range: Specifies the identifiers to be scanned. + :type scan_range: list or tuple or range or iterable + :param disable_tps_while_sending: Temporary disables a TesterPresentSender + to not interact with a seed request. + :type disable_tps_while_sending: bool + :param inter: delay between two packets during sending + :type inter: int or float""" + + def __init__(self): + # type: () -> None + super(ServiceEnumerator, self).__init__() + self._result_packets = OrderedDict() # type: Dict[bytes, Packet] + self._results = list() # type: List[_AutomotiveTestCaseScanResult] + self._request_iterators = dict() # type: Dict[EcuState, Iterable[Packet]] # noqa: E501 + self._retry_pkt = defaultdict(list) # type: Dict[EcuState, Union[Packet, Iterable[Packet]]] # noqa: E501 + self._negative_response_blacklist = [0x10, 0x11] # type: List[int] + self._requests_per_state_estimated = None # type: Optional[int] + self._tester_present_sender = None # type: Optional[PeriodicSenderThread] + + @staticmethod + @abc.abstractmethod + def _get_negative_response_code(resp): + # type: (Packet) -> int + raise NotImplementedError() + + @staticmethod + @abc.abstractmethod + def _get_negative_response_desc(nrc): + # type: (int) -> str + raise NotImplementedError() + + def _get_table_entry_x(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + """ + Provides a table entry for the column which gets print during `show()`. + :param tup: A results tuple + :return: A string which describes the state + """ + return str(tup[0]) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + """ + Provides a table entry for the line which gets print during `show()`. + :param tup: A results tuple + :return: A string which describes the request + """ + return repr(tup[1]) + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + """ + Provides a table entry for the field which gets print during `show()`. + :param tup: A results tuple + :return: A string which describes the response + """ + return repr(tup[2]) + + @staticmethod + @abc.abstractmethod + def _get_negative_response_label(response): + # type: (Packet) -> str + raise NotImplementedError() + + @abc.abstractmethod + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + raise NotImplementedError("Overwrite this method") + + def __reduce__(self): # type: ignore + f, t, d = super(ServiceEnumerator, self).__reduce__() # type: ignore + + try: + del d["_tester_present_sender"] + except KeyError: + pass + + try: + for k, v in d["_request_iterators"].items(): + d["_request_iterators"][k] = list(v) + except KeyError: + pass + + try: + for k in d["_retry_pkt"]: + d["_retry_pkt"][k] = list(self._get_retry_iterator(k)) + except KeyError: + pass + return f, t, d + + @property + def negative_response_blacklist(self): + # type: () -> List[int] + return self._negative_response_blacklist + + @property + def completed(self): + # type: () -> bool + if len(self._results): + return all([self.has_completed(s) for s in self.scanned_states]) + else: + return super(ServiceEnumerator, self).completed + + def _store_result(self, state, req, res): + # type: (EcuState, Packet, Optional[Packet]) -> None + if bytes(req) not in self._result_packets: + self._result_packets[bytes(req)] = req + + if res and bytes(res) not in self._result_packets: + self._result_packets[bytes(res)] = res + + self._results.append(_AutomotiveTestCaseScanResult( + state, + self._result_packets[bytes(req)], + self._result_packets[bytes(res)] if res is not None else None, + req.sent_time or 0.0, + res.time if res is not None else None)) + + def _get_retry_iterator(self, state): + # type: (EcuState) -> Iterable[Packet] + retry_entry = self._retry_pkt[state] + if isinstance(retry_entry, Packet): + log_automotive.debug("Provide retry packet") + return [retry_entry] + elif isinstance(retry_entry, list): + if len(retry_entry): + log_automotive.debug("Provide retry list") + else: + log_automotive.debug("Provide retry iterator") + # assume self.retry_pkt is a generator or list + + return retry_entry + + def _get_initial_request_iterator(self, state, **kwargs): + # type: (EcuState, Any) -> Iterable[Packet] + if state not in self._request_iterators: + self._request_iterators[state] = iter( + self._get_initial_requests(**kwargs)) + + return self._request_iterators[state] + + def _get_request_iterator(self, state, **kwargs): + # type: (EcuState, Optional[Dict[str, Any]]) -> Iterable[Packet] + return chain(self._get_retry_iterator(state), + self._get_initial_request_iterator(state, **kwargs)) + + def _prepare_runtime_estimation(self, **kwargs): + # type: (Optional[Dict[str, Any]]) -> None + if self._requests_per_state_estimated is None: + try: + initial_requests = self._get_initial_requests(**kwargs) + self._requests_per_state_estimated = len(list(initial_requests)) + except NotImplementedError: + pass + + def runtime_estimation(self): + # type: () -> Optional[Tuple[int, int, float]] + if self._requests_per_state_estimated is None: + return None + + pkts_tbs = max( + len(self.scanned_states) * self._requests_per_state_estimated, 1) + pkts_snt = len(self.results) + + return pkts_tbs, pkts_snt, float(pkts_snt) / pkts_tbs + + def pre_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + try: + self._tester_present_sender = global_configuration["tps"] + except KeyError: + self._tester_present_sender = None + + def post_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + self._tester_present_sender = None + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + self.check_kwargs(kwargs) + timeout = kwargs.pop('timeout', 1) + count = kwargs.pop('count', None) + execution_time = kwargs.pop("execution_time", 1200) + stop_event = kwargs.pop("stop_event", None) # type: Optional[threading.Event] # noqa: E501 + disable_tps = kwargs.pop("disable_tps_while_sending", False) + inter = kwargs.pop("inter", 0) + + self._prepare_runtime_estimation(**kwargs) + + state_block_list = kwargs.get('state_block_list', list()) + + if state_block_list and state in state_block_list: + self._state_completed[state] = True + log_automotive.debug("State %s in block list!", repr(state)) + return + + state_allow_list = kwargs.get('state_allow_list', list()) + + if state_allow_list and state not in state_allow_list: + self._state_completed[state] = True + log_automotive.debug("State %s not in allow list!", + repr(state)) + return + + it = self._get_request_iterator(state, **kwargs) + + # log_automotive.debug("[i] Using iterator %s in state %s", it, state) + + start_time = time.monotonic() + log_automotive.debug( + "Start execution of enumerator: %s", time.ctime()) + + for req in it: + if stop_event: + stop_event.wait(timeout=inter) + else: + time.sleep(inter) + + if disable_tps and self._tester_present_sender: + self._tester_present_sender.disable() + + res = self.sr1_with_retry_on_error(req, socket, state, timeout) + + if disable_tps and self._tester_present_sender: + self._tester_present_sender.enable() + + self._store_result(state, req, res) + + if self._evaluate_response(state, req, res, **kwargs): + log_automotive.debug( + "Stop test_case execution because of response evaluation") + return + + if count is not None: + count -= 1 + if count <= 0: + log_automotive.debug( + "Finished execution count of enumerator") + return + + if (start_time + execution_time) < time.monotonic(): + log_automotive.debug( + "[i] Finished execution time of enumerator: %s", + time.ctime()) + return + + if stop_event is not None and stop_event.is_set(): + log_automotive.info( + "Stop test_case execution because of stop event") + return + + log_automotive.info("Finished iterator execution") + self._state_completed[state] = True + log_automotive.debug("States completed %s", + repr(self._state_completed)) + + execute.__doc__ = _supported_kwargs_doc + + def sr1_with_retry_on_error(self, req, socket, state, timeout): + # type: (Packet, _SocketUnion, EcuState, int) -> Optional[Packet] + try: + res = socket.sr1(req, timeout=timeout, verbose=False, + chainEX=True, chainCC=True) + except (OSError, ValueError, Scapy_Exception) as e: + if not self._populate_retry(state, req): + log_automotive.exception( + "Exception during retry. This is bad") + raise e + return res + + def _evaluate_response(self, + state, # type: EcuState + request, # type: Packet + response, # type: Optional[Packet] + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool + """ + Evaluates the response and determines if the current scan execution + should be stopped. + :param state: Current state of the ECU under test + :param request: Sent request + :param response: Received response + :param kwargs: Arguments to modify the behavior of this function. + Supported arguments: + - retry_if_none_received: True/False + - exit_if_no_answer_received: True/False + - exit_if_service_not_supported: True/False + - exit_scan_on_first_negative_response: True/False + - retry_if_busy_returncode: True/False + :return: True, if current execution needs to be interrupted. + False, if enumerator should proceed with the execution. + """ + if response is None: + if cast(bool, kwargs.pop("retry_if_none_received", False)): + log_automotive.debug( + "Retry %s because None received", repr(request)) + return self._populate_retry(state, request) + return cast(bool, kwargs.pop("exit_if_no_answer_received", False)) + + if self._evaluate_negative_response_code( + state, response, **kwargs): + # leave current execution, because of a negative response code + return True + + if self._evaluate_retry(state, request, response, **kwargs): + # leave current execution, because a retry was set + return True + + # cleanup retry packet + self._retry_pkt[state] = [] + + return self._evaluate_ecu_state_modifications(state, request, response) + + def _evaluate_ecu_state_modifications(self, + state, # type: EcuState + request, # type: Packet + response, # type: Packet + ): # type: (...) -> bool + if EcuState.is_modifier_pkt(response): + if state != EcuState.get_modified_ecu_state( + response, request, state): + log_automotive.debug( + "Exit execute. Ecu state was modified!") + return True + return False + + def _evaluate_negative_response_code(self, + state, # type: EcuState + response, # type: Packet + **kwargs # type: Optional[Dict[str, Any]] # noqa: E501 + ): # type: (...) -> bool + exit_if_service_not_supported = \ + kwargs.pop("exit_if_service_not_supported", False) + exit_scan_on_first_negative_response = \ + kwargs.pop("exit_scan_on_first_negative_response", False) + + if exit_scan_on_first_negative_response and response.service == 0x7f: + return True + + if exit_if_service_not_supported and response.service == 0x7f: + response_code = self._get_negative_response_code(response) + if response_code in [0x11, 0x7f]: + names = {0x11: "serviceNotSupported", + 0x7f: "serviceNotSupportedInActiveSession"} + log_automotive.debug( + "Exit execute because negative response %s received!", + names[response_code]) + # execute of current state is completed, + # since a serviceNotSupported negative response was received + self._state_completed[state] = True + # stop current execute and exit + return True + return False + + def _populate_retry(self, + state, # type: EcuState + request, # type: Packet + ): # type: (...) -> bool + """ + Populates internal storage with request for a retry. + + :param state: Current state + :param request: Request which needs a retry + :return: True, if storage was populated. If False is returned, the + retry storage is still populated. This indicates that the + current execution was already a retry execution. + """ + + if not self._get_retry_iterator(state): + # This was no retry since the retry_pkt is None + self._retry_pkt[state] = request + log_automotive.debug( + "Exit execute. Retry packet next time!") + return True + else: + # This was a unsuccessful retry, continue execute + log_automotive.debug("Unsuccessful retry!") + return False + + def _evaluate_retry(self, + state, # type: EcuState + request, # type: Packet + response, # type: Packet + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool + retry_if_busy_returncode = \ + kwargs.pop("retry_if_busy_returncode", True) + + if retry_if_busy_returncode and response.service == 0x7f \ + and self._get_negative_response_code(response) == 0x21: + log_automotive.debug( + "Retry %s because retry_if_busy_returncode received", + repr(request)) + return self._populate_retry(state, request) + return False + + def _compute_statistics(self): + # type: () -> List[Tuple[str, str, str]] + data_sets = [("all", self._results)] + + for state in self._state_completed.keys(): + data_sets.append((repr(state), + [r for r in self._results if r.state == state])) + + stats = list() # type: List[Tuple[str, str, str]] + + for desc, data in data_sets: + answered = [cast(_AutomotiveTestCaseFilteredScanResult, r) + for r in data if r.resp is not None and + r.resp_ts is not None] + unanswered = [r for r in data if r.resp is None] + answertimes = [float(x.resp_ts) - float(x.req_ts) + for x in answered] + answertimes_nr = [float(x.resp_ts) - float(x.req_ts) + for x in answered if x.resp.service == 0x7f] + answertimes_pr = [float(x.resp_ts) - float(x.req_ts) + for x in answered if x.resp.service != 0x7f] + + nrs = [r.resp for r in answered if r.resp.service == 0x7f] + stats.append((desc, "num_answered", str(len(answered)))) + stats.append((desc, "num_unanswered", str(len(unanswered)))) + stats.append((desc, "num_negative_resps", str(len(nrs)))) + + for postfix, times in zip( + ["", "_nr", "_pr"], + [answertimes, answertimes_nr, answertimes_pr]): + try: + ma = str(round(max(times), 5)) + except ValueError: + ma = "-" + + try: + mi = str(round(min(times), 5)) + except ValueError: + mi = "-" + + try: + avg = str(round(sum(times) / len(times), 5)) + except (ValueError, ZeroDivisionError): + avg = "-" + + stats.append((desc, "answertime_min" + postfix, mi)) + stats.append((desc, "answertime_max" + postfix, ma)) + stats.append((desc, "answertime_avg" + postfix, avg)) + + return stats + + def _show_statistics(self, **kwargs): + # type: (Any) -> str + stats = self._compute_statistics() + + s = "%d requests were sent, %d answered, %d unanswered" % \ + (len(self._results), + len(self.results_with_response), + len(self.results_without_response)) + "\n" + + s += "Statistics per state\n" + s += make_lined_table(stats, lambda *x: x, dump=True, sortx=str, + sorty=str) or "" + + return s + "\n" + + def _prepare_negative_response_blacklist(self): + # type: () -> None + nrc_dict = defaultdict(int) # type: Dict[int, int] + for nr in self.results_with_negative_response: + nrc_dict[self._get_negative_response_code(nr.resp)] += 1 + + total_nr_count = len(self.results_with_negative_response) + for nrc, nr_count in nrc_dict.items(): + if nrc not in self.negative_response_blacklist and \ + nr_count > 30 and (nr_count / total_nr_count) > 0.3: + log_automotive.info("Added NRC 0x%02x to filter", nrc) + self.negative_response_blacklist.append(nrc) + + if nrc in self.negative_response_blacklist and nr_count < 10: + log_automotive.info("Removed NRC 0x%02x to filter", nrc) + self.negative_response_blacklist.remove(nrc) + + @property + def results(self): + # type: () -> List[_AutomotiveTestCaseScanResult] + return self._results + + @property + def results_with_response(self): + # type: () -> List[_AutomotiveTestCaseFilteredScanResult] + filtered_results = list() + for r in self._results: + if r.resp is None: + continue + if r.resp_ts is None: + continue + fr = cast(_AutomotiveTestCaseFilteredScanResult, r) + filtered_results.append(fr) + return filtered_results + + @property + def filtered_results(self): + # type: () -> List[_AutomotiveTestCaseFilteredScanResult] + filtered_results = self.results_with_positive_response + + for r in self.results_with_negative_response: + nrc = self._get_negative_response_code(r.resp) + if nrc not in self.negative_response_blacklist: + filtered_results.append(r) + return filtered_results + + @property + def scanned_states(self): + # type: () -> Set[EcuState] + """ + Helper function to get all sacnned states in results + :return: all scanned states + """ + return set([tup.state for tup in self._results]) + + @property + def results_with_negative_response(self): + # type: () -> List[_AutomotiveTestCaseFilteredScanResult] + """ + Helper function to get all results with negative response + :return: all results with negative response + """ + return [r for r in self.results_with_response + if r.resp and r.resp.service == 0x7f] + + @property + def results_with_positive_response(self): + # type: () -> List[_AutomotiveTestCaseFilteredScanResult] + """ + Helper function to get all results with positive response + :return: all results with positive response + """ + return [r for r in self.results_with_response # noqa: E501 + if r.resp and r.resp.service != 0x7f] + + @property + def results_without_response(self): + # type: () -> List[_AutomotiveTestCaseScanResult] + """ + Helper function to get all results without response + :return: all results without response + """ + return [r for r in self._results if r.resp is None] + + def _show_negative_response_details(self, **kwargs): + # type: (Any) -> str + nrc_dict = defaultdict(int) # type: Dict[int, int] + for nr in self.results_with_negative_response: + nrc_dict[self._get_negative_response_code(nr.resp)] += 1 + + s = "These negative response codes were received " + \ + " ".join([hex(c) for c in nrc_dict.keys()]) + "\n" + for nrc, nr_count in nrc_dict.items(): + s += "\tNRC 0x%02x: %s received %d times" % ( + nrc, self._get_negative_response_desc(nrc), nr_count) + s += "\n" + + return s + "\n" + + def _show_negative_response_information(self, **kwargs): + # type: (Any) -> str + filtered = kwargs.get("filtered", True) + s = "%d negative responses were received\n" % \ + len(self.results_with_negative_response) + + s += "\n" + + s += self._show_negative_response_details(**kwargs) or "" + "\n" + if filtered and len(self.negative_response_blacklist): + s += "The following negative response codes are blacklisted: %s\n" \ + % [self._get_negative_response_desc(nr) + for nr in self.negative_response_blacklist] + + return s + "\n" + + def _show_results_information(self, **kwargs): + # type: (Any) -> str + def _get_table_entry( + *args: Any + ): # type: (...) -> Tuple[str, str, str] + tup = cast(_AutomotiveTestCaseScanResult, args) + return self._get_table_entry_x(tup), \ + self._get_table_entry_y(tup), \ + self._get_table_entry_z(tup) + + filtered = kwargs.get("filtered", True) + s = "=== No data to display ===\n" + data = self._results if not filtered else self.filtered_results # type: Union[List[_AutomotiveTestCaseScanResult], List[_AutomotiveTestCaseFilteredScanResult]] # noqa: E501 + if len(data): + s = make_lined_table( + data, _get_table_entry, dump=True, sortx=str) or "" + + return s + "\n" + + def show(self, dump=False, filtered=True, verbose=False): + # type: (bool, bool, bool) -> Optional[str] + if filtered: + self._prepare_negative_response_blacklist() + + show_functions = [self._show_header, + self._show_statistics, + self._show_negative_response_information, + self._show_results_information] + + if verbose: + show_functions.append(self._show_state_information) + + s = "\n".join(x(filtered=filtered) for x in show_functions) + + if dump: + return s + "\n" + else: + print(s) + return None + + def _get_label(self, response, positive_case="PR: PositiveResponse"): + # type: (Optional[Packet], Union[Callable[[Packet], str], str]) -> str + if response is None: + return "Timeout" + elif orb(bytes(response)[0]) == 0x7f: + return self._get_negative_response_label(response) + else: + if isinstance(positive_case, str): + return positive_case + elif callable(positive_case): + return positive_case(response) + else: + raise Scapy_Exception("Unsupported Type for positive_case. " + "Provide a string or a function.") + + @property + def supported_responses(self): + # type: () -> List[EcuResponse] + supported_resps = list() + all_responses = [p for p in self._result_packets.values() + if orb(bytes(p)[0]) & 0x40] + for resp in all_responses: + states = list(set([t.state for t in self.results_with_response + if t.resp == resp])) + supported_resps.append(EcuResponse(state=states, responses=resp)) + return supported_resps + + +class StateGeneratingServiceEnumerator( + ServiceEnumerator, + StateGenerator, + metaclass=abc.ABCMeta +): + def __init__(self): + # type: () -> None + super(StateGeneratingServiceEnumerator, self).__init__() + + # Internal storage of request packets for a certain Edge. If an edge + # is found during the evaluation of the last result of the + # ServiceEnumerator, the according request of the result tuple is + # stored together with the new Edge. + self._edge_requests = dict() # type: Dict[_Edge, Packet] + + def get_new_edge(self, + socket, # type: _SocketUnion + config # type: AutomotiveTestCaseExecutorConfiguration + ): + # type: (...) -> Optional[_Edge] + """ + Basic identification of a new edge. The last response is evaluated. + If this response packet can modify the state of an Ecu, this new + state is returned, otherwise None. + + :param socket: Socket to the DUT (unused) + :param config: Global configuration of the executor (unused) + :return: tuple of old EcuState and new EcuState, or None + """ + try: + state, req, resp, _, _ = cast(ServiceEnumerator, self).results[-1] + except IndexError: + return None + + if resp is not None and EcuState.is_modifier_pkt(resp): + new_state = EcuState.get_modified_ecu_state(resp, req, state) + if new_state == state: + return None + else: + edge = (state, new_state) + self._edge_requests[edge] = req + return edge + else: + return None + + @staticmethod + def transition_function( + sock, # type: _SocketUnion + config, # type: AutomotiveTestCaseExecutorConfiguration + kwargs # type: Dict[str, Any] + ): + # type: (...) -> bool + """ + Very basic transition function. This function sends a given request + in kwargs and evaluates the response. + + :param sock: Connection to the DUT + :param config: Global configuration of the executor (unused) + :param kwargs: Dictionary with arguments. This function only uses + the argument *"req"* which must contain a Packet, + causing an EcuState transition of the DUT. + :return: True in case of a successful transition, else False + """ + req = kwargs.get("req", None) + if req is None: + return False + + try: + res = sock.sr1(req, timeout=20, verbose=False, chainEX=True) + return res is not None and res.service != 0x7f + except (OSError, ValueError, Scapy_Exception) as e: + log_automotive.exception( + "Exception in transition function: %s", e) + return False + + def get_transition_function_description(self, edge): + # type: (_Edge) -> str + return repr(self._edge_requests[edge]) + + def get_transition_function_kwargs(self, edge): + # type: (_Edge) -> Dict[str, Any] + req = self._edge_requests[edge] + kwargs = { + "desc": self.get_transition_function_description(edge), + "req": req + } + return kwargs + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + try: + return self.transition_function, \ + self.get_transition_function_kwargs(edge), None + except KeyError: + return None diff --git a/scapy/contrib/automotive/scanner/executor.py b/scapy/contrib/automotive/scanner/executor.py new file mode 100644 index 00000000000..3d0ff3a5991 --- /dev/null +++ b/scapy/contrib/automotive/scanner/executor.py @@ -0,0 +1,483 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = AutomotiveTestCaseExecutor base class +# scapy.contrib.status = library + +import abc +import time + +from itertools import product + +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.scanner.graph import Graph +from scapy.error import Scapy_Exception +from scapy.supersocket import SuperSocket +from scapy.utils import make_lined_table, SingleConversationSocket +from scapy.contrib.automotive.ecu import EcuState, EcuResponse, Ecu +from scapy.contrib.automotive.scanner.configuration import \ + AutomotiveTestCaseExecutorConfiguration +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCaseABC, \ + _SocketUnion, _CleanupCallable, StateGenerator, TestCaseGenerator, \ + AutomotiveTestCase + +# Typing imports +from typing import ( + Any, + Union, + List, + Optional, + Dict, + Callable, + Type, + cast, + TypeVar, +) + +T = TypeVar("T") + + +class AutomotiveTestCaseExecutor(metaclass=abc.ABCMeta): + """ + Base class for different automotive scanners. This class handles + the connection to a scan target, ensures the execution of all it's + test cases, and stores the system state machine + + + :param socket: A socket object to communicate with the scan target + :param reset_handler: A function to reset the scan target + :param reconnect_handler: In case the communication needs to be + established after a reset, provide a + reconnect function which returns a socket object + :param test_cases: A list of TestCase instances or classes + :param kwargs: Arguments for the internal + AutomotiveTestCaseExecutorConfiguration instance + """ + + @property + def _initial_ecu_state(self): + # type: () -> EcuState + return EcuState(session=1) + + def __init__( + self, + socket, # type: Optional[_SocketUnion] + reset_handler=None, # type: Optional[Callable[[], None]] + reconnect_handler=None, # type: Optional[Callable[[], _SocketUnion]] # noqa: E501 + test_cases=None, # type: Optional[List[Union[AutomotiveTestCaseABC, Type[AutomotiveTestCaseABC]]]] # noqa: E501 + software_reset_handler=None, # type: Optional[Callable[[_SocketUnion], None]] # noqa: E501 + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> None + + # The TesterPresentSender can interfere with a test_case, since a + # target may only allow one request at a time. + # The SingleConversationSocket prevents interleaving requests. + if socket and not isinstance(socket, SingleConversationSocket): + self.socket = SingleConversationSocket(socket) # type: Optional[_SocketUnion] # noqa: E501 + else: + self.socket = socket + + self.target_state = self._initial_ecu_state + self.reset_handler = reset_handler + self.reconnect_handler = reconnect_handler + self.software_reset_handler = software_reset_handler + + self.cleanup_functions = list() # type: List[_CleanupCallable] + + self.configuration = AutomotiveTestCaseExecutorConfiguration( + test_cases or self.default_test_case_clss, **kwargs) + self.validate_test_case_kwargs() + + def __reduce__(self): # type: ignore + f, t, d = super(AutomotiveTestCaseExecutor, self).__reduce__() # type: ignore # noqa: E501 + try: + del d["socket"] + except KeyError: + pass + try: + del d["reset_handler"] + except KeyError: + pass + try: + del d["reconnect_handler"] + except KeyError: + pass + return f, t, d + + @property + @abc.abstractmethod + def default_test_case_clss(self): + # type: () -> List[Type[AutomotiveTestCaseABC]] + raise NotImplementedError() + + @property + def state_graph(self): + # type: () -> Graph + return self.configuration.state_graph + + @property + def state_paths(self): + # type: () -> List[List[EcuState]] + """ + Returns all state paths. A path is represented by a list of EcuState + objects. + :return: A list of paths. + """ + paths = [Graph.dijkstra(self.state_graph, self._initial_ecu_state, s) + for s in self.state_graph.nodes + if s != self._initial_ecu_state] + return sorted( + [p for p in paths if p] + [[self._initial_ecu_state]], + key=lambda x: x[-1]) + + @property + def final_states(self): + # type: () -> List[EcuState] + """ + Returns a list with all final states. A final state is the last + state of a path. + :return: + """ + return [p[-1] for p in self.state_paths] + + @property + def scan_completed(self): + # type: () -> bool + return all(t.has_completed(s) for t, s in + product(self.configuration.test_cases, self.final_states)) + + def reset_target(self): + # type: () -> None + log_automotive.info("Target reset") + if self.reset_handler: + self.reset_handler() + elif self.software_reset_handler: + if self.socket and self.socket.closed: + self.reconnect() + if self.socket: + self.software_reset_handler(self.socket) + self.target_state = self._initial_ecu_state + + def reconnect(self): + # type: () -> None + if not self.reconnect_handler: + return + + try: + if self.socket: + self.socket.close() + except Exception as e: + log_automotive.exception( + "Exception '%s' during socket.close", e) + + log_automotive.info("Target reconnect") + socket = self.reconnect_handler() + self.socket = socket if isinstance(socket, SingleConversationSocket) \ + else SingleConversationSocket(socket) + + if self.socket and self.socket.closed: + raise Scapy_Exception( + "Socket closed even after reconnect. Stop scan!") + + def execute_test_case(self, test_case, kill_time=None): + # type: (AutomotiveTestCaseABC, Optional[float]) -> None + """ + This function ensures the correct execution of a testcase, including + the pre_execute, execute and post_execute. + Finally, the testcase is asked if a new edge or a new testcase was + generated. + + :param test_case: A test case to be executed + :param kill_time: If set, this defines the maximum execution time for + the current test_case + :return: None + """ + + if not self.socket: + log_automotive.warning("Socket is None! Leaving execute_test_case") + return + + test_case.pre_execute( + self.socket, self.target_state, self.configuration) + + try: + test_case_kwargs = self.configuration[test_case.__class__.__name__] + except KeyError: + test_case_kwargs = dict() + + if kill_time: + max_execution_time = max(int(kill_time - time.monotonic()), 5) + cur_execution_time = test_case_kwargs.get("execution_time", 1200) + test_case_kwargs["execution_time"] = min(max_execution_time, + cur_execution_time) + + log_automotive.debug("Execute test_case %s with args %s", + test_case.__class__.__name__, test_case_kwargs) + + test_case.execute(self.socket, self.target_state, **test_case_kwargs) + test_case.post_execute( + self.socket, self.target_state, self.configuration) + + self.check_new_states(test_case) + self.check_new_testcases(test_case) + + if hasattr(test_case, "runtime_estimation"): + estimation = test_case.runtime_estimation() + if estimation is not None: + log_automotive.debug( + "[i] Test_case %s: TODO %d, " + "DONE %d, TOTAL %0.2f", + test_case.__class__.__name__, estimation[0], + estimation[1], estimation[2]) + + def check_new_testcases(self, test_case): + # type: (AutomotiveTestCaseABC) -> None + if isinstance(test_case, TestCaseGenerator): + new_test_case = test_case.get_generated_test_case() + if new_test_case: + log_automotive.debug("Testcase generated %s", new_test_case) + self.configuration.add_test_case(new_test_case) + + def check_new_states(self, test_case): + # type: (AutomotiveTestCaseABC) -> None + if not self.socket: + log_automotive.warning("Socket is None! Leaving check_new_states") + return + + if isinstance(test_case, StateGenerator): + edge = test_case.get_new_edge(self.socket, self.configuration) + if edge: + log_automotive.debug("Edge found %s", edge) + tf = test_case.get_transition_function(self.socket, edge) + self.state_graph.add_edge(edge, tf) + + def validate_test_case_kwargs(self): + # type: () -> None + for test_case in self.configuration.test_cases: + if isinstance(test_case, AutomotiveTestCase): + test_case_kwargs = self.configuration[test_case.__class__.__name__] + test_case.check_kwargs(test_case_kwargs) + + def stop_scan(self): + # type: () -> None + self.configuration.stop_event.set() + log_automotive.debug("Internal stop event set!") + + def progress(self): + # type: () -> float + progress = [] + for tc in self.configuration.test_cases: + if not hasattr(tc, "runtime_estimation"): + continue + est = tc.runtime_estimation() + if est is None: + continue + progress.append(est[2]) + + return sum(progress) / len(progress) if len(progress) else 0.0 + + def scan(self, timeout=None): + # type: (Optional[int]) -> None + """ + Executes all testcases for a given time. + :param timeout: Time for execution. + :return: None + """ + self.configuration.stop_event.clear() + if timeout is None: + kill_time = None + else: + kill_time = time.monotonic() + timeout + while True: + terminate = kill_time and kill_time <= time.monotonic() + if terminate: + log_automotive.debug( + "Execution time exceeded. Terminating scan!") + return + test_case_executed = False + log_automotive.info("[i] Scan progress %0.2f", self.progress()) + log_automotive.debug("[i] Scan paths %s", self.state_paths) + for p, test_case in product( + self.state_paths, self.configuration.test_cases): + log_automotive.info("Scan path %s", p) + terminate = kill_time and kill_time <= time.monotonic() + if terminate or self.configuration.stop_event.is_set(): + log_automotive.debug( + "Execution time exceeded. Terminating scan!") + break + + final_state = p[-1] + if test_case.has_completed(final_state): + log_automotive.debug("State %s for %s completed", + repr(final_state), test_case) + continue + + try: + if not self.enter_state_path(p): + log_automotive.error( + "Error entering path %s", p) + continue + log_automotive.info( + "Execute %s for path %s", str(test_case), p) + self.execute_test_case(test_case, kill_time) + test_case_executed = True + except (OSError, ValueError, Scapy_Exception) as e: + log_automotive.exception("Exception: %s", e) + if self.configuration.debug: + raise e + if isinstance(e, OSError): + log_automotive.exception( + "OSError occurred, closing socket") + if self.socket: + self.socket.close() + if (self.socket + and cast(SuperSocket, self.socket).closed + and self.reconnect_handler is None): + log_automotive.critical( + "Socket went down. Need to leave scan") + raise e + finally: + self.cleanup_state() + + if not test_case_executed: + log_automotive.info( + "Execute failure or scan completed. Exit scan!") + break + + self.cleanup_state() + self.reset_target() + + def enter_state_path(self, path): + # type: (List[EcuState]) -> bool + """ + Resets and reconnects to a target and applies all transition functions + to traversal a given path. + :param path: Path to be applied to the scan target. + :return: True, if all transition functions could be executed. + """ + if path[0] != self._initial_ecu_state: + raise Scapy_Exception( + "Initial state of path not equal reset state of the target") + + self.reset_target() + self.reconnect() + + if len(path) == 1: + return True + + for next_state in path[1:]: + if self.configuration.stop_event.is_set(): + self.cleanup_state() + return False + + edge = (self.target_state, next_state) + self.configuration.stop_event.wait( + timeout=self.configuration.delay_enter_state) + if not self.enter_state(*edge): + self.state_graph.downrate_edge(edge) + self.cleanup_state() + return False + return True + + def enter_state(self, prev_state, next_state): + # type: (EcuState, EcuState) -> bool + """ + Obtains a transition function from the system state graph and executes + it. On success, the cleanup function is added for a later cleanup of + the new state. + :param prev_state: Current state + :param next_state: Desired state + :return: True, if state could be changed successful + """ + if not self.socket: + log_automotive.warning("Socket is None! Leaving enter_state") + return False + + edge = (prev_state, next_state) + funcs = self.state_graph.get_transition_tuple_for_edge(edge) + + if funcs is None: + log_automotive.error("No transition function for %s", edge) + return False + + trans_func, trans_kwargs, clean_func = funcs + state_changed = trans_func( + self.socket, self.configuration, trans_kwargs) + + if self.socket.closed: + for i in range(5): + try: + self.reconnect() + break + except Exception: + if i == 4: + raise + if self.configuration.stop_event: + self.configuration.stop_event.wait(1) + else: + time.sleep(1) + + if state_changed: + self.target_state = next_state + + if clean_func is not None: + self.cleanup_functions += [clean_func] + return True + else: + log_automotive.info("Transition for edge %s failed", edge) + return False + + def cleanup_state(self): + # type: () -> None + """ + Executes all collected cleanup functions from a traversed path + :return: None + """ + for f in self.cleanup_functions: + if not callable(f): + continue + try: + if not f(self.socket, self.configuration): # type: ignore + log_automotive.info( + "Cleanup function %s failed", repr(f)) + except (OSError, ValueError, Scapy_Exception) as e: + log_automotive.critical("Exception during cleanup: %s", e) + + self.cleanup_functions = list() + + def show_testcases(self): + # type: () -> None + for t in self.configuration.test_cases: + t.show() + + def show_testcases_status(self): + # type: () -> None + data = list() + for t in self.configuration.test_cases: + for s in self.state_graph.nodes: + data += [(repr(s), t.__class__.__name__, t.has_completed(s))] + make_lined_table(data, lambda *tup: (tup[0], tup[1], tup[2])) + + def get_test_cases_by_class(self, cls): + # type: (Type[T]) -> List[T] + return [x for x in self.configuration.test_cases if isinstance(x, cls)] + + @property + def supported_responses(self): + # type: () -> List[EcuResponse] + """ + Returns a sorted list of supported responses, gathered from all + enumerators. The sort is done in a way + to provide the best possible results, if this list of supported + responses is used to simulate an real world Ecu with the + EcuAnsweringMachine object. + :return: A sorted list of EcuResponse objects + """ + supported_responses = list() + for tc in self.configuration.test_cases: + supported_responses += tc.supported_responses + + supported_responses.sort(key=Ecu.sort_key_func) + return supported_responses diff --git a/scapy/contrib/automotive/scanner/graph.py b/scapy/contrib/automotive/scanner/graph.py new file mode 100644 index 00000000000..3a3aa46cbd2 --- /dev/null +++ b/scapy/contrib/automotive/scanner/graph.py @@ -0,0 +1,179 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = Graph library for AutomotiveTestCaseExecutor +# scapy.contrib.status = library + +from collections import defaultdict + +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.ecu import EcuState + +# Typing imports +from typing import ( + Union, + List, + Optional, + Dict, + Tuple, + Set, + TYPE_CHECKING, +) + +_Edge = Tuple[EcuState, EcuState] + +if TYPE_CHECKING: + from scapy.contrib.automotive.scanner.test_case import _TransitionTuple + + +class Graph(object): + """ + Helper object to store a directional Graph of EcuState objects. An edge in + this Graph is defined as Tuple of two EcuStates. A node is defined as + EcuState. + + self.edges is a dict of all possible next nodes + e.g. {'X': ['A', 'B', 'C', 'E'], ...} + + self.__transition_functions has all the transition_functions between + two nodes, with the two nodes as a tuple as the key + e.g. {('X', 'A'): 7, ('X', 'B'): 2, ...} + """ + def __init__(self): + # type: () -> None + self.edges = defaultdict(list) # type: Dict[EcuState, List[EcuState]] + self.__transition_functions = {} # type: Dict[_Edge, Optional["_TransitionTuple"]] # noqa: E501 + self.weights = {} # type: Dict[_Edge, int] + + def add_edge(self, edge, transition_function=None): + # type: (_Edge, Optional["_TransitionTuple"]) -> None + """ + Inserts new edge in directional graph + :param edge: edge from node to node + :param transition_function: tuple with enter and cleanup function + """ + if edge[1] in self.edges[edge[0]]: + # Edge already exists + return + self.edges[edge[0]].append(edge[1]) + self.weights[edge] = 1 + self.__transition_functions[edge] = transition_function + + def get_transition_tuple_for_edge(self, edge): + # type: (_Edge) -> Optional["_TransitionTuple"] # noqa: E501 + """ + Returns a TransitionTuple for an Edge, if available. + :param edge: Tuple of EcuStates + :return: According TransitionTuple or None + """ + return self.__transition_functions.get(edge, None) + + def downrate_edge(self, edge): + # type: (_Edge) -> None + """ + Increases the weight of an Edge + :param edge: Edge on which the weight has t obe increased + """ + try: + self.weights[edge] += 1 + except KeyError: + pass + + @property + def transition_functions(self): + # type: () -> Dict[_Edge, Optional["_TransitionTuple"]] + """ + Get the dict of all TransistionTuples + :return: + """ + return self.__transition_functions + + @property + def nodes(self): + # type: () -> Union[List[EcuState], Set[EcuState]] + """ + Get a set of all nodes in this Graph + :return: + """ + return set([n for k, p in self.edges.items() for n in p + [k]]) + + def render(self, filename="SystemStateGraph.gv", view=True): + # type: (str, bool) -> None + """ + Renders this Graph as PDF, if `graphviz` is installed. + + :param filename: A filename for the rendered PDF. + :param view: If True, rendered file will be opened. + """ + try: + from graphviz import Digraph + except ImportError: + log_automotive.info("Please install graphviz.") + return + + ps = Digraph(name="SystemStateGraph", + node_attr={"fillcolor": "lightgrey", + "style": "filled", + "shape": "box"}, + graph_attr={"concentrate": "true"}) + for n in self.nodes: + ps.node(str(n)) + + for e, f in self.__transition_functions.items(): + try: + desc = "" if f is None else f[1]["desc"] + except (AttributeError, KeyError): + desc = "" + ps.edge(str(e[0]), str(e[1]), label=desc) + + ps.render(filename, view=view) + + @staticmethod + def dijkstra(graph, initial, end): + # type: (Graph, EcuState, EcuState) -> List[EcuState] + """ + Compute shortest paths from initial to end in graph + Partly from https://benalexkeen.com/implementing-djikstras-shortest-path-algorithm-with-python/ # noqa: E501 + :param graph: Graph where path is computed + :param initial: Start node + :param end: End node + :return: A path as list of nodes + """ + shortest_paths = {initial: (None, 0)} # type: Dict[EcuState, Tuple[Optional[EcuState], int]] # noqa: E501 + current_node = initial + visited = set() + + while current_node != end: + visited.add(current_node) + destinations = graph.edges[current_node] + weight_to_current_node = shortest_paths[current_node][1] + + for next_node in destinations: + weight = graph.weights[(current_node, next_node)] + \ + weight_to_current_node + if next_node not in shortest_paths: + shortest_paths[next_node] = (current_node, weight) + else: + current_shortest_weight = shortest_paths[next_node][1] + if current_shortest_weight > weight: + shortest_paths[next_node] = (current_node, weight) + + next_destinations = {node: shortest_paths[node] for node in + shortest_paths if node not in visited} + if not next_destinations: + return [] + # next node is the destination with the lowest weight + current_node = min(next_destinations, + key=lambda k: next_destinations[k][1]) + + # Work back through destinations in shortest path + last_node = shortest_paths[current_node][0] + path = [current_node] + while last_node is not None: + path.append(last_node) + last_node = shortest_paths[last_node][0] + # Reverse path + path.reverse() + return path diff --git a/scapy/contrib/automotive/scanner/staged_test_case.py b/scapy/contrib/automotive/scanner/staged_test_case.py new file mode 100644 index 00000000000..2544793a488 --- /dev/null +++ b/scapy/contrib/automotive/scanner/staged_test_case.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = Staged AutomotiveTestCase base classes +# scapy.contrib.status = library + + +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.scanner.graph import _Edge +from scapy.contrib.automotive.ecu import EcuState, EcuResponse, Ecu +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCaseABC, \ + TestCaseGenerator, StateGenerator, _SocketUnion + +# Typing imports +from typing import ( + Any, + List, + Optional, + Dict, + Callable, + cast, + Tuple, + TYPE_CHECKING, +) +if TYPE_CHECKING: + from scapy.contrib.automotive.scanner.test_case import _TransitionTuple + from scapy.contrib.automotive.scanner.configuration import \ + AutomotiveTestCaseExecutorConfiguration + +# type definitions +_TestCaseConnectorCallable = \ + Callable[[AutomotiveTestCaseABC, AutomotiveTestCaseABC], Dict[str, Any]] + + +class StagedAutomotiveTestCase(AutomotiveTestCaseABC, TestCaseGenerator, StateGenerator): # noqa: E501 + """ Helper object to build a pipeline of TestCases. This allows to combine + TestCases and to execute them after each other. Custom connector functions + can be used to exchange and manipulate the configuration of a subsequent + TestCase. + + :param test_cases: A list of objects following the AutomotiveTestCaseABC + interface + :param connectors: A list of connector functions. A connector function + takes two TestCase objects and returns a dictionary which is provided + to the second TestCase as kwargs of the execute function. + + + Example: + >>> class MyTestCase2(AutomotiveTestCaseABC): + >>> pass + >>> + >>> class MyTestCase1(AutomotiveTestCaseABC): + >>> pass + >>> + >>> def connector(testcase1, testcase2): + >>> scan_range = len(testcase1.results) + >>> return {"verbose": True, "scan_range": scan_range} + >>> + >>> tc1 = MyTestCase1() + >>> tc2 = MyTestCase2() + >>> pipeline = StagedAutomotiveTestCase([tc1, tc2], [None, connector]) + """ + + # Delay the increment of a stage after the current stage is finished + # has_completed() has to be called five times in order to increment the + # current stage. This ensures, that the current stage is executed for + # all possible states of the DUT, and no state is missed for the first + # TestCase. + __delay_stages = 5 + + def __init__(self, + test_cases, # type: List[AutomotiveTestCaseABC] + connectors=None # type: Optional[List[Optional[_TestCaseConnectorCallable]]] # noqa: E501 + ): # type: (...) -> None + super(StagedAutomotiveTestCase, self).__init__() + self.__test_cases = test_cases + self.__connectors = connectors + self.__stage_index = 0 + self.__completion_delay = 0 + self.__current_kwargs = None # type: Optional[Dict[str, Any]] + + def __getitem__(self, item): + # type: (int) -> AutomotiveTestCaseABC + return self.__test_cases[item] + + def __len__(self): + # type: () -> int + return len(self.__test_cases) + + # TODO: Fix unit tests and remove this function + def __reduce__(self): # type: ignore + f, t, d = super(StagedAutomotiveTestCase, self).__reduce__() # type: ignore # noqa: E501 + try: + del d["_StagedAutomotiveTestCase__connectors"] + except KeyError: + pass + return f, t, d + + @property + def test_cases(self): + # type: () -> List[AutomotiveTestCaseABC] + return self.__test_cases + + @property + def current_test_case(self): + # type: () -> AutomotiveTestCaseABC + return self[self.__stage_index] + + @property + def current_connector(self): + # type: () -> Optional[_TestCaseConnectorCallable] + if not self.__connectors: + return None + else: + return self.__connectors[self.__stage_index] + + @property + def previous_test_case(self): + # type: () -> Optional[AutomotiveTestCaseABC] + return self.__test_cases[self.__stage_index - 1] if \ + self.__stage_index > 0 else None + + def get_generated_test_case(self): + # type: () -> Optional[AutomotiveTestCaseABC] + try: + test_case = cast(TestCaseGenerator, self.current_test_case) + return test_case.get_generated_test_case() + except AttributeError: + return None + + def get_new_edge(self, + socket, # type: _SocketUnion + config # type: AutomotiveTestCaseExecutorConfiguration + ): # type: (...) -> Optional[_Edge] + try: + test_case = cast(StateGenerator, self.current_test_case) + return test_case.get_new_edge(socket, config) + except AttributeError: + return None + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + try: + test_case = cast(StateGenerator, self.current_test_case) + return test_case.get_transition_function(socket, edge) + except AttributeError: + return None + + def has_completed(self, state): + # type: (EcuState) -> bool + if not (self.current_test_case.has_completed(state) and + self.current_test_case.completed): + # current test_case not fully completed + # reset completion delay, since new states could have been appeared + self.__completion_delay = 0 + return False + + # current stage is finished. We have to increase the stage + if self.__completion_delay < StagedAutomotiveTestCase.__delay_stages: + # First we wait five more iteration of the executor + # Maybe one more execution reveals new states of other + # test_cases + self.__completion_delay += 1 + return False + + # current test_case is fully completed + elif self.__stage_index == len(self.__test_cases) - 1: + # this test_case was the last test_case... nothing to do + return True + + else: + # We waited more iterations and no new state appeared, + # let's enter the next stage + log_automotive.info( + "Staged AutomotiveTestCase %s completed", + self.current_test_case.__class__.__name__) + self.__stage_index += 1 + self.__completion_delay = 0 + return False + + def pre_execute(self, + socket, # type: _SocketUnion + state, # type: EcuState + global_configuration # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + ): # type: (...) -> None + test_case_cls = self.current_test_case.__class__ + try: + self.__current_kwargs = global_configuration[ + test_case_cls.__name__] + except KeyError: + self.__current_kwargs = dict() + global_configuration[test_case_cls.__name__] = \ + self.__current_kwargs + + if callable(self.current_connector) and self.__stage_index > 0: + if self.previous_test_case: + con = self.current_connector # type: _TestCaseConnectorCallable # noqa: E501 + con_kwargs = con(self.previous_test_case, + self.current_test_case) + if self.__current_kwargs is not None and con_kwargs is not None: # noqa: E501 + self.__current_kwargs.update(con_kwargs) + + log_automotive.debug("Stage AutomotiveTestCase %s kwargs: %s", + self.current_test_case.__class__.__name__, + self.__current_kwargs) + + self.current_test_case.pre_execute(socket, state, global_configuration) + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + kwargs.update(self.__current_kwargs or dict()) + self.current_test_case.execute(socket, state, **kwargs) + + def post_execute(self, + socket, # type: _SocketUnion + state, # type: EcuState + global_configuration # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + ): # type: (...) -> None + self.current_test_case.post_execute( + socket, state, global_configuration) + + @staticmethod + def _show_headline(headline, sep="="): + # type: (str, str) -> str + s = "\n\n" + sep * (len(headline) + 10) + "\n" + s += " " * 5 + headline + "\n" + s += sep * (len(headline) + 10) + "\n" + return s + "\n" + + def show(self, dump=False, filtered=True, verbose=False): + # type: (bool, bool, bool) -> Optional[str] + s = self._show_headline("AutomotiveTestCase Pipeline", "=") + for idx, t in enumerate(self.__test_cases): + s += self._show_headline( + "AutomotiveTestCase Stage %d" % idx, "-") + s += t.show(True, filtered, verbose) or "" + + if dump: + return s + "\n" + else: + print(s) + return None + + @property + def completed(self): + # type: () -> bool + return all(e.completed for e in self.__test_cases) and \ + self.__completion_delay >= StagedAutomotiveTestCase.__delay_stages + + @property + def supported_responses(self): + # type: () -> List[EcuResponse] + supported_responses = list() + for tc in self.test_cases: + supported_responses += tc.supported_responses + + supported_responses.sort(key=Ecu.sort_key_func) + return supported_responses + + def runtime_estimation(self): + # type: () -> Optional[Tuple[int, int, float]] + + if hasattr(self.current_test_case, "runtime_estimation"): + cur_est = self.current_test_case.runtime_estimation() + if cur_est: + return len(self.test_cases), \ + self.__stage_index, \ + float(self.__stage_index) / len(self.test_cases) + \ + cur_est[2] / len(self.test_cases) + + return len(self.test_cases), \ + self.__stage_index, \ + float(self.__stage_index) / len(self.test_cases) diff --git a/scapy/contrib/automotive/scanner/test_case.py b/scapy/contrib/automotive/scanner/test_case.py new file mode 100644 index 00000000000..854ee1ca794 --- /dev/null +++ b/scapy/contrib/automotive/scanner/test_case.py @@ -0,0 +1,270 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = TestCase base class definitions +# scapy.contrib.status = library + + +import abc +from collections import defaultdict + +from scapy.utils import make_lined_table, SingleConversationSocket +from scapy.supersocket import SuperSocket +from scapy.contrib.automotive.scanner.graph import _Edge +from scapy.contrib.automotive.ecu import EcuState, EcuResponse +from scapy.error import Scapy_Exception + + +# Typing imports +from typing import ( + Any, + Union, + List, + Optional, + Dict, + Tuple, + Set, + Callable, + TYPE_CHECKING, +) +if TYPE_CHECKING: + from scapy.contrib.automotive.scanner.configuration import AutomotiveTestCaseExecutorConfiguration # noqa: E501 + + +# type definitions +_SocketUnion = Union[SuperSocket, SingleConversationSocket] +_TransitionCallable = Callable[[_SocketUnion, "AutomotiveTestCaseExecutorConfiguration", Dict[str, Any]], bool] # noqa: E501 +_CleanupCallable = Callable[[_SocketUnion, "AutomotiveTestCaseExecutorConfiguration"], bool] # noqa: E501 +_TransitionTuple = Tuple[_TransitionCallable, Dict[str, Any], Optional[_CleanupCallable]] # noqa: E501 + + +class AutomotiveTestCaseABC(metaclass=abc.ABCMeta): + """ + Base class for "TestCase" objects. In automotive scanners, these TestCase + objects are used for individual tasks, for example enumerating over one + kind of functionality of the protocol. It is also possible, that + these TestCase objects execute complex tests on an ECU. + The TestCaseExecuter object has a list of TestCases. The executer + manipulates a device under test (DUT), to enter a certain state. In this + state, the TestCase object gets executed. + """ + + _supported_kwargs = {} # type: Dict[str, Tuple[Any, Optional[Callable[[Any], bool]]]] # noqa: E501 + _supported_kwargs_doc = "" + + @abc.abstractmethod + def has_completed(self, state): + # type: (EcuState) -> bool + """ + Tells if this TestCase was executed for a certain state + :param state: State of interest + :return: True, if TestCase was executed in the questioned state + """ + raise NotImplementedError() + + @abc.abstractmethod + def pre_execute(self, + socket, # type: _SocketUnion + state, # type: EcuState + global_configuration # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + ): # type: (...) -> None + """ + Will be executed previously to ``execute``. This function can be used + to manipulate the configuration passed to execute. + + :param socket: Socket object with the connection to a DUT + :param state: Current state of the DUT + :param global_configuration: Configuration of the TestCaseExecutor + """ + raise NotImplementedError() + + @abc.abstractmethod + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + """ + Executes this TestCase for a given state + + :param socket: Socket object with the connection to a DUT + :param state: Current state of the DUT + :param kwargs: Local configuration of the TestCasesExecutor + :return: + """ + raise NotImplementedError() + + @abc.abstractmethod + def post_execute(self, + socket, # type: _SocketUnion + state, # type: EcuState + global_configuration # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + ): # type: (...) -> None + """ + Will be executed subsequently to ``execute``. This function can be used + for additional evaluations after the ``execute``. + + :param socket: Socket object with the connection to a DUT + :param state: Current state of the DUT + :param global_configuration: Configuration of the TestCaseExecutor + """ + raise NotImplementedError() + + @abc.abstractmethod + def show(self, dump=False, filtered=True, verbose=False): + # type: (bool, bool, bool) -> Optional[str] + """ + Shows results of TestCase + + :param dump: If True, the results will be returned; If False, the + results will be printed. + :param filtered: If True, the negative responses will be filtered + dynamically. + :param verbose: If True, additional information will be provided. + :return: test results of TestCase if parameter ``dump`` is True, + else ``None`` + """ + raise NotImplementedError() + + @property + @abc.abstractmethod + def completed(self): + # type: () -> bool + """ + Tells if this TestCase is completely executed + :return: True, if TestCase is completely executed + """ + raise NotImplementedError + + @property + @abc.abstractmethod + def supported_responses(self): + # type: () -> List[EcuResponse] + """ + Tells the supported responses in TestCase + :return: The list of supported responses + """ + raise NotImplementedError + + +class AutomotiveTestCase(AutomotiveTestCaseABC): + """ Base class for TestCases""" + + _description = "AutomotiveTestCase" + _supported_kwargs = AutomotiveTestCaseABC._supported_kwargs + _supported_kwargs_doc = AutomotiveTestCaseABC._supported_kwargs_doc + + def __init__(self): + # type: () -> None + self._state_completed = defaultdict(bool) # type: Dict[EcuState, bool] + + def has_completed(self, state): + # type: (EcuState) -> bool + return self._state_completed[state] + + @classmethod + def check_kwargs(cls, kwargs): + # type: (Dict[str, Any]) -> None + for k, v in kwargs.items(): + if k not in cls._supported_kwargs.keys(): + raise Scapy_Exception( + "Keyword-Argument %s not supported for %s" % + (k, cls.__name__)) + ti, vf = cls._supported_kwargs[k] + if ti is not None and not isinstance(v, ti): + raise Scapy_Exception( + "Keyword-Value '%s' is not instance of type %s" % + (k, str(ti))) + if vf is not None and not vf(v): + raise Scapy_Exception( + "Validation Error: '%s: %s' is not in the allowed " + "value range" % (k, str(v)) + ) + + @property + def completed(self): + # type: () -> bool + return all(v for _, v in self._state_completed.items()) + + @property + def scanned_states(self): + # type: () -> Set[EcuState] + """ + Helper function to get all scanned states + :return: all scanned states + """ + return set(self._state_completed.keys()) + + def pre_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + pass + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + raise NotImplementedError() + + def post_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + pass + + def _show_header(self, **kwargs): + # type: (Any) -> str + s = "\n\n" + "=" * (len(self._description) + 10) + "\n" + s += " " * 5 + self._description + "\n" + s += "-" * (len(self._description) + 10) + "\n" + + return s + "\n" + + def _show_state_information(self, **kwargs): + # type: (Any) -> str + completed = [(state, self._state_completed[state]) + for state in self.scanned_states] + return make_lined_table( + completed, lambda x, y: ("Scan state completed", x, y), + dump=True) or "" + + def show(self, dump=False, filtered=True, verbose=False): + # type: (bool, bool, bool) -> Optional[str] + + s = self._show_header() + + if verbose: + s += self._show_state_information() + + if dump: + return s + "\n" + else: + print(s) + return None + + +class TestCaseGenerator(metaclass=abc.ABCMeta): + @abc.abstractmethod + def get_generated_test_case(self): + # type: () -> Optional[AutomotiveTestCaseABC] + raise NotImplementedError() + + +class StateGenerator(metaclass=abc.ABCMeta): + + @abc.abstractmethod + def get_new_edge(self, socket, config): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration) -> Optional[_Edge] # noqa: E501 + raise NotImplementedError + + @abc.abstractmethod + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + """ + + :param socket: Socket to target + :param edge: Tuple of EcuState objects for the requested + transition function + :return: Returns an optional tuple consisting of a transition function, + a keyword arguments dictionary for the transition function + and a cleanup function. Both functions + take a Socket and the TestCaseExecutor configuration as + arguments and return True if the execution was successful. + The first function is the state enter function, the second + function is a cleanup function + """ + raise NotImplementedError diff --git a/scapy/contrib/automotive/someip.py b/scapy/contrib/automotive/someip.py index 523de6905f5..40d43a09fd4 100644 --- a/scapy/contrib/automotive/someip.py +++ b/scapy/contrib/automotive/someip.py @@ -1,47 +1,26 @@ -# MIT License - -# Copyright (c) 2018 Jose Amores - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Sebastian Baar -# This program is published under a GPLv2 license +# Copyright (c) 2018 Jose Amores # scapy.contrib.description = Scalable service-Oriented MiddlewarE/IP (SOME/IP) # scapy.contrib.status = loads -import ctypes -import collections import struct from scapy.layers.inet import TCP, UDP from scapy.layers.inet6 import IP6Field from scapy.compat import raw, orb from scapy.config import conf -from scapy.modules.six.moves import range -from scapy.packet import Packet, Raw, bind_top_down, bind_bottom_up -from scapy.fields import XShortField, BitEnumField, ConditionalField, \ - BitField, XBitField, IntField, XByteField, ByteEnumField, \ - ShortField, X3BytesField, StrLenField, IPField, FieldLenField, \ - PacketListField, XIntField +from scapy.packet import (Packet, Raw, bind_top_down, bind_bottom_up, + bind_layers) +from scapy.fields import (XShortField, ConditionalField, + BitField, XBitField, XByteField, ByteEnumField, + ShortField, X3BytesField, StrLenField, IPField, + FieldLenField, PacketListField, XIntField, + MultipleTypeField, FlagsField, + XByteEnumField, BitScalingField, LenField) class SOMEIP(Packet): @@ -64,8 +43,8 @@ class SOMEIP(Packet): TYPE_TP_REQUEST = 0x20 TYPE_TP_REQUEST_NO_RET = 0x21 TYPE_TP_NOTIFICATION = 0x22 - TYPE_TP_RESPONSE = 0x23 - TYPE_TP_ERROR = 0x24 + TYPE_TP_RESPONSE = 0xa0 + TYPE_TP_ERROR = 0xa1 RET_E_OK = 0x00 RET_E_NOT_OK = 0x01 RET_E_UNKNOWN_SERVICE = 0x02 @@ -84,12 +63,18 @@ class SOMEIP(Packet): fields_desc = [ XShortField("srv_id", 0), - BitEnumField("sub_id", 0, 1, {0: "METHOD_ID", 1: "EVENT_ID"}), - ConditionalField(XBitField("method_id", 0, 15), - lambda pkt: pkt.sub_id == 0), - ConditionalField(XBitField("event_id", 0, 15), - lambda pkt: pkt.sub_id == 1), - IntField("len", None), + MultipleTypeField( + [ + (XShortField("sub_id", 0), + (lambda pkt: False, + lambda pkt, val: val < 0x8000), "method_id"), + (XShortField("sub_id", 0), + (lambda pkt: False, + lambda pkt, val: val >= 0x8000), "event_id"), + ], + XShortField("sub_id", 0), + ), + LenField("len", None, fmt=">I", adjust=lambda x: x + 8), XShortField("client_id", 0), XShortField("session_id", 0), XByteField("proto_ver", PROTOCOL_VERSION), @@ -124,14 +109,27 @@ class SOMEIP(Packet): RET_E_MALFORMED_MSG: "E_MALFORMED_MESSAGE", RET_E_WRONG_MESSAGE_TYPE: "E_WRONG_MESSAGE_TYPE", }), - ConditionalField(BitField("offset", 0, 28), - lambda pkt: SOMEIP._is_tp(pkt)), - ConditionalField(BitField("res", 0, 3), - lambda pkt: SOMEIP._is_tp(pkt)), - ConditionalField(BitField("more_seg", 0, 1), - lambda pkt: SOMEIP._is_tp(pkt)) + ConditionalField( + BitScalingField("offset", 0, 28, scaling=16, unit="bytes"), + lambda pkt: SOMEIP._is_tp(pkt)), # noqa: E501 + ConditionalField( + BitField("res", 0, 3), + lambda pkt: SOMEIP._is_tp(pkt)), # noqa: E501 + ConditionalField( + BitField("more_seg", 0, 1), + lambda pkt: SOMEIP._is_tp(pkt)), # noqa: E501 + PacketListField( + "data", [Raw()], Raw, + length_from=lambda pkt: pkt.len - (SOMEIP.LEN_OFFSET_TP if (SOMEIP._is_tp(pkt) and (pkt.len is None or pkt.len >= SOMEIP.LEN_OFFSET_TP)) else SOMEIP.LEN_OFFSET), # noqa: E501 + next_cls_cb=lambda pkt, lst, cur, remain: SOMEIP.get_payload_cls_by_srv_id(pkt, lst, cur, remain)) # noqa: E501 ] + payload_cls_by_srv_id = dict() # To be customized + + @staticmethod + def get_payload_cls_by_srv_id(pkt, lst, cur, remain): + return SOMEIP.payload_cls_by_srv_id.get(pkt.srv_id, Raw) + def post_build(self, pkt, pay): length = self.len if length is None: @@ -144,7 +142,7 @@ def post_build(self, pkt, pay): return pkt + pay def answers(self, other): - if other.__class__ == self.__class__: + if isinstance(other, type(self)): if self.msg_type in [SOMEIP.TYPE_REQUEST_NO_RET, SOMEIP.TYPE_REQUEST_NORET_ACK, SOMEIP.TYPE_NOTIFICATION, @@ -157,14 +155,18 @@ def answers(self, other): @staticmethod def _is_tp(pkt): """Returns true if pkt is using SOMEIP-TP, else returns false.""" + if isinstance(pkt, Packet): + return pkt.msg_type & 0x20 + else: + return pkt[15] & 0x20 - tp = [SOMEIP.TYPE_TP_REQUEST, SOMEIP.TYPE_TP_REQUEST_NO_RET, - SOMEIP.TYPE_TP_NOTIFICATION, SOMEIP.TYPE_TP_RESPONSE, - SOMEIP.TYPE_TP_ERROR] + @staticmethod + def _is_sd(pkt): + """Returns true if pkt is using SOMEIP-SD, else returns false.""" if isinstance(pkt, Packet): - return pkt.msg_type in tp + return pkt.srv_id == 0xffff and pkt.sub_id == 0x8100 else: - return pkt[15] in tp + return pkt[:4] == b"\xff\xff\x81\x00" def fragment(self, fragsize=1392): """Fragment SOME/IP-TP""" @@ -175,21 +177,35 @@ def fragment(self, fragsize=1392): fnb += 1 fl = fl.underlayer + has_payload = len(self.data) == 0 or sum(len(p) for p in self.data) == 0 + for p in fl: - s = raw(p[fnb].payload) + if has_payload: + s = raw(p[fnb].payload) + else: + s = raw(p[fnb].data[0]) nb = (len(s) + fragsize) // fragsize for i in range(nb): q = p.copy() - del q[fnb].payload + if has_payload: + del q[fnb].payload + else: + del q[fnb].data[0] q[fnb].len = SOMEIP.LEN_OFFSET_TP + \ len(s[i * fragsize:(i + 1) * fragsize]) q[fnb].more_seg = 1 if i == nb - 1: q[fnb].more_seg = 0 - q[fnb].offset += i * fragsize // 16 + q[fnb].offset += i * fragsize r = conf.raw_layer(load=s[i * fragsize:(i + 1) * fragsize]) - r.overload_fields = p[fnb].payload.overload_fields.copy() - q.add_payload(r) + if has_payload: + r.overload_fields = p[fnb].payload.overload_fields.copy() + else: + r.overload_fields = p[fnb].data[0].overload_fields.copy() + if has_payload: + q.add_payload(r) + else: + q.data.append(r) lst.append(q) return lst @@ -206,10 +222,12 @@ def _bind_someip_layers(): _bind_someip_layers() +bind_layers(SOMEIP, SOMEIP) class _SDPacketBase(Packet): """ base class to be used among all SD Packet definitions.""" + def extract_padding(self, s): return "", s @@ -227,7 +245,11 @@ def extract_padding(self, s): def _MAKE_SDENTRY_COMMON_FIELDS_DESC(type): return [ - XByteField("type", type), + XByteEnumField("type", type, { + 0: "FindService", + 1: "OfferService", + 6: "SubscribeEventgroup", + 7: "SubscribeEventgroupACK"}), XByteField("index_1", 0), XByteField("index_2", 0), XBitField("n_opt_1", 0, 4), @@ -310,7 +332,15 @@ def _sdoption_class(payload, **kargs): def _MAKE_COMMON_SDOPTION_FIELDS_DESC(type, length=None): return [ ShortField("len", length), - XByteField("type", type), + XByteEnumField("type", type, { + SDOPTION_CFG_TYPE: "Configuration", + SDOPTION_LOADBALANCE_TYPE: "LoadBalancing", + SDOPTION_IP4_ENDPOINT_TYPE: "IPv4Endpoint", + SDOPTION_IP4_MCAST_TYPE: "IPv4MultiCast", + SDOPTION_IP4_SDENDPOINT_TYPE: "IPv4SDEndpoint", + SDOPTION_IP6_ENDPOINT_TYPE: "IPv6Endpoint", + SDOPTION_IP6_MCAST_TYPE: "IPv6MultiCast", + SDOPTION_IP6_SDENDPOINT_TYPE: "IPv6SDEndpoint"}), XByteField("res_hdr", 0) ] @@ -326,7 +356,7 @@ def _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC(): class SDOption_Config(_SDPacketBase): name = "Config Option" fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC(SDOPTION_CFG_TYPE) + [ - StrLenField("cfg_str", "\x00", length_from=lambda pkt: pkt.len - 1) + StrLenField("cfg_str", b"\x00", length_from=lambda pkt: pkt.len - 1) ] def post_build(self, pkt, pay): @@ -442,8 +472,7 @@ class SD(_SDPacketBase): p.option_array = [SDOption_Config(),SDOption_IP6_EndPoint()] """ SOMEIP_MSGID_SRVID = 0xffff - SOMEIP_MSGID_SUBID = 0x1 - SOMEIP_MSGID_EVENTID = 0x100 + SOMEIP_MSGID_SUBID = 0x8100 SOMEIP_CLIENT_ID = 0x0000 SOMEIP_MINIMUM_SESSION_ID = 0x0001 SOMEIP_PROTO_VER = 0x01 @@ -451,41 +480,22 @@ class SD(_SDPacketBase): SOMEIP_MSG_TYPE = SOMEIP.TYPE_NOTIFICATION SOMEIP_RETCODE = SOMEIP.RET_E_OK - _sdFlag = collections.namedtuple('Flag', 'mask offset') - FLAGSDEF = { - "REBOOT": _sdFlag(mask=0x80, offset=7), - "UNICAST": _sdFlag(mask=0x40, offset=6) - } - name = "SD" fields_desc = [ - XByteField("flags", 0), + FlagsField("flags", 0, 8, [ + "res0", "res1", "res2", "res3", "res4", + "EXPLICIT_INITIAL_DATA_CONTROL", "UNICAST", "REBOOT"]), X3BytesField("res", 0), FieldLenField("len_entry_array", None, length_of="entry_array", fmt="!I"), - PacketListField("entry_array", None, cls=_sdentry_class, + PacketListField("entry_array", None, _sdentry_class, length_from=lambda pkt: pkt.len_entry_array), FieldLenField("len_option_array", None, length_of="option_array", fmt="!I"), - PacketListField("option_array", None, cls=_sdoption_class, + PacketListField("option_array", None, _sdoption_class, length_from=lambda pkt: pkt.len_option_array) ] - def get_flag(self, name): - name = name.upper() - if name in self.FLAGSDEF: - return ((self.flags & self.FLAGSDEF[name].mask) >> - self.FLAGSDEF[name].offset) - else: - return None - - def set_flag(self, name, value): - name = name.upper() - if name in self.FLAGSDEF: - self.flags = (self.flags & - (ctypes.c_ubyte(~self.FLAGSDEF[name].mask).value)) \ - | ((value & 0x01) << self.FLAGSDEF[name].offset) - def set_entryArray(self, entry_list): if isinstance(entry_list, list): self.entry_array = entry_list @@ -504,7 +514,6 @@ def set_optionArray(self, option_list): sub_id=SD.SOMEIP_MSGID_SUBID, client_id=SD.SOMEIP_CLIENT_ID, session_id=SD.SOMEIP_MINIMUM_SESSION_ID, - event_id=SD.SOMEIP_MSGID_EVENTID, proto_ver=SD.SOMEIP_PROTO_VER, iface_ver=SD.SOMEIP_IFACE_VER, msg_type=SD.SOMEIP_MSG_TYPE, @@ -513,7 +522,6 @@ def set_optionArray(self, option_list): bind_bottom_up(SOMEIP, SD, srv_id=SD.SOMEIP_MSGID_SRVID, sub_id=SD.SOMEIP_MSGID_SUBID, - event_id=SD.SOMEIP_MSGID_EVENTID, proto_ver=SD.SOMEIP_PROTO_VER, iface_ver=SD.SOMEIP_IFACE_VER, msg_type=SD.SOMEIP_MSG_TYPE, diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index ef2d1d7282a..aedd4f5652b 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -1,37 +1,79 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.description = Unified Diagnostic Service (UDS) # scapy.contrib.status = loads +""" +UDS +""" + import struct -from itertools import product +from collections import defaultdict + from scapy.fields import ByteEnumField, StrField, ConditionalField, \ BitEnumField, BitField, XByteField, FieldListField, \ XShortField, X3BytesField, XIntField, ByteField, \ - ShortField, ObservableDict, XShortEnumField, XByteEnumField -from scapy.packet import Packet, bind_layers, NoPayload + ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ + FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ + PacketField +from scapy.packet import Packet, NoPayload, Raw, bind_layers +from scapy.compat import orb from scapy.config import conf -from scapy.error import log_loading from scapy.utils import PeriodicSenderThread from scapy.contrib.isotp import ISOTP -""" -UDS -""" +# Typing imports +from typing import ( # noqa: F401 + Dict, + Type, + Union, +) try: if conf.contribs['UDS']['treat-response-pending-as-answer']: pass except KeyError: - log_loading.info("Specify \"conf.contribs['UDS'] = " - "{'treat-response-pending-as-answer': True}\" to treat " - "a negative response 'requestCorrectlyReceived-" - "ResponsePending' as answer of a request. \n" - "The default value is False.") - conf.contribs['UDS'] = {'treat-response-pending-as-answer': False} + # log_loading.info("Specify \"conf.contribs['UDS'] = " + # "{'treat-response-pending-as-answer': True}\" to treat " + # "a negative response 'requestCorrectlyReceived-" + # "ResponsePending' as answer of a request. \n" + # "The default value is False.") + conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} + +conf.debug_dissector = True + + +def _uds_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['UDS']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`UDS` acts as a dispatch layer and returns the + matching service sub-packet directly (e.g. + ``UDS(b'\\x10\\x01')`` → ``UDS_DSC``). Each sub-packet gains its + own ``service`` field so that it can be built and dissected + stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`UDS` packet. This prevents a + duplicate service byte when a sub-packet is stacked on top of a UDS + base layer (``UDS()/UDS_DSC()``). Set to *False* to always emit the + ``service`` byte from the sub-packet regardless of stacking. + """ + if not conf.contribs['UDS'].get('single_layer_mode', False): + return False + if conf.contribs['UDS'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, UDS) + return True class UDS(ISOTP): @@ -45,6 +87,7 @@ class UDS(ISOTP): 0x24: 'ReadScalingDataByIdentifier', 0x27: 'SecurityAccess', 0x28: 'CommunicationControl', + 0x29: 'Authentication', 0x2A: 'ReadDataPeriodicIdentifier', 0x2C: 'DynamicallyDefineDataIdentifier', 0x2E: 'WriteDataByIdentifier', @@ -54,6 +97,7 @@ class UDS(ISOTP): 0x35: 'RequestUpload', 0x36: 'TransferData', 0x37: 'RequestTransferExit', + 0x38: 'RequestFileTransfer', 0x3D: 'WriteMemoryByAddress', 0x3E: 'TesterPresent', 0x50: 'DiagnosticSessionControlPositiveResponse', @@ -65,6 +109,7 @@ class UDS(ISOTP): 0x64: 'ReadScalingDataByIdentifierPositiveResponse', 0x67: 'SecurityAccessPositiveResponse', 0x68: 'CommunicationControlPositiveResponse', + 0x69: 'AuthenticationPositiveResponse', 0x6A: 'ReadDataPeriodicIdentifierPositiveResponse', 0x6C: 'DynamicallyDefineDataIdentifierPositiveResponse', 0x6E: 'WriteDataByIdentifierPositiveResponse', @@ -74,6 +119,7 @@ class UDS(ISOTP): 0x75: 'RequestUploadPositiveResponse', 0x76: 'TransferDataPositiveResponse', 0x77: 'RequestTransferExitPositiveResponse', + 0x78: 'RequestFileTransferPositiveResponse', 0x7D: 'WriteMemoryByAddressPositiveResponse', 0x7E: 'TesterPresentPositiveResponse', 0x83: 'AccessTimingParameter', @@ -86,30 +132,43 @@ class UDS(ISOTP): 0xC5: 'ControlDTCSettingPositiveResponse', 0xC6: 'ResponseOnEventPositiveResponse', 0xC7: 'LinkControlPositiveResponse', - 0x7f: 'NegativeResponse'}) + 0x7f: 'NegativeResponse'}) # type: Dict[int, str] name = 'UDS' fields_desc = [ XByteEnumField('service', 0, services) ] def answers(self, other): + # type: (Union[UDS, Packet]) -> bool if other.__class__ != self.__class__: return False if self.service == 0x7f: - return self.payload.answers(other) + return bool(self.payload.answers(other)) if self.service == (other.service + 0x40): if isinstance(self.payload, NoPayload) or \ isinstance(other.payload, NoPayload): return len(self) <= len(other) else: - return self.payload.answers(other.payload) + return bool(self.payload.answers(other.payload)) return False def hashret(self): - if self.service == 0x7f: - return struct.pack('B', self.requestServiceId) + # type: () -> bytes + if self.service == 0x7f and len(self) >= 3: + return struct.pack('B', bytes(self)[1] & ~0x40) return struct.pack('B', self.service & ~0x40) + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct UDS service class in single layer mode.""" + if conf.contribs['UDS'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + # ########################DSC################################### class UDS_DSC(Packet): @@ -122,41 +181,31 @@ class UDS_DSC(Packet): 0x7F: 'ISOSAEReserved'}) name = 'DiagnosticSessionControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x10, UDS.services), _uds_slm), ByteEnumField('diagnosticSessionType', 0, diagnosticSessionTypes) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_DSC.diagnosticSessionType%") - bind_layers(UDS, UDS_DSC, service=0x10) +UDS._service_cls[0x10] = UDS_DSC class UDS_DSCPR(Packet): name = 'DiagnosticSessionControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x50, UDS.services), _uds_slm), ByteEnumField('diagnosticSessionType', 0, UDS_DSC.diagnosticSessionTypes), - StrField('sessionParameterRecord', B"") + StrField('sessionParameterRecord', b"") ] def answers(self, other): - return other.__class__ == UDS_DSC and \ + return isinstance(other, UDS_DSC) and \ other.diagnosticSessionType == self.diagnosticSessionType - @staticmethod - def modifies_ecu_state(pkt, ecu): - ecu.current_session = pkt.diagnosticSessionType - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_DSCPR.diagnosticSessionType%") - bind_layers(UDS, UDS_DSCPR, service=0x50) +UDS._service_cls[0x50] = UDS_DSCPR # #########################ER################################### @@ -168,97 +217,69 @@ class UDS_ER(Packet): 0x03: 'softReset', 0x04: 'enableRapidPowerShutDown', 0x05: 'disableRapidPowerShutDown', + 0x41: 'powerDown', 0x7F: 'ISOSAEReserved'} name = 'ECUReset' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x11, UDS.services), _uds_slm), ByteEnumField('resetType', 0, resetTypes) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_ER.resetType%") - bind_layers(UDS, UDS_ER, service=0x11) +UDS._service_cls[0x11] = UDS_ER class UDS_ERPR(Packet): name = 'ECUResetPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x51, UDS.services), _uds_slm), ByteEnumField('resetType', 0, UDS_ER.resetTypes), ConditionalField(ByteField('powerDownTime', 0), lambda pkt: pkt.resetType == 0x04) ] def answers(self, other): - return other.__class__ == UDS_ER - - @staticmethod - def modifies_ecu_state(_, ecu): - ecu.reset() - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_ER.resetType%") + return isinstance(other, UDS_ER) and other.resetType == self.resetType bind_layers(UDS, UDS_ERPR, service=0x51) +UDS._service_cls[0x51] = UDS_ERPR # #########################SA################################### class UDS_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x27, UDS.services), _uds_slm), ByteField('securityAccessType', 0), - ConditionalField(StrField('securityAccessDataRecord', B""), + ConditionalField(StrField('securityAccessDataRecord', b""), lambda pkt: pkt.securityAccessType % 2 == 1), - ConditionalField(StrField('securityKey', B""), + ConditionalField(StrField('securityKey', b""), lambda pkt: pkt.securityAccessType % 2 == 0) ] - @staticmethod - def get_log(pkt): - if pkt.securityAccessType % 2 == 1: - return pkt.sprintf("%UDS.service%"),\ - (pkt.securityAccessType, None) - else: - return pkt.sprintf("%UDS.service%"),\ - (pkt.securityAccessType, pkt.securityKey) - bind_layers(UDS, UDS_SA, service=0x27) +UDS._service_cls[0x27] = UDS_SA class UDS_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x67, UDS.services), _uds_slm), ByteField('securityAccessType', 0), - ConditionalField(StrField('securitySeed', B""), + ConditionalField(StrField('securitySeed', b""), lambda pkt: pkt.securityAccessType % 2 == 1), ] def answers(self, other): - return other.__class__ == UDS_SA \ + return isinstance(other, UDS_SA) \ and other.securityAccessType == self.securityAccessType - @staticmethod - def modifies_ecu_state(pkt, ecu): - if pkt.securityAccessType % 2 == 0: - ecu.current_security_level = pkt.securityAccessType - - @staticmethod - def get_log(pkt): - if pkt.securityAccessType % 2 == 0: - return pkt.sprintf("%UDS.service%"),\ - (pkt.securityAccessType, None) - else: - return pkt.sprintf("%UDS.service%"),\ - (pkt.securityAccessType, pkt.securitySeed) - bind_layers(UDS, UDS_SAPR, service=0x67) +UDS._service_cls[0x67] = UDS_SAPR # #########################CC################################### @@ -271,6 +292,7 @@ class UDS_CC(Packet): } name = 'CommunicationControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x28, UDS.services), _uds_slm), ByteEnumField('controlType', 0, controlTypes), BitEnumField('communicationType0', 0, 2, {0: 'ISOSAEReserved', @@ -298,68 +320,215 @@ class UDS_CC(Packet): 15: 'Disable/Enable network'}) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_CC.controlType%") - bind_layers(UDS, UDS_CC, service=0x28) +UDS._service_cls[0x28] = UDS_CC class UDS_CCPR(Packet): name = 'CommunicationControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x68, UDS.services), _uds_slm), ByteEnumField('controlType', 0, UDS_CC.controlTypes) ] def answers(self, other): - return other.__class__ == UDS_CC \ + return isinstance(other, UDS_CC) \ and other.controlType == self.controlType - @staticmethod - def modifies_ecu_state(pkt, ecu): - ecu.communication_control = pkt.controlType - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_CCPR.controlType%") +bind_layers(UDS, UDS_CCPR, service=0x68) +UDS._service_cls[0x68] = UDS_CCPR + + +# #########################AUTH################################### +class UDS_AUTH(Packet): + subFunctions = { + 0x00: 'deAuthenticate', + 0x01: 'verifyCertificateUnidirectional', + 0x02: 'verifyCertificateBidirectional', + 0x03: 'proofOfOwnership', + 0x04: 'transmitCertificate', + 0x05: 'requestChallengeForAuthentication', + 0x06: 'verifyProofOfOwnershipUnidirectional', + 0x07: 'verifyProofOfOwnershipBidirectional', + 0x08: 'authenticationConfiguration', + 0x7F: 'ISOSAEReserved' + } + name = "Authentication" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x29, UDS.services), _uds_slm), + ByteEnumField('subFunction', 0, subFunctions), + ConditionalField(XByteField('communicationConfiguration', 0), + lambda pkt: pkt.subFunction in [0x01, 0x02, 0x5]), + ConditionalField(XShortField('certificateEvaluationId', 0), + lambda pkt: pkt.subFunction == 0x04), + ConditionalField( + XStrFixedLenField('algorithmIndicator', b'\x00' * 16, length=16), + lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), + ConditionalField(FieldLenField('lengthOfCertificateClient', None, + fmt="H", length_of='certificateClient'), + lambda pkt: pkt.subFunction in [0x01, 0x02]), + ConditionalField(XStrLenField('certificateClient', b"", + length_from=lambda p: + p.lengthOfCertificateClient), + lambda pkt: pkt.subFunction in [0x01, 0x02]), + ConditionalField(FieldLenField('lengthOfProofOfOwnershipClient', None, + fmt="H", + length_of='proofOfOwnershipClient'), + lambda pkt: pkt.subFunction in [0x03, 0x06, 0x07]), + ConditionalField(XStrLenField('proofOfOwnershipClient', b"", + length_from=lambda p: + p.lengthOfProofOfOwnershipClient), + lambda pkt: pkt.subFunction in [0x03, 0x06, 0x07]), + ConditionalField(FieldLenField('lengthOfChallengeClient', None, + fmt="H", length_of='challengeClient'), + lambda pkt: pkt.subFunction in [0x01, 0x02, 0x06, + 0x07]), + ConditionalField(XStrLenField('challengeClient', b"", + length_from=lambda p: + p.lengthOfChallengeClient), + lambda pkt: pkt.subFunction in [0x01, 0x02, 0x06, + 0x07]), + ConditionalField(FieldLenField('lengthOfEphemeralPublicKeyClient', + None, fmt="H", + length_of='ephemeralPublicKeyClient'), + lambda pkt: pkt.subFunction == 0x03), + ConditionalField(XStrLenField('ephemeralPublicKeyClient', b"", + length_from=lambda p: + p.lengthOfEphemeralPublicKeyClient), + lambda pkt: pkt.subFunction == 0x03), + ConditionalField(FieldLenField('lengthOfCertificateData', None, + fmt="H", length_of='certificateData'), + lambda pkt: pkt.subFunction == 0x04), + ConditionalField(XStrLenField('certificateData', b"", + length_from=lambda p: + p.lengthOfCertificateData), + lambda pkt: pkt.subFunction == 0x04), + ConditionalField(FieldLenField('lengthOfAdditionalParameter', None, + fmt="H", + length_of='additionalParameter'), + lambda pkt: pkt.subFunction in [0x06, 0x07]), + ConditionalField(XStrLenField('additionalParameter', b"", + length_from=lambda p: + p.lengthOfAdditionalParameter), + lambda pkt: pkt.subFunction in [0x06, 0x07]), + ] -bind_layers(UDS, UDS_CCPR, service=0x68) +bind_layers(UDS, UDS_AUTH, service=0x29) +UDS._service_cls[0x29] = UDS_AUTH + + +class UDS_AUTHPR(Packet): + authenticationReturnParameterTypes = { + 0x00: 'requestAccepted', + 0x01: 'generalReject', + # Authentication with PKI Certificate Exchange (ACPE) + 0x02: 'authenticationConfigurationAPCE', + # Authentication with Challenge-Response (ACR) + 0x03: 'authenticationConfigurationACRWithAsymmetricCryptography', + 0x04: 'authenticationConfigurationACRWithSymmetricCryptography', + 0x05: 'ISOSAEReserved', + 0x0F: 'ISOSAEReserved', + 0x10: 'deAuthenticationSuccessful', + 0x11: 'certificateVerifiedOwnershipVerificationNecessary', + 0x12: 'ownershipVerifiedAuthenticationComplete', + 0x13: 'certificateVerified', + 0x14: 'ISOSAEReserved', + 0x9F: 'ISOSAEReserved', + 0xFF: 'ISOSAEReserved' + } + name = 'AuthenticationPositiveResponse' + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x69, UDS.services), _uds_slm), + ByteEnumField('subFunction', 0, UDS_AUTH.subFunctions), + ByteEnumField('returnValue', 0, authenticationReturnParameterTypes), + ConditionalField( + XStrFixedLenField('algorithmIndicator', b'\x00' * 16, length=16), + lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), + ConditionalField(FieldLenField('lengthOfChallengeServer', None, + fmt="H", length_of='challengeServer'), + lambda pkt: pkt.subFunction in [0x01, 0x02, 0x05]), + ConditionalField(XStrLenField('challengeServer', b"", + length_from=lambda p: + p.lengthOfChallengeServer), + lambda pkt: pkt.subFunction in [0x01, 0x02, 0x05]), + ConditionalField(FieldLenField('lengthOfCertificateServer', None, + fmt="H", length_of='certificateServer'), + lambda pkt: pkt.subFunction == 0x02), + ConditionalField(XStrLenField('certificateServer', b"", + length_from=lambda p: + p.lengthOfCertificateServer), + lambda pkt: pkt.subFunction == 0x02), + ConditionalField(FieldLenField('lengthOfProofOfOwnershipServer', None, + fmt="H", + length_of='proofOfOwnershipServer'), + lambda pkt: pkt.subFunction in [0x02, 0x07]), + ConditionalField(XStrLenField('proofOfOwnershipServer', b"", + length_from=lambda p: + p.lengthOfProofOfOwnershipServer), + lambda pkt: pkt.subFunction in [0x02, 0x07]), + ConditionalField(FieldLenField('lengthOfSessionKeyInfo', None, fmt="H", + length_of='sessionKeyInfo'), + lambda pkt: pkt.subFunction in [0x03, 0x06, 0x07]), + ConditionalField(XStrLenField('sessionKeyInfo', b"", + length_from=lambda p: + p.lengthOfSessionKeyInfo), + lambda pkt: pkt.subFunction in [0x03, 0x06, 0x07]), + ConditionalField(FieldLenField('lengthOfEphemeralPublicKeyServer', + None, fmt="H", + length_of='ephemeralPublicKeyServer'), + lambda pkt: pkt.subFunction in [0x01, 0x02]), + ConditionalField(XStrLenField('ephemeralPublicKeyServer', b"", + length_from=lambda p: + p.lengthOfEphemeralPublicKeyServer), + lambda pkt: pkt.subFunction in [0x1, 0x02]), + ConditionalField(FieldLenField('lengthOfNeededAdditionalParameter', + None, fmt="H", + length_of='neededAdditionalParameter'), + lambda pkt: pkt.subFunction == 0x05), + ConditionalField(XStrLenField('neededAdditionalParameter', b"", + length_from=lambda p: + p.lengthOfNeededAdditionalParameter), + lambda pkt: pkt.subFunction == 0x05), + ] + + def answers(self, other): + return isinstance(other, UDS_AUTH) \ + and other.subFunction == self.subFunction + + +bind_layers(UDS, UDS_AUTHPR, service=0x69) +UDS._service_cls[0x69] = UDS_AUTHPR # #########################TP################################### class UDS_TP(Packet): name = 'TesterPresent' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3e, UDS.services), _uds_slm), ByteField('subFunction', 0) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.subFunction - -bind_layers(UDS, UDS_TP, service=0x3E) +bind_layers(UDS, UDS_TP, service=0x3e) +UDS._service_cls[0x3e] = UDS_TP class UDS_TPPR(Packet): name = 'TesterPresentPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7e, UDS.services), _uds_slm), ByteField('zeroSubFunction', 0) ] def answers(self, other): - return other.__class__ == UDS_TP - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.zeroSubFunction + return isinstance(other, UDS_TP) -bind_layers(UDS, UDS_TPPR, service=0x7E) +bind_layers(UDS, UDS_TPPR, service=0x7e) +UDS._service_cls[0x7e] = UDS_TPPR # #########################ATP################################### @@ -373,64 +542,88 @@ class UDS_ATP(Packet): } name = 'AccessTimingParameter' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x83, UDS.services), _uds_slm), ByteEnumField('timingParameterAccessType', 0, timingParameterAccessTypes), - ConditionalField(StrField('timingParameterRequestRecord', B""), + ConditionalField(StrField('timingParameterRequestRecord', b""), lambda pkt: pkt.timingParameterAccessType == 0x4) ] bind_layers(UDS, UDS_ATP, service=0x83) +UDS._service_cls[0x83] = UDS_ATP class UDS_ATPPR(Packet): name = 'AccessTimingParameterPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc3, UDS.services), _uds_slm), ByteEnumField('timingParameterAccessType', 0, UDS_ATP.timingParameterAccessTypes), - ConditionalField(StrField('timingParameterResponseRecord', B""), + ConditionalField(StrField('timingParameterResponseRecord', b""), lambda pkt: pkt.timingParameterAccessType == 0x3) ] def answers(self, other): - return other.__class__ == UDS_ATP \ + return isinstance(other, UDS_ATP) \ and other.timingParameterAccessType == \ self.timingParameterAccessType -bind_layers(UDS, UDS_ATPPR, service=0xC3) +bind_layers(UDS, UDS_ATPPR, service=0xc3) +UDS._service_cls[0xc3] = UDS_ATPPR # #########################SDT################################### +# TODO: Implement correct internal message service handling here, +# instead of using just the dataRecord class UDS_SDT(Packet): name = 'SecuredDataTransmission' fields_desc = [ - StrField('securityDataRequestRecord', B"") + ConditionalField(XByteEnumField('service', 0x84, UDS.services), _uds_slm), + BitField('requestMessage', 0, 1), + BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), + BitField('preEstablishedKeyUsed', 0, 1), + BitField('encryptedMessage', 0, 1), + BitField('signedMessage', 0, 1), + BitField('signedResponseRequested', 0, 1), + BitField('ISOSAEReserved', 0, 9), + ByteField('signatureEncryptionCalculation', 0), + XShortField('signatureLength', 0), + XShortField('antiReplayCounter', 0), + ByteField('internalMessageServiceRequestId', 0), + StrField('dataRecord', b"", fmt="B") ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.securityDataRequestRecord - bind_layers(UDS, UDS_SDT, service=0x84) +UDS._service_cls[0x84] = UDS_SDT class UDS_SDTPR(Packet): name = 'SecuredDataTransmissionPositiveResponse' fields_desc = [ - StrField('securityDataResponseRecord', B"") + ConditionalField(XByteEnumField('service', 0xc4, UDS.services), _uds_slm), + BitField('requestMessage', 0, 1), + BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), + BitField('preEstablishedKeyUsed', 0, 1), + BitField('encryptedMessage', 0, 1), + BitField('signedMessage', 0, 1), + BitField('signedResponseRequested', 0, 1), + BitField('ISOSAEReserved', 0, 9), + ByteField('signatureEncryptionCalculation', 0), + XShortField('signatureLength', 0), + XShortField('antiReplayCounter', 0), + ByteField('internalMessageServiceResponseId', 0), + StrField('dataRecord', b"", fmt="B") ] def answers(self, other): - return other.__class__ == UDS_SDT - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.securityDataResponseRecord + return isinstance(other, UDS_SDT) -bind_layers(UDS, UDS_SDTPR, service=0xC4) +bind_layers(UDS, UDS_SDTPR, service=0xc4) +UDS._service_cls[0xc4] = UDS_SDTPR # #########################CDTCS################################### @@ -442,35 +635,29 @@ class UDS_CDTCS(Packet): } name = 'ControlDTCSetting' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x85, UDS.services), _uds_slm), ByteEnumField('DTCSettingType', 0, DTCSettingTypes), - StrField('DTCSettingControlOptionRecord', B"") + StrField('DTCSettingControlOptionRecord', b"") ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_CDTCS.DTCSettingType%") - bind_layers(UDS, UDS_CDTCS, service=0x85) +UDS._service_cls[0x85] = UDS_CDTCS class UDS_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc5, UDS.services), _uds_slm), ByteEnumField('DTCSettingType', 0, UDS_CDTCS.DTCSettingTypes) ] def answers(self, other): - return other.__class__ == UDS_CDTCS - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_CDTCSPR.DTCSettingType%") + return isinstance(other, UDS_CDTCS) -bind_layers(UDS, UDS_CDTCSPR, service=0xC5) +bind_layers(UDS, UDS_CDTCSPR, service=0xc5) +UDS._service_cls[0xc5] = UDS_CDTCSPR # #########################ROE################################### @@ -482,30 +669,34 @@ class UDS_ROE(Packet): } name = 'ResponseOnEvent' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x86, UDS.services), _uds_slm), ByteEnumField('eventType', 0, eventTypes), ByteField('eventWindowTime', 0), - StrField('eventTypeRecord', B"") + StrField('eventTypeRecord', b"") ] bind_layers(UDS, UDS_ROE, service=0x86) +UDS._service_cls[0x86] = UDS_ROE class UDS_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc6, UDS.services), _uds_slm), ByteEnumField('eventType', 0, UDS_ROE.eventTypes), ByteField('numberOfIdentifiedEvents', 0), ByteField('eventWindowTime', 0), - StrField('eventTypeRecord', B"") + StrField('eventTypeRecord', b"") ] def answers(self, other): - return other.__class__ == UDS_ROE \ + return isinstance(other, UDS_ROE) \ and other.eventType == self.eventType -bind_layers(UDS, UDS_ROEPR, service=0xC6) +bind_layers(UDS, UDS_ROEPR, service=0xc6) +UDS._service_cls[0xc6] = UDS_ROEPR # #########################LC################################### @@ -518,6 +709,7 @@ class UDS_LC(Packet): } name = 'LinkControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x87, UDS.services), _uds_slm), ByteEnumField('linkControlType', 0, linkControlTypes), ConditionalField(ByteField('baudrateIdentifier', 0), lambda pkt: pkt.linkControlType == 0x1), @@ -529,32 +721,25 @@ class UDS_LC(Packet): lambda pkt: pkt.linkControlType == 0x2) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS.linkControlType%") - bind_layers(UDS, UDS_LC, service=0x87) +UDS._service_cls[0x87] = UDS_LC class UDS_LCPR(Packet): name = 'LinkControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc7, UDS.services), _uds_slm), ByteEnumField('linkControlType', 0, UDS_LC.linkControlTypes) ] def answers(self, other): - return other.__class__ == UDS_LC \ + return isinstance(other, UDS_LC) \ and other.linkControlType == self.linkControlType - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS.linkControlType%") - -bind_layers(UDS, UDS_LCPR, service=0xC7) +bind_layers(UDS, UDS_LCPR, service=0xc7) +UDS._service_cls[0xc7] = UDS_LCPR # #########################RDBI################################### @@ -562,44 +747,39 @@ class UDS_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' fields_desc = [ - FieldListField("identifiers", [0], + ConditionalField(XByteEnumField('service', 0x22, UDS.services), _uds_slm), + FieldListField("identifiers", None, XShortEnumField('dataIdentifier', 0, dataIdentifiers)) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_RDBI.identifiers%") - bind_layers(UDS, UDS_RDBI, service=0x22) +UDS._service_cls[0x22] = UDS_RDBI class UDS_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x62, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] def answers(self, other): - return other.__class__ == UDS_RDBI \ + return isinstance(other, UDS_RDBI) \ and self.dataIdentifier in other.identifiers - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_RDBIPR.dataIdentifier%") - bind_layers(UDS, UDS_RDBIPR, service=0x62) +UDS._service_cls[0x62] = UDS_RDBIPR # #########################RMBA################################### class UDS_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x23, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -620,31 +800,24 @@ class UDS_RMBA(Packet): lambda pkt: pkt.memorySizeLen == 4), ] - @staticmethod - def get_log(pkt): - addr = getattr(pkt, "memoryAddress%d" % pkt.memoryAddressLen) - size = getattr(pkt, "memorySize%d" % pkt.memorySizeLen) - return pkt.sprintf("%UDS.service%"), (addr, size) - bind_layers(UDS, UDS_RMBA, service=0x23) +UDS._service_cls[0x23] = UDS_RMBA class UDS_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ - StrField('dataRecord', None, fmt="B") + ConditionalField(XByteEnumField('service', 0x63, UDS.services), _uds_slm), + StrField('dataRecord', b"", fmt="B") ] def answers(self, other): - return other.__class__ == UDS_RMBA - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.dataRecord + return isinstance(other, UDS_RMBA) bind_layers(UDS, UDS_RMBAPR, service=0x63) +UDS._service_cls[0x63] = UDS_RMBAPR # #########################RSDBI################################### @@ -652,32 +825,37 @@ class UDS_RSDBI(Packet): name = 'ReadScalingDataByIdentifier' dataIdentifiers = ObservableDict() fields_desc = [ + ConditionalField(XByteEnumField('service', 0x24, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, dataIdentifiers) ] bind_layers(UDS, UDS_RSDBI, service=0x24) +UDS._service_cls[0x24] = UDS_RSDBI # TODO: Implement correct scaling here, instead of using just the dataRecord class UDS_RSDBIPR(Packet): name = 'ReadScalingDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x64, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RSDBI.dataIdentifiers), ByteField('scalingByte', 0), - StrField('dataRecord', None, fmt="B") + StrField('dataRecord', b"", fmt="B") ] def answers(self, other): - return other.__class__ == UDS_RSDBI \ + return isinstance(other, UDS_RSDBI) \ and other.dataIdentifier == self.dataIdentifier bind_layers(UDS, UDS_RSDBIPR, service=0x64) +UDS._service_cls[0x64] = UDS_RSDBIPR # #########################RDBPI################################### class UDS_RDBPI(Packet): + periodicDataIdentifiers = ObservableDict() transmissionModes = { 0: 'ISOSAEReserved', 1: 'sendAtSlowRate', @@ -687,29 +865,33 @@ class UDS_RDBPI(Packet): } name = 'ReadDataByPeriodicIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2a, UDS.services), _uds_slm), ByteEnumField('transmissionMode', 0, transmissionModes), - ByteField('periodicDataIdentifier', 0), - StrField('furtherPeriodicDataIdentifier', 0, fmt="B") + ByteEnumField('periodicDataIdentifier', 0, periodicDataIdentifiers), + StrField('furtherPeriodicDataIdentifier', b"", fmt="B") ] -bind_layers(UDS, UDS_RDBPI, service=0x2A) +bind_layers(UDS, UDS_RDBPI, service=0x2a) +UDS._service_cls[0x2a] = UDS_RDBPI # TODO: Implement correct scaling here, instead of using just the dataRecord class UDS_RDBPIPR(Packet): name = 'ReadDataByPeriodicIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6a, UDS.services), _uds_slm), ByteField('periodicDataIdentifier', 0), - StrField('dataRecord', None, fmt="B") + StrField('dataRecord', b"", fmt="B") ] def answers(self, other): - return other.__class__ == UDS_RDBPI \ + return isinstance(other, UDS_RDBPI) \ and other.periodicDataIdentifier == self.periodicDataIdentifier -bind_layers(UDS, UDS_RDBPIPR, service=0x6A) +bind_layers(UDS, UDS_RDBPIPR, service=0x6a) +UDS._service_cls[0x6a] = UDS_RDBPIPR # #########################DDDI################################### @@ -721,70 +903,69 @@ class UDS_DDDI(Packet): 0x2: "defineByMemoryAddress", 0x3: "clearDynamicallyDefinedDataIdentifier"} fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2c, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, subFunctions), - StrField('dataRecord', 0, fmt="B") + StrField('dataRecord', b"", fmt="B") ] -bind_layers(UDS, UDS_DDDI, service=0x2C) +bind_layers(UDS, UDS_DDDI, service=0x2c) +UDS._service_cls[0x2c] = UDS_DDDI class UDS_DDDIPR(Packet): name = 'DynamicallyDefineDataIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6c, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, UDS_DDDI.subFunctions), XShortField('dynamicallyDefinedDataIdentifier', 0) ] def answers(self, other): - return other.__class__ == UDS_DDDI \ + return isinstance(other, UDS_DDDI) \ and other.subFunction == self.subFunction -bind_layers(UDS, UDS_DDDIPR, service=0x6C) +bind_layers(UDS, UDS_DDDIPR, service=0x6c) +UDS._service_cls[0x6c] = UDS_DDDIPR # #########################WDBI################################### class UDS_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2e, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_WDBI.dataIdentifier%") - -bind_layers(UDS, UDS_WDBI, service=0x2E) +bind_layers(UDS, UDS_WDBI, service=0x2e) +UDS._service_cls[0x2e] = UDS_WDBI class UDS_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6e, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] def answers(self, other): - return other.__class__ == UDS_WDBI \ + return isinstance(other, UDS_WDBI) \ and other.dataIdentifier == self.dataIdentifier - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - pkt.sprintf("%UDS_WDBIPR.dataIdentifier%") - -bind_layers(UDS, UDS_WDBIPR, service=0x6E) +bind_layers(UDS, UDS_WDBIPR, service=0x6e) +UDS._service_cls[0x6e] = UDS_WDBIPR # #########################WMBA################################### class UDS_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3d, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -803,23 +984,19 @@ class UDS_WMBA(Packet): lambda pkt: pkt.memorySizeLen == 3), ConditionalField(XIntField('memorySize4', 0), lambda pkt: pkt.memorySizeLen == 4), - StrField('dataRecord', b'\x00', fmt="B"), + StrField('dataRecord', b'', fmt="B"), ] - @staticmethod - def get_log(pkt): - addr = getattr(pkt, "memoryAddress%d" % pkt.memoryAddressLen) - size = getattr(pkt, "memorySize%d" % pkt.memorySizeLen) - return pkt.sprintf("%UDS.service%"), (addr, size, pkt.dataRecord) - -bind_layers(UDS, UDS_WMBA, service=0x3D) +bind_layers(UDS, UDS_WMBA, service=0x3d) +UDS._service_cls[0x3d] = UDS_WMBA class UDS_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7d, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -841,51 +1018,66 @@ class UDS_WMBAPR(Packet): ] def answers(self, other): - return other.__class__ == UDS_WMBA \ + return isinstance(other, UDS_WMBA) \ and other.memorySizeLen == self.memorySizeLen \ and other.memoryAddressLen == self.memoryAddressLen - @staticmethod - def get_log(pkt): - addr = getattr(pkt, "memoryAddress%d" % pkt.memoryAddressLen) - size = getattr(pkt, "memorySize%d" % pkt.memorySizeLen) - return pkt.sprintf("%UDS.service%"), (addr, size) +bind_layers(UDS, UDS_WMBAPR, service=0x7d) +UDS._service_cls[0x7d] = UDS_WMBAPR + + +# ##########################DTC##################################### +class DTC(Packet): + name = 'Diagnostic Trouble Code' + dtc_descriptions = {} # type: Dict[int, str] -bind_layers(UDS, UDS_WMBAPR, service=0x7D) + fields_desc = [ + BitEnumField("system", 0, 2, { + 0: "Powertrain", + 1: "Chassis", + 2: "Body", + 3: "Network"}), + BitEnumField("type", 0, 2, { + 0: "Generic", + 1: "ManufacturerSpecific", + 2: "Generic", + 3: "Generic"}), + BitField("numeric_value_code", 0, 12), + ByteField("additional_information_code", 0), + ] + + def extract_padding(self, s): + return '', s # #########################CDTCI################################### class UDS_CDTCI(Packet): name = 'ClearDiagnosticInformation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x14, UDS.services), _uds_slm), ByteField('groupOfDTCHighByte', 0), ByteField('groupOfDTCMiddleByte', 0), ByteField('groupOfDTCLowByte', 0), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), (pkt.groupOfDTCHighByte, - pkt.groupOfDTCMiddleByte, - pkt.groupOfDTCLowByte) - bind_layers(UDS, UDS_CDTCI, service=0x14) +UDS._service_cls[0x14] = UDS_CDTCI class UDS_CDTCIPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x54, UDS.services), _uds_slm), + ] name = 'ClearDiagnosticInformationPositiveResponse' def answers(self, other): - return other.__class__ == UDS_CDTCI - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), None + return isinstance(other, UDS_CDTCI) bind_layers(UDS, UDS_CDTCIPR, service=0x54) +UDS._service_cls[0x54] = UDS_CDTCIPR # #########################RDTCI################################### @@ -914,49 +1106,126 @@ class UDS_RDTCI(Packet): 20: 'reportDTCFaultDetectionCounter', 21: 'reportDTCWithPermanentStatus' } + dtcStatus = { + 1: 'TestFailed', + 2: 'TestFailedThisOperationCycle', + 4: 'PendingDTC', + 8: 'ConfirmedDTC', + 16: 'TestNotCompletedSinceLastClear', + 32: 'TestFailedSinceLastClear', + 64: 'TestNotCompletedThisOperationCycle', + 128: 'WarningIndicatorRequested' + } + dtcStatusMask = { + 1: 'ActiveDTCs', + 4: 'PendingDTCs', + 8: 'ConfirmedOrStoredDTCs', + 255: 'AllRecordDTCs' + } + dtcSeverityMask = { + # 0: 'NoSeverityInformation', + 1: 'NoClassInformation', + 2: 'WWH-OBDClassA', + 4: 'WWH-OBDClassB1', + 8: 'WWH-OBDClassB2', + 16: 'WWH-OBDClassC', + 32: 'MaintenanceRequired', + 64: 'CheckAtNextHalt', + 128: 'CheckImmediately' + } name = 'ReadDTCInformation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x19, UDS.services), _uds_slm), ByteEnumField('reportType', 0, reportTypes), - ConditionalField(XByteField('DTCStatusMask', 0), - lambda pkt: pkt.reportType in [0x01, 0x02, 0x0f, - 0x11, 0x12, 0x13]), - ConditionalField(ByteField('DTCHighByte', 0), - lambda pkt: pkt.reportType in [0x3, 0x4, 0x6, - 0x10, 0x09]), - ConditionalField(ByteField('DTCMiddleByte', 0), - lambda pkt: pkt.reportType in [0x3, 0x4, 0x6, - 0x10, 0x09]), - ConditionalField(ByteField('DTCLowByte', 0), + ConditionalField(FlagsField('DTCSeverityMask', 0, 8, dtcSeverityMask), + lambda pkt: pkt.reportType in [0x07, 0x08]), + ConditionalField(FlagsField('DTCStatusMask', 0, 8, dtcStatusMask), + lambda pkt: pkt.reportType in [ + 0x01, 0x02, 0x07, 0x08, 0x0f, 0x11, 0x12, 0x13]), + ConditionalField(PacketField("dtc", None, pkt_cls=DTC), lambda pkt: pkt.reportType in [0x3, 0x4, 0x6, 0x10, 0x09]), ConditionalField(ByteField('DTCSnapshotRecordNumber', 0), lambda pkt: pkt.reportType in [0x3, 0x4, 0x5]), ConditionalField(ByteField('DTCExtendedDataRecordNumber', 0), - lambda pkt: pkt.reportType in [0x6, 0x10]), - ConditionalField(ByteField('DTCSeverityMask', 0), - lambda pkt: pkt.reportType in [0x07, 0x08]), - ConditionalField(ByteField('DTCStatusMask', 0), - lambda pkt: pkt.reportType in [0x07, 0x08]), + lambda pkt: pkt.reportType in [0x6, 0x10]) ] + +bind_layers(UDS, UDS_RDTCI, service=0x19) +UDS._service_cls[0x19] = UDS_RDTCI + + +class DTCAndStatusRecord(Packet): + name = 'DTC and status record' + fields_desc = [ + PacketField("dtc", None, pkt_cls=DTC), + FlagsField("status", 0, 8, UDS_RDTCI.dtcStatus) + ] + + def extract_padding(self, s): + return '', s + + +class DTCExtendedData(Packet): + name = 'Diagnostic Trouble Code Extended Data' + dataTypes = ObservableDict() + fields_desc = [ + ByteEnumField("data_type", 0, dataTypes), + XByteField("record", 0) + ] + + def extract_padding(self, s): + return '', s + + +class DTCExtendedDataRecord(Packet): + fields_desc = [ + PacketField("dtcAndStatus", None, pkt_cls=DTCAndStatusRecord), + PacketListField("extendedData", None, pkt_cls=DTCExtendedData) + ] + + +class DTCSnapshot(Packet): + identifiers = defaultdict(list) # type: Dict[int, list] # for later extension + @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), repr(pkt) + def next_identifier_cb(pkt, lst, cur, remain): + return Raw + + fields_desc = [ + ByteField("record_number", 0), + ByteField("record_number_of_identifiers", 0), + PacketListField( + "snapshotData", None, + next_cls_cb=lambda pkt, lst, cur, remain: DTCSnapshot.next_identifier_cb( + pkt, lst, cur, remain)) + ] + def extract_padding(self, s): + return '', s -bind_layers(UDS, UDS_RDTCI, service=0x19) + +class DTCSnapshotRecord(Packet): + fields_desc = [ + PacketField("dtcAndStatus", None, pkt_cls=DTCAndStatusRecord), + PacketListField("snapshots", None, pkt_cls=DTCSnapshot) + ] class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x59, UDS.services), _uds_slm), ByteEnumField('reportType', 0, UDS_RDTCI.reportTypes), - ConditionalField(XByteField('DTCStatusAvailabilityMask', 0), - lambda pkt: pkt.reportType in [0x01, 0x07, 0x11, - 0x12, 0x02, 0x0A, - 0x0B, 0x0C, 0x0D, - 0x0E, 0x0F, 0x13, - 0x15]), + ConditionalField( + FlagsField('DTCStatusAvailabilityMask', 0, 8, UDS_RDTCI.dtcStatus), + lambda pkt: pkt.reportType in [0x01, 0x07, 0x09, 0x11, 0x12, 0x02, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x13, 0x15]), + ConditionalField(ByteField('DTCSeverity', 0), + lambda pkt: pkt.reportType in [0x09]), + ConditionalField(ByteField('DTCFunctionalUnit', 0), + lambda pkt: pkt.reportType in [0x09]), ConditionalField(ByteEnumField('DTCFormatIdentifier', 0, {0: 'ISO15031-6DTCFormat', 1: 'UDS-1DTCFormat', @@ -967,26 +1236,37 @@ class UDS_RDTCIPR(Packet): ConditionalField(ShortField('DTCCount', 0), lambda pkt: pkt.reportType in [0x01, 0x07, 0x11, 0x12]), - ConditionalField(StrField('DTCAndStatusRecord', 0), - lambda pkt: pkt.reportType in [0x02, 0x0A, 0x0B, + ConditionalField(PacketListField('DTCAndStatusRecord', None, + pkt_cls=DTCAndStatusRecord), + lambda pkt: pkt.reportType in [0x02, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x13, 0x15]), - ConditionalField(StrField('dataRecord', 0), - lambda pkt: pkt.reportType in [0x03, 0x04, 0x05, - 0x06, 0x08, 0x09, - 0x10, 0x14]) + ConditionalField(StrField('dataRecord', b""), + lambda pkt: pkt.reportType in [0x03, 0x08, 0x10, 0x14]), + ConditionalField(PacketField('snapshotRecord', None, + pkt_cls=DTCSnapshotRecord), + lambda pkt: pkt.reportType in [0x04]), + ConditionalField(PacketField('extendedDataRecord', None, + pkt_cls=DTCExtendedDataRecord), + lambda pkt: pkt.reportType in [0x06]) ] def answers(self, other): - return other.__class__ == UDS_RDTCI \ - and other.reportType == self.reportType - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), repr(pkt) + if not isinstance(other, UDS_RDTCI): + return False + if not other.reportType == self.reportType: + return False + if self.reportType == 0x02: + return other.DTCStatusMask & self.DTCStatusAvailabilityMask + if self.reportType == 0x06: + return other.dtc == self.extendedDataRecord.dtcAndStatus.dtc + if self.reportType == 0x04: + return other.dtc == self.snapshotRecord.dtcAndStatus.dtc + return True bind_layers(UDS, UDS_RDTCIPR, service=0x59) +UDS._service_cls[0x59] = UDS_RDTCIPR # #########################RC################################### @@ -1000,45 +1280,38 @@ class UDS_RC(Packet): routineControlIdentifiers = ObservableDict() name = 'RoutineControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x31, UDS.services), _uds_slm), ByteEnumField('routineControlType', 0, routineControlTypes), - XShortEnumField('routineIdentifier', 0, routineControlIdentifiers), - StrField('routineControlOptionRecord', 0, fmt="B"), + XShortEnumField('routineIdentifier', 0, routineControlIdentifiers) ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"),\ - (pkt.routineControlType, - pkt.routineIdentifier, - pkt.routineControlOptionRecord) - bind_layers(UDS, UDS_RC, service=0x31) +UDS._service_cls[0x31] = UDS_RC class UDS_RCPR(Packet): name = 'RoutineControlPositiveResponse' fields_desc = [ - ByteEnumField('routineControlType', 0, - UDS_RC.routineControlTypes), + ConditionalField(XByteEnumField('service', 0x71, UDS.services), _uds_slm), + ByteEnumField('routineControlType', 0, UDS_RC.routineControlTypes), XShortEnumField('routineIdentifier', 0, UDS_RC.routineControlIdentifiers), - StrField('routineStatusRecord', 0, fmt="B"), ] def answers(self, other): - return other.__class__ == UDS_RC \ - and other.routineControlType == self.routineControlType - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"),\ - (pkt.routineControlType, - pkt.routineIdentifier, - pkt.routineStatusRecord) + if isinstance(other, UDS_RC) \ + and other.routineControlType == self.routineControlType \ + and other.routineIdentifier == self.routineIdentifier: + if isinstance(self.payload, NoPayload): + return True + else: + return self.payload.answers(other.payload) + return False bind_layers(UDS, UDS_RCPR, service=0x71) +UDS._service_cls[0x71] = UDS_RCPR # #########################RD################################### @@ -1048,6 +1321,7 @@ class UDS_RD(Packet): }) name = 'RequestDownload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x34, UDS.services), _uds_slm), ByteEnumField('dataFormatIdentifier', 0, dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), @@ -1069,39 +1343,33 @@ class UDS_RD(Packet): lambda pkt: pkt.memorySizeLen == 4) ] - @staticmethod - def get_log(pkt): - addr = getattr(pkt, "memoryAddress%d" % pkt.memoryAddressLen) - size = getattr(pkt, "memorySize%d" % pkt.memorySizeLen) - return pkt.sprintf("%UDS.service%"), (addr, size) - bind_layers(UDS, UDS_RD, service=0x34) +UDS._service_cls[0x34] = UDS_RD class UDS_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x74, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), - StrField('maxNumberOfBlockLength', 0, fmt="B"), + StrField('maxNumberOfBlockLength', b"", fmt="B"), ] def answers(self, other): - return other.__class__ == UDS_RD - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.memorySizeLen + return isinstance(other, UDS_RD) bind_layers(UDS, UDS_RDPR, service=0x74) +UDS._service_cls[0x74] = UDS_RDPR # #########################RU################################### class UDS_RU(Packet): name = 'RequestUpload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x35, UDS.services), _uds_slm), ByteEnumField('dataFormatIdentifier', 0, UDS_RD.dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), @@ -1124,140 +1392,205 @@ class UDS_RU(Packet): lambda pkt: pkt.memorySizeLen == 4) ] - @staticmethod - def get_log(pkt): - addr = getattr(pkt, "memoryAddress%d" % pkt.memoryAddressLen) - size = getattr(pkt, "memorySize%d" % pkt.memorySizeLen) - return pkt.sprintf("%UDS.service%"), (addr, size) - bind_layers(UDS, UDS_RU, service=0x35) +UDS._service_cls[0x35] = UDS_RU class UDS_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x75, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), - StrField('maxNumberOfBlockLength', 0, fmt="B"), + StrField('maxNumberOfBlockLength', b"", fmt="B"), ] def answers(self, other): - return other.__class__ == UDS_RU - - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.memorySizeLen + return isinstance(other, UDS_RU) bind_layers(UDS, UDS_RUPR, service=0x75) +UDS._service_cls[0x75] = UDS_RUPR # #########################TD################################### class UDS_TD(Packet): name = 'TransferData' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x36, UDS.services), _uds_slm), ByteField('blockSequenceCounter', 0), - StrField('transferRequestParameterRecord', 0, fmt="B") + StrField('transferRequestParameterRecord', b"", fmt="B") ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"),\ - (pkt.blockSequenceCounter, pkt.transferRequestParameterRecord) - bind_layers(UDS, UDS_TD, service=0x36) +UDS._service_cls[0x36] = UDS_TD class UDS_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x76, UDS.services), _uds_slm), ByteField('blockSequenceCounter', 0), - StrField('transferResponseParameterRecord', 0, fmt="B") + StrField('transferResponseParameterRecord', b"", fmt="B") ] def answers(self, other): - return other.__class__ == UDS_TD \ + return isinstance(other, UDS_TD) \ and other.blockSequenceCounter == self.blockSequenceCounter - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.blockSequenceCounter - bind_layers(UDS, UDS_TDPR, service=0x76) +UDS._service_cls[0x76] = UDS_TDPR # #########################RTE################################### class UDS_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ - StrField('transferRequestParameterRecord', 0, fmt="B") + ConditionalField(XByteEnumField('service', 0x37, UDS.services), _uds_slm), + StrField('transferRequestParameterRecord', b"", fmt="B") ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"),\ - pkt.transferRequestParameterRecord - bind_layers(UDS, UDS_RTE, service=0x37) +UDS._service_cls[0x37] = UDS_RTE class UDS_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ - StrField('transferResponseParameterRecord', 0, fmt="B") + ConditionalField(XByteEnumField('service', 0x77, UDS.services), _uds_slm), + StrField('transferResponseParameterRecord', b"", fmt="B") ] def answers(self, other): - return other.__class__ == UDS_RTE + return isinstance(other, UDS_RTE) + + +bind_layers(UDS, UDS_RTEPR, service=0x77) +UDS._service_cls[0x77] = UDS_RTEPR + + +# #########################RFT################################### +class UDS_RFT(Packet): + name = 'RequestFileTransfer' + + modeOfOperations = { + 0x00: "ISO/SAE Reserved", + 0x01: "Add File", + 0x02: "Delete File", + 0x03: "Replace File", + 0x04: "Read File", + 0x05: "Read Directory" + } @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"),\ - pkt.transferResponseParameterRecord + def _contains_file_size(packet): + return packet.modeOfOperation not in [2, 4, 5] + + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x38, UDS.services), _uds_slm), + XByteEnumField('modeOfOperation', 0, modeOfOperations), + FieldLenField('filePathAndNameLength', None, + length_of='filePathAndName', fmt='H'), + StrLenField('filePathAndName', b"", + length_from=lambda p: p.filePathAndNameLength), + ConditionalField(BitField('compressionMethod', 0, 4), + lambda p: p.modeOfOperation not in [2, 5]), + ConditionalField(BitField('encryptingMethod', 0, 4), + lambda p: p.modeOfOperation not in [2, 5]), + ConditionalField(FieldLenField('fileSizeParameterLength', None, + fmt="B", + length_of='fileSizeUnCompressed'), + lambda p: UDS_RFT._contains_file_size(p)), + ConditionalField(StrLenField('fileSizeUnCompressed', b"", + length_from=lambda p: + p.fileSizeParameterLength), + lambda p: UDS_RFT._contains_file_size(p)), + ConditionalField(StrLenField('fileSizeCompressed', b"", + length_from=lambda p: + p.fileSizeParameterLength), + lambda p: UDS_RFT._contains_file_size(p)) + ] -bind_layers(UDS, UDS_RTEPR, service=0x77) +bind_layers(UDS, UDS_RFT, service=0x38) +UDS._service_cls[0x38] = UDS_RFT + + +class UDS_RFTPR(Packet): + name = 'RequestFileTransferPositiveResponse' + + @staticmethod + def _contains_data_format_identifier(packet): + return packet.modeOfOperation != 0x02 + + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x78, UDS.services), _uds_slm), + XByteEnumField('modeOfOperation', 0, UDS_RFT.modeOfOperations), + ConditionalField(FieldLenField('lengthFormatIdentifier', None, + length_of='maxNumberOfBlockLength', + fmt='B'), + lambda p: p.modeOfOperation != 2), + ConditionalField(StrLenField('maxNumberOfBlockLength', b"", + length_from=lambda p: p.lengthFormatIdentifier), + lambda p: p.modeOfOperation != 2), + ConditionalField(BitField('compressionMethod', 0, 4), + lambda p: p.modeOfOperation != 0x02), + ConditionalField(BitField('encryptingMethod', 0, 4), + lambda p: p.modeOfOperation != 0x02), + ConditionalField(FieldLenField('fileSizeOrDirInfoParameterLength', + None, + length_of='fileSizeUncompressedOrDirInfoLength'), + lambda p: p.modeOfOperation not in [1, 2, 3]), + ConditionalField(StrLenField('fileSizeUncompressedOrDirInfoLength', + b"", + length_from=lambda p: + p.fileSizeOrDirInfoParameterLength), + lambda p: p.modeOfOperation not in [1, 2, 3]), + ConditionalField(StrLenField('fileSizeCompressed', b"", + length_from=lambda p: + p.fileSizeOrDirInfoParameterLength), + lambda p: p.modeOfOperation not in [1, 2, 3, 5]), + ] + + def answers(self, other): + return isinstance(other, UDS_RFT) + + +bind_layers(UDS, UDS_RFTPR, service=0x78) +UDS._service_cls[0x78] = UDS_RFTPR # #########################IOCBI################################### class UDS_IOCBI(Packet): name = 'InputOutputControlByIdentifier' - dataIdentifiers = ObservableDict() fields_desc = [ - XShortEnumField('dataIdentifier', 0, dataIdentifiers), - ByteField('controlOptionRecord', 0), - StrField('controlEnableMaskRecord', 0, fmt="B") + ConditionalField(XByteEnumField('service', 0x2f, UDS.services), _uds_slm), + XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.dataIdentifier - -bind_layers(UDS, UDS_IOCBI, service=0x2F) +bind_layers(UDS, UDS_IOCBI, service=0x2f) +UDS._service_cls[0x2f] = UDS_IOCBI class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' fields_desc = [ - XShortField('dataIdentifier', 0), - StrField('controlStatusRecord', 0, fmt="B") + ConditionalField(XByteEnumField('service', 0x6f, UDS.services), _uds_slm), + XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] def answers(self, other): - return other.__class__ == UDS_IOCBI \ + return isinstance(other, UDS_IOCBI) \ and other.dataIdentifier == self.dataIdentifier - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), pkt.dataIdentifier - -bind_layers(UDS, UDS_IOCBIPR, service=0x6F) +bind_layers(UDS, UDS_IOCBIPR, service=0x6f) +UDS._service_cls[0x6f] = UDS_IOCBIPR # #########################NR################################### @@ -1278,9 +1611,25 @@ class UDS_NR(Packet): 0x26: 'failurePreventsExecutionOfRequestedAction', 0x31: 'requestOutOfRange', 0x33: 'securityAccessDenied', + 0x34: 'authenticationRequired', 0x35: 'invalidKey', 0x36: 'exceedNumberOfAttempts', 0x37: 'requiredTimeDelayNotExpired', + 0x3A: 'secureDataVerificationFailed', + 0x50: 'certificateVerificationFailedInvalidTimePeriod', + 0x51: 'certificateVerificationFailedInvalidSignature', + 0x52: 'certificateVerificationFailedInvalidChainOfTrust', + 0x53: 'certificateVerificationFailedInvalidType', + 0x54: 'certificateVerificationFailedInvalidFormat', + 0x55: 'certificateVerificationFailedInvalidContent', + 0x56: 'certificateVerificationFailedInvalidScope', + 0x57: 'certificateVerificationFailedInvalidCertificateRevoked', + 0x58: 'ownershipVerificationFailed', + 0x59: 'challengeCalculationFailed', + 0x5a: 'settingAccessRightsFailed', + 0x5b: 'sessionKeyCreationOrDerivationFailed', + 0x5c: 'configurationDataUsageFailed', + 0x5d: 'deAuthenticationFailed', 0x70: 'uploadDownloadNotAccepted', 0x71: 'transferDataSuspended', 0x72: 'generalProgrammingFailure', @@ -1311,6 +1660,7 @@ class UDS_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7f, UDS.services), _uds_slm), XByteEnumField('requestServiceId', 0, UDS.services), ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) ] @@ -1320,14 +1670,9 @@ def answers(self, other): (self.negativeResponseCode != 0x78 or conf.contribs['UDS']['treat-response-pending-as-answer']) - @staticmethod - def get_log(pkt): - return pkt.sprintf("%UDS.service%"), \ - (pkt.sprintf("%UDS_NR.requestServiceId%"), - pkt.sprintf("%UDS_NR.negativeResponseCode%")) - bind_layers(UDS, UDS_NR, service=0x7f) +UDS._service_cls[0x7f] = UDS_NR # ################################################################## @@ -1336,7 +1681,7 @@ def get_log(pkt): class UDS_TesterPresentSender(PeriodicSenderThread): - def __init__(self, sock, pkt=UDS() / UDS_TP(), interval=2): + def __init__(self, sock, pkt=UDS() / UDS_TP(subFunction=0x80), interval=2): """ Thread to send TesterPresent messages packets periodically Args: @@ -1345,68 +1690,3 @@ def __init__(self, sock, pkt=UDS() / UDS_TP(), interval=2): interval: interval between two packets """ PeriodicSenderThread.__init__(self, sock, pkt, interval) - - -def UDS_SessionEnumerator(sock, session_range=range(0x100), reset_wait=1.5): - """ Enumerates session ID's in given range - and returns list of UDS()/UDS_DSC() packets - with valid session types - - Args: - sock: socket where packets are sent - session_range: range for session ID's - reset_wait: wait time in sec after every packet - """ - pkts = (req for tup in - product(UDS() / UDS_DSC(diagnosticSessionType=session_range), - UDS() / UDS_ER(resetType='hardReset')) for req in tup) - results, _ = sock.sr(pkts, timeout=len(session_range) * reset_wait * 2 + 1, - verbose=False, inter=reset_wait) - return [req for req, res in results if req is not None and - req.service != 0x11 and - (res.service == 0x50 or - res.negativeResponseCode not in [0x10, 0x11, 0x12])] - - -def UDS_ServiceEnumerator(sock, session="DefaultSession", - filter_responses=True): - """ Enumerates every service ID - and returns list of tuples. Each tuple contains - the session and the respective positive response - - Args: - sock: socket where packet is sent periodically - session: session in which the services are enumerated - """ - pkts = (UDS(service=x) for x in set(x & ~0x40 for x in range(0x100))) - found_services = sock.sr(pkts, timeout=5, verbose=False) - return [(session, p) for _, p in found_services[0] if - p.service != 0x7f or - (p.negativeResponseCode not in [0x10, 0x11] or not - filter_responses)] - - -def getTableEntry(tup): - """ Helping function for make_lined_table. - Returns the session and response code of tup. - - Args: - tup: tuple with session and UDS response package - - Example: - make_lined_table([('DefaultSession', UDS()/UDS_SAPR(), - 'ExtendedDiagnosticSession', UDS()/UDS_IOCBI())], - getTableEntry) - """ - session, pkt = tup - if pkt.service == 0x7f: - return (session, - "0x%02x: %s" % (pkt.requestServiceId, - pkt.sprintf("%UDS_NR.requestServiceId%")), - pkt.sprintf("%UDS_NR.negativeResponseCode%")) - else: - return (session, - "0x%02x: %s" % (pkt.service & ~0x40, - pkt.get_field('service'). - i2s[pkt.service & ~0x40]), - "PositiveResponse") diff --git a/scapy/contrib/automotive/uds_ecu_states.py b/scapy/contrib/automotive/uds_ecu_states.py new file mode 100644 index 00000000000..47b81f91211 --- /dev/null +++ b/scapy/contrib/automotive/uds_ecu_states.py @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = UDS EcuState modifications +# scapy.contrib.status = library +from scapy.contrib.automotive.uds import UDS_DSCPR, UDS_ERPR, UDS_SAPR, \ + UDS_RDBPIPR, UDS_CCPR, UDS_TPPR, UDS_RDPR, UDS +from scapy.packet import Packet +from scapy.contrib.automotive.ecu import EcuState + + +__all__ = ["UDS_DSCPR_modify_ecu_state", "UDS_CCPR_modify_ecu_state", + "UDS_ERPR_modify_ecu_state", "UDS_RDBPIPR_modify_ecu_state", + "UDS_TPPR_modify_ecu_state", "UDS_SAPR_modify_ecu_state", + "UDS_RDPR_modify_ecu_state"] + + +@EcuState.extend_pkt_with_modifier(UDS_DSCPR) +def UDS_DSCPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + state.session = self.diagnosticSessionType # type: ignore + try: + del state.security_level # type: ignore + except AttributeError: + pass + + +@EcuState.extend_pkt_with_modifier(UDS_ERPR) +def UDS_ERPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + state.reset() + state.session = 1 # type: ignore + + +@EcuState.extend_pkt_with_modifier(UDS_SAPR) +def UDS_SAPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + if self.securityAccessType % 2 == 0 and \ + self.securityAccessType > 0 and len(req) >= 3: + state.security_level = self.securityAccessType # type: ignore + elif self.securityAccessType % 2 == 1 and \ + self.securityAccessType > 0 and \ + len(req) >= 3 and not any(self.securitySeed): + state.security_level = self.securityAccessType + 1 # type: ignore + + +@EcuState.extend_pkt_with_modifier(UDS_CCPR) +def UDS_CCPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + state.communication_control = self.controlType # type: ignore + + +@EcuState.extend_pkt_with_modifier(UDS_TPPR) +def UDS_TPPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + state.tp = 1 # type: ignore + + +@EcuState.extend_pkt_with_modifier(UDS_RDBPIPR) +def UDS_RDBPIPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + state.pdid = self.periodicDataIdentifier # type: ignore + + +@EcuState.extend_pkt_with_modifier(UDS_RDPR) +def UDS_RDPR_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + oldstr = getattr(state, "req_download", "") + newstr = str(req.fields) + state.req_download = oldstr if newstr in oldstr else oldstr + newstr # type: ignore # noqa: E501 + + +@EcuState.extend_pkt_with_modifier(UDS) +def UDS_modify_ecu_state(self, req, state): + # type: (Packet, Packet, EcuState) -> None + if self.service == 0x77: # UDS RequestTransferExitPositiveResponse + try: + state.download_complete = state.req_download # type: ignore + except (KeyError, AttributeError): + pass + state.req_download = "" # type: ignore diff --git a/scapy/contrib/automotive/uds_logging.py b/scapy/contrib/automotive/uds_logging.py new file mode 100644 index 00000000000..bb791c9ba32 --- /dev/null +++ b/scapy/contrib/automotive/uds_logging.py @@ -0,0 +1,327 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = UDS Ecu logging additions +# scapy.contrib.status = library + +from scapy.contrib.automotive.uds import UDS_DSCPR, UDS_ERPR, UDS_SAPR, \ + UDS_CCPR, UDS_TPPR, UDS_DSC, UDS_ER, UDS_RDPR, UDS_TDPR, UDS_RD, UDS_TD, \ + UDS_CC, UDS_NR, UDS_SA, UDS_RDBIPR, UDS_LC, UDS_RC, UDS_TP, UDS_RU, \ + UDS_IOCBIPR, UDS_WDBIPR, UDS_CDTCIPR, UDS_CDTCI, UDS_RDTCIPR, \ + UDS_RDTCI, UDS_RMBAPR, UDS_WMBAPR, UDS_WMBA, UDS_LCPR, UDS_RCPR, UDS_RFT, \ + UDS_RTE, UDS_RTEPR, UDS_RFTPR, UDS_IOCBI, UDS_RDBI, UDS_RMBA, UDS_WDBI, \ + UDS_CDTCS, UDS_CDTCSPR, UDS_SDT, UDS_SDTPR, UDS_RUPR +from scapy.packet import Packet +from scapy.contrib.automotive.ecu import Ecu + +from typing import ( + Any, + Tuple, +) + + +@Ecu.extend_pkt_with_logging(UDS_DSC) +def UDS_DSC_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_DSC.diagnosticSessionType%") + + +@Ecu.extend_pkt_with_logging(UDS_DSCPR) +def UDS_DSCPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_DSCPR.diagnosticSessionType%") + + +@Ecu.extend_pkt_with_logging(UDS_ER) +def UDS_ER_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_ER.resetType%") + + +@Ecu.extend_pkt_with_logging(UDS_ERPR) +def UDS_ERPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_ER.resetType%") + + +@Ecu.extend_pkt_with_logging(UDS_SA) +def UDS_SA_get_log(self): + # type: (Packet) -> Tuple[str, Any] + if self.securityAccessType % 2 == 1: + return self.sprintf("%UDS.service%"),\ + (self.securityAccessType, None) + else: + return self.sprintf("%UDS.service%"),\ + (self.securityAccessType, self.securityKey) + + +@Ecu.extend_pkt_with_logging(UDS_SAPR) +def UDS_SAPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + if self.securityAccessType % 2 == 0: + return self.sprintf("%UDS.service%"),\ + (self.securityAccessType, None) + else: + return self.sprintf("%UDS.service%"),\ + (self.securityAccessType, self.securitySeed) + + +@Ecu.extend_pkt_with_logging(UDS_CC) +def UDS_CC_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_CC.controlType%") + + +@Ecu.extend_pkt_with_logging(UDS_CCPR) +def UDS_CCPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_CCPR.controlType%") + + +@Ecu.extend_pkt_with_logging(UDS_TP) +def UDS_TP_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.subFunction + + +@Ecu.extend_pkt_with_logging(UDS_TPPR) +def UDS_TPPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.zeroSubFunction + + +@Ecu.extend_pkt_with_logging(UDS_SDT) +def UDS_SDT_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.securityDataRequestRecord + + +@Ecu.extend_pkt_with_logging(UDS_SDTPR) +def UDS_SDTPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.securityDataResponseRecord + + +@Ecu.extend_pkt_with_logging(UDS_CDTCS) +def UDS_CDTCS_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_CDTCS.DTCSettingType%") + + +@Ecu.extend_pkt_with_logging(UDS_CDTCSPR) +def UDS_CDTCSPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_CDTCSPR.DTCSettingType%") + + +@Ecu.extend_pkt_with_logging(UDS_LC) +def UDS_LC_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS.linkControlType%") + + +@Ecu.extend_pkt_with_logging(UDS_LCPR) +def UDS_LCPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS.linkControlType%") + + +@Ecu.extend_pkt_with_logging(UDS_RDBI) +def UDS_RDBI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_RDBI.identifiers%") + + +@Ecu.extend_pkt_with_logging(UDS_RDBIPR) +def UDS_RDBIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_RDBIPR.dataIdentifier%") + + +@Ecu.extend_pkt_with_logging(UDS_RMBA) +def UDS_RMBA_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + (getattr(self, "memoryAddress%d" % self.memoryAddressLen), + getattr(self, "memorySize%d" % self.memorySizeLen)) + + +@Ecu.extend_pkt_with_logging(UDS_RMBAPR) +def UDS_RMBAPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.dataRecord + + +@Ecu.extend_pkt_with_logging(UDS_WDBI) +def UDS_WDBI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_WDBI.dataIdentifier%") + + +@Ecu.extend_pkt_with_logging(UDS_WDBIPR) +def UDS_WDBIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + self.sprintf("%UDS_WDBIPR.dataIdentifier%") + + +@Ecu.extend_pkt_with_logging(UDS_WMBA) +def UDS_WMBA_get_log(self): + # type: (Packet) -> Tuple[str, Any] + addr = getattr(self, "memoryAddress%d" % self.memoryAddressLen) + size = getattr(self, "memorySize%d" % self.memorySizeLen) + return self.sprintf("%UDS.service%"), (addr, size, self.dataRecord) + + +@Ecu.extend_pkt_with_logging(UDS_WMBAPR) +def UDS_WMBAPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + addr = getattr(self, "memoryAddress%d" % self.memoryAddressLen) + size = getattr(self, "memorySize%d" % self.memorySizeLen) + return self.sprintf("%UDS.service%"), (addr, size) + + +@Ecu.extend_pkt_with_logging(UDS_CDTCI) +def UDS_CDTCI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + (self.groupOfDTCHighByte, self.groupOfDTCMiddleByte, + self.groupOfDTCLowByte) + + +@Ecu.extend_pkt_with_logging(UDS_CDTCIPR) +def UDS_CDTCIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), None + + +@Ecu.extend_pkt_with_logging(UDS_RDTCI) +def UDS_RDTCI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), repr(self) + + +@Ecu.extend_pkt_with_logging(UDS_RDTCIPR) +def UDS_RDTCIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), repr(self) + + +@Ecu.extend_pkt_with_logging(UDS_RC) +def UDS_RC_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"),\ + (self.routineControlType, + self.routineIdentifier) + + +@Ecu.extend_pkt_with_logging(UDS_RCPR) +def UDS_RCPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"),\ + (self.routineControlType, + self.routineIdentifier) + + +@Ecu.extend_pkt_with_logging(UDS_RD) +def UDS_RD_get_log(self): + # type: (Packet) -> Tuple[str, Any] + addr = getattr(self, "memoryAddress%d" % self.memoryAddressLen) + size = getattr(self, "memorySize%d" % self.memorySizeLen) + return self.sprintf("%UDS.service%"), (addr, size) + + +@Ecu.extend_pkt_with_logging(UDS_RDPR) +def UDS_RDPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.memorySizeLen + + +@Ecu.extend_pkt_with_logging(UDS_RU) +def UDS_RU_get_log(self): + # type: (Packet) -> Tuple[str, Any] + addr = getattr(self, "memoryAddress%d" % self.memoryAddressLen) + size = getattr(self, "memorySize%d" % self.memorySizeLen) + return self.sprintf("%UDS.service%"), (addr, size) + + +@Ecu.extend_pkt_with_logging(UDS_RUPR) +def UDS_RUPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.memorySizeLen + + +@Ecu.extend_pkt_with_logging(UDS_TD) +def UDS_TD_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"),\ + (self.blockSequenceCounter, self.transferRequestParameterRecord) + + +@Ecu.extend_pkt_with_logging(UDS_TDPR) +def UDS_TDPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.blockSequenceCounter + + +@Ecu.extend_pkt_with_logging(UDS_RTE) +def UDS_RTE_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"),\ + self.transferRequestParameterRecord + + +@Ecu.extend_pkt_with_logging(UDS_RTEPR) +def UDS_RTEPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"),\ + self.transferResponseParameterRecord + + +@Ecu.extend_pkt_with_logging(UDS_RFT) +def UDS_RFT_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"),\ + self.modeOfOperation + + +@Ecu.extend_pkt_with_logging(UDS_RFTPR) +def UDS_RFTPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"),\ + self.modeOfOperation + + +@Ecu.extend_pkt_with_logging(UDS_IOCBI) +def UDS_IOCBI_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.dataIdentifier + + +@Ecu.extend_pkt_with_logging(UDS_IOCBIPR) +def UDS_IOCBIPR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), self.dataIdentifier + + +@Ecu.extend_pkt_with_logging(UDS_NR) +def UDS_NR_get_log(self): + # type: (Packet) -> Tuple[str, Any] + return self.sprintf("%UDS.service%"), \ + (self.sprintf("%UDS_NR.requestServiceId%"), + self.sprintf("%UDS_NR.negativeResponseCode%")) diff --git a/scapy/contrib/automotive/uds_scan.py b/scapy/contrib/automotive/uds_scan.py new file mode 100644 index 00000000000..6271b051868 --- /dev/null +++ b/scapy/contrib/automotive/uds_scan.py @@ -0,0 +1,1340 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss +import copy +import inspect +import itertools +import logging +import random +import struct +import time +from abc import ABC +from collections import defaultdict +# typing imports +from typing import ( + Dict, + Optional, + NamedTuple, + List, + Type, + Any, + Iterable, + cast, + Union, + Set, + Sequence, +) + +from scapy.compat import orb +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.ecu import EcuState +from scapy.contrib.automotive.scanner.configuration import \ + AutomotiveTestCaseExecutorConfiguration # noqa: E501 +from scapy.contrib.automotive.scanner.enumerator import ServiceEnumerator, \ + _AutomotiveTestCaseScanResult, _AutomotiveTestCaseFilteredScanResult, \ + StateGeneratingServiceEnumerator +from scapy.contrib.automotive.scanner.executor import AutomotiveTestCaseExecutor # noqa: E501 +from scapy.contrib.automotive.scanner.graph import _Edge +from scapy.contrib.automotive.scanner.staged_test_case import StagedAutomotiveTestCase # noqa: E501 +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCaseABC, \ + _SocketUnion, _TransitionTuple, StateGenerator +from scapy.contrib.automotive.uds import UDS, UDS_NR, UDS_DSC, UDS_TP, \ + UDS_RDBI, UDS_WDBI, UDS_SA, UDS_RC, UDS_IOCBI, UDS_RMBA, UDS_ER, \ + UDS_TesterPresentSender, UDS_CC, UDS_RDBPI, UDS_RD, UDS_TD, UDS_DSCPR +# TODO: Refactor this import +from scapy.contrib.automotive.uds_ecu_states import * # noqa: F401, F403 +from scapy.error import Scapy_Exception +from scapy.packet import Raw, Packet + +# scapy.contrib.description = UDS AutomotiveTestCaseExecutor +# scapy.contrib.status = loads + +# Definition outside the class UDS_RMBASequentialEnumerator +# to allow pickling +_PointOfInterest = NamedTuple("_PointOfInterest", [ + ("memory_address", int), + ("direction", bool), + # True = increasing / upward, False = decreasing / downward # noqa: E501 + ("memorySizeLen", int), + ("memoryAddressLen", int), + ("memorySize", int)]) + + +class UDS_Enumerator(ServiceEnumerator, ABC): + @staticmethod + def _get_negative_response_code(resp): + # type: (Packet) -> int + return resp.negativeResponseCode + + @staticmethod + def _get_negative_response_desc(nrc): + # type: (int) -> str + return UDS_NR(negativeResponseCode=nrc).sprintf( + "%UDS_NR.negativeResponseCode%") + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], "PR: Supported") + + @staticmethod + def _get_negative_response_label(response): + # type: (Packet) -> str + return response.sprintf("NR: %UDS_NR.negativeResponseCode%") + + +class UDS_DSCEnumerator(UDS_Enumerator, StateGeneratingServiceEnumerator): + _description = "Available sessions" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'delay_state_change': (int, lambda x: x >= 0), + 'overwrite_timeout': (bool, None), + 'close_socket_when_entering_session_2': (bool, None), + 'support_suppress_positive_response': (bool, None) + }) + _supported_kwargs["scan_range"] = ( + (list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param int delay_state_change: Specifies an additional delay after + after a session is modified from + the transition function. In unit-test + scenarios, this delay should be set to + zero. + :param bool overwrite_timeout: True by default. This enumerator + overwrites the timeout argument, since + most ECUs take some time until a session + is changed. This ensures that more + results are gathered by default. In + unit-test scenarios, this value should + be set to False, in order to use the + timeout specified by the 'timeout' + argument. + :param bool close_socket_when_entering_session_2: False by default. + This enumerator will close the socket + if session 2 (ProgrammingSession) + was entered, if True. This will + force a reconnect by the executor. + :param bool support_suppress_positive_response: False by default. + If True, this enumerator will treat + no response for a DSC request with a + session type > 0x80 as a positive + response and will therefore create a + new state with a session value - 0x80.""" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + session_range = kwargs.pop("scan_range", range(2, 0x100)) + return UDS() / UDS_DSC(diagnosticSessionType=session_range) + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + + # fix configuration in kwargs to avoid overwrite from user + kwargs["exit_if_service_not_supported"] = False + kwargs["retry_if_busy_returncode"] = False + + # Apply a fixed timeout for this execute. + # Unit-tests may want to overwrite the timeout to speed up testing + if kwargs.pop("overwrite_timeout", True): + kwargs["timeout"] = 3 + + super(UDS_DSCEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % ( + tup[1].diagnosticSessionType, + tup[1].sprintf("%UDS_DSC.diagnosticSessionType%")) + + @staticmethod + def enter_state(socket, # type: _SocketUnion + configuration, # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + request # type: Packet + ): # type: (...) -> bool + try: + timeout = configuration[UDS_DSCEnumerator.__name__]["timeout"] + except KeyError: + timeout = 3 + ans = socket.sr1(request, timeout=timeout, verbose=False) + if ans is not None: + if configuration.verbose: + log_automotive.debug( + "Try to enter session req: %s, resp: %s" % + (repr(request), repr(ans))) + return cast(int, ans.service) != 0x7f + else: + return False + + def get_new_edge(self, + socket, # type: _SocketUnion + config # type: AutomotiveTestCaseExecutorConfiguration + ): # type: (...) -> Optional[_Edge] + edge = super(UDS_DSCEnumerator, self).get_new_edge(socket, config) + + try: + close_socket = config[UDS_DSCEnumerator.__name__]["close_socket_when_entering_session_2"] # noqa: E501 + except KeyError: + close_socket = False + + try: + support_out_of_spec = config[UDS_DSCEnumerator.__name__]["support_suppress_positive_response"] # noqa: E501 + except KeyError: + support_out_of_spec = False + + if edge is None: + try: + state, req, resp, _, _ = cast(ServiceEnumerator, self).results[-1] # noqa: E501 + except IndexError: + return None + + if support_out_of_spec and resp is None and req.diagnosticSessionType > 0x80: # noqa: E501 + resp = UDS() / UDS_DSCPR(diagnosticSessionType=0x80 - req.diagnosticSessionType) # noqa: E501 + new_state = EcuState.get_modified_ecu_state(resp, req, state) + if new_state == state: + return None + else: + edge = (state, new_state) + self._edge_requests[edge] = req + return edge + else: + return None + + if edge: + state, new_state = edge + # Force TesterPresent if session is changed + new_state.tp = 1 # type: ignore + try: + if close_socket and new_state.session == 2: # type: ignore + new_state.tp = 0 # type: ignore + except (AttributeError, KeyError): + pass + return state, new_state + return None + + @staticmethod + def enter_state_with_tp(sock, # type: _SocketUnion + conf, # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + kwargs # type: Dict[str, Any] + ): # type: (...) -> bool + UDS_TPEnumerator.enter(sock, conf, kwargs) + # Wait 5 seconds, since some ECUs require time + # to switch to the bootloader + try: + delay = conf[UDS_DSCEnumerator.__name__]["delay_state_change"] + except KeyError: + delay = 5 + + try: + close_socket = conf[UDS_DSCEnumerator.__name__]["close_socket_when_entering_session_2"] # noqa: E501 + except KeyError: + close_socket = False + + conf.stop_event.wait(delay) + state_changed = UDS_DSCEnumerator.enter_state( + sock, conf, kwargs["req"]) + + try: + session = kwargs["req"].diagnosticSessionType + except AttributeError: + session = 0 + + if close_socket and session == 2: + if not hasattr(sock, "ip"): + log_automotive.warning("Likely closing a CAN based socket! " + "This might be a configuration issue.") + log_automotive.info( + "Entered Programming Session: Closing socket connection") + sock.close() + conf.stop_event.wait(delay) + + if not state_changed: + UDS_TPEnumerator.cleanup(sock, conf) + return state_changed + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + return UDS_DSCEnumerator.enter_state_with_tp, { + "req": self._results[-1].req, + "desc": "DSC=%d" % self._results[-1].req.diagnosticSessionType + }, UDS_TPEnumerator.cleanup + + +class UDS_TPEnumerator(UDS_Enumerator, StateGeneratingServiceEnumerator): + _description = "TesterPresent supported" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return [UDS() / UDS_TP()] + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "TesterPresent:" + + @staticmethod + def enter(socket, # type: _SocketUnion + configuration, # type: AutomotiveTestCaseExecutorConfiguration + _ # type: Dict[str, Any] + ): # type: (...) -> bool + if configuration.unittest: + configuration["tps"] = None + socket.sr1(UDS() / UDS_TP(), timeout=0.1, verbose=False) + return True + + UDS_TPEnumerator.cleanup(socket, configuration) + configuration["tps"] = UDS_TesterPresentSender(socket, interval=3) + configuration["tps"].start() + return True + + @staticmethod + def cleanup(_, configuration): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration) -> bool + try: + configuration["tps"].stop() + configuration["tps"] = None + except (AttributeError, KeyError): + pass + # log_automotive.debug("Cleanup TP-Sender Error: %s", e) + return True + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + return self.enter, {"desc": "TP"}, self.cleanup + + +class UDS_EREnumerator(UDS_Enumerator): + _description = "ECUReset supported" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + reset_type = kwargs.pop("scan_range", range(0x100)) + return cast(Iterable[Packet], UDS() / UDS_ER(resetType=reset_type)) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % ( + tup[1].resetType, tup[1].sprintf("%UDS_ER.resetType%")) + + +class UDS_CCEnumerator(UDS_Enumerator): + _description = "CommunicationControl supported" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + control_type = kwargs.pop("scan_range", range(0x100)) + return cast(Iterable[Packet], UDS() / UDS_CC( + controlType=control_type, communicationType0=1, + communicationType2=15)) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % ( + tup[1].controlType, tup[1].sprintf("%UDS_CC.controlType%")) + + +class UDS_RDBPIEnumerator(UDS_Enumerator): + _description = "ReadDataByPeriodicIdentifier supported" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs["scan_range"] = ( + (list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + pdid = kwargs.pop("scan_range", range(0x100)) + return cast(Iterable[Packet], UDS() / UDS_RDBPI( + transmissionMode=1, periodicDataIdentifier=pdid)) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + resp = tup[2] + if resp is not None and resp.service != 0x7f: + return "0x%02x %s: %s" % ( + tup[1].periodicDataIdentifier, + tup[1].sprintf("%UDS_RDBPI.periodicDataIdentifier%"), + resp.dataRecord) + else: + return "0x%02x %s: No response" % ( + tup[1].periodicDataIdentifier, + tup[1].sprintf("%UDS_RDBPI.periodicDataIdentifier%")) + + +class UDS_ServiceEnumerator(UDS_Enumerator): + _description = "Available services and negative response per state" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + "request_length": (int, lambda x: 1 <= x < 5) + }) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param int request_length: Specifies the maximum length of arequest + packet. The enumerator will generate all + packets from a length of 1 (UDS Service + ID only) up to the specified + `request_length`.""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(UDS_ServiceEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + # Only generate services with unset positive response bit (0x40) as + # default scan_range + scan_range = kwargs.pop("scan_range", + (x for x in range(0x100) if not x & 0x40)) + request_length = kwargs.pop("request_length", 1) + return itertools.chain.from_iterable( + ([UDS(service=x) / Raw(b"\x00" * req_len) + for req_len in range(request_length)] for x in scan_range)) + + def _evaluate_response(self, + state, # type: EcuState + request, # type: Packet + response, # type: Optional[Packet] + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool + if response and response.service == 0x51: + log_automotive.warning( + "ECUResetPositiveResponse detected! This might have changed " + "the state of the ECU under test.") + + # remove args from kwargs since they will be overwritten + kwargs["exit_if_service_not_supported"] = False # type: ignore + + return super(UDS_ServiceEnumerator, self)._evaluate_response( + state, request, response, **kwargs) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x-%d: %s" % ( + tup[1].service, len(tup[1]), tup[1].sprintf("%UDS.service%")) + + +class UDS_RDBIEnumerator(UDS_Enumerator): + _description = "Readable data identifier per state" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x10000 and min(x) >= 0) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x10000)) + return (UDS() / UDS_RDBI(identifiers=[x]) for x in scan_range) + + @staticmethod + def print_information(resp): + # type: (Packet) -> str + load = bytes(resp)[3:] if len(resp) > 3 else "No data available" + return "PR: %s" % load + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x: %s" % (tup[1].identifiers[0], + tup[1].sprintf("%UDS_RDBI.identifiers%")[1:-1]) + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], self.print_information) + + +class UDS_RDBISelectiveEnumerator(StagedAutomotiveTestCase): + @staticmethod + def __connector_rnd_to_seq(rdbi_random, # type: AutomotiveTestCaseABC + _ # type: AutomotiveTestCaseABC + ): # type: (...) -> Dict[str, Any] + rdbi_random = cast(UDS_Enumerator, rdbi_random) + identifiers_with_positive_response = \ + [p.resp.dataIdentifier + for p in rdbi_random.results_with_positive_response] + + scan_range = UDS_RDBISelectiveEnumerator. \ + points_to_blocks(identifiers_with_positive_response) + return {"scan_range": scan_range} + + @staticmethod + def points_to_blocks(pois): + # type: (Sequence[int]) -> Iterable[int] + + if len(pois) == 0: + # quick path for better performance + return [] + + block_size = UDS_RDBIRandomEnumerator.block_size + generators = [] + for start in range(0, 2 ** 16, block_size): + end = start + block_size + pr_in_block = any((start <= identifier < end + for identifier in pois)) + if pr_in_block: + generators.append(range(start, end)) + scan_range = list(itertools.chain.from_iterable(generators)) + return scan_range + + def __init__(self): + # type: () -> None + super(UDS_RDBISelectiveEnumerator, self).__init__( + [UDS_RDBIRandomEnumerator(), UDS_RDBIEnumerator()], + [None, self.__connector_rnd_to_seq]) + + +class UDS_RDBIRandomEnumerator(UDS_RDBIEnumerator): + _supported_kwargs = copy.copy(UDS_RDBIEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'probe_start': (int, lambda x: 0 <= x <= 0xffff), + 'probe_end': (int, lambda x: 0 <= x <= 0xffff) + }) + block_size = 2 ** 6 + + _supported_kwargs_doc = UDS_RDBIEnumerator._supported_kwargs_doc + """ + :param int probe_start: Specifies the start identifier for probing. + :param int probe_end: Specifies the end identifier for probing.""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(UDS_RDBIRandomEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + + samples_per_block = { + 4: 29, 5: 22, 6: 19, 8: 11, 9: 11, 10: 13, 11: 14, 12: 31, 13: 4, + 14: 26, 16: 30, 17: 4, 18: 20, 19: 5, 20: 49, 21: 54, 22: 9, 23: 4, + 24: 10, 25: 8, 28: 6, 29: 3, 32: 11, 36: 4, 37: 3, 40: 9, 41: 9, + 42: 3, 44: 2, 47: 3, 48: 4, 49: 3, 52: 8, 64: 35, 66: 2, 68: 24, + 69: 19, 70: 30, 71: 28, 72: 16, 73: 4, 74: 6, 75: 27, 76: 41, + 77: 11, 78: 6, 81: 2, 88: 3, 90: 2, 92: 16, 97: 15, 98: 20, 100: 6, + 101: 5, 102: 5, 103: 10, 106: 10, 108: 4, 124: 3, 128: 7, 136: 15, + 137: 14, 138: 27, 139: 10, 148: 9, 150: 2, 152: 2, 168: 23, + 169: 15, 170: 16, 171: 16, 172: 2, 176: 3, 177: 4, 178: 2, 187: 2, + 232: 3, 235: 2, 240: 8, 252: 25, 256: 7, 257: 2, 287: 6, 290: 2, + 316: 2, 319: 3, 323: 3, 324: 19, 326: 2, 327: 2, 330: 4, 331: 10, + 332: 3, 334: 8, 338: 3, 832: 6, 833: 2, 900: 4, 956: 4, 958: 3, + 964: 12, 965: 13, 966: 34, 967: 3, 972: 10, 1000: 3, 1012: 23, + 1013: 14, 1014: 15 + } + to_scan = [] + block_size = UDS_RDBIRandomEnumerator.block_size + + probe_start = kwargs.pop("probe_start", 0) + probe_end = kwargs.pop("probe_end", 0x10000) + probe_range = range(probe_start, probe_end, block_size) + + for block_index, start in enumerate(probe_range): + end = start + block_size + count_samples = samples_per_block.get(block_index, 1) + to_scan += random.sample(range(start, end), count_samples) + + # Use locality effect + # If an identifier brought a positive response in any state, + # it is likely that in another state it is available as well + positive_identifiers = [t.resp.dataIdentifier for t in + self.results_with_positive_response] + to_scan += positive_identifiers + + # make all identifiers unique with set() + # Sort for better logs + to_scan = sorted(list(set(to_scan))) + return (UDS() / UDS_RDBI(identifiers=[x]) for x in to_scan) + + +class UDS_WDBIEnumerator(UDS_Enumerator): + _description = "Writeable data identifier per state" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'rdbi_enumerator': (UDS_RDBIEnumerator, None) + }) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param rdbi_enumerator: Specifies an instance of an UDS_RDBIEnumerator + which is used to extract possible data + identifiers. + :type rdbi_enumerator: UDS_RDBIEnumerator""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(UDS_WDBIEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x10000)) + rdbi_enumerator = kwargs.pop("rdbi_enumerator", None) + + if rdbi_enumerator is None: + log_automotive.debug("Use entire scan range") + return (UDS() / UDS_WDBI(dataIdentifier=x) for x in scan_range) + elif isinstance(rdbi_enumerator, UDS_RDBIEnumerator): + log_automotive.debug("Selective scan based on RDBI results") + return (UDS() / UDS_WDBI(dataIdentifier=t.resp.dataIdentifier) / + Raw(load=bytes(t.resp)[3:]) + for t in rdbi_enumerator.results_with_positive_response + if len(bytes(t.resp)) >= 3) + else: + raise Scapy_Exception("rdbi_enumerator has to be an instance " + "of UDS_RDBIEnumerator") + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x: %s" % (tup[1].dataIdentifier, + tup[1].sprintf("%UDS_WDBI.dataIdentifier%")) + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], "PR: Writeable") + + +class UDS_WDBISelectiveEnumerator(StagedAutomotiveTestCase): + @staticmethod + def __connector_rdbi_to_wdbi(rdbi, # type: AutomotiveTestCaseABC + _ # type: AutomotiveTestCaseABC + ): # type: (...) -> Dict[str, Any] + return {"rdbi_enumerator": rdbi} + + def __init__(self): + # type: () -> None + super(UDS_WDBISelectiveEnumerator, self).__init__( + [UDS_RDBIEnumerator(), UDS_WDBIEnumerator()], + [None, self.__connector_rdbi_to_wdbi]) + + +class UDS_SAEnumerator(UDS_Enumerator): + _description = "Available security seeds with access type and state" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(1, 256, 2)) + return (UDS() / UDS_SA(securityAccessType=x) for x in scan_range) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return tup[1].securityAccessType + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], lambda r: "PR: %s" % r.securitySeed) + + def pre_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + if cast(ServiceEnumerator, self)._retry_pkt[state]: + # this is a retry execute. Wait much longer than usual because + # a required time delay not expired could have been received + # on the previous attempt + if not global_configuration.unittest: + global_configuration.stop_event.wait(11) + + def _evaluate_retry(self, + state, # type: EcuState + request, # type: Packet + response, # type: Packet + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool + + if super(UDS_SAEnumerator, self)._evaluate_retry( + state, request, response, **kwargs): + return True + + if response.service == 0x7f and \ + self._get_negative_response_code(response) in [0x24, 0x37]: + log_automotive.debug( + "Retry %s because requiredTimeDelayNotExpired or " + "requestSequenceError received", + repr(request)) + return super(UDS_SAEnumerator, self)._populate_retry( + state, request) + return False + + def _evaluate_response(self, + state, # type: EcuState + request, # type: Packet + response, # type: Optional[Packet] + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool + if super(UDS_SAEnumerator, self)._evaluate_response( + state, request, response, **kwargs): + return True + + if response is not None and \ + response.service == 0x67 and \ + response.securityAccessType % 2 == 1: + log_automotive.debug("Seed received. Leave scan to try a key") + return True + return False + + @staticmethod + def get_seed_pkt(sock, level=1, record=b""): + # type: (_SocketUnion, int, bytes) -> Optional[Packet] + req = UDS() / UDS_SA(securityAccessType=level, + securityAccessDataRecord=record) + for _ in range(10): + seed = sock.sr1(req, timeout=5, verbose=False) + if seed is None: + return None + elif seed.service == 0x7f and \ + UDS_Enumerator._get_negative_response_code(seed) != 0x37: + log_automotive.info( + "Security access no seed! NR: %s", repr(seed)) + return None + + elif seed.service == 0x7f and seed.negativeResponseCode == 0x37: + log_automotive.info("Security access retry to get seed") + time.sleep(10) + continue + else: + return seed + return None + + @staticmethod + def evaluate_security_access_response(res, seed, key): + # type: (Optional[Packet], Packet, Optional[Packet]) -> bool + if res is None or res.service == 0x7f: + log_automotive.info(repr(seed)) + log_automotive.info(repr(key)) + log_automotive.info(repr(res)) + log_automotive.info("Security access error!") + return False + else: + log_automotive.info("Security access granted!") + return True + + +class UDS_SA_XOR_Enumerator(UDS_SAEnumerator, StateGenerator): + _description = "XOR SecurityAccess supported" + _transition_function_args = dict() # type: Dict[_Edge, Dict[str, Any]] + + @staticmethod + def get_key_pkt(seed, level=1): + # type: (Packet, int) -> Optional[Packet] + + def key_function_int(s): + # type: (int) -> int + return 0xffffffff & ~s + + def key_function_short(s): + # type: (int) -> int + return 0xffff & ~s + + try: + s = seed.securitySeed + except AttributeError: + return None + + fmt = None + key_function = None # Optional[Callable[[int], int]] + + if len(s) == 2: + fmt = "H" + key_function = key_function_short + + if len(s) == 4: + fmt = "I" + key_function = key_function_int + + if key_function is not None and fmt is not None: + key = struct.pack(fmt, key_function(struct.unpack(fmt, s)[0])) + return cast(Packet, UDS() / UDS_SA(securityAccessType=level + 1, + securityKey=key)) + else: + return None + + def get_security_access(self, sock, level=1, seed_pkt=None): + # type: (_SocketUnion, int, Optional[Packet]) -> bool + log_automotive.info( + "Try bootloader security access for level %d" % level) + if seed_pkt is None: + seed_pkt = self.get_seed_pkt(sock, level) + if not seed_pkt: + return False + + if not any(seed_pkt.securitySeed): + log_automotive.info( + "Security access for level %d already granted!" % level) + return True + + key_pkt = self.get_key_pkt(seed_pkt, level) + if key_pkt is None: + return False + + try: + res = sock.sr1(key_pkt, timeout=5, verbose=False) + if sock.closed: + log_automotive.critical("Socket closed during scan.") + raise Scapy_Exception("Socket closed during scan") + except (OSError, ValueError, Scapy_Exception) as e: + try: + last_seed_req = self._results[-1].req + last_state = self._results[-1].state + if not self._populate_retry(last_state, last_seed_req): + log_automotive.exception( + "Exception during retry. This is bad") + except IndexError: + log_automotive.warning("Couldn't populate retry.") + raise e + + return self.evaluate_security_access_response( + res, seed_pkt, key_pkt) + + def transition_function(self, sock, _, kwargs): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration, Dict[str, Any]) -> bool # noqa: E501 + spec = inspect.getfullargspec(self.get_security_access) + + func_kwargs = {k: kwargs[k] for k in spec.args if k in kwargs.keys()} + return self.get_security_access(sock, **func_kwargs) + + def get_new_edge(self, socket, config): + # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration) -> Optional[_Edge] # noqa: E501 + last_resp = self._results[-1].resp + last_state = self._results[-1].state + + if last_resp is None or last_resp.service == 0x7f: + return None + + try: + if last_resp.service != 0x67 or \ + last_resp.securityAccessType % 2 != 1: + return None + + seed = last_resp + sec_lvl = seed.securityAccessType + + if self.get_security_access(socket, sec_lvl, seed): + log_automotive.debug("Security Access found.") + # create edge + new_state = copy.copy(last_state) + new_state.security_level = seed.securityAccessType + 1 # type: ignore # noqa: E501 + if last_state == new_state: + return None + edge = (last_state, new_state) + self._transition_function_args[edge] = \ + {"level": sec_lvl, "desc": "SA=%d" % sec_lvl} + return edge + except AttributeError: + pass + + return None + + def get_transition_function(self, socket, edge): + # type: (_SocketUnion, _Edge) -> Optional[_TransitionTuple] + return self.transition_function, \ + self._transition_function_args[edge], None + + +class UDS_RCEnumerator(UDS_Enumerator): + _description = "Available RoutineControls and negative response per state" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'type_list': (list, lambda x: max(x) < 0x100 and min(x) >= 0) + }) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x10000 and min(x) >= 0) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param list type_list: A list of RoutineControlTypes which should + be enumerated. Possible values = [1, 2, 3]. + """ + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + type_list = kwargs.pop("type_list", [1, 2, 3]) + scan_range = kwargs.pop("scan_range", range(0x10000)) + + return ( + UDS() / UDS_RC(routineControlType=rc_type, + routineIdentifier=data_id) + for rc_type, data_id in itertools.product(type_list, scan_range) + ) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x-%d: %s" % ( + tup[1].routineIdentifier, tup[1].routineControlType, + tup[1].sprintf("%UDS_RC.routineIdentifier%")) + + +class UDS_RCStartEnumerator(UDS_RCEnumerator): + _description = "Available RoutineControls and negative response per state" + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + if "type_list" in kwargs: + raise KeyError("'type_list' already set in kwargs.") + kwargs["type_list"] = [1] + return super(UDS_RCStartEnumerator, self). \ + _get_initial_requests(**kwargs) + + +class UDS_RCSelectiveEnumerator(StagedAutomotiveTestCase): + # Used to expand points to both sites + # So, the total block size will be 253 * 2 = 506 + expansion_width = 253 + + @staticmethod + def points_to_ranges(pois): + # type: (Iterable[int]) -> Iterable[int] + expansion_width = UDS_RCSelectiveEnumerator.expansion_width + generators = [] + for identifier in pois: + start = max(identifier - expansion_width, 0) + end = min(identifier + expansion_width + 1, 0x10000) + generators.append(range(start, end)) + ranges_with_overlaps = itertools.chain.from_iterable(generators) + return sorted(set(ranges_with_overlaps)) + + @staticmethod + def __connector_start_to_rest(rc_start, _rc_stop): + # type: (AutomotiveTestCaseABC, AutomotiveTestCaseABC) -> Dict[str, Any] # noqa: E501 + rc_start = cast(UDS_Enumerator, rc_start) + identifiers_with_pr = [resp.routineIdentifier for _, _, resp, _, _ + in rc_start.results_with_positive_response] + scan_range = UDS_RCSelectiveEnumerator.points_to_ranges( + identifiers_with_pr) + + return {"type_list": [2, 3], + "scan_range": scan_range} + + def __init__(self): + # type: () -> None + super(UDS_RCSelectiveEnumerator, self).__init__( + [UDS_RCStartEnumerator(), UDS_RCEnumerator()], + [None, self.__connector_start_to_rest]) + + +class UDS_IOCBIEnumerator(UDS_Enumerator): + _description = "Available Input Output Controls By Identifier " \ + "and negative response per state" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x10000 and min(x) >= 0) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + scan_range = kwargs.pop("scan_range", range(0x10000)) + return (UDS() / UDS_IOCBI(dataIdentifier=x) for x in scan_range) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + resp = tup[2] + if resp is not None: + return "0x%04x: %s" % \ + (tup[1].dataIdentifier, + repr(resp.payload)) + else: + return "0x%04x: No response" % tup[1].dataIdentifier + + +class UDS_RMBAEnumeratorABC(UDS_Enumerator): + _description = "Readable Memory Addresses " \ + "and negative response per state" + + @staticmethod + def get_addr(pkt): + # type: (UDS_RMBA) -> int + """ + Helper function to get the memoryAddress from a UDS_RMBA packet + :param pkt: UDS_RMBA request + :return: memory address of the request + """ + return getattr(pkt, "memoryAddress%d" % pkt.memoryAddressLen) + + @staticmethod + def set_addr(pkt, addr): + # type: (UDS_RMBA, int) -> None + """ + Helper function to set the memoryAddress of a UDS_RMBA packet + :param pkt: UDS_RMBA request + :param addr: memory address to be set + """ + setattr(pkt, "memoryAddress%d" % pkt.memoryAddressLen, addr) + + @staticmethod + def get_size(pkt): + # type: (UDS_RMBA) -> int + """ + Helper function to gets the memorySize of a UDS_RMBA packet + :param pkt: UDS_RMBA request + """ + return getattr(pkt, "memorySize%d" % pkt.memorySizeLen) + + @staticmethod + def set_size(pkt, size): + # type: (UDS_RMBA, int) -> None + """ + Helper function to set the memorySize of a UDS_RMBA packet + :param pkt: UDS_RMBA request + :param size: memory size to be set + """ + set_size = min(2 ** (pkt.memorySizeLen * 8) - 1, size) + setattr(pkt, "memorySize%d" % pkt.memorySizeLen, set_size) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x" % self.get_addr(tup[1]) + + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], lambda r: "PR: %s" % r.dataRecord) + + +class UDS_RMBARandomEnumerator(UDS_RMBAEnumeratorABC): + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'unittest': (bool, None) + }) + del _supported_kwargs["scan_range"] + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param bool unittest: Enables smaller search space for unit-test + scenarios. This saves execution time.""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(UDS_RMBARandomEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + @staticmethod + def _random_memory_addr_pkt(addr_len=None, size_len=None, size=None): + # type: (Optional[int], Optional[int], Optional[int]) -> Packet + pkt = UDS() / UDS_RMBA() # type: Packet + pkt.memorySizeLen = size_len or random.randint(1, 4) + pkt.memoryAddressLen = addr_len or random.randint(1, 4) + UDS_RMBARandomEnumerator.set_size(pkt, size or 4) + UDS_RMBARandomEnumerator.set_addr( + pkt, random.randint( + 0, (2 ** (8 * pkt.memoryAddressLen) - 1)) & 0xfffffff0) + return pkt + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + if kwargs.get("unittest", False): + return itertools.chain( + (self._random_memory_addr_pkt(addr_len=2, size_len=2) for _ in range(100)), # noqa: E501 + (self._random_memory_addr_pkt(addr_len=3) for _ in range(2)), + (self._random_memory_addr_pkt(addr_len=4) for _ in range(2))) + + return itertools.chain( + (self._random_memory_addr_pkt(addr_len=1) for _ in range(100)), + (self._random_memory_addr_pkt(addr_len=2) for _ in range(500)), + (self._random_memory_addr_pkt(addr_len=3) for _ in range(1000)), + (self._random_memory_addr_pkt(addr_len=4) for _ in range(5000))) + + +class UDS_RMBASequentialEnumerator(UDS_RMBAEnumeratorABC): + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'points_of_interest': (list, None) + }) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param list points_of_interest: A list of _PointOfInterest objects as + starting points for sequential search. + """ + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(UDS_RMBASequentialEnumerator, self).execute( + socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + def __init__(self): + # type: () -> None + super(UDS_RMBASequentialEnumerator, self).__init__() + self.__points_of_interest = defaultdict( + list) # type: Dict[EcuState, List[_PointOfInterest]] # noqa: E501 + self.__initial_points_of_interest = None # type: Optional[List[_PointOfInterest]] # noqa: E501 + + def _get_memory_addresses_from_results(self, results): + # type: (Union[List[_AutomotiveTestCaseScanResult], List[_AutomotiveTestCaseFilteredScanResult]]) -> Set[int] # noqa: E501 + mem_areas = list() + for tup in results: + resp = tup.resp + if resp is not None and resp.service == 0x23: + mem_areas += [ + range(self.get_addr(tup.req), + self.get_addr(tup.req) + len(resp.dataRecord))] + else: + mem_areas += [ + range(self.get_addr(tup.req), self.get_addr(tup.req) + 16)] + + return set(list(itertools.chain.from_iterable(mem_areas))) + + def __pois_to_requests(self, pois): + # type: (List[_PointOfInterest]) -> List[Packet] + tested_addrs = self._get_memory_addresses_from_results( + self.results_with_response) + testing_addrs = set() + new_requests = list() + + for addr, upward, mem_size_len, mem_addr_len, mem_size in pois: + for i in range(0, mem_size * 50, mem_size): + if upward: + addr = min(addr + i, 2 ** (8 * mem_addr_len) - 1) + else: + addr = max(addr - i, 0) + + if addr not in tested_addrs and \ + (addr, mem_size) not in testing_addrs: + pkt = UDS() / UDS_RMBA(memorySizeLen=mem_size_len, + memoryAddressLen=mem_addr_len) + self.set_size(pkt, mem_size) + self.set_addr(pkt, addr) + new_requests.append(pkt) + testing_addrs.add((addr, mem_size)) + + return new_requests + + def __request_to_pois(self, req, resp): + # type: (Packet, Optional[Packet]) -> List[_PointOfInterest] + + addr = self.get_addr(req) + size = self.get_size(req) + msl = req.memorySizeLen + mal = req.memoryAddressLen + + if (resp is None or resp.service == 0x7f) and size > 1: + size = size // 2 + + return [ + _PointOfInterest(addr, True, msl, mal, size), + _PointOfInterest(addr, False, msl, mal, size)] + + if resp is not None and resp.service == 0x23: + return [ + _PointOfInterest(addr + size, True, msl, mal, size), + _PointOfInterest(addr - size, False, msl, mal, size)] + + return [] + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + raise NotImplementedError + + def pre_execute(self, socket, state, global_configuration): + # type: (_SocketUnion, EcuState, AutomotiveTestCaseExecutorConfiguration) -> None # noqa: E501 + + if self.__initial_points_of_interest is None: + self.__initial_points_of_interest = \ + global_configuration[self.__class__.__name__].get( + "points_of_interest", list()) + + if not self.__points_of_interest[state]: + # Transfer initial pois to current state pois + self.__points_of_interest[state] = \ + self.__initial_points_of_interest + + new_requests = self.__pois_to_requests( + self.__points_of_interest[state]) + + if len(new_requests): + self._state_completed[state] = False + self._request_iterators[state] = new_requests + self.__points_of_interest[state] = list() + else: + self._request_iterators[state] = list() + + def _evaluate_response(self, + state, # type: EcuState + request, # type: Packet + response, # type: Optional[Packet] + **kwargs # type: Optional[Dict[str, Any]] + ): # type: (...) -> bool # noqa: E501 + self.__points_of_interest[state] += \ + self.__request_to_pois(request, response) + return super(UDS_RMBASequentialEnumerator, self)._evaluate_response( + state, request, response, **kwargs) + + def show(self, dump=False, filtered=True, verbose=False): + # type: (bool, bool, bool) -> Optional[str] + s = super(UDS_RMBASequentialEnumerator, self).show( + dump, filtered, verbose) or "" + + try: + from intelhex import IntelHex + + ih = IntelHex() + for tup in self.results_with_positive_response: + for i, b in enumerate(tup.resp.dataRecord): + addr = self.get_addr(tup.req) + ih[addr + i] = orb(b) + + ih.tofile("RMBA_dump.hex", format="hex") + except ImportError: + err_msg = "Install 'intelhex' to create a hex file of the memory" + log_automotive.exception(err_msg) + with open("RMBA_dump.hex", "w") as file: + file.write(err_msg) + + if dump: + return s + "\n" + else: + print(s) + return None + + +class UDS_RMBAEnumerator(StagedAutomotiveTestCase): + @staticmethod + def __connector_rand_to_seq(rand, _): + # type: (AutomotiveTestCaseABC, AutomotiveTestCaseABC) -> Dict[str, Any] # noqa: E501 + points_of_interest = list() # type: List[_PointOfInterest] + rand = cast(UDS_RMBARandomEnumerator, rand) + for tup in rand.results_with_positive_response: + points_of_interest += \ + [_PointOfInterest(UDS_RMBAEnumeratorABC.get_addr(tup.req), + True, tup.req.memorySizeLen, + tup.req.memoryAddressLen, 0x80), + _PointOfInterest(UDS_RMBAEnumeratorABC.get_addr(tup.req), + False, tup.req.memorySizeLen, + tup.req.memoryAddressLen, 0x80)] + + return {"points_of_interest": points_of_interest} + + def __init__(self): + # type: () -> None + super(UDS_RMBAEnumerator, self).__init__( + [UDS_RMBARandomEnumerator(), UDS_RMBASequentialEnumerator()], + [None, self.__connector_rand_to_seq]) + + +class UDS_RDEnumerator(UDS_Enumerator): + _description = "RequestDownload supported" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'unittest': (bool, None) + }) + + _supported_kwargs_doc = ServiceEnumerator._supported_kwargs_doc + """ + :param bool unittest: Enables smaller search space for unit-test + scenarios. This safes execution time.""" + + def execute(self, socket, state, **kwargs): + # type: (_SocketUnion, EcuState, Any) -> None + super(UDS_RDEnumerator, self).execute(socket, state, **kwargs) + + execute.__doc__ = _supported_kwargs_doc + + @staticmethod + def _random_memory_addr_pkt(addr_len=None): # noqa: E501 + # type: (Optional[int]) -> Packet + pkt = UDS() / UDS_RD() # type: Packet + pkt.dataFormatIdentifiers = random.randint(0, 16) + pkt.memorySizeLen = random.randint(1, 4) + pkt.memoryAddressLen = addr_len or random.randint(1, 4) + UDS_RMBARandomEnumerator.set_size(pkt, 0x10) + addr = random.randint(0, 2 ** (8 * pkt.memoryAddressLen) - 1) & (0xffffffff << (4 * pkt.memoryAddressLen)) # noqa: E501 + UDS_RMBARandomEnumerator.set_addr(pkt, addr) + return pkt + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + if kwargs.get("unittest", False): + return itertools.chain( + (self._random_memory_addr_pkt(addr_len=1) for _ in range(100)), + (self._random_memory_addr_pkt(addr_len=2) for _ in range(500))) + + return itertools.chain( + (self._random_memory_addr_pkt(addr_len=1) for _ in range(100)), + (self._random_memory_addr_pkt(addr_len=2) for _ in range(500)), + (self._random_memory_addr_pkt(addr_len=3) for _ in range(1000)), + (self._random_memory_addr_pkt(addr_len=4) for _ in range(5000))) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%04x" % UDS_RMBAEnumeratorABC.get_addr(tup[1]) + + +class UDS_TDEnumerator(UDS_Enumerator): + _description = "TransferData supported" + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs["scan_range"] = \ + ((list, tuple, range), lambda x: max(x) < 0x100 and min(x) >= 0) + + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + cnt = kwargs.pop("scan_range", range(0x100)) + return cast(Iterable[Packet], UDS() / UDS_TD(blockSequenceCounter=cnt)) + + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % ( + tup[1].blockSequenceCounter, + tup[1].sprintf("%UDS_TD.blockSequenceCounter%")) + + +class UDS_Scanner(AutomotiveTestCaseExecutor): + """ + Example: + >>> def reconnect(): + >>> return UDS_DoIPSocket("169.254.186.237") + >>> + >>> es = [UDS_ServiceEnumerator, UDS_DSCEnumerator] + >>> + >>> def reset(): + >>> reconnect().sr1(UDS()/UDS_ER(resetType="hardReset"), + >>> verbose=False, timeout=1) + >>> + >>> s = UDS_Scanner(reconnect(), reconnect_handler=reconnect, + >>> reset_handler=reset, test_cases=es, + >>> UDS_DSCEnumerator_kwargs={ + >>> "timeout": 20, + >>> "overwrite_timeout": False, + >>> "scan_range": [1, 3]}) + >>> + >>> try: + >>> s.scan() + >>> except KeyboardInterrupt: + >>> pass + >>> + >>> s.show_testcases_status() + >>> s.show_testcases() + """ + + @property + def default_test_case_clss(self): + # type: () -> List[Type[AutomotiveTestCaseABC]] + return [UDS_ServiceEnumerator, UDS_DSCEnumerator, UDS_TPEnumerator, + UDS_SAEnumerator, UDS_WDBISelectiveEnumerator, + UDS_RMBAEnumerator, UDS_RCEnumerator, UDS_IOCBIEnumerator] + + +def uds_software_reset(connection, # type: _SocketUnion + logger=log_automotive, # type: logging.Logger + timeout=0.5 # type: Union[int, float] + ): # type: (...) -> None + logger.debug("Reset procedure of target started.") + resp = connection.sr1(UDS() / UDS_ER(resetType=1), + timeout=timeout, + verbose=False) + if resp and resp.service != 0x7f: + logger.debug("Reset procedure of target complete") + return + + logger.debug("Couldn't reset target with UDS_ER. " + "At least try to set target back to DefaultSession") + resp = connection.sr1(UDS() / UDS_DSC(b"\x01"), + verbose=False, + timeout=timeout) + if resp and resp.service != 0x7f: + logger.debug("Target in DefaultSession") + return + + logger.error("Target not in DefaultSession. Software reset failed.") diff --git a/scapy/contrib/automotive/volkswagen/__init__.py b/scapy/contrib/automotive/volkswagen/__init__.py index f06000ab15a..618bfe6c8d2 100644 --- a/scapy/contrib/automotive/volkswagen/__init__.py +++ b/scapy/contrib/automotive/volkswagen/__init__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/automotive/volkswagen/definitions.py b/scapy/contrib/automotive/volkswagen/definitions.py index 56c1ab57186..17854a98289 100644 --- a/scapy/contrib/automotive/volkswagen/definitions.py +++ b/scapy/contrib/automotive/volkswagen/definitions.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss # Copyright (C) Jonas Schmidt -# This program is published under a GPLv2 license # scapy.contrib.description = Volkswagen specific definitions for UDS # scapy.contrib.status = skip @@ -3155,16 +3155,16 @@ UDS_RDBI.dataIdentifiers[0xf1df] = "ECU Programming Information" -UDS_RC.routineControlTypes[0x0202] = "Check Memory" -UDS_RC.routineControlTypes[0x0203] = "Check Programming Preconditions" -UDS_RC.routineControlTypes[0x0317] = "Reset of Adaption Values" -UDS_RC.routineControlTypes[0x0366] = "Reset of all Adaptions" -UDS_RC.routineControlTypes[0x03e7] = "Reset to Factory Settings" -UDS_RC.routineControlTypes[0x045a] = "Clear user defined DTC information" -UDS_RC.routineControlTypes[0x0544] = "Verify partial software checksum" -UDS_RC.routineControlTypes[0x0594] = "Check upload preconditions" -UDS_RC.routineControlTypes[0xff00] = "Erase Memory" -UDS_RC.routineControlTypes[0xff01] = "Check Programming Dependencies" +UDS_RC.routineControlIdentifiers[0x0202] = "Check Memory" +UDS_RC.routineControlIdentifiers[0x0203] = "Check Programming Preconditions" +UDS_RC.routineControlIdentifiers[0x0317] = "Reset of Adaption Values" +UDS_RC.routineControlIdentifiers[0x0366] = "Reset of all Adaptions" +UDS_RC.routineControlIdentifiers[0x03e7] = "Reset to Factory Settings" +UDS_RC.routineControlIdentifiers[0x045a] = "Clear user defined DTC information" +UDS_RC.routineControlIdentifiers[0x0544] = "Verify partial software checksum" +UDS_RC.routineControlIdentifiers[0x0594] = "Check upload preconditions" +UDS_RC.routineControlIdentifiers[0xff00] = "Erase Memory" +UDS_RC.routineControlIdentifiers[0xff01] = "Check Programming Dependencies" UDS_RD.dataFormatIdentifiers[0x0000] = "Uncompressed" diff --git a/scapy/contrib/automotive/xcp/__init__.py b/scapy/contrib/automotive/xcp/__init__.py new file mode 100644 index 00000000000..bd954beb1fc --- /dev/null +++ b/scapy/contrib/automotive/xcp/__init__.py @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Tabea Spahn + +# scapy.contrib.status = skip + +""" +Package of contrib automotive xcp specific modules +that have to be loaded explicitly. +""" diff --git a/scapy/contrib/automotive/xcp/cto_commands_master.py b/scapy/contrib/automotive/xcp/cto_commands_master.py new file mode 100644 index 00000000000..84433bc9b92 --- /dev/null +++ b/scapy/contrib/automotive/xcp/cto_commands_master.py @@ -0,0 +1,543 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Tabea Spahn + +# scapy.contrib.status = skip + +from scapy.contrib.automotive.xcp.utils import get_ag, get_max_cto, \ + XCPEndiannessField, StrVarLenField +from scapy.fields import ByteEnumField, ByteField, ShortField, StrLenField, \ + IntField, ThreeBytesField, FlagsField, ConditionalField, XByteField, \ + XIntField, FieldLenField +from scapy.packet import Packet, bind_layers + + +# ##### CTO COMMANDS ###### + +# STANDARD COMMANDS + +class Connect(Packet): + commands = {0x00: "NORMAL", 0x01: "USER_DEFINED"} + fields_desc = [ + ByteEnumField("connection_mode", 0, commands), + ] + + +class Disconnect(Packet): + # DISCONNECT has no data + pass + + +class GetStatus(Packet): + # GET_STATUS has no data + pass + + +class Synch(Packet): + # SYNCH has no data + pass + + +class GetCommModeInfo(Packet): + # GET_COMM_MODE_INFO has no data + pass + + +class GetId(Packet): + """Get identification from slave""" + types = {0x00: "ASCII", + 0x01: "file_name_without_path_and_extension", + 0x02: "file_name_with_path_and_extension", + 0x03: "URL", + 0x04: "File" + } + fields_desc = [ByteEnumField("identification_type", 0x00, types)] + + +class SetRequest(Packet): + """Request to save to non-volatile memory""" + fields_desc = [ + FlagsField("mode", 0, 8, [ + "store_cal_req", "store_daq_req", "clear_daq_req", "x3", "x4", + "x5", "x6", "x7"]), + XCPEndiannessField(ShortField("session_configuration_id", 0x00)) + ] + + +class GetSeed(Packet): + # Get seed for unlocking a protected resource + seed_mode = {0x00: "first", 0x01: "remaining"} + res = {0x00: "resource", 0x01: "ignore"} + fields_desc = [ + ByteEnumField("mode", 0, seed_mode), + ByteEnumField("resource", 0, res) + ] + + +class Unlock(Packet): + # Send key for unlocking a protected resource + fields_desc = [ + FieldLenField("len", None, length_of="seed", fmt="B"), + StrVarLenField("seed", b"", length_from=lambda p: p.len, + max_length=lambda: get_max_cto() - 2) + ] + + +class SetMta(Packet): + # Set Memory Transfer Address in slave + fields_desc = [ + # specification says: position 1,2 type byte (not WORD) The example( + # Part 5 Example Communication Sequences ) shows 2 bytes for + # "reserved" + # http://read.pudn.com/downloads192/doc/comm/903802/XCP%20-Part%205-%20Example%20Communication%20Sequences%20-1.0.pdf # noqa: E501 + # --> 2 bytes + XCPEndiannessField(ShortField("reserved", 0)), + ByteField("address_extension", 0), + XCPEndiannessField(XIntField("address", 0)) + ] + + +class Upload(Packet): + # Upload from slave to master + fields_desc = [ByteField("nr_of_data_elements", 0)] + + +class ShortUpload(Packet): + # Upload from slave to master (short version) + fields_desc = [ + ByteField("nr_of_data_elements", 0), + ByteField("reserved", 0), + XByteField("address_extension", 0), + XCPEndiannessField(IntField("address", 0)) + ] + + +class BuildChecksum(Packet): + # Build checksum over memory range + fields_desc = [ + # specification says: position 1-3 type byte The example(Part 5 + # Example Communication Sequences ) shows 3 bytes for "reserved" + # http://read.pudn.com/downloads192/doc/comm/903802/XCP%20-Part%205-%20Example%20Communication%20Sequences%20-1.0.pdf # noqa: E501 + # --> 3 bytes + XCPEndiannessField(ThreeBytesField("reserved", 0)), + XCPEndiannessField(XIntField("block_size", 0)) + ] + + +class TransportLayerCmd(Packet): + # Refer to transport layer specific command + sub_commands = { + 0xFF: "GET_SLAVE_ID", + 0xFE: "GET_DAQ_ID", + 0xFD: "SET_DAQ_ID", + } + fields_desc = [ + ByteEnumField("sub_command_code", 0xFF, sub_commands), + ] + + +class TransportLayerCmdGetSlaveId(Packet): + echo_mode = { + 0x00: "identify_by_echo", + 0x01: "confirm_by_inverse_echo", + } + + fields_desc = [ + XByteField("x", 0x58), # ASCII = X + XByteField("c", 0x43), # ASCII = C + XByteField("p", 0x50), # ASCII = P + ByteEnumField("mode", 0x00, echo_mode), + ] + + +bind_layers(TransportLayerCmd, TransportLayerCmdGetSlaveId, + sub_command_code=0xFF) + + +class TransportLayerCmdGetDAQId(Packet): + fields_desc = [ + XCPEndiannessField(ShortField("daq_list_number", 0)), + ] + + +bind_layers(TransportLayerCmd, TransportLayerCmdGetDAQId, + sub_command_code=0xFE) + + +class TransportLayerCmdSetDAQId(Packet): + sub_command = { + 0xFD: "SET_DAQ_ID", + } + fields_desc = [ + XCPEndiannessField(ShortField("daq_list_number", 0)), + XCPEndiannessField(IntField("can_identifier", 0)) + ] + + +bind_layers(TransportLayerCmd, TransportLayerCmdSetDAQId, + sub_command_code=0xFD) + + +class UserCmd(Packet): + # Refer to user defined command + fields_desc = [ + ByteField("sub_command_code", 0), + ] + + +# Calibration Commands + +class Download(Packet): + # Download from master to slave + fields_desc = [ + ByteField("nr_of_data_elements", 0), + ConditionalField( + StrLenField("alignment", b"", + length_from=lambda pkt: get_ag() - 2), + lambda pkt: get_ag() > 2), + StrLenField("data_elements", b"", + length_from=lambda pkt: get_max_cto() - 2 if get_ag() == 1 + else get_max_cto() - get_ag()), + ] + + +class DownloadNext(Download): + # Used for the download from master to slave in block mode + # Same as "Download", but with different command code + pass + + +class DownloadMax(Packet): + # Download from master to slave (fixed size) + fields_desc = [ + ConditionalField( + StrLenField("alignment", b"", length_from=lambda _: get_ag() - 1), + lambda _: get_ag() > 1), + StrLenField("data_elements", b"", + length_from=lambda _: get_max_cto() - (get_ag() * 2 - 1)) + ] + + +class ShortDownload(Packet): + # Download from master to slave (short version) + fields_desc = [ + FieldLenField("len", None, length_of="data_elements", fmt="B"), + ByteField("reserved", 0), + ByteField("address_extension", 0), + XCPEndiannessField(IntField("address", 0)), + StrVarLenField("data_elements", b"", length_from=lambda p: p.len, + max_length=lambda: get_max_cto() - 8) + ] + + +class ModifyBits(Packet): + # Modify bits + fields_desc = [ + ByteField("shift_value", 0), + XCPEndiannessField(ShortField("and_mask", 0)), + XCPEndiannessField(ShortField("xor_mask", 0)) + ] + + +# Page Switching commands +class SetCalPage(Packet): + """Set calibration page""" + fields_desc = [ + FlagsField("mode", 0, 8, + ["ecu", "xcp", "x2", "x3", "x4", "x5", "x6", "all"]), + ByteField("data_segment_num", 0), + ByteField("data_page_num", 0) + ] + + +class GetCalPage(Packet): + """Get calibration page""" + fields_desc = [ + ByteField("access_mode", 0), + ByteField("data_segment_num", 0) + ] + + +class GetPagProcessorInfo(Packet): + """Get general information on PAG processor""" + pass + + +class GetSegmentInfo(Packet): + """Get specific information for a SEGMENT""" + info_mode = { + 0x00: "get_basic_address_info", + 0x01: "get_standard_info", + 0x02: "get_address_mapping_info" + } + + fields_desc = [ + ByteEnumField("mode", 0x00, info_mode), + ByteField("segment_number", 0), + ByteField("segment_info", 0), + ByteField("mapping_index", 0) + + ] + + +class GetPageInfo(Packet): + """Get specific information for a PAGE""" + fields_desc = [ + ByteField("reserved", 0), + ByteField("segment_number", 0), + ByteField("page_number", 0) + ] + + +class SetSegmentMode(Packet): + """Set mode for a SEGMENT""" + fields_desc = [ + FlagsField("mode", 0, 8, + ["freeze", "x1", "x2", "x3", "x4", "x5", "x6", "x7"]), + ByteField("segment_number", 0) + ] + + +class GetSegmentMode(Packet): + """Get mode for a SEGMENT""" + fields_desc = [ + ByteField("reserved", 0), + ByteField("segment_number", 0) + ] + + +class CopyCalPage(Packet): + """This command forces the slave to copy one calibration page to another. + This command is only available if more than one calibration page is defined + """ + fields_desc = [ + ByteField("segment_num_src", 0), + ByteField("page_num_src", 0), + ByteField("segment_num_dst", 0), + ByteField("page_num_dst", 0) + ] + + +class SetDaqPtr(Packet): + """Data acquisition and stimulation, static, mandatory""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("daq_list_num", 0)), + ByteField("odt_num", 0), + ByteField("odt_entry_num", 0) + ] + + +class WriteDaq(Packet): + """Data acquisition and stimulation, static, mandatory""" + fields_desc = [ + ByteField("bit_offset", 0), + ByteField("size_of_daq_element", 0), + ByteField("address_extension", 0), + XCPEndiannessField(IntField("address", 0)) + ] + + +class SetDaqListMode(Packet): + """Set mode for DAQ list""" + fields_desc = [ + FlagsField("mode", 0, 8, + ["x0", "direction", "x2", "x3", "timestamp", "pid_off", + "x6", "x7"]), + XCPEndiannessField(ShortField("daq_list_num", 0)), + XCPEndiannessField(ShortField("event_channel_num", 0)), + ByteField("transmission_rate_prescaler", 0), + ByteField("daq_list_prio", 0) + ] + + +class GetDaqListMode(Packet): + """Get mode from DAQ list""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("daq_list_number", 0)) + ] + + +class StartStopDaqList(Packet): + """Start/stop/select DAQ list""" + mode_enum = {0x00: "stop", 0x01: "start", 0x02: "select"} + fields_desc = [ + ByteEnumField("mode", 0, mode_enum), + XCPEndiannessField(ShortField("daq_list_number", 0)) + ] + + +class StartStopSynch(Packet): + """Start/stop DAQ lists (synchronously)""" + mode_enum = {0x00: "stop", 0x01: "start", 0x02: "select"} + fields_desc = [ + ByteEnumField("mode", 0x00, mode_enum) + ] + + +class ReadDaq(Packet): + """Read element from ODT entry""" + pass + + +class GetDaqClock(Packet): + """Get DAQ clock from slave""" + pass + + +class GetDaqProcessorInfo(Packet): + """Get general information on DAQ processor""" + pass + + +class GetDaqResolutionInfo(Packet): + """Get general information on DAQ processing resolutioin""" + pass + + +class GetDaqListInfo(Packet): + """Get specific information for a DAQ list""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("daq_list_num", 0)) + ] + + +class GetDaqEventInfo(Packet): + """Get specific information for an event channel""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("event_channel_num", 0)) + ] + + # Cyclic data transfer - static configuration commands + + +class ClearDaqList(Packet): + """Clear DAQ list configuration""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("daq_list_num", 0)) + ] + + +# Cyclic Data transfer - dynamic configuration commands + + +class FreeDaq(Packet): + """Clear dynamic DAQ configuration""" + pass + + +class AllocDaq(Packet): + """Allocate DAQ lists""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("daq_count", 0)) + ] + + +class AllocOdt(Packet): + """Allocate ODTs to a DAQ list""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("daq_list_num", 0)), + ByteField("odt_count", 0) + ] + + +class AllocOdtEntry(Packet): + """Allocate ODT entries to an ODT""" + fields_desc = [ + ByteField("reserved", 0), + XCPEndiannessField(ShortField("daq_list_num", 0)), + ByteField("odt_num", 0), + ByteField("odt_entries_count", 0) + ] + + +# Flash Programming commands + +class ProgramStart(Packet): + """Indicate the beginning of a programming sequence""" + pass + + +class ProgramClear(Packet): + """Clear a part of non-volatile memory""" + access_mode = {0x00: "absolute_access", 0x01: "functional_access"} + fields_desc = [ + ByteEnumField("mode", 0, access_mode), + XCPEndiannessField(ShortField("reserved", 0)), + XCPEndiannessField(IntField("clear_range", 0)) + ] + + +class Program(Download): + """Program a non-volatile memory segment""" + # Same structure as "Download", but with different command code + pass + + +class ProgramReset(Packet): + """Indicate the end of a programming sequence""" + pass + + +class GetPgmProcessorInfo(Packet): + """Get general information on PGM processor""" + pass + + +class GetSectorInfo(Packet): + """Get specific information for a SECTOR""" + address_mode = {0x00: "get_address", 0x01: "get_length"} + fields_desc = [ + ByteEnumField("mode", 0, address_mode), + ByteField("sector_number", 0) + ] + + +class ProgramPrepare(Packet): + """Prepare non-volatile memory programming""" + fields_desc = [ + ByteField("not_used", 0), + XCPEndiannessField(ShortField("code_size", 0)) + ] + + +class ProgramFormat(Packet): + """Set data format before programming""" + fields_desc = [ + ByteField("compression_method", 0), + ByteField("encryption_mode", 0), + ByteField("programming_method", 0), + ByteField("access_method", 0) + ] + + +class ProgramNext(Download): + """Program a non-volatile memory segment (Block Mode)""" + # Same structure as "Download", but with different command code + pass + + +class ProgramMax(DownloadMax): + """Program a non-volatile memory segment (fixed size)""" + # Same as "DownloadMax", but with different command code + pass + + +class ProgramVerify(Packet): + """Program Verify""" + start_mode = { + 0x00: "request_to_start_internal_routine", + 0x01: "sending_verification_value" + } + fields_desc = [ + ByteEnumField("verification_mode", 0, start_mode), + XCPEndiannessField(ShortField("verification_type", 0)), + XCPEndiannessField(IntField("verification_value", 0)) + ] diff --git a/scapy/contrib/automotive/xcp/cto_commands_slave.py b/scapy/contrib/automotive/xcp/cto_commands_slave.py new file mode 100644 index 00000000000..20e042d73aa --- /dev/null +++ b/scapy/contrib/automotive/xcp/cto_commands_slave.py @@ -0,0 +1,478 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Tabea Spahn + +# scapy.contrib.status = skip + +from scapy.config import conf +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.xcp.utils import get_max_cto, get_ag, \ + XCPEndiannessField, StrVarLenField +from scapy.fields import ByteEnumField, ByteField, ShortField, StrLenField, \ + FlagsField, IntField, ThreeBytesField, ConditionalField, XByteField, \ + StrField, LEShortField, XIntField, FieldLenField +from scapy.packet import Packet + + +# ##### CTO COMMANDS ###### + +# STANDARD COMMANDS + +class NegativeResponse(Packet): + """Error Packet""" + error_code_enum = { + 0x00: "ERR_CMD_SYNCH", + 0x10: "ERR_CMD_BUSY", + 0x11: "ERR_DAQ_ACTIVE", + 0x12: "ERR_PGM_ACTIVE", + 0x20: "ERR_CMD_UNKNOWN", + 0x21: "ERR_CMD_SYNTAX", + 0x22: "ERR_OUT_OF_RANGE", + 0x23: "ERR_WRITE_PROTECTED", + 0x24: "ERR_ACCESS_DENIED", + 0x25: "ERR_ACCESS_LOCKED", + 0x26: "ERR_PAGE_NOT_VALID", + 0x27: "ERR_MODE_NOT_VALID", + 0x28: "ERR_SEGMENT_NOT_VALID", + 0x29: "ERR_SEQUENCE", + 0x2A: "ERR_DAQ_CONFIG", + 0x30: "ERR_MEMORY_OVERFLOW", + 0x31: "ERR_GENERIC", + 0x32: "ERR_VERIFY" + } + fields_desc = [ + ByteEnumField("error_code", 0, error_code_enum), + StrField("error_info", "") + ] + + +class GenericResponse(Packet): + """Command Response packet """ + fields_desc = [ + StrField("command_response_data", "") + ] + + +class ConnectPositiveResponse(Packet): + fields_desc = [ + FlagsField("resource", 0, 8, + ["cal_pag", "x1", "daq", "stim", "pgm", "x5", "x6", "x7"]), + FlagsField("comm_mode_basic", 0, 8, + ["byte_order", "address_granularity_0", + "address_granularity_1", "x3", "x4", "x5", + "slave_block_mode", "optional"]), + ByteField("max_cto", 0), + ConditionalField(ShortField("max_dto", 0), + lambda p: p.comm_mode_basic.byte_order), + ConditionalField(LEShortField("max_dto_le", 0), + lambda p: not p.comm_mode_basic.byte_order), + ByteField("xcp_protocol_layer_version_number_msb", 1), + ByteField("xcp_transport_layer_version_number_msb", 1) + ] + + def post_dissection(self, pkt): + if conf.contribs["XCP"]["allow_byte_order_change"]: + new_value = int(self.comm_mode_basic.byte_order) + if new_value != conf.contribs["XCP"]["byte_order"]: + conf.contribs["XCP"]["byte_order"] = new_value + + desc = "Big Endian" if new_value else "Little Endian" + log_automotive.warning("Byte order changed to {0} because of received " + "positive connect packet".format(desc)) + + if conf.contribs["XCP"]["allow_ag_change"]: + conf.contribs["XCP"][ + "Address_Granularity_Byte"] = self.get_address_granularity() + + if conf.contribs["XCP"]["allow_cto_and_dto_change"]: + conf.contribs["XCP"]["MAX_CTO"] = self.max_cto + conf.contribs["XCP"]["MAX_DTO"] = self.max_dto or self.max_dto_le + + def get_address_granularity(self): + comm_mode_basic = self.comm_mode_basic + if not comm_mode_basic.address_granularity_0 and \ + not comm_mode_basic.address_granularity_1: + return 1 + if comm_mode_basic.address_granularity_0 and \ + not comm_mode_basic.address_granularity_1: + return 2 + if not comm_mode_basic.address_granularity_0 and \ + comm_mode_basic.address_granularity_1: + return 4 + else: + log_automotive.warning( + "Getting address granularity from packet failed:" + "both flags are 1") + + +class StatusPositiveResponse(Packet): + fields_desc = [ + FlagsField("current_session_status", 0, 8, + ["store_cal_req", "x1", "store_daq_req", + "clear_daq_request", "x4", "x5", "daq_running", "resume"]), + FlagsField("current_resource_protection_status", 0, 8, + ["cal_pag", "x1", "daq", "stim", "pgm", "x5", "x6", "x7"]), + ByteField("reserved", 0), + XCPEndiannessField(ShortField("session_configuration_id", 0)) + ] + + +class CommonModeInfoPositiveResponse(Packet): + fields_desc = [ + ByteField("reserved1", 0), + FlagsField("comm_mode_optional", 0, 8, + ["master_block_mode", "interleaved_mode", "x2", "x3", "x4", + "x5", "x6", "x7"]), + ByteField("reserved2", 0), + ByteField("max_bs", 0), + ByteField("min_st", 0), + ByteField("queue_size", 0), + ByteField("xcp_driver_version_number", 0), + ] + + +class IdPositiveResponse(Packet): + fields_desc = [ + ByteField("mode", 0), + XCPEndiannessField(ShortField("reserved", 0)), + XCPEndiannessField(FieldLenField("length", None, length_of="element", + fmt="I")), + StrVarLenField("element", b"", length_from=lambda p: p.length, + max_length=lambda pkt: get_ag()) + ] + + +class SeedPositiveResponse(Packet): + fields_desc = [ + FieldLenField("seed_length", None, length_of="seed", fmt="B"), + StrVarLenField("seed", b"", length_from=lambda p: p.seed_length, + max_length=lambda: get_max_cto() - 2) + ] + + +class UnlockPositiveResponse(Packet): + fields_desc = [ + FlagsField("current_resource_protection_status", 0, 8, + ["cal_pag", "x1", "daq", "stim", "pgm", "x5", "x6", "x7"]) + ] + + +class UploadPositiveResponse(Packet): + fields_desc = [ + ConditionalField( + StrLenField("alignment", b"", + length_from=lambda pkt: get_ag() - 1), + lambda _: get_ag() > 1), + StrLenField("element", b"", + length_from=lambda pkt: get_max_cto() - get_ag()), + ] + + +class ShortUploadPositiveResponse(Packet): + fields_desc = [ + ConditionalField( + StrLenField("alignment", b"", + length_from=lambda pkt: get_ag() - 1), + lambda _: get_ag() > 1), + StrLenField("element", b"", + length_from=lambda pkt: get_max_cto() - get_ag()), + ] + + +class ChecksumPositiveResponse(Packet): + checksum_type_dict = { + 0x01: "XCP_ADD_11", + 0x02: "XCP_ADD_12", + 0x03: "XCP_ADD_14", + 0x04: "XCP_ADD_22", + 0x05: "XCP_ADD_24", + 0x06: "XCP_ADD_44", + 0x07: "XCP_CRC_16", + 0x08: "XCP_CRC_16_CITT", + 0x09: "XCP_CRC_32", + 0xFF: "XCP_USER_DEFINED" + } + fields_desc = [ + ByteEnumField("checksum_type", 0, checksum_type_dict), + # specification says: position 2,3 type byte (not WORD) The example( + # Part 5 Example Communication Sequences) shows 2 bytes for + # "reserved" + # http://read.pudn.com/downloads192/doc/comm/903802/XCP%20-Part%205-%20Example%20Communication%20Sequences%20-1.0.pdf # noqa: E501 + # --> 2 bytes + XCPEndiannessField(ShortField("reserved", 0)), + XCPEndiannessField(XIntField("checksum", 0)), + ] + + +class TransportLayerCmdGetSlaveIdResponse(Packet): + fields_desc = [ + XByteField("position_1", 0x58), # 0xA7 (inversed echo) + XByteField("position_2", 0x43), # 0xBC (inversed echo) + XByteField("position_3", 0x50), # 0xAF (inversed echo) + XCPEndiannessField(IntField("can_identifier", 0)) + ] + + +class TransportLayerCmdGetDAQIdResponse(Packet): + can_id_fixed_enum = { + 0x00: "configurable", + 0x01: "fixed" + } + fields_desc = [ + ByteEnumField("can_id_fixed", 0xFE, can_id_fixed_enum), + XCPEndiannessField(ShortField("reserved", 0)), + XCPEndiannessField(IntField("can_identifier", 0)) + ] + + +class CalPagePositiveResponse(Packet): + fields_desc = [ + ByteField("reserved_1", 0), + ByteField("reserved_2", 0), + ByteField("logical_data_page_number", 0), + ] + + +class PagProcessorInfoPositiveResponse(Packet): + fields_desc = [ + ByteField("max_segment", 0), + FlagsField("pag_properties", 0, 8, + ["freeze_supported", "x1", "x2", "x3", "x4", "x5", "x6", + "x7"]), + ] + + +class SegmentInfoMode0PositiveResponse(Packet): + fields_desc = [ + # spec: position 1-3: type byte + # --> take position over type + XCPEndiannessField(ThreeBytesField("reserved", 0)), + XCPEndiannessField(IntField("basic_info", 0)), + ] + + +class SegmentInfoMode1PositiveResponse(Packet): + fields_desc = [ + ByteField("max_pages", 0), + ByteField("address_extension", 0), + ByteField("max_extension", 0), + ByteField("compression_method", 0), + ByteField("encryption_method", 0), + ] + + +class SegmentInfoMode2PositiveResponse(Packet): + fields_desc = [ + # spec: position 1-3: type byte + # --> take position over type + XCPEndiannessField(ThreeBytesField("reserved", 0)), + XCPEndiannessField(IntField("mapping_info", 0)), + ] + + +class PageInfoPositiveResponse(Packet): + fields_desc = [ + FlagsField("page_properties", 0, 8, + ["ecu_access_without_xcp", "ecu_access_with_xcp", + "xcp_read_access_without_ecu", "xcp_read_access_with_ecu", + "xcp_write_access_without_ecu", + "xcp_write_access_with_ecu", "x6", "x7"]), + ByteField("init_segment", 0), + ] + + +class SegmentModePositiveResponse(Packet): + fields_desc = [ + ByteField("reserved", 0), + FlagsField("mode", 0, 8, + ["freeze", "x1", "x2", "x3", "x4", "x5", "x6", "x7"]), + ] + + +class DAQListModePositiveResponse(Packet): + fields_desc = [ + FlagsField("current_mode", 0, 8, + ["selected", "direction", "x2", "x3", "timestamp", + "pid_off", "running", "resume"]), + XCPEndiannessField(ShortField("reserved", 0)), + XCPEndiannessField(ShortField("current_event_channel_number", 0)), + ByteField("current_prescaler", 0), + ByteField("current_daq_list_priority", 0), + ] + + +class StartStopDAQListPositiveResponse(Packet): + fields_desc = [ + ByteField("first_pid", 0), + ] + + +class DAQClockListPositiveResponse(Packet): + fields_desc = [ + # spec: position 1-3: type byte + # --> take position over type + XCPEndiannessField(ThreeBytesField("reserved", 0)), + XCPEndiannessField(IntField("receive_timestamp", 0)) + ] + + +class ReadDAQPositiveResponse(Packet): + fields_desc = [ + ByteField("bit_offset", 0), + ByteField("size_daq_element", 0), + ByteField("address_extension_daq_element", 0), + XCPEndiannessField(IntField("daq_element_address", 0)) + ] + + +class DAQProcessorInfoPositiveResponse(Packet): + fields_desc = [ + FlagsField("daq_properties", 0, 8, + ["daq_config_type", "prescaler_supported", + "resume_supported", "bit_stim_supported", + "timestamp_supported", "pid_off_supported", "overload_msb", + "overload_event"]), + XCPEndiannessField(ShortField("max_daq", 0)), + XCPEndiannessField(ShortField("max_event_channel", 0)), + ByteField("min_daq", 0), + FlagsField("daq_key_byte", 0, 8, + ["optimisation_type_0", "optimisation_type_1", + "optimisation_type_2", "optimisation_type_3", + "address_extension_odt", "address_extension_daq", + "identification_field_type_0", + "identification_field_type_1"]), + ] + + def write_identification_field_type_to_config(self): + conf.contribs["XCP"][ + "identification_field_type_0"] = bool( + self.daq_key_byte.identification_field_type_0) + conf.contribs["XCP"][ + "identification_field_type_1"] = bool( + self.daq_key_byte.identification_field_type_1) + + def post_dissection(self, pkt): + self.write_identification_field_type_to_config() + + +class DAQResolutionInfoPositiveResponse(Packet): + fields_desc = [ + ByteField("granularity_odt_entry_size_daq", 0), + ByteField("max_odt_entry_size_daq", 0), + ByteField("granularity_odt_entry_size_stim", 0), + ByteField("max_odt_entry_size_stim", 0), + FlagsField("timestamp_mode", 0, 8, + ["size_0", "size_1", "size_2", "timestamp_fixed", "unit_0", + "unit_1", "unit_2", "unit_3"]), + XCPEndiannessField(ShortField("timestamp_ticks", 0)), + ] + + def get_timestamp_size(self): + size_0 = bool(self.timestamp_mode.size_0) + size_1 = bool(self.timestamp_mode.size_1) + size_2 = bool(self.timestamp_mode.size_2) + + if not size_2 and not size_1 == 0 and size_0: + return 1 + if not size_2 and size_1 and not size_0: + return 2 + if size_2 and not size_1 and not size_0: + return 4 + return 0 + + def write_timestamp_size_to_config(self): + conf.contribs["XCP"]["timestamp_size"] = self.get_timestamp_size() + + def post_dissection(self, pkt): + self.write_timestamp_size_to_config() + + +class DAQListInfoPositiveResponse(Packet): + fields_desc = [ + FlagsField("daq_list_properties", 0, 8, + ["predefined", "event_fixed", "daq", "stim", "x4", "x5", + "x6", "x7"]), + ByteField("max_odt", 0), + ByteField("max_odt_entries", 0), + XCPEndiannessField(ShortField("fixed_event", 0)), + ] + + +class DAQEventInfoPositiveResponse(Packet): + fields_desc = [ + FlagsField("daq_event_properties", 0, 8, + ["x0", "x1", "daq", "stim", "x4", "x5", "x6", "x7"]), + ByteField("max_daq_list", 0), + ByteField("event_channel_name_length", 0), + ByteField("event_channel_time_cycle", 0), + ByteField("event_channel_time_unit", 0), + ByteField("event_channel_priority", 0), + ] + + +class ProgramStartPositiveResponse(Packet): + fields_desc = [ + ByteField("reserved", 0), + FlagsField("comm_mode_pgm", 0, 8, + ["master_block_mode", "interleaved_mode", "x2", "x3", "x4", + "x5", "slave_block_mode", "x7"]), + ByteField("max_cto_pgm", 0), + ByteField("max_bs_pgm", 0), + ByteField("min_bs_pgm", 0), + ByteField("queue_size_pgm", 0), + ] + + +class PgmProcessorPositiveResponse(Packet): + fields_desc = [ + FlagsField("pgm_properties", 0, 8, + ["absolute_mode", "functional_mode", + "compression_supported", "compression_required", + "encryption_supported", "encryption_required", + "non_seq_pgm_supported", "non_seq_pgm_required"]), + ByteField("max_sector", 0), + ] + + +class SectorInfoPositiveResponse(Packet): + fields_desc = [ + ByteField("clear_sequence_number", 0), + ByteField("program_sequence_number", 0), + ByteField("programming_method", 0), + XCPEndiannessField(IntField("sector_info", 0)) + ] + + +class EvPacket(Packet): + """Event packet""" + event_code_enum = { + 0x00: "EV_RESUME_MODE", + 0x01: "EV_CLEAR_DAQ", + 0x02: "EV_STORE_DAQ", + 0x03: "EV_STORE_CAL", + 0x05: "EV_CMD_PENDING", + 0x06: "EV_DAQ_OVERLOAD", + 0x07: "EV_SESSION_TERMINATED", + 0xFE: "EV_USER", + 0xFF: "EV_TRANSPORT", + } + fields_desc = [ + ByteEnumField("event_code", 0, event_code_enum), + StrLenField("event_information_data", b"", + max_length=lambda _: get_max_cto() - 2) + ] + + +class ServPacket(Packet): + """Service Request packet""" + service_request_code_enum = { + 0x00: "SERV_RESET", + 0x01: "SERV_TEXT", + } + + fields_desc = [ + ByteEnumField("service_request_code", 0, service_request_code_enum), + StrLenField("command_response_data", b"", + max_length=lambda _: get_max_cto() - 2) + ] diff --git a/scapy/contrib/automotive/xcp/scanner.py b/scapy/contrib/automotive/xcp/scanner.py new file mode 100644 index 00000000000..af952004848 --- /dev/null +++ b/scapy/contrib/automotive/xcp/scanner.py @@ -0,0 +1,158 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Tabea Spahn + +# scapy.contrib.description = XCPScanner +# scapy.contrib.status = loads +import logging +from collections import namedtuple + +from scapy.config import conf +from scapy.contrib.automotive import log_automotive +from scapy.contrib.automotive.xcp.cto_commands_master import \ + TransportLayerCmd, TransportLayerCmdGetSlaveId, Connect +from scapy.contrib.automotive.xcp.cto_commands_slave import \ + ConnectPositiveResponse, TransportLayerCmdGetSlaveIdResponse +from scapy.contrib.automotive.xcp.xcp import CTORequest, XCPOnCAN +from scapy.contrib.cansocket_native import CANSocket + +# Typing imports +from typing import ( + Optional, + List, + Type, + Iterator, +) + +XCPScannerResult = namedtuple('XCPScannerResult', 'request_id response_id') + + +class XCPOnCANScanner: + """ + Scans for XCP Slave on CAN + """ + + def __init__(self, can_socket, id_range=None, + sniff_time=0.1, add_padding=False, verbose=False): + # type: (CANSocket, Optional[Iterator[int]], Optional[float], Optional[bool], Optional[bool]) -> None # noqa: E501 + + """ + Constructor + :param can_socket: Can Socket with XCPonCAN as basecls for scan + :param id_range: CAN id range to scan + :param sniff_time: time the scan waits for a response + after sending a request + """ + + conf.contribs["XCP"]["add_padding_for_can"] = add_padding + self.__socket = can_socket + self.id_range = id_range or range(0, 0x800) + self.__sniff_time = sniff_time + if verbose: + log_automotive.setLevel(logging.DEBUG) + + def _scan(self, identifier, body, pid, answer_type): + # type: (int, CTORequest, int, Type) -> List # noqa: E501 + + log_automotive.info("Scan for id: " + str(identifier)) + flags = 'extended' if identifier >= 0x800 else 0 + cto_request = \ + XCPOnCAN(identifier=identifier, flags=flags) \ + / CTORequest(pid=pid) / body + + req_and_res_list, _unanswered = \ + self.__socket.sr(cto_request, timeout=self.__sniff_time, + verbose=False, multi=True) + + if len(req_and_res_list) == 0: + log_automotive.info( + "No answer for identifier: " + str(identifier)) + return [] + + valid_req_and_res_list = filter( + lambda req_and_res: req_and_res[1].haslayer(answer_type), + req_and_res_list) + return list(valid_req_and_res_list) + + def _send_connect(self, identifier): + # type: (int) -> List[XCPScannerResult] + """ + Sends CONNECT Message on the Control Area Network + """ + all_slaves = [] + body = Connect(connection_mode=0x00) + xcp_req_and_res_list = self._scan(identifier, body, 0xFF, + ConnectPositiveResponse) + + for req_and_res in xcp_req_and_res_list: + result = XCPScannerResult(response_id=req_and_res[1].identifier, + request_id=identifier) + all_slaves.append(result) + log_automotive.info( + "Detected XCP slave for broadcast identifier: " + str( + identifier) + "\nResponse: " + str(result)) + + if len(all_slaves) == 0: + log_automotive.info( + "No XCP slave detected for identifier: " + str(identifier)) + return all_slaves + + def _send_get_slave_id(self, identifier): + # type: (int) -> List[XCPScannerResult] + """ + Sends GET_SLAVE_ID message on the Control Area Network + """ + all_slaves = [] + body = TransportLayerCmd() / TransportLayerCmdGetSlaveId() + xcp_req_and_res_list = \ + self._scan( + identifier, body, 0xF2, TransportLayerCmdGetSlaveIdResponse) + + for req_and_res in xcp_req_and_res_list: + response = req_and_res[1] + # The protocol will also mark other XCP messages that might be + # send as TransportLayerCmdGetSlaveIdResponse + # -> Payload must be checked. It must include XCP + if response.position_1 != 0x58 or response.position_2 != 0x43 or \ + response.position_3 != 0x50: + continue + + # Identifier that the master must use to send packets to the slave + # and the slave will answer with + request_id = \ + response["TransportLayerCmdGetSlaveIdResponse"].can_identifier + + result = XCPScannerResult(request_id=request_id, + response_id=response.identifier) + all_slaves.append(result) + log_automotive.info( + "Detected XCP slave for broadcast identifier: " + str( + identifier) + "\nResponse: " + str(result)) + + return all_slaves + + def scan_with_get_slave_id(self): + # type: () -> List[XCPScannerResult] + """Starts the scan for XCP devices on CAN with the transport specific + GetSlaveId Message""" + log_automotive.info("Start scan with GetSlaveId id in range: " + str( + self.id_range)) + + for identifier in self.id_range: + ids = self._send_get_slave_id(identifier) + if len(ids) > 0: + return ids + + return [] + + def scan_with_connect(self): + # type: () -> List[XCPScannerResult] + log_automotive.info("Start scan with CONNECT id in range: " + str( + self.id_range)) + results = [] + for identifier in self.id_range: + result = self._send_connect(identifier) + if len(result) > 0: + results.extend(result) + return results diff --git a/scapy/contrib/automotive/xcp/utils.py b/scapy/contrib/automotive/xcp/utils.py new file mode 100644 index 00000000000..0213c28cfe5 --- /dev/null +++ b/scapy/contrib/automotive/xcp/utils.py @@ -0,0 +1,135 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Tabea Spahn + +# scapy.contrib.status = skip + +import struct + +from scapy.config import conf +from scapy.contrib.automotive import log_automotive +from scapy.fields import StrLenField +from scapy.volatile import RandBin, RandNum + + +def get_max_cto(): + max_cto = conf.contribs['XCP']['MAX_CTO'] + if max_cto: + return max_cto + + log_automotive.warning("Define conf.contribs['XCP']['MAX_CTO'].") + raise KeyError("conf.contribs['XCP']['MAX_CTO'] not defined") + + +def get_max_dto(): + max_dto = conf.contribs['XCP']['MAX_DTO'] + if max_dto: + return max_dto + else: + log_automotive.warning("Define conf.contribs['XCP']['MAX_DTO'].") + raise KeyError("conf.contribs['XCP']['MAX_DTO'] not defined") + + +def get_ag(): + address_granularity = conf.contribs['XCP']['Address_Granularity_Byte'] + if address_granularity and address_granularity in [1, 2, 4]: + return address_granularity + else: + log_automotive.warning( + "Define conf.contribs['XCP']['Address_Granularity_Byte']." + "Assign either 1, 2 or 4") + return 1 + + +# With TIMESTAMP_MODE and TIMESTAMP_TICKS at GET_DAQ_RESOLUTION_INFO, +# the slave informs the master about the Type of Timestamp Field +# the slave will use when transferring DAQ Packets to the master. +# The master has to use the same Type of Timestamp Field when transferring +# STIM Packets to the slave. TIMESTAMP_MODE and TIMEPSTAMP_TICKS contain +# information on the resolution of the data transfer clock. +def get_timestamp_length(): + return conf.contribs['XCP']['timestamp_size'] + + +def identification_field_needs_alignment(): + try: + identification_field_type_0 = conf.contribs['XCP'][ + 'identification_field_type_0'] + identification_field_type_1 = conf.contribs['XCP'][ + 'identification_field_type_1'] + if identification_field_type_1 == 1 and \ + identification_field_type_0 == 1: + # relative odt with daq as word (aligned) + return True + return False + except KeyError: + return False + + +def get_daq_length(): + try: + identification_field_type_0 = conf.contribs['XCP'][ + 'identification_field_type_0'] + identification_field_type_1 = conf.contribs['XCP'][ + 'identification_field_type_1'] + + if identification_field_type_1 == 0 and \ + identification_field_type_0 == 0: + # absolute odt number + return 0 + if identification_field_type_1 == 0 and \ + identification_field_type_0 == 1: + # relative odt with daq as byte + return 1 + # relative odt with daq as word + return 2 + except KeyError: + return 0 + + +def get_daq_data_field_length(): + try: + data_length = get_max_dto() + except KeyError: + return 0 + data_length -= 1 # pid + if identification_field_needs_alignment(): + data_length -= 1 + data_length -= get_daq_length() + + return data_length + + +# Idea taken from scapy/scapy/contrib/dce_rpc.py +class XCPEndiannessField(object): + """Field which changes the endianness of a sub-field""" + __slots__ = ["fld"] + + def __init__(self, fld): + self.fld = fld + + def set_endianness(self): + """Add the endianness to the format""" + byte_oder = conf.contribs['XCP']['byte_order'] + endianness = ">" if byte_oder == 1 else "<" + + self.fld.fmt = endianness + self.fld.fmt[1:] + self.fld.struct = struct.Struct(self.fld.fmt) + + def getfield(self, pkt, s): + self.set_endianness() + + return self.fld.getfield(pkt, s) + + def addfield(self, pkt, s, val): + self.set_endianness() + return self.fld.addfield(pkt, s, val) + + def __getattr__(self, attr): + return getattr(self.fld, attr) + + +class StrVarLenField(StrLenField): + def randval(self): + return RandBin(RandNum(0, self.max_length() or 1200)) diff --git a/scapy/contrib/automotive/xcp/xcp.py b/scapy/contrib/automotive/xcp/xcp.py new file mode 100644 index 00000000000..dcadac43894 --- /dev/null +++ b/scapy/contrib/automotive/xcp/xcp.py @@ -0,0 +1,470 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss +# Copyright (C) Tabea Spahn + +# scapy.contrib.description = Universal calibration and measurement protocol (XCP) # noqa: E501 +# scapy.contrib.status = loads +import struct + +from scapy.config import conf +from scapy.contrib.automotive.xcp.cto_commands_master import Connect, \ + Disconnect, GetStatus, Synch, GetCommModeInfo, GetId, SetRequest, \ + GetSeed, Unlock, SetMta, Upload, ShortUpload, BuildChecksum, \ + TransportLayerCmd, TransportLayerCmdGetSlaveId, \ + TransportLayerCmdGetDAQId, TransportLayerCmdSetDAQId, UserCmd, Download, \ + DownloadNext, DownloadMax, ShortDownload, ModifyBits, SetCalPage, \ + GetCalPage, GetPagProcessorInfo, GetSegmentInfo, GetPageInfo, \ + SetSegmentMode, GetSegmentMode, CopyCalPage, SetDaqPtr, WriteDaq, \ + SetDaqListMode, GetDaqListMode, StartStopDaqList, StartStopSynch, \ + ReadDaq, GetDaqClock, GetDaqProcessorInfo, GetDaqResolutionInfo, \ + GetDaqListInfo, GetDaqEventInfo, ClearDaqList, FreeDaq, AllocDaq, \ + AllocOdt, AllocOdtEntry, ProgramStart, ProgramClear, Program, \ + ProgramReset, GetPgmProcessorInfo, GetSectorInfo, ProgramPrepare, \ + ProgramFormat, ProgramNext, ProgramMax, ProgramVerify +from scapy.contrib.automotive.xcp.cto_commands_slave import \ + GenericResponse, NegativeResponse, EvPacket, ServPacket, \ + TransportLayerCmdGetSlaveIdResponse, TransportLayerCmdGetDAQIdResponse, \ + SegmentInfoMode0PositiveResponse, SegmentInfoMode1PositiveResponse, \ + SegmentInfoMode2PositiveResponse, ConnectPositiveResponse, \ + StatusPositiveResponse, CommonModeInfoPositiveResponse, \ + IdPositiveResponse, SeedPositiveResponse, UnlockPositiveResponse, \ + UploadPositiveResponse, ShortUploadPositiveResponse, \ + ChecksumPositiveResponse, CalPagePositiveResponse, \ + PagProcessorInfoPositiveResponse, PageInfoPositiveResponse, \ + SegmentModePositiveResponse, DAQListModePositiveResponse, \ + StartStopDAQListPositiveResponse, DAQClockListPositiveResponse, \ + ReadDAQPositiveResponse, DAQProcessorInfoPositiveResponse, \ + DAQResolutionInfoPositiveResponse, DAQListInfoPositiveResponse, \ + DAQEventInfoPositiveResponse, ProgramStartPositiveResponse, \ + PgmProcessorPositiveResponse, SectorInfoPositiveResponse +from scapy.contrib.automotive.xcp.utils import get_timestamp_length, \ + identification_field_needs_alignment, get_daq_length, \ + get_daq_data_field_length +from scapy.fields import ByteEnumField, ShortField, XBitField, \ + FlagsField, ByteField, ThreeBytesField, StrField, ConditionalField, \ + XByteField, StrLenField +from scapy.layers.can import CAN +from scapy.layers.inet import UDP, TCP +from scapy.packet import Packet, bind_layers, bind_bottom_up, bind_top_down + +conf.contribs.setdefault("XCP", {}) + +# 0 stands for Intel/little-endian format, 1 for Motorola/big-endian format +conf.contribs["XCP"].setdefault("byte_order", 1) +conf.contribs["XCP"].setdefault("allow_byte_order_change", True) +# Can be 1, 2 or 4 +conf.contribs["XCP"].setdefault("Address_Granularity_Byte", None) +conf.contribs["XCP"].setdefault("allow_ag_change", True) + +conf.contribs["XCP"].setdefault("MAX_CTO", None) +conf.contribs["XCP"].setdefault("MAX_DTO", None) +conf.contribs["XCP"].setdefault("allow_cto_and_dto_change", True) +conf.contribs["XCP"].setdefault("add_padding_for_can", False) + +conf.contribs['XCP'].setdefault('timestamp_size', 0) + + +# Specifications from: +# http://read.pudn.com/downloads293/doc/comm/1316424/ASAM_XCP_Part1-Overview_V1.0.0.pdf # noqa: E501 +# http://read.pudn.com/downloads192/doc/comm/903802/XCP%20-Part%202-%20Protocol%20Layer%20Specification%20-1.0.pdf # noqa: E501 +# http://read.pudn.com/downloads192/doc/comm/903802/XCP%20-Part%203-%20Transport_layer_specification_xcp_on_can_1-0.pdf # noqa: E501 +# http://read.pudn.com/downloads192/doc/comm/903802/XCP%20-Part%204-%20Interface%20Specification%20-1.0.pdf # noqa: E501 +# http://read.pudn.com/downloads192/doc/comm/903802/XCP%20-Part%205-%20Example%20Communication%20Sequences%20-1.0.pdf # noqa: E501 + +# XCP on USB is left out because it has "no practical meaning" +# XCP on Lin is left out because it has no official specification +class XCPOnCAN(CAN): + name = "Universal calibration and measurement protocol on CAN" + fields_desc = [ + FlagsField("flags", 0, 3, ["error", + "remote_transmission_request", + "extended"]), + XBitField("identifier", 0, 29), + ByteField("length", None), + ThreeBytesField("reserved", 0), + ] + + def post_build(self, pkt, pay): + if self.length is None or \ + (len(pay) < 8 and conf.contribs["XCP"]["add_padding_for_can"]): + tmp_len = 8 if conf.contribs["XCP"]["add_padding_for_can"] else \ + len(pay) + pkt = pkt[:4] + struct.pack("B", tmp_len) + pkt[5:] + pay += b"\xCC" * (tmp_len - len(pay)) + return super(XCPOnCAN, self).post_build(pkt, pay) + + def extract_padding(self, p): + return p[:self.length], None + + +class XCPOnUDP(UDP): + name = "Universal calibration and measurement protocol on Ethernet" + fields_desc = UDP.fields_desc + [ + ShortField("length", None), + ShortField("ctr", 0), # counter + ] + + def post_build(self, pkt, pay): + if self.length is None: + tmp_len = len(pay) + pkt = pkt[:8] + struct.pack("!H", tmp_len) + pkt[10:] + return super(XCPOnUDP, self).post_build(pkt, pay) + + +class XCPOnTCP(TCP): + name = "Universal calibration and measurement protocol on Ethernet" + + fields_desc = TCP.fields_desc + [ + ShortField("length", None), + ShortField("ctr", 0), # counter + ] + + def answers(self, other): + if not isinstance(other, XCPOnTCP): + return 0 + if isinstance(other.payload, CTORequest) and isinstance(self.payload, + CTOResponse): + return self.payload.answers(other.payload) + + def post_build(self, pkt, pay): + if self.length is None: + len_offset = 20 + len(self.options) + tmp_len = len(pay) + tmp_len = struct.pack("!H", tmp_len) + pkt = pkt[:len_offset] + tmp_len + pkt[len_offset + 2:] + return super(XCPOnTCP, self).post_build(pkt, pay) + + +class XCPOnCANTail(Packet): + name = "XCP Tail on CAN" + + fields_desc = [ + StrField("control_field", "") + ] + + +class CTORequest(Packet): + pids = { + # Standard commands + 0xFF: "CONNECT", + 0xFE: "DISCONNECT", + 0xFD: "GET_STATUS", + 0xFC: "SYNCH", + 0xFB: "GET_COMM_MODE_INFO", + 0xFA: "GET_ID", + 0xF9: "SET_REQUEST", + 0xF8: "GET_SEED", + 0xF7: "UNLOCK", + 0xF6: "SET_MTA", + 0xF5: "UPLOAD", + 0xF4: "SHORT_UPLOAD", + 0xF3: "BUILD_CHECKSUM", + 0xF2: "TRANSPORT_LAYER_CMD", + 0xF1: "USER_CMD", + # Calibration commands + 0xF0: "DOWNLOAD", + 0xEF: "DOWNLOAD_NEXT", + 0xEE: "DOWNLOAD_MAX", + 0xED: "SHORT_DOWNLOAD", + 0xEC: "MODIFY_BITS", + # Page change commands + 0xEB: "SET_CAL_PAGE", + 0xEA: "GET_CAL_PAGE", + 0xE9: "GET_PAG_PROCESSOR_INFO", + 0xE8: "GET_SEGMENT_INFO", + 0xE7: "GET_PAGE_INFO", + 0xE6: "SET_SEGMENT_MODE", + 0xE5: "GET_SEGMENT_MODE", + 0xE4: "COPY_CAL_PAGE", + # Periodic data exchange basics + 0xE2: "SET_DAQ_PTR", + 0xE1: "WRITE_DAQ", + 0xE0: "SET_DAQ_LIST_MODE", + 0xDF: "GET_DAQ_LIST_MODE", + 0xDE: "START_STOP_DAQ_LIST", + 0xDD: "START_STOP_SYNCH", + 0xC7: "WRITE_DAQ_MULTIPLE", + 0xDB: "READ_DAQ", + 0xDC: "GET_DAQ_CLOCK", + 0xDA: "GET_DAQ_PROCESSOR_INFO", + 0xD9: "GET_DAQ_RESOLUTION_INFO", + 0xD8: "GET_DAQ_LIST_INFO", + 0xD7: "GET_DAQ_EVENT_INFO", + # Periodic data exchange static configuration + 0xE3: "CLEAR_DAQ_LIST", + # Cyclic data exchange dynamic configuration + 0xD6: "FREE_DAQ", + 0xD5: "ALLOC_DAQ", + 0xD4: "ALLOC_ODT", + 0xD3: "ALLOC_ODT_ENTRY", + # Flash programming + 0xD2: "PROGRAM_START", + 0xD1: "PROGRAM_CLEAR", + 0xD0: "PROGRAM", + 0xCF: "PROGRAM_RESET", + 0xCE: "GET_PGM_PROCESSOR_INFO", + 0xCD: "GET_SECTOR_INFO", + 0xCC: "PROGRAM_PREPARE", + 0xCB: "PROGRAM_FORMAT", + 0xCA: "PROGRAM_NEXT", + 0xC9: "PROGRAM_MAX", + 0xC8: "PROGRAM_VERIFY", + } + + for pid in range(0, 192): + pids[pid] = "STIM" + name = "Command Transfer Object Request" + + fields_desc = [ + ByteEnumField("pid", 0xFF, pids), + ] + + +# ##### CTO COMMANDS ###### + +# STANDARD COMMANDS +bind_layers(CTORequest, Connect, pid=0xFF) +bind_layers(CTORequest, Disconnect, pid=0xFE) +bind_layers(CTORequest, GetStatus, pid=0xFD) +bind_layers(CTORequest, Synch, pid=0xFC) +bind_layers(CTORequest, GetCommModeInfo, pid=0xFB) +bind_layers(CTORequest, GetId, pid=0xFA) +bind_layers(CTORequest, SetRequest, pid=0xF9) +bind_layers(CTORequest, GetSeed, pid=0xF8) +bind_layers(CTORequest, Unlock, pid=0xF7) +bind_layers(CTORequest, SetMta, pid=0xF6) +bind_layers(CTORequest, Upload, pid=0xF5) +bind_layers(CTORequest, ShortUpload, pid=0xF4) +bind_layers(CTORequest, BuildChecksum, pid=0xF3) +bind_layers(CTORequest, TransportLayerCmd, pid=0xF2) +bind_layers(CTORequest, TransportLayerCmdGetSlaveId, pid=0xF2, + sub_command_code=0xFF) +bind_layers(CTORequest, TransportLayerCmdGetDAQId, pid=0xF2, + sub_command_code=0xFE) +bind_layers(CTORequest, TransportLayerCmdSetDAQId, pid=0xF2, + sub_command_code=0xFD) +bind_layers(CTORequest, UserCmd, pid=0xF1) + +# Calibration Commands +bind_layers(CTORequest, Download, pid=0xF0) +bind_layers(CTORequest, DownloadNext, pid=0xEF) +bind_layers(CTORequest, DownloadMax, pid=0xEE) +bind_layers(CTORequest, ShortDownload, pid=0xED) +bind_layers(CTORequest, ModifyBits, pid=0xEC) + +# Page Switching commands +bind_layers(CTORequest, SetCalPage, pid=0xEB) +bind_layers(CTORequest, GetCalPage, pid=0xEA) +bind_layers(CTORequest, GetPagProcessorInfo, pid=0xE9) +bind_layers(CTORequest, GetSegmentInfo, pid=0xE8) +bind_layers(CTORequest, GetPageInfo, pid=0xE7) +bind_layers(CTORequest, SetSegmentMode, pid=0xE6) +bind_layers(CTORequest, GetSegmentMode, pid=0xE5) +bind_layers(CTORequest, CopyCalPage, pid=0xE4) + +# Cyclic Data exchange Basic commands +bind_layers(CTORequest, SetDaqPtr, pid=0xE2) +bind_layers(CTORequest, WriteDaq, pid=0xE1) +bind_layers(CTORequest, SetDaqListMode, pid=0xE0) +bind_layers(CTORequest, GetDaqListMode, pid=0xDF) +bind_layers(CTORequest, StartStopDaqList, pid=0xDE) +bind_layers(CTORequest, StartStopSynch, pid=0xDD) +bind_layers(CTORequest, ReadDaq, pid=0xDB) +bind_layers(CTORequest, GetDaqClock, pid=0xDC) +bind_layers(CTORequest, GetDaqProcessorInfo, pid=0xDA) +bind_layers(CTORequest, GetDaqResolutionInfo, pid=0xD9) +bind_layers(CTORequest, GetDaqListInfo, pid=0xD8) +bind_layers(CTORequest, GetDaqEventInfo, pid=0xD7) + +# Cyclic data transfer - static configuration commands +bind_layers(CTORequest, ClearDaqList, pid=0xE3) + +# Cyclic Data transfer - dynamic configuration commands +bind_layers(CTORequest, FreeDaq, pid=0xD6) +bind_layers(CTORequest, AllocDaq, pid=0xD5) +bind_layers(CTORequest, AllocOdt, pid=0xD4) +bind_layers(CTORequest, AllocOdtEntry, pid=0xD3) + +# Flash Programming commands +bind_layers(CTORequest, ProgramStart, pid=0xD2) +bind_layers(CTORequest, ProgramClear, pid=0xD1) +bind_layers(CTORequest, Program, pid=0xD0) +bind_layers(CTORequest, ProgramReset, pid=0xCF) +bind_layers(CTORequest, GetPgmProcessorInfo, pid=0xCE) +bind_layers(CTORequest, GetSectorInfo, pid=0xCD) +bind_layers(CTORequest, ProgramPrepare, pid=0xCC) +bind_layers(CTORequest, ProgramFormat, pid=0xCB) +bind_layers(CTORequest, ProgramNext, pid=0xCA) +bind_layers(CTORequest, ProgramMax, pid=0xC9) +bind_layers(CTORequest, ProgramVerify, pid=0xC8) + + +# ##### DTOs ##### +# Master -> Slave: STIM (Stimulation) +# Slave -> Master: DAQ (Data AcQuisition) +class DTO(Packet): + name = "Data transfer object" + fields_desc = [ + ConditionalField(XByteField("fill", 0x00), + lambda _: identification_field_needs_alignment()), + ConditionalField( + StrLenField("daq", b"", length_from=lambda _: get_daq_length()), + lambda _: get_daq_length() > 0), + ConditionalField( + StrLenField("timestamp", b"", + length_from=lambda _: get_timestamp_length()), + lambda _: get_timestamp_length() > 0), + ConditionalField( + StrLenField("data", b"", + length_from=lambda _: get_daq_data_field_length()), + lambda _: get_daq_data_field_length() > 0) + ] + + +for pid in range(0, 0xBF + 1): + bind_layers(CTORequest, DTO, pid=pid) + + +class CTOResponse(Packet): + packet_codes = { + 0xFF: "RES", + 0xFE: "ERR", + 0xFD: "EV", + 0xFC: "SERV", + } + name = "Command Transfer Object Response" + + fields_desc = [ + ByteEnumField("packet_code", 0xFF, packet_codes), + ] + + @staticmethod + def get_positive_response_cls(request): + # The pid of the request this packet is the response for + request_pid = request.pid + # First check the special cases with sub commands + # They can't be fit in a simple dictionary, + # so deal with them separately + if request_pid == 0xF2: + if request.sub_command_code == 255: + return TransportLayerCmdGetSlaveIdResponse + if request.sub_command_code == 254: + return TransportLayerCmdGetDAQIdResponse + if request_pid == 0xE8: + if request.mode == "get_basic_address_info": + return SegmentInfoMode0PositiveResponse + if request.mode == "get_standard_info": + return SegmentInfoMode1PositiveResponse + if request.mode == "get_address_mapping_info": + return SegmentInfoMode2PositiveResponse + return {0xFF: ConnectPositiveResponse, + 0xFD: StatusPositiveResponse, + 0xFB: CommonModeInfoPositiveResponse, + 0xFA: IdPositiveResponse, + 0xF8: SeedPositiveResponse, + 0xF7: UnlockPositiveResponse, + 0xF5: UploadPositiveResponse, + 0xF4: ShortUploadPositiveResponse, + 0xF3: ChecksumPositiveResponse, + 0xEA: CalPagePositiveResponse, + 0xE9: PagProcessorInfoPositiveResponse, + 0xE7: PageInfoPositiveResponse, + 0xE5: SegmentModePositiveResponse, + 0xDF: DAQListModePositiveResponse, + 0xDE: StartStopDAQListPositiveResponse, + 0xDC: DAQClockListPositiveResponse, + 0xDB: ReadDAQPositiveResponse, + 0xDA: DAQProcessorInfoPositiveResponse, + 0xD9: DAQResolutionInfoPositiveResponse, + 0xD8: DAQListInfoPositiveResponse, + 0xD7: DAQEventInfoPositiveResponse, + 0xD2: ProgramStartPositiveResponse, + 0xCE: PgmProcessorPositiveResponse, + 0xCD: SectorInfoPositiveResponse, + }.get(request_pid, GenericResponse) + + def answers(self, request): + """In XCP, the payload of a response packet is dependent on the pid + field of the corresponding request. + This method changes the class of the payload to the class + which is expected for the given request.""" + if not isinstance(request, CTORequest): + return False + + # FE: Negative Response + # FD: Event Packet + # FC: Service Packet + # They are always a valid response + if self.packet_code in [0xFE, 0xFD, 0xFC]: + return True + # FF: Positive Response + if self.packet_code != 0xFF: + return False + + payload_cls = self.get_positive_response_cls(request) + + minimum_expected_byte_count = len(payload_cls()) + given_byte_count = len(self.payload) + + if given_byte_count < minimum_expected_byte_count: + return False + + # Even if there are enough bytes, we can't be sure that they align + # correctly to the fields. Then a struct.error exception is thrown. + # For example + # Fields: byte, byte, short + # Packet: 01 02 03 + # This would fail because there are enough bytes that scapy starts + # to parse the short field, but there are actually not enough bytes + # to fill it. + try: + data = bytes(self.payload) + self.remove_payload() + self.add_payload(payload_cls(data)) + except struct.error: + return False + return True + + +for pid in range(0, 0xFB + 1): + bind_layers(CTOResponse, DTO, pid=pid) + +positive_response_classes = [ConnectPositiveResponse, + StatusPositiveResponse, + CommonModeInfoPositiveResponse, + IdPositiveResponse, + SeedPositiveResponse, + UnlockPositiveResponse, + UploadPositiveResponse, + ShortUploadPositiveResponse, + ChecksumPositiveResponse, + CalPagePositiveResponse, + PagProcessorInfoPositiveResponse, + PageInfoPositiveResponse, + SegmentModePositiveResponse, + DAQListModePositiveResponse, + StartStopDAQListPositiveResponse, + DAQClockListPositiveResponse, + ReadDAQPositiveResponse, + DAQProcessorInfoPositiveResponse, + DAQResolutionInfoPositiveResponse, + DAQListInfoPositiveResponse, + DAQEventInfoPositiveResponse, + ProgramStartPositiveResponse, + PgmProcessorPositiveResponse, + SectorInfoPositiveResponse] + +for cls in positive_response_classes: + bind_top_down(CTOResponse, cls, packet_code=0xFF) + +bind_layers(CTOResponse, NegativeResponse, packet_code=0xFE) + +# Asynchronous Event/request messages from the slave +bind_layers(CTOResponse, EvPacket, packet_code=0xFD) +bind_layers(CTOResponse, ServPacket, packet_code=0xFC) + +bind_bottom_up(XCPOnCAN, CTOResponse) +bind_bottom_up(XCPOnUDP, CTOResponse) +bind_bottom_up(XCPOnTCP, CTOResponse) diff --git a/scapy/contrib/avs.py b/scapy/contrib/avs.py index de3ead56ce6..c7598cf954f 100644 --- a/scapy/contrib/avs.py +++ b/scapy/contrib/avs.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = AVS WLAN Monitor Header # scapy.contrib.status = loads diff --git a/scapy/contrib/bfd.py b/scapy/contrib/bfd.py new file mode 100644 index 00000000000..1c694d28dcf --- /dev/null +++ b/scapy/contrib/bfd.py @@ -0,0 +1,154 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Parag Bhide + +""" +BFD - Bidirectional Forwarding Detection - RFC 5880, 5881, 7130, 7881 +""" + +# scapy.contrib.description = BFD +# scapy.contrib.status = loads + + +from scapy.packet import Packet, bind_layers, bind_bottom_up +from scapy.fields import ( + BitField, + BitEnumField, + ByteEnumField, + XNBytesField, + XByteField, + MultipleTypeField, + IntField, + FieldLenField, + FlagsField, + ByteField, + PacketField, + ConditionalField, + StrFixedLenField, +) +from scapy.layers.inet import UDP + +_sta_names = { + 0: "AdminDown", + 1: "Down", + 2: "Init", + 3: "Up", +} + + +# https://www.iana.org/assignments/bfd-parameters/bfd-parameters.xhtml +_diagnostics = { + 0: "No Diagnostic", + 1: "Control Detection Time Expired", + 2: "Echo Function Failed", + 3: "Neighbor Signaled Session Down", + 4: "Forwarding Plane Reset", + 5: "Path Down", + 6: "Concatenated Path Down", + 7: "Administratively Down", + 8: "Reverse Concatenated Path Down", + 9: "Mis-Connectivity Defect", +} + + +# https://www.rfc-editor.org/rfc/rfc5880 [Page 10] +_authentification_type = { + 0: "Reserved", + 1: "Simple Password", + 2: "Keyed MD5", + 3: "Meticulous Keyed MD5", + 4: "Keyed SHA1", + 5: "Meticulous Keyed SHA1", +} + + +class OptionalAuth(Packet): + name = "Optional Auth" + fields_desc = [ + ByteEnumField("auth_type", 1, _authentification_type), + FieldLenField( + "auth_len", + None, + fmt="B", + length_of="auth_key", + adjust=lambda pkt, x: x + 3 if pkt.auth_type <= 1 else x + 8, + ), + ByteField("auth_keyid", 1), + ConditionalField( + XByteField("reserved", 0), + lambda pkt: pkt.auth_type > 1, + ), + ConditionalField( + IntField("sequence_number", 0), + lambda pkt: pkt.auth_type > 1, + ), + MultipleTypeField( + [ + ( + StrFixedLenField( + "auth_key", "", length_from=lambda pkt: pkt.auth_len + ), + lambda pkt: pkt.auth_type == 0, + ), + ( + XNBytesField("auth_key", 0x5F4DCC3B5AA765D61D8327DEB882CF99, 16), + lambda pkt: pkt.auth_type == 2 or pkt.auth_type == 3, + ), + ( + XNBytesField( + "auth_key", 0x5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8, 20 + ), + lambda pkt: pkt.auth_type == 4 or pkt.auth_type == 5, + ), + ], + StrFixedLenField( + "auth_key", "password", length_from=lambda pkt: pkt.auth_len + ), + ), + ] + + +class BFD(Packet): + name = "BFD" + fields_desc = [ + BitField("version", 1, 3), + BitEnumField("diag", 0, 5, _diagnostics), + BitEnumField("sta", 3, 2, _sta_names), + FlagsField("flags", 0, 6, "MDACFP"), + ByteField("detect_mult", 3), + FieldLenField( + "len", + None, + fmt="B", + length_of="optional_auth", + adjust=lambda pkt, x: x + 24, + ), + BitField("my_discriminator", 0x11111111, 32), + BitField("your_discriminator", 0x22222222, 32), + BitField("min_tx_interval", 1000000000, 32), + BitField("min_rx_interval", 1000000000, 32), + BitField("echo_rx_interval", 1000000000, 32), + ConditionalField( + PacketField("optional_auth", None, OptionalAuth), + lambda pkt: pkt.flags.A, + ), + ] + + def mysummary(self): + return self.sprintf( + "BFD (my_disc=%BFD.my_discriminator%," + "your_disc=%BFD.your_discriminator%," + "state=%BFD.sta%)" + ) + + +for _bfd_port in [ + 3784, # single-hop BFD + 4784, # multi-hop BFD + 6784, # BFD for LAG a.k.a micro-BFD + 7784, # seamless BFD +]: + bind_bottom_up(UDP, BFD, dport=_bfd_port) + bind_bottom_up(UDP, BFD, sport=_bfd_port) + bind_layers(UDP, BFD, dport=_bfd_port, sport=_bfd_port) diff --git a/scapy/contrib/bgp.py b/scapy/contrib/bgp.py index b2bb75a3ff2..0060e5f9f32 100644 --- a/scapy/contrib/bgp.py +++ b/scapy/contrib/bgp.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = BGP v0.1 # scapy.contrib.status = loads @@ -19,7 +9,6 @@ BGP (Border Gateway Protocol). """ -from __future__ import absolute_import import struct import re import socket @@ -35,11 +24,9 @@ MultiEnumField) from scapy.layers.inet import TCP from scapy.layers.inet6 import IP6Field -from scapy.utils import issubtype from scapy.config import conf, ConfClass from scapy.compat import orb, chb from scapy.error import log_runtime -import scapy.modules.six as six # @@ -123,7 +110,7 @@ def i2repr(self, pkt, i): return self.i2h(pkt, i) def i2len(self, pkt, i): - mask, ip = i + mask, _ = i return self.mask2iplen(mask) + 1 def i2m(self, pkt, i): @@ -199,56 +186,122 @@ def has_extended_length(flags): return flags & _BGP_PA_EXTENDED_LENGTH == _BGP_PA_EXTENDED_LENGTH +def detect_add_path_prefix46(s, max_bit_length): + """ + Detect IPv4/IPv6 prefixes conform to BGP Additional Path but NOT conform + to standard BGP.. + + This is an adapted version of wireshark's detect_add_path_prefix46 + https://github.com/wireshark/wireshark/blob/ed9e958a2ed506220fdab320738f1f96a3c2ffbb/epan/dissectors/packet-bgp.c#L2905 + Kudos to them ! + """ + # Must be compatible with BGP Additional Path + i = 0 + while i + 4 < len(s): + i += 4 + prefix_len = orb(s[i]) + if prefix_len > max_bit_length: + return False + addr_len = (prefix_len + 7) // 8 + i += 1 + addr_len + if i > len(s): + return False + if prefix_len % 8: + if orb(s[i - 1]) & (0xFF >> (prefix_len % 8)): + return False + # Must NOT be compatible with standard BGP + i = 0 + while i + 4 < len(s): + prefix_len = orb(s[i]) + if prefix_len == 0 and len(s) > 1: + return True + if prefix_len > max_bit_length: + return True + addr_len = (prefix_len + 7) // 8 + i += 1 + addr_len + if i > len(s): + return True + if prefix_len % 8: + if orb(s[i - 1]) & (0xFF >> (prefix_len % 8)): + return True + return False + + class BGPNLRI_IPv4(Packet): """ Packet handling IPv4 NLRI fields. """ - name = "IPv4 NLRI" fields_desc = [BGPFieldIPv4("prefix", "0.0.0.0/0")] + def default_payload_class(self, payload): + return conf.padding_layer + class BGPNLRI_IPv6(Packet): """ Packet handling IPv6 NLRI fields. """ - name = "IPv6 NLRI" fields_desc = [BGPFieldIPv6("prefix", "::/0")] + def default_payload_class(self, payload): + return conf.padding_layer + + +class BGPNLRI_IPv4_AP(BGPNLRI_IPv4): + """ + Packet handling IPv4 NLRI fields WITH BGP ADDITIONAL PATH + """ + + name = "IPv4 NLRI (Additional Path)" + fields_desc = [IntField("nlri_path_id", 0), + BGPFieldIPv4("prefix", "0.0.0.0/0")] + + +class BGPNLRI_IPv6_AP(BGPNLRI_IPv6): + """ + Packet handling IPv6 NLRI fields WITH BGP ADDITIONAL PATH + """ + + name = "IPv6 NLRI (Additional Path)" + fields_desc = [IntField("nlri_path_id", 0), + BGPFieldIPv6("prefix", "::/0")] + class BGPNLRIPacketListField(PacketListField): """ PacketListField handling NLRI fields. """ + __slots__ = ["max_bit_length", "cls_group", "no_length"] - def getfield(self, pkt, s): - lst = [] - length = None - ret = b"" - - if self.length_from is not None: - length = self.length_from(pkt) + def __init__(self, name, default, ip_mode, **kwargs): + super(BGPNLRIPacketListField, self).__init__( + name, default, Packet, **kwargs + ) + self.max_bit_length, self.cls_group = { + "IPv4": (32, [BGPNLRI_IPv4, BGPNLRI_IPv4_AP]), + "IPv6": (128, [BGPNLRI_IPv6, BGPNLRI_IPv6_AP]), + }[ip_mode] + self.no_length = "length_from" not in kwargs - if length is not None: - remain, ret = s[:length], s[length:] - else: + def getfield(self, pkt, s): + if self.no_length: index = s.find(_BGP_HEADER_MARKER) + if index == 0: + return s, [] if index != -1: - remain = s[:index] - ret = s[index:] - else: - remain = s - - while remain: - mask_length_in_bits = orb(remain[0]) - mask_length_in_bytes = (mask_length_in_bits + 7) // 8 - current = remain[:mask_length_in_bytes + 1] - remain = remain[mask_length_in_bytes + 1:] - packet = self.m2i(pkt, current) - lst.append(packet) + self.length_from = lambda pkt: index + remain = s[:self.length_from(pkt)] if self.length_from else s - return remain + ret, lst + cls = self.cls_group[ + detect_add_path_prefix46(remain, self.max_bit_length) + ] + self.next_cls_cb = lambda *args: cls + res = super(BGPNLRIPacketListField, self).getfield(pkt, s) + if self.no_length: + self.length_from = None + return res class _BGPInvalidDataException(Exception): @@ -408,6 +461,17 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return _bgp_dispatcher(_pkt) + @classmethod + def tcp_reassemble(cls, data, *args, **kwargs): + if len(data) < 18: + return None + if data[:16] == _BGP_HEADER_MARKER: + length = struct.unpack("!H", data[16:18])[0] + if len(data) >= length: + return cls(data[:length]) / conf.padding_layer(data[length:]) + else: + return cls(data) + def post_build(self, p, pay): if self.len is None: length = len(p) @@ -467,6 +531,8 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return _bgp_dispatcher(_pkt) + tcp_reassemble = BGPHeader.tcp_reassemble + def guess_payload_class(self, p): cls = None if len(p) > 15 and p[:16] == _BGP_HEADER_MARKER: @@ -576,11 +642,11 @@ def __new__(cls, clsname, bases, attrs): return newclass -class _BGPCapability_metaclass(Packet_metaclass, _BGPCap_metaclass): +class _BGPCapability_metaclass(_BGPCap_metaclass, Packet_metaclass): pass -class BGPCapability(six.with_metaclass(_BGPCapability_metaclass, Packet)): +class BGPCapability(Packet, metaclass=_BGPCapability_metaclass): """ Generic BGP capability. """ @@ -604,21 +670,6 @@ def pre_dissect(self, s): raise _BGPInvalidDataException(err) return s - # Every BGP capability object inherits from BGPCapability. - def haslayer(self, cls): - if cls == "BGPCapability": - if isinstance(self, BGPCapability): - return True - elif issubtype(cls, BGPCapability): - if isinstance(self, cls): - return True - return super(BGPCapability, self).haslayer(cls) - - def getlayer(self, cls, nb=1, _track=None, _subclass=True, **flt): - return super(BGPCapability, self).getlayer( - cls, nb=nb, _track=_track, _subclass=True, **flt - ) - def post_build(self, p, pay): length = 0 if self.length is None: @@ -635,6 +686,7 @@ class BGPCapGeneric(BGPCapability): """ name = "BGP Capability" + match_subclass = True fields_desc = [ ByteEnumField("code", 0, _capabilities), FieldLenField("length", None, fmt="B", length_of="cap_data"), @@ -655,6 +707,7 @@ class BGPCapMultiprotocol(BGPCapability): """ name = "Multiprotocol Extensions for BGP-4" + match_subclass = True fields_desc = [ ByteEnumField("code", 1, _capabilities), ByteField("length", 4), @@ -715,7 +768,8 @@ class ORFTuple(Packet): "entries", [], ORFTuple, - count_from=lambda p: p.orf_number + count_from=lambda p: p.orf_number, + max_count=20000, ) ] @@ -763,6 +817,7 @@ class BGPCapORF(BGPCapability): """ name = "Outbound Route Filtering Capability" + match_subclass = True fields_desc = [ ByteEnumField("code", 3, _capabilities), ByteField("length", None), @@ -770,7 +825,8 @@ class BGPCapORF(BGPCapability): "orf", [], BGPCapORFBlock, - length_from=lambda p: p.length + length_from=lambda p: p.length, + max_count=20000, ) ] @@ -800,6 +856,7 @@ class GRTuple(Packet): ByteEnumField("flags", 0, gr_address_family_flags)] name = "Graceful Restart Capability" + match_subclass = True fields_desc = [ByteEnumField("code", 64, _capabilities), ByteField("length", None), BitField("restart_flags", 0, 4), @@ -819,6 +876,7 @@ class BGPCapFourBytesASN(BGPCapability): """ name = "Support for 4-octet AS number capability" + match_subclass = True fields_desc = [ByteEnumField("code", 65, _capabilities), ByteField("length", 4), IntField("asn", 0)] @@ -999,6 +1057,7 @@ def post_build(self, p, pay): 27: "PE Distinguisher Labels", # RFC 6514 28: "BGP Entropy Label Capability Attribute (deprecated)", # RFC 6790, RFC 7447 # noqa: E501 29: "BGP-LS Attribute", # RFC 7752 + 32: "LARGE_COMMUNITY", # RFC 8092, RFC 8195 40: "BGP Prefix-SID", # (TEMPORARY - registered 2015-09-30, expires 2016-09-30) # noqa: E501 # draft-ietf-idr-bgp-prefix-sid 128: "ATTR_SET", # RFC 6368 @@ -1036,6 +1095,7 @@ def post_build(self, p, pay): 27: 0xc0, # PE Distinguisher Labels (RFC 6514) 28: 0xc0, # BGP Entropy Label Capability Attribute 29: 0x80, # BGP-LS Attribute + 32: 0xc0, # LARGE_COMMUNITY 40: 0xc0, # BGP Prefix-SID 128: 0xc0 # ATTR_SET (RFC 6368) } @@ -1661,8 +1721,8 @@ class _ExtCommValuePacketField(PacketField): __slots__ = ["type_from"] - def __init__(self, name, default, cls, remain=0, type_from=(0, 0)): - PacketField.__init__(self, name, default, cls, remain) + def __init__(self, name, default, cls, type_from=(0, 0)): + PacketField.__init__(self, name, default, cls) self.type_from = type_from def m2i(self, pkt, m): @@ -1696,6 +1756,9 @@ def m2i(self, pkt, m): elif type_low == 0x09: ret = BGPPAExtCommTrafficMarking(m) + else: + ret = conf.raw_layer(m) + elif type_high == 0x81: # FlowSpec if type_low == 0x08: @@ -1835,7 +1898,7 @@ def getfield(self, pkt, s): length_in_bytes = (mask + 7) // 8 current = remain[:length_in_bytes + 1] remain = remain[length_in_bytes + 1:] - prefix = BGPNLRI_IPv6(current) + prefix = self.m2i(pkt, current) lst.append(prefix) return remain, lst @@ -1862,7 +1925,8 @@ class BGPPAMPReachNLRI(Packet): ConditionalField(IP6Field("nh_v6_link_local", "::"), lambda x: x.afi == 2 and x.nh_addr_len == 32), ByteField("reserved", 0), - MPReachNLRIPacketListField("nlri", [], Packet)] + MPReachNLRIPacketListField("nlri", [], BGPNLRI_IPv6, + max_count=20000)] def post_build(self, p, pay): if self.nlri is None: @@ -1881,8 +1945,7 @@ class BGPPAMPUnreachNLRI_IPv6(Packet): """ name = "MP_UNREACH_NLRI (IPv6 NLRI)" - fields_desc = [BGPNLRIPacketListField( - "withdrawn_routes", [], BGPNLRI_IPv6)] + fields_desc = [BGPNLRIPacketListField("withdrawn_routes", [], "IPv6")] class MPUnreachNLRIPacketField(PacketField): @@ -1950,10 +2013,37 @@ def post_build(self, p, pay): return p + pay +# +# LARGE_COMMUNITY +# + +class BGPLargeCommunitySegment(Packet): + """ + Provides an implementation for LARGE_COMMUNITY segments + which holds 3*4 bytes integers. + """ + + fields_desc = [ + IntField("global_administrator", None), + IntField("local_data_part1", None), + IntField("local_data_part2", None) + ] + + +class BGPPALargeCommunity(Packet): + """ + Provides an implementation of the LARGE_COMMUNITY attribute. + References: RFC 8092, RFC 8195 + """ + + name = "LARGE_COMMUNITY" + fields_desc = [PacketListField("segments", [], BGPLargeCommunitySegment)] + # # AS4_AGGREGATOR # + class BGPPAAS4Aggregator(Packet): """ Provides an implementation of the AS4_AGGREGATOR attribute @@ -1981,7 +2071,8 @@ class BGPPAAS4Aggregator(Packet): 0x0F: "BGPPAMPUnreachNLRI", 0x10: "BGPPAExtComms", 0x11: "BGPPAAS4Path", - 0x19: "BGPPAIPv6AddressSpecificExtComm" + 0x19: "BGPPAIPv6AddressSpecificExtComm", + 0x20: "BGPPALargeCommunity" } @@ -1998,7 +2089,7 @@ def m2i(self, pkt, m): if type_code == 0 or type_code == 255: ret = conf.raw_layer(m) # Unassigned - elif (type_code >= 30 and type_code <= 39) or\ + elif (type_code >= 33 and type_code <= 39) or\ (type_code >= 41 and type_code <= 127) or\ (type_code >= 129 and type_code <= 254): ret = conf.raw_layer(m) @@ -2006,6 +2097,8 @@ def m2i(self, pkt, m): else: if type_code == 0x02 and not bgp_module_conf.use_2_bytes_asn: ret = BGPPAAS4BytesPath(m) + elif type_code == 0x20: + ret = BGPPALargeCommunity(m) else: ret = _get_cls( _path_attr_objects.get(type_code, conf.raw_layer))(m) @@ -2124,7 +2217,7 @@ class BGPUpdate(BGP): BGPNLRIPacketListField( "withdrawn_routes", [], - BGPNLRI_IPv4, + "IPv4", length_from=lambda p: p.withdrawn_routes_len ), FieldLenField( @@ -2139,7 +2232,8 @@ class BGPUpdate(BGP): BGPPathAttr, length_from=lambda p: p.path_attr_len ), - BGPNLRIPacketListField("nlri", [], BGPNLRI_IPv4) + BGPNLRIPacketListField("nlri", [], "IPv4", + max_count=20000) ] def post_build(self, p, pay): @@ -2321,7 +2415,7 @@ class BGPORFEntry(Packet): Provides an implementation of an ORF entry. References: RFC 5291 """ - + __slots__ = ["afi", "safi"] name = "ORF entry" fields_desc = [ BitEnumField("action", 0, 2, _orf_actions), @@ -2330,6 +2424,11 @@ class BGPORFEntry(Packet): StrField("value", "") ] + def __init__(self, *args, **kwargs): + self.afi = kwargs.pop("afi", 1) + self.safi = kwargs.pop("safi", 1) + super(BGPORFEntry, self).__init__(*args, **kwargs) + class _ORFNLRIPacketField(PacketField): """ @@ -2339,11 +2438,11 @@ class _ORFNLRIPacketField(PacketField): def m2i(self, pkt, m): ret = None - if _orf_entry_afi == 1: + if pkt.afi == 1: # IPv4 ret = BGPNLRI_IPv4(m) - elif _orf_entry_afi == 2: + elif pkt.afi == 2: # IPv6 ret = BGPNLRI_IPv6(m) @@ -2357,7 +2456,6 @@ class BGPORFAddressPrefix(BGPORFEntry): """ Provides an implementation of the Address Prefix ORF (RFC 5292). """ - name = "Address Prefix ORF" fields_desc = [ BitEnumField("action", 0, 2, _orf_actions), @@ -2370,11 +2468,10 @@ class BGPORFAddressPrefix(BGPORFEntry): ] -class BGPORFCoveringPrefix(Packet): +class BGPORFCoveringPrefix(BGPORFEntry): """ Provides an implementation of the CP-ORF (RFC 7543). """ - name = "CP-ORF" fields_desc = [ BitEnumField("action", 0, 2, _orf_actions), @@ -2398,12 +2495,19 @@ class BGPORFEntryPacketListField(PacketListField): def m2i(self, pkt, m): ret = None + if isinstance(pkt.underlayer, BGPRouteRefresh): + afi = pkt.underlayer.afi + safi = pkt.underlayer.safi + else: + afi = 1 + safi = 1 + # Cisco also uses 128 if pkt.orf_type == 64 or pkt.orf_type == 128: - ret = BGPORFAddressPrefix(m) + ret = BGPORFAddressPrefix(m, afi=afi, safi=safi) elif pkt.orf_type == 65: - ret = BGPORFCoveringPrefix(m) + ret = BGPORFCoveringPrefix(m, afi=afi, safi=safi) else: ret = conf.raw_layer(m) @@ -2417,11 +2521,13 @@ def getfield(self, pkt, s): if self.length_from is not None: length = self.length_from(pkt) remain = s + if length <= 0: + return s, [] if length is not None: remain, ret = s[:length], s[length:] while remain: - orf_len = 0 + orf_len = length # Get value length, depending on the ORF type if pkt.orf_type == 64 or pkt.orf_type == 128: @@ -2437,19 +2543,19 @@ def getfield(self, pkt, s): elif pkt.orf_type == 65: # Covering Prefix ORF - if _orf_entry_afi == 1: + if pkt.afi == 1: # IPv4 # sequence (4 bytes) + min_len (1 byte) + max_len (1 byte) + # noqa: E501 # rt (8 bytes) + import_rt (8 bytes) + route_type (1 byte) orf_len = 23 + 4 - elif _orf_entry_afi == 2: + elif pkt.afi == 2: # IPv6 # sequence (4 bytes) + min_len (1 byte) + max_len (1 byte) + # noqa: E501 # rt (8 bytes) + import_rt (8 bytes) + route_type (1 byte) orf_len = 23 + 16 - elif _orf_entry_afi == 25: + elif pkt.afi == 25: # sequence (4 bytes) + min_len (1 byte) + max_len (1 byte) + # noqa: E501 # rt (8 bytes) + import_rt (8 bytes) route_type = orb(remain[22]) @@ -2485,6 +2591,7 @@ class BGPORF(Packet): [], Packet, length_from=lambda p: p.orf_len, + max_count=20000, ) ] @@ -2510,10 +2617,11 @@ class BGPRouteRefresh(BGP): ShortEnumField("afi", 1, address_family_identifiers), ByteEnumField("subtype", 0, rr_message_subtypes), ByteEnumField("safi", 1, subsequent_afis), - PacketField( - 'orf_data', - "", BGPORF, - lambda p: _update_orf_afi_safi(p.afi, p.safi) + ConditionalField( + PacketField('orf_data', "", BGPORF), + lambda p: ( + (p.underlayer and p.underlayer.len or 24) > 23 + ) ) ] diff --git a/scapy/contrib/bier.py b/scapy/contrib/bier.py index 1a0260cede0..b6bf9122190 100644 --- a/scapy/contrib/bier.py +++ b/scapy/contrib/bier.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Bit Index Explicit Replication (BIER) # scapy.contrib.status = loads diff --git a/scapy/contrib/bp.py b/scapy/contrib/bp.py index c69449ec41e..37ed86ac938 100644 --- a/scapy/contrib/bp.py +++ b/scapy/contrib/bp.py @@ -1,26 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright (C) 2012 The MITRE Corporation """ - Copyright 2012, The MITRE Corporation:: - - NOTICE +.. centered:: + NOTICE This software/technical data was produced for the U.S. Government under Prime Contract No. NASA-03001 and JPL Contract No. 1295026 - and is subject to FAR 52.227-14 (6/87) Rights in Data General, - and Article GP-51, Rights in Data General, respectively. - This software is publicly released under MITRE case #12-3054 + and is subject to FAR 52.227-14 (6/87) Rights in Data General, + and Article GP-51, Rights in Data General, respectively. + This software is publicly released under MITRE case #12-3054 """ # scapy.contrib.description = Bundle Protocol (BP) diff --git a/scapy/contrib/cansocket.py b/scapy/contrib/cansocket.py index 4de13f8f30f..a50d2352c76 100644 --- a/scapy/contrib/cansocket.py +++ b/scapy/contrib/cansocket.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.description = CANSocket Utils # scapy.contrib.status = loads @@ -13,7 +13,6 @@ from scapy.error import log_loading from scapy.consts import LINUX from scapy.config import conf -import scapy.modules.six as six PYTHON_CAN = False @@ -21,8 +20,6 @@ if conf.contribs['CANSocket']['use-python-can']: from can import BusABC as can_BusABC # noqa: F401 PYTHON_CAN = True - else: - PYTHON_CAN = False except ImportError: log_loading.info("Can't import python-can.") except KeyError: @@ -30,18 +27,12 @@ if PYTHON_CAN: - log_loading.info("Using python-can CANSocket.") - log_loading.info("Specify 'conf.contribs['CANSocket'] = " - "{'use-python-can': False}' to enable native CANSockets.") - from scapy.contrib.cansocket_python_can import (PythonCANSocket, CANSocket, CAN_FRAME_SIZE, CAN_INV_FILTER) # noqa: E501 F401 - -elif LINUX and six.PY3 and not conf.use_pypy: - log_loading.info("Using native CANSocket.") - log_loading.info("Specify 'conf.contribs['CANSocket'] = " - "{'use-python-can': True}' " - "to enable python-can CANSockets.") - from scapy.contrib.cansocket_native import (NativeCANSocket, CANSocket, CAN_FRAME_SIZE, CAN_INV_FILTER) # noqa: E501 F401 + log_loading.info("Using python-can CANSockets.\nSpecify 'conf.contribs['CANSocket'] = {'use-python-can': False}' to enable native CANSockets.") # noqa: E501 + from scapy.contrib.cansocket_python_can import (PythonCANSocket, CANSocket) # noqa: E501 F401 + +elif LINUX and not conf.use_pypy: + log_loading.info("Using native CANSockets.\nSpecify 'conf.contribs['CANSocket'] = {'use-python-can': True}' to enable python-can CANSockets.") # noqa: E501 + from scapy.contrib.cansocket_native import (NativeCANSocket, CANSocket) # noqa: E501 F401 else: - log_loading.info("No CAN support available. Install python-can or " - "use Linux and python3.") + log_loading.info("No CAN support available. Install python-can or use Linux and python3.") # noqa: E501 diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 9be0652ec4c..49efacd457c 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license # scapy.contrib.description = Native CANSocket # scapy.contrib.status = loads @@ -13,34 +13,63 @@ import struct import socket import time + from scapy.config import conf +from scapy.data import SO_TIMESTAMPNS from scapy.supersocket import SuperSocket -from scapy.error import Scapy_Exception, warning -from scapy.layers.can import CAN -from scapy.packet import Padding -from scapy.arch.linux import get_last_packet_timestamp +from scapy.error import Scapy_Exception, warning, log_runtime +from scapy.packet import Packet +from scapy.layers.can import CAN, CAN_MTU, CAN_FD_MTU +from scapy.compat import raw + +from typing import ( + List, + Dict, + Type, + Any, + Optional, + Tuple, + cast, +) conf.contribs['NativeCANSocket'] = {'channel': "can0"} -CAN_FRAME_SIZE = 16 -CAN_INV_FILTER = 0x20000000 - class NativeCANSocket(SuperSocket): + """Initializes a Linux PF_CAN socket object. + + Example: + >>> socket = NativeCANSocket(channel="vcan0", can_filters=[{'can_id': 0x200, 'can_mask': 0x7FF}]) + + :param channel: Network interface name + :param receive_own_messages: Messages, sent by this socket are will + also be received. + :param can_filters: A list of can filter dictionaries. + :param basecls: Packet type in which received data gets interpreted. + :param kwargs: Various keyword arguments for compatibility with + PythonCANSockets + """ # noqa: E501 desc = "read/write packets at a given CAN interface using PF_CAN sockets" - nonblocking_socket = True - def __init__(self, channel=None, receive_own_messages=False, - can_filters=None, remove_padding=True, basecls=CAN, **kwargs): - bustype = kwargs.pop("bustype", None) + def __init__(self, + channel=None, # type: Optional[str] + receive_own_messages=False, # type: bool + can_filters=None, # type: Optional[List[Dict[str, int]]] + fd=False, # type: bool + basecls=CAN, # type: Type[Packet] + **kwargs # type: Dict[str, Any] + ): + # type: (...) -> None + bustype = cast(Optional[str], kwargs.pop("bustype", None)) if bustype and bustype != "socketcan": warning("You created a NativeCANSocket. " "If you're providing the argument 'bustype', please use " "the correct one to achieve compatibility with python-can" "/PythonCANSocket. \n'bustype=socketcan'") + self.MTU = CAN_MTU + self.fd = fd self.basecls = basecls - self.remove_padding = remove_padding self.channel = conf.contribs['NativeCANSocket']['channel'] if \ channel is None else channel self.ins = socket.socket(socket.PF_CAN, @@ -55,6 +84,31 @@ def __init__(self, channel=None, receive_own_messages=False, "Could not modify receive own messages (%s)", exception ) + try: + # Receive Auxiliary Data (Timestamps) + self.ins.setsockopt( + socket.SOL_SOCKET, + SO_TIMESTAMPNS, + 1 + ) + self.auxdata_available = True + except OSError: + # Note: Auxiliary Data is only supported since + # Linux 2.6.21 + msg = "Your Linux Kernel does not support Auxiliary Data!" + log_runtime.info(msg) + + if self.fd: + try: + self.ins.setsockopt(socket.SOL_CAN_RAW, + socket.CAN_RAW_FD_FRAMES, + 1) + self.MTU = CAN_FD_MTU + except Exception as exception: + raise Scapy_Exception( + "Could not enable CAN FD support (%s)", exception + ) + if can_filters is None: can_filters = [{ "can_id": 0, @@ -74,50 +128,55 @@ def __init__(self, channel=None, receive_own_messages=False, self.ins.bind((self.channel,)) self.outs = self.ins - def recv(self, x=CAN_FRAME_SIZE): + def recv_raw(self, x=CAN_MTU): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + """Returns a tuple containing (cls, pkt_data, time)""" + pkt = None + ts = None try: - pkt, sa_ll = self.ins.recvfrom(x) + pkt, _, ts = self._recv_raw(self.ins, self.MTU) except BlockingIOError: # noqa: F821 warning("Captured no data, socket in non-blocking mode.") - return None except socket.timeout: warning("Captured no data, socket read timed out.") - return None except OSError: # something bad happened (e.g. the interface went down) warning("Captured no data.") - return None # need to change the byte order of the first four bytes, # required by the underlying Linux SocketCAN frame format - if not conf.contribs['CAN']['swap-bytes']: - pkt = struct.pack("I12s", pkt)) + if not conf.contribs['CAN']['swap-bytes'] and pkt: + pack_fmt = " int + if x is None: + return 0 + try: - if hasattr(x, "sent_time"): - x.sent_time = time.time() - - # need to change the byte order of the first four bytes, - # required by the underlying Linux SocketCAN frame format - bs = bytes(x) - if not conf.contribs['CAN']['swap-bytes']: - bs = bs + b'\x00' * (CAN_FRAME_SIZE - len(bs)) - bs = struct.pack("I12s", bs)) - return SuperSocket.send(self, bs) - except socket.error as msg: - raise msg - - def close(self): - self.ins.close() + x.sent_time = time.time() + except AttributeError: + pass + + # need to change the byte order of the first four bytes, + # required by the underlying Linux SocketCAN frame format + bs = raw(x) + if not conf.contribs['CAN']['swap-bytes']: + pack_fmt = " -# This program is published under a GPLv2 license -# scapy.contrib.description = Python-Can CANSocket +# scapy.contrib.description = python-can CANSocket # scapy.contrib.status = loads """ @@ -13,139 +13,333 @@ import time import struct import threading -import copy from functools import reduce from operator import add +from collections import deque + from scapy.config import conf from scapy.supersocket import SuperSocket from scapy.layers.can import CAN -from scapy.automaton import SelectableObject -from scapy.modules.six.moves import queue +from scapy.packet import Packet +from scapy.error import warning +from typing import ( + List, + Type, + Tuple, + Dict, + Any, + Optional, + cast, +) + from can import Message as can_Message from can import CanError as can_CanError from can import BusABC as can_BusABC from can.interface import Bus as can_Bus +__all__ = ["CANSocket", "PythonCANSocket"] -CAN_FRAME_SIZE = 16 -CAN_INV_FILTER = 0x20000000 - -class SocketMapper: +class SocketMapper(object): + """Internal Helper class to map a python-can bus object to + a list of SocketWrapper instances + """ def __init__(self, bus, sockets): + # type: (can_BusABC, List[SocketWrapper]) -> None + """Initializes the SocketMapper helper class + + :param bus: A python-can Bus object + :param sockets: A list of SocketWrapper objects which want to receive + messages from the provided python-can Bus object. + """ self.bus = bus self.sockets = sockets - - def mux(self): + self.closing = False + + # Maximum time (seconds) to spend reading frames in one read_bus() + # call. On serial interfaces (slcan) the final bus.recv(timeout=0) + # when the buffer is empty blocks for the serial port's read timeout + # (typically 100ms in python-can's slcan driver). During that block + # the TimeoutScheduler thread cannot run any other callbacks. By + # capping total read time, we ensure the scheduler stays responsive + # even on slow serial interfaces with heavy background traffic. + READ_BUS_TIME_LIMIT = 0.020 # 20 ms + + def read_bus(self): + # type: () -> List[can_Message] + """Read available frames from the bus, up to READ_BUS_TIME_LIMIT. + + On slow serial interfaces (slcan), bus.recv(timeout=0) can + block for ~100ms when the serial buffer is empty (python-can's + slcan serial timeout). This method limits total time spent + reading so the TimeoutScheduler thread stays responsive. + + This method intentionally does NOT hold pool_mutex so that + concurrent send() calls are not blocked during the serial I/O. + """ + if self.closing: + return [] + msgs = [] + deadline = time.monotonic() + self.READ_BUS_TIME_LIMIT while True: try: msg = self.bus.recv(timeout=0) if msg is None: - return - except Exception: - return - for sock in self.sockets: - if sock._matches_filters(msg): - sock.rx_queue.put(copy.copy(msg)) - - -class SocketsPool(object): - __instance = None - - def __new__(cls): - if SocketsPool.__instance is None: - SocketsPool.__instance = object.__new__(cls) - SocketsPool.__instance.pool = dict() - SocketsPool.__instance.pool_mutex = threading.Lock() - return SocketsPool.__instance + break + else: + msgs.append(msg) + if time.monotonic() >= deadline: + break + except Exception as e: + if not self.closing: + warning("[MUX] python-can exception caught: %s" % e) + break + return msgs + + def distribute(self, msgs): + # type: (List[can_Message]) -> None + """Distribute received messages to all subscribed sockets.""" + for sock in self.sockets: + with sock.lock: + for msg in msgs: + if sock._matches_filters(msg): + sock.rx_queue.append(msg) + + +class _SocketsPool(object): + """Helper class to organize all SocketWrapper and SocketMapper objects""" + def __init__(self): + # type: () -> None + self.pool = dict() # type: Dict[str, SocketMapper] + self.pool_mutex = threading.Lock() + self.last_call = 0.0 def internal_send(self, sender, msg): + # type: (SocketWrapper, can_Message) -> None + """Internal send function. + + A given SocketWrapper wants to send a CAN message. The python-can + Bus object is obtained from an internal pool of SocketMapper objects. + The given message is sent on the python-can Bus object and also + inserted into the message queues of all other SocketWrapper objects + which are connected to the same python-can bus object + by the SocketMapper. + + :param sender: SocketWrapper which initiated a send of a CAN message + :param msg: CAN message to be sent + """ + if sender.name is None: + raise TypeError("SocketWrapper.name should never be None") + with self.pool_mutex: try: - t = self.pool[sender.name] + mapper = self.pool[sender.name] + mapper.bus.send(msg) + for sock in mapper.sockets: + if sock == sender: + continue + if not sock._matches_filters(msg): + continue + + with sock.lock: + sock.rx_queue.append(msg) except KeyError: - return - - try: - t.bus.send(msg) - for sock in t.sockets: - if sock != sender and sock._matches_filters(msg): - m = copy.copy(msg) - m.timestamp = time.time() - sock.rx_queue.put(m) - except can_CanError: - pass + warning("[SND] Socket %s not found in pool" % sender.name) + except can_CanError as e: + warning("[SND] python-can exception caught: %s" % e) def multiplex_rx_packets(self): + # type: () -> None + """This calls the mux() function of all SocketMapper + objects in this SocketPool + """ + if time.monotonic() - self.last_call < 0.001: + # Avoid starvation if multiple threads are doing selects, since + # this object is singleton and all python-CAN sockets are using + # the same instance and locking the same locks. + return + # Snapshot pool entries under the lock, then read from each bus + # WITHOUT holding pool_mutex. On slow serial interfaces (slcan) + # bus.recv(timeout=0) can take ~2-3ms per frame; holding the + # mutex during those reads would block send() for the entire + # duration. with self.pool_mutex: - for _, t in self.pool.items(): - t.mux() + mappers = list(self.pool.values()) + for mapper in mappers: + msgs = mapper.read_bus() + if msgs: + mapper.distribute(msgs) + self.last_call = time.monotonic() def register(self, socket, *args, **kwargs): - k = str( - str(kwargs.get("bustype", "unknown_bustype")) + "_" + - str(kwargs.get("channel", "unknown_channel")) - ) + # type: (SocketWrapper, Tuple[Any, ...], Dict[str, Any]) -> None + """Registers a SocketWrapper object. Every SocketWrapper describes to + a python-can bus object. This python-can bus object can only exist + once. In case this object already exists in this SocketsPool, organized + by a SocketMapper object, the new SocketWrapper is inserted in the + list of subscribers of the SocketMapper. Otherwise a new python-can + Bus object is created from the provided args and kwargs and inserted, + encapsulated in a SocketMapper, into this SocketsPool. + + :param socket: SocketWrapper object which needs to be registered. + :param args: Arguments for the python-can Bus object + :param kwargs: Keyword arguments for the python-can Bus object + """ + if "interface" in kwargs.keys(): + k = str(kwargs.get("interface", "unknown_interface")) + "_" + \ + str(kwargs.get("channel", "unknown_channel")) + else: + k = str(kwargs.get("bustype", "unknown_bustype")) + "_" + \ + str(kwargs.get("channel", "unknown_channel")) with self.pool_mutex: if k in self.pool: t = self.pool[k] t.sockets.append(socket) - filters = [s.filters for s in t.sockets - if s.filters is not None] - if filters: - t.bus.set_filters(reduce(add, filters)) + # Update bus-level filters to the union of all sockets' + # filters. For non-slcan interfaces (socketcan, kvaser, + # vector), this enables efficient hardware/kernel + # filtering. For slcan, the bus filters were already + # cleared on creation, so this is a no-op (all sockets + # on slcan share the unfiltered bus). + if not k.lower().startswith('slcan'): + filters = [s.filters for s in t.sockets + if s.filters is not None] + if filters: + t.bus.set_filters(reduce(add, filters)) socket.name = k else: bus = can_Bus(*args, **kwargs) + # Serial interfaces like slcan only do software + # filtering inside BusABC.recv(): the recv loop reads + # one frame, finds it doesn't match, and returns + # None -- silently consuming serial bandwidth without + # returning the frame to the mux. This starves the + # mux on busy buses. + # + # For slcan, clear the filters from the bus so that + # bus.recv() returns ALL frames. Per-socket filtering + # in distribute() via _matches_filters() handles + # delivery. Other interfaces (socketcan, kvaser, + # vector, candle) perform efficient hardware/kernel + # filtering and should keep their bus-level filters. + if kwargs.get('can_filters') and \ + k.lower().startswith('slcan'): + bus.set_filters(None) socket.name = k self.pool[k] = SocketMapper(bus, [socket]) def unregister(self, socket): + # type: (SocketWrapper) -> None + """Unregisters a SocketWrapper from its subscription to a SocketMapper. + + If a SocketMapper doesn't have any subscribers, the python-can Bus + get shutdown. + + :param socket: SocketWrapper to be unregistered + """ + if socket.name is None: + raise TypeError("SocketWrapper.name should never be None") + with self.pool_mutex: - t = self.pool[socket.name] - t.sockets.remove(socket) - if not t.sockets: - t.bus.shutdown() - del self.pool[socket.name] + try: + t = self.pool[socket.name] + t.sockets.remove(socket) + if not t.sockets: + t.closing = True + t.bus.shutdown() + del self.pool[socket.name] + except KeyError: + warning("Socket %s already removed from pool" % socket.name) + + +SocketsPool = _SocketsPool() class SocketWrapper(can_BusABC): - """Socket for specific Bus or Interface. - """ + """Helper class to wrap a python-can Bus object as socket""" def __init__(self, *args, **kwargs): + # type: (Tuple[Any, ...], Dict[str, Any]) -> None + """Initializes a new python-can based socket, described by the provided + arguments and keyword arguments. This SocketWrapper gets automatically + registered in the SocketsPool. + + :param args: Arguments for the python-can Bus object + :param kwargs: Keyword arguments for the python-can Bus object + """ super(SocketWrapper, self).__init__(*args, **kwargs) - self.rx_queue = queue.Queue() # type: queue.Queue[can_Message] - self.name = None - SocketsPool().register(self, *args, **kwargs) + self.lock = threading.Lock() + self.rx_queue = deque() # type: deque[can_Message] + self.name = None # type: Optional[str] + SocketsPool.register(self, *args, **kwargs) def _recv_internal(self, timeout): - SocketsPool().multiplex_rx_packets() - try: - return self.rx_queue.get(block=True, timeout=timeout), True - except queue.Empty: + # type: (int) -> Tuple[Optional[can_Message], bool] + """Internal blocking receive method, + following the ``can_BusABC`` interface of python-can. + + This triggers the multiplex function of the general SocketsPool. + + :param timeout: Time to wait for a packet + :return: Returns a tuple of either a can_Message or None and a bool to + indicate if filtering was already applied. + """ + if not self.rx_queue: + # Early return without locking if it looks like rx_queue is empty return None, True + with self.lock: + # It could be that 2 threads are using this same socket, so it's + # necessary to check again if the queue was emptied between the + # previous check and now + if len(self.rx_queue) == 0: + return None, True + msg = self.rx_queue.popleft() + return msg, True + def send(self, msg, timeout=None): - SocketsPool().internal_send(self, msg) + # type: (can_Message, Optional[int]) -> None + """Send function, following the ``can_BusABC`` interface of python-can. + + :param msg: Message to be sent. + :param timeout: Not used. + """ + SocketsPool.internal_send(self, msg) def shutdown(self): - SocketsPool().unregister(self) + # type: () -> None + """Shutdown function, following the ``can_BusABC`` interface of + python-can. + """ + SocketsPool.unregister(self) + super().shutdown() + + +class PythonCANSocket(SuperSocket): + """Initializes a python-can bus object as Scapy PythonCANSocket. + All provided keyword arguments, except *basecls* are forwarded to + the python-can can_Bus init function. For further details on python-can + check: https://python-can.readthedocs.io/ -class PythonCANSocket(SuperSocket, SelectableObject): + Example: + >>> socket = PythonCANSocket(bustype='socketcan', channel='vcan0', bitrate=250000) + """ # noqa: E501 desc = "read/write packets at a given CAN interface " \ "using a python-can bus object" nonblocking_socket = True def __init__(self, **kwargs): - self.basecls = kwargs.pop("basecls", CAN) - self.iface = SocketWrapper(**kwargs) + # type: (Dict[str, Any]) -> None + self.basecls = cast(Optional[Type[Packet]], kwargs.pop("basecls", CAN)) + self.can_iface = SocketWrapper(**kwargs) def recv_raw(self, x=0xffff): - msg = self.iface.recv() + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + """Returns a tuple containing (cls, pkt_data, time)""" + msg = self.can_iface.recv() hdr = msg.is_extended_id << 31 | msg.is_remote_frame << 30 | \ msg.is_error_frame << 29 | msg.arbitration_id @@ -153,34 +347,62 @@ def recv_raw(self, x=0xffff): if conf.contribs['CAN']['swap-bytes']: hdr = struct.unpack("I", hdr))[0] - dlc = msg.dlc << 24 + dlc = msg.dlc << 24 | msg.is_fd << 18 | \ + msg.error_state_indicator << 17 | msg.bitrate_switch << 16 pkt_data = struct.pack("!II", hdr, dlc) + bytes(msg.data) return self.basecls, pkt_data, msg.timestamp def send(self, x): + # type: (Packet) -> int + bx = bytes(x) msg = can_Message(is_remote_frame=x.flags == 0x2, is_extended_id=x.flags == 0x4, is_error_frame=x.flags == 0x1, arbitration_id=x.identifier, + is_fd=bx[5] & 4 > 0, + error_state_indicator=bx[5] & 2 > 0, + bitrate_switch=bx[5] & 1 > 0, dlc=x.length, - data=bytes(x)[8:]) + data=bx[8:]) + msg.timestamp = time.time() try: - x.sent_time = time.time() + x.sent_time = msg.timestamp except AttributeError: pass - self.iface.send(msg) + self.can_iface.send(msg) + return len(x) @staticmethod - def select(sockets, *args, **kwargs): - SocketsPool().multiplex_rx_packets() - return [s for s in sockets if isinstance(s, PythonCANSocket) and - not s.iface.rx_queue.empty()], PythonCANSocket.recv + def select(sockets, remain=conf.recv_poll_rate): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + """This function is called during sendrecv() routine to select + the available sockets. + + :param sockets: an array of sockets that need to be selected + :returns: an array of sockets that were selected and + the function to be called next to get the packets (i.g. recv) + """ + SocketsPool.multiplex_rx_packets() + ready_sockets = \ + [s for s in sockets if isinstance(s, PythonCANSocket) and + len(s.can_iface.rx_queue)] + # checking the queue length without locking might sound + # dangerous, but for the purpose of this select, if another + # thread is reading the same socket, then even proper locking + # wouldn't help + if not ready_sockets: + # yield this thread to avoid starvation + time.sleep(0) + + return cast(List[SuperSocket], ready_sockets) def close(self): + # type: () -> None + """Closes this socket""" if self.closed: return super(PythonCANSocket, self).close() - self.iface.shutdown() + self.can_iface.shutdown() CANSocket = PythonCANSocket diff --git a/scapy/contrib/carp.py b/scapy/contrib/carp.py index 3aea2f3483f..a1436002439 100644 --- a/scapy/contrib/carp.py +++ b/scapy/contrib/carp.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Common Address Redundancy Protocol (CARP) # scapy.contrib.status = loads diff --git a/scapy/contrib/cdp.py b/scapy/contrib/cdp.py index 9a5e02e447d..030b3b0772d 100644 --- a/scapy/contrib/cdp.py +++ b/scapy/contrib/cdp.py @@ -1,37 +1,40 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2006 Nicolas Bareil +# Arnaud Ebalard +# EADS/CRC security team + # scapy.contrib.description = Cisco Discovery Protocol (CDP) # scapy.contrib.status = loads -############################################################################# -# # -# cdp.py --- Cisco Discovery Protocol (CDP) extension for Scapy # -# # -# Copyright (C) 2006 Nicolas Bareil # -# Arnaud Ebalard # -# EADS/CRC security team # -# # -# This file is part of Scapy # -# Scapy is free software: you can redistribute it and/or modify it # -# under the terms of the GNU General Public License version 2 as # -# published by the Free Software Foundation; version 2. # -# # -# This program is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -# # -############################################################################# - -from __future__ import absolute_import +""" +Cisco Discovery Protocol (CDP) extension for Scapy +""" + import struct from scapy.packet import Packet, bind_layers -from scapy.fields import ByteEnumField, ByteField, FieldLenField, FlagsField, \ - IP6Field, IPField, PacketListField, ShortField, StrLenField, \ - X3BytesField, XByteField, XShortEnumField, XShortField +from scapy.fields import ( + ByteEnumField, + ByteField, + FieldLenField, + FieldListField, + FlagsField, + IntField, + IP6Field, + IPField, + OUIField, + PacketListField, + ShortField, + StrLenField, + XByteField, + XShortEnumField, + XShortField, +) from scapy.layers.inet import checksum from scapy.layers.l2 import SNAP from scapy.compat import orb, chb -from scapy.modules.six.moves import range from scapy.config import conf @@ -62,8 +65,8 @@ # 0x0015: "CDPMsgSystemOID", 0x0016: "CDPMsgMgmtAddr", # 0x0017: "CDPMsgLocation", - 0x0019: "CDPMsgUnknown19", - # 0x001a: "CDPPowerAvailable" + 0x0019: "CDPMsgPowerRequest", + 0x001a: "CDPMsgPowerAvailable" } _cdp_tlv_types = {0x0001: "Device ID", @@ -90,7 +93,7 @@ 0x0016: "Management Address", 0x0017: "Location", 0x0018: "CDP Unknown command (send us a pcap file)", - 0x0019: "CDP Unknown command (send us a pcap file)", + 0x0019: "Power Request", 0x001a: "Power Available"} @@ -189,14 +192,13 @@ class CDPMsgAddr(CDPMsgGeneric): name = "Addresses" fields_desc = [XShortEnumField("type", 0x0002, _cdp_tlv_types), ShortField("len", None), - FieldLenField("naddr", None, "addr", "!I"), + FieldLenField("naddr", None, fmt="!I", count_of="addr"), PacketListField("addr", [], _CDPGuessAddrRecord, length_from=lambda x:x.len - 8)] def post_build(self, pkt, pay): if self.len is None: - tmp_len = 8 + len(self.addr) * 9 - pkt = pkt[:2] + struct.pack("!H", tmp_len) + pkt[4:] + pkt = pkt[:2] + struct.pack("!H", len(pkt)) + pkt[4:] p = pkt + pay return p @@ -248,13 +250,23 @@ class CDPMsgIPGateway(CDPMsgGeneric): IPField("defaultgw", "192.168.0.1")] +class CDPIPPrefix(Packet): + fields_desc = [ + IPField("prefix", "192.168.0.1"), + ByteField("plen", 24), + ] + + def guess_payload_class(self, p): + return conf.padding_layer + + class CDPMsgIPPrefix(CDPMsgGeneric): name = "IP Prefix" type = 0x0007 fields_desc = [XShortEnumField("type", 0x0007, _cdp_tlv_types), ShortField("len", 9), - IPField("prefix", "192.168.0.1"), - ByteField("plen", 24)] + PacketListField("prefixes", [], CDPIPPrefix, + length_from=lambda p: p.len - 4)] class CDPMsgProtoHello(CDPMsgGeneric): @@ -262,7 +274,7 @@ class CDPMsgProtoHello(CDPMsgGeneric): type = 0x0008 fields_desc = [XShortEnumField("type", 0x0008, _cdp_tlv_types), ShortField("len", 32), - X3BytesField("oui", 0x00000c), + OUIField("oui", 0x00000c), XShortField("protocol_id", 0x0), # TLV length (len) - 2 (type) - 2 (len) - 3 (OUI) - 2 # (Protocol ID) @@ -292,7 +304,7 @@ class CDPMsgVoIPVLANReply(CDPMsgGeneric): name = "VoIP VLAN Reply" fields_desc = [XShortEnumField("type", 0x000e, _cdp_tlv_types), ShortField("len", 7), - ByteField("status?", 1), + ByteField("status", 1), ShortField("vlan", 1)] @@ -351,9 +363,28 @@ class CDPMsgMgmtAddr(CDPMsgAddr): type = 0x0016 -class CDPMsgUnknown19(CDPMsgGeneric): - name = "Unknown CDP Message" - type = 0x0019 +class CDPMsgPowerRequest(CDPMsgGeneric): + name = "Power Request" + fields_desc = [XShortEnumField("type", 0x0019, _cdp_tlv_types), + FieldLenField("len", None, "power_requested_list", fmt="!H", + adjust=lambda pkt, x: x + 8), + ShortField("req_id", 0), + ShortField("mgmt_id", 0), + FieldListField("power_requested_list", [], + IntField("power_requested", 0), + count_from=lambda pkt: (pkt.len - 8) // 4)] + + +class CDPMsgPowerAvailable(CDPMsgGeneric): + name = "Power Available" + fields_desc = [XShortEnumField("type", 0x001a, _cdp_tlv_types), + FieldLenField("len", None, "power_available_list", fmt="!H", + adjust=lambda pkt, x: x + 8), + ShortField("req_id", 0), + ShortField("mgmt_id", 0), + FieldListField("power_available_list", [], + IntField("power_available", 0), + count_from=lambda pkt: (pkt.len - 8) // 4)] class CDPMsg(CDPMsgGeneric): diff --git a/scapy/contrib/chdlc.py b/scapy/contrib/chdlc.py index e17201203a5..b6946842022 100644 --- a/scapy/contrib/chdlc.py +++ b/scapy/contrib/chdlc.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Cisco HDLC and SLARP # scapy.contrib.status = loads diff --git a/scapy/contrib/coap.py b/scapy/contrib/coap.py index 0bd0145cd4f..1c8eb3d7b14 100644 --- a/scapy/contrib/coap.py +++ b/scapy/contrib/coap.py @@ -1,19 +1,6 @@ -# This file is part of Scapy. -# See http://www.secdev.org/projects/scapy for more information. -# -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . -# +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2016 Anmol Sarma # scapy.contrib.description = Constrained Application Protocol (CoAP) @@ -120,7 +107,7 @@ def _get_abs_val(val, ext_val): if val >= 15: warning("Invalid Option Length or Delta %d" % val) if val == 14: - return 269 + struct.unpack('H', ext_val)[0] + return 269 + struct.unpack('!H', ext_val)[0] if val == 13: return 13 + struct.unpack('B', ext_val)[0] return val @@ -133,14 +120,14 @@ def _get_opt_val_size(pkt): class _CoAPOpt(Packet): fields_desc = [BitField("delta", 0, 4), BitField("len", 0, 4), - StrLenField("delta_ext", None, length_from=_get_delta_ext_size), # noqa: E501 - StrLenField("len_ext", None, length_from=_get_len_ext_size), - StrLenField("opt_val", None, length_from=_get_opt_val_size)] + StrLenField("delta_ext", "", length_from=_get_delta_ext_size), # noqa: E501 + StrLenField("len_ext", "", length_from=_get_len_ext_size), + StrLenField("opt_val", "", length_from=_get_opt_val_size)] @staticmethod def _populate_extended(val): if val >= 269: - return struct.pack('H', val - 269), 14 + return struct.pack('!H', val - 269), 14 if val >= 13: return struct.pack('B', val - 13), 13 return None, val diff --git a/scapy/contrib/concox.py b/scapy/contrib/concox.py index 0fd2e861ce5..c4d87b219f4 100644 --- a/scapy/contrib/concox.py +++ b/scapy/contrib/concox.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2019 Juciano Cardoso # 2019 Guillaume Valadon -## -# This program is published under a GPLv2 license # scapy.contrib.description = Concox CRX1 unit tests # scapy.contrib.status = loads @@ -222,7 +223,7 @@ class CRX1NewPacketContent(Packet): "", length_from=lambda pkt: pkt.command_length - 4), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (0x80, 0x15)), - # Commun + # Common ConditionalField( ByteEnumField( "alarm_extended", 0x00, { diff --git a/scapy/contrib/dce_rpc.py b/scapy/contrib/dce_rpc.py deleted file mode 100644 index 651a9c4c7d0..00000000000 --- a/scapy/contrib/dce_rpc.py +++ /dev/null @@ -1,164 +0,0 @@ -# This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - -# Copyright (C) 2016 Gauthier Sebaux - -# scapy.contrib.description = DCE/RPC -# scapy.contrib.status = loads - -""" -A basic dissector for DCE/RPC. -Isn't reliable for all packets and for building -""" - -import struct - -# TODO: namespace locally used fields -from scapy.packet import Packet, Raw, bind_layers -from scapy.fields import BitEnumField, ByteEnumField, ByteField, \ - FlagsField, IntField, LenField, ShortField, UUIDField, XByteField, \ - XShortField - - -# Fields -class EndiannessField(object): - """Field which change the endianness of a sub-field""" - __slots__ = ["fld", "endianess_from"] - - def __init__(self, fld, endianess_from): - self.fld = fld - self.endianess_from = endianess_from - - def set_endianess(self, pkt): - """Add the endianness to the format""" - end = self.endianess_from(pkt) - if isinstance(end, str) and end: - if isinstance(self.fld, UUIDField): - self.fld.uuid_fmt = (UUIDField.FORMAT_LE if end == '<' - else UUIDField.FORMAT_BE) - else: - # fld.fmt should always start with a order specifier, cf field - # init - self.fld.fmt = end[0] + self.fld.fmt[1:] - self.fld.struct = struct.Struct(self.fld.fmt) - - def getfield(self, pkt, buf): - """retrieve the field with endianness""" - self.set_endianess(pkt) - return self.fld.getfield(pkt, buf) - - def addfield(self, pkt, buf, val): - """add the field with endianness to the buffer""" - self.set_endianess(pkt) - return self.fld.addfield(pkt, buf, val) - - def __getattr__(self, attr): - return getattr(self.fld, attr) - - -# DCE/RPC Packet -DCE_RPC_TYPE = ["request", "ping", "response", "fault", "working", "no_call", - "reject", "acknowledge", "connectionless_cancel", "frag_ack", - "cancel_ack"] -DCE_RPC_FLAGS1 = ["reserved_0", "last_frag", "frag", "no_frag_ack", "maybe", - "idempotent", "broadcast", "reserved_7"] -DCE_RPC_FLAGS2 = ["reserved_0", "cancel_pending", "reserved_2", "reserved_3", - "reserved_4", "reserved_5", "reserved_6", "reserved_7"] - - -def dce_rpc_endianess(pkt): - """Determine the right endianness sign for a given DCE/RPC packet""" - if pkt.endianness == 0: # big endian - return ">" - elif pkt.endianness == 1: # little endian - return "<" - else: - return "!" - - -class DceRpc(Packet): - """DCE/RPC packet""" - name = "DCE/RPC" - fields_desc = [ - ByteField("version", 4), - ByteEnumField("type", 0, DCE_RPC_TYPE), - FlagsField("flags1", 0, 8, DCE_RPC_FLAGS1), - FlagsField("flags2", 0, 8, DCE_RPC_FLAGS2), - BitEnumField("endianness", 0, 4, ["big", "little"]), - BitEnumField("encoding", 0, 4, ["ASCII", "EBCDIC"]), - ByteEnumField("float", 0, ["IEEE", "VAX", "CRAY", "IBM"]), - ByteField("DataRepr_reserved", 0), - XByteField("serial_high", 0), - EndiannessField(UUIDField("object_uuid", None), - endianess_from=dce_rpc_endianess), - EndiannessField(UUIDField("interface_uuid", None), - endianess_from=dce_rpc_endianess), - EndiannessField(UUIDField("activity", None), - endianess_from=dce_rpc_endianess), - EndiannessField(IntField("boot_time", 0), - endianess_from=dce_rpc_endianess), - EndiannessField(IntField("interface_version", 1), - endianess_from=dce_rpc_endianess), - EndiannessField(IntField("sequence_num", 0), - endianess_from=dce_rpc_endianess), - EndiannessField(ShortField("opnum", 0), - endianess_from=dce_rpc_endianess), - EndiannessField(XShortField("interface_hint", 0xffff), - endianess_from=dce_rpc_endianess), - EndiannessField(XShortField("activity_hint", 0xffff), - endianess_from=dce_rpc_endianess), - EndiannessField(LenField("frag_len", None, fmt="H"), - endianess_from=dce_rpc_endianess), - EndiannessField(ShortField("frag_num", 0), - endianess_from=dce_rpc_endianess), - ByteEnumField("auth", 0, ["none"]), # TODO other auth ? - XByteField("serial_low", 0), - ] - - -# Heuristically way to find the payload class -# -# To add a possible payload to a DCE/RPC packet, one must first create the -# packet class, then instead of binding layers using bind_layers, he must -# call DceRpcPayload.register_possible_payload() with the payload class as -# parameter. -# -# To be able to decide if the payload class is capable of handling the rest of -# the dissection, the classmethod can_handle() should be implemented in the -# payload class. This method is given the rest of the string to dissect as -# first argument, and the DceRpc packet instance as second argument. Based on -# this information, the method must return True if the class is capable of -# handling the dissection, False otherwise -class DceRpcPayload(Packet): - """Dummy class which use the dispatch_hook to find the payload class""" - _payload_class = [] - - @classmethod - def dispatch_hook(cls, _pkt, _underlayer=None, *args, **kargs): - """dispatch_hook to choose among different registered payloads""" - for klass in cls._payload_class: - if hasattr(klass, "can_handle") and \ - klass.can_handle(_pkt, _underlayer): - return klass - print("DCE/RPC payload class not found or undefined (using Raw)") - return Raw - - @classmethod - def register_possible_payload(cls, pay): - """Method to call from possible DCE/RPC endpoint to register it as - possible payload""" - cls._payload_class.append(pay) - - -bind_layers(DceRpc, DceRpcPayload) diff --git a/scapy/contrib/diameter.py b/scapy/contrib/diameter.py index ff72cc538b7..7bd74e28245 100644 --- a/scapy/contrib/diameter.py +++ b/scapy/contrib/diameter.py @@ -1,22 +1,23 @@ -########################################################################## -# -# Diameter protocol implementation for Scapy -# Original Author: patrick battistello -# -# This implements the base Diameter protocol RFC6733 and the additional standards: # noqa: E501 -# RFC7155, RFC4004, RFC4006, RFC4072, RFC4740, RFC5778, RFC5447, RFC6942, RFC5777 # noqa: E501 -# ETS29229 V12.3.0 (2014-09), ETS29272 V13.1.0 (2015-03), ETS29329 V12.5.0 (2014-12), # noqa: E501 -# ETS29212 V13.1.0 (2015-03), ETS32299 V13.0.0 (2015-03), ETS29210 V6.7.0 (2006-12), # noqa: E501 -# ETS29214 V13.1.0 (2015-03), ETS29273 V12.7.0 (2015-03), ETS29173 V12.3.0 (2015-03), # noqa: E501 -# ETS29172 V12.5.0 (2015-03), ETS29215 V13.1.0 (2015-03), ETS29209 V6.8.0 (2011-09), # noqa: E501 -# ETS29061 V13.0.0 (2015-03), ETS29219 V13.0.0 (2014-12) -# -# IMPORTANT note: -# -# - Some Diameter fields (Unsigned64, Float32, ...) have not been tested yet due to lack # noqa: E501 -# of network captures containing AVPs of that types contributions are welcomed. # noqa: E501 -# -########################################################################## +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Acknowledgment: Patrick Battistello + +""" +Diameter protocol implementation for Scapy + +This implements the base Diameter protocol RFC6733 and the additional standards: # noqa: E501 + RFC7155, RFC4004, RFC4006, RFC4072, RFC4740, RFC5778, RFC5447, RFC6942, RFC5777 # noqa: E501 + ETS29229 V12.3.0 (2014-09), ETS29272 V13.1.0 (2015-03), ETS29329 V12.5.0 (2014-12), # noqa: E501 + ETS29212 V13.1.0 (2015-03), ETS32299 V13.0.0 (2015-03), ETS29210 V6.7.0 (2006-12), # noqa: E501 + ETS29214 V13.1.0 (2015-03), ETS29273 V12.7.0 (2015-03), ETS29173 V12.3.0 (2015-03), # noqa: E501 + ETS29172 V12.5.0 (2015-03), ETS29215 V13.1.0 (2015-03), ETS29209 V6.8.0 (2011-09), # noqa: E501 + ETS29061 V13.0.0 (2015-03), ETS29219 V13.0.0 (2014-12) + +IMPORTANT note: + - Some Diameter fields (Unsigned64, Float32, ...) have not been tested yet due to lack # noqa: E501 + of network captures containing AVPs of that types contributions are welcomed. # noqa: E501 +""" # scapy.contrib.description = Diameter # scapy.contrib.status = loads @@ -32,8 +33,6 @@ XByteField, XIntField from scapy.layers.inet import TCP from scapy.layers.sctp import SCTPChunkData -import scapy.modules.six as six -from scapy.modules.six.moves import range from scapy.compat import chb, orb, raw, bytes_hex, plain_str from scapy.error import warning from scapy.utils import inet_ntoa, inet_aton @@ -85,7 +84,7 @@ def i2repr(self, pkt, x): return "None" res = hex(int(x)) r = '' - cmdt = (x & 128) and ' Request' or ' Answer' + cmdt = ' Request' if (x & 128) else ' Answer' if x & 15: # Check if reserved bits are used nb = 8 offset = 0 @@ -353,7 +352,7 @@ def GuessAvpType(p, **kargs): avpCode = struct.unpack("!I", p[:AVP_Code_length])[0] vnd = bool(struct.unpack( "!B", p[AVP_Code_length:AVP_Code_length + AVP_Flag_length])[0] & 128) # noqa: E501 - vndCode = vnd and struct.unpack("!I", p[8:12])[0] or 0 + vndCode = struct.unpack("!I", p[8:12])[0] if vnd else 0 # Check if vendor and code defined and fetch the corresponding AVP # definition if vndCode in AvpDefDict: @@ -430,7 +429,7 @@ def AVP(avpId, **fields): if val: fields['avpFlags'] = val[2] else: - fields['avpFlags'] = vnd and 128 or 0 + fields['avpFlags'] = 128 if vnd else 0 # Finally, set the name and class if possible if val: classType = val[1] @@ -2528,8 +2527,8 @@ class AVP_10415_1259 (AVP_FL_V): 'val', None, { - 1: "Pre-emptive priority: ", - 2: "High priority: Lower than Pre-emptive priority", + 1: "Preemptive priority: ", + 2: "High priority: Lower than Preemptive priority", 3: "Normal priority: Normal level. Lower than High priority", 4: "Low priority: Lowest level priority", })] @@ -4781,14 +4780,14 @@ def getCmdParams(cmd, request, **fields): val = fields['drAppId'] if isinstance(val, str): # Translate into application Id code found = False - for k, v in six.iteritems(AppIDsEnum): + for k, v in AppIDsEnum.items(): if v.find(val) != -1: drAppId = k fields['drAppId'] = drAppId found = True break if not found: - del(fields['drAppId']) + del fields['drAppId'] warning( 'Application ID with name %s not found in AppIDsEnum dictionary.' % # noqa: E501 val) @@ -4799,12 +4798,12 @@ def getCmdParams(cmd, request, **fields): drAppId = next(iter(params[2])) # The first record is taken fields['drAppId'] = drAppId # Set the command name - name = request and params[0] + '-Request' or params[0] + '-Answer' + name = params[0] + '-Request' if request else params[0] + '-Answer' # Processing of flags (only if not provided manually) if 'drFlags' not in fields: if drAppId in params[2]: flags = params[2][drAppId] - fields['drFlags'] = request and flags[0] or flags[1] + fields['drFlags'] = flags[0] if request else flags[1] return (fields, name) @@ -4829,7 +4828,5 @@ def DiamAns(cmd, **fields): bind_layers(TCP, DiamG, dport=3868) bind_layers(TCP, DiamG, sport=3868) -bind_layers(SCTPChunkData, DiamG, dport=3868) -bind_layers(SCTPChunkData, DiamG, sport=3868) bind_layers(SCTPChunkData, DiamG, proto_id=46) bind_layers(SCTPChunkData, DiamG, proto_id=47) diff --git a/scapy/contrib/dicom.py b/scapy/contrib/dicom.py new file mode 100644 index 00000000000..de0558a2e38 --- /dev/null +++ b/scapy/contrib/dicom.py @@ -0,0 +1,1649 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Tyler M + +# scapy.contrib.description = DICOM (Digital Imaging and Communications in Medicine) +# scapy.contrib.status = loads + +""" +DICOM (Digital Imaging and Communications in Medicine) Protocol +Reference: DICOM PS3.8 - Network Communication Support for Message Exchange +https://dicom.nema.org/medical/dicom/current/output/html/part08.html +""" + +import logging +import socket +import struct +import time +from typing import Any, Dict, List, Optional, Tuple, Union + +from scapy.compat import Self +from scapy.packet import Packet, bind_layers +from scapy.error import Scapy_Exception +from scapy.fields import ( + BitField, + ByteEnumField, + ByteField, + ConditionalField, + Field, + FieldLenField, + IntField, + LenField, + PacketListField, + ShortField, + StrFixedLenField, + StrLenField, +) +from scapy.layers.inet import TCP +from scapy.supersocket import StreamSocket +from scapy.volatile import RandShort, RandInt, RandString + +__all__ = [ + "DICOM_PORT", + "DICOM_PORT_ALT", + "APP_CONTEXT_UID", + "DEFAULT_TRANSFER_SYNTAX_UID", + "VERIFICATION_SOP_CLASS_UID", + "CT_IMAGE_STORAGE_SOP_CLASS_UID", + "PATIENT_ROOT_QR_FIND_SOP_CLASS_UID", + "PATIENT_ROOT_QR_MOVE_SOP_CLASS_UID", + "PATIENT_ROOT_QR_GET_SOP_CLASS_UID", + "STUDY_ROOT_QR_FIND_SOP_CLASS_UID", + "STUDY_ROOT_QR_MOVE_SOP_CLASS_UID", + "STUDY_ROOT_QR_GET_SOP_CLASS_UID", + "DICOM", + "A_ASSOCIATE_RQ", + "A_ASSOCIATE_AC", + "A_ASSOCIATE_RJ", + "P_DATA_TF", + "PresentationDataValueItem", + "A_RELEASE_RQ", + "A_RELEASE_RP", + "A_ABORT", + "DICOMVariableItem", + "DICOMApplicationContext", + "DICOMPresentationContextRQ", + "DICOMPresentationContextAC", + "DICOMAbstractSyntax", + "DICOMTransferSyntax", + "DICOMUserInformation", + "DICOMMaximumLength", + "DICOMImplementationClassUID", + "DICOMAsyncOperationsWindow", + "DICOMSCPSCURoleSelection", + "DICOMImplementationVersionName", + "DICOMSOPClassExtendedNegotiation", + "DICOMSOPClassCommonExtendedNegotiation", + "DICOMUserIdentity", + "DICOMUserIdentityResponse", + "DICOMElementField", + "DICOMAETitleField", + "DICOMUIDField", + "DICOMUIDFieldRaw", + "DICOMUSField", + "DICOMULField", + "DICOMAEDIMSEField", + "DIMSEPacket", + "C_ECHO_RQ", + "C_ECHO_RSP", + "C_STORE_RQ", + "C_STORE_RSP", + "C_FIND_RQ", + "C_FIND_RSP", + "C_MOVE_RQ", + "C_MOVE_RSP", + "C_GET_RQ", + "C_GET_RSP", + "DICOMSocket", + "parse_dimse_status", + "_uid_to_bytes", + "_uid_to_bytes_raw", + "build_presentation_context_rq", + "build_user_information", +] + +log = logging.getLogger("scapy.contrib.dicom") + +DICOM_PORT = 104 +DICOM_PORT_ALT = 11112 +APP_CONTEXT_UID = "1.2.840.10008.3.1.1.1" +DEFAULT_TRANSFER_SYNTAX_UID = "1.2.840.10008.1.2" +VERIFICATION_SOP_CLASS_UID = "1.2.840.10008.1.1" +CT_IMAGE_STORAGE_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.1.2" + +PATIENT_ROOT_QR_FIND_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.2.1.1" +PATIENT_ROOT_QR_MOVE_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.2.1.2" +PATIENT_ROOT_QR_GET_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.2.1.3" +STUDY_ROOT_QR_FIND_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.2.2.1" +STUDY_ROOT_QR_MOVE_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.2.2.2" +STUDY_ROOT_QR_GET_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.2.2.3" + +PDU_TYPES = { + 0x01: "A-ASSOCIATE-RQ", + 0x02: "A-ASSOCIATE-AC", + 0x03: "A-ASSOCIATE-RJ", + 0x04: "P-DATA-TF", + 0x05: "A-RELEASE-RQ", + 0x06: "A-RELEASE-RP", + 0x07: "A-ABORT", +} + +ITEM_TYPES = { + 0x10: "Application Context", + 0x20: "Presentation Context RQ", + 0x21: "Presentation Context AC", + 0x30: "Abstract Syntax", + 0x40: "Transfer Syntax", + 0x50: "User Information", + 0x51: "Maximum Length", + 0x52: "Implementation Class UID", + 0x53: "Asynchronous Operations Window", + 0x54: "SCP/SCU Role Selection", + 0x55: "Implementation Version Name", + 0x56: "SOP Class Extended Negotiation", + 0x57: "SOP Class Common Extended Negotiation", + 0x58: "User Identity", + 0x59: "User Identity Server Response", +} + + +def _uid_to_bytes(uid: Union[str, bytes]) -> bytes: + """Convert UID to bytes with even-length padding (null byte if needed).""" + if isinstance(uid, bytes): + b_uid = uid + elif isinstance(uid, str): + b_uid = uid.encode("ascii") + else: + return b"" + if len(b_uid) % 2 != 0: + b_uid += b"\x00" + return b_uid + + +def _uid_to_bytes_raw(uid: Union[str, bytes]) -> bytes: + """Convert UID to bytes without padding.""" + if isinstance(uid, bytes): + return uid + elif isinstance(uid, str): + return uid.encode("ascii") + else: + return b"" + + +class DICOMAETitleField(StrFixedLenField): + """DICOM AE Title field - 16 bytes, space-padded per PS3.5 Section 6.2.""" + + def __init__(self, name: str, default: bytes = b"") -> None: + super(DICOMAETitleField, self).__init__(name, default, length=16) + + def i2m(self, pkt: Optional[Packet], val: Any) -> bytes: + if val is None: + val = b"" + if isinstance(val, str): + val = val.encode("ascii") + return val.ljust(16, b" ")[:16] + + def m2i(self, pkt: Optional[Packet], val: bytes) -> bytes: + return val + + def i2repr(self, pkt: Optional[Packet], val: Any) -> str: + if isinstance(val, bytes): + return val.decode("ascii", errors="replace").rstrip() + return str(val).rstrip() + + +class DICOMElementField(Field[bytes, bytes]): + """DICOM data element field with explicit tag and length encoding.""" + + __slots__ = ["tag_group", "tag_elem"] + + def __init__(self, name: str, default: Any, tag_group: int, + tag_elem: int) -> None: + self.tag_group = tag_group + self.tag_elem = tag_elem + Field.__init__(self, name, default) + + def addfield(self, pkt: Optional[Packet], s: bytes, val: Any) -> bytes: + if val is None: + val = b"" + if isinstance(val, str): + val = val.encode("ascii") + hdr = struct.pack(" Tuple[bytes, bytes]: + if len(s) < 8: + return s, b"" + tag_g, tag_e, length = struct.unpack(" str: + if isinstance(val, bytes): + try: + return val.decode("ascii").rstrip("\x00") + except UnicodeDecodeError: + return val.hex() + return repr(val) + + def randval(self) -> RandString: + return RandString(8) + + +class DICOMUIDField(DICOMElementField): + """DICOM UID element field with automatic even-length padding.""" + + def addfield(self, pkt: Optional[Packet], s: bytes, val: Any) -> bytes: + val = _uid_to_bytes(val) if val else b"" + return DICOMElementField.addfield(self, pkt, s, val) + + def i2repr(self, pkt: Optional[Packet], val: Any) -> str: + if isinstance(val, bytes): + return val.decode("ascii").rstrip("\x00") + return str(val) + + def randval(self) -> str: + from scapy.volatile import RandNum + return "1.2.3.%d.%d.%d" % ( + RandNum(1, 99999)._fix(), + RandNum(1, 99999)._fix(), + RandNum(1, 99999)._fix() + ) + + +class DICOMUIDFieldRaw(DICOMElementField): + """DICOM UID element field without automatic padding.""" + + def addfield(self, pkt: Optional[Packet], s: bytes, val: Any) -> bytes: + val = _uid_to_bytes_raw(val) if val else b"" + return DICOMElementField.addfield(self, pkt, s, val) + + +class DICOMUSField(DICOMElementField): + """DICOM Unsigned Short (US) element field.""" + + def addfield(self, pkt: Optional[Packet], s: bytes, val: int) -> bytes: + val_bytes = struct.pack(" Tuple[bytes, int]: + remain, val_bytes = DICOMElementField.getfield(self, pkt, s) + if len(val_bytes) >= 2: + return remain, struct.unpack(" str: + return "0x%04X" % val + + def randval(self) -> RandShort: + return RandShort() + + +class DICOMULField(DICOMElementField): + """DICOM Unsigned Long (UL) element field.""" + + def addfield(self, pkt: Optional[Packet], s: bytes, val: int) -> bytes: + val_bytes = struct.pack(" Tuple[bytes, int]: + remain, val_bytes = DICOMElementField.getfield(self, pkt, s) + if len(val_bytes) >= 4: + return remain, struct.unpack(" RandInt: + return RandInt() + + +class DICOMAEDIMSEField(DICOMElementField): + """DICOM AE element field for DIMSE - 16 bytes, space-padded.""" + + def addfield(self, pkt: Optional[Packet], s: bytes, val: Any) -> bytes: + if val is None: + val = b"" + if isinstance(val, str): + val = val.encode("ascii") + val = val.ljust(16, b" ")[:16] + return DICOMElementField.addfield(self, pkt, s, val) + + def i2repr(self, pkt: Optional[Packet], val: Any) -> str: + if isinstance(val, bytes): + return val.decode("ascii", errors="replace").strip() + return str(val).strip() + + +class DIMSEPacket(Packet): + """Base class for DIMSE command packets with automatic group length.""" + + GROUP_LENGTH_ELEMENT_SIZE = 12 + + def post_build(self, pkt: bytes, pay: bytes) -> bytes: + group_len = len(pkt) + header = struct.pack(" str: + return self.sprintf("C-ECHO-RQ msg_id=%message_id%") + + def hashret(self) -> bytes: + return struct.pack(" str: + return self.sprintf("C-ECHO-RSP status=%status%") + + def hashret(self) -> bytes: + return struct.pack(" int: + if isinstance(other, C_ECHO_RQ): + return self.message_id_responded == other.message_id + return 0 + + +class C_STORE_RQ(DIMSEPacket): + """C-STORE-RQ DIMSE Command for storing DICOM objects.""" + + name = "C-STORE-RQ" + fields_desc = [ + DICOMUIDField("affected_sop_class_uid", + CT_IMAGE_STORAGE_SOP_CLASS_UID, 0x0000, 0x0002), + DICOMUSField("command_field", 0x0001, 0x0000, 0x0100), + DICOMUSField("message_id", 1, 0x0000, 0x0110), + DICOMUSField("priority", 0x0002, 0x0000, 0x0700), + DICOMUSField("data_set_type", 0x0000, 0x0000, 0x0800), + DICOMUIDField("affected_sop_instance_uid", + "1.2.3.4.5.6.7.8.9", 0x0000, 0x1000), + ] + + def mysummary(self) -> str: + return self.sprintf("C-STORE-RQ msg_id=%message_id%") + + def hashret(self) -> bytes: + return struct.pack(" str: + return self.sprintf("C-STORE-RSP status=%status%") + + def hashret(self) -> bytes: + return struct.pack(" int: + if isinstance(other, C_STORE_RQ): + return self.message_id_responded == other.message_id + return 0 + + +class C_FIND_RQ(DIMSEPacket): + """C-FIND-RQ DIMSE Command for querying DICOM objects.""" + + name = "C-FIND-RQ" + fields_desc = [ + DICOMUIDField("affected_sop_class_uid", + PATIENT_ROOT_QR_FIND_SOP_CLASS_UID, 0x0000, 0x0002), + DICOMUSField("command_field", 0x0020, 0x0000, 0x0100), + DICOMUSField("message_id", 1, 0x0000, 0x0110), + DICOMUSField("priority", 0x0002, 0x0000, 0x0700), + DICOMUSField("data_set_type", 0x0000, 0x0000, 0x0800), + ] + + def mysummary(self) -> str: + return self.sprintf("C-FIND-RQ msg_id=%message_id%") + + def hashret(self) -> bytes: + return struct.pack(" str: + return self.sprintf("C-FIND-RSP status=%status%") + + def hashret(self) -> bytes: + return struct.pack(" int: + if isinstance(other, C_FIND_RQ): + return self.message_id_responded == other.message_id + return 0 + + +class C_MOVE_RQ(DIMSEPacket): + """C-MOVE-RQ DIMSE Command for retrieving DICOM objects.""" + + name = "C-MOVE-RQ" + fields_desc = [ + DICOMUIDField("affected_sop_class_uid", + PATIENT_ROOT_QR_MOVE_SOP_CLASS_UID, 0x0000, 0x0002), + DICOMUSField("command_field", 0x0021, 0x0000, 0x0100), + DICOMUSField("message_id", 1, 0x0000, 0x0110), + DICOMUSField("priority", 0x0002, 0x0000, 0x0700), + DICOMUSField("data_set_type", 0x0000, 0x0000, 0x0800), + DICOMAEDIMSEField("move_destination", b"", 0x0000, 0x0600), + ] + + def mysummary(self) -> str: + return self.sprintf("C-MOVE-RQ msg_id=%message_id%") + + def hashret(self) -> bytes: + return struct.pack(" str: + return self.sprintf("C-MOVE-RSP status=%status%") + + def hashret(self) -> bytes: + return struct.pack(" int: + if isinstance(other, C_MOVE_RQ): + return self.message_id_responded == other.message_id + return 0 + + +class C_GET_RQ(DIMSEPacket): + """C-GET-RQ DIMSE Command for retrieving objects on same association.""" + + name = "C-GET-RQ" + fields_desc = [ + DICOMUIDField("affected_sop_class_uid", + PATIENT_ROOT_QR_GET_SOP_CLASS_UID, 0x0000, 0x0002), + DICOMUSField("command_field", 0x0010, 0x0000, 0x0100), + DICOMUSField("message_id", 1, 0x0000, 0x0110), + DICOMUSField("priority", 0x0002, 0x0000, 0x0700), + DICOMUSField("data_set_type", 0x0000, 0x0000, 0x0800), + ] + + def mysummary(self) -> str: + return self.sprintf("C-GET-RQ msg_id=%message_id%") + + def hashret(self) -> bytes: + return struct.pack(" str: + return self.sprintf("C-GET-RSP status=%status%") + + def hashret(self) -> bytes: + return struct.pack(" int: + if isinstance(other, C_GET_RQ): + return self.message_id_responded == other.message_id + return 0 + + +def parse_dimse_status(dimse_bytes: bytes) -> Optional[int]: + """Extract status code from DIMSE response bytes.""" + try: + if len(dimse_bytes) < 12: + return None + cmd_group_len = struct.unpack(" len(dimse_bytes) or offset + 10 > group_end_offset: + break + return struct.unpack( + " Tuple[bytes, bytes]: + return b"", s + + +class DICOMVariableItem(Packet): + """DICOM variable item header with type and length fields.""" + + name = "DICOM Variable Item" + fields_desc = [ + ByteEnumField("item_type", 0x10, ITEM_TYPES), + ByteField("reserved", 0), + LenField("length", None, fmt="!H"), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + if self.length is not None: + if len(s) < self.length: + raise Scapy_Exception("PDU payload incomplete") + return s[:self.length], s[self.length:] + return s, b"" + + def guess_payload_class(self, payload: bytes) -> type: + type_to_class = { + 0x10: DICOMApplicationContext, + 0x20: DICOMPresentationContextRQ, + 0x21: DICOMPresentationContextAC, + 0x30: DICOMAbstractSyntax, + 0x40: DICOMTransferSyntax, + 0x50: DICOMUserInformation, + 0x51: DICOMMaximumLength, + 0x52: DICOMImplementationClassUID, + 0x53: DICOMAsyncOperationsWindow, + 0x54: DICOMSCPSCURoleSelection, + 0x55: DICOMImplementationVersionName, + 0x56: DICOMSOPClassExtendedNegotiation, + 0x57: DICOMSOPClassCommonExtendedNegotiation, + 0x58: DICOMUserIdentity, + 0x59: DICOMUserIdentityResponse, + } + return type_to_class.get(self.item_type, DICOMGenericItem) + + def mysummary(self) -> str: + return self.sprintf("Item %item_type%") + + +class DICOMApplicationContext(Packet): + """DICOM Application Context item.""" + + name = "DICOM Application Context" + fields_desc = [ + StrLenField( + "uid", _uid_to_bytes(APP_CONTEXT_UID), + length_from=lambda pkt: ( + pkt.underlayer.length + if pkt.underlayer and pkt.underlayer.length + else len(pkt.uid) + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "AppContext %s" % self.uid.decode("ascii").rstrip("\x00") + + +class DICOMAbstractSyntax(Packet): + """DICOM Abstract Syntax item.""" + + name = "DICOM Abstract Syntax" + fields_desc = [ + StrLenField( + "uid", b"", + length_from=lambda pkt: ( + pkt.underlayer.length + if pkt.underlayer and pkt.underlayer.length + else len(pkt.uid) + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "AbstractSyntax %s" % self.uid.decode("ascii").rstrip("\x00") + + +class DICOMTransferSyntax(Packet): + """DICOM Transfer Syntax item.""" + + name = "DICOM Transfer Syntax" + fields_desc = [ + StrLenField( + "uid", _uid_to_bytes(DEFAULT_TRANSFER_SYNTAX_UID), + length_from=lambda pkt: ( + pkt.underlayer.length + if pkt.underlayer and pkt.underlayer.length + else len(pkt.uid) + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "TransferSyntax %s" % self.uid.decode("ascii").rstrip("\x00") + + +class DICOMPresentationContextRQ(Packet): + """DICOM Presentation Context item for association requests.""" + + name = "DICOM Presentation Context RQ" + fields_desc = [ + ByteField("context_id", 1), + ByteField("reserved1", 0), + ByteField("reserved2", 0), + ByteField("reserved3", 0), + PacketListField( + "sub_items", [], + DICOMVariableItem, + max_count=64, + length_from=lambda pkt: ( + pkt.underlayer.length - 4 + if pkt.underlayer and pkt.underlayer.length + else 0 + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "PresentationContext-RQ ctx_id=%d" % self.context_id + + +class DICOMPresentationContextAC(Packet): + """DICOM Presentation Context item for association accepts.""" + + name = "DICOM Presentation Context AC" + + RESULT_CODES = { + 0: "acceptance", + 1: "user-rejection", + 2: "no-reason", + 3: "abstract-syntax-not-supported", + 4: "transfer-syntaxes-not-supported", + } + + fields_desc = [ + ByteField("context_id", 1), + ByteField("reserved1", 0), + ByteEnumField("result", 0, RESULT_CODES), + ByteField("reserved2", 0), + PacketListField( + "sub_items", [], + DICOMVariableItem, + max_count=8, + length_from=lambda pkt: ( + pkt.underlayer.length - 4 + if pkt.underlayer and pkt.underlayer.length + else 0 + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return self.sprintf( + "PresentationContext-AC ctx_id=%context_id% result=%result%" + ) + + +class DICOMMaximumLength(Packet): + """DICOM Maximum Length sub-item.""" + + name = "DICOM Maximum Length" + fields_desc = [ + IntField("max_pdu_length", 16384), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "MaxLength %d" % self.max_pdu_length + + +class DICOMImplementationClassUID(Packet): + """DICOM Implementation Class UID sub-item.""" + + name = "DICOM Implementation Class UID" + fields_desc = [ + StrLenField( + "uid", b"", + length_from=lambda pkt: ( + pkt.underlayer.length + if pkt.underlayer and pkt.underlayer.length + else len(pkt.uid) + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "ImplClassUID %s" % self.uid.decode("ascii").rstrip("\x00") + + +class DICOMImplementationVersionName(Packet): + """DICOM Implementation Version Name sub-item.""" + + name = "DICOM Implementation Version Name" + fields_desc = [ + StrLenField( + "name", b"", + length_from=lambda pkt: ( + pkt.underlayer.length + if pkt.underlayer and pkt.underlayer.length + else len(pkt.name) + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "ImplVersion %s" % self.name.decode("ascii").rstrip("\x00") + + +class DICOMAsyncOperationsWindow(Packet): + """DICOM Asynchronous Operations Window sub-item.""" + + name = "DICOM Async Operations Window" + fields_desc = [ + ShortField("max_ops_invoked", 1), + ShortField("max_ops_performed", 1), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "AsyncOps inv=%d perf=%d" % ( + self.max_ops_invoked, self.max_ops_performed + ) + + +class DICOMSCPSCURoleSelection(Packet): + """DICOM SCP/SCU Role Selection sub-item.""" + + name = "DICOM SCP/SCU Role Selection" + fields_desc = [ + FieldLenField("uid_length", None, length_of="sop_class_uid", fmt="!H"), + StrLenField("sop_class_uid", b"", + length_from=lambda pkt: pkt.uid_length), + ByteField("scu_role", 0), + ByteField("scp_role", 0), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "RoleSelection SCU=%d SCP=%d" % (self.scu_role, self.scp_role) + + +class DICOMSOPClassExtendedNegotiation(Packet): + """DICOM SOP Class Extended Negotiation sub-item (PS3.7 D.3.3.5).""" + + name = "DICOM SOP Class Extended Negotiation" + fields_desc = [ + FieldLenField("sop_class_uid_length", None, + length_of="sop_class_uid", fmt="!H"), + StrLenField("sop_class_uid", b"", + length_from=lambda pkt: pkt.sop_class_uid_length), + StrLenField("service_class_application_information", b"", + length_from=lambda pkt: ( + pkt.underlayer.length - 2 - pkt.sop_class_uid_length + if pkt.underlayer and pkt.underlayer.length + else 0 + )), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "SOPClassExtNeg %s" % self.sop_class_uid.decode("ascii").rstrip("\x00") + + +class DICOMSOPClassCommonExtendedNegotiation(Packet): + """DICOM SOP Class Common Extended Negotiation sub-item (PS3.7 D.3.3.6).""" + + name = "DICOM SOP Class Common Extended Negotiation" + fields_desc = [ + FieldLenField("sop_class_uid_length", None, + length_of="sop_class_uid", fmt="!H"), + StrLenField("sop_class_uid", b"", + length_from=lambda pkt: pkt.sop_class_uid_length), + FieldLenField("service_class_uid_length", None, + length_of="service_class_uid", fmt="!H"), + StrLenField("service_class_uid", b"", + length_from=lambda pkt: pkt.service_class_uid_length), + FieldLenField("related_sop_class_uid_length", None, + length_of="related_sop_class_uids", fmt="!H"), + StrLenField("related_sop_class_uids", b"", + length_from=lambda pkt: pkt.related_sop_class_uid_length), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + uid = self.sop_class_uid.decode("ascii").rstrip("\x00") + return "SOPClassCommonExtNeg %s" % uid + + +USER_IDENTITY_TYPES = { + 1: "Username", + 2: "Username and Passcode", + 3: "Kerberos Service Ticket", + 4: "SAML Assertion", + 5: "JSON Web Token (JWT)", +} + + +class DICOMUserIdentity(Packet): + """DICOM User Identity sub-item.""" + + name = "DICOM User Identity" + fields_desc = [ + ByteEnumField("user_identity_type", 1, USER_IDENTITY_TYPES), + ByteField("positive_response_requested", 0), + FieldLenField("primary_field_length", None, + length_of="primary_field", fmt="!H"), + StrLenField("primary_field", b"", + length_from=lambda pkt: pkt.primary_field_length), + ConditionalField( + FieldLenField("secondary_field_length", None, + length_of="secondary_field", fmt="!H"), + lambda pkt: pkt.user_identity_type == 2 + ), + ConditionalField( + StrLenField("secondary_field", b"", + length_from=lambda pkt: pkt.secondary_field_length), + lambda pkt: pkt.user_identity_type == 2 + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return self.sprintf("UserIdentity %user_identity_type%") + + +class DICOMUserIdentityResponse(Packet): + """DICOM User Identity Server Response sub-item.""" + + name = "DICOM User Identity Response" + fields_desc = [ + FieldLenField("response_length", None, + length_of="server_response", fmt="!H"), + StrLenField("server_response", b"", + length_from=lambda pkt: pkt.response_length), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "UserIdentityResponse" + + +class DICOMUserInformation(Packet): + """DICOM User Information item.""" + + name = "DICOM User Information" + fields_desc = [ + PacketListField( + "sub_items", [], + DICOMVariableItem, + max_count=32, + length_from=lambda pkt: ( + pkt.underlayer.length + if pkt.underlayer and pkt.underlayer.length + else 0 + ) + ), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + return "UserInfo (%d items)" % len(self.sub_items) + + +bind_layers(DICOMVariableItem, DICOMApplicationContext, item_type=0x10) +bind_layers(DICOMVariableItem, DICOMPresentationContextRQ, item_type=0x20) +bind_layers(DICOMVariableItem, DICOMPresentationContextAC, item_type=0x21) +bind_layers(DICOMVariableItem, DICOMAbstractSyntax, item_type=0x30) +bind_layers(DICOMVariableItem, DICOMTransferSyntax, item_type=0x40) +bind_layers(DICOMVariableItem, DICOMUserInformation, item_type=0x50) +bind_layers(DICOMVariableItem, DICOMMaximumLength, item_type=0x51) +bind_layers(DICOMVariableItem, DICOMImplementationClassUID, item_type=0x52) +bind_layers(DICOMVariableItem, DICOMAsyncOperationsWindow, item_type=0x53) +bind_layers(DICOMVariableItem, DICOMSCPSCURoleSelection, item_type=0x54) +bind_layers(DICOMVariableItem, DICOMImplementationVersionName, item_type=0x55) +bind_layers(DICOMVariableItem, DICOMSOPClassExtendedNegotiation, item_type=0x56) +bind_layers(DICOMVariableItem, DICOMSOPClassCommonExtendedNegotiation, item_type=0x57) +bind_layers(DICOMVariableItem, DICOMUserIdentity, item_type=0x58) +bind_layers(DICOMVariableItem, DICOMUserIdentityResponse, item_type=0x59) +bind_layers(DICOMVariableItem, DICOMGenericItem) + + +class DICOM(Packet): + """DICOM Upper Layer (UL) PDU header.""" + + name = "DICOM UL" + fields_desc = [ + ByteEnumField("pdu_type", 0x01, PDU_TYPES), + ByteField("reserved1", 0), + LenField("length", None, fmt="!I"), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + if self.length is not None: + return s[:self.length], s[self.length:] + return s, b"" + + def mysummary(self) -> str: + return self.sprintf("DICOM %pdu_type%") + + +class PresentationDataValueItem(Packet): + """Presentation Data Value (PDV) item within P-DATA-TF PDU.""" + + name = "PresentationDataValueItem" + fields_desc = [ + FieldLenField("length", None, length_of="data", fmt="!I", + adjust=lambda pkt, x: x + 2), + ByteField("context_id", 1), + BitField("reserved_bits", 0, 6), + BitField("is_last", 0, 1), + BitField("is_command", 0, 1), + StrLenField("data", b"", + length_from=lambda pkt: max(0, (pkt.length or 2) - 2)), + ] + + def extract_padding(self, s: bytes) -> Tuple[bytes, bytes]: + return b"", s + + def mysummary(self) -> str: + cmd_or_data = "CMD" if self.is_command else "DATA" + last = " LAST" if self.is_last else "" + return "PDV ctx=%d %s%s len=%d" % ( + self.context_id, cmd_or_data, last, len(self.data) + ) + + +class A_ASSOCIATE_RQ(Packet): + """A-ASSOCIATE-RQ PDU for initiating DICOM associations.""" + + name = "A-ASSOCIATE-RQ" + fields_desc = [ + ShortField("protocol_version", 1), + ShortField("reserved1", 0), + DICOMAETitleField("called_ae_title", b""), + DICOMAETitleField("calling_ae_title", b""), + StrFixedLenField("reserved2", b"\x00" * 32, 32), + PacketListField( + "variable_items", [], + DICOMVariableItem, + max_count=256, + length_from=lambda pkt: ( + pkt.underlayer.length - 68 + if pkt.underlayer and pkt.underlayer.length + else 0 + ) + ), + ] + + def mysummary(self) -> str: + called = self.called_ae_title.strip().decode("ascii", errors="replace") + calling = self.calling_ae_title.strip().decode("ascii", errors="replace") + return "A-ASSOCIATE-RQ %s -> %s" % (calling, called) + + def hashret(self) -> bytes: + return self.called_ae_title + self.calling_ae_title + + +class A_ASSOCIATE_AC(Packet): + """A-ASSOCIATE-AC PDU for accepting DICOM associations.""" + + name = "A-ASSOCIATE-AC" + fields_desc = [ + ShortField("protocol_version", 1), + ShortField("reserved1", 0), + DICOMAETitleField("called_ae_title", b""), + DICOMAETitleField("calling_ae_title", b""), + StrFixedLenField("reserved2", b"\x00" * 32, 32), + PacketListField( + "variable_items", [], + DICOMVariableItem, + max_count=256, + length_from=lambda pkt: ( + pkt.underlayer.length - 68 + if pkt.underlayer and pkt.underlayer.length + else 0 + ) + ), + ] + + def mysummary(self) -> str: + called = self.called_ae_title.strip().decode("ascii", errors="replace") + calling = self.calling_ae_title.strip().decode("ascii", errors="replace") + return "A-ASSOCIATE-AC %s <- %s" % (calling, called) + + def hashret(self) -> bytes: + return self.called_ae_title + self.calling_ae_title + + def answers(self, other: Packet) -> bool: + return isinstance(other, A_ASSOCIATE_RQ) + + +class A_ASSOCIATE_RJ(Packet): + """A-ASSOCIATE-RJ PDU for rejecting DICOM associations.""" + + name = "A-ASSOCIATE-RJ" + + RESULT_CODES = { + 1: "rejected-permanent", + 2: "rejected-transient", + } + + SOURCE_CODES = { + 1: "DICOM UL service-user", + 2: "DICOM UL service-provider (ACSE)", + 3: "DICOM UL service-provider (Presentation)", + } + + fields_desc = [ + ByteField("reserved1", 0), + ByteEnumField("result", 1, RESULT_CODES), + ByteEnumField("source", 1, SOURCE_CODES), + ByteField("reason_diag", 1), + ] + + def mysummary(self) -> str: + return self.sprintf("A-ASSOCIATE-RJ %result% %source%") + + def answers(self, other: Packet) -> bool: + return isinstance(other, A_ASSOCIATE_RQ) + + +class P_DATA_TF(Packet): + """P-DATA-TF PDU for transferring DICOM data.""" + + name = "P-DATA-TF" + fields_desc = [ + PacketListField( + "pdv_items", [], + PresentationDataValueItem, + max_count=256, + length_from=lambda pkt: ( + pkt.underlayer.length + if pkt.underlayer and pkt.underlayer.length + else 0 + ) + ), + ] + + def mysummary(self) -> str: + return "P-DATA-TF (%d PDVs)" % len(self.pdv_items) + + +class A_RELEASE_RQ(Packet): + """A-RELEASE-RQ PDU for requesting association release.""" + + name = "A-RELEASE-RQ" + fields_desc = [IntField("reserved1", 0)] + + def mysummary(self) -> str: + return "A-RELEASE-RQ" + + +class A_RELEASE_RP(Packet): + """A-RELEASE-RP PDU for confirming association release.""" + + name = "A-RELEASE-RP" + fields_desc = [IntField("reserved1", 0)] + + def mysummary(self) -> str: + return "A-RELEASE-RP" + + def answers(self, other: Packet) -> bool: + return isinstance(other, A_RELEASE_RQ) + + +class A_ABORT(Packet): + """A-ABORT PDU for aborting DICOM associations.""" + + name = "A-ABORT" + + SOURCE_CODES = { + 0: "DICOM UL service-user", + 2: "DICOM UL service-provider", + } + + fields_desc = [ + ByteField("reserved1", 0), + ByteField("reserved2", 0), + ByteEnumField("source", 0, SOURCE_CODES), + ByteField("reason_diag", 0), + ] + + def mysummary(self) -> str: + return self.sprintf("A-ABORT %source%") + + +bind_layers(TCP, DICOM, dport=DICOM_PORT) +bind_layers(TCP, DICOM, sport=DICOM_PORT) +bind_layers(TCP, DICOM, dport=DICOM_PORT_ALT) +bind_layers(TCP, DICOM, sport=DICOM_PORT_ALT) +bind_layers(DICOM, A_ASSOCIATE_RQ, pdu_type=0x01) +bind_layers(DICOM, A_ASSOCIATE_AC, pdu_type=0x02) +bind_layers(DICOM, A_ASSOCIATE_RJ, pdu_type=0x03) +bind_layers(DICOM, P_DATA_TF, pdu_type=0x04) +bind_layers(DICOM, A_RELEASE_RQ, pdu_type=0x05) +bind_layers(DICOM, A_RELEASE_RP, pdu_type=0x06) +bind_layers(DICOM, A_ABORT, pdu_type=0x07) + + +def build_presentation_context_rq(context_id: int, + abstract_syntax_uid: str, + transfer_syntax_uids: List[str]) -> Packet: + """Build a Presentation Context RQ item.""" + abs_uid = _uid_to_bytes(abstract_syntax_uid) + abs_syn = DICOMVariableItem() / DICOMAbstractSyntax(uid=abs_uid) + + sub_items = [abs_syn] + for ts_uid in transfer_syntax_uids: + ts = DICOMVariableItem() / DICOMTransferSyntax(uid=_uid_to_bytes(ts_uid)) + sub_items.append(ts) + + return DICOMVariableItem() / DICOMPresentationContextRQ( + context_id=context_id, + sub_items=sub_items, + ) + + +def build_user_information(max_pdu_length: int = 16384, + implementation_class_uid: Optional[str] = None, + implementation_version: Optional[Union[str, bytes]] = None + ) -> Packet: + """Build a User Information item.""" + sub_items = [ + DICOMVariableItem() / DICOMMaximumLength(max_pdu_length=max_pdu_length) + ] + + if implementation_class_uid: + uid = _uid_to_bytes(implementation_class_uid) + sub_items.append( + DICOMVariableItem() / DICOMImplementationClassUID(uid=uid) + ) + + if implementation_version: + if isinstance(implementation_version, bytes): + ver_bytes = implementation_version + else: + ver_bytes = implementation_version.encode('ascii') + sub_items.append( + DICOMVariableItem() / DICOMImplementationVersionName(name=ver_bytes) + ) + + return DICOMVariableItem() / DICOMUserInformation(sub_items=sub_items) + + +class DICOMSocket: + """DICOM application-layer socket for associations and DIMSE operations.""" + + def __init__(self, dst_ip: str, dst_port: int, dst_ae: str, + src_ae: str = "SCAPY_SCU", read_timeout: int = 10) -> None: + self.dst_ip = dst_ip + self.dst_port = dst_port + self.dst_ae = dst_ae + self.src_ae = src_ae + self.sock: Optional[socket.socket] = None + self.stream: Optional[StreamSocket] = None + self.assoc_established = False + self.accepted_contexts: Dict[int, Tuple[str, str]] = {} + self.read_timeout = read_timeout + self._current_message_id_counter = int(time.time()) % 50000 + self._proposed_max_pdu = 16384 + self.max_pdu_length = 16384 + self._proposed_context_map: Dict[int, str] = {} + + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: + if self.assoc_established: + try: + self.release() + except (socket.error, socket.timeout, OSError): + pass + self.close() + return False + + def connect(self) -> bool: + try: + self.sock = socket.create_connection( + (self.dst_ip, self.dst_port), + timeout=self.read_timeout, + ) + self.stream = StreamSocket(self.sock, basecls=DICOM) + return True + except (socket.error, socket.timeout, OSError) as e: + log.error("Connection failed: %s", e) + return False + + def send(self, pkt: Packet) -> None: + self.stream.send(pkt) + + def recv(self) -> Optional[Packet]: + try: + return self.stream.recv() + except socket.timeout: + return None + except (socket.error, OSError) as e: + log.error("Error receiving PDU: %s", e) + return None + + def sr1(self, *args, **kargs): + # type: (*Any, **Any) -> Optional[Packet] + """Send one packet and receive one answer.""" + timeout = kargs.pop("timeout", self.read_timeout) + try: + return self.stream.sr1(*args, timeout=timeout, **kargs) + except (socket.error, OSError) as e: + log.error("Error in sr1: %s", e) + return None + + def send_raw_bytes(self, raw_bytes: bytes) -> None: + self.sock.sendall(raw_bytes) + + def associate(self, requested_contexts: Optional[Dict[str, List[str]]] = None + ) -> bool: + if not self.stream and not self.connect(): + return False + + if requested_contexts is None: + requested_contexts = { + VERIFICATION_SOP_CLASS_UID: [DEFAULT_TRANSFER_SYNTAX_UID] + } + + self._proposed_context_map = {} + + variable_items: List[Packet] = [ + DICOMVariableItem() / DICOMApplicationContext() + ] + + ctx_id = 1 + for abs_syntax, trn_syntaxes in requested_contexts.items(): + self._proposed_context_map[ctx_id] = abs_syntax + pctx = build_presentation_context_rq(ctx_id, abs_syntax, trn_syntaxes) + variable_items.append(pctx) + ctx_id += 2 + + user_info = build_user_information(max_pdu_length=self._proposed_max_pdu) + variable_items.append(user_info) + + assoc_rq = DICOM() / A_ASSOCIATE_RQ( + called_ae_title=self.dst_ae, + calling_ae_title=self.src_ae, + variable_items=variable_items, + ) + + response = self.sr1(assoc_rq) + + if response: + if response.haslayer(A_ASSOCIATE_AC): + self.assoc_established = True + self._parse_accepted_contexts(response) + self._parse_max_pdu_length(response) + return True + elif response.haslayer(A_ASSOCIATE_RJ): + log.error( + "Association rejected: result=%d, source=%d, reason=%d", + response[A_ASSOCIATE_RJ].result, + response[A_ASSOCIATE_RJ].source, + response[A_ASSOCIATE_RJ].reason_diag, + ) + return False + + log.error("Association failed: no valid response received") + return False + + def _parse_max_pdu_length(self, response: Packet) -> None: + try: + for item in response[A_ASSOCIATE_AC].variable_items: + if item.item_type != 0x50: + continue + if not item.haslayer(DICOMUserInformation): + continue + user_info = item[DICOMUserInformation] + for sub_item in user_info.sub_items: + if sub_item.item_type != 0x51: + continue + if not sub_item.haslayer(DICOMMaximumLength): + continue + max_len = sub_item[DICOMMaximumLength] + server_max = max_len.max_pdu_length + self.max_pdu_length = min( + self._proposed_max_pdu, server_max + ) + return + except (KeyError, IndexError, AttributeError): + pass + self.max_pdu_length = self._proposed_max_pdu + + def _parse_accepted_contexts(self, response: Packet) -> None: + for item in response[A_ASSOCIATE_AC].variable_items: + if item.item_type != 0x21: + continue + if not item.haslayer(DICOMPresentationContextAC): + continue + pctx = item[DICOMPresentationContextAC] + ctx_id = pctx.context_id + result = pctx.result + + if result != 0: + continue + + abs_syntax = self._proposed_context_map.get(ctx_id) + if abs_syntax is None: + continue + + for sub_item in pctx.sub_items: + if sub_item.item_type != 0x40: + continue + if not sub_item.haslayer(DICOMTransferSyntax): + continue + ts_uid = sub_item[DICOMTransferSyntax].uid + ts_uid = ts_uid.rstrip(b"\x00").decode("ascii") + self.accepted_contexts[ctx_id] = (abs_syntax, ts_uid) + break + + def _get_next_message_id(self) -> int: + self._current_message_id_counter += 1 + return self._current_message_id_counter & 0xFFFF + + def _find_accepted_context_id(self, sop_class_uid: str, + transfer_syntax_uid: Optional[str] = None + ) -> Optional[int]: + for ctx_id, (abs_syntax, ts_syntax) in self.accepted_contexts.items(): + if abs_syntax == sop_class_uid: + if transfer_syntax_uid is None or transfer_syntax_uid == ts_syntax: + return ctx_id + return None + + def c_echo(self) -> Optional[int]: + if not self.assoc_established: + log.error("Association not established") + return None + + echo_ctx_id = self._find_accepted_context_id(VERIFICATION_SOP_CLASS_UID) + if echo_ctx_id is None: + log.error("No accepted context for Verification SOP Class") + return None + + msg_id = self._get_next_message_id() + dimse_rq = bytes(C_ECHO_RQ(message_id=msg_id)) + + pdv_rq = PresentationDataValueItem( + context_id=echo_ctx_id, + data=dimse_rq, + is_command=1, + is_last=1, + ) + pdata_rq = DICOM() / P_DATA_TF(pdv_items=[pdv_rq]) + + response = self.sr1(pdata_rq) + + if response and response.haslayer(P_DATA_TF): + pdv_items = response[P_DATA_TF].pdv_items + if pdv_items: + pdv_rsp = pdv_items[0] + return parse_dimse_status(pdv_rsp.data) + return None + + def c_store(self, dataset_bytes: bytes, sop_class_uid: str, + sop_instance_uid: str, transfer_syntax_uid: str + ) -> Optional[int]: + if not self.assoc_established: + log.error("Association not established") + return None + + store_ctx_id = self._find_accepted_context_id( + sop_class_uid, + transfer_syntax_uid, + ) + if store_ctx_id is None: + log.error( + "No accepted context for SOP %s with TS %s", + sop_class_uid, + transfer_syntax_uid, + ) + return None + + msg_id = self._get_next_message_id() + + dimse_rq = bytes(C_STORE_RQ( + affected_sop_class_uid=sop_class_uid, + affected_sop_instance_uid=sop_instance_uid, + message_id=msg_id, + )) + + cmd_pdv = PresentationDataValueItem( + context_id=store_ctx_id, + data=dimse_rq, + is_command=1, + is_last=1, + ) + pdata_cmd = DICOM() / P_DATA_TF(pdv_items=[cmd_pdv]) + self.send(pdata_cmd) + + max_pdv_data = self.max_pdu_length - 12 + + if len(dataset_bytes) <= max_pdv_data: + data_pdv = PresentationDataValueItem( + context_id=store_ctx_id, + data=dataset_bytes, + is_command=0, + is_last=1, + ) + pdata_data = DICOM() / P_DATA_TF(pdv_items=[data_pdv]) + self.send(pdata_data) + else: + offset = 0 + while offset < len(dataset_bytes): + chunk = dataset_bytes[offset:offset + max_pdv_data] + is_last = 1 if (offset + len(chunk) >= len(dataset_bytes)) else 0 + data_pdv = PresentationDataValueItem( + context_id=store_ctx_id, + data=chunk, + is_command=0, + is_last=is_last, + ) + pdata_data = DICOM() / P_DATA_TF(pdv_items=[data_pdv]) + self.send(pdata_data) + offset += len(chunk) + + response = self.recv() + + if response and response.haslayer(P_DATA_TF): + pdv_items = response[P_DATA_TF].pdv_items + if pdv_items: + pdv_rsp = pdv_items[0] + return parse_dimse_status(pdv_rsp.data) + return None + + def release(self) -> bool: + if not self.assoc_established: + return True + + release_rq = DICOM() / A_RELEASE_RQ() + response = self.sr1(release_rq) + self.close() + + if response: + return response.haslayer(A_RELEASE_RP) + return False + + def close(self) -> None: + if self.stream: + try: + self.stream.close() + except (socket.error, OSError): + pass + self.sock = None + self.stream = None + self.assoc_established = False diff --git a/scapy/contrib/dtp.py b/scapy/contrib/dtp.py index 513a9a1d882..603d16e7a34 100644 --- a/scapy/contrib/dtp.py +++ b/scapy/contrib/dtp.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Dynamic Trunking Protocol (DTP) # scapy.contrib.status = loads @@ -27,8 +17,6 @@ - TLV code derived from the CDP implementation of scapy. (Thanks to Nicolas Bareil and Arnaud Ebalard) # noqa: E501 """ -from __future__ import absolute_import -from __future__ import print_function import struct from scapy.packet import Packet, bind_layers diff --git a/scapy/contrib/eddystone.py b/scapy/contrib/eddystone.py index 62406697e23..554f8b3bdb6 100644 --- a/scapy/contrib/eddystone.py +++ b/scapy/contrib/eddystone.py @@ -1,10 +1,7 @@ -# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*- -# eddystone.py - protocol handlers for Eddystone beacons -# +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Michael Farrell -# This program is published under a GPLv2 (or later) license # # scapy.contrib.description = Eddystone BLE proximity beacon # scapy.contrib.status = loads @@ -27,7 +24,6 @@ StrFixedLenField, ShortField, FixedPointField, ByteEnumField from scapy.layers.bluetooth import EIR_Hdr, EIR_ServiceData16BitUUID, \ EIR_CompleteList16BitServiceUUIDs, LowEnergyBeaconHelper -from scapy.modules import six from scapy.packet import bind_layers, Packet EDDYSTONE_UUID = 0xfeaa @@ -97,7 +93,7 @@ def m2i(self, pkt, x): return bytes(o) def any2i(self, pkt, x): - if isinstance(x, six.text_type): + if isinstance(x, str): x = x.encode("ascii") return x diff --git a/scapy/contrib/eigrp.py b/scapy/contrib/eigrp.py index fc6e11b7cdd..6bfb1b5aa78 100644 --- a/scapy/contrib/eigrp.py +++ b/scapy/contrib/eigrp.py @@ -1,16 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright (C) 2009 Jochen Bartl # scapy.contrib.description = Enhanced Interior Gateway Routing Protocol (EIGRP) # scapy.contrib.status = loads @@ -20,9 +11,7 @@ ~~~~~~~~~~~~~~~~~~~~~ :version: 2009-08-13 - :copyright: 2009 by Jochen Bartl :e-mail: lobo@c3a.de / jochen.bartl@gmail.com - :license: GPL v2 :TODO @@ -30,28 +19,23 @@ * http://trac.secdev.org/scapy/ticket/90 - Write function for calculating authentication data - :Known bugs: - - - - :Thanks: - TLV code derived from the CDP implementation of scapy. (Thanks to Nicolas Bareil and Arnaud Ebalard) http://trac.secdev.org/scapy/ticket/18 - IOS / EIGRP Version Representation FIX by Dirk Loss """ -from __future__ import absolute_import import socket import struct from scapy.packet import Packet from scapy.fields import StrField, IPField, XShortField, FieldLenField, \ StrLenField, IntField, ByteEnumField, ByteField, ConditionalField, \ - FlagsField, IP6Field, PacketField, PacketListField, ShortEnumField, \ + FlagsField, IP6Field, PacketListField, ShortEnumField, \ ShortField, StrFixedLenField, ThreeBytesField from scapy.layers.inet import IP, checksum, bind_layers from scapy.layers.inet6 import IPv6 -from scapy.compat import chb, raw +from scapy.compat import chb from scapy.config import conf from scapy.utils import inet_aton, inet_ntoa from scapy.pton_ntop import inet_ntop, inet_pton @@ -279,8 +263,9 @@ def i2repr(self, pkt, x): def h2i(self, pkt, x): """The field accepts string values like v12.1, v1.1 or integer values. - String values have to start with a "v" folled by a floating point number. - Valid numbers are between 0 and 255. + String values have to start with a "v" followed by a + floating point number. Valid numbers are between 0 and 255. + """ if isinstance(x, str) and x.startswith("v") and len(x) <= 8: @@ -295,7 +280,7 @@ def h2i(self, pkt, x): if not hasattr(self, "default"): return x if self.default is not None: - warning("set value to default. Format of %r is invalid" % x) + warning("set value to default. Format of %r is invalid", x) return self.default else: raise Scapy_Exception("Format of value is invalid") @@ -448,28 +433,6 @@ class EIGRPv6ExtRoute(EIGRPGeneric): } -class RepeatedTlvListField(PacketListField): - def __init__(self, name, default, cls): - PacketField.__init__(self, name, default, cls) - - def getfield(self, pkt, s): - lst = [] - remain = s - while len(remain) > 0: - p = self.m2i(pkt, remain) - if conf.padding_layer in p: - pad = p[conf.padding_layer] - remain = pad.load - del(pad.underlayer.payload) - else: - remain = b"" - lst.append(p) - return remain, lst - - def addfield(self, pkt, s, val): - return s + b"".join(raw(v) for v in val) - - def _EIGRPGuessPayloadClass(p, **kargs): cls = conf.raw_layer if len(p) >= 2: @@ -503,7 +466,7 @@ class EIGRP(Packet): IntField("seq", 0), IntField("ack", 0), IntField("asn", 100), - RepeatedTlvListField("tlvlist", [], _EIGRPGuessPayloadClass) + PacketListField("tlvlist", [], _EIGRPGuessPayloadClass) ] def post_build(self, p, pay): diff --git a/scapy/contrib/enipTCP.py b/scapy/contrib/enipTCP.py index bfd19fa25a7..e35d2e27f67 100644 --- a/scapy/contrib/enipTCP.py +++ b/scapy/contrib/enipTCP.py @@ -1,36 +1,29 @@ -# coding: utf8 - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright (C) 2019 Jose Diogo Monteiro +# Updated (C) 2023 Claire Vacherot # scapy.contrib.description = EtherNet/IP # scapy.contrib.status = loads -# Copyright (C) 2019 Jose Diogo Monteiro -# Based on https://github.com/scy-phy/scapy-cip-enip -# Routines for EtherNet/IP (Industrial Protocol) dissection -# EtherNet/IP Home: www.odva.org +""" +EtherNet/IP (Industrial Protocol) + +Based on https://github.com/scy-phy/scapy-cip-enip +EtherNet/IP Home: www.odva.org +""" import struct from scapy.packet import Packet, bind_layers from scapy.layers.inet import TCP from scapy.fields import LEShortField, LEShortEnumField, LEIntEnumField, \ LEIntField, LELongField, FieldLenField, PacketListField, ByteField, \ - PacketField, MultipleTypeField, StrLenField, StrFixedLenField, \ - XLEIntField, XLEStrLenField + StrLenField, StrFixedLenField, XLEIntField, XLEStrLenField, \ + LEFieldLenField, ShortField, IPField, LongField, XLEShortField _commandIdList = { + 0x0001: "UnknownCommand", 0x0004: "ListServices", # Request Struct Don't Have Command Spec Data 0x0063: "ListIdentity", # Request Struct Don't Have Command Spec Data 0x0064: "ListInterfaces", # Request Struct Don't Have Command Spec Data @@ -52,13 +45,72 @@ 105: "unsupported_prot_rev" } -_itemID = { +_typeIdList = { 0x0000: "Null Address Item", - 0x00a1: "Connection-based Address Item", - 0x00b1: "Connected Transport packet Data Item", - 0x00b2: "Unconnected message Data Item", - 0x8000: "Sockaddr Info, originator-to-target Data Item", - 0x8001: "Sockaddr Info, target-to-originator Data Item" + 0x000c: "CIP Identity", + 0x0086: "CIP Security Information", + 0x0087: "EtherNet/IP Capability", + 0x0088: "EtherNet/IP Usage", + 0x00a1: "Connected Address Item", + 0x00B1: "Connected Data Item", + 0x00B2: "Unconnected Data Item", + 0x0100: "List Services Response", + 0x8000: "Socket Address Info O->T", + 0x8001: "Socket Address Info T->O", + 0x8002: "Sequenced Address Item", + 0x8003: "Unconnected Message over UDP" +} + +_deviceTypeList = { + 0x0000: "Generic Device (deprecated)", + 0x0002: "AC Drive", + 0x0003: "Motor Overload", + 0x0004: "Limit Switch", + 0x0005: "Inductive Proximity Switch", + 0x0006: "Photoelectric Sensor", + 0x0007: "General Purpose Discrete I/O", + 0x0009: "Resolver", + 0x000C: "Communications Adapter", + 0x000E: "Programmable Logic Controller", + 0x0010: "Position Controller", + 0x0013: "DC Drive", + 0x0015: "Contactor", + 0x0016: "Motor Starter", + 0x0017: "Soft Start", + 0x0018: "Human-Machine Interface", + 0x001A: "Mass Flow Controller", + 0x001B: "Pneumatic Valve", + 0x001C: "Vacuum Pressure Gauge", + 0x001D: "Process Control Value", + 0x001E: "Residual Gas Analyzer", + 0x001F: "DC Power Generator", + 0x0020: "RF Power Generator", + 0x0021: "Turbomolecular Vacuum Pump", + 0x0022: "Encoder", + 0x0023: "Safety Discrete I/O Device", + 0x0024: "Fluid Flow Controller", + 0x0025: "CIP Motion Drive", + 0x0026: "CompoNet Repeater", + 0x0027: "Mass Flow Controller, Enhanced", + 0x0028: "CIP Modbus Device", + 0x0029: "CIP Modbus Translator", + 0x002A: "Safety Analog I/O Device", + 0x002B: "Generic Device (keyable)", + 0x002C: "Managed Ethernet Switch", + 0x002D: "CIP Motion Safety Drive Device", + 0x002E: "Safety Drive Device", + 0x002F: "CIP Motion Encoder", + 0x0030: "CIP Motion Converter", + 0x0031: "CIP Motion I/O", + 0x0032: "ControlNet Physical Layer Component", + 0x0033: "Circuit Breaker", + 0x0034: "HART Device", + 0x0035: "CIP-HART Translator", + 0x00C8: "Embedded Component", +} + +_interfaceList = { + 0x00: "CIP" } @@ -66,7 +118,7 @@ class ItemData(Packet): """Common Packet Format""" name = "Item Data" fields_desc = [ - LEShortEnumField("typeId", 0, _itemID), + LEShortEnumField("typeId", 0, _typeIdList), LEShortField("length", 0), XLEStrLenField("data", "", length_from=lambda pkt: pkt.length), ] @@ -75,97 +127,105 @@ def extract_padding(self, s): return '', s -class EncapsulatedPacket(Packet): - """Encapsulated Packet""" - name = "Encapsulated Packet" - fields_desc = [LEShortField("itemCount", 2), PacketListField( - "item", None, ItemData, count_from=lambda pkt: pkt.itemCount), ] +# Unknown command (0x0001) -class BaseSendPacket(Packet): - """ Abstract Class""" - fields_desc = [ - LEIntField("interfaceHandle", 0), - LEShortField("timeout", 0), - PacketField("encapsulatedPacket", None, EncapsulatedPacket), - ] - - -class CommandSpecificData(Packet): - """Command Specific Data Field Default""" +class ENIPUnknownCommand(Packet): + """Unknown Command reply""" + name = "ENIPUnknownCommand" pass -class ENIPSendUnitData(BaseSendPacket): - """Send Unit Data Command Field""" - name = "ENIPSendUnitData" +# List services (0x0004) -class ENIPSendRRData(BaseSendPacket): - """Send RR Data Command Field""" - name = "ENIPSendRRData" - - -class ENIPListInterfacesReplyItems(Packet): - """List Interfaces Items Field""" - name = "ENIPListInterfacesReplyItems" +class ENIPListServicesItem(Packet): + """List Services Item Field""" + name = "ENIPListServicesItem" fields_desc = [ - LEIntField("itemTypeCode", 0), - FieldLenField("itemLength", 0, length_of="itemData"), - StrLenField("itemData", "", length_from=lambda pkt: pkt.itemLength), + LEShortEnumField("itemTypeCode", 0, _typeIdList), + LEFieldLenField("itemLength", 0), + LEShortField("protocolVersion", 0), + XLEShortField("flag", 0), # TODO: detail with BitFields + StrFixedLenField("serviceName", None, 16), ] -class ENIPListInterfacesReply(Packet): - """List Interfaces Command Field""" - name = "ENIPListInterfacesReply" +class ENIPListServices(Packet): + """List Services Command Field""" + name = "ENIPListServices" fields_desc = [ - FieldLenField("itemCount", 0, count_of="identityItems"), - PacketField("identityItems", 0, ENIPListInterfacesReplyItems), + FieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ENIPListServicesItem), ] -class ENIPListIdentityReplyItems(Packet): - """List Identity Items Field""" - name = "ENIPListIdentityReplyItems" +# List identity (0x0063) + + +class ENIPListIdentityItem(Packet): + """List Identity Item Fields""" + name = "ENIPListIdentityReplyItem" fields_desc = [ - LEIntField("itemTypeCode", 0), - FieldLenField("itemLength", 0, length_of="itemData"), - StrLenField("itemData", "", length_from=lambda pkt: pkt.item_length), + LEShortEnumField("itemTypeCode", 0, _typeIdList), + LEFieldLenField("itemLength", 0), + LEShortField("protocolVersion", 0), + # Socket address + ShortField("sinFamily", 0), + ShortField("sinPort", 0), + IPField("sinAddress", None), + LongField("sinZero", 0), + # End socket address + LEShortField("vendorId", 0), # Vendor list could be added (long list) + LEShortEnumField("deviceType", 0, _deviceTypeList), + LEShortField("productCode", 0), + ByteField("revisionMajor", 0), + ByteField("revisionMinor", 0), + LEShortField("status", 0), + XLEIntField("serialNumber", 0), + ByteField("productNameLength", 0), + StrLenField("productName", None, + length_from=lambda pkt: pkt.productNameLength), + ByteField("state", 0) ] -class ENIPListIdentityReply(Packet): - """List Identity Command Field""" - name = "ENIPListIdentityReply" +class ENIPListIdentity(Packet): + """List identity request and response""" + name = "ENIPListIdentity" fields_desc = [ - FieldLenField("itemCount", 0, count_of="identityItems"), - PacketField("identityItems", None, ENIPListIdentityReplyItems), + FieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ENIPListIdentityItem) ] -class ENIPListServicesReplyItems(Packet): - """List Services Items Field""" - name = "ENIPListServicesReplyItems" +# List Interfaces (0x0064) + + +class ENIPListInterfacesItem(Packet): + """List Interfaces Item Fields""" + name = "ENIPListInterfacesItem" fields_desc = [ - LEIntField("itemTypeCode", 0), - LEIntField("itemLength", 0), - ByteField("version", 1), - ByteField("flag", 0), - StrFixedLenField("serviceName", None, 16 * 4), + LEShortEnumField("itemTypeCode", 0, _typeIdList), + FieldLenField("itemLength", 0, length_of="itemData"), + # TODO: Could be detailed + StrLenField("itemData", "", length_from=lambda pkt: pkt.itemLength), ] -class ENIPListServicesReply(Packet): - """List Services Command Field""" - name = "ENIPListServicesReply" +class ENIPListInterfaces(Packet): + """List Interfaces Command Field""" + name = "ENIPListInterfaces" fields_desc = [ - FieldLenField("itemCount", 0, count_of="identityItems"), - PacketField("targetItems", None, ENIPListServicesReplyItems), + FieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ENIPListInterfacesItem), ] -class ENIPRegisterSession(CommandSpecificData): +# Register Session (0x0065) + + +class ENIPRegisterSession(Packet): """Register Session Command Field""" name = "ENIPRegisterSession" fields_desc = [ @@ -174,6 +234,47 @@ class ENIPRegisterSession(CommandSpecificData): ] +# Unregister Session (0x0066) -- Requires further testing + + +class ENIPUnregisterSession(Packet): + """Unregister Session Command Field""" + name = "ENIPUnregisterSession" + pass + + +# Send RR Data (0x006f) + + +class ENIPSendRRData(Packet): + """Send RR Data Command Field""" + name = "ENIPSendRRData" + fields_desc = [ + LEIntEnumField("interface", 0, _interfaceList), + LEShortField("timeout", 0xff), + LEFieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ItemData) + # TODO: Send RR Data is usually followed by a CIP packet + ] + + +# Send Unit Data (0x006f) + + +class ENIPSendUnitData(Packet): + """Send Unit Data Command Field""" + name = "ENIPSendUnitData" + fields_desc = [ + LEIntEnumField("interface", 0, _interfaceList), + LEShortField("timeout", 0xff), + LEFieldLenField("itemCount", 0, count_of="items"), + PacketListField("items", None, ItemData) + ] + + +# Main Ethernet/IP packet structure with header + + class ENIPTCP(Packet): """Ethernet/IP packet over TCP""" name = "ENIPTCP" @@ -184,38 +285,6 @@ class ENIPTCP(Packet): LEIntEnumField("status", None, _statusList), LELongField("senderContext", 0), LEIntField("options", 0), - MultipleTypeField( - [ - # List Services Reply - (PacketField("commandSpecificData", ENIPListServicesReply, - ENIPListServicesReply), - lambda pkt: pkt.commandId == 0x4), - # List Identity Reply - (PacketField("commandSpecificData", ENIPListIdentityReply, - ENIPListIdentityReply), - lambda pkt: pkt.commandId == 0x63), - # List Interfaces Reply - (PacketField("commandSpecificData", ENIPListInterfacesReply, - ENIPListInterfacesReply), - lambda pkt: pkt.commandId == 0x64), - # Register Session - (PacketField("commandSpecificData", ENIPRegisterSession, - ENIPRegisterSession), - lambda pkt: pkt.commandId == 0x65), - # Send RR Data - (PacketField("commandSpecificData", ENIPSendRRData, - ENIPSendRRData), - lambda pkt: pkt.commandId == 0x6f), - # Send Unit Data - (PacketField("commandSpecificData", ENIPSendUnitData, - ENIPSendUnitData), - lambda pkt: pkt.commandId == 0x70), - ], - PacketField( - "commandSpecificData", - None, - CommandSpecificData) # By default - ), ] def post_build(self, pkt, pay): @@ -226,3 +295,12 @@ def post_build(self, pkt, pay): bind_layers(TCP, ENIPTCP, dport=44818) bind_layers(TCP, ENIPTCP, sport=44818) + +bind_layers(ENIPTCP, ENIPUnknownCommand, commandId=0x0001) +bind_layers(ENIPTCP, ENIPListServices, commandId=0x0004) +bind_layers(ENIPTCP, ENIPListIdentity, commandId=0x0063) +bind_layers(ENIPTCP, ENIPListInterfaces, commandId=0x0064) +bind_layers(ENIPTCP, ENIPRegisterSession, commandId=0x0065) +bind_layers(ENIPTCP, ENIPUnregisterSession, commandId=0x0066) +bind_layers(ENIPTCP, ENIPSendRRData, commandId=0x006f) +bind_layers(ENIPTCP, ENIPSendUnitData, commandId=0x0070) diff --git a/scapy/contrib/erspan.py b/scapy/contrib/erspan.py new file mode 100644 index 00000000000..3ef2d6157fc --- /dev/null +++ b/scapy/contrib/erspan.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +ERSPAN - Encapsulated Remote SPAN + +https://datatracker.ietf.org/doc/html/draft-foschiano-erspan-03 +""" + +# scapy.contrib.description = ERSPAN - Encapsulated Remote SPAN +# scapy.contrib.status = loads + +# This file inspired by scapy-vxlan + +from scapy.packet import Packet, bind_layers +from scapy.fields import BitField, BitEnumField, XIntField, \ + XShortField +from scapy.layers.l2 import Ether, GRE + + +class ERSPAN(Packet): + """ + A generic ERSPAN packet + """ + name = "ERSPAN" + fields_desc = [] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + ver = _pkt[0] >> 4 + if ver == 1: + return ERSPAN_II + elif ver == 2: + return ERSPAN_III + else: + return ERSPAN_I + if cls == ERSPAN: + return ERSPAN_II + return cls + + +class ERSPAN_I(ERSPAN): + name = "ERSPAN I" + match_subclass = True + fields_desc = [] + + +class ERSPAN_II(ERSPAN): + name = "ERSPAN II" + match_subclass = True + fields_desc = [BitField("ver", 1, 4), + BitField("vlan", 0, 12), + BitField("cos", 0, 3), + BitField("en", 0, 2), + BitField("t", 0, 1), + BitField("session_id", 0, 10), + BitField("reserved", 0, 12), + BitField("index", 0, 20), + ] + + +class ERSPAN_III(ERSPAN): + name = "ERSPAN III" + match_subclass = True + fields_desc = [BitField("ver", 2, 4), + BitField("vlan", 0, 12), + BitField("cos", 0, 3), + BitField("bso", 0, 2), + BitField("t", 0, 1), + BitField("session_id", 0, 10), + XIntField("timestamp", 0x00000000), + XShortField("sgt_other", 0x00000000), + BitField("p", 0, 1), + BitEnumField("ft", 0, 5, + {0: "Ethernet", 2: "IP"}), + BitField("hw", 0, 6), + BitField("d", 0, 1), + BitEnumField("gra", 0, 2, + {0: "100us", 1: "100ns", 2: "IEEE 1588"}), + BitField("o", 0, 1) + ] + + +class ERSPAN_PlatformSpecific(Packet): + name = "PlatformSpecific" + fields_desc = [BitField("platf_id", 0, 6), + BitField("info1", 0, 26), + XIntField("info2", 0x00000000)] + + +bind_layers(ERSPAN_I, Ether) +bind_layers(ERSPAN_II, Ether) +bind_layers(ERSPAN_III, Ether, o=0) +bind_layers(ERSPAN_III, ERSPAN_PlatformSpecific, o=1) +bind_layers(ERSPAN_PlatformSpecific, Ether) + +bind_layers(GRE, ERSPAN, proto=0x88be, seqnum_present=0) +bind_layers(GRE, ERSPAN_II, proto=0x88be, seqnum_present=1) +bind_layers(GRE, ERSPAN_III, proto=0x22eb) diff --git a/scapy/contrib/esmc.py b/scapy/contrib/esmc.py new file mode 100644 index 00000000000..728095d1146 --- /dev/null +++ b/scapy/contrib/esmc.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = Ethernet Synchronization Message Channel (ESMC) +# scapy.contrib.status = loads + +from scapy.packet import Packet, bind_layers +from scapy.fields import BitField, ByteField, XByteField, ShortField, XStrFixedLenField # noqa: E501 +from scapy.contrib.slowprot import SlowProtocol +from scapy.compat import orb + + +class ESMC(Packet): + name = "ESMC" + fields_desc = [ + XStrFixedLenField("ituOui", b"\x00\x19\xa7", 3), + ShortField("ituSubtype", 1), + BitField("version", 1, 4), + BitField("event", 0, 1), + BitField("reserved1", 0, 3), + XStrFixedLenField("reserved2", b"\x00" * 3, 3), + ] + + def guess_payload_class(self, payload): + if orb(payload[0]) == 1: + return QLTLV + if orb(payload[0]) == 2: + return EQLTLV + return Packet.guess_payload_class(self, payload) + + +class QLTLV(ESMC): + name = "QLTLV" + fields_desc = [ + ByteField("type", 1), + ShortField("length", 4), + XByteField("ssmCode", 0xf), + ] + + +class EQLTLV(ESMC): + name = "EQLTLV" + fields_desc = [ + ByteField("type", 2), + ShortField("length", 0x14), + XByteField("enhancedSsmCode", 0xFF), + XStrFixedLenField("clockIdentity", b"\x00" * 8, 8), + ByteField("flag", 0), + ByteField("cascaded_eEEcs", 1), + ByteField("cascaded_EEcs", 0), + XStrFixedLenField("reserved", b"\x00" * 5, 5), + ] + + +bind_layers(SlowProtocol, ESMC, subtype=10) diff --git a/scapy/contrib/ethercat.py b/scapy/contrib/ethercat.py index 43aa84eb909..2a8d63db981 100644 --- a/scapy/contrib/ethercat.py +++ b/scapy/contrib/ethercat.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = EtherCat # scapy.contrib.status = loads @@ -6,17 +10,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :author: Thomas Tannhaeuser, hecke@naberius.de - :license: GPLv2 - - This module is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This module is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. :description: @@ -51,7 +44,6 @@ from scapy.fields import BitField, ByteField, LEShortField, FieldListField, \ LEIntField, FieldLenField, _EnumField, EnumField from scapy.layers.l2 import Ether, Dot1Q -from scapy.modules import six from scapy.packet import bind_layers, Packet, Padding ''' @@ -259,7 +251,7 @@ def __init__(self, name, default, size, length_of=None, count_of=None, adjust=la self.adjust = adjust def i2m(self, pkt, x): - return (FieldLenField.i2m.__func__ if six.PY2 else FieldLenField.i2m)(self, pkt, x) # noqa: E501 + return FieldLenField.i2m(self, pkt, x) class LEBitEnumField(LEBitField, _EnumField): diff --git a/scapy/contrib/etherip.py b/scapy/contrib/etherip.py index 5f4a8161578..9991796c85a 100644 --- a/scapy/contrib/etherip.py +++ b/scapy/contrib/etherip.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = EtherIP # scapy.contrib.status = loads diff --git a/scapy/contrib/exposure_notification.py b/scapy/contrib/exposure_notification.py new file mode 100644 index 00000000000..866ed329e02 --- /dev/null +++ b/scapy/contrib/exposure_notification.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2020 Michael Farrell + +# scapy.contrib.description = Apple/Google Exposure Notification System (ENS) +# scapy.contrib.status = loads + +""" +Apple/Google Exposure Notification System (ENS), formerly known as +Privacy-Preserving Contact Tracing Project. + +This module parses the Bluetooth Low Energy beacon payloads used by the system. +This does **not** yet implement any cryptographic functionality. + +More info: + +* `Apple: Privacy-Preserving Contact Tracing`__ +* `Google: Exposure Notifications`__ +* `Wikipedia: Exposure Notification`__ + +__ https://www.apple.com/covid19/contacttracing/ +__ https://www.google.com/covid19/exposurenotifications/ +__ https://en.wikipedia.org/wiki/Exposure_Notification + +Bluetooth protocol specifications: + +* `v1.1`_ (April 2020) +* `v1.2`_ (April 2020) + +.. _v1.1: https://blog.google/documents/58/Contact_Tracing_-_Bluetooth_Specification_v1.1_RYGZbKW.pdf +.. _v1.2: https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ExposureNotification-BluetoothSpecificationv1.2.pdf +""" # noqa: E501 + +from scapy.fields import StrFixedLenField +from scapy.layers.bluetooth import EIR_Hdr, EIR_ServiceData16BitUUID, \ + EIR_CompleteList16BitServiceUUIDs, LowEnergyBeaconHelper +from scapy.packet import bind_layers, Packet + + +EXPOSURE_NOTIFICATION_UUID = 0xFD6F + + +class Exposure_Notification_Frame(Packet, LowEnergyBeaconHelper): + """Apple/Google BLE Exposure Notification broadcast frame.""" + name = "Exposure Notification broadcast" + + fields_desc = [ + # Rolling Proximity Identifier + StrFixedLenField("identifier", None, 16), + # Associated Encrypted Metadata (added in v1.2) + StrFixedLenField("metadata", None, 4), + ] + + def build_eir(self): + """Builds a list of EIR messages to wrap this frame.""" + + return LowEnergyBeaconHelper.base_eir + [ + EIR_Hdr() / EIR_CompleteList16BitServiceUUIDs(svc_uuids=[ + EXPOSURE_NOTIFICATION_UUID]), + EIR_Hdr() / EIR_ServiceData16BitUUID() / self + ] + + +bind_layers(EIR_ServiceData16BitUUID, Exposure_Notification_Frame, + svc_uuid=EXPOSURE_NOTIFICATION_UUID) diff --git a/scapy/contrib/geneve.py b/scapy/contrib/geneve.py index 7ef52d231b4..6515f299abb 100644 --- a/scapy/contrib/geneve.py +++ b/scapy/contrib/geneve.py @@ -1,18 +1,7 @@ -# Copyright (C) 2018 Hao Zheng - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright (C) 2018 Hao Zheng # scapy.contrib.description = Generic Network Virtualization Encapsulation (GENEVE) # scapy.contrib.status = loads @@ -20,28 +9,48 @@ """ Geneve: Generic Network Virtualization Encapsulation -draft-ietf-nvo3-geneve-06 +https://datatracker.ietf.org/doc/html/rfc8926 """ -from scapy.fields import BitField, XByteField, XShortEnumField, X3BytesField, \ - XStrField +import struct + +from scapy.fields import BitField, XByteField, XShortEnumField, X3BytesField, StrLenField, PacketListField from scapy.packet import Packet, bind_layers from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6 from scapy.layers.l2 import Ether, ETHER_TYPES -from scapy.compat import chb, orb -from scapy.error import warning +CLASS_IDS = {0x0100: "Linux", + 0x0101: "Open vSwitch", + 0x0102: "Open Virtual Networking (OVN)", + 0x0103: "In-band Network Telemetry (INT)", + 0x0104: "VMware", + 0x0105: "Amazon.com, Inc.", + 0x0106: "Cisco Systems, Inc.", + 0x0107: "Oracle Corporation", + 0x0110: "Amazon.com, Inc.", + 0x0118: "IBM", + 0x0128: "Ericsson", + 0xFEFF: "Unassigned", + 0xFFFF: "Experimental"} -class GENEVEOptionsField(XStrField): - islist = 1 - def getfield(self, pkt, s): - opln = pkt.optionlen * 4 - if opln < 0: - warning("bad optionlen (%i). Assuming optionlen=0" % pkt.optionlen) - opln = 0 - return s[opln:], self.m2i(pkt, s[:opln]) +class GeneveOptions(Packet): + name = "Geneve Options" + fields_desc = [XShortEnumField("classid", 0x0000, CLASS_IDS), + XByteField("type", 0x00), + BitField("reserved", 0, 3), + BitField("length", None, 5), + StrLenField('data', '', length_from=lambda x: x.length * 4)] + + def extract_padding(self, s): + return "", s + + def post_build(self, p, pay): + if self.length is None: + tmp_len = len(self.data) // 4 + p = p[:3] + struct.pack("!B", (p[3] & 0x3) | (tmp_len & 0x1f)) + p[4:] + return p + pay class GENEVE(Packet): @@ -54,15 +63,14 @@ class GENEVE(Packet): XShortEnumField("proto", 0x0000, ETHER_TYPES), X3BytesField("vni", 0), XByteField("reserved2", 0x00), - GENEVEOptionsField("options", "")] + PacketListField("options", [], GeneveOptions, + length_from=lambda pkt: pkt.optionlen * 4)] def post_build(self, p, pay): - p += pay - optionlen = self.optionlen - if optionlen is None: - optionlen = (len(self.options) + 3) // 4 - p = chb(optionlen & 0x2f | orb(p[0]) & 0xc0) + p[1:] - return p + if self.optionlen is None: + tmp_len = (len(p) - 8) // 4 + p = struct.pack("!B", (p[0] & 0xc0) | (tmp_len & 0x3f)) + p[1:] + return p + pay def answers(self, other): if isinstance(other, GENEVE): diff --git a/scapy/contrib/gtp.py b/scapy/contrib/gtp.py index 9ceff24fe7c..d4ad4d57493 100644 --- a/scapy/contrib/gtp.py +++ b/scapy/contrib/gtp.py @@ -1,28 +1,51 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2018 Leonardo Monteiro # 2017 Alexis Sultan # 2017 Alessio Deiana # 2014 Guillaume Valadon # 2012 ffranz -## -# This program is published under a GPLv2 license # scapy.contrib.description = GPRS Tunneling Protocol (GTP) # scapy.contrib.status = loads -from __future__ import absolute_import -import struct +""" +GPRS Tunneling Protocol (GTP) + +Spec: 3GPP TS 29.060 and 3GPP TS 29.274 +Some IEs: 3GPP TS 24.008 +""" +import struct from scapy.compat import chb, orb, bytes_encode +from scapy.config import conf from scapy.error import warning -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - ConditionalField, FieldLenField, FieldListField, FlagsField, IntField, \ - IPField, PacketListField, ShortField, StrFixedLenField, StrLenField, \ - XBitField, XByteField, XIntField +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + FlagsField, + IPField, + IntField, + PacketListField, + ShortField, + StrFixedLenField, + StrLenField, + X3BytesField, + XBitField, + XByteField, + XIntField, +) from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6, IP6Field from scapy.layers.ppp import PPP -from scapy.modules.six.moves import range +from scapy.layers.dns import DNSStrField from scapy.packet import bind_layers, bind_bottom_up, bind_top_down, \ Packet, Raw from scapy.volatile import RandInt, RandIP, RandNum, RandString @@ -186,6 +209,24 @@ def i2m(self, pkt, val): return ret_string +class FQDNField(DNSStrField): + """ + DNSStrField without ending null. + + See ETSI TS 129.244 18.07.00 - 8.66, NOTE 1 + """ + + def h2i(self, pkt, x): + return bytes_encode(x) + + def i2m(self, pkt, x): + return b"".join(chb(len(y)) + y for y in (k[:63] for k in x.split(b"."))) + + def getfield(self, pkt, s): + remain, s = super().getfield(pkt, s) + return remain, s[:-1] + + TBCD_TO_ASCII = b"0123456789*#abc" @@ -198,7 +239,7 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): class GTP_UDPPort_ExtensionHeader(GTP_ExtensionHeader): - fields_desc = [ByteField("length", 0x40), + fields_desc = [ByteField("length", 0x01), ShortField("udp_port", None), ByteEnumField("next_ex", 0, ExtensionHeadersTypes), ] @@ -221,23 +262,37 @@ class GTPHeader(Packet): ByteEnumField("gtp_type", None, GTPmessageType), ShortField("length", None), IntField("teid", 0), - ConditionalField(XBitField("seq", 0, 16), lambda pkt:pkt.E == 1 or pkt.S == 1 or pkt.PN == 1), # noqa: E501 - ConditionalField(ByteField("npdu", 0), lambda pkt:pkt.E == 1 or pkt.S == 1 or pkt.PN == 1), # noqa: E501 - ConditionalField(ByteEnumField("next_ex", 0, ExtensionHeadersTypes), lambda pkt:pkt.E == 1 or pkt.S == 1 or pkt.PN == 1), ] # noqa: E501 + ConditionalField( + XBitField("seq", 0, 16), + lambda pkt:pkt.E == 1 or pkt.S == 1 or pkt.PN == 1), + ConditionalField( + ByteField("npdu", 0), + lambda pkt:pkt.E == 1 or pkt.S == 1 or pkt.PN == 1), + ConditionalField( + ByteEnumField("next_ex", 0, ExtensionHeadersTypes), + lambda pkt:pkt.E == 1 or pkt.S == 1 or pkt.PN == 1), + ] def post_build(self, p, pay): p += pay if self.length is None: - tmp_len = len(p) - 8 + # The message length field is calculated different in GTPv1 and GTPv2. # noqa: E501 + # For GTPv1 it is defined as the rest of the packet following the mandatory 8-byte GTP header # noqa: E501 + # For GTPv2 it is defined as the length of the message in bytes excluding the mandatory part of the GTP-C header (the first 4 bytes) # noqa: E501 + tmp_len = len(p) - 4 if self.version == 2 else len(p) - 8 p = p[:2] + struct.pack("!H", tmp_len) + p[4:] return p def hashret(self): - return struct.pack("B", self.version) + self.payload.hashret() + hsh = struct.pack("B", self.version) + if self.seq: + hsh += struct.pack("H", self.seq) + return hsh + self.payload.hashret() def answers(self, other): return (isinstance(other, GTPHeader) and self.version == other.version and + (not self.seq or self.seq == other.seq) and self.payload.answers(other.payload)) @classmethod @@ -294,24 +349,80 @@ def guess_payload_class(self, payload): class GTPPDUSessionContainer(Packet): + # TS 38.415-g30 sect 5 name = "GTP PDU Session Container" + deprecated_fields = { + "qmp": ("QMP", "2.4.5"), + "P": ("PPP", "2.4.5"), + "R": ("RQI", "2.4.5"), + "extraPadding": ("padding", "2.4.5"), + } fields_desc = [ByteField("ExtHdrLen", None), - BitField("type", 0, 4), - BitField("spare1", 0, 4), - BitField("P", 0, 1), - BitField("R", 0, 1), - BitField("QFI", 0, 6), + BitEnumField("type", 0, 4, + {0: "DL PDU SESSION INFORMATION", + 1: "UL PDU SESSION INFORMATION"}), + BitField("QMP", 0, 1), + # UL (type 1) + ConditionalField(BitField("dlDelayInd", 0, 1), + lambda pkt: pkt.type == 1), + ConditionalField(BitField("ulDelayInd", 0, 1), + lambda pkt: pkt.type == 1), + # Common + BitField("SNP", 0, 1), + # UL (type 1) + ConditionalField(BitField("N3N9DelayInd", 0, 1), + lambda pkt: pkt.type == 1), + ConditionalField(XBitField("spareUl1", 0, 1), + lambda pkt: pkt.type == 1), + # DL (type 0) + ConditionalField(XBitField("spareDl1", 0, 2), + lambda pkt: pkt.type == 0), + ConditionalField(BitField("PPP", 0, 1), + lambda pkt: pkt.type == 0), + ConditionalField(BitField("RQI", 0, 1), + lambda pkt: pkt.type == 0), + # Common + BitField("QFI", 0, 6), # QoS Flow Identifier + # DL (type 0) ConditionalField(XBitField("PPI", 0, 3), - lambda pkt: pkt.P == 1), - ConditionalField(XBitField("spare2", 0, 5), - lambda pkt: pkt.P == 1), - ConditionalField(ByteField("pad1", 0), - lambda pkt: pkt.P == 1), - ConditionalField(ByteField("pad2", 0), - lambda pkt: pkt.P == 1), - ConditionalField(ByteField("pad3", 0), - lambda pkt: pkt.P == 1), - ByteEnumField("NextExtHdr", 0, ExtensionHeadersTypes), ] + lambda pkt: pkt.type == 0 and + pkt.PPP == 1), + ConditionalField(XBitField("spareDl2", 0, 5), + lambda pkt: pkt.type == 0 and + pkt.PPP == 1), + ConditionalField(XBitField("dlSendTime", 0, 64), + lambda pkt: pkt.type == 0 and + pkt.QMP == 1), + ConditionalField(X3BytesField("dlQFISeqNum", 0), + lambda pkt: pkt.type == 0 and + pkt.SNP == 1), + # UL (type 1) + ConditionalField(XBitField("dlSendTimeRpt", 0, 64), + lambda pkt: pkt.type == 1 and + pkt.QMP == 1), + ConditionalField(XBitField("dlRecvTime", 0, 64), + lambda pkt: pkt.type == 1 and + pkt.QMP == 1), + ConditionalField(XBitField("ulSendTime", 0, 64), + lambda pkt: pkt.type == 1 and + pkt.QMP == 1), + ConditionalField(XBitField("dlDelayRslt", 0, 32), + lambda pkt: pkt.type == 1 and + pkt.dlDelayInd == 1), + ConditionalField(XBitField("ulDelayRslt", 0, 32), + lambda pkt: pkt.type == 1 and + pkt.ulDelayInd == 1), + ConditionalField(XBitField("UlQFISeqNum", 0, 24), + lambda pkt: pkt.type == 1 and + pkt.SNP == 1), + ConditionalField(XBitField("N3N9DelayRslt", 0, 32), + lambda pkt: pkt.type == 1 and + pkt.N3N9DelayInd == 1), + # Common + ByteEnumField("NextExtHdr", 0, ExtensionHeadersTypes), + ConditionalField( + StrLenField("padding", b"", length_from=lambda p: 0), + lambda pkt: pkt.NextExtHdr == 0)] def guess_payload_class(self, payload): if self.NextExtHdr == 0: @@ -324,33 +435,44 @@ def guess_payload_class(self, payload): return PPP return GTPHeader.guess_payload_class(self, payload) + def post_dissect(self, s): + if self.NextExtHdr == 0: + # Padding is handled in this layer + length = len(self.original) - len(s) + pad_length = (- length) % 4 + self.padding = s[:pad_length] + return s[pad_length:] + return s + def post_build(self, p, pay): - p += pay + # Length + if self.NextExtHdr == 0: + p += b"\x00" * ((-len(p)) % 4) + else: + pay += b"\x00" * ((-len(p + pay)) % 4) if self.ExtHdrLen is None: - if self.P == 1: - hdr_len = 2 - else: - hdr_len = 1 - p = struct.pack("!B", hdr_len) + p[1:] - return p - - def hashret(self): - return struct.pack("H", self.seq) + p = struct.pack("!B", len(p) // 4) + p[1:] + return p + pay class GTPEchoRequest(Packet): # 3GPP TS 29.060 V9.1.0 (2009-12) name = "GTP Echo Request" - def hashret(self): - return struct.pack("H", self.seq) - class IE_Base(Packet): - def extract_padding(self, pkt): return "", pkt + def post_build(self, p, pay): + if self.fields_desc[1].name == "length": + if self.length is None: + tmp_len = len(p) + if isinstance(self.payload, conf.padding_layer): + tmp_len += len(self.payload.load) + p = p[:1] + struct.pack("!H", tmp_len - 4) + p[3:] + return p + pay + class IE_Cause(IE_Base): name = "Cause" @@ -530,8 +652,17 @@ class IE_ProtocolConfigurationOptions(IE_Base): class IE_GSNAddress(IE_Base): name = "GSN Address" fields_desc = [ByteEnumField("ietype", 133, IEType), - ShortField("length", 4), - IPField("address", RandIP())] + ShortField("length", None), + ConditionalField(IPField("ipv4_address", RandIP()), + lambda pkt: pkt.length == 4), + ConditionalField(IP6Field("ipv6_address", '::1'), + lambda pkt: pkt.length == 16)] + + def post_build(self, p, pay): + if self.length is None: + tmp_len = len(p) - 3 + p = p[:2] + struct.pack("!B", tmp_len) + p[3:] + return p class IE_MSInternationalNumber(IE_Base): @@ -544,15 +675,16 @@ class IE_MSInternationalNumber(IE_Base): class QoS_Profile(IE_Base): name = "QoS profile" + # 3GPP TS 24.008 10.5.6.5 fields_desc = [ByteField("qos_ei", 0), ByteField("length", None), - XBitField("spare", 0x00, 2), + XBitField("spare1", 0x00, 2), XBitField("delay_class", 0x000, 3), XBitField("reliability_class", 0x000, 3), XBitField("peak_troughput", 0x0000, 4), - BitField("spare", 0, 1), + BitField("spare2", 0, 1), XBitField("precedence_class", 0x000, 3), - XBitField("spare", 0x000, 3), + XBitField("spare3", 0x000, 3), XBitField("mean_troughput", 0x00000, 5), XBitField("traffic_class", 0x000, 3), XBitField("delivery_order", 0x00, 2), @@ -573,8 +705,8 @@ class IE_QoS(IE_Base): fields_desc = [ByteEnumField("ietype", 135, IEType), ShortField("length", None), ByteField("allocation_retention_prioiry", 1), - - ConditionalField(XBitField("spare", 0x00, 2), + # 3GPP TS 24.008 10.5.6.5 + ConditionalField(XBitField("spare1", 0x00, 2), lambda p: p.length and p.length > 1), ConditionalField(XBitField("delay_class", 0x000, 3), lambda p: p.length and p.length > 1), @@ -583,12 +715,12 @@ class IE_QoS(IE_Base): ConditionalField(XBitField("peak_troughput", 0x0000, 4), lambda p: p.length and p.length > 2), - ConditionalField(BitField("spare", 0, 1), + ConditionalField(BitField("spare2", 0, 1), lambda p: p.length and p.length > 2), ConditionalField(XBitField("precedence_class", 0x000, 3), lambda p: p.length and p.length > 2), - ConditionalField(XBitField("spare", 0x000, 3), + ConditionalField(XBitField("spare3", 0x000, 3), lambda p: p.length and p.length > 3), ConditionalField(XBitField("mean_troughput", 0x00000, 5), lambda p: p.length and p.length > 3), @@ -624,7 +756,7 @@ class IE_QoS(IE_Base): None), lambda p: p.length and p.length > 11), - ConditionalField(XBitField("spare", 0x000, 3), + ConditionalField(XBitField("spare4", 0x000, 3), lambda p: p.length and p.length > 12), ConditionalField(BitField("signaling_indication", 0, 1), lambda p: p.length and p.length > 12), @@ -699,12 +831,7 @@ class IE_MSTimeZone(IE_Base): fields_desc = [ByteEnumField("ietype", 153, IEType), ShortField("length", None), ByteField("timezone", 0), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), + BitField("spare", 0, 6), XBitField("daylight_saving_time", 0x00, 2)] @@ -724,13 +851,10 @@ class IE_MSInfoChangeReportingAction(IE_Base): class IE_DirectTunnelFlags(IE_Base): name = "Direct Tunnel Flags" + # 29.060 7.7.81 fields_desc = [ByteEnumField("ietype", 182, IEType), ShortField("length", 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), - BitField("Spare", 0, 1), + BitField("spare", 0, 5), BitField("EI", 0, 1), BitField("GCSI", 0, 1), BitField("DTI", 0, 1)] @@ -745,17 +869,18 @@ class IE_BearerControlMode(IE_Base): class IE_EvolvedAllocationRetentionPriority(IE_Base): name = "Evolved Allocation/Retention Priority" + # 29.060 7.7.91 fields_desc = [ByteEnumField("ietype", 191, IEType), ShortField("length", 1), - BitField("Spare", 0, 1), + BitField("spare1", 0, 1), BitField("PCI", 0, 1), XBitField("PL", 0x0000, 4), - BitField("Spare", 0, 1), + BitField("spare2", 0, 1), BitField("PVI", 0, 1)] class IE_CharginGatewayAddress(IE_Base): - name = "Chargin Gateway Address" + name = "Charging Gateway Address" fields_desc = [ByteEnumField("ietype", 251, IEType), ShortField("length", 4), ConditionalField(IPField("ipv4_address", "127.0.0.1"), @@ -769,7 +894,7 @@ class IE_PrivateExtension(IE_Base): name = "Private Extension" fields_desc = [ByteEnumField("ietype", 255, IEType), ShortField("length", 1), - ByteField("extension identifier", 0), + ByteField("extension_identifier", 0), StrLenField("extention_value", "", length_from=lambda x: x.length)] @@ -845,35 +970,26 @@ class GTPEchoResponse(Packet): name = "GTP Echo Response" fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] - def hashret(self): - return struct.pack("H", self.seq) - def answers(self, other): - return self.seq == other.seq + return isinstance(other, GTPEchoRequest) class GTPCreatePDPContextRequest(Packet): # 3GPP TS 29.060 V9.1.0 (2009-12) name = "GTP Create PDP Context Request" - fields_desc = [PacketListField("IE_list", [IE_TEIDI(), IE_NSAPI(), IE_GSNAddress(), # noqa: E501 - IE_GSNAddress(), + fields_desc = [PacketListField("IE_list", [IE_TEIDI(), IE_NSAPI(), IE_GSNAddress(length=4, ipv4_address=RandIP()), # noqa: E501 + IE_GSNAddress(length=4, ipv4_address=RandIP()), # noqa: E501 IE_NotImplementedTLV(ietype=135, length=15, data=RandString(15))], # noqa: E501 IE_Dispatcher)] - def hashret(self): - return struct.pack("H", self.seq) - class GTPCreatePDPContextResponse(Packet): # 3GPP TS 29.060 V9.1.0 (2009-12) name = "GTP Create PDP Context Response" fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] - def hashret(self): - return struct.pack("H", self.seq) - def answers(self, other): - return self.seq == other.seq + return isinstance(other, GTPCreatePDPContextRequest) class GTPUpdatePDPContextRequest(Packet): @@ -901,17 +1017,14 @@ class GTPUpdatePDPContextRequest(Packet): IE_PrivateExtension()], IE_Dispatcher)] - def hashret(self): - return struct.pack("H", self.seq) - class GTPUpdatePDPContextResponse(Packet): # 3GPP TS 29.060 V9.1.0 (2009-12) name = "GTP Update PDP Context Response" fields_desc = [PacketListField("IE_list", None, IE_Dispatcher)] - def hashret(self): - return struct.pack("H", self.seq) + def answers(self, other): + return isinstance(other, GTPUpdatePDPContextRequest) class GTPErrorIndication(Packet): @@ -931,6 +1044,9 @@ class GTPDeletePDPContextResponse(Packet): name = "GTP Delete PDP Context Response" fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + def answers(self, other): + return isinstance(other, GTPDeletePDPContextRequest) + class GTPPDUNotificationRequest(Packet): # 3GPP TS 29.060 V9.1.0 (2009-12) @@ -939,7 +1055,7 @@ class GTPPDUNotificationRequest(Packet): IE_TEICP(TEICI=RandInt()), IE_EndUserAddress(PDPTypeNumber=0x21), # noqa: E501 IE_AccessPointName(), - IE_GSNAddress(address="127.0.0.1"), # noqa: E501 + IE_GSNAddress(ipv4_address="127.0.0.1"), # noqa: E501 ], IE_Dispatcher)] diff --git a/scapy/contrib/gtp_v2.py b/scapy/contrib/gtp_v2.py index 3e8113879cc..d89b35be358 100755 --- a/scapy/contrib/gtp_v2.py +++ b/scapy/contrib/gtp_v2.py @@ -1,20 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2017 Alessio Deiana # 2017 Alexis Sultan -# This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - # scapy.contrib.description = GPRS Tunneling Protocol v2 (GTPv2) # scapy.contrib.status = loads @@ -22,12 +11,28 @@ from scapy.compat import orb -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - ConditionalField, IntField, IPField, LongField, PacketField, \ - PacketListField, ShortEnumField, ShortField, StrFixedLenField, \ - StrLenField, ThreeBytesField, XBitField, XIntField, XShortField, \ - FieldLenField from scapy.data import IANA_ENTERPRISE_NUMBERS +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + IPField, + IntField, + MultipleTypeField, + PacketField, + PacketListField, + ShortEnumField, + ShortField, + StrFixedLenField, + StrLenField, + ThreeBytesField, + XBitField, + XIntField, + XShortField, +) +from scapy.layers.inet6 import IP6Field from scapy.packet import bind_layers, Packet, Raw from scapy.volatile import RandIP, RandShort @@ -36,7 +41,16 @@ RATType = { + 1: "UTRAN", + 2: "GERAN", + 3: "WLAN", + 4: "GAN", + 5: "HSPA Evolution", 6: "EUTRAN", + 7: "Virtual", + 8: "EUTRAN-NB-IoT", + 9: "LTE-M", + 10: "NR", } # 3GPP TS 29.274 v16.1.0 table 6.1-1 @@ -189,7 +203,7 @@ 71: "APN", 72: "AMBR", 73: "EPS Bearer ID", - 74: "IPv4", + 74: "IP Address", 75: "MEI", 76: "MSISDN", 77: "Indication", @@ -198,71 +212,86 @@ 80: "Bearer QoS", 82: "RAT", 83: "Serving Network", + 84: "Bearer TFT", 86: "ULI", 87: "F-TEID", 93: "Bearer Context", 94: "Charging ID", 95: "Charging Characteristics", + 97: "Bearer Flags", 99: "PDN Type", + 107: "MM Context (EPS Security Context and Quadruplets)", + 109: "PDN Connection", 114: "UE Time zone", 126: "Port Number", 127: "APN Restriction", 128: "Selection Mode", + 132: "FQ-CSID", + 136: "FQDN", + 145: "UCI", 161: "Max MBR/APN-AMBR (MMBR)", + 163: "Additional Protocol Configuration Options", + 170: "ULI Timestamp", + 172: "RAN/NAS Cause", + 197: "Extended Protocol Configuration Options", + 202: "UP Function Selection Indication Flags", 255: "Private Extension", } -class GTPHeader(Packet): +class GTPHeader(gtp.GTPHeader): # 3GPP TS 29.060 V9.1.0 (2009-12) # without the version name = "GTP v2 Header" fields_desc = [BitField("version", 2, 3), BitField("P", 1, 1), BitField("T", 1, 1), - BitField("SPARE", 0, 1), - BitField("SPARE", 0, 1), - BitField("SPARE", 0, 1), + BitField("MP", 0, 1), + BitField("SPARE1", 0, 1), + BitField("SPARE2", 0, 1), ByteEnumField("gtp_type", None, GTPmessageType), ShortField("length", None), ConditionalField(XIntField("teid", 0), lambda pkt:pkt.T == 1), ThreeBytesField("seq", RandShort()), - ByteField("SPARE", 0) - ] - - def post_build(self, p, pay): - p += pay - if self.length is None: - tmp_len = len(p) - 8 - p = p[:2] + struct.pack("!H", tmp_len) + p[4:] - return p - - def hashret(self): - return struct.pack("B", self.version) + self.payload.hashret() - - def answers(self, other): - return (isinstance(other, GTPHeader) and - self.version == other.version and - self.payload.answers(other.payload)) + ConditionalField(BitField("msg_priority", 0, 4), + lambda pkt:pkt.MP == 1), + ConditionalField( + MultipleTypeField( + [(BitField("SPARE3", 0, 4), + lambda pkt: pkt.MP == 1)], + ByteField("SPARE3", 0)), + lambda pkt: pkt.MP in [0, 1])] -class IE_IPv4(gtp.IE_Base): - name = "IE IPv4" +class IE_IP_Address(gtp.IE_Base): + name = "IE IP Address" fields_desc = [ByteEnumField("ietype", 74, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), - IPField("address", RandIP())] + ConditionalField( + IPField("address", RandIP()), + lambda pkt: pkt.length == 4), + ConditionalField( + IP6Field("address6", None), + lambda pkt: pkt.length == 16)] + + def post_build(self, p, pay): + if self.length is None: + tmp_len = 16 if self.address6 is not None else 4 + p = p[:1] + struct.pack("!H", tmp_len) + p[2:] + return p + pay class IE_MEI(gtp.IE_Base): name = "IE MEI" fields_desc = [ByteEnumField("ietype", 75, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), - LongField("MEI", 0)] + gtp.TBCDByteField("MEI", "175675478970685", + length_from=lambda x: x.length)] def IE_Dispatcher(s): @@ -282,7 +311,7 @@ def IE_Dispatcher(s): class IE_EPSBearerID(gtp.IE_Base): name = "IE EPS Bearer ID" fields_desc = [ByteEnumField("ietype", 73, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), ByteField("EBI", 0)] @@ -291,7 +320,7 @@ class IE_EPSBearerID(gtp.IE_Base): class IE_RAT(gtp.IE_Base): name = "IE RAT" fields_desc = [ByteEnumField("ietype", 82, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), ByteEnumField("RAT_type", None, RATType)] @@ -300,7 +329,7 @@ class IE_RAT(gtp.IE_Base): class IE_ServingNetwork(gtp.IE_Base): name = "IE Serving Network" fields_desc = [ByteEnumField("ietype", 83, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), gtp.TBCDByteField("MCC", "", 2), @@ -323,8 +352,8 @@ class ULI_CGI(ULI_Field): fields_desc = [ gtp.TBCDByteField("MCC", "", 2), gtp.TBCDByteField("MNC", "", 1), - BitField("LAC", 0, 4), - BitField("CI", 0, 28), + ShortField("LAC", 0), + ShortField("CI", 0), ] @@ -382,7 +411,7 @@ class IE_ULI(gtp.IE_Base): name = "IE User Location Information" fields_desc = [ ByteEnumField("ietype", 86, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), BitField("SPARE", 0, 2), @@ -413,6 +442,16 @@ class IE_ULI(gtp.IE_Base): ] +class IE_ULI_Timestamp(gtp.IE_Base): + name = "IE ULI Timestamp" + fields_desc = [ + ByteEnumField("ietype", 170, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + XIntField("timestamp", 0)] + + # 3GPP TS 29.274 v12.12.0 section 8.22 INTERFACE_TYPES = { 0: "S1-U eNodeB GTP-U interface", @@ -456,10 +495,26 @@ class IE_ULI(gtp.IE_Base): } +class IE_UCI(gtp.IE_Base): + name = "IE UCI" + fields_desc = [ByteEnumField("ietype", 145, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + gtp.TBCDByteField("MCC", "", 2), + gtp.TBCDByteField("MNC", "", 1), + BitField("SPARE1", 0, 5), + BitField("CSG_ID", 0, 27), + BitField("AccessMode", 0, 2), + BitField("SPARE2", 0, 4), + BitField("LCSG", 0, 1), + BitField("CMI", 0, 1)] + + class IE_FTEID(gtp.IE_Base): name = "IE F-TEID" fields_desc = [ByteEnumField("ietype", 87, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), BitField("ipv4_present", 0, 1), @@ -475,13 +530,70 @@ class IE_FTEID(gtp.IE_Base): class IE_BearerContext(gtp.IE_Base): name = "IE Bearer Context" fields_desc = [ByteEnumField("ietype", 93, IEType), - ShortField("length", 0), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), PacketListField("IE_list", None, IE_Dispatcher, length_from=lambda pkt: pkt.length)] +class IE_BearerFlags(gtp.IE_Base): + name = "IE Bearer Flags" + fields_desc = [ByteEnumField("ietype", 97, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + BitField("SPARE", 0, 4), + BitField("ASI", 0, 1), + BitField("Vind", 0, 1), + BitField("VB", 0, 1), + BitField("PPC", 0, 1)] + + +class IE_MMContext_EPS(gtp.IE_Base): + name = "IE MM Context (EPS Security Context and Quadruplets)" + fields_desc = [ByteEnumField("ietype", 107, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + BitField("Sec_Mode", 0, 3), + BitField("Nhi", 0, 1), + BitField("Drxi", 0, 1), + BitField("Ksi", 0, 3), + BitField("Num_quint", 0, 3), + BitField("Num_Quad", 0, 3), + BitField("Uambri", 0, 1), + BitField("Osci", 0, 1), + BitField("Sambri", 0, 1), + BitField("Nas_algo", 0, 3), + BitField("Nas_cipher", 0, 4), + ThreeBytesField("Nas_dl_count", 0), + ThreeBytesField("Nas_ul_count", 0), + BitField("Kasme", 0, 256), + ConditionalField(StrLenField("fields", "", + length_from=lambda x: x.length - 41), + lambda pkt: pkt.length > 40)] + + +class IE_PDNConnection(gtp.IE_Base): + name = "IE PDN Connection" + fields_desc = [ByteEnumField("ietype", 109, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + PacketListField("IE_list", None, IE_Dispatcher, + length_from=lambda pkt: pkt.length)] + + +class IE_FQDN(gtp.IE_Base): + name = "IE FQDN" + fields_desc = [ByteEnumField("ietype", 136, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + gtp.FQDNField("fqdn", b"", length_from=lambda x: x.length)] + + class IE_NotImplementedTLV(gtp.IE_Base): name = "IE not implemented" fields_desc = [ByteEnumField("ietype", 0, IEType), @@ -603,7 +715,7 @@ class IE_IMSI(gtp.IE_Base): class IE_Cause(gtp.IE_Base): name = "IE Cause" fields_desc = [ByteEnumField("ietype", 2, IEType), - ShortField("length", 6), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), ByteEnumField("Cause", 1, CAUSE_VALUES), @@ -616,7 +728,7 @@ class IE_Cause(gtp.IE_Base): class IE_RecoveryRestart(gtp.IE_Base): name = "IE Recovery Restart" fields_desc = [ByteEnumField("ietype", 3, IEType), - ShortField("length", 5), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), ByteField("restart_counter", 0)] @@ -625,18 +737,27 @@ class IE_RecoveryRestart(gtp.IE_Base): class IE_APN(gtp.IE_Base): name = "IE APN" fields_desc = [ByteEnumField("ietype", 71, IEType), - FieldLenField("length", None, length_of="APN", - adjust=lambda pkt, x: x + 4, fmt="H"), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), gtp.APNStrLenField("APN", "internet", length_from=lambda x: x.length)] +class IE_BearerTFT(gtp.IE_Base): + name = "IE Bearer TFT" + fields_desc = [ByteEnumField("ietype", 84, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + StrLenField("Bearer_TFT", "", + length_from=lambda x: x.length)] + + class IE_AMBR(gtp.IE_Base): name = "IE AMBR" fields_desc = [ByteEnumField("ietype", 72, IEType), - ShortField("length", 12), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), IntField("AMBR_Uplink", 0), @@ -646,8 +767,7 @@ class IE_AMBR(gtp.IE_Base): class IE_MSISDN(gtp.IE_Base): name = "IE MSISDN" fields_desc = [ByteEnumField("ietype", 76, IEType), - FieldLenField("length", None, length_of="digits", - adjust=lambda pkt, x: x + 4, fmt="H"), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), gtp.TBCDByteField("digits", "33123456789", @@ -660,23 +780,38 @@ class IE_Indication(gtp.IE_Base): ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), - BitField("DAF", 0, 1), - BitField("DTF", 0, 1), - BitField("HI", 0, 1), - BitField("DFI", 0, 1), - BitField("OI", 0, 1), - BitField("ISRSI", 0, 1), - BitField("ISRAI", 0, 1), - BitField("SGWCI", 0, 1), - BitField("SQCI", 0, 1), - BitField("UIMSI", 0, 1), - BitField("CFSI", 0, 1), - BitField("CRSI", 0, 1), - BitField("PS", 0, 1), - BitField("PT", 0, 1), - BitField("SI", 0, 1), - BitField("MSV", 0, 1), - + ConditionalField( + BitField("DAF", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("DTF", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("HI", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("DFI", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("OI", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("ISRSI", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("ISRAI", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("SGWCI", 0, 1), lambda pkt: pkt.length > 0), + ConditionalField( + BitField("SQCI", 0, 1), lambda pkt: pkt.length > 1), + ConditionalField( + BitField("UIMSI", 0, 1), lambda pkt: pkt.length > 1), + ConditionalField( + BitField("CFSI", 0, 1), lambda pkt: pkt.length > 1), + ConditionalField( + BitField("CRSI", 0, 1), lambda pkt: pkt.length > 1), + ConditionalField( + BitField("PS", 0, 1), lambda pkt: pkt.length > 1), + ConditionalField( + BitField("PT", 0, 1), lambda pkt: pkt.length > 1), + ConditionalField( + BitField("SI", 0, 1), lambda pkt: pkt.length > 1), + ConditionalField( + BitField("MSV", 0, 1), lambda pkt: pkt.length > 1), ConditionalField( BitField("RetLoc", 0, 1), lambda pkt: pkt.length > 2), ConditionalField( @@ -710,6 +845,70 @@ class IE_Indication(gtp.IE_Base): BitField("CLII", 0, 1), lambda pkt: pkt.length > 3), ConditionalField( BitField("CPSR", 0, 1), lambda pkt: pkt.length > 3), + ConditionalField( + BitField("NSI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("UASI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("DTCI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("BDWI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("PSCI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("PCRI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("AOSI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("AOPI", 0, 1), lambda pkt: pkt.length > 4), + ConditionalField( + BitField("ROAAI", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("EPCOSI", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("CPOPCI", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("PMTSMI", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("S11TF", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("PNSI", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("UNACCSI", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("WPMSI", 0, 1), lambda pkt: pkt.length > 5), + ConditionalField( + BitField("_5GSNN26", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("REPREFI", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("_5GSIWKI", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("EEVRSI", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("LTEMUI", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("LTEMPI", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("ENBCRSI", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("TSPCMI", 0, 1), lambda pkt: pkt.length > 6), + ConditionalField( + BitField("SPARE1", 0, 1), lambda pkt: pkt.length > 7), + ConditionalField( + BitField("SPARE2", 0, 1), lambda pkt: pkt.length > 7), + ConditionalField( + BitField("SPARE3", 0, 1), lambda pkt: pkt.length > 7), + ConditionalField( + BitField("N5GNMI", 0, 1), lambda pkt: pkt.length > 7), + ConditionalField( + BitField("_5GCNRS", 0, 1), lambda pkt: pkt.length > 7), + ConditionalField( + BitField("_5GCNRI", 0, 1), lambda pkt: pkt.length > 7), + ConditionalField( + BitField("_5SRHOI", 0, 1), lambda pkt: pkt.length > 7), + ConditionalField( + BitField("ETHPDN", 0, 1), lambda pkt: pkt.length > 7), ] @@ -733,50 +932,76 @@ class PCO_Option(Packet): def extract_padding(self, pkt): return "", pkt + def post_build(self, p, pay): + if self.length is None: + p = p[:1] + struct.pack("!B", len(p) - 2) + p[2:] + return p + pay + + +class PCO_Protocol(Packet): + # 10.5.6.3 of 3GPP TS 24.008 + def extract_padding(self, pkt): + return "", pkt + + def post_build(self, p, pay): + if self.length is None: + p = p[:2] + struct.pack("!B", len(p) - 3) + p[3:] + return p + pay + class PCO_IPv4(PCO_Option): name = "IPv4" fields_desc = [ByteEnumField("type", None, PCO_OPTION_TYPES), - ByteField("length", 0), + ByteField("length", None), IPField("address", RandIP())] class PCO_Primary_DNS(PCO_Option): name = "Primary DNS Server IP Address" fields_desc = [ByteEnumField("type", None, PCO_OPTION_TYPES), - ByteField("length", 0), + ByteField("length", None), IPField("address", RandIP())] class PCO_Primary_NBNS(PCO_Option): name = "Primary DNS Server IP Address" fields_desc = [ByteEnumField("type", None, PCO_OPTION_TYPES), - ByteField("length", 0), + ByteField("length", None), IPField("address", RandIP())] class PCO_Secondary_DNS(PCO_Option): name = "Secondary DNS Server IP Address" fields_desc = [ByteEnumField("type", None, PCO_OPTION_TYPES), - ByteField("length", 0), + ByteField("length", None), IPField("address", RandIP())] class PCO_Secondary_NBNS(PCO_Option): name = "Secondary NBNS Server IP Address" fields_desc = [ByteEnumField("type", None, PCO_OPTION_TYPES), - ByteField("length", 0), + ByteField("length", None), IPField("address", RandIP())] PCO_PROTOCOL_TYPES = { 0x0001: 'P-CSCF IPv6 Address Request', + 0x0002: 'IM CN Subsystem Signaling Flag', 0x0003: 'DNS Server IPv6 Address Request', 0x0005: 'MS Support of Network Requested Bearer Control indicator', 0x000a: 'IP Allocation via NAS', 0x000d: 'DNS Server IPv4 Address Request', 0x000c: 'P-CSCF IPv4 Address Request', 0x0010: 'IPv4 Link MTU Request', + 0x0012: 'P-CSCF Re-selection Support', + 0x001a: 'PDU session ID', + 0x0022: '5GSM Cause Value', + 0x0023: 'QoS Rules With Support Indicator', + 0x0024: 'QoS Flow Descriptions With Support Indicator', + 0x001b: 'S-NSSAI', + 0x001c: 'QoS Rules', + 0x001d: 'Session-AMBR', + 0x001f: 'QoS Flow Descriptions', 0x8021: 'IPCP', 0xc023: 'Password Authentication Protocol', 0xc223: 'Challenge Handshake Authentication Protocol', @@ -803,36 +1028,44 @@ def len_options(pkt): return pkt.length - 4 if pkt.length else 0 -class PCO_P_CSCF_IPv6_Address_Request(PCO_Option): +class PCO_P_CSCF_IPv6_Address_Request(PCO_Protocol): name = "PCO PCO-P CSCF IPv6 Address Request" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), ConditionalField(XBitField("address", "2001:db8:0:42::", 128), lambda pkt: pkt.length)] -class PCO_DNS_Server_IPv6(PCO_Option): +class PCO_IM_CN_Subsystem_Signaling_Flag(PCO_Protocol): + name = "PCO IM CN Subsystem Signaling Flag" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + PacketListField("Options", None, PCO_option_dispatcher, + length_from=len_options)] + + +class PCO_DNS_Server_IPv6(PCO_Protocol): name = "PCO DNS Server IPv6 Address Request" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), ConditionalField(XBitField("address", "2001:db8:0:42::", 128), lambda pkt: pkt.length)] -class PCO_SOF(PCO_Option): +class PCO_SOF(PCO_Protocol): name = "PCO MS Support of Network Requested Bearer Control indicator" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), ] -class PCO_PPP(PCO_Option): +class PCO_PPP(PCO_Protocol): name = "PPP IP Control Protocol" fields_desc = [ByteField("Code", 0), ByteField("Identifier", 0), - ShortField("length", 0), + ShortField("length", None), PacketListField("Options", None, PCO_option_dispatcher, length_from=len_options)] @@ -840,50 +1073,129 @@ def extract_padding(self, pkt): return "", pkt -class PCO_IP_Allocation_via_NAS(PCO_Option): +class PCO_IP_Allocation_via_NAS(PCO_Protocol): name = "PCO IP Address allocation via NAS Signaling" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), PacketListField("Options", None, PCO_option_dispatcher, length_from=len_options)] -class PCO_P_CSCF_IPv4_Address_Request(PCO_Option): +class PCO_P_CSCF_IPv4_Address_Request(PCO_Protocol): name = "PCO PCO-P CSCF IPv4 Address Request" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), ConditionalField(IPField("address", RandIP()), lambda pkt: pkt.length)] -class PCO_DNS_Server_IPv4(PCO_Option): +class PCO_DNS_Server_IPv4(PCO_Protocol): name = "PCO DNS Server IPv4 Address Request" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), ConditionalField(IPField("address", RandIP()), lambda pkt: pkt.length)] -class PCO_IPv4_Link_MTU_Request(PCO_Option): +class PCO_IPv4_Link_MTU_Request(PCO_Protocol): name = "PCO IPv4 Link MTU Request" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), ConditionalField(ShortField("MTU_size", 1500), lambda pkt: pkt.length)] -class PCO_IPCP(PCO_Option): +class PCO_P_CSCF_Re_selection_Support(PCO_Protocol): + name = "PCO P-CSCF Re-selection Support" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + PacketListField("Options", None, PCO_option_dispatcher, + length_from=len_options)] + + +class PCO_PDU_Session_Id(PCO_Protocol): + name = "PCO PDU session ID" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", 1), + ByteField("PduSessionId", 1)] + + +class PCO_5GSM_Cause_Value(PCO_Protocol): + name = "PCO 5GSM Cause Value" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + PacketListField("Options", None, PCO_option_dispatcher, + length_from=len_options)] + + +class PCO_QoS_Rules_With_Support_Indicator(PCO_Protocol): + name = "PCO QoS Rules With Support Indicator" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + PacketListField("Options", None, PCO_option_dispatcher, + length_from=lambda pkt: pkt.length)] + + +class PCO_QoS_Flow_Descriptions_With_Support_Indicator(PCO_Protocol): + name = "PCO QoS Flow Descriptions With Support Indicator" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + PacketListField("Options", None, PCO_option_dispatcher, + length_from=lambda pkt: pkt.length)] + + +class PCO_S_Nssai(PCO_Protocol): + name = "PCO S-NSSAI" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + ConditionalField( + ByteField("SST", 0), lambda pkt: pkt.length > 0), + ConditionalField( + ShortField("SD", 0), lambda pkt: pkt.length > 1), + ConditionalField( + ByteField("Hplmn_Sst", 0), lambda pkt: pkt.length >= 4), + ConditionalField( + ShortField("Hplmn_Sd", 0), lambda pkt: pkt.length > 4)] + + +class PCO_Qos_Rules(PCO_Protocol): + name = "PCO QoS Rules" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + PacketListField("Options", None, PCO_option_dispatcher, + length_from=lambda pkt: pkt.length)] + + +class PCO_Session_AMBR(PCO_Protocol): + name = "PCO Session AMBR" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", 6), + ByteField("dlunit", 0), + ShortField("dlambr", 0), + ByteField("ulunit", 0), + ShortField("ulambr", 0)] + + +class PCO_QoS_Flow_Descriptions(PCO_Protocol): + name = "PCO QoS Flow Descriptions" + fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), + ByteField("length", None), + PacketListField("Options", None, PCO_option_dispatcher, + length_from=lambda pkt: pkt.length)] + + +class PCO_IPCP(PCO_Protocol): name = "PCO Internet Protocol Control Protocol" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), PacketField("PPP", None, PCO_PPP)] -class PCO_PPP_Auth(PCO_Option): +class PCO_PPP_Auth(PCO_Protocol): name = "PPP Password Authentication Protocol" fields_desc = [ByteField("Code", 0), ByteField("Identifier", 0), - ShortField("length", 0), + ShortField("length", None), ByteField("PeerID_length", 0), ConditionalField(StrFixedLenField( "PeerID", @@ -899,18 +1211,18 @@ class PCO_PPP_Auth(PCO_Option): lambda pkt: pkt.Password_length)] -class PCO_PasswordAuthentificationProtocol(PCO_Option): +class PCO_PasswordAuthentificationProtocol(PCO_Protocol): name = "PCO Password Authentication Protocol" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), PacketField("PPP", None, PCO_PPP_Auth)] -class PCO_PPP_Challenge(PCO_Option): +class PCO_PPP_Challenge(PCO_Protocol): name = "PPP Password Authentication Protocol" fields_desc = [ByteField("Code", 0), ByteField("Identifier", 0), - ShortField("length", 0), + ShortField("length", None), ByteField("value_size", 0), ConditionalField(StrFixedLenField( "value", "", @@ -922,21 +1234,31 @@ class PCO_PPP_Challenge(PCO_Option): lambda pkt: pkt.length)] -class PCO_ChallengeHandshakeAuthenticationProtocol(PCO_Option): +class PCO_ChallengeHandshakeAuthenticationProtocol(PCO_Protocol): name = "PCO Password Authentication Protocol" fields_desc = [ShortEnumField("type", None, PCO_PROTOCOL_TYPES), - ByteField("length", 0), + ByteField("length", None), PacketField("PPP", None, PCO_PPP_Challenge)] PCO_PROTOCOL_CLASSES = { 0x0001: PCO_P_CSCF_IPv6_Address_Request, + 0x0002: PCO_IM_CN_Subsystem_Signaling_Flag, 0x0003: PCO_DNS_Server_IPv6, 0x0005: PCO_SOF, 0x000a: PCO_IP_Allocation_via_NAS, 0x000c: PCO_P_CSCF_IPv4_Address_Request, 0x000d: PCO_DNS_Server_IPv4, 0x0010: PCO_IPv4_Link_MTU_Request, + 0x0012: PCO_P_CSCF_Re_selection_Support, + 0x001a: PCO_PDU_Session_Id, + 0x0022: PCO_5GSM_Cause_Value, + 0x0023: PCO_QoS_Rules_With_Support_Indicator, + 0x0024: PCO_QoS_Flow_Descriptions_With_Support_Indicator, + 0x001b: PCO_S_Nssai, + 0x001c: PCO_Qos_Rules, + 0x001d: PCO_Session_AMBR, + 0x001f: PCO_QoS_Flow_Descriptions, 0x8021: PCO_IPCP, 0xc023: PCO_PasswordAuthentificationProtocol, 0xc223: PCO_ChallengeHandshakeAuthenticationProtocol, @@ -963,6 +1285,32 @@ class IE_PCO(gtp.IE_Base): length_from=lambda pkt: pkt.length - 1)] +class IE_EPCO(gtp.IE_Base): + name = "IE Extended Protocol Configuration Options" + fields_desc = [ByteEnumField("ietype", 197, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + BitField("Extension", 0, 1), + BitField("SPARE", 0, 4), + BitField("PPP", 0, 3), + PacketListField("Protocols", None, PCO_protocol_dispatcher, + length_from=lambda pkt: pkt.length - 1)] + + +class IE_APCO(gtp.IE_Base): + name = "IE Additional Protocol Configuration Options" + fields_desc = [ByteEnumField("ietype", 163, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + BitField("extension", 0, 1), + BitField("SPARE", 0, 4), + BitField("PPP", 0, 3), + PacketListField("Protocols", None, PCO_protocol_dispatcher, + length_from=lambda pkt: pkt.length - 1)] + + class IE_PAA(gtp.IE_Base): name = "IE PAA" fields_desc = [ByteEnumField("ietype", 79, IEType), @@ -988,10 +1336,10 @@ class IE_Bearer_QoS(gtp.IE_Base): ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), - BitField("SPARE", 0, 1), + BitField("SPARE1", 0, 1), BitField("PCI", 0, 1), BitField("PriorityLevel", 0, 4), - BitField("SPARE", 0, 1), + BitField("SPARE2", 0, 1), BitField("PVI", 0, 1), ByteField("QCI", 0), BitField("MaxBitRateForUplink", 0, 40), @@ -1010,12 +1358,15 @@ class IE_ChargingID(gtp.IE_Base): class IE_ChargingCharacteristics(gtp.IE_Base): - name = "IE Charging ID" + name = "IE Charging Characteristics" + deprecated_fields = { + "ChargingCharacteristric": ("ChargingCharacteristic", "2.6.0") + } fields_desc = [ByteEnumField("ietype", 95, IEType), ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), - XShortField("ChargingCharacteristric", 0)] + XShortField("ChargingCharacteristic", 0)] class IE_PDN_type(gtp.IE_Base): @@ -1041,7 +1392,7 @@ class IE_UE_Timezone(gtp.IE_Base): class IE_Port_Number(gtp.IE_Base): name = "IE Port Number" fields_desc = [ByteEnumField("ietype", 126, IEType), - ShortField("length", 2), + ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), ShortField("PortNumber", RandShort())] @@ -1068,7 +1419,7 @@ class IE_SelectionMode(gtp.IE_Base): class IE_MMBR(gtp.IE_Base): name = "IE Max MBR/APN-AMBR (MMBR)" - fields_desc = [ByteEnumField("ietype", 72, IEType), + fields_desc = [ByteEnumField("ietype", 161, IEType), ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), @@ -1076,6 +1427,47 @@ class IE_MMBR(gtp.IE_Base): IntField("downlink_rate", 0)] +class IE_UPF_SelInd_Flags(gtp.IE_Base): + name = "IE UP Function Selection Indication Flags" + fields_desc = [ByteEnumField("ietype", 202, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + BitField("SPARE", 0, 7), + BitField("DCNR", 0, 1)] + + +class IE_FQCSID(gtp.IE_Base): + name = "IE FQ-CSID" + fields_desc = [ByteEnumField("ietype", 132, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + BitField("nodeid_type", 0, 4), + BitField("num_csid", 0, 4), + ConditionalField( + IPField("nodeid_v4", 0), + lambda pkt: pkt.nodeid_type == 0), + ConditionalField( + XBitField("nodeid_v6", "2001:db8:0:42::", 128), + lambda pkt: pkt.nodeid_type == 1), + ConditionalField( + BitField("nodeid_nonip", 0, 32), + lambda pkt: pkt.nodeid_type == 2), + ShortField("csid", 0)] + + +class IE_Ran_Nas_Cause(gtp.IE_Base): + name = "IE RAN/NAS Cause" + fields_desc = [ByteEnumField("ietype", 172, IEType), + ShortField("length", None), + BitField("CR_flag", 0, 4), + BitField("instance", 0, 4), + BitField("protocol_type", 0, 4), + BitField("cause_type", 0, 4), + ByteField("cause_value", 0)] + + # 3GPP TS 29.274 v16.1.0 section 8.67. class IE_PrivateExtension(gtp.IE_Base): name = "Private Extension" @@ -1085,10 +1477,8 @@ class IE_PrivateExtension(gtp.IE_Base): BitField("SPARE", 0, 4), BitField("instance", 0, 4), ShortEnumField("enterprisenum", None, IANA_ENTERPRISE_NUMBERS), - ] - - def extract_padding(self, s): - return s[:self.length], '' + StrLenField("proprietaryvalue", "", + length_from=lambda x: x.length - 2)] ietypecls = {1: IE_IMSI, @@ -1097,7 +1487,7 @@ def extract_padding(self, s): 71: IE_APN, 72: IE_AMBR, 73: IE_EPSBearerID, - 74: IE_IPv4, + 74: IE_IP_Address, 75: IE_MEI, 76: IE_MSISDN, 77: IE_Indication, @@ -1106,17 +1496,29 @@ def extract_padding(self, s): 80: IE_Bearer_QoS, 82: IE_RAT, 83: IE_ServingNetwork, + 84: IE_BearerTFT, 86: IE_ULI, 87: IE_FTEID, 93: IE_BearerContext, 94: IE_ChargingID, 95: IE_ChargingCharacteristics, + 97: IE_BearerFlags, 99: IE_PDN_type, + 107: IE_MMContext_EPS, + 109: IE_PDNConnection, 114: IE_UE_Timezone, 126: IE_Port_Number, 127: IE_APN_Restriction, 128: IE_SelectionMode, + 132: IE_FQCSID, + 136: IE_FQDN, + 145: IE_UCI, 161: IE_MMBR, + 163: IE_APCO, + 170: IE_ULI_Timestamp, + 172: IE_Ran_Nas_Cause, + 197: IE_EPCO, + 202: IE_UPF_SelInd_Flags, 255: IE_PrivateExtension} # @@ -1136,6 +1538,9 @@ class GTPV2EchoRequest(GTPV2Command): class GTPV2EchoResponse(GTPV2Command): name = "GTPv2 Echo Response" + def answers(self, other): + return isinstance(other, GTPV2EchoRequest) + class GTPV2CreateSessionRequest(GTPV2Command): name = "GTPv2 Create Session Request" @@ -1144,6 +1549,9 @@ class GTPV2CreateSessionRequest(GTPV2Command): class GTPV2CreateSessionResponse(GTPV2Command): name = "GTPv2 Create Session Response" + def answers(self, other): + return isinstance(other, GTPV2CreateSessionRequest) + class GTPV2DeleteSessionRequest(GTPV2Command): name = "GTPv2 Delete Session Request" @@ -1152,13 +1560,32 @@ class GTPV2DeleteSessionRequest(GTPV2Command): class GTPV2DeleteSessionResponse(GTPV2Command): name = "GTPv2 Delete Session Request" + def answers(self, other): + return isinstance(other, GTPV2DeleteSessionRequest) + class GTPV2ModifyBearerCommand(GTPV2Command): name = "GTPv2 Modify Bearer Command" -class GTPV2ModifyBearerFailureNotification(GTPV2Command): - name = "GTPv2 Modify Bearer Command" +class GTPV2ModifyBearerFailureIndication(GTPV2Command): + name = "GTPv2 Modify Bearer Failure Indication" + + +class GTPV2DeleteBearerCommand(GTPV2Command): + name = "GTPv2 Delete Bearer Command" + + +class GTPV2DeleteBearerFailureIndication(GTPV2Command): + name = "GTPv2 Delete Bearer Failure Indication" + + +class GTPV2BearerResourceCommand(GTPV2Command): + name = "GTPv2 Bearer Resource Command" + + +class GTPV2BearerResourceFailureIndication(GTPV2Command): + name = "GTPv2 Bearer Resource Failure Indication" class GTPV2DownlinkDataNotifFailureIndication(GTPV2Command): @@ -1172,6 +1599,20 @@ class GTPV2ModifyBearerRequest(GTPV2Command): class GTPV2ModifyBearerResponse(GTPV2Command): name = "GTPv2 Modify Bearer Response" + def answers(self, other): + return isinstance(other, GTPV2ModifyBearerRequest) + + +class GTPV2CreateBearerRequest(GTPV2Command): + name = "GTPv2 Create Bearer Request" + + +class GTPV2CreateBearerResponse(GTPV2Command): + name = "GTPv2 Create Bearer Response" + + def answers(self, other): + return isinstance(other, GTPV2CreateBearerRequest) + class GTPV2UpdateBearerRequest(GTPV2Command): name = "GTPv2 Update Bearer Request" @@ -1180,6 +1621,9 @@ class GTPV2UpdateBearerRequest(GTPV2Command): class GTPV2UpdateBearerResponse(GTPV2Command): name = "GTPv2 Update Bearer Response" + def answers(self, other): + return isinstance(other, GTPV2UpdateBearerRequest) + class GTPV2DeleteBearerRequest(GTPV2Command): name = "GTPv2 Delete Bearer Request" @@ -1205,6 +1649,21 @@ class GTPV2DeleteBearerResponse(GTPV2Command): name = "GTPv2 Delete Bearer Response" +class GTPV2ContextRequest(GTPV2Command): + name = "GTPv2 Context Request" + + +class GTPV2ContextResponse(GTPV2Command): + name = "GTPv2 Context Response" + + def answers(self, other): + return isinstance(other, GTPV2ContextRequest) + + +class GTPV2ContextAcknowledge(GTPV2Command): + name = "GTPv2 Context Acknowledge" + + class GTPV2CreateIndirectDataForwardingTunnelRequest(GTPV2Command): name = "GTPv2 Create Indirect Data Forwarding Tunnel Request" @@ -1212,6 +1671,12 @@ class GTPV2CreateIndirectDataForwardingTunnelRequest(GTPV2Command): class GTPV2CreateIndirectDataForwardingTunnelResponse(GTPV2Command): name = "GTPv2 Create Indirect Data Forwarding Tunnel Response" + def answers(self, other): + return isinstance( + other, + GTPV2CreateIndirectDataForwardingTunnelRequest + ) + class GTPV2DeleteIndirectDataForwardingTunnelRequest(GTPV2Command): name = "GTPv2 Delete Indirect Data Forwarding Tunnel Request" @@ -1220,6 +1685,12 @@ class GTPV2DeleteIndirectDataForwardingTunnelRequest(GTPV2Command): class GTPV2DeleteIndirectDataForwardingTunnelResponse(GTPV2Command): name = "GTPv2 Delete Indirect Data Forwarding Tunnel Response" + def answers(self, other): + return isinstance( + other, + GTPV2DeleteIndirectDataForwardingTunnelRequest + ) + class GTPV2ReleaseBearerRequest(GTPV2Command): name = "GTPv2 Release Bearer Request" @@ -1228,6 +1699,9 @@ class GTPV2ReleaseBearerRequest(GTPV2Command): class GTPV2ReleaseBearerResponse(GTPV2Command): name = "GTPv2 Release Bearer Response" + def answers(self, other): + return isinstance(other, GTPV2ReleaseBearerRequest) + class GTPV2DownlinkDataNotif(GTPV2Command): name = "GTPv2 Download Data Notification" @@ -1246,12 +1720,21 @@ class GTPV2DownlinkDataNotifAck(GTPV2Command): bind_layers(GTPHeader, GTPV2DeleteSessionRequest, gtp_type=36) bind_layers(GTPHeader, GTPV2DeleteSessionResponse, gtp_type=37) bind_layers(GTPHeader, GTPV2ModifyBearerCommand, gtp_type=64) -bind_layers(GTPHeader, GTPV2ModifyBearerFailureNotification, gtp_type=65) +bind_layers(GTPHeader, GTPV2ModifyBearerFailureIndication, gtp_type=65) +bind_layers(GTPHeader, GTPV2DeleteBearerCommand, gtp_type=66) +bind_layers(GTPHeader, GTPV2DeleteBearerFailureIndication, gtp_type=67) +bind_layers(GTPHeader, GTPV2BearerResourceCommand, gtp_type=68) +bind_layers(GTPHeader, GTPV2BearerResourceFailureIndication, gtp_type=69) bind_layers(GTPHeader, GTPV2DownlinkDataNotifFailureIndication, gtp_type=70) +bind_layers(GTPHeader, GTPV2CreateBearerRequest, gtp_type=95) +bind_layers(GTPHeader, GTPV2CreateBearerResponse, gtp_type=96) bind_layers(GTPHeader, GTPV2UpdateBearerRequest, gtp_type=97) bind_layers(GTPHeader, GTPV2UpdateBearerResponse, gtp_type=98) bind_layers(GTPHeader, GTPV2DeleteBearerRequest, gtp_type=99) bind_layers(GTPHeader, GTPV2DeleteBearerResponse, gtp_type=100) +bind_layers(GTPHeader, GTPV2ContextRequest, gtp_type=130) +bind_layers(GTPHeader, GTPV2ContextResponse, gtp_type=131) +bind_layers(GTPHeader, GTPV2ContextAcknowledge, gtp_type=132) bind_layers(GTPHeader, GTPV2SuspendNotification, gtp_type=162) bind_layers(GTPHeader, GTPV2SuspendAcknowledge, gtp_type=163) bind_layers(GTPHeader, GTPV2ResumeNotification, gtp_type=164) diff --git a/scapy/contrib/gxrp.py b/scapy/contrib/gxrp.py new file mode 100644 index 00000000000..73561915fd6 --- /dev/null +++ b/scapy/contrib/gxrp.py @@ -0,0 +1,220 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = Generic Attribute Register Protocol (GARP) +# scapy.contrib.status = loads + +""" + GARP - Generic Attribute Register Protocol + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :author: Sergey Matsievskiy, matsievskiysv@gmail.com + + :description: + + This module provides Scapy layers for the GARP protocol and its + two applications: GARP VLAN Registration Protocol (GVRP) and + GARP Multicast Registration Protocol (GMRP) + + normative references: + - IEEE 802.1D 2004 - Media Access Control (MAC) Bridges + - IEEE 802.1Q 1998 - Virtual Bridged Local Area Networks + +""" +from scapy.fields import ( + LenField, + EnumField, + ByteField, + PacketListField, + ShortField, + MACField, +) +from scapy.packet import Packet, bind_layers, split_layers +from scapy.layers.l2 import LLC, Dot3 +from scapy.error import warning + + +class GVRP(Packet): + """ + GVRP + """ + + name = "GVRP" + + # IEEE802.1Q-1998 11.2.3.1.3 + fields_desc = [ShortField("vlan", 1)] + + def extract_padding(self, s): + return b"", s + + +class GMRP_GROUP(Packet): + """ + GMRP Group + """ + + name = "GMRP Group" + + # IEEE802.1D-2004 10.3.1.4 + fields_desc = [MACField("addr", None)] + + def extract_padding(self, s): + return b"", s + + +class GMRP_SERVICE(Packet): + """ + GMRP Service + """ + + name = "GMRP Service" + + # IEEE802.1D-2004 10.3.1.4 + fields_desc = [ + EnumField( + "event", + 0, + {0x0: "All Groups", 0x1: "All Unregistered Groups"}, + fmt="B", + ) + ] + + def extract_padding(self, s): + return b"", s + + +class GARP_ATTRIBUTE(Packet): + """ + GARP attribute container + """ + + name = "GARP Attribute" + + # IEEE802.1D-2004 12.10.2.4-5 + fields_desc = [ + LenField("len", None, fmt="B", adjust=lambda l: l + 2), + EnumField( + "event", + 0, + { + 0x0: "LeaveAll", + 0x1: "JoinEmpty", + 0x2: "JoinIn", + 0x3: "LeaveEmpty", + 0x4: "LeaveIn", + 0x5: "Empty", + }, + fmt="B", + ), + ] + + def do_dissect(self, s): + s = super(GARP_ATTRIBUTE, self).do_dissect(s) + if self.len is not None and self.event == 0 and self.len > 2: + warning("Non-empty payload at LeaveAll event") + return s + + def extract_padding(self, s): + boundary = self.len - 2 + return s[:boundary], s[boundary:] + + def guess_payload_class(self, payload): + try: + garp_message = self.parent + garp = garp_message.parent + llc = garp.underlayer + dot3 = llc.underlayer + if ( + dot3.dst == "01:80:c2:00:00:21" + ): # IEEE802.1D-2004 12.4 Table 12-1 + return GVRP + elif ( + dot3.dst == "01:80:c2:00:00:20" + ): # IEEE802.1D-2004 12.4 Table 12-1 + if garp_message.type == 1: # IEEE802.1D-2004 10.3.1.3 + return GMRP_GROUP + elif garp_message.type == 2: # IEEE802.1D-2004 10.3.1.3 + return GMRP_SERVICE + except AttributeError: + pass + return super(GARP_ATTRIBUTE, self).guess_payload_class(payload) + + +def parse_next_attr(pkt, lst, cur, remain): + # IEEE802.1D-2004 12.10.2.7 + if not remain or len(remain) == 0 or remain[0:1] == b"\x00": + return None + elif ord(remain[0:1]) >= 2: # minimal attribute size + return GARP_ATTRIBUTE + else: + return None + + +class GARP_MESSAGE(Packet): + """ + GARP message container + """ + + name = "GARP Message" + fields_desc = [ + ByteField("type", 0x01), + PacketListField("attrs", [], next_cls_cb=parse_next_attr), + ByteField("end_mark", 0x0), + ] + + def extract_padding(self, s): + return b"", s + + +def parse_next_msg(pkt, lst, cur, remain): + # IEEE802.1D-2004 12.10.2.7 + if not remain and len(remain) == 0 or remain[0:1] == b"\x00": + return None + else: + return GARP_MESSAGE + + +class GARP(Packet): + """ + GARP packet + """ + + name = "GARP" + + fields_desc = [ + ShortField("proto_id", 0x0001), # IEEE802.1D-2004 12.10.2.1 + PacketListField("msgs", [], next_cls_cb=parse_next_msg), + ByteField("end_mark", 0x0), + ] # IEEE802.1D-2004 12.10.2.7 + + +class LLC_GARP(LLC): + """ + Dummy class for layer binding + """ + + payload_guess = [] + + +split_layers(Dot3, LLC) +# IEEE802.1D-2004 12.4 Table 12-1 +for mac in ["01:80:c2:00:00:20", + "01:80:c2:00:00:21", + "01:80:c2:00:00:22", + "01:80:c2:00:00:23", + "01:80:c2:00:00:24", + "01:80:c2:00:00:25", + "01:80:c2:00:00:26", + "01:80:c2:00:00:27", + "01:80:c2:00:00:28", + "01:80:c2:00:00:29", + "01:80:c2:00:00:2a", + "01:80:c2:00:00:2b", + "01:80:c2:00:00:2c", + "01:80:c2:00:00:2d", + "01:80:c2:00:00:2e", + "01:80:c2:00:00:2f"]: + bind_layers(Dot3, LLC_GARP, dst=mac) +bind_layers(Dot3, LLC) +bind_layers(LLC_GARP, GARP) diff --git a/scapy/contrib/hicp.py b/scapy/contrib/hicp.py new file mode 100644 index 00000000000..61e7448ec96 --- /dev/null +++ b/scapy/contrib/hicp.py @@ -0,0 +1,278 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2023 - Claire VACHEROT + +"""HICP + +Support for HICP (Host IP Control Protocol). + +This protocol is used by HMS Anybus software for device discovery and +configuration. + +Note : As the specification is not public, this layer was built based on the +Wireshark dissector and HMS's HICP DLL. It was tested with a Anybus X-gateway +device. Therefore, this implementation may differ from what is written in the +standard. +""" + +# scapy.contrib.name = HICP +# scapy.contrib.description = HMS Anybus Host IP Control Protocol +# scapy.contrib.status = loads + +from re import match + +from scapy.packet import Packet, bind_layers, bind_bottom_up +from scapy.fields import StrField, MACField, IPField, ByteField, RawVal +from scapy.layers.inet import UDP + +# HICP command codes +CMD_MODULESCAN = b"Module scan" +CMD_MSRESPONSE = b"Module scan response" +CMD_CONFIGURE = b"Configure" +CMD_RECONFIGURED = b"Reconfigured" +CMD_INVALIDCONF = b"Invalid Configuration" +CMD_INVALIDPWD = b"Invalid Password" +CMD_WINK = b"Wink" +# These commands are implemented in the DLL but never seen in use +CMD_START = b"Start" +CMD_STOP = b"Stop" + +# Most of the fields have the format "KEY = value" for each field +KEYS = { + "protocol_version": "Protocol version", + "fieldbus_type": "FB type", + "module_version": "Module version", + "mac_address": "MAC", + "new_password": "New password", + "password": "PSWD", + "ip_address": "IP", + "subnet_mask": "SN", + "gateway_address": "GW", + "dhcp": "DHCP", + "hostname": "HN", + "dns1": "DNS1", + "dns2": "DNS2" +} + +# HICP MAC format is xx-xx-xx-xx-xx-xx (not with :) as str. +FROM_MACFIELD = lambda x: x.replace(":", "-") +TO_MACFIELD = lambda x: x.replace("-", ":") + +# Note on building and dissecting: Since the protocol is primarily text-based +# but also highly inconsistent in terms of message format, most of the +# dissection and building process must be reworked for each message type. + + +class HICPConfigure(Packet): + name = "Configure request" + fields_desc = [ + MACField("target", "ff:ff:ff:ff:ff:ff"), + StrField("password", ""), + StrField("new_password", ""), + IPField("ip_address", "255.255.255.255"), + IPField("subnet_mask", "255.255.255.0"), + IPField("gateway_address", "0.0.0.0"), + StrField("dhcp", "OFF"), # ON or OFF + StrField("hostname", ""), + IPField("dns1", "0.0.0.0"), + IPField("dns2", "0.0.0.0"), + ByteField("padding", 0) + ] + + def post_build(self, p, pay): + p = ["{0}: {1};".format(CMD_CONFIGURE.decode('utf-8'), + FROM_MACFIELD(self.target))] + for field in self.fields_desc[1:]: + if field.name in KEYS: + value = getattr(self, field.name) + if isinstance(value, bytes): + value = value.decode('utf-8') + if field.name in ["password", "new_password"] and not value: + continue + key = KEYS[field.name] + # The key for password is not the same as usual... + if field.name == "password": + key = "Password" + p.append("{0} = {1};".format(key, value)) + return "".join(p).encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(".*: ([^;]+);", s.decode('utf-8')) + if res: + self.target = TO_MACFIELD(res.group(1)) + s = s[len(self.target) + 3:] + for arg in s.split(b";"): + kv = [x.strip().replace(b"\x00", b"") for x in arg.split(b"=")] + if len(kv) != 2 or not kv[1]: + continue + kv[0] = kv[0].decode('utf-8') + if kv[0] in KEYS.values(): + field = [x for x, y in KEYS.items() if y == kv[0]][0] + setattr(self, field, kv[1]) + + +class HICPReconfigured(Packet): + name = "Reconfigured" + fields_desc = [ + MACField("source", "ff:ff:ff:ff:ff:ff") + ] + + def post_build(self, p, pay): + p = "{0}: {1}".format(CMD_RECONFIGURED.decode('utf-8'), + FROM_MACFIELD(self.source)) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(r".*: ([a-fA-F0-9\-\:]+)", s.decode('utf-8')) + if res: + self.source = TO_MACFIELD(res.group(1)) + return None + + +class HICPInvalidConfiguration(Packet): + name = "Invalid configuration" + fields_desc = [ + MACField("source", "ff:ff:ff:ff:ff:ff") + ] + + def post_build(self, p, pay): + p = "{0}: {1}".format(CMD_INVALIDCONF.decode('utf-8'), + FROM_MACFIELD(self.source)) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(r".*: ([a-fA-F0-9\-\:]+)", s.decode('utf-8')) + if res: + self.source = TO_MACFIELD(res.group(1)) + return None + + +class HICPInvalidPassword(Packet): + name = "Invalid password" + fields_desc = [ + MACField("source", "ff:ff:ff:ff:ff:ff") + ] + + def post_build(self, p, pay): + p = "{0}: {1}".format(CMD_INVALIDPWD.decode('utf-8'), + FROM_MACFIELD(self.source)) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match(r".*: ([a-fA-F0-9\-\:]+)", s.decode('utf-8')) + if res: + self.source = TO_MACFIELD(res.group(1)) + return None + + +class HICPWink(Packet): + name = "Wink" + fields_desc = [ + MACField("target", "ff:ff:ff:ff:ff:ff"), + ByteField("padding", 0) + ] + + def post_build(self, p, pay): + p = "To: {0};{1};".format(FROM_MACFIELD(self.target), + CMD_WINK.decode('utf-8').upper()) + return p.encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + res = match("^To: ([^;]+);", s.decode('utf-8')) + if res: + self.target = TO_MACFIELD(res.group(1)) + + +class HICPModuleScanResponse(Packet): + name = "Module scan response" + fields_desc = [ + StrField("protocol_version", "1.00"), + StrField("fieldbus_type", ""), + StrField("module_version", ""), + MACField("mac_address", "ff:ff:ff:ff:ff:ff"), + IPField("ip_address", "255.255.255.255"), + IPField("subnet_mask", "255.255.255.0"), + IPField("gateway_address", "0.0.0.0"), + StrField("dhcp", "OFF"), # ON or OFF + StrField("password", "OFF"), # ON or OFF + StrField("hostname", ""), + IPField("dns1", "0.0.0.0"), + IPField("dns2", "0.0.0.0"), + ByteField("padding", 0) + ] + + def post_build(self, p, pay): + p = [] + for field in self.fields_desc: + if field.name in KEYS: + value = getattr(self, field.name) + if isinstance(value, bytes): + value = value.decode('utf-8') + p.append("{0} = {1};".format(KEYS[field.name], value)) + return "".join(p).encode('utf-8') + b"\x00" + pay + + def do_dissect(self, s): + for arg in s.split(b";"): + kv = [x.strip().replace(b"\x00", b"") for x in arg.split(b"=")] + if len(kv) != 2 or not kv[1]: + continue + kv[0] = kv[0].decode('utf-8') + if kv[0] in KEYS.values(): + field = [x for x, y in KEYS.items() if y == kv[0]][0] + if field == "mac_address": + kv[1] = TO_MACFIELD(kv[1].decode('utf-8')) + setattr(self, field, kv[1]) + + +class HICPModuleScan(Packet): + name = "Module scan request" + fields_desc = [ + StrField("hicp_command", CMD_MODULESCAN), + ByteField("padding", 0) + ] + + def do_dissect(self, s): + if len(s) > len(CMD_MODULESCAN): + self.hicp_command = s[:len(CMD_MODULESCAN)] + self.padding = s[len(CMD_MODULESCAN):] + else: + self.padding = RawVal(s) + + def post_build(self, p, pay): + return p.upper() + pay + + +class HICP(Packet): + name = "HICP" + fields_desc = [ + StrField("hicp_command", "") + ] + + def do_dissect(self, s): + for cmd in [CMD_MODULESCAN, CMD_CONFIGURE, CMD_RECONFIGURED, + CMD_INVALIDCONF, CMD_INVALIDPWD]: + if s[:len(cmd)] == cmd: + self.hicp_command = cmd + return s[len(cmd):] + if s[:len("To:")] == b"To:": + self.hicp_command = CMD_WINK + else: + self.hicp_command = CMD_MSRESPONSE + return s + + def post_build(self, p, pay): + p = p[len(self.hicp_command):] + return p + pay + + +bind_bottom_up(UDP, HICP, dport=3250) +bind_bottom_up(UDP, HICP, sport=3250) +bind_layers(UDP, HICP, sport=3250, dport=3250) +bind_layers(HICP, HICPModuleScan, hicp_command=CMD_MODULESCAN) +bind_layers(HICP, HICPModuleScanResponse, hicp_command=CMD_MSRESPONSE) +bind_layers(HICP, HICPWink, hicp_command=CMD_WINK) +bind_layers(HICP, HICPConfigure, hicp_command=CMD_CONFIGURE) +bind_layers(HICP, HICPReconfigured, hicp_command=CMD_RECONFIGURED) +bind_layers(HICP, HICPInvalidConfiguration, hicp_command=CMD_INVALIDCONF) +bind_layers(HICP, HICPInvalidPassword, hicp_command=CMD_INVALIDPWD) diff --git a/scapy/contrib/homeplugav.py b/scapy/contrib/homeplugav.py index 57823766868..d408815a38d 100644 --- a/scapy/contrib/homeplugav.py +++ b/scapy/contrib/homeplugav.py @@ -1,40 +1,48 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = HomePlugAV Layer # scapy.contrib.status = loads -from __future__ import absolute_import +""" +HomePlugAV Layer for Scapy + +Copyright (C) FlUxIuS (Sebastien Dudek) + +HomePlugAV Management Message Type +Key (type value) : Description +""" + import struct from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, ByteEnumField, ByteField, \ - ConditionalField, EnumField, FieldLenField, IntField, LEIntField, \ - LELongField, LEShortField, MACField, PacketListField, ShortField, \ - StrFixedLenField, StrLenField, X3BytesField, XByteField, XIntField, \ - XLongField, XShortField, LEShortEnumField +from scapy.fields import ( + BitField, + ByteEnumField, + ByteField, + ConditionalField, + EnumField, + FieldLenField, + IntField, + LEIntField, + LELongField, + LEShortEnumField, + LEShortField, + MACField, + OUIField, + PacketListField, + ShortField, + StrFixedLenField, + StrLenField, + X3BytesField, + XByteField, + XIntField, + XLongField, + XShortField, +) from scapy.layers.l2 import Ether -from scapy.modules.six.moves import range -""" - Copyright (C) HomePlugAV Layer for Scapy by FlUxIuS (Sebastien Dudek) -""" - -""" - HomePlugAV Management Message Type - Key (type value) : Description -""" HPAVTypeList = {0xA000: "'Get Device/sw version Request'", 0xA001: "'Get Device/sw version Confirmation'", 0xA008: "'Read MAC Memory Request'", @@ -90,7 +98,7 @@ # Qualcomm Vendor Specific Management Message Types; # # from https://github.com/qca/open-plc-utils/blob/master/mme/qualcomm.h # ######################################################################### -# Commented commands are already in HPAVTypeList, the other have to be implemted # noqa: E501 +# Commented commands are already in HPAVTypeList, the other have to be implemented # noqa: E501 QualcommTypeList = { # 0xA000 : "VS_SW_VER", 0xA004: "VS_WR_MEM", # 0xA008 : "VS_RD_MEM", @@ -176,7 +184,7 @@ class MACManagementHeader(Packet): class VendorMME(Packet): name = "VendorMME " - fields_desc = [X3BytesField("OUI", 0x00b052)] + fields_desc = [OUIField("OUI", 0x00b052)] class GetDeviceVersion(Packet): @@ -633,10 +641,10 @@ class WriteModuleDataRequest(Packet): def post_build(self, p, pay): if self.DataLen is None: _len = len(self.ModuleData) - p = p[:2] + struct.pack('h', _len) + p[4:] + p = p[:2] + struct.pack('= pkt.__offset and 0x1FBD <= pkt.__offset + pkt.__length)), # noqa: E501 ConditionalField(XByteField("OptimizationBackwardCompatible", 0), lambda pkt:(0x1FBD >= pkt.__offset and 0x1FBE <= pkt.__offset + pkt.__length)), # noqa: E501 - ConditionalField(XByteField("reserved_21", 0), + ConditionalField(XByteField("reserved_21b", 0), lambda pkt:(0x1FBE >= pkt.__offset and 0x1FBF <= pkt.__offset + pkt.__length)), # noqa: E501 ConditionalField(XByteField("MaxPbsPerSymbol", 0), lambda pkt:(0x1FBF >= pkt.__offset and 0x1FC0 <= pkt.__offset + pkt.__length)), # noqa: E501 diff --git a/scapy/contrib/homepluggp.py b/scapy/contrib/homepluggp.py index b7c5d35b8b3..4e89492c6db 100644 --- a/scapy/contrib/homepluggp.py +++ b/scapy/contrib/homepluggp.py @@ -1,23 +1,10 @@ -#! /usr/bin/env python - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = HomePlugGP Layer # scapy.contrib.status = loads -from __future__ import absolute_import from scapy.packet import Packet, bind_layers from scapy.fields import ByteEnumField, ByteField, FieldLenField, \ @@ -143,7 +130,7 @@ class CM_SLAC_MATCH_REQ(Packet): fields_desc = [ByteField("ApplicationType", 0x0), ByteField("SecurityType", 0x0), FieldLenField("MatchVariableFieldLen", None, - count_of="VariableField", fmt="H"), + length_of="VariableField", fmt=". +# See https://scapy.net/ for more information # scapy.contrib.description = HomePlugSG Layer # scapy.contrib.status = loads -from __future__ import absolute_import from scapy.packet import Packet, bind_layers from scapy.fields import FieldLenField, StrFixedLenField, StrLenField diff --git a/scapy/contrib/http2.py b/scapy/contrib/http2.py index 1a0c20067f7..6c4706608b1 100644 --- a/scapy/contrib/http2.py +++ b/scapy/contrib/http2.py @@ -1,23 +1,14 @@ -############################################################################# -# # -# http2.py --- HTTP/2 support for Scapy # -# see RFC7540 and RFC7541 # -# for more information # -# # -# Copyright (C) 2016 Florian Maury # -# # -# This file is part of Scapy # -# Scapy is free software: you can redistribute it and/or modify it # -# under the terms of the GNU General Public License version 2 as # -# published by the Free Software Foundation. # -# # -# This program is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -# # -############################################################################# -"""http2 Module +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2016 Florian Maury + +""" +http2 + +HTTP/2 support for Scapy +see RFC7540 and RFC7541 for more information + Implements packets and fields required to encode/decode HTTP/2 Frames and HPack encoded headers """ @@ -27,21 +18,25 @@ # base_classes triggers an unwanted import warning -from __future__ import absolute_import -from __future__ import print_function import abc import re -import sys from io import BytesIO import struct -import scapy.modules.six as six -from scapy.compat import raw, plain_str, bytes_hex, orb, chb, bytes_encode +from scapy.compat import raw, plain_str, hex_bytes, orb, chb, bytes_encode # Only required if using mypy-lang for static typing # Most symbols are used in mypy-interpreted "comments". # Sized must be one of the superclasses of a class implementing __len__ -from scapy.compat import Optional, List, Union, Callable, Any, \ - Tuple, Sized, Pattern # noqa: F401 +from typing import ( + Optional, + List, + Union, + Callable, + Any, + Tuple, + Sized, + Pattern, +) from scapy.base_classes import Packet_metaclass # noqa: F401 import scapy.fields as fields @@ -73,9 +68,9 @@ def __init__(self, name, default, size): :return: None :raises: AssertionError """ - assert(default >= 0) + assert default >= 0 # size can be negative if encoding is little-endian (see rev property of bitfields) # noqa: E501 - assert(size != 0) + assert size != 0 self._magic = default super(HPackMagicBitField, self).__init__(name, default, size) @@ -106,7 +101,7 @@ def getfield(self, pkt, s): assert ( isinstance(r, tuple) and len(r) == 2 and - isinstance(r[1], six.integer_types) + isinstance(r[1], int) ), 'Second element of BitField.getfield return value expected to be an int or a long; API change detected' # noqa: E501 assert r[1] == self._magic, 'Invalid value parsed from s; error in class guessing detected!' # noqa: E501 return r @@ -198,14 +193,14 @@ def __init__(self, name, default, size): :return: None :raises: AssertionError """ - assert(default is None or (isinstance(default, six.integer_types) and default >= 0)) # noqa: E501 - assert(0 < size <= 8) + assert default is None or (isinstance(default, int) and default >= 0) + assert 0 < size <= 8 super(AbstractUVarIntField, self).__init__(name, default) self.size = size self._max_value = (1 << self.size) - 1 - # Configuring the fake property that is useless for this class but that is # noqa: E501 - # expected from BitFields + # Configuring the fake property that is useless for this class + # but that is expected from BitFields self.rev = False def h2i(self, pkt, x): @@ -216,7 +211,7 @@ def h2i(self, pkt, x): :return: int|None: the converted value. :raises: AssertionError """ - assert(not isinstance(x, six.integer_types) or x >= 0) + assert not isinstance(x, int) or x >= 0 return x def i2h(self, pkt, x): @@ -239,7 +234,7 @@ def _detect_multi_byte(self, fb): :return: bool: True if multibyte repr detected, else False. :raises: AssertionError """ - assert(isinstance(fb, int) or len(fb) == 1) + assert isinstance(fb, int) or len(fb) == 1 return (orb(fb) & self._max_value) == self._max_value def _parse_multi_byte(self, s): @@ -253,7 +248,7 @@ def _parse_multi_byte(self, s): :raises:: Scapy_Exception if the input value encodes an integer larger than 1<<64 # noqa: E501 """ - assert(len(s) >= 2) + assert len(s) >= 2 tmp_len = len(s) @@ -275,7 +270,7 @@ def _parse_multi_byte(self, s): value += byte << (7 * (i - 1)) value += self._max_value - assert(value >= 0) + assert value >= 0 return value def m2i(self, pkt, x): @@ -289,7 +284,7 @@ def m2i(self, pkt, x): :param str|(str, int) x: the string to convert. If bits were consumed by a previous bitfield-compatible field. # noqa: E501 :raises: AssertionError """ - assert(isinstance(x, bytes) or (isinstance(x, tuple) and x[1] >= 0)) + assert isinstance(x, bytes) or (isinstance(x, tuple) and x[1] >= 0) if isinstance(x, tuple): assert (8 - x[1]) == self.size, 'EINVAL: x: not enough bits remaining in current byte to read the prefix' # noqa: E501 @@ -303,7 +298,7 @@ def m2i(self, pkt, x): else: ret = orb(val[0]) & self._max_value - assert(ret >= 0) + assert ret >= 0 return ret def i2m(self, pkt, x): @@ -314,7 +309,7 @@ def i2m(self, pkt, x): :return: str: the converted value. :raises: AssertionError """ - assert(x >= 0) + assert x >= 0 if x < self._max_value: return chb(x) @@ -342,14 +337,14 @@ def any2i(self, pkt, x): """ if isinstance(x, type(None)): return x - if isinstance(x, six.integer_types): - assert(x >= 0) + if isinstance(x, int): + assert x >= 0 ret = self.h2i(pkt, x) - assert(isinstance(ret, six.integer_types) and ret >= 0) + assert isinstance(ret, int) and ret >= 0 return ret elif isinstance(x, bytes): ret = self.m2i(pkt, x) - assert (isinstance(ret, six.integer_types) and ret >= 0) + assert (isinstance(ret, int) and ret >= 0) return ret assert False, 'EINVAL: x: No idea what the parameter format is' @@ -383,14 +378,14 @@ def addfield(self, pkt, s, val): field. :raises: AssertionError """ - assert(val >= 0) + assert val >= 0 if isinstance(s, bytes): assert self.size == 8, 'EINVAL: s: tuple expected when prefix_len is not a full byte' # noqa: E501 return s + self.i2m(pkt, val) # s is a tuple - # assert(s[1] >= 0) - # assert(s[2] >= 0) + # assert s[1] >= 0 + # assert s[2] >= 0 # assert (8 - s[1]) == self.size, 'EINVAL: s: not enough bits remaining in current byte to read the prefix' # noqa: E501 if val >= self._max_value: @@ -411,7 +406,7 @@ def _detect_bytelen_from_str(s): :return: The bytelength of the AbstractUVarIntField. :raises: AssertionError """ - assert(len(s) >= 2) + assert len(s) >= 2 tmp_len = len(s) i = 1 @@ -420,7 +415,7 @@ def _detect_bytelen_from_str(s): assert i < tmp_len, 'EINVAL: s: out-of-bound read: unfinished AbstractUVarIntField detected' # noqa: E501 ret = i + 1 - assert(ret >= 0) + assert ret >= 0 return ret def i2len(self, pkt, x): @@ -430,7 +425,7 @@ def i2len(self, pkt, x): :param int x: the positive or null value whose binary size if requested. # noqa: E501 :raises: AssertionError """ - assert(x >= 0) + assert x >= 0 if x < self._max_value: return 1 @@ -444,7 +439,7 @@ def i2len(self, pkt, x): i += 1 ret = i - assert(ret >= 0) + assert ret >= 0 return ret def getfield(self, pkt, s): @@ -461,10 +456,10 @@ def getfield(self, pkt, s): :raises: AssertionError """ if isinstance(s, tuple): - assert(len(s) == 2) + assert len(s) == 2 temp = s # type: Tuple[str, int] ts, ti = temp - assert(ti >= 0) + assert ti >= 0 assert 8 - ti == self.size, 'EINVAL: s: not enough bits remaining in current byte to read the prefix' # noqa: E501 val = ts else: @@ -477,7 +472,7 @@ def getfield(self, pkt, s): tmp_len = 1 ret = val[tmp_len:], self.m2i(pkt, s) - assert(ret[1] >= 0) + assert ret[1] >= 0 return ret def randval(self): @@ -496,8 +491,8 @@ def __init__(self, name, default, size): :param default: the default value for this field instance. default must be positive or null. # noqa: E501 :raises: AssertionError """ - assert(default >= 0) - assert(0 < size <= 8) + assert default >= 0 + assert 0 < size <= 8 super(UVarIntField, self).__init__(name, default, size) self.size = size @@ -517,7 +512,7 @@ def h2i(self, pkt, x): :raises: AssertionError """ ret = super(UVarIntField, self).h2i(pkt, x) - assert(not isinstance(ret, type(None)) and ret >= 0) + assert not isinstance(ret, type(None)) and ret >= 0 return ret def i2h(self, pkt, x): @@ -530,7 +525,7 @@ def i2h(self, pkt, x): :raises: AssertionError """ ret = super(UVarIntField, self).i2h(pkt, x) - assert(not isinstance(ret, type(None)) and ret >= 0) + assert not isinstance(ret, type(None)) and ret >= 0 return ret def any2i(self, pkt, x): @@ -543,7 +538,7 @@ def any2i(self, pkt, x): :raises: AssertionError """ ret = super(UVarIntField, self).any2i(pkt, x) - assert(not isinstance(ret, type(None)) and ret >= 0) + assert not isinstance(ret, type(None)) and ret >= 0 return ret def i2repr(self, pkt, x): @@ -578,8 +573,8 @@ def __init__(self, name, default, size, length_of, adjust=lambda x: x): :return: None :raises: AssertionError """ - assert(default is None or default >= 0) - assert(0 < size <= 8) + assert default is None or default >= 0 + assert 0 < size <= 8 super(FieldUVarLenField, self).__init__(name, default, size) self._length_of = length_of @@ -634,24 +629,15 @@ def _compute_value(self, pkt): fld, fval = pkt.getfield_and_val(self._length_of) val = fld.i2len(pkt, fval) ret = self._adjust(val) - assert(ret >= 0) + assert ret >= 0 return ret ############################################################################### # HPACK String Fields # ############################################################################### -# Welcome the magic of Python inconsistencies ! -# https://stackoverflow.com/a/41622155 - -if sys.version_info >= (3, 4): - ABC = abc.ABC -else: - ABC = abc.ABCMeta('ABC', (), {}) - - -class HPackStringsInterface(ABC, Sized): # type: ignore +class HPackStringsInterface(Sized, metaclass=abc.ABCMeta): # type: ignore @abc.abstractmethod def __str__(self): pass @@ -1024,7 +1010,7 @@ def _huffman_encode_char(cls, c): if isinstance(c, EOS): return cls.static_huffman_code[-1] else: - assert(isinstance(c, int) or len(c) == 1) + assert isinstance(c, int) or len(c) == 1 return cls.static_huffman_code[orb(c)] @classmethod @@ -1051,7 +1037,7 @@ def huffman_encode(cls, s): ibl += padlen ret = i, ibl - assert(ret[0] >= 0) + assert ret[0] >= 0 assert (ret[1] >= 0) return ret @@ -1065,12 +1051,12 @@ def huffman_decode(cls, i, ibl): :return: str: the string decoded from the bitstring :raises: AssertionError, InvalidEncodingException """ - assert(i >= 0) - assert(ibl >= 0) + assert i >= 0 + assert ibl >= 0 if isinstance(cls.static_huffman_tree, type(None)): cls.huffman_compute_decode_tree() - assert(not isinstance(cls.static_huffman_tree, type(None))) + assert not isinstance(cls.static_huffman_tree, type(None)) s = [] j = 0 @@ -1124,8 +1110,8 @@ def huffman_conv2str(cls, bit_str, bit_len): :return: str: the converted bitstring as a bytestring. :raises: AssertionError """ - assert(bit_str >= 0) - assert(bit_len >= 0) + assert bit_str >= 0 + assert bit_len >= 0 byte_len = bit_len // 8 rem_bit = bit_len % 8 @@ -1159,8 +1145,8 @@ def huffman_conv2bitstring(cls, s): i = (i << 8) + orb(c) ret = i, ibl - assert(ret[0] >= 0) - assert(ret[1] >= 0) + assert ret[0] >= 0 + assert ret[1] >= 0 return ret @classmethod @@ -1289,9 +1275,9 @@ def any2i(self, pkt, x): :raises: AssertionError, InvalidEncodingException """ if isinstance(x, bytes): - assert(isinstance(pkt, packet.Packet)) + assert isinstance(pkt, packet.Packet) return self.m2i(pkt, x) - assert(isinstance(x, HPackStringsInterface)) + assert isinstance(x, HPackStringsInterface) return x def i2m(self, pkt, x): @@ -1333,14 +1319,14 @@ def guess_payload_class(self, payload): # underlayer packet return config.conf.padding_layer - def self_build(self, field_pos_list=None): + def self_build(self, **kwargs): # type: (Any) -> str """self_build is overridden because type and len are determined at build time, based on the "data" field internal type """ if self.getfieldval('type') is None: self.type = 1 if isinstance(self.getfieldval('data'), HPackZString) else 0 # noqa: E501 - return super(HPackHdrString, self).self_build(field_pos_list) + return super(HPackHdrString, self).self_build(**kwargs) class HPackHeaders(packet.Packet): @@ -1487,7 +1473,7 @@ def get_data_len(self): padding_len_len = fld.i2len(self, fval) ret = self.s_len - padding_len_len - padding_len - assert(ret >= 0) + assert ret >= 0 return ret def pre_dissect(self, s): @@ -1568,7 +1554,7 @@ def get_hdrs_len(self): padding_len_len = fld.i2len(self, fval) ret = self.s_len - padding_len_len - padding_len - assert(ret >= 0) + assert ret >= 0 return ret def pre_dissect(self, s): @@ -1646,7 +1632,7 @@ def get_hdrs_len(self): (bit_cnt / 8) - weight_len ) - assert(ret >= 0) + assert ret >= 0 return ret def pre_dissect(self, s): @@ -1787,7 +1773,7 @@ def __init__(self, *args, **kwargs): """ # RFC7540 par6.5 p36 - assert( + assert ( len(args) == 0 or ( isinstance(args[0], bytes) and len(args[0]) % 6 == 0 @@ -1857,7 +1843,7 @@ def get_hdrs_len(self): padding_len - (bit_len / 8) ) - assert(ret >= 0) + assert ret >= 0 return ret def pre_dissect(self, s): @@ -1894,10 +1880,9 @@ def __init__(self, *args, **kwargs): :raises: AssertionError """ # RFC7540 par6.7 p42 - assert( + assert ( len(args) == 0 or ( - (isinstance(args[0], bytes) or - isinstance(args[0], str)) and + isinstance(args[0], (bytes, str)) and len(args[0]) == 8 ) ), 'Invalid ping frame; length is not 8' @@ -1938,10 +1923,9 @@ def __init__(self, *args, **kwargs): :raises: AssertionError """ # RFC7540 par6.9 p46 - assert( + assert ( len(args) == 0 or ( - (isinstance(args[0], bytes) or - isinstance(args[0], str)) and + isinstance(args[0], (bytes, str)) and len(args[0]) == 4 ) ), 'Invalid window update frame; length is not 4' @@ -2069,7 +2053,7 @@ def extract_padding(self, s): :return: (str, str): the padding and the payload data strings :raises: AssertionError """ - assert isinstance(self.len, six.integer_types) and self.len >= 0, 'Invalid length: negative len?' # noqa: E501 + assert isinstance(self.len, int) and self.len >= 0, 'Invalid length: negative len?' # noqa: E501 assert len(s) >= self.len, 'Invalid length: string too short for this length' # noqa: E501 return s[:self.len], s[self.len:] @@ -2084,7 +2068,7 @@ def post_build(self, p, pay): # This logic, while awkward in the post_build and more reasonable in # a self_build is implemented here for performance tricks reason if self.getfieldval('len') is None: - assert(len(pay) < (1 << 24)), 'Invalid length: payload is too long' + assert len(pay) < (1 << 24), 'Invalid length: payload is too long' p = struct.pack('!L', len(pay))[1:] + p[3:] return super(H2Frame, self).post_build(p, pay) @@ -2123,7 +2107,7 @@ def guess_payload_class(self, payload): # HTTP/2 Connection Preface # # noqa: E501 # From RFC 7540 par3.5 -H2_CLIENT_CONNECTION_PREFACE = bytes_hex('505249202a20485454502f322e300d0a0d0a534d0d0a0d0a') # noqa: E501 +H2_CLIENT_CONNECTION_PREFACE = hex_bytes('505249202a20485454502f322e300d0a0d0a534d0d0a0d0a') # noqa: E501 ############################################################################### @@ -2143,7 +2127,7 @@ def __init__(self, name, value): """ :raises: AssertionError """ - assert(len(name) > 0) + assert len(name) > 0 self._name = name.lower() self._value = value @@ -2309,7 +2293,7 @@ def __getitem__(self, idx): raised :raises: KeyError, AssertionError """ - assert(idx >= 0) + assert idx >= 0 if idx > type(self)._static_entries_last_idx: idx -= type(self)._static_entries_last_idx + 1 if idx >= len(self._dynamic_table): @@ -2342,7 +2326,7 @@ def recap(self, nc): :param int nc: the new cap of the dynamic table (that is the maximum-maximum size) # noqa: E501 :raises: AssertionError """ - assert(nc >= 0) + assert nc >= 0 t = self._dynamic_table_cap_size > nc self._dynamic_table_cap_size = nc @@ -2361,7 +2345,7 @@ def _reduce_dynamic_table(self, new_entry_size=0): the RFC7541 definition of the size of an entry) :raises: AssertionError """ - assert(new_entry_size >= 0) + assert new_entry_size >= 0 cur_sz = len(self) dyn_tbl_sz = len(self._dynamic_table) while dyn_tbl_sz > 0 and cur_sz + new_entry_size > self._dynamic_table_max_size: # noqa: E501 @@ -2413,7 +2397,7 @@ def register(self, hdrs): # then throw an assertion error if the new entry does not fit in new_entry_len = len(entry) self._reduce_dynamic_table(new_entry_len) - assert(new_entry_len <= self._dynamic_table_max_size) + assert new_entry_len <= self._dynamic_table_max_size self._dynamic_table.insert(0, entry) def get_idx_by_name(self, name): @@ -2427,7 +2411,7 @@ def get_idx_by_name(self, name): If no matching header is found, this method returns None. """ name = name.lower() - for key, val in six.iteritems(type(self)._static_entries): + for key, val in type(self)._static_entries.items(): if val.name() == name: return key for idx, val in enumerate(self._dynamic_table): @@ -2446,7 +2430,7 @@ def get_idx_by_name_and_value(self, name, value): If no matching header is found, this method returns None. """ name = name.lower() - for key, val in six.iteritems(type(self)._static_entries): + for key, val in type(self)._static_entries.items(): if val.name() == name and val.value() == value: return key for idx, val in enumerate(self._dynamic_table): @@ -2615,13 +2599,13 @@ def _convert_a_header_to_a_h2_header(self, hdr_name, hdr_value, is_sensitive, sh ) ) - def _parse_header_line(self, l): + def _parse_header_line(self, line): # type: (str) -> Union[Tuple[None, None], Tuple[str, str]] if self._regexp is None: self._regexp = re.compile(br'^(?::([a-z\-0-9]+)|([a-z\-0-9]+):)\s+(.+)$') # noqa: E501 - hdr_line = l.rstrip() + hdr_line = line.rstrip() grp = self._regexp.match(hdr_line) if grp is None or len(grp.groups()) != 3: @@ -2634,7 +2618,7 @@ def _parse_header_line(self, l): return plain_str(hdr_name.lower()), plain_str(grp.group(3)) def parse_txt_hdrs(self, - s, # type: str + s, # type: Union[bytes, str] stream_id=1, # type: int body=None, # type: Optional[str] max_frm_sz=4096, # type: int @@ -2680,7 +2664,7 @@ def parse_txt_hdrs(self, :raises: Exception """ - sio = BytesIO(s) + sio = BytesIO(s.encode() if isinstance(s, str) else s) base_frm_len = len(raw(H2Frame())) diff --git a/scapy/contrib/ibeacon.py b/scapy/contrib/ibeacon.py index fddae43190a..0326f915ef3 100644 --- a/scapy/contrib/ibeacon.py +++ b/scapy/contrib/ibeacon.py @@ -1,11 +1,8 @@ -# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*- -# ibeacon.py - protocol handlers for iBeacons and other Apple devices -# +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Michael Farrell -# This program is published under a GPLv2 (or later) license -# + # scapy.contrib.description = iBeacon BLE proximity beacon # scapy.contrib.status = loads """ @@ -18,8 +15,8 @@ """ -from scapy.fields import ByteEnumField, LenField, PacketListField, \ - ShortField, SignedByteField, UUIDField +from scapy.fields import ByteEnumField, ConditionalField, LenField, \ + PacketListField, ShortField, SignedByteField, UUIDField from scapy.layers.bluetooth import EIR_Hdr, EIR_Manufacturer_Specific_Data, \ LowEnergyBeaconHelper from scapy.packet import bind_layers, Packet @@ -35,6 +32,7 @@ class Apple_BLE_Submessage(Packet, LowEnergyBeaconHelper): name = "Apple BLE submessage" fields_desc = [ ByteEnumField("subtype", None, { + 0x01: "overflow", 0x02: "ibeacon", 0x05: "airdrop", 0x07: "airpods", @@ -43,11 +41,18 @@ class Apple_BLE_Submessage(Packet, LowEnergyBeaconHelper): 0x0c: "handoff", 0x10: "nearby", }), - LenField("len", None, fmt="B") + ConditionalField( + # "overflow" messages omit `len` field + LenField("len", None, fmt="B"), + lambda pkt: pkt.subtype != 0x01 + ), ] def extract_padding(self, s): # Needed to end each EIR_Element packet and make PacketListField work. + if self.subtype == 0x01: + # Overflow messages are always 16 bytes. + return s[:16], s[16:] return s[:self.len], s[self.len:] # These methods are here in case you only want to send 1 submessage. @@ -97,5 +102,5 @@ class IBeacon_Data(Packet): bind_layers(EIR_Manufacturer_Specific_Data, Apple_BLE_Frame, - company_id=APPLE_MFG) + company_identifier=APPLE_MFG) bind_layers(Apple_BLE_Submessage, IBeacon_Data, subtype=2) diff --git a/scapy/contrib/icmp_extensions.py b/scapy/contrib/icmp_extensions.py index b2fa0fea3ac..393fa959341 100644 --- a/scapy/contrib/icmp_extensions.py +++ b/scapy/contrib/icmp_extensions.py @@ -1,198 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - -# scapy.contrib.description = ICMP Extensions -# scapy.contrib.status = loads - -from __future__ import absolute_import -import struct - -import scapy -from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, ByteField, ConditionalField, \ - FieldLenField, IPField, IntField, PacketListField, ShortField, \ - StrLenField -from scapy.layers.inet import IP, ICMP, checksum -from scapy.layers.inet6 import IP6Field -from scapy.error import warning -from scapy.contrib.mpls import MPLS -import scapy.modules.six as six -from scapy.config import conf - - -class ICMPExtensionObject(Packet): - name = 'ICMP Extension Object' - fields_desc = [ShortField('len', None), - ByteField('classnum', 0), - ByteField('classtype', 0)] - - def post_build(self, p, pay): - if self.len is None: - tmp_len = len(p) + len(pay) - p = struct.pack('!H', tmp_len) + p[2:] - return p + pay - - -class ICMPExtensionHeader(Packet): - name = 'ICMP Extension Header (RFC4884)' - fields_desc = [BitField('version', 2, 4), - BitField('reserved', 0, 12), - BitField('chksum', None, 16)] - - _min_ieo_len = len(ICMPExtensionObject()) - - def post_build(self, p, pay): - if self.chksum is None: - ck = checksum(p) - p = p[:2] + chr(ck >> 8) + chr(ck & 0xff) + p[4:] - return p + pay - - def guess_payload_class(self, payload): - if len(payload) < self._min_ieo_len: - return Packet.guess_payload_class(self, payload) - - # Look at fields of the generic ICMPExtensionObject to determine which - # bound extension type to use. - ieo = ICMPExtensionObject(payload) - if ieo.len < self._min_ieo_len: - return Packet.guess_payload_class(self, payload) - - for fval, cls in self.payload_guess: - if all(hasattr(ieo, k) and v == ieo.getfieldval(k) - for k, v in six.iteritems(fval)): - return cls - return ICMPExtensionObject - - -def ICMPExtension_post_dissection(self, pkt): - # RFC4884 section 5.2 says if the ICMP packet length - # is >144 then ICMP extensions start at byte 137. - - lastlayer = pkt.lastlayer() - if not isinstance(lastlayer, conf.padding_layer): - return - - if IP in pkt: - if (ICMP in pkt and - pkt[ICMP].type in [3, 11, 12] and - pkt.len > 144): - bytes = pkt[ICMP].build()[136:] - else: - return - elif scapy.layers.inet6.IPv6 in pkt: - if ((scapy.layers.inet6.ICMPv6TimeExceeded in pkt or - scapy.layers.inet6.ICMPv6DestUnreach in pkt) and - pkt.plen > 144): - bytes = pkt[scapy.layers.inet6.ICMPv6TimeExceeded].build()[136:] - else: - return - else: - return - - # validate checksum - ieh = ICMPExtensionHeader(bytes) - if checksum(ieh.build()): - return # failed - - lastlayer.load = lastlayer.load[:-len(ieh)] - lastlayer.add_payload(ieh) - - -class ICMPExtensionMPLS(ICMPExtensionObject): - name = 'ICMP Extension Object - MPLS (RFC4950)' - - fields_desc = [ShortField('len', None), - ByteField('classnum', 1), - ByteField('classtype', 1), - PacketListField('stack', [], MPLS, - length_from=lambda pkt: pkt.len - 4)] - - -class ICMPExtensionInterfaceInformation(ICMPExtensionObject): - name = 'ICMP Extension Object - Interface Information Object (RFC5837)' - - fields_desc = [ShortField('len', None), - ByteField('classnum', 2), - BitField('interface_role', 0, 2), - BitField('reserved', 0, 2), - BitField('has_ifindex', 0, 1), - BitField('has_ipaddr', 0, 1), - BitField('has_ifname', 0, 1), - BitField('has_mtu', 0, 1), - - ConditionalField( - IntField('ifindex', None), - lambda pkt: pkt.has_ifindex == 1), - - ConditionalField( - ShortField('afi', None), - lambda pkt: pkt.has_ipaddr == 1), - ConditionalField( - ShortField('reserved2', 0), - lambda pkt: pkt.has_ipaddr == 1), - ConditionalField( - IPField('ip4', None), - lambda pkt: pkt.afi == 1), - ConditionalField( - IP6Field('ip6', None), - lambda pkt: pkt.afi == 2), - - ConditionalField( - FieldLenField('ifname_len', None, fmt='B', - length_of='ifname'), - lambda pkt: pkt.has_ifname == 1), - ConditionalField( - StrLenField('ifname', None, - length_from=lambda pkt: pkt.ifname_len), - lambda pkt: pkt.has_ifname == 1), - - ConditionalField( - IntField('mtu', None), - lambda pkt: pkt.has_mtu == 1)] - - def self_build(self, field_pos_list=None): - if self.afi is None: - if self.ip4 is not None: - self.afi = 1 - elif self.ip6 is not None: - self.afi = 2 - - if self.has_ifindex and self.ifindex is None: - warning('has_ifindex set but ifindex is not set.') - if self.has_ipaddr and self.afi is None: - warning('has_ipaddr set but afi is not set.') - if self.has_ipaddr and self.ip4 is None and self.ip6 is None: - warning('has_ipaddr set but ip4 or ip6 is not set.') - if self.has_ifname and self.ifname is None: - warning('has_ifname set but ifname is not set.') - if self.has_mtu and self.mtu is None: - warning('has_mtu set but mtu is not set.') - - return ICMPExtensionObject.self_build(self, field_pos_list=field_pos_list) # noqa: E501 - - -# Add the post_dissection() method to the existing ICMPv4 and -# ICMPv6 error messages -scapy.layers.inet.ICMPerror.post_dissection = ICMPExtension_post_dissection -scapy.layers.inet.TCPerror.post_dissection = ICMPExtension_post_dissection -scapy.layers.inet.UDPerror.post_dissection = ICMPExtension_post_dissection - -scapy.layers.inet6.ICMPv6DestUnreach.post_dissection = ICMPExtension_post_dissection # noqa: E501 -scapy.layers.inet6.ICMPv6TimeExceeded.post_dissection = ICMPExtension_post_dissection # noqa: E501 - - -# ICMPExtensionHeader looks at fields from the upper layer object when -# determining which upper layer to use. -bind_layers(ICMPExtensionHeader, ICMPExtensionMPLS, classnum=1, classtype=1) -bind_layers(ICMPExtensionHeader, ICMPExtensionInterfaceInformation, classnum=2) +# See https://scapy.net/ for more information + +# scapy.contrib.description = ICMP Extensions (deprecated) +# scapy.contrib.status = deprecated + +__all__ = [ + "ICMPExtensionObject", + "ICMPExtensionHeader", + "ICMPExtensionInterfaceInformation", + "ICMPExtensionMPLS", +] + +import warnings + +from scapy.layers.inet import ( + ICMPExtension_Object as ICMPExtensionObject, + ICMPExtension_Header as ICMPExtensionHeader, + ICMPExtension_InterfaceInformation as ICMPExtensionInterfaceInformation, +) +from scapy.contrib.mpls import ( + ICMPExtension_MPLS as ICMPExtensionMPLS, +) + +warnings.warn( + "scapy.contrib.icmp_extensions is deprecated. Behavior has changed ! " + "Use scapy.layers.inet", + DeprecationWarning +) diff --git a/scapy/contrib/ife.py b/scapy/contrib/ife.py index ad28d7bef8f..ac93f2aebb9 100644 --- a/scapy/contrib/ife.py +++ b/scapy/contrib/ife.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = ForCES Inter-FE LFB type (IFE) # scapy.contrib.status = loads @@ -6,17 +10,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :author: Alexander Aring, aring@mojatatu.com - :license: GPLv2 - - This module is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This module is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. :description: @@ -38,7 +31,7 @@ from scapy.layers.l2 import Ether ETH_P_IFE = 0xed3e -ETHER_TYPES['IFE'] = ETH_P_IFE +ETHER_TYPES[ETH_P_IFE] = 'IFE' # The value to set for the skb mark. IFE_META_SKBMARK = 0x0001 @@ -66,7 +59,7 @@ class IFETlv(Packet): """ - Parent Class interhit by all ForCES TLV strucutures + Parent Class interhit by all ForCES TLV structures """ name = "IFETlv" diff --git a/scapy/contrib/igmp.py b/scapy/contrib/igmp.py index 5c11745f61f..f01fa637c89 100644 --- a/scapy/contrib/igmp.py +++ b/scapy/contrib/igmp.py @@ -1,21 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Internet Group Management Protocol v1/v2 (IGMP/IGMPv2) # scapy.contrib.status = loads -from __future__ import print_function from scapy.compat import chb, orb from scapy.error import warning from scapy.fields import ByteEnumField, ByteField, IPField, XShortField diff --git a/scapy/contrib/igmpv3.py b/scapy/contrib/igmpv3.py index 55e8e966847..620d389c133 100644 --- a/scapy/contrib/igmpv3.py +++ b/scapy/contrib/igmpv3.py @@ -1,21 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Internet Group Management Protocol v3 (IGMPv3) # scapy.contrib.status = loads -from __future__ import print_function from scapy.packet import Packet, bind_layers from scapy.fields import BitField, ByteEnumField, ByteField, FieldLenField, \ FieldListField, IPField, PacketListField, ShortField, XShortField @@ -86,7 +75,7 @@ def encode_maxrespcode(self): else: exp = 0 value >>= 3 - while(value > 31): + while value > 31: exp += 1 value >>= 1 exp <<= 4 diff --git a/scapy/contrib/ikev2.py b/scapy/contrib/ikev2.py index 668660a4aed..b1ffd8cdc17 100644 --- a/scapy/contrib/ikev2.py +++ b/scapy/contrib/ikev2.py @@ -1,106 +1,174 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - -# scapy.contrib.description = Internet Key Exchange v2 (IKEv2) +# See https://scapy.net/ for more information + +""" +Internet Key Exchange Protocol Version 2 (IKEv2), RFC 7296 +""" + +# scapy.contrib.description = Internet Key Exchange Protocol Version 2 (IKEv2), RFC 7296 # scapy.contrib.status = loads -import logging import struct # Modified from the original ISAKMP code by Yaron Sheffer , June 2010. # noqa: E501 -from scapy.packet import Packet, bind_layers, split_layers, Raw -from scapy.fields import ByteEnumField, ByteField, ConditionalField, \ - FieldLenField, FlagsField, IP6Field, IPField, IntField, MultiEnumField, \ - PacketField, PacketLenField, PacketListField, ShortEnumField, ShortField, \ - StrFixedLenField, StrLenField, X3BytesField, XByteField +from scapy.packet import ( + Packet, + Raw, + bind_bottom_up, + bind_layers, + bind_top_down, + split_bottom_up, +) +from scapy.fields import ( + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + FlagsField, + IP6Field, + IPField, + IntField, + MultiEnumField, + MultipleTypeField, + PacketField, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + StrLenField, + X3BytesField, + XByteField, + XStrFixedLenField, + XStrLenField, +) from scapy.layers.x509 import X509_Cert, X509_CRL from scapy.layers.inet import IP, UDP +from scapy.layers.ipsec import NON_ESP from scapy.layers.isakmp import ISAKMP from scapy.sendrecv import sr from scapy.config import conf from scapy.volatile import RandString -# see http://www.iana.org/assignments/ikev2-parameters for details -IKEv2AttributeTypes = {"Encryption": (1, {"DES-IV64": 1, - "DES": 2, - "3DES": 3, - "RC5": 4, - "IDEA": 5, - "CAST": 6, - "Blowfish": 7, - "3IDEA": 8, - "DES-IV32": 9, - "AES-CBC": 12, - "AES-CTR": 13, - "AES-CCM-8": 14, - "AES-CCM-12": 15, - "AES-CCM-16": 16, - "AES-GCM-8ICV": 18, - "AES-GCM-12ICV": 19, - "AES-GCM-16ICV": 20, - "Camellia-CBC": 23, - "Camellia-CTR": 24, - "Camellia-CCM-8ICV": 25, - "Camellia-CCM-12ICV": 26, - "Camellia-CCM-16ICV": 27, - }, 0), - "PRF": (2, {"PRF_HMAC_MD5": 1, - "PRF_HMAC_SHA1": 2, - "PRF_HMAC_TIGER": 3, - "PRF_AES128_XCBC": 4, - "PRF_HMAC_SHA2_256": 5, - "PRF_HMAC_SHA2_384": 6, - "PRF_HMAC_SHA2_512": 7, - "PRF_AES128_CMAC": 8, - }, 0), - "Integrity": (3, {"HMAC-MD5-96": 1, - "HMAC-SHA1-96": 2, - "DES-MAC": 3, - "KPDK-MD5": 4, - "AES-XCBC-96": 5, - "HMAC-MD5-128": 6, - "HMAC-SHA1-160": 7, - "AES-CMAC-96": 8, - "AES-128-GMAC": 9, - "AES-192-GMAC": 10, - "AES-256-GMAC": 11, - "SHA2-256-128": 12, - "SHA2-384-192": 13, - "SHA2-512-256": 14, - }, 0), - "GroupDesc": (4, {"768MODPgr": 1, - "1024MODPgr": 2, - "1536MODPgr": 5, - "2048MODPgr": 14, - "3072MODPgr": 15, - "4096MODPgr": 16, - "6144MODPgr": 17, - "8192MODPgr": 18, - "256randECPgr": 19, - "384randECPgr": 20, - "521randECPgr": 21, - "1024MODP160POSgr": 22, - "2048MODP224POSgr": 23, - "2048MODP256POSgr": 24, - "192randECPgr": 25, - "224randECPgr": 26, - }, 0), - "Extended Sequence Number": (5, {"No ESN": 0, - "ESN": 1}, 0), - } +# see https://www.iana.org/assignments/ikev2-parameters for details +IKEv2AttributeTypes = { + 1: ( + "Encryption", + { + 1: "DES-IV64", + 2: "DES", + 3: "3DES", + 4: "RC5", + 5: "IDEA", + 6: "CAST", + 7: "Blowfish", + 8: "3IDEA", + 9: "DES-IV32", + 12: "AES-CBC", + 13: "AES-CTR", + 14: "AES-CCM-8", + 15: "AES-CCM-12", + 16: "AES-CCM-16", + 18: "AES-GCM-8ICV", + 19: "AES-GCM-12ICV", + 20: "AES-GCM-16ICV", + 23: "Camellia-CBC", + 24: "Camellia-CTR", + 25: "Camellia-CCM-8ICV", + 26: "Camellia-CCM-12ICV", + 27: "Camellia-CCM-16ICV", + 28: "ChaCha20-Poly1305", + 32: "Kuzneychik-MGM-KTREE", + 33: "MAGMA-MGM-KTREE", + } + ), + 2: ( + "PRF", + { + 1: "PRF_HMAC_MD5", + 2: "PRF_HMAC_SHA1", + 3: "PRF_HMAC_TIGER", + 4: "PRF_AES128_XCBC", + 5: "PRF_HMAC_SHA2_256", + 6: "PRF_HMAC_SHA2_384", + 7: "PRF_HMAC_SHA2_512", + 8: "PRF_AES128_CMAC", + 9: "PRF_HMAC_STREEBOG_512", + } + ), + 3: ( + "Integrity", + { + 1: "HMAC-MD5-96", + 2: "HMAC-SHA1-96", + 3: "DES-MAC", + 4: "KPDK-MD5", + 5: "AES-XCBC-96", + 6: "HMAC-MD5-128", + 7: "HMAC-SHA1-160", + 8: "AES-CMAC-96", + 9: "AES-128-GMAC", + 10: "AES-192-GMAC", + 11: "AES-256-GMAC", + 12: "SHA2-256-128", + 13: "SHA2-384-192", + 14: "SHA2-512-256", + } + ), + 4: ( + "GroupDesc", + { + 1: "768MODPgr", + 2: "1024MODPgr", + 5: "1536MODPgr", + 14: "2048MODPgr", + 15: "3072MODPgr", + 16: "4096MODPgr", + 17: "6144MODPgr", + 18: "8192MODPgr", + 19: "256randECPgr", + 20: "384randECPgr", + 21: "521randECPgr", + 22: "1024MODP160POSgr", + 23: "2048MODP224POSgr", + 24: "2048MODP256POSgr", + 25: "192randECPgr", + 26: "224randECPgr", + 27: "brainpoolP224r1gr", + 28: "brainpoolP256r1gr", + 29: "brainpoolP384r1gr", + 30: "brainpoolP512r1gr", + 31: "curve25519gr", + 32: "curve448gr", + 33: "GOST3410_2012_256", + 34: "GOST3410_2012_512", + } + ), + 5: ( + "Extended Sequence Number", + { + 0: "No ESN", + 1: "ESN" + } + ), +} + +IKEv2TransformTypes = { + tf_num: tf_name for tf_name, (tf_num, _) in IKEv2AttributeTypes.items() +} + +IKEv2TransformAlgorithms = { + tf_num: tf_dict for tf_num, (_, tf_dict) in IKEv2AttributeTypes.items() +} + +IKEv2ProtocolTypes = { + 1: "IKE", + 2: "AH", + 3: "ESP" +} IKEv2AuthenticationTypes = { 0: "Reserved", @@ -138,6 +206,7 @@ 44: "CHILD_SA_NOT_FOUND", 45: "INVALID_GROUP_ID", 46: "AUTHORIZATION_FAILED", + 47: "NOTIFY_STATE_NOT_FOUND", 16384: "INITIAL_CONTACT", 16385: "SET_WINDOW_SIZE", 16386: "ADDITIONAL_TS_POSSIBLE", @@ -187,7 +256,22 @@ 16430: "IKEV2_FRAGMENTATION_SUPPORTED", 16431: "SIGNATURE_HASH_ALGORITHMS", 16432: "CLONE_IKE_SA_SUPPORTED", - 16433: "CLONE_IKE_SA" + 16433: "CLONE_IKE_SA", + 16434: "IV2_NOTIFY_PUZZLE", + 16435: "IV2_NOTIFY_USE_PPK", + 16436: "IV2_NOTIFY_PPK_IDENTITY", + 16437: "IV2_NOTIFY_NO_PPK_AUTH", + 16438: "IV2_NOTIFY_INTERMEDIATE_EXCHANGE_SUPPORTED", + 16439: "IV2_NOTIFY_IP4_ALLOWED", + 16440: "IV2_NOTIFY_IP6_ALLOWED", + 16441: "IV2_NOTIFY_ADDITIONAL_KEY_EXCHANGE", + 16442: "IV2_NOTIFY_USE_AGGFRAG", +} + +IKEv2GatewayIDTypes = { + 1: "IPv4_addr", + 2: "IPv6_addr", + 3: "FQDN" } IKEv2CertificateEncodings = { @@ -211,6 +295,39 @@ 9: "TS_FC_ADDR_RANGE" } +IKEv2ConfigurationPayloadCFGTypes = { + 1: "CFG_REQUEST", + 2: "CFG_REPLY", + 3: "CFG_SET", + 4: "CFG_ACK" +} + +IKEv2ConfigurationAttributeTypes = { + 1: "INTERNAL_IP4_ADDRESS", + 2: "INTERNAL_IP4_NETMASK", + 3: "INTERNAL_IP4_DNS", + 4: "INTERNAL_IP4_NBNS", + 6: "INTERNAL_IP4_DHCP", + 7: "APPLICATION_VERSION", + 8: "INTERNAL_IP6_ADDRESS", + 10: "INTERNAL_IP6_DNS", + 12: "INTERNAL_IP6_DHCP", + 13: "INTERNAL_IP4_SUBNET", + 14: "SUPPORTED_ATTRIBUTES", + 15: "INTERNAL_IP6_SUBNET", + 16: "MIP6_HOME_PREFIX", + 17: "INTERNAL_IP6_LINK", + 18: "INTERNAL_IP6_PREFIX", + 19: "HOME_AGENT_ADDRESS", + 20: "P_CSCF_IP4_ADDRESS", + 21: "P_CSCF_IP6_ADDRESS", + 22: "FTT_KAT", + 23: "EXTERNAL_SOURCE_IP4_NAT_INFO", + 24: "TIMEOUT_PERIOD_FOR_LIVENESS_CHECK", + 25: "INTERNAL_DNS_DOMAIN", + 26: "INTERNAL_DNSSEC_TA" +} + IPProtocolIDs = { 0: "All protocols", 1: "Internet Control Message Protocol", @@ -357,71 +474,70 @@ 142: "Robust Header Compression", } -# the name 'IKEv2TransformTypes' is actually a misnomer (since the table -# holds info for all IKEv2 Attribute types, not just transforms, but we'll -# keep it for backwards compatibility... for now at least -IKEv2TransformTypes = IKEv2AttributeTypes - -IKEv2TransformNum = {} -for n in IKEv2TransformTypes: - val = IKEv2TransformTypes[n] - tmp = {} - for e in val[1]: - tmp[val[1][e]] = e - IKEv2TransformNum[val[0]] = tmp - -IKEv2Transforms = {} -for n in IKEv2TransformTypes: - IKEv2Transforms[IKEv2TransformTypes[n][0]] = n - -del(n) -del(e) -del(tmp) -del(val) - -# Note: Transform and Proposal can only be used inside the SA payload -IKEv2_payload_type = ["None", "", "Proposal", "Transform"] - -IKEv2_payload_type.extend([""] * 29) -IKEv2_payload_type.extend(["SA", "KE", "IDi", "IDr", "CERT", "CERTREQ", "AUTH", "Nonce", "Notify", "Delete", # noqa: E501 - "VendorID", "TSi", "TSr", "Encrypted", "CP", "EAP", "", "", "", "", "Encrypted Fragment"]) # noqa: E501 - -IKEv2_exchange_type = [""] * 34 -IKEv2_exchange_type.extend(["IKE_SA_INIT", "IKE_AUTH", "CREATE_CHILD_SA", - "INFORMATIONAL", "IKE_SESSION_RESUME"]) - - -class IKEv2_class(Packet): - def guess_payload_class(self, payload): - np = self.next_payload - logging.debug("For IKEv2_class np=%d" % np) - if np == 0: - return conf.raw_layer - elif np < len(IKEv2_payload_type): - pt = IKEv2_payload_type[np] - logging.debug(globals().get("IKEv2_payload_%s" % pt, IKEv2_payload)) # noqa: E501 - return globals().get("IKEv2_payload_%s" % pt, IKEv2_payload) - else: - return IKEv2_payload - - -class IKEv2(IKEv2_class): # rfc4306 +IKEv2PayloadTypes = { + 0: "None", + 2: "Proposal", # used only inside the SA payload + 3: "Transform", # used only inside the SA payload + 33: "SA", + 34: "KE", + 35: "IDi", + 36: "IDr", + 37: "CERT", + 38: "CERTREQ", + 39: "AUTH", + 40: "Nonce", + 41: "Notify", + 42: "Delete", + 43: "VendorID", + 44: "TSi", + 45: "TSr", + 46: "Encrypted", + 47: "CP", + 48: "EAP", + 49: "GSPM", + 50: "IDg", + 51: "GSA", + 52: "KD", + 53: "Encrypted_Fragment", + 54: "PS" +} + + +IKEv2ExchangeTypes = { + 34: "IKE_SA_INIT", + 35: "IKE_AUTH", + 36: "CREATE_CHILD_SA", + 37: "INFORMATIONAL", + 38: "IKE_SESSION_RESUME", + 43: "IKE_INTERMEDIATE" +} + + +class _IKEv2_Packet(Packet): + def default_payload_class(self, payload): + return IKEv2_Payload if self.next_payload else conf.raw_layer + + +class IKEv2(_IKEv2_Packet): # rfc4306 name = "IKEv2" fields_desc = [ - StrFixedLenField("init_SPI", "", 8), - StrFixedLenField("resp_SPI", "", 8), - ByteEnumField("next_payload", 0, IKEv2_payload_type), + XStrFixedLenField("init_SPI", "", 8), + XStrFixedLenField("resp_SPI", "", 8), + ByteEnumField("next_payload", 0, IKEv2PayloadTypes), XByteField("version", 0x20), - ByteEnumField("exch_type", 0, IKEv2_exchange_type), + ByteEnumField("exch_type", 0, IKEv2ExchangeTypes), FlagsField("flags", 0, 8, ["res0", "res1", "res2", "Initiator", "Version", "Response", "res6", "res7"]), # noqa: E501 IntField("id", 0), IntField("length", None) # Length of total message: packets + all payloads # noqa: E501 ] - def guess_payload_class(self, payload): - if self.flags & 1: - return conf.raw_layer - return IKEv2_class.guess_payload_class(self, payload) + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 18: + version = struct.unpack("!B", _pkt[17:18])[0] + if version < 0x20: + return ISAKMP + return cls def answers(self, other): if isinstance(other, IKEv2): @@ -448,65 +564,57 @@ def h2i(self, pkt, x): return IntField.h2i(self, pkt, (x if x is not None else 0) | 0x800E0000) # noqa: E501 -class IKEv2_payload_Transform(IKEv2_class): - name = "IKE Transform" +class IKEv2_Payload(_IKEv2_Packet): + name = "IKEv2 Payload" fields_desc = [ - ByteEnumField("next_payload", None, {0: "last", 3: "Transform"}), - ByteField("res", 0), - ShortField("length", 8), - ByteEnumField("transform_type", None, IKEv2Transforms), + ByteEnumField("next_payload", None, IKEv2PayloadTypes), + FlagsField("flags", 0, 8, {0x80: "critical"}), + ShortField("length", None), + XStrLenField("load", "", length_from=lambda pkt: pkt.length - 4), + ] + + def post_build(self, pkt, pay): + if self.length is None: + pkt = pkt[:2] + struct.pack("!H", len(pkt)) + pkt[4:] + return pkt + pay + + +class IKEv2_Transform(IKEv2_Payload): + name = "IKEv2 Transform" + fields_desc = IKEv2_Payload.fields_desc[:2] + [ + ShortField("length", 8), # can't be None, because 'key_length' depends on it + ByteEnumField("transform_type", None, IKEv2TransformTypes), ByteField("res2", 0), - MultiEnumField("transform_id", None, IKEv2TransformNum, depends_on=lambda pkt: pkt.transform_type, fmt="H"), # noqa: E501 + MultiEnumField("transform_id", None, IKEv2TransformAlgorithms, depends_on=lambda pkt: pkt.transform_type, fmt="H"), # noqa: E501 ConditionalField(IKEv2_Key_Length_Attribute("key_length"), lambda pkt: pkt.length > 8), # noqa: E501 ] -class IKEv2_payload_Proposal(IKEv2_class): +class IKEv2_Proposal(IKEv2_Payload): name = "IKEv2 Proposal" - fields_desc = [ - ByteEnumField("next_payload", None, {0: "last", 2: "Proposal"}), - ByteField("res", 0), - FieldLenField("length", None, "trans", "H", adjust=lambda pkt, x: x + 8 + (pkt.SPIsize if pkt.SPIsize else 0)), # noqa: E501 + fields_desc = IKEv2_Payload.fields_desc[:3] + [ ByteField("proposal", 1), - ByteEnumField("proto", 1, {1: "IKEv2", 2: "AH", 3: "ESP"}), + ByteEnumField("proto", 1, IKEv2ProtocolTypes), FieldLenField("SPIsize", None, "SPI", "B"), ByteField("trans_nb", None), - StrLenField("SPI", "", length_from=lambda pkt: pkt.SPIsize), - PacketLenField("trans", conf.raw_layer(), IKEv2_payload_Transform, length_from=lambda pkt: pkt.length - 8 - pkt.SPIsize), # noqa: E501 + XStrLenField("SPI", "", length_from=lambda pkt: pkt.SPIsize), + PacketLenField("trans", conf.raw_layer(), IKEv2_Transform, length_from=lambda pkt: pkt.length - 8 - pkt.SPIsize), # noqa: E501 ] -class IKEv2_payload(IKEv2_class): - name = "IKEv2 Payload" - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - FlagsField("flags", 0, 8, ["critical", "res1", "res2", "res3", "res4", "res5", "res6", "res7"]), # noqa: E501 - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 4), - StrLenField("load", "", length_from=lambda x:x.length - 4), - ] - - -class IKEv2_payload_AUTH(IKEv2_class): +class IKEv2_AUTH(IKEv2_Payload): name = "IKEv2 Authentication" - overload_fields = {IKEv2: {"next_payload": 39}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 8), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ ByteEnumField("auth_type", None, IKEv2AuthenticationTypes), X3BytesField("res2", 0), - StrLenField("load", "", length_from=lambda x:x.length - 8), + XStrLenField("load", "", length_from=lambda pkt: pkt.length - 8), ] -class IKEv2_payload_VendorID(IKEv2_class): +class IKEv2_VendorID(IKEv2_Payload): name = "IKEv2 Vendor ID" - overload_fields = {IKEv2: {"next_payload": 43}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "vendorID", "H", adjust=lambda pkt, x:x + 4), # noqa: E501 - StrLenField("vendorID", "", length_from=lambda x:x.length - 4), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + XStrLenField("vendorID", "", length_from=lambda pkt: pkt.length - 4), ] @@ -525,6 +633,9 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return RawTrafficSelector return IPv4TrafficSelector + def extract_padding(self, s): + return '', s + class IPv4TrafficSelector(TrafficSelector): name = "IKEv2 IPv4 Traffic Selector" @@ -574,227 +685,275 @@ class RawTrafficSelector(TrafficSelector): fields_desc = [ ByteEnumField("TS_type", None, IKEv2TrafficSelectorTypes), ByteEnumField("IP_protocol_ID", None, IPProtocolIDs), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 4), + FieldLenField("length", None, "load", "H", adjust=lambda pkt, x: x + 4), PacketField("load", "", Raw) ] -class IKEv2_payload_TSi(IKEv2_class): +class IKEv2_TSi(IKEv2_Payload): name = "IKEv2 Traffic Selector - Initiator" - overload_fields = {IKEv2: {"next_payload": 44}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "traffic_selector", "H", adjust=lambda pkt, x:x + 8), # noqa: E501 - ByteField("number_of_TSs", 0), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + FieldLenField("number_of_TSs", None, fmt="B", + count_of="traffic_selector"), X3BytesField("res2", 0), - PacketListField("traffic_selector", None, TrafficSelector, length_from=lambda x:x.length - 8, count_from=lambda x:x.number_of_TSs), # noqa: E501 + PacketListField("traffic_selector", None, TrafficSelector, + length_from=lambda pkt: pkt.length - 8, + count_from=lambda pkt: pkt.number_of_TSs), ] -class IKEv2_payload_TSr(IKEv2_class): +class IKEv2_TSr(IKEv2_Payload): name = "IKEv2 Traffic Selector - Responder" - overload_fields = {IKEv2: {"next_payload": 45}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "traffic_selector", "H", adjust=lambda pkt, x:x + 8), # noqa: E501 - ByteField("number_of_TSs", 0), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + FieldLenField("number_of_TSs", None, fmt="B", + count_of="traffic_selector"), X3BytesField("res2", 0), - PacketListField("traffic_selector", None, TrafficSelector, length_from=lambda x:x.length - 8, count_from=lambda x:x.number_of_TSs), # noqa: E501 + PacketListField("traffic_selector", None, TrafficSelector, + length_from=lambda pkt: pkt.length - 8, + count_from=lambda pkt: pkt.number_of_TSs), ] -class IKEv2_payload_Delete(IKEv2_class): - name = "IKEv2 Vendor ID" - overload_fields = {IKEv2: {"next_payload": 42}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "vendorID", "H", adjust=lambda pkt, x:x + 4), # noqa: E501 - StrLenField("vendorID", "", length_from=lambda x:x.length - 4), +class IKEv2_Delete(IKEv2_Payload): + name = "IKEv2 Delete" + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + ByteEnumField("proto", None, {0: "Reserved", 1: "IKE", 2: "AH", 3: "ESP"}), # noqa: E501 + FieldLenField("SPIsize", None, "SPI", "B"), + ShortField("SPInum", 0), + FieldListField("SPI", [], + XStrLenField("", "", length_from=lambda pkt: pkt.SPIsize), + count_from=lambda pkt: pkt.SPInum) ] -class IKEv2_payload_SA(IKEv2_class): +class IKEv2_SA(IKEv2_Payload): name = "IKEv2 SA" - overload_fields = {IKEv2: {"next_payload": 33}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "prop", "H", adjust=lambda pkt, x:x + 4), - PacketLenField("prop", conf.raw_layer(), IKEv2_payload_Proposal, length_from=lambda x:x.length - 4), # noqa: E501 + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + PacketLenField("prop", conf.raw_layer(), IKEv2_Proposal, length_from=lambda pkt: pkt.length - 4), # noqa: E501 ] -class IKEv2_payload_Nonce(IKEv2_class): +class IKEv2_Nonce(IKEv2_Payload): name = "IKEv2 Nonce" - overload_fields = {IKEv2: {"next_payload": 40}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 4), - StrLenField("load", "", length_from=lambda x:x.length - 4), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + XStrLenField("nonce", "", length_from=lambda pkt: pkt.length - 4), ] -class IKEv2_payload_Notify(IKEv2_class): +class IKEv2_Notify(IKEv2_Payload): name = "IKEv2 Notify" - overload_fields = {IKEv2: {"next_payload": 41}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 8), - ByteEnumField("proto", None, {0: "Reserved", 1: "IKE", 2: "AH", 3: "ESP"}), # noqa: E501 + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + ByteEnumField("proto", None, IKEv2ProtocolTypes), FieldLenField("SPIsize", None, "SPI", "B"), ShortEnumField("type", 0, IKEv2NotifyMessageTypes), - StrLenField("SPI", "", length_from=lambda x: x.SPIsize), - StrLenField("load", "", length_from=lambda x: x.length - 8), + XStrLenField("SPI", "", length_from=lambda pkt: pkt.SPIsize), + ConditionalField( + XStrLenField("notify", "", + length_from=lambda pkt: pkt.length - 8 - pkt.SPIsize), + lambda pkt: pkt.type not in (16407, 16408) + ), + ConditionalField( + # REDIRECT, REDIRECTED_FROM (RFC 5685) + ByteEnumField("gw_id_type", 1, IKEv2GatewayIDTypes), + lambda pkt: pkt.type in (16407, 16408) + ), + ConditionalField( + # REDIRECT, REDIRECTED_FROM (RFC 5685) + FieldLenField("gw_id_len", None, "gw_id", "B"), + lambda pkt: pkt.type in (16407, 16408) + ), + ConditionalField( + # REDIRECT, REDIRECTED_FROM (RFC 5685) + MultipleTypeField( + [ + (IPField("gw_id", "127.0.0.1"), lambda x: x.gw_id_type == 1), + (IP6Field("gw_id", "::1"), lambda x: x.gw_id_type == 2), + ], + StrLenField("gw_id", "", length_from=lambda x: x.gw_id_len) + ), + lambda pkt: pkt.type in (16407, 16408) + ), + ConditionalField( + # REDIRECT (RFC 5685) + XStrLenField("nonce", "", length_from=lambda x:x.length - 10 - x.gw_id_len), + lambda pkt: pkt.type == 16407 + ) ] -class IKEv2_payload_KE(IKEv2_class): +class IKEv2_KE(IKEv2_Payload): name = "IKEv2 Key Exchange" - overload_fields = {IKEv2: {"next_payload": 34}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 8), - ShortEnumField("group", 0, IKEv2TransformTypes['GroupDesc'][1]), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + ShortEnumField("group", 0, IKEv2TransformAlgorithms[4]), ShortField("res2", 0), - StrLenField("load", "", length_from=lambda x:x.length - 8), + XStrLenField("ke", "", length_from=lambda pkt: pkt.length - 8), ] -class IKEv2_payload_IDi(IKEv2_class): +class IKEv2_IDi(IKEv2_Payload): # RFC 7296, section 3.5 name = "IKEv2 Identification - Initiator" - overload_fields = {IKEv2: {"next_payload": 35}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 8), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ ByteEnumField("IDtype", 1, {1: "IPv4_addr", 2: "FQDN", 3: "Email_addr", 5: "IPv6_addr", 11: "Key"}), # noqa: E501 - ByteEnumField("ProtoID", 0, {0: "Unused"}), - ShortEnumField("Port", 0, {0: "Unused"}), - # IPField("IdentData","127.0.0.1"), - StrLenField("load", "", length_from=lambda x: x.length - 8), + X3BytesField("res2", 0), + MultipleTypeField( + [ + (IPField("ID", "127.0.0.1"), lambda pkt: pkt.IDtype == 1), + (IP6Field("ID", "::1"), lambda pkt: pkt.IDtype == 5), + ], + XStrLenField("ID", "", length_from=lambda pkt: pkt.length - 8), + ) ] -class IKEv2_payload_IDr(IKEv2_class): +class IKEv2_IDr(IKEv2_Payload): # RFC 7296, section 3.5 name = "IKEv2 Identification - Responder" - overload_fields = {IKEv2: {"next_payload": 36}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 8), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ ByteEnumField("IDtype", 1, {1: "IPv4_addr", 2: "FQDN", 3: "Email_addr", 5: "IPv6_addr", 11: "Key"}), # noqa: E501 - ByteEnumField("ProtoID", 0, {0: "Unused"}), - ShortEnumField("Port", 0, {0: "Unused"}), - # IPField("IdentData","127.0.0.1"), - StrLenField("load", "", length_from=lambda x: x.length - 8), + X3BytesField("res2", 0), + MultipleTypeField( + [ + (IPField("ID", "127.0.0.1"), lambda pkt: pkt.IDtype == 1), + (IP6Field("ID", "::1"), lambda pkt: pkt.IDtype == 5), + ], + XStrLenField("ID", "", length_from=lambda pkt: pkt.length - 8), + ) ] -class IKEv2_payload_Encrypted(IKEv2_class): +class IKEv2_Encrypted(IKEv2_Payload): name = "IKEv2 Encrypted and Authenticated" - overload_fields = {IKEv2: {"next_payload": 46}} + + +class ConfigurationAttribute(Packet): + name = "IKEv2 Configuration Attribute" fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 4), - StrLenField("load", "", length_from=lambda x:x.length - 4), + ShortEnumField("type", 1, IKEv2ConfigurationAttributeTypes), + FieldLenField("length", None, "value", "H"), + MultipleTypeField( + [ + (IPField("value", "127.0.0.1"), + lambda pkt: pkt.length == 4 and pkt.type in (1, 2, 3, 4, 6, 20)), + (IP6Field("value", "::1"), + lambda pkt: pkt.length == 16 and pkt.type in (10, 12, 21)), + ], + XStrLenField("value", "", length_from=lambda pkt: pkt.length), + ) ] + def extract_padding(self, s): + return b'', s -class IKEv2_payload_Encrypted_Fragment(IKEv2_class): - name = "IKEv2 Encrypted Fragment" - overload_fields = {IKEv2: {"next_payload": 53}} - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x: x + 8), # noqa: E501 + +class IKEv2_CP(IKEv2_Payload): # RFC 7296, section 3.15 + name = "IKEv2 Configuration" + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + ByteEnumField("CFGType", 1, IKEv2ConfigurationPayloadCFGTypes), + X3BytesField("res2", 0), + PacketListField("attributes", None, ConfigurationAttribute, + length_from=lambda pkt: pkt.length - 8), + ] + + +class IKEv2_Encrypted_Fragment(IKEv2_Payload): + name = "IKEv2 Encrypted and Authenticated Fragment" + fields_desc = IKEv2_Payload.fields_desc[:3] + [ ShortField("frag_number", 1), ShortField("frag_total", 1), - StrLenField("load", "", length_from=lambda x: x.length - 8), + XStrLenField("load", "", length_from=lambda pkt: pkt.length - 8), ] -class IKEv2_payload_CERTREQ(IKEv2_class): +class IKEv2_CERTREQ(IKEv2_Payload): name = "IKEv2 Certificate Request" - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "cert_data", "H", adjust=lambda pkt, x:x + 5), # noqa: E501 - ByteEnumField("cert_type", 0, IKEv2CertificateEncodings), - StrLenField("cert_data", "", length_from=lambda x:x.length - 5), + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + ByteEnumField("cert_encoding", 0, IKEv2CertificateEncodings), + XStrLenField("cert_authority", "", length_from=lambda pkt: pkt.length - 5), ] -class IKEv2_payload_CERT(IKEv2_class): - @classmethod - def dispatch_hook(cls, _pkt=None, *args, **kargs): - if _pkt and len(_pkt) >= 16: - ts_type = struct.unpack("!B", _pkt[4:5])[0] - if ts_type == 4: - return IKEv2_payload_CERT_CRT - elif ts_type == 7: - return IKEv2_payload_CERT_CRL - else: - return IKEv2_payload_CERT_STR - return IKEv2_payload_CERT_STR - - -class IKEv2_payload_CERT_CRT(IKEv2_payload_CERT): +class IKEv2_CERT(IKEv2_Payload): name = "IKEv2 Certificate" - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "x509Cert", "H", adjust=lambda pkt, x: x + len(pkt.x509Cert) + 5), # noqa: E501 - ByteEnumField("cert_type", 4, IKEv2CertificateEncodings), - PacketLenField("x509Cert", X509_Cert(''), X509_Cert, length_from=lambda x:x.length - 5), # noqa: E501 + fields_desc = IKEv2_Payload.fields_desc[:3] + [ + ByteEnumField("cert_encoding", 4, IKEv2CertificateEncodings), + MultipleTypeField( + [ + (PacketLenField("cert_data", X509_Cert(), X509_Cert, + length_from=lambda pkt: pkt.length - 5), + lambda pkt: pkt.cert_encoding == 4), + (PacketLenField("cert_data", X509_CRL(), X509_CRL, + length_from=lambda pkt: pkt.length - 5), + lambda pkt: pkt.cert_encoding == 7) + ], + XStrLenField("cert_data", "", length_from=lambda pkt: pkt.length - 5), + ) ] -class IKEv2_payload_CERT_CRL(IKEv2_payload_CERT): - name = "IKEv2 Certificate" - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "x509CRL", "H", adjust=lambda pkt, x: x + len(pkt.x509CRL) + 5), # noqa: E501 - ByteEnumField("cert_type", 7, IKEv2CertificateEncodings), - PacketLenField("x509CRL", X509_CRL(''), X509_CRL, length_from=lambda x:x.length - 5), # noqa: E501 - ] +# TODO: the following payloads are not fully dissected yet +class IKEv2_EAP(IKEv2_Payload): + name = "IKEv2 Extensible Authentication" -class IKEv2_payload_CERT_STR(IKEv2_payload_CERT): - name = "IKEv2 Certificate" - fields_desc = [ - ByteEnumField("next_payload", None, IKEv2_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "cert_data", "H", adjust=lambda pkt, x: x + 5), # noqa: E501 - ByteEnumField("cert_type", 0, IKEv2CertificateEncodings), - StrLenField("cert_data", "", length_from=lambda x:x.length - 5), - ] + +class IKEv2_GSPM(IKEv2_Payload): + name = "Generic Secure Password Method" + + +class IKEv2_IDg(IKEv2_Payload): + name = "Group Identification" + + +class IKEv2_GSA(IKEv2_Payload): + name = "Group Security Association" + + +class IKEv2_KD(IKEv2_Payload): + name = "Key Download" + + +class IKEv2_PS(IKEv2_Payload): + name = "Puzzle Solution" -IKEv2_payload_type_overload = {} -for i, payloadname in enumerate(IKEv2_payload_type): - name = "IKEv2_payload_%s" % payloadname - if name in globals(): - IKEv2_payload_type_overload[globals()[name]] = {"next_payload": i} +# bind all IKEv2 payload classes together +bind_layers(_IKEv2_Packet, IKEv2_Proposal, next_payload=2) +bind_layers(_IKEv2_Packet, IKEv2_Transform, next_payload=3) +bind_layers(_IKEv2_Packet, IKEv2_SA, next_payload=33) +bind_layers(_IKEv2_Packet, IKEv2_KE, next_payload=34) +bind_layers(_IKEv2_Packet, IKEv2_IDi, next_payload=35) +bind_layers(_IKEv2_Packet, IKEv2_IDr, next_payload=36) +bind_layers(_IKEv2_Packet, IKEv2_CERT, next_payload=37) +bind_layers(_IKEv2_Packet, IKEv2_CERTREQ, next_payload=38) +bind_layers(_IKEv2_Packet, IKEv2_AUTH, next_payload=39) +bind_layers(_IKEv2_Packet, IKEv2_Nonce, next_payload=40) +bind_layers(_IKEv2_Packet, IKEv2_Notify, next_payload=41) +bind_layers(_IKEv2_Packet, IKEv2_Delete, next_payload=42) +bind_layers(_IKEv2_Packet, IKEv2_VendorID, next_payload=43) +bind_layers(_IKEv2_Packet, IKEv2_TSi, next_payload=44) +bind_layers(_IKEv2_Packet, IKEv2_TSr, next_payload=45) +bind_layers(_IKEv2_Packet, IKEv2_Encrypted, next_payload=46) +bind_layers(_IKEv2_Packet, IKEv2_CP, next_payload=47) +bind_layers(_IKEv2_Packet, IKEv2_EAP, next_payload=48) +bind_layers(_IKEv2_Packet, IKEv2_GSPM, next_payload=49) +bind_layers(_IKEv2_Packet, IKEv2_IDg, next_payload=50) +bind_layers(_IKEv2_Packet, IKEv2_GSA, next_payload=51) +bind_layers(_IKEv2_Packet, IKEv2_KD, next_payload=52) +bind_layers(_IKEv2_Packet, IKEv2_Encrypted_Fragment, next_payload=53) +bind_layers(_IKEv2_Packet, IKEv2_PS, next_payload=54) -del i, payloadname, name -IKEv2_class._overload_fields = IKEv2_payload_type_overload.copy() +# the upper bindings for port 500 to ISAKMP are handled by IKEv2.dispatch_hook +split_bottom_up(UDP, ISAKMP, dport=500) +split_bottom_up(UDP, ISAKMP, sport=500) -split_layers(UDP, ISAKMP, sport=500) -split_layers(UDP, ISAKMP, dport=500) +bind_bottom_up(UDP, IKEv2, dport=500) +bind_bottom_up(UDP, IKEv2, sport=500) +bind_top_down(UDP, IKEv2, dport=500, sport=500) -bind_layers(UDP, IKEv2, dport=500, sport=500) # TODO: distinguish IKEv1/IKEv2 -bind_layers(UDP, IKEv2, dport=4500, sport=4500) +split_bottom_up(NON_ESP, ISAKMP) +bind_bottom_up(NON_ESP, IKEv2) def ikev2scan(ip, **kwargs): """Send a IKEv2 SA to an IP and wait for answers.""" return sr(IP(dst=ip) / UDP() / IKEv2(init_SPI=RandString(8), - exch_type=34) / IKEv2_payload_SA(prop=IKEv2_payload_Proposal()), **kwargs) # noqa: E501 + exch_type=34) / IKEv2_SA(prop=IKEv2_Proposal()), **kwargs) # noqa: E501 diff --git a/scapy/contrib/isis.py b/scapy/contrib/isis.py index e5c3fd0c8dd..d277a27b568 100644 --- a/scapy/contrib/isis.py +++ b/scapy/contrib/isis.py @@ -1,16 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright (C) 2014-2016 BENOCS GmbH, Berlin (Germany) +# Copyright (C) 2020 Metaswitch, London (UK) # scapy.contrib.description = Intermediate System to Intermediate System (ISIS) # scapy.contrib.status = loads @@ -19,20 +11,9 @@ IS-IS Scapy Extension ~~~~~~~~~~~~~~~~~~~~~ - :copyright: 2014-2016 BENOCS GmbH, Berlin (Germany) - :author: Marcel Patzlaff, mpatzlaff@benocs.com - Michal Kaliszan, mkaliszan@benocs.com - :license: GPLv2 - - This module is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This module is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + :authors: Marcel Patzlaff, mpatzlaff@benocs.com + Michal Kaliszan, mkaliszan@benocs.com + Tom Zhu, tom.zhu@metaswitch.com :description: @@ -48,6 +29,7 @@ * RFC 5303 (three-way handshake) * RFC 5304 (cryptographic authentication) * RFC 5308 (routing IPv6 with IS-IS) + * RFC 8667 (IS-IS extensions for segment routing) :TODO: @@ -60,7 +42,6 @@ """ -from __future__ import absolute_import import struct import random @@ -68,17 +49,16 @@ from scapy.fields import BitField, BitFieldLenField, BoundStrLenField, \ ByteEnumField, ByteField, ConditionalField, Field, FieldLenField, \ FieldListField, FlagsField, IEEEFloatField, IP6PrefixField, IPField, \ - IPPrefixField, IntField, LongField, MACField, PacketListField, \ - ShortField, ThreeBytesField, XIntField, XShortField + IPPrefixField, IntField, LongField, MACField, PacketField, \ + PacketListField, ShortField, ThreeBytesField, XIntField, XShortField from scapy.packet import bind_layers, Packet from scapy.layers.clns import network_layer_protocol_ids, register_cln_protocol from scapy.layers.inet6 import IP6ListField, IP6Field from scapy.utils import fletcher16_checkbytes from scapy.volatile import RandString, RandByte -from scapy.modules.six.moves import range from scapy.compat import orb, hex_bytes -EXT_VERSION = "v0.0.2" +EXT_VERSION = "v0.0.3" ####################################################################### @@ -379,12 +359,14 @@ class ISIS_TEDefaultMetricSubTlv(ISIS_GenericSubTlv): ####################################################################### _isis_subtlv_classes_2 = { 1: "ISIS_32bitAdministrativeTagSubTlv", - 2: "ISIS_64bitAdministrativeTagSubTlv" + 2: "ISIS_64bitAdministrativeTagSubTlv", + 3: "ISIS_PrefixSegmentIdentifierSubTlv" } _isis_subtlv_names_2 = { 1: "32-bit Administrative Tag", - 2: "64-bit Administrative Tag" + 2: "64-bit Administrative Tag", + 3: "Prefix Segment Identifier" } @@ -406,6 +388,113 @@ class ISIS_64bitAdministrativeTagSubTlv(ISIS_GenericSubTlv): FieldListField("tags", [], LongField("", 0), count_from=lambda pkt: pkt.len // 8)] # noqa: E501 +class ISIS_PrefixSegmentIdentifierSubTlv(ISIS_GenericSubTlv): + name = "ISIS Prefix SID sub TLV" + fields_desc = [ByteEnumField("type", 3, _isis_subtlv_names_2), + ByteField("len", 5), + FlagsField( + "flags", 0, 8, + ["res1", "res2", "L", "V", "E", "P", "N", "R"]), + ByteField("algorithm", 0), + ConditionalField(ThreeBytesField("sid", 0), + lambda pkt: pkt.len == 5), + ConditionalField(IntField("idx", 0), + lambda pkt: pkt.len == 6)] + + +####################################################################### +# ISIS Sub-TLVs for TLVs 149, 150 # +####################################################################### +_isis_subtlv_classes_3 = { + 1: "ISIS_SIDLabelSubTLV" +} + +_isis_subtlv_names_3 = { + 1: "ISIS SID/Label sub TLV" +} + + +def _ISIS_GuessSubTlvClass_3(p, **kargs): + return _ISIS_GuessTlvClass_Helper( + _isis_subtlv_classes_3, "ISIS_GenericSubTlv", p, **kargs) + + +class ISIS_SIDLabelSubTLV(ISIS_GenericSubTlv): + name = "ISIS SID Label sub TLV" + fields_desc = [ + ByteEnumField("type", 1, _isis_subtlv_names_3), + ByteField("len", 3), + ConditionalField(ThreeBytesField("sid", 0), + lambda pkt: pkt.len == 3), + ConditionalField(IntField("idx", 0), + lambda pkt: pkt.len == 4) + ] + + +####################################################################### +# ISIS Sub-TLVs for TLV 242 # +####################################################################### +_isis_subtlv_classes_4 = { + 2: "ISIS_SRCapabilitiesSubTLV", + 19: "ISIS_SRAlgorithmSubTLV", +} + +_isis_subtlv_names_4 = { + 2: "Segment Routing Capability sub TLV", + 19: "Segment Routing Algorithm", +} + + +def _ISIS_GuessSubTlvClass_4(p, **kargs): + return _ISIS_GuessTlvClass_Helper( + _isis_subtlv_classes_4, "ISIS_GenericSubTlv", p, **kargs) + + +class ISIS_SRGBDescriptorEntry(Packet): + name = "ISIS SRGB Descriptor" + fields_desc = [ + ThreeBytesField("range", 0), + PacketField("sid_label", None, ISIS_SIDLabelSubTLV) + ] + + def extract_padding(self, s): + return "", s + + +class ISIS_SRCapabilitiesSubTLV(ISIS_GenericSubTlv): + name = "ISIS SR Capabilities TLV" + fields_desc = [ + ByteEnumField("type", 2, _isis_subtlv_names_3), + FieldLenField( + "len", + None, + length_of="srgb_ranges", + adjust=lambda pkt, x: x + 1, + fmt="B"), + FlagsField( + "flags", 0, 8, + ["res1", "res2", "res3", "res4", "res5", "res6", "V", "I"]), + PacketListField( + "srgb_ranges", + [], + ISIS_SRGBDescriptorEntry, + length_from=lambda pkt: pkt.len - 1) + ] + + +class ISIS_SRAlgorithmSubTLV(ISIS_GenericSubTlv): + name = "ISIS SR Algorithm sub TLV" + fields_desc = [ + ByteEnumField("type", 19, _isis_subtlv_names_4), + FieldLenField("len", None, length_of="algorithms", fmt="B"), + FieldListField( + "algorithms", + [0], + ByteField("", 0), + count_from=lambda pkt:pkt.len) + ] + + ####################################################################### # ISIS TLVs # ####################################################################### @@ -427,7 +516,8 @@ class ISIS_64bitAdministrativeTagSubTlv(ISIS_GenericSubTlv): 137: "ISIS_DynamicHostnameTlv", 232: "ISIS_Ipv6InterfaceAddressTlv", 236: "ISIS_Ipv6ReachabilityTlv", - 240: "ISIS_P2PAdjacencyStateTlv" + 240: "ISIS_P2PAdjacencyStateTlv", + 242: "ISIS_RouterCapabilityTlv" } _isis_tlv_names = { @@ -687,6 +777,28 @@ class ISIS_ProtocolsSupportedTlv(ISIS_GenericTlv): ] +class ISIS_RouterCapabilityTlv(ISIS_GenericTlv): + name = "ISIS Router Capability TLV" + fields_desc = [ + ByteEnumField("type", 242, _isis_tlv_names), + FieldLenField( + "len", + None, + length_of="subtlvs", + adjust=lambda pkt, x: x + 5, + fmt="B"), + IPField("routerid", "0.0.0.0"), + FlagsField( + "flags", 0, 8, + ["S", "D", "res1", "res2", "res3", "res4", "res5", "res6"]), + PacketListField( + "subtlvs", + [], + _ISIS_GuessSubTlvClass_4, + length_from=lambda pkt: pkt.len - 5) + ] + + ####################################################################### # ISIS Old-Style TLVs # ####################################################################### diff --git a/scapy/contrib/isotp.py b/scapy/contrib/isotp.py deleted file mode 100644 index 873a1a58d4b..00000000000 --- a/scapy/contrib/isotp.py +++ /dev/null @@ -1,2273 +0,0 @@ -# This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Nils Weiss -# Copyright (C) Enrico Pozzobon -# Copyright (C) Alexander Schroeder -# This program is published under a GPLv2 license - -# scapy.contrib.description = ISO-TP (ISO 15765-2) -# scapy.contrib.status = loads - -""" -ISOTPSocket. -""" - - -import ctypes -from ctypes.util import find_library -import struct -import socket -import time -import traceback -import heapq -from threading import Thread, Event, Lock - -from scapy.packet import Packet -from scapy.fields import BitField, FlagsField, StrLenField, \ - ThreeBytesField, XBitField, ConditionalField, \ - BitEnumField, ByteField, XByteField, BitFieldLenField, StrField -from scapy.compat import chb, orb -from scapy.layers.can import CAN -import scapy.modules.six as six -import scapy.automaton as automaton -import six.moves.queue as queue -from scapy.error import Scapy_Exception, warning, log_loading, log_runtime -from scapy.supersocket import SuperSocket, SO_TIMESTAMPNS -from scapy.config import conf -from scapy.consts import LINUX -from scapy.contrib.cansocket import PYTHON_CAN -from scapy.sendrecv import sniff -from scapy.sessions import DefaultSession - -__all__ = ["ISOTP", "ISOTPHeader", "ISOTPHeaderEA", "ISOTP_SF", "ISOTP_FF", - "ISOTP_CF", "ISOTP_FC", "ISOTPSoftSocket", "ISOTPSession", - "ISOTPSocket", "ISOTPSocketImplementation", "ISOTPMessageBuilder", - "ISOTPScan"] - -USE_CAN_ISOTP_KERNEL_MODULE = False -if six.PY3 and LINUX: - LIBC = ctypes.cdll.LoadLibrary(find_library("c")) - try: - if conf.contribs['ISOTP']['use-can-isotp-kernel-module']: - USE_CAN_ISOTP_KERNEL_MODULE = True - except KeyError: - log_loading.info("Specify 'conf.contribs['ISOTP'] = " - "{'use-can-isotp-kernel-module': True}' to enable " - "usage of can-isotp kernel module.") - -CAN_MAX_IDENTIFIER = (1 << 29) - 1 # Maximum 29-bit identifier -CAN_MTU = 16 -CAN_MAX_DLEN = 8 -ISOTP_MAX_DLEN_2015 = (1 << 32) - 1 # Maximum for 32-bit FF_DL -ISOTP_MAX_DLEN = (1 << 12) - 1 # Maximum for 12-bit FF_DL - -N_PCI_SF = 0x00 # /* single frame */ -N_PCI_FF = 0x10 # /* first frame */ -N_PCI_CF = 0x20 # /* consecutive frame */ -N_PCI_FC = 0x30 # /* flow control */ - - -class ISOTP(Packet): - name = 'ISOTP' - fields_desc = [ - StrField('data', B"") - ] - __slots__ = Packet.__slots__ + ["src", "dst", "exsrc", "exdst"] - - def answers(self, other): - if other.__class__ == self.__class__: - return self.payload.answers(other.payload) - return 0 - - def __init__(self, *args, **kwargs): - self.src = None - self.dst = None - self.exsrc = None - self.exdst = None - if "src" in kwargs: - self.src = kwargs["src"] - del kwargs["src"] - if "dst" in kwargs: - self.dst = kwargs["dst"] - del kwargs["dst"] - if "exsrc" in kwargs: - self.exsrc = kwargs["exsrc"] - del kwargs["exsrc"] - if "exdst" in kwargs: - self.exdst = kwargs["exdst"] - del kwargs["exdst"] - Packet.__init__(self, *args, **kwargs) - self.validate_fields() - - def validate_fields(self): - if self.src is not None: - if not 0 <= self.src <= CAN_MAX_IDENTIFIER: - raise Scapy_Exception("src is not a valid CAN identifier") - if self.dst is not None: - if not 0 <= self.dst <= CAN_MAX_IDENTIFIER: - raise Scapy_Exception("dst is not a valid CAN identifier") - if self.exsrc is not None: - if not 0 <= self.exsrc <= 0xff: - raise Scapy_Exception("exsrc is not a byte") - if self.exdst is not None: - if not 0 <= self.exdst <= 0xff: - raise Scapy_Exception("exdst is not a byte") - - def fragment(self): - data_bytes_in_frame = 7 - if self.exdst is not None: - data_bytes_in_frame = 6 - - if len(self.data) > ISOTP_MAX_DLEN_2015: - raise Scapy_Exception("Too much data in ISOTP message") - - if len(self.data) <= data_bytes_in_frame: - # We can do this in a single frame - frame_data = struct.pack('B', len(self.data)) + self.data - if self.exdst: - frame_data = struct.pack('B', self.exdst) + frame_data - - if self.dst is None or self.dst <= 0x7ff: - pkt = CAN(identifier=self.dst, data=frame_data) - else: - pkt = CAN(identifier=self.dst, flags="extended", - data=frame_data) - return [pkt] - - # Construct the first frame - if len(self.data) <= ISOTP_MAX_DLEN: - frame_header = struct.pack(">H", len(self.data) + 0x1000) - else: - frame_header = struct.pack(">HI", 0x1000, len(self.data)) - if self.exdst: - frame_header = struct.pack('B', self.exdst) + frame_header - idx = 8 - len(frame_header) - frame_data = self.data[0:idx] - if self.dst is None or self.dst <= 0x7ff: - frame = CAN(identifier=self.dst, data=frame_header + frame_data) - else: - frame = CAN(identifier=self.dst, flags="extended", - data=frame_header + frame_data) - - # Construct consecutive frames - n = 1 - pkts = [frame] - while idx < len(self.data): - frame_data = self.data[idx:idx + data_bytes_in_frame] - frame_header = struct.pack("b", (n % 16) + N_PCI_CF) - - n += 1 - idx += len(frame_data) - - if self.exdst: - frame_header = struct.pack('B', self.exdst) + frame_header - if self.dst is None or self.dst <= 0x7ff: - pkt = CAN(identifier=self.dst, data=frame_header + frame_data) - else: - pkt = CAN(identifier=self.dst, flags="extended", - data=frame_header + frame_data) - pkts.append(pkt) - return pkts - - @staticmethod - def defragment(can_frames, use_extended_addressing=None): - if len(can_frames) == 0: - raise Scapy_Exception("ISOTP.defragment called with 0 frames") - - dst = can_frames[0].identifier - for frame in can_frames: - if frame.identifier != dst: - warning("Not all CAN frames have the same identifier") - - parser = ISOTPMessageBuilder(use_extended_addressing) - for c in can_frames: - parser.feed(c) - - results = [] - while parser.count > 0: - p = parser.pop() - if (use_extended_addressing is True and p.exdst is not None) \ - or (use_extended_addressing is False and p.exdst is None) \ - or (use_extended_addressing is None): - results.append(p) - - if len(results) == 0: - return None - - if len(results) > 0: - warning("More than one ISOTP frame could be defragmented from the " - "provided CAN frames, returning the first one.") - - return results[0] - - -class ISOTPHeader(CAN): - name = 'ISOTPHeader' - fields_desc = [ - FlagsField('flags', 0, 3, ['error', - 'remote_transmission_request', - 'extended']), - XBitField('identifier', 0, 29), - ByteField('length', None), - ThreeBytesField('reserved', 0), - ] - - def extract_padding(self, p): - return p, None - - def post_build(self, pkt, pay): - """ - This will set the ByteField 'length' to the correct value. - """ - if self.length is None: - pkt = pkt[:4] + chb(len(pay)) + pkt[5:] - return pkt + pay - - def guess_payload_class(self, payload): - """ - ISOTP encodes the frame type in the first nibble of a frame. - """ - t = (orb(payload[0]) & 0xf0) >> 4 - if t == 0: - return ISOTP_SF - elif t == 1: - return ISOTP_FF - elif t == 2: - return ISOTP_CF - else: - return ISOTP_FC - - -class ISOTPHeaderEA(ISOTPHeader): - name = 'ISOTPHeaderExtendedAddress' - fields_desc = ISOTPHeader.fields_desc + [ - XByteField('extended_address', 0), - ] - - def post_build(self, p, pay): - """ - This will set the ByteField 'length' to the correct value. - 'chb(len(pay) + 1)' is required, because the field 'extended_address' - is counted as payload on the CAN layer - """ - if self.length is None: - p = p[:4] + chb(len(pay) + 1) + p[5:] - return p + pay - - -ISOTP_TYPE = {0: 'single', - 1: 'first', - 2: 'consecutive', - 3: 'flow_control'} - - -class ISOTP_SF(Packet): - name = 'ISOTPSingleFrame' - fields_desc = [ - BitEnumField('type', 0, 4, ISOTP_TYPE), - BitFieldLenField('message_size', None, 4, length_of='data'), - StrLenField('data', '', length_from=lambda pkt: pkt.message_size) - ] - - -class ISOTP_FF(Packet): - name = 'ISOTPFirstFrame' - fields_desc = [ - BitEnumField('type', 1, 4, ISOTP_TYPE), - BitField('message_size', 0, 12), - ConditionalField(BitField('extended_message_size', 0, 32), - lambda pkt: pkt.message_size == 0), - StrField('data', '', fmt="B") - ] - - -class ISOTP_CF(Packet): - name = 'ISOTPConsecutiveFrame' - fields_desc = [ - BitEnumField('type', 2, 4, ISOTP_TYPE), - BitField('index', 0, 4), - StrField('data', '', fmt="B") - ] - - -class ISOTP_FC(Packet): - name = 'ISOTPFlowControlFrame' - fields_desc = [ - BitEnumField('type', 3, 4, ISOTP_TYPE), - BitEnumField('fc_flag', 0, 4, {0: 'continue', - 1: 'wait', - 2: 'abort'}), - ByteField('block_size', 0), - ByteField('separation_time', 0), - ] - - -class ISOTPMessageBuilderIter(object): - slots = ["builder"] - - def __init__(self, builder): - self.builder = builder - - def __iter__(self): - return self - - def __next__(self): - while self.builder.count: - return self.builder.pop() - raise StopIteration - - next = __next__ - - -class ISOTPMessageBuilder(object): - """ - Utility class to build ISOTP messages out of CAN frames, used by both - ISOTP.defragment() and ISOTPSession. - - This class attempts to interpret some CAN frames as ISOTP frames, both with - and without extended addressing at the same time. For example, if an - extended address of 07 is being used, all frames will also be interpreted - as ISOTP single-frame messages. - - CAN frames are fed to an ISOTPMessageBuilder object with the feed() method - and the resulting ISOTP frames can be extracted using the pop() method. - """ - - class Bucket(object): - def __init__(self, total_len, first_piece, ts=None): - self.pieces = list() - self.total_len = total_len - self.current_len = 0 - self.ready = None - self.src = None - self.exsrc = None - self.time = ts - self.push(first_piece) - - def push(self, piece): - self.pieces.append(piece) - self.current_len += len(piece) - if self.current_len >= self.total_len: - if six.PY3: - isotp_data = b"".join(self.pieces) - else: - isotp_data = "".join(map(str, self.pieces)) - self.ready = isotp_data[:self.total_len] - - def __init__(self, use_ext_addr=None, did=None, basecls=None): - """ - Initialize a ISOTPMessageBuilder object - - :param use_ext_addr: True for only attempting to defragment with - extended addressing, False for only attempting - to defragment without extended addressing, - or None for both - :param basecls: the class of packets that will be returned, - defaults to ISOTP - - """ - self.ready = [] - self.buckets = {} - self.use_ext_addr = use_ext_addr - self.basecls = basecls or ISOTP - self.dst_ids = None - self.last_ff = None - self.last_ff_ex = None - if did is not None: - if hasattr(did, "__iter__"): - self.dst_ids = did - else: - self.dst_ids = [did] - - def feed(self, can): - """Attempt to feed an incoming CAN frame into the state machine""" - if not isinstance(can, Packet) and hasattr(can, "__iter__"): - for p in can: - self.feed(p) - return - identifier = can.identifier - - if self.dst_ids is not None and identifier not in self.dst_ids: - return - - data = bytes(can.data) - - if len(data) > 1 and self.use_ext_addr is not True: - self._try_feed(identifier, None, data, can.time) - if len(data) > 2 and self.use_ext_addr is not False: - ea = six.indexbytes(data, 0) - self._try_feed(identifier, ea, data[1:], can.time) - - @property - def count(self): - """Returns the number of ready ISOTP messages built from the provided - can frames""" - return len(self.ready) - - def __len__(self): - return self.count - - def pop(self, identifier=None, ext_addr=None): - """ - Returns a built ISOTP message - - :param identifier: if not None, only return isotp messages with this - destination - :param ext_addr: if identifier is not None, only return isotp messages - with this extended address for destination - :returns: an ISOTP packet, or None if no message is ready - """ - - if identifier is not None: - for i in range(len(self.ready)): - b = self.ready[i] - iden = b[0] - ea = b[1] - if iden == identifier and ext_addr == ea: - return ISOTPMessageBuilder._build(self.ready.pop(i), - self.basecls) - return None - - if len(self.ready) > 0: - return ISOTPMessageBuilder._build(self.ready.pop(0), self.basecls) - return None - - def __iter__(self): - return ISOTPMessageBuilderIter(self) - - @staticmethod - def _build(t, basecls=ISOTP): - bucket = t[2] - p = basecls(bucket.ready) - if hasattr(p, "dst"): - p.dst = t[0] - if hasattr(p, "exdst"): - p.exdst = t[1] - if hasattr(p, "src"): - p.src = bucket.src - if hasattr(p, "exsrc"): - p.exsrc = bucket.exsrc - if hasattr(p, "time"): - p.time = bucket.time - return p - - def _feed_first_frame(self, identifier, ea, data, ts): - if len(data) < 3: - # At least 3 bytes are necessary: 2 for length and 1 for data - return False - - header = struct.unpack('>H', bytes(data[:2]))[0] - expected_length = header & 0x0fff - isotp_data = data[2:] - if expected_length == 0 and len(data) >= 6: - expected_length = struct.unpack('>I', bytes(data[2:6]))[0] - isotp_data = data[6:] - - key = (ea, identifier, 1) - if ea is None: - self.last_ff = key - else: - self.last_ff_ex = key - self.buckets[key] = self.Bucket(expected_length, isotp_data, ts) - return True - - def _feed_single_frame(self, identifier, ea, data, ts): - if len(data) < 2: - # At least 2 bytes are necessary: 1 for length and 1 for data - return False - - length = six.indexbytes(data, 0) & 0x0f - isotp_data = data[1:length + 1] - - if length > len(isotp_data): - # CAN frame has less data than expected - return False - - self.ready.append((identifier, ea, - self.Bucket(length, isotp_data, ts))) - return True - - def _feed_consecutive_frame(self, identifier, ea, data): - if len(data) < 2: - # At least 2 bytes are necessary: 1 for sequence number and - # 1 for data - return False - - first_byte = six.indexbytes(data, 0) - seq_no = first_byte & 0x0f - isotp_data = data[1:] - - key = (ea, identifier, seq_no) - bucket = self.buckets.pop(key, None) - - if bucket is None: - # There is no message constructor waiting for this frame - return False - - bucket.push(isotp_data) - if bucket.ready is None: - # full ISOTP message is not ready yet, put it back in - # buckets list - next_seq = (seq_no + 1) % 16 - key = (ea, identifier, next_seq) - self.buckets[key] = bucket - else: - self.ready.append((identifier, ea, bucket)) - - return True - - def _feed_flow_control_frame(self, identifier, ea, data): - if len(data) < 3: - # At least 2 bytes are necessary: 1 for sequence number and - # 1 for data - return False - - keys = [self.last_ff, self.last_ff_ex] - if not any(keys): - return False - - buckets = [self.buckets.pop(k, None) for k in keys] - - self.last_ff = None - self.last_ff_ex = None - - if not any(buckets): - # There is no message constructor waiting for this frame - return False - - for key, bucket in zip(keys, buckets): - if bucket is None: - continue - bucket.src = identifier - bucket.exsrc = ea - self.buckets[key] = bucket - return True - - def _try_feed(self, identifier, ea, data, ts): - first_byte = six.indexbytes(data, 0) - if len(data) > 1 and first_byte & 0xf0 == N_PCI_SF: - self._feed_single_frame(identifier, ea, data, ts) - if len(data) > 2 and first_byte & 0xf0 == N_PCI_FF: - self._feed_first_frame(identifier, ea, data, ts) - if len(data) > 1 and first_byte & 0xf0 == N_PCI_CF: - self._feed_consecutive_frame(identifier, ea, data) - if len(data) > 1 and first_byte & 0xf0 == N_PCI_FC: - self._feed_flow_control_frame(identifier, ea, data) - - -class ISOTPSession(DefaultSession): - """Defragment ISOTP packets 'on-the-flow'. - - Usage: - >>> sniff(session=ISOTPSession) - """ - - def __init__(self, *args, **kwargs): - DefaultSession.__init__(self, *args, **kwargs) - self.m = ISOTPMessageBuilder( - use_ext_addr=kwargs.pop("use_ext_addr", None), - did=kwargs.pop("did", None), - basecls=kwargs.pop("basecls", None)) - - def on_packet_received(self, pkt): - if not pkt: - return - if isinstance(pkt, list): - for p in pkt: - ISOTPSession.on_packet_received(self, p) - return - self.m.feed(pkt) - while len(self.m) > 0: - rcvd = self.m.pop() - if self._supersession: - self._supersession.on_packet_received(rcvd) - else: - DefaultSession.on_packet_received(self, rcvd) - - -class ISOTPSoftSocket(SuperSocket): - """ - This class is a wrapper around the ISOTPSocketImplementation, for the - reasons described below. - - The ISOTPSoftSocket aims to be fully compatible with the Linux ISOTP - sockets provided by the can-isotp kernel module, while being usable on any - operating system. - Therefore, this socket needs to be able to respond to an incoming FF frame - with a FC frame even before the recv() method is called. - A thread is needed for receiving CAN frames in the background, and since - the lower layer CAN implementation is not guaranteed to have a functioning - POSIX select(), each ISOTP socket needs its own CAN receiver thread. - SuperSocket automatically calls the close() method when the GC destroys an - ISOTPSoftSocket. However, note that if any thread holds a reference to - an ISOTPSoftSocket object, it will not be collected by the GC. - - The implementation of the ISOTP protocol, along with the necessary - thread, are stored in the ISOTPSocketImplementation class, and therefore: - - * There no reference from ISOTPSocketImplementation to ISOTPSoftSocket - * ISOTPSoftSocket can be normally garbage collected - * Upon destruction, ISOTPSoftSocket.close() will be called - * ISOTPSoftSocket.close() will call ISOTPSocketImplementation.close() - * RX background thread can be stopped by the garbage collector - """ - - nonblocking_socket = True - - def __init__(self, - can_socket=None, - sid=0, - did=0, - extended_addr=None, - extended_rx_addr=None, - rx_block_size=0, - rx_separation_time_min=0, - padding=False, - listen_only=False, - basecls=ISOTP): - """ - Initialize an ISOTPSoftSocket using the provided underlying can socket - - :param can_socket: a CANSocket instance, preferably filtering only can - frames with identifier equal to did - :param sid: the CAN identifier of the sent CAN frames - :param did: the CAN identifier of the received CAN frames - :param extended_addr: the extended address of the sent ISOTP frames - (can be None) - :param extended_rx_addr: the extended address of the received ISOTP - frames (can be None) - :param rx_block_size: block size sent in Flow Control ISOTP frames - :param rx_separation_time_min: minimum desired separation time sent in - Flow Control ISOTP frames - :param padding: If True, pads sending packets with 0x00 which not - count to the payload. - Does not affect receiving packets. - :param basecls: base class of the packets emitted by this socket - """ - - if six.PY3 and LINUX and isinstance(can_socket, six.string_types): - from scapy.contrib.cansocket import CANSocket - can_socket = CANSocket(can_socket) - elif isinstance(can_socket, six.string_types): - raise Scapy_Exception("Provide a CANSocket object instead") - - self.exsrc = extended_addr - self.exdst = extended_rx_addr - self.src = sid - self.dst = did - - impl = ISOTPSocketImplementation( - can_socket, - src_id=sid, - dst_id=did, - padding=padding, - extended_addr=extended_addr, - extended_rx_addr=extended_rx_addr, - rx_block_size=rx_block_size, - rx_separation_time_min=rx_separation_time_min, - listen_only=listen_only - ) - - self.ins = impl - self.outs = impl - self.impl = impl - - if basecls is None: - warning('Provide a basecls ') - self.basecls = basecls - - def close(self): - if not self.closed: - self.impl.close() - self.outs = None - self.ins = None - SuperSocket.close(self) - - def begin_send(self, p): - """Begin the transmission of message p. This method returns after - sending the first frame. If multiple frames are necessary to send the - message, this socket will unable to send other messages until either - the transmission of this frame succeeds or it fails.""" - if hasattr(p, "sent_time"): - p.sent_time = time.time() - - return self.outs.begin_send(bytes(p)) - - def recv_raw(self, x=0xffff): - """Receive a complete ISOTP message, blocking until a message is - received or the specified timeout is reached. - If self.timeout is 0, then this function doesn't block and returns the - first frame in the receive buffer or None if there isn't any.""" - msg = self.ins.recv() - t = time.time() - return self.basecls, msg, t - - def recv(self, x=0xffff): - msg = SuperSocket.recv(self, x) - - if hasattr(msg, "src"): - msg.src = self.src - if hasattr(msg, "dst"): - msg.dst = self.dst - if hasattr(msg, "exsrc"): - msg.exsrc = self.exsrc - if hasattr(msg, "exdst"): - msg.exdst = self.exdst - return msg - - @staticmethod - def select(sockets, remain=None): - """This function is called during sendrecv() routine to wait for - sockets to be ready to receive - """ - blocking = remain is None or remain > 0 - - def find_ready_sockets(): - return list(filter(lambda x: not x.ins.rx_queue.empty(), sockets)) - - ready_sockets = find_ready_sockets() - if len(ready_sockets) > 0 or not blocking: - return ready_sockets, None - - exit_select = Event() - - def my_cb(msg): - exit_select.set() - - try: - for s in sockets: - s.ins.rx_callbacks.append(my_cb) - - exit_select.wait(remain) - - finally: - for s in sockets: - try: - s.ins.rx_callbacks.remove(my_cb) - except ValueError: - pass - - ready_sockets = find_ready_sockets() - return ready_sockets, None - - -ISOTPSocket = ISOTPSoftSocket - - -class CANReceiverThread(Thread): - """ - Helper class that receives CAN frames and feeds them to the provided - callback. It relies on CAN frames being enqueued in the CANSocket object - and not being lost if they come before the sniff method is called. This is - true in general since sniff is usually implemented as repeated recv(), but - might be false in some implementation of CANSocket - """ - - def __init__(self, can_socket, callback): - """ - Initialize the thread. In order for this thread to be able to be - stopped by the destructor of another object, it is important to not - keep a reference to the object in the callback function. - - :param socket: the CANSocket upon which this class will call the - sniff() method - :param callback: function to call whenever a CAN frame is received - """ - self.socket = can_socket - self.callback = callback - self.exiting = False - self._thread_started = Event() - self.exception = None - - Thread.__init__(self) - self.name = "CANReceiver" + self.name - - def start(self): - Thread.start(self) - if not self._thread_started.wait(5): - raise Scapy_Exception("CAN RX thread not started in 5s.") - - def run(self): - self._thread_started.set() - try: - def prn(msg): - if not self.exiting: - self.callback(msg) - - while 1: - try: - sniff(store=False, timeout=1, count=1, - stop_filter=lambda x: self.exiting, - prn=prn, opened_socket=self.socket) - except ValueError as ex: - if not self.exiting: - raise ex - if self.exiting: - return - except Exception as ex: - self.exception = ex - - def stop(self): - self.exiting = True - - -class TimeoutScheduler: - """A timeout scheduler which uses a single thread for all timeouts, unlike - python's own Timer objects which use a thread each.""" - VERBOSE = False - GRACE = .1 - _mutex = Lock() - _event = Event() - _thread = None - _handles = [] # must use heapq functions! - - @staticmethod - def schedule(timeout, callback): - """Schedules the execution of a timeout. - - The function `callback` will be called in `timeout` seconds. - - Returns a handle that can be used to remove the timeout.""" - when = TimeoutScheduler._time() + timeout - handle = TimeoutScheduler.Handle(when, callback) - handles = TimeoutScheduler._handles - - with TimeoutScheduler._mutex: - # Add the handler to the heap, keeping the invariant - # Time complexity is O(log n) - heapq.heappush(handles, handle) - must_interrupt = (handles[0] == handle) - - # Start the scheduling thread if it is not started already - if TimeoutScheduler._thread is None: - t = Thread(target=TimeoutScheduler._task) - must_interrupt = False - TimeoutScheduler._thread = t - TimeoutScheduler._event.clear() - t.start() - - if must_interrupt: - # if the new timeout got in front of the one we are currently - # waiting on, the current wait operation must be aborted and - # updated with the new timeout - TimeoutScheduler._event.set() - - # Return the handle to the timeout so that the user can cancel it - return handle - - @staticmethod - def cancel(handle): - """Provided its handle, cancels the execution of a timeout.""" - - handles = TimeoutScheduler._handles - with TimeoutScheduler._mutex: - if handle in handles: - # Time complexity is O(n) - handle._cb = None - handles.remove(handle) - heapq.heapify(handles) - - if len(handles) == 0: - # set the event to stop the wait - this kills the thread - TimeoutScheduler._event.set() - else: - Exception("Handle not found") - - @staticmethod - def clear(): - """Cancels the execution of all timeouts.""" - with TimeoutScheduler._mutex: - TimeoutScheduler._handles.clear() - - # set the event to stop the wait - this kills the thread - TimeoutScheduler._event.set() - - @staticmethod - def _peek_next(): - """Returns the next timeout to execute, or `None` if list is empty, - without modifying the list""" - with TimeoutScheduler._mutex: - handles = TimeoutScheduler._handles - if len(handles) == 0: - return None - else: - return handles[0] - - @staticmethod - def _wait(handle): - """Waits until it is time to execute the provided handle, or until - another thread calls _event.set()""" - - if handle is None: - when = TimeoutScheduler.GRACE - else: - when = handle._when - - # Check how much time until the next timeout - now = TimeoutScheduler._time() - to_wait = when - now - - # Wait until the next timeout, - # or until event.set() gets called in another thread. - if to_wait > 0: - log_runtime.debug("TimeoutScheduler Thread going to sleep @ %f " + - "for %fs", now, to_wait) - interrupted = TimeoutScheduler._event.wait(to_wait) - new = TimeoutScheduler._time() - log_runtime.debug("TimeoutScheduler Thread awake @ %f, slept for" + - " %f, interrupted=%d", new, new - now, - interrupted) - - # Clear the event so that we can wait on it again, - # Must be done before doing the callbacks to avoid losing a set(). - TimeoutScheduler._event.clear() - - @staticmethod - def _task(): - """Executed in a background thread, this thread will automatically - start when the first timeout is added and stop when the last timeout - is removed or executed.""" - - log_runtime.debug("TimeoutScheduler Thread spawning @ %f", - TimeoutScheduler._time()) - - time_empty = None - - try: - while 1: - handle = TimeoutScheduler._peek_next() - if handle is None: - now = TimeoutScheduler._time() - if time_empty is None: - time_empty = now - # 100 ms of grace time before killing the thread - if TimeoutScheduler.GRACE < now - time_empty: - return - TimeoutScheduler._wait(handle) - TimeoutScheduler._poll() - - finally: - # Worst case scenario: if this thread dies, the next scheduled - # timeout will start a new one - log_runtime.debug("TimeoutScheduler Thread dying @ %f", - TimeoutScheduler._time()) - TimeoutScheduler._thread = None - - @staticmethod - def _poll(): - """Execute all the callbacks that were due until now""" - - handles = TimeoutScheduler._handles - handle = None - while 1: - with TimeoutScheduler._mutex: - now = TimeoutScheduler._time() - if len(handles) == 0 or handles[0]._when > now: - # There is nothing to execute yet - return - - # Time complexity is O(log n) - handle = heapq.heappop(handles) - - # Call the callback here, outside of the mutex - callback = handle._cb if handle is not None else None - if callback is not None: - try: - callback() - except Exception: - traceback.print_exc() - - @staticmethod - def _time(): - if six.PY2: - return time.time() - return time.monotonic() - - class Handle: - """Handle for a timeout, consisting of a callback and a time when it - should be executed.""" - __slots__ = '_when', '_cb' - - def __init__(self, when, cb): - self._when = when - self._cb = cb - - def cancel(self): - """Cancels this timeout, preventing it from executing its - callback""" - self._cb = None - return TimeoutScheduler.cancel(self) - - def __cmp__(self, other): - diff = self._when - other._when - return 0 if diff == 0 else (1 if diff > 0 else -1) - - def __lt__(self, other): - return self._when < other._when - - -"""ISOTPSoftSocket definitions.""" - -# Enum states -ISOTP_IDLE = 0 -ISOTP_WAIT_FIRST_FC = 1 -ISOTP_WAIT_FC = 2 -ISOTP_WAIT_DATA = 3 -ISOTP_SENDING = 4 - -# /* Flow Status given in FC frame */ -ISOTP_FC_CTS = 0 # /* clear to send */ -ISOTP_FC_WT = 1 # /* wait */ -ISOTP_FC_OVFLW = 2 # /* overflow */ - - -class ISOTPSocketImplementation(automaton.SelectableObject): - """ - Implementation of an ISOTP "state machine". - - Most of the ISOTP logic was taken from - https://github.com/hartkopp/can-isotp/blob/master/net/can/isotp.c - - This class is separated from ISOTPSoftSocket to make sure the background - thread can't hold a reference to ISOTPSoftSocket, allowing it to be - collected by the GC. - """ - - def __init__(self, - can_socket, - src_id, - dst_id, - padding=False, - extended_addr=None, - extended_rx_addr=None, - rx_block_size=0, - rx_separation_time_min=0, - listen_only=False): - """ - :param can_socket: a CANSocket instance, preferably filtering only can - frames with identifier equal to did - :param src_id: the CAN identifier of the sent CAN frames - :param dst_id: the CAN identifier of the received CAN frames - :param padding: If True, pads sending packets with 0x00 which not - count to the payload. - Does not affect receiving packets. - :param extended_addr: Extended Address byte to be added at the - beginning of every CAN frame _sent_ by this object. Can be None - in order to disable extended addressing on sent frames. - :param extended_rx_addr: Extended Address byte expected to be found at - the beginning of every CAN frame _received_ by this object. Can - be None in order to disable extended addressing on received - frames. - :param rx_block_size: Block Size byte to be included in every Control - Flow Frame sent by this object. The default value of 0 means - that all the data will be received in a single block. - :param rx_separation_time_min: Time Minimum Separation byte to be - included in every Control Flow Frame sent by this object. The - default value of 0 indicates that the peer will not wait any - time between sending frames. - :param listen_only: Disables send of flow control frames - """ - - automaton.SelectableObject.__init__(self) - - self.can_socket = can_socket - self.dst_id = dst_id - self.src_id = src_id - self.padding = padding - self.fc_timeout = 1 - self.cf_timeout = 1 - - self.filter_warning_emitted = False - - self.extended_rx_addr = extended_rx_addr - self.ea_hdr = b"" - if extended_addr is not None: - self.ea_hdr = struct.pack("B", extended_addr) - self.listen_only = listen_only - - self.rxfc_bs = rx_block_size - self.rxfc_stmin = rx_separation_time_min - - self.rx_queue = queue.Queue() - self.rx_len = -1 - self.rx_buf = None - self.rx_sn = 0 - self.rx_bs = 0 - self.rx_idx = 0 - self.rx_state = ISOTP_IDLE - - self.txfc_bs = 0 - self.txfc_stmin = 0 - self.tx_gap = 0 - - self.tx_buf = None - self.tx_sn = 0 - self.tx_bs = 0 - self.tx_idx = 0 - self.rx_ll_dl = 0 - self.tx_state = ISOTP_IDLE - - self.tx_timeout_handle = None - self.rx_timeout_handle = None - self.rx_thread = CANReceiverThread(can_socket, self.on_can_recv) - - self.tx_mutex = Lock() - self.rx_mutex = Lock() - self.send_mutex = Lock() - - self.tx_done = Event() - self.tx_exception = None - - self.tx_callbacks = [] - self.rx_callbacks = [] - - self.rx_thread.start() - - def __del__(self): - self.close() - - def can_send(self, load): - if self.padding: - load += bytearray(CAN_MAX_DLEN - len(load)) - if self.src_id is None or self.src_id <= 0x7ff: - self.can_socket.send(CAN(identifier=self.src_id, data=load)) - else: - self.can_socket.send(CAN(identifier=self.src_id, flags="extended", - data=load)) - - def on_can_recv(self, p): - if not isinstance(p, CAN): - raise Scapy_Exception("argument is not a CAN frame") - if p.identifier != self.dst_id: - if not self.filter_warning_emitted: - warning("You should put a filter for identifier=%x on your" - "CAN socket" % self.dst_id) - self.filter_warning_emitted = True - else: - self.on_recv(p) - - def close(self): - self.rx_thread.stop() - - def _rx_timer_handler(self): - """Method called every time the rx_timer times out, due to the peer not - sending a consecutive frame within the expected time window""" - - with self.rx_mutex: - if self.rx_state == ISOTP_WAIT_DATA: - # we did not get new data frames in time. - # reset rx state - self.rx_state = ISOTP_IDLE - warning("RX state was reset due to timeout") - - def _tx_timer_handler(self): - """Method called every time the tx_timer times out, which can happen in - two situations: either a Flow Control frame was not received in time, - or the Separation Time Min is expired and a new frame must be sent.""" - - with self.tx_mutex: - if (self.tx_state == ISOTP_WAIT_FC or - self.tx_state == ISOTP_WAIT_FIRST_FC): - # we did not get any flow control frame in time - # reset tx state - self.tx_state = ISOTP_IDLE - self.tx_exception = "TX state was reset due to timeout" - self.tx_done.set() - raise Scapy_Exception(self.tx_exception) - elif self.tx_state == ISOTP_SENDING: - # push out the next segmented pdu - src_off = len(self.ea_hdr) - max_bytes = 7 - src_off - - while 1: - load = self.ea_hdr - load += struct.pack("B", N_PCI_CF + self.tx_sn) - load += self.tx_buf[self.tx_idx:self.tx_idx + max_bytes] - self.can_send(load) - - self.tx_sn = (self.tx_sn + 1) % 16 - self.tx_bs += 1 - self.tx_idx += max_bytes - - if len(self.tx_buf) <= self.tx_idx: - # we are done - self.tx_state = ISOTP_IDLE - self.tx_done.set() - for cb in self.tx_callbacks: - cb() - return - - if self.txfc_bs != 0 and self.tx_bs >= self.txfc_bs: - # stop and wait for FC - self.tx_state = ISOTP_WAIT_FC - self.tx_timeout_handle = TimeoutScheduler.schedule( - self.fc_timeout, self._tx_timer_handler) - return - - if self.tx_gap == 0: - continue - else: - self.tx_timeout_handle = TimeoutScheduler.schedule( - self.tx_gap, self._tx_timer_handler) - - def on_recv(self, cf): - """Function that must be called every time a CAN frame is received, to - advance the state machine.""" - - data = bytes(cf.data) - - if len(data) < 2: - return - - ae = 0 - if self.extended_rx_addr is not None: - ae = 1 - if len(data) < 3: - return - if six.indexbytes(data, 0) != self.extended_rx_addr: - return - - n_pci = six.indexbytes(data, ae) & 0xf0 - - if n_pci == N_PCI_FC: - with self.tx_mutex: - self._recv_fc(data[ae:]) - elif n_pci == N_PCI_SF: - with self.rx_mutex: - self._recv_sf(data[ae:]) - elif n_pci == N_PCI_FF: - with self.rx_mutex: - self._recv_ff(data[ae:]) - elif n_pci == N_PCI_CF: - with self.rx_mutex: - self._recv_cf(data[ae:]) - - def _recv_fc(self, data): - """Process a received 'Flow Control' frame""" - if (self.tx_state != ISOTP_WAIT_FC and - self.tx_state != ISOTP_WAIT_FIRST_FC): - return 0 - - if self.tx_timeout_handle is not None: - self.tx_timeout_handle.cancel() - self.tx_timeout_handle = None - - if len(data) < 3: - self.tx_state = ISOTP_IDLE - self.tx_exception = "CF frame discarded because it was too short" - self.tx_done.set() - raise Scapy_Exception(self.tx_exception) - - # get communication parameters only from the first FC frame - if self.tx_state == ISOTP_WAIT_FIRST_FC: - self.txfc_bs = six.indexbytes(data, 1) - self.txfc_stmin = six.indexbytes(data, 2) - - if ((self.txfc_stmin > 0x7F) and - ((self.txfc_stmin < 0xF1) or (self.txfc_stmin > 0xF9))): - self.txfc_stmin = 0x7F - - if six.indexbytes(data, 2) <= 127: - tx_gap = six.indexbytes(data, 2) / 1000.0 - elif 0xf1 <= six.indexbytes(data, 2) <= 0xf9: - tx_gap = (six.indexbytes(data, 2) & 0x0f) / 10000.0 - else: - tx_gap = 0 - self.tx_gap = tx_gap - - self.tx_state = ISOTP_WAIT_FC - - isotp_fc = six.indexbytes(data, 0) & 0x0f - - if isotp_fc == ISOTP_FC_CTS: - self.tx_bs = 0 - self.tx_state = ISOTP_SENDING - # start cyclic timer for sending CF frame - self.tx_timeout_handle = TimeoutScheduler.schedule( - self.tx_gap, self._tx_timer_handler) - elif isotp_fc == ISOTP_FC_WT: - # start timer to wait for next FC frame - self.tx_state = ISOTP_WAIT_FC - self.tx_timeout_handle = TimeoutScheduler.schedule( - self.fc_timeout, self._tx_timer_handler) - elif isotp_fc == ISOTP_FC_OVFLW: - # overflow in receiver side - self.tx_state = ISOTP_IDLE - self.tx_exception = "Overflow happened at the receiver side" - self.tx_done.set() - raise Scapy_Exception(self.tx_exception) - else: - self.tx_state = ISOTP_IDLE - self.tx_exception = "Unknown FC frame type" - self.tx_done.set() - raise Scapy_Exception(self.tx_exception) - - return 0 - - def _recv_sf(self, data): - """Process a received 'Single Frame' frame""" - if self.rx_timeout_handle is not None: - self.rx_timeout_handle.cancel() - self.rx_timeout_handle = None - - if self.rx_state != ISOTP_IDLE: - warning("RX state was reset because single frame was received") - self.rx_state = ISOTP_IDLE - - length = six.indexbytes(data, 0) & 0xf - if len(data) - 1 < length: - return 1 - - msg = data[1:1 + length] - self.rx_queue.put(msg) - for cb in self.rx_callbacks: - cb(msg) - self.call_release() - return 0 - - def _recv_ff(self, data): - """Process a received 'First Frame' frame""" - if self.rx_timeout_handle is not None: - self.rx_timeout_handle.cancel() - self.rx_timeout_handle = None - - if self.rx_state != ISOTP_IDLE: - warning("RX state was reset because first frame was received") - self.rx_state = ISOTP_IDLE - - if len(data) < 7: - return 1 - self.rx_ll_dl = len(data) - - # get the FF_DL - self.rx_len = (six.indexbytes(data, 0) & 0x0f) * 256 + six.indexbytes( - data, 1) - ff_pci_sz = 2 - - # Check for FF_DL escape sequence supporting 32 bit PDU length - if self.rx_len == 0: - # FF_DL = 0 => get real length from next 4 bytes - self.rx_len = six.indexbytes(data, 2) << 24 - self.rx_len += six.indexbytes(data, 3) << 16 - self.rx_len += six.indexbytes(data, 4) << 8 - self.rx_len += six.indexbytes(data, 5) - ff_pci_sz = 6 - - # copy the first received data bytes - data_bytes = data[ff_pci_sz:] - self.rx_idx = len(data_bytes) - self.rx_buf = data_bytes - - # initial setup for this pdu reception - self.rx_sn = 1 - self.rx_state = ISOTP_WAIT_DATA - - # no creation of flow control frames - if not self.listen_only: - # send our first FC frame - load = self.ea_hdr - load += struct.pack("BBB", N_PCI_FC, self.rxfc_bs, self.rxfc_stmin) - self.can_send(load) - - # wait for a CF - self.rx_bs = 0 - self.rx_timeout_handle = TimeoutScheduler.schedule( - self.cf_timeout, self._rx_timer_handler) - - return 0 - - def _recv_cf(self, data): - """Process a received 'Consecutive Frame' frame""" - if self.rx_state != ISOTP_WAIT_DATA: - return 0 - - if self.rx_timeout_handle is not None: - self.rx_timeout_handle.cancel() - self.rx_timeout_handle = None - - # CFs are never longer than the FF - if len(data) > self.rx_ll_dl: - return 1 - - # CFs have usually the LL_DL length - if len(data) < self.rx_ll_dl: - # this is only allowed for the last CF - if self.rx_len - self.rx_idx > self.rx_ll_dl: - warning("Received a CF with insuffifient length") - return 1 - - if six.indexbytes(data, 0) & 0x0f != self.rx_sn: - # Wrong sequence number - warning("RX state was reset because wrong sequence number was " - "received") - self.rx_state = ISOTP_IDLE - return 1 - - self.rx_sn = (self.rx_sn + 1) % 16 - self.rx_buf += data[1:] - self.rx_idx = len(self.rx_buf) - - if self.rx_idx >= self.rx_len: - # we are done - self.rx_buf = self.rx_buf[0:self.rx_len] - self.rx_state = ISOTP_IDLE - self.rx_queue.put(self.rx_buf) - for cb in self.rx_callbacks: - cb(self.rx_buf) - self.call_release() - self.rx_buf = None - return 0 - - # perform blocksize handling, if enabled - if self.rxfc_bs != 0: - self.rx_bs += 1 - - # check if we reached the end of the block - if self.rx_bs >= self.rxfc_bs and not self.listen_only: - # send our FC frame - load = self.ea_hdr - load += struct.pack("BBB", N_PCI_FC, self.rxfc_bs, - self.rxfc_stmin) - self.can_send(load) - - # wait for another CF - self.rx_timeout_handle = TimeoutScheduler.schedule( - self.cf_timeout, self._rx_timer_handler) - return 0 - - def begin_send(self, x): - """Begins sending an ISOTP message. This method does not block.""" - with self.tx_mutex: - if self.tx_state != ISOTP_IDLE: - raise Scapy_Exception("Socket is already sending, retry later") - - self.tx_done.clear() - self.tx_exception = None - self.tx_state = ISOTP_SENDING - - length = len(x) - if length > ISOTP_MAX_DLEN_2015: - raise Scapy_Exception("Too much data for ISOTP message") - - if len(self.ea_hdr) + length <= 7: - # send a single frame - data = self.ea_hdr - data += struct.pack("B", length) - data += x - self.tx_state = ISOTP_IDLE - self.can_send(data) - self.tx_done.set() - for cb in self.tx_callbacks: - cb() - return - - # send the first frame - data = self.ea_hdr - if length > ISOTP_MAX_DLEN: - data += struct.pack(">HI", 0x1000, length) - else: - data += struct.pack(">H", 0x1000 | length) - load = x[0:8 - len(data)] - data += load - self.can_send(data) - - self.tx_buf = x - self.tx_sn = 1 - self.tx_bs = 0 - self.tx_idx = len(load) - - self.tx_state = ISOTP_WAIT_FIRST_FC - self.tx_timeout_handle = TimeoutScheduler.schedule( - self.fc_timeout, self._tx_timer_handler) - - def send(self, p): - """Send an ISOTP frame and block until the message is sent or an error - happens.""" - with self.send_mutex: - self.begin_send(p) - - # Wait until the tx callback is called - send_done = self.tx_done.wait(30) - if self.tx_exception is not None: - raise Scapy_Exception(self.tx_exception) - if not send_done: - raise Scapy_Exception("ISOTP send not completed in 30s") - return - - def recv(self, timeout=None): - """Receive an ISOTP frame, blocking if none is available in the buffer - for at most 'timeout' seconds.""" - - try: - return self.rx_queue.get(timeout is None or timeout > 0, timeout) - except queue.Empty: - return None - - def check_recv(self): - """Implementation for SelectableObject""" - return not self.rx_queue.empty() - - -if six.PY3 and LINUX: - - from scapy.arch.linux import get_last_packet_timestamp, SIOCGIFINDEX - - """ISOTPNativeSocket definitions:""" - - CAN_ISOTP = 6 # ISO 15765-2 Transport Protocol - - SOL_CAN_BASE = 100 # from can.h - SOL_CAN_ISOTP = SOL_CAN_BASE + CAN_ISOTP - # /* for socket options affecting the socket (not the global system) */ - CAN_ISOTP_OPTS = 1 # /* pass struct can_isotp_options */ - CAN_ISOTP_RECV_FC = 2 # /* pass struct can_isotp_fc_options */ - - # /* sockopts to force stmin timer values for protocol regression tests */ - CAN_ISOTP_TX_STMIN = 3 # /* pass __u32 value in nano secs */ - CAN_ISOTP_RX_STMIN = 4 # /* pass __u32 value in nano secs */ - CAN_ISOTP_LL_OPTS = 5 # /* pass struct can_isotp_ll_options */ - - CAN_ISOTP_LISTEN_MODE = 0x001 # /* listen only (do not send FC) */ - CAN_ISOTP_EXTEND_ADDR = 0x002 # /* enable extended addressing */ - CAN_ISOTP_TX_PADDING = 0x004 # /* enable CAN frame padding tx path */ - CAN_ISOTP_RX_PADDING = 0x008 # /* enable CAN frame padding rx path */ - CAN_ISOTP_CHK_PAD_LEN = 0x010 # /* check received CAN frame padding */ - CAN_ISOTP_CHK_PAD_DATA = 0x020 # /* check received CAN frame padding */ - CAN_ISOTP_HALF_DUPLEX = 0x040 # /* half duplex error state handling */ - CAN_ISOTP_FORCE_TXSTMIN = 0x080 # /* ignore stmin from received FC */ - CAN_ISOTP_FORCE_RXSTMIN = 0x100 # /* ignore CFs depending on rx stmin */ - CAN_ISOTP_RX_EXT_ADDR = 0x200 # /* different rx extended addressing */ - - # /* default values */ - CAN_ISOTP_DEFAULT_FLAGS = 0 - CAN_ISOTP_DEFAULT_EXT_ADDRESS = 0x00 - CAN_ISOTP_DEFAULT_PAD_CONTENT = 0xCC # /* prevent bit-stuffing */ - CAN_ISOTP_DEFAULT_FRAME_TXTIME = 0 - CAN_ISOTP_DEFAULT_RECV_BS = 0 - CAN_ISOTP_DEFAULT_RECV_STMIN = 0x00 - CAN_ISOTP_DEFAULT_RECV_WFTMAX = 0 - CAN_ISOTP_DEFAULT_LL_MTU = CAN_MTU - CAN_ISOTP_DEFAULT_LL_TX_DL = CAN_MAX_DLEN - CAN_ISOTP_DEFAULT_LL_TX_FLAGS = 0 - - class SOCKADDR(ctypes.Structure): - # See /usr/include/i386-linux-gnu/bits/socket.h for original struct - _fields_ = [("sa_family", ctypes.c_uint16), - ("sa_data", ctypes.c_char * 14)] - - class TP(ctypes.Structure): - # This struct is only used within the SOCKADDR_CAN struct - _fields_ = [("rx_id", ctypes.c_uint32), - ("tx_id", ctypes.c_uint32)] - - class ADDR_INFO(ctypes.Union): - # This struct is only used within the SOCKADDR_CAN struct - # This union is to future proof for future can address information - _fields_ = [("tp", TP)] - - class SOCKADDR_CAN(ctypes.Structure): - # See /usr/include/linux/can.h for original struct - _fields_ = [("can_family", ctypes.c_uint16), - ("can_ifindex", ctypes.c_int), - ("can_addr", ADDR_INFO)] - - class IFREQ(ctypes.Structure): - # The two fields in this struct were originally unions. - # See /usr/include/net/if.h for original struct - _fields_ = [("ifr_name", ctypes.c_char * 16), - ("ifr_ifindex", ctypes.c_int)] - - class ISOTPNativeSocket(SuperSocket): - desc = "read/write packets at a given CAN interface using CAN_ISOTP " \ - "socket " - can_isotp_options_fmt = "@2I4B" - can_isotp_fc_options_fmt = "@3B" - can_isotp_ll_options_fmt = "@3B" - sockaddr_can_fmt = "@H3I" - auxdata_available = True - - def __build_can_isotp_options( - self, - flags=CAN_ISOTP_DEFAULT_FLAGS, - frame_txtime=0, - ext_address=CAN_ISOTP_DEFAULT_EXT_ADDRESS, - txpad_content=0, - rxpad_content=0, - rx_ext_address=CAN_ISOTP_DEFAULT_EXT_ADDRESS): - return struct.pack(self.can_isotp_options_fmt, - flags, - frame_txtime, - ext_address, - txpad_content, - rxpad_content, - rx_ext_address) - - # == Must use native not standard types for packing == - # struct can_isotp_options { - # __u32 flags; /* set flags for isotp behaviour. */ - # /* __u32 value : flags see below */ - # - # __u32 frame_txtime; /* frame transmission time (N_As/N_Ar) */ - # /* __u32 value : time in nano secs */ - # - # __u8 ext_address; /* set address for extended addressing */ - # /* __u8 value : extended address */ - # - # __u8 txpad_content; /* set content of padding byte (tx) */ - # /* __u8 value : content on tx path */ - # - # __u8 rxpad_content; /* set content of padding byte (rx) */ - # /* __u8 value : content on rx path */ - # - # __u8 rx_ext_address; /* set address for extended addressing */ - # /* __u8 value : extended address (rx) */ - # }; - - def __build_can_isotp_fc_options(self, - bs=CAN_ISOTP_DEFAULT_RECV_BS, - stmin=CAN_ISOTP_DEFAULT_RECV_STMIN, - wftmax=CAN_ISOTP_DEFAULT_RECV_WFTMAX): - return struct.pack(self.can_isotp_fc_options_fmt, - bs, - stmin, - wftmax) - - # == Must use native not standard types for packing == - # struct can_isotp_fc_options { - # - # __u8 bs; /* blocksize provided in FC frame */ - # /* __u8 value : blocksize. 0 = off */ - # - # __u8 stmin; /* separation time provided in FC frame */ - # /* __u8 value : */ - # /* 0x00 - 0x7F : 0 - 127 ms */ - # /* 0x80 - 0xF0 : reserved */ - # /* 0xF1 - 0xF9 : 100 us - 900 us */ - # /* 0xFA - 0xFF : reserved */ - # - # __u8 wftmax; /* max. number of wait frame transmiss. */ - # /* __u8 value : 0 = omit FC N_PDU WT */ - # }; - - def __build_can_isotp_ll_options(self, - mtu=CAN_ISOTP_DEFAULT_LL_MTU, - tx_dl=CAN_ISOTP_DEFAULT_LL_TX_DL, - tx_flags=CAN_ISOTP_DEFAULT_LL_TX_FLAGS - ): - return struct.pack(self.can_isotp_ll_options_fmt, - mtu, - tx_dl, - tx_flags) - - # == Must use native not standard types for packing == - # struct can_isotp_ll_options { - # - # __u8 mtu; /* generated & accepted CAN frame type */ - # /* __u8 value : */ - # /* CAN_MTU (16) -> standard CAN 2.0 */ - # /* CANFD_MTU (72) -> CAN FD frame */ - # - # __u8 tx_dl; /* tx link layer data length in bytes */ - # /* (configured maximum payload length) */ - # /* __u8 value : 8,12,16,20,24,32,48,64 */ - # /* => rx path supports all LL_DL values */ - # - # __u8 tx_flags; /* set into struct canfd_frame.flags */ - # /* at frame creation: e.g. CANFD_BRS */ - # /* Obsolete when the BRS flag is fixed */ - # /* by the CAN netdriver configuration */ - # }; - - def __get_sock_ifreq(self, sock, iface): - socket_id = ctypes.c_int(sock.fileno()) - ifr = IFREQ() - ifr.ifr_name = iface.encode('ascii') - ret = LIBC.ioctl(socket_id, SIOCGIFINDEX, ctypes.byref(ifr)) - - if ret < 0: - m = u'Failure while getting "{}" interface index.'.format( - iface) - raise Scapy_Exception(m) - return ifr - - def __bind_socket(self, sock, iface, sid, did): - socket_id = ctypes.c_int(sock.fileno()) - ifr = self.__get_sock_ifreq(sock, iface) - - if sid > 0x7ff: - sid = sid | socket.CAN_EFF_FLAG - if did > 0x7ff: - did = did | socket.CAN_EFF_FLAG - - # select the CAN interface and bind the socket to it - addr = SOCKADDR_CAN(ctypes.c_uint16(socket.PF_CAN), - ifr.ifr_ifindex, - ADDR_INFO(TP(ctypes.c_uint32(did), - ctypes.c_uint32(sid)))) - - error = LIBC.bind(socket_id, ctypes.byref(addr), - ctypes.sizeof(addr)) - - if error < 0: - warning("Couldn't bind socket") - - def __set_option_flags(self, sock, extended_addr=None, - extended_rx_addr=None, - listen_only=False, - padding=False, - transmit_time=100): - option_flags = CAN_ISOTP_DEFAULT_FLAGS - if extended_addr is not None: - option_flags = option_flags | CAN_ISOTP_EXTEND_ADDR - else: - extended_addr = CAN_ISOTP_DEFAULT_EXT_ADDRESS - - if extended_rx_addr is not None: - option_flags = option_flags | CAN_ISOTP_RX_EXT_ADDR - else: - extended_rx_addr = CAN_ISOTP_DEFAULT_EXT_ADDRESS - - if listen_only: - option_flags = option_flags | CAN_ISOTP_LISTEN_MODE - - if padding: - option_flags = option_flags | CAN_ISOTP_TX_PADDING \ - | CAN_ISOTP_RX_PADDING - - sock.setsockopt(SOL_CAN_ISOTP, - CAN_ISOTP_OPTS, - self.__build_can_isotp_options( - frame_txtime=transmit_time, - flags=option_flags, - ext_address=extended_addr, - rx_ext_address=extended_rx_addr)) - - def __init__(self, - iface=None, - sid=0, - did=0, - extended_addr=None, - extended_rx_addr=None, - listen_only=False, - padding=False, - transmit_time=100, - basecls=ISOTP): - - if not isinstance(iface, six.string_types): - if hasattr(iface, "ins") and hasattr(iface.ins, "getsockname"): - iface = iface.ins.getsockname() - if isinstance(iface, tuple): - iface = iface[0] - else: - raise Scapy_Exception("Provide a string or a CANSocket " - "object as iface parameter") - - self.iface = iface or conf.contribs['NativeCANSocket']['iface'] - self.can_socket = socket.socket(socket.PF_CAN, socket.SOCK_DGRAM, - CAN_ISOTP) - self.__set_option_flags(self.can_socket, - extended_addr, - extended_rx_addr, - listen_only, - padding, - transmit_time) - - self.src = sid - self.dst = did - self.exsrc = extended_addr - self.exdst = extended_rx_addr - - self.can_socket.setsockopt(SOL_CAN_ISOTP, - CAN_ISOTP_RECV_FC, - self.__build_can_isotp_fc_options()) - self.can_socket.setsockopt(SOL_CAN_ISOTP, - CAN_ISOTP_LL_OPTS, - self.__build_can_isotp_ll_options()) - self.can_socket.setsockopt( - socket.SOL_SOCKET, - SO_TIMESTAMPNS, - 1 - ) - - self.__bind_socket(self.can_socket, self.iface, sid, did) - self.ins = self.can_socket - self.outs = self.can_socket - if basecls is None: - warning('Provide a basecls ') - self.basecls = basecls - - def recv_raw(self, x=0xffff): - """ - Receives a packet, then returns a tuple containing - (cls, pkt_data, time) - """ # noqa: E501 - try: - pkt, _, ts = self._recv_raw(self.ins, x) - except BlockingIOError: # noqa: F821 - warning('Captured no data, socket in non-blocking mode.') - return None - except socket.timeout: - warning('Captured no data, socket read timed out.') - return None - except OSError: - # something bad happened (e.g. the interface went down) - warning("Captured no data.") - return None - - if ts is None: - ts = get_last_packet_timestamp(self.ins) - return self.basecls, pkt, ts - - def recv(self, x=0xffff): - msg = SuperSocket.recv(self, x) - - if hasattr(msg, "src"): - msg.src = self.src - if hasattr(msg, "dst"): - msg.dst = self.dst - if hasattr(msg, "exsrc"): - msg.exsrc = self.exsrc - if hasattr(msg, "exdst"): - msg.exdst = self.exdst - return msg - - __all__.append("ISOTPNativeSocket") - -if USE_CAN_ISOTP_KERNEL_MODULE: - ISOTPSocket = ISOTPNativeSocket - - -# ################################################################### -# #################### ISOTPSCAN #################################### -# ################################################################### -def send_multiple_ext(sock, ext_id, packet, number_of_packets): - """ Send multiple packets with extended addresses at once - - Args: - sock: socket for can interface - ext_id: extended id. First id to send. - packet: packet to send - number_of_packets: number of packets send - - This function is used for scanning with extended addresses. - It sends multiple packets at once. The number of packets - is defined in the number_of_packets variable. - It only iterates the extended ID, NOT the actual ID of the packet. - This method is used in extended scan function. - """ - end_id = min(ext_id + number_of_packets, 255) - for i in range(ext_id, end_id + 1): - packet.extended_address = i - sock.send(packet) - - -def get_isotp_packet(identifier=0x0, extended=False, extended_can_id=False): - """ Craft ISO TP packet - Args: - identifier: identifier of crafted packet - extended: boolean if packet uses extended address - extended_can_id: boolean if CAN should use extended Ids - """ - - if extended: - pkt = ISOTPHeaderEA() / ISOTP_FF() - pkt.extended_address = 0 - pkt.data = b'\x00\x00\x00\x00\x00' - else: - pkt = ISOTPHeader() / ISOTP_FF() - pkt.data = b'\x00\x00\x00\x00\x00\x00' - if extended_can_id: - pkt.flags = "extended" - - pkt.identifier = identifier - pkt.message_size = 100 - return pkt - - -def filter_periodic_packets(packet_dict, verbose=False): - """ Filter for periodic packets - - Args: - packet_dict: Dictionary with Send-to-ID as key and a tuple - (received packet, Recv_ID) - verbose: Displays further information - - ISOTP-Filter for periodic packets (same ID, always same timegap) - Deletes periodic packets in packet_dict - """ - filter_dict = {} - - for key, value in packet_dict.items(): - pkt = value[0] - idn = value[1] - if idn not in filter_dict: - filter_dict[idn] = ([key], [pkt]) - else: - key_lst, pkt_lst = filter_dict[idn] - filter_dict[idn] = (key_lst + [key], pkt_lst + [pkt]) - - for idn in filter_dict: - key_lst = filter_dict[idn][0] - pkt_lst = filter_dict[idn][1] - if len(pkt_lst) < 3: - continue - - tg = [p1.time - p2.time for p1, p2 in zip(pkt_lst[1:], pkt_lst[:-1])] - if all(abs(t1 - t2) < 0.001 for t1, t2 in zip(tg[1:], tg[:-1])): - if verbose: - print("[i] Identifier 0x%03x seems to be periodic. " - "Filtered.") - for k in key_lst: - del packet_dict[k] - - -def get_isotp_fc(id_value, id_list, noise_ids, extended, packet, - verbose=False): - """Callback for sniff function when packet received - - Args: - id_value: packet id of send packet - id_list: list of received IDs - noise_ids: list of packet IDs which will not be considered when - received during scan - extended: boolean if extended scan - packet: received packet - verbose: displays information during scan - - If received packet is a FlowControl - and not in noise_ids - append it to id_list - """ - if packet.flags and packet.flags != "extended": - return - - if noise_ids is not None and packet.identifier in noise_ids: - return - - try: - index = 1 if extended else 0 - isotp_pci = orb(packet.data[index]) >> 4 - isotp_fc = orb(packet.data[index]) & 0x0f - if isotp_pci == 3 and 0 <= isotp_fc <= 2: - if verbose: - print("[+] Found flow-control frame from identifier 0x%03x" - " when testing identifier 0x%03x" % - (packet.identifier, id_value)) - if isinstance(id_list, dict): - id_list[id_value] = (packet, packet.identifier) - elif isinstance(id_list, list): - id_list.append(id_value) - else: - raise TypeError("Unknown type of id_list") - else: - noise_ids.append(packet.identifier) - except Exception as e: - print("[!] Unknown message Exception: %s on packet: %s" % - (e, repr(packet))) - - -def scan(sock, scan_range=range(0x800), noise_ids=None, sniff_time=0.1, - extended_can_id=False, verbose=False): - """Scan and return dictionary of detections - - Args: - sock: socket for can interface - scan_range: hexadecimal range of IDs to scan. - Default is 0x0 - 0x7ff - noise_ids: list of packet IDs which will not be considered when - received during scan - sniff_time: time the scan waits for isotp flow control responses - after sending a first frame - extended_can_id: Send extended can frames - verbose: displays information during scan - - ISOTP-Scan - NO extended IDs - found_packets = Dictionary with Send-to-ID as - key and a tuple (received packet, Recv_ID) - """ - return_values = dict() - for value in scan_range: - sock.sniff(prn=lambda pkt: get_isotp_fc(value, return_values, - noise_ids, False, pkt, - verbose), - timeout=sniff_time, - started_callback=lambda: sock.send( - get_isotp_packet(value, False, extended_can_id))) - - cleaned_ret_val = dict() - - for tested_id in return_values.keys(): - for value in range(max(0, tested_id - 2), tested_id + 2, 1): - sock.sniff(prn=lambda pkt: get_isotp_fc(value, cleaned_ret_val, - noise_ids, False, pkt, - verbose), - timeout=sniff_time * 10, - started_callback=lambda: sock.send( - get_isotp_packet(value, False, extended_can_id))) - - return cleaned_ret_val - - -def scan_extended(sock, scan_range=range(0x800), scan_block_size=32, - extended_scan_range=range(0x100), noise_ids=None, - sniff_time=0.1, extended_can_id=False, verbose=False): - """Scan with ISOTP extended addresses and return dictionary of detections - - Args: - sock: socket for can interface - scan_range: hexadecimal range of IDs to scan. - Default is 0x0 - 0x7ff - scan_block_size: count of packets send at once - extended_scan_range: range to search for extended ISOTP addresses - noise_ids: list of packet IDs which will not be considered when - received during scan - sniff_time: time the scan waits for isotp flow control responses - after sending a first frame - extended_can_id: Send extended can frames - verbose: displays information during scan - - If an answer-packet found -> slow scan with - single packages with extended ID 0 - 255 - found_packets = Dictionary with Send-to-ID - as key and a tuple (received packet, Recv_ID) - """ - - return_values = dict() - scan_block_size = scan_block_size or 1 - - for value in scan_range: - pkt = get_isotp_packet(value, extended=True, - extended_can_id=extended_can_id) - id_list = [] - r = list(extended_scan_range) - for ext_isotp_id in range(r[0], r[-1], scan_block_size): - sock.sniff(prn=lambda p: get_isotp_fc(ext_isotp_id, id_list, - noise_ids, True, p, - verbose), - timeout=sniff_time * 3, - started_callback=lambda: send_multiple_ext( - sock, ext_isotp_id, pkt, scan_block_size)) - # sleep to prevent flooding - time.sleep(sniff_time) - - # remove duplicate IDs - id_list = list(set(id_list)) - for ext_isotp_id in id_list: - for ext_id in range(max(ext_isotp_id - 2, 0), - min(ext_isotp_id + scan_block_size + 2, 256)): - pkt.extended_address = ext_id - full_id = (value << 8) + ext_id - sock.sniff(prn=lambda pkt: get_isotp_fc(full_id, - return_values, - noise_ids, True, - pkt, verbose), - timeout=sniff_time * 2, - started_callback=lambda: sock.send(pkt)) - - return return_values - - -def ISOTPScan(sock, - scan_range=range(0x7ff + 1), - extended_addressing=False, - extended_scan_range=range(0x100), - noise_listen_time=2, - sniff_time=0.1, - output_format=None, - can_interface=None, - extended_can_id=False, - verbose=False): - - """Scan for ISOTP Sockets on a bus and return findings - - Args: - sock: CANSocket object to communicate with the bus under scan - scan_range: hexadecimal range of CAN-Identifiers to scan. - Default is 0x0 - 0x7ff - extended_addressing: scan with ISOTP extended addressing - extended_scan_range: range for ISOTP extended addressing values - noise_listen_time: seconds to listen for default - communication on the bus - sniff_time: time the scan waits for isotp flow control responses - after sending a first frame - output_format: defines the format of the returned - results (text, code or sockets). Provide a string - e.g. "text". Default is "socket". - can_interface: interface used to create the returned code/sockets - extended_can_id: Use Extended CAN-Frames - verbose: displays information during scan - - Scan for ISOTP Sockets in the defined range and returns found sockets - in a specified format. The format can be: - - - text: human readable output - - code: python code for copy&paste - - sockets: if output format is not specified, ISOTPSockets will be - created and returned in a list - """ - - if verbose: - print("Filtering background noise...") - - # Send dummy packet. In most cases, this triggers activity on the bus. - - dummy_pkt = CAN(identifier=0x123, - data=b'\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb') - - background_pkts = sock.sniff(timeout=noise_listen_time, - started_callback=lambda: - sock.send(dummy_pkt)) - - noise_ids = list(set(pkt.identifier for pkt in background_pkts)) - - if extended_addressing: - found_packets = scan_extended(sock, scan_range, - extended_scan_range=extended_scan_range, - noise_ids=noise_ids, - sniff_time=sniff_time, - extended_can_id=extended_can_id, - verbose=verbose) - else: - found_packets = scan(sock, scan_range, - noise_ids=noise_ids, - sniff_time=sniff_time, - extended_can_id=extended_can_id, - verbose=verbose) - - filter_periodic_packets(found_packets, verbose) - - if output_format == "text": - return generate_text_output(found_packets, extended_addressing) - if output_format == "code": - return generate_code_output(found_packets, can_interface, - extended_addressing) - if can_interface is None: - can_interface = sock - - return generate_isotp_list(found_packets, can_interface, - extended_addressing) - - -def generate_text_output(found_packets, extended_addressing=False): - """Generate a human readable output from the result of the `scan` or - the `scan_extended` function. - - Args: - found_packets: result of the `scan` or `scan_extended` function - extended_addressing: print results from a scan with ISOTP - extended addressing - """ - if not found_packets: - return "No packets found." - - text = "\nFound %s ISOTP-FlowControl Packet(s):" % len(found_packets) - for pack in found_packets: - if extended_addressing: - send_id = pack // 256 - send_ext = pack - (send_id * 256) - ext_id = hex(orb(found_packets[pack][0].data[0])) - text += "\nSend to ID: %s" \ - "\nSend to extended ID: %s" \ - "\nReceived ID: %s" \ - "\nReceived extended ID: %s" \ - "\nMessage: %s" % \ - (hex(send_id), hex(send_ext), - hex(found_packets[pack][0].identifier), ext_id, - repr(found_packets[pack][0])) - else: - text += "\nSend to ID: %s" \ - "\nReceived ID: %s" \ - "\nMessage: %s" % \ - (hex(pack), - hex(found_packets[pack][0].identifier), - repr(found_packets[pack][0])) - - padding = found_packets[pack][0].length == 8 - if padding: - text += "\nPadding enabled" - else: - text += "\nNo Padding" - - text += "\n" - return text - - -def generate_code_output(found_packets, can_interface, - extended_addressing=False): - """Generate a copy&past-able output from the result of the `scan` or - the `scan_extended` function. - - Args: - found_packets: result of the `scan` or `scan_extended` function - can_interface: description string for a CAN interface to be - used for the creation of the output. - extended_addressing: print results from a scan with ISOTP - extended addressing - """ - result = "" - if not found_packets: - return result - - header = "\n\nimport can\n" \ - "conf.contribs['CANSocket'] = {'use-python-can': %s}\n" \ - "load_contrib('cansocket')\n" \ - "load_contrib('isotp')\n\n" % PYTHON_CAN - - for pack in found_packets: - if extended_addressing: - send_id = pack // 256 - send_ext = pack - (send_id * 256) - ext_id = orb(found_packets[pack][0].data[0]) - result += "ISOTPSocket(%s, sid=0x%x, did=0x%x, padding=%s, " \ - "extended_addr=0x%x, extended_rx_addr=0x%x, " \ - "basecls=ISOTP)\n" % \ - (can_interface, send_id, - int(found_packets[pack][0].identifier), - found_packets[pack][0].length == 8, - send_ext, - ext_id) - - else: - result += "ISOTPSocket(%s, sid=0x%x, did=0x%x, padding=%s, " \ - "basecls=ISOTP)\n" % \ - (can_interface, pack, - int(found_packets[pack][0].identifier), - found_packets[pack][0].length == 8) - return header + result - - -def generate_isotp_list(found_packets, can_interface, - extended_addressing=False): - """Generate a list of ISOTPSocket objects from the result of the `scan` or - the `scan_extended` function. - - Args: - found_packets: result of the `scan` or `scan_extended` function - can_interface: description string for a CAN interface to be - used for the creation of the output. - extended_addressing: print results from a scan with ISOTP - extended addressing - """ - socket_list = [] - for pack in found_packets: - pkt = found_packets[pack][0] - - dest_id = pkt.identifier - pad = True if pkt.length == 8 else False - - if extended_addressing: - source_id = pack >> 8 - source_ext = int(pack - (source_id * 256)) - dest_ext = orb(pkt.data[0]) - socket_list.append(ISOTPSocket(can_interface, sid=source_id, - extended_addr=source_ext, - did=dest_id, - extended_rx_addr=dest_ext, - padding=pad, - basecls=ISOTP)) - else: - source_id = pack - socket_list.append(ISOTPSocket(can_interface, sid=source_id, - did=dest_id, padding=pad, - basecls=ISOTP)) - return socket_list diff --git a/scapy/contrib/isotp/__init__.py b/scapy/contrib/isotp/__init__.py new file mode 100644 index 00000000000..142b381ed27 --- /dev/null +++ b/scapy/contrib/isotp/__init__.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = ISO-TP (ISO 15765-2) +# scapy.contrib.status = loads + +import logging + +from scapy.consts import LINUX +from scapy.config import conf +from scapy.error import log_loading + +from scapy.contrib.isotp.isotp_packet import ISOTP, ISOTPHeader, \ + ISOTPHeaderEA, ISOTP_SF, ISOTP_FF, ISOTP_CF, ISOTP_FC, \ + ISOTP_FF_FD, ISOTP_SF_FD, ISOTPHeaderEA_FD, ISOTPHeader_FD +from scapy.contrib.isotp.isotp_utils import ISOTPSession, \ + ISOTPMessageBuilder +from scapy.contrib.isotp.isotp_soft_socket import ISOTPSoftSocket +from scapy.contrib.isotp.isotp_scanner import isotp_scan + +__all__ = ["ISOTP", "ISOTPHeader", "ISOTPHeaderEA", "ISOTP_SF", "ISOTP_FF", + "ISOTP_CF", "ISOTP_FC", "ISOTP_FF_FD", "ISOTP_SF_FD", + "ISOTPSoftSocket", "ISOTPSession", "ISOTPHeader_FD", + "ISOTPHeaderEA_FD", + "ISOTPSocket", "ISOTPMessageBuilder", "isotp_scan", + "USE_CAN_ISOTP_KERNEL_MODULE", "log_isotp"] + +USE_CAN_ISOTP_KERNEL_MODULE = False + +log_isotp = logging.getLogger("scapy.contrib.isotp") +log_isotp.setLevel(logging.INFO) + +if LINUX: + try: + if conf.contribs['ISOTP']['use-can-isotp-kernel-module']: + USE_CAN_ISOTP_KERNEL_MODULE = True + except KeyError: + log_loading.info( + "Specify 'conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': True}' " # noqa: E501 + "to enable usage of can-isotp kernel module.") + + from scapy.contrib.isotp.isotp_native_socket import ISOTPNativeSocket + __all__.append("ISOTPNativeSocket") + +if USE_CAN_ISOTP_KERNEL_MODULE: + ISOTPSocket = ISOTPNativeSocket +else: + ISOTPSocket = ISOTPSoftSocket diff --git a/scapy/contrib/isotp/isotp_native_socket.py b/scapy/contrib/isotp/isotp_native_socket.py new file mode 100644 index 00000000000..76f4332ef15 --- /dev/null +++ b/scapy/contrib/isotp/isotp_native_socket.py @@ -0,0 +1,443 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = ISO-TP (ISO 15765-2) Native Socket Library +# scapy.contrib.status = library + +import ctypes +import socket +import struct +from ctypes.util import find_library +# Typing imports +from typing import ( + Any, + Optional, + Union, + Tuple, + Type, + cast, +) + +from scapy.arch.linux import get_last_packet_timestamp, SIOCGIFINDEX +from scapy.config import conf +from scapy.contrib.isotp import log_isotp +from scapy.contrib.isotp.isotp_packet import ISOTP +from scapy.data import SO_TIMESTAMPNS +from scapy.error import Scapy_Exception +from scapy.layers.can import CAN_MTU, CAN_FD_MTU, CAN_MAX_DLEN, CAN_FD_MAX_DLEN +from scapy.packet import Packet +from scapy.supersocket import SuperSocket + +LIBC = ctypes.cdll.LoadLibrary(find_library("c")) # type: ignore + +CAN_ISOTP = 6 # ISO 15765-2 Transport Protocol + +SOL_CAN_BASE = 100 # from can.h +SOL_CAN_ISOTP = SOL_CAN_BASE + CAN_ISOTP +# /* for socket options affecting the socket (not the global system) */ +CAN_ISOTP_OPTS = 1 # /* pass struct can_isotp_options */ +CAN_ISOTP_RECV_FC = 2 # /* pass struct can_isotp_fc_options */ + +# /* sockopts to force stmin timer values for protocol regression tests */ +CAN_ISOTP_TX_STMIN = 3 # /* pass __u32 value in nano secs */ +CAN_ISOTP_RX_STMIN = 4 # /* pass __u32 value in nano secs */ +CAN_ISOTP_LL_OPTS = 5 # /* pass struct can_isotp_ll_options */ + +CAN_ISOTP_LISTEN_MODE = 0x001 # /* listen only (do not send FC) */ +CAN_ISOTP_EXTEND_ADDR = 0x002 # /* enable extended addressing */ +CAN_ISOTP_TX_PADDING = 0x004 # /* enable CAN frame padding tx path */ +CAN_ISOTP_RX_PADDING = 0x008 # /* enable CAN frame padding rx path */ +CAN_ISOTP_CHK_PAD_LEN = 0x010 # /* check received CAN frame padding */ +CAN_ISOTP_CHK_PAD_DATA = 0x020 # /* check received CAN frame padding */ +CAN_ISOTP_HALF_DUPLEX = 0x040 # /* half duplex error state handling */ +CAN_ISOTP_FORCE_TXSTMIN = 0x080 # /* ignore stmin from received FC */ +CAN_ISOTP_FORCE_RXSTMIN = 0x100 # /* ignore CFs depending on rx stmin */ +CAN_ISOTP_RX_EXT_ADDR = 0x200 # /* different rx extended addressing */ + +# /* default values */ +CAN_ISOTP_DEFAULT_FLAGS = 0 +CAN_ISOTP_DEFAULT_EXT_ADDRESS = 0x00 +CAN_ISOTP_DEFAULT_PAD_CONTENT = 0xCC # /* prevent bit-stuffing */ +CAN_ISOTP_DEFAULT_FRAME_TXTIME = 0 +CAN_ISOTP_DEFAULT_RECV_BS = 0 +CAN_ISOTP_DEFAULT_RECV_STMIN = 0x00 +CAN_ISOTP_DEFAULT_RECV_WFTMAX = 0 +CAN_ISOTP_DEFAULT_LL_MTU = CAN_MTU +CAN_ISOTP_CANFD_MTU = CAN_FD_MTU +CAN_ISOTP_DEFAULT_LL_TX_DL = CAN_MAX_DLEN +CAN_FD_ISOTP_DEFAULT_LL_TX_DL = CAN_FD_MAX_DLEN +CAN_ISOTP_DEFAULT_LL_TX_FLAGS = 0 + +CANFD_BRS = 1 # /* CAN FD Bit Rate Switch */ +CANFD_ESI = 2 # /* CAN FD Error State Indicator */ +CANFD_FDF = 4 # /* CAN FD FD Flag */ + + +class tp(ctypes.Structure): + # This struct is only used within the sockaddr_can struct + _fields_ = [("rx_id", ctypes.c_uint32), + ("tx_id", ctypes.c_uint32)] + + +class addr_info(ctypes.Union): + # This struct is only used within the sockaddr_can struct + # This union is to future proof for future can address information + _fields_ = [("tp", tp)] + + +class sockaddr_can(ctypes.Structure): + # See /usr/include/linux/can.h for original struct + _fields_ = [("can_family", ctypes.c_uint16), + ("can_ifindex", ctypes.c_int), + ("can_addr", addr_info)] + + +class ifreq(ctypes.Structure): + # The two fields in this struct were originally unions. + # See /usr/include/net/if.h for original struct + _fields_ = [("ifr_name", ctypes.c_char * 16), + ("ifr_ifindex", ctypes.c_int)] + + +class ISOTPNativeSocket(SuperSocket): + """ + ISOTPSocket using the can-isotp kernel module + + :param iface: a CANSocket instance or an interface name + :param tx_id: the CAN identifier of the sent CAN frames + :param rx_id: the CAN identifier of the received CAN frames + :param ext_address: the extended address of the sent ISOTP frames + :param rx_ext_address: the extended address of the received ISOTP frames + :param bs: block size sent in Flow Control ISOTP frames + :param stmin: minimum desired separation time sent in + Flow Control ISOTP frames + :param padding: If True, pads sending packets with 0x00 which not + count to the payload. + Does not affect receiving packets. + :param listen_only: Does not send Flow Control frames if a First Frame is + received + :param frame_txtime: Separation time between two CAN frames during send + :param basecls: base class of the packets emitted by this socket + """ + desc = "read/write packets at a given CAN interface using CAN_ISOTP socket " # noqa: E501 + can_isotp_options_fmt = "@2I4B" + can_isotp_fc_options_fmt = "@3B" + can_isotp_ll_options_fmt = "@3B" + sockaddr_can_fmt = "@H3I" + auxdata_available = True + + def __build_can_isotp_options( + self, + flags=CAN_ISOTP_DEFAULT_FLAGS, + frame_txtime=CAN_ISOTP_DEFAULT_FRAME_TXTIME, + ext_address=CAN_ISOTP_DEFAULT_EXT_ADDRESS, + txpad_content=CAN_ISOTP_DEFAULT_PAD_CONTENT, + rxpad_content=CAN_ISOTP_DEFAULT_PAD_CONTENT, + rx_ext_address=CAN_ISOTP_DEFAULT_EXT_ADDRESS): + # type: (int, int, int, int, int, int) -> bytes + return struct.pack(self.can_isotp_options_fmt, + flags, + frame_txtime, + ext_address, + txpad_content, + rxpad_content, + rx_ext_address) + + # == Must use native not standard types for packing == + # struct can_isotp_options { + # __u32 flags; /* set flags for isotp behaviour. */ + # /* __u32 value : flags see below */ + # + # __u32 frame_txtime; /* frame transmission time (N_As/N_Ar) */ + # /* __u32 value : time in nano secs */ + # + # __u8 ext_address; /* set address for extended addressing */ + # /* __u8 value : extended address */ + # + # __u8 txpad_content; /* set content of padding byte (tx) */ + # /* __u8 value : content on tx path */ + # + # __u8 rxpad_content; /* set content of padding byte (rx) */ + # /* __u8 value : content on rx path */ + # + # __u8 rx_ext_address; /* set address for extended addressing */ + # /* __u8 value : extended address (rx) */ + # }; + + def __build_can_isotp_fc_options(self, + bs=CAN_ISOTP_DEFAULT_RECV_BS, + stmin=CAN_ISOTP_DEFAULT_RECV_STMIN, + wftmax=CAN_ISOTP_DEFAULT_RECV_WFTMAX): + # type: (int, int, int) -> bytes + return struct.pack(self.can_isotp_fc_options_fmt, + bs, + stmin, + wftmax) + + # == Must use native not standard types for packing == + # struct can_isotp_fc_options { + # + # __u8 bs; /* blocksize provided in FC frame */ + # /* __u8 value : blocksize. 0 = off */ + # + # __u8 stmin; /* separation time provided in FC frame */ + # /* __u8 value : */ + # /* 0x00 - 0x7F : 0 - 127 ms */ + # /* 0x80 - 0xF0 : reserved */ + # /* 0xF1 - 0xF9 : 100 us - 900 us */ + # /* 0xFA - 0xFF : reserved */ + # + # __u8 wftmax; /* max. number of wait frame transmiss. */ + # /* __u8 value : 0 = omit FC N_PDU WT */ + # }; + + def __build_can_isotp_ll_options(self, + mtu=CAN_ISOTP_DEFAULT_LL_MTU, + tx_dl=CAN_ISOTP_DEFAULT_LL_TX_DL, + tx_flags=CAN_ISOTP_DEFAULT_LL_TX_FLAGS + ): + # type: (int, int, int) -> bytes + return struct.pack(self.can_isotp_ll_options_fmt, + mtu, + tx_dl, + tx_flags) + + # == Must use native not standard types for packing == + # struct can_isotp_ll_options { + # + # __u8 mtu; /* generated & accepted CAN frame type */ + # /* __u8 value : */ + # /* CAN_MTU (16) -> standard CAN 2.0 */ + # /* CANFD_MTU (72) -> CAN FD frame */ + # + # __u8 tx_dl; /* tx link layer data length in bytes */ + # /* (configured maximum payload length) */ + # /* __u8 value : 8,12,16,20,24,32,48,64 */ + # /* => rx path supports all LL_DL values */ + # + # __u8 tx_flags; /* set into struct canfd_frame.flags */ + # /* at frame creation: e.g. CANFD_BRS */ + # /* Obsolete when the BRS flag is fixed */ + # /* by the CAN netdriver configuration */ + # }; + + def __get_sock_ifreq(self, sock, iface): + # type: (socket.socket, str) -> ifreq + socket_id = ctypes.c_int(sock.fileno()) + ifr = ifreq() + ifr.ifr_name = iface.encode('ascii') + ret = LIBC.ioctl(socket_id, SIOCGIFINDEX, ctypes.byref(ifr)) + + if ret < 0: + m = u'Failure while getting "{}" interface index.'.format( + iface) + raise Scapy_Exception(m) + return ifr + + def __bind_socket(self, sock, iface, tx_id, rx_id): + # type: (socket.socket, str, int, int) -> None + socket_id = ctypes.c_int(sock.fileno()) + ifr = self.__get_sock_ifreq(sock, iface) + + if tx_id > 0x7ff: + tx_id = tx_id | socket.CAN_EFF_FLAG + if rx_id > 0x7ff: + rx_id = rx_id | socket.CAN_EFF_FLAG + + # select the CAN interface and bind the socket to it + addr = sockaddr_can(ctypes.c_uint16(socket.PF_CAN), + ifr.ifr_ifindex, + addr_info(tp(ctypes.c_uint32(rx_id), + ctypes.c_uint32(tx_id)))) + + error = LIBC.bind(socket_id, ctypes.byref(addr), + ctypes.sizeof(addr)) + + if error < 0: + log_isotp.warning("Couldn't bind socket") + + def __set_option_flags(self, + sock, # type: socket.socket + extended_addr=None, # type: Optional[int] + extended_rx_addr=None, # type: Optional[int] + listen_only=False, # type: bool + padding=False, # type: bool + transmit_time=100 # type: int + ): + # type: (...) -> None + option_flags = CAN_ISOTP_DEFAULT_FLAGS + if extended_addr is not None: + option_flags = option_flags | CAN_ISOTP_EXTEND_ADDR + else: + extended_addr = CAN_ISOTP_DEFAULT_EXT_ADDRESS + + if extended_rx_addr is not None: + option_flags = option_flags | CAN_ISOTP_RX_EXT_ADDR + else: + extended_rx_addr = CAN_ISOTP_DEFAULT_EXT_ADDRESS + + if listen_only: + option_flags = option_flags | CAN_ISOTP_LISTEN_MODE + + if padding: + option_flags = option_flags | CAN_ISOTP_TX_PADDING | CAN_ISOTP_RX_PADDING # noqa: E501 + + sock.setsockopt(SOL_CAN_ISOTP, + CAN_ISOTP_OPTS, + self.__build_can_isotp_options( + frame_txtime=transmit_time, + flags=option_flags, + ext_address=extended_addr, + rx_ext_address=extended_rx_addr)) + + def __init__(self, + iface=None, # type: Optional[Union[str, SuperSocket]] + tx_id=0, # type: int + rx_id=0, # type: int + ext_address=None, # type: Optional[int] + rx_ext_address=None, # type: Optional[int] + bs=CAN_ISOTP_DEFAULT_RECV_BS, # type: int + stmin=CAN_ISOTP_DEFAULT_RECV_STMIN, # type: int + padding=False, # type: bool + listen_only=False, # type: bool + frame_txtime=CAN_ISOTP_DEFAULT_FRAME_TXTIME, # type: int + fd=False, # type: bool + brs=False, # type: bool + basecls=ISOTP # type: Type[Packet] + ): + # type: (...) -> None + + if not isinstance(iface, str): + # This is for interoperability with ISOTPSoftSockets. + # If a NativeCANSocket is provided, the interface name of this + # socket is extracted and an ISOTPNativeSocket will be opened + # on this interface. + iface = cast(SuperSocket, iface) + if hasattr(iface, "ins") and hasattr(iface.ins, "getsockname"): + iface = iface.ins.getsockname() + if isinstance(iface, tuple): + iface = cast(str, iface[0]) + else: + raise Scapy_Exception("Provide a string or a CANSocket " + "object as iface parameter") + + self.iface: str = cast(str, iface) or conf.contribs['NativeCANSocket']['iface'] # noqa: E501 + # store arguments internally + self.tx_id = tx_id + self.rx_id = rx_id + self.ext_address = ext_address + self.rx_ext_address = rx_ext_address + self.bs = bs + self.stmin = stmin + self.padding = padding + self.listen_only = listen_only + self.frame_txtime = frame_txtime + self.fd = fd + self.brs = brs + if basecls is None: + log_isotp.warning('Provide a basecls ') + self.basecls = basecls + self._init_socket() + + def _init_socket(self) -> None: + can_socket = socket.socket( + socket.PF_CAN, socket.SOCK_DGRAM, CAN_ISOTP) + + self.__set_option_flags( + can_socket, + self.ext_address, + self.rx_ext_address, + self.listen_only, + self.padding, + self.frame_txtime) + + can_socket.setsockopt( + SOL_CAN_ISOTP, + CAN_ISOTP_RECV_FC, + self.__build_can_isotp_fc_options(stmin=self.stmin, bs=self.bs)) + + tx_flags = ((CANFD_FDF if self.fd else 0) + + (CANFD_BRS if (self.brs + self.fd) else 0)) + tx_dl = CAN_FD_ISOTP_DEFAULT_LL_TX_DL if self.fd else CAN_ISOTP_DEFAULT_LL_TX_DL + + can_socket.setsockopt( + SOL_CAN_ISOTP, + CAN_ISOTP_LL_OPTS, + self.__build_can_isotp_ll_options( + mtu=CAN_ISOTP_CANFD_MTU if self.fd else CAN_ISOTP_DEFAULT_LL_MTU, + tx_dl=tx_dl, + tx_flags=tx_flags)) + can_socket.setsockopt( + socket.SOL_SOCKET, + SO_TIMESTAMPNS, + 1 + ) + + self.__bind_socket(can_socket, self.iface, self.tx_id, self.rx_id) + # make sure existing sockets are closed, + # required in case of a reconnect. + if getattr(self, "outs", None): + if getattr(self, "ins", None) != self.outs: + if self.outs and self.outs.fileno() != -1: + self.outs.close() + if getattr(self, "ins", None): + if self.ins.fileno() != -1: + self.ins.close() + + self.ins = can_socket + self.outs = can_socket + self.closed = False + + def recv_raw(self, x=0xffff): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + """ + Receives a packet, then returns a tuple containing + (cls, pkt_data, time) + """ + try: + pkt, _, ts = self._recv_raw(self.ins, x) + except BlockingIOError: # noqa: F821 + log_isotp.warning('Captured no data, socket in non-blocking mode.') + return None, None, None + except socket.timeout: + log_isotp.warning('Captured no data, socket read timed out.') + return None, None, None + except OSError as e: + # something bad happened (e.g. the interface went down) + log_isotp.warning("Captured no data. %s" % e) + if e.errno == 84: + log_isotp.warning("Maybe a consecutive frame was missed. " + "Increasing `stmin` could solve this problem.") + elif e.errno == 110: + log_isotp.warning('Captured no data, socket read timed out.') + elif e.errno == 70: + log_isotp.warning( + 'Communication error on send. ' + 'TX path flowcontrol reception timeout.') + else: + log_isotp.error( + 'Unknown error code received %d. Closing socket!', e.errno) + self.close() + return None, None, None + + if pkt and ts is None: + ts = get_last_packet_timestamp(self.ins) + return self.basecls, pkt, ts + + def recv(self, x=0xffff, **kwargs): + # type: (int, **Any) -> Optional[Packet] + msg = SuperSocket.recv(self, x, **kwargs) + if msg is None: + return msg + + if hasattr(msg, "tx_id"): + msg.tx_id = self.tx_id + if hasattr(msg, "rx_id"): + msg.rx_id = self.rx_id + if hasattr(msg, "ext_address"): + msg.ext_address = self.ext_address + if hasattr(msg, "rx_ext_address"): + msg.rx_ext_address = self.rx_ext_address + return msg diff --git a/scapy/contrib/isotp/isotp_packet.py b/scapy/contrib/isotp/isotp_packet.py new file mode 100644 index 00000000000..3da14956dda --- /dev/null +++ b/scapy/contrib/isotp/isotp_packet.py @@ -0,0 +1,433 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + +# scapy.contrib.description = ISO-TP (ISO 15765-2) Packet Definitions +# scapy.contrib.status = library + +import logging +import struct +# Typing imports +from typing import ( + Optional, + List, + Tuple, + Any, + Type, + cast, +) + +from scapy.compat import chb, orb +from scapy.config import conf +from scapy.error import Scapy_Exception +from scapy.fields import BitField, FlagsField, StrLenField, \ + ThreeBytesField, XBitField, ConditionalField, \ + BitEnumField, ByteField, XByteField, BitFieldLenField, StrField, \ + FieldLenField, IntField, ShortField +from scapy.layers.can import CAN, CAN_FD_MAX_DLEN as CAN_FD_MAX_DLEN, CANFD +from scapy.packet import Packet + +log_isotp = logging.getLogger("scapy.contrib.isotp") + +CAN_MAX_IDENTIFIER = (1 << 29) - 1 # Maximum 29-bit identifier +CAN_MTU = 16 +CAN_MAX_DLEN = 8 +ISOTP_MAX_DLEN_2015 = (1 << 32) - 1 # Maximum for 32-bit FF_DL +ISOTP_MAX_DLEN = (1 << 12) - 1 # Maximum for 12-bit FF_DL +ISOTP_TYPES = {0: 'single', + 1: 'first', + 2: 'consecutive', + 3: 'flow_control'} + +N_PCI_SF = 0x00 # /* single frame */ +N_PCI_FF = 0x10 # /* first frame */ +N_PCI_CF = 0x20 # /* consecutive frame */ +N_PCI_FC = 0x30 # /* flow control */ + + +class ISOTP(Packet): + """Packet class for ISOTP messages. This class contains additional + slots for source address (tx_id), destination address (rx_id), + extended source address (ext_address) and + extended destination address (rx_ext_address) information. This information + gets filled from ISOTPSockets or the ISOTPMessageBuilder, if it + is available. Address information is not used for Packet comparison. + + :param args: Arguments for Packet init, for example bytes string + :param kwargs: Keyword arguments for Packet init. + """ + name = 'ISOTP' + fields_desc = [ + StrField('data', b"") + ] + __slots__ = Packet.__slots__ + ["tx_id", "rx_id", "ext_address", "rx_ext_address"] # noqa: E501 + + def __init__(self, *args, **kwargs): + # type: (Any, Any) -> None + self.tx_id = kwargs.pop("tx_id", None) # type: Optional[int] + self.rx_id = kwargs.pop("rx_id", None) # type: Optional[int] + self.ext_address = kwargs.pop("ext_address", None) # type: Optional[int] # noqa: E501 + self.rx_ext_address = kwargs.pop("rx_ext_address", None) # type: Optional[int] # noqa: E501 + Packet.__init__(self, *args, **kwargs) + self.validate_fields() + + def validate_fields(self): + # type: () -> None + """Helper function to validate information in tx_id, rx_id, + ext_address and rx_ext_address slots + """ + if self.tx_id is not None: + if not 0 <= self.tx_id <= CAN_MAX_IDENTIFIER: + raise Scapy_Exception("tx_id is not a valid CAN identifier") + if self.rx_id is not None: + if not 0 <= self.rx_id <= CAN_MAX_IDENTIFIER: + raise Scapy_Exception("rx_id is not a valid CAN identifier") + if self.ext_address is not None: + if not 0 <= self.ext_address <= 0xff: + raise Scapy_Exception("ext_address is not a byte") + if self.rx_ext_address is not None: + if not 0 <= self.rx_ext_address <= 0xff: + raise Scapy_Exception("rx_ext_address is not a byte") + + def fragment(self, *args, **kargs): + # type: (*Any, **Any) -> List[Packet] + """Helper function to fragment an ISOTP message into multiple + CAN frames. + + :param fd: type: Optional[bool]: will fragment the can frames + with size CAN_FD_MAX_DLEN + + :return: A list of CAN frames + """ + + fd = kargs.pop("fd", False) + pkt_cls = CANFD if fd else CAN + + def _get_data_len(): + # type: () -> int + return CAN_MAX_DLEN if not fd else CAN_FD_MAX_DLEN + + data_bytes_in_frame = _get_data_len() - 1 + if self.rx_ext_address is not None: + data_bytes_in_frame = data_bytes_in_frame - 1 + + if len(self.data) > ISOTP_MAX_DLEN_2015: + raise Scapy_Exception("Too much data in ISOTP message") + + if len(self.data) <= data_bytes_in_frame: + # We can do this in a single frame + frame_data = struct.pack('B', len(self.data)) + self.data + if self.rx_ext_address: + frame_data = struct.pack('B', self.rx_ext_address) + frame_data + + if self.rx_id is None or self.rx_id <= 0x7ff: + pkt = pkt_cls(identifier=self.rx_id, data=frame_data) + else: + pkt = pkt_cls(identifier=self.rx_id, flags="extended", + data=frame_data) + return [pkt] + + # Construct the first frame + if len(self.data) <= ISOTP_MAX_DLEN: + frame_header = struct.pack(">H", len(self.data) + 0x1000) + else: + frame_header = struct.pack(">HI", 0x1000, len(self.data)) + if self.rx_ext_address: + frame_header = struct.pack('B', self.rx_ext_address) + frame_header + idx = _get_data_len() - len(frame_header) + frame_data = self.data[0:idx] + if self.rx_id is None or self.rx_id <= 0x7ff: + frame = pkt_cls(identifier=self.rx_id, data=frame_header + frame_data) + else: + frame = pkt_cls(identifier=self.rx_id, flags="extended", + data=frame_header + frame_data) + + # Construct consecutive frames + n = 1 + pkts = [frame] + while idx < len(self.data): + frame_data = self.data[idx:idx + data_bytes_in_frame] + frame_header = struct.pack("b", (n % 16) + N_PCI_CF) + + n += 1 + idx += len(frame_data) + + if self.rx_ext_address: + frame_header = struct.pack('B', self.rx_ext_address) + frame_header # noqa: E501 + if self.rx_id is None or self.rx_id <= 0x7ff: + pkt = pkt_cls(identifier=self.rx_id, data=frame_header + frame_data) # noqa: E501 + else: + pkt = pkt_cls(identifier=self.rx_id, flags="extended", + data=frame_header + frame_data) + pkts.append(pkt) + return cast(List[Packet], pkts) + + @staticmethod + def defragment(can_frames, use_extended_addressing=None): + # type: (List[Packet], Optional[bool]) -> Optional[ISOTP] + """Helper function to defragment a list of CAN frames to one ISOTP + message + + :param can_frames: A list of CAN frames + :param use_extended_addressing: Specify if extended ISO-TP addressing + is used in the packets for + defragmentation. + :return: An ISOTP message containing the data of the CAN frames or None + """ + from scapy.contrib.isotp.isotp_utils import ISOTPMessageBuilder + + if len(can_frames) == 0: + raise Scapy_Exception("ISOTP.defragment called with 0 frames") + + dst = can_frames[0].identifier + if any(frame.identifier != dst for frame in can_frames): + log_isotp.warning("Not all CAN frames have the same identifier") + + parser = ISOTPMessageBuilder(use_extended_addressing) + parser.feed(can_frames) + + results = [] + for p in parser: + if (use_extended_addressing is True and + p.rx_ext_address is not None) \ + or (use_extended_addressing is False and + p.rx_ext_address is None) \ + or (use_extended_addressing is None): + results.append(p) + + if not results: + return None + + if len(results) > 1: + log_isotp.warning( + "More than one ISOTP frame could be defragmented from the " + "provided CAN frames, only returning the first one.") + + return results[0] + + +class ISOTPHeader(CAN): + name = 'ISOTPHeader' + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + ByteField('length', None), + ThreeBytesField('reserved', 0) + ] + + def extract_padding(self, p): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + return p, None + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + """ + This will set the ByteField 'length' to the correct value. + """ + if self.length is None: + pkt = pkt[:4] + chb(len(pay)) + pkt[5:] + + if conf.contribs['CAN']['swap-bytes']: + data = CAN.inv_endianness(pkt) # type: bytes + return data + pay + return pkt + pay + + def guess_payload_class(self, payload): + # type: (bytes) -> Type[Packet] + """ISO-TP encodes the frame type in the first nibble of a frame. This + is used to determine the payload_class + + :param payload: payload bytes string + :return: Type of payload class + """ + if len(payload) < 1: + return self.default_payload_class(payload) + + t = (orb(payload[0]) & 0xf0) >> 4 + if t == 0: + length = (orb(payload[0]) & 0x0f) + if length == 0: + return ISOTP_SF_FD + else: + return ISOTP_SF + elif t == 1: + if len(payload) < 2: + return self.default_payload_class(payload) + length = ((orb(payload[0]) & 0x0f) << 12) + orb(payload[1]) + if length == 0: + return ISOTP_FF_FD + else: + return ISOTP_FF + elif t == 2: + return ISOTP_CF + else: + return ISOTP_FC + + +class ISOTPHeader_FD(ISOTPHeader): + name = 'ISOTPHeaderFD' + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + ByteField('length', None), + FlagsField('fd_flags', 4, 8, ['bit_rate_switch', + 'error_state_indicator', + 'fd_frame']), + ShortField('reserved', 0), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + + data = super().post_build(pkt, pay) + + length = data[4] + + if 8 < length <= 24: + wire_length = length + (-length) % 4 + elif 24 < length <= 64: + wire_length = length + (-length) % 8 + elif length > 64: + raise NotImplementedError + else: + wire_length = length + + pad = b"\x00" * (wire_length - length) + + return data[0:4] + chb(wire_length) + data[5:] + pad + + +class ISOTPHeaderEA(ISOTPHeader): + name = 'ISOTPHeaderExtendedAddress' + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + ByteField('length', None), + ThreeBytesField('reserved', 0), + XByteField('extended_address', 0) + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + """ + This will set the ByteField 'length' to the correct value. + 'chb(len(pay) + 1)' is required, because the field 'extended_address' + is counted as payload on the CAN layer + """ + if self.length is None: + pkt = pkt[:4] + chb(len(pay) + 1) + pkt[5:] + + if conf.contribs['CAN']['swap-bytes']: + data = CAN.inv_endianness(pkt) # type: bytes + return data + pay + return pkt + pay + + +class ISOTPHeaderEA_FD(ISOTPHeaderEA): + name = 'ISOTPHeaderExtendedAddressFD' + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + ByteField('length', None), + FlagsField('fd_flags', 4, 8, ['bit_rate_switch', + 'error_state_indicator', + 'fd_frame']), + ShortField('reserved', 0), + XByteField('extended_address', 0) + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + + data = super().post_build(pkt, pay) + + length = data[4] + + if 8 < length <= 24: + wire_length = length + (-length) % 4 + elif 24 < length <= 64: + wire_length = length + (-length) % 8 + elif length > 64: + raise NotImplementedError + else: + wire_length = length + + pad = b"\x00" * (wire_length - length) + + return data[0:4] + chb(wire_length) + data[5:] + pad + + +ISOTP_TYPE = {0: 'single', + 1: 'first', + 2: 'consecutive', + 3: 'flow_control'} + + +class ISOTP_SF(Packet): + name = 'ISOTPSingleFrame' + fields_desc = [ + BitEnumField('type', 0, 4, ISOTP_TYPE), + BitFieldLenField('message_size', None, 4, length_of='data'), + StrLenField('data', b'', length_from=lambda pkt: pkt.message_size) + ] + + +class ISOTP_SF_FD(Packet): + name = 'ISOTPSingleFrameFD' + fields_desc = [ + BitEnumField('type', 0, 4, ISOTP_TYPE), + BitField('zero_field', 0, 4), + FieldLenField('message_size', None, length_of='data', fmt="B"), + StrLenField('data', b'', length_from=lambda pkt: pkt.message_size) + ] + + +class ISOTP_FF(Packet): + name = 'ISOTPFirstFrame' + fields_desc = [ + BitEnumField('type', 1, 4, ISOTP_TYPE), + BitField('message_size', 0, 12), + ConditionalField(IntField('extended_message_size', 0), + lambda pkt: pkt.message_size == 0), + StrField('data', b'', fmt="B") + ] + + +class ISOTP_FF_FD(Packet): + name = 'ISOTPFirstFrame' + fields_desc = [ + BitEnumField('type', 1, 4, ISOTP_TYPE), + BitField('zero_field', 0, 12), + IntField('message_size', 0), + StrField('data', b'', fmt="B") + ] + + +class ISOTP_CF(Packet): + name = 'ISOTPConsecutiveFrame' + fields_desc = [ + BitEnumField('type', 2, 4, ISOTP_TYPE), + BitField('index', 0, 4), + StrField('data', b'', fmt="B") + ] + + +class ISOTP_FC(Packet): + name = 'ISOTPFlowControlFrame' + fields_desc = [ + BitEnumField('type', 3, 4, ISOTP_TYPE), + BitEnumField('fc_flag', 0, 4, {0: 'continue', + 1: 'wait', + 2: 'abort'}), + ByteField('block_size', 0), + ByteField('separation_time', 0), + ] diff --git a/scapy/contrib/isotp/isotp_scanner.py b/scapy/contrib/isotp/isotp_scanner.py new file mode 100644 index 00000000000..c3d4c5bdfc8 --- /dev/null +++ b/scapy/contrib/isotp/isotp_scanner.py @@ -0,0 +1,593 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss +# Copyright (C) Alexander Schroeder + +# scapy.contrib.description = ISO-TP (ISO 15765-2) Scanner Utility +# scapy.contrib.status = library + +import itertools +import json +import logging +import time +from threading import Event +# Typing imports +from typing import ( + Any, + Dict, + Iterable, + List, + Optional, + Tuple, + Union, Type, +) + +from scapy.compat import orb +from scapy.contrib.cansocket import PYTHON_CAN +from scapy.contrib.isotp import ISOTPHeader_FD +from scapy.contrib.isotp.isotp_packet import ISOTPHeader, ISOTPHeaderEA, \ + ISOTP_FF, ISOTP, ISOTPHeaderEA_FD +from scapy.layers.can import CAN, CANFD +from scapy.packet import Packet +from scapy.supersocket import SuperSocket + +log_isotp = logging.getLogger("scapy.contrib.isotp") + + +def send_multiple_ext(sock, ext_id, packet, number_of_packets): + # type: (SuperSocket, int, Packet, int) -> None + """Send multiple packets with extended addresses at once. + + This function is used for scanning with extended addresses. + It sends multiple packets at once. The number of packets + is defined in the number_of_packets variable. + It only iterates the extended ID, NOT the actual CAN ID of the packet. + This method is used in extended scan function. + + :param sock: CAN interface to send packets + :param ext_id: Extended ISOTP-Address + :param packet: Template Packet + :param number_of_packets: number of packets to send in one batch + """ + end_id = min(ext_id + number_of_packets, 255) + for i in range(ext_id, end_id + 1): + packet.extended_address = i + sock.send(packet) + + +def get_isotp_packet(identifier=0x0, extended=False, extended_can_id=False, fd=False): + # type: (int, bool, bool, bool) -> Packet + """Craft ISO-TP packet + + :param identifier: identifier of crafted packet + :param extended: boolean if packet uses extended address + :param extended_can_id: boolean if CAN should use extended Ids + :param fd: boolean if CANFD packets should be used + :return: Crafted Packet + """ + + if extended: + if fd: + pkt = ISOTPHeaderEA_FD() / ISOTP_FF() # type: Packet + else: + pkt = ISOTPHeaderEA() / ISOTP_FF() + pkt.extended_address = 0 + pkt.data = b'\x00\x00\x00\x00\x00' + else: + if fd: + pkt = ISOTPHeader_FD() / ISOTP_FF() + else: + pkt = ISOTPHeader() / ISOTP_FF() + pkt.data = b'\x00\x00\x00\x00\x00\x00' + if extended_can_id: + pkt.flags = "extended" + + pkt.identifier = identifier + pkt.message_size = 100 + return pkt + + +def filter_periodic_packets(packet_dict): + # type: (Dict[int, Tuple[Packet, int]]) -> None + """Filter to remove periodic packets from packet_dict + + ISOTP-Filter for periodic packets (same ID, always same time-gaps) + Deletes periodic packets in packet_dict + + :param packet_dict: Dictionary, where the filter is applied + """ + filter_dict = {} # type: Dict[int, Tuple[List[int], List[Packet]]] + + for key, value in packet_dict.items(): + pkt = value[0] + idn = value[1] + if idn not in filter_dict: + filter_dict[idn] = ([key], [pkt]) + else: + key_lst, pkt_lst = filter_dict[idn] + filter_dict[idn] = (key_lst + [key], pkt_lst + [pkt]) + + for idn in filter_dict: + key_lst = filter_dict[idn][0] + pkt_lst = filter_dict[idn][1] + if len(pkt_lst) < 3: + continue + + tg = [float(p1.time) - float(p2.time) + for p1, p2 in zip(pkt_lst[1:], pkt_lst[:-1])] + if all(abs(t1 - t2) < 0.001 for t1, t2 in zip(tg[1:], tg[:-1])): + log_isotp.info( + "[i] Identifier 0x%03x seems to be periodic. Filtered.") + for k in key_lst: + del packet_dict[k] + + +def get_isotp_fc( + id_value, # type: int + id_list, # type: Union[List[int], Dict[int, Tuple[Packet, int]]] + noise_ids, # type: Optional[List[int]] + extended, # type: bool + packet, # type: Packet +): + # type: (...) -> None + """Callback for sniff function when packet received + + If received packet is a FlowControl and not in noise_ids append it + to id_list. + + :param id_value: packet id of send packet + :param id_list: list of received IDs + :param noise_ids: list of packet IDs which will not be considered when + received during scan + :param extended: boolean if extended scan + :param packet: received packet + """ + if packet.flags and packet.flags != "extended": + return + + if noise_ids is not None and packet.identifier in noise_ids: + return + + try: + index = 1 if extended else 0 + isotp_pci = orb(packet.data[index]) >> 4 + isotp_fc = orb(packet.data[index]) & 0x0f + if isotp_pci == 3 and 0 <= isotp_fc <= 2: + log_isotp.info("Found flow-control frame from identifier " + "0x%03x when testing identifier 0x%03x", + packet.identifier, id_value) + if isinstance(id_list, dict): + id_list[id_value] = (packet, packet.identifier) + elif isinstance(id_list, list): + id_list.append(id_value) + else: + raise TypeError("Unknown type of id_list") + else: + if noise_ids is not None: + noise_ids.append(packet.identifier) + except Exception as e: + log_isotp.exception( + "Unknown message Exception: %s on packet: %s", + e, repr(packet)) + + +def scan(sock, # type: SuperSocket + scan_range=range(0x800), # type: Iterable[int] + noise_ids=None, # type: Optional[List[int]] + sniff_time=0.1, # type: float + extended_can_id=False, # type: bool + verify_results=True, # type: bool + stop_event=None, # type: Optional[Event] + fd=False # type: bool + ): # type: (...) -> Dict[int, Tuple[Packet, int]] + """Scan and return dictionary of detections + + ISOTP-Scan - NO extended IDs + found_packets = Dictionary with Send-to-ID as + key and a tuple (received packet, Recv_ID) + + :param sock: socket for can interface + :param scan_range: hexadecimal range of IDs to scan. Default is 0x0 - 0x7ff + :param noise_ids: list of packet IDs which will not be tested during scan + :param sniff_time: time the scan waits for isotp flow control responses + after sending a first frame + :param extended_can_id: Send extended can frames + :param verify_results: Verify scan results. This will cause a second scan + of all possible candidates for ISOTP Sockets + :param stop_event: Event object to asynchronously stop the scan + :param fd: Use CANFD packets for scan + :return: Dictionary with all found packets + """ + return_values = dict() # type: Dict[int, Tuple[Packet, int]] + for value in scan_range: + if stop_event is not None and stop_event.is_set(): + break + if noise_ids and value in noise_ids: + continue + sock.send(get_isotp_packet(value, False, extended_can_id, fd)) + sock.sniff(prn=lambda pkt: get_isotp_fc(value, return_values, + noise_ids, False, pkt), + timeout=sniff_time, store=False) + + if not verify_results: + return return_values + + cleaned_ret_val = dict() # type: Dict[int, Tuple[Packet, int]] + retest_ids = list(set( + itertools.chain.from_iterable( + range(max(0, i - 2), i + 2) for i in return_values.keys()))) + for value in retest_ids: + if stop_event is not None and stop_event.is_set(): + break + sock.send(get_isotp_packet(value, False, extended_can_id, fd)) + sock.sniff(prn=lambda pkt: get_isotp_fc(value, cleaned_ret_val, + noise_ids, False, pkt), + timeout=sniff_time * 10, store=False) + + if return_values != cleaned_ret_val: + log_isotp.error("Some ISOTP endpoints detected in first scan didn't " + "answer validation round. Possible bug on target.") + + return cleaned_ret_val + + +def scan_extended(sock, # type: SuperSocket + scan_range=range(0x800), # type: Iterable[int] + scan_block_size=32, # type: int + extended_scan_range=range(0x100), # type: Iterable[int] + noise_ids=None, # type: Optional[List[int]] + sniff_time=0.1, # type: float + extended_can_id=False, # type: bool + stop_event=None, # type: Optional[Event] + fd=False # type: bool + ): # type: (...) -> Dict[int, Tuple[Packet, int]] + """Scan with ISOTP extended addresses and return dictionary of detections + + If an answer-packet found -> slow scan with + single packages with extended ID 0 - 255 + found_packets = Dictionary with Send-to-ID + as key and a tuple (received packet, Recv_ID) + + :param sock: socket for can interface + :param scan_range: hexadecimal range of IDs to scan. Default is 0x0 - 0x7ff + :param scan_block_size: count of packets send at once + :param extended_scan_range: range to search for extended ISOTP addresses + :param noise_ids: list of packet IDs which will not be tested during scan + :param sniff_time: time the scan waits for isotp flow control responses + after sending a first frame + :param extended_can_id: Send extended can frames + :param stop_event: Event object to asynchronously stop the scan + :param fd: Use CANFD packets for scan + :return: Dictionary with all found packets + """ + return_values = dict() # type: Dict[int, Tuple[Packet, int]] + scan_block_size = scan_block_size or 1 + r = list(extended_scan_range) + + for value in scan_range: + if noise_ids and value in noise_ids: + continue + + pkt = get_isotp_packet( + value, extended=True, extended_can_id=extended_can_id, fd=fd) + id_list = [] # type: List[int] + for ext_isotp_id in range(r[0], r[-1], scan_block_size): + if stop_event is not None and stop_event.is_set(): + break + send_multiple_ext(sock, ext_isotp_id, pkt, scan_block_size) + sock.sniff(prn=lambda p: get_isotp_fc(ext_isotp_id, id_list, + noise_ids, True, p), + timeout=sniff_time * 3, store=False) + # sleep to prevent flooding + time.sleep(sniff_time) + + # remove duplicate IDs + id_list = list(set(id_list)) + for ext_isotp_id in id_list: + if stop_event is not None and stop_event.is_set(): + break + for ext_id in range(max(ext_isotp_id - 2, 0), + min(ext_isotp_id + scan_block_size + 2, 256)): + if stop_event is not None and stop_event.is_set(): + break + pkt.extended_address = ext_id + full_id = (value << 8) + ext_id + sock.send(pkt) + sock.sniff(prn=lambda pkt: get_isotp_fc(full_id, + return_values, + noise_ids, True, + pkt), + timeout=sniff_time * 2, store=False) + + return return_values + + +def isotp_scan(sock, # type: SuperSocket + scan_range=range(0x7ff + 1), # type: Iterable[int] + extended_addressing=False, # type: bool + extended_scan_range=range(0x100), # type: Iterable[int] + noise_listen_time=2, # type: int + sniff_time=0.1, # type: float + output_format=None, # type: Optional[str] + can_interface=None, # type: Optional[str] + extended_can_id=False, # type: bool + verify_results=True, # type: bool + verbose=False, # type: bool + stop_event=None, # type: Optional[Event] + fd=False # type: bool + ): + # type: (...) -> Union[str, List[SuperSocket]] + """Scan for ISOTP Sockets on a bus and return findings + + Scan for ISOTP Sockets in the defined range and returns found sockets + in a specified format. The format can be: + + - text: human readable output + - code: python code for copy&paste + - json: json string + - sockets: if output format is not specified, ISOTPSockets will be + created and returned in a list + + :param sock: CANSocket object to communicate with the bus under scan + :param scan_range: range of CAN-Identifiers to scan. Default is 0x0 - 0x7ff + :param extended_addressing: scan with ISOTP extended addressing + :param extended_scan_range: range for ISOTP extended addressing values + :param noise_listen_time: seconds to listen for default communication on + the bus + :param sniff_time: time the scan waits for isotp flow control responses + after sending a first frame + :param output_format: defines the format of the returned results + (text, code or sockets). Provide a string e.g. + "text". Default is "socket". + :param can_interface: interface used to create the returned code/sockets + :param extended_can_id: Use Extended CAN-Frames + :param verify_results: Verify scan results. This will cause a second scan + of all possible candidates for ISOTP Sockets + :param verbose: displays information during scan + :param stop_event: Event object to asynchronously stop the scan + :param fd: Create CANFD frames + :return: + """ + if verbose: + log_isotp.setLevel(logging.DEBUG) + + log_isotp.info("Filtering background noise...") + + # Send dummy packet. In most cases, this triggers activity on the bus. + if fd: + dummy_pkt_cls = CANFD # type: Union[Type[CAN], Type[CANFD]] + else: + dummy_pkt_cls = CAN + + dummy_pkt = dummy_pkt_cls(identifier=0x123, + data=b'\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb') + + background_pkts = sock.sniff( + timeout=noise_listen_time, + started_callback=lambda: sock.send(dummy_pkt)) + + noise_ids = list(set(pkt.identifier for pkt in background_pkts)) + + if extended_addressing: + found_packets = scan_extended(sock, scan_range, + extended_scan_range=extended_scan_range, + noise_ids=noise_ids, + sniff_time=sniff_time, + extended_can_id=extended_can_id, + stop_event=stop_event, + fd=fd) + else: + found_packets = scan(sock, scan_range, + noise_ids=noise_ids, + sniff_time=sniff_time, + extended_can_id=extended_can_id, + verify_results=verify_results, + stop_event=stop_event, + fd=fd) + + filter_periodic_packets(found_packets) + + if output_format == "text": + return generate_text_output(found_packets, extended_addressing, fd) + + if output_format == "code": + return generate_code_output(found_packets, can_interface, + extended_addressing, fd) + + if output_format == "json": + return generate_json_output(found_packets, can_interface, + extended_addressing, fd) + + return generate_isotp_list(found_packets, can_interface or sock, + extended_addressing, fd) + + +def generate_text_output(found_packets, extended_addressing=False, fd=False): + # type: (Dict[int, Tuple[Packet, int]], bool, bool) -> str + """Generate a human readable output from the result of the `scan` or the + `scan_extended` function. + + :param found_packets: result of the `scan` or `scan_extended` function + :param extended_addressing: print results from a scan with + ISOTP extended addressing + :param fd: set CANFD flag in output + :return: human readable scan results + """ + if not found_packets: + return "No packets found." + + text = "\nFound %s ISOTP-FlowControl Packet(s):" % len(found_packets) + for pack in found_packets: + if extended_addressing: + send_id = pack // 256 + send_ext = pack - (send_id * 256) + ext_id = hex(orb(found_packets[pack][0].data[0])) + text += "\nSend to ID: %s" \ + "\nSend to extended ID: %s" \ + "\nReceived ID: %s" \ + "\nReceived extended ID: %s" \ + "\nMessage: %s" % \ + (hex(send_id), hex(send_ext), + hex(found_packets[pack][0].identifier), ext_id, + repr(found_packets[pack][0])) + else: + text += "\nSend to ID: %s" \ + "\nReceived ID: %s" \ + "\nMessage: %s" % \ + (hex(pack), + hex(found_packets[pack][0].identifier), + repr(found_packets[pack][0])) + + padding = found_packets[pack][0].length == 8 + if padding: + text += "\nPadding enabled" + else: + text += "\nNo Padding" + + if fd: + text += "\nCANFD enabled" + + text += "\n" + return text + + +def generate_code_output(found_packets, can_interface="iface", + extended_addressing=False, fd=False): + # type: (Dict[int, Tuple[Packet, int]], Optional[str], bool, bool) -> str + """Generate a copy&past-able output from the result of the `scan` or + the `scan_extended` function. + + :param found_packets: result of the `scan` or `scan_extended` function + :param can_interface: description string for a CAN interface to be + used for the creation of the output. + :param extended_addressing: print results from a scan with ISOTP + extended addressing + :param fd: set CANFD flag in output + :return: Python-code as string to generate all found sockets + """ + result = "" + if not found_packets: + return result + + header = "\n\nimport can\n" \ + "conf.contribs['CANSocket'] = {'use-python-can': %s}\n" \ + "load_contrib('cansocket')\n" \ + "load_contrib('isotp')\n\n" % PYTHON_CAN + + for pack in found_packets: + if extended_addressing: + send_id = pack // 256 + send_ext = pack - (send_id * 256) + ext_id = orb(found_packets[pack][0].data[0]) + result += "ISOTPSocket(%s, tx_id=0x%x, rx_id=0x%x, padding=%s, " \ + "ext_address=0x%x, rx_ext_address=0x%x, fd=%s, " \ + "basecls=ISOTP)\n" % \ + (can_interface, send_id, + int(found_packets[pack][0].identifier), + found_packets[pack][0].length == 8, + send_ext, + ext_id, + fd) + + else: + result += "ISOTPSocket(%s, tx_id=0x%x, rx_id=0x%x, padding=%s, fd=%s, " \ + "basecls=ISOTP)\n" % \ + (can_interface, pack, + int(found_packets[pack][0].identifier), + found_packets[pack][0].length == 8, + fd) + return header + result + + +def generate_json_output(found_packets, # type: Dict[int, Tuple[Packet, int]] + can_interface="iface", # type: Optional[str] + extended_addressing=False, # type: bool + fd=False # type: bool + ): + # type: (...) -> str + """Generate a list of ISOTPSocket objects from the result of the `scan` or + the `scan_extended` function. + + :param found_packets: result of the `scan` or `scan_extended` function + :param can_interface: description string for a CAN interface to be + used for the creation of the output. + :param extended_addressing: print results from a scan with ISOTP + extended addressing + :param fd: set CANFD flag in output + :return: A list of all found ISOTPSockets + """ + socket_list = [] # type: List[Dict[str, Any]] + for pack in found_packets: + pkt = found_packets[pack][0] + + dest_id = pkt.identifier + pad = True if pkt.length == 8 else False + + if extended_addressing: + source_id = pack >> 8 + source_ext = int(pack - (source_id * 256)) + dest_ext = orb(pkt.data[0]) + socket_list.append({"iface": can_interface, + "tx_id": source_id, + "ext_address": source_ext, + "rx_id": dest_id, + "rx_ext_address": dest_ext, + "padding": pad, + "fd": fd, + "basecls": ISOTP.__name__}) + else: + source_id = pack + socket_list.append({"iface": can_interface, + "tx_id": source_id, + "rx_id": dest_id, + "padding": pad, + "fd": fd, + "basecls": ISOTP.__name__}) + return json.dumps(socket_list) + + +def generate_isotp_list(found_packets, # type: Dict[int, Tuple[Packet, int]] + can_interface, # type: Union[SuperSocket, str] + extended_addressing=False, # type: bool + fd=False # type: bool + ): + # type: (...) -> List[SuperSocket] + """Generate a list of ISOTPSocket objects from the result of the `scan` or + the `scan_extended` function. + + :param found_packets: result of the `scan` or `scan_extended` function + :param can_interface: description string for a CAN interface to be + used for the creation of the output. + :param extended_addressing: print results from a scan with ISOTP + extended addressing + :param fd: set CANFD flag in output + :return: A list of all found ISOTPSockets + """ + from scapy.contrib.isotp import ISOTPSocket + + socket_list = [] # type: List[SuperSocket] + for pack in found_packets: + pkt = found_packets[pack][0] + + dest_id = pkt.identifier + pad = True if pkt.length == 8 else False + + if extended_addressing: + source_id = pack >> 8 + source_ext = int(pack - (source_id * 256)) + dest_ext = orb(pkt.data[0]) + socket_list.append(ISOTPSocket(can_interface, tx_id=source_id, + ext_address=source_ext, + rx_id=dest_id, + rx_ext_address=dest_ext, + padding=pad, + fd=fd, + basecls=ISOTP)) + else: + source_id = pack + socket_list.append(ISOTPSocket(can_interface, tx_id=source_id, + rx_id=dest_id, padding=pad, + fd=fd, + basecls=ISOTP)) + return socket_list diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py new file mode 100644 index 00000000000..e6baa1dbbf7 --- /dev/null +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -0,0 +1,1084 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss +# Copyright (C) Enrico Pozzobon + +import heapq +# scapy.contrib.description = ISO-TP (ISO 15765-2) Soft Socket Library +# scapy.contrib.status = library +import logging +import socket +import struct +import time +import traceback +from bisect import bisect_left +from threading import Thread, Event, RLock +# Typing imports +from typing import ( + Optional, + Union, + List, + Tuple, + Any, + Type, + cast, + Callable, + TYPE_CHECKING, +) + +from scapy.automaton import ObjectPipe, select_objects +from scapy.config import conf +from scapy.consts import LINUX +from scapy.contrib.isotp.isotp_packet import ISOTP, CAN_MAX_DLEN, N_PCI_SF, \ + N_PCI_CF, N_PCI_FC, N_PCI_FF, ISOTP_MAX_DLEN, ISOTP_MAX_DLEN_2015, CAN_FD_MAX_DLEN +from scapy.error import Scapy_Exception +from scapy.layers.can import CAN, CANFD +from scapy.packet import Packet +from scapy.supersocket import SuperSocket +from scapy.utils import EDecimal + +if TYPE_CHECKING: + from scapy.contrib.cansocket import CANSocket + +log_isotp = logging.getLogger("scapy.contrib.isotp") + +# Enum states +ISOTP_IDLE = 0 +ISOTP_WAIT_FIRST_FC = 1 +ISOTP_WAIT_FC = 2 +ISOTP_WAIT_DATA = 3 +ISOTP_SENDING = 4 + +# /* Flow Status given in FC frame */ +ISOTP_FC_CTS = 0 # /* clear to send */ +ISOTP_FC_WT = 1 # /* wait */ +ISOTP_FC_OVFLW = 2 # /* overflow */ + + +class ISOTPSoftSocket(SuperSocket): + """ + This class is a wrapper around the ISOTPSocketImplementation, for the + reasons described below. + + The ISOTPSoftSocket aims to be fully compatible with the Linux ISOTP + sockets provided by the can-isotp kernel module, while being usable on any + operating system. + Therefore, this socket needs to be able to respond to an incoming FF frame + with a FC frame even before the recv() method is called. + A thread is needed for receiving CAN frames in the background, and since + the lower layer CAN implementation is not guaranteed to have a functioning + POSIX select(), each ISOTP socket needs its own CAN receiver thread. + SuperSocket automatically calls the close() method when the GC destroys an + ISOTPSoftSocket. However, note that if any thread holds a reference to + an ISOTPSoftSocket object, it will not be collected by the GC. + + The implementation of the ISOTP protocol, along with the necessary + thread, are stored in the ISOTPSocketImplementation class, and therefore: + + * There no reference from ISOTPSocketImplementation to ISOTPSoftSocket + * ISOTPSoftSocket can be normally garbage collected + * Upon destruction, ISOTPSoftSocket.close() will be called + * ISOTPSoftSocket.close() will call ISOTPSocketImplementation.close() + * RX background thread can be stopped by the garbage collector + + Initialize an ISOTPSoftSocket using the provided underlying can socket. + + Example (with NativeCANSocket underneath): + >>> conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} + >>> load_contrib('isotp') + >>> with ISOTPSocket("can0", tx_id=0x641, rx_id=0x241) as sock: + >>> sock.send(...) + + Example (with PythonCANSocket underneath): + >>> conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} + >>> conf.contribs['CANSocket'] = {'use-python-can': True} + >>> load_contrib('isotp') + >>> with ISOTPSocket(CANSocket(bustype='socketcan', channel="can0"), tx_id=0x641, rx_id=0x241) as sock: + >>> sock.send(...) + + :param can_socket: a CANSocket instance, preferably filtering only can + frames with identifier equal to rx_id + :param tx_id: the CAN identifier of the sent CAN frames + :param rx_id: the CAN identifier of the received CAN frames + :param ext_address: the extended address of the sent ISOTP frames + :param rx_ext_address: the extended address of the received ISOTP frames + :param bs: block size sent in Flow Control ISOTP frames + :param stmin: minimum desired separation time sent in + Flow Control ISOTP frames + :param padding: If True, pads sending packets with 0x00 which not + count to the payload. + Does not affect receiving packets. + :param listen_only: Does not send Flow Control frames if a First Frame is + received + :param basecls: base class of the packets emitted by this socket + :param fd: enables the CanFD support for this socket + """ # noqa: E501 + + def __init__(self, + can_socket=None, # type: Optional["CANSocket"] + tx_id=0, # type: int + rx_id=0, # type: int + ext_address=None, # type: Optional[int] + rx_ext_address=None, # type: Optional[int] + bs=0, # type: int + stmin=0, # type: int + padding=False, # type: bool + listen_only=False, # type: bool + basecls=ISOTP, # type: Type[Packet] + fd=False # type: bool + ): + # type: (...) -> None + + if LINUX and isinstance(can_socket, str): + from scapy.contrib.cansocket_native import NativeCANSocket + can_socket = NativeCANSocket(can_socket, fd=fd) + elif isinstance(can_socket, str): + raise Scapy_Exception("Provide a CANSocket object instead") + + self.ext_address = ext_address + self.rx_ext_address = rx_ext_address or ext_address + self.tx_id = tx_id + self.rx_id = rx_id + self.fd = fd + + impl = ISOTPSocketImplementation( + can_socket, + tx_id=self.tx_id, + rx_id=self.rx_id, + padding=padding, + ext_address=self.ext_address, + rx_ext_address=self.rx_ext_address, + bs=bs, + stmin=stmin, + listen_only=listen_only, + fd=fd + ) + + # Cast for compatibility to functions from SuperSocket. + self.ins = cast(socket.socket, impl) + self.outs = cast(socket.socket, impl) + self.impl = impl + self.basecls = basecls + if basecls is None: + log_isotp.warning('Provide a basecls ') + + def close(self): + # type: () -> None + if not self.closed: + if hasattr(self, "impl"): + self.impl.close() + self.closed = True + + def failure_analysis(self): + # type: () -> None + self.impl.failure_analysis() + + def recv_raw(self, x=0xffff): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + """Receive a complete ISOTP message, blocking until a message is + received or the specified timeout is reached. + If self.timeout is 0, then this function doesn't block and returns the + first frame in the receive buffer or None if there isn't any.""" + if not self.closed: + tup = self.impl.recv() + if tup is not None: + return self.basecls, tup[0], float(tup[1]) + return self.basecls, None, None + + def recv(self, x=0xffff, **kwargs): + # type: (int, **Any) -> Optional[Packet] + msg = super(ISOTPSoftSocket, self).recv(x, **kwargs) + if msg is None: + return None + + if hasattr(msg, "tx_id"): + msg.tx_id = self.tx_id + if hasattr(msg, "rx_id"): + msg.rx_id = self.rx_id + if hasattr(msg, "ext_address"): + msg.ext_address = self.ext_address + if hasattr(msg, "rx_ext_address"): + msg.rx_ext_address = self.rx_ext_address + return msg + + @staticmethod + def select(sockets, remain=None): # type: ignore[override] + # type: (List[Union[SuperSocket, ObjectPipe[Any]]], Optional[float]) -> List[Union[SuperSocket, ObjectPipe[Any]]] # noqa: E501 + """This function is called during sendrecv() routine to wait for + sockets to be ready to receive + """ + obj_pipes: List[Union[SuperSocket, ObjectPipe[Tuple[bytes, Union[float, EDecimal]]]]] = [ # noqa: E501 + x.impl.rx_queue for x in sockets if + isinstance(x, ISOTPSoftSocket) and not x.closed] + obj_pipes += [x for x in sockets if isinstance(x, ObjectPipe) and not x.closed] + + ready_pipes = select_objects(obj_pipes, remain) + + result: List[Union[SuperSocket, ObjectPipe[Any]]] = [ + x for x in sockets if isinstance(x, ISOTPSoftSocket) and + not x.closed and x.impl.rx_queue in ready_pipes] + result += [x for x in sockets if isinstance(x, ObjectPipe) and + x in ready_pipes] + return result + + +class TimeoutScheduler: + """A timeout scheduler which uses a single thread for all timeouts, unlike + python's own Timer objects which use a thread each.""" + GRACE = .1 + _mutex = RLock() + _event = Event() + _thread = None # type: Optional[Thread] + + # use heapq functions on _handles! + _handles = [] # type: List[TimeoutScheduler.Handle] + + logger = logging.getLogger("scapy.contrib.automotive.timeout_scheduler") + + @classmethod + def schedule(cls, timeout, callback): + # type: (float, Callable[[], None]) -> TimeoutScheduler.Handle + """Schedules the execution of a timeout. + + The function `callback` will be called in `timeout` seconds. + + Returns a handle that can be used to remove the timeout.""" + when = cls._time() + timeout + handle = cls.Handle(when, callback) + + with cls._mutex: + # Add the handler to the heap, keeping the invariant + # Time complexity is O(log n) + heapq.heappush(cls._handles, handle) + must_interrupt = cls._handles[0] == handle + + # Start the scheduling thread if it is not started already + if cls._thread is None: + t = Thread(target=cls._task, name="TimeoutScheduler._task") + t.daemon = True + must_interrupt = False + cls._thread = t + cls._event.clear() + t.start() + + if must_interrupt: + # if the new timeout got in front of the one we are currently + # waiting on, the current wait operation must be aborted and + # updated with the new timeout + cls._event.set() + time.sleep(0) # call "yield" + + # Return the handle to the timeout so that the user can cancel it + return handle + + @classmethod + def cancel(cls, handle): + # type: (TimeoutScheduler.Handle) -> None + """Provided its handle, cancels the execution of a timeout.""" + + with cls._mutex: + if handle in cls._handles: + # Time complexity is O(n) + handle._cb = None + cls._handles.remove(handle) + heapq.heapify(cls._handles) + + if len(cls._handles) == 0: + # set the event to stop the wait - this kills the thread + cls._event.set() + else: + raise Scapy_Exception("Handle not found") + + @classmethod + def clear(cls): + # type: () -> None + """Cancels the execution of all timeouts.""" + with cls._mutex: + cls._handles = [] + + # set the event to stop the wait - this kills the thread + cls._event.set() + + @classmethod + def _peek_next(cls): + # type: () -> Optional[TimeoutScheduler.Handle] + """Returns the next timeout to execute, or `None` if list is empty, + without modifying the list""" + with cls._mutex: + return cls._handles[0] if cls._handles else None + + @classmethod + def _wait(cls, handle): + # type: (Optional[TimeoutScheduler.Handle]) -> None + """Waits until it is time to execute the provided handle, or until + another thread calls _event.set()""" + + now = cls._time() + + # Check how much time until the next timeout + if handle is None: + to_wait = cls.GRACE + else: + to_wait = handle._when - now + + # Wait until the next timeout, + # or until event.set() gets called in another thread. + if to_wait > 0: + cls.logger.debug("Thread going to sleep @ %f " + + "for %fs", now, to_wait) + interrupted = cls._event.wait(to_wait) + new = cls._time() + cls.logger.debug("Thread awake @ %f, slept for" + + " %f, interrupted=%d", new, new - now, + interrupted) + + # Clear the event so that we can wait on it again, + # Must be done before doing the callbacks to avoid losing a set(). + cls._event.clear() + + @classmethod + def _task(cls): + # type: () -> None + """Executed in a background thread, this thread will automatically + start when the first timeout is added and stop when the last timeout + is removed or executed.""" + + cls.logger.debug("Thread spawning @ %f", cls._time()) + + time_empty = None + + try: + while 1: + handle = cls._peek_next() + if handle is None: + now = cls._time() + if time_empty is None: + time_empty = now + # 100 ms of grace time before killing the thread + if cls.GRACE < now - time_empty: + return + else: + time_empty = None + cls._wait(handle) + cls._poll() + + finally: + # Worst case scenario: if this thread dies, the next scheduled + # timeout will start a new one + cls.logger.debug("Thread died @ %f", cls._time()) + cls._thread = None + + @classmethod + def _poll(cls): + # type: () -> None + """Execute all the callbacks that were due until now""" + + while 1: + with cls._mutex: + now = cls._time() + if len(cls._handles) == 0 or cls._handles[0]._when > now: + # There is nothing to execute yet + return + + # Time complexity is O(log n) + handle = heapq.heappop(cls._handles) + callback = None + if handle is not None: + callback = handle._cb + handle._cb = True + + # Call the callback here, outside the mutex + if callable(callback): + try: + callback() + except Exception: + traceback.print_exc() + + @staticmethod + def _time(): + # type: () -> float + return time.monotonic() + + class Handle: + """Handle for a timeout, consisting of a callback and a time when it + should be executed.""" + __slots__ = ['_when', '_cb'] + + def __init__(self, + when, # type: float + cb # type: Optional[Union[Callable[[], None], bool]] + ): + # type: (...) -> None + self._when = when + self._cb = cb + + def cancel(self): + # type: () -> bool + """Cancels this timeout, preventing it from executing its + callback""" + if self._cb is None: + raise Scapy_Exception( + "cancel() called on previous canceled Handle") + else: + with TimeoutScheduler._mutex: + if isinstance(self._cb, bool): + # Handle was already executed. + # We don't need to cancel anymore + return False + else: + self._cb = None + TimeoutScheduler.cancel(self) + return True + + def __lt__(self, other): + # type: (Any) -> bool + if not isinstance(other, TimeoutScheduler.Handle): + raise TypeError() + return self._when < other._when + + def __le__(self, other): + # type: (Any) -> bool + if not isinstance(other, TimeoutScheduler.Handle): + raise TypeError() + return self._when <= other._when + + def __gt__(self, other): + # type: (Any) -> bool + if not isinstance(other, TimeoutScheduler.Handle): + raise TypeError() + return self._when > other._when + + def __ge__(self, other): + # type: (Any) -> bool + if not isinstance(other, TimeoutScheduler.Handle): + raise TypeError() + return self._when >= other._when + + +class ISOTPSocketImplementation: + """ + Implementation of an ISOTP "state machine". + + Most of the ISOTP logic was taken from + https://github.com/hartkopp/can-isotp/blob/master/net/can/isotp.c + + This class is separated from ISOTPSoftSocket to make sure the background + thread can't hold a reference to ISOTPSoftSocket, allowing it to be + collected by the GC. + + :param can_socket: a CANSocket instance, preferably filtering only can + frames with identifier equal to rx_id + :param tx_id: the CAN identifier of the sent CAN frames + :param rx_id: the CAN identifier of the received CAN frames + :param padding: If True, pads sending packets with 0x00 which not + count to the payload. + Does not affect receiving packets. + :param ext_address: Extended Address byte to be added at the + beginning of every CAN frame _sent_ by this object. Can be None + in order to disable extended addressing on sent frames. + :param rx_ext_address: Extended Address byte expected to be found at + the beginning of every CAN frame _received_ by this object. Can + be None in order to disable extended addressing on received + frames. + :param bs: Block Size byte to be included in every Control + Flow Frame sent by this object. The default value of 0 means + that all the data will be received in a single block. + :param stmin: Time Minimum Separation byte to be + included in every Control Flow Frame sent by this object. The + default value of 0 indicates that the peer will not wait any + time between sending frames. + :param listen_only: Disables send of flow control frames + """ + + def __init__(self, + can_socket, # type: "CANSocket" + tx_id, # type: int + rx_id, # type: int + padding=False, # type: bool + ext_address=None, # type: Optional[int] + rx_ext_address=None, # type: Optional[int] + bs=0, # type: int + stmin=0, # type: int + listen_only=False, # type: bool + fd=False # type: bool + ): + # type: (...) -> None + self.can_socket = can_socket + self.rx_id = rx_id + self.tx_id = tx_id + self.padding = padding + self.fc_timeout = 1 + self.cf_timeout = 1 + + self.fd = fd + + self.max_dlen = CAN_FD_MAX_DLEN if fd else CAN_MAX_DLEN + + self.filter_warning_emitted = False + self.closed = False + + self.rx_ext_address = rx_ext_address + self.ea_hdr = b"" + if ext_address is not None: + self.ea_hdr = struct.pack("B", ext_address) + self.listen_only = listen_only + + self.rxfc_bs = bs + self.rxfc_stmin = stmin + + self.rx_queue = ObjectPipe[Tuple[bytes, Union[float, EDecimal]]]() + self.rx_len = -1 + self.rx_buf = None # type: Optional[bytes] + self.rx_sn = 0 + self.rx_bs = 0 + self.rx_idx = 0 + self.rx_ts = 0.0 # type: Union[float, EDecimal] + self.rx_state = ISOTP_IDLE + + self.tx_queue = ObjectPipe[bytes]() + self.txfc_bs = 0 + self.txfc_stmin = 0 + self.tx_gap = 0. + + self.tx_buf = None # type: Optional[bytes] + self.tx_sn = 0 + self.tx_bs = 0 + self.tx_idx = 0 + self.rx_ll_dl = 0 + self.tx_state = ISOTP_IDLE + + self.rx_tx_poll_rate = 0.005 + self.tx_timeout_handle = None # type: Optional[TimeoutScheduler.Handle] # noqa: E501 + self.rx_timeout_handle = None # type: Optional[TimeoutScheduler.Handle] # noqa: E501 + self.rx_handle = TimeoutScheduler.schedule( + self.rx_tx_poll_rate, self.can_recv) + self.tx_handle = TimeoutScheduler.schedule( + self.rx_tx_poll_rate, self._send) + self.last_rx_call = 0.0 + self.rx_start_time = 0.0 + + def failure_analysis(self): + # type: () -> None + log_isotp.debug("Failure analysis") + log_isotp.debug("Last_rx_call: %s", str(self.last_rx_call)) + log_isotp.debug("self.rx_handle: %s", str(self.rx_handle)) + log_isotp.debug("self.rx_handle._cb: %s", str(self.rx_handle._cb)) + log_isotp.debug("self.rx_handle._when: %s", str(self.rx_handle._when)) + log_isotp.debug("Now: %s", TimeoutScheduler._time()) + + def __del__(self): + # type: () -> None + self.close() + + def can_send(self, load): + # type: (bytes) -> None + def _get_padding_size(pl_size): + # type: (int) -> int + if not self.fd: + return CAN_MAX_DLEN + else: + fd_accepted_sizes = [0, 8, 12, 16, 20, 24, 32, 48, 64] + pos = bisect_left(fd_accepted_sizes, pl_size) + if pos == 0: + return fd_accepted_sizes[0] + if pos == len(fd_accepted_sizes): + return fd_accepted_sizes[-1] + return fd_accepted_sizes[pos] + + pkt_cls = CANFD if self.fd else CAN + + if self.padding: + load += b"\xCC" * (_get_padding_size(len(load)) - len(load)) + if self.tx_id is None or self.tx_id <= 0x7ff: + self.can_socket.send(pkt_cls(identifier=self.tx_id, data=load)) + else: + self.can_socket.send(pkt_cls(identifier=self.tx_id, flags="extended", + data=load)) + + def can_recv(self): + # type: () -> None + self.last_rx_call = TimeoutScheduler._time() + try: + while self.can_socket.select([self.can_socket], 0): + pkt = self.can_socket.recv() + if pkt: + self.on_can_recv(pkt) + else: + break + except Exception: + if not self.closed: + log_isotp.warning("Error in can_recv: %s", + traceback.format_exc()) + if not self.closed and not self.can_socket.closed: + # Determine poll_time from ISOTP state only. + # Avoid calling select() here — on slow serial interfaces + # (slcan), each select() triggers a mux() call that reads + # N frames at ~2.5ms each, wasting time that could be spent + # processing frames already in the rx_queue. + if self.rx_state == ISOTP_WAIT_DATA or \ + self.tx_state == ISOTP_WAIT_FC or \ + self.tx_state == ISOTP_WAIT_FIRST_FC: + poll_time = 0.0 + else: + poll_time = self.rx_tx_poll_rate + self.rx_handle = TimeoutScheduler.schedule( + poll_time, self.can_recv) + else: + try: + self.rx_handle.cancel() + except Scapy_Exception: + pass + + def on_can_recv(self, p): + # type: (Packet) -> None + if p.identifier != self.rx_id: + if not self.filter_warning_emitted and conf.verb >= 2: + log_isotp.warning("You should put a filter for identifier=%x on your " + "CAN socket", self.rx_id) + self.filter_warning_emitted = True + else: + self.on_recv(p) + + def close(self): + # type: () -> None + try: + if select_objects([self.tx_queue], 0): + log_isotp.warning("TX queue not empty") + time.sleep(0.1) + except OSError: + pass + + try: + if select_objects([self.rx_queue], 0): + log_isotp.warning("RX queue not empty") + except OSError: + pass + + self.closed = True + try: + self.rx_handle.cancel() + except Scapy_Exception: + pass + try: + self.tx_handle.cancel() + except Scapy_Exception: + pass + if self.rx_timeout_handle is not None: + try: + self.rx_timeout_handle.cancel() + except Scapy_Exception: + pass + if self.tx_timeout_handle is not None: + try: + self.tx_timeout_handle.cancel() + except Scapy_Exception: + pass + try: + self.rx_queue.close() + except (OSError, EOFError): + pass + try: + self.tx_queue.close() + except (OSError, EOFError): + pass + + def _rx_timer_handler(self): + # type: () -> None + """Method called every time the rx_timer times out, due to the peer not + sending a consecutive frame within the expected time window""" + + if self.closed: + return + + if self.rx_state == ISOTP_WAIT_DATA: + # On slow serial interfaces (slcan), the mux reads frames + # from an OS serial buffer that may contain hundreds of + # background CAN frames. Consecutive Frames from the ECU + # are queued behind this backlog and can take several + # seconds to reach the ISOTP state machine. Extend the + # timeout up to 10 × cf_timeout to give the mux enough + # time to drain the backlog. + total_wait = TimeoutScheduler._time() - self.rx_start_time + if total_wait < self.cf_timeout * 10: + self.rx_timeout_handle = TimeoutScheduler.schedule( + self.cf_timeout, self._rx_timer_handler) + return + # we did not get new data frames in time. + # reset rx state + self.rx_state = ISOTP_IDLE + if conf.verb > 2: + log_isotp.warning("RX state was reset due to timeout") + + def _tx_timer_handler(self): + # type: () -> None + """Method called every time the tx_timer times out, which can happen in + two situations: either a Flow Control frame was not received in time, + or the Separation Time Min is expired and a new frame must be sent.""" + + if self.closed: + return + + if (self.tx_state == ISOTP_WAIT_FC or + self.tx_state == ISOTP_WAIT_FIRST_FC): + # we did not get any flow control frame in time + # reset tx state + self.tx_state = ISOTP_IDLE + log_isotp.warning("TX state was reset due to timeout") + return + elif self.tx_state == ISOTP_SENDING: + # push out the next segmented pdu + src_off = len(self.ea_hdr) + max_bytes = (self.max_dlen - 1) - src_off + if self.tx_buf is None: + self.tx_state = ISOTP_IDLE + log_isotp.warning("TX buffer is not filled") + return + while 1: + load = self.ea_hdr + load += struct.pack("B", N_PCI_CF + self.tx_sn) + load += self.tx_buf[self.tx_idx:self.tx_idx + max_bytes] + self.can_send(load) + + self.tx_sn = (self.tx_sn + 1) % 16 + self.tx_bs += 1 + self.tx_idx += max_bytes + + if len(self.tx_buf) <= self.tx_idx: + # we are done + self.tx_state = ISOTP_IDLE + return + + if self.txfc_bs != 0 and self.tx_bs >= self.txfc_bs: + # stop and wait for FC + self.tx_state = ISOTP_WAIT_FC + self.tx_timeout_handle = TimeoutScheduler.schedule( + self.fc_timeout, self._tx_timer_handler) + return + + if self.tx_gap == 0: + continue + else: + # stop and wait for tx gap + self.tx_timeout_handle = TimeoutScheduler.schedule( + self.tx_gap, self._tx_timer_handler) + return + + def on_recv(self, cf): + # type: (Packet) -> None + """Function that must be called every time a CAN frame is received, to + advance the state machine.""" + + data = bytes(cf.data) + + if len(data) < 2: + return + + ae = 0 + if self.rx_ext_address is not None: + ae = 1 + if len(data) < 3: + return + if data[0] != self.rx_ext_address: + return + + n_pci = data[ae] & 0xf0 + + if n_pci == N_PCI_FC: + self._recv_fc(data[ae:]) + elif n_pci == N_PCI_SF: + self._recv_sf(data[ae:], cf.time) + elif n_pci == N_PCI_FF: + self._recv_ff(data[ae:], cf.time) + elif n_pci == N_PCI_CF: + self._recv_cf(data[ae:]) + + def _recv_fc(self, data): + # type: (bytes) -> None + """Process a received 'Flow Control' frame""" + log_isotp.debug("Processing FC") + + if (self.tx_state != ISOTP_WAIT_FC and + self.tx_state != ISOTP_WAIT_FIRST_FC): + return + + if self.tx_timeout_handle is not None: + self.tx_timeout_handle.cancel() + self.tx_timeout_handle = None + + if len(data) < 3: + self.tx_state = ISOTP_IDLE + log_isotp.warning("CF frame discarded because it was too short") + return + + # get communication parameters only from the first FC frame + if self.tx_state == ISOTP_WAIT_FIRST_FC: + self.txfc_bs = data[1] + self.txfc_stmin = data[2] + + if ((self.txfc_stmin > 0x7F) and + ((self.txfc_stmin < 0xF1) or (self.txfc_stmin > 0xF9))): + self.txfc_stmin = 0x7F + + if data[2] <= 127: + self.tx_gap = data[2] / 1000 + elif 0xf1 <= data[2] <= 0xf9: + self.tx_gap = (data[2] & 0x0f) / 10000 + else: + self.tx_gap = 0. + + self.tx_state = ISOTP_WAIT_FC + + isotp_fc = data[0] & 0x0f + + if isotp_fc == ISOTP_FC_CTS: + self.tx_bs = 0 + self.tx_state = ISOTP_SENDING + # start cyclic timer for sending CF frame + self.tx_timeout_handle = TimeoutScheduler.schedule( + self.tx_gap, self._tx_timer_handler) + elif isotp_fc == ISOTP_FC_WT: + # start timer to wait for next FC frame + self.tx_state = ISOTP_WAIT_FC + self.tx_timeout_handle = TimeoutScheduler.schedule( + self.fc_timeout, self._tx_timer_handler) + elif isotp_fc == ISOTP_FC_OVFLW: + # overflow in receiver side + self.tx_state = ISOTP_IDLE + log_isotp.warning("Overflow happened at the receiver side") + return + else: + self.tx_state = ISOTP_IDLE + log_isotp.warning("Unknown FC frame type") + return + + def _recv_sf(self, data, ts): + # type: (bytes, Union[float, EDecimal]) -> None + """Process a received 'Single Frame' frame""" + log_isotp.debug("Processing SF") + + if self.rx_timeout_handle is not None: + self.rx_timeout_handle.cancel() + self.rx_timeout_handle = None + + if self.rx_state != ISOTP_IDLE: + if conf.verb > 2: + log_isotp.warning("RX state was reset because " + "single frame was received") + self.rx_state = ISOTP_IDLE + + length = data[0] & 0xf + is_fd_frame = self.fd and length == 0 and len(data) >= 2 + + if is_fd_frame: + length = data[1] + + if len(data) - 1 < length: + return + + msg = None + if is_fd_frame: + msg = data[2:2 + length] + else: + msg = data[1:1 + length] + self.rx_queue.send((msg, ts)) + + def _recv_ff(self, data, ts): + # type: (bytes, Union[float, EDecimal]) -> None + """Process a received 'First Frame' frame""" + log_isotp.debug("Processing FF") + + if self.rx_timeout_handle is not None: + self.rx_timeout_handle.cancel() + self.rx_timeout_handle = None + + if self.rx_state != ISOTP_IDLE: + if conf.verb > 2: + log_isotp.warning("RX state was reset because first frame was received") + self.rx_state = ISOTP_IDLE + + if len(data) < 7: + return + self.rx_ll_dl = len(data) + + # get the FF_DL + self.rx_len = (data[0] & 0x0f) * 256 + data[1] + ff_pci_sz = 2 + + # Check for FF_DL escape sequence supporting 32 bit PDU length + if self.rx_len == 0: + # FF_DL = 0 => get real length from next 4 bytes + self.rx_len = data[2] << 24 + self.rx_len += data[3] << 16 + self.rx_len += data[4] << 8 + self.rx_len += data[5] + ff_pci_sz = 6 + + # copy the first received data bytes + data_bytes = data[ff_pci_sz:] + self.rx_idx = len(data_bytes) + self.rx_buf = data_bytes + self.rx_ts = ts + + # initial setup for this pdu reception + self.rx_sn = 1 + self.rx_state = ISOTP_WAIT_DATA + self.rx_start_time = TimeoutScheduler._time() + + # no creation of flow control frames + if not self.listen_only: + # send our first FC frame + load = self.ea_hdr + load += struct.pack("BBB", N_PCI_FC, self.rxfc_bs, self.rxfc_stmin) + self.can_send(load) + + # wait for a CF + self.rx_bs = 0 + self.rx_timeout_handle = TimeoutScheduler.schedule( + self.cf_timeout, self._rx_timer_handler) + + def _recv_cf(self, data): + # type: (bytes) -> None + """Process a received 'Consecutive Frame' frame""" + log_isotp.debug("Processing CF") + + if self.rx_state != ISOTP_WAIT_DATA: + return + + if self.rx_timeout_handle is not None: + self.rx_timeout_handle.cancel() + self.rx_timeout_handle = None + + # CFs are never longer than the FF + if len(data) > self.rx_ll_dl: + return + + # CFs have usually the LL_DL length + if len(data) < self.rx_ll_dl: + # this is only allowed for the last CF + if self.rx_len - self.rx_idx > self.rx_ll_dl: + if conf.verb > 2: + log_isotp.warning("Received a CF with insufficient length") + return + + if data[0] & 0x0f != self.rx_sn: + # Wrong sequence number + if conf.verb > 2: + log_isotp.warning("RX state was reset because wrong sequence " + "number was received") + self.rx_state = ISOTP_IDLE + return + + if self.rx_buf is None: + if conf.verb > 2: + log_isotp.warning("rx_buf not filled with data!") + self.rx_state = ISOTP_IDLE + return + + self.rx_sn = (self.rx_sn + 1) % 16 + self.rx_buf += data[1:] + self.rx_idx = len(self.rx_buf) + + if self.rx_idx >= self.rx_len: + # we are done + self.rx_buf = self.rx_buf[0:self.rx_len] + self.rx_state = ISOTP_IDLE + self.rx_queue.send((self.rx_buf, self.rx_ts)) + self.rx_buf = None + return + + # perform blocksize handling, if enabled + if self.rxfc_bs != 0: + self.rx_bs += 1 + + # check if we reached the end of the block + if self.rx_bs >= self.rxfc_bs and not self.listen_only: + # send our FC frame + load = self.ea_hdr + load += struct.pack("BBB", N_PCI_FC, self.rxfc_bs, + self.rxfc_stmin) + self.rx_bs = 0 + self.can_send(load) + + # wait for another CF + log_isotp.debug("Wait for another CF") + self.rx_timeout_handle = TimeoutScheduler.schedule( + self.cf_timeout, self._rx_timer_handler) + + def begin_send(self, x): + # type: (bytes) -> None + """Begins sending an ISOTP message. This method does not block.""" + if self.tx_state != ISOTP_IDLE: + log_isotp.warning("Socket is already sending, retry later") + return + + self.tx_state = ISOTP_SENDING + length = len(x) + if length > ISOTP_MAX_DLEN_2015: + log_isotp.warning("Too much data for ISOTP message") + + sf_size_check = self.max_dlen - 1 + + if len(self.ea_hdr) + length + int(self.fd) <= sf_size_check: + # send a single frame + data = self.ea_hdr + if not self.fd or length <= 7: + data += struct.pack("B", length) + else: + data += struct.pack("BB", 0, length) + data += x + self.tx_state = ISOTP_IDLE + self.can_send(data) + return + + # send the first frame + data = self.ea_hdr + if length > ISOTP_MAX_DLEN: + data += struct.pack(">HI", 0x1000, length) + else: + data += struct.pack(">H", 0x1000 | length) + load = x[0:self.max_dlen - len(data)] + data += load + self.can_send(data) + + self.tx_buf = x + self.tx_sn = 1 + self.tx_bs = 0 + self.tx_idx = len(load) + + self.tx_state = ISOTP_WAIT_FIRST_FC + self.tx_timeout_handle = TimeoutScheduler.schedule( + self.fc_timeout, self._tx_timer_handler) + + def _send(self): + # type: () -> None + try: + if self.tx_state == ISOTP_IDLE: + if select_objects([self.tx_queue], 0): + pkt = self.tx_queue.recv() + if pkt: + self.begin_send(pkt) + except Exception: + if not self.closed: + log_isotp.warning("Error in _send: %s", + traceback.format_exc()) + + if not self.closed: + self.tx_handle = TimeoutScheduler.schedule( + self.rx_tx_poll_rate, self._send) + else: + try: + self.tx_handle.cancel() + except Scapy_Exception: + pass + + def send(self, p): + # type: (bytes) -> None + """Send an ISOTP frame and block until the message is sent or an error + happens.""" + self.tx_queue.send(p) + + def recv(self, timeout=None): + # type: (Optional[int]) -> Optional[Tuple[bytes, Union[float, EDecimal]]] # noqa: E501 + """Receive an ISOTP frame, blocking if none is available in the buffer.""" + return self.rx_queue.recv() diff --git a/scapy/contrib/isotp/isotp_utils.py b/scapy/contrib/isotp/isotp_utils.py new file mode 100644 index 00000000000..da1d5e73ab9 --- /dev/null +++ b/scapy/contrib/isotp/isotp_utils.py @@ -0,0 +1,361 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss +# Copyright (C) Enrico Pozzobon +# Copyright (C) Alexander Schroeder + +# scapy.contrib.description = ISO-TP (ISO 15765-2) Utilities +# scapy.contrib.status = library + +import struct + +from scapy.config import conf +from scapy.utils import EDecimal +from scapy.packet import Packet +from scapy.sessions import DefaultSession +from scapy.supersocket import SuperSocket +from scapy.contrib.isotp.isotp_packet import ISOTP, N_PCI_CF, N_PCI_SF, \ + N_PCI_FF, N_PCI_FC + +# Typing imports +from typing import ( + cast, + Iterable, + Iterator, + Optional, + Union, + List, + Tuple, + Dict, + Any, + Type, +) + + +class ISOTPMessageBuilderIter(object): + """ + Iterator class for ISOTPMessageBuilder + """ + slots = ["builder"] + + def __init__(self, builder): + # type: (ISOTPMessageBuilder) -> None + self.builder = builder + + def __iter__(self): + # type: () -> ISOTPMessageBuilderIter + return self + + def __next__(self): + # type: () -> ISOTP + while self.builder.count: + p = self.builder.pop() + if p is None: + break + else: + return p + raise StopIteration + + next = __next__ + + +class ISOTPMessageBuilder(object): + """ + Initialize a ISOTPMessageBuilder object + + Utility class to build ISOTP messages out of CAN frames, used by both + ISOTP.defragment() and ISOTPSession. + + This class attempts to interpret some CAN frames as ISOTP frames, both with + and without extended addressing at the same time. For example, if an + extended address of 07 is being used, all frames will also be interpreted + as ISOTP single-frame messages. + + CAN frames are fed to an ISOTPMessageBuilder object with the feed() method + and the resulting ISOTP frames can be extracted using the pop() method. + + :param use_ext_address: True for only attempting to defragment with + extended addressing, False for only attempting + to defragment without extended addressing, + or None for both + :param rx_id: Destination Identifier + :param basecls: The class of packets that will be returned, + defaults to ISOTP + """ + + class Bucket(object): + """ + Helper class to store not finished ISOTP messages while building. + """ + + def __init__(self, total_len, first_piece, ts): + # type: (int, bytes, Union[EDecimal, float]) -> None + self.pieces = list() # type: List[bytes] + self.total_len = total_len + self.current_len = 0 + self.ready = None # type: Optional[bytes] + self.tx_id = None # type: Optional[int] + self.ext_address = None # type: Optional[int] + self.time = ts # type: Union[float, EDecimal] + self.push(first_piece) + + def push(self, piece): + # type: (bytes) -> None + self.pieces.append(piece) + self.current_len += len(piece) + if self.current_len >= self.total_len: + isotp_data = b"".join(self.pieces) + self.ready = isotp_data[:self.total_len] + + def __init__( + self, + use_ext_address=None, # type: Optional[bool] + rx_id=None, # type: Optional[Union[int, List[int], Iterable[int]]] + basecls=ISOTP # type: Type[ISOTP] + ): + # type: (...) -> None + self.ready = [] # type: List[Tuple[int, Optional[int], ISOTPMessageBuilder.Bucket]] # noqa: E501 + self.buckets = {} # type: Dict[Tuple[Optional[int], int, int], ISOTPMessageBuilder.Bucket] # noqa: E501 + self.use_ext_addr = use_ext_address + self.basecls = basecls + self.rx_ids = None # type: Optional[Iterable[int]] + self.last_ff = None # type: Optional[Tuple[Optional[int], int, int]] + self.last_ff_ex = None # type: Optional[Tuple[Optional[int], int, int]] # noqa: E501 + if rx_id is not None: + if isinstance(rx_id, list): + self.rx_ids = rx_id + elif isinstance(rx_id, int): + self.rx_ids = [rx_id] + elif hasattr(rx_id, "__iter__"): + self.rx_ids = rx_id + else: + raise TypeError("Invalid type for argument rx_id!") + + def feed(self, can): + # type: (Union[Iterable[Packet], Packet]) -> None + """Attempt to feed an incoming CAN frame into the state machine""" + if not isinstance(can, Packet) and hasattr(can, "__iter__"): + for p in can: + self.feed(p) + return + + if not isinstance(can, Packet): + return + + if self.rx_ids is not None and can.identifier not in self.rx_ids: + return + + data = bytes(can.data) + + if len(data) > 1 and self.use_ext_addr is not True: + self._try_feed(can.identifier, None, data, can.time) + if len(data) > 2 and self.use_ext_addr is not False: + ea = data[0] + self._try_feed(can.identifier, ea, data[1:], can.time) + + @property + def count(self): + # type: () -> int + """Returns the number of ready ISOTP messages built from the provided + can frames + + :return: Number of ready ISOTP messages + """ + return len(self.ready) + + def __len__(self): + # type: () -> int + return self.count + + def pop(self, identifier=None, ext_addr=None): + # type: (Optional[int], Optional[int]) -> Optional[ISOTP] + """Returns a built ISOTP message + + :param identifier: if not None, only return isotp messages with this + destination + :param ext_addr: if identifier is not None, only return isotp messages + with this extended address for destination + :returns: an ISOTP packet, or None if no message is ready + """ + + if identifier is not None: + for i in range(len(self.ready)): + b = self.ready[i] + iden = b[0] + ea = b[1] + if iden == identifier and ext_addr == ea: + return ISOTPMessageBuilder._build(self.ready.pop(i), + self.basecls) + return None + + if len(self.ready) > 0: + return ISOTPMessageBuilder._build(self.ready.pop(0), self.basecls) + return None + + def __iter__(self): + # type: () -> ISOTPMessageBuilderIter + return ISOTPMessageBuilderIter(self) + + @staticmethod + def _build( + t, # type: Tuple[int, Optional[int], ISOTPMessageBuilder.Bucket] + basecls=ISOTP # type: Type[ISOTP] + ): + # type: (...) -> ISOTP + bucket = t[2] + data = bucket.ready or b"" + try: + p = basecls(data) + except Exception: + if conf.debug_dissector: + from scapy.sendrecv import debug + debug.crashed_on = (basecls, data) + raise + if hasattr(p, "rx_id"): + p.rx_id = t[0] + if hasattr(p, "rx_ext_address"): + p.rx_ext_address = t[1] + if hasattr(p, "tx_id"): + p.tx_id = bucket.tx_id + if hasattr(p, "ext_address"): + p.ext_address = bucket.ext_address + if hasattr(p, "time"): + p.time = bucket.time + return p + + def _feed_first_frame(self, identifier, ea, data, ts): + # type: (int, Optional[int], bytes, Union[EDecimal, float]) -> bool + if len(data) < 3: + # At least 3 bytes are necessary: 2 for length and 1 for data + return False + + header = struct.unpack('>H', bytes(data[:2]))[0] + expected_length = header & 0x0fff + isotp_data = data[2:] + if expected_length == 0 and len(data) >= 6: + expected_length = struct.unpack('>I', bytes(data[2:6]))[0] + isotp_data = data[6:] + + key = (ea, identifier, 1) + if ea is None: + self.last_ff = key + else: + self.last_ff_ex = key + self.buckets[key] = self.Bucket(expected_length, isotp_data, ts) + return True + + def _feed_single_frame(self, identifier, ea, data, ts): + # type: (int, Optional[int], bytes, Union[EDecimal, float]) -> bool + if len(data) < 2: + # At least 2 bytes are necessary: 1 for length and 1 for data + return False + + length = data[0] & 0x0f + isotp_data = data[1:length + 1] + + if length > len(isotp_data): + # CAN frame has less data than expected + return False + + self.ready.append((identifier, ea, + self.Bucket(length, isotp_data, ts))) + return True + + def _feed_consecutive_frame(self, identifier, ea, data): + # type: (int, Optional[int], bytes) -> bool + if len(data) < 2: + # At least 2 bytes are necessary: 1 for sequence number and + # 1 for data + return False + + first_byte = data[0] + seq_no = first_byte & 0x0f + isotp_data = data[1:] + + key = (ea, identifier, seq_no) + bucket = self.buckets.pop(key, None) + + if bucket is None: + # There is no message constructor waiting for this frame + return False + + bucket.push(isotp_data) + if bucket.ready is None: + # full ISOTP message is not ready yet, put it back in + # buckets list + next_seq = (seq_no + 1) % 16 + key = (ea, identifier, next_seq) + self.buckets[key] = bucket + else: + self.ready.append((identifier, ea, bucket)) + + return True + + def _feed_flow_control_frame(self, identifier, ea, data): + # type: (int, Optional[int], bytes) -> bool + if len(data) < 3: + # At least 2 bytes are necessary: 1 for sequence number and + # 1 for data + return False + + keys = [x for x in (self.last_ff, self.last_ff_ex) if x is not None] + buckets = [self.buckets.pop(k, None) for k in keys] + + self.last_ff = None + self.last_ff_ex = None + + if not any(buckets) or not any(keys): + # There is no message constructor waiting for this frame + return False + + for key, bucket in zip(keys, buckets): + if bucket is None: + continue + bucket.tx_id = identifier + bucket.ext_address = ea + self.buckets[key] = bucket + return True + + def _try_feed(self, identifier, ea, data, ts): + # type: (int, Optional[int], bytes, Union[EDecimal, float]) -> None + first_byte = data[0] + if len(data) > 1 and first_byte & 0xf0 == N_PCI_SF: + self._feed_single_frame(identifier, ea, data, ts) + if len(data) > 2 and first_byte & 0xf0 == N_PCI_FF: + self._feed_first_frame(identifier, ea, data, ts) + if len(data) > 1 and first_byte & 0xf0 == N_PCI_CF: + self._feed_consecutive_frame(identifier, ea, data) + if len(data) > 1 and first_byte & 0xf0 == N_PCI_FC: + self._feed_flow_control_frame(identifier, ea, data) + + +class ISOTPSession(DefaultSession): + """Defragment ISOTP packets 'on-the-flow'. + + Usage: + >>> sniff(session=ISOTPSession) + """ + + def __init__(self, *args, **kwargs): + # type: (Any, Any) -> None + self.m = ISOTPMessageBuilder( + use_ext_address=kwargs.pop("use_ext_address", None), + rx_id=kwargs.pop("rx_id", None), + basecls=kwargs.pop("basecls", ISOTP)) + super(ISOTPSession, self).__init__(*args, **kwargs) + + def recv(self, sock: SuperSocket) -> Iterator[Packet]: + """ + Will be called by sniff() to ask for a packet + """ + pkt = sock.recv() + if not pkt: + return + self.m.feed(pkt) + while len(self.m) > 0: + rcvd = cast(Optional[Packet], self.m.pop()) + if rcvd: + rcvd = self.process(rcvd) + if rcvd: + yield rcvd diff --git a/scapy/contrib/knx.py b/scapy/contrib/knx.py new file mode 100644 index 00000000000..5193db01ecd --- /dev/null +++ b/scapy/contrib/knx.py @@ -0,0 +1,637 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2021 Julien BEDEL +# Claire VACHEROT + +""" +KNXNet/IP + +This module provides Scapy layers for KNXNet/IP communications over UDP +according to KNX specifications v2.1 / ISO-IEC 14543-3. +Specifications can be downloaded for free here : +https://my.knx.org/en/shop/knx-specifications + +Currently, the module (partially) supports the following services : +* SEARCH REQUEST/RESPONSE +* DESCRIPTION REQUEST/RESPONSE +* CONNECT, DISCONNECT, CONNECTION_STATE REQUEST/RESPONSE +* CONFIGURATION REQUEST/RESPONSE +* TUNNELING REQUEST/RESPONSE +""" + +# scapy.contrib.description = KNX Protocol +# scapy.contrib.status = loads +import struct + +from scapy.fields import PacketField, MultipleTypeField, ByteField, \ + XByteField, ShortEnumField, ShortField, \ + ByteEnumField, IPField, StrFixedLenField, MACField, XBitField, \ + PacketListField, FieldLenField, \ + StrLenField, BitEnumField, BitField, ConditionalField +from scapy.packet import Packet, bind_layers, bind_bottom_up, Padding +from scapy.layers.inet import UDP + +# KNX CODES + +# KNX Standard v2.1 - 03_08_02 p20 +SERVICE_IDENTIFIER_CODES = { + 0x0201: "SEARCH_REQUEST", + 0x0202: "SEARCH_RESPONSE", + 0x0203: "DESCRIPTION_REQUEST", + 0x0204: "DESCRIPTION_RESPONSE", + 0x0205: "CONNECT_REQUEST", + 0x0206: "CONNECT_RESPONSE", + 0x0207: "CONNECTIONSTATE_REQUEST", + 0x0208: "CONNECTIONSTATE_RESPONSE", + 0x0209: "DISCONNECT_REQUEST", + 0x020A: "DISCONNECT_RESPONSE", + 0x0310: "CONFIGURATION_REQUEST", + 0x0311: "CONFIGURATION_ACK", + 0x0420: "TUNNELING_REQUEST", + 0x0421: "TUNNELING_ACK" +} + +# KNX Standard v2.1 - 03_08_02 p39 +HOST_PROTOCOL_CODES = { + 0x01: "IPV4_UDP", + 0x02: "IPV4_TCP" +} + +# KNX Standard v2.1 - 03_08_02 p23 +DESCRIPTION_TYPE_CODES = { + 0x01: "DEVICE_INFO", + 0x02: "SUPP_SVC_FAMILIES", + 0x03: "IP_CONFIG", + 0x04: "IP_CUR_CONFIG", + 0x05: "KNX_ADDRESSES", + 0x06: "Reserved", + 0xFE: "MFR_DATA", + 0xFF: "not used" +} + +# KNX Standard v2.1 - 03_08_02 p30 +CONNECTION_TYPE_CODES = { + 0x03: "DEVICE_MANAGEMENT_CONNECTION", + 0x04: "TUNNEL_CONNECTION", + 0x06: "REMLOG_CONNECTION", + 0x07: "REMCONF_CONNECTION", + 0x08: "OBJSVR_CONNECTION" +} + +# KNX Standard v2.1 - 03_08_04 +MESSAGE_CODES = { + 0x11: "L_Data.req", + 0x2e: "L_Data.con", + 0xFC: "M_PropRead.req", + 0xFB: "M_PropRead.con", + 0xF6: "M_PropWrite.req", + 0xF5: "M_PropWrite.con" +} + +# KNX Standard v2.1 - 03_08_02 p24 +KNX_MEDIUM_CODES = { + 0x01: "reserved", + 0x02: "TP1", + 0x04: "PL110", + 0x08: "reserved", + 0x10: "RF", + 0x20: "KNX IP" +} + +# KNX Standard v2.1 - 03_03_07 p9 +KNX_ACPI_CODES = { + 0: "GroupValueRead", + 1: "GroupValueResp", + 2: "GroupValueWrite", + 3: "IndAddrWrite", + 4: "IndAddrRead", + 5: "IndAddrResp", + 6: "AdcRead", + 7: "AdcResp" +} + +CEMI_OBJECT_TYPES = { + 0: "DEVICE", + 11: "IP PARAMETER_OBJECT" +} + +# KNX Standard v2.1 - 03_05_01 p25 +CEMI_PROPERTIES = { + 12: "PID_MANUFACTURER_ID", + 51: "PID_PROJECT_INSTALLATION_ID", + 52: "PID_KNX_INDIVIDUAL_ADDRESS", + 53: "PID_ADDITIONAL_INDIVIDUAL_ADDRESSES", + 54: "PID_CURRENT_IP_ASSIGNMENT_METHOD", + 55: "PID_IP_ASSIGNMENT_METHOD", + 56: "PID_IP_CAPABILITIES", + 57: "PID_CURRENT_IP_ADDRESS", + 58: "PID_CURRENT_SUBNET_MASK", + 59: "PID_CURRENT_DEFAULT_GATEWAY", + 60: "PID_IP_ADDRESS", + 61: "PID_SUBNET_MASK", + 62: "PID_DEFAULT_GATEWAY", + 63: "PID_DHCP_BOOTP_SERVER", + 64: "PID_MAC_ADDRESS", + 65: "PID_SYSTEM_SETUP_MULTICAST_ADDRESS", + 66: "PID_ROUTING_MULTICAST_ADDRESS", + 67: "PID_TTL", + 68: "PID_KNXNETIP_DEVICE_CAPABILITIES", + 69: "PID_KNXNETIP_DEVICE_STATE", + 70: "PID_KNXNETIP_ROUTING_CAPABILITIES", + 71: "PID_PRIORITY_FIFO_ENABLED", + 72: "PID_QUEUE_OVERFLOW_TO_IP", + 73: "PID_QUEUE_OVERFLOW_TO_KNX", + 74: "PID_MSG_TRANSMIT_TO_IP", + 75: "PID_MSG_TRANSMIT_TO_KNX", + 76: "PID_FRIENDLY_NAME", + 78: "PID_ROUTING_BUSY_WAIT_TIME" +} + + +# KNX SPECIFIC FIELDS + +# KNX Standard v2.1 - 03_05_01 p.17 +class KNXAddressField(ShortField): + def i2repr(self, pkt, x): + if x is None: + return None + else: + return "%d.%d.%d" % ((x >> 12) & 0xf, (x >> 8) & 0xf, (x & 0xff)) + + def any2i(self, pkt, x): + if isinstance(x, str): + try: + a, b, c = map(int, x.split(".")) + x = (a << 12) | (b << 8) | c + except ValueError: + raise ValueError(x) + return ShortField.any2i(self, pkt, x) + + +# KNX Standard v2.1 - 03_05_01 p.18 +class KNXGroupField(ShortField): + def i2repr(self, pkt, x): + return "%d/%d/%d" % ((x >> 11) & 0x1f, (x >> 8) & 0x7, (x & 0xff)) + + def any2i(self, pkt, x): + if isinstance(x, str): + try: + a, b, c = map(int, x.split("/")) + x = (a << 11) | (b << 8) | c + except ValueError: + raise ValueError(x) + return ShortField.any2i(self, pkt, x) + + +# KNX PLACEHOLDERS + +# KNX Standard v2.1 - 03_08_02 p21 +class HPAI(Packet): + name = "HPAI" + fields_desc = [ + ByteField("structure_length", None), + ByteEnumField("host_protocol", 0x01, HOST_PROTOCOL_CODES), + IPField("ip_address", None), + ShortField("port", None) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p)) + p[1:] + return p + pay + + +# DIB, KNX Standard v2.1 - 03_08_02 p22 +class ServiceFamily(Packet): + name = "Service Family" + fields_desc = [ + ByteField("id", None), + ByteField("version", None) + ] + + +# Different DIB types depends on the "description_type_code" field +# Defining a generic DIB packet and differentiating with `dispatch_hook` or +# `MultipleTypeField` may better fit KNX specs +class DIBDeviceInfo(Packet): + name = "DIB: DEVICE_INFO" + fields_desc = [ + ByteField("structure_length", None), + ByteEnumField("description_type", 0x01, DESCRIPTION_TYPE_CODES), + ByteEnumField("knx_medium", 0x02, KNX_MEDIUM_CODES), + ByteField("device_status", None), + KNXAddressField("knx_address", None), + ShortField("project_installation_identifier", None), + XBitField("device_serial_number", None, 48), + IPField("device_multicast_address", None), + MACField("device_mac_address", None), + StrFixedLenField("device_friendly_name", None, 30) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p)) + p[1:] + return p + pay + + +class DIBSuppSvcFamilies(Packet): + name = "DIB: SUPP_SVC_FAMILIES" + fields_desc = [ + ByteField("structure_length", 0x02), + ByteEnumField("description_type", 0x02, DESCRIPTION_TYPE_CODES), + ConditionalField( + PacketListField("service_family", + ServiceFamily(), + ServiceFamily, + length_from=lambda pkt: + pkt.structure_length - 0x02), + lambda pkt: pkt.structure_length > 0x02) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p)) + p[1:] + return p + pay + + +# CRI and CRD, KNX Standard v2.1 - 03_08_02 p21 + +class TunnelingConnection(Packet): + name = "Tunneling Connection" + fields_desc = [ + ByteField("knx_layer", 0x02), + ByteField("reserved", None) + ] + + +class CRDTunnelingConnection(Packet): + name = "CRD Tunneling Connection" + fields_desc = [ + KNXAddressField("knx_individual_address", None) + ] + + +class CRI(Packet): + name = "CRI (Connection Request Information)" + fields_desc = [ + ByteField("structure_length", 0x02), + ByteEnumField("connection_type", 0x03, CONNECTION_TYPE_CODES), + ConditionalField(PacketField("connection_data", + TunnelingConnection(), + TunnelingConnection), + lambda pkt: pkt.connection_type == 0x04) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p)) + p[1:] + return p + pay + + +class CRD(Packet): + name = "CRD (Connection Response Data)" + fields_desc = [ + ByteField("structure_length", 0x00), + ByteEnumField("connection_type", 0x03, CONNECTION_TYPE_CODES), + ConditionalField(PacketField("connection_data", + CRDTunnelingConnection(), + CRDTunnelingConnection), + lambda pkt: pkt.connection_type == 0x04) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p)) + p[1:] + return p + pay + + +# cEMI blocks + +class LcEMI(Packet): + name = "L_cEMI" + fields_desc = [ + FieldLenField("additional_information_length", 0, fmt="B", + length_of="additional_information"), + StrLenField("additional_information", None, + length_from=lambda pkt: pkt.additional_information_length), + # Controlfield 1 (1 byte made of 8*1 bits) + BitEnumField("frame_type", 1, 1, { + 1: "standard" + }), + BitField("reserved_1", 0, 1), + BitField("repeat_on_error", 1, 1), + BitEnumField("broadcast_type", 1, 1, { + 1: "domain" + }), + BitEnumField("priority", 3, 2, { + 3: "low" + }), + BitField("ack_request", 0, 1), + BitField("confirmation_error", 0, 1), + # Controlfield 2 (1 byte made of 1+3+4 bits) + BitEnumField("address_type", 1, 1, { + 1: "group" + }), + BitField("hop_count", 6, 3), + BitField("extended_frame_format", 0, 4), + KNXAddressField("source_address", None), + KNXGroupField("destination_address", "1/2/3"), + FieldLenField("npdu_length", 0x01, fmt="B", length_of="data"), + # TPCI and APCI (2 byte made of 1+1+4+4+6 bits) + BitEnumField("packet_type", 0, 1, { + 0: "data" + }), + BitEnumField("sequence_type", 0, 1, { + 0: "unnumbered" + }), + BitField("reserved_2", 0, 4), + BitEnumField("acpi", 2, 4, KNX_ACPI_CODES), + BitField("data", 0, 6) + + ] + + +class DPcEMI(Packet): + name = "DP_cEMI" + fields_desc = [ + # see if best representation is str or hex + ShortField("object_type", None), + ByteField("object_instance", 1), + ByteField("property_id", None), + BitField("number_of_elements", 1, 4), + BitField("start_index", None, 12) + ] + + +class CEMI(Packet): + name = "CEMI" + fields_desc = [ + ByteEnumField("message_code", None, MESSAGE_CODES), + MultipleTypeField( + [ + (PacketField("cemi_data", LcEMI(), LcEMI), + lambda pkt: pkt.message_code == 0x11), + (PacketField("cemi_data", LcEMI(), LcEMI), + lambda pkt: pkt.message_code == 0x2e), + (PacketField("cemi_data", DPcEMI(), DPcEMI), + lambda pkt: pkt.message_code == 0xFC), + (PacketField("cemi_data", DPcEMI(), DPcEMI), + lambda pkt: pkt.message_code == 0xFB), + (PacketField("cemi_data", DPcEMI(), DPcEMI), + lambda pkt: pkt.message_code == 0xF6), + (PacketField("cemi_data", DPcEMI(), DPcEMI), + lambda pkt: pkt.message_code == 0xF5) + ], + PacketField("cemi_data", LcEMI(), LcEMI) + ) + ] + + +# KNX SERVICES + +# KNX Standard v2.1 - 03_08_02 p28 +class KNXSearchRequest(Packet): + name = "SEARCH_REQUEST", + fields_desc = [ + PacketField("discovery_endpoint", HPAI(), HPAI) + ] + + +# KNX Standard v2.1 - 03_08_02 p28 +class KNXSearchResponse(Packet): + name = "SEARCH_RESPONSE", + fields_desc = [ + PacketField("control_endpoint", HPAI(), HPAI), + PacketField("device_info", DIBDeviceInfo(), DIBDeviceInfo), + PacketField("supported_service_families", DIBSuppSvcFamilies(), + DIBSuppSvcFamilies) + ] + + +# KNX Standard v2.1 - 03_08_02 p29 +class KNXDescriptionRequest(Packet): + name = "DESCRIPTION_REQUEST" + fields_desc = [ + PacketField("control_endpoint", HPAI(), HPAI) + ] + + +# KNX Standard v2.1 - 03_08_02 p29 +class KNXDescriptionResponse(Packet): + name = "DESCRIPTION_RESPONSE" + fields_desc = [ + PacketField("device_info", DIBDeviceInfo(), DIBDeviceInfo), + PacketField("supported_service_families", DIBSuppSvcFamilies(), + DIBSuppSvcFamilies) + # TODO: this is an optional field in KNX specs, + # => Add conditions to take it into account + # PacketField("other_device_info", DIBDeviceInfo(), DIBDeviceInfo) + ] + + +# KNX Standard v2.1 - 03_08_02 p30 +class KNXConnectRequest(Packet): + name = "CONNECT_REQUEST" + fields_desc = [ + PacketField("control_endpoint", HPAI(), HPAI), + PacketField("data_endpoint", HPAI(), HPAI), + PacketField("connection_request_information", CRI(), CRI) + ] + + +# KNX Standard v2.1 - 03_08_02 p31 +class KNXConnectResponse(Packet): + name = "CONNECT_RESPONSE" + fields_desc = [ + ByteField("communication_channel_id", None), + ByteField("status", None), + PacketField("data_endpoint", HPAI(), HPAI), + PacketField("connection_response_data_block", CRD(), CRD) + ] + + +# KNX Standard v2.1 - 03_08_02 p32 +class KNXConnectionstateRequest(Packet): + name = "CONNECTIONSTATE_REQUEST" + fields_desc = [ + ByteField("communication_channel_id", None), + ByteField("reserved", None), + PacketField("control_endpoint", HPAI(), HPAI) + ] + + +# KNX Standard v2.1 - 03_08_02 p32 +class KNXConnectionstateResponse(Packet): + name = "CONNECTIONSTATE_RESPONSE" + fields_desc = [ + ByteField("communication_channel_id", None), + ByteField("status", 0x00) + ] + + +# KNX Standard v2.1 - 03_08_02 p33 +class KNXDisconnectRequest(Packet): + name = "DISCONNECT_REQUEST" + fields_desc = [ + ByteField("communication_channel_id", 0x01), + ByteField("reserved", None), + PacketField("control_endpoint", HPAI(), HPAI) + ] + + +# KNX Standard v2.1 - 03_08_02 p34 +class KNXDisconnectResponse(Packet): + name = "DISCONNECT_RESPONSE" + fields_desc = [ + ByteField("communication_channel_id", None), + ByteField("status", 0x00) + ] + + +# KNX Standard v2.1 - 03_08_03 p22 +class KNXConfigurationRequest(Packet): + name = "CONFIGURATION_REQUEST" + fields_desc = [ + ByteField("structure_length", 0x04), + ByteField("communication_channel_id", 0x01), + ByteField("sequence_counter", None), + ByteField("reserved", None), + PacketField("cemi", CEMI(), CEMI) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p[:4])) + p[1:] + return p + pay + + +# KNX Standard v2.1 - 03_08_03 p22 +class KNXConfigurationACK(Packet): + name = "CONFIGURATION_ACK" + fields_desc = [ + ByteField("structure_length", None), + ByteField("communication_channel_id", 0x01), + ByteField("sequence_counter", None), + ByteField("status", None) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p)) + p[1:] + return p + pay + + +# KNX Standard v2.1 - 03_08_04 p.17 +class KNXTunnelingRequest(Packet): + name = "TUNNELING_REQUEST" + fields_desc = [ + ByteField("structure_length", 0x04), + ByteField("communication_channel_id", 0x01), + ByteField("sequence_counter", None), + ByteField("reserved", None), + PacketField("cemi", CEMI(), CEMI) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p[:4])) + p[1:] + return p + pay + + +# KNX Standard v2.1 - 03_08_04 p.18 +class KNXTunnelingACK(Packet): + name = "TUNNELING_ACK" + fields_desc = [ + ByteField("structure_length", None), + ByteField("communication_channel_id", 0x01), + ByteField("sequence_counter", None), + ByteField("status", None) + ] + + def post_build(self, p, pay): + if self.structure_length is None: + p = struct.pack("!B", len(p)) + p[1:] + return p + pay + + +# KNX FRAME + +# we made the choice to define a KNX service as a payload for a KNX Header +# it could also be possible to define the body as a conditional PacketField +# contained after header + +class KNX(Packet): + name = "KNXnet/IP" + fields_desc = [ + ByteField("header_length", None), + XByteField("protocol_version", 0x10), + ShortEnumField("service_identifier", None, SERVICE_IDENTIFIER_CODES), + ShortField("total_length", None) + ] + + def post_build(self, p, pay): + # computes header_length + if self.header_length is None: + p = struct.pack("!B", len(p)) + p[1:] + # computes total_length + if self.total_length is None: + p = p[:-2] + struct.pack("!H", len(p) + len(pay)) + return p + pay + + +# LAYERS BINDING +bind_bottom_up(UDP, KNX, dport=3671) +bind_bottom_up(UDP, KNX, sport=3671) +bind_layers(UDP, KNX, sport=3671, dport=3671) + +bind_layers(KNX, KNXSearchRequest, service_identifier=0x0201) +bind_layers(KNX, KNXSearchResponse, service_identifier=0x0202) +bind_layers(KNX, KNXDescriptionRequest, service_identifier=0x0203) +bind_layers(KNX, KNXDescriptionResponse, service_identifier=0x0204) +bind_layers(KNX, KNXConnectRequest, service_identifier=0x0205) +bind_layers(KNX, KNXConnectResponse, service_identifier=0x0206) +bind_layers(KNX, KNXConnectionstateRequest, service_identifier=0x0207) +bind_layers(KNX, KNXConnectionstateResponse, service_identifier=0x0208) +bind_layers(KNX, KNXDisconnectResponse, service_identifier=0x020A) +bind_layers(KNX, KNXDisconnectRequest, service_identifier=0x0209) +bind_layers(KNX, KNXConfigurationRequest, service_identifier=0x0310) +bind_layers(KNX, KNXConfigurationACK, service_identifier=0x0311) +bind_layers(KNX, KNXTunnelingRequest, service_identifier=0x0420) +bind_layers(KNX, KNXTunnelingACK, service_identifier=0x0421) + +# we bind every layer to Padding in order to delete their payloads +# (from https://github.com/secdev/scapy/issues/360) +# we could also define a new Packet class with no payload and +# inherit every KNX packet from it : +# class _KNXBodyNoPayload(Packet): +# +# def extract_padding(self, s): +# return b"", None + +bind_layers(HPAI, Padding) +bind_layers(ServiceFamily, Padding) +bind_layers(DIBDeviceInfo, Padding) +bind_layers(DIBSuppSvcFamilies, Padding) +bind_layers(TunnelingConnection, Padding) +bind_layers(CRDTunnelingConnection, Padding) +bind_layers(CRI, Padding) +bind_layers(CRD, Padding) +bind_layers(LcEMI, Padding) +bind_layers(DPcEMI, Padding) +bind_layers(CEMI, Padding) + +bind_layers(KNXSearchRequest, Padding) +bind_layers(KNXSearchResponse, Padding) +bind_layers(KNXDescriptionRequest, Padding) +bind_layers(KNXDescriptionResponse, Padding) +bind_layers(KNXConnectRequest, Padding) +bind_layers(KNXConnectResponse, Padding) +bind_layers(KNXConnectionstateRequest, Padding) +bind_layers(KNXConnectionstateResponse, Padding) +bind_layers(KNXDisconnectRequest, Padding) +bind_layers(KNXDisconnectResponse, Padding) +bind_layers(KNXConfigurationRequest, Padding) +bind_layers(KNXConfigurationACK, Padding) +bind_layers(KNXTunnelingRequest, Padding) +bind_layers(KNXTunnelingACK, Padding) diff --git a/scapy/contrib/lacp.py b/scapy/contrib/lacp.py index 2a69d6a93ee..d6a7aced8f0 100644 --- a/scapy/contrib/lacp.py +++ b/scapy/contrib/lacp.py @@ -1,44 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Link Aggregation Control Protocol (LACP) # scapy.contrib.status = loads from scapy.packet import Packet, bind_layers from scapy.fields import ByteField, MACField, ShortField, ByteEnumField, IntField, XStrFixedLenField # noqa: E501 -from scapy.layers.l2 import Ether -from scapy.data import ETHER_TYPES - - -ETHER_TYPES['SlowProtocol'] = 0x8809 -SLOW_SUB_TYPES = { - 'Unused': 0, - 'LACP': 1, - 'Marker Protocol': 2, -} - - -class SlowProtocol(Packet): - name = "SlowProtocol" - fields_desc = [ByteEnumField("subtype", 0, SLOW_SUB_TYPES)] - - -bind_layers(Ether, SlowProtocol, type=0x8809, dst='01:80:c2:00:00:02') +from scapy.contrib.slowprot import SlowProtocol class LACP(Packet): name = "LACP" + deprecated_fields = { + "actor_port_numer": ("actor_port_number", "2.4.4"), + "partner_port_numer": ("partner_port_number", "2.4.4"), + "colletctor_reserved": ("collector_reserved", "2.4.4"), + } fields_desc = [ ByteField("version", 1), ByteField("actor_type", 1), @@ -47,7 +25,7 @@ class LACP(Packet): MACField("actor_system", None), ShortField("actor_key", 0), ShortField("actor_port_priority", 0), - ShortField("actor_port_numer", 0), + ShortField("actor_port_number", 0), ByteField("actor_state", 0), XStrFixedLenField("actor_reserved", "", 3), ByteField("partner_type", 2), @@ -56,13 +34,13 @@ class LACP(Packet): MACField("partner_system", None), ShortField("partner_key", 0), ShortField("partner_port_priority", 0), - ShortField("partner_port_numer", 0), + ShortField("partner_port_number", 0), ByteField("partner_state", 0), XStrFixedLenField("partner_reserved", "", 3), ByteField("collector_type", 3), ByteField("collector_length", 16), ShortField("collector_max_delay", 0), - XStrFixedLenField("colletctor_reserved", "", 12), + XStrFixedLenField("collector_reserved", "", 12), ByteField("terminator_type", 0), ByteField("terminator_length", 0), XStrFixedLenField("reserved", "", 50), diff --git a/scapy/contrib/ldp.py b/scapy/contrib/ldp.py index 25152ab769b..bd08ee8f58f 100644 --- a/scapy/contrib/ldp.py +++ b/scapy/contrib/ldp.py @@ -1,33 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2010 Florian Duraffourg + # scapy.contrib.description = Label Distribution Protocol (LDP) # scapy.contrib.status = loads -# http://git.savannah.gnu.org/cgit/ldpscapy.git/snapshot/ldpscapy-5285b81d6e628043df2a83301b292f24a95f0ba1.tar.gz +""" +Label Distribution Protocol (LDP) -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +http://git.savannah.gnu.org/cgit/ldpscapy.git/snapshot/ldpscapy-5285b81d6e628043df2a83301b292f24a95f0ba1.tar.gz -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +""" -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Copyright (C) 2010 Florian Duraffourg - -from __future__ import absolute_import import struct from scapy.compat import orb from scapy.packet import Packet, bind_layers, bind_bottom_up -from scapy.fields import BitField, IPField, IntField, ShortField, StrField, \ - XBitField +from scapy.fields import ( + BitField, + MayEnd, + IPField, + IntField, + ShortField, + StrField, + XBitField, +) from scapy.layers.inet import UDP from scapy.layers.inet import TCP -from scapy.modules.six.moves import range from scapy.config import conf from scapy.utils import inet_aton, inet_ntoa @@ -175,6 +175,8 @@ def size(self, s): return tmp_len def getfield(self, pkt, s): + if not s: + return s, [] tmp_len = self.size(s) return s[tmp_len:], self.m2i(pkt, s[:tmp_len]) @@ -365,7 +367,7 @@ class LDPLabelMM(_LDP_Packet): XBitField("type", 0x0400, 15), ShortField("len", None), IntField("id", 0), - FecTLVField("fec", None), + MayEnd(FecTLVField("fec", None)), LabelTLVField("label", 0)] # 3.5.8. Label Request Message @@ -400,7 +402,7 @@ class LDPLabelWM(_LDP_Packet): XBitField("type", 0x0402, 15), ShortField("len", None), IntField("id", 0), - FecTLVField("fec", None), + MayEnd(FecTLVField("fec", None)), LabelTLVField("label", 0)] # 3.5.11. Label Release Message diff --git a/scapy/contrib/lldp.py b/scapy/contrib/lldp.py index e3285327af6..6ab62755419 100644 --- a/scapy/contrib/lldp.py +++ b/scapy/contrib/lldp.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = Link Layer Discovery Protocol (LLDP) # scapy.contrib.status = loads @@ -6,17 +10,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :author: Thomas Tannhaeuser, hecke@naberius.de - :license: GPLv2 - - This module is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This module is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. :description: @@ -45,22 +38,29 @@ from scapy.config import conf from scapy.error import Scapy_Exception from scapy.layers.l2 import Ether, Dot1Q -from scapy.fields import MACField, IPField, BitField, \ +from scapy.fields import MACField, IPField, IP6Field, BitField, \ StrLenField, ByteEnumField, BitEnumField, \ EnumField, ThreeBytesField, BitFieldLenField, \ ShortField, XStrLenField, ByteField, ConditionalField, \ - MultipleTypeField + MultipleTypeField, FlagsField, ShortEnumField, ScalingField, \ + BitScalingField from scapy.packet import Packet, bind_layers -from scapy.modules.six.moves import range from scapy.data import ETHER_TYPES -from scapy.compat import orb +from scapy.compat import orb, bytes_int LLDP_NEAREST_BRIDGE_MAC = '01:80:c2:00:00:0e' LLDP_NEAREST_NON_TPMR_BRIDGE_MAC = '01:80:c2:00:00:03' LLDP_NEAREST_CUSTOMER_BRIDGE_MAC = '01:80:c2:00:00:00' LLDP_ETHER_TYPE = 0x88cc -ETHER_TYPES['LLDP'] = LLDP_ETHER_TYPE +ETHER_TYPES[LLDP_ETHER_TYPE] = 'LLDP' + + +class LLDPInvalidFieldValue(Scapy_Exception): + """ + field value is out of allowed range + """ + pass class LLDPInvalidFrameStructure(Scapy_Exception): @@ -106,10 +106,43 @@ class LLDPDU(Packet): 0x06: 'system description', 0x07: 'system capabilities', 0x08: 'management address', - range(0x09, 0x7e): 'reserved - future standardization', 127: 'organisation specific TLV' } + IANA_ADDRESS_FAMILY_NUMBERS = { + 0x00: 'other', + 0x01: 'IPv4', + 0x02: 'IPv6', + 0x03: 'NSAP', + 0x04: 'HDLC', + 0x05: 'BBN', + 0x06: '802', + 0x07: 'E.163', + 0x08: 'E.164', + 0x09: 'F.69', + 0x0a: 'X.121', + 0x0b: 'IPX', + 0x0c: 'Appletalk', + 0x0d: 'Decnet IV', + 0x0e: 'Banyan Vines', + 0x0f: 'E.164 with NSAP', + 0x10: 'DNS', + 0x11: 'Distinguished Name', + 0x12: 'AS Number', + 0x13: 'XTP over IPv4', + 0x14: 'XTP over IPv6', + 0x15: 'XTP native mode XTP', + 0x16: 'Fiber Channel World-Wide Port Name', + 0x17: 'Fiber Channel World-Wide Node Name', + 0x18: 'GWID', + 0x19: 'AFI for L2VPN', + 0x1a: 'MPLS-TP Section Endpoint ID', + 0x1b: 'MPLS-TP LSP Endpoint ID', + 0x1c: 'MPLS-TP Pseudowire Endpoint ID', + 0x1d: 'MT IP Multi-Topology IPv4', + 0x1e: 'MT IP Multi-Topology IPv6' + } + DOT1Q_HEADER_LEN = 4 ETHER_HEADER_LEN = 14 ETHER_FSC_LEN = 4 @@ -122,7 +155,13 @@ def guess_payload_class(self, payload): # type is a 7-bit bitfield spanning bits 1..7 -> div 2 try: lldpdu_tlv_type = orb(payload[0]) // 2 - return LLDPDU_CLASS_TYPES.get(lldpdu_tlv_type, conf.raw_layer) + class_type = LLDPDU_CLASS_TYPES.get(lldpdu_tlv_type, conf.raw_layer) + if isinstance(class_type, list): + for cls in class_type: + if cls._match_organization_specific(payload): + return cls + else: + return class_type except IndexError: return conf.raw_layer @@ -265,7 +304,7 @@ def dissection_done(self, pkt): super(LLDPDU, self).dissection_done(pkt) def _check(self): - """Overwrited by LLDPU objects""" + """Overwritten by LLDPU objects""" pass def post_dissect(self, s): @@ -289,6 +328,19 @@ def _ldp_id_adjustlen(pkt, x): return length +def _ldp_id_lengthfrom(pkt): + length = pkt._length + if length is None: + return 0 + # Subtract the subtype field + length -= 1 + if (isinstance(pkt, LLDPDUPortID) and pkt.subtype == 0x4) or \ + (isinstance(pkt, LLDPDUChassisID) and pkt.subtype == 0x5): + # Take the ConditionalField into account + length -= 1 + return length + + class LLDPDUChassisID(LLDPDU): """ ieee 802.1ab-2016 - sec. 8.5.2 / p. 26 @@ -302,7 +354,6 @@ class LLDPDUChassisID(LLDPDU): 0x05: 'network address', 0x06: 'interface name', 0x07: 'locally assigned', - range(0x08, 0xff): 'reserved' } SUBTYPE_RESERVED = 0x00 @@ -320,7 +371,7 @@ class LLDPDUChassisID(LLDPDU): adjust=lambda pkt, x: _ldp_id_adjustlen(pkt, x)), ByteEnumField('subtype', 0x00, LLDP_CHASSIS_ID_TLV_SUBTYPES), ConditionalField( - ByteField('family', 0), + ByteEnumField('family', 0, LLDPDU.IANA_ADDRESS_FAMILY_NUMBERS), lambda pkt: pkt.subtype == 0x05 ), MultipleTypeField([ @@ -330,9 +381,13 @@ class LLDPDUChassisID(LLDPDU): ), ( IPField('id', None), - lambda pkt: pkt.subtype == 0x05 + lambda pkt: pkt.subtype == 0x05 and pkt.family == 0x01 ), - ], StrLenField('id', '', length_from=lambda pkt: pkt._length - 1) + ( + IP6Field('id', None), + lambda pkt: pkt.subtype == 0x05 and pkt.family == 0x02 + ), + ], StrLenField('id', '', length_from=_ldp_id_lengthfrom) ) ] @@ -357,7 +412,6 @@ class LLDPDUPortID(LLDPDU): 0x05: 'interface name', 0x06: 'agent circuit ID', 0x07: 'locally assigned', - range(0x08, 0xff): 'reserved' } SUBTYPE_RESERVED = 0x00 @@ -375,7 +429,7 @@ class LLDPDUPortID(LLDPDU): adjust=lambda pkt, x: _ldp_id_adjustlen(pkt, x)), ByteEnumField('subtype', 0x00, LLDP_PORT_ID_TLV_SUBTYPES), ConditionalField( - ByteField('family', 0), + ByteEnumField('family', 0, LLDPDU.IANA_ADDRESS_FAMILY_NUMBERS), lambda pkt: pkt.subtype == 0x04 ), MultipleTypeField([ @@ -385,9 +439,13 @@ class LLDPDUPortID(LLDPDU): ), ( IPField('id', None), - lambda pkt: pkt.subtype == 0x04 + lambda pkt: pkt.subtype == 0x04 and pkt.family == 0x01 ), - ], StrLenField('id', '', length_from=lambda pkt: pkt._length - 1) + ( + IP6Field('id', None), + lambda pkt: pkt.subtype == 0x04 and pkt.family == 0x02 + ), + ], StrLenField('id', '', length_from=_ldp_id_lengthfrom) ) ] @@ -532,39 +590,6 @@ class LLDPDUManagementAddress(LLDPDU): see https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml # noqa: E501 """ - IANA_ADDRESS_FAMILY_NUMBERS = { - 0x00: 'other', - 0x01: 'IPv4', - 0x02: 'IPv6', - 0x03: 'NSAP', - 0x04: 'HDLC', - 0x05: 'BBN', - 0x06: '802', - 0x07: 'E.163', - 0x08: 'E.164', - 0x09: 'F.69', - 0x0a: 'X.121', - 0x0b: 'IPX', - 0x0c: 'Appletalk', - 0x0d: 'Decnet IV', - 0x0e: 'Banyan Vines', - 0x0f: 'E.164 with NSAP', - 0x10: 'DNS', - 0x11: 'Distinguished Name', - 0x12: 'AS Number', - 0x13: 'XTP over IPv4', - 0x14: 'XTP over IPv6', - 0x15: 'XTP native mode XTP', - 0x16: 'Fiber Channel World-Wide Port Name', - 0x17: 'Fiber Channel World-Wide Node Name', - 0x18: 'GWID', - 0x19: 'AFI for L2VPN', - 0x1a: 'MPLS-TP Section Endpoint ID', - 0x1b: 'MPLS-TP LSP Endpoint ID', - 0x1c: 'MPLS-TP Pseudowire Endpoint ID', - 0x1d: 'MT IP Multi-Topology IPv4', - 0x1e: 'MT IP Multi-Topology IPv6' - } SUBTYPE_MANAGEMENT_ADDRESS_OTHER = 0x00 SUBTYPE_MANAGEMENT_ADDRESS_IPV4 = 0x01 @@ -629,9 +654,10 @@ class LLDPDUManagementAddress(LLDPDU): length_of='management_address', adjust=lambda pkt, x: len(pkt.management_address) + 1), # noqa: E501 ByteEnumField('management_address_subtype', 0x00, - IANA_ADDRESS_FAMILY_NUMBERS), + LLDPDU.IANA_ADDRESS_FAMILY_NUMBERS), XStrLenField('management_address', '', - length_from=lambda pkt: + length_from=lambda pkt: 0 + if pkt._management_address_string_length is None else pkt._management_address_string_length - 1), ByteEnumField('interface_numbering_subtype', SUBTYPE_INTERFACE_NUMBER_UNKNOWN, @@ -681,9 +707,520 @@ class LLDPDUGenericOrganisationSpecific(LLDPDU): BitFieldLenField('_length', None, 9, length_of='data', adjust=lambda pkt, x: len(pkt.data) + 4), # noqa: E501 ThreeBytesEnumField('org_code', 0, ORG_UNIQUE_CODES), ByteField('subtype', 0x00), - XStrLenField('data', '', length_from=lambda pkt: pkt._length - 4) + XStrLenField('data', '', + length_from=lambda pkt: 0 if pkt._length is None else + pkt._length - 4) + ] + + @staticmethod + def _match_organization_specific(payload): + return True + + +class LLDPDUPowerViaMDI(LLDPDUGenericOrganisationSpecific): + """ + Legacy PoE TLV originally defined in IEEE Std 802.1AB-2005 Annex G.3. + + IEEE802.3bt-2018 - sec. 79.3.2. + """ + + # IEEE802.3bt-2018 - sec. 79.3.2.1 + MDI_POWER_SUPPORT = { + (1 << 3): 'PSE pairs controlled', + (1 << 2): 'PSE MDI power enabled', + (1 << 1): 'PSE MDI power supported', + (1 << 0): 'port class PSE', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.2 + PSE_POWER_PAIR = { + 1: 'alt A', + 2: 'alt B', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.3 + POWER_CLASS = { + 1: 'class 0', + 2: 'class 1', + 3: 'class 2', + 4: 'class 3', + 5: 'class 4 and above', + } + + fields_desc = [ + BitEnumField('_type', 127, 7, LLDPDU.TYPES), + BitField('_length', 7, 9), + ThreeBytesField('org_code', LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3), # noqa: E501 + ByteField('subtype', 2), + FlagsField('MDI_power_support', 0, 8, MDI_POWER_SUPPORT), + ByteEnumField('PSE_power_pair', 1, PSE_POWER_PAIR), + ByteEnumField('power_class', 1, POWER_CLASS), ] + @staticmethod + def _match_organization_specific(payload): + """ + match organization specific TLV + """ + return (orb(payload[5]) == 2 and orb(payload[1]) == 7 + and bytes_int(payload[2:5]) == + LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3) + + def _check(self): + """ + run layer specific checks + """ + if conf.contribs['LLDP'].strict_mode() and self._length != 7: + raise LLDPInvalidLengthField('length must be 7 - got ' + '{}'.format(self._length)) + + +class LLDPDUPowerViaMDIDDL(LLDPDUPowerViaMDI): + """ + PoE TLV with DLL classification extension specified in IEEE802.3at-2009 + + Note: power values are expressed in units of Watts, + converted to tenth of Watts internally + + IEEE802.3bt-2018 - sec. 79.3.2 + """ + + # IEEE802.3bt-2018 - sec. 79.3.2.4 + POWER_TYPE_NO = { + 1: 'type 1', + 0: 'type 2', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.4 + POWER_TYPE_DIR = { + 1: 'PD', + 0: 'PSE', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.4 + POWER_SOURCE_PD = { + 0b11: 'PSE and local', + 0b10: 'reserved', + 0b01: 'PSE', + 0b00: 'unknown', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.4 + POWER_SOURCE_PSE = { + 0b11: 'reserved', + 0b10: 'backup source', + 0b01: 'primary source', + 0b00: 'unknown', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.4 + PD_4PID_SUP = { + 0: 'not supported', + 1: 'supported', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.4 + POWER_PRIO = { + 0b11: 'low', + 0b10: 'high', + 0b01: 'critical', + 0b00: 'unknown', + } + + fields_desc = [ + BitEnumField('_type', 127, 7, LLDPDU.TYPES), + BitField('_length', 12, 9), + ThreeBytesField('org_code', LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3), # noqa: E501 + ByteField('subtype', 2), + FlagsField('MDI_power_support', 0, 8, LLDPDUPowerViaMDI.MDI_POWER_SUPPORT), + ByteEnumField('PSE_power_pair', 1, LLDPDUPowerViaMDI.PSE_POWER_PAIR), + ByteEnumField('power_class', 1, LLDPDUPowerViaMDI.POWER_CLASS), + BitEnumField('power_type_no', 1, 1, POWER_TYPE_NO), + BitEnumField('power_type_dir', 1, 1, POWER_TYPE_DIR), + MultipleTypeField([ + ( + BitEnumField('power_source', 0b01, 2, POWER_SOURCE_PD), + lambda pkt: pkt.power_type_dir == 1 + ), + ], BitEnumField('power_source', 0b01, 2, POWER_SOURCE_PSE)), + MultipleTypeField([ + ( + BitEnumField('PD_4PID', 0, 2, PD_4PID_SUP), + lambda pkt: pkt.power_type_dir == 1 + ), + ], BitField('PD_4PID', 0, 2)), + BitEnumField('power_prio', 0, 2, POWER_PRIO), + ScalingField('PD_requested_power', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + ScalingField('PSE_allocated_power', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + ] + + @staticmethod + def _match_organization_specific(payload): + """ + match organization specific TLV + """ + return (orb(payload[5]) == 2 and orb(payload[1]) == 12 + and bytes_int(payload[2:5]) == + LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3) + + def _check(self): + """ + run layer specific checks + """ + if conf.contribs['LLDP'].strict_mode() and self._length != 12: + raise LLDPInvalidLengthField('length must be 12 - got ' + '{}'.format(self._length)) + # IEEE802.3bt-2018 - sec. 79.3.2.{5,6} + for field, description, max_value in [('PD_requested_power', + 'PSE requested power', + 99.9), + ('PSE_allocated_power', + 'PSE allocated power', + 99.9)]: + val = getattr(self, field) + if (conf.contribs['LLDP'].strict_mode() and val > max_value): + raise LLDPInvalidFieldValue( + 'exceeded maximum {} of {} - got ' + '{}'.format(description, max_value, val)) + + +class LLDPDUPowerViaMDIType34(LLDPDUPowerViaMDIDDL): + """ + PoE TLV with DLL classification and type 3 and 4 extensions + specified in IEEE802.3bt-2018 + + Note: power values are expressed in units of Watts, + converted to tenth of Watts internally + + IEEE802.3bt-2018 - sec. 79.3.2 + """ + + # IEEE802.3bt-2018 - sec. 79.3.2.6e + PSE_POWERING_STATUS = { + 0b11: '4-pair powering dual-signature PD', + 0b10: '4-pair powering single-signature PD', + 0b01: '2-pair powering', + 0b00: 'ignore', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6e + PD_POWERED_STATUS = { + 0b11: '4-pair powered dual-signature PD', + 0b10: '2-pair powered dual-signature PD', + 0b01: 'powered single-signature PD', + 0b00: 'ignore', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6e + PSE_POWER_PAIRS_EXT = { + 0b11: 'both alts', + 0b10: 'alt A', + 0b01: 'alt B', + 0b00: 'ignore', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6e + DUAL_SIGNATURE_POWER_CLASS = { + 0b111: 'single-signature PD or 2-pair only PSE', + 0b110: 'ignore', + 0b101: 'class 5', + 0b100: 'class 4', + 0b011: 'class 3', + 0b010: 'class 2', + 0b001: 'class 1', + 0b000: 'ignore', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6e + POWER_CLASS_EXT = { + 0b1111: 'dual-signature pd', + 0b1110: 'ignore', + 0b1101: 'ignore', + 0b1100: 'ignore', + 0b1011: 'ignore', + 0b1010: 'ignore', + 0b1001: 'ignore', + 0b1000: 'class 8', + 0b0111: 'class 7', + 0b0110: 'class 6', + 0b0101: 'class 5', + 0b0100: 'class 4', + 0b0011: 'class 3', + 0b0010: 'class 2', + 0b0001: 'class 1', + 0b0000: 'ignore', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6d + POWER_TYPE_EXT = { + 0b111: 'ignore', + 0b110: 'ignore', + 0b101: 'type 4 dual-signature PD', + 0b100: 'type 4 single-signature PD', + 0b011: 'type 3 dual-signature PD', + 0b010: 'type 3 single-signature PD', + 0b001: 'type 4 PSE', + 0b000: 'type 3 PSE', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6d + PD_LOAD = { + 1: 'dual-signature and electrically isolated', + 0: 'single-signature or not electrically isolated', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6h + AUTOCLASS = { + (1 << 2): 'PSE autoclass support', + (1 << 1): 'autoclass completed', + (1 << 0): 'autoclass request', + } + + # IEEE802.3bt-2018 - sec. 79.3.2.6i + POWER_DOWN_REQ = { + 0x1d: 'power down', + 0: 'ignore', + } + + fields_desc = [ + BitEnumField('_type', 127, 7, LLDPDU.TYPES), + BitField('_length', 29, 9), + ThreeBytesField('org_code', LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3), # noqa: E501 + ByteField('subtype', 2), + FlagsField('MDI_power_support', 0, 8, LLDPDUPowerViaMDI.MDI_POWER_SUPPORT), + ByteEnumField('PSE_power_pair', 1, LLDPDUPowerViaMDI.PSE_POWER_PAIR), + ByteEnumField('power_class', 1, LLDPDUPowerViaMDI.POWER_CLASS), + BitEnumField('power_type_no', 1, 1, LLDPDUPowerViaMDIDDL.POWER_TYPE_NO), + BitEnumField('power_type_dir', 1, 1, LLDPDUPowerViaMDIDDL.POWER_TYPE_DIR), + MultipleTypeField([ + ( + BitEnumField('power_source', 0b01, 2, LLDPDUPowerViaMDIDDL.POWER_SOURCE_PD), # noqa: E501 + lambda pkt: pkt.power_type_dir == 1 + ), + ], BitEnumField('power_source', 0b01, 2, LLDPDUPowerViaMDIDDL.POWER_SOURCE_PSE)), # noqa: E501 + MultipleTypeField([ + ( + BitEnumField('PD_4PID', 0, 2, LLDPDUPowerViaMDIDDL.PD_4PID_SUP), + lambda pkt: pkt.power_type_dir == 1 + ), + ], BitField('PD_4PID', 0, 2)), + BitEnumField('power_prio', 0, 2, LLDPDUPowerViaMDIDDL.POWER_PRIO), + ScalingField('PD_requested_power', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + ScalingField('PSE_allocated_power', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + ScalingField('PD_requested_power_mode_A', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + ScalingField('PD_requested_power_mode_B', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + ScalingField('PD_allocated_power_alt_A', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + ScalingField('PD_allocated_power_alt_B', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + BitEnumField('PSE_powering_status', 0, 2, PSE_POWERING_STATUS), + BitEnumField('PD_powered_status', 0, 2, PD_POWERED_STATUS), + BitEnumField('PD_power_pair_ext', 0, 2, PSE_POWER_PAIRS_EXT), + BitEnumField('dual_signature_class_mode_A', + 0b111, 3, DUAL_SIGNATURE_POWER_CLASS), + BitEnumField('dual_signature_class_mode_B', + 0b111, 3, DUAL_SIGNATURE_POWER_CLASS), + BitEnumField('power_class_ext', 0, 4, POWER_CLASS_EXT), + BitEnumField('power_type_ext', 0, 7, POWER_TYPE_EXT), + BitEnumField('PD_load', 0, 1, PD_LOAD), + ScalingField('PSE_max_available_power', 0, scaling=0.1, + unit='W', ndigits=1, fmt='H'), + FlagsField('autoclass', 0, 8, AUTOCLASS), + BitEnumField('power_down_req', 0, 6, POWER_DOWN_REQ), + BitScalingField('power_down_time', 0, 18, unit='s'), + ] + + @staticmethod + def _match_organization_specific(payload): + ''' + match organization specific TLV + ''' + return (orb(payload[5]) == 2 and orb(payload[1]) == 29 + and bytes_int(payload[2:5]) == + LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3) + + def _check(self): + """ + run layer specific checks + """ + if conf.contribs['LLDP'].strict_mode() and self._length != 29: + raise LLDPInvalidLengthField('length must be 29 - got ' + '{}'.format(self._length)) + # IEEE802.3bt-2018 - sec. 79.3.2.6{a..b,e,g} + for field, description, max_value in [('PD_requested_power', + 'PSE requested power', + 99.9), + ('PSE_allocated_power', + 'PSE allocated power', + 99.9), + ('PD_requested_power_mode_A', + 'PD requested power mode A', + 49.9), + ('PD_requested_power_mode_B', + 'PD requested power mode B', + 49.9), + ('PD_allocated_power_alt_A', + 'PD allocated power alt A', + 49.9), + ('PD_allocated_power_alt_B', + 'PD allocated power alt B', + 49.9), + ('PSE_max_available_power', + 'PSE maximum available power', + 99.9), + ('power_down_time', + 'power down time', + 262143)]: + val = getattr(self, field) or 0 + if (conf.contribs['LLDP'].strict_mode() and val > max_value): + raise LLDPInvalidFieldValue( + 'exceeded maximum {} of {} - got ' + '{}'.format(description, max_value, val)) + + +class LLDPDUPowerViaMDIMeasure(LLDPDUGenericOrganisationSpecific): + """ + PoE TLV measurements in IEEE802.3bt-2018 + + Note: power values are expressed in units of Watts, + converted to hundredths of Watts internally; + energy values are expressed in units of Joules, + converted to tenths of kilo-Joules internally; + voltage values are expressed in units of Volts, + converted to milli-Volts internally; + current values are expressed in units of Amperes, + converted to tenths of milli-Amperes internally. + PSE price index is converted internally. + + IEEE802.3bt-2018 - sec. 79.3.8 + """ + + MEASURE_TYPE = { + (1 << 3): 'voltage', + (1 << 2): 'current', + (1 << 1): 'power', + (1 << 0): 'energy', + } + + MEASURE_SOURCE = { + 0b00: 'no request', + 0b01: 'mode A', + 0b10: 'mode B', + 0b11: 'port total', + } + + POWER_PRICE_INDEX = { + 0xffff: 'not available', + } + + @staticmethod + def _encode_ppi(val): + # IEEE802.3bt-2018 - sec. 79.3.8 + return int(75046 / 2.512 * (val ** (1 / 5)) - 10046) + + @staticmethod + def _decode_ppi(val): + # IEEE802.3bt-2018 - sec. 79.3.8 + return ((val + 10046) * 2.512 / 75046) ** 5 + + fields_desc = [ + BitEnumField('_type', 127, 7, LLDPDU.TYPES), + BitField('_length', 26, 9), + ThreeBytesField('org_code', LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3), # noqa: E501 + ByteField('subtype', 8), + FlagsField('support', 0, 4, MEASURE_TYPE), + BitEnumField('source', 0, 4, MEASURE_SOURCE), + FlagsField('request', 0, 4, MEASURE_TYPE), + FlagsField('valid', 0, 4, MEASURE_TYPE), + ScalingField('voltage_uncertainty', 0, scaling=0.001, + unit='V', ndigits=3, fmt='H'), + ScalingField('current_uncertainty', 0, scaling=0.0001, + unit='A', ndigits=4, fmt='H'), + ScalingField('power_uncertainty', 0, scaling=0.01, + unit='W', ndigits=2, fmt='H'), + ScalingField('energy_uncertainty', 0, scaling=100, + unit='J', ndigits=0, fmt='H'), + ScalingField('voltage_measurement', 0, scaling=0.001, + unit='V', ndigits=3, fmt='H'), + ScalingField('current_measurement', 0, scaling=0.0001, + unit='A', ndigits=4, fmt='H'), + ScalingField('power_measurement', 0, scaling=0.01, + unit='W', ndigits=2, fmt='H'), + ScalingField('energy_measurement', 0, scaling=100, + unit='J', ndigits=0, fmt='I'), + ShortEnumField('power_price_index', 0xffff, POWER_PRICE_INDEX), + ] + + def do_build(self): + backup_ppi = self.power_price_index + self.power_price_index = 0xffff if self.power_price_index == 0xffff \ + else LLDPDUPowerViaMDIMeasure._encode_ppi(self.power_price_index) + s = super(LLDPDUPowerViaMDIMeasure, self).do_build() + self.power_price_index = backup_ppi + return s + + def post_dissect(self, s): + s = super(LLDPDUPowerViaMDIMeasure, self).post_dissect(s) + self.power_price_index = 0xffff if self.power_price_index == 0xffff \ + else LLDPDUPowerViaMDIMeasure._decode_ppi(self.power_price_index) + return s + + @staticmethod + def _match_organization_specific(payload): + ''' + match organization specific TLV + ''' + return (orb(payload[5]) == 8 and orb(payload[1]) == 26 + and bytes_int(payload[2:5]) == + LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3) + + def _check(self): + """ + run layer specific checks + """ + if conf.contribs['LLDP'].strict_mode() and self._length != 26: + raise LLDPInvalidLengthField('length must be 26 - got ' + '{}'.format(self._length)) + # IEEE802.3bt-2018 - sec. 79.3.8 + for field, description, max_value in [('voltage_uncertainty', + 'voltage uncertainty', + 65), + ('voltage_measurement', + 'voltage measurement', + 65), + ('current_uncertainty', + 'current uncertainty', + 6.5), + ('current_measurement', + 'current measurement', + 6.5), + ('energy_uncertainty', + 'energy uncertainty', + 6500000), + ('power_uncertainty', + 'power uncertainty', + 650), + ('power_measurement', + 'power measurement', + 650)]: + val = getattr(self, field) or 0 + if (conf.contribs['LLDP'].strict_mode() and val > max_value): + raise LLDPInvalidFieldValue( + 'exceeded maximum {} of {} - got ' + '{}'.format(description, max_value, val)) + val = self.power_price_index or 0xffff + if val > 65000 and val != 0xffff: + raise LLDPInvalidFieldValue( + 'exceeded maximum power price index of {} - got ' + '{}'.format(LLDPDUPowerViaMDIMeasure._decode_ppi(65000), + LLDPDUPowerViaMDIMeasure._decode_ppi(val))) + # 0x09 .. 0x7e is reserved for future standardization and for now treated as Raw() data # noqa: E501 LLDPDU_CLASS_TYPES = { @@ -696,7 +1233,13 @@ class LLDPDUGenericOrganisationSpecific(LLDPDU): 0x06: LLDPDUSystemDescription, 0x07: LLDPDUSystemCapabilities, 0x08: LLDPDUManagementAddress, - 127: LLDPDUGenericOrganisationSpecific + 127: [ + LLDPDUPowerViaMDI, + LLDPDUPowerViaMDIDDL, + LLDPDUPowerViaMDIType34, + LLDPDUPowerViaMDIMeasure, + LLDPDUGenericOrganisationSpecific, + ] } diff --git a/scapy/contrib/loraphy2wan.py b/scapy/contrib/loraphy2wan.py new file mode 100644 index 00000000000..3750466aa95 --- /dev/null +++ b/scapy/contrib/loraphy2wan.py @@ -0,0 +1,727 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2020 Sebastien Dudek (@FlUxIuS) + +# scapy.contrib.description = LoRa PHY to WAN Layer +# scapy.contrib.status = loads + +""" +LoRa PHY to WAN Layer + +Initially developed @PentHertz +and improved at @Trend Micro + +Spec: lorawantm_specification v1.1 +""" + +from scapy.packet import Packet +from scapy.fields import ( + BitEnumField, + BitField, + BitFieldLenField, + ByteEnumField, + ByteField, + ConditionalField, + IntField, + LEShortField, + MayEnd, + MultipleTypeField, + PacketField, + PacketListField, + StrField, + StrFixedLenField, + X3BytesField, + XBitField, + XByteField, + XIntField, + XLE3BytesField, + XLEIntField, + XShortField, +) + + +class FCtrl_DownLink(Packet): + name = "FCtrl_DownLink" + fields_desc = [BitField("ADR", 0, 1), + BitField("ADRACKReq", 0, 1), + BitField("ACK", 0, 1), + BitField("FPending", 0, 1), + BitFieldLenField("FOptsLen", 0, 4)] + + def extract_padding(self, p): + return "", p + + +class FCtrl_Link(Packet): + name = "FCtrl_UpLink" + fields_desc = [BitField("ADR", 0, 1), + BitField("ADRACKReq", 0, 1), + BitField("ACK", 0, 1), + BitField("UpClassB_DownFPending", 0, 1), + BitFieldLenField("FOptsLen", 0, 4)] + + def extract_padding(self, p): + return "", p + + +class FCtrl_UpLink(Packet): + name = "FCtrl_UpLink" + fields_desc = [BitField("ADR", 0, 1), + BitField("ADRACKReq", 0, 1), + BitField("ACK", 0, 1), + BitField("ClassB", 0, 1), + BitFieldLenField("FOptsLen", 0, 4)] + + def extract_padding(self, p): + return "", p + + +class DevAddrElem(Packet): + name = "DevAddrElem" + fields_desc = [XByteField("NwkID", 0x0), + XLE3BytesField("NwkAddr", b"\x00" * 3)] + + +CIDs_up = {0x01: "ResetInd", + 0x02: "LinkCheckReq", + 0x03: "LinkADRReq", + 0x04: "DutyCycleReq", + 0x05: "RXParamSetupReq", + 0x06: "DevStatusReq", + 0x07: "NewChannelReq", + 0x08: "RXTimingSetupReq", + 0x09: "TxParamSetupReq", # LoRa 1.1 specs + 0x0A: "DlChannelReq", + 0x0B: "RekeyInd", + 0x0C: "ADRParamSetupReq", + 0x0D: "DeviceTimeReq", + 0x0E: "ForceRejoinReq", + 0x0F: "RejoinParamSetupReq"} # end of LoRa 1.1 specs + + +CIDs_down = {0x01: "ResetConf", + 0x02: "LinkCheckAns", + 0x03: "LinkADRAns", + 0x04: "DutyCycleAns", + 0x05: "RXParamSetupAns", + 0x06: "DevStatusAns", + 0x07: "NewChannelAns", + 0x08: "RXTimingSetupAns", + 0x09: "TxParamSetupAns", # LoRa 1.1 specs here + 0x0A: "DlChannelAns", + 0x0B: "RekeyConf", + 0x0C: "ADRParamSetupAns", + 0x0D: "DeviceTimeAns", + 0x0F: "RejoinParamSetupAns"} # end of LoRa 1.1 specs + + +class ResetInd(Packet): + name = "ResetInd" + fields_desc = [ByteField("Dev_version", 0)] + + +class ResetConf(Packet): + name = "ResetConf" + fields_desc = [ByteField("Serv_version", 0)] + + +class LinkCheckReq(Packet): + name = "LinkCheckReq" + + +class LinkCheckAns(Packet): + name = "LinkCheckAns" + fields_desc = [ByteField("Margin", 0), + ByteField("GwCnt", 0)] + + +class DataRate_TXPower(Packet): + name = "DataRate_TXPower" + fields_desc = [XBitField("DataRate", 0, 4), + XBitField("TXPower", 0, 4)] + + +class Redundancy(Packet): + name = "Redundancy" + fields_desc = [XBitField("RFU", 0, 1), + XBitField("ChMaskCntl", 0, 3), + XBitField("NbTrans", 0, 4)] + + +class LinkADRReq(Packet): + name = "LinkADRReq" + fields_desc = [DataRate_TXPower, + XShortField("ChMask", 0), + Redundancy] + + +class LinkADRAns_Status(Packet): + name = "LinkADRAns_Status" + fields_desc = [BitField("RFU", 0, 5), + BitField("PowerACK", 0, 1), + BitField("DataRate", 0, 1), + BitField("ChannelMaskACK", 0, 1)] + + +class LinkADRAns(Packet): + name = "LinkADRAns" + fields_desc = [PacketField("status", + LinkADRAns_Status(), + LinkADRAns_Status)] + + +class DutyCyclePL(Packet): + name = "DutyCyclePL" + fields_desc = [BitField("MaxDCycle", 0, 4)] + + +class DutyCycleReq(Packet): + name = "DutyCycleReq" + fields_desc = [DutyCyclePL] + + +class DutyCycleAns(Packet): + name = "DutyCycleAns" + fields_desc = [] + + +class DLsettings(Packet): + name = "DLsettings" + fields_desc = [BitField("OptNeg", 0, 1), + XBitField("RX1DRoffset", 0, 3), + XBitField("RX2_Data_rate", 0, 4)] + + +class RXParamSetupReq(Packet): + name = "RXParamSetupReq" + fields_desc = [DLsettings, + X3BytesField("Frequency", 0)] + + +class RXParamSetupAns_Status(Packet): + name = "RXParamSetupAns_Status" + fields_desc = [XBitField("RFU", 0, 5), + BitField("RX1DRoffsetACK", 0, 1), + BitField("RX2DatarateACK", 0, 1), + BitField("ChannelACK", 0, 1)] + + +class RXParamSetupAns(Packet): + name = "RXParamSetupAns" + fields_desc = [RXParamSetupAns_Status] + + +Battery_state = {0: "End-device connected to external source", + 255: "Battery level unknown"} + + +class DevStatusReq(Packet): + name = "DevStatusReq" + fields_desc = [ByteEnumField("Battery", 0, Battery_state), + ByteField("Margin", 0)] + + +class DevStatusAns_Status(Packet): + name = "DevStatusAns_Status" + fields_desc = [XBitField("RFU", 0, 2), + XBitField("Margin", 0, 6)] + + +class DevStatusAns(Packet): + name = "DevStatusAns" + fields_desc = [DevStatusAns_Status] + + +class DrRange(Packet): + name = "DrRange" + fields_desc = [XBitField("MaxDR", 0, 4), + XBitField("MinDR", 0, 4)] + + +class NewChannelReq(Packet): + name = "NewChannelReq" + fields_desc = [ByteField("ChIndex", 0), + X3BytesField("Freq", 0), + DrRange] + + +class NewChannelAns_Status(Packet): + name = "NewChannelAns_Status" + fields_desc = [XBitField("RFU", 0, 6), + BitField("Dataraterangeok", 0, 1), + BitField("Channelfrequencyok", 0, 1)] + + +class NewChannelAns(Packet): + name = "NewChannelAns" + fields_desc = [NewChannelAns_Status] + + +class RXTimingSetupReq_Settings(Packet): + name = "RXTimingSetupReq_Settings" + fields_desc = [XBitField("RFU", 0, 4), + XBitField("Del", 0, 4)] + + +class RXTimingSetupReq(Packet): + name = "RXTimingSetupReq" + fields_desc = [RXTimingSetupReq_Settings] + + +class RXTimingSetupAns(Packet): + name = "RXTimingSetupAns" + fields_desc = [] + + +# Specific commands for LoRa 1.1 here + +MaxEIRPs = {0: "8 dbm", + 1: "10 dbm", + 2: "12 dbm", + 3: "13 dbm", + 4: "14 dbm", + 5: "16 dbm", + 6: "18 dbm", + 7: "20 dbm", + 8: "21 dbm", + 9: "24 dbm", + 10: "26 dbm", + 11: "27 dbm", + 12: "29 dbm", + 13: "30 dbm", + 14: "33 dbm", + 15: "36 dbm"} + + +DwellTimes = {0: "No limit", + 1: "400 ms"} + + +class EIRP_DwellTime(Packet): + name = "EIRP_DwellTime" + fields_desc = [BitField("RFU", 0b0, 2), + BitEnumField("DownlinkDwellTime", 0b0, 1, DwellTimes), + BitEnumField("UplinkDwellTime", 0b0, 1, DwellTimes), + BitEnumField("MaxEIRP", 0b0000, 4, MaxEIRPs)] + + +class TxParamSetupReq(Packet): + name = "TxParamSetupReq" + fields_desc = [EIRP_DwellTime] + + +class TxParamSetupAns(Packet): + name = "TxParamSetupAns" + fields_desc = [] + + +class DlChannelReq(Packet): + name = "DlChannelReq" + fields_desc = [ByteField("ChIndex", 0), + X3BytesField("Freq", 0)] + + +class DlChannelAns(Packet): + name = "DlChannelAns" + fields_desc = [ByteField("Status", 0)] + + +class DevLoraWANversion(Packet): + name = "DevLoraWANversion" + fields_desc = [BitField("RFU", 0b0000, 4), + BitField("Minor", 0b0001, 4)] + + +class RekeyInd(Packet): + name = "RekeyInd" + fields_desc = [PacketListField("LoRaWANversion", b"", + DevLoraWANversion, length_from=lambda pkt:1)] + + +class RekeyConf(Packet): + name = "RekeyConf" + fields_desc = [ByteField("ServerVersion", 0)] + + +class ADRparam(Packet): + name = "ADRparam" + fields_desc = [BitField("Limit_exp", 0b0000, 4), + BitField("Delay_exp", 0b0000, 4)] + + +class ADRParamSetupReq(Packet): + name = "ADRParamSetupReq" + fields_desc = [ADRparam] + + +class ADRParamSetupAns(Packet): + name = "ADRParamSetupReq" + fields_desc = [] + + +class DeviceTimeReq(Packet): + name = "DeviceTimeReq" + fields_desc = [] + + +class DeviceTimeAns(Packet): + name = "DeviceTimeAns" + fields_desc = [IntField("SecondsSinceEpoch", 0), + ByteField("FracSecond", 0x00)] + + +class ForceRejoinReq(Packet): + name = "ForceRejoinReq" + fields_desc = [BitField("RFU", 0, 2), + BitField("Period", 0, 3), + BitField("Max_Retries", 0, 3), + BitField("RFU2", 0, 1), + BitField("RejoinType", 0, 3), + BitField("DR", 0, 4)] + + +class RejoinParamSetupReq(Packet): + name = "RejoinParamSetupReq" + fields_desc = [BitField("MaxTimeN", 0, 4), + BitField("MaxCountN", 0, 4)] + + +class RejoinParamSetupAns(Packet): + name = "RejoinParamSetupAns" + fields_desc = [BitField("RFU", 0, 7), + BitField("TimeOK", 0, 1)] + + +# End of specific 1.1 commands + + +class MACCommand_up(Packet): + name = "MACCommand_up" + fields_desc = [ByteEnumField("CID", 0, CIDs_up), + ConditionalField(PacketListField("Reset", b"", + ResetInd, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x01)), + ConditionalField(PacketListField("LinkCheck", b"", + LinkCheckReq, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x02)), + ConditionalField(PacketListField("LinkADR", b"", + LinkADRReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x03)), + ConditionalField(PacketListField("DutyCycle", b"", + DutyCycleReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x04)), + ConditionalField(PacketListField("RXParamSetup", b"", + RXParamSetupReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x05)), + ConditionalField(PacketListField("DevStatus", b"", + DevStatusReq, + length_from=lambda pkt:2), + lambda pkt:(pkt.CID == 0x06)), + ConditionalField(PacketListField("NewChannel", b"", + NewChannelReq, + length_from=lambda pkt:5), + lambda pkt:(pkt.CID == 0x07)), + ConditionalField(PacketListField("RXTimingSetup", b"", + RXTimingSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x08)), + # specific to 1.1 from here + ConditionalField(PacketListField("TxParamSetup", b"", + TxParamSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x09)), + ConditionalField(PacketListField("DlChannel", b"", + DlChannelReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x0A)), + ConditionalField(PacketListField("Rekey", b"", + RekeyInd, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0B)), + ConditionalField(PacketListField("ADRParamSetup", b"", + ADRParamSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0C)), + ConditionalField(PacketListField("DeviceTime", b"", + DeviceTimeReq, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x0D)), + ConditionalField(PacketListField("ForceRejoin", b"", + ForceRejoinReq, + length_from=lambda pkt:2), + lambda pkt:(pkt.CID == 0x0E)), + ConditionalField(PacketListField("RejoinParamSetup", b"", + RejoinParamSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0F))] + + # pylint: disable=R0201 + def extract_padding(self, p): + return "", p + + +class MACCommand_down(Packet): + name = "MACCommand_down" + fields_desc = [ByteEnumField("CID", 0, CIDs_up), + ConditionalField(PacketListField("Reset", b"", + ResetConf, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x01)), + ConditionalField(PacketListField("LinkCheck", b"", + LinkCheckAns, + length_from=lambda pkt:2), + lambda pkt:(pkt.CID == 0x02)), + ConditionalField(PacketListField("LinkADR", b"", + LinkADRAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x03)), + ConditionalField(PacketListField("DutyCycle", b"", + DutyCycleAns, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x04)), + ConditionalField(PacketListField("RXParamSetup", b"", + RXParamSetupAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x05)), + ConditionalField(PacketListField("DevStatusAns", b"", + RXParamSetupAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x06)), + ConditionalField(PacketListField("NewChannel", b"", + NewChannelAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x07)), + ConditionalField(PacketListField("RXTimingSetup", b"", + RXTimingSetupAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x08)), + ConditionalField(PacketListField("TxParamSetup", b"", + TxParamSetupAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x09)), + ConditionalField(PacketListField("DlChannel", b"", + DlChannelAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0A)), + ConditionalField(PacketListField("Rekey", b"", + RekeyConf, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0B)), + ConditionalField(PacketListField("ADRParamSetup", b"", + ADRParamSetupAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x0C)), + ConditionalField(PacketListField("DeviceTime", b"", + DeviceTimeAns, + length_from=lambda pkt:5), + lambda pkt:(pkt.CID == 0x0D)), + ConditionalField(PacketListField("RejoinParamSetup", b"", + RejoinParamSetupAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0F))] + + +class FOpts(Packet): + name = "FOpts" + fields_desc = [ConditionalField(PacketListField("FOpts_up", b"", + # UL piggy MAC Command + MACCommand_up, + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), # noqa: E501 + lambda pkt:(pkt.FCtrl[0].FOptsLen > 0 and + pkt.MType & 0b1 == 0 and + pkt.MType >= 0b010)), + ConditionalField(PacketListField("FOpts_down", b"", + # DL piggy MAC Command + MACCommand_down, + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), # noqa: E501 + lambda pkt:(pkt.FCtrl[0].FOptsLen > 0 and + pkt.MType & 0b1 == 1 and + pkt.MType <= 0b101))] + + +def FOptsDownShow(pkt): + try: + if pkt.FCtrl[0].FOptsLen > 0 and pkt.MType & 0b1 == 1 and pkt.MType <= 0b101 and (pkt.MType & 0b101 > 0): # noqa: E501 + return True + return False + except Exception: + return False + + +def FOptsUpShow(pkt): + try: + if pkt.FCtrl[0].FOptsLen > 0 and pkt.MType & 0b1 == 0 and pkt.MType >= 0b010 and (pkt.MType & 0b110 > 0): # noqa: E501 + return True + return False + except Exception: + return False + + +class FHDR(Packet): + name = "FHDR" + fields_desc = [ConditionalField(PacketListField("DevAddr", b"", DevAddrElem, # noqa: E501 + length_from=lambda pkt:4), + lambda pkt:(pkt.MType >= 0b010 and + pkt.MType <= 0b101)), + ConditionalField(PacketListField("FCtrl", b"", + FCtrl_Link, + length_from=lambda pkt:1), + lambda pkt:((pkt.MType & 0b1 == 1 and + pkt.MType <= 0b101 and + (pkt.MType & 0b10 > 0)) or + (pkt.MType & 0b1 == 0 and + pkt.MType >= 0b010))), + ConditionalField(LEShortField("FCnt", 0), + lambda pkt:(pkt.MType >= 0b010 and + pkt.MType <= 0b101)), + ConditionalField(PacketListField("FOpts_up", b"", + MACCommand_up, + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), # noqa: E501 + FOptsUpShow), + ConditionalField(PacketListField("FOpts_down", b"", + MACCommand_down, + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), # noqa: E501 + FOptsDownShow)] + + +FPorts = {0: "NwkSKey"} # anything else is AppSKey + + +JoinReqTypes = {0xFF: "Join-request", + 0x00: "Rejoin-request type 0", + 0x01: "Rejoin-request type 1", + 0x02: "Rejoin-request type 2"} + + +class Join_Request(Packet): + name = "Join_Request" + fields_desc = [StrFixedLenField("AppEUI", b"\x00" * 8, 8), + StrFixedLenField("DevEUI", b"\00" * 8, 8), + LEShortField("DevNonce", 0x0000)] + + +class Join_Accept(Packet): + name = "Join_Accept" + dcflist = False + fields_desc = [XLE3BytesField("JoinAppNonce", 0), + XLE3BytesField("NetID", 0), + XLEIntField("DevAddr", 0), + DLsettings, + XByteField("RxDelay", 0), + ConditionalField(StrFixedLenField("CFList", b"\x00" * 16, 16), # noqa: E501 + lambda pkt:(Join_Accept.dcflist is True))] + + def extract_padding(self, p): + return "", p + + def __init__(self, packet=""): # CFList calculated with rest of packet len + if len(packet) > 18: + Join_Accept.dcflist = True + super(Join_Accept, self).__init__(packet) + + +RejoinType = {0: "NetID+DevEUI", + 1: "JoinEUI+DevEUI", + 2: "NetID+DevEUI"} + + +class RejoinReq(Packet): # LoRa 1.1 specs + name = "RejoinReq" + fields_desc = [ByteField("Type", 0), + X3BytesField("NetID", 0), + StrFixedLenField("DevEUI", b"\x00" * 8), + XShortField("RJcount0", 0)] + + +def dpload_type(pkt): + if (pkt.MType == 0b101 or pkt.MType == 0b011): + return 0 # downlink + elif (pkt.MType == 0b100 or pkt.MType == 0b010): + return 1 # uplink + return None + + +datapayload_list = [(StrField("DataPayload", "", remain=4), + lambda pkt:(dpload_type(pkt) == 0)), + (StrField("DataPayload", "", remain=6), + lambda pkt:(dpload_type(pkt) == 1))] + + +class FRMPayload(Packet): + name = "FRMPayload" + fields_desc = [ConditionalField(MultipleTypeField(datapayload_list, + StrField("DataPayload", + "", remain=4)), + lambda pkt:(dpload_type(pkt) is not None)), + ConditionalField(PacketListField("Join_Request_Field", b"", + Join_Request, + length_from=lambda pkt:18), + lambda pkt:(pkt.MType == 0b000)), + ConditionalField(PacketListField("Join_Accept_Field", b"", + Join_Accept, + count_from=lambda pkt:1), + lambda pkt:(pkt.MType == 0b001 and + LoRa.encrypted is False)), + ConditionalField(StrField("Join_Accept_Encrypted", 0), + lambda pkt:(pkt.MType == 0b001 and LoRa.encrypted is True)), # noqa: E501 + ConditionalField(PacketListField("ReJoin_Request_Field", b"", # noqa: E501 + RejoinReq, + length_from=lambda pkt:14), + lambda pkt:(pkt.MType == 0b111))] + + +class MACPayload(Packet): + name = "MACPayload" + eFPort = False + fields_desc = [FHDR, + ConditionalField(ByteEnumField("FPort", 0, FPorts), + lambda pkt:(pkt.MType >= 0b010 and + pkt.MType <= 0b101 and + pkt.FCtrl[0].FOptsLen == 0)), + FRMPayload] + + +MTypes = {0b000: "Join-request", + 0b001: "Join-accept", + 0b010: "Unconfirmed Data Up", + 0b011: "Unconfirmed Data Down", + 0b100: "Confirmed Data Up", + 0b101: "Confirmed Data Down", + 0b110: "Rejoin-request", # Only in LoRa 1.1 specs + 0b111: "Proprietary"} + + +class MHDR(Packet): # Same for 1.0 as for 1.1 + name = "MHDR" + + fields_desc = [BitEnumField("MType", 0b000, 3, MTypes), + BitField("RFU", 0b000, 3), + BitField("Major", 0b00, 2)] + + +class PHYPayload(Packet): + name = "PHYPayload" + fields_desc = [MHDR, + MACPayload, + MayEnd(ConditionalField(XIntField("MIC", 0), + lambda pkt: (pkt.MType != 0b001 or + LoRa.encrypted is False)))] + + +class LoRa(Packet): # default frame (unclear specs => taken from https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5677147/) # noqa: E501 + name = "LoRa" + version = "1.1" # default version to parse + encrypted = True + + fields_desc = [XBitField("Preamble", 0, 4), + XBitField("PHDR", 0, 16), + XBitField("PHDR_CRC", 0, 4), + PHYPayload, + ConditionalField(XShortField("CRC", 0), + lambda pkt:(pkt.MType & 0b1 == 0))] diff --git a/scapy/contrib/ltp.py b/scapy/contrib/ltp.py index 875e62c0a6e..bf2e903ab52 100755 --- a/scapy/contrib/ltp.py +++ b/scapy/contrib/ltp.py @@ -1,32 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright 2012 (C) The MITRE Corporation """ - Copyright 2012, The MITRE Corporation:: - - NOTICE +.. centered:: + NOTICE This software/technical data was produced for the U.S. Government under Prime Contract No. NASA-03001 and JPL Contract No. 1295026 - and is subject to FAR 52.227-14 (6/87) Rights in Data General, - and Article GP-51, Rights in Data General, respectively. - This software is publicly released under MITRE case #12-3054 + and is subject to FAR 52.227-14 (6/87) Rights in Data General, + and Article GP-51, Rights in Data General, respectively. + This software is publicly released under MITRE case #12-3054 """ # scapy.contrib.description = Licklider Transmission Protocol (LTP) # scapy.contrib.status = loads -import scapy.modules.six as six from scapy.packet import Packet, bind_layers, bind_top_down from scapy.fields import BitEnumField, BitField, BitFieldLenField, \ ByteEnumField, ConditionalField, PacketListField, StrLenField @@ -110,7 +99,7 @@ def default_payload_class(self, pay): def _ltp_guess_payload(pkt, *args): - for k, v in six.iteritems(_ltp_payload_conditions): + for k, v in _ltp_payload_conditions.items(): if v(pkt): return k return conf.raw_layer @@ -140,8 +129,12 @@ class LTP(Packet): # ConditionalField(SDNV2("CheckpointSerialNo", 0), lambda x: x.flags in _ltp_checkpoint_segment), + # + # For segments that are checkpoints or reception reports. + # ConditionalField(SDNV2("ReportSerialNo", 0), - lambda x: x.flags in _ltp_checkpoint_segment), + lambda x: x.flags in _ltp_checkpoint_segment \ + or x.flags == 8), # # Then comes the actual payload for data carrying segments. # @@ -154,10 +147,9 @@ class LTP(Packet): ConditionalField(SDNV2("RA_ReportSerialNo", 0), lambda x: x.flags == 9), # - # Reception reports have the following fields. + # Reception reports have the following fields, + # excluding ReportSerialNo defined above. # - ConditionalField(SDNV2("ReportSerialNo", 0), - lambda x: x.flags == 8), ConditionalField(SDNV2("ReportCheckpointSerialNo", 0), lambda x: x.flags == 8), ConditionalField(SDNV2("ReportUpperBound", 0), @@ -179,13 +171,6 @@ class LTP(Packet): 15, _ltp_cancel_reasons), lambda x: x.flags == 14), # - # Cancellation Acknowldgements - # - ConditionalField(SDNV2("CancelAckToBlockSender", 0), - lambda x: x.flags == 13), - ConditionalField(SDNV2("CancelAckToBlockReceiver", 0), - lambda x: x.flags == 15), - # # Finally, trailing extensions # PacketListField("TrailerExtensions", [], LTPex, count_from=lambda x: x.TrailerExtensionCount) # noqa: E501 diff --git a/scapy/contrib/mac_control.py b/scapy/contrib/mac_control.py index bed2e0d7776..af2e2a8f1c3 100644 --- a/scapy/contrib/mac_control.py +++ b/scapy/contrib/mac_control.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = MACControl # scapy.contrib.status = loads @@ -6,17 +10,6 @@ ~~~~~~~~~~ :author: Thomas Tannhaeuser, hecke@naberius.de - :license: GPLv2 - - This module is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This module is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. :description: @@ -45,7 +38,7 @@ from scapy.layers.l2 import Ether, Dot1Q, bind_layers MAC_CONTROL_ETHER_TYPE = 0x8808 -ETHER_TYPES['MAC_CONTROL'] = MAC_CONTROL_ETHER_TYPE +ETHER_TYPES[MAC_CONTROL_ETHER_TYPE] = 'MAC_CONTROL' ETHER_SPEED_MBIT_10 = 0x01 ETHER_SPEED_MBIT_100 = 0x02 diff --git a/scapy/contrib/macsec.py b/scapy/contrib/macsec.py index 64ae04542af..f3e75d61afa 100755 --- a/scapy/contrib/macsec.py +++ b/scapy/contrib/macsec.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Sabrina Dubroca -# This program is published under a GPLv2 license # scapy.contrib.description = 802.1AE - IEEE MAC Security standard (MACsec) # scapy.contrib.status = loads @@ -10,8 +10,6 @@ Classes and functions for MACsec. """ -from __future__ import absolute_import -from __future__ import print_function import struct import copy @@ -26,7 +24,6 @@ from scapy.compat import raw from scapy.data import ETH_P_MACSEC, ETHER_TYPES, ETH_P_IP, ETH_P_IPV6 from scapy.error import log_loading -import scapy.modules.six as six if conf.crypto_valid: from cryptography.hazmat.backends import default_backend @@ -36,7 +33,7 @@ modes, ) else: - log_loading.info("Can't import python-cryptography v1.7+. " + log_loading.info("Can't import python-cryptography v2.0+. " "Disabled MACsec encryption/authentication.") @@ -52,7 +49,7 @@ class MACsecSA(object): of MACsec frames """ def __init__(self, sci, an, pn, key, icvlen, encrypt, send_sci, xpn_en=False, ssci=None, salt=None): # noqa: E501 - if isinstance(sci, six.integer_types): + if isinstance(sci, int): self.sci = struct.pack('!Q', sci) elif isinstance(sci, bytes): self.sci = sci @@ -67,7 +64,7 @@ def __init__(self, sci, an, pn, key, icvlen, encrypt, send_sci, xpn_en=False, ss self.xpn_en = xpn_en if self.xpn_en: # Get SSCI (32 bits) - if isinstance(ssci, six.integer_types): + if isinstance(ssci, int): self.ssci = struct.pack('!L', ssci) elif isinstance(ssci, bytes): self.ssci = ssci @@ -82,11 +79,11 @@ def __init__(self, sci, an, pn, key, icvlen, encrypt, send_sci, xpn_en=False, ss def make_iv(self, pkt): """generate an IV for the packet""" if self.xpn_en: - tmp_pn = (self.pn & 0xFFFFFFFF00000000) | (pkt[MACsec].pn & 0xFFFFFFFF) # noqa: E501 + tmp_pn = (self.pn & 0xFFFFFFFF00000000) | (pkt[MACsec].PN & 0xFFFFFFFF) # noqa: E501 tmp_iv = self.ssci + struct.pack('!Q', tmp_pn) return bytes(bytearray([a ^ b for a, b in zip(bytearray(tmp_iv), bytearray(self.salt))])) # noqa: E501 else: - return self.sci + struct.pack('!I', pkt[MACsec].pn) + return self.sci + struct.pack('!I', pkt[MACsec].PN) @staticmethod def split_pkt(pkt, assoclen, icvlen=0): @@ -127,11 +124,11 @@ def encap(self, pkt): hdr = copy.deepcopy(pkt) payload = hdr.payload del hdr.payload - tag = MACsec(sci=self.sci, an=self.an, + tag = MACsec(SCI=self.sci, AN=self.an, SC=self.send_sci, E=self.e_bit(), C=self.c_bit(), - shortlen=MACsecSA.shortlen(pkt), - pn=(self.pn & 0xFFFFFFFF), type=pkt.type) + SL=MACsecSA.shortlen(pkt), + PN=(self.pn & 0xFFFFFFFF), type=pkt.type) hdr.type = ETH_P_MACSEC return hdr / tag / payload @@ -139,8 +136,11 @@ def encap(self, pkt): # encap(), it is def decap(self, orig_pkt): """decapsulate a MACsec frame""" - if orig_pkt.name != Ether().name or orig_pkt.payload.name != MACsec().name: # noqa: E501 - raise TypeError('cannot decapsulate MACsec packet, must be Ethernet/MACsec') # noqa: E501 + if not isinstance(orig_pkt, Ether) or \ + not isinstance(orig_pkt.payload, MACsec): + raise TypeError( + 'cannot decapsulate MACsec packet, must be Ethernet/MACsec' + ) packet = copy.deepcopy(orig_pkt) prev_layer = packet[MACsec].underlayer prev_layer.type = packet[MACsec].type @@ -210,24 +210,40 @@ def decrypt(self, orig_pkt, assoclen=None): class MACsec(Packet): """representation of one MACsec frame""" name = '802.1AE' - fields_desc = [BitField('Ver', 0, 1), - BitField('ES', 0, 1), - BitField('SC', 0, 1), - BitField('SCB', 0, 1), - BitField('E', 0, 1), - BitField('C', 0, 1), - BitField('an', 0, 2), - BitField('reserved', 0, 2), - BitField('shortlen', 0, 6), - IntField("pn", 1), - ConditionalField(PacketField("sci", None, MACsecSCI), lambda pkt: pkt.SC), # noqa: E501 - ConditionalField(XShortEnumField("type", None, ETHER_TYPES), - lambda pkt: pkt.type is not None)] + deprecated_fields = { + 'an': ("AN", "2.4.4"), + 'pn': ("PN", "2.4.4"), + 'sci': ("SCI", "2.4.4"), + 'shortlen': ("SL", "2.4.4"), + } + # 802.1AE-2018 - Section 9 + fields_desc = [ + # 802.1AE-2018 - Section 9.5 + BitField('Ver', 0, 1), + BitField('ES', 0, 1), # End Station + BitField('SC', 0, 1), # Secure Channel + BitField('SCB', 0, 1), # Single Copy Broadcast + BitField('E', 0, 1), # Encryption + BitField('C', 0, 1), # Changed Text + BitField('AN', 0, 2), # Association Number + # 802.1AE-2018 - Section 9.7 + BitField('reserved', 0, 2), + BitField('SL', 0, 6), # Short Length + # 802.1AE-2018 - Section 9.8 + IntField("PN", 1), # Packet Number + # 802.1AE-2018 - Section 9.9 + ConditionalField( + PacketField("SCI", None, MACsecSCI), + lambda pkt: pkt.SC + ), + # Off-spec. Used for conveniency (only present if passed manually) + ConditionalField(XShortEnumField("type", None, ETHER_TYPES), + lambda pkt: "type" in pkt.fields)] def mysummary(self): - summary = self.sprintf("an=%MACsec.an%, pn=%MACsec.pn%") + summary = self.sprintf("AN=%MACsec.AN%, PN=%MACsec.PN%") if self.SC: - summary += self.sprintf(", sci=%MACsec.sci%") + summary += self.sprintf(", SCI=%MACsec.SCI%") if self.type is not None: summary += self.sprintf(", %MACsec.type%") return summary diff --git a/scapy/contrib/metawatch.py b/scapy/contrib/metawatch.py new file mode 100644 index 00000000000..e09670742c0 --- /dev/null +++ b/scapy/contrib/metawatch.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# Copyright (C) 2019 Brandon Ewing +# 2019 Guillaume Valadon + +# scapy.contrib.description = Arista Metawatch +# scapy.contrib.status = loads + +from scapy.layers.l2 import Ether +from scapy.fields import ( + ByteField, + ShortField, + FlagsField, + SecondsIntField, + TrailerField, + UTCTimeField, +) + + +class MetawatchEther(Ether): + name = "Ethernet (with MetaWatch trailer)" + match_subclass = True + fields_desc = Ether.fields_desc + [ + TrailerField(ByteField("metamako_portid", None)), + TrailerField(ShortField("metamako_devid", None)), + TrailerField(FlagsField("metamako_flags", 0x0, 8, "VX______")), + TrailerField(SecondsIntField("metamako_nanos", 0, use_nano=True)), + TrailerField(UTCTimeField("metamako_seconds", 0)), + # TODO: Add TLV support + ] diff --git a/scapy/contrib/modbus.py b/scapy/contrib/modbus.py index 9d96c802570..9e9cd2ed915 100644 --- a/scapy/contrib/modbus.py +++ b/scapy/contrib/modbus.py @@ -1,24 +1,14 @@ -# coding: utf8 - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright (C) 2017 Arthur Gervais +# Ken LE PRADO, +# Sebastien Mainand +# Thomas Aurel # scapy.contrib.description = ModBus Protocol # scapy.contrib.status = loads -# Copyright (C) 2017 Arthur Gervais, Ken LE PRADO, Sébastien Mainand, -# Thomas Aurel import struct @@ -112,7 +102,8 @@ class ModbusPDU03ReadHoldingRegistersResponse(Packet): adjust=lambda pkt, x: x * 2), FieldListField("registerVal", [0x0000], ShortField("", 0x0000), - count_from=lambda pkt: pkt.byteCount)] + count_from=lambda pkt: pkt.byteCount, + max_count=123)] class ModbusPDU03ReadHoldingRegistersError(Packet): @@ -694,7 +685,7 @@ class ModbusPDUReservedFunctionCodeRequest(_ModbusPDUNoPayload): name = "Reserved Function Code Request" fields_desc = [ ByteEnumField("funcCode", 0x00, _reserved_funccode_request), - StrFixedLenField('payload', '', 255), ] + StrFixedLenField('mb_payload', '', 255), ] def mysummary(self): return self.sprintf("Modbus Reserved Request %funcCode%") @@ -704,7 +695,7 @@ class ModbusPDUReservedFunctionCodeResponse(_ModbusPDUNoPayload): name = "Reserved Function Code Response" fields_desc = [ ByteEnumField("funcCode", 0x00, _reserved_funccode_response), - StrFixedLenField('payload', '', 255), ] + StrFixedLenField('mb_payload', '', 255), ] def mysummary(self): return self.sprintf("Modbus Reserved Response %funcCode%") @@ -714,7 +705,7 @@ class ModbusPDUReservedFunctionCodeError(_ModbusPDUNoPayload): name = "Reserved Function Code Error" fields_desc = [ ByteEnumField("funcCode", 0x00, _reserved_funccode_error), - StrFixedLenField('payload', '', 255), ] + StrFixedLenField('mb_payload', '', 255), ] def mysummary(self): return self.sprintf("Modbus Reserved Error %funcCode%") @@ -750,7 +741,7 @@ class ModbusPDUUserDefinedFunctionCodeRequest(_ModbusPDUNoPayload): ModbusByteEnumField( "funcCode", 0x00, _userdefined_funccode_request, "Unknown user-defined request function Code"), - StrFixedLenField('payload', '', 255), ] + StrFixedLenField('mb_payload', '', 255), ] def mysummary(self): return self.sprintf("Modbus User-Defined Request %funcCode%") @@ -762,7 +753,7 @@ class ModbusPDUUserDefinedFunctionCodeResponse(_ModbusPDUNoPayload): ModbusByteEnumField( "funcCode", 0x00, _userdefined_funccode_response, "Unknown user-defined response function Code"), - StrFixedLenField('payload', '', 255), ] + StrFixedLenField('mb_payload', '', 255), ] def mysummary(self): return self.sprintf("Modbus User-Defined Response %funcCode%") @@ -774,7 +765,7 @@ class ModbusPDUUserDefinedFunctionCodeError(_ModbusPDUNoPayload): ModbusByteEnumField( "funcCode", 0x00, _userdefined_funccode_error, "Unknown user-defined error function Code"), - StrFixedLenField('payload', '', 255), ] + StrFixedLenField('mb_payload', '', 255), ] def mysummary(self): return self.sprintf("Modbus User-Defined Error %funcCode%") @@ -821,7 +812,7 @@ def guess_payload_class(self, payload): 0x87: ModbusPDU07ReadExceptionStatusError, 0x88: ModbusPDU08DiagnosticsError, 0x8B: ModbusPDU0BGetCommEventCounterError, - 0x0C: ModbusPDU0CGetCommEventLogError, + 0x8C: ModbusPDU0CGetCommEventLogError, 0x8F: ModbusPDU0FWriteMultipleCoilsError, 0x90: ModbusPDU10WriteMultipleRegistersError, 0x91: ModbusPDU11ReportSlaveIdError, @@ -840,8 +831,8 @@ def guess_payload_class(self, payload): 0x05: ModbusPDU05WriteSingleCoilResponse, 0x06: ModbusPDU06WriteSingleRegisterResponse, 0x07: ModbusPDU07ReadExceptionStatusResponse, - 0x88: ModbusPDU08DiagnosticsResponse, - 0x8B: ModbusPDU0BGetCommEventCounterRequest, + 0x08: ModbusPDU08DiagnosticsResponse, + 0x0B: ModbusPDU0BGetCommEventCounterResponse, 0x0C: ModbusPDU0CGetCommEventLogResponse, 0x0F: ModbusPDU0FWriteMultipleCoilsResponse, 0x10: ModbusPDU10WriteMultipleRegistersResponse, diff --git a/scapy/contrib/mount.py b/scapy/contrib/mount.py index 5ad8ee1001b..9f469e849c0 100644 --- a/scapy/contrib/mount.py +++ b/scapy/contrib/mount.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Lucas Preston -# This program is published under a GPLv2 license # scapy.contrib.description = NFS Mount v3 # scapy.contrib.status = loads diff --git a/scapy/contrib/mpls.py b/scapy/contrib/mpls.py index e5235b4beee..0808b8a70b0 100644 --- a/scapy/contrib/mpls.py +++ b/scapy/contrib/mpls.py @@ -1,27 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Multiprotocol Label Switching (MPLS) # scapy.contrib.status = loads from scapy.packet import Packet, bind_layers, Padding -from scapy.fields import BitField, ByteField, ShortField -from scapy.layers.inet import IP, UDP -from scapy.contrib.bier import BIER +from scapy.fields import ( + BitField, + ByteField, + ByteEnumField, + PacketListField, + ShortField, +) + +from scapy.layers.inet import ( + _ICMP_classnums, + ICMPExtension_Object, + IP, + UDP, +) from scapy.layers.inet6 import IPv6 from scapy.layers.l2 import Ether, GRE -from scapy.compat import orb + +from scapy.contrib.bier import BIER class EoMCW(Packet): @@ -47,7 +49,7 @@ def guess_payload_class(self, payload): if len(payload) >= 1: if not self.s: return MPLS - ip_version = (orb(payload[0]) >> 4) & 0xF + ip_version = (payload[0] >> 4) & 0xF if ip_version == 4: return IP elif ip_version == 5: @@ -55,13 +57,28 @@ def guess_payload_class(self, payload): elif ip_version == 6: return IPv6 else: - if orb(payload[0]) == 0 and orb(payload[1]) == 0: + if payload[0] == 0 and payload[1] == 0: return EoMCW else: return Ether return Padding +# ICMP Extension + +class ICMPExtension_MPLS(ICMPExtension_Object): + name = "ICMP Extension Object - MPLS (RFC4950)" + + fields_desc = [ + ShortField("len", None), + ByteEnumField("classnum", 1, _ICMP_classnums), + ByteField("classtype", 1), + PacketListField("stack", [], MPLS, length_from=lambda pkt: pkt.len - 4), + ] + + +# Bindings + bind_layers(Ether, MPLS, type=0x8847) bind_layers(IP, MPLS, proto=137) bind_layers(IPv6, MPLS, nh=137) diff --git a/scapy/contrib/mqtt.py b/scapy/contrib/mqtt.py index f66a5a8bb88..afd75e78a02 100644 --- a/scapy/contrib/mqtt.py +++ b/scapy/contrib/mqtt.py @@ -1,14 +1,23 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Santiago Hernandez Ramos -# This program is published under GPLv2 license # scapy.contrib.description = Message Queuing Telemetry Transport (MQTT) # scapy.contrib.status = loads from scapy.packet import Packet, bind_layers -from scapy.fields import FieldLenField, BitEnumField, StrLenField, \ - ShortField, ConditionalField, ByteEnumField, ByteField, PacketListField +from scapy.fields import ( + BitEnumField, + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + PacketListField, + ShortField, + StrLenField, +) from scapy.layers.inet import TCP from scapy.error import Scapy_Exception from scapy.compat import orb, chb @@ -158,6 +167,11 @@ class MQTTConnect(Packet): ] +class MQTTDisconnect(Packet): + name = "MQTT disconnect" + fields_desc = [] + + RETURN_CODE = { 0: 'Connection Accepted', 1: 'Unacceptable protocol version', @@ -187,8 +201,9 @@ class MQTTPublish(Packet): lambda pkt: (pkt.underlayer.QOS == 1 or pkt.underlayer.QOS == 2)), StrLenField("value", "", - length_from=lambda pkt: (pkt.underlayer.len - - pkt.length - 2)), + length_from=lambda pkt: pkt.underlayer.len - pkt.length - 2 + if pkt.underlayer.QOS == 0 else + pkt.underlayer.len - pkt.length - 4) ] @@ -220,22 +235,42 @@ class MQTTPubcomp(Packet): ] +class MQTTTopic(Packet): + name = "MQTT topic" + fields_desc = [ + FieldLenField("length", None, length_of="topic"), + StrLenField("topic", "", length_from=lambda pkt:pkt.length) + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class MQTTTopicQOS(MQTTTopic): + fields_desc = MQTTTopic.fields_desc + [ByteEnumField("QOS", 0, QOS_LEVEL)] + + class MQTTSubscribe(Packet): name = "MQTT subscribe" fields_desc = [ ShortField("msgid", None), - FieldLenField("length", None, length_of="topic"), - StrLenField("topic", "", - length_from=lambda pkt: pkt.length), - ByteEnumField("QOS", 0, QOS_LEVEL), + PacketListField("topics", [], pkt_cls=MQTTTopicQOS) ] ALLOWED_RETURN_CODE = { - 0: 'Success', - 1: 'Success', - 2: 'Success', - 128: 'Failure' + 0x00: 'Granted QoS 0', + 0x01: 'Granted QoS 1', + 0x02: 'Granted QoS 2', + 0x80: 'Unspecified error', + 0x83: 'Implementation specific error', + 0x87: 'Not authorized', + 0x8F: 'Topic Filter invalid', + 0x91: 'Packet Identifier in use', + 0x97: 'Quota exceeded', + 0x9E: 'Shared Subscriptions not supported', + 0xA1: 'Subscription Identifiers not supported', + 0xA2: 'Wildcard Subscriptions not supported', } @@ -243,36 +278,15 @@ class MQTTSuback(Packet): name = "MQTT suback" fields_desc = [ ShortField("msgid", None), - ByteEnumField("retcode", None, ALLOWED_RETURN_CODE) - ] - - -class MQTTTopic(Packet): - name = "MQTT topic" - fields_desc = [ - FieldLenField("len", None, length_of="topic"), - StrLenField("topic", "", length_from=lambda pkt:pkt.len) + FieldListField("retcodes", None, ByteEnumField("", None, ALLOWED_RETURN_CODE)) ] - def guess_payload_class(self, payload): - return conf.padding_layer - - -def cb_topic(pkt, lst, cur, remain): - """ - Decode the remaining bytes as a MQTT topic - """ - if len(remain) > 3: - return MQTTTopic - else: - return conf.raw_layer - class MQTTUnsubscribe(Packet): name = "MQTT unsubscribe" fields_desc = [ ShortField("msgid", None), - PacketListField("topics", [], next_cls_cb=cb_topic) + PacketListField("topics", [], pkt_cls=MQTTTopic) ] @@ -298,6 +312,7 @@ class MQTTUnsuback(Packet): bind_layers(MQTT, MQTTSuback, type=9) bind_layers(MQTT, MQTTUnsubscribe, type=10) bind_layers(MQTT, MQTTUnsuback, type=11) +bind_layers(MQTT, MQTTDisconnect, type=14) bind_layers(MQTTConnect, MQTT) bind_layers(MQTTConnack, MQTT) bind_layers(MQTTPublish, MQTT) @@ -309,3 +324,4 @@ class MQTTUnsuback(Packet): bind_layers(MQTTSuback, MQTT) bind_layers(MQTTUnsubscribe, MQTT) bind_layers(MQTTUnsuback, MQTT) +bind_layers(MQTTDisconnect, MQTT) diff --git a/scapy/contrib/mqttsn.py b/scapy/contrib/mqttsn.py index df74805e03f..681465e4afa 100644 --- a/scapy/contrib/mqttsn.py +++ b/scapy/contrib/mqttsn.py @@ -1,10 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) 2019 Freie Universitaet Berlin -# This program is published under GPLv2 license -# -# Specification: -# http://www.mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf + +""" +MQTT for Sensor Networks (MQTT-SN) + +Specification: +http://www.mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf +""" + # scapy.contrib.description = MQTT for Sensor Networks (MQTT-SN) # scapy.contrib.status = loads diff --git a/scapy/contrib/nfs.py b/scapy/contrib/nfs.py index 974adc44af0..faaa431f1ef 100644 --- a/scapy/contrib/nfs.py +++ b/scapy/contrib/nfs.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Lucas Preston -# This program is published under a GPLv2 license # scapy.contrib.description = Network File System (NFS) v3 # scapy.contrib.status = loads @@ -12,7 +12,6 @@ from scapy.fields import IntField, IntEnumField, FieldListField, LongField, \ XIntField, XLongField, ConditionalField, PacketListField, StrLenField, \ PacketField -from scapy.modules.six import integer_types nfsstat3 = { 0: 'NFS3_OK', @@ -58,7 +57,7 @@ def loct(x): - if isinstance(x, integer_types): + if isinstance(x, int): return oct(x) if isinstance(x, tuple): return "(%s)" % ", ".join(map(loct, x)) @@ -442,7 +441,7 @@ class READDIRPLUS_Reply(Packet): ), ConditionalField( PacketListField( - 'files', None, cls=File_From_Dir_Plus, + 'files', None, File_From_Dir_Plus, next_cls_cb=lambda pkt, lst, cur, remain: File_From_Dir_Plus if pkt.value_follows == 1 and (len(lst) == 0 or cur.value_follows == 1) and @@ -722,7 +721,7 @@ class READDIR_Reply(Packet): ), ConditionalField( PacketListField( - 'files', None, cls=File_From_Dir, + 'files', None, File_From_Dir, next_cls_cb=lambda pkt, lst, cur, remain: File_From_Dir if pkt.value_follows == 1 and (len(lst) == 0 or cur.value_follows == 1) and diff --git a/scapy/contrib/nlm.py b/scapy/contrib/nlm.py index 5f296839629..bd59bdba350 100644 --- a/scapy/contrib/nlm.py +++ b/scapy/contrib/nlm.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Lucas Preston -# This program is published under a GPLv2 license # scapy.contrib.description = Network Lock Manager (NLM) v4 # scapy.contrib.status = loads diff --git a/scapy/contrib/nrf_sniffer.py b/scapy/contrib/nrf_sniffer.py new file mode 100644 index 00000000000..86ba91dfce2 --- /dev/null +++ b/scapy/contrib/nrf_sniffer.py @@ -0,0 +1,154 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Michael Farrell + +""" +nRF sniffer + +Firmware and documentation related to this module is available at: +https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Sniffer +https://github.com/adafruit/Adafruit_BLESniffer_Python +https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-nordic_ble.c +""" + +# scapy.contrib.description = nRF sniffer +# scapy.contrib.status = works + +import struct + +from scapy.config import conf +from scapy.data import DLT_NORDIC_BLE +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + LEIntField, + LEShortField, + LenField, + ScalingField, +) +from scapy.layers.bluetooth4LE import BTLE +from scapy.packet import Packet, bind_layers + + +# nRF Sniffer v2 + + +class NRFS2_Packet(Packet): + """ + nRF Sniffer v2 Packet + """ + + fields_desc = [ + LenField("len", None, fmt=". +# See https://scapy.net/ for more information # scapy.contrib.description = Network Services Headers (NSH) # scapy.contrib.status = loads @@ -37,7 +27,7 @@ class NSHTLV(Packet): "NSH MD-type 2 - Variable Length Context Headers" name = "NSHTLV" fields_desc = [ - ShortField('class', 0), + ShortField('class_', 0), BitField('type', 0, 8), BitField('reserved', 0, 1), BitField('length', 0, 7), @@ -85,11 +75,11 @@ def mysummary(self): bind_layers(Ether, NSH, {'type': 0x894F}, type=0x894F) -bind_layers(VXLAN, NSH, {'flags': 0xC, 'nextprotocol': 4}, nextprotocol=4) +bind_layers(VXLAN, NSH, {'flags': 0xC, 'NextProtocol': 4}, NextProtocol=4) bind_layers(GRE, NSH, {'proto': 0x894F}, proto=0x894F) -bind_layers(NSH, IP, {'nextprotocol': 1}, nextprotocol=1) -bind_layers(NSH, IPv6, {'nextprotocol': 2}, nextprotocol=2) -bind_layers(NSH, Ether, {'nextprotocol': 3}, nextprotocol=3) -bind_layers(NSH, NSH, {'nextprotocol': 4}, nextprotocol=4) -bind_layers(NSH, MPLS, {'nextprotocol': 5}, nextprotocol=5) +bind_layers(NSH, IP, nextproto=1) +bind_layers(NSH, IPv6, nextproto=2) +bind_layers(NSH, Ether, nextproto=3) +bind_layers(NSH, NSH, nextproto=4) +bind_layers(NSH, MPLS, nextproto=5) diff --git a/scapy/contrib/oam.py b/scapy/contrib/oam.py new file mode 100644 index 00000000000..a1e861b65ff --- /dev/null +++ b/scapy/contrib/oam.py @@ -0,0 +1,663 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = Operation, administration and maintenance (OAM) +# scapy.contrib.status = loads + +""" + Operation, administration and maintenance (OAM) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :author: Sergey Matsievskiy, matsievskiysv@gmail.com + + :description: + + This module provides Scapy layers for the OAM protocol. + + normative references: + - ITU-T Rec. G.8013/Y.1731 (08/2019) - Operation, administration and + maintenance (OAM) functions and mechanisms for Ethernet-based + networks (https://www.itu.int/rec/T-REC-G.8013) + - ITU-T Rec. G.8031/Y.1342 (01/2015) - Ethernet linear protection + switching (https://www.itu.int/rec/T-REC-G.8031) + - ITU-T Rec. G.8032/Y.1344 (02/2022) - Ethernet ring protection + switching (https://www.itu.int/rec/T-REC-G.8032) +""" + +from scapy.fields import ( + BitEnumField, + BitField, + ByteField, + ConditionalField, + EnumField, + FCSField, + FlagsField, + IntField, + LenField, + LongField, + MACField, + MultipleTypeField, + NBytesField, + OUIField, + PacketField, + PadField, + PacketListField, + ShortField, + FieldListField, +) +from scapy.layers.l2 import Dot1Q +from scapy.packet import Packet, bind_layers +from binascii import crc32 +import struct + + +class MepIdField(ShortField): + """ + Short field with insignificant three leading bytes + """ + + def __init__(self, name, default): + super(MepIdField, self).__init__( + name, default & 0x1FFF if default is not None else default + ) + + +class MegId(Packet): + """ + MEG ID + """ + + name = "MEG ID" + + fields_desc = [ + ByteField("resv", 1), + ByteField("format", 0), + MultipleTypeField( + [ + ( + LenField("length", 13, fmt="B"), + lambda p: p.format == 32, + ), + ( + LenField("length", 15, fmt="B"), + lambda p: p.format == 33, + ) + ], + LenField("length", 45, fmt="B"), + ), + PadField( + MultipleTypeField( + [ + ( + FieldListField("values", [0] * 13, + ByteField("value", 0), + count_from=lambda pkt: pkt.length), + lambda x: x.format == 32, + ), + ( + FieldListField("values", [0] * 15, + ByteField("value", 0), + count_from=lambda pkt: pkt.length), + lambda x: x.format == 33, + ) + ], + NBytesField("values", 0, sz=45), + ), + 45), + ] + + def extract_padding(self, s): + return b"", s + + +class OAM_TLV(Packet): + """ + OAM TLV + """ + + name = "OAM TLV" + fields_desc = [ByteField("type", 1), LenField("length", None)] + + def extract_padding(self, s): + return s[:self.length], s[self.length:] + + +class OAM_DATA_TLV(Packet): + """ + OAM Data TLV + """ + + name = "OAM Data TLV" + fields_desc = [ByteField("type", 3), LenField("length", None)] + + def extract_padding(self, s): + return s[:self.length], s[self.length:] + + +class OAM_TEST_TLV(Packet): + """ + OAM test TLV data + """ + + name = "OAM test TLV" + + fields_desc = [ + ByteField("type", 32), + MultipleTypeField( + [ + ( + LenField("length", None, adjust=lambda l: l + 5), + lambda p: p.pat_type == 1 or p.pat_type == 3, + ) + ], + LenField("length", None, adjust=lambda l: l + 1), + ), + EnumField( + "pat_type", + 0, + { + 0: "Null signal without CRC-32", + 1: "Null signal with CRC-32", + 2: "PRBS 2^-31 - 1 without CRC-32", + 3: "PRBS 2^-31 - 1 with CRC-32", + }, + fmt="B", + ), + ConditionalField( + FCSField("crc", None, fmt="!I"), + lambda p: p.pat_type == 1 or p.pat_type == 3, + ), + ] + + def do_dissect(self, s): + if ord(s[3:4]) == 1 or ord(s[3:4]) == 3: + # move crc to the end of packet + length = struct.unpack("!H", s[1:3])[0] + crc_end = 3 + length + crc_start = crc_end - 4 + s1 = s[:crc_start] + s2 = s[crc_start:crc_end] + s3 = s[crc_end:] + s = s1 + s3 + s2 + s = super(OAM_TEST_TLV, self).do_dissect(s) + return s + + def post_build(self, p, pay): + if ord(p[3:4]) == 1 or ord(p[3:4]) == 3: + p1 = p + p2 = pay[:-4] + p3 = struct.pack("!I", crc32(p1 + p2) % (1 << 32)) + return p1 + p2 + p3 + else: + return p + pay + + def extract_padding(self, s): + if self.pat_type == 1 or self.pat_type == 3: + # we already consumed crc + return s[:self.length - 5], s[self.length - 5:] + else: + return s[:self.length - 1], s[self.length - 1:] + + +class OAM_LTM_TLV(Packet): + """ + OAM LTM TLV data + """ + + name = "OAM LTM Egress ID TLV" + + fields_desc = [ + ByteField("type", 7), + LenField("length", 8), + LongField("egress_id", 0), + ] + + def extract_padding(self, s): + return b"", s + + +class OAM_LTR_TLV(Packet): + """ + OAM LTR TLV data + """ + + name = "OAM LTR Egress ID TLV" + + fields_desc = [ + ByteField("type", 8), + LenField("length", 16), + # NOTE: wireshark interprets this field as short+MAC + LongField("last_egress_id", 0), + # NOTE: wireshark interprets this field as short+MAC + LongField("next_egress_id", 0), + ] + + def extract_padding(self, s): + return b"", s + + +class OAM_LTR_IG_TLV(Packet): + """ + OAM LTR TLV data + """ + + name = "OAM LTR Ingress TLV" + + fields_desc = [ + ByteField("type", 5), + LenField("length", 7), + ByteField("ingress_act", 0), + MACField("ingress_mac", None), + ] + + def extract_padding(self, s): + return b"", s + + +class OAM_LTR_EG_TLV(Packet): + """ + OAM LTR TLV data + """ + + name = "OAM LTR Egress TLV" + + fields_desc = [ + ByteField("type", 6), + LenField("length", 7), + ByteField("egress_act", 0), + MACField("egress_mac", None), + ] + + def extract_padding(self, s): + return b"", s + + +class OAM_TEST_ID_TLV(Packet): + """ + OAM Test ID TLV data + """ + + name = "OAM Test ID TLV" + + fields_desc = [ + ByteField("type", 36), + LenField("length", 32), + IntField("test_id", 0), + ] + + def extract_padding(self, s): + return b"", s + + +def guess_tlv_type(pkt, lst, cur, remain): + if remain[0:1] == b'\x00': + return None + elif remain[0:1] == b'\x03': + return OAM_DATA_TLV + elif remain[0:1] == b'\x05': + return OAM_LTR_IG_TLV + elif remain[0:1] == b'\x06': + return OAM_LTR_EG_TLV + elif remain[0:1] == b'\x07': + return OAM_LTM_TLV + elif remain[0:1] == b'\x08': + return OAM_LTR_TLV + elif remain[0:1] == b'\x20': + return OAM_TEST_TLV + elif remain[0:1] == b'\x24': + return OAM_TEST_ID_TLV + else: + return OAM_TLV + + +class PTP_TIMESTAMP(Packet): + """ + PTP timestamp + """ + + # TODO: should be a part of PTP module + name = "PTP timestamp" + fields_desc = [IntField("seconds", 0), IntField("nanoseconds", 0)] + + def extract_padding(self, s): + return b"", s + + +class APS(Packet): + """ + Linear protective switching APS data packet + """ + + name = "APS" + + fields_desc = [ + BitEnumField( + "req_st", + 0, + 4, + { + 0b0000: "No request (NR)", + 0b0001: "Do not request (DNR)", + 0b0010: "Reverse request (RR)", + 0b0100: "Exercise (EXER)", + 0b0101: "Wait-to-restore (WTR)", + 0b0110: "Deprecated", + 0b0111: "Manual switch (MS)", + 0b1001: "Signal degrade (SD)", + 0b1011: "Signal fail for working (SF)", + 0b1101: "Forced switch (FS)", + 0b1110: "Signal fail on protection (SF-P)", + 0b1111: "Lockout of protection (LO)", + }, + ), + FlagsField( + "prot_type", + 0, + 4, + { + (1 << 3): "A", + (1 << 2): "B", + (1 << 1): "D", + (1 << 0): "R", + }, + ), + EnumField( + "req_sig", 0, {0: "Null signal", 1: "Normal traffic"}, fmt="B" + ), + EnumField( + "br_sig", 0, {0: "Null signal", 1: "Normal traffic"}, fmt="B" + ), + FlagsField("br_type", 0, 8, {(1 << 7): "T"}), + ] + + def extract_padding(self, s): + return b"", s + + +class RAPS(Packet): + """ + Ring protective switching R-APS data packet + """ + + name = "R-APS" + + fields_desc = [ + BitEnumField( + "req_st", + 0, + 4, + { + 0b0000: "No request (NR)", + 0b0111: "Manual switch (MS)", + 0b1011: "Signal fail(SF)", + 0b1101: "Forced switch (FS)", + 0b1110: "Event", + }, + ), + MultipleTypeField( + [ + ( + BitEnumField("sub_code", 0, 4, {0b0000: "Flush"}), + lambda p: p.req_st == 0b1110, + ) + ], + BitField("sub_code", 0, 4), + ), + FlagsField( + "status", + 0, + 8, + { + (1 << 7): "RB", + (1 << 6): "DNF", + (1 << 5): "BPR", + }, + ), + MACField("node_id", None), + NBytesField("resv", 0, 24), + ] + + def extract_padding(self, s): + return b"", s + + +class OAM(Packet): + """ + OAM data unit + """ + + name = "OAM" + + OPCODES = { + 1: "Continuity Check Message (CCM)", + 3: "Loopback Message (LBM)", + 2: "Loopback Reply (LBR)", + 5: "Linktrace Message (LTM)", + 4: "Linktrace Reply (LTR)", + 32: "Generic Notification Message (GNM)", + 33: "Alarm Indication Signal (AIS)", + 35: "Lock Signal (LCK)", + 37: "Test Signal (TST)", + 39: "Automatic Protection Switching (APS)", + 40: "Ring-Automatic Protection Switching (R-APS)", + 41: "Maintenance Communication Channel (MCC)", + 43: "Loss Measurement Message (LMM)", + 42: "Loss Measurement Reply (LMR)", + 45: "One Way Delay Measurement (1DM)", + 47: "Delay Measurement Message (DMM)", + 46: "Delay Measurement Reply (DMR)", + 49: "Experimental OAM Message (EXM)", + 48: "Experimental OAM Reply (EXR)", + 51: "Vendor Specific Message (VSM)", + 50: "Vendor Specific Reply (VSR)", + 52: "Client Signal Fail (CSF)", + 53: "One Way Synthetic Loss Measurement (1SL)", + 55: "Synthetic Loss Message (SLM)", + 54: "Synthetic Loss Reply (SLR)", + } + + TIME_FLAGS = { + 0b000: "Invalid value", + 0b001: "Trans Int 3.33ms", + 0b010: "Trans Int 10ms", + 0b011: "Trans Int 100ms", + 0b100: "Trans Int 1s", + 0b101: "Trans Int 10s", + 0b110: "Trans Int 1min", + 0b111: "Trans Int 10min", + } + + PERIOD_FLAGS = { + 0b100: "1 frame per second", + 0b110: "1 frame per minute", + } + + BNM_PERIOD_FLAGS = { + 0b100: "1 frame per second", + 0b101: "1 frame per 10 seconds", + 0b110: "1 frame per minute", + } + + fields_desc = [ + # Common fields + BitField("mel", 0, 3), + MultipleTypeField( + [(BitField("version", 1, 5), lambda x: x.opcode in [43, 45, 47])], + BitField("version", 0, 5), + ), + EnumField("opcode", None, OPCODES, fmt="B"), + MultipleTypeField( + [ + ( + FlagsField("flags", 0, 5, {(1 << 4): "RDI"}), + lambda x: x.opcode == 1, + ), + ( + FlagsField("flags", 0, 8, {(1 << 7): "HWonly"}), + lambda x: x.opcode == 5, + ), + ( + FlagsField( + "flags", + 0, + 8, + { + (1 << 7): "HWonly", + (1 << 6): "FwdYes", + (1 << 5): "TerminalMEP", + }, + ), + lambda x: x.opcode == 4, + ), + (BitField("flags", 0, 5), lambda x: x.opcode in [33, 35, 32]), + ( + FlagsField("flags", 0, 8, {1: "Proactive"}), + lambda x: x.opcode in [43, 45, 47], + ), + ( + BitEnumField( + "flags", + 0, + 5, + { + 0b000: "LOS", + 0b001: "FDI", + 0b010: "RDI", + 0b011: "DCI", + }, + ), + lambda x: x.opcode == 52, + ), + ], + ByteField("flags", 0), + ), + ConditionalField( + MultipleTypeField( + [ + ( + BitEnumField("period", 1, 3, TIME_FLAGS), + lambda x: x.opcode == 1, + ), + ( + BitEnumField("period", 0b110, 3, BNM_PERIOD_FLAGS), + lambda x: x.opcode in [13, 32], + ), + ], + BitEnumField("period", 0b110, 3, PERIOD_FLAGS), + ), + lambda x: x.opcode in [1, 33, 35, 52, 32], + ), + MultipleTypeField( + [ + (ByteField("tlv_offset", 70), lambda x: x.opcode == 1), + ( + ByteField("tlv_offset", 4), + lambda x: x.opcode in [3, 2, 37, 39], + ), + (ByteField("tlv_offset", 17), lambda x: x.opcode == 5), + (ByteField("tlv_offset", 6), lambda x: x.opcode == 4), + (ByteField("tlv_offset", 32), lambda x: x.opcode in [40, 47]), + (ByteField("tlv_offset", 12), lambda x: x.opcode == 43), + ( + ByteField("tlv_offset", 16), + lambda x: x.opcode in [45, 54, 53, 55], + ), + (ByteField("tlv_offset", 13), lambda x: x.opcode == 32), + ( + ByteField("tlv_offset", 10), + lambda x: x.opcode == 41 + ), + ], + ByteField("tlv_offset", 0), + ), + # End common fields + ConditionalField( + IntField("seq_num", 0), lambda x: x.opcode in [1, 3, 2, 37] + ), + ConditionalField(IntField("trans_id", 0), + lambda x: x.opcode in [5, 4]), + ConditionalField( + OUIField("oui", None), lambda x: x.opcode in [41, 49, 48, 51, 50] + ), + ConditionalField( + MultipleTypeField( + [(ByteField("subopcode", 1), lambda x: x.opcode == 32)], + ByteField("subopcode", 0), + ), + lambda x: x.opcode in [41, 49, 48, 51, 50, 32], + ), + ConditionalField( + MepIdField("mep_id", 0), + lambda x: x.opcode == 1 \ + or (x.opcode == 41 and x.subopcode == 1 and x.oui == 6567), + ), + ConditionalField( + PacketField("meg_id", MegId(), MegId), lambda x: x.opcode == 0x01 + ), + ConditionalField( + ShortField("src_mep_id", 0), lambda x: x.opcode in [55, 54, 53] + ), + ConditionalField( + ShortField("rcv_mep_id", 0), lambda x: x.opcode in [55, 54, 53] + ), + ConditionalField( + IntField("test_id", 0), lambda x: x.opcode in [55, 54, 53] + ), + ConditionalField( + IntField("txfcf", 0), lambda x: x.opcode in [1, 43, 42, 55, 54, 53] + ), + ConditionalField(IntField("rxfcb", 0), lambda x: x.opcode == 1), + ConditionalField(IntField("rxfcf", 0), lambda x: x.opcode in [43, 42]), + ConditionalField( + IntField("txfcb", 0), lambda x: x.opcode in [1, 43, 42, 55, 54] + ), + ConditionalField(IntField("resv", 0), lambda x: x.opcode in [1, 53]), + ConditionalField(ByteField("ttl", 0), lambda x: x.opcode in [5, 4]), + ConditionalField(MACField("orig_mac", None), lambda x: x.opcode == 5), + ConditionalField(MACField("targ_mac", None), lambda x: x.opcode == 5), + ConditionalField(ByteField("relay_act", None), + lambda x: x.opcode == 4), + ConditionalField( + PacketField("txtsf", PTP_TIMESTAMP(), PTP_TIMESTAMP), + lambda x: x.opcode in [45, 47, 46], + ), + ConditionalField( + PacketField("rxtsf", PTP_TIMESTAMP(), PTP_TIMESTAMP), + lambda x: x.opcode in [45, 47, 46], + ), + ConditionalField( + PacketField("txtsb", PTP_TIMESTAMP(), PTP_TIMESTAMP), + lambda x: x.opcode in [47, 46], + ), + ConditionalField( + PacketField("rxtsb", PTP_TIMESTAMP(), PTP_TIMESTAMP), + lambda x: x.opcode in [47, 46], + ), + ConditionalField( + IntField("expct_dur", None), + lambda x: x.opcode == 41 and x.subopcode == 1 and x.oui == 6567, + ), + ConditionalField(IntField("nom_bdw", None), lambda x: x.opcode == 32), + ConditionalField(IntField("curr_bdw", None), lambda x: x.opcode == 32), + ConditionalField(IntField("port_id", None), lambda x: x.opcode == 32), + ConditionalField( + PacketField("aps", APS(), APS), lambda x: x.opcode == 39 + ), + ConditionalField( + PacketField("raps", RAPS(), RAPS), lambda x: x.opcode == 40 + ), + ConditionalField( + PacketListField("tlvs", [], next_cls_cb=guess_tlv_type), + lambda x: x.opcode in [3, 2, 5, 4, 37, 45, 47, 46, 55, 54, 53], + ), + ConditionalField( + IntField("opt_data", None), + lambda x: x.opcode in [49, 48, 51, 50] and False, + ), # FIXME: field documented elsewhere + # TODO: add EXM, EXR, VSM and VSR data + ByteField("end_tlv", 0), + ] + + +bind_layers(Dot1Q, OAM, type=0x8902) diff --git a/scapy/contrib/oncrpc.py b/scapy/contrib/oncrpc.py index f12394ea97b..2eccad6fdae 100644 --- a/scapy/contrib/oncrpc.py +++ b/scapy/contrib/oncrpc.py @@ -1,13 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Lucas Preston -# This program is published under a GPLv2 license # scapy.contrib.description = ONC-RPC v2 # scapy.contrib.status = loads from scapy.fields import XIntField, IntField, IntEnumField, StrLenField, \ - FieldListField, ConditionalField, PacketField + FieldListField, ConditionalField, PacketField, FieldLenField from scapy.packet import Packet, bind_layers import struct @@ -73,6 +73,31 @@ def extract_padding(self, s): return '', s +class Auth_RPCSEC_GSS(Packet): + name = 'Auth RPCSEC_GSS' + fields_desc = [ + IntField('gss_version', 0), + IntField('gss_procedure', 0), + IntField('gss_seq_num', 0), + IntField('gss_service', 0), + PacketField('gss_context', Object_Name(), Object_Name) + ] + + def extract_padding(self, s): + return '', s + + +class Verifier_RPCSEC_GSS(Packet): + name = 'Verifier RPCSEC_GSS' + fields_desc = [ + FieldLenField("len", None, length_of="data"), + StrLenField("data", "", length_from=lambda pkt:pkt.len) + ] + + def extract_padding(self, s): + return '', s + + class RPC_Call(Packet): name = 'RPC Call' @@ -81,17 +106,38 @@ class RPC_Call(Packet): IntField('program', 100003), IntField('pversion', 3), IntField('procedure', 0), - IntEnumField('aflavor', 1, {0: 'AUTH_NULL', 1: 'AUTH_UNIX'}), + IntEnumField( + 'aflavor', 1, + {0: 'AUTH_NULL', 1: 'AUTH_UNIX', 6: 'RPCSEC_GSS'} + ), IntField('alength', None), ConditionalField( PacketField('a_unix', Auth_Unix(), Auth_Unix), lambda pkt: pkt.aflavor == 1 ), - IntEnumField('vflavor', 0, {0: 'AUTH_NULL', 1: 'AUTH_UNIX'}), - IntField('vlength', None), + ConditionalField( + PacketField('a_rpcsec_gss', Auth_RPCSEC_GSS(), Auth_RPCSEC_GSS), + lambda pkt: pkt.aflavor == 6 + ), + IntEnumField( + 'vflavor', 0, + {0: 'AUTH_NULL', 1: 'AUTH_UNIX', 6: 'RPCSEC_GSS'} + ), + ConditionalField( + IntField('vlength', None), + lambda pkt: pkt.vflavor != 6 + ), ConditionalField( PacketField('v_unix', Auth_Unix(), Auth_Unix), lambda pkt: pkt.vflavor == 1 + ), + ConditionalField( + PacketField( + 'v_rpcsec_gss', + Verifier_RPCSEC_GSS(), + Verifier_RPCSEC_GSS + ), + lambda pkt: pkt.vflavor == 6 ) ] @@ -118,8 +164,13 @@ def post_build(self, pkt, pay): # default will be correct return Packet.post_build(self, pkt, pay) if self.aflavor != 0 and self.alength is None: + if self.aflavor == 6: + pack_len = len(self.a_rpcsec_gss) + else: + pack_len = len(self.a_unix) + pkt = pkt[:20] \ - + struct.pack('!I', len(self.a_unix)) \ + + struct.pack('!I', pack_len) \ + pkt[24:] return Packet.post_build(self, pkt, pay) if self.vflavor != 0 and self.vlength is None: diff --git a/scapy/contrib/opc_da.py b/scapy/contrib/opc_da.py index 887d0d99a22..49bb3dec63a 100644 --- a/scapy/contrib/opc_da.py +++ b/scapy/contrib/opc_da.py @@ -1,22 +1,8 @@ -# coding: utf8 - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software FounDation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - -# Copyright (C) -# @Author: GuillaumeF -# @Email: guillaume4favre@gmail.com +# See https://scapy.net/ for more information +# Copyright (C) GuillaumeF + # @Date: 2016-10-18 # @Last modified by: GuillaumeF # @Last modified by: Sebastien Mainand @@ -27,23 +13,52 @@ # scapy.contrib.status = loads """ -Opc Data Access. -References: Data Access Custom Interface StanDard -Using the website: http://pubs.opengroup.org/onlinepubs/9629399/chap12.htm +Opc Data Access + +Spec: Google 'OPCDA3.00.pdf' + +RPC PDU encodings: +- DCE 1.1 RPC: https://pubs.opengroup.org/onlinepubs/9629399/toc.pdf +- http://pubs.opengroup.org/onlinepubs/9629399/chap12.htm DCOM Remote Protocol. -References: Specifies Distributed Component Object Model (DCOM) Remote Protocol -Using the website: https://msdn.microsoft.com/en-us/library/cc226801.aspx +[MS-DCOM]: Distributed Component Object Model (DCOM) Remote Protocol +https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/4a893f3d-bd29-48cd-9f43-d9777a4415b0 +XXX TODO: does not appear to have been linked to RPC """ +import struct + from scapy.config import conf -from scapy.fields import Field, ByteField, ShortField, LEShortField, \ - IntField, LEIntField, LongField, LELongField, StrField, StrLenField, \ - StrFixedLenField, BitEnumField, ByteEnumField, ShortEnumField, \ - LEShortEnumField, IntEnumField, LEIntEnumField, FieldLenField, \ - LEFieldLenField, PacketField, PacketListField, PacketLenField, \ - ConditionalField, FlagsField, UUIDField +from scapy.fields import ( + BitEnumField, + ByteEnumField, + ByteField, + ConditionalField, + Field, + FieldLenField, + FlagsField, + IntEnumField, + IntField, + LEIntEnumField, + LEIntField, + LELongField, + LEShortField, + MultipleTypeField, + PacketField, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + StrField, + StrFixedLenField, + StrLenField, + UUIDField, + _FieldContainer, + _PacketField, +) from scapy.packet import Packet +from scapy.layers.ntlm import NTLM_Header # Defined values _tagOPCDataSource = { @@ -233,6 +248,31 @@ 1: 'OsfDcePrivateKeyAuthentication', } +# Util + + +def _make_le(pkt_cls): + """ + Make all fields in a packet LE. + """ + flds = [f.copy() for f in pkt_cls.fields_desc] + for f in flds: + if isinstance(f, _FieldContainer): + f = f.fld + if isinstance(f, UUIDField): + f.uuid_fmt = UUIDField.FORMAT_LE + elif isinstance(f, _PacketField): + f.cls = globals().get(f.cls.__name__ + "LE", f.cls) + elif not isinstance(f, StrField): + f.fmt = "<" + f.fmt.replace(">", "").replace("!", "") + f.struct = struct.Struct(f.fmt) + + class LEPacket(pkt_cls): + fields_desc = flds + name = pkt_cls().name + " (LE)" + LEPacket.__name__ = pkt_cls.__name__ + "LE" + return LEPacket + # Sub class for dissection class AuthentificationProtocol(Packet): @@ -242,11 +282,11 @@ def extract_padding(self, p): return b"", p def guess_payload_class(self, payload): - if self.underlayer and hasattr(self.underlayer, "auth_length"): - auth_length = self.underlayer.auth_length - if auth_length != 0: + if self.underlayer and hasattr(self.underlayer, "authLength"): + authLength = self.underlayer.authLength + if authLength != 0: try: - return _authentification_protocol[auth_length] + return _authentification_protocol[authLength] except Exception: pass return conf.raw_layer @@ -269,35 +309,22 @@ def extract_padding(self, p): class LenStringPacket(Packet): + # Among other things, can be (port_any_t - DCE 1.1 RPC - p592) name = "len string packet" fields_desc = [ FieldLenField('length', 0, length_of='data', fmt="H"), - ConditionalField(StrLenField('data', None, - length_from=lambda pkt:pkt.length + 2), - lambda pkt:pkt.length == 0), - ConditionalField(StrLenField('data', '', - length_from=lambda pkt:pkt.length), - lambda pkt:pkt.length != 0), + MultipleTypeField( + [(StrFixedLenField('data', '', length=2), + lambda pkt: not pkt.length)], + StrLenField('data', '', length_from=lambda pkt: pkt.length) + ) ] def extract_padding(self, p): return b"", p -class LenStringPacketLE(Packet): - name = "len string packet" - fields_desc = [ - LEFieldLenField('length', 0, length_of='data', fmt=" -# This program is published under a GPLv2 license - # Copyright (C) 2014 Maxence Tury -# OpenFlow is an open standard used in SDN deployments. -# Based on OpenFlow v1.0.1 -# Specifications can be retrieved from https://www.opennetworking.org/ + +""" +OpenFlow v1.0.1 + +OpenFlow is an open standard used in SDN deployments. +Specifications can be retrieved from https://www.opennetworking.org/ +""" # scapy.contrib.description = Openflow v1.0 # scapy.contrib.status = loads -from __future__ import absolute_import import struct @@ -23,7 +25,6 @@ from scapy.layers.inet import TCP from scapy.packet import Packet, Raw, bind_bottom_up, bind_top_down from scapy.utils import binrepr -from scapy.modules import six # If prereq_autocomplete is True then match prerequisites will be @@ -494,7 +495,7 @@ class OFPPacketQueue(Packet): ShortField("len", None), XShortField("pad", 0), PacketListField("properties", [], OFPQT, - length_from=lambda pkt:pkt.len - 8)] # noqa: E501 + length_from=lambda pkt:pkt.len - 8)] def extract_padding(self, s): return b"", s @@ -714,7 +715,7 @@ class OFPTFeaturesRequest(_ofp_header): IntField("xid", 0)] -ofp_action_types_flags = [v for v in six.itervalues(ofp_action_types) +ofp_action_types_flags = [v for v in ofp_action_types.values() if v != 'OFPAT_VENDOR'] diff --git a/scapy/contrib/openflow3.py b/scapy/contrib/openflow3.py index c7307045bfb..6622ca59b8d 100755 --- a/scapy/contrib/openflow3.py +++ b/scapy/contrib/openflow3.py @@ -1,17 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license - # Copyright (C) 2014 Maxence Tury -# OpenFlow is an open standard used in SDN deployments. -# Based on OpenFlow v1.3.4 -# Specifications can be retrieved from https://www.opennetworking.org/ + +""" +OpenFlow v1.3.4 + +OpenFlow is an open standard used in SDN deployments. +Specifications can be retrieved from https://www.opennetworking.org/ +""" + # scapy.contrib.description = OpenFlow v1.3 # scapy.contrib.status = loads -from __future__ import absolute_import import copy import struct @@ -25,7 +28,6 @@ XIntField, XShortField, PacketLenField from scapy.layers.l2 import Ether from scapy.packet import Packet, Padding, Raw -from scapy.modules import six from scapy.contrib.openflow import _ofp_header, _ofp_header_item, \ OFPacketField, OpenFlow, _UnknownOpenFlow @@ -265,7 +267,7 @@ def extract_padding(self, s): def add_ofp_oxm_fields(i, org): - ofp_oxm_fields[i] = [ShortEnumField("class", "OFPXMC_OPENFLOW_BASIC", ofp_oxm_classes), # noqa: E501 + ofp_oxm_fields[i] = [ShortEnumField("class_", "OFPXMC_OPENFLOW_BASIC", ofp_oxm_classes), # noqa: E501 BitEnumField("field", i // 2, 7, ofp_oxm_names), BitField("hasmask", i % 2, 1)] ofp_oxm_fields[i].append(ByteField("len", org[2] + org[2] * (i % 2))) @@ -672,7 +674,7 @@ def getfield(self, pkt, s): if Padding in r: p = r[Padding] i.payload = p - del(r.payload) + del r.payload return r.load, i else: return b"", i @@ -977,10 +979,8 @@ def post_build(self, p, pay): zero_bytes = (8 - tmp_len % 8) % 8 tmp_len = tmp_len + zero_bytes # add padding length p = p[:2] + struct.pack("!H", tmp_len) + p[4:] - else: - zero_bytes = (8 - tmp_len % 8) % 8 - # every message will be padded correctly - p += b"\x00" * zero_bytes + p += b"\x00" * zero_bytes + # message with user-defined length will not be automatically padded return p + pay def extract_padding(self, s): @@ -2103,7 +2103,7 @@ class OFPTFlowMod(_ofp_header): XShortField("pad", 0), MatchField("match"), PacketListField("instructions", [], OFPIT, - length_from=lambda pkt:pkt.len - 48 - (pkt.match.len + (8 - pkt.match.len % 8) % 8))] # noqa: E501 + length_from=lambda pkt:pkt.len - 48 - (pkt.match.len + (8 - pkt.match.len % 8) % 8))] # noqa: E501 # include match padding to match.len @@ -2246,7 +2246,7 @@ class OFPFlowStats(_ofp_header_item): LongField("byte_count", 0), MatchField("match"), PacketListField("instructions", [], OFPIT, - length_from=lambda pkt:pkt.len - 56 - pkt.match.len)] # noqa: E501 + length_from=lambda pkt:pkt.len - 52 - pkt.match.len)] # noqa: E501 def extract_padding(self, s): return b"", s @@ -2513,7 +2513,7 @@ class OFPMPRequestGroupFeatures(_ofp_header): XIntField("pad1", 0)] -ofp_action_types_flags = [v for v in six.itervalues(ofp_action_types) +ofp_action_types_flags = [v for v in ofp_action_types.values() if v != 'OFPAT_EXPERIMENTER'] @@ -2695,9 +2695,9 @@ def post_build(self, p, pay): if tmp_len is None: tmp_len = len(p) + len(pay) p = p[:2] + struct.pack("!H", tmp_len) + p[4:] - # every message will be padded correctly - zero_bytes = (8 - tmp_len % 8) % 8 - p += b"\x00" * zero_bytes + zero_bytes = (8 - tmp_len % 8) % 8 + p += b"\x00" * zero_bytes + # message with user-defined length will not be automatically padded return p + pay def extract_padding(self, s): diff --git a/scapy/contrib/ospf.py b/scapy/contrib/ospf.py index 97e924df5d5..44a538a1c66 100644 --- a/scapy/contrib/ospf.py +++ b/scapy/contrib/ospf.py @@ -1,28 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (c) 2008 Dirk Loss +# Copyright (c) 2010 Jochen Bartl + # scapy.contrib.description = Open Shortest Path First (OSPF) # scapy.contrib.status = loads -# This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - """ OSPF extension for Scapy This module provides Scapy layers for the Open Shortest Path First routing protocol as defined in RFC 2328 and RFC 5340. - -Copyright (c) 2008 Dirk Loss : mail dirk-loss de -Copyright (c) 2010 Jochen Bartl : jochen.bartl gmail com """ @@ -36,7 +25,7 @@ ShortField, StrLenField, X3BytesField, XIntField, XLongField, XShortField from scapy.layers.inet import IP, DestIPField from scapy.layers.inet6 import IPv6, in6_chksum -from scapy.utils import fletcher16_checkbytes, checksum +from scapy.utils import fletcher16_checkbytes, checksum, inet_aton from scapy.compat import orb from scapy.config import conf @@ -202,14 +191,20 @@ def post_build(self, p, pay): 3: "summaryIP", 4: "summaryASBR", 5: "external", - 7: "NSSAexternal"} + 7: "NSSAexternal", + 9: "linkScopeOpaque", + 10: "areaScopeOpaque", + 11: "asScopeOpaque"} _OSPF_LSclasses = {1: "OSPF_Router_LSA", 2: "OSPF_Network_LSA", 3: "OSPF_SummaryIP_LSA", 4: "OSPF_SummaryASBR_LSA", 5: "OSPF_External_LSA", - 7: "OSPF_NSSA_External_LSA"} + 7: "OSPF_NSSA_External_LSA", + 9: "OSPF_Link_Scope_Opaque_LSA", + 10: "OSPF_Area_Scope_Opaque_LSA", + 11: "OSPF_AS_Scope_Opaque_LSA"} def ospf_lsa_checksum(lsa): @@ -367,15 +362,51 @@ class OSPF_NSSA_External_LSA(OSPF_External_LSA): type = 7 +class OSPF_Link_Scope_Opaque_LSA(OSPF_BaseLSA): + name = "OSPF Link Scope External LSA" + type = 9 + fields_desc = [ShortField("age", 1), + OSPFOptionsField(), + ByteField("type", 9), + IPField("id", "192.0.2.1"), + IPField("adrouter", "198.51.100.100"), + XIntField("seq", 0x80000001), + XShortField("chksum", None), + ShortField("len", None), + StrLenField("data", "data", + length_from=lambda pkt: pkt.len - 20) + ] + + def opaqueid(self): + return struct.unpack('>I', inet_aton(self.id))[0] & 0xFFFFFF + + def opaquetype(self): + return (struct.unpack('>I', inet_aton(self.id))[0] >> 24) & 0xFF + + +class OSPF_Area_Scope_Opaque_LSA(OSPF_Link_Scope_Opaque_LSA): + name = "OSPF Area Scope External LSA" + type = 10 + + +class OSPF_AS_Scope_Opaque_LSA(OSPF_Link_Scope_Opaque_LSA): + name = "OSPF AS Scope External LSA" + type = 11 + + class OSPF_DBDesc(Packet): name = "OSPF Database Description" - fields_desc = [ShortField("mtu", 1500), - OSPFOptionsField(), - FlagsField("dbdescr", 0, 8, ["MS", "M", "I", "R", "4", "3", "2", "1"]), # noqa: E501 - IntField("ddseq", 1), - PacketListField("lsaheaders", None, OSPF_LSA_Hdr, - count_from=lambda pkt: None, - length_from=lambda pkt: pkt.underlayer.len - 24 - 8)] # noqa: E501 + fields_desc = [ + ShortField("mtu", 1500), + OSPFOptionsField(), + FlagsField("dbdescr", 0, 8, ["MS", "M", "I", "R", "4", "3", "2", "1"]), + IntField("ddseq", 1), + PacketListField( + "lsaheaders", None, OSPF_LSA_Hdr, + count_from=lambda pkt: None, + length_from=lambda pkt: pkt.underlayer and pkt.underlayer.len - 24 - 8, + ) + ] def guess_payload_class(self, payload): # check presence of LLS data block flag @@ -397,24 +428,36 @@ def extract_padding(self, s): class OSPF_LSReq(Packet): name = "OSPF Link State Request (container)" - fields_desc = [PacketListField("requests", None, OSPF_LSReq_Item, - count_from=lambda pkt:None, - length_from=lambda pkt:pkt.underlayer.len - 24)] # noqa: E501 + fields_desc = [ + PacketListField( + "requests", None, OSPF_LSReq_Item, + count_from=lambda pkt: None, + length_from=lambda pkt: pkt.underlayer and pkt.underlayer.len - 24, + ) + ] class OSPF_LSUpd(Packet): name = "OSPF Link State Update" - fields_desc = [FieldLenField("lsacount", None, fmt="!I", count_of="lsalist"), # noqa: E501 - PacketListField("lsalist", None, _LSAGuessPayloadClass, - count_from=lambda pkt: pkt.lsacount, - length_from=lambda pkt: pkt.underlayer.len - 24)] # noqa: E501 + fields_desc = [ + FieldLenField("lsacount", None, fmt="!I", count_of="lsalist"), + PacketListField( + "lsalist", None, _LSAGuessPayloadClass, + count_from=lambda pkt: pkt.lsacount, + length_from=lambda pkt: pkt.underlayer and pkt.underlayer.len - 24, + ) + ] class OSPF_LSAck(Packet): name = "OSPF Link State Acknowledgement" - fields_desc = [PacketListField("lsaheaders", None, OSPF_LSA_Hdr, - count_from=lambda pkt: None, - length_from=lambda pkt: pkt.underlayer.len - 24)] # noqa: E501 + fields_desc = [ + PacketListField( + "lsaheaders", None, OSPF_LSA_Hdr, + count_from=lambda pkt: None, + length_from=lambda pkt: pkt.underlayer and pkt.underlayer.len - 24, + ) + ] def answers(self, other): if isinstance(other, OSPF_LSUpd): diff --git a/scapy/contrib/pfcp.py b/scapy/contrib/pfcp.py new file mode 100644 index 00000000000..fc2e3d41322 --- /dev/null +++ b/scapy/contrib/pfcp.py @@ -0,0 +1,2639 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2019 Travelping GmbH + +""" +3GPP TS 29.244 +""" + +# scapy.contrib.description = 3GPP Packet Forwarding Control Protocol +# scapy.contrib.status = loads + +import struct + +from scapy.compat import chb, orb +from scapy.error import warning +from scapy.fields import Field, BitEnumField, BitField, ByteEnumField, \ + ShortEnumField, ByteField, IntField, LongField, \ + ConditionalField, FieldLenField, BitFieldLenField, FieldListField, \ + IPField, MACField, PacketListField, ShortField, \ + StrLenField, StrField, XBitField, XByteField, XIntField, XLongField, \ + ThreeBytesField, SignedLongField, SignedIntField, MultipleTypeField +from scapy.layers.inet import UDP +from scapy.layers.inet6 import IP6Field +from scapy.data import IANA_ENTERPRISE_NUMBERS +from scapy.packet import bind_layers, bind_bottom_up, \ + Packet, Raw +from scapy.volatile import RandNum, RandBin + +PFCPmessageType = { + 1: "heartbeat_request", + 2: "heartbeat_response", + 3: "pfd_management_request", + 4: "pfd_management_response", + 5: "association_setup_request", + 6: "association_setup_response", + 7: "association_update_request", + 8: "association_update_response", + 9: "association_release_request", + 10: "association_release_response", + 11: "version_not_supported_response", + 12: "node_report_request", + 13: "node_report_response", + 14: "session_set_deletion_request", + 15: "session_set_deletion_response", + 50: "session_establishment_request", + 51: "session_establishment_response", + 52: "session_modification_request", + 53: "session_modification_response", + 54: "session_deletion_request", + 55: "session_deletion_response", + 56: "session_report_request", + 57: "session_report_response", +} + +IEType = { + 0: "Reserved", + 1: "Create PDR", + 2: "PDI", + 3: "Create FAR", + 4: "Forwarding Parameters", + 5: "Duplicating Parameters", + 6: "Create URR", + 7: "Create QER", + 8: "Created PDR", + 9: "Update PDR", + 10: "Update FAR", + 11: "Update Forwarding Parameters", + 12: "Update BAR (PFCP Session Report Response)", + 13: "Update URR", + 14: "Update QER", + 15: "Remove PDR", + 16: "Remove FAR", + 17: "Remove URR", + 18: "Remove QER", + 19: "Cause", + 20: "Source Interface", + 21: "F-TEID", + 22: "Network Instance", + 23: "SDF Filter", + 24: "Application ID", + 25: "Gate Status", + 26: "MBR", + 27: "GBR", + 28: "QER Correlation ID", + 29: "Precedence", + 30: "Transport Level Marking", + 31: "Volume Threshold", + 32: "Time Threshold", + 33: "Monitoring Time", + 34: "Subsequent Volume Threshold", + 35: "Subsequent Time Threshold", + 36: "Inactivity Detection Time", + 37: "Reporting Triggers", + 38: "Redirect Information", + 39: "Report Type", + 40: "Offending IE", + 41: "Forwarding Policy", + 42: "Destination Interface", + 43: "UP Function Features", + 44: "Apply Action", + 45: "Downlink Data Service Information", + 46: "Downlink Data Notification Delay", + 47: "DL Buffering Duration", + 48: "DL Buffering Suggested Packet Count", + 49: "PFCPSMReq-Flags", + 50: "PFCPSRRsp-Flags", + 51: "Load Control Information", + 52: "Sequence Number", + 53: "Metric", + 54: "Overload Control Information", + 55: "Timer", + 56: "PDR ID", + 57: "F-SEID", + 58: "Application ID's PFDs", + 59: "PFD context", + 60: "Node ID", + 61: "PFD contents", + 62: "Measurement Method", + 63: "Usage Report Trigger", + 64: "Measurement Period", + 65: "FQ-CSID", + 66: "Volume Measurement", + 67: "Duration Measurement", + 68: "Application Detection Information", + 69: "Time of First Packet", + 70: "Time of Last Packet", + 71: "Quota Holding Time", + 72: "Dropped DL Traffic Threshold", + 73: "Volume Quota", + 74: "Time Quota", + 75: "Start Time", + 76: "End Time", + 77: "Query URR", + 78: "Usage Report (Session Modification Response)", + 79: "Usage Report (Session Deletion Response)", + 80: "Usage Report (Session Report Request)", + 81: "URR ID", + 82: "Linked URR ID", + 83: "Downlink Data Report", + 84: "Outer Header Creation", + 85: "Create BAR", + 86: "Update BAR (Session Modification Request)", + 87: "Remove BAR", + 88: "BAR ID", + 89: "CP Function Features", + 90: "Usage Information", + 91: "Application Instance ID", + 92: "Flow Information", + 93: "UE IP Address", + 94: "Packet Rate", + 95: "Outer Header Removal", + 96: "Recovery Time Stamp", + 97: "DL Flow Level Marking", + 98: "Header Enrichment", + 99: "Error Indication Report", + 100: "Measurement Information", + 101: "Node Report Type", + 102: "User Plane Path Failure Report", + 103: "Remote GTP-U Peer", + 104: "UR-SEQN", + 105: "Update Duplicating Parameters", + 106: "Activate Predefined Rules", + 107: "Deactivate Predefined Rules", + 108: "FAR ID", + 109: "QER ID", + 110: "OCI Flags", + 111: "PFCP Association Release Request", + 112: "Graceful Release Period", + 113: "PDN Type", + 114: "Failed Rule ID", + 115: "Time Quota Mechanism", + 116: "User Plane IP Resource Information", + 117: "User Plane Inactivity Timer", + 118: "Aggregated URRs", + 119: "Multiplier", + 120: "Aggregated URR ID", + 121: "Subsequent Volume Quota", + 122: "Subsequent Time Quota", + 123: "RQI", + 124: "QFI", + 125: "Query URR Reference", + 126: "Additional Usage Reports Information", + 127: "Create Traffic Endpoint", + 128: "Created Traffic Endpoint", + 129: "Update Traffic Endpoint", + 130: "Remove Traffic Endpoint", + 131: "Traffic Endpoint ID", + 132: "Ethernet Packet Filter", + 133: "MAC Address", + 134: "C-TAG", + 135: "S-TAG", + 136: "Ethertype", + 137: "Proxying", + 138: "Ethernet Filter ID", + 139: "Ethernet Filter Properties", + 140: "Suggested Buffering Packets Count", + 141: "User ID", + 142: "Ethernet PDU Session Information", + 143: "Ethernet Traffic Information", + 144: "MAC Addresses Detected", + 145: "MAC Addresses Removed", + 146: "Ethernet Inactivity Timer", + 147: "Additional Monitoring Time", + 148: "Event Quota", + 149: "Event Threshold", + 150: "Subsequent Event Quota", + 151: "Subsequent Event Threshold", + 152: "Trace Information", + 153: "Framed-Route", + 154: "Framed-Routing", + 155: "Framed-IPv6-Route", + 156: "Event Time Stamp", + 157: "Averaging Window", + 158: "Paging Policy Indicator", + 159: "APN/DNN", + 160: "3GPP Interface Type", +} + +CauseValues = { + 0: "Reserved", + 1: "Request accepted", + 64: "Request rejected", + 65: "Session context not found", + 66: "Mandatory IE missing", + 67: "Conditional IE missing", + 68: "Invalid length", + 69: "Mandatory IE incorrect", + 70: "Invalid Forwarding Policy", + 71: "Invalid F-TEID allocation option", + 72: "No established Sx Association", + 73: "Rule creation/modification Failure", + 74: "PFCP entity in congestion", + 75: "No resources available", + 76: "Service not supported", + 77: "System failure", +} + +SourceInterface = { + 0: "Access", + 1: "Core", + 2: "SGi-LAN/N6-LAN", + 3: "CP-function", +} + +DestinationInterface = { + 0: "Access", + 1: "Core", + 2: "SGi-LAN/N6-LAN", + 3: "CP-function", + 4: "LI function", +} + +RedirectAddressType = { + 0: "IPv4 address", + 1: "IPv6 address", + 2: "URL", + 3: "SIP URI", +} + +GateStatus = { + 0: "OPEN", + 1: "CLOSED", + 2: "CLOSED_RESERVED_2", + 3: "CLOSED_RESERVED_3", +} + +TimerUnit = { + 0: '2 seconds', + 1: '1 minute', + 2: '10 minutes', + 3: '1 hour', + 4: '10 hours', + 7: 'infinite', +} + +OuterHeaderRemovalDescription = { + 0: "GTP-U/UDP/IPv4", + 1: "GTP-U/UDP/IPv6", + 2: "UDP/IPv4", + 3: "UDP/IPv6", + 4: "IPv4", + 5: "IPv6", + 6: "GTP-U/UDP/IP", + 7: "VLAN S-TAG", + 8: "S-TAG and C-TAG", +} + +NodeIdType = { + 0: "IPv4", + 1: "IPv6", + 2: "FQDN", +} + +FqCSIDNodeIdType = { + 0: "IPv4", + 1: "IPv6", + 2: "MCCMNCId", +} + +FlowDirection = { + 0: "Unspecified", + 1: "Downlink", # traffic to the UE + 2: "Uplink", # traffic from the UE + 3: "Bidirectional", + 4: "Unspecified4", + 5: "Unspecified5", + 6: "Unspecified6", + 7: "Unspecified7", +} + +TimeUnit = { + 0: "minute", + 1: "6 minutes", + 2: "hour", + 3: "day", + 4: "week", + 5: "min5", # same as 0 (minute) + 6: "min6", # same as 0 (minute) + 7: "min7", # same as 0 (minute) +} + +HeaderType = { + 0: "HTTP", +} + +PDNType = { + 0: "IPv4", + 1: "IPv6", + 2: "IPv4v6", + 3: "Non-IP", + 4: "Ethernet", +} + +RuleIDType = { + 0: "PDR", + 1: "FAR", + 2: "QER", + 3: "URR", + 4: "BAR", + # TODO: other values should be interpreted as '1' if received +} + +BaseTimeInterval = { + 0: "CTP", + 1: "DTP", +} + +InterfaceType = { + 0: "S1-U", + 1: "S5 /S8-U", + 2: "S4-U", + 3: "S11-U", + 4: "S12-U", + 5: "Gn/Gp-U", + 6: "S2a-U", + 7: "S2b-U", + 8: "eNodeB GTP-U interface for DL data forwarding", + 9: "eNodeB GTP-U interface for UL data forwarding", + 10: "SGW/UPF GTP-U interface for DL data forwarding", + 11: "N3 3GPP Access", + 12: "N3 Trusted Non-3GPP Access", + 13: "N3 Untrusted Non-3GPP Access", + 14: "N3 for data forwarding", + 15: "N9", +} + + +class PFCPLengthMixin(object): + def post_build(self, p, pay): + p += pay + if self.length is None: + tmp_len = len(p) - 4 + p = p[:2] + struct.pack("!H", tmp_len) + p[4:] + return p + + +class PFCP(PFCPLengthMixin, Packet): + # 3GPP TS 29.244 V15.6.0 (2019-07) + # without the version + name = "PFCP (v1) Header" + fields_desc = [ + BitField("version", 1, 3), + XBitField("spare_b2", 0, 1), + XBitField("spare_b3", 0, 1), + XBitField("spare_b4", 0, 1), + BitField("MP", 0, 1), + BitField("S", 1, 1), + ByteEnumField("message_type", None, PFCPmessageType), + ShortField("length", None), + ConditionalField(XLongField("seid", 0), + lambda pkt:pkt.S == 1), + ThreeBytesField("seq", 0), + ConditionalField(BitField("priority", 0, 4), + lambda pkt:pkt.MP == 1), + ConditionalField(BitField("spare_p", 0, 4), + lambda pkt:pkt.MP == 1), + ConditionalField(ByteField("spare_oct", 0), + lambda pkt:pkt.MP == 0), + ] + + def hashret(self): + return struct.pack("B", self.version) + struct.pack("I", self.seq) + \ + self.payload.hashret() + + def answers(self, other): + return (isinstance(other, PFCP) and + self.version == other.version and + self.seq == other.seq and + self.payload.answers(other.payload)) + + +class APNStrLenField(StrLenField): + # Inspired by DNSStrField + def m2i(self, pkt, s): + ret_s = b"" + tmp_s = s + while tmp_s: + tmp_len = orb(tmp_s[0]) + 1 + if tmp_len > len(tmp_s): + warning("APN prematured end of character-string (size=%i, remaining bytes=%i)" % (tmp_len, len(tmp_s))) # noqa: E501 + ret_s += tmp_s[1:tmp_len] + tmp_s = tmp_s[tmp_len:] + if len(tmp_s): + ret_s += b"." + s = ret_s + return s + + def i2m(self, pkt, s): + s = b"".join(chb(len(x)) + x for x in s.split(b".")) + return s + + +class ExtraDataField(StrField): + def __init__(self, name, default=b""): + StrField.__init__(self, name, default) + + def addfield(self, pkt, s, val): + return s + self.i2m(pkt, val) + + def getfield(self, pkt, s): + # + 4 accounts for the ietype and length fields + p = len(pkt.original) - len(s) + length = pkt.length + 4 - p + return s[length:], self.m2i(pkt, s[:length]) + + def randval(self): + return RandBin(RandNum(0, 2)) + + +class Int40Field(Field): + def __init__(self, name, default): + Field.__init__(self, name, default, "BI") + + def addfield(self, pkt, s, val): + val = self.i2m(pkt, val) + return s + struct.pack("!BI", val >> 32, val & 0xffffffff) + + def getfield(self, pkt, s): + hi, lo = struct.unpack("!BI", s[:5]) + return s[5:], self.m2i(pkt, (hi << 32) + lo) + + def randval(self): + return RandNum(0, 2**40 - 1) + + +def IE_Dispatcher(s): + """Choose the correct Information Element class.""" + + # Get the IE type + ietype = (orb(s[0]) * 256) + orb(s[1]) + if ietype & 0x8000: + return IE_EnterpriseSpecific(s) + + cls = ietypecls.get(ietype, Raw) + if cls is Raw: + cls = IE_NotImplemented + + return cls(s) + + +class IE_Base(PFCPLengthMixin, Packet): + default_length = None + + def __init__(self, *args, **kwargs): + self.fields_desc[0].default = self.ie_type + self.fields_desc[1].default = self.default_length + super(IE_Base, self).__init__(*args, **kwargs) + + def extract_padding(self, pkt): + return "", pkt + + fields_desc = [ + ShortEnumField("ietype", 0, IEType), + ShortField("length", None) + ] + + +class IE_Compound(IE_Base): + fields_desc = IE_Base.fields_desc + [ + PacketListField("IE_list", None, IE_Dispatcher, + length_from=lambda pkt: pkt.length) + ] + + +class IE_CreatePDR(IE_Compound): + name = "IE Create PDR" + ie_type = 1 + + +class IE_PDI(IE_Compound): + name = "IE PDI" + ie_type = 2 + + +class IE_CreateFAR(IE_Compound): + name = "IE Create FAR" + ie_type = 3 + + +class IE_ForwardingParameters(IE_Compound): + name = "IE Forwarding Parameters" + ie_type = 4 + + +class IE_DuplicatingParameters(IE_Compound): + name = "IE Duplicating Parameters" + ie_type = 5 + + +class IE_CreateURR(IE_Compound): + name = "IE Create URR" + ie_type = 6 + + +class IE_CreateQER(IE_Compound): + name = "IE Create QER" + ie_type = 7 + + +class IE_CreatedPDR(IE_Compound): + name = "IE Created PDR" + ie_type = 8 + + +class IE_UpdatePDR(IE_Compound): + name = "IE Update PDR" + ie_type = 9 + + +class IE_UpdateFAR(IE_Compound): + name = "IE Update FAR" + ie_type = 10 + + +class IE_UpdateForwardingParameters(IE_Compound): + name = "IE Update Forwarding Parameters" + ie_type = 11 + + +class IE_UpdateBAR_SRR(IE_Compound): + name = "IE Update BAR (PFCP Session Report Response)" + ie_type = 12 + + +class IE_UpdateURR(IE_Compound): + name = "IE Update URR" + ie_type = 13 + + +class IE_UpdateQER(IE_Compound): + name = "IE Update QER" + ie_type = 14 + + +class IE_RemovePDR(IE_Compound): + name = "IE Remove PDR" + ie_type = 15 + + +class IE_RemoveFAR(IE_Compound): + name = "IE Remove FAR" + ie_type = 16 + + +class IE_RemoveURR(IE_Compound): + name = "IE Remove URR" + ie_type = 17 + + +class IE_RemoveQER(IE_Compound): + name = "IE Remove QER" + ie_type = 18 + + +class IE_LoadControlInformation(IE_Compound): + name = "IE Load Control Information" + ie_type = 51 + + +class IE_OverloadControlInformation(IE_Compound): + name = "IE Overload Control Information" + ie_type = 54 + + +class IE_ApplicationID_PFDs(IE_Compound): + name = "IE Application ID's PFDs" + ie_type = 58 + + +class IE_PFDContext(IE_Compound): + name = "IE PFD context" + ie_type = 59 + + +class IE_ApplicationDetectionInformation(IE_Compound): + name = "IE Application Detection Information" + ie_type = 68 + + +class IE_QueryURR(IE_Compound): + name = "IE Query URR" + ie_type = 77 + + +class IE_UsageReport_SMR(IE_Compound): + name = "IE Usage Report (Session Modification Response)" + ie_type = 78 + + +class IE_UsageReport_SDR(IE_Compound): + name = "IE Usage Report (Session Deletion Response)" + ie_type = 79 + + +class IE_UsageReport_SRR(IE_Compound): + name = "IE Usage Report (Session Report Request)" + ie_type = 80 + + +class IE_DownlinkDataReport(IE_Compound): + name = "IE Downlink Data Report" + ie_type = 83 + + +class IE_Create_BAR(IE_Compound): + name = "IE Create BAR" + ie_type = 85 + + +class IE_Update_BAR_SMR(IE_Compound): + name = "IE Update BAR (Session Modification Request)" + ie_type = 86 + + +class IE_Remove_BAR(IE_Compound): + name = "IE Remove BAR" + ie_type = 87 + + +class IE_ErrorIndicationReport(IE_Compound): + name = "IE Error Indication Report" + ie_type = 99 + + +class IE_UserPlanePathFailureReport(IE_Compound): + name = "IE User Plane Path Failure Report" + ie_type = 102 + + +class IE_UpdateDuplicatingParameters(IE_Compound): + name = "IE Update Duplicating Parameters" + ie_type = 105 + + +class IE_AggregatedURRs(IE_Compound): + name = "IE Aggregated URRs" + ie_type = 118 + + +class IE_CreateTrafficEndpoint(IE_Compound): + name = "IE Create Traffic Endpoint" + ie_type = 127 + + +class IE_CreatedTrafficEndpoint(IE_Compound): + name = "IE Created Traffic Endpoint" + ie_type = 128 + + +class IE_UpdateTrafficEndpoint(IE_Compound): + name = "IE Update Traffic Endpoint" + ie_type = 129 + + +class IE_RemoveTrafficEndpoint(IE_Compound): + name = "IE Remove Traffic Endpoint" + ie_type = 130 + + +class IE_EthernetPacketFilter(IE_Compound): + name = "IE Ethernet Packet Filter" + ie_type = 132 + + +class IE_EthernetTrafficInformation(IE_Compound): + name = "IE Ethernet Traffic Information" + ie_type = 143 + + +class IE_AdditionalMonitoringTime(IE_Compound): + name = "IE Additional Monitoring Time" + ie_type = 147 + + +class IE_Cause(IE_Base): + ie_type = 19 + name = "IE Cause" + fields_desc = IE_Base.fields_desc + [ + ByteEnumField("cause", None, CauseValues) + ] + + +class IE_SourceInterface(IE_Base): + name = "IE Source Interface" + ie_type = 20 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitEnumField("interface", "Access", 4, SourceInterface), + ExtraDataField("extra_data"), + ] + + +class IE_FTEID(IE_Base): + name = "IE F-TEID" + ie_type = 21 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitField("CHID", 0, 1), + BitField("CH", 0, 1), + BitField("V6", 0, 1), + BitField("V4", 0, 1), + ConditionalField(XIntField("TEID", 0), lambda x: x.CH == 0), + ConditionalField(IPField("ipv4", 0), + lambda x: x.V4 == 1 and x.CH == 0), + ConditionalField(IP6Field("ipv6", 0), + lambda x: x.V6 == 1 and x.CH == 0), + ConditionalField(ByteField("choose_id", 0), + lambda x: x.CHID == 1), + ExtraDataField("extra_data"), + ] + + +class IE_NetworkInstance(IE_Base): + name = "IE Network Instance" + ie_type = 22 + fields_desc = IE_Base.fields_desc + [ + APNStrLenField("instance", "", length_from=lambda x: x.length) + ] + + +class IE_SDF_Filter(IE_Base): + name = "IE SDF Filter" + ie_type = 23 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 3), + BitField("BID", 0, 1), + BitField("FL", 0, 1), + BitField("SPI", 0, 1), + BitField("TTC", 0, 1), + BitField("FD", 0, 1), + ByteField("spare_oct", 0), + ConditionalField(FieldLenField("flow_description_length", None, + length_of="flow_description"), + lambda pkt: pkt.FD == 1), + ConditionalField(StrLenField("flow_description", "", + length_from=lambda pkt: + pkt.flow_description_length), + lambda pkt: pkt.FD == 1), + ConditionalField(ByteField("tos_traffic_class", 0), + lambda pkt: pkt.TTC == 1), + ConditionalField(ByteField("tos_traffic_mask", 0), + lambda pkt: pkt.TTC == 1), + ConditionalField(IntField("security_parameter_index", 0), + lambda pkt: pkt.SPI == 1), + ConditionalField(ThreeBytesField("flow_label", 0), + lambda pkt: pkt.FL == 1), + ConditionalField(IntField("sdf_filter_id", 0), + lambda pkt: pkt.BID == 1), + ExtraDataField("extra_data"), + ] + + +class IE_ApplicationId(IE_Base): + name = "IE Application ID" + ie_type = 24 + fields_desc = IE_Base.fields_desc + [ + StrLenField("id", "", length_from=lambda x: x.length), + ] + + +class IE_GateStatus(IE_Base): + name = "IE Gate Status" + ie_type = 25 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitEnumField("ul", "OPEN", 2, GateStatus), + BitEnumField("dl", "OPEN", 2, GateStatus), + ExtraDataField("extra_data"), + ] + + +class IE_MBR(IE_Base): + name = "IE MBR" + ie_type = 26 + fields_desc = IE_Base.fields_desc + [ + Int40Field("ul", 0), + Int40Field("dl", 0), + ExtraDataField("extra_data"), + ] + + +class IE_GBR(IE_Base): + name = "IE GBR" + ie_type = 27 + fields_desc = IE_Base.fields_desc + [ + Int40Field("ul", 0), + Int40Field("dl", 0), + ExtraDataField("extra_data"), + ] + + +class IE_QERCorrelationId(IE_Base): + name = "IE QER Correlation ID" + ie_type = 28 + fields_desc = IE_Base.fields_desc + [ + IntField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_Precedence(IE_Base): + name = "IE Precedence" + ie_type = 29 + fields_desc = IE_Base.fields_desc + [ + IntField("precedence", 0), + ExtraDataField("extra_data"), + ] + + +class IE_TransportLevelMarking(IE_Base): + name = "IE Transport Level Marking" + ie_type = 30 + fields_desc = IE_Base.fields_desc + [ + XByteField("tos", 0), + XByteField("traffic_class", 0), + ExtraDataField("extra_data"), + ] + + +class IE_VolumeThreshold(IE_Base): + name = "IE Volume Threshold" + ie_type = 31 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("DLVOL", 0, 1), + BitField("ULVOL", 0, 1), + BitField("TOVOL", 0, 1), + ConditionalField(XLongField("total", 0), lambda x: x.TOVOL == 1), + ConditionalField(XLongField("uplink", 0), lambda x: x.ULVOL == 1), + ConditionalField(XLongField("downlink", 0), lambda x: x.DLVOL == 1), + ExtraDataField("extra_data"), + ] + + +class IE_TimeThreshold(IE_Base): + name = "IE Time Threshold" + ie_type = 32 + fields_desc = IE_Base.fields_desc + [ + IntField("threshold", 0), + ExtraDataField("extra_data"), + ] + + +class IE_MonitoringTime(IE_Base): + name = "IE Monitoring Time" + ie_type = 33 + fields_desc = IE_Base.fields_desc + [ + IntField("time_value", 0), + ExtraDataField("extra_data"), + ] + + +class IE_SubsequentVolumeThreshold(IE_Base): + name = "IE Subsequent Volume Threshold" + ie_type = 34 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("DLVOL", 0, 1), + BitField("ULVOL", 0, 1), + BitField("TOVOL", 0, 1), + ConditionalField(XLongField("total", 0), lambda x: x.TOVOL == 1), + ConditionalField(XLongField("uplink", 0), lambda x: x.ULVOL == 1), + ConditionalField(XLongField("downlink", 0), lambda x: x.DLVOL == 1), + ExtraDataField("extra_data"), + ] + + +class IE_SubsequentTimeThreshold(IE_Base): + name = "IE Subsequent Time Threshold" + ie_type = 35 + fields_desc = IE_Base.fields_desc + [ + IntField("threshold", 0), + ExtraDataField("extra_data"), + ] + + +class IE_InactivityDetectionTime(IE_Base): + name = "IE Inactivity Detection Time" + ie_type = 36 + fields_desc = IE_Base.fields_desc + [ + IntField("time_value", 0), + ExtraDataField("extra_data"), + ] + + +class IE_ReportingTriggers(IE_Base): + name = "IE Reporting Triggers" + ie_type = 37 + fields_desc = IE_Base.fields_desc + [ + BitField("linked_usage_reporting", 0, 1), + BitField("dropped_dl_traffic_threshold", 0, 1), + BitField("stop_of_traffic", 0, 1), + BitField("start_of_traffic", 0, 1), + BitField("quota_holding_time", 0, 1), + BitField("time_threshold", 0, 1), + BitField("volume_threshold", 0, 1), + BitField("periodic_reporting", 0, 1), + XBitField("spare", 0, 2), + BitField("event_quota", 0, 1), + BitField("event_threshold", 0, 1), + BitField("mac_addresses_reporting", 0, 1), + BitField("envelope_closure", 0, 1), + BitField("time_quota", 0, 1), + BitField("volume_quota", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_RedirectInformation(IE_Base): + name = "IE Redirect Information" + ie_type = 38 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitEnumField("type", "IPv4 address", 4, RedirectAddressType), + FieldLenField("address_length", None, length_of="address"), + StrLenField("address", "", length_from=lambda pkt: pkt.address_length), + ExtraDataField("extra_data"), + ] + + +class IE_ReportType(IE_Base): + name = "IE Report Type" + ie_type = 39 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitField("UPIR", 0, 1), + BitField("ERIR", 0, 1), + BitField("USAR", 0, 1), + BitField("DLDR", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_OffendingIE(IE_Base): + name = "IE Offending IE" + ie_type = 40 + fields_desc = IE_Base.fields_desc + [ + ShortEnumField("type", None, IEType) + ] + + +class IE_ForwardingPolicy(IE_Base): + name = "IE Forwarding Policy" + ie_type = 41 + fields_desc = IE_Base.fields_desc + [ + FieldLenField("policy_identifier_length", None, + length_of="policy_identifier", fmt="B"), + StrLenField("policy_identifier", "", + length_from=lambda pkt: pkt.policy_identifier_length) + ] + + +class IE_DestinationInterface(IE_Base): + name = "IE Destination Interface" + ie_type = 42 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitEnumField("interface", "Access", 4, DestinationInterface), + ExtraDataField("extra_data"), + ] + + +class IE_UPFunctionFeatures(IE_Base): + name = "IE UP Function Features" + ie_type = 43 + default_length = 2 + fields_desc = IE_Base.fields_desc + [ + ConditionalField(BitField("TREU", None, 1), lambda x: x.length > 0), + ConditionalField(BitField("HEEU", None, 1), lambda x: x.length > 0), + ConditionalField(BitField("PFDM", None, 1), lambda x: x.length > 0), + ConditionalField(BitField("FTUP", None, 1), lambda x: x.length > 0), + + ConditionalField(BitField("TRST", None, 1), lambda x: x.length > 0), + ConditionalField(BitField("DLBD", None, 1), lambda x: x.length > 0), + ConditionalField(BitField("DDND", None, 1), lambda x: x.length > 0), + ConditionalField(BitField("BUCP", None, 1), lambda x: x.length > 0), + + ConditionalField(BitField("spare", None, 1), lambda x: x.length > 1), + ConditionalField(BitField("PFDE", None, 1), lambda x: x.length > 1), + ConditionalField(BitField("FRRT", None, 1), lambda x: x.length > 1), + ConditionalField(BitField("TRACE", None, 1), lambda x: x.length > 1), + + ConditionalField(BitField("QUOAC", None, 1), lambda x: x.length > 1), + ConditionalField(BitField("UDBC", None, 1), lambda x: x.length > 1), + ConditionalField(BitField("PDIU", None, 1), lambda x: x.length > 1), + ConditionalField(BitField("EMPU", None, 1), lambda x: x.length > 1), + + ExtraDataField("extra_data"), + ] + + +class IE_ApplyAction(IE_Base): + name = "IE Apply Action" + ie_type = 44 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", None, 3), + BitField("DUPL", 0, 1), + BitField("NOCP", 0, 1), + BitField("BUFF", 0, 1), + BitField("FORW", 0, 1), + BitField("DROP", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_DownlinkDataServiceInformation(IE_Base): + name = "IE Downlink Data Service Information" + ie_type = 45 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare_1", None, 6), + BitField("QFII", 0, 1), + BitField("PPI", 0, 1), + ConditionalField( + XBitField("spare_2", None, 2), + lambda x: x.PPI == 1), + ConditionalField( + XBitField("ppi_val", None, 6), + lambda x: x.PPI == 1), + ConditionalField( + XBitField("spare_3", None, 2), + lambda x: x.QFII == 1), + ConditionalField( + XBitField("qfi_val", None, 6), + lambda x: x.QFII == 1), + ExtraDataField("extra_data"), + ] + + +class IE_DownlinkDataNotificationDelay(IE_Base): + name = "IE Downlink Data Notification Delay" + ie_type = 46 + fields_desc = IE_Base.fields_desc + [ + ByteField("delay", 0), # in multiples of 50 + ExtraDataField("extra_data"), + ] + + +class IE_DLBufferingDuration(IE_Base): + name = "IE DL Buffering Duration" + ie_type = 47 + fields_desc = IE_Base.fields_desc + [ + BitEnumField("timer_unit", "2 seconds", 3, TimerUnit), + BitField("timer_value", 0, 5), + ExtraDataField("extra_data"), + ] + + +class IE_DLBufferingSuggestedPacketCount(IE_Base): + name = "IE DL Buffering Suggested Packet Count" + ie_type = 48 + fields_desc = IE_Base.fields_desc + [ + MultipleTypeField([ + ( + ByteField("count", 0), + (lambda x: x.length == 1, + lambda x, val: x.length == 1 or + (x.length is None and val < 256)), + ), + ( + ShortField("count", 0), + (lambda x: x.length == 2, + lambda x, val: x.length == 1 or + (x.length is None and val >= 256)) + ), + ], ByteField("count", 0)) + ] + + +class IE_PFCPSMReqFlags(IE_Base): + name = "IE PFCPSMReq-Flags" + ie_type = 49 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", None, 5), + BitField("QUARR", 0, 1), + BitField("SNDEM", 0, 1), + BitField("DROBU", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_PFCPSRRspFlags(IE_Base): + name = "IE PFCPSRRsp-Flags" + ie_type = 50 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", None, 7), + BitField("DROBU", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_SequenceNumber(IE_Base): + name = "IE Sequence Number" + ie_type = 52 + fields_desc = IE_Base.fields_desc + [ + IntField("number", 0), + ] + + +class IE_Metric(IE_Base): + name = "IE Metric" + ie_type = 53 + fields_desc = IE_Base.fields_desc + [ + ByteField("metric", 0), + ] + + +class IE_Timer(IE_Base): + name = "IE Timer" + ie_type = 55 + fields_desc = IE_Base.fields_desc + [ + BitEnumField("timer_unit", "2 seconds", 3, TimerUnit), + BitField("timer_value", 0, 5), + ExtraDataField("extra_data"), + ] + + +class IE_PDR_Id(IE_Base): + name = "IE PDR ID" + ie_type = 56 + fields_desc = IE_Base.fields_desc + [ + ShortField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_FSEID(IE_Base): + name = "IE F-SEID" + ie_type = 57 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 6), + BitField("v4", 0, 1), + BitField("v6", 0, 1), + XLongField("seid", 0), + ConditionalField(IPField("ipv4", 0), + lambda x: x.v4 == 1), + ConditionalField(IP6Field("ipv6", 0), + lambda x: x.v6 == 1), + ExtraDataField("extra_data"), + ] + + +class IE_NodeId(IE_Base): + name = "IE Node ID" + ie_type = 60 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitEnumField("id_type", "IPv4", 4, NodeIdType), + ConditionalField(IPField("ipv4", 0), + lambda x: x.id_type == 0), + ConditionalField(IP6Field("ipv6", 0), + lambda x: x.id_type == 1), + ConditionalField( + APNStrLenField("id", "", length_from=lambda x: x.length - 1), + lambda x: x.id_type == 2), + ExtraDataField("extra_data"), + ] + + +class IE_PFDContents(IE_Base): + name = "IE PFD contents" + ie_type = 61 + fields_desc = IE_Base.fields_desc + [ + BitField("ADNP", 0, 1), + BitField("AURL", 0, 1), + BitField("AFD", 0, 1), + BitField("DNP", 0, 1), + BitField("CP", 0, 1), + BitField("DN", 0, 1), + BitField("URL", 0, 1), + BitField("FD", 0, 1), + ByteField("spare_2", 0), + ConditionalField(FieldLenField("flow_length", None, length_of="flow"), + lambda pkt: pkt.FD == 1), + ConditionalField(StrLenField("flow", "", + length_from=lambda pkt: pkt.flow_length), + lambda pkt: pkt.FD == 1), + ConditionalField(FieldLenField("url_length", None, length_of="url"), + lambda pkt: pkt.URL == 1), + ConditionalField(StrLenField("url", "", + length_from=lambda pkt: pkt.url_length), + lambda pkt: pkt.URL == 1), + ConditionalField(FieldLenField("domain_length", None, + length_of="domain"), + lambda pkt: pkt.DN == 1), + ConditionalField( + StrLenField("domain", "", + length_from=lambda pkt: pkt.domain_length), + lambda pkt: pkt.DN == 1), + ConditionalField(FieldLenField("custom_length", None, + length_of="custom"), + lambda pkt: pkt.CP == 1), + ConditionalField( + StrLenField("custom", "", + length_from=lambda pkt: pkt.custom_length), + lambda pkt: pkt.CP == 1), + ConditionalField(FieldLenField("dnp_length", None, length_of="dnp"), + lambda pkt: pkt.DNP == 1), + ConditionalField(StrLenField("dnp", "", + length_from=lambda pkt: pkt.dnp_length), + lambda pkt: pkt.DNP == 1), + ConditionalField(FieldLenField("additional_flow_length", None, + length_of="additional_flow"), + lambda pkt: pkt.AFD == 1), + ConditionalField( + StrLenField("additional_flow", "", + length_from=lambda pkt: pkt.additional_flow_length), + lambda pkt: pkt.AFD == 1), + ConditionalField(FieldLenField("additional_url_length", None, + length_of="additional_url"), + lambda pkt: pkt.AURL == 1), + ConditionalField( + StrLenField("additional_url", "", + length_from=lambda pkt: pkt.additional_url_length), + lambda pkt: pkt.AURL == 1), + ConditionalField( + FieldLenField("additional_dn_dnp_length", None, + length_of="additional_dn_dnp"), + lambda pkt: pkt.ADNP == 1), + ConditionalField( + StrLenField("additional_dn_dnp", "", + length_from=lambda pkt: pkt.additional_dn_dnp_length), + lambda pkt: pkt.ADNP == 1), + ExtraDataField("extra_data"), + ] + + +class IE_MeasurementMethod(IE_Base): + name = "IE Measurement Method" + ie_type = 62 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("EVENT", 0, 1), + BitField("VOLUM", 0, 1), + BitField("DURAT", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_UsageReportTrigger(IE_Base): + name = "IE Usage Report Trigger" + ie_type = 63 + fields_desc = IE_Base.fields_desc + [ + BitField("IMMER", 0, 1), + BitField("DROTH", 0, 1), + BitField("STOPT", 0, 1), + BitField("START", 0, 1), + BitField("QUHTI", 0, 1), + BitField("TIMTH", 0, 1), + BitField("VOLTH", 0, 1), + BitField("PERIO", 0, 1), + BitField("EVETH", 0, 1), + BitField("MACAR", 0, 1), + BitField("ENVCL", 0, 1), + BitField("MONIT", 0, 1), + BitField("TERMR", 0, 1), + BitField("LIUSA", 0, 1), + BitField("TIMQU", 0, 1), + BitField("VOLQU", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_MeasurementPeriod(IE_Base): + name = "IE Measurement Period" + ie_type = 64 + fields_desc = IE_Base.fields_desc + [ + IntField("period", 0), + ExtraDataField("extra_data"), + ] + + +class IE_FqCSID(IE_Base): + name = "IE FQ-CSID" + ie_type = 65 + fields_desc = IE_Base.fields_desc + [ + BitEnumField("node_id_type", "IPv4", 4, FqCSIDNodeIdType), + BitFieldLenField("num_csids", None, 4, count_of="csids"), + ConditionalField(IPField("ipv4", 0), + lambda x: x.node_id_type == 0), + ConditionalField(IP6Field("ipv6", 0), + lambda x: x.node_id_type == 1), + ConditionalField( + # FIXME: split (value = mcc * 1000 + mnc) + BitField("mcc_mnc", 0, 20), + lambda x: x.node_id_type == 2), + # "Least significant 12 bits is a 12 bit integer assigned by + # an operator to an MME, SGW-C, SGW-U, PGW-C or PGW-U." + ConditionalField( + BitField("extra_id", 0, 12), + lambda x: x.node_id_type == 2), + FieldListField("csids", None, ShortField("csid", 0), + count_from=lambda x: x.num_csids), + ExtraDataField("extra_data"), + ] + + +class IE_VolumeMeasurement(IE_Base): + name = "IE Volume Measurement" + ie_type = 66 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("DLVOL", 0, 1), + BitField("ULVOL", 0, 1), + BitField("TOVOL", 0, 1), + ConditionalField(XLongField("total", 0), lambda x: x.TOVOL == 1), + ConditionalField(XLongField("uplink", 0), lambda x: x.ULVOL == 1), + ConditionalField(XLongField("downlink", 0), lambda x: x.DLVOL == 1), + ExtraDataField("extra_data"), + ] + + +class IE_DurationMeasurement(IE_Base): + name = "IE Duration Measurement" + ie_type = 67 + fields_desc = IE_Base.fields_desc + [ + IntField("duration", 0), + ExtraDataField("extra_data"), + ] + + +class IE_TimeOfFirstPacket(IE_Base): + name = "IE Time of First Packet" + ie_type = 69 + fields_desc = IE_Base.fields_desc + [ + IntField("timestamp", 0), + ExtraDataField("extra_data"), + ] + + +class IE_TimeOfLastPacket(IE_Base): + name = "IE Time of Last Packet" + ie_type = 70 + fields_desc = IE_Base.fields_desc + [ + IntField("timestamp", 0), + ExtraDataField("extra_data"), + ] + + +class IE_QuotaHoldingTime(IE_Base): + name = "IE Quota Holding Time" + ie_type = 71 + fields_desc = IE_Base.fields_desc + [ + IntField("time_value", 0), + ExtraDataField("extra_data"), + ] + + +class IE_DroppedDLTrafficThreshold(IE_Base): + name = "IE Dropped DL Traffic Threshold" + ie_type = 72 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 6), + BitField("DLBY", 0, 1), + BitField("DLPA", 0, 1), + ConditionalField(LongField("packet_count", 0), + lambda x: x.DLPA == 1), + ConditionalField(LongField("byte_count", 0), + lambda x: x.DLBY == 1), + ExtraDataField("extra_data"), + ] + + +class IE_VolumeQuota(IE_Base): + name = "IE Volume Quota" + ie_type = 73 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("DLVOL", 0, 1), + BitField("ULVOL", 0, 1), + BitField("TOVOL", 0, 1), + ConditionalField(XLongField("total", 0), lambda x: x.TOVOL == 1), + ConditionalField(XLongField("uplink", 0), lambda x: x.ULVOL == 1), + ConditionalField(XLongField("downlink", 0), lambda x: x.DLVOL == 1), + ExtraDataField("extra_data"), + ] + + +class IE_TimeQuota(IE_Base): + name = "IE Time Quota" + ie_type = 74 + fields_desc = IE_Base.fields_desc + [ + IntField("quota", 0), + ExtraDataField("extra_data"), + ] + + +class IE_StartTime(IE_Base): + name = "IE Start Time" + ie_type = 75 + fields_desc = IE_Base.fields_desc + [ + IntField("timestamp", 0), + ExtraDataField("extra_data"), + ] + + +class IE_EndTime(IE_Base): + name = "IE End Time" + ie_type = 76 + fields_desc = IE_Base.fields_desc + [ + IntField("timestamp", 0), + ExtraDataField("extra_data"), + ] + + +class IE_URR_Id(IE_Base): + name = "IE URR ID" + ie_type = 81 + fields_desc = IE_Base.fields_desc + [ + IntField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_LinkedURR_Id(IE_Base): + name = "IE Linked URR ID" + ie_type = 82 + fields_desc = IE_Base.fields_desc + [ + IntField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_OuterHeaderCreation(IE_Base): + name = "IE Outer Header Creation" + ie_type = 84 + fields_desc = IE_Base.fields_desc + [ + BitField("STAG", 0, 1), + BitField("CTAG", 0, 1), + BitField("IPV6", 0, 1), + BitField("IPV4", 0, 1), + BitField("UDPIPV6", 0, 1), + BitField("UDPIPV4", 0, 1), + BitField("GTPUUDPIPV6", 0, 1), + BitField("GTPUUDPIPV4", 0, 1), + ByteField("spare", 0), + ConditionalField(XIntField("TEID", 0), + lambda x: x.GTPUUDPIPV4 == 1 or x.GTPUUDPIPV6 == 1), + ConditionalField(IPField("ipv4", 0), + lambda x: + x.IPV4 == 1 or x.UDPIPV4 == 1 or x.GTPUUDPIPV4 == 1), + ConditionalField(IP6Field("ipv6", 0), + lambda x: + x.IPV6 == 1 or x.UDPIPV6 == 1 or x.GTPUUDPIPV6 == 1), + ConditionalField(ShortField("port", 0), + lambda x: x.UDPIPV4 == 1 or x.UDPIPV6 == 1), + ConditionalField(ThreeBytesField("ctag", 0), + lambda x: x.CTAG == 1), + ConditionalField(ThreeBytesField("stag", 0), + lambda x: x.STAG == 1), + ExtraDataField("extra_data"), + ] + + +class IE_BAR_Id(IE_Base): + name = "IE BAR ID" + ie_type = 88 + fields_desc = IE_Base.fields_desc + [ + ByteField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_CPFunctionFeatures(IE_Base): + name = "IE CP Function Features" + ie_type = 89 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 6), + BitField("OVRL", 0, 1), + BitField("LOAD", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_UsageInformation(IE_Base): + name = "IE Usage Information" + ie_type = 90 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitField("UBE", 0, 1), + BitField("UAE", 0, 1), + BitField("AFT", 0, 1), + BitField("BEF", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_ApplicationInstanceId(IE_Base): + name = "IE Application Instance ID" + ie_type = 91 + fields_desc = IE_Base.fields_desc + [ + StrLenField("id", "", length_from=lambda x: x.length) + ] + + +class IE_FlowInformation(IE_Base): + name = "IE Flow Information" + ie_type = 92 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitEnumField("direction", "Unspecified", 3, FlowDirection), + FieldLenField("flow_length", None, length_of="flow"), + StrLenField("flow", "", length_from=lambda x: x.flow_length), + ExtraDataField("extra_data"), + ] + + +class IE_UE_IP_Address(IE_Base): + name = "IE UE IP Address" + ie_type = 93 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("SD", 0, 1), # source or dest + BitField("V4", 0, 1), + BitField("V6", 0, 1), + ConditionalField(IPField("ipv4", 0), lambda x: x.V4 == 1), + ConditionalField(IP6Field("ipv6", 0), lambda x: x.V6 == 1), + ExtraDataField("extra_data"), + ] + + +class IE_PacketRate(IE_Base): + name = "IE Packet Rate" + ie_type = 94 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare_1", 0, 6), + BitField("DLPR", 0, 1), + BitField("ULPR", 0, 1), + ConditionalField(BitField("spare_2", 0, 5), lambda x: x.ULPR == 1), + ConditionalField(BitEnumField("ul_time_unit", "minute", 3, TimeUnit), + lambda x: x.ULPR == 1), + ConditionalField(ShortField("ul_max_packet_rate", 0), + lambda x: x.ULPR == 1), + ConditionalField(BitField("spare_3", 0, 5), lambda x: x.DLPR == 1), + ConditionalField(BitEnumField("dl_time_unit", "minute", 3, TimeUnit), + lambda x: x.DLPR == 1), + ConditionalField(ShortField("dl_max_packet_rate", 0), + lambda x: x.DLPR == 1), + ExtraDataField("extra_data"), + ] + + +class IE_OuterHeaderRemoval(IE_Base): + name = "IE Outer Header Removal" + ie_type = 95 + fields_desc = IE_Base.fields_desc + [ + ByteEnumField("header", None, OuterHeaderRemovalDescription), + ConditionalField(XBitField("spare", None, 7), + lambda x: x.length is not None and x.length > 1), + ConditionalField(BitField("pdu_session_container", None, 1), + lambda x: x.length is not None and x.length > 1), + ExtraDataField("extra_data"), + ] + + +class IE_RecoveryTimeStamp(IE_Base): + name = "IE Recovery Time Stamp" + ie_type = 96 + default_length = 4 + fields_desc = IE_Base.fields_desc + [ + IntField("timestamp", 0), + ExtraDataField("extra_data"), + ] + + +class IE_DLFlowLevelMarking(IE_Base): + name = "IE DL Flow Level Marking" + ie_type = 97 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare_1", 0, 6), + BitField("SCI", 0, 1), + BitField("TTC", 0, 1), + ConditionalField(ByteField("traffic_class", 0), lambda x: x.TTC), + ConditionalField(ByteField("traffic_class_mask", 0), lambda x: x.TTC), + ConditionalField(ByteField("service_class_indicator", 0), + lambda x: x.SCI), + ConditionalField(ByteField("spare_2", 0), lambda x: x.SCI), + ExtraDataField("extra_data"), + ] + + +class IE_HeaderEnrichment(IE_Base): + name = "IE Header Enrichment" + ie_type = 98 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 3), + BitEnumField("header_type", "HTTP", 5, HeaderType), + FieldLenField("name_length", None, fmt="B", length_of="name"), + StrLenField("name", "", length_from=lambda x: x.name_length), + FieldLenField("value_length", None, fmt="B", length_of="value"), + StrLenField("value", "", length_from=lambda x: x.value_length), + ExtraDataField("extra_data"), + ] + + +class IE_MeasurementInformation(IE_Base): + name = "IE Measurement Information" + ie_type = 100 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 3), + BitField("MNOP", 0, 1), + BitField("ISTM", 0, 1), + BitField("RADI", 0, 1), + BitField("INAM", 0, 1), + BitField("MBQE", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_NodeReportType(IE_Base): + name = "IE Node Report Type" + ie_type = 101 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 7), + BitField("UPFR", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_RemoteGTP_U_Peer(IE_Base): + name = "IE Remote GTP-U Peer" + ie_type = 103 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare_1", 0, 4), + BitField("NI", 0, 1), + BitField("DI", 0, 1), + BitField("V4", 0, 1), + BitField("V6", 0, 1), + ConditionalField(IPField("ipv4", 0), lambda x: x.V4 == 1), + ConditionalField(IP6Field("ipv6", 0), lambda x: x.V6 == 1), + ConditionalField(ByteField("dest_interface_length", 1), + lambda x: x.DI == 1), + ConditionalField(XBitField("spare_2", 0, 4), lambda x: x.DI == 1), + ConditionalField( + BitEnumField("dest_interface", "Access", 4, DestinationInterface), + lambda x: x.DI == 1), + ConditionalField( + FieldLenField("network_instance_length", 1, + length_of="network_instance"), + lambda x: x.NI == 1), + ConditionalField( + APNStrLenField("network_instance", "", + length_from=lambda x: x.network_instance_length), + lambda x: x.NI == 1), + ExtraDataField("extra_data"), + ] + + +class IE_UR_SEQN(IE_Base): + name = "IE UR-SEQN" + ie_type = 104 + fields_desc = IE_Base.fields_desc + [ + IntField("number", 0), + ] + + +class IE_ActivatePredefinedRules(IE_Base): + name = "IE Activate Predefined Rules" + ie_type = 106 + fields_desc = IE_Base.fields_desc + [ + StrLenField("name", "", length_from=lambda x: x.length) + ] + + +class IE_DeactivatePredefinedRules(IE_Base): + name = "IE Deactivate Predefined Rules" + ie_type = 107 + fields_desc = IE_Base.fields_desc + [ + StrLenField("name", "", length_from=lambda x: x.length) + ] + + +class IE_FAR_Id(IE_Base): + name = "IE FAR ID" + ie_type = 108 + fields_desc = IE_Base.fields_desc + [ + IntField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_QER_Id(IE_Base): + name = "IE QER ID" + ie_type = 109 + fields_desc = IE_Base.fields_desc + [ + IntField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_OCIFlags(IE_Base): + name = "IE OCI Flags" + ie_type = 110 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", None, 7), + BitField("AOCI", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_PFCPAssociationReleaseRequest(IE_Base): + name = "IE PFCP Association Release Request" + ie_type = 111 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", None, 7), + BitField("SARR", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_GracefulReleasePeriod(IE_Base): + name = "IE Graceful Release Period" + ie_type = 112 + fields_desc = IE_Base.fields_desc + [ + BitEnumField("release_timer_unit", "2 seconds", 3, TimerUnit), + BitField("release_timer_value", 0, 5), + ExtraDataField("extra_data"), + ] + + +class IE_PDNType(IE_Base): + name = "IE PDN Type" + ie_type = 113 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitEnumField("pdn_type", "IPv4", 3, PDNType), + ExtraDataField("extra_data"), + ] + + +class IE_FailedRuleId(IE_Base): + name = "IE Failed Rule ID" + ie_type = 114 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 3), + BitEnumField("type", "PDR", 5, RuleIDType), + ConditionalField(ShortField("pdr_id", 0), + lambda x: x.type == 0), + ConditionalField(IntField("far_id", 0), + lambda x: x.type == 1 or x.type > 4), + ConditionalField(IntField("qer_id", 0), lambda x: x.type == 2), + ConditionalField(IntField("urr_id", 0), lambda x: x.type == 3), + ConditionalField(ByteField("bar_id", 0), + lambda x: x.type == 4), + ExtraDataField("extra_data"), + ] + + +class IE_TimeQuotaMechanism(IE_Base): + name = "IE Time Quota Mechanism" + ie_type = 115 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 6), + BitEnumField("base_time_interval_type", "CTP", 2, BaseTimeInterval), + IntField("interval", 0), + ExtraDataField("extra_data"), + ] + + +class IE_UserPlaneIPResourceInformation(IE_Base): + name = "IE User Plane IP Resource Information" + ie_type = 116 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare1", 0, 1), + BitField("ASSOSI", 0, 1), + BitField("ASSONI", 0, 1), + BitField("TEIDRI", 0, 3), + BitField("V6", 0, 1), + BitField("V4", 0, 1), + ConditionalField(XByteField("teid_range", 0), lambda x: x.TEIDRI != 0), + ConditionalField(IPField("ipv4", 0), lambda x: x.V4 == 1), + ConditionalField(IP6Field("ipv6", 0), + lambda x: x.V6 == 1), + ConditionalField( + APNStrLenField("network_instance", "", + length_from=lambda x: + x.length - 1 - (1 if x.TEIDRI != 0 else 0) - + (x.V4 * 4) - (x.V6 * 16) - x.ASSOSI), + lambda x: x.ASSONI == 1), + ConditionalField( + XBitField("spare2", None, 4), + lambda x: x.ASSOSI == 1), + ConditionalField( + BitEnumField("interface", "Access", 4, SourceInterface), + lambda x: x.ASSOSI == 1), + ExtraDataField("extra_data"), + ] + + +class IE_UserPlaneInactivityTimer(IE_Base): + name = "IE User Plane Inactivity Timer" + ie_type = 117 + fields_desc = IE_Base.fields_desc + [ + IntField("timer", 0), + ExtraDataField("extra_data"), + ] + + +class IE_Multiplier(IE_Base): + name = "IE Multiplier" + ie_type = 119 + fields_desc = IE_Base.fields_desc + [ + SignedLongField("digits", 0), + SignedIntField("exponent", 0), + ] + + +class IE_AggregatedURR_Id(IE_Base): + name = "IE Aggregated URR ID" + ie_type = 120 + fields_desc = IE_Base.fields_desc + [ + IntField("id", 0), + ] + + +class IE_SubsequentVolumeQuota(IE_Base): + name = "IE Subsequent Volume Quota" + ie_type = 121 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("DLVOL", 0, 1), + BitField("ULVOL", 0, 1), + BitField("TOVOL", 0, 1), + ConditionalField(XLongField("total", 0), lambda x: x.TOVOL == 1), + ConditionalField(XLongField("uplink", 0), lambda x: x.ULVOL == 1), + ConditionalField(XLongField("downlink", 0), lambda x: x.DLVOL == 1), + ExtraDataField("extra_data"), + ] + + +class IE_SubsequentTimeQuota(IE_Base): + name = "IE Subsequent Time Quota" + ie_type = 122 + fields_desc = IE_Base.fields_desc + [ + IntField("quota", 0), + ExtraDataField("extra_data"), + ] + + +class IE_RQI(IE_Base): + name = "IE RQI" + ie_type = 123 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", None, 7), + BitField("RQI", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_QFI(IE_Base): + name = "IE QFI" + ie_type = 124 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", None, 2), + BitField("QFI", 0, 6), + ExtraDataField("extra_data"), + ] + + +class IE_QueryURRReference(IE_Base): + name = "IE Query URR Reference" + ie_type = 125 + fields_desc = IE_Base.fields_desc + [ + IntField("reference", 0), + ExtraDataField("extra_data"), + ] + + +class IE_AdditionalUsageReportsInformation(IE_Base): + name = "IE Additional Usage Reports Information" + ie_type = 126 + fields_desc = IE_Base.fields_desc + [ + BitField("AURI", 0, 1), + BitField("reports", 0, 15), + ExtraDataField("extra_data"), + ] + + +class IE_TrafficEndpointId(IE_Base): + name = "IE Traffic Endpoint ID" + ie_type = 131 + fields_desc = IE_Base.fields_desc + [ + ByteField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_MACAddress(IE_Base): + name = "IE MAC Address" + ie_type = 133 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitField("UDES", 0, 1), + BitField("USOU", 0, 1), + BitField("DEST", 0, 1), + BitField("SOUR", 0, 1), + ConditionalField(MACField("source_mac", 0), + lambda x: x.SOUR == 1), + ConditionalField(MACField("destination_mac", 0), + lambda x: x.DEST == 1), + ConditionalField(MACField("upper_source_mac", 0), + lambda x: x.USOU == 1), + ConditionalField(MACField("upper_destination_mac", 0), + lambda x: x.UDES == 1), + ExtraDataField("extra_data"), + ] + + +class IE_C_TAG(IE_Base): + name = "IE C-TAG" + ie_type = 134 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare_1", 0, 5), + BitField("VID", 0, 1), + BitField("DEI", 0, 1), + BitField("PCP", 0, 1), + # TODO: fix cvid_value + ConditionalField( + BitField("cvid_value_hi", 0, 4), lambda x: x.VID == 1), + ConditionalField(BitField("spare_2", 0, 4), lambda x: x.VID == 0), + ConditionalField(BitField("dei_flag", 0, 1), lambda x: x.DEI == 1), + ConditionalField(BitField("spare_3", 0, 1), lambda x: x.DEI == 0), + ConditionalField(BitField("pcp_value", 0, 3), lambda x: x.PCP == 1), + ConditionalField(BitField("spare_4", 0, 3), lambda x: x.PCP == 0), + ConditionalField(ByteField("cvid_value_low", 0), + lambda x: x.VID == 1), + ConditionalField(ByteField("spare_5", 0), lambda x: x.VID == 0), + ExtraDataField("extra_data"), + ] + + +class IE_S_TAG(IE_Base): + name = "IE S-TAG" + ie_type = 135 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare_1", 0, 5), + BitField("VID", 0, 1), + BitField("DEI", 0, 1), + BitField("PCP", 0, 1), + # TODO: fix svid_value + ConditionalField(BitField("svid_value_hi", 0, 4), + lambda x: x.VID == 1), + ConditionalField(BitField("spare_2", 0, 4), lambda x: x.VID == 0), + ConditionalField(BitField("dei_flag", 0, 1), lambda x: x.DEI == 1), + ConditionalField(BitField("spare_3", 0, 1), lambda x: x.DEI == 0), + ConditionalField(BitField("pcp_value", 0, 3), lambda x: x.PCP == 1), + ConditionalField(BitField("spare_4", 0, 3), lambda x: x.PCP == 0), + ConditionalField(ByteField("svid_value_low", 0), + lambda x: x.VID == 1), + ConditionalField(ByteField("spare_5", 0), lambda x: x.VID == 0), + ExtraDataField("extra_data"), + ] + + +class IE_Ethertype(IE_Base): + name = "IE Ethertype" + ie_type = 136 + fields_desc = IE_Base.fields_desc + [ + ShortField("type", 0), + ExtraDataField("extra_data"), + ] + + +class IE_Proxying(IE_Base): + name = "IE Proxying" + ie_type = 137 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 6), + BitField("INS", 0, 1), + BitField("ARP", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_EthernetFilterId(IE_Base): + name = "IE Ethernet Filter ID" + ie_type = 138 + fields_desc = IE_Base.fields_desc + [ + IntField("id", 0), + ExtraDataField("extra_data"), + ] + + +class IE_EthernetFilterProperties(IE_Base): + name = "IE Ethernet Filter Properties" + ie_type = 139 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 7), + BitField("BIDE", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_SuggestedBufferingPacketsCount(IE_Base): + name = "IE Suggested Buffering Packets Count" + ie_type = 140 + fields_desc = IE_Base.fields_desc + [ + ByteField("count", 0), + ExtraDataField("extra_data"), + ] + + +class IE_UserId(IE_Base): + name = "IE User ID" + ie_type = 141 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 4), + BitField("NAIF", 0, 1), + BitField("MSISDNF", 0, 1), + BitField("IMEIF", 0, 1), + BitField("IMSIF", 0, 1), + ConditionalField( + FieldLenField("imsi_length", None, length_of="imsi", fmt="B"), + lambda x: x.IMSIF == 1), + ConditionalField( + StrLenField("imsi", "", length_from=lambda x: x.imsi_length), + lambda x: x.IMSIF == 1), + ConditionalField( + FieldLenField("imei_length", None, length_of="imei", fmt="B"), + lambda x: x.IMEIF == 1), + ConditionalField( + StrLenField("imei", "", length_from=lambda x: x.imei_length), + lambda x: x.IMEIF == 1), + ConditionalField( + FieldLenField("msisdn_length", None, length_of="msisdn", fmt="B"), + lambda x: x.MSISDNF == 1), + ConditionalField( + StrLenField("msisdn", "", length_from=lambda x: x.msisdn_length), + lambda x: x.MSISDNF == 1), + ConditionalField( + FieldLenField("nai_length", None, length_of="nai", fmt="B"), + lambda x: x.NAIF == 1), + ConditionalField( + StrLenField("nai", "", length_from=lambda x: x.nai_length), + lambda x: x.NAIF == 1), + ExtraDataField("extra_data"), + ] + + +class IE_EthernetPDUSessionInformation(IE_Base): + name = "IE Ethernet PDU Session Information" + ie_type = 142 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 7), + BitField("ETHI", 0, 1), + ExtraDataField("extra_data"), + ] + + +class IE_MACAddressesDetected(IE_Base): + name = "IE MAC Addresses Detected" + ie_type = 144 + fields_desc = IE_Base.fields_desc + [ + FieldLenField("num_macs", None, count_of="macs", fmt="B"), + FieldListField("macs", None, MACField("mac", 0), + count_from=lambda x: x.num_macs), + ExtraDataField("extra_data"), + ] + + +class IE_MACAddressesRemoved(IE_Base): + name = "IE MAC Addresses Removed" + ie_type = 145 + fields_desc = IE_Base.fields_desc + [ + FieldLenField("num_macs", None, count_of="macs", fmt="B"), + FieldListField("macs", None, MACField("mac", 0), + count_from=lambda x: x.num_macs), + ExtraDataField("extra_data"), + ] + + +class IE_EthernetInactivityTimer(IE_Base): + name = "IE Ethernet Inactivity Timer" + ie_type = 146 + fields_desc = IE_Base.fields_desc + [ + IntField("timer", 0), + ExtraDataField("extra_data"), + ] + + +class IE_EventQuota(IE_Base): + name = "IE Event Quota" + ie_type = 148 + fields_desc = IE_Base.fields_desc + [ + IntField("event_quota", 0), + ExtraDataField("extra_data"), + ] + + +class IE_EventThreshold(IE_Base): + name = "IE Event Threshold" + ie_type = 149 + fields_desc = IE_Base.fields_desc + [ + IntField("event_threshold", 0), + ExtraDataField("extra_data"), + ] + + +class IE_SubsequentEventQuota(IE_Base): + name = "IE Subsequent Event Quota" + ie_type = 150 + fields_desc = IE_Base.fields_desc + [ + IntField("subsequent_event_quota", 0), + ExtraDataField("extra_data"), + ] + + +class IE_SubsequentEventThreshold(IE_Base): + name = "IE Subsequent Event Threshold" + ie_type = 151 + fields_desc = IE_Base.fields_desc + [ + IntField("subsequent_event_threshold", 0), + ExtraDataField("extra_data"), + ] + + +class IE_TraceInformation(IE_Base): + # TODO: more detailed decoding + # TODO: fix IP address handling + name = "IE Trace Information" + ie_type = 152 + fields_desc = IE_Base.fields_desc + [ + BitField("mcc_digit_2", 0, 4), + BitField("mcc_digit_1", 0, 4), + BitField("mnc_digit_3", 0, 4), + BitField("mcc_digit_3", 0, 4), + BitField("mnc_digit_2", 0, 4), + BitField("mnc_digit_1", 0, 4), + ThreeBytesField("trace_id", 0), # FIXME + FieldLenField("triggering_events_length", None, + length_of="triggering_events", fmt="B"), + StrLenField("triggering_events", "", + length_from=lambda x: x.triggering_events_length), + ByteField("session_trace_depth", 0), + FieldLenField("list_of_interfaces_length", None, + length_of="list_of_interfaces", fmt="B"), + StrLenField("list_of_interfaces", "", + length_from=lambda x: x.list_of_interfaces_length), + FieldLenField("ip_address_length", None, + length_of="ip_address", fmt="B"), + StrLenField("ip_address", "", + length_from=lambda x: x.ip_address_length), + ExtraDataField("extra_data"), + ] + + +class IE_FramedRoute(IE_Base): + name = "IE Framed-Route" + ie_type = 153 + fields_desc = IE_Base.fields_desc + [ + StrLenField("framed_route", "", length_from=lambda x: x.length) + ] + + +class IE_FramedRouting(IE_Base): + name = "IE Framed-Routing" + ie_type = 154 + fields_desc = IE_Base.fields_desc + [ + StrLenField("framed_routing", "", length_from=lambda x: x.length) + ] + + +class IE_FramedIPv6Route(IE_Base): + name = "IE Framed-IPv6-Route" + ie_type = 155 + fields_desc = IE_Base.fields_desc + [ + StrLenField("framed_ipv6_route", "", length_from=lambda x: x.length) + ] + + +class IE_EventTimeStamp(IE_Base): + name = "IE Event Time Stamp" + ie_type = 156 + fields_desc = IE_Base.fields_desc + [ + IntField("timestamp", 0), + ExtraDataField("extra_data"), + ] + + +class IE_AveragingWindow(IE_Base): + name = "IE Averaging Window" + ie_type = 157 + fields_desc = IE_Base.fields_desc + [ + IntField("averaging_window", 0), + ExtraDataField("extra_data"), + ] + + +class IE_PagingPolicyIndicator(IE_Base): + name = "IE Paging Policy Indicator" + ie_type = 158 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare", 0, 5), + BitField("ppi", 0, 3), + ExtraDataField("extra_data"), + ] + + +class IE_APN_DNN(IE_Base): + name = "IE APN/DNN" + ie_type = 159 + fields_desc = IE_Base.fields_desc + [ + APNStrLenField("apn_dnn", "", length_from=lambda x: x.length) + ] + + +class IE_3GPP_InterfaceType(IE_Base): + name = "IE 3GPP Interface Type" + ie_type = 160 + fields_desc = IE_Base.fields_desc + [ + XBitField("spare_1", 0, 2), + BitEnumField("interface_type", "S1-U", 6, InterfaceType), + ExtraDataField("extra_data"), + ] + + +class IE_EnterpriseSpecific(IE_Base): + name = "Enterpise Specific" + ie_type = None + fields_desc = IE_Base.fields_desc + [ + ShortEnumField("enterprise_id", None, IANA_ENTERPRISE_NUMBERS), + StrLenField("data", "", length_from=lambda x: x.length - 2), + ] + + +class IE_NotImplemented(IE_Base): + name = "IE not implemented" + ie_type = 0 + fields_desc = IE_Base.fields_desc + [ + StrLenField("data", "", length_from=lambda x: x.length) + ] + + +ietypecls = { + 1: IE_CreatePDR, + 2: IE_PDI, + 3: IE_CreateFAR, + 4: IE_ForwardingParameters, + 5: IE_DuplicatingParameters, + 6: IE_CreateURR, + 7: IE_CreateQER, + 8: IE_CreatedPDR, + 9: IE_UpdatePDR, + 10: IE_UpdateFAR, + 11: IE_UpdateForwardingParameters, + 12: IE_UpdateBAR_SRR, + 13: IE_UpdateURR, + 14: IE_UpdateQER, + 15: IE_RemovePDR, + 16: IE_RemoveFAR, + 17: IE_RemoveURR, + 18: IE_RemoveQER, + 19: IE_Cause, + 20: IE_SourceInterface, + 21: IE_FTEID, + 22: IE_NetworkInstance, + 23: IE_SDF_Filter, + 24: IE_ApplicationId, + 25: IE_GateStatus, + 26: IE_MBR, + 27: IE_GBR, + 28: IE_QERCorrelationId, + 29: IE_Precedence, + 30: IE_TransportLevelMarking, + 31: IE_VolumeThreshold, + 32: IE_TimeThreshold, + 33: IE_MonitoringTime, + 34: IE_SubsequentVolumeThreshold, + 35: IE_SubsequentTimeThreshold, + 36: IE_InactivityDetectionTime, + 37: IE_ReportingTriggers, + 38: IE_RedirectInformation, + 39: IE_ReportType, + 40: IE_OffendingIE, + 41: IE_ForwardingPolicy, + 42: IE_DestinationInterface, + 43: IE_UPFunctionFeatures, + 44: IE_ApplyAction, + 45: IE_DownlinkDataServiceInformation, + 46: IE_DownlinkDataNotificationDelay, + 47: IE_DLBufferingDuration, + 48: IE_DLBufferingSuggestedPacketCount, + 49: IE_PFCPSMReqFlags, + 50: IE_PFCPSRRspFlags, + 51: IE_LoadControlInformation, + 52: IE_SequenceNumber, + 53: IE_Metric, + 54: IE_OverloadControlInformation, + 55: IE_Timer, + 56: IE_PDR_Id, + 57: IE_FSEID, + 58: IE_ApplicationID_PFDs, + 59: IE_PFDContext, + 60: IE_NodeId, + 61: IE_PFDContents, + 62: IE_MeasurementMethod, + 63: IE_UsageReportTrigger, + 64: IE_MeasurementPeriod, + 65: IE_FqCSID, + 66: IE_VolumeMeasurement, + 67: IE_DurationMeasurement, + 68: IE_ApplicationDetectionInformation, + 69: IE_TimeOfFirstPacket, + 70: IE_TimeOfLastPacket, + 71: IE_QuotaHoldingTime, + 72: IE_DroppedDLTrafficThreshold, + 73: IE_VolumeQuota, + 74: IE_TimeQuota, + 75: IE_StartTime, + 76: IE_EndTime, + 77: IE_QueryURR, + 78: IE_UsageReport_SMR, + 79: IE_UsageReport_SDR, + 80: IE_UsageReport_SRR, + 81: IE_URR_Id, + 82: IE_LinkedURR_Id, + 83: IE_DownlinkDataReport, + 84: IE_OuterHeaderCreation, + 85: IE_Create_BAR, + 86: IE_Update_BAR_SMR, + 87: IE_Remove_BAR, + 88: IE_BAR_Id, + 89: IE_CPFunctionFeatures, + 90: IE_UsageInformation, + 91: IE_ApplicationInstanceId, + 92: IE_FlowInformation, + 93: IE_UE_IP_Address, + 94: IE_PacketRate, + 95: IE_OuterHeaderRemoval, + 96: IE_RecoveryTimeStamp, + 97: IE_DLFlowLevelMarking, + 98: IE_HeaderEnrichment, + 99: IE_ErrorIndicationReport, + 100: IE_MeasurementInformation, + 101: IE_NodeReportType, + 102: IE_UserPlanePathFailureReport, + 103: IE_RemoteGTP_U_Peer, + 104: IE_UR_SEQN, + 105: IE_UpdateDuplicatingParameters, + 106: IE_ActivatePredefinedRules, + 107: IE_DeactivatePredefinedRules, + 108: IE_FAR_Id, + 109: IE_QER_Id, + 110: IE_OCIFlags, + 111: IE_PFCPAssociationReleaseRequest, + 112: IE_GracefulReleasePeriod, + 113: IE_PDNType, + 114: IE_FailedRuleId, + 115: IE_TimeQuotaMechanism, + 116: IE_UserPlaneIPResourceInformation, + 117: IE_UserPlaneInactivityTimer, + 118: IE_AggregatedURRs, + 119: IE_Multiplier, + 120: IE_AggregatedURR_Id, + 121: IE_SubsequentVolumeQuota, + 122: IE_SubsequentTimeQuota, + 123: IE_RQI, + 124: IE_QFI, + 125: IE_QueryURRReference, + 126: IE_AdditionalUsageReportsInformation, + 127: IE_CreateTrafficEndpoint, + 128: IE_CreatedTrafficEndpoint, + 129: IE_UpdateTrafficEndpoint, + 130: IE_RemoveTrafficEndpoint, + 131: IE_TrafficEndpointId, + 132: IE_EthernetPacketFilter, + 133: IE_MACAddress, + 134: IE_C_TAG, + 135: IE_S_TAG, + 136: IE_Ethertype, + 137: IE_Proxying, + 138: IE_EthernetFilterId, + 139: IE_EthernetFilterProperties, + 140: IE_SuggestedBufferingPacketsCount, + 141: IE_UserId, + 142: IE_EthernetPDUSessionInformation, + 143: IE_EthernetTrafficInformation, + 144: IE_MACAddressesDetected, + 145: IE_MACAddressesRemoved, + 146: IE_EthernetInactivityTimer, + 147: IE_AdditionalMonitoringTime, + 148: IE_EventQuota, + 149: IE_EventThreshold, + 150: IE_SubsequentEventQuota, + 151: IE_SubsequentEventThreshold, + 152: IE_TraceInformation, + 153: IE_FramedRoute, + 154: IE_FramedRouting, + 155: IE_FramedIPv6Route, + 156: IE_EventTimeStamp, + 157: IE_AveragingWindow, + 158: IE_PagingPolicyIndicator, + 159: IE_APN_DNN, + 160: IE_3GPP_InterfaceType, +} + + +# +# PFCP Messages +# 3GPP TS 29.244 V15.6.0 (2019-07) +# + +# class PFCPMessage(Packet): +# fields_desc = [PacketListField("IE_list", None, IE_Dispatcher)] + + +class PFCPHeartbeatRequest(Packet): + name = "PFCP Heartbeat Request" + fields_desc = [ + PacketListField("IE_list", [IE_RecoveryTimeStamp()], IE_Dispatcher) + ] + + +class PFCPHeartbeatResponse(Packet): + name = "PFCP Heartbeat Response" + fields_desc = [ + PacketListField("IE_list", [IE_RecoveryTimeStamp()], IE_Dispatcher) + ] + + def answers(self, other): + return isinstance(other, PFCPHeartbeatRequest) + + +class PFCPPFDManagementRequest(Packet): + name = "PFCP PFD Management Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPPFDManagementResponse(Packet): + name = "PFCP PFD Management Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPPFDManagementRequest) + + +class PFCPAssociationSetupRequest(Packet): + name = "PFCP Association Setup Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPAssociationSetupResponse(Packet): + name = "PFCP Association Setup Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPAssociationSetupRequest) + + +class PFCPAssociationUpdateRequest(Packet): + name = "PFCP Association Update Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPAssociationUpdateResponse(Packet): + name = "PFCP Association Update Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPAssociationUpdateRequest) + + +class PFCPAssociationReleaseRequest(Packet): + name = "PFCP Association Release Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPAssociationReleaseResponse(Packet): + name = "PFCP Association Release Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPAssociationReleaseRequest) + + +class PFCPVersionNotSupportedResponse(Packet): + name = "PFCP Version Not Supported Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + # TODO: answers() + + +class PFCPNodeReportRequest(Packet): + name = "PFCP Node Report Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPNodeReportResponse(Packet): + name = "PFCP Node Report Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPNodeReportRequest) + + +class PFCPSessionSetDeletionRequest(Packet): + name = "PFCP Session Set Deletion Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPSessionSetDeletionResponse(Packet): + name = "PFCP Session Set Deletion Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPSessionSetDeletionRequest) + + +class PFCPSessionEstablishmentRequest(Packet): + name = "PFCP Session Establishment Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPSessionEstablishmentResponse(Packet): + name = "PFCP Session Establishment Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPSessionEstablishmentRequest) + + +class PFCPSessionModificationRequest(Packet): + name = "PFCP Session Modification Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPSessionModificationResponse(Packet): + name = "PFCP Session Modification Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPSessionModificationRequest) + + +class PFCPSessionDeletionRequest(Packet): + name = "PFCP Session Deletion Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPSessionDeletionResponse(Packet): + name = "PFCP Session Deletion Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPSessionDeletionRequest) + + +class PFCPSessionReportRequest(Packet): + name = "PFCP Session Report Request" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + +class PFCPSessionReportResponse(Packet): + name = "PFCP Session Report Response" + fields_desc = [PacketListField("IE_list", [], IE_Dispatcher)] + + def answers(self, other): + return isinstance(other, PFCPSessionReportRequest) + + +bind_bottom_up(UDP, PFCP, dport=8805) +bind_bottom_up(UDP, PFCP, sport=8805) +bind_layers(UDP, PFCP, dport=8805, sport=8805) +bind_layers(PFCP, PFCPHeartbeatRequest, message_type=1) +bind_layers(PFCP, PFCPHeartbeatResponse, message_type=2) +bind_layers(PFCP, PFCPPFDManagementRequest, message_type=3) +bind_layers(PFCP, PFCPPFDManagementResponse, message_type=4) +bind_layers(PFCP, PFCPAssociationSetupRequest, message_type=5) +bind_layers(PFCP, PFCPAssociationSetupResponse, message_type=6) +bind_layers(PFCP, PFCPAssociationUpdateRequest, message_type=7) +bind_layers(PFCP, PFCPAssociationUpdateResponse, message_type=8) +bind_layers(PFCP, PFCPAssociationReleaseRequest, message_type=9) +bind_layers(PFCP, PFCPAssociationReleaseResponse, message_type=10) +bind_layers(PFCP, PFCPVersionNotSupportedResponse, message_type=11) +bind_layers(PFCP, PFCPNodeReportRequest, message_type=12) +bind_layers(PFCP, PFCPNodeReportResponse, message_type=13) +bind_layers(PFCP, PFCPSessionSetDeletionRequest, message_type=14) +bind_layers(PFCP, PFCPSessionSetDeletionResponse, message_type=15) +bind_layers(PFCP, PFCPSessionEstablishmentRequest, message_type=50) +bind_layers(PFCP, PFCPSessionEstablishmentResponse, message_type=51) +bind_layers(PFCP, PFCPSessionModificationRequest, message_type=52) +bind_layers(PFCP, PFCPSessionModificationResponse, message_type=53) +bind_layers(PFCP, PFCPSessionDeletionRequest, message_type=54) +bind_layers(PFCP, PFCPSessionDeletionResponse, message_type=55) +bind_layers(PFCP, PFCPSessionReportRequest, message_type=56) +bind_layers(PFCP, PFCPSessionReportResponse, message_type=57) + +# FIXME: the following fails with pfcplib-generated pcaps: +# bind_layers(PFCP, PFCPSessionEstablishmentRequest, message_type=50, S=1) +# bind_layers(PFCP, PFCPSessionEstablishmentResponse, message_type=51, S=1) +# bind_layers(PFCP, PFCPSessionModificationRequest, message_type=52, S=1) +# bind_layers(PFCP, PFCPSessionModificationResponse, message_type=53, S=1) +# bind_layers(PFCP, PFCPSessionDeletionRequest, message_type=54, S=1) +# bind_layers(PFCP, PFCPSessionDeletionResponse, message_type=55, S=1) +# bind_layers(PFCP, PFCPSessionReportRequest, message_type=56, S=1) +# bind_layers(PFCP, PFCPSessionReportResponse, message_type=57, S=1) + +# TODO: limit possible child IEs based on IE type + +IE_UE_IP_Address(SD=0, V4=0, V6=0, spare=0) diff --git a/scapy/contrib/pim.py b/scapy/contrib/pim.py new file mode 100644 index 00000000000..1b568eaaad1 --- /dev/null +++ b/scapy/contrib/pim.py @@ -0,0 +1,292 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = Protocol Independent Multicast (PIM) +# scapy.contrib.status = loads +""" +References: + - https://tools.ietf.org/html/rfc4601 + - https://www.iana.org/assignments/pim-parameters/pim-parameters.xhtml +""" +import struct +from scapy.packet import Packet, bind_layers +from scapy.fields import BitFieldLenField, BitField, BitEnumField, ByteField, \ + ShortField, XShortField, IPField, IP6Field, PacketListField, \ + IntField, FieldLenField, BoundStrLenField, MultipleTypeField +from scapy.layers.inet import IP +from scapy.layers.inet6 import IPv6, in6_chksum, _IPv6ExtHdr +from scapy.utils import checksum +from scapy.compat import orb +from scapy.config import conf +from scapy.volatile import RandInt + + +PIM_TYPE = { + 0: "Hello", + 1: "Register", + 2: "Register-Stop", + 3: "Join/Prune", + 4: "Bootstrap", + 5: "Assert", + 6: "Graft", + 7: "Graft-Ack", + 8: "Candidate-RP-Advertisement" +} + + +class PIMv2Hdr(Packet): + name = "Protocol Independent Multicast Version 2 Header" + fields_desc = [BitField("version", 2, 4), + BitEnumField("type", 0, 4, PIM_TYPE), + ByteField("reserved", 0), + XShortField("chksum", None)] + + def post_build(self, p, pay): + """ + Called implicitly before a packet is sent to compute and + place PIM checksum. + + Parameters: + self The instantiation of an PIMv2Hdr class + p The PIMv2Hdr message in hex in network byte order + pay Additional payload for the PIMv2Hdr message + """ + p += pay + if self.chksum is None: + if isinstance(self.underlayer, IP): + ck = checksum(p) + # ck = in4_chksum(103, self.underlayer, p) + # According to RFC768 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501 + if ck == 0: + ck = 0xFFFF + p = p[:2] + struct.pack("!H", ck) + p[4:] + + elif isinstance(self.underlayer, IPv6) or isinstance(self.underlayer, _IPv6ExtHdr): # noqa: E501 + ck = in6_chksum(103, self.underlayer, p) # noqa: E501 + # According to RFC2460 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501 + if ck == 0: + ck = 0xFFFF + p = p[:2] + struct.pack("!H", ck) + p[4:] + + return p + + +def _guess_pim_tlv_class(h_classes, default_key, pkt, **kargs): + cls = conf.raw_layer + if len(pkt) >= 2: + tlvtype = orb(pkt[1]) + cls = h_classes.get(tlvtype, default_key) + return cls(pkt, **kargs) + + +class _PIMGenericTlvBase(Packet): + fields_desc = [ByteField("type", 0), + FieldLenField("length", None, length_of="value", fmt="B"), + BoundStrLenField("value", "", + length_from=lambda pkt: pkt.length)] + + def guess_payload_class(self, p): + return conf.padding_layer + + def extract_padding(self, s): + return "", s + + +################################## +# PIMv2 Hello +################################## +class _PIMv2GenericHello(_PIMGenericTlvBase): + name = "PIMv2 Generic Hello" + + +def _guess_pimv2_hello_class(p, **kargs): + return _guess_pim_tlv_class(PIMv2_HELLO_CLASSES, None, p, **kargs) + + +class _PIMv2HelloListField(PacketListField): + def __init__(self): + PacketListField.__init__(self, "option", [], _guess_pimv2_hello_class) + + +class PIMv2Hello(Packet): + name = "PIMv2 Hello Options" + fields_desc = [ + _PIMv2HelloListField() + ] + + +class PIMv2HelloHoldtime(_PIMv2GenericHello): + name = "PIMv2 Hello Options : Holdtime" + fields_desc = [ + ShortField("type", 1), + FieldLenField("length", None, length_of="holdtime", fmt="!H"), + ShortField("holdtime", 105) + ] + + +class PIMv2HelloLANPruneDelayValue(_PIMv2GenericHello): + name = "PIMv2 Hello Options : LAN Prune Delay Value" + fields_desc = [ + BitField("t", 0, 1), + BitField("propagation_delay", 500, 15), + ShortField("override_interval", 2500), + ] + + +class PIMv2HelloLANPruneDelay(_PIMv2GenericHello): + name = "PIMv2 Hello Options : LAN Prune Delay" + fields_desc = [ + ShortField("type", 2), + FieldLenField("length", None, length_of="value", fmt="!H"), + PacketListField("value", PIMv2HelloLANPruneDelayValue(), + PIMv2HelloLANPruneDelayValue, + length_from=lambda pkt: pkt.length) + ] + + +class PIMv2HelloDRPriority(_PIMv2GenericHello): + name = "PIMv2 Hello Options : DR Priority" + fields_desc = [ + ShortField("type", 19), + FieldLenField("length", None, length_of="dr_priority", fmt="!H"), + IntField("dr_priority", 1) + ] + + +class PIMv2HelloGenerationID(_PIMv2GenericHello): + name = "PIMv2 Hello Options : Generation ID" + fields_desc = [ + ShortField("type", 20), + FieldLenField( + "length", None, length_of="generation_id", fmt="!H" + ), + IntField("generation_id", RandInt()) + ] + + +class PIMv2HelloStateRefreshValue(_PIMv2GenericHello): + name = "PIMv2 Hello Options : State-Refresh Value" + fields_desc = [ByteField("version", 1), + ByteField("interval", 0), + ShortField("reserved", 0)] + + +class PIMv2HelloStateRefresh(_PIMv2GenericHello): + name = "PIMv2 Hello Options : State-Refresh" + fields_desc = [ + ShortField("type", 21), + FieldLenField( + "length", None, length_of="value", fmt="!H" + ), + PacketListField("value", PIMv2HelloStateRefreshValue(), + PIMv2HelloStateRefreshValue) + ] + + +class PIMv2HelloAddrListValue(_PIMv2GenericHello): + name = "PIMv2 Hello Options : Address List Value" + fields_desc = [ + ByteField("addr_family", 1), + ByteField("encoding_type", 0), + IP6Field("prefix", "::"), + ] + + +class PIMv2HelloAddrList(_PIMv2GenericHello): + name = "PIMv2 Hello Options : Address List" + fields_desc = [ + ShortField("type", 24), + FieldLenField( + "length", None, length_of="value" , fmt="!H" + ), + PacketListField("value", PIMv2HelloAddrListValue(), + PIMv2HelloAddrListValue) + ] + + +PIMv2_HELLO_CLASSES = { + 1: PIMv2HelloHoldtime, + 2: PIMv2HelloLANPruneDelay, + 19: PIMv2HelloDRPriority, + 20: PIMv2HelloGenerationID, + 21: PIMv2HelloStateRefresh, + 24: PIMv2HelloAddrList, + None: _PIMv2GenericHello, +} + + +################################## +# PIMv2 Join/Prune +################################## +class PIMv2JoinPruneAddrsBase(_PIMGenericTlvBase): + fields_desc = [ + ByteField("addr_family", 1), + ByteField("encoding_type", 0), + BitField("rsrvd", 0, 5), + BitField("sparse", 0, 1), + BitField("wildcard", 0, 1), + BitField("rpt", 1, 1), + ByteField("mask_len", 32), + MultipleTypeField( + [(IP6Field("src_ip", "::"), + lambda pkt: pkt.addr_family == 2)], + IPField("src_ip", "0.0.0.0") + ), + + ] + + +class PIMv2JoinAddrs(PIMv2JoinPruneAddrsBase): + name = "PIMv2 Join: Source Address" + + +class PIMv2PruneAddrs(PIMv2JoinPruneAddrsBase): + name = "PIMv2 Prune: Source Address" + + +class PIMv2GroupAddrs(_PIMGenericTlvBase): + name = "PIMv2 Join/Prune: Multicast Group Address" + fields_desc = [ + ByteField("addr_family", 1), + ByteField("encoding_type", 0), + BitField("bidirection", 0, 1), + BitField("reserved", 0, 6), + BitField("admin_scope_zone", 0, 1), + ByteField("mask_len", 32), + MultipleTypeField( + [(IP6Field("gaddr", "::"), + lambda pkt: pkt.addr_family == 2)], + IPField("gaddr", "0.0.0.0") + ), + BitFieldLenField("num_joins", None, size=16, count_of="join_ips"), + BitFieldLenField("num_prunes", None, size=16, count_of="prune_ips"), + PacketListField("join_ips", [], PIMv2JoinAddrs, + count_from=lambda x: x.num_joins), + PacketListField("prune_ips", [], PIMv2PruneAddrs, + count_from=lambda x: x.num_prunes), + ] + + +class PIMv2JoinPrune(_PIMGenericTlvBase): + name = "PIMv2 Join/Prune Options" + fields_desc = [ + ByteField("up_addr_family", 1), + ByteField("up_encoding_type", 0), + MultipleTypeField( + [(IP6Field("up_neighbor_ip", "::"), + lambda pkt: pkt.up_addr_family == 2)], + IPField("up_neighbor_ip", "0.0.0.0") + ), + ByteField("reserved", 0), + FieldLenField("num_group", None, count_of="jp_ips", fmt="B"), + ShortField("holdtime", 210), + PacketListField("jp_ips", [], PIMv2GroupAddrs, + count_from=lambda pkt: pkt.num_group) + ] + + +bind_layers(IP, PIMv2Hdr, proto=103) +bind_layers(IPv6, PIMv2Hdr, nh=103) +bind_layers(PIMv2Hdr, PIMv2Hello, type=0) +bind_layers(PIMv2Hdr, PIMv2JoinPrune, type=3) diff --git a/scapy/contrib/pnio.py b/scapy/contrib/pnio.py index 72afcd6e4ba..0c5583cadb0 100644 --- a/scapy/contrib/pnio.py +++ b/scapy/contrib/pnio.py @@ -1,18 +1,6 @@ -# coding: utf8 +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # Copyright (C) 2016 Gauthier Sebaux # scapy.contrib.description = ProfinetIO RTC (+Profisafe) layer @@ -30,7 +18,6 @@ StrFixedLenField, ShortField, FlagsField, ByteField, XIntField, X3BytesField ) -from scapy.modules import six PNIO_FRAME_IDS = { 0x0020: "PTCP-RTSyncPDU-followup", @@ -89,7 +76,7 @@ def s2i_frameid(x): except KeyError: pass try: - return next(key for key, value in six.iteritems(PNIO_FRAME_IDS) + return next(key for key, value in PNIO_FRAME_IDS.items() if value == x) except StopIteration: pass @@ -111,6 +98,12 @@ def guess_payload_class(self, payload): if self.frameID in [0xfefe, 0xfeff, 0xfefd]: from scapy.contrib.pnio_dcp import ProfinetDCP return ProfinetDCP + elif self.frameID == 0xFE01: + from scapy.contrib.pnio_rpc import Alarm_Low + return Alarm_Low + elif self.frameID == 0xFC01: + from scapy.contrib.pnio_rpc import Alarm_High + return Alarm_High elif ( (0x0100 <= self.frameID < 0x1000) or (0x8000 <= self.frameID < 0xFC00) @@ -220,12 +213,12 @@ def get_padding_length(self): pad_len = len(self.getfieldval("padding")) # Constraints from IEC-61158-6-10/FDIS ED 3, Table 163 - assert(0 <= pad_len <= 40) + assert 0 <= pad_len <= 40 q = self while not isinstance(q, UDP) and hasattr(q, "underlayer"): q = q.underlayer if isinstance(q, UDP): - assert(0 <= pad_len <= 12) + assert 0 <= pad_len <= 12 return pad_len def next_cls_cb(self, _lst, _p, _remain): @@ -362,7 +355,7 @@ def get_max_data_length(): @staticmethod def build_PROFIsafe_class(cls, data_length): - assert(cls.get_max_data_length() >= data_length) + assert cls.get_max_data_length() >= data_length return type( "{}Len{}".format(cls.__name__, data_length), (cls,), diff --git a/scapy/contrib/pnio_dcp.py b/scapy/contrib/pnio_dcp.py index 2ea3c6cec84..d3a22c8707e 100644 --- a/scapy/contrib/pnio_dcp.py +++ b/scapy/contrib/pnio_dcp.py @@ -1,19 +1,6 @@ -# coding: utf8 - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # Copyright (C) 2019 Stefan Mehner (stefan.mehner@b-tu.de) # scapy.contrib.description = Profinet DCP layer @@ -21,10 +8,25 @@ from scapy.compat import orb from scapy.all import Packet, bind_layers, Padding -from scapy.fields import ByteEnumField, ShortField, XShortField, \ - ShortEnumField, FieldLenField, XByteField, XIntField, MultiEnumField, \ - IPField, MACField, StrLenField, PacketListField, PadField, \ - ConditionalField, LenField +from scapy.fields import ( + ByteEnumField, + ConditionalField, + FieldLenField, + FieldListField, + IPField, + LenField, + MACField, + MultiEnumField, + MultipleTypeField, + PacketListField, + PadField, + ShortEnumField, + ShortField, + StrLenField, + XByteField, + XIntField, + XShortField, +) # minimum packet is 60 bytes.. 14 bytes are Ether() MIN_PACKET_LENGTH = 44 @@ -85,7 +87,8 @@ 0x01: { 0x00: "Reserved", 0x01: "MAC Address", - 0x02: "IP Parameter" + 0x02: "IP Parameter", + 0x03: "Full IP Suite", }, # device properties 0x02: { @@ -210,6 +213,27 @@ def extract_padding(self, s): return '', s +class DCPFullIPBlock(Packet): + fields_desc = [ + ByteEnumField("option", 1, DCP_OPTIONS), + MultiEnumField("sub_option", 3, DCP_SUBOPTIONS, fmt='B', + depends_on=lambda p: p.option), + LenField("dcp_block_length", None), + ShortEnumField("block_info", 1, IP_BLOCK_INFOS), + IPField("ip", "192.168.0.2"), + IPField("netmask", "255.255.255.0"), + IPField("gateway", "192.168.0.1"), + FieldListField("dnsaddr", [], IPField("", "0.0.0.0"), + count_from=lambda x: 4), + PadField(StrLenField("padding", b"\x00", + length_from=lambda p: p.dcp_block_length % 2), 1, + padwith=b"\x00") + ] + + def extract_padding(self, s): + return '', s + + class DCPMACBlock(Packet): fields_desc = [ ByteEnumField("option", 1, DCP_OPTIONS), @@ -372,6 +396,24 @@ def extract_padding(self, s): return '', s +class DCPOEMIDBlock(Packet): + fields_desc = [ + ByteEnumField("option", 2, DCP_OPTIONS), + MultiEnumField("sub_option", 8, DCP_SUBOPTIONS, fmt='B', + depends_on=lambda p: p.option), + LenField("dcp_block_length", None), + ShortEnumField("block_info", 0, BLOCK_INFOS), + XShortField("vendor_id", 0x002a), + XShortField("device_id", 0x0313), + PadField(StrLenField("padding", b"\x00", + length_from=lambda p: p.dcp_block_length % 2), 1, + padwith=b"\x00") + ] + + def extract_padding(self, s): + return '', s + + class DCPControlBlock(Packet): fields_desc = [ ByteEnumField("option", 5, DCP_OPTIONS), @@ -391,6 +433,23 @@ def extract_padding(self, s): return '', s +class DCPDeviceInitiativeBlock(Packet): + """ + device initiative DCP block + """ + fields_desc = [ + ByteEnumField("option", 6, DCP_OPTIONS), + MultiEnumField("sub_option", 1, DCP_SUBOPTIONS, fmt='B', + depends_on=lambda p: p.option), + FieldLenField("dcp_block_length", None, length_of="device_initiative"), + ShortEnumField("block_info", 0, BLOCK_INFOS), + ShortField("device_initiative", 1), + ] + + def extract_padding(self, s): + return '', s + + def guess_dcp_block_class(packet, **kargs): """ returns the correct dcp block class needed to dissect the current tag @@ -422,7 +481,7 @@ def guess_dcp_block_class(packet, **kargs): 0x05: "DCPDeviceOptionsBlock", 0x06: "DCPAliasNameBlock", 0x07: "DCPDeviceInstanceBlock", - 0x08: "OEM Device ID" + 0x08: "DCPOEMIDBlock" }, # DHCP 0x03: @@ -452,7 +511,7 @@ def guess_dcp_block_class(packet, **kargs): 0x06: { 0x00: "Reserved (0x00)", - 0x01: "Device Initiative (0x01)" + 0x01: "DCPDeviceInitiativeBlock" }, # ALL Selector 0xff: @@ -539,13 +598,21 @@ class ProfinetDCP(Packet): BLOCK_QUALIFIERS), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0), - # Name Of Station - ConditionalField(StrLenField("name_of_station", "et200sp", - length_from=lambda x: x.dcp_block_length - 2), - lambda pkt: pkt.service_id == 4 and - pkt.service_type == 0 and pkt.option == 2 and - pkt.sub_option == 2), - + # (Common) Name Of Station + ConditionalField( + MultipleTypeField( + [ + (StrLenField("name_of_station", "et200sp", + length_from=lambda x: x.dcp_block_length - 2), + lambda pkt: pkt.service_id == 4), + ], + StrLenField("name_of_station", "et200sp", + length_from=lambda x: x.dcp_block_length), + ), + lambda pkt: pkt.service_type == 0 and pkt.option == 2 and + pkt.sub_option == 2 + ), + # DCP SET REQUEST # # MAC ConditionalField(MACField("mac", "00:00:00:00:00:00"), lambda pkt: pkt.service_id == 4 and @@ -555,23 +622,25 @@ class ProfinetDCP(Packet): ConditionalField(IPField("ip", "192.168.0.2"), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 1 and - pkt.sub_option == 2), + pkt.sub_option in [2, 3]), ConditionalField(IPField("netmask", "255.255.255.0"), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 1 and - pkt.sub_option == 2), + pkt.sub_option in [2, 3]), ConditionalField(IPField("gateway", "192.168.0.1"), lambda pkt: pkt.service_id == 4 and pkt.service_type == 0 and pkt.option == 1 and - pkt.sub_option == 2), + pkt.sub_option in [2, 3]), + + # Full IP + ConditionalField(FieldListField("dnsaddr", [], IPField("", "0.0.0.0"), + count_from=lambda x: 4), + lambda pkt: pkt.service_id == 4 and + pkt.service_type == 0 and pkt.option == 1 and + pkt.sub_option == 3), # DCP IDENTIFY REQUEST # - # Name of station - ConditionalField(StrLenField("name_of_station", "et200sp", - length_from=lambda x: x.dcp_block_length), - lambda pkt: pkt.service_id == 5 and - pkt.service_type == 0 and pkt.option == 2 and - pkt.sub_option == 2), + # Name of station (handled above) # Alias name ConditionalField(StrLenField("alias_name", "et200sp", diff --git a/scapy/contrib/pnio_rpc.py b/scapy/contrib/pnio_rpc.py index f835473298c..cd9b4de4ad4 100644 --- a/scapy/contrib/pnio_rpc.py +++ b/scapy/contrib/pnio_rpc.py @@ -1,17 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # Copyright (C) 2016 Gauthier Sebaux # scapy.contrib.description = ProfinetIO Remote Procedure Call (RPC) @@ -24,21 +13,22 @@ import struct from uuid import UUID -from scapy.packet import Packet, bind_layers +from scapy.packet import Packet, Raw, bind_layers from scapy.config import conf -from scapy.fields import BitField, ByteField, BitEnumField, ConditionalField, \ - FieldLenField, FieldListField, IntField, IntEnumField, \ +from scapy.fields import BitField, ByteField, BitEnumField, ByteEnumField, \ + ConditionalField, FieldLenField, FieldListField, IntField, IntEnumField, \ LenField, MACField, PadField, PacketField, PacketListField, \ ShortEnumField, ShortField, StrFixedLenField, StrLenField, \ UUIDField, XByteField, XIntField, XShortEnumField, XShortField -from scapy.contrib.dce_rpc import DceRpc, EndiannessField, DceRpcPayload +from scapy.layers.dcerpc import DceRpc4, DceRpc4Payload +from scapy.contrib.rtps.common_types import EField from scapy.compat import bytes_hex from scapy.volatile import RandUUID # Block Packet BLOCK_TYPES_ENUM = { - 0x0000: "AlarmNotification_High", - 0x0001: "AlarmNotification_Low", + 0x0001: "AlarmNotification_High", + 0x0002: "AlarmNotification_Low", 0x0008: "IODWriteReqHeader", 0x0009: "IODReadReqHeader", 0x0010: "DiagnosisData", @@ -276,6 +266,44 @@ 0x4: "RT_CLASS_UDP", } +MAU_TYPE = { + 0x0000: "Radio", + 0x001e: "1000-BaseT-FD" +} + +MAU_EXTENSION = { + 0x0000: "None", + 0x0100: "Polymeric-Optical-Fiber" +} + +LINKSTATE_LINK = { + 0x0000: "Reserved", + 0x0001: "Up", + 0x0002: "Down", + 0x0003: "Testing", + 0x0004: "Unknown", + 0x0005: "Dormant", + 0x0006: "NotPresent", + 0x0007: "LowerLayerDown", +} + +LINKSTATE_PORT = { + 0x0000: "Unknown", + 0x0001: "Disabled/Discarding", + 0x0002: "Blocking", + 0x0003: "Listening", + 0x0004: "Learning", + 0x0005: "Forwarding", + 0x0006: "Broken", + 0x0007: "Reserved", +} + +MEDIA_TYPE = { + 0x00: "Unknown", + 0x01: "Copper cable", + 0x02: "Fiber optic cable", + 0x03: "Radio communication" +} # List of all valid activity UUIDs for the DceRpc layer with PROFINET RPC # endpoint. @@ -474,6 +502,53 @@ class IODWriteRes(Block): block_type = 0x8008 +# IODReadRe{q,s} +class IODReadReq(Block): + """IODRead request block""" + fields_desc = [ + BlockHeader, + ShortField("seqNum", 0), + UUIDField("ARUUID", None), + XIntField("API", 0), + XShortField("slotNumber", 0), + XShortField("subslotNumber", 0), + StrFixedLenField("padding", "", length=2), + XShortEnumField("index", 0, IOD_WRITE_REQ_INDEX), + LenField("recordDataLength", None, fmt="I"), + StrFixedLenField("RWPadding", "", length=24), + ] + block_type = 0x0009 + + def payload_length(self): + return self.recordDataLength + + def get_response(self): + res = IODReadRes() + for field in ["seqNum", "ARUUID", "API", "slotNumber", + "subslotNumber", "index"]: + res.setfieldval(field, self.getfieldval(field)) + return res + + +class IODReadRes(Block): + """IODRead response block""" + fields_desc = [ + BlockHeader, + ShortField("seqNum", 0), + UUIDField("ARUUID", None), + XIntField("API", 0), + XShortField("slotNumber", 0), + XShortField("subslotNumber", 0), + StrFixedLenField("padding", "", length=2), + XShortEnumField("index", 0, IOD_WRITE_REQ_INDEX), + LenField("recordDataLength", None, fmt="I"), + XShortField("additionalValue1", 0), + XShortField("additionalValue2", 0), + StrFixedLenField("RWPadding", "", length=20), + ] + block_type = 0x8009 + + F_PARAMETERS_BLOCK_ID = [ "No_F_WD_Time2_No_F_iPar_CRC", "No_F_WD_Time2_F_iPar_CRC", "F_WD_Time2_No_F_iPar_CRC", "F_WD_Time2_F_iPar_CRC", @@ -526,10 +601,11 @@ class FParametersBlock(Packet): # IODWriteMultipleRe{q,s} class PadFieldWithLen(PadField): """PadField which handles the i2len function to include padding""" + def i2len(self, pkt, val): """get the length of the field, including the padding length""" - fld_len = self._fld.i2len(pkt, val) - return fld_len + self.padlen(fld_len) + fld_len = self.fld.i2len(pkt, val) + return fld_len + self.padlen(fld_len, pkt) class IODWriteMultipleReq(Block): @@ -566,7 +642,7 @@ def post_build(self, p, pay): fld, val = self.getfield_and_val("blocks") if fld.i2count(self, val) > 0: length = len(val[-1]) - pad = fld.field.padlen(length) + pad = fld.field.padlen(length, self) if pad > 0: p = p[:-pad] # also reduce the recordDataLength accordingly @@ -626,6 +702,76 @@ def post_build(self, p, pay): return Packet.post_build(self, p, pay) +# I&M0 +class IM0Block(Block): + """Identification and Maintenance 0""" + fields_desc = [ + BlockHeader, + ByteField("VendorIDHigh", 0x00), + ByteField("VendorIDLow", 0x00), + StrFixedLenField("OrderID", "", length=20), + StrFixedLenField("IMSerialNumber", "", length=16), + ShortField("IMHardwareRevision", 0), + StrFixedLenField("IMSWRevisionPrefix", "V", length=1), + ByteField("IMSWRevisionFunctionalEnhancement", 0), + ByteField("IMSWRevisionBugFix", 0), + ByteField("IMSWRevisionInternalChange", 0), + ShortField("IMRevisionCounter", 0), + ShortField("IMProfileID", 0), + ShortField("IMProfileSpecificType", 0), + ByteField("IMVersionMajor", 1), + ByteField("IMVersionMinor", 1), + ShortField("IMSupported", 0x0), + ] + + block_type = 0x0020 + + +# I&M1 +class IM1Block(Block): + """Identification and Maintenance 1""" + fields_desc = [ + BlockHeader, + StrFixedLenField("IMTagFunction", "", length=32), + StrFixedLenField("IMTagLocation", "", length=22), + ] + + block_type = 0x0021 + + +# I&M2 +class IM2Block(Block): + """Identification and Maintenance 2""" + fields_desc = [ + BlockHeader, + StrFixedLenField("IMDate", "", length=16), + ] + + block_type = 0x0022 + + +# I&M3 +class IM3Block(Block): + """Identification and Maintenance 3""" + fields_desc = [ + BlockHeader, + StrFixedLenField("IMDescriptor", "", length=54), + ] + + block_type = 0x0023 + + +# I&M4 +class IM4Block(Block): + """Identification and Maintenance 4""" + fields_desc = [ + BlockHeader, + StrFixedLenField("IMSignature", "", 54) + ] + + block_type = 0x0024 + + # ARBlockRe{q,s} class ARBlockReq(Block): """Application relationship block request""" @@ -776,6 +922,128 @@ class IOCRBlockRes(Block): block_type = 0x8102 +class AdjustLinkState(Block): + fields_desc = [ + BlockHeader, + StrFixedLenField("padding", "", length=2), + XShortEnumField("LinkState", 0, LINKSTATE_LINK), + ShortField("AdjustProperties", 0) + ] + + block_type = 0x021B + + +class AdjustPeerToPeerBoundary(Block): + fields_desc = [ + BlockHeader, + StrFixedLenField("padding1", "", length=2), + IntField("peerToPeerBoundary", 0), + ShortField("adjustProperties", 0), + PadField(ShortField("padding2", 0), 2), + ] + + block_type = 0x0224 + + +class AdjustDomainBoundary(Block): + fields_desc = [ + BlockHeader, + StrFixedLenField("padding1", "", length=2), + IntEnumField("DomainBoundaryIngress", 0, { + 0x00: "No Block", + 0x01: "Block", + }), + IntEnumField("DomainBoundaryEgress", 0, { + 0x00: "No Block", + 0x01: "Block", + }), + ShortField("adjustProperties", 0), + PadField(ShortField("padding2", 0), 2) + ] + + block_type = 0x0209 + + +class AdjustMulticastBoundary(Block): + fields_desc = [ + BlockHeader, + StrFixedLenField("padding1", "", length=2), + IntField("MulticastAddress", 0), + ShortField("adjustProperties", 0), + PadField(ShortField("padding2", 0), 2) + ] + + block_type = 0x0210 + + +class AdjustMauType(Block): + fields_desc = [ + BlockHeader, + PadField(ShortField("padding", 0), 2), + XShortEnumField("MAUType", 1, MAU_TYPE), + ShortField("adjustProperties", 0), + ] + + block_type = 0x020E + + +class AdjustMauTypeExtension(Block): + fields_desc = [ + BlockHeader, + PadField(ShortField("padding", 0), 2), + XShortEnumField("MAUTypeExtension", 0, MAU_EXTENSION), + ShortField("adjustProperties", 0), + ] + + block_type = 0x0229 + + +class AdjustDCPBoundary(Block): + fields_desc = [ + BlockHeader, + StrFixedLenField("padding1", "", length=2), + IntField("dcpBoundary", 0), + ShortField("adjustProperties", 0), + PadField(ShortField("padding2", 0), 2), + ] + + block_type = 0x0225 + + +PDPORT_ADJUST_BLOCK_ASSOCIATION = { + 0x0209: AdjustDomainBoundary, + 0x020e: AdjustMauType, + 0x0210: AdjustMulticastBoundary, + 0x021b: AdjustLinkState, + 0x0224: AdjustPeerToPeerBoundary, + 0x0225: AdjustDCPBoundary, + 0x0229: AdjustMauTypeExtension, +} + + +def _guess_pdportadjust_block(_pkt, *args, **kargs): + cls = Block + + btype = struct.unpack("!H", _pkt[:2])[0] + if btype in PDPORT_ADJUST_BLOCK_ASSOCIATION: + cls = PDPORT_ADJUST_BLOCK_ASSOCIATION[btype] + + return cls(_pkt, *args, **kargs) + + +class PDPortDataAdjust(Block): + fields_desc = [ + BlockHeader, + StrFixedLenField("padding", "", length=2), + XShortField("slotNumber", 0), + XShortField("subslotNumber", 0), + PacketListField("blocks", [], _guess_pdportadjust_block, + length_from=lambda p: p.block_length) + ] + + block_type = 0x0202 + + # ExpectedSubmoduleBlockReq class ExpectedSubmoduleDataDescription(Packet): """Description of the data of a submodule""" @@ -852,11 +1120,278 @@ def get_response(self): return None # no response associated (should be modulediffblock) +ALARM_CR_TYPE = { + 0x0001: "AlarmCR", +} + +ALARM_CR_TRANSPORT = { + 0x0: "RTA_CLASS_1", + 0x1: "RTA_CLASS_UDP" +} + + +class AlarmCRBlockReq(Block): + """Alarm CR block request""" + fields_desc = [ + BlockHeader, + XShortEnumField("AlarmCRType", 1, ALARM_CR_TYPE), + ShortField("LT", 0x8892), + BitField("AlarmCRProperties_Priority", 0, 1), + BitEnumField("AlarmCRProperties_Transport", 0, 1, ALARM_CR_TRANSPORT), + BitField("AlarmCRProperties_Reserved1", 0, 22), + BitField("AlarmCRProperties_Reserved2", 0, 8), + ShortField("RTATimeoutFactor", 0x0001), + ShortField("RTARetries", 0x0003), + ShortField("LocalAlarmReference", 0x0003), + ShortField("MaxAlarmDataLength", 0x00C8), + ShortField("AlarmCRTagHeaderHigh", 0xC000), + ShortField("AlarmCRTagHeaderLow", 0xA000), + ] + # default block_type value + block_type = 0x0103 + + def post_build(self, p, pay): + # Set the LT based on transport + if self.AlarmCRProperties_Transport == 0x1: + p = p[:8] + struct.pack("!H", 0x0800) + p[10:] + + return Block.post_build(self, p, pay) + + def get_response(self): + """Generate the response block of this request. + Careful: it only sets the fields which can be set from the request + """ + res = AlarmCRBlockRes() + for field in ["AlarmCRType", "LocalAlarmReference"]: + res.setfieldval(field, self.getfieldval(field)) + + res.block_type = self.block_type + 0x8000 + return res + + +class AlarmCRBlockRes(Block): + fields_desc = [ + BlockHeader, + XShortEnumField("AlarmCRType", 1, ALARM_CR_TYPE), + ShortField("LocalAlarmReference", 0), + ShortField("MaxAlarmDataLength", 0) + ] + # default block_type value + block_type = 0x8103 + + +class AlarmItem(Packet): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + PacketField("load", "", Raw), + ] + + def extract_padding(self, s): + return None, s # No extra payload + + +class MaintenanceItem(AlarmItem): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + BlockHeader, + StrFixedLenField("padding", "", length=2), + XIntField("MaintenanceStatus", 0), + ] + + +class DiagnosisItem(AlarmItem): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + XShortField("ChannelNumber", 0), + XShortField("ChannelProperties", 0), + XShortField("ChannelErrorType", 0), + ConditionalField( + cond=lambda p: p.getfieldval("UserStructureIdentifier") in [ + 0x8002, 0x8003], + fld=XShortField("ExtChannelErrorType", 0)), + ConditionalField( + cond=lambda p: p.getfieldval("UserStructureIdentifier") in [ + 0x8002, 0x8003], + fld=XIntField("ExtChannelAddValue", 0)), + ConditionalField( + cond=lambda p: p.getfieldval("UserStructureIdentifier") == 0x8003, + fld=XIntField("QualifiedChannelQualifier", 0)), + ] + + +class UploadRetrievalItem(AlarmItem): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + BlockHeader, + StrFixedLenField("padding", "", length=2), + XIntField("URRecordIndex", 0), + XIntField("URRecordLength", 0), + ] + + +class iParameterItem(AlarmItem): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + BlockHeader, + StrFixedLenField("padding", "", length=2), + XIntField("iPar_Req_Header", 0), + XIntField("Max_Segm_Size", 0), + XIntField("Transfer_Index", 0), + XIntField("Total_iPar_Size", 0), + ] + + +PE_OPERATIONAL_MODE = { + 0x00: "PE_PowerOff", + 0xF0: "PE_Operate", + 0xFE: "PE_SleepModeWOL", + 0xFF: "PE_ReadyToOperate", +} +PE_OPERATIONAL_MODE.update({i: "PE_EnergySavingMode_{}".format(i) + for i in range(0x1, 0x20)}) +PE_OPERATIONAL_MODE.update({i: "Reserved" for i in range(0x20, 0xF0)}) +PE_OPERATIONAL_MODE.update({i: "Reserved" for i in range(0xF1, 0xFE)}) + + +class PE_AlarmItem(AlarmItem): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + BlockHeader, + ByteEnumField("PE_OperationalMode", 0, PE_OPERATIONAL_MODE), + ] + + +class RS_AlarmItem(AlarmItem): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + XShortField("RS_AlarmInfo", 0), + ] + + +class PRAL_AlarmItem(AlarmItem): + fields_desc = [ + XShortField("UserStructureIdentifier", 0), + XShortField("ChannelNumber", 0), + XShortField("PRAL_ChannelProperties", 0), + XShortField("PRAL_Reason", 0), + XShortField("PRAL_ExtReason", 0), + StrLenField("PRAL_ReasonAddValue", "", + length_from=lambda x:x.len - 10), + ] + + +PNIO_RPC_ALARM_ASSOCIATION = { + "8000": DiagnosisItem, + "8002": DiagnosisItem, + "8003": DiagnosisItem, + "8100": MaintenanceItem, + "8200": UploadRetrievalItem, + "8201": iParameterItem, + "8300": RS_AlarmItem, + "8301": RS_AlarmItem, + "8302": RS_AlarmItem, + # "8303": RS_AlarmItem, + "8310": PE_AlarmItem, + "8320": PRAL_AlarmItem, +} + + +def _guess_alarm_payload(_pkt, *args, **kargs): + cls = AlarmItem + + btype = bytes_hex(_pkt[:2]).decode("utf8") + if btype in PNIO_RPC_ALARM_ASSOCIATION: + cls = PNIO_RPC_ALARM_ASSOCIATION[btype] + + return cls(_pkt, *args, **kargs) + + +class AlarmNotificationPDU(Block): + fields_desc = [ + # IEC-61158-6-10:2021, Table 513 + BlockHeader, + ShortField("AlarmType", 0), + XIntField("API", 0), + ShortField("SlotNumber", 0), + ShortField("SubslotNumber", 0), + XIntField("ModuleIdentNumber", 0), + XIntField("SubmoduleIdentNUmber", 0), + XShortField("AlarmSpecifier", 0), + PacketListField("AlarmPayload", [], _guess_alarm_payload) + ] + + +class AlarmNotification_High(AlarmNotificationPDU): + block_type = 0x0001 + + +class AlarmNotification_Low(AlarmNotificationPDU): + block_type = 0x0002 + + +PDU_TYPE_TYPE = { + 0x01: "RTA_TYPE_DATA", + 0x02: "RTA_TYPE_NACK", + 0x03: "RTA_TYPE_ACK", + 0x04: "RTA_TYPE_ERR", + 0x05: "RTA_TYPE_FREQ", + 0x06: "RTA_TYPE_FRSP", +} +PDU_TYPE_TYPE.update({i: "Reserved" for i in range(0x07, 0x10)}) + + +PDU_TYPE_VERSION = { + 0x00: "Reserved", + 0x01: "Version 1", + 0x02: "Version 2", +} +PDU_TYPE_VERSION.update({i: "Reserved" for i in range(0x03, 0x10)}) + + +class PNIORealTimeAcyclicPDUHeader(Packet): + fields_desc = [ + # IEC-61158-6-10:2021, Table 241 + ShortField("AlarmDstEndpoint", 0), + ShortField("AlarmSrcEndpoint", 0), + BitEnumField("PDUTypeType", 0, 4, PDU_TYPE_TYPE), + BitEnumField("PDUTypeVersion", 0, 4, PDU_TYPE_VERSION), + BitField("AddFlags", 0, 8), + XShortField("SendSeqNum", 0), + XShortField("AckSeqNum", 0), + XShortField("VarPartLen", 0), + ] + + def __new__(cls, name, bases, dct): + raise NotImplementedError() + + +class Alarm_Low(Packet): + fields_desc = [ + PNIORealTimeAcyclicPDUHeader, + PacketField("RTA_SDU", None, AlarmNotification_Low), + ] + + +class Alarm_High(Packet): + fields_desc = [ + PNIORealTimeAcyclicPDUHeader, + PacketField("RTA_SDU", None, AlarmNotification_High), + ] + + # PROFINET IO DCE/RPC PDU PNIO_RPC_BLOCK_ASSOCIATION = { + # I&M Records + "0020": IM0Block, + "0021": IM1Block, + "0022": IM2Block, + "0023": IM3Block, + "0024": IM4Block, + # requests "0101": ARBlockReq, "0102": IOCRBlockReq, + "0103": AlarmCRBlockReq, "0104": ExpectedSubmoduleBlockReq, "0110": IODControlReq, "0111": IODControlReq, @@ -866,10 +1401,12 @@ def get_response(self): "0116": IODControlReq, "0117": IODControlReq, "0118": IODControlReq, + "0202": PDPortDataAdjust, # responses "8101": ARBlockRes, "8102": IOCRBlockRes, + "8103": AlarmCRBlockRes, "8110": IODControlRes, "8111": IODControlRes, "8112": IODControlRes, @@ -891,12 +1428,18 @@ def _guess_block_class(_pkt, *args, **kargs): else: cls = IODWriteReq + elif _pkt[:2] == b'\x00\x09': # IODReadReq + cls = IODReadReq + elif _pkt[:2] == b'\x80\x08': # IODWriteRes if _pkt[34:36] == b'\xe0@': # IODWriteMultipleRes cls = IODWriteMultipleRes else: cls = IODWriteRes + elif _pkt[:2] == b'\x80\x09': # IODReadRes + cls = IODReadRes + # Common cases else: btype = bytes_hex(_pkt[:2]).decode("utf8") @@ -906,10 +1449,10 @@ def _guess_block_class(_pkt, *args, **kargs): return cls(_pkt, *args, **kargs) -def dce_rpc_endianess(pkt): +def dce_rpc_endianness(pkt): """determine the symbol for the endianness of a the DCE/RPC""" try: - endianness = pkt.underlayer.endianness + endianness = pkt.underlayer.endian except AttributeError: # handle the case where a PNIO class is # built without its DCE-RPC under-layer @@ -926,18 +1469,18 @@ def dce_rpc_endianess(pkt): class NDRData(Packet): """Base NDRData to centralize some fields. It can't be instantiated""" fields_desc = [ - EndiannessField( + EField( FieldLenField("args_length", None, fmt="I", length_of="blocks"), - endianess_from=dce_rpc_endianess), - EndiannessField( + endianness_from=dce_rpc_endianness), + EField( FieldLenField("max_count", None, fmt="I", length_of="blocks"), - endianess_from=dce_rpc_endianess), - EndiannessField( + endianness_from=dce_rpc_endianness), + EField( IntField("offset", 0), - endianess_from=dce_rpc_endianess), - EndiannessField( + endianness_from=dce_rpc_endianness), + EField( FieldLenField("actual_count", None, fmt="I", length_of="blocks"), - endianess_from=dce_rpc_endianess), + endianness_from=dce_rpc_endianness), PacketListField("blocks", [], _guess_block_class, length_from=lambda p: p.args_length) ] @@ -949,19 +1492,19 @@ def __new__(cls, name, bases, dct): class PNIOServiceReqPDU(Packet): """PNIO PDU for RPC Request""" fields_desc = [ - EndiannessField( + EField( FieldLenField("args_max", None, fmt="I", length_of="blocks"), - endianess_from=dce_rpc_endianess), + endianness_from=dce_rpc_endianness), NDRData, ] overload_fields = { - DceRpc: { - # random object_uuid in the appropriate range - "object_uuid": RandUUID("dea00000-6c97-11d1-8271-******"), + DceRpc4: { + # random object in the appropriate range + "object": RandUUID("dea00000-6c97-11d1-8271-******"), # interface uuid to send to a device - "interface_uuid": RPC_INTERFACE_UUID["UUID_IO_DeviceInterface"], + "if_id": RPC_INTERFACE_UUID["UUID_IO_DeviceInterface"], # Request DCE/RPC type - "type": 0, + "ptype": 0, }, } @@ -969,31 +1512,31 @@ class PNIOServiceReqPDU(Packet): def can_handle(cls, pkt, rpc): """heuristic guess_payload_class""" # type = 0 => request - if rpc.getfieldval("type") == 0 and \ - str(rpc.object_uuid).startswith("dea00000-6c97-11d1-8271-"): + if rpc.ptype == 0 and \ + str(rpc.object).startswith("dea00000-6c97-11d1-8271-"): return True return False -DceRpcPayload.register_possible_payload(PNIOServiceReqPDU) +DceRpc4Payload.register_possible_payload(PNIOServiceReqPDU) class PNIOServiceResPDU(Packet): """PNIO PDU for RPC Response""" fields_desc = [ - EndiannessField(IntEnumField("status", 0, ["OK"]), - endianess_from=dce_rpc_endianess), + EField(IntEnumField("status", 0, ["OK"]), + endianness_from=dce_rpc_endianness), NDRData, ] overload_fields = { - DceRpc: { - # random object_uuid in the appropriate range - "object_uuid": RandUUID("dea00000-6c97-11d1-8271-******"), + DceRpc4: { + # random object in the appropriate range + "object": RandUUID("dea00000-6c97-11d1-8271-******"), # interface uuid to send to a host - "interface_uuid": RPC_INTERFACE_UUID[ + "if_id": RPC_INTERFACE_UUID[ "UUID_IO_ControllerInterface"], # Request DCE/RPC type - "type": 2, + "ptype": 2, }, } @@ -1001,10 +1544,10 @@ class PNIOServiceResPDU(Packet): def can_handle(cls, pkt, rpc): """heuristic guess_payload_class""" # type = 2 => response - if rpc.getfieldval("type") == 2 and \ - str(rpc.object_uuid).startswith("dea00000-6c97-11d1-8271-"): + if rpc.ptype == 2 and \ + str(rpc.object).startswith("dea00000-6c97-11d1-8271-"): return True return False -DceRpcPayload.register_possible_payload(PNIOServiceResPDU) +DceRpc4Payload.register_possible_payload(PNIOServiceResPDU) diff --git a/scapy/contrib/portmap.py b/scapy/contrib/portmap.py index 21cf66c4df6..ed30b0994a2 100644 --- a/scapy/contrib/portmap.py +++ b/scapy/contrib/portmap.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Lucas Preston -# This program is published under a GPLv2 license # scapy.contrib.description = Portmapper v2 # scapy.contrib.status = loads @@ -73,7 +73,7 @@ class DUMP_Reply(Packet): name = 'PORTMAP DUMP Reply' fields_desc = [ IntField('value_follows', 0), - PacketListField('mappings', [], cls=Map_Entry, + PacketListField('mappings', [], Map_Entry, next_cls_cb=lambda pkt, lst, cur, remain: Map_Entry if pkt.value_follows == 1 and (len(lst) == 0 or cur.value_follows == 1) and diff --git a/scapy/contrib/postgres.py b/scapy/contrib/postgres.py new file mode 100644 index 00000000000..fb72d3d53b0 --- /dev/null +++ b/scapy/contrib/postgres.py @@ -0,0 +1,800 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = Postgres PSQL Binary Protocol +# scapy.contrib.status = loads + +import struct + +from typing import ( + Optional, + Callable, + Any, + Tuple, +) +from scapy.fields import ( + ByteField, + CharEnumField, + Field, + FieldLenField, + FieldListField, + IntEnumField, + PacketListField, + ShortField, + SignedIntField, + SignedShortField, + StrField, + StrLenField, + StrNullField, +) +from scapy.packet import Packet, bind_layers +from scapy.layers.inet import TCP +from scapy.sessions import TCPSession + +AUTH_CODES = { + 0: "AuthenticationOk", + 1: "AuthenticationKerberosV4", + 2: "AuthenticationKerberosV5", + 3: "AuthenticationCleartextPassword", + 4: "AuthenticationCryptPassword", + 5: "AuthenticationMD5Password", + 6: "AuthenticationSCMCredential", + 7: "AuthenticationGSS", + 8: "AuthenticationGSSContinue", + 9: "AuthenticationSSPI", + 10: "AuthenticationSASL", + 11: "AuthenticationSASLContinue", + 12: "AuthenticationSASLFinal", +} + + +class KeepAlive(Packet): + name = "Keep Alive" + fields_desc = [ + SignedIntField("len", 4), + ] + + +class SSLRequest(Packet): + name = "SSL request code message" + fields_desc = [ + FieldLenField("length", None, fmt="I"), + SignedIntField("request_code", 80877103), + ] + + +class _DictStrField(StrField): + """Takes a dictionary as an argument and packs back into a byte string.""" + + def i2m(self, pkt, x): + if isinstance(x, bytes): + return x + if isinstance(x, dict): + result = bytes() + for k, v in x.items(): + result += k + b"\x00" + v + b"\x00" + return result + b"\x00" + else: + return super(_DictStrField, self).i2m(pkt, x) + + def i2len(self, pkt, x): + # type: (Optional[Packet], Any) -> int + if x is None: + return 0 + return len(self.i2m(pkt, x)) + + +class Startup(Packet): + name = "Startup Request Packet" + fields_desc = [ + FieldLenField( + "len", None, length_of="options", fmt="I", adjust=lambda pkt, x: x + 8 + ), + ShortField("protocol_version_major", 3), + ShortField("protocol_version_minor", 0), + _DictStrField("options", None), + ] + + +class _FieldsLenField(Field[int, int]): + """Same as FieldLenField but takes a tuple of fields for length_of.""" + + __slots__ = ["length_of", "adjust"] + + def __init__( + self, + name, # type: str + default, # type: Optional[Any] + length_of=None, # type: Optional[Tuple[str]] + fmt="H", # type: str + adjust=lambda pkt, x: x, # type: Callable[[Packet, int], int] + ): + # type: (...) -> None + super(_FieldsLenField, self).__init__(name, default, fmt) + self.length_of = length_of + self.adjust = adjust + + def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[int]) -> int + if x is None and pkt is not None: + if self.length_of is not None: + f = 0 + for length_of_field in self.length_of: + fld, fval = pkt.getfield_and_val(length_of_field) + f += fld.i2len(pkt, fval) + else: + raise ValueError("Field should have either length_of or count_of") + x = self.adjust(pkt, f) + elif x is None: + x = 0 + return x + + +def determine_pg_field(pkt, lst, cur, remain): + key = b"" + if remain: + key = remain[0:1] # Python 2/3 compat + if key in pkt.cls_mapping: + return pkt.cls_mapping[key] + elif remain[0:1] == b"\x00" and len(remain) >= 4: + length = struct.unpack("!I", remain[0:3])[0] + if length == 0: + return KeepAlive + elif length == 8: + return SSLRequest + else: + return Startup + else: + return None + + +class ByteTagField(ByteField): + def __init__( + self, default # type: bytes + ): + super(ByteTagField, self).__init__("tag", ord(default)) + + def randval(self): + return ord(self.default) + + +class _BasePostgres(Packet, TCPSession): + name = "Regular packet" + fields_desc = [PacketListField("contents", [], next_cls_cb=determine_pg_field)] + + @classmethod + def tcp_reassemble(cls, data, metadata): + if data and data[0:1] == b"\x00": + length = struct.unpack("!I", data[0:3])[0] + if length == 8: + return SSLRequest(data) + else: + return Startup(data) + else: + return cls(data) + + +class _ZeroPadding(Packet): + def extract_padding(self, p): + return b"", p + + +class SignedIntStrPair(_ZeroPadding): + name = "Bytes data" + fields_desc = [ + FieldLenField("len", 0, fmt="i", length_of="value"), + StrLenField( + "data", None, length_from=lambda pkt: pkt.len if pkt.len > 0 else 0 + ), + ] + + +class Authentication(_ZeroPadding): + name = "Authentication Request" + fields_desc = [ + ByteTagField(b"R"), + FieldLenField( + "len", None, length_of="optional", fmt="I", adjust=lambda pkt, x: x + 8 + ), + IntEnumField("method", default=0, enum=AUTH_CODES), + StrLenField("optional", None, length_from=lambda pkt: pkt.len - 8), + ] + + +class ParameterStatus(_ZeroPadding): + name = "Parameter Status" + fields_desc = [ + ByteTagField(b"S"), + FieldLenField( + "len", + None, + fmt="I", + length_of=("parameter", "value"), + adjust=lambda pkt, x: x + 4, + ), + StrNullField( + "parameter", + "", + ), + StrNullField( + "value", + "", + ), + ] + + +class Query(_ZeroPadding): + name = "Simple Query" + fields_desc = [ + ByteTagField(b"Q"), + FieldLenField( + "len", None, length_of="query", fmt="I", adjust=lambda pkt, x: x + 5 + ), + StrNullField("query", None), + ] + + +class CommandComplete(_ZeroPadding): + name = "Command Completion Response" + fields_desc = [ + ByteTagField(b"C"), + FieldLenField( + "len", None, length_of="cmdtag", fmt="I", adjust=lambda pkt, x: x + 4 + ), + StrLenField("cmdtag", "", length_from=lambda pkt: pkt.len - 4), + ] + + +class BackendKeyData(_ZeroPadding): + name = "Backend Key Data" + fields_desc = [ + ByteTagField(b"K"), + FieldLenField("len", None, fmt="I"), + SignedIntField("pid", 0), + SignedIntField("key", 0), + ] + + +STATUS_TYPE = { + b"E": "InFailedTransaction", + b"I": "Idle", + b"T": "InTransaction", +} + + +class ReadyForQuery(_ZeroPadding): + name = "Ready Signal" + fields_desc = [ + ByteTagField(b"Z"), + SignedIntField("len", 6), + CharEnumField("status", b"I", STATUS_TYPE), + ] + + +class ColumnDescription(_ZeroPadding): + name = "Column Description" + fields_desc = [ + StrNullField("col", None), + SignedIntField("tableoid", 0), + SignedShortField("colno", 0), + SignedIntField("typeoid", 0), + SignedShortField("typelen", 0), + SignedIntField("typemod", 0), + SignedShortField("format", 0), + ] + + +class RowDescription(_ZeroPadding): + name = "Row Description" + fields_desc = [ + ByteTagField(b"T"), + FieldLenField( + "len", None, fmt="I", length_of="cols", adjust=lambda pkt, x: x + 6 + ), + SignedShortField("numfields", 0), + PacketListField( + "cols", + [], + pkt_cls=ColumnDescription, + count_from=lambda pkt: pkt.numfields, + length_from=lambda pkt: pkt.len - 6, + ), + ] + + +class DataRow(_ZeroPadding): + name = "Data Row" + fields_desc = [ + ByteTagField(b"D"), + FieldLenField( + "len", None, fmt="I", length_of="data", adjust=lambda pkt, x: len(pkt) - 1 + ), + FieldLenField("numfields", 0), + PacketListField( + "data", + [], + SignedIntStrPair, + count_from=lambda pkt: pkt.numfields, + ), + ] + + +# See https://www.postgresql.org/docs/current/protocol-error-fields.html +ERROR_FIELD = { + b"S": "Severity", + b"V": "SeverityNonLocalized", + b"C": "Code", + b"M": "Message", + b"D": "Detail", + b"H": "Hint", + b"P": "Position", + b"p": "InternalPosition", + b"q": "InternalQuery", + b"W": "Where", + b"s": "SchemaName", + b"t": "TableName", + b"c": "ColumnName", + b"d": "DataTypeName", + b"n": "ConstraintName", + b"F": "File", + b"L": "Line", + b"R": "Routine", +} + + +class ErrorResponseField(StrNullField): + def m2i(self, pkt, x): + """Unpack into a tuple of Field, Value.""" + i = super(ErrorResponseField, self).m2i(pkt, x) + i_code = i[0:1] # Python 2/3 compatible + return (ERROR_FIELD.get(i_code, i_code), i[1:]) + + +class ErrorResponse(_ZeroPadding): + name = "Error Response" + fields_desc = [ + ByteTagField(b"E"), + FieldLenField( + "len", None, length_of="error_fields", fmt="I", adjust=lambda pkt, x: x + 5 + ), + FieldListField( + "error_fields", + [], + ErrorResponseField("value", None), + length_from=lambda pkt: pkt.len - 5, + ), + ByteField("terminator", None), + ] + + +class Terminate(_ZeroPadding): + name = "Termination Request" + fields_desc = [ + ByteTagField(b"X"), + SignedIntField("len", 4), + ] + + +class _Todo(_ZeroPadding): + name = "Unsupported message" + fields_desc = [ + ByteTagField(b"?"), + FieldLenField("len", None, fmt="I", length_of="body"), + StrLenField("body", None, length_from=lambda pkt: pkt.len - 4), + ] + + +class Bind(_ZeroPadding): + name = "Bind Request" + fields_desc = [ + ByteTagField(b"?"), + FieldLenField( + "len", None, fmt="I", length_of="body", adjust=lambda pkt, x: len(pkt) - 1 + ), + StrNullField("destination", ""), + StrNullField("statement", ""), + FieldLenField("codes_count", 0, fmt="H", count_of="codes"), + FieldListField( + "codes", [], ShortField("", 0), count_from=lambda pkt: pkt.codes_count + ), + FieldLenField("values_count", 0, fmt="H", count_of="values"), + PacketListField( + "values", [], SignedIntStrPair, count_from=lambda pkt: pkt.values_count + ), + FieldLenField("results_count", 0, fmt="H", count_of="results"), + FieldListField( + "results", [], ShortField("", 0), count_from=lambda pkt: pkt.results_count + ), + ] + + +class BindComplete(_ZeroPadding): + name = "Bind Complete" + fields_desc = [ + ByteTagField(b"2"), + SignedIntField("len", 4), + ] + + +CLOSE_DESCRIBE_TYPE = {b"S": "PreparedStatement", b"P": "Portal"} + + +class Close(_ZeroPadding): + name = "Close Request" + fields_desc = [ + ByteTagField(b"C"), + FieldLenField( + "len", None, fmt="I", length_of="statement", adjust=lambda pkt, x: x + 6 + ), + CharEnumField("close_type", b"S", enum=CLOSE_DESCRIBE_TYPE), + StrNullField( + "statement", + "", + ), + ] + + +class CloseComplete(_ZeroPadding): + name = "Close Complete" + fields_desc = [ + ByteTagField(b"3"), + SignedIntField("len", 4), + ] + + +class Describe(_ZeroPadding): + name = "Describe" + fields_desc = [ + ByteTagField(b"D"), + FieldLenField( + "len", None, fmt="I", length_of="statement", adjust=lambda pkt, x: x + 6 + ), + CharEnumField("close_type", b"S", enum=CLOSE_DESCRIBE_TYPE), + StrNullField("statement", ""), + ] + + +class EmptyQueryResponse(_ZeroPadding): + name = "Empty Query Response" + fields_desc = [ + ByteTagField(b"I"), + SignedIntField("len", 4), + ] + + +class Flush(_ZeroPadding): + name = "Flush Request" + fields_desc = [ + ByteTagField(b"H"), + SignedIntField("len", 4), + ] + + +class NoData(_ZeroPadding): + name = "No Data Response" + fields_desc = [ + ByteTagField(b"n"), + SignedIntField("len", 4), + ] + + +class ParseComplete(_ZeroPadding): + name = "Parse Complete Response" + fields_desc = [ + ByteTagField(b"1"), + SignedIntField("len", 4), + ] + + +class PortalSuspended(_ZeroPadding): + name = "Portal Suspended Response" + fields_desc = [ + ByteTagField(b"s"), + SignedIntField("len", 4), + ] + + +class Sync(_ZeroPadding): + name = "Sync Request" + fields_desc = [ + ByteTagField(b"S"), + SignedIntField("len", 4), + ] + + +class Parse(_ZeroPadding): + name = "Parse Request" + fields_desc = [ + ByteTagField(b"P"), + FieldLenField("len", None, fmt="I", adjust=lambda pkt, x: len(pkt) - 1), + StrNullField("destination", ""), + StrNullField("query", ""), + FieldLenField("num_param_dtypes", None, fmt="H", count_of="params"), + FieldListField( + "params", + [], + SignedIntField("param", None), + count_from=lambda pkt: pkt.num_param_dtypes, + ), + ] + + +class Execute(_ZeroPadding): + name = "Execute Request" + fields_desc = [ + ByteTagField(b"E"), + FieldLenField( + "len", None, fmt="I", length_of="portal", adjust=lambda pkt, x: x + 9 + ), + StrNullField( + "portal", + "", + ), + SignedIntField("rows", 0), + ] + + +class PasswordMessage(_ZeroPadding): + """ + Identifies the message as a password response. + Note that this is also used for GSSAPI, SSPI and SASL + response messages. The exact message type can be deduced + from the context. + """ + + name = "Password Request Response" + fields_desc = [ + ByteTagField(b"p"), + FieldLenField( + "len", None, fmt="I", length_of="password", adjust=lambda pkt, x: x + 4 + ), + StrLenField("password", None, length_from=lambda pkt: pkt.len - 4), + ] + + +class NoticeResponse(_ZeroPadding): + name = "Notice Response" + fields_desc = [ + ByteTagField(b"N"), + FieldLenField( + "len", None, length_of="notice_fields", fmt="I", adjust=lambda pkt, x: x + 5 + ), + FieldListField( + "notice_fields", + [], + ErrorResponseField("value", None), + length_from=lambda pkt: pkt.len - 5, + ), + ByteField("terminator", None), + ] + + +class NotificationResponse(_ZeroPadding): + name = "Password Request Response" + fields_desc = [ + ByteTagField(b"A"), + _FieldsLenField( + "len", + None, + fmt="I", + length_of=("channel", "payload"), + adjust=lambda pkt, x: x + 8, + ), + SignedIntField("process_id", 0), + StrNullField("channel", None), + StrNullField("payload", None), + ] + + +class NegotiateProtocolVersion(_ZeroPadding): + name = "Negotiate Protocol Version Response" + fields_desc = [ + ByteTagField(b"v"), + FieldLenField( + "len", None, fmt="I", length_of="option", adjust=lambda pkt, x: x + 12 + ), + SignedIntField("min_minor_version", 0), + SignedIntField("unrecognized_options", 0), + StrNullField("option", None), + ] + + +class FunctionCallResponse(_ZeroPadding): + name = "Function Call Response" + fields_desc = [ + ByteTagField(b"V"), + FieldLenField( + "len", None, fmt="I", length_of="result", adjust=lambda pkt, x: x + 8 + ), + FieldLenField("result_len", None, length_of="result"), + StrLenField("result", None, length_from=lambda pkt: pkt.result_len), + ] + + +class ParameterDescription(_ZeroPadding): + name = "Parameter Description" + fields_desc = [ + ByteTagField(b"t"), + FieldLenField( + "len", None, fmt="I", length_of="dtypes", adjust=lambda pkt, x: x + 6 + ), + SignedShortField("dtypes_len", 0), + FieldListField( + "dtypes", + [], + SignedIntField("dtype", None), + count_from=lambda pkt: pkt.dtypes_len, + ), + ] + + +class CopyData(_ZeroPadding): + name = "Copy Data" + fields_desc = [ + ByteTagField(b"d"), + FieldLenField( + "len", None, fmt="I", length_of="data", adjust=lambda pkt, x: x + 4 + ), + StrLenField("data", None, length_from=lambda pkt: pkt.len - 4), + ] + + +class CopyDone(_ZeroPadding): + name = "Copy Done" + fields_desc = [ + ByteTagField(b"c"), + SignedIntField("len", 4), + ] + + +class CopyFail(_ZeroPadding): + name = "Copy Fail Reason" + fields_desc = [ + ByteTagField(b"f"), + FieldLenField( + "len", None, fmt="I", length_of="reason", adjust=lambda pkt, x: x + 4 + ), + StrLenField("reason", None, length_from=lambda pkt: pkt.len - 4), + ] + + +class CancelRequest(Packet): + name = "Cancel Request" + fields_desc = [ + SignedIntField("len", 16), + SignedIntField("request_code", 80877102), + SignedIntField("process_id", 0), + SignedIntField("secret", 0), + ] + + +class GSSENCRequest(Packet): + name = "GSSENC Request" + fields_desc = [ + SignedIntField("len", 8), + SignedIntField("request_code", 80877104), + ] + + +class CopyInResponse(_ZeroPadding): + name = "Copy in Response" + fields_desc = [ + ByteTagField(b"G"), + FieldLenField( + "len", None, fmt="I", length_of="cols", adjust=lambda pkt, x: x + 7 + ), + ByteField("format", 0), + ShortField("ncols", 0), + FieldListField( + "cols", + [], + ShortField("format", None), + count_from=lambda pkt: pkt.ncols, + ), + ] + + +class CopyOutResponse(_ZeroPadding): + name = "Copy out Response" + fields_desc = [ + ByteTagField(b"H"), + FieldLenField( + "len", None, fmt="I", length_of="cols", adjust=lambda pkt, x: x + 7 + ), + ByteField("format", 0), + ShortField("ncols", 0), + FieldListField( + "cols", + [], + ShortField("format", None), + count_from=lambda pkt: pkt.ncols, + ), + ] + + +class CopyBothResponse(_ZeroPadding): + name = "Copy both Response" + fields_desc = [ + ByteTagField(b"W"), + FieldLenField( + "len", None, fmt="I", length_of="cols", adjust=lambda pkt, x: x + 7 + ), + ByteField("format", 0), + ShortField("ncols", 0), + FieldListField( + "cols", + [], + ShortField("format", None), + count_from=lambda pkt: pkt.ncols, + ), + ] + + +FRONTEND_TAG_TO_PACKET_CLS = { + b"B": Bind, + b"C": Close, + b"d": CopyData, + b"c": CopyDone, + b"f": CopyFail, + b"D": Describe, + b"E": Execute, + b"H": Flush, + b"F": _Todo, + b"P": Parse, + b"p": PasswordMessage, + b"Q": Query, + b"S": Sync, + b"X": Terminate, +} + +BACKEND_TAG_TO_PACKET_CLS = { + b"R": Authentication, + b"K": BackendKeyData, + b"2": BindComplete, + b"3": CloseComplete, + b"C": CommandComplete, + b"d": CopyData, + b"c": CopyDone, + b"G": CopyInResponse, + b"H": CopyOutResponse, + b"W": CopyBothResponse, + b"D": DataRow, + b"I": EmptyQueryResponse, + b"E": ErrorResponse, + b"V": FunctionCallResponse, + b"v": NegotiateProtocolVersion, + b"n": NoData, + b"N": NoticeResponse, + b"A": NotificationResponse, + b"t": ParameterDescription, + b"S": ParameterStatus, + b"1": ParseComplete, + b"s": PortalSuspended, + b"Z": ReadyForQuery, + b"T": RowDescription, +} + + +class PostgresFrontend(_BasePostgres): + cls_mapping = FRONTEND_TAG_TO_PACKET_CLS + + @classmethod + def tcp_reassemble(cls, data, metadata): + msgs = PostgresFrontend(data) + if msgs.contents and "Sync" in msgs.contents[-1]: + return msgs + + +class PostgresBackend(_BasePostgres): + cls_mapping = BACKEND_TAG_TO_PACKET_CLS + + @classmethod + def tcp_reassemble(cls, data, metadata): + msgs = PostgresBackend(data) + if msgs.contents and "ReadyForQuery" in msgs.contents[-1]: + return msgs + + +bind_layers(TCP, PostgresFrontend, dport=5432) +bind_layers(TCP, PostgresBackend, sport=5432) diff --git a/scapy/contrib/ppi_cace.py b/scapy/contrib/ppi_cace.py index d472f7bd948..e8190adb0c4 100644 --- a/scapy/contrib/ppi_cace.py +++ b/scapy/contrib/ppi_cace.py @@ -1,17 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # author: # scapy.contrib.description = CACE Per-Packet Information (PPI) diff --git a/scapy/contrib/ppi_geotag.py b/scapy/contrib/ppi_geotag.py index a7cc6345468..6a704a16fa7 100644 --- a/scapy/contrib/ppi_geotag.py +++ b/scapy/contrib/ppi_geotag.py @@ -1,17 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # author: # scapy.contrib.description = CACE Per-Packet Information (PPI) Geolocation @@ -22,7 +11,6 @@ PPI-GEOLOCATION tags """ -from __future__ import absolute_import import functools import struct @@ -34,8 +22,6 @@ UTCTimeField, XLEIntField, SignedByteField, XLEShortField from scapy.layers.ppi import PPI_Hdr, PPI_Element from scapy.error import warning -import scapy.modules.six as six -from scapy.modules.six.moves import range CURR_GEOTAG_VER = 2 # Major revision of specification @@ -212,7 +198,7 @@ def any2i(self, pkt, x): pass else: y = x - # print "any2i: %s --> %s" % (str(x), str(y)) + # print("any2i: %s --> %s" % (str(x), str(y))) return y @@ -265,7 +251,7 @@ def __init__(self, name, default): def _FlagsList(myfields): flags = ["Reserved%02d" % i for i in range(32)] - for i, value in six.iteritems(myfields): + for i, value in myfields.items(): flags[i] = value return flags @@ -292,7 +278,7 @@ def _FlagsList(myfields): 8: "GPS Derived", 9: "INS Derived", 10: "Compass Derived", - 11: "Acclerometer Derived", + 11: "Accelerometer Derived", 12: "Human Derived", }) @@ -372,7 +358,7 @@ def __new__(cls, name, bases, dct): return x -class HCSIPacket(six.with_metaclass(_Geotag_metaclass, PPI_Element)): +class HCSIPacket(PPI_Element, metaclass=_Geotag_metaclass): def post_build(self, p, pay): if self.geotag_len is None: sl_g = struct.pack('>> payload = IP() / UDP(sport=1234, dport=5678) / Raw("A" * 9) +>>> iv = b'\x01\x02\x03\x04\x05\x06\x07\x08' +>>> spi = 0x11223344 +>>> key = b'\xFF\xEE\xDD\xCC\xBB\xAA\x99\x88\x77\x66\x55\x44\x33\x22\x11\x00' +>>> psp_packet = PSP(nexthdr=4, cryptoffset=5, spi=spi, iv=iv, data=payload) +>>> hexdump(psp_packet) +0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........ +0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|..... +0020 7F 00 00 01 04 D2 16 2E 00 11 A0 C4 41 41 41 41 ............AAAA +0030 41 41 41 41 41 AAAAA +>>> +>>> psp_packet.encrypt(key) +>>> hexdump(psp_packet) +0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........ +0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|..... +0020 7F 00 00 01 8E 3E 2B 13 45 C7 6B F9 5C DA C3 9B .....>+.E.k.\... +0030 86 17 62 A0 CF DF FB BE BB C6 31 3A 2B 9D E0 64 ..b.......1:+..d +0040 75 9C DD 71 C9 u..q. +>>> +>>> psp_packet.decrypt(key) +>>> hexdump(psp_packet) +0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........ +0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|..... +0020 7F 00 00 01 04 D2 16 2E 00 11 A0 C4 41 41 41 41 ............AAAA +0030 41 41 41 41 41 AAAAA +>>> + +""" + +from scapy.config import conf +from scapy.error import log_loading +from scapy.fields import ( + BitField, + ByteField, + ConditionalField, + XIntField, + XStrField, + StrFixedLenField, +) +from scapy.packet import ( + Packet, + bind_bottom_up, + bind_top_down, +) +from scapy.layers.inet import UDP + +############################################################################### +if conf.crypto_valid: + from cryptography.exceptions import InvalidTag + from cryptography.hazmat.primitives.ciphers import ( + aead, + ) +else: + log_loading.info("Can't import python-cryptography v2.0+. " + "Disabled PSP encryption/authentication.") + +############################################################################### +import struct + + +class PSP(Packet): + """ + PSP Security Protocol + + See https://github.com/google/psp/blob/main/doc/PSP_Arch_Spec.pdf + """ + name = 'PSP' + + fields_desc = [ + ByteField('nexthdr', 0), + ByteField('hdrextlen', 1), + BitField("reserved", 0, 2), + BitField("cryptoffset", 0, 6), + BitField("sample", 0, 1), + BitField("drop", 0, 1), + BitField("version", 0, 4), + BitField("is_virt", 0, 1), + BitField("one_bit", 1, 1), + XIntField('spi', 0x00), + StrFixedLenField('iv', '\x00' * 8, 8), + ConditionalField(XIntField("virtkey", 0x00), lambda pkt: pkt.is_virt == 1), + ConditionalField(XIntField("sectoken", 0x00), lambda pkt: pkt.is_virt == 1), + XStrField('data', None), + ] + + def sanitize_cipher(self): + """ + Ensure we support the cipher to encrypt/decrypt this packet + + :returns: the supported cipher suite + :raise scapy.layers.psp.PSPCipherError: if the requested cipher + is unsupported + """ + if self.version not in (0, 1): + raise PSPCipherError('Can not encrypt/decrypt using unsupported version %s' + % (self.version)) + return aead.AESGCM + + def encrypt(self, key): + """ + Encrypt a PSP packet + + :param key: the secret key used for encryption + :raise scapy.layers.psp.PSPCipherError: if the requested cipher + is unsupported + """ + cipher = self.sanitize_cipher() + encrypt_start_offset = 16 + self.cryptoffset * 4 + iv = struct.pack("!L", self.spi) + self.iv + plain = b'' + to_encrypt = bytes(self.data) + self.data = b'' + psp_header = bytes(self) + header_length = len(psp_header) + # Header should always be fully plaintext + if header_length < encrypt_start_offset: + plain = to_encrypt[:encrypt_start_offset - header_length] + to_encrypt = to_encrypt[encrypt_start_offset - header_length:] + cipher = cipher(key) + payload = cipher.encrypt(iv, to_encrypt, psp_header + plain) + self.data = plain + payload + + def decrypt(self, key): + """ + Decrypt a PSP packet + + :param key: the secret key used for encryption + :raise scapy.layers.psp.PSPIntegrityError: if the integrity check + fails with an AEAD algorithm + :raise scapy.layers.psp.PSPCipherError: if the requested cipher + is unsupported + """ + cipher = self.sanitize_cipher() + self.icv_size = 16 + iv = struct.pack("!L", self.spi) + self.iv + data = self.data[:len(self.data) - self.icv_size] + icv = self.data[len(self.data) - self.icv_size:] + + decrypt_start_offset = 16 + self.cryptoffset * 4 + plain = b'' + to_decrypt = bytes(data) + self.data = b'' + psp_header = bytes(self) + header_length = len(psp_header) + # Header should always be fully plaintext + if header_length < decrypt_start_offset: + plain = to_decrypt[:decrypt_start_offset - header_length] + to_decrypt = to_decrypt[decrypt_start_offset - header_length:] + cipher = cipher(key) + try: + data = cipher.decrypt(iv, to_decrypt + icv, psp_header + plain) + self.data = plain + data + except InvalidTag as err: + raise PSPIntegrityError(err) + + +bind_bottom_up(UDP, PSP, dport=1000) +bind_bottom_up(UDP, PSP, sport=1000) +bind_top_down(UDP, PSP, dport=1000, sport=1000) + +############################################################################### + + +class PSPCipherError(Exception): + """ + Error risen when the cipher is unsupported. + """ + pass + + +class PSPIntegrityError(Exception): + """ + Error risen when the integrity check fails. + """ + pass diff --git a/scapy/contrib/ptp_v2.py b/scapy/contrib/ptp_v2.py new file mode 100644 index 00000000000..a9dbbe0297a --- /dev/null +++ b/scapy/contrib/ptp_v2.py @@ -0,0 +1,163 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi +# Copyright (C) Satveer Brar + +# scapy.contrib.description = Precision Time Protocol v2 +# scapy.contrib.status = loads + +""" +PTP (Precision Time Protocol). +References : IEEE 1588-2008 +""" + +import struct + +from scapy.packet import Packet, bind_layers +from scapy.fields import ( + BitEnumField, + BitField, + ByteField, + IntField, + LongField, + ShortField, + ByteEnumField, + FlagsField, + XLongField, + XByteField, + ConditionalField, +) +from scapy.layers.inet import UDP + + +############################################################################# +# PTPv2 +############################################################################# + +# IEEE 1588-2008 / Section 13.3.2.2 + +_message_type = { + 0x0: "Sync", + 0x1: "Delay_Req", + 0x2: "Pdelay_Req", + 0x3: "Pdelay_Resp", + 0x4: "Reserved", + 0x5: "Reserved", + 0x6: "Reserved", + 0x7: "Reserved", + 0x8: "Follow_Up", + 0x9: "Delay_Resp", + 0xA: "Pdelay_Resp_Follow", + 0xB: "Announce", + 0xC: "Signaling", + 0xD: "Management", + 0xE: "Reserved", + 0xF: "Reserved" +} + +_control_field = { + 0x00: "Sync", + 0x01: "Delay_Req", + 0x02: "Follow_Up", + 0x03: "Delay_Resp", + 0x04: "Management", + 0x05: "All others", +} + +_flags = { + 0x0001: "alternateMasterFlag", + 0x0002: "twoStepFlag", + 0x0004: "unicastFlag", + 0x0010: "ptpProfileSpecific1", + 0x0020: "ptpProfileSpecific2", + 0x0040: "reserved", + 0x0100: "leap61", + 0x0200: "leap59", + 0x0400: "currentUtcOffsetValid", + 0x0800: "ptpTimescale", + 0x1000: "timeTraceable", + 0x2000: "frequencyTraceable" +} + + +class PTP(Packet): + """ + PTP packet based on IEEE 1588-2008 / Section 13.3 + """ + + name = "PTP" + match_subclass = True + fields_desc = [ + BitField("transportSpecific", 0, 4), + BitEnumField("messageType", 0x0, 4, _message_type), + BitField("reserved1", 0, 4), + BitField("version", 2, 4), + ShortField("messageLength", None), + ByteField("domainNumber", 0), + ByteField("reserved2", 0), + FlagsField("flags", 0, 16, _flags), + LongField("correctionField", 0), + IntField("reserved3", 0), + XLongField("clockIdentity", 0), + ShortField("portNumber", 0), + ShortField("sequenceId", 0), + ByteEnumField("controlField", 0, _control_field), + ByteField("logMessageInterval", 0), + ConditionalField(BitField("originTimestamp_seconds", 0, 48), + lambda pkt: pkt.messageType in [0x0, 0x1, 0x2, 0xB]), + ConditionalField(IntField("originTimestamp_nanoseconds", 0), + lambda pkt: pkt.messageType in [0x0, 0x1, 0x2, 0xB]), + ConditionalField(BitField("preciseOriginTimestamp_seconds", 0, 48), + lambda pkt: pkt.messageType == 0x8), + ConditionalField(IntField("preciseOriginTimestamp_nanoseconds", 0), + lambda pkt: pkt.messageType == 0x8), + ConditionalField(BitField("requestReceiptTimestamp_seconds", 0, 48), + lambda pkt: pkt.messageType == 0x3), + ConditionalField(IntField("requestReceiptTimestamp_nanoseconds", 0), + lambda pkt: pkt.messageType == 0x3), + ConditionalField(BitField("receiveTimestamp_seconds", 0, 48), + lambda pkt: pkt.messageType == 0x9), + ConditionalField(IntField("receiveTimestamp_nanoseconds", 0), + lambda pkt: pkt.messageType == 0x9), + ConditionalField(BitField("responseOriginTimestamp_seconds", 0, 48), + lambda pkt: pkt.messageType == 0xA), + ConditionalField(IntField("responseOriginTimestamp_nanoseconds", 0), + lambda pkt: pkt.messageType == 0xA), + ConditionalField(ShortField("currentUtcOffset", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(ByteField("reserved4", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(ByteField("grandmasterPriority1", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(ByteField("grandmasterClockClass", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(XByteField("grandmasterClockAccuracy", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(ShortField("grandmasterClockVariance", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(ByteField("grandmasterPriority2", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(XLongField("grandmasterIdentity", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(ShortField("stepsRemoved", 0), + lambda pkt: pkt.messageType == 0xB), + ConditionalField(XByteField("timeSource", 0), + lambda pkt: pkt.messageType == 0xB) + + ] + + def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes + """ + Update the messageLength field after building the packet + """ + if self.messageLength is None: + pkt = pkt[:2] + struct.pack("!H", len(pkt)) + pkt[4:] + + return pkt + pay + + +# Layer bindings + +bind_layers(UDP, PTP, sport=319, dport=319) +bind_layers(UDP, PTP, sport=320, dport=320) diff --git a/scapy/contrib/ripng.py b/scapy/contrib/ripng.py index e5f71500777..34ce9d74703 100644 --- a/scapy/contrib/ripng.py +++ b/scapy/contrib/ripng.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = Routing Information Protocol next gen (RIPng) # scapy.contrib.status = loads diff --git a/scapy/contrib/roce.py b/scapy/contrib/roce.py new file mode 100644 index 00000000000..93dceab693b --- /dev/null +++ b/scapy/contrib/roce.py @@ -0,0 +1,258 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Haggai Eran + +# scapy.contrib.description = RoCE v2 +# scapy.contrib.status = loads + +""" +RoCE: RDMA over Converged Ethernet +""" + +from scapy.packet import Packet, bind_layers, Raw +from scapy.fields import ByteEnumField, ByteField, XByteField, \ + ShortField, XShortField, XLongField, BitField, XBitField, FCSField +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether +from scapy.compat import raw +from scapy.error import warning +from zlib import crc32 +import struct + +from typing import ( + Tuple +) + +_transports = { + 'RC': 0x00, + 'UC': 0x20, + 'RD': 0x40, + 'UD': 0x60, +} + +_ops = { + 'SEND_FIRST': 0x00, + 'SEND_MIDDLE': 0x01, + 'SEND_LAST': 0x02, + 'SEND_LAST_WITH_IMMEDIATE': 0x03, + 'SEND_ONLY': 0x04, + 'SEND_ONLY_WITH_IMMEDIATE': 0x05, + 'RDMA_WRITE_FIRST': 0x06, + 'RDMA_WRITE_MIDDLE': 0x07, + 'RDMA_WRITE_LAST': 0x08, + 'RDMA_WRITE_LAST_WITH_IMMEDIATE': 0x09, + 'RDMA_WRITE_ONLY': 0x0a, + 'RDMA_WRITE_ONLY_WITH_IMMEDIATE': 0x0b, + 'RDMA_READ_REQUEST': 0x0c, + 'RDMA_READ_RESPONSE_FIRST': 0x0d, + 'RDMA_READ_RESPONSE_MIDDLE': 0x0e, + 'RDMA_READ_RESPONSE_LAST': 0x0f, + 'RDMA_READ_RESPONSE_ONLY': 0x10, + 'ACKNOWLEDGE': 0x11, + 'ATOMIC_ACKNOWLEDGE': 0x12, + 'COMPARE_SWAP': 0x13, + 'FETCH_ADD': 0x14, +} + + +CNP_OPCODE = 0x81 + + +def opcode(transport, op): + # type: (str, str) -> Tuple[int, str] + return (_transports[transport] + _ops[op], '{}_{}'.format(transport, op)) + + +_bth_opcodes = dict([ + opcode('RC', 'SEND_FIRST'), + opcode('RC', 'SEND_MIDDLE'), + opcode('RC', 'SEND_LAST'), + opcode('RC', 'SEND_LAST_WITH_IMMEDIATE'), + opcode('RC', 'SEND_ONLY'), + opcode('RC', 'SEND_ONLY_WITH_IMMEDIATE'), + opcode('RC', 'RDMA_WRITE_FIRST'), + opcode('RC', 'RDMA_WRITE_MIDDLE'), + opcode('RC', 'RDMA_WRITE_LAST'), + opcode('RC', 'RDMA_WRITE_LAST_WITH_IMMEDIATE'), + opcode('RC', 'RDMA_WRITE_ONLY'), + opcode('RC', 'RDMA_WRITE_ONLY_WITH_IMMEDIATE'), + opcode('RC', 'RDMA_READ_REQUEST'), + opcode('RC', 'RDMA_READ_RESPONSE_FIRST'), + opcode('RC', 'RDMA_READ_RESPONSE_MIDDLE'), + opcode('RC', 'RDMA_READ_RESPONSE_LAST'), + opcode('RC', 'RDMA_READ_RESPONSE_ONLY'), + opcode('RC', 'ACKNOWLEDGE'), + opcode('RC', 'ATOMIC_ACKNOWLEDGE'), + opcode('RC', 'COMPARE_SWAP'), + opcode('RC', 'FETCH_ADD'), + + opcode('UC', 'SEND_FIRST'), + opcode('UC', 'SEND_MIDDLE'), + opcode('UC', 'SEND_LAST'), + opcode('UC', 'SEND_LAST_WITH_IMMEDIATE'), + opcode('UC', 'SEND_ONLY'), + opcode('UC', 'SEND_ONLY_WITH_IMMEDIATE'), + opcode('UC', 'RDMA_WRITE_FIRST'), + opcode('UC', 'RDMA_WRITE_MIDDLE'), + opcode('UC', 'RDMA_WRITE_LAST'), + opcode('UC', 'RDMA_WRITE_LAST_WITH_IMMEDIATE'), + opcode('UC', 'RDMA_WRITE_ONLY'), + opcode('UC', 'RDMA_WRITE_ONLY_WITH_IMMEDIATE'), + + opcode('RD', 'SEND_FIRST'), + opcode('RD', 'SEND_MIDDLE'), + opcode('RD', 'SEND_LAST'), + opcode('RD', 'SEND_LAST_WITH_IMMEDIATE'), + opcode('RD', 'SEND_ONLY'), + opcode('RD', 'SEND_ONLY_WITH_IMMEDIATE'), + opcode('RD', 'RDMA_WRITE_FIRST'), + opcode('RD', 'RDMA_WRITE_MIDDLE'), + opcode('RD', 'RDMA_WRITE_LAST'), + opcode('RD', 'RDMA_WRITE_LAST_WITH_IMMEDIATE'), + opcode('RD', 'RDMA_WRITE_ONLY'), + opcode('RD', 'RDMA_WRITE_ONLY_WITH_IMMEDIATE'), + opcode('RD', 'RDMA_READ_REQUEST'), + opcode('RD', 'RDMA_READ_RESPONSE_FIRST'), + opcode('RD', 'RDMA_READ_RESPONSE_MIDDLE'), + opcode('RD', 'RDMA_READ_RESPONSE_LAST'), + opcode('RD', 'RDMA_READ_RESPONSE_ONLY'), + opcode('RD', 'ACKNOWLEDGE'), + opcode('RD', 'ATOMIC_ACKNOWLEDGE'), + opcode('RD', 'COMPARE_SWAP'), + opcode('RD', 'FETCH_ADD'), + + opcode('UD', 'SEND_ONLY'), + opcode('UD', 'SEND_ONLY_WITH_IMMEDIATE'), + + (CNP_OPCODE, 'CNP'), +]) + + +class BTH(Packet): + name = "BTH" + fields_desc = [ + ByteEnumField("opcode", 0, _bth_opcodes), + BitField("solicited", 0, 1), + BitField("migreq", 0, 1), + BitField("padcount", 0, 2), + BitField("version", 0, 4), + XShortField("pkey", 0xffff), + BitField("fecn", 0, 1), + BitField("becn", 0, 1), + BitField("resv6", 0, 6), + BitField("dqpn", 0, 24), + BitField("ackreq", 0, 1), + BitField("resv7", 0, 7), + BitField("psn", 0, 24), + + FCSField("icrc", None, fmt="!I")] + + @staticmethod + def pack_icrc(icrc): + # type: (int) -> bytes + return struct.pack("!I", icrc & 0xffffffff)[::-1] + + def compute_icrc(self, p): + # type: (bytes) -> bytes + udp = self.underlayer + if udp is None or not isinstance(udp, UDP): + warning("Expecting UDP underlayer to compute checksum. Got %s.", + udp and udp.name) + return self.pack_icrc(0) + ip = udp.underlayer + if isinstance(ip, IP): + # pseudo-LRH / IP / UDP / BTH / payload + pshdr = Raw(b'\xff' * 8) / ip.copy() + pshdr.chksum = 0xffff + pshdr.ttl = 0xff + pshdr.tos = 0xff + pshdr[UDP].chksum = 0xffff + pshdr[BTH].fecn = 1 + pshdr[BTH].becn = 1 + pshdr[BTH].resv6 = 0xff + bth = pshdr[BTH].self_build() + payload = raw(pshdr[BTH].payload) + # add ICRC placeholder just to get the right IP.totlen and + # UDP.length + icrc_placeholder = b'\xff\xff\xff\xff' + pshdr[UDP].payload = Raw(bth + payload + icrc_placeholder) + icrc = crc32(raw(pshdr)[:-4]) & 0xffffffff + return self.pack_icrc(icrc) + elif isinstance(ip, IPv6): + # pseudo-LRH / IPv6 / UDP / BTH / payload + pshdr = Raw(b'\xff' * 8) / ip.copy() + pshdr.hlim = 0xff + pshdr.fl = 0xfffff + pshdr.tc = 0xff + pshdr[UDP].chksum = 0xffff + pshdr[BTH].fecn = 1 + pshdr[BTH].becn = 1 + pshdr[BTH].resv6 = 0xff + bth = pshdr[BTH].self_build() + payload = raw(pshdr[BTH].payload) + # add ICRC placeholder just to get the right IPv6.plen and + # UDP.length + icrc_placeholder = b'\xff\xff\xff\xff' + pshdr[UDP].payload = Raw(bth + payload + icrc_placeholder) + icrc = crc32(raw(pshdr)[:-4]) & 0xffffffff + return self.pack_icrc(icrc) + else: + warning("The underlayer protocol %s is not supported.", + ip and ip.name) + return self.pack_icrc(0) + + # RoCE packets end with ICRC - a 32-bit CRC of the packet payload and + # pseudo-header. Add the ICRC header if it is missing and calculate its + # value. + def post_build(self, p, pay): + # type: (bytes, bytes) -> bytes + p += pay + if self.icrc is None: + p = p[:-4] + self.compute_icrc(p) + return p + + +class CNPPadding(Packet): + name = "CNPPadding" + fields_desc = [ + XLongField("reserved1", 0), + XLongField("reserved2", 0), + ] + + +def cnp(dqpn): + # type: (int) -> BTH + return BTH(opcode=CNP_OPCODE, becn=1, dqpn=dqpn) / CNPPadding() + + +class GRH(Packet): + name = "GRH" + fields_desc = [ + BitField("ipver", 6, 4), + BitField("tclass", 0, 8), + BitField("flowlabel", 6, 20), + ShortField("paylen", 0), + ByteField("nexthdr", 0), + ByteField("hoplmt", 0), + XBitField("sgid", 0, 128), + XBitField("dgid", 0, 128), + ] + + +class AETH(Packet): + name = "AETH" + fields_desc = [ + XByteField("syndrome", 0), + XBitField("msn", 0, 24), + ] + + +bind_layers(BTH, CNPPadding, opcode=CNP_OPCODE) + +bind_layers(Ether, GRH, type=0x8915) +bind_layers(GRH, BTH) +bind_layers(BTH, AETH, opcode=opcode('RC', 'ACKNOWLEDGE')[0]) +bind_layers(BTH, AETH, opcode=opcode('RD', 'ACKNOWLEDGE')[0]) +bind_layers(UDP, BTH, dport=4791) diff --git a/scapy/contrib/rpl.py b/scapy/contrib/rpl.py new file mode 100644 index 00000000000..d282c56e05c --- /dev/null +++ b/scapy/contrib/rpl.py @@ -0,0 +1,309 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2020 Rahul Jadhav + +# scapy.contrib.description = Routing Protocol for LLNs (RPL) +# scapy.contrib.status = loads + +""" +RPL +=== + +RFC 6550 - Routing Protocol for Low-Power and Lossy Networks (RPL) +draft-ietf-roll-efficient-npdao-17 - Efficient Route Invalidation + ++----------------------------------------------------------------------+ +| RPL Options : Pad1 PadN TIO RIO PIO Tgt TgtDesc DODAGConfig DAGMC ...| ++----------------------------------------------------------------------+ +| RPL Msgs : DIS DIO DAO DAOACK DCO DCOACK | ++----------------------------------------------------------------------+ +| ICMPv6 : type 155 RPL | ++----------------------------------------------------------------------+ + +""" + + +from scapy.packet import Packet, bind_layers +from scapy.fields import ByteEnumField, ByteField, IP6Field, ShortField, \ + BitField, BitEnumField, FieldLenField, StrLenField, IntField, \ + ConditionalField +from scapy.layers.inet6 import ICMPv6RPL, icmp6ndraprefs, _IP6PrefixField + + +# https://www.iana.org/assignments/rpl/rpl.xhtml#mop +RPLMOP = {0: "No Downward routes", + 1: "Non-Storing", + 2: "Storing with no multicast support", + 3: "Storing with multicast support", + 4: "P2P Route Discovery"} + + +# https://www.iana.org/assignments/rpl/rpl.xhtml#control-message-options +RPLOPTSSTR = {0: "Pad1", + 1: "PadN", + 2: "DAG Metric Container", + 3: "Routing Information", + 4: "DODAG Configuration", + 5: "RPL Target", + 6: "Transit Information", + 7: "Solicited Information", + 8: "Prefix Information Option", + 9: "Target Descriptor", + 10: "P2P Route Discovery"} + + +class _RPLGuessOption(Packet): + name = "Dummy RPL Option class" + + +class RPLOptRIO(_RPLGuessOption): + """ + Control Option: Routing Information Option (RIO) + """ + name = "Routing Information" + fields_desc = [ByteEnumField("otype", 3, RPLOPTSSTR), + FieldLenField("len", None, length_of="prefix", fmt="B", + adjust=lambda pkt, x: x + 6), + ByteField("plen", None), + BitField("res1", 0, 3), + BitEnumField("prf", 0, 2, icmp6ndraprefs), + BitField("res2", 0, 3), + IntField("rtlifetime", 0xffffffff), + _IP6PrefixField("prefix", None)] + + +class RPLOptDODAGConfig(_RPLGuessOption): + """ + Control Option: DODAG Configuration + """ + name = "DODAG Configuration" + fields_desc = [ByteEnumField("otype", 4, RPLOPTSSTR), + ByteField("len", 14), + BitField("flags", 0, 4), + BitField("A", 0, 1), + BitField("PCS", 0, 3), + ByteField("DIOIntDoubl", 20), + ByteField("DIOIntMin", 3), + ByteField("DIORedun", 10), + ShortField("MaxRankIncrease", 0), + ShortField("MinRankIncrease", 256), + ShortField("OCP", 1), + ByteField("reserved", 0), + ByteField("DefLifetime", 0xff), + ShortField("LifetimeUnit", 0xffff)] + + +class RPLOptTgt(_RPLGuessOption): + """ + Control Option: RPL Target + """ + name = "RPL Target" + fields_desc = [ByteEnumField("otype", 5, RPLOPTSSTR), + FieldLenField("len", None, length_of="prefix", fmt="B", + adjust=lambda pkt, x: x + 2), + ByteField("flags", 0), + ByteField("plen", 0), + _IP6PrefixField("prefix", None)] + + +class RPLOptTIO(_RPLGuessOption): + """ + Control Option: Transit Information Option (TIO) + """ + name = "Transit Information" + fields_desc = [ByteEnumField("otype", 6, RPLOPTSSTR), + FieldLenField("len", None, length_of="parentaddr", fmt="B", + adjust=lambda pkt, x: x + 4), + BitField("E", 0, 1), + BitField("flags", 0, 7), + ByteField("pathcontrol", 0), + ByteField("pathseq", 0), + ByteField("pathlifetime", 0xff), + _IP6PrefixField("parentaddr", None)] + + +class RPLOptSolInfo(_RPLGuessOption): + """ + Control Option: Solicited Information + """ + name = "Solicited Information" + fields_desc = [ByteEnumField("otype", 7, RPLOPTSSTR), + ByteField("len", 19), + ByteField("RPLInstanceID", 0), + BitField("V", 0, 1), + BitField("I", 0, 1), + BitField("D", 0, 1), + BitField("flags", 0, 5), + IP6Field("dodagid", "::1"), + ByteField("ver", 0)] + + +class RPLOptPIO(_RPLGuessOption): + """ + Control Option: Prefix Information Option (PIO) + """ + name = "Prefix Information" + fields_desc = [ByteEnumField("otype", 8, RPLOPTSSTR), + ByteField("len", 30), + ByteField("plen", 64), + BitField("L", 0, 1), + BitField("A", 0, 1), + BitField("R", 0, 1), + BitField("reserved1", 0, 5), + IntField("validlifetime", 0xffffffff), + IntField("preflifetime", 0xffffffff), + IntField("reserved2", 0), + IP6Field("prefix", "::1")] + + +class RPLOptTgtDesc(_RPLGuessOption): + """ + Control Option: RPL Target Descriptor + """ + name = "RPL Target Descriptor" + fields_desc = [ByteEnumField("otype", 9, RPLOPTSSTR), + ByteField("len", 4), + IntField("descriptor", 0)] + + +class RPLOptPad1(_RPLGuessOption): + """ + Control Option: Pad 1 byte + """ + name = "Pad1" + fields_desc = [ByteEnumField("otype", 0x00, RPLOPTSSTR)] + + +class RPLOptPadN(_RPLGuessOption): + """ + Control Option: Pad N bytes + """ + name = "PadN" + fields_desc = [ByteEnumField("otype", 0x01, RPLOPTSSTR), + FieldLenField("optlen", None, length_of="optdata", fmt="B"), + StrLenField("optdata", "", + length_from=lambda pkt: pkt.optlen)] + + +# https://www.iana.org/assignments/rpl/rpl.xhtml#control-message-options +RPLOPTS = {0: RPLOptPad1, + 1: RPLOptPadN, + # 2: RPLOptDAGMC, defined in rpl_metrics.py + 3: RPLOptRIO, + 4: RPLOptDODAGConfig, + 5: RPLOptTgt, + 6: RPLOptTIO, + 7: RPLOptSolInfo, + 8: RPLOptPIO, + 9: RPLOptTgtDesc} + + +# RPL Control Message Handling + + +class _RPLGuessMsgType(Packet): + name = "Dummy RPL Message class" + + def guess_payload_class(self, payload): + if isinstance(payload, str): + otype = ord(payload[0]) + else: + otype = payload[0] + return RPLOPTS.get(otype) + + +class RPLDIS(_RPLGuessMsgType, _RPLGuessOption): + """ + Control Message: DODAG Information Solicitation (DIS) + """ + name = "DODAG Information Solicitation" + fields_desc = [ByteField("flags", 0), + ByteField("reserved", 0)] + + +class RPLDIO(_RPLGuessMsgType, _RPLGuessOption): + """ + Control Message: DODAG Information Object (DIO) + """ + name = "DODAG Information Object" + fields_desc = [ByteField("RPLInstanceID", 50), + ByteField("ver", 0), + ShortField("rank", 1), + BitField("G", 1, 1), + BitField("unused1", 0, 1), + BitEnumField("mop", 1, 3, RPLMOP), + BitField("prf", 0, 3), + ByteField("dtsn", 240), + ByteField("flags", 0), + ByteField("reserved", 0), + IP6Field("dodagid", "::1")] + + +class RPLDAO(_RPLGuessMsgType, _RPLGuessOption): + """ + Control Message: Destination Advertisement Object (DAO) + """ + name = "Destination Advertisement Object" + fields_desc = [ByteField("RPLInstanceID", 50), + BitField("K", 0, 1), + BitField("D", 0, 1), + BitField("flags", 0, 6), + ByteField("reserved", 0), + ByteField("daoseq", 1), + ConditionalField(IP6Field("dodagid", None), + lambda pkt: pkt.D == 1)] + + +class RPLDAOACK(_RPLGuessMsgType, _RPLGuessOption): + """ + Control Message: Destination Advertisement Object Acknowledgement (DAOACK) + """ + name = "Destination Advertisement Object Acknowledgement" + fields_desc = [ByteField("RPLInstanceID", 50), + BitField("D", 0, 1), + BitField("reserved", 0, 7), + ByteField("daoseq", 1), + ByteField("status", 0), + ConditionalField(IP6Field("dodagid", None), + lambda pkt: pkt.D == 1)] + + +# https://datatracker.ietf.org/doc/draft-ietf-roll-efficient-npdao/ +class RPLDCO(_RPLGuessMsgType, _RPLGuessOption): + """ + Control Message: Destination Cleanup Object (DCO) + """ + name = "Destination Cleanup Object" + fields_desc = [ByteField("RPLInstanceID", 50), + BitField("K", 0, 1), + BitField("D", 0, 1), + BitField("flags", 0, 6), + ByteField("status", 0), + ByteField("dcoseq", 1), + ConditionalField(IP6Field("dodagid", None), + lambda pkt: pkt.D == 1)] + + +# https://datatracker.ietf.org/doc/draft-ietf-roll-efficient-npdao/ +class RPLDCOACK(_RPLGuessMsgType, _RPLGuessOption): + """ + Control Message: Destination Cleanup Object Acknowledgement (DCOACK) + """ + name = "Destination Cleanup Object Acknowledgement" + fields_desc = [ByteField("RPLInstanceID", 50), + BitField("D", 0, 1), + BitField("flags", 0, 7), + ByteField("dcoseq", 1), + ByteField("status", 0), + ConditionalField(IP6Field("dodagid", None), + lambda pkt: pkt.D == 1)] + + +# https://www.iana.org/assignments/rpl/rpl.xhtml#control-codes +bind_layers(ICMPv6RPL, RPLDIS, code=0) +bind_layers(ICMPv6RPL, RPLDIO, code=1) +bind_layers(ICMPv6RPL, RPLDAO, code=2) +bind_layers(ICMPv6RPL, RPLDAOACK, code=3) +bind_layers(ICMPv6RPL, RPLDCO, code=7) +bind_layers(ICMPv6RPL, RPLDCOACK, code=8) diff --git a/scapy/contrib/rpl_metrics.py b/scapy/contrib/rpl_metrics.py new file mode 100644 index 00000000000..f8e7531f81f --- /dev/null +++ b/scapy/contrib/rpl_metrics.py @@ -0,0 +1,247 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2020 Rahul Jadhav + +# scapy.contrib.description = Routing Metrics used for Path Calc in LLNs +# scapy.contrib.status = loads + +""" +RFC 6551 - Routing Metrics Used for Path Calculation in LLNs + ++----------------------------+ +| Metrics & Constraint Types | ++----------------------------+ +| DAGMC Option | ++----------------------------+ +| RPL-DIO | ++----------------------------+ +""" + +import struct +from scapy.compat import orb +from scapy.packet import Packet +from scapy.fields import ByteEnumField, ByteField, ShortField, BitField, \ + BitEnumField, FieldLenField, StrLenField, IntField +from scapy.layers.inet6 import _PhantomAutoPadField, _OptionsField +from scapy.contrib.rpl import RPLOPTSSTR, RPLOPTS + + +class _DAGMetricContainer(Packet): + name = 'Dummy DAG Metric container' + + def post_build(self, pkt, pay): + pkt += pay + tmp_len = self.len + if self.len is None: + tmp_len = len(pkt) - 2 + pkt = pkt[:1] + struct.pack("B", tmp_len) + pkt[2:] + return pkt + + +DAGMC_OBJTYPE = {1: "Node State and Attributes", + 2: "Node Energy", + 3: "Hop Count", + 4: "Link Throughput", + 5: "Link Latency", + 6: "Link Quality Level", + 7: "Link ETX", + 8: "Link Color"} + + +class DAGMCObjUnknown(Packet): + """ + Dummy unknown metric/constraint + """ + name = 'Unknown DAGMC Object Option' + fields_desc = [ByteEnumField("otype", 3, DAGMC_OBJTYPE), + FieldLenField("olen", None, length_of="odata", fmt="B"), + StrLenField("odata", "", + length_from=lambda pkt: pkt.olen)] + + @classmethod + def dispatch_hook(cls, _pkt=None, *_, **kargs): + """ + Dispatch hook for DAGMC sub-fields + """ + if _pkt: + opt_type = orb(_pkt[0]) # Option type + if opt_type in DAGMC_CLS: + return DAGMC_CLS[opt_type] + return cls + + +AGG_RTMETRIC = {0: "additive", + 1: "maximum", + 2: "minimum", + 3: "multiplicative"} # RFC 6551 + + +class DAGMCObj(Packet): + """ + Set the length field in DAG Metric Constraint Control Option + """ + name = 'Dummy DAG MC Object' + # RFC 6551 - 2.1 + fields_desc = [ByteEnumField("otype", 0, DAGMC_OBJTYPE), + BitField("resflags", 0, 5), + BitField("P", 0, 1), + BitField("C", 0, 1), + BitField("O", 0, 1), + BitField("R", 0, 1), + BitEnumField("A", 0, 3, AGG_RTMETRIC), + BitField("prec", 0, 4), + ByteField("len", None)] + + def post_build(self, pkt, pay): + pkt += pay + tmp_len = self.len + if self.len is None: + tmp_len = len(pkt) - 4 + pkt = pkt[:3] + struct.pack("B", tmp_len) + pkt[4:] + return pkt + + +class RPLDAGMCNSA(DAGMCObj): + """ + DAG Metric: Node State and Attributes + """ + name = "Node State and Attributes" + otype = 1 + # RFC 6551 - 3.1 + fields_desc = DAGMCObj.fields_desc + [ + # NSA Object Body Format + ByteField("res", 0), + BitField("flags", 0, 6), + BitField("Agg", 0, 1), # A + BitField("Overload", 0, 1), # O + ] + + +class RPLDAGMCNodeEnergy(DAGMCObj): + """ + DAG Metric: Node Energy + """ + name = "Node Energy" + otype = 2 + # RFC 6551 - 3.2 + fields_desc = DAGMCObj.fields_desc + [ + # NE Sub-Object Format + BitField("flags", 0, 4), + BitField("I", 0, 1), + BitField("T", 0, 2), + BitField("E", 0, 1), + ByteField("E_E", 0) + ] + + +class RPLDAGMCHopCount(DAGMCObj): + """ + DAG Metric: Hop Count + """ + name = "Hop Count" + otype = 3 + # RFC 6551 - 3.3 + fields_desc = DAGMCObj.fields_desc + [ + # Sub-Object Format + BitField("res", 0, 4), + BitField("flags", 0, 4), + ByteField("HopCount", 1) + ] + + +class RPLDAGMCLinkThroughput(DAGMCObj): + """ + DAG Metric: Link Throughput + """ + name = "Link Throughput" + otype = 4 + # RFC 6551 - 4.1 + fields_desc = DAGMCObj.fields_desc + [ + # Sub-Object Format + IntField("Throughput", 1) + ] + + +class RPLDAGMCLinkLatency(DAGMCObj): + """ + DAG Metric: Link Latency + """ + name = "Link Latency" + otype = 5 + # RFC 6551 - 4.2 + fields_desc = DAGMCObj.fields_desc + [ + # NE Sub-Object Format + IntField("Latency", 1) + ] + + +class RPLDAGMCLinkQualityLevel(DAGMCObj): + """ + DAG Metric: Link Quality Level (LQL) + """ + name = "Link Quality Level" + otype = 6 + # RFC 6551 - 4.3.1 + fields_desc = DAGMCObj.fields_desc + [ + # Sub-Object Format + ByteField("res", 0), + BitField("val", 0, 3), + BitField("counter", 0, 5) + ] + + +class RPLDAGMCLinkETX(DAGMCObj): + """ + DAG Metric: Link ETX + """ + name = "Link ETX" + otype = 7 + # RFC 6551 - 4.3.2 + fields_desc = DAGMCObj.fields_desc + [ + # Sub-Object Format + ShortField("ETX", 1) + ] + + +# Note: Wireshark shows warning decoding LinkColor. +# This seems to be wireshark issue! +class RPLDAGMCLinkColor(DAGMCObj): + """ + DAG Metric: Link Color + """ + name = "Link Color" + otype = 8 + # RFC 6551 - 4.4.1 + fields_desc = DAGMCObj.fields_desc + [ + # Sub-Object Format + ByteField("res", 0), + BitField("color", 1, 10), + BitField("counter", 1, 6) + ] + + +DAGMC_CLS = {1: RPLDAGMCNSA, + 2: RPLDAGMCNodeEnergy, + 3: RPLDAGMCHopCount, + 4: RPLDAGMCLinkThroughput, + 5: RPLDAGMCLinkLatency, + 6: RPLDAGMCLinkQualityLevel, + 7: RPLDAGMCLinkETX, + 8: RPLDAGMCLinkColor} + + +class RPLOptDAGMC(_DAGMetricContainer): + """ + Control Option: DAG Metric Container + """ + name = "DAG Metric Container" + fields_desc = [ByteEnumField("otype", 2, RPLOPTSSTR), + ByteField("len", None), + _PhantomAutoPadField("autopad", 0), + _OptionsField("options", [], DAGMCObjUnknown, 8, + length_from=lambda pkt: 8 * pkt.len)] + + +# https://www.iana.org/assignments/rpl/rpl.xhtml#control-message-options +RPLOPTS.update({2: RPLOptDAGMC}) diff --git a/scapy/contrib/rsvp.py b/scapy/contrib/rsvp.py index a720826bde3..e68ad9631f5 100644 --- a/scapy/contrib/rsvp.py +++ b/scapy/contrib/rsvp.py @@ -1,18 +1,10 @@ -# RSVP layer - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information + +""" +RSVP layer +""" # scapy.contrib.description = Resource Reservation Protocol (RSVP) # scapy.contrib.status = loads @@ -137,7 +129,7 @@ class RSVP_Object(Packet): name = "RSVP_Object" fields_desc = [ShortField("Length", 4), ByteEnumField("Class", 0x01, rsvptypes), - ByteField("C-Type", 1)] + ByteField("C_Type", 1)] def guess_payload_class(self, payload): if self.Class == 0x03: diff --git a/scapy/contrib/rtcp.py b/scapy/contrib/rtcp.py new file mode 100644 index 00000000000..a41673870dd --- /dev/null +++ b/scapy/contrib/rtcp.py @@ -0,0 +1,148 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Pavel Oborin + +# RFC 3550 +# scapy.contrib.description = Real-Time Transport Control Protocol +# scapy.contrib.status = loads + +""" +RTCP (rfc 3550) + +Use bind_layers(UDP, RTCP, dport=...) to start using it +""" + +import struct + +from scapy.packet import Packet +from scapy.fields import ( + BitField, + BitFieldLenField, + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + IntField, + LenField, + LongField, + PacketField, + PacketListField, + StrLenField, + X3BytesField, +) + + +_rtcp_packet_types = { + 200: 'Sender report', + 201: 'Receiver report', + 202: 'Source description', + 203: 'BYE', + 204: 'APP' +} + + +class SenderInfo(Packet): + name = "Sender info" + fields_desc = [ + LongField('ntp_timestamp', None), + IntField('rtp_timestamp', None), + IntField('sender_packet_count', None), + IntField('sender_octet_count', None) + ] + + def extract_padding(self, p): + return "", p + + +class ReceptionReport(Packet): + name = "Reception report" + fields_desc = [ + IntField('sourcesync', None), + ByteField('fraction_lost', None), + X3BytesField('cumulative_lost', None), + IntField('highest_seqnum_recv', None), + IntField('interarrival_jitter', None), + IntField('last_SR_timestamp', None), + IntField('delay_since_last_SR', None) + ] + + def extract_padding(self, p): + return "", p + + +_sdes_chunk_types = { + 0: "END", + 1: "CNAME", + 2: "NAME", + 3: "EMAIL", + 4: "PHONE", + 5: "LOC", + 6: "TOOL", + 7: "NOTE", + 8: "PRIV" +} + + +class SDESItem(Packet): + name = "SDES item" + fields_desc = [ + ByteEnumField('chunk_type', None, _sdes_chunk_types), + FieldLenField('length', None, fmt='!b', length_of='value'), + StrLenField('value', None, length_from=lambda pkt: pkt.length) + ] + + def extract_padding(self, p): + return "", p + + +class SDESChunk(Packet): + name = "SDES chunk" + fields_desc = [ + IntField('sourcesync', None), + PacketListField( + 'items', None, + next_cls_cb=( + lambda x, y, p, z: None if (p and p.chunk_type == 0) else SDESItem + ) + ) + ] + + +class RTCP(Packet): + name = "RTCP" + + fields_desc = [ + # HEADER + BitField('version', 2, 2), + BitField('padding', 0, 1), + BitFieldLenField('count', 0, 5, count_of='report_blocks'), + ByteEnumField('packet_type', 0, _rtcp_packet_types), + LenField('length', None, fmt='!h'), + # SR/RR + ConditionalField( + IntField('sourcesync', 0), + lambda pkt: pkt.packet_type in (200, 201) + ), + ConditionalField( + PacketField('sender_info', SenderInfo(), SenderInfo), + lambda pkt: pkt.packet_type == 200 + ), + ConditionalField( + PacketListField('report_blocks', None, pkt_cls=ReceptionReport, + count_from=lambda pkt: pkt.count), + lambda pkt: pkt.packet_type in (200, 201) + ), + # SDES + ConditionalField( + PacketListField('sdes_chunks', None, pkt_cls=SDESChunk, + count_from=lambda pkt: pkt.count), + lambda pkt: pkt.packet_type == 202 + ), + ] + + def post_build(self, pkt, pay): + pkt += pay + if self.length is None: + pkt = pkt[:2] + struct.pack("!h", len(pkt) // 4 - 1) + pkt[4:] + return pkt diff --git a/scapy/contrib/rtps/__init__.py b/scapy/contrib/rtps/__init__.py new file mode 100644 index 00000000000..6b82983e592 --- /dev/null +++ b/scapy/contrib/rtps/__init__.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2021 Trend Micro Incorporated + +""" +Real-Time Publish-Subscribe Protocol (RTPS) dissection +""" + +# scapy.contrib.description = Real-Time Publish-Subscribe Protocol (RTPS) +# scapy.contrib.status = loads +# scapy.contrib.name = rtps + +from scapy.contrib.rtps.rtps import * # noqa F403,F401 +from scapy.contrib.rtps.pid_types import * # noqa F403,F401 diff --git a/scapy/contrib/rtps/common_types.py b/scapy/contrib/rtps/common_types.py new file mode 100644 index 00000000000..071c0903ed5 --- /dev/null +++ b/scapy/contrib/rtps/common_types.py @@ -0,0 +1,331 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2021 Trend Micro Incorporated +# Copyright (C) 2021 Alias Robotics S.L. + +""" +Real-Time Publish-Subscribe Protocol (RTPS) dissection +""" + +# scapy.contrib.description = RTPS common types +# scapy.contrib.status = library + +import struct + +from scapy.fields import ( + _FieldContainer, + BitField, + ConditionalField, + EnumField, + ByteField, + IntField, + IPField, + LEIntField, + PacketField, + PacketListField, + ReversePadField, + StrField, + StrLenField, + UUIDField, + XIntField, + XStrFixedLenField, +) +from scapy.packet import Packet + +FORMAT_LE = "<" +FORMAT_BE = ">" +STR_MAX_LEN = 8192 +DEFAULT_ENDIANNESS = FORMAT_LE + + +def is_le(pkt): + if hasattr(pkt, "submessageFlags"): + end = pkt.submessageFlags & 0b000000001 == 0b000000001 + return end + + return False + + +def e_flags(pkt): + if is_le(pkt): + return FORMAT_LE + else: + return FORMAT_BE + + +class EField(_FieldContainer): + """ + A field that manages endianness of a nested field passed to the constructor + """ + + __slots__ = ["fld", "endianness", "endianness_from"] + + def __init__(self, fld, endianness=None, endianness_from=None): + self.fld = fld + self.endianness = endianness + self.endianness_from = endianness_from + + def set_endianness(self, pkt): + if getattr(pkt, "endianness", None) is not None: + self.endianness = pkt.endianness + elif self.endianness_from is not None: + self.endianness = self.endianness_from(pkt) + + if isinstance(self.endianness, str) and self.endianness: + if isinstance(self.fld, UUIDField): + self.fld.uuid_fmt = (UUIDField.FORMAT_LE if + self.endianness == '<' + else UUIDField.FORMAT_BE) + elif hasattr(self.fld, "fmt"): + if len(self.fld.fmt) == 1: # if it's only "I" + _end = self.fld.fmt[0] + else: # if it's ". - +# See https://scapy.net/ for more information # Copyright (C) 2018 Francois Contat -# Based on RTR RFC 6810 https://tools.ietf.org/html/rfc6810 for version 0 -# Based on RTR RFC 8210 https://tools.ietf.org/html/rfc8210 for version 1 +""" +RTR + +Based on RTR RFC 6810 https://tools.ietf.org/html/rfc6810 for version 0 +Based on RTR RFC 8210 https://tools.ietf.org/html/rfc8210 for version 1 +""" # scapy.contrib.description = The RPKI to Router Protocol # scapy.contrib.status = loads diff --git a/scapy/contrib/rtsp.py b/scapy/contrib/rtsp.py new file mode 100644 index 00000000000..ddffce4201d --- /dev/null +++ b/scapy/contrib/rtsp.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Real Time Streaming Protocol (RTSP) +RFC 2326 +""" + +# scapy.contrib.description = Real Time Streaming Protocol (RTSP) +# scapy.contrib.status = loads + +import re + +from scapy.packet import ( + bind_bottom_up, + bind_layers, +) +from scapy.layers.http import ( + HTTP, + _HTTPContent, + _HTTPHeaderField, + _generate_headers, + _dissect_headers, +) +from scapy.layers.inet import TCP + + +RTSP_REQ_HEADERS = [ + "Accept", + "Accept-Encoding", + "Accept-Language", + "Authorization", + "From", + "If-Modified-Since", + "Range", + "Referer", + "User-Agent", +] +RTSP_RESP_HEADERS = [ + "Location", + "Proxy-Authenticate", + "Public", + "Retry-After", + "Server", + "Vary", + "WWW-Authenticate", +] + + +class RTSPRequest(_HTTPContent): + name = "RTSP Request" + fields_desc = ( + [ + # First line + _HTTPHeaderField("Method", "DESCRIBE"), + _HTTPHeaderField("Request_Uri", "*"), + _HTTPHeaderField("Version", "RTSP/1.0"), + # Headers + ] + + ( + _generate_headers( + RTSP_REQ_HEADERS, + ) + ) + + [ + _HTTPHeaderField("Unknown-Headers", None), + ] + ) + + def do_dissect(self, s): + first_line, body = _dissect_headers(self, s) + try: + method, uri, version = re.split(rb"\s+", first_line, maxsplit=2) + self.setfieldval("Method", method) + self.setfieldval("Request_Uri", uri) + self.setfieldval("Version", version) + except ValueError: + pass + if body: + self.raw_packet_cache = s[: -len(body)] + else: + self.raw_packet_cache = s + return body + + def mysummary(self): + return self.sprintf( + "%RTSPRequest.Method% %RTSPRequest.Request_Uri% " "%RTSPRequest.Version%" + ) + + +class RTSPResponse(_HTTPContent): + name = "RTSP Response" + fields_desc = ( + [ + # First line + _HTTPHeaderField("Version", "RTSP/1.1"), + _HTTPHeaderField("Status_Code", "200"), + _HTTPHeaderField("Reason_Phrase", "OK"), + # Headers + ] + + ( + _generate_headers( + RTSP_RESP_HEADERS, + ) + ) + + [ + _HTTPHeaderField("Unknown-Headers", None), + ] + ) + + def answers(self, other): + return RTSPRequest in other + + def do_dissect(self, s): + first_line, body = _dissect_headers(self, s) + try: + Version, Status, Reason = re.split(rb"\s+", first_line, maxsplit=2) + self.setfieldval("Version", Version) + self.setfieldval("Status_Code", Status) + self.setfieldval("Reason_Phrase", Reason) + except ValueError: + pass + if body: + self.raw_packet_cache = s[: -len(body)] + else: + self.raw_packet_cache = s + return body + + def mysummary(self): + return self.sprintf( + "%RTSPResponse.Version% %RTSPResponse.Status_Code% " + "%RTSPResponse.Reason_Phrase%" + ) + + +class RTSP(HTTP): + name = "RTSP" + clsreq = RTSPRequest + clsresp = RTSPResponse + hdr = b"RTSP" + reqmethods = b"|".join( + [ + b"DESCRIBE", + b"ANNOUNCE", + b"GET_PARAMETER", + b"OPTIONS", + b"PAUSE", + b"PLAY", + b"RECORD", + b"REDIRECT", + b"SETUP", + b"SET_PARAMETER", + b"TEARDOWN", + ] + ) + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + return cls + + +bind_bottom_up(TCP, RTSP, sport=554) +bind_bottom_up(TCP, RTSP, dport=554) +bind_layers(TCP, RTSP, dport=554, sport=554) diff --git a/scapy/contrib/scada/__init__.py b/scapy/contrib/scada/__init__.py index ded0ace44f2..f67d3dfdd7c 100644 --- a/scapy/contrib/scada/__init__.py +++ b/scapy/contrib/scada/__init__.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Thomas Tannhaeuser -# This program is published under a GPLv2 license -# + # scapy.contrib.status = skip diff --git a/scapy/contrib/scada/iec104/__init__.py b/scapy/contrib/scada/iec104/__init__.py index 896a9f43a1d..5184b16fdb3 100644 --- a/scapy/contrib/scada/iec104/__init__.py +++ b/scapy/contrib/scada/iec104/__init__.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Thomas Tannhaeuser -# This program is published under a GPLv2 license -# + # scapy.contrib.description = IEC-60870-5-104 APCI / APDU layer definitions # scapy.contrib.status = loads diff --git a/scapy/contrib/scada/iec104/iec104_fields.py b/scapy/contrib/scada/iec104/iec104_fields.py index e1f48db6bfc..59208b93a75 100644 --- a/scapy/contrib/scada/iec104/iec104_fields.py +++ b/scapy/contrib/scada/iec104/iec104_fields.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Thomas Tannhaeuser -# This program is published under a GPLv2 license # scapy.contrib.status = skip diff --git a/scapy/contrib/scada/iec104/iec104_information_elements.py b/scapy/contrib/scada/iec104/iec104_information_elements.py index 39327ad340f..f82813d1720 100644 --- a/scapy/contrib/scada/iec104/iec104_information_elements.py +++ b/scapy/contrib/scada/iec104/iec104_information_elements.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Thomas Tannhaeuser -# This program is published under a GPLv2 license # scapy.contrib.status = skip @@ -29,9 +29,16 @@ from scapy.contrib.scada.iec104.iec104_fields import \ IEC60870_5_4_NormalizedFixPoint, IEC104SignedSevenBitValue, \ LESignedShortField, LEIEEEFloatField -from scapy.fields import BitEnumField, ByteEnumField, ByteField, \ - ThreeBytesField, \ - BitField, LEShortField, LESignedIntField +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + LEShortField, + LESignedIntField, + MayEnd, + ThreeBytesField, +) def _generate_attributes_and_dicts(cls): @@ -219,7 +226,7 @@ class IEC104_IE_QDP(IEC104_IE_CommonQualityFlags): # blocked BitEnumField('ei', 0, 1, IEC104_IE_CommonQualityFlags.EI_FLAGS), # blocked - BitField('reserved', 0, 3) + BitField('reserved_qdp', 0, 3) ] @@ -489,7 +496,7 @@ class IEC104_IE_QOC: } informantion_element_fields = [ - BitEnumField('s/e', 0, 1, SE_FLAGS), + BitEnumField('s_or_e', 0, 1, SE_FLAGS), BitEnumField('qu', 0, 5, QU_FLAGS) ] @@ -605,7 +612,7 @@ class IEC104_IE_CP56TIME2A(IEC104_IE_CommonQualityFlags): informantion_element_fields = [ LEShortField('sec_milli', 0), - BitEnumField('iv', 0, 1, IEC104_IE_CommonQualityFlags.IV_FLAGS), + BitEnumField('iv_time', 0, 1, IEC104_IE_CommonQualityFlags.IV_FLAGS), BitEnumField('gen', 0, 1, GEN_FLAGS), # only valid in monitor direction ToDo: special treatment needed? BitField('minutes', 0, 6), @@ -613,7 +620,7 @@ class IEC104_IE_CP56TIME2A(IEC104_IE_CommonQualityFlags): BitField('reserved_2', 0, 2), BitField('hours', 0, 5), BitEnumField('weekday', 0, 3, WEEK_DAY_FLAGS), - BitField('day-of-month', 0, 5), + MayEnd(BitField('day_of_month', 0, 5)), BitField('reserved_3', 0, 4), BitField('month', 0, 4), BitField('reserved_4', 0, 1), @@ -630,7 +637,7 @@ class IEC104_IE_CP56TIME2A_START_TIME(IEC104_IE_CP56TIME2A): informantion_element_fields = [ LEShortField('start_sec_milli', 0), BitEnumField('start_iv', 0, 1, IEC104_IE_CommonQualityFlags.IV_FLAGS), - BitEnumField('start_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS), + MayEnd(BitEnumField('start_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS)), # only valid in monitor direction ToDo: special treatment needed? BitField('start_minutes', 0, 6), BitEnumField('start_su', 0, 1, IEC104_IE_CP56TIME2A.SU_FLAGS), @@ -638,7 +645,7 @@ class IEC104_IE_CP56TIME2A_START_TIME(IEC104_IE_CP56TIME2A): BitField('start_hours', 0, 5), BitEnumField('start_weekday', 0, 3, IEC104_IE_CP56TIME2A.WEEK_DAY_FLAGS), - BitField('start_day-of-month', 0, 5), + BitField('start_day_of_month', 0, 5), BitField('start_reserved_3', 0, 4), BitField('start_month', 0, 4), BitField('start_reserved_4', 0, 1), @@ -655,7 +662,7 @@ class IEC104_IE_CP56TIME2A_STOP_TIME(IEC104_IE_CP56TIME2A): informantion_element_fields = [ LEShortField('stop_sec_milli', 0), BitEnumField('stop_iv', 0, 1, IEC104_IE_CommonQualityFlags.IV_FLAGS), - BitEnumField('stop_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS), + MayEnd(BitEnumField('stop_gen', 0, 1, IEC104_IE_CP56TIME2A.GEN_FLAGS)), # only valid in monitor direction ToDo: special treatment needed? BitField('stop_minutes', 0, 6), BitEnumField('stop_su', 0, 1, IEC104_IE_CP56TIME2A.SU_FLAGS), @@ -663,7 +670,7 @@ class IEC104_IE_CP56TIME2A_STOP_TIME(IEC104_IE_CP56TIME2A): BitField('stop_hours', 0, 5), BitEnumField('stop_weekday', 0, 3, IEC104_IE_CP56TIME2A.WEEK_DAY_FLAGS), - BitField('stop_day-of-month', 0, 5), + BitField('stop_day_of_month', 0, 5), BitField('stop_reserved_3', 0, 4), BitField('stop_month', 0, 4), BitField('stop_reserved_4', 0, 1), @@ -1327,7 +1334,7 @@ class IEC104_IE_SOF: informantion_element_fields = [ BitEnumField('fa', 0, 1, FA_FLAGS), - BitEnumField('for', 0, 1, FOR_FLAGS), + BitEnumField('for_', 0, 1, FOR_FLAGS), BitEnumField('lfd', 0, 1, LFD_FLAGS), BitEnumField('status', 0, 5, STATUS_FLAGS) ] diff --git a/scapy/contrib/scada/iec104/iec104_information_objects.py b/scapy/contrib/scada/iec104/iec104_information_objects.py index 23546121e35..700029ab29c 100644 --- a/scapy/contrib/scada/iec104/iec104_information_objects.py +++ b/scapy/contrib/scada/iec104/iec104_information_objects.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Thomas Tannhaeuser -# This program is published under a GPLv2 license -# + # scapy.contrib.description = IEC-60870-5-104 ASDU layers / IO definitions # scapy.contrib.status = loads diff --git a/scapy/contrib/scada/pcom.py b/scapy/contrib/scada/pcom.py index e5e17cfbf92..98603260588 100755 --- a/scapy/contrib/scada/pcom.py +++ b/scapy/contrib/scada/pcom.py @@ -1,28 +1,19 @@ -# coding: utf8 - +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information +# Copyright (C) 2019 Luis Rosa # scapy.contrib.description = PCOM Protocol # scapy.contrib.status = loads -# Copyright (C) 2019 Luis Rosa -# -# PCOM is a protocol to communicate with Unitronics PLCs either by serial -# or TCP. Two modes are available, ASCII and Binary. -# -# See https://unitronicsplc.com/Download/SoftwareUtilities/Unitronics%20PCOM%20Protocol.pdf # noqa +""" +PCOM + +PCOM is a protocol to communicate with Unitronics PLCs either by serial +or TCP. Two modes are available, ASCII and Binary. + +https://unitronicsplc.com/Download/SoftwareUtilities/Unitronics%20PCOM%20Protocol.pdf +""" import struct @@ -30,7 +21,7 @@ from scapy.layers.inet import TCP from scapy.fields import XShortField, ByteEnumField, XByteField, \ StrFixedLenField, StrLenField, LEShortField, \ - LEFieldLenField, LEX3BytesField, XLEShortField + LEFieldLenField, XLE3BytesField, XLEShortField from scapy.volatile import RandShort from scapy.compat import bytes_encode, orb @@ -92,7 +83,7 @@ class PCOM(Packet): def post_build(self, pkt, pay): if self.len is None and pay: - pkt = pkt[:4] + struct.pack("H", len(pay)) + pkt = pkt[:4] + struct.pack(". +# See https://scapy.net/ for more information +# Copyright 2012 (C) The MITRE Corporation """ - Copyright 2012, The MITRE Corporation:: - - NOTICE +.. centered:: + NOTICE This software/technical data was produced for the U.S. Government under Prime Contract No. NASA-03001 and JPL Contract No. 1295026 - and is subject to FAR 52.227-14 (6/87) Rights in Data General, - and Article GP-51, Rights in Data General, respectively. - This software is publicly released under MITRE case #12-3054 + and is subject to FAR 52.227-14 (6/87) Rights in Data General, + and Article GP-51, Rights in Data General, respectively. + This software is publicly released under MITRE case #12-3054 """ # scapy.contrib.description = Self-Delimiting Numeric Values (SDNV) @@ -64,7 +54,7 @@ def encode(self, number): temp.append(thisByte) foo = temp + foo - return(foo) + return foo def decode(self, ba, offset): number = 0 @@ -81,7 +71,7 @@ def decode(self, ba, offset): numBytes += 1 if (number > self.maxValue): raise SDNVValueError(self.maxValue) - return(number, numBytes) + return number, numBytes SDNVUtil = SDNV() diff --git a/scapy/contrib/sebek.py b/scapy/contrib/sebek.py index 65543c3c391..fa467174a1a 100644 --- a/scapy/contrib/sebek.py +++ b/scapy/contrib/sebek.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Sebek: kernel module for data collection on honeypots. diff --git a/scapy/contrib/send.py b/scapy/contrib/send.py index 6a6474890c4..0eec1052469 100644 --- a/scapy/contrib/send.py +++ b/scapy/contrib/send.py @@ -1,25 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # Copyright (C) 2009 Adline Stephane -# Copyright 2018 Gabriel Potter +# Copyright 2018 Gabriel Potter + +""" +Secure Neighbor Discovery (SEND) - RFC3971 +""" -# Partial support of RFC3971 # scapy.contrib.description = Secure Neighbor Discovery (SEND) (ICMPv6) # scapy.contrib.status = loads -from __future__ import absolute_import from scapy.packet import Packet from scapy.fields import BitField, ByteField, FieldLenField, PacketField, \ diff --git a/scapy/contrib/skinny.py b/scapy/contrib/skinny.py index c12cb94ed58..33389ebaa4a 100644 --- a/scapy/contrib/skinny.py +++ b/scapy/contrib/skinny.py @@ -1,27 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2006 Nicolas Bareil eads.net> +# EADS/CRC security team + # scapy.contrib.description = Skinny Call Control Protocol (SCCP) # scapy.contrib.status = loads +""" +Skinny Call Control Protocol (SCCP) extension +""" -############################################################################# -# # -# scapy-skinny.py --- Skinny Call Control Protocol (SCCP) extension # -# # -# Copyright (C) 2006 Nicolas Bareil # -# EADS/CRC security team # -# # -# This file is part of Scapy # -# Scapy is free software: you can redistribute it and/or modify # -# under the terms of the GNU General Public License version 2 as # -# published by the Free Software Foundation; version 2. # -# # -# This program is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -# # -############################################################################# - -from __future__ import absolute_import import time import struct @@ -29,7 +18,6 @@ from scapy.fields import FlagsField, IPField, LEIntEnumField, LEIntField, \ StrFixedLenField from scapy.layers.inet import TCP -from scapy.modules.six.moves import range from scapy.volatile import RandShort from scapy.config import conf diff --git a/scapy/contrib/slowprot.py b/scapy/contrib/slowprot.py new file mode 100644 index 00000000000..4a1785871fc --- /dev/null +++ b/scapy/contrib/slowprot.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = Slow Protocol +# scapy.contrib.status = loads + +from scapy.packet import Packet, bind_layers +from scapy.fields import ByteEnumField +from scapy.layers.l2 import Ether +from scapy.data import ETHER_TYPES + + +ETHER_TYPES[0x8809] = 'SlowProtocol' +SLOW_SUB_TYPES = { + 'Unused': 0, + 'LACP': 1, + 'Marker Protocol': 2, + 'OAM': 3, + 'OSSP': 10, +} + + +class SlowProtocol(Packet): + name = "SlowProtocol" + fields_desc = [ByteEnumField("subtype", 0, SLOW_SUB_TYPES)] + + +bind_layers(Ether, SlowProtocol, type=0x8809, dst='01:80:c2:00:00:02') diff --git a/scapy/contrib/socks.py b/scapy/contrib/socks.py index d4ec6b23b18..983e23f47f4 100644 --- a/scapy/contrib/socks.py +++ b/scapy/contrib/socks.py @@ -1,7 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information # scapy.contrib.description = Socket Secure (SOCKS) # scapy.contrib.status = loads @@ -17,8 +16,15 @@ from scapy.layers.dns import DNSStrField from scapy.layers.inet import TCP, UDP from scapy.layers.inet6 import IP6Field -from scapy.fields import ByteField, ByteEnumField, ShortField, IPField, \ - StrField, MultipleTypeField +from scapy.fields import ( + ByteEnumField, + ByteField, + IPField, + MultipleTypeField, + ShortField, + StrField, + StrNullField, +) from scapy.packet import Packet, bind_layers, bind_bottom_up # TODO: support the 3 different authentication exchange procedures for SOCKS5 # noqa: E501 @@ -87,8 +93,7 @@ class SOCKS4Request(Packet): ByteEnumField("cd", 1, _socks4_cd_request), ShortField("dstport", 80), IPField("dst", "0.0.0.0"), - StrField("userid", ""), - ByteField("null", 0), + StrNullField("userid", ""), ] @@ -105,7 +110,7 @@ class SOCKS4Reply(Packet): overload_fields = {SOCKS: {"vn": 0x0}} fields_desc = [ ByteEnumField("cd", 90, _socks4_cd_reply), - ] + SOCKS4Request.fields_desc[1:-2] # Re-use dstport, dst and userid + ] + SOCKS4Request.fields_desc[1:-2] # Reuse dstport, dst and userid # SOCKS v5 - TCP @@ -169,7 +174,7 @@ class SOCKS5UDP(Packet): fields_desc = [ ShortField("res", 0), ByteField("frag", 0), - ] + SOCKS5Request.fields_desc[2:] # Re-use the atyp, addr and port fields + ] + SOCKS5Request.fields_desc[2:] # Reuse the atyp, addr and port fields def guess_payload_class(self, s): if self.port == 0: diff --git a/scapy/contrib/spbm.py b/scapy/contrib/spbm.py deleted file mode 100644 index 119d757f134..00000000000 --- a/scapy/contrib/spbm.py +++ /dev/null @@ -1,60 +0,0 @@ -# This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - -# IEEE 802.1aq - Shorest Path Bridging Mac-in-mac (SPBM): -# Ethernet based link state protocol that enables Layer 2 Unicast, Layer 2 Multicast, Layer 3 Unicast, and Layer 3 Multicast virtualized services # noqa: E501 -# https://en.wikipedia.org/wiki/IEEE_802.1aq -# Modeled after the scapy VXLAN contribution - -# scapy.contrib.description = Shorest Path Bridging Mac-in-mac (SBPM) -# scapy.contrib.status = loads - -""" - Example SPB Frame Creation - - Note the outer Dot1Q Ethertype marking (0x88e7) - - backboneEther = Ether(dst='00:bb:00:00:90:00', src='00:bb:00:00:40:00', type=0x8100) # noqa: E501 - backboneDot1Q = Dot1Q(vlan=4051,type=0x88e7) - backboneServiceID = SPBM(prio=1,isid=20011) - customerEther = Ether(dst='00:1b:4f:5e:ca:00',src='00:00:00:00:00:01',type=0x8100) # noqa: E501 - customerDot1Q = Dot1Q(prio=1,vlan=11,type=0x0800) - customerIP = IP(src='10.100.11.10',dst='10.100.12.10',id=0x0629,len=106) # noqa: E501 - customerUDP = UDP(sport=1024,dport=1025,chksum=0,len=86) - - spb_example = backboneEther/backboneDot1Q/backboneServiceID/customerEther/customerDot1Q/customerIP/customerUDP/"Payload" # noqa: E501 -""" - -from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, ThreeBytesField -from scapy.layers.l2 import Ether, Dot1Q, Dot1AD - - -class SPBM(Packet): - name = "SPBM" - fields_desc = [BitField("prio", 0, 3), - BitField("dei", 0, 1), - BitField("nca", 0, 1), - BitField("res1", 0, 1), - BitField("res2", 0, 2), - ThreeBytesField("isid", 0)] - - def mysummary(self): - return self.sprintf("SPBM (isid=%SPBM.isid%") - - -bind_layers(Ether, SPBM, type=0x88e7) -bind_layers(Dot1Q, SPBM, type=0x88e7) -bind_layers(Dot1AD, SPBM, type=0x88e7) -bind_layers(SPBM, Ether) diff --git a/scapy/contrib/stamp.py b/scapy/contrib/stamp.py new file mode 100644 index 00000000000..e2c35389921 --- /dev/null +++ b/scapy/contrib/stamp.py @@ -0,0 +1,304 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Carmine Scarpitta + +# scapy.contrib.description = Simple Two-Way Active Measurement Protocol (STAMP) +# scapy.contrib.status = loads + +""" +STAMP (Simple Two-Way Active Measurement Protocol) - RFC 8762. + +References: + * `Simple Two-Way Active Measurement Protocol [RFC 8762] + `_ + * `Simple Two-Way Active Measurement Protocol Optional Extensions [RFC 8972] + `_ +""" + +from scapy import config +from scapy.base_classes import Packet_metaclass +from scapy.layers.inet import UDP +from scapy.layers.ntp import TimeStampField +from scapy.packet import Packet, bind_layers +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + FlagsField, + IntField, + MultipleTypeField, + NBytesField, + PacketField, + PacketListField, + ShortField, + StrLenField, + UTCTimeField +) + + +_sync_types = { + 0: 'No External Synchronization for the Time Source', + 1: 'Clock Synchronized to UTC using an External Source' +} + +_timestamp_types = { + 0: 'NTP 64-bit Timestamp Format', + 1: 'PTPv2 Truncated Timestamp Format' +} + +_stamp_tlvs = { + +} + + +class ErrorEstimate(Packet): + """ + The Error Estimate specifies the estimate of the error and + synchronization. The format of the Error Estimate field + (defined in Section 4.1.2 of `RFC 4656 + `_) is reported below:: + + 0 1 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |S|Z| Scale | Multiplier | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + ``S`` is interpreted as follows: + + +-------+-------------------------------------------------------+ + | Value | Description | + +-------+-------------------------------------------------------+ + | 0 | there is no notion of external synchronization for | + | | the time source | + +-------+-------------------------------------------------------+ + | 1 | the party generating the timestamp has a clock that | + | | is synchronized to UTC using an external source | + +-------+-------------------------------------------------------+ + + ``Z`` is interpreted as follows (defined in Section 2.3 of `RFC 8186 + `_): + + +-------+---------------------------------------+ + | Value | Description | + +-------+---------------------------------------+ + | 0 | NTP 64-bit format of a timestamp | + +-------+---------------------------------------+ + | 1 | PTPv2 truncated format of a timestamp | + +-------+---------------------------------------+ + + ``Scale`` and ``Multiplier`` are linked by the following relationship:: + + ErrorEstimate = Multiplier*2^(-32)*2^Scale (in seconds) + + + References: + * `A One-way Active Measurement Protocol (OWAMP) [RFC 4656] + `_ + * `Support of the IEEE 1588 Timestamp Format in a Two-Way Active + Measurement Protocol (TWAMP) [RFC 8186] + `_ + """ + + name = 'Error Estimate' + fields_desc = [ + BitEnumField('S', 0, 1, _sync_types), + BitEnumField('Z', 0, 1, _timestamp_types), + BitField('scale', 0, 6), + ByteField('multiplier', 1), + ] + + def guess_payload_class(self, payload): + # type: (str) -> Packet_metaclass + # Trick to tell scapy that the remaining bytes of the currently + # dissected string is not a payload of this packet but of some other + # underlayer packet + return config.conf.padding_layer + + +class STAMPTestTLV(Packet): + """ + The STAMP Test TLV defined in Section 4 of [RFC 8972] provides a flexible + extension mechanism for optional informational elements. + + The TLV Format in a STAMP Test packet is reported below:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |STAMP TLV Flags| Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ~ Value ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + +-------+---------+-------------------------------------------------+ + | Field | Description | + +-----------------+-------------------------------------------------+ + | STAMP TLV Flags | 8-bit field; for the details about the STAMP | + | | TLV Flags Format, see RFC 8972 | + +-----------------+-------------------------------------------------+ + | Type | characterizes the interpretation of the Value | + | | field | + +-----------------+-------------------------------------------------+ + | Length | the length of the Value field in octets | + +-----------------+-------------------------------------------------+ + | Value | interpreted according to the value of the Type | + | | field | + +-----------------+-------------------------------------------------+ + + + References: + * `Simple Two-Way Active Measurement Protocol Optional Extensions + [RFC 8972] `_ + """ + + name = 'STAMP Test Packet - Generic TLV' + fields_desc = [ + FlagsField('flags', 0, 8, "UMIRRRRR"), + ByteEnumField('type', None, _stamp_tlvs), + ShortField('len', 0), + StrLenField('value', '', length_from=lambda pkt: pkt.len), + ] + + def extract_padding(self, p): + return b"", p + + registered_stamp_tlv = {} + + @classmethod + def register_variant(cls): + cls.registered_stamp_tlv[cls.type.default] = cls + + @classmethod + def dispatch_hook(cls, pkt=None, *args, **kargs): + if pkt: + tmp_type = ord(pkt[1:2]) + return cls.registered_stamp_tlv.get(tmp_type, cls) + return cls + + +class STAMPSessionSenderTestUnauthenticated(Packet): + """ + Extended STAMP Session-Sender Test Packet in Unauthenticated Mode. + + The format (defined in Section 3 of `RFC 8972 + `_) is shown below:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Timestamp | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Error Estimate | SSID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | | + | MBZ (28 octets) | + | | + | | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ~ TLVs ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + References: + * `Simple Two-Way Active Measurement Protocol Optional Extensions + [RFC 8972] `_ + """ + name = 'STAMP Session-Sender Test' + fields_desc = [ + IntField('seq', 0), + MultipleTypeField( + [ + (TimeStampField('ts', 0), + lambda pkt:pkt.err_estimate.Z == 0) + ], + UTCTimeField('ts', 0, fmt='Q') + ), + PacketField('err_estimate', ErrorEstimate(), ErrorEstimate), + ShortField('ssid', 1), + NBytesField('mbz', 0, 28), # 28 bytes MBZ + PacketListField('tlv_objects', [], STAMPTestTLV), + ] + + +class STAMPSessionReflectorTestUnauthenticated(Packet): + """ + Extended STAMP Session-Reflector Test Packet in Unauthenticated Mode. + + The format (defined in Section 3 of `RFC 8972 + `_) is shown below:: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Timestamp | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Error Estimate | SSID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Receive Timestamp | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Session-Sender Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Session-Sender Timestamp | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Session-Sender Error Estimate | MBZ | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Ses-Sender TTL | MBZ | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ~ TLVs ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + References: + * `Simple Two-Way Active Measurement Protocol Optional Extensions + [RFC 8972] `_ + """ + name = 'STAMP Session-Reflector Test' + fields_desc = [ + IntField('seq', 0), + MultipleTypeField( + [ + (TimeStampField('ts', 0), + lambda pkt:pkt.err_estimate.Z == 0), + ], + UTCTimeField('ts', 0, fmt='Q') + ), + PacketField('err_estimate', ErrorEstimate(), ErrorEstimate), + ShortField('ssid', 1), + MultipleTypeField( + [ + (TimeStampField('ts_rx', 0), + lambda pkt:pkt.err_estimate.Z == 0) + ], + UTCTimeField('ts_rx', 0, fmt='Q') + ), + IntField('seq_sender', 0), + MultipleTypeField( + [ + (TimeStampField('ts_sender', 0), + lambda pkt:pkt.err_estimate_sender.Z == 0) + ], + UTCTimeField('ts_sender', 0, fmt='Q') + ), + PacketField('err_estimate_sender', ErrorEstimate(), ErrorEstimate), + ShortField('mbz1', 0), + ByteField('ttl_sender', 255), + NBytesField('mbz2', 0, 3), # 3 bytes MBZ + PacketListField('tlv_objects', [], STAMPTestTLV), + ] + + +bind_layers(UDP, STAMPSessionSenderTestUnauthenticated, dport=862) +bind_layers(UDP, STAMPSessionReflectorTestUnauthenticated, sport=862) diff --git a/scapy/contrib/stun.py b/scapy/contrib/stun.py new file mode 100644 index 00000000000..ee243551597 --- /dev/null +++ b/scapy/contrib/stun.py @@ -0,0 +1,316 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Pavel Oborin + +# RFC 8489 +# scapy.contrib.description = Session Traversal Utilities for NAT (STUN) +# scapy.contrib.status = loads + +""" + STUN (RFC 8489) + + TLV code derived from the DTP implementation: + Thanks to Nicolas Bareil, + Arnaud Ebalard, + Jochen Bartl. +""" +import struct +import itertools + +from scapy.layers.inet import UDP, TCP +from scapy.config import conf +from scapy.packet import Packet, bind_bottom_up, bind_top_down +from scapy.utils import inet_ntoa, inet_aton +from scapy.fields import ( + BitField, + BitEnumField, + LenField, + IntField, + PadField, + StrLenField, + PacketListField, + XShortField, + FieldLenField, + ShortField, + ByteEnumField, + ByteField, + XNBytesField, + XLongField, + XIntField, + XBitField, + IPField, + IP6Field, + MultipleTypeField, +) + +MAGIC_COOKIE = 0x2112A442 + +_stun_class = { + "request": 0b00, + "indication": 0b01, + "success response": 0b10, + "error response": 0b11 +} + +_stun_method = { + "Binding": 0b000000000001 +} + +# fmt: off +_stun_message_type = { + "{} {}".format(method, class_): + (method_code & 0b000000001111) | # noqa: E221,W504 + (class_code & 0b01) << 4 | # noqa: E221,W504 + (method_code & 0b000001110000) << 5 | # noqa: E221,W504 + (class_code & 0b10) << 7 | # noqa: E221,W504 + (method_code & 0b111110000000) << 9 + for (method, method_code), (class_, class_code) in + itertools.product(_stun_method.items(), _stun_class.items()) # noqa: E131 +} +# fmt: on + + +class STUNGenericTlv(Packet): + name = "STUN Generic TLV" + + fields_desc = [ + XShortField("type", 0x0000), + FieldLenField("length", None, length_of="value"), + PadField(StrLenField("value", "", length_from=lambda pkt:pkt.length), align=4) + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kwargs): + if _pkt and len(_pkt) >= 2: + t = struct.unpack("!H", _pkt[:2])[0] + return _stun_tlv_class.get(t, cls) + return cls + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class STUNUsername(STUNGenericTlv): + name = "STUN Username" + + fields_desc = [ + XShortField("type", 0x0006), + FieldLenField("length", None, length_of="username"), + PadField( + StrLenField("username", '', length_from=lambda pkt: pkt.length), + align=4, padwith=b"\x20" + ) + ] + + +class STUNMessageIntegrity(STUNGenericTlv): + name = "STUN Message Integrity" + + fields_desc = [ + XShortField("type", 0x0008), + ShortField("length", 20), + XNBytesField("hmac_sha1", 0, 20) + ] + + def post_build(self, pkt, pay): + pkt += pay + return pkt + + +class STUNPriority(STUNGenericTlv): + name = "STUN Priority" + + fields_desc = [ + XShortField("type", 0x0024), + ShortField("length", 4), + IntField("priority", 0) + ] + + +_xor_mapped_address_family = { + "IPv4": 0x01, + "IPv6": 0x02 +} + + +class XorPort(ShortField): + + def m2i(self, pkt, x): + return x ^ (MAGIC_COOKIE >> 16) + + def i2m(self, pkt, x): + return x ^ (MAGIC_COOKIE >> 16) + + +class XorIp(IPField): + + def m2i(self, pkt, x): + return inet_ntoa(struct.pack(">i", (struct.unpack(">i", x)[0] ^ MAGIC_COOKIE))) + + def i2m(self, pkt, x): + if x is None: + return b"\x00\x00\x00\x00" + return struct.pack(">i", struct.unpack(">i", inet_aton(x))[0] ^ MAGIC_COOKIE) + + +class XorIp6(IP6Field): + + def m2i(self, pkt, x): + addr = self._xor_address(pkt, x) + return super().m2i(pkt, addr) + + def i2m(self, pkt, x): + addr = super().i2m(pkt, x) + return self._xor_address(pkt, addr) + + def _xor_address(self, pkt, addr): + xor_words = [pkt.parent.magic_cookie] + xor_words += struct.unpack( + ">III", pkt.parent.transaction_id.to_bytes(12, "big") + ) + addr_words = struct.unpack(">IIII", addr) + xor_addr = [a ^ b for a, b in zip(addr_words, xor_words)] + return struct.pack(">IIII", *xor_addr) + + +class STUNXorMappedAddress(STUNGenericTlv): + name = "STUN XOR Mapped Address" + + fields_desc = [ + XShortField("type", 0x0020), + FieldLenField("length", None, length_of="xip", adjust=lambda pkt, x: x + 4), + ByteField("RESERVED", 0), + ByteEnumField("address_family", 1, _xor_mapped_address_family), + XorPort("xport", 0), + MultipleTypeField( + [ + (XorIp("xip", "127.0.0.1"), lambda pkt: pkt.address_family == 1), + (XorIp6("xip", "::1"), lambda pkt: pkt.address_family == 2), + ], + XorIp("xip", "127.0.0.1"), + ), + ] + + +class STUNMappedAddress(STUNGenericTlv): + name = "STUN Mapped Address" + + fields_desc = [ + XShortField("type", 0x0001), + FieldLenField("length", None, length_of="ip", adjust=lambda pkt, x: x + 4), + ByteField("RESERVED", 0), + ByteEnumField("address_family", 1, _xor_mapped_address_family), + ShortField("port", 0), + MultipleTypeField( + [ + (IPField("ip", "127.0.0.1"), lambda pkt: pkt.address_family == 1), + (IP6Field("ip", "::1"), lambda pkt: pkt.address_family == 2), + ], + IPField("ip", "127.0.0.1"), + ), + ] + + +class STUNUseCandidate(STUNGenericTlv): + name = "STUN Use Candidate" + + fields_desc = [ + XShortField("type", 0x0025), + FieldLenField("length", 0, length_of="value"), + PadField(StrLenField("value", "", length_from=lambda pkt: pkt.length), align=4) + ] + + +class STUNFingerprint(STUNGenericTlv): + name = "STUN Fingerprint" + + fields_desc = [ + XShortField("type", 0x8028), + ShortField("length", 4), + XIntField("crc_32", None) + ] + + +class STUNIceControlling(STUNGenericTlv): + name = "STUN ICE-controlling" + + fields_desc = [ + XShortField("type", 0x802a), + ShortField("length", 8), + XLongField("tie_breaker", None) + ] + + +class STUNGoogNetworkInfo(STUNGenericTlv): + name = "STUN Google Network Info" + + fields_desc = [ + XShortField("type", 0xc057), + ShortField("length", 4), + ShortField("network_id", 0), + ShortField("network_cost", 999) + ] + + +_stun_tlv_class = { + 0x0001: STUNMappedAddress, + 0x0006: STUNUsername, + 0x0008: STUNMessageIntegrity, + 0x0020: STUNXorMappedAddress, + 0x0025: STUNUseCandidate, + 0x0024: STUNPriority, + 0x8028: STUNFingerprint, + 0x802a: STUNIceControlling, + 0xc057: STUNGoogNetworkInfo +} + +_stun_tlv_attribute_types = { + "MAPPED-ADDRESS": 0x0001, + "USERNAME": 0x0006, + "MESSAGE-INTEGRITY": 0x0008, + "ERROR-CODE": 0x0009, + "UNKNOWN-ATTRIBUTES": 0x000A, + "REALM": 0x0014, + "NONCE": 0x0015, + "XOR-MAPPED-ADDRESS": 0x0020, + "PRIORITY": 0x0024, + "USE-CANDIDATE": 0x0025, + "SOFTWARE": 0x8022, + "ALTERNATE-SERVER": 0x8023, + "FINGERPRINT": 0x8028, + "ICE-CONTROLLED": 0x8029, + "ICE-CONTROLLING": 0x802a, + "GOOG-NETWORK-INFO": 0xc057 +} + + +class STUN(Packet): + description = "" + + fields_desc = [ + BitField('RESERVED', 0b00, size=2), # <- always zeroes + BitEnumField('stun_message_type', None, 14, _stun_message_type), + LenField('length', None, fmt='!h'), + XIntField('magic_cookie', MAGIC_COOKIE), + XBitField('transaction_id', None, 96), + PacketListField("attributes", [], STUNGenericTlv) + ] + + def post_build(self, pkt, pay): + pkt += pay + if self.length is None: + pkt = pkt[:2] + struct.pack("!h", len(pkt) - 20) + pkt[4:] + for attr in self.attributes: + if isinstance(attr, STUNMessageIntegrity): + pass # TODO Fill hmac-sha1 in MESSAGE-INTEGRITY attribute + return pkt + + +bind_bottom_up(UDP, STUN, sport=3478) +bind_bottom_up(UDP, STUN, dport=3478) +bind_top_down(UDP, STUN, sport=3478, dport=3478) + +bind_bottom_up(TCP, STUN, sport=3478) +bind_bottom_up(TCP, STUN, dport=3478) +bind_top_down(TCP, STUN, sport=3478, dport=3478) diff --git a/scapy/contrib/tacacs.py b/scapy/contrib/tacacs.py index ed933f10d4f..814136dd87d 100755 --- a/scapy/contrib/tacacs.py +++ b/scapy/contrib/tacacs.py @@ -1,20 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # Copyright (C) 2017 Francois Contat -# Based on tacacs+ v6 draft https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06 # noqa: E501 +""" +TACACS + +Based on tacacs+ v6 draft +https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06 +""" # scapy.contrib.description = Terminal Access Controller Access-Control System+ # scapy.contrib.status = loads @@ -29,7 +23,6 @@ from scapy.layers.inet import TCP from scapy.compat import chb, orb from scapy.config import conf -from scapy.modules.six.moves import range SECRET = 'test' @@ -369,7 +362,8 @@ def post_dissect(self, pay): if self.flags == 0: pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq) # noqa: E501 - return pay + + return pay class TacacsHeader(TacacsClientPacket): @@ -427,11 +421,9 @@ def post_build(self, p, pay): p = p[:-4] + struct.pack('!I', len(pay)) if self.flags == 0: - pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq) # noqa: E501 - return p + pay - return p + return p + pay def hashret(self): return struct.pack('I', self.session_id) diff --git a/scapy/contrib/tcpao.py b/scapy/contrib/tcpao.py new file mode 100644 index 00000000000..6f18aee9d8a --- /dev/null +++ b/scapy/contrib/tcpao.py @@ -0,0 +1,255 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Leonard Crestez + +# scapy.contrib.description = TCP-AO Signature Calculation +# scapy.contrib.status = loads + +"""Packet-processing utilities implementing RFC5925 and RFC5926""" + +import logging +from scapy.compat import orb +from scapy.layers.inet import IP, TCP +from scapy.layers.inet import tcp_pseudoheader +from scapy.layers.inet6 import IPv6 +from scapy.packet import Packet +from scapy.pton_ntop import inet_pton +import socket +import struct + +from typing import ( + Union, +) + +logger = logging.getLogger(__name__) + + +def _hmac_sha1_digest(key, msg): + # type: (bytes, bytes) -> bytes + import hmac + import hashlib + + return hmac.new(key, msg, hashlib.sha1).digest() + + +def _cmac_aes_digest(key, msg): + # type: (bytes, bytes) -> bytes + from cryptography.hazmat.primitives import cmac + from cryptography.hazmat.primitives.ciphers import algorithms + from cryptography.hazmat.backends import default_backend + + backend = default_backend() + c = cmac.CMAC(algorithms.AES(key), backend=backend) + c.update(bytes(msg)) + return c.finalize() + + +class TCPAOAlg: + @classmethod + def kdf(cls, master_key, context): + # type: (bytes, bytes) -> bytes + raise NotImplementedError() + + @classmethod + def mac(cls, traffic_key, context): + # type: (bytes, bytes) -> bytes + raise NotImplementedError() + + maclen = -1 + + +class TCPAOAlg_HMAC_SHA1(TCPAOAlg): + @classmethod + def kdf(cls, master_key, context): + # type: (bytes, bytes) -> bytes + input = b"\x01" + b"TCP-AO" + context + b"\x00\xa0" + return _hmac_sha1_digest(master_key, input) + + @classmethod + def mac(cls, traffic_key, message): + # type: (bytes, bytes) -> bytes + return _hmac_sha1_digest(traffic_key, message)[:12] + + maclen = 12 + + +class TCPAOAlg_CMAC_AES(TCPAOAlg): + @classmethod + def kdf(self, master_key, context): + # type: (bytes, bytes) -> bytes + if len(master_key) == 16: + key = master_key + else: + key = _cmac_aes_digest(b"\x00" * 16, master_key) + return _cmac_aes_digest(key, b"\x01TCP-AO" + context + b"\x00\x80") + + @classmethod + def mac(self, traffic_key, message): + # type: (bytes, bytes) -> bytes + return _cmac_aes_digest(traffic_key, message)[:12] + + maclen = 12 + + +def get_alg(name): + # type: (str) -> TCPAOAlg + if name.upper() == "HMAC-SHA-1-96": + return TCPAOAlg_HMAC_SHA1() + elif name.upper() == "AES-128-CMAC-96": + return TCPAOAlg_CMAC_AES() + else: + raise ValueError("Bad TCP AuthOpt algorithms {}".format(name)) + + +def _get_ipvx_src(u): + # type: (Union[IP, IPv6]) -> bytes + if isinstance(u, IP): + return inet_pton(socket.AF_INET, u.src) + elif isinstance(u, IPv6): + return inet_pton(socket.AF_INET6, u.src) + else: + raise Exception("Neither IP nor IPv6 found on packet") + + +def _get_ipvx_dst(u): + # type: (Union[IP, IPv6]) -> bytes + if isinstance(u, IP): + return inet_pton(socket.AF_INET, u.dst) + elif isinstance(u, IPv6): + return inet_pton(socket.AF_INET6, u.dst) + else: + raise Exception("Neither IP nor IPv6 found on packet") + + +def build_context( + saddr, # type: bytes + daddr, # type: bytes + sport, # type: int + dport, # type: int + src_isn, # type: int + dst_isn, # type: int +): + # type: (...) -> bytes + """Build context bytes as specified by RFC5925 section 5.2""" + if len(saddr) != len(daddr) or (len(saddr) != 4 and len(saddr) != 16): + raise ValueError("saddr and daddr must be 4-byte or 16-byte addresses") + return ( + saddr + + daddr + + struct.pack( + "!HHII", + sport, + dport, + src_isn, + dst_isn, + ) + ) + + +def build_context_from_packet( + p, # type: Packet + src_isn, # type: int + dst_isn, # type: int +): + # type: (...) -> bytes + """Build context bytes as specified by RFC5925 section 5.2""" + tcp = p[TCP] + return build_context( + _get_ipvx_src(tcp.underlayer), + _get_ipvx_dst(tcp.underlayer), + tcp.sport, + tcp.dport, + src_isn, + dst_isn, + ) + + +def build_message_from_packet(p, include_options=True, sne=0): + # type: (Packet, bool, int) -> bytes + """Build message bytes as described by RFC5925 section 5.1""" + result = bytearray() + result += struct.pack("!I", sne) + result += tcp_pseudoheader(p[TCP]) + + # tcp header with checksum set to zero + th_bytes = bytes(p[TCP]) + result += th_bytes[:16] + result += b"\x00\x00" + result += th_bytes[18:20] + + # Even if include_options=False the TCP-AO option itself is still included + # with the MAC set to all-zeros. This means we need to parse TCP options. + pos = 20 + th = p[TCP] + doff = th.dataofs + if doff is None: + opt_len = len(th.get_field("options").i2m(th, th.options)) + doff = 5 + ((opt_len + 3) // 4) + tcphdr_optend = doff * 4 + while pos < tcphdr_optend: + optnum = orb(th_bytes[pos]) + pos += 1 + if optnum == 0 or optnum == 1: + if include_options: + result += bytearray([optnum]) + continue + + optlen = orb(th_bytes[pos]) + pos += 1 + if pos + optlen - 2 > tcphdr_optend: + logger.info("bad tcp option %d optlen %d beyond end-of-header", + optnum, optlen) + break + if optlen < 2: + logger.info("bad tcp option %d optlen %d less than two", + optnum, optlen) + break + if optnum == 29: + if optlen < 4: + logger.info("bad tcp option %d optlen %d", optnum, optlen) + break + result += th_bytes[pos - 2: pos + 2] + result += (optlen - 4) * b"\x00" + elif include_options: + result += th_bytes[pos - 2: pos + optlen - 2] + pos += optlen - 2 + result += bytes(p[TCP].payload) + return result + + +def calc_tcpao_traffic_key(p, alg, master_key, sisn, disn): + # type: (Packet, TCPAOAlg, bytes, int, int) -> bytes + """Calculate TCP-AO traffic-key from packet and initial sequence numbers + + This is constant for an established connection. + """ + return alg.kdf(master_key, build_context_from_packet(p, sisn, disn)) + + +def calc_tcpao_mac(p, alg, traffic_key, include_options=True, sne=0): + # type: (Packet, TCPAOAlg, bytes, bool, int) -> bytes + """Calculate TCP-AO MAC from packet and traffic key""" + return alg.mac(traffic_key, build_message_from_packet( + p, include_options=include_options, sne=sne + )) + + +def sign_tcpao( + p, + alg, + traffic_key, + keyid=0, + rnextkeyid=0, + include_options=True, + sne=0, +): + # type: (Packet, TCPAOAlg, bytes, int, int, bool, int) -> None + """Calculate TCP-AO option value and insert into packet""" + th = p[TCP] + keyids = struct.pack("BB", keyid, rnextkeyid) + th.options = th.options + [('AO', keyids + alg.maclen * b"\x00")] + message_bytes = calc_tcpao_mac( + p, alg, traffic_key, include_options=include_options, sne=sne) + mac = alg.mac(traffic_key, message_bytes) + th.options[-1] = ('AO', keyids + mac) diff --git a/scapy/contrib/tcpros.py b/scapy/contrib/tcpros.py new file mode 100644 index 00000000000..90773d32a7b --- /dev/null +++ b/scapy/contrib/tcpros.py @@ -0,0 +1,714 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Víctor Mayoral-Vilches + +""" +TCPROS transport layer for ROS Melodic Morenia 1.14.5 +""" + +# scapy.contrib.description = TCPROS transport layer for ROS Melodic Morenia +# scapy.contrib.status = loads +# scapy.contrib.name = tcpros + +import struct +from scapy.compat import raw +from scapy.fields import ( + LEIntField, + StrLenField, + FieldLenField, + StrFixedLenField, + ByteField, +) +from scapy.layers.http import HTTP, HTTPRequest, HTTPResponse +from scapy.packet import Packet, Raw, PacketListField +from scapy.config import conf + + +class TCPROS(Packet): + """ + TCPROS is a transport layer for ROS Messages and Services. It uses standard + TCP/IP sockets for transporting message data. Inbound connections are + received via a TCP Server Socket with a header containing message data type + and routing information. + + This class focuses on capturing the ROS Slave API + + An example package is presented below:: + + B0 00 00 00 26 00 00 00 63 61 6C 6C 65 72 69 64 ....&...callerid + 3D 2F 72 6F 73 74 6F 70 69 63 5F 38 38 33 30 35 =/rostopic_88305 + 5F 31 35 39 31 35 33 38 37 38 37 35 30 31 0A 00 _1591538787501.. + 00 00 6C 61 74 63 68 69 6E 67 3D 31 27 00 00 00 ..latching=1'... + 6D 64 35 73 75 6D 3D 39 39 32 63 65 38 61 31 36 md5sum=992ce8a16 + 38 37 63 65 63 38 63 38 62 64 38 38 33 65 63 37 87cec8c8bd883ec7 + 33 63 61 34 31 64 31 1F 00 00 00 6D 65 73 73 61 3ca41d1....messa + 67 65 5F 64 65 66 69 6E 69 74 69 6F 6E 3D 73 74 ge_definition=st + 72 69 6E 67 20 64 61 74 61 0A 0E 00 00 00 74 6F ring data.....to + 70 69 63 3D 2F 63 68 61 74 74 65 72 14 00 00 00 pic=/chatter.... + 74 79 70 65 3D 73 74 64 5F 6D 73 67 73 2F 53 74 type=std_msgs/St + 72 69 6E 67 ring + + Sources: + - http://wiki.ros.org/ROS/TCPROS + - http://wiki.ros.org/ROS/Connection%20Header + - https://docs.python.org/3/library/struct.html + - https://scapy.readthedocs.io/en/latest/build_dissect.html + + TODO: + - Extend to support subscriber's interactions + - Unify with subscriber's header + + NOTES: + - 4-byte length + [4-byte field length + field=value ]* + - All length fields are little-endian integers. Field names and + values are strings. + - Cooked as of ROS Melodic Morenia v1.14.5. + """ + + name = "TCPROS" + + def guess_payload_class(self, payload): + string_payload = payload.decode("iso-8859-1") # decode to string + # for search + + # flag indicating if the TCPROS encoding format is met + # 4-byte length + [4-byte field length + field=value ]* + total_length = len(payload) + total_length_payload = struct.unpack(" total_length_payload) and ( + total_length_payload == remain_len + ) + + if conf.debug_dissector: + print(payload) + print(string_payload) + print("total_length: " + str(total_length)) + print("total_length_payload: " + str(total_length_payload)) + print("remain: " + str(remain)) + print(flag_encoding_format) + + flag_encoding_format_subfields = False + if flag_encoding_format: + # flag indicating that sub-fields meet + # TCPROS encoding format: + # [4-byte field length + field=value ]* + flag_encoding_format_subfields = True + while remain: + field_len_bytes = struct.unpack(" + 0100 0A 3C 6D 65 74 68 6F 64 43 61 6C 6C 3E 0A 3C 6D ..getPid + 0120 3C 2F 6D 65 74 68 6F 64 4E 61 6D 65 3E 0A 3C 70 .

..< + 0140 76 61 6C 75 65 3E 3C 73 74 72 69 6E 67 3E 2F 72 value>/r + 0150 6F 73 74 6F 70 69 63 3C 2F 73 74 72 69 6E 67 3E ostopic + 0160 3C 2F 76 61 6C 75 65 3E 0A 3C 2F 70 61 72 61 6D .... + + The counterpart (the Master) answers with (HTTP Response):: + + 0000 02 42 0C 00 00 04 02 42 0C 00 00 02 08 00 45 00 .B.....B......E. + 0010 01 A2 8C CD 40 00 40 06 94 83 0C 00 00 02 0C 00 ....@.@......... + 0020 00 04 2C 2F 8E 62 87 00 82 4C C7 A9 93 F1 80 18 ..,/.b...L...... + 0030 01 F6 19 9A 00 00 01 01 08 0A 39 82 4B 7B BB 36 ..........9.K{.6 + 0040 D2 1A 48 54 54 50 2F 31 2E 31 20 32 30 30 20 4F ..HTTP/1.1 200 O + 0050 4B 0D 0A 53 65 72 76 65 72 3A 20 42 61 73 65 48 K..Server: BaseH + 0060 54 54 50 2F 30 2E 33 20 50 79 74 68 6F 6E 2F 32 TTP/0.3 Python/2 + 0070 2E 37 2E 31 37 0D 0A 44 61 74 65 3A 20 53 75 6E .7.17..Date: Sun + 0080 2C 20 30 36 20 44 65 63 20 32 30 32 30 20 31 35 , 06 Dec 2020 15 + 0090 3A 31 37 3A 33 38 20 47 4D 54 0D 0A 43 6F 6E 74 :17:38 GMT..Cont + 00a0 65 6E 74 2D 74 79 70 65 3A 20 74 65 78 74 2F 78 ent-type: text/x + 00b0 6D 6C 0D 0A 43 6F 6E 74 65 6E 74 2D 6C 65 6E 67 ml..Content-leng + 00c0 74 68 3A 20 32 32 39 0D 0A 0D 0A 3C 3F 78 6D 6C th: 229.... + 00e0 0A 3C 6D 65 74 68 6F 64 52 65 73 70 6F 6E 73 65 .....< + 0120 69 6E 74 3E 31 3C 2F 69 6E 74 3E 3C 2F 76 61 6C int>1..398..... + + + In another communication, and endpoint could request a parameter using the + Parameter Server API (HTTP Request):: + + 0000 02 42 0C 00 00 02 02 42 0C 00 00 04 08 00 45 00 .B.....B......E. + 0010 01 C0 8B 72 40 00 40 06 95 C0 0C 00 00 04 0C 00 ...r@.@......... + 0020 00 02 90 10 2C 2F 9D 09 47 7F EC C3 08 BD 80 18 ....,/..G....... + 0030 01 FD 19 B8 00 00 01 01 08 0A BB 86 68 91 39 D1 ............h.9. + 0040 E1 F1 50 4F 53 54 20 2F 52 50 43 32 20 48 54 54 ..POST /RPC2 HTT + 0050 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 31 32 2E P/1.1..Host: 12. + 0060 30 2E 30 2E 32 3A 31 31 33 31 31 0D 0A 41 63 63 0.0.2:11311..Acc + 0070 65 70 74 2D 45 6E 63 6F 64 69 6E 67 3A 20 67 7A ept-Encoding: gz + 0080 69 70 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A 20 ip..User-Agent: + 0090 78 6D 6C 72 70 63 6C 69 62 2E 70 79 2F 31 2E 30 xmlrpclib.py/1.0 + 00a0 2E 31 20 28 62 79 20 77 77 77 2E 70 79 74 68 6F .1 (by www.pytho + 00b0 6E 77 61 72 65 2E 63 6F 6D 29 0D 0A 43 6F 6E 74 nware.com)..Cont + 00c0 65 6E 74 2D 54 79 70 65 3A 20 74 65 78 74 2F 78 ent-Type: text/x + 00d0 6D 6C 0D 0A 43 6F 6E 74 65 6E 74 2D 4C 65 6E 67 ml..Content-Leng + 00e0 74 68 3A 20 32 32 37 0D 0A 0D 0A 3C 3F 78 6D 6C th: 227.... + 0100 0A 3C 6D 65 74 68 6F 64 43 61 6C 6C 3E 0A 3C 6D ..getPar + 0120 61 6D 3C 2F 6D 65 74 68 6F 64 4E 61 6D 65 3E 0A am. + 0130 3C 70 61 72 61 6D 73 3E 0A 3C 70 61 72 61 6D 3E . + 0140 0A 3C 76 61 6C 75 65 3E 3C 73 74 72 69 6E 67 3E . + 0150 2F 72 6F 73 70 61 72 61 6D 2D 38 32 30 34 33 3C /rosparam-82043< + 0160 2F 73 74 72 69 6E 67 3E 3C 2F 76 61 6C 75 65 3E /string> + 0170 0A 3C 2F 70 61 72 61 6D 3E 0A 3C 70 61 72 61 6D .../rosdistro.

.. + 01c0 3C 2F 6D 65 74 68 6F 64 43 61 6C 6C 3E 0A
. + + Sources: + - https://aliasrobotics.com/files/ + securing_robot_endpoints_ot_environment.pdf + - http://wiki.ros.org/ROS/Master_API + - http://wiki.ros.org/ROS/Slave_API + - http://wiki.ros.org/ROS/Parameter%20Server%20API + + """ + + name = "XMLRPC" + + def guess_payload_class(self, payload): + string_payload = payload.decode("iso-8859-1") # decode for search + # total_length = len(payload) + + if "xml" in string_payload and "version='1.0'" in string_payload: + if isinstance(self.underlayer, HTTPRequest): + return XMLRPCCall + elif isinstance(self.underlayer, HTTPResponse): + return XMLRPCResponse + else: + print("failed to match") + return Raw + else: + return Raw(self, payload) # returns Raw layer grouping not only + # the payload but this layer itself. + + +# Fields +class XMLRPCSeparator(ByteField): + """ + Separator of XML-RPC components - 0x0a + + """ + + def __init__(self, name, default="0x0a"): + ByteField.__init__(self, name, default) + + +# Packages +class XMLRPCCall(Packet): + """ + Request side of the ROS XMLPC elements used by Master and Parameter APIs + Exemplary package:: + + 0000 02 42 0C 00 00 02 02 42 0C 00 00 04 08 00 45 00 .B.....B......E. + 0010 01 C0 8B 72 40 00 40 06 95 C0 0C 00 00 04 0C 00 ...r@.@......... + 0020 00 02 90 10 2C 2F 9D 09 47 7F EC C3 08 BD 80 18 ....,/..G....... + 0030 01 FD 19 B8 00 00 01 01 08 0A BB 86 68 91 39 D1 ............h.9. + 0040 E1 F1 50 4F 53 54 20 2F 52 50 43 32 20 48 54 54 ..POST /RPC2 HTT + 0050 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 31 32 2E P/1.1..Host: 12. + 0060 30 2E 30 2E 32 3A 31 31 33 31 31 0D 0A 41 63 63 0.0.2:11311..Acc + 0070 65 70 74 2D 45 6E 63 6F 64 69 6E 67 3A 20 67 7A ept-Encoding: gz + 0080 69 70 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A 20 ip..User-Agent: + 0090 78 6D 6C 72 70 63 6C 69 62 2E 70 79 2F 31 2E 30 xmlrpclib.py/1.0 + 00a0 2E 31 20 28 62 79 20 77 77 77 2E 70 79 74 68 6F .1 (by www.pytho + 00b0 6E 77 61 72 65 2E 63 6F 6D 29 0D 0A 43 6F 6E 74 nware.com)..Cont + 00c0 65 6E 74 2D 54 79 70 65 3A 20 74 65 78 74 2F 78 ent-Type: text/x + 00d0 6D 6C 0D 0A 43 6F 6E 74 65 6E 74 2D 4C 65 6E 67 ml..Content-Leng + 00e0 74 68 3A 20 32 32 37 0D 0A 0D 0A 3C 3F 78 6D 6C th: 227.... + 0100 0A 3C 6D 65 74 68 6F 64 43 61 6C 6C 3E 0A 3C 6D ..getPar + 0120 61 6D 3C 2F 6D 65 74 68 6F 64 4E 61 6D 65 3E 0A am. + 0130 3C 70 61 72 61 6D 73 3E 0A 3C 70 61 72 61 6D 3E . + 0140 0A 3C 76 61 6C 75 65 3E 3C 73 74 72 69 6E 67 3E . + 0150 2F 72 6F 73 70 61 72 61 6D 2D 38 32 30 34 33 3C /rosparam-82043< + 0160 2F 73 74 72 69 6E 67 3E 3C 2F 76 61 6C 75 65 3E /string> + 0170 0A 3C 2F 70 61 72 61 6D 3E 0A 3C 70 61 72 61 6D .../rosdistro.

.
. + 01c0 3C 2F 6D 65 74 68 6F 64 43 61 6C 6C 3E 0A
. + + """ + + name = "XMLRPCCall" + __slots__ = Packet.__slots__ + ["methodname_size", "params_size"] + fields_desc = [ + # .. + StrFixedLenField( + "version", + "\n", + length=22, # 22 + ), + # XMLRPCSeparator("separator_version"), + StrFixedLenField("methodcall_opentag", "\n", length=13), + # getParam. + StrFixedLenField("methodname_opentag", "", length=12), + StrLenField("methodname", "getParam", + length_from=lambda pkt: pkt.methodname_size), + StrFixedLenField("methodname_closetag", "\n", length=14), + # . + StrFixedLenField("params_opentag", "\n", length=9), + # [./rosparam-82043..] + StrLenField( + "params", + "\n/rosparam-82043" + \ + "\n\n", + length_from=lambda pkt: pkt.params_size, + ), + # .. + StrFixedLenField("params_closetag", "\n", length=10), + StrFixedLenField("methodcall_closetag", "\n", length=14), + ] + + def pre_dissect(self, s): + """ + Calculate the sizes of: + - methodname + - params + + See https://docs.python.org/3/library/struct.html + for the unpack (e.g. "") + + len(""):decoded_s.find("") + ] + ) + + self.params_size = len( + decoded_s[ + decoded_s.find("\n") + + len("\n"):decoded_s.find("") + ] + ) + + if conf.debug_dissector: + print(self.methodname_size) + print(self.params_size) + return s + + def do_dissect_payload(self, s): + self.guess_payload_class(s) + + +class XMLRPCResponse(Packet): + """ + Response side of the ROS XMLPC elements used by Master and Parameter APIs + Exemplary package:: + + 0000 02 42 0C 00 00 04 02 42 0C 00 00 02 08 00 45 00 .B.....B......E. + 0010 01 A2 8C CD 40 00 40 06 94 83 0C 00 00 02 0C 00 ....@.@......... + 0020 00 04 2C 2F 8E 62 87 00 82 4C C7 A9 93 F1 80 18 ..,/.b...L...... + 0030 01 F6 19 9A 00 00 01 01 08 0A 39 82 4B 7B BB 36 ..........9.K{.6 + 0040 D2 1A 48 54 54 50 2F 31 2E 31 20 32 30 30 20 4F ..HTTP/1.1 200 O + 0050 4B 0D 0A 53 65 72 76 65 72 3A 20 42 61 73 65 48 K..Server: BaseH + 0060 54 54 50 2F 30 2E 33 20 50 79 74 68 6F 6E 2F 32 TTP/0.3 Python/2 + 0070 2E 37 2E 31 37 0D 0A 44 61 74 65 3A 20 53 75 6E .7.17..Date: Sun + 0080 2C 20 30 36 20 44 65 63 20 32 30 32 30 20 31 35 , 06 Dec 2020 15 + 0090 3A 31 37 3A 33 38 20 47 4D 54 0D 0A 43 6F 6E 74 :17:38 GMT..Cont + 00a0 65 6E 74 2D 74 79 70 65 3A 20 74 65 78 74 2F 78 ent-type: text/x + 00b0 6D 6C 0D 0A 43 6F 6E 74 65 6E 74 2D 6C 65 6E 67 ml..Content-leng + 00c0 74 68 3A 20 32 32 39 0D 0A 0D 0A 3C 3F 78 6D 6C th: 229.... + 00e0 0A 3C 6D 65 74 68 6F 64 52 65 73 70 6F 6E 73 65 .....< + 0120 69 6E 74 3E 31 3C 2F 69 6E 74 3E 3C 2F 76 61 6C int>1..398..... + """ + + name = "XMLRPCResponse" + __slots__ = Packet.__slots__ + ["params_size"] + fields_desc = [ + # \n + StrFixedLenField("version", "\n", length=22), + # XMLRPCSeparator("separator_version"), + # \n + StrFixedLenField("methodcall_opentag", "\n", + length=17), + # \n + StrFixedLenField("params_opentag", "\n", + length=9), + # \n\n + # 1\n + # Parameter [/rosdistro]\n + # melodic\n\n + # \n\n + StrLenField("params", "", length_from=lambda pkt: pkt.params_size), + # \n\n + StrFixedLenField("params_closetag", "\n", length=10), + StrFixedLenField("methodcall_closetag", "\n", + length=18), + ] + + def pre_dissect(self, s): + """ + Calculate the sizes of: + - methodname + - params + + See https://docs.python.org/3/library/struct.html + for the unpack (e.g. "\n") + + len("\n"):decoded_s.find("") + ] + ) + + if conf.debug_dissector: + print(self.params_size) + return s + + def do_dissect_payload(self, s): + self.guess_payload_class(s) diff --git a/scapy/contrib/tzsp.py b/scapy/contrib/tzsp.py index 31d2eb29ece..29206e3616f 100644 --- a/scapy/contrib/tzsp.py +++ b/scapy/contrib/tzsp.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = TaZmen Sniffer Protocol (TZSP) # scapy.contrib.status = loads @@ -6,17 +10,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :author: Thomas Tannhaeuser, hecke@naberius.de - :license: GPLv2 - - This module is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This module is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. :description: @@ -111,7 +104,8 @@ def get_encapsulated_payload_class(self): def guess_payload_class(self, payload): if self.type == TZSP.TYPE_KEEPALIVE: if len(payload): - warning('payload (%i bytes) in KEEPALIVE/NULL packet' % len(payload)) # noqa: E501 + warning('payload (%i bytes) in KEEPALIVE/NULL packet', + len(payload)) return Raw else: return _tzsp_guess_next_tag(payload) @@ -133,17 +127,19 @@ def _tzsp_handle_unknown_tag(payload, tag_type): payload_len = len(payload) if payload_len < 2: - warning('invalid or unknown tag type (%i) and too short packet - treat remaining data as Raw' % tag_type) # noqa: E501 + warning('invalid or unknown tag type (%i) and too short packet - ' + 'treat remaining data as Raw', tag_type) return Raw tag_data_length = orb(payload[1]) tag_data_fits_in_payload = (tag_data_length + 2) <= payload_len if not tag_data_fits_in_payload: - warning('invalid or unknown tag type (%i) and too short packet - treat remaining data as Raw' % tag_type) # noqa: E501 + warning('invalid or unknown tag type (%i) and too short packet - ' + 'treat remaining data as Raw', tag_type) return Raw - warning('invalid or unknown tag type (%i)' % tag_type) + warning('invalid or unknown tag type (%i)', tag_type) return TZSPTagUnknown @@ -175,13 +171,13 @@ def _tzsp_guess_next_tag(payload): length = None if not length: - warning('no tag length given - packet to short') + warning('no tag length given - packet too short') return Raw try: return tag_class_definition[length] except KeyError: - warning('invalid tag length {} for tag type {}'.format(length, tag_type)) # noqa: E501 + warning('invalid tag length %s for tag type %s', length, tag_type) return Raw diff --git a/scapy/contrib/ubberlogger.py b/scapy/contrib/ubberlogger.py deleted file mode 100644 index 92721d13712..00000000000 --- a/scapy/contrib/ubberlogger.py +++ /dev/null @@ -1,129 +0,0 @@ -# This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - -# Author: Sylvain SARMEJEANNE - -# scapy.contrib.description = Ubberlogger dissectors -# scapy.contrib.status = loads - -from scapy.packet import Packet, bind_layers -from scapy.fields import ByteEnumField, ByteField, IntField, ShortField - -# Syscalls known by Uberlogger -uberlogger_sys_calls = {0: "READ_ID", - 1: "OPEN_ID", - 2: "WRITE_ID", - 3: "CHMOD_ID", - 4: "CHOWN_ID", - 5: "SETUID_ID", - 6: "CHROOT_ID", - 7: "CREATE_MODULE_ID", - 8: "INIT_MODULE_ID", - 9: "DELETE_MODULE_ID", - 10: "CAPSET_ID", - 11: "CAPGET_ID", - 12: "FORK_ID", - 13: "EXECVE_ID"} - -# First part of the header - - -class Uberlogger_honeypot_caract(Packet): - name = "Uberlogger honeypot_caract" - fields_desc = [ByteField("honeypot_id", 0), - ByteField("reserved", 0), - ByteField("os_type_and_version", 0)] - -# Second part of the header - - -class Uberlogger_uber_h(Packet): - name = "Uberlogger uber_h" - fields_desc = [ByteEnumField("syscall_type", 0, uberlogger_sys_calls), - IntField("time_sec", 0), - IntField("time_usec", 0), - IntField("pid", 0), - IntField("uid", 0), - IntField("euid", 0), - IntField("cap_effective", 0), - IntField("cap_inheritable", 0), - IntField("cap_permitted", 0), - IntField("res", 0), - IntField("length", 0)] - -# The 9 following classes are options depending on the syscall type - - -class Uberlogger_capget_data(Packet): - name = "Uberlogger capget_data" - fields_desc = [IntField("target_pid", 0)] - - -class Uberlogger_capset_data(Packet): - name = "Uberlogger capset_data" - fields_desc = [IntField("target_pid", 0), - IntField("effective_cap", 0), - IntField("permitted_cap", 0), - IntField("inheritable_cap", 0)] - - -class Uberlogger_chmod_data(Packet): - name = "Uberlogger chmod_data" - fields_desc = [ShortField("mode", 0)] - - -class Uberlogger_chown_data(Packet): - name = "Uberlogger chown_data" - fields_desc = [IntField("uid", 0), - IntField("gid", 0)] - - -class Uberlogger_open_data(Packet): - name = "Uberlogger open_data" - fields_desc = [IntField("flags", 0), - IntField("mode", 0)] - - -class Uberlogger_read_data(Packet): - name = "Uberlogger read_data" - fields_desc = [IntField("fd", 0), - IntField("count", 0)] - - -class Uberlogger_setuid_data(Packet): - name = "Uberlogger setuid_data" - fields_desc = [IntField("uid", 0)] - - -class Uberlogger_create_module_data(Packet): - name = "Uberlogger create_module_data" - fields_desc = [IntField("size", 0)] - - -class Uberlogger_execve_data(Packet): - name = "Uberlogger execve_data" - fields_desc = [IntField("nbarg", 0)] - - -# Layer bounds for Uberlogger -bind_layers(Uberlogger_honeypot_caract, Uberlogger_uber_h) -bind_layers(Uberlogger_uber_h, Uberlogger_capget_data) -bind_layers(Uberlogger_uber_h, Uberlogger_capset_data) -bind_layers(Uberlogger_uber_h, Uberlogger_chmod_data) -bind_layers(Uberlogger_uber_h, Uberlogger_chown_data) -bind_layers(Uberlogger_uber_h, Uberlogger_open_data) -bind_layers(Uberlogger_uber_h, Uberlogger_read_data) -bind_layers(Uberlogger_uber_h, Uberlogger_setuid_data) -bind_layers(Uberlogger_uber_h, Uberlogger_create_module_data) -bind_layers(Uberlogger_uber_h, Uberlogger_execve_data) diff --git a/scapy/contrib/vqp.py b/scapy/contrib/vqp.py index 9bc5a7503c1..f1add3f83c6 100644 --- a/scapy/contrib/vqp.py +++ b/scapy/contrib/vqp.py @@ -1,25 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = VLAN Query Protocol # scapy.contrib.status = loads -import struct - -from scapy.packet import Packet, bind_layers -from scapy.fields import ByteEnumField, ByteField, ConditionalField, \ - FieldLenField, IntEnumField, IntField, IPField, MACField, StrLenField +from scapy.packet import Packet, bind_layers, bind_bottom_up +from scapy.fields import ( + ByteEnumField, + ByteField, + FieldLenField, + IPField, + IntEnumField, + IntField, + MACField, + MultipleTypeField, + StrLenField, +) from scapy.layers.inet import UDP @@ -51,26 +48,22 @@ class VQPEntry(Packet): 3078: "ReqMACAddress", 3079: "unknown", 3080: "ResMACAddress" }), - FieldLenField("len", None), - ConditionalField(IPField("datatom", "0.0.0.0"), - lambda p: p.datatype == 3073), - ConditionalField(MACField("data", "00:00:00:00:00:00"), - lambda p: p.datatype == 3078), - ConditionalField(MACField("data", "00:00:00:00:00:00"), - lambda p: p.datatype == 3080), - ConditionalField(StrLenField("data", None, - length_from=lambda p: p.len), - lambda p: p.datatype not in [3073, 3078, 3080]), + FieldLenField("len", None, length_of="data", fmt="H"), + MultipleTypeField( + [ + (IPField("data", "0.0.0.0"), + lambda p: p.datatype == 3073), + (MACField("data", "00:00:00:00:00:00"), + lambda p: p.datatype in [3078, 3080]), + ], + StrLenField("data", None, length_from=lambda p: p.len) + ) ] - def post_build(self, p, pay): - if self.len is None: - tmp_len = len(p.data) - p = p[:2] + struct.pack("!H", tmp_len) + p[4:] - return p +bind_bottom_up(UDP, VQP, sport=1589) +bind_bottom_up(UDP, VQP, dport=1589) +bind_layers(UDP, VQP, sport=1589, dport=1589) -bind_layers(UDP, VQP, sport=1589) -bind_layers(UDP, VQP, dport=1589) bind_layers(VQP, VQPEntry,) bind_layers(VQPEntry, VQPEntry,) diff --git a/scapy/contrib/vtp.py b/scapy/contrib/vtp.py index 8ae85709ce7..fa53d490783 100644 --- a/scapy/contrib/vtp.py +++ b/scapy/contrib/vtp.py @@ -1,16 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . +# See https://scapy.net/ for more information # scapy.contrib.description = VLAN Trunking Protocol (VTP) # scapy.contrib.status = loads diff --git a/scapy/contrib/wireguard.py b/scapy/contrib/wireguard.py index 2263d00095b..0e7e0b1cea9 100644 --- a/scapy/contrib/wireguard.py +++ b/scapy/contrib/wireguard.py @@ -1,17 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # scapy.contrib.description = WireGuard # scapy.contrib.status = loads diff --git a/scapy/contrib/wpa_eapol.py b/scapy/contrib/wpa_eapol.py deleted file mode 100644 index 388ca77f7c0..00000000000 --- a/scapy/contrib/wpa_eapol.py +++ /dev/null @@ -1,51 +0,0 @@ -# This file is part of Scapy -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - -# scapy.contrib.description = WPA EAPOL-KEY -# scapy.contrib.status = loads - -from scapy.packet import Packet, bind_layers -from scapy.fields import ByteField, LenField, ShortField, StrFixedLenField, \ - StrLenField -from scapy.layers.eap import EAPOL - - -class WPA_key(Packet): - name = "WPA_key" - fields_desc = [ByteField("descriptor_type", 1), - ShortField("key_info", 0), - LenField("len", None, "H"), - StrFixedLenField("replay_counter", "", 8), - StrFixedLenField("nonce", "", 32), - StrFixedLenField("key_iv", "", 16), - StrFixedLenField("wpa_key_rsc", "", 8), - StrFixedLenField("wpa_key_id", "", 8), - StrFixedLenField("wpa_key_mic", "", 16), - LenField("wpa_key_length", None, "H"), - StrLenField("wpa_key", "", length_from=lambda pkt:pkt.wpa_key_length)] # noqa: E501 - - def extract_padding(self, s): - tmp_len = self.len - return s[:tmp_len], s[tmp_len:] - - def hashret(self): - return chr(self.type) + self.payload.hashret() - - def answers(self, other): - if isinstance(other, WPA_key): - return 1 - return 0 - - -bind_layers(EAPOL, WPA_key, type=3) diff --git a/scapy/dadict.py b/scapy/dadict.py index d1b2bc259fc..fa3679e6746 100644 --- a/scapy/dadict.py +++ b/scapy/dadict.py @@ -1,24 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Direct Access dictionary. """ -from __future__ import absolute_import -from __future__ import print_function from scapy.error import Scapy_Exception -import scapy.modules.six as six from scapy.compat import plain_str +# Typing +from typing import ( + Any, + Dict, + Generic, + Iterator, + List, + Tuple, + Type, + TypeVar, + Union, +) +from scapy.compat import Self + + ############################### # Direct Access dictionary # ############################### def fixname(x): + # type: (Union[bytes, str]) -> str + """ + Modifies a string to make sure it can be used as an attribute name. + """ + x = plain_str(x) if x and str(x[0]) in "0123456789": x = "n_" + x return x.translate( @@ -34,89 +51,116 @@ class DADict_Exception(Scapy_Exception): pass -class DADict: - def __init__(self, _name="DADict", **kargs): - self._name = _name - self.update(kargs) +_K = TypeVar('_K') # Key type +_V = TypeVar('_V') # Value type - def fixname(self, val): - return fixname(plain_str(val)) - def __contains__(self, val): - return val in self.__dict__ +class DADict(Generic[_K, _V]): + """ + Direct Access Dictionary - def __getitem__(self, attr): - return getattr(self, attr) - - def __setitem__(self, attr, val): - return setattr(self, self.fixname(attr), val) + This acts like a dict, but it provides a direct attribute access + to its keys through its values. This is used to store protocols, + manuf... - def __iter__(self): - return (value for key, value in six.iteritems(self.__dict__) - if key and key[0] != '_') + For instance, scapy fields will use a DADict as an enum:: - def _show(self): - for k in self.__dict__: - if k and k[0] != "_": - print("%10s = %r" % (k, getattr(self, k))) + ETHER_TYPES[2048] -> IPv4 - def __repr__(self): - return "<%s - %s elements>" % (self._name, len(self.__dict__)) + Whereas humans can access:: - def _branch(self, br, uniq=0): - if uniq and br._name in self: - raise DADict_Exception("DADict: [%s] already branched in [%s]" % (br._name, self._name)) # noqa: E501 - self[br._name] = br + ETHER_TYPES.IPv4 -> 2048 + """ + __slots__ = ["_name", "d"] - def _my_find(self, *args, **kargs): - if args and self._name not in args: - return False - return all(k in self and self[k] == v for k, v in six.iteritems(kargs)) + def __init__(self, _name="DADict", **kargs): + # type: (str, **Any) -> None + self._name = _name + self.d = {} # type: Dict[_K, _V] + self.update(kargs) # type: ignore + + def ident(self, v): + # type: (_V) -> str + """ + Return value that is used as key for the direct access + """ + if isinstance(v, (str, bytes)): + return fixname(v) + return "unknown" def update(self, *args, **kwargs): - for k, v in six.iteritems(dict(*args, **kwargs)): - self[k] = v - - def _find(self, *args, **kargs): - return self._recurs_find((), *args, **kargs) - - def _recurs_find(self, path, *args, **kargs): - if self in path: - return None - if self._my_find(*args, **kargs): - return self - for o in self: - if isinstance(o, DADict): - p = o._recurs_find(path + (self,), *args, **kargs) - if p is not None: - return p - return None - - def _find_all(self, *args, **kargs): - return self._recurs_find_all((), *args, **kargs) - - def _recurs_find_all(self, path, *args, **kargs): - r = [] - if self in path: - return r - if self._my_find(*args, **kargs): - r.append(self) - for o in self: - if isinstance(o, DADict): - p = o._recurs_find_all(path + (self,), *args, **kargs) - r += p - return r + # type: (*Dict[_K, _V], **Dict[_K, _V]) -> None + for k, v in dict(*args, **kwargs).items(): + self[k] = v # type: ignore + + def iterkeys(self): + # type: () -> Iterator[_K] + for x in self.d: + if not isinstance(x, str) or x[0] != "_": + yield x def keys(self): + # type: () -> List[_K] return list(self.iterkeys()) - def iterkeys(self): - return (x for x in self.__dict__ if x and x[0] != "_") + def __iter__(self): + # type: () -> Iterator[_K] + return self.iterkeys() + + def itervalues(self): + # type: () -> Iterator[_V] + return self.d.values() # type: ignore + + def values(self): + # type: () -> List[_V] + return list(self.itervalues()) + + def _show(self): + # type: () -> None + for k in self.iterkeys(): + print("%10s = %r" % (k, self[k])) + + def __repr__(self): + # type: () -> str + return "<%s - %s elements>" % (self._name, len(self)) + + def __getitem__(self, attr): + # type: (_K) -> _V + return self.d[attr] + + def __setitem__(self, attr, val): + # type: (_K, _V) -> None + self.d[attr] = val def __len__(self): - return len(self.__dict__) + # type: () -> int + return len(self.d) def __nonzero__(self): + # type: () -> bool # Always has at least its name - return len(self.__dict__) > 1 + return len(self) > 1 __bool__ = __nonzero__ + + def __getattr__(self, attr): + # type: (str) -> _K + try: + return object.__getattribute__(self, attr) # type: ignore + except AttributeError: + for k, v in self.d.items(): + if self.ident(v) == attr: + return k + raise AttributeError + + def __dir__(self): + # type: () -> List[str] + return [self.ident(x) for x in self.itervalues()] + + def __reduce__(self): + # type: () -> Tuple[Type[Self], Tuple[str], Tuple[Dict[_K, _V]]] + return (self.__class__, (self._name,), (self.d,)) + + def __setstate__(self, state): + # type: (Tuple[Dict[_K, _V]]) -> Self + self.d.update(state[0]) + return self diff --git a/scapy/data.py b/scapy/data.py index 632bc0231a9..a9e16b1938f 100644 --- a/scapy/data.py +++ b/scapy/data.py @@ -1,22 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Global variables and functions for handling external data sets. """ import calendar +import hashlib import os -import re +import pickle +import warnings - -from scapy.dadict import DADict +from scapy.dadict import DADict, fixname from scapy.consts import FREEBSD, NETBSD, OPENBSD, WINDOWS from scapy.error import log_loading -from scapy.compat import plain_str -import scapy.modules.six as six + +# Typing imports +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Tuple, + Union, + cast, +) +from scapy.compat import DecoratorCallable ############ @@ -106,16 +119,23 @@ DLT_PPP_WITH_DIR = 204 DLT_FC_2 = 224 DLT_CAN_SOCKETCAN = 227 -DLT_IPV4 = 228 -DLT_IPV6 = 229 +if OPENBSD: + DLT_IPV4 = DLT_RAW + DLT_IPV6 = DLT_RAW +else: + DLT_IPV4 = 228 + DLT_IPV6 = 229 DLT_IEEE802_15_4_NOFCS = 230 DLT_USBPCAP = 249 DLT_NETLINK = 253 DLT_USB_DARWIN = 266 DLT_BLUETOOTH_LE_LL = 251 +DLT_BLUETOOTH_LINUX_MONITOR = 254 DLT_BLUETOOTH_LE_LL_WITH_PHDR = 256 DLT_VSOCK = 271 +DLT_NORDIC_BLE = 272 DLT_ETHERNET_MPACKET = 274 +DLT_LINUX_SLL2 = 276 # From net/ipv6.h on Linux (+ Additions) IPV6_ADDR_UNICAST = 0x01 @@ -182,7 +202,7 @@ ARPHRD_CHAOS: DLT_CHAOS, ARPHRD_CAN: DLT_LINUX_SLL, ARPHRD_IEEE802_TR: DLT_IEEE802, - ARPHRD_IEEE802: DLT_IEEE802, + ARPHRD_IEEE802: DLT_EN10MB, ARPHRD_ARCNET: DLT_ARCNET_LINUX, ARPHRD_FDDI: DLT_FDDI, ARPHRD_ATM: -1, @@ -272,24 +292,82 @@ } -def load_protocols(filename, _fallback=None, _integer_base=10): - """"Parse /etc/protocols and return values as a dictionary.""" - spaces = re.compile(b"[ \t]+|\n") - dct = DADict(_name=filename) +def scapy_data_cache(name): + # type: (str) -> Callable[[DecoratorCallable], DecoratorCallable] + """ + This decorator caches the loading of 'data' dictionaries, in order to reduce + loading times. + """ + from scapy.main import SCAPY_CACHE_FOLDER + if SCAPY_CACHE_FOLDER is None: + # Cannot cache. + return lambda x: x + cachepath = SCAPY_CACHE_FOLDER / (name + ".pickle") + + def _cached_loader(func, name=name): + # type: (DecoratorCallable, str) -> DecoratorCallable + def load(filename=None): + # type: (Optional[str]) -> Any + cache_id = hashlib.sha256((filename or "").encode()).hexdigest() + if cachepath.exists(): + try: + with cachepath.open("rb") as fd: + data = pickle.load(fd) + if data["id"] == cache_id: + return data["content"] + except Exception as ex: + log_loading.info( + "Couldn't load cache from %s: %s" % ( + str(cachepath), + str(ex), + ) + ) + cachepath.unlink(missing_ok=True) + # Cache does not exist or is invalid. + content = func(filename) + data = { + "content": content, + "id": cache_id, + } + try: + cachepath.parent.mkdir(parents=True, exist_ok=True) + with cachepath.open("wb") as fd: + pickle.dump(data, fd) + return content + except Exception as ex: + log_loading.info( + "Couldn't write cache into %s: %s" % ( + str(cachepath), + str(ex) + ) + ) + return content + return load # type: ignore + return _cached_loader + + +def load_protocols(filename, _fallback=None, _integer_base=10, + _cls=DADict[int, str]): + # type: (str, Optional[Callable[[], Iterator[str]]], int, type) -> DADict[int, str] + """" + Parse /etc/protocols and return values as a dictionary. + """ + dct = _cls(_name=filename) # type: DADict[int, str] def _process_data(fdesc): + # type: (Iterator[str]) -> None for line in fdesc: try: - shrp = line.find(b"#") + shrp = line.find("#") if shrp >= 0: line = line[:shrp] line = line.strip() if not line: continue - lt = tuple(re.split(spaces, line)) + lt = tuple(line.split()) if len(lt) < 2 or not lt[0]: continue - dct[lt[0]] = int(lt[1], _integer_base) + dct[int(lt[1], _integer_base)] = fixname(lt[0]) except Exception as e: log_loading.info( "Couldn't parse file [%s]: line [%r] (%s)", @@ -300,27 +378,67 @@ def _process_data(fdesc): try: if not filename: raise IOError - with open(filename, "rb") as fdesc: + with open(filename, "r", errors="backslashreplace") as fdesc: _process_data(fdesc) except IOError: if _fallback: - _process_data(_fallback.split(b"\n")) + _process_data(_fallback()) else: log_loading.info("Can't open %s file", filename) return dct -def load_ethertypes(filename): +class EtherDA(DADict[int, str]): + # Backward compatibility: accept + # ETHER_TYPES["MY_GREAT_TYPE"] = 12 + def __setitem__(self, attr, val): + # type: (int, str) -> None + if isinstance(attr, str): + attr, val = val, attr + warnings.warn( + "ETHER_TYPES now uses the integer value as key !", + DeprecationWarning + ) + super(EtherDA, self).__setitem__(attr, val) + + def __getitem__(self, attr): + # type: (int) -> Any + if isinstance(attr, str): + warnings.warn( + "Please use 'ETHER_TYPES.%s'" % attr, + DeprecationWarning + ) + return super(EtherDA, self).__getattr__(attr) + return super(EtherDA, self).__getitem__(attr) + + +@scapy_data_cache("ethertypes") +def load_ethertypes(filename=None): + # type: (Optional[str]) -> EtherDA """"Parse /etc/ethertypes and return values as a dictionary. If unavailable, use the copy bundled with Scapy.""" - from scapy.libs.ethertypes import DATA - return load_protocols(filename, _fallback=DATA, _integer_base=16) - - + def _fallback() -> Iterator[str]: + # Fallback. Lazy loaded as the file is big. + from scapy.libs.ethertypes import DATA + return iter(DATA.split("\n")) + prot = load_protocols(filename or "scapy/ethertypes", + _fallback=_fallback, + _integer_base=16, + _cls=EtherDA) + return cast(EtherDA, prot) + + +@scapy_data_cache("services") def load_services(filename): - spaces = re.compile(b"[ \t]+|\n") - tdct = DADict(_name="%s-tcp" % filename) - udct = DADict(_name="%s-udp" % filename) + # type: (str) -> Tuple[DADict[int, str], DADict[int, str], DADict[int, str]] # noqa: E501 + tdct = DADict(_name="%s-tcp" % filename) # type: DADict[int, str] + udct = DADict(_name="%s-udp" % filename) # type: DADict[int, str] + sdct = DADict(_name="%s-sctp" % filename) # type: DADict[int, str] + dcts = { + b"tcp": tdct, + b"udp": udct, + b"sctp": sdct, + } try: with open(filename, "rb") as fdesc: for line in fdesc: @@ -331,13 +449,23 @@ def load_services(filename): line = line.strip() if not line: continue - lt = tuple(re.split(spaces, line)) + lt = tuple(line.split()) if len(lt) < 2 or not lt[0]: continue - if lt[1].endswith(b"/tcp"): - tdct[lt[0]] = int(lt[1].split(b'/')[0]) - elif lt[1].endswith(b"/udp"): - udct[lt[0]] = int(lt[1].split(b'/')[0]) + if b"/" not in lt[1]: + continue + port, proto = lt[1].split(b"/", 1) + try: + dtct = dcts[proto] + except KeyError: + continue + name = fixname(lt[0]) + if b"-" in port: + sport, eport = port.split(b"-") + for i in range(int(sport), int(eport) + 1): + dtct[i] = name + else: + dtct[int(port)] = name except Exception as e: log_loading.warning( "Couldn't parse file [%s]: line [%r] (%s)", @@ -347,38 +475,41 @@ def load_services(filename): ) except IOError: log_loading.info("Can't open /etc/services file") - return tdct, udct - + return tdct, udct, sdct -class ManufDA(DADict): - def fixname(self, val): - return plain_str(val) - def __dir__(self): - return ["lookup", "reverse_lookup"] +class ManufDA(DADict[str, Tuple[str, str]]): + def ident(self, v): + # type: (Any) -> str + return fixname(v[0] if isinstance(v, tuple) else v) def _get_manuf_couple(self, mac): + # type: (str) -> Tuple[str, str] oui = ":".join(mac.split(":")[:3]).upper() - return self.__dict__.get(oui, (mac, mac)) + return self.d.get(oui, (mac, mac)) def _get_manuf(self, mac): + # type: (str) -> str return self._get_manuf_couple(mac)[1] def _get_short_manuf(self, mac): + # type: (str) -> str return self._get_manuf_couple(mac)[0] def _resolve_MAC(self, mac): + # type: (str) -> str oui = ":".join(mac.split(":")[:3]).upper() if oui in self: return ":".join([self[oui][0]] + mac.split(":")[3:]) return mac def lookup(self, mac): + # type: (str) -> Tuple[str, str] """Find OUI name matching to a MAC""" - oui = ":".join(mac.split(":")[:3]).upper() - return self[oui] + return self._get_manuf_couple(mac) def reverse_lookup(self, name, case_sensitive=False): + # type: (str, bool) -> Dict[str, str] """ Find all MACs registered to a OUI @@ -387,83 +518,130 @@ def reverse_lookup(self, name, case_sensitive=False): :returns: a dict of mac:tuples (Name, Extended Name) """ if case_sensitive: - filtr = lambda x, l: any(x == z for z in l) + filtr = lambda x, l: any(x in z for z in l) # type: Callable[[str, Tuple[str, str]], bool] # noqa: E501 else: name = name.lower() - filtr = lambda x, l: any(x == z.lower() for z in l) - return {k: v for k, v in six.iteritems(self.__dict__) - if filtr(name, v)} - + filtr = lambda x, l: any(x in z.lower() for z in l) + return {k: v for k, v in self.d.items() if filtr(name, v)} # type: ignore -def load_manuf(filename): + def __dir__(self): + # type: () -> List[str] + return [ + "_get_manuf", + "_get_short_manuf", + "_resolve_MAC", + "loopkup", + "reverse_lookup", + ] + super(ManufDA, self).__dir__() + + +@scapy_data_cache("manufdb") +def load_manuf(filename=None): + # type: (Optional[str]) -> ManufDA """ Loads manuf file from Wireshark. :param filename: the file to load the manuf file from :returns: a ManufDA filled object """ - manufdb = ManufDA(_name=filename) - with open(filename, "rb") as fdesc: + manufdb = ManufDA(_name=filename or "scapy/manufdb") + + def _process_data(fdesc): + # type: (Iterator[str]) -> None for line in fdesc: try: line = line.strip() - if not line or line.startswith(b"#"): + if not line or line.startswith("#"): continue parts = line.split(None, 2) oui, shrt = parts[:2] - lng = parts[2].lstrip(b"#").strip() if len(parts) > 2 else "" + lng = parts[2].lstrip("#").strip() if len(parts) > 2 else "" lng = lng or shrt - manufdb[oui] = plain_str(shrt), plain_str(lng) + manufdb[oui] = shrt, lng except Exception: log_loading.warning("Couldn't parse one line from [%s] [%r]", filename, line, exc_info=True) + + try: + if not filename: + raise IOError + with open(filename, "r", errors="backslashreplace") as fdesc: + _process_data(fdesc) + except IOError: + # Fallback. Lazy loaded as the file is big. + from scapy.libs.manuf import DATA + _process_data(iter(DATA.split("\n"))) return manufdb +@scapy_data_cache("bluetoothids") +def load_bluetoothids(filename=None): + # type: (Optional[str]) -> Dict[int, str] + """Load Bluetooth IDs into the cache""" + from scapy.libs.bluetoothids import DATA + return cast(Dict[int, str], DATA) + + def select_path(directories, filename): + # type: (List[str], str) -> Optional[str] """Find filename among several directories""" for directory in directories: path = os.path.join(directory, filename) if os.path.exists(path): return path + return None if WINDOWS: - IP_PROTOS = load_protocols(os.environ["SystemRoot"] + "\\system32\\drivers\\etc\\protocol") # noqa: E501 - TCP_SERVICES, UDP_SERVICES = load_services(os.environ["SystemRoot"] + "\\system32\\drivers\\etc\\services") # noqa: E501 - # Default values, will be updated by arch.windows - ETHER_TYPES = load_ethertypes(None) - MANUFDB = ManufDA() + IP_PROTOS = load_protocols(os.path.join( + os.environ["SystemRoot"], + "system32", + "drivers", + "etc", + "protocol", + )) + TCP_SERVICES, UDP_SERVICES, SCTP_SERVICES = load_services(os.path.join( + os.environ["SystemRoot"], + "system32", + "drivers", + "etc", + "services", + )) + ETHER_TYPES = load_ethertypes() + MANUFDB = load_manuf() else: IP_PROTOS = load_protocols("/etc/protocols") + TCP_SERVICES, UDP_SERVICES, SCTP_SERVICES = load_services("/etc/services") ETHER_TYPES = load_ethertypes("/etc/ethertypes") - TCP_SERVICES, UDP_SERVICES = load_services("/etc/services") - MANUFDB = ManufDA() - manuf_path = select_path( - ['/usr', '/usr/local', '/opt', '/opt/wireshark', - '/Applications/Wireshark.app/Contents/Resources'], - "share/wireshark/manuf" + MANUFDB = load_manuf( + select_path( + ['/usr', '/usr/local', '/opt', '/opt/wireshark', + '/Applications/Wireshark.app/Contents/Resources'], + "share/wireshark/manuf" + ) ) - if manuf_path: - try: - MANUFDB = load_manuf(manuf_path) - except (IOError, OSError): - log_loading.warning("Cannot read wireshark manuf database") + +BLUETOOTH_CORE_COMPANY_IDENTIFIERS = load_bluetoothids() ##################### # knowledge bases # ##################### +KBBaseType = Optional[Union[str, List[Tuple[str, Dict[str, Dict[str, str]]]]]] -class KnowledgeBase: + +class KnowledgeBase(object): def __init__(self, filename): + # type: (Optional[Any]) -> None self.filename = filename - self.base = None + self.base = None # type: KBBaseType def lazy_init(self): + # type: () -> None self.base = "" def reload(self, filename=None): + # type: (Optional[Any]) -> None if filename is not None: self.filename = filename oldbase = self.base @@ -473,6 +651,7 @@ def reload(self, filename=None): self.base = oldbase def get_base(self): + # type: () -> Union[str, List[Tuple[str, Dict[str,Dict[str,str]]]]] if self.base is None: self.lazy_init() - return self.base + return cast(Union[str, List[Tuple[str, Dict[str, Dict[str, str]]]]], self.base) diff --git a/scapy/error.py b/scapy/error.py index 636008219b3..ff3fdc13eb4 100644 --- a/scapy/error.py +++ b/scapy/error.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Logging subsystem and basic exception class. @@ -16,6 +16,16 @@ import traceback import time +from scapy.consts import WINDOWS + +# Typing imports +from logging import LogRecord +from typing import ( + Any, + Dict, + Tuple, +) + class Scapy_Exception(Exception): pass @@ -31,16 +41,21 @@ class ScapyNoDstMacException(Scapy_Exception): class ScapyFreqFilter(logging.Filter): def __init__(self): + # type: () -> None logging.Filter.__init__(self) - self.warning_table = {} + self.warning_table = {} # type: Dict[int, Tuple[float, int]] # noqa: E501 def filter(self, record): + # type: (LogRecord) -> bool from scapy.config import conf + # Levels below INFO are not covered + if record.levelno <= logging.INFO: + return True wt = conf.warning_threshold if wt > 0: stk = traceback.extract_stack() - caller = None - for f, l, n, c in stk: + caller = 0 # type: int + for _, l, n, _ in stk: if n == 'warning': break caller = l @@ -53,14 +68,13 @@ def filter(self, record): if nb < 2: nb += 1 if nb == 2: - record.msg = "more " + record.msg + record.msg = "more " + str(record.msg) else: - return 0 + return False self.warning_table[caller] = (tm, nb) - return 1 + return True -# Inspired from python-colorbg (MIT) class ScapyColoredFormatter(logging.Formatter): """A subclass of logging.Formatter that handles colors.""" levels_colored = { @@ -72,6 +86,7 @@ class ScapyColoredFormatter(logging.Formatter): } def format(self, record): + # type: (LogRecord) -> str message = super(ScapyColoredFormatter, self).format(record) from scapy.config import conf message = conf.color_theme.format( @@ -81,9 +96,30 @@ def format(self, record): return message +if WINDOWS: + # colorama is bundled within IPython, but + # logging.StreamHandler will be overwritten when called, + # so we can't wait for IPython to call it + try: + import colorama + colorama.init() + except ImportError: + pass + +# get Scapy's master logger log_scapy = logging.getLogger("scapy") -log_scapy.setLevel(logging.WARNING) -log_scapy.addHandler(logging.NullHandler()) +log_scapy.propagate = False +# override the level if not already set +if log_scapy.level == logging.NOTSET: + log_scapy.setLevel(logging.WARNING) +# add a custom handler controlled by Scapy's config +_handler = logging.StreamHandler() +_handler.setFormatter( + ScapyColoredFormatter( + "%(levelname)s: %(message)s", + ) +) +log_scapy.addHandler(_handler) # logs at runtime log_runtime = logging.getLogger("scapy.runtime") log_runtime.addFilter(ScapyFreqFilter()) @@ -95,6 +131,7 @@ def format(self, record): def warning(x, *args, **kargs): + # type: (str, *Any, **Any) -> None """ Prints a warning during runtime. """ diff --git a/scapy/fields.py b/scapy/fields.py index fc13aaa459c..46064b726e2 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -1,25 +1,28 @@ -# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*- +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi # Copyright (C) Michael Farrell -# This program is published under a GPLv2 license + """ Fields: basic data structures that make up parts of packets. """ -from __future__ import absolute_import import calendar import collections import copy +import datetime import inspect +import math import socket import struct import time +import warnings + from types import MethodType from uuid import UUID - +from enum import Enum from scapy.config import conf from scapy.dadict import DADict @@ -29,41 +32,104 @@ RandSLong, RandFloat from scapy.data import EPOCH from scapy.error import log_runtime, Scapy_Exception -from scapy.compat import bytes_hex, chb, orb, plain_str, raw, bytes_encode +from scapy.compat import bytes_hex, plain_str, raw, bytes_encode from scapy.pton_ntop import inet_ntop, inet_pton -from scapy.utils import inet_aton, inet_ntoa, lhex, mac2str, str2mac +from scapy.utils import inet_aton, inet_ntoa, lhex, mac2str, str2mac, EDecimal from scapy.utils6 import in6_6to4ExtractAddr, in6_isaddr6to4, \ in6_isaddrTeredo, in6_ptop, Net6, teredoAddrExtractInfo -from scapy.base_classes import BasePacket, Gen, Net, Field_metaclass -from scapy.error import warning -import scapy.modules.six as six -from scapy.modules.six.moves import range +from scapy.base_classes import ( + _ScopedIP, + BasePacket, + Field_metaclass, + Net, + ScopedIP, +) + +# Typing imports +from typing import ( + Any, + AnyStr, + Callable, + Dict, + List, + Generic, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + # func + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + # Do not import on runtime ! (import loop) + from scapy.packet import Packet + + +class RawVal: + r""" + A raw value that will not be processed by the field and inserted + as-is in the packet string. + + Example:: + + >>> a = IP(len=RawVal("####")) + >>> bytes(a) + b'F\x00####\x00\x01\x00\x005\xb5\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00' + + """ + def __init__(self, val=b""): + # type: (bytes) -> None + self.val = bytes_encode(val) -""" -Helper class to specify a protocol extendable for runtime modifications -""" + def __str__(self): + # type: () -> str + return str(self.val) + def __bytes__(self): + # type: () -> bytes + return self.val + + def __len__(self): + # type: () -> int + return len(self.val) + + def __repr__(self): + # type: () -> str + return "" % self.val + + +class ObservableDict(Dict[int, str]): + """ + Helper class to specify a protocol extendable for runtime modifications + """ -class ObservableDict(dict): def __init__(self, *args, **kw): - self.observers = [] + # type: (*Dict[int, str], **Any) -> None + self.observers = [] # type: List[_EnumField[Any]] super(ObservableDict, self).__init__(*args, **kw) def observe(self, observer): + # type: (_EnumField[Any]) -> None self.observers.append(observer) def __setitem__(self, key, value): + # type: (int, str) -> None for o in self.observers: o.notify_set(self, key, value) super(ObservableDict, self).__setitem__(key, value) def __delitem__(self, key): + # type: (int) -> None for o in self.observers: o.notify_del(self, key) super(ObservableDict, self).__delitem__(key) - def update(self, anotherDict): + def update(self, anotherDict): # type: ignore for k in anotherDict: self[k] = anotherDict[k] @@ -72,11 +138,16 @@ def update(self, anotherDict): # Fields # ############ -class Field(six.with_metaclass(Field_metaclass, object)): +I = TypeVar('I') # Internal storage # noqa: E741 +M = TypeVar('M') # Machine storage + + +class Field(Generic[I, M], metaclass=Field_metaclass): """ - For more information on how this work, please refer to - http://www.secdev.org/projects/scapy/files/scapydoc.pdf - chapter ``Adding a New Field`` + For more information on how this works, please refer to the + 'Adding new protocols' chapter in the online documentation: + + https://scapy.readthedocs.io/en/stable/build_dissect.html """ __slots__ = [ "name", @@ -91,6 +162,9 @@ class Field(six.with_metaclass(Field_metaclass, object)): holds_packets = 0 def __init__(self, name, default, fmt="H"): + # type: (str, Any, str) -> None + if not isinstance(name, str): + raise ValueError("name should be a string") self.name = name if fmt[0] in "@=<>!": self.fmt = fmt @@ -98,58 +172,82 @@ def __init__(self, name, default, fmt="H"): self.fmt = "!" + fmt self.struct = struct.Struct(self.fmt) self.default = self.any2i(None, default) - self.sz = struct.calcsize(self.fmt) - self.owners = [] + self.sz = struct.calcsize(self.fmt) # type: int + self.owners = [] # type: List[Type[Packet]] def register_owner(self, cls): + # type: (Type[Packet]) -> None self.owners.append(cls) - def i2len(self, pkt, x): + def i2len(self, + pkt, # type: Packet + x, # type: Any + ): + # type: (...) -> int """Convert internal value to a length usable by a FieldLenField""" + if isinstance(x, RawVal): + return len(x) return self.sz def i2count(self, pkt, x): + # type: (Optional[Packet], I) -> int """Convert internal value to a number of elements usable by a FieldLenField. Always 1 except for list fields""" return 1 def h2i(self, pkt, x): + # type: (Optional[Packet], Any) -> I """Convert human value to internal value""" - return x + return cast(I, x) def i2h(self, pkt, x): + # type: (Optional[Packet], I) -> Any """Convert internal value to human value""" return x def m2i(self, pkt, x): + # type: (Optional[Packet], M) -> I """Convert machine value to internal value""" - return x + return cast(I, x) def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[I]) -> M """Convert internal value to machine value""" if x is None: - x = 0 + return cast(M, 0) elif isinstance(x, str): - return bytes_encode(x) - return x + return cast(M, bytes_encode(x)) + return cast(M, x) def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> Optional[I] """Try to understand the most input values possible and make an internal value from them""" # noqa: E501 return self.h2i(pkt, x) def i2repr(self, pkt, x): + # type: (Optional[Packet], I) -> str """Convert internal value to a nice representation""" return repr(self.i2h(pkt, x)) def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[I]) -> bytes """Add an internal value to a string Copy the network representation of field `val` (belonging to layer `pkt`) to the raw string packet `s`, and return the new string packet. """ - return s + self.struct.pack(self.i2m(pkt, val)) + try: + return s + self.struct.pack(self.i2m(pkt, val)) + except struct.error as ex: + raise ValueError( + "Incorrect type of value for field %s:\n" % self.name + + "struct.error('%s')\n" % ex + + "To inject bytes into the field regardless of the type, " + + "use RawVal. See help(RawVal)" + ) def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, I] """Extract an internal value from a string Extract from the raw packet `s` the field value belonging to layer @@ -162,22 +260,31 @@ def getfield(self, pkt, s): return s[self.sz:], self.m2i(pkt, self.struct.unpack(s[:self.sz])[0]) def do_copy(self, x): - if hasattr(x, "copy"): - return x.copy() + # type: (I) -> I if isinstance(x, list): - x = x[:] + x = x[:] # type: ignore for i in range(len(x)): if isinstance(x[i], BasePacket): x[i] = x[i].copy() + return x # type: ignore + if hasattr(x, "copy"): + return x.copy() # type: ignore return x def __repr__(self): - return "" % (",".join(x.__name__ for x in self.owners), self.name) # noqa: E501 + # type: () -> str + return "<%s (%s).%s>" % ( + self.__class__.__name__, + ",".join(x.__name__ for x in self.owners), + self.name + ) def copy(self): + # type: () -> Field[I, M] return copy.copy(self) def randval(self): + # type: () -> VolatileValue[Any] """Return a volatile object whose value is both random and suitable for this field""" # noqa: E501 fmtt = self.fmt[-1] if fmtt in "BbHhIiQq": @@ -192,112 +299,203 @@ def randval(self): value = int(self.fmt[1:-1]) return RandBin(value) else: - warning("no random class for [%s] (fmt=%s).", self.name, self.fmt) + raise ValueError( + "no random class for [%s] (fmt=%s)." % ( + self.name, self.fmt + ) + ) + + +class _FieldContainer(object): + """ + A field that acts as a container for another field + """ + __slots__ = ["fld"] + def __getattr__(self, attr): + # type: (str) -> Any + return getattr(self.fld, attr) + + +AnyField = Union[Field[Any, Any], _FieldContainer] -class Emph(object): + +class Emph(_FieldContainer): """Empathize sub-layer for display""" __slots__ = ["fld"] def __init__(self, fld): + # type: (Any) -> None self.fld = fld - def __getattr__(self, attr): - return getattr(self.fld, attr) - def __eq__(self, other): - return self.fld == other + # type: (Any) -> bool + return bool(self.fld == other) - def __ne__(self, other): - # Python 2.7 compat - return not self == other + def __hash__(self): + # type: () -> int + return hash(self.fld) + + +class MayEnd(_FieldContainer): + """ + Allow packet dissection to end after the dissection of this field + if no bytes are left. + + A good example would be a length field that can be 0 or a set value, + and where it would be too annoying to use multiple ConditionalFields + + Important note: any field below this one MUST default + to an empty value, else the behavior will be unexpected. + """ + __slots__ = ["fld"] + + def __init__(self, fld): + # type: (Any) -> None + self.fld = fld - __hash__ = None + def __eq__(self, other): + # type: (Any) -> bool + return bool(self.fld == other) + + def __hash__(self): + # type: () -> int + return hash(self.fld) -class ActionField(object): - __slots__ = ["_fld", "_action_method", "_privdata"] +class ActionField(_FieldContainer): + __slots__ = ["fld", "_action_method", "_privdata"] def __init__(self, fld, action_method, **kargs): - self._fld = fld + # type: (Field[Any, Any], str, **Any) -> None + self.fld = fld self._action_method = action_method self._privdata = kargs def any2i(self, pkt, val): - getattr(pkt, self._action_method)(val, self._fld, **self._privdata) - return getattr(self._fld, "any2i")(pkt, val) - - def __getattr__(self, attr): - return getattr(self._fld, attr) + # type: (Optional[Packet], int) -> Any + getattr(pkt, self._action_method)(val, self.fld, **self._privdata) + return getattr(self.fld, "any2i")(pkt, val) -class ConditionalField(object): +class ConditionalField(_FieldContainer): __slots__ = ["fld", "cond"] - def __init__(self, fld, cond): + def __init__(self, + fld, # type: AnyField + cond # type: Callable[[Packet], bool] + ): + # type: (...) -> None self.fld = fld self.cond = cond def _evalcond(self, pkt): - return self.cond(pkt) + # type: (Packet) -> bool + return bool(self.cond(pkt)) + + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> Any + # BACKWARD COMPATIBILITY + # Note: we shouldn't need this function. (it's not correct) + # However, having i2h implemented (#2364), it changes the default + # behavior and broke all packets that wrongly use two ConditionalField + # with the same name. Those packets are the problem: they are wrongly + # built (they should either be reusing the same conditional field, or + # using a MultipleTypeField). + # But I don't want to dive into fixing all of them just yet, + # so for now, let's keep this this way, even though it's not correct. + if type(self.fld) is Field: + return x + return self.fld.any2i(pkt, x) + + def i2h(self, pkt, val): + # type: (Optional[Packet], Any) -> Any + if pkt and not self._evalcond(pkt): + return None + return self.fld.i2h(pkt, val) def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, Any] if self._evalcond(pkt): return self.fld.getfield(pkt, s) else: return s, None def addfield(self, pkt, s, val): + # type: (Packet, bytes, Any) -> bytes if self._evalcond(pkt): return self.fld.addfield(pkt, s, val) else: return s def __getattr__(self, attr): + # type: (str) -> Any return getattr(self.fld, attr) -class MultipleTypeField(object): - """MultipleTypeField are used for fields that can be implemented by -various Field subclasses, depending on conditions on the packet. - -It is initialized with `flds` and `dflt`. - -`dflt` is the default field type, to be used when none of the -conditions matched the current packet. - -`flds` is a list of tuples (`fld`, `cond`), where `fld` if a field -type, and `cond` a "condition" to determine if `fld` is the field type -that should be used. +class MultipleTypeField(_FieldContainer): + """ + MultipleTypeField are used for fields that can be implemented by + various Field subclasses, depending on conditions on the packet. -`cond` is either: + It is initialized with `flds` and `dflt`. - - a callable `cond_pkt` that accepts one argument (the packet) and - returns True if `fld` should be used, False otherwise. + :param dflt: is the default field type, to be used when none of the + conditions matched the current packet. + :param flds: is a list of tuples (`fld`, `cond`) or (`fld`, `cond`, `hint`) + where `fld` if a field type, and `cond` a "condition" to + determine if `fld` is the field type that should be used. - - a tuple (`cond_pkt`, `cond_pkt_val`), where `cond_pkt` is the same - as in the previous case and `cond_pkt_val` is a callable that - accepts two arguments (the packet, and the value to be set) and - returns True if `fld` should be used, False otherwise. + ``cond`` is either: -See scapy.layers.l2.ARP (type "help(ARP)" in Scapy) for an example of -use. + - a callable `cond_pkt` that accepts one argument (the packet) and + returns True if `fld` should be used, False otherwise. + - a tuple (`cond_pkt`, `cond_pkt_val`), where `cond_pkt` is the same + as in the previous case and `cond_pkt_val` is a callable that + accepts two arguments (the packet, and the value to be set) and + returns True if `fld` should be used, False otherwise. + See scapy.layers.l2.ARP (type "help(ARP)" in Scapy) for an example of + use. """ - __slots__ = ["flds", "dflt", "name"] - - def __init__(self, flds, dflt): - self.flds = flds + __slots__ = ["flds", "dflt", "hints", "name", "default"] + + def __init__( + self, + flds: List[Union[ + Tuple[Field[Any, Any], Any, str], + Tuple[Field[Any, Any], Any] + ]], + dflt: Field[Any, Any] + ) -> None: + self.hints = { + x[0]: x[2] + for x in flds + if len(x) == 3 + } + self.flds = [ + (x[0], x[1]) for x in flds + ] self.dflt = dflt + self.default = None # So that we can detect changes in defaults self.name = self.dflt.name + if any(x[0].name != self.name for x in self.flds): + warnings.warn( + ("All fields should have the same name in a " + "MultipleTypeField (%s). Use hints.") % self.name, + SyntaxWarning + ) def _iterate_fields_cond(self, pkt, val, use_val): + # type: (Optional[Packet], Any, bool) -> Field[Any, Any] """Internal function used by _find_fld_pkt & _find_fld_pkt_val""" # Iterate through the fields for fld, cond in self.flds: if isinstance(cond, tuple): if use_val: + if val is None: + val = self.dflt.default if cond[1](pkt, val): return fld continue @@ -308,6 +506,7 @@ def _iterate_fields_cond(self, pkt, val, use_val): return self.dflt def _find_fld_pkt(self, pkt): + # type: (Optional[Packet]) -> Field[Any, Any] """Given a Packet instance `pkt`, returns the Field subclass to be used. If you know the value to be set (e.g., in .addfield()), use ._find_fld_pkt_val() instead. @@ -315,20 +514,22 @@ def _find_fld_pkt(self, pkt): """ return self._iterate_fields_cond(pkt, None, False) - def _find_fld_pkt_val(self, pkt, val): + def _find_fld_pkt_val(self, + pkt, # type: Optional[Packet] + val, # type: Any + ): + # type: (...) -> Tuple[Field[Any, Any], Any] """Given a Packet instance `pkt` and the value `val` to be set, returns the Field subclass to be used, and the updated `val` if necessary. """ fld = self._iterate_fields_cond(pkt, val, True) - # Default ? (in this case, let's make sure it's up-do-date) - dflts_pkt = pkt.default_fields - if val == dflts_pkt[self.name] and self.name not in pkt.fields: - dflts_pkt[self.name] = fld.default + if val is None: val = fld.default return fld, val def _find_fld(self): + # type: () -> Field[Any, Any] """Returns the Field subclass to be used, depending on the Packet instance, or the default subclass. @@ -342,7 +543,7 @@ def _find_fld(self): """ # Hack to preserve current Scapy API # See https://stackoverflow.com/a/7272464/3223422 - frame = inspect.currentframe().f_back.f_back + frame = inspect.currentframe().f_back.f_back # type: ignore while frame is not None: try: pkt = frame.f_locals['self'] @@ -357,194 +558,323 @@ def _find_fld(self): frame = frame.f_back return self.dflt - def getfield(self, pkt, s): + def getfield(self, + pkt, # type: Packet + s, # type: bytes + ): + # type: (...) -> Tuple[bytes, Any] return self._find_fld_pkt(pkt).getfield(pkt, s) def addfield(self, pkt, s, val): + # type: (Packet, bytes, Any) -> bytes fld, val = self._find_fld_pkt_val(pkt, val) return fld.addfield(pkt, s, val) def any2i(self, pkt, val): + # type: (Optional[Packet], Any) -> Any fld, val = self._find_fld_pkt_val(pkt, val) return fld.any2i(pkt, val) def h2i(self, pkt, val): + # type: (Optional[Packet], Any) -> Any fld, val = self._find_fld_pkt_val(pkt, val) return fld.h2i(pkt, val) - def i2h(self, pkt, val): + def i2h(self, + pkt, # type: Packet + val, # type: Any + ): + # type: (...) -> Any fld, val = self._find_fld_pkt_val(pkt, val) return fld.i2h(pkt, val) def i2m(self, pkt, val): + # type: (Optional[Packet], Optional[Any]) -> Any fld, val = self._find_fld_pkt_val(pkt, val) return fld.i2m(pkt, val) def i2len(self, pkt, val): + # type: (Packet, Any) -> int fld, val = self._find_fld_pkt_val(pkt, val) return fld.i2len(pkt, val) def i2repr(self, pkt, val): + # type: (Optional[Packet], Any) -> str fld, val = self._find_fld_pkt_val(pkt, val) - return fld.i2repr(pkt, val) + hint = "" + if fld in self.hints: + hint = " (%s)" % self.hints[fld] + return fld.i2repr(pkt, val) + hint def register_owner(self, cls): + # type: (Type[Packet]) -> None for fld, _ in self.flds: fld.owners.append(cls) self.dflt.owners.append(cls) - def __getattr__(self, attr): - return getattr(self._find_fld(), attr) + def get_fields_list(self): + # type: () -> List[Any] + return [self] + @property + def fld(self): + # type: () -> Field[Any, Any] + return self._find_fld() -class PadField(object): + +class PadField(_FieldContainer): """Add bytes after the proxified field so that it ends at the specified alignment from its beginning""" - __slots__ = ["_fld", "_align", "_padwith"] + __slots__ = ["fld", "_align", "_padwith"] def __init__(self, fld, align, padwith=None): - self._fld = fld + # type: (AnyField, int, Optional[bytes]) -> None + self.fld = fld self._align = align self._padwith = padwith or b"\x00" - def padlen(self, flen): + def padlen(self, flen, pkt): + # type: (int, Packet) -> int return -flen % self._align - def getfield(self, pkt, s): - remain, val = self._fld.getfield(pkt, s) - padlen = self.padlen(len(s) - len(remain)) + def getfield(self, + pkt, # type: Packet + s, # type: bytes + ): + # type: (...) -> Tuple[bytes, Any] + remain, val = self.fld.getfield(pkt, s) + padlen = self.padlen(len(s) - len(remain), pkt) return remain[padlen:], val - def addfield(self, pkt, s, val): - sval = self._fld.addfield(pkt, b"", val) - return s + sval + struct.pack("%is" % (self.padlen(len(sval))), self._padwith) # noqa: E501 - - def __getattr__(self, attr): - return getattr(self._fld, attr) + def addfield(self, + pkt, # type: Packet + s, # type: bytes + val, # type: Any + ): + # type: (...) -> bytes + sval = self.fld.addfield(pkt, b"", val) + return s + sval + ( + self.padlen(len(sval), pkt) * self._padwith + ) class ReversePadField(PadField): """Add bytes BEFORE the proxified field so that it starts at the specified alignment from its beginning""" - def getfield(self, pkt, s): + def original_length(self, pkt): + # type: (Packet) -> int + return len(pkt.original) + + def getfield(self, + pkt, # type: Packet + s, # type: bytes + ): + # type: (...) -> Tuple[bytes, Any] # We need to get the length that has already been dissected - padlen = self.padlen(len(pkt.original) - len(s)) - remain, val = self._fld.getfield(pkt, s[padlen:]) - return remain, val + padlen = self.padlen(self.original_length(pkt) - len(s), pkt) + return self.fld.getfield(pkt, s[padlen:]) + + def addfield(self, + pkt, # type: Packet + s, # type: bytes + val, # type: Any + ): + # type: (...) -> bytes + sval = self.fld.addfield(pkt, b"", val) + return s + struct.pack("%is" % ( + self.padlen(len(s), pkt) + ), self._padwith) + sval + + +class TrailerBytes(bytes): + """ + Reverses slice operations to take from the back of the packet, + not the front + """ - def addfield(self, pkt, s, val): - sval = self._fld.addfield(pkt, b"", val) - return s + struct.pack("%is" % (self.padlen(len(s))), self._padwith) + sval # noqa: E501 + def __getitem__(self, item): # type: ignore + # type: (Union[int, slice]) -> Union[int, bytes] + if isinstance(item, int): + if item < 0: + item = 1 + item + else: + item = len(self) - 1 - item + elif isinstance(item, slice): + start, stop, step = item.start, item.stop, item.step + new_start = -stop if stop else None + new_stop = -start if start else None + item = slice(new_start, new_stop, step) + return super(self.__class__, self).__getitem__(item) -class FCSField(Field): +class TrailerField(_FieldContainer): """Special Field that gets its value from the end of the *packet* (Note: not layer, but packet). Mostly used for FCS """ + __slots__ = ["fld"] + + def __init__(self, fld): + # type: (Field[Any, Any]) -> None + self.fld = fld + + # Note: this is ugly. Very ugly. + # Do not copy this crap elsewhere, so that if one day we get + # brave enough to refactor it, it'll be easier. + def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, int] previous_post_dissect = pkt.post_dissect - val = self.m2i(pkt, struct.unpack(self.fmt, s[-self.sz:])[0]) def _post_dissect(self, s): + # type: (Packet, bytes) -> bytes # Reset packet to allow post_build self.raw_packet_cache = None - self.post_dissect = previous_post_dissect + self.post_dissect = previous_post_dissect # type: ignore return previous_post_dissect(s) - pkt.post_dissect = MethodType(_post_dissect, pkt) - return s[:-self.sz], val + pkt.post_dissect = MethodType(_post_dissect, pkt) # type: ignore + s = TrailerBytes(s) + s, val = self.fld.getfield(pkt, s) + return bytes(s), val def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[int]) -> bytes previous_post_build = pkt.post_build - value = struct.pack(self.fmt, self.i2m(pkt, val)) + value = self.fld.addfield(pkt, b"", val) def _post_build(self, p, pay): + # type: (Packet, bytes, bytes) -> bytes pay += value - self.post_build = previous_post_build + self.post_build = previous_post_build # type: ignore return previous_post_build(p, pay) - pkt.post_build = MethodType(_post_build, pkt) + pkt.post_build = MethodType(_post_build, pkt) # type: ignore return s + +class FCSField(TrailerField): + """ + A FCS field that gets appended at the end of the *packet* (not layer). + """ + + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + super(FCSField, self).__init__(Field(*args, **kwargs)) + def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return lhex(self.i2h(pkt, x)) -class DestField(Field): +class DestField(Field[str, bytes]): __slots__ = ["defaultdst"] # Each subclass must have its own bindings attribute - # bindings = {} + bindings = {} # type: Dict[Type[Packet], Tuple[str, Any]] def __init__(self, name, default): + # type: (str, str) -> None self.defaultdst = default def dst_from_pkt(self, pkt): + # type: (Packet) -> str for addr, condition in self.bindings.get(pkt.payload.__class__, []): try: if all(pkt.payload.getfieldval(field) == value - for field, value in six.iteritems(condition)): - return addr + for field, value in condition.items()): + return addr # type: ignore except AttributeError: pass return self.defaultdst @classmethod def bind_addr(cls, layer, addr, **condition): - cls.bindings.setdefault(layer, []).append((addr, condition)) + # type: (Type[Packet], str, **Any) -> None + cls.bindings.setdefault(layer, []).append( # type: ignore + (addr, condition) + ) -class MACField(Field): +class MACField(Field[Optional[str], bytes]): def __init__(self, name, default): + # type: (str, Optional[Any]) -> None Field.__init__(self, name, default, "6s") def i2m(self, pkt, x): - if x is None: + # type: (Optional[Packet], Optional[str]) -> bytes + if not x: return b"\0\0\0\0\0\0" try: - x = mac2str(x) - except (struct.error, OverflowError): - x = bytes_encode(x) - - return x + y = mac2str(x) + except (struct.error, OverflowError, ValueError): + y = bytes_encode(x) + return y def m2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> str return str2mac(x) def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> str if isinstance(x, bytes) and len(x) == 6: - x = self.m2i(pkt, x) - return x + return self.m2i(pkt, x) + return cast(str, x) def i2repr(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> str x = self.i2h(pkt, x) + if x is None: + return repr(x) if self in conf.resolve: x = conf.manufdb._resolve_MAC(x) return x def randval(self): + # type: () -> RandMAC return RandMAC() -class IPField(Field): - slots = [] +class LEMACField(MACField): + def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> bytes + return MACField.i2m(self, pkt, x)[::-1] + + def m2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> str + return MACField.m2i(self, pkt, x[::-1]) + +class IPField(Field[Union[str, Net], bytes]): def __init__(self, name, default): + # type: (str, Optional[str]) -> None Field.__init__(self, name, default, "4s") def h2i(self, pkt, x): + # type: (Optional[Packet], Union[AnyStr, List[AnyStr]]) -> Any if isinstance(x, bytes): - x = plain_str(x) - if isinstance(x, str): + x = plain_str(x) # type: ignore + if isinstance(x, _ScopedIP): + return x + elif isinstance(x, str): + x = ScopedIP(x) try: inet_aton(x) except socket.error: - x = Net(x) + return Net(x) + elif isinstance(x, tuple): + if len(x) != 2: + raise ValueError("Invalid IP format") + return Net(*x) elif isinstance(x, list): - x = [self.h2i(pkt, n) for n in x] + return [self.h2i(pkt, n) for n in x] return x + def i2h(self, pkt, x): + # type: (Optional[Packet], Optional[Union[str, Net]]) -> str + return cast(str, x) + def resolve(self, x): + # type: (str) -> str if self in conf.resolve: try: ret = socket.gethostbyaddr(x)[0] @@ -556,83 +886,101 @@ def resolve(self, x): return x def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[Union[str, Net]]) -> bytes if x is None: return b'\x00\x00\x00\x00' return inet_aton(plain_str(x)) def m2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> str return inet_ntoa(x) def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> Any return self.h2i(pkt, x) def i2repr(self, pkt, x): + # type: (Optional[Packet], Union[str, Net]) -> str + if isinstance(x, _ScopedIP) and x.scope: + return repr(x) r = self.resolve(self.i2h(pkt, x)) return r if isinstance(r, str) else repr(r) def randval(self): + # type: () -> RandIP return RandIP() class SourceIPField(IPField): - __slots__ = ["dstname"] - - def __init__(self, name, dstname): + def __init__(self, name): + # type: (str) -> None IPField.__init__(self, name, None) - self.dstname = dstname def __findaddr(self, pkt): + # type: (Packet) -> Optional[str] if conf.route is None: # unused import, only to initialize conf.route import scapy.route # noqa: F401 - dst = ("0.0.0.0" if self.dstname is None - else getattr(pkt, self.dstname) or "0.0.0.0") - if isinstance(dst, (Gen, list)): - r = {conf.route.route(str(daddr)) for daddr in dst} - if len(r) > 1: - warning("More than one possible route for %r" % (dst,)) - return min(r)[1] - return conf.route.route(dst)[1] + return pkt.route()[1] or conf.route.route()[1] def i2m(self, pkt, x): - if x is None: + # type: (Optional[Packet], Optional[Union[str, Net]]) -> bytes + if x is None and pkt is not None: x = self.__findaddr(pkt) - return IPField.i2m(self, pkt, x) + return super(SourceIPField, self).i2m(pkt, x) def i2h(self, pkt, x): - if x is None: + # type: (Optional[Packet], Optional[Union[str, Net]]) -> str + if x is None and pkt is not None: x = self.__findaddr(pkt) - return IPField.i2h(self, pkt, x) + return super(SourceIPField, self).i2h(pkt, x) -class IP6Field(Field): +class IP6Field(Field[Optional[Union[str, Net6]], bytes]): def __init__(self, name, default): + # type: (str, Optional[str]) -> None Field.__init__(self, name, default, "16s") def h2i(self, pkt, x): + # type: (Optional[Packet], Any) -> str if isinstance(x, bytes): x = plain_str(x) - if isinstance(x, str): + if isinstance(x, _ScopedIP): + return x + elif isinstance(x, str): + x = ScopedIP(x) try: - x = in6_ptop(x) + x = ScopedIP(in6_ptop(x), scope=x.scope) except socket.error: - x = Net6(x) + return Net6(x) # type: ignore + elif isinstance(x, tuple): + if len(x) != 2: + raise ValueError("Invalid IPv6 format") + return Net6(*x) # type: ignore elif isinstance(x, list): x = [self.h2i(pkt, n) for n in x] - return x + return x # type: ignore + + def i2h(self, pkt, x): + # type: (Optional[Packet], Optional[Union[str, Net6]]) -> str + return cast(str, x) def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[Union[str, Net6]]) -> bytes if x is None: x = "::" return inet_pton(socket.AF_INET6, plain_str(x)) def m2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> str return inet_ntop(socket.AF_INET6, x) def any2i(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> str return self.h2i(pkt, x) def i2repr(self, pkt, x): + # type: (Optional[Packet], Optional[Union[str, Net6]]) -> str if x is None: return self.i2h(pkt, x) elif not isinstance(x, Net6) and not isinstance(x, list): @@ -642,110 +990,188 @@ def i2repr(self, pkt, x): elif in6_isaddr6to4(x): # print encapsulated address vaddr = in6_6to4ExtractAddr(x) return "%s [6to4 GW: %s]" % (self.i2h(pkt, x), vaddr) + elif isinstance(x, _ScopedIP) and x.scope: + return repr(x) r = self.i2h(pkt, x) # No specific information to return return r if isinstance(r, str) else repr(r) def randval(self): + # type: () -> RandIP6 return RandIP6() class SourceIP6Field(IP6Field): - __slots__ = ["dstname"] - - def __init__(self, name, dstname): + def __init__(self, name): + # type: (str) -> None IP6Field.__init__(self, name, None) - self.dstname = dstname + + def __findaddr(self, pkt): + # type: (Packet) -> Optional[str] + if conf.route6 is None: + # unused import, only to initialize conf.route + import scapy.route6 # noqa: F401 + return pkt.route()[1] def i2m(self, pkt, x): - if x is None: - dst = ("::" if self.dstname is None else - getattr(pkt, self.dstname) or "::") - iff, x, nh = conf.route6.route(dst) - return IP6Field.i2m(self, pkt, x) + # type: (Optional[Packet], Optional[Union[str, Net6]]) -> bytes + if x is None and pkt is not None: + x = self.__findaddr(pkt) + return super(SourceIP6Field, self).i2m(pkt, x) def i2h(self, pkt, x): - if x is None: - if conf.route6 is None: - # unused import, only to initialize conf.route6 - import scapy.route6 # noqa: F401 - dst = ("::" if self.dstname is None else getattr(pkt, self.dstname)) # noqa: E501 - if isinstance(dst, (Gen, list)): - r = {conf.route6.route(str(daddr)) for daddr in dst} - if len(r) > 1: - warning("More than one possible route for %r" % (dst,)) - x = min(r)[1] - else: - x = conf.route6.route(dst)[1] - return IP6Field.i2h(self, pkt, x) + # type: (Optional[Packet], Optional[Union[str, Net6]]) -> str + if x is None and pkt is not None: + x = self.__findaddr(pkt) + return super(SourceIP6Field, self).i2h(pkt, x) class DestIP6Field(IP6Field, DestField): - bindings = {} + bindings = {} # type: Dict[Type[Packet], Tuple[str, Any]] def __init__(self, name, default): + # type: (str, str) -> None IP6Field.__init__(self, name, None) DestField.__init__(self, name, default) def i2m(self, pkt, x): - if x is None: + # type: (Optional[Packet], Optional[Union[str, Net6]]) -> bytes + if x is None and pkt is not None: x = self.dst_from_pkt(pkt) return IP6Field.i2m(self, pkt, x) def i2h(self, pkt, x): - if x is None: + # type: (Optional[Packet], Optional[Union[str, Net6]]) -> str + if x is None and pkt is not None: x = self.dst_from_pkt(pkt) - return IP6Field.i2h(self, pkt, x) + return super(DestIP6Field, self).i2h(pkt, x) -class ByteField(Field): +class ByteField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "B") class XByteField(ByteField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return lhex(self.i2h(pkt, x)) +# XXX Unused field: at least add some tests class OByteField(ByteField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return "%03o" % self.i2h(pkt, x) -class ThreeBytesField(ByteField): +class ThreeBytesField(Field[int, int]): def __init__(self, name, default): + # type: (str, int) -> None Field.__init__(self, name, default, "!I") def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[int]) -> bytes return s + struct.pack(self.fmt, self.i2m(pkt, val))[1:4] def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, int] return s[3:], self.m2i(pkt, struct.unpack(self.fmt, b"\x00" + s[:3])[0]) # noqa: E501 class X3BytesField(ThreeBytesField, XByteField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return XByteField.i2repr(self, pkt, x) class LEThreeBytesField(ByteField): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, " bytes return s + struct.pack(self.fmt, self.i2m(pkt, val))[:3] def getfield(self, pkt, s): + # type: (Optional[Packet], bytes) -> Tuple[bytes, int] return s[3:], self.m2i(pkt, struct.unpack(self.fmt, s[:3] + b"\x00")[0]) # noqa: E501 -class LEX3BytesField(LEThreeBytesField, XByteField): +class XLE3BytesField(LEThreeBytesField, XByteField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return XByteField.i2repr(self, pkt, x) -class SignedByteField(Field): +def LEX3BytesField(*args, **kwargs): + # type: (*Any, **Any) -> Any + warnings.warn( + "LEX3BytesField is deprecated. Use XLE3BytesField", + DeprecationWarning + ) + return XLE3BytesField(*args, **kwargs) + + +class NBytesField(Field[int, List[int]]): + def __init__(self, name, default, sz): + # type: (str, Optional[int], int) -> None + Field.__init__(self, name, default, "<" + "B" * sz) + + def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[int]) -> List[int] + if x is None: + return [0] * self.sz + x2m = list() + for _ in range(self.sz): + x2m.append(x % 256) + x //= 256 + return x2m[::-1] + + def m2i(self, pkt, x): + # type: (Optional[Packet], Union[List[int], int]) -> int + if isinstance(x, int): + return x + # x can be a tuple when coming from struct.unpack (from getfield) + if isinstance(x, (list, tuple)): + return sum(d * (256 ** i) for i, d in enumerate(reversed(x))) + return 0 + + def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str + if isinstance(x, int): + return '%i' % x + return super(NBytesField, self).i2repr(pkt, x) + + def addfield(self, pkt, s, val): + # type: (Optional[Packet], bytes, Optional[int]) -> bytes + return s + self.struct.pack(*self.i2m(pkt, val)) + + def getfield(self, pkt, s): + # type: (Optional[Packet], bytes) -> Tuple[bytes, int] + return (s[self.sz:], + self.m2i(pkt, self.struct.unpack(s[:self.sz]))) # type: ignore + + def randval(self): + # type: () -> RandNum + return RandNum(0, 2 ** (self.sz * 8) - 1) + + +class XNBytesField(NBytesField): + def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str + if isinstance(x, int): + return '0x%x' % x + # x can be a tuple when coming from struct.unpack (from getfield) + if isinstance(x, (list, tuple)): + return "0x" + "".join("%02x" % b for b in x) + return super(XNBytesField, self).i2repr(pkt, x) + + +class SignedByteField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "b") @@ -753,6 +1179,10 @@ class FieldValueRangeException(Scapy_Exception): pass +class MaximumItemsCount(Scapy_Exception): + pass + + class FieldAttributeException(Scapy_Exception): pass @@ -814,6 +1244,7 @@ class YesNoByteField(ByteField): __slots__ = ['eval_fn'] def _build_config_representation(self, config): + # type: (Dict[str, Any]) -> None assoc_table = dict() for key in config: value_spec = config[key] @@ -855,141 +1286,169 @@ def _build_config_representation(self, config): self.eval_fn = lambda x: assoc_table[x] if x in assoc_table else x - def __init__(self, name, default, config=None, *args, **kargs): + def __init__(self, name, default, config=None): + # type: (str, int, Optional[Dict[str, Any]]) -> None if not config: # this represents the common use case and therefore it is kept small # noqa: E501 self.eval_fn = lambda x: 'no' if x == 0 else 'yes' else: self._build_config_representation(config) - ByteField.__init__(self, name, default, *args, **kargs) + ByteField.__init__(self, name, default) def i2repr(self, pkt, x): - return self.eval_fn(x) + # type: (Optional[Packet], int) -> str + return self.eval_fn(x) # type: ignore -class ShortField(Field): +class ShortField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "H") -class SignedShortField(Field): +class SignedShortField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "h") -class LEShortField(Field): +class LEShortField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, " None Field.__init__(self, name, default, " str return lhex(self.i2h(pkt, x)) -class IntField(Field): +class IntField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "I") -class SignedIntField(Field): +class SignedIntField(Field[int, int]): def __init__(self, name, default): + # type: (str, int) -> None Field.__init__(self, name, default, "i") -class LEIntField(Field): +class LEIntField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, " None Field.__init__(self, name, default, " str return lhex(self.i2h(pkt, x)) class XLEIntField(LEIntField, XIntField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return XIntField.i2repr(self, pkt, x) class XLEShortField(LEShortField, XShortField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return XShortField.i2repr(self, pkt, x) -class LongField(Field): +class LongField(Field[int, int]): def __init__(self, name, default): + # type: (str, int) -> None Field.__init__(self, name, default, "Q") -class SignedLongField(Field): +class SignedLongField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "q") class LELongField(LongField): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, " None Field.__init__(self, name, default, " str return lhex(self.i2h(pkt, x)) class XLELongField(LELongField, XLongField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return XLongField.i2repr(self, pkt, x) -class IEEEFloatField(Field): +class IEEEFloatField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "f") -class IEEEDoubleField(Field): +class IEEEDoubleField(Field[int, int]): def __init__(self, name, default): + # type: (str, Optional[int]) -> None Field.__init__(self, name, default, "d") -class StrField(Field): +class _StrField(Field[I, bytes]): __slots__ = ["remain"] def __init__(self, name, default, fmt="H", remain=0): + # type: (str, Optional[I], str, int) -> None Field.__init__(self, name, default, fmt) self.remain = remain def i2len(self, pkt, x): + # type: (Optional[Packet], Any) -> int + if x is None: + return 0 return len(x) def any2i(self, pkt, x): - if isinstance(x, six.text_type): + # type: (Optional[Packet], Any) -> I + if isinstance(x, str): x = bytes_encode(x) - return super(StrField, self).any2i(pkt, x) + return super(_StrField, self).any2i(pkt, x) # type: ignore def i2repr(self, pkt, x): - val = super(StrField, self).i2repr(pkt, x) - if val[:2] in ['b"', "b'"]: - return val[1:] - return val + # type: (Optional[Packet], I) -> str + if x and isinstance(x, bytes): + return repr(x) + return super(_StrField, self).i2repr(pkt, x) def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[I]) -> bytes if x is None: return b"" if not isinstance(x, bytes): @@ -997,77 +1456,191 @@ def i2m(self, pkt, x): return x def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[I]) -> bytes return s + self.i2m(pkt, val) def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, I] if self.remain == 0: return b"", self.m2i(pkt, s) else: return s[-self.remain:], self.m2i(pkt, s[:-self.remain]) def randval(self): + # type: () -> RandBin return RandBin(RandNum(0, 1200)) -class PacketField(StrField): +class StrField(_StrField[bytes]): + pass + + +class StrFieldUtf16(StrField): + def any2i(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> bytes + if isinstance(x, str): + return self.h2i(pkt, x) + return super(StrFieldUtf16, self).any2i(pkt, x) + + def i2repr(self, pkt, x): + # type: (Optional[Packet], bytes) -> str + return plain_str(self.i2h(pkt, x)) + + def h2i(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> bytes + return plain_str(x).encode('utf-16-le', errors="replace") + + def i2h(self, pkt, x): + # type: (Optional[Packet], bytes) -> str + return bytes_encode(x).decode('utf-16-le', errors="replace") + + +class _StrEnumField: + def __init__(self, **kwargs): + # type: (**Any) -> None + self.enum = kwargs.pop("enum", {}) + + def i2repr(self, pkt, v): + # type: (Optional[Packet], bytes) -> str + r = v.rstrip(b"\0") + rr = repr(r) + if self.enum: + if v in self.enum: + rr = "%s (%s)" % (rr, self.enum[v]) + elif r in self.enum: + rr = "%s (%s)" % (rr, self.enum[r]) + return rr + + +class StrEnumField(_StrEnumField, StrField): + __slots__ = ["enum"] + + def __init__( + self, + name, # type: str + default, # type: bytes + enum=None, # type: Optional[Dict[str, str]] + **kwargs # type: Any + ): + # type: (...) -> None + StrField.__init__(self, name, default, **kwargs) # type: ignore + self.enum = enum + + +K = TypeVar('K', List[BasePacket], BasePacket, Optional[BasePacket]) + + +class _PacketField(_StrField[K]): __slots__ = ["cls"] holds_packets = 1 - def __init__(self, name, default, cls, remain=0): - StrField.__init__(self, name, default, remain=remain) - self.cls = cls - - def i2m(self, pkt, i): + def __init__(self, + name, # type: str + default, # type: Optional[K] + pkt_cls, # type: Union[Callable[[bytes], Packet], Type[Packet]] # noqa: E501 + ): + # type: (...) -> None + super(_PacketField, self).__init__(name, default) + self.cls = pkt_cls + + def i2m(self, + pkt, # type: Optional[Packet] + i, # type: Any + ): + # type: (...) -> bytes if i is None: return b"" return raw(i) - def m2i(self, pkt, m): - return self.cls(m) + def m2i(self, pkt, m): # type: ignore + # type: (Optional[Packet], bytes) -> Packet + try: + # we want to set parent wherever possible + return self.cls(m, _parent=pkt) # type: ignore + except TypeError: + return self.cls(m) - def getfield(self, pkt, s): + +class _PacketFieldSingle(_PacketField[K]): + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> K + if x and pkt and hasattr(x, "add_parent"): + cast("Packet", x).add_parent(pkt) + return super(_PacketFieldSingle, self).any2i(pkt, x) + + def getfield(self, + pkt, # type: Packet + s, # type: bytes + ): + # type: (...) -> Tuple[bytes, K] i = self.m2i(pkt, s) remain = b"" if conf.padding_layer in i: r = i[conf.padding_layer] - del(r.underlayer.payload) + del r.underlayer.payload remain = r.load - return remain, i + return remain, i # type: ignore - def randval(self): + +class PacketField(_PacketFieldSingle[BasePacket]): + def randval(self): # type: ignore + # type: () -> Packet from scapy.packet import fuzz - return fuzz(self.cls()) + return fuzz(self.cls()) # type: ignore -class PacketLenField(PacketField): +class PacketLenField(_PacketFieldSingle[Optional[BasePacket]]): __slots__ = ["length_from"] - def __init__(self, name, default, cls, length_from=None): - PacketField.__init__(self, name, default, cls) - self.length_from = length_from - - def getfield(self, pkt, s): + def __init__(self, + name, # type: str + default, # type: Packet + cls, # type: Union[Callable[[bytes], Packet], Type[Packet]] # noqa: E501 + length_from=None # type: Optional[Callable[[Packet], int]] # noqa: E501 + ): + # type: (...) -> None + super(PacketLenField, self).__init__(name, default, cls) + self.length_from = length_from or (lambda x: 0) + + def getfield(self, + pkt, # type: Packet + s, # type: bytes + ): + # type: (...) -> Tuple[bytes, Optional[BasePacket]] len_pkt = self.length_from(pkt) - try: - i = self.m2i(pkt, s[:len_pkt]) - except Exception: - if conf.debug_dissector: - raise - i = conf.raw_layer(load=s[:len_pkt]) + i = None + if len_pkt: + try: + i = self.m2i(pkt, s[:len_pkt]) + except Exception: + if conf.debug_dissector: + raise + i = conf.raw_layer(load=s[:len_pkt]) return s[len_pkt:], i -class PacketListField(PacketField): - """PacketListField represents a series of Packet instances that might - occur right in the middle of another Packet field list. +class PacketListField(_PacketField[List[BasePacket]]): + """PacketListField represents a list containing a series of Packet instances + that might occur right in the middle of another Packet field. This field type may also be used to indicate that a series of Packet instances have a sibling semantic instead of a parent/child relationship - (i.e. a stack of layers). + (i.e. a stack of layers). All elements in PacketListField have current + packet referenced in parent field. """ - __slots__ = ["count_from", "length_from", "next_cls_cb"] + __slots__ = ["count_from", "length_from", "next_cls_cb", "max_count"] islist = 1 - def __init__(self, name, default, cls=None, count_from=None, length_from=None, next_cls_cb=None): # noqa: E501 + def __init__( + self, + name, # type: str + default, # type: Optional[List[BasePacket]] + pkt_cls=None, # type: Optional[Union[Callable[[bytes], Packet], Type[Packet]]] # noqa: E501 + count_from=None, # type: Optional[Callable[[Packet], int]] + length_from=None, # type: Optional[Callable[[Packet], int]] + next_cls_cb=None, # type: Optional[Callable[[Packet, List[BasePacket], Optional[Packet], bytes], Optional[Type[Packet]]]] # noqa: E501 + max_count=None, # type: Optional[int] + ): + # type: (...) -> None """ The number of Packet instances that are dissected by this field can be parametrized using one of three different mechanisms/parameters: @@ -1096,14 +1669,14 @@ def __init__(self, name, default, cls=None, count_from=None, length_from=None, n The type of the Packet instances that are dissected with this field is specified or discovered using one of the following mechanism: - * the cls parameter may contain a callable that returns an + * the pkt_cls parameter may contain a callable that returns an instance of the dissected Packet. This may either be a reference of a Packet subclass (e.g. DNSRROPT in layers/dns.py) to generate an homogeneous PacketListField or a function deciding the type of the Packet instance (e.g. _CDPGuessAddrRecord in contrib/cdp.py) - * the cls parameter may contain a class object with a defined + * the pkt_cls parameter may contain a class object with a defined ``dispatch_hook`` classmethod. That method must return a Packet instance. The ``dispatch_hook`` callmethod must implement the following prototype:: @@ -1111,7 +1684,7 @@ def __init__(self, name, default, cls=None, count_from=None, length_from=None, n dispatch_hook(cls, _pkt:Optional[Packet], *args, **kargs - ) -> Packet_metaclass + ) -> Type[Packet] The _pkt parameter may contain a reference to the packet instance containing the PacketListField that is being @@ -1123,8 +1696,8 @@ def __init__(self, name, default, cls=None, count_from=None, length_from=None, n cbk(pkt:Packet, lst:List[Packet], cur:Optional[Packet], - remain:str - ) -> Optional[Packet_metaclass] + remain:bytes, + ) -> Optional[Type[Packet]] The pkt argument contains a reference to the Packet instance containing the PacketListField that is being dissected. @@ -1148,8 +1721,8 @@ def __init__(self, name, default, cls=None, count_from=None, length_from=None, n continuation based on a look-ahead on the bytes to be dissected... - The cls and next_cls_cb parameters are semantically exclusive, - although one could specify both. If both are specified, cls is + The pkt_cls and next_cls_cb parameters are semantically exclusive, + although one could specify both. If both are specified, pkt_cls is silently ignored. The same is true for count_from and next_cls_cb. length_from and next_cls_cb are compatible and the dissection will @@ -1158,42 +1731,59 @@ def __init__(self, name, default, cls=None, count_from=None, length_from=None, n :param name: the name of the field :param default: the default value of this field; generally an empty Python list - @param cls: either a callable returning a Packet instance or a class - object defining a ``dispatch_hook`` class method + :param pkt_cls: either a callable returning a Packet instance or a + class object defining a ``dispatch_hook`` class method :param count_from: a callback returning the number of Packet instances to dissect. :param length_from: a callback returning the number of bytes to dissect :param next_cls_cb: a callback returning either None or the type of the next Packet to dissect. + :param max_count: an int containing the max amount of results. This is + a safety mechanism, exceeding this value will raise a Scapy_Exception. """ if default is None: default = [] # Create a new list for each instance - PacketField.__init__(self, name, default, cls) + super(PacketListField, self).__init__( + name, + default, + pkt_cls # type: ignore + ) self.count_from = count_from self.length_from = length_from self.next_cls_cb = next_cls_cb + self.max_count = max_count def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> List[BasePacket] if not isinstance(x, list): + if x and pkt and hasattr(x, "add_parent"): + x.add_parent(pkt) return [x] - else: - return x + elif pkt: + for i in x: + if not i or not hasattr(i, "add_parent"): + continue + i.add_parent(pkt) + return x - def i2count(self, pkt, val): + def i2count(self, + pkt, # type: Optional[Packet] + val, # type: List[BasePacket] + ): + # type: (...) -> int if isinstance(val, list): return len(val) return 1 - def i2len(self, pkt, val): - return sum(len(p) for p in val) - - def do_copy(self, x): - if x is None: - return None - else: - return [p if isinstance(p, (str, bytes)) else p.copy() for p in x] + def i2len(self, + pkt, # type: Optional[Packet] + val, # type: List[Packet] + ): + # type: (...) -> int + return sum(len(self.i2m(pkt, p)) for p in val) def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, List[BasePacket]] c = len_pkt = cls = None if self.length_from is not None: len_pkt = self.length_from(pkt) @@ -1202,8 +1792,10 @@ def getfield(self, pkt, s): if self.next_cls_cb is not None: cls = self.next_cls_cb(pkt, [], None, s) c = 1 + if cls is None: + c = 0 - lst = [] + lst = [] # type: List[BasePacket] ret = b"" remain = s if len_pkt is not None: @@ -1215,7 +1807,11 @@ def getfield(self, pkt, s): c -= 1 try: if cls is not None: - p = cls(remain) + try: + # we want to set parent wherever possible + p = cls(remain, _parent=pkt) + except TypeError: + p = cls(remain) else: p = self.m2i(pkt, remain) except Exception: @@ -1227,7 +1823,7 @@ def getfield(self, pkt, s): if conf.padding_layer in p: pad = p[conf.padding_layer] remain = pad.load - del(pad.underlayer.payload) + del pad.underlayer.payload if self.next_cls_cb is not None: cls = self.next_cls_cb(pkt, lst, p, remain) if cls is not None: @@ -1236,122 +1832,198 @@ def getfield(self, pkt, s): else: remain = b"" lst.append(p) - return remain + ret, lst + if len(lst) > (self.max_count or conf.max_list_count): + raise MaximumItemsCount( + "Maximum amount of items reached in PacketListField: %s " + "(defaults to conf.max_list_count)" + % (self.max_count or conf.max_list_count) + ) + + if isinstance(remain, tuple): + remain, nb = remain + return (remain + ret, nb), lst + else: + return remain + ret, lst + + def i2m(self, + pkt, # type: Optional[Packet] + i, # type: Any + ): + # type: (...) -> bytes + return bytes_encode(i) def addfield(self, pkt, s, val): - return s + b"".join(bytes_encode(v) for v in val) + # type: (Packet, bytes, Any) -> bytes + return s + b"".join(self.i2m(pkt, v) for v in val) class StrFixedLenField(StrField): __slots__ = ["length_from"] - def __init__(self, name, default, length=None, length_from=None): - StrField.__init__(self, name, default) - self.length_from = length_from + def __init__( + self, + name, # type: str + default, # type: Optional[bytes] + length=None, # type: Optional[int] + length_from=None, # type: Optional[Callable[[Packet], int]] + ): + # type: (...) -> None + super(StrFixedLenField, self).__init__(name, default) + self.length_from = length_from or (lambda x: 0) if length is not None: - self.length_from = lambda pkt, length=length: length - - def i2repr(self, pkt, v): - if isinstance(v, bytes): + self.sz = length + self.length_from = lambda x, length=length: length # type: ignore + + def i2repr(self, + pkt, # type: Optional[Packet] + v, # type: bytes + ): + # type: (...) -> str + if isinstance(v, bytes) and not conf.debug_strfixedlenfield: v = v.rstrip(b"\0") return super(StrFixedLenField, self).i2repr(pkt, v) def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, bytes] len_pkt = self.length_from(pkt) + if len_pkt == 0: + return s, b"" return s[len_pkt:], self.m2i(pkt, s[:len_pkt]) def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[bytes]) -> bytes len_pkt = self.length_from(pkt) if len_pkt is None: return s + self.i2m(pkt, val) return s + struct.pack("%is" % len_pkt, self.i2m(pkt, val)) def randval(self): + # type: () -> RandBin try: - len_pkt = self.length_from(None) + return RandBin(self.length_from(None)) # type: ignore except Exception: - len_pkt = RandNum(0, 200) - return RandBin(len_pkt) + return RandBin(RandNum(0, 200)) + + +class StrFixedLenFieldUtf16(StrFixedLenField, StrFieldUtf16): + pass -class StrFixedLenEnumField(StrFixedLenField): +class StrFixedLenEnumField(_StrEnumField, StrFixedLenField): __slots__ = ["enum"] - def __init__(self, name, default, length=None, enum=None, length_from=None): # noqa: E501 + def __init__( + self, + name, # type: str + default, # type: bytes + enum=None, # type: Optional[Dict[str, str]] + length=None, # type: Optional[int] + length_from=None # type: Optional[Callable[[Optional[Packet]], int]] # noqa: E501 + ): + # type: (...) -> None StrFixedLenField.__init__(self, name, default, length=length, length_from=length_from) # noqa: E501 self.enum = enum - def i2repr(self, pkt, v): - r = v.rstrip("\0" if isinstance(v, str) else b"\0") - rr = repr(r) - if v in self.enum: - rr = "%s (%s)" % (rr, self.enum[v]) - elif r in self.enum: - rr = "%s (%s)" % (rr, self.enum[r]) - return rr - class NetBIOSNameField(StrFixedLenField): def __init__(self, name, default, length=31): + # type: (str, bytes, int) -> None StrFixedLenField.__init__(self, name, default, length) - def i2m(self, pkt, x): - len_pkt = self.length_from(pkt) // 2 - x = bytes_encode(x) - if x is None: - x = b"" + def h2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> bytes + if x and len(x) > 15: + x = x[:15] + return x + + def i2m(self, pkt, y): + # type: (Optional[Packet], Optional[bytes]) -> bytes + if pkt: + len_pkt = self.length_from(pkt) // 2 + else: + len_pkt = 0 + x = bytes_encode(y or b"") # type: bytes x += b" " * len_pkt x = x[:len_pkt] - x = b"".join(chb(0x41 + (orb(b) >> 4)) + chb(0x41 + (orb(b) & 0xf)) for b in x) # noqa: E501 - x = b" " + x - return x + x = b"".join( + struct.pack( + "!BB", + 0x41 + (b >> 4), + 0x41 + (b & 0xf), + ) + for b in x + ) + return b" " + x def m2i(self, pkt, x): - x = x.strip(b"\x00").strip(b" ") - return b"".join(map(lambda x, y: chb((((orb(x) - 1) & 0xf) << 4) + ((orb(y) - 1) & 0xf)), x[::2], x[1::2])) # noqa: E501 + # type: (Optional[Packet], bytes) -> bytes + x = x[1:].strip(b"\x00") + return b"".join(map( + lambda x, y: struct.pack( + "!B", + (((x - 1) & 0xf) << 4) + ((y - 1) & 0xf) + ), + x[::2], x[1::2] + )).rstrip(b" ") class StrLenField(StrField): - __slots__ = ["length_from", "max_length"] + """ + StrField with a length - def __init__(self, name, default, fld=None, length_from=None, max_length=None): # noqa: E501 - StrField.__init__(self, name, default) + :param length_from: a function that returns the size of the string + :param max_length: max size to use as randval + """ + __slots__ = ["length_from", "max_length"] + ON_WIRE_SIZE_UTF16 = True + + def __init__( + self, + name, # type: str + default, # type: bytes + length_from=None, # type: Optional[Callable[[Packet], int]] + max_length=None, # type: Optional[Any] + ): + # type: (...) -> None + super(StrLenField, self).__init__(name, default) self.length_from = length_from self.max_length = max_length def getfield(self, pkt, s): - len_pkt = self.length_from(pkt) + # type: (Any, bytes) -> Tuple[bytes, bytes] + len_pkt = (self.length_from or (lambda x: 0))(pkt) + if not self.ON_WIRE_SIZE_UTF16: + len_pkt *= 2 + if len_pkt == 0: + return s, b"" return s[len_pkt:], self.m2i(pkt, s[:len_pkt]) def randval(self): + # type: () -> RandBin return RandBin(RandNum(0, self.max_length or 1200)) -class XStrField(StrField): +class _XStrField(Field[bytes, bytes]): + def i2repr(self, pkt, x): + # type: (Optional[Packet], bytes) -> str + if isinstance(x, bytes): + return bytes_hex(x).decode() + return super(_XStrField, self).i2repr(pkt, x) + + +class XStrField(_XStrField, StrField): """ StrField which value is printed as hexadecimal. """ - def i2repr(self, pkt, x): - if x is None: - return repr(x) - return bytes_hex(x).decode() - -class _XStrLenField: - def i2repr(self, pkt, x): - if not x: - return repr(x) - return bytes_hex(x[:self.length_from(pkt)]).decode() - - -class XStrLenField(_XStrLenField, StrLenField): +class XStrLenField(_XStrField, StrLenField): """ StrLenField which value is printed as hexadecimal. """ -class XStrFixedLenField(_XStrLenField, StrFixedLenField): +class XStrFixedLenField(_XStrField, StrFixedLenField): """ StrFixedLenField which value is printed as hexadecimal. """ @@ -1359,73 +2031,118 @@ class XStrFixedLenField(_XStrLenField, StrFixedLenField): class XLEStrLenField(XStrLenField): def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[bytes]) -> bytes + if not x: + return b"" return x[:: -1] def m2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> bytes return x[:: -1] -class StrLenFieldUtf16(StrLenField): - def h2i(self, pkt, x): - return plain_str(x).encode('utf-16')[2:] +class StrLenFieldUtf16(StrLenField, StrFieldUtf16): + pass - def i2h(self, pkt, x): - return x.decode('utf-16') + +class StrLenEnumField(_StrEnumField, StrLenField): + __slots__ = ["enum"] + + def __init__( + self, + name, # type: str + default, # type: bytes + enum=None, # type: Optional[Dict[str, str]] + **kwargs # type: Any + ): + # type: (...) -> None + StrLenField.__init__(self, name, default, **kwargs) + self.enum = enum class BoundStrLenField(StrLenField): __slots__ = ["minlen", "maxlen"] - def __init__(self, name, default, minlen=0, maxlen=255, fld=None, length_from=None): # noqa: E501 - StrLenField.__init__(self, name, default, fld, length_from) + def __init__( + self, + name, # type: str + default, # type: bytes + minlen=0, # type: int + maxlen=255, # type: int + length_from=None # type: Optional[Callable[[Packet], int]] + ): + # type: (...) -> None + StrLenField.__init__(self, name, default, length_from=length_from) self.minlen = minlen self.maxlen = maxlen def randval(self): + # type: () -> RandBin return RandBin(RandNum(self.minlen, self.maxlen)) -class FieldListField(Field): - __slots__ = ["field", "count_from", "length_from"] +class FieldListField(Field[List[Any], List[Any]]): + __slots__ = ["field", "count_from", "length_from", "max_count"] islist = 1 - def __init__(self, name, default, field, length_from=None, count_from=None): # noqa: E501 + def __init__( + self, + name, # type: str + default, # type: Optional[List[AnyField]] + field, # type: AnyField + length_from=None, # type: Optional[Callable[[Packet], int]] + count_from=None, # type: Optional[Callable[[Packet], int]] + max_count=None, # type: Optional[int] + ): + # type: (...) -> None if default is None: default = [] # Create a new list for each instance self.field = field Field.__init__(self, name, default) self.count_from = count_from self.length_from = length_from + self.max_count = max_count def i2count(self, pkt, val): + # type: (Optional[Packet], List[Any]) -> int if isinstance(val, list): return len(val) return 1 def i2len(self, pkt, val): + # type: (Packet, List[Any]) -> int return int(sum(self.field.i2len(pkt, v) for v in val)) - def i2m(self, pkt, val): - if val is None: - val = [] - return val - def any2i(self, pkt, x): + # type: (Optional[Packet], List[Any]) -> List[Any] if not isinstance(x, list): return [self.field.any2i(pkt, x)] else: return [self.field.any2i(pkt, e) for e in x] - def i2repr(self, pkt, x): + def i2repr(self, + pkt, # type: Optional[Packet] + x, # type: List[Any] + ): + # type: (...) -> str return "[%s]" % ", ".join(self.field.i2repr(pkt, v) for v in x) - def addfield(self, pkt, s, val): + def addfield(self, + pkt, # type: Packet + s, # type: bytes + val, # type: Optional[List[Any]] + ): + # type: (...) -> bytes val = self.i2m(pkt, val) for v in val: s = self.field.addfield(pkt, s, v) return s - def getfield(self, pkt, s): + def getfield(self, + pkt, # type: Packet + s, # type: bytes + ): + # type: (...) -> Any c = len_pkt = None if self.length_from is not None: len_pkt = self.length_from(pkt) @@ -1444,116 +2161,231 @@ def getfield(self, pkt, s): c -= 1 s, v = self.field.getfield(pkt, s) val.append(v) - return s + ret, val + if len(val) > (self.max_count or conf.max_list_count): + raise MaximumItemsCount( + "Maximum amount of items reached in FieldListField: %s " + "(defaults to conf.max_list_count)" + % (self.max_count or conf.max_list_count) + ) + + if isinstance(s, tuple): + s, bn = s + return (s + ret, bn), val + else: + return s + ret, val -class FieldLenField(Field): +class FieldLenField(Field[int, int]): __slots__ = ["length_of", "count_of", "adjust"] - def __init__(self, name, default, length_of=None, fmt="H", count_of=None, adjust=lambda pkt, x: x, fld=None): # noqa: E501 + def __init__( + self, + name, # type: str + default, # type: Optional[Any] + length_of=None, # type: Optional[str] + fmt="H", # type: str + count_of=None, # type: Optional[str] + adjust=lambda pkt, x: x, # type: Callable[[Packet, int], int] + ): + # type: (...) -> None Field.__init__(self, name, default, fmt) self.length_of = length_of self.count_of = count_of self.adjust = adjust - if fld is not None: - # FIELD_LENGTH_MANAGEMENT_DEPRECATION(self.__class__.__name__) - self.length_of = fld def i2m(self, pkt, x): - if x is None: + # type: (Optional[Packet], Optional[int]) -> int + if x is None and pkt is not None: if self.length_of is not None: fld, fval = pkt.getfield_and_val(self.length_of) f = fld.i2len(pkt, fval) - else: + elif self.count_of is not None: fld, fval = pkt.getfield_and_val(self.count_of) f = fld.i2count(pkt, fval) + else: + raise ValueError( + "Field should have either length_of or count_of" + ) x = self.adjust(pkt, f) + elif x is None: + x = 0 return x class StrNullField(StrField): - def addfield(self, pkt, s, val): - return s + self.i2m(pkt, val) + b"\x00" + DELIMITER = b"\x00" - def getfield(self, pkt, s): - len_str = s.find(b"\x00") - if len_str < 0: - # XXX \x00 not found - return b"", s - return s[len_str + 1:], self.m2i(pkt, s[:len_str]) + def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[bytes]) -> bytes + return s + self.i2m(pkt, val) + self.DELIMITER + + def getfield(self, + pkt, # type: Packet + s, # type: bytes + ): + # type: (...) -> Tuple[bytes, bytes] + len_str = 0 + while True: + len_str = s.find(self.DELIMITER, len_str) + if len_str < 0: + # DELIMITER not found: return empty + return b"", s + if len_str % len(self.DELIMITER): + len_str += 1 + else: + break + return s[len_str + len(self.DELIMITER):], self.m2i(pkt, s[:len_str]) def randval(self): - return RandTermString(RandNum(0, 1200), b"\x00") + # type: () -> RandTermString + return RandTermString(RandNum(0, 1200), self.DELIMITER) + + def i2len(self, pkt, x): + # type: (Optional[Packet], Any) -> int + return super(StrNullField, self).i2len(pkt, x) + 1 + + +class StrNullFieldUtf16(StrNullField, StrFieldUtf16): + DELIMITER = b"\x00\x00" class StrStopField(StrField): __slots__ = ["stop", "additional"] def __init__(self, name, default, stop, additional=0): + # type: (str, str, bytes, int) -> None Field.__init__(self, name, default) self.stop = stop self.additional = additional def getfield(self, pkt, s): + # type: (Optional[Packet], bytes) -> Tuple[bytes, bytes] len_str = s.find(self.stop) if len_str < 0: return b"", s -# raise Scapy_Exception,"StrStopField: stop value [%s] not found" %stop # noqa: E501 len_str += len(self.stop) + self.additional return s[len_str:], s[:len_str] def randval(self): + # type: () -> RandTermString return RandTermString(RandNum(0, 1200), self.stop) -class LenField(Field): +class LenField(Field[int, int]): + """ + If None, will be filled with the size of the payload + """ __slots__ = ["adjust"] def __init__(self, name, default, fmt="H", adjust=lambda x: x): + # type: (str, Optional[Any], str, Callable[[int], int]) -> None Field.__init__(self, name, default, fmt) self.adjust = adjust - def i2m(self, pkt, x): + def i2m(self, + pkt, # type: Optional[Packet] + x, # type: Optional[int] + ): + # type: (...) -> int if x is None: - x = self.adjust(len(pkt.payload)) + x = 0 + if pkt is not None: + x = self.adjust(len(pkt.payload)) return x -class BCDFloatField(Field): +class BCDFloatField(Field[float, int]): def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[float]) -> int + if x is None: + return 0 return int(256 * x) def m2i(self, pkt, x): + # type: (Optional[Packet], int) -> float return x / 256.0 -class BitField(Field): - __slots__ = ["rev", "size"] +class _BitField(Field[I, int]): + """ + Field to handle bits. - def __init__(self, name, default, size): - Field.__init__(self, name, default) - self.rev = size < 0 - self.size = abs(size) - self.sz = self.size / 8. + :param name: name of the field + :param default: default value + :param size: size (in bits). If negative, Low endian + :param tot_size: size of the total group of bits (in bytes) the bitfield + is in. If negative, Low endian. + :param end_tot_size: same but for the BitField ending a group. - def reverse(self, val): - if self.size == 16: - # Replaces socket.ntohs (but work on both little/big endian) - val = struct.unpack('>H', struct.pack('I', struct.pack(' None + Field.__init__(self, name, default) + if callable(size): + size = size(self) + self.rev = size < 0 or tot_size < 0 or end_tot_size < 0 + self.size = abs(size) + if not tot_size: + tot_size = self.size // 8 + self.tot_size = abs(tot_size) + if not end_tot_size: + end_tot_size = self.size // 8 + self.end_tot_size = abs(end_tot_size) + # Fields always have a round sz except BitField + # so to keep it simple, we'll ignore it here. + self.sz = self.size / 8. # type: ignore + + # We need to # type: ignore a few things because of how special + # BitField is + def addfield(self, # type: ignore + pkt, # type: Packet + s, # type: Union[Tuple[bytes, int, int], bytes] + ival, # type: I + ): + # type: (...) -> Union[Tuple[bytes, int, int], bytes] + val = self.i2m(pkt, ival) if isinstance(s, tuple): s, bitsdone, v = s else: bitsdone = 0 v = 0 - if self.rev: - val = self.reverse(val) v <<= self.size v |= val & ((1 << self.size) - 1) bitsdone += self.size @@ -1564,13 +2396,24 @@ def addfield(self, pkt, s, val): if bitsdone: return s, bitsdone, v else: + # Apply LE if necessary + if self.rev and self.end_tot_size > 1: + s = s[:-self.end_tot_size] + s[-self.end_tot_size:][::-1] return s - def getfield(self, pkt, s): + def getfield(self, # type: ignore + pkt, # type: Packet + s, # type: Union[Tuple[bytes, int], bytes] + ): + # type: (...) -> Union[Tuple[Tuple[bytes, int], I], Tuple[bytes, I]] # noqa: E501 if isinstance(s, tuple): s, bn = s else: bn = 0 + # Apply LE if necessary + if self.rev and self.tot_size > 1: + s = s[:self.tot_size][::-1] + s[self.tot_size:] + # we don't want to process all the string nb_bytes = (self.size + bn - 1) // 8 + 1 w = s[:nb_bytes] @@ -1588,242 +2431,458 @@ def getfield(self, pkt, s): # remove low order bits b = b >> (nb_bytes * 8 - self.size - bn) - if self.rev: - b = self.reverse(b) - bn += self.size s = s[bn // 8:] bn = bn % 8 - b = self.m2i(pkt, b) + b2 = self.m2i(pkt, b) if bn: - return (s, bn), b + return (s, bn), b2 else: - return s, b + return s, b2 def randval(self): + # type: () -> RandNum return RandNum(0, 2**self.size - 1) - def i2len(self, pkt, x): + def i2len(self, pkt, x): # type: ignore + # type: (Optional[Packet], Optional[float]) -> float return float(self.size) / 8 -class BitFieldLenField(BitField): - __slots__ = ["length_of", "count_of", "adjust"] +class BitField(_BitField[int]): + __doc__ = _BitField.__doc__ + - def __init__(self, name, default, size, length_of=None, count_of=None, adjust=lambda pkt, x: x): # noqa: E501 - BitField.__init__(self, name, default, size) +class BitLenField(BitField): + __slots__ = ["length_from"] + + def __init__(self, + name, # type: str + default, # type: Optional[int] + length_from # type: Callable[[Packet], int] + ): + # type: (...) -> None + self.length_from = length_from + super(BitLenField, self).__init__(name, default, 0) + + def getfield(self, # type: ignore + pkt, # type: Packet + s, # type: Union[Tuple[bytes, int], bytes] + ): + # type: (...) -> Union[Tuple[Tuple[bytes, int], int], Tuple[bytes, int]] # noqa: E501 + self.size = self.length_from(pkt) + return super(BitLenField, self).getfield(pkt, s) + + def addfield(self, # type: ignore + pkt, # type: Packet + s, # type: Union[Tuple[bytes, int, int], bytes] + val # type: int + ): + # type: (...) -> Union[Tuple[bytes, int, int], bytes] + self.size = self.length_from(pkt) + return super(BitLenField, self).addfield(pkt, s, val) + + +class BitFieldLenField(BitField): + __slots__ = ["length_of", "count_of", "adjust", "tot_size", "end_tot_size"] + + def __init__(self, + name, # type: str + default, # type: Optional[int] + size, # type: int + length_of=None, # type: Optional[Union[Callable[[Optional[Packet]], int], str]] # noqa: E501 + count_of=None, # type: Optional[str] + adjust=lambda pkt, x: x, # type: Callable[[Optional[Packet], int], int] # noqa: E501 + tot_size=0, # type: int + end_tot_size=0, # type: int + ): + # type: (...) -> None + super(BitFieldLenField, self).__init__(name, default, size, + tot_size, end_tot_size) self.length_of = length_of self.count_of = count_of self.adjust = adjust def i2m(self, pkt, x): - return (FieldLenField.i2m.__func__ if six.PY2 else FieldLenField.i2m)(self, pkt, x) # noqa: E501 + # type: (Optional[Packet], Optional[Any]) -> int + return FieldLenField.i2m(self, pkt, x) # type: ignore class XBitField(BitField): def i2repr(self, pkt, x): + # type: (Optional[Packet], int) -> str return lhex(self.i2h(pkt, x)) -class _EnumField(Field): - def __init__(self, name, default, enum, fmt="H"): +_EnumType = Union[Dict[I, str], Dict[str, I], List[str], DADict[I, str], Type[Enum], Tuple[Callable[[I], str], Callable[[str], I]]] # noqa: E501 + + +class _EnumField(Field[Union[List[I], I], I]): + def __init__(self, + name, # type: str + default, # type: Optional[I] + enum, # type: _EnumType[I] + fmt="H", # type: str + ): + # type: (...) -> None """ Initializes enum fields. @param name: name of this field @param default: default value of this field - @param enum: either a dict or a tuple of two callables. Dict keys are # noqa: E501 - the internal values, while the dict values are the - user-friendly representations. If the tuple is provided, # noqa: E501 - the first callable receives the internal value as - parameter and returns the user-friendly representation - and the second callable does the converse. The first - callable may return None to default to a literal string - (repr()) representation. + @param enum: either an enum, a dict or a tuple of two callables. + Dict keys are the internal values, while the dict + values are the user-friendly representations. If the + tuple is provided, the first callable receives the + internal value as parameter and returns the + user-friendly representation and the second callable + does the converse. The first callable may return None + to default to a literal string (repr()) representation. @param fmt: struct.pack format used to parse and serialize the internal value from and to machine representation. """ if isinstance(enum, ObservableDict): - enum.observe(self) + cast(ObservableDict, enum).observe(self) if isinstance(enum, tuple): - self.i2s_cb = enum[0] - self.s2i_cb = enum[1] - self.i2s = None - self.s2i = None + self.i2s_cb = enum[0] # type: Optional[Callable[[I], str]] + self.s2i_cb = enum[1] # type: Optional[Callable[[str], I]] + self.i2s = None # type: Optional[Dict[I, str]] + self.s2i = None # type: Optional[Dict[str, I]] + elif isinstance(enum, type) and issubclass(enum, Enum): + # Python's Enum + i2s = self.i2s = {} + s2i = self.s2i = {} + self.i2s_cb = None + self.s2i_cb = None + names = [x.name for x in enum] + for n in names: + value = enum[n].value + i2s[value] = n + s2i[n] = value else: i2s = self.i2s = {} s2i = self.s2i = {} self.i2s_cb = None self.s2i_cb = None + keys = [] # type: List[I] if isinstance(enum, list): - keys = list(range(len(enum))) + keys = list(range(len(enum))) # type: ignore elif isinstance(enum, DADict): keys = enum.keys() else: - keys = list(enum) - if any(isinstance(x, str) for x in keys): - i2s, s2i = s2i, i2s + keys = list(enum) # type: ignore + if any(isinstance(x, str) for x in keys): + i2s, s2i = s2i, i2s # type: ignore for k in keys: - i2s[k] = enum[k] - s2i[enum[k]] = k + value = cast(str, enum[k]) # type: ignore + i2s[k] = value + s2i[value] = k Field.__init__(self, name, default, fmt) def any2i_one(self, pkt, x): - if isinstance(x, str): - try: + # type: (Optional[Packet], Any) -> I + if isinstance(x, Enum): + return cast(I, x.value) + elif isinstance(x, str): + if self.s2i: x = self.s2i[x] - except TypeError: + elif self.s2i_cb: x = self.s2i_cb(x) - return x + return cast(I, x) + + def _i2repr(self, pkt, x): + # type: (Optional[Packet], I) -> str + return repr(x) def i2repr_one(self, pkt, x): + # type: (Optional[Packet], I) -> str if self not in conf.noenum and not isinstance(x, VolatileValue): - try: - return self.i2s[x] - except KeyError: - pass - except TypeError: + if self.i2s: + try: + return self.i2s[x] + except KeyError: + pass + elif self.i2s_cb: ret = self.i2s_cb(x) if ret is not None: return ret - return repr(x) + return self._i2repr(pkt, x) def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> Union[I, List[I]] if isinstance(x, list): return [self.any2i_one(pkt, z) for z in x] else: return self.any2i_one(pkt, x) - def i2repr(self, pkt, x): + def i2repr(self, pkt, x): # type: ignore + # type: (Optional[Packet], Any) -> Union[List[str], str] if isinstance(x, list): return [self.i2repr_one(pkt, z) for z in x] else: return self.i2repr_one(pkt, x) def notify_set(self, enum, key, value): - log_runtime.debug("At %s: Change to %s at 0x%x" % (self, value, key)) - self.i2s[key] = value - self.s2i[value] = key + # type: (ObservableDict, I, str) -> None + ks = "0x%x" if isinstance(key, int) else "%s" + log_runtime.debug( + "At %s: Change to %s at " + ks, self, value, key + ) + if self.i2s is not None and self.s2i is not None: + self.i2s[key] = value + self.s2i[value] = key def notify_del(self, enum, key): - log_runtime.debug("At %s: Delete value at 0x%x" % (self, key)) - value = self.i2s[key] - del self.i2s[key] - del self.s2i[value] + # type: (ObservableDict, I) -> None + ks = "0x%x" if isinstance(key, int) else "%s" + log_runtime.debug("At %s: Delete value at " + ks, self, key) + if self.i2s is not None and self.s2i is not None: + value = self.i2s[key] + del self.i2s[key] + del self.s2i[value] -class EnumField(_EnumField): +class EnumField(_EnumField[I]): __slots__ = ["i2s", "s2i", "s2i_cb", "i2s_cb"] -class CharEnumField(EnumField): - def __init__(self, name, default, enum, fmt="1s"): - EnumField.__init__(self, name, default, enum, fmt) +class CharEnumField(EnumField[str]): + def __init__(self, + name, # type: str + default, # type: str + enum, # type: _EnumType[str] + fmt="1s", # type: str + ): + # type: (...) -> None + super(CharEnumField, self).__init__(name, default, enum, fmt) if self.i2s is not None: k = list(self.i2s) if k and len(k[0]) != 1: self.i2s, self.s2i = self.s2i, self.i2s def any2i_one(self, pkt, x): + # type: (Optional[Packet], str) -> str if len(x) != 1: - if self.s2i is None: - x = self.s2i_cb(x) - else: + if self.s2i: x = self.s2i[x] + elif self.s2i_cb: + x = self.s2i_cb(x) return x -class BitEnumField(BitField, _EnumField): +class BitEnumField(_BitField[Union[List[int], int]], _EnumField[int]): __slots__ = EnumField.__slots__ - def __init__(self, name, default, size, enum): + def __init__(self, + name, # type: str + default, # type: Optional[int] + size, # type: int + enum, # type: _EnumType[int] + **kwargs # type: Any + ): + # type: (...) -> None _EnumField.__init__(self, name, default, enum) - self.rev = size < 0 - self.size = abs(size) - self.sz = self.size / 8. + _BitField.__init__(self, name, default, size, **kwargs) def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> Union[List[int], int] return _EnumField.any2i(self, pkt, x) - def i2repr(self, pkt, x): + def i2repr(self, + pkt, # type: Optional[Packet] + x, # type: Union[List[int], int] + ): + # type: (...) -> Any return _EnumField.i2repr(self, pkt, x) -class ShortEnumField(EnumField): +class BitLenEnumField(BitLenField, _EnumField[int]): __slots__ = EnumField.__slots__ - def __init__(self, name, default, enum): - EnumField.__init__(self, name, default, enum, "H") + def __init__(self, + name, # type: str + default, # type: Optional[int] + length_from, # type: Callable[[Packet], int] + enum, # type: _EnumType[int] + **kwargs, # type: Any + ): + # type: (...) -> None + _EnumField.__init__(self, name, default, enum) + BitLenField.__init__(self, name, default, length_from, **kwargs) + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> int + return _EnumField.any2i(self, pkt, x) # type: ignore + + def i2repr(self, + pkt, # type: Optional[Packet] + x, # type: Union[List[int], int] + ): + # type: (...) -> Any + return _EnumField.i2repr(self, pkt, x) -class LEShortEnumField(EnumField): - def __init__(self, name, default, enum): - EnumField.__init__(self, name, default, enum, " None + super(ShortEnumField, self).__init__(name, default, enum, "H") + + +class LEShortEnumField(EnumField[int]): + def __init__(self, + name, # type: str + default, # type: Optional[int] + enum, # type: _EnumType[int] + ): + # type: (...) -> None + super(LEShortEnumField, self).__init__(name, default, enum, " None + super(LongEnumField, self).__init__(name, default, enum, "Q") + + +class LELongEnumField(EnumField[int]): + def __init__(self, + name, # type: str + default, # type: Optional[int] + enum, # type: _EnumType[int] + ): + # type: (...) -> None + super(LELongEnumField, self).__init__(name, default, enum, " None + super(ByteEnumField, self).__init__(name, default, enum, "B") class XByteEnumField(ByteEnumField): def i2repr_one(self, pkt, x): + # type: (Optional[Packet], int) -> str if self not in conf.noenum and not isinstance(x, VolatileValue): - try: - return self.i2s[x] - except KeyError: - pass - except TypeError: + if self.i2s: + try: + return self.i2s[x] + except KeyError: + pass + elif self.i2s_cb: ret = self.i2s_cb(x) if ret is not None: return ret return lhex(x) -class IntEnumField(EnumField): - def __init__(self, name, default, enum): - EnumField.__init__(self, name, default, enum, "I") +class IntEnumField(EnumField[int]): + def __init__(self, + name, # type: str + default, # type: Optional[int] + enum, # type: _EnumType[int] + ): + # type: (...) -> None + super(IntEnumField, self).__init__(name, default, enum, "I") -class SignedIntEnumField(EnumField): - def __init__(self, name, default, enum): - EnumField.__init__(self, name, default, enum, "i") +class SignedIntEnumField(EnumField[int]): + def __init__(self, + name, # type: str + default, # type: Optional[int] + enum, # type: _EnumType[int] + ): + # type: (...) -> None + super(SignedIntEnumField, self).__init__(name, default, enum, "i") -class LEIntEnumField(EnumField): - def __init__(self, name, default, enum): - EnumField.__init__(self, name, default, enum, " None + super(LEIntEnumField, self).__init__(name, default, enum, " str + return lhex(x) class XShortEnumField(ShortEnumField): - def i2repr_one(self, pkt, x): - if self not in conf.noenum and not isinstance(x, VolatileValue): - try: - return self.i2s[x] - except KeyError: - pass - except TypeError: - ret = self.i2s_cb(x) - if ret is not None: - return ret + def _i2repr(self, pkt, x): + # type: (Optional[Packet], Any) -> str return lhex(x) -class _MultiEnumField(_EnumField): - def __init__(self, name, default, enum, depends_on, fmt="H"): +class LE3BytesEnumField(LEThreeBytesField, _EnumField[int]): + __slots__ = EnumField.__slots__ + + def __init__(self, + name, # type: str + default, # type: Optional[int] + enum, # type: _EnumType[int] + ): + # type: (...) -> None + _EnumField.__init__(self, name, default, enum) + LEThreeBytesField.__init__(self, name, default) + + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> int + return _EnumField.any2i(self, pkt, x) # type: ignore + + def i2repr(self, pkt, x): # type: ignore + # type: (Optional[Packet], Any) -> Union[List[str], str] + return _EnumField.i2repr(self, pkt, x) + + +class XLE3BytesEnumField(LE3BytesEnumField): + def _i2repr(self, pkt, x): + # type: (Optional[Packet], Any) -> str + return lhex(x) + + +class _MultiEnumField(_EnumField[I]): + def __init__(self, + name, # type: str + default, # type: int + enum, # type: Dict[I, Dict[I, str]] + depends_on, # type: Callable[[Optional[Packet]], I] + fmt="H" # type: str + ): + # type: (...) -> None self.depends_on = depends_on self.i2s_multi = enum - self.s2i_multi = {} - self.s2i_all = {} + self.s2i_multi = {} # type: Dict[I, Dict[str, I]] + self.s2i_all = {} # type: Dict[str, I] for m in enum: - self.s2i_multi[m] = s2i = {} - for k, v in six.iteritems(enum[m]): + s2i = {} # type: Dict[str, I] + self.s2i_multi[m] = s2i + for k, v in enum[m].items(): s2i[v] = k self.s2i_all[v] = k Field.__init__(self, name, default, fmt) def any2i_one(self, pkt, x): + # type: (Optional[Packet], Any) -> I if isinstance(x, str): v = self.depends_on(pkt) if v in self.s2i_multi: @@ -1831,78 +2890,123 @@ def any2i_one(self, pkt, x): if x in s2i: return s2i[x] return self.s2i_all[x] - return x + return cast(I, x) def i2repr_one(self, pkt, x): + # type: (Optional[Packet], I) -> str v = self.depends_on(pkt) if isinstance(v, VolatileValue): return repr(v) if v in self.i2s_multi: - return self.i2s_multi[v].get(x, x) - return x + return str(self.i2s_multi[v].get(x, x)) + return str(x) -class MultiEnumField(_MultiEnumField, EnumField): +class MultiEnumField(_MultiEnumField[int], EnumField[int]): __slots__ = ["depends_on", "i2s_multi", "s2i_multi", "s2i_all"] -class BitMultiEnumField(BitField, _MultiEnumField): +class BitMultiEnumField(_BitField[Union[List[int], int]], + _MultiEnumField[int]): __slots__ = EnumField.__slots__ + MultiEnumField.__slots__ - def __init__(self, name, default, size, enum, depends_on): + def __init__( + self, + name, # type: str + default, # type: int + size, # type: int + enum, # type: Dict[int, Dict[int, str]] + depends_on # type: Callable[[Optional[Packet]], int] + ): + # type: (...) -> None _MultiEnumField.__init__(self, name, default, enum, depends_on) self.rev = size < 0 self.size = abs(size) - self.sz = self.size / 8. + self.sz = self.size / 8. # type: ignore def any2i(self, pkt, x): - return _MultiEnumField.any2i(self, pkt, x) - - def i2repr(self, pkt, x): - return _MultiEnumField.i2repr(self, pkt, x) + # type: (Optional[Packet], Any) -> Union[List[int], int] + return _MultiEnumField[int].any2i( + self, # type: ignore + pkt, + x + ) + + def i2repr( # type: ignore + self, + pkt, # type: Optional[Packet] + x # type: Union[List[int], int] + ): + # type: (...) -> Union[str, List[str]] + return _MultiEnumField[int].i2repr( + self, # type: ignore + pkt, + x + ) class ByteEnumKeysField(ByteEnumField): """ByteEnumField that picks valid values when fuzzed. """ def randval(self): - return RandEnumKeys(self.i2s) + # type: () -> RandEnumKeys + return RandEnumKeys(self.i2s or {}) class ShortEnumKeysField(ShortEnumField): """ShortEnumField that picks valid values when fuzzed. """ def randval(self): - return RandEnumKeys(self.i2s) + # type: () -> RandEnumKeys + return RandEnumKeys(self.i2s or {}) class IntEnumKeysField(IntEnumField): """IntEnumField that picks valid values when fuzzed. """ def randval(self): - return RandEnumKeys(self.i2s) + # type: () -> RandEnumKeys + return RandEnumKeys(self.i2s or {}) # Little endian fixed length field class LEFieldLenField(FieldLenField): - def __init__(self, name, default, length_of=None, fmt=" None + FieldLenField.__init__( + self, name, default, + length_of=length_of, + fmt=fmt, + count_of=count_of, + adjust=adjust + ) class FlagValueIter(object): - slots = ["flagvalue", "cursor"] + __slots__ = ["flagvalue", "cursor"] def __init__(self, flagvalue): + # type: (FlagValue) -> None self.flagvalue = flagvalue self.cursor = 0 def __iter__(self): + # type: () -> FlagValueIter return self def __next__(self): + # type: () -> str x = int(self.flagvalue) x >>= self.cursor while x: @@ -1919,9 +3023,10 @@ class FlagValue(object): __slots__ = ["value", "names", "multi"] def _fixvalue(self, value): + # type: (Any) -> int if not value: return 0 - if isinstance(value, six.string_types): + if isinstance(value, str): value = value.split('+') if self.multi else list(value) if isinstance(value, list): y = 0 @@ -1931,79 +3036,118 @@ def _fixvalue(self, value): return int(value) def __init__(self, value, names): + # type: (Union[List[str], int, str], Union[List[str], str]) -> None self.multi = isinstance(names, list) self.names = names self.value = self._fixvalue(value) def __hash__(self): + # type: () -> int return hash(self.value) def __int__(self): + # type: () -> int return self.value def __eq__(self, other): + # type: (Any) -> bool return self.value == self._fixvalue(other) def __lt__(self, other): + # type: (Any) -> bool return self.value < self._fixvalue(other) def __le__(self, other): + # type: (Any) -> bool return self.value <= self._fixvalue(other) def __gt__(self, other): + # type: (Any) -> bool return self.value > self._fixvalue(other) def __ge__(self, other): + # type: (Any) -> bool return self.value >= self._fixvalue(other) def __ne__(self, other): + # type: (Any) -> bool return self.value != self._fixvalue(other) def __and__(self, other): + # type: (int) -> FlagValue return self.__class__(self.value & self._fixvalue(other), self.names) __rand__ = __and__ def __or__(self, other): + # type: (int) -> FlagValue return self.__class__(self.value | self._fixvalue(other), self.names) __ror__ = __or__ + __add__ = __or__ # + is an alias for | + + def __sub__(self, other): + # type: (int) -> FlagValue + return self.__class__( + self.value & (2 ** len(self.names) - 1 - self._fixvalue(other)), + self.names + ) + + def __xor__(self, other): + # type: (int) -> FlagValue + return self.__class__(self.value ^ self._fixvalue(other), self.names) def __lshift__(self, other): + # type: (int) -> int return self.value << self._fixvalue(other) def __rshift__(self, other): + # type: (int) -> int return self.value >> self._fixvalue(other) def __nonzero__(self): + # type: () -> bool return bool(self.value) __bool__ = __nonzero__ def flagrepr(self): - warning("obj.flagrepr() is obsolete. Use str(obj) instead.") + # type: () -> str + warnings.warn( + "obj.flagrepr() is obsolete. Use str(obj) instead.", + DeprecationWarning + ) return str(self) def __str__(self): + # type: () -> str i = 0 r = [] x = int(self) while x: if x & 1: - r.append(self.names[i]) + try: + name = self.names[i] + except IndexError: + name = "?" + r.append(name) i += 1 x >>= 1 return ("+" if self.multi else "").join(r) def __iter__(self): + # type: () -> FlagValueIter return FlagValueIter(self) def __repr__(self): + # type: () -> str return "" % (self, self) def __deepcopy__(self, memo): + # type: (Dict[Any, Any]) -> FlagValue return self.__class__(int(self), self.names) def __getattr__(self, attr): + # type: (str) -> Any if attr in self.__slots__: - return super(FlagValue, self).__getattr__(attr) + return super(FlagValue, self).__getattribute__(attr) try: if self.multi: return bool((2 ** self.names.index(attr)) & int(self)) @@ -2015,10 +3159,11 @@ def __getattr__(self, attr): return self.__getattr__(attr.replace('_', '-')) except AttributeError: pass - return super(FlagValue, self).__getattr__(attr) + return super(FlagValue, self).__getattribute__(attr) def __setattr__(self, attr, value): - if attr == "value" and not isinstance(value, six.integer_types): + # type: (str, Union[List[str], int, str]) -> None + if attr == "value" and not isinstance(value, int): raise ValueError(value) if attr in self.__slots__: return super(FlagValue, self).__setattr__(attr, value) @@ -2031,85 +3176,135 @@ def __setattr__(self, attr, value): return super(FlagValue, self).__setattr__(attr, value) def copy(self): + # type: () -> FlagValue return self.__class__(self.value, self.names) -class FlagsField(BitField): +class FlagsField(_BitField[Optional[Union[int, FlagValue]]]): """ Handle Flag type field Make sure all your flags have a label - Example: + Example (list): >>> from scapy.packet import Packet >>> class FlagsTest(Packet): fields_desc = [FlagsField("flags", 0, 8, ["f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7"])] # noqa: E501 >>> FlagsTest(flags=9).show2() ###[ FlagsTest ]### flags = f0+f3 - >>> FlagsTest(flags=0).show2().strip() + + Example (str): + >>> from scapy.packet import Packet + >>> class TCPTest(Packet): + fields_desc = [ + BitField("reserved", 0, 7), + FlagsField("flags", 0x2, 9, "FSRPAUECN") + ] + >>> TCPTest(flags=3).show2() ###[ FlagsTest ]### - flags = + reserved = 0 + flags = FS + + Example (dict): + >>> from scapy.packet import Packet + >>> class FlagsTest2(Packet): + fields_desc = [ + FlagsField("flags", 0x2, 16, { + 0x0001: "A", + 0x0008: "B", + }) + ] :param name: field's name :param default: default value for the field - :param size: number of bits in the field - :param names: (list or dict) label for each flag, Least Significant Bit tag's name is written first # noqa: E501 + :param size: number of bits in the field (in bits). if negative, LE + :param names: (list or str or dict) label for each flag + If it's a str or a list, the least Significant Bit tag's name + is written first. """ ismutable = True - __slots__ = ["multi", "names"] - - def __init__(self, name, default, size, names): - self.multi = isinstance(names, list) + __slots__ = ["names"] + + def __init__(self, + name, # type: str + default, # type: Optional[Union[int, FlagValue]] + size, # type: int + names, # type: Union[List[str], str, Dict[int, str]] + **kwargs # type: Any + ): + # type: (...) -> None + # Convert the dict to a list + if isinstance(names, dict): + tmp = ["bit_%d" % i for i in range(abs(size))] + for i, v in names.items(): + tmp[int(math.floor(math.log(i, 2)))] = v + names = tmp + # Store the names as str or list self.names = names - BitField.__init__(self, name, default, size) + super(FlagsField, self).__init__(name, default, size, **kwargs) def _fixup_val(self, x): + # type: (Any) -> Optional[FlagValue] """Returns a FlagValue instance when needed. Internal method, to be used in *2i() and i2*() methods. """ - if isinstance(x, FlagValue): - return x + if isinstance(x, (FlagValue, VolatileValue)): + return x # type: ignore if x is None: return None return FlagValue(x, self.names) def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> Optional[FlagValue] return self._fixup_val(super(FlagsField, self).any2i(pkt, x)) def m2i(self, pkt, x): + # type: (Optional[Packet], int) -> Optional[FlagValue] return self._fixup_val(super(FlagsField, self).m2i(pkt, x)) def i2h(self, pkt, x): - if isinstance(x, VolatileValue): - return super(FlagsField, self).i2h(pkt, x) + # type: (Optional[Packet], Any) -> Optional[FlagValue] return self._fixup_val(super(FlagsField, self).i2h(pkt, x)) - def i2repr(self, pkt, x): + def i2repr(self, + pkt, # type: Optional[Packet] + x, # type: Any + ): + # type: (...) -> str if isinstance(x, (list, tuple)): return repr(type(x)( - None if v is None else str(self._fixup_val(v)) for v in x + "None" if v is None else str(self._fixup_val(v)) for v in x )) - return None if x is None else str(self._fixup_val(x)) + return "None" if x is None else str(self._fixup_val(x)) -MultiFlagsEntry = collections.namedtuple('MultiFlagEntry', ['short', 'long']) +MultiFlagsEntry = collections.namedtuple('MultiFlagsEntry', ['short', 'long']) -class MultiFlagsField(BitField): +class MultiFlagsField(_BitField[Set[str]]): __slots__ = FlagsField.__slots__ + ["depends_on"] - def __init__(self, name, default, size, names, depends_on): + def __init__(self, + name, # type: str + default, # type: Set[str] + size, # type: int + names, # type: Dict[int, Dict[int, MultiFlagsEntry]] + depends_on, # type: Callable[[Optional[Packet]], int] + ): + # type: (...) -> None self.names = names self.depends_on = depends_on super(MultiFlagsField, self).__init__(name, default, size) def any2i(self, pkt, x): - assert isinstance(x, six.integer_types + (set,)), 'set expected' + # type: (Optional[Packet], Any) -> Set[str] + if not isinstance(x, (set, int)): + raise ValueError('set expected') if pkt is not None: - if isinstance(x, six.integer_types): - x = self.m2i(pkt, x) + if isinstance(x, int): + return self.m2i(pkt, x) else: v = self.depends_on(pkt) if v is not None: @@ -2117,23 +3312,28 @@ def any2i(self, pkt, x): these_names = self.names[v] s = set() for i in x: - for val in six.itervalues(these_names): + for val in these_names.values(): if val.short == i: s.add(i) break else: assert False, 'Unknown flag "{}" with this dependency'.format(i) # noqa: E501 continue - x = s + return s + if isinstance(x, int): + return set() return x def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[Set[str]]) -> int v = self.depends_on(pkt) these_names = self.names.get(v, {}) r = 0 + if x is None: + return r for flag_set in x: - for i, val in six.iteritems(these_names): + for i, val in these_names.items(): if val.short == flag_set: r |= 1 << i break @@ -2142,6 +3342,7 @@ def i2m(self, pkt, x): return r def m2i(self, pkt, x): + # type: (Optional[Packet], int) -> Set[str] v = self.depends_on(pkt) these_names = self.names.get(v, {}) @@ -2158,12 +3359,13 @@ def m2i(self, pkt, x): return r def i2repr(self, pkt, x): + # type: (Optional[Packet], Set[str]) -> str v = self.depends_on(pkt) these_names = self.names.get(v, {}) r = set() for flag_set in x: - for i in six.itervalues(these_names): + for i in these_names.values(): if i.short == flag_set: r.add("{} ({})".format(i.long, i.short)) break @@ -2176,10 +3378,12 @@ class FixedPointField(BitField): __slots__ = ['frac_bits'] def __init__(self, name, default, size, frac_bits=16): + # type: (str, int, int, int) -> None self.frac_bits = frac_bits - BitField.__init__(self, name, default, size) + super(FixedPointField, self).__init__(name, default, size) def any2i(self, pkt, val): + # type: (Optional[Packet], Optional[float]) -> Optional[int] if val is None: return val ival = int(val) @@ -2187,50 +3391,79 @@ def any2i(self, pkt, val): return (ival << self.frac_bits) | fract def i2h(self, pkt, val): + # type: (Optional[Packet], Optional[int]) -> Optional[EDecimal] + # A bit of trickery to get precise floats + if val is None: + return val int_part = val >> self.frac_bits - frac_part = val & (1 << self.frac_bits) - 1 - frac_part /= 2.0**self.frac_bits - return int_part + frac_part + pw = 2.0**self.frac_bits + frac_part = EDecimal(val & (1 << self.frac_bits) - 1) + frac_part /= pw # type: ignore + return int_part + frac_part.normalize(int(math.log10(pw))) def i2repr(self, pkt, val): - return self.i2h(pkt, val) + # type: (Optional[Packet], int) -> str + return str(self.i2h(pkt, val)) # Base class for IPv4 and IPv6 Prefixes inspired by IPField and IP6Field. # Machine values are encoded in a multiple of wordbytes bytes. -class _IPPrefixFieldBase(Field): +class _IPPrefixFieldBase(Field[Tuple[str, int], Tuple[bytes, int]]): __slots__ = ["wordbytes", "maxbytes", "aton", "ntoa", "length_from"] - def __init__(self, name, default, wordbytes, maxbytes, aton, ntoa, length_from): # noqa: E501 + def __init__( + self, + name, # type: str + default, # type: Tuple[str, int] + wordbytes, # type: int + maxbytes, # type: int + aton, # type: Callable[..., Any] + ntoa, # type: Callable[..., Any] + length_from=None # type: Optional[Callable[[Packet], int]] + ): + # type: (...) -> None self.wordbytes = wordbytes self.maxbytes = maxbytes self.aton = aton self.ntoa = ntoa Field.__init__(self, name, default, "%is" % self.maxbytes) + if length_from is None: + length_from = lambda x: 0 self.length_from = length_from def _numbytes(self, pfxlen): + # type: (int) -> int wbits = self.wordbytes * 8 return ((pfxlen + (wbits - 1)) // wbits) * self.wordbytes def h2i(self, pkt, x): + # type: (Optional[Packet], str) -> Tuple[str, int] # "fc00:1::1/64" -> ("fc00:1::1", 64) [pfx, pfxlen] = x.split('/') self.aton(pfx) # check for validity return (pfx, int(pfxlen)) def i2h(self, pkt, x): + # type: (Optional[Packet], Tuple[str, int]) -> str # ("fc00:1::1", 64) -> "fc00:1::1/64" (pfx, pfxlen) = x return "%s/%i" % (pfx, pfxlen) - def i2m(self, pkt, x): + def i2m(self, + pkt, # type: Optional[Packet] + x # type: Optional[Tuple[str, int]] + ): + # type: (...) -> Tuple[bytes, int] # ("fc00:1::1", 64) -> (b"\xfc\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 64) # noqa: E501 - (pfx, pfxlen) = x + if x is None: + pfx, pfxlen = "", 0 + else: + (pfx, pfxlen) = x s = self.aton(pfx) return (s[:self._numbytes(pfxlen)], pfxlen) def m2i(self, pkt, x): + # type: (Optional[Packet], Tuple[bytes, int]) -> Tuple[str, int] # (b"\xfc\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 64) -> ("fc00:1::1", 64) # noqa: E501 (s, pfxlen) = x @@ -2239,21 +3472,25 @@ def m2i(self, pkt, x): return (self.ntoa(s), pfxlen) def any2i(self, pkt, x): + # type: (Optional[Packet], Optional[Any]) -> Tuple[str, int] if x is None: return (self.ntoa(b"\0" * self.maxbytes), 1) return self.h2i(pkt, x) def i2len(self, pkt, x): + # type: (Packet, Tuple[str, int]) -> int (_, pfxlen) = x return pfxlen def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[Tuple[str, int]]) -> bytes (rawpfx, pfxlen) = self.i2m(pkt, val) fmt = "!%is" % self._numbytes(pfxlen) return s + struct.pack(fmt, rawpfx) def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, Tuple[str, int]] pfxlen = self.length_from(pkt) numbytes = self._numbytes(pfxlen) fmt = "!%is" % numbytes @@ -2261,28 +3498,63 @@ def getfield(self, pkt, s): class IPPrefixField(_IPPrefixFieldBase): - def __init__(self, name, default, wordbytes=1, length_from=None): - _IPPrefixFieldBase.__init__(self, name, default, wordbytes, 4, inet_aton, inet_ntoa, length_from) # noqa: E501 + def __init__( + self, + name, # type: str + default, # type: Tuple[str, int] + wordbytes=1, # type: int + length_from=None # type: Optional[Callable[[Packet], int]] + ): + _IPPrefixFieldBase.__init__( + self, + name, + default, + wordbytes, + 4, + inet_aton, + inet_ntoa, + length_from + ) class IP6PrefixField(_IPPrefixFieldBase): - def __init__(self, name, default, wordbytes=1, length_from=None): - _IPPrefixFieldBase.__init__(self, name, default, wordbytes, 16, lambda a: inet_pton(socket.AF_INET6, a), lambda n: inet_ntop(socket.AF_INET6, n), length_from) # noqa: E501 - - -class UTCTimeField(IntField): + def __init__( + self, + name, # type: str + default, # type: Tuple[str, int] + wordbytes=1, # type: int + length_from=None # type: Optional[Callable[[Packet], int]] + ): + # type: (...) -> None + _IPPrefixFieldBase.__init__( + self, + name, + default, + wordbytes, + 16, + lambda a: inet_pton(socket.AF_INET6, a), + lambda n: inet_ntop(socket.AF_INET6, n), + length_from + ) + + +class UTCTimeField(Field[float, int]): __slots__ = ["epoch", "delta", "strf", - "use_msec", "use_micro", "use_nano"] - - # Do not change the order of the keywords in here - # Netflow heavily rely on this - def __init__(self, name, default, - use_msec=False, - use_micro=False, - use_nano=False, - epoch=None, - strf="%a, %d %b %Y %H:%M:%S %z"): - IntField.__init__(self, name, default) + "use_msec", "use_micro", "use_nano", "custom_scaling"] + + def __init__(self, + name, # type: str + default, # type: int + use_msec=False, # type: bool + use_micro=False, # type: bool + use_nano=False, # type: bool + epoch=None, # type: Optional[Tuple[int, int, int, int, int, int, int, int, int]] # noqa: E501 + strf="%a, %d %b %Y %H:%M:%S %z", # type: str + custom_scaling=None, # type: Optional[int] + fmt="I" # type: str + ): + # type: (...) -> None + Field.__init__(self, name, default, fmt=fmt) mk_epoch = EPOCH if epoch is None else calendar.timegm(epoch) self.epoch = mk_epoch self.delta = mk_epoch - EPOCH @@ -2290,51 +3562,139 @@ def __init__(self, name, default, self.use_msec = use_msec self.use_micro = use_micro self.use_nano = use_nano + self.custom_scaling = custom_scaling def i2repr(self, pkt, x): + # type: (Optional[Packet], float) -> str if x is None: - x = 0 + x = time.time() - self.delta elif self.use_msec: x = x / 1e3 elif self.use_micro: x = x / 1e6 elif self.use_nano: x = x / 1e9 - x = int(x) + self.delta - t = time.strftime(self.strf, time.gmtime(x)) - return "%s (%d)" % (t, x) + elif self.custom_scaling: + x = x / self.custom_scaling + x += self.delta + # To make negative timestamps work on all plateforms (e.g. Windows), + # we need a trick. + t = ( + datetime.datetime(1970, 1, 1) + + datetime.timedelta(seconds=x) + ).strftime(self.strf) + return "%s (%d)" % (t, int(x)) def i2m(self, pkt, x): - return int(x) if x is not None else 0 - - -class SecondsIntField(IntField): + # type: (Optional[Packet], Optional[float]) -> int + if x is None: + x = time.time() - self.delta + if self.use_msec: + x = x * 1e3 + elif self.use_micro: + x = x * 1e6 + elif self.use_nano: + x = x * 1e9 + elif self.custom_scaling: + x = x * self.custom_scaling + return int(x) + return int(x) + + +class SecondsIntField(Field[float, int]): __slots__ = ["use_msec", "use_micro", "use_nano"] - # Do not change the order of the keywords in here - # Netflow heavily rely on this def __init__(self, name, default, use_msec=False, use_micro=False, use_nano=False): - IntField.__init__(self, name, default) + # type: (str, int, bool, bool, bool) -> None + Field.__init__(self, name, default, "I") self.use_msec = use_msec self.use_micro = use_micro self.use_nano = use_nano def i2repr(self, pkt, x): + # type: (Optional[Packet], Optional[float]) -> str if x is None: - x = 0 + y = 0 # type: Union[int, float] elif self.use_msec: - x = x / 1e3 + y = x / 1e3 elif self.use_micro: - x = x / 1e6 + y = x / 1e6 elif self.use_nano: - x = x / 1e9 - return "%s sec" % x + y = x / 1e9 + else: + y = x + return "%s sec" % y + + +class _ScalingField(object): + def __init__(self, + name, # type: str + default, # type: float + scaling=1, # type: Union[int, float] + unit="", # type: str + offset=0, # type: Union[int, float] + ndigits=3, # type: int + fmt="B", # type: str + ): + # type: (...) -> None + self.scaling = scaling + self.unit = unit + self.offset = offset + self.ndigits = ndigits + Field.__init__(self, name, default, fmt) # type: ignore + + def i2m(self, + pkt, # type: Optional[Packet] + x # type: Optional[Union[int, float]] + ): + # type: (...) -> Union[int, float] + if x is None: + x = 0 + x = (x - self.offset) / self.scaling + if isinstance(x, float) and self.fmt[-1] != "f": # type: ignore + x = int(round(x)) + return x + + def m2i(self, pkt, x): + # type: (Optional[Packet], Union[int, float]) -> Union[int, float] + x = x * self.scaling + self.offset + if isinstance(x, float) and self.fmt[-1] != "f": # type: ignore + x = round(x, self.ndigits) + return x + + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> Union[int, float] + if isinstance(x, (str, bytes)): + x = struct.unpack(self.fmt, bytes_encode(x))[0] # type: ignore + x = self.m2i(pkt, x) + if not isinstance(x, (int, float)): + raise ValueError("Unknown type") + return x + def i2repr(self, pkt, x): + # type: (Optional[Packet], Union[int, float]) -> str + return "%s %s" % ( + self.i2h(pkt, x), # type: ignore + self.unit + ) -class ScalingField(Field): + def randval(self): + # type: () -> RandFloat + value = Field.randval(self) # type: ignore + if value is not None: + min_val = round(value.min * self.scaling + self.offset, + self.ndigits) + max_val = round(value.max * self.scaling + self.offset, + self.ndigits) + + return RandFloat(min(min_val, max_val), max(min_val, max_val)) + + +class ScalingField(_ScalingField, + Field[Union[int, float], Union[int, float]]): """ Handle physical values which are scaled and/or offset for communication Example: @@ -2374,51 +3734,36 @@ class ScalingField(Field): :param ndigits: number of fractional digits for the internal conversion :param fmt: struct.pack format used to parse and serialize the internal value from and to machine representation # noqa: E501 """ - __slots__ = ["scaling", "unit", "offset", "ndigits"] - - def __init__(self, name, default, scaling=1, unit="", - offset=0, ndigits=3, fmt="B"): - self.scaling = scaling - self.unit = unit - self.offset = offset - self.ndigits = ndigits - Field.__init__(self, name, default, fmt) - def i2m(self, pkt, x): - if x is None: - x = 0 - x = (x - self.offset) / self.scaling - if isinstance(x, float) and self.fmt[-1] != "f": - x = int(round(x)) - return x - def m2i(self, pkt, x): - x = x * self.scaling + self.offset - if isinstance(x, float) and self.fmt[-1] != "f": - x = round(x, self.ndigits) - return x +class BitScalingField(_ScalingField, BitField): # type: ignore + """ + A ScalingField that is a BitField + """ - def any2i(self, pkt, x): - if isinstance(x, (str, bytes)): - x = struct.unpack(self.fmt, bytes_encode(x))[0] - x = self.m2i(pkt, x) - return x + def __init__(self, name, default, size, *args, **kwargs): + # type: (str, int, int, *Any, **Any) -> None + _ScalingField.__init__(self, name, default, *args, **kwargs) + BitField.__init__(self, name, default, size) # type: ignore - def i2repr(self, pkt, x): - return "%s %s" % (self.i2h(pkt, x), self.unit) - def randval(self): - value = super(ScalingField, self).randval() - if value is not None: - min_val = round(value.min * self.scaling + self.offset, - self.ndigits) - max_val = round(value.max * self.scaling + self.offset, - self.ndigits) +class OUIField(X3BytesField): + """ + A field designed to carry a OUI (3 bytes) + """ - return RandFloat(min(min_val, max_val), max(min_val, max_val)) + def i2repr(self, pkt, val): + # type: (Optional[Packet], int) -> str + by_val = struct.pack("!I", val or 0)[1:] + oui = str2mac(by_val + b"\0" * 3)[:8] + if conf.manufdb: + fancy = conf.manufdb._get_manuf(oui) + if fancy != oui: + return "%s (%s)" % (fancy, oui) + return oui -class UUIDField(Field): +class UUIDField(Field[UUID, bytes]): """Field for UUID storage, wrapping Python's uuid.UUID type. The internal storage format of this field is ``uuid.UUID`` from the Python @@ -2482,17 +3827,20 @@ class UUIDField(Field): FORMATS = (FORMAT_BE, FORMAT_LE, FORMAT_REV) def __init__(self, name, default, uuid_fmt=FORMAT_BE): + # type: (str, Optional[int], int) -> None self.uuid_fmt = uuid_fmt self._check_uuid_fmt() Field.__init__(self, name, default, "16s") def _check_uuid_fmt(self): + # type: () -> None """Checks .uuid_fmt, and raises an exception if it is not valid.""" if self.uuid_fmt not in UUIDField.FORMATS: raise FieldValueRangeException( "Unsupported uuid_fmt ({})".format(self.uuid_fmt)) def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[UUID]) -> bytes self._check_uuid_fmt() if x is None: return b'\0' * 16 @@ -2502,8 +3850,14 @@ def i2m(self, pkt, x): return x.bytes_le elif self.uuid_fmt == UUIDField.FORMAT_REV: return x.bytes[::-1] + else: + raise FieldAttributeException("Unknown fmt") - def m2i(self, pkt, x): + def m2i(self, + pkt, # type: Optional[Packet] + x, # type: bytes + ): + # type: (...) -> UUID self._check_uuid_fmt() if self.uuid_fmt == UUIDField.FORMAT_BE: return UUID(bytes=x) @@ -2511,15 +3865,21 @@ def m2i(self, pkt, x): return UUID(bytes_le=x) elif self.uuid_fmt == UUIDField.FORMAT_REV: return UUID(bytes=x[::-1]) + else: + raise FieldAttributeException("Unknown fmt") - def any2i(self, pkt, x): + def any2i(self, + pkt, # type: Optional[Packet] + x # type: Any # noqa: E501 + ): + # type: (...) -> Optional[UUID] # Python's uuid doesn't handle bytearray, so convert to an immutable # type first. if isinstance(x, bytearray): - x = bytes(x) + x = bytes_encode(x) - if isinstance(x, six.integer_types): - x = UUID(int=x) + if isinstance(x, int): + u = UUID(int=x) elif isinstance(x, tuple): if len(x) == 11: # For compatibility with dce_rpc: this packs into a tuple where @@ -2530,23 +3890,48 @@ def any2i(self, pkt, x): x = (x[0], x[1], x[2], x[3], x[4], node) - x = UUID(fields=x) - elif isinstance(x, (six.binary_type, six.text_type)): + u = UUID(fields=x) + elif isinstance(x, (str, bytes)): if len(x) == 16: # Raw bytes - x = self.m2i(pkt, x) + u = self.m2i(pkt, bytes_encode(x)) else: - x = UUID(plain_str(x)) - return x + u = UUID(plain_str(x)) + elif isinstance(x, (UUID, RandUUID)): + u = cast(UUID, x) + else: + return None + return u @staticmethod def randval(): + # type: () -> RandUUID return RandUUID() -class BitExtendedField(Field): +class UUIDEnumField(UUIDField, _EnumField[UUID]): + __slots__ = EnumField.__slots__ + + def __init__(self, name, default, enum, uuid_fmt=0): + # type: (str, Optional[int], Any, int) -> None + _EnumField.__init__(self, name, default, enum, "16s") # type: ignore + UUIDField.__init__(self, name, default, uuid_fmt=uuid_fmt) + + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> UUID + return _EnumField.any2i(self, pkt, x) # type: ignore + + def i2repr(self, + pkt, # type: Optional[Packet] + x, # type: UUID + ): + # type: (...) -> Any + return _EnumField.i2repr(self, pkt, x) + + +class BitExtendedField(Field[Optional[int], int]): """ - Bit Extended Field + Low E Bit Extended Field This type of field has a variable number of bytes. Each byte is defined as follows: @@ -2562,100 +3947,55 @@ class BitExtendedField(Field): __slots__ = ["extension_bit"] - def prepare_byte(self, x): - # Moves the forwarding bit to the LSB - x = int(x) - fx_bit = (x & 2**self.extension_bit) >> self.extension_bit - lsb_bits = x & 2**self.extension_bit - 1 - msb_bits = x >> (self.extension_bit + 1) - x = (msb_bits << (self.extension_bit + 1)) + (lsb_bits << 1) + fx_bit - return x - - def str2extended(self, x=""): - # For convenience, we reorder the byte so that the forwarding - # bit is always the LSB. We then apply the same algorithm - # whatever the real forwarding bit position - - # First bit is the stopping bit at zero - bits = 0b0 - end = None - - # We retrieve 7 bits. - # If "forwarding bit" is 1 then we continue on another byte - i = 0 - for c in bytearray(x): - c = self.prepare_byte(c) - bits = bits << 7 | (int(c) >> 1) - if not int(c) & 0b1: - end = x[i + 1:] - break - i = i + 1 - if end is None: - # We reached the end of the data but there was no - # "ending bit". This is not normal. - return None, None - else: - return end, bits - - def extended2str(self, x): - x = int(x) - s = [] - LSByte = True - FX_Missing = True - bits = 0b0 - i = 0 - while (x > 0 or FX_Missing): - if i == 8: - # End of byte - i = 0 - s.append(bits) - bits = 0b0 - FX_Missing = True - else: - if i % 8 == self.extension_bit: - # This is extension bit - if LSByte: - bits = bits | 0b0 << i - LSByte = False - else: - bits = bits | 0b1 << i - FX_Missing = False - else: - bits = bits | (x & 0b1) << i - x = x >> 1 - # Still some bits - i = i + 1 - s.append(bits) - - result = "".encode() - for x in s[:: -1]: - result = result + struct.pack(">B", x) - return result - def __init__(self, name, default, extension_bit): + # type: (str, Optional[Any], int) -> None Field.__init__(self, name, default, "B") + assert extension_bit in [7, 0] self.extension_bit = extension_bit - def i2m(self, pkt, x): - return self.extended2str(x) - - def m2i(self, pkt, x): - return self.str2extended(x)[1] - def addfield(self, pkt, s, val): - return s + self.i2m(pkt, val) + # type: (Optional[Packet], bytes, Optional[int]) -> bytes + val = self.i2m(pkt, val) + if not val: + return s + b"\0" + rv = b"" + mask = 1 << self.extension_bit + shift = (self.extension_bit + 1) % 8 + while val: + bv = (val & 0x7F) << shift + val = val >> 7 + if val: + bv |= mask + rv += struct.pack("!B", bv) + return s + rv def getfield(self, pkt, s): - return self.str2extended(s) + # type: (Optional[Any], bytes) -> Tuple[bytes, Optional[int]] + val = 0 + smask = 1 << self.extension_bit + mask = 0xFF & ~ (1 << self.extension_bit) + shift = (self.extension_bit + 1) % 8 + i = 0 + while s: + val |= ((s[0] & mask) >> shift) << (7 * i) + if (s[0] & smask) == 0: # extension bit is 0 + # end + s = s[1:] + break + s = s[1:] + i += 1 + return s, self.m2i(pkt, val) class LSBExtendedField(BitExtendedField): # This is a BitExtendedField with the extension bit on LSB def __init__(self, name, default): + # type: (str, Optional[Any]) -> None BitExtendedField.__init__(self, name, default, extension_bit=0) class MSBExtendedField(BitExtendedField): # This is a BitExtendedField with the extension bit on MSB def __init__(self, name, default): + # type: (str, Optional[Any]) -> None BitExtendedField.__init__(self, name, default, extension_bit=7) diff --git a/scapy/fwdmachine.py b/scapy/fwdmachine.py new file mode 100644 index 00000000000..10c2f6a4e92 --- /dev/null +++ b/scapy/fwdmachine.py @@ -0,0 +1,500 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Forwarding machine. +""" + +import enum +import functools +import os +import select +import socket +import ssl +import threading +import traceback + +from scapy.asn1.asn1 import ASN1_OID +from scapy.config import conf +from scapy.data import MTU +from scapy.packet import Packet +from scapy.supersocket import StreamSocket, StreamSocketPeekless +from scapy.themes import DefaultTheme +from scapy.utils import get_temp_file +from scapy.volatile import RandInt + +from scapy.layers.tls.all import ( + Cert, + PrivKeyECDSA, +) +from scapy.layers.x509 import ( + X509_AlgorithmIdentifier, +) + +from cryptography.hazmat.primitives import serialization + +# Typing imports +from typing import ( + Type, + Optional, +) + + +class ForwardMachine: + """ + Forward Machine + + This binds a port and relay any connections from 'clients' to + their original destination a 'server'. Forwarding machine can be used in + two modes: + + - SERVER: the server binds a port on its local IP and forwards packets to a + ``remote_address``. + - TPROXY: the server binds can intercept packets to any IP destination, provided + that they are routed through the local server, and some tweaking of the OS + routes; + + The TPROXY mode is expected to be used on a router with FORWARDING and only a + specific set of nat rules set to -j TPROXY. A script called 'vethrelay.sh' + is provided in the documentation for setting this up. + + ForwardMachine supports transparently proxifying TLS. By default, it will generate + lookalike self-signed certificates, but it's also possible to specify a certificate + by using crtfile and keyfile. + + Parameters: + + :param port: the port to listen on + :param cls: the scapy class to parse on that port + :param af: the address family to use (default AF_INET) + :param proto: the proto to use (default SOCK_STREAM) + :param remote_address: the IP to use in SERVER mode, or by default in TPROXY when + the destination is the local IP. + :param remote_af: (optional) if provided, use a different address family to connect + to the remote host. + :param bind_address: the IP to bind locally. "0.0.0.0" by default in SERVER mode, + but "2.2.2.2" by default in TPROXY (if you are using the provided + 'vethrelay.sh' script). + :param tls: enable TLS (in both the server and client) + :param crtfile: (optional) if provided, uses a certificate instead of self signed + ones. + :param keyfile: (optional) path to the key file + :param timeout: the timeout before connecting to the real server (default 2) + + Methods to override: + + :func xfrmcs: a function to call when forwarding a packet from the 'client' to + the server. If it raises a FORWARD exception, the packet is forwarded as it. If + it raises a DROP Exception, the packet is discarded. If it raises a + FORWARD_REPLACE(pkt) exception, then pkt is forwarded instead of the original + packet. + :func xfrmsc: same as xfrmcs for packets forwarded from the 'server' to the + 'client'. + """ + + class MODE(enum.Enum): + SERVER = 0 + TPROXY = 1 + + def __init__( + self, + mode: MODE, + port: int, + cls: Type[Packet], + af: socket.AddressFamily = socket.AF_INET, + proto: socket.SocketKind = socket.SOCK_STREAM, + remote_address: str = None, + remote_af: Optional[socket.AddressFamily] = None, + bind_address: str = None, + tls: bool = False, + crtfile: Optional[str] = None, + keyfile: Optional[str] = None, + timeout: int = 2, + MTU: int = MTU, + **kwargs, + ): + self.mode = mode + self.port = port + self.cls = cls + self.af = af + self.remote_af = remote_af if remote_af is not None else af + self.proto = proto + self.tls = tls + self.crtfile = crtfile + self.keyfile = keyfile + self.timeout = timeout + self.MTU = MTU + self.remote_address = remote_address + if self.tls or self.af == 40: # TLS or VSOCK + self.sockcls = StreamSocketPeekless + else: + self.sockcls = StreamSocket + # Chose 'bind_address' depending on the mode + self.bind_address = bind_address + if self.bind_address is None: + if self.mode == ForwardMachine.MODE.SERVER: + self.bind_address = "0.0.0.0" + elif self.mode == ForwardMachine.MODE.TPROXY: + self.bind_address = "2.2.2.2" + else: + raise ValueError("Unknown mode :/") + red = lambda z: functools.reduce(lambda x, y: x + y, z) + # Utils + self.ct = DefaultTheme() + self.local_ips = red(red(list(x.ips.values())) for x in conf.ifaces.values()) + self.cache = {} + super(ForwardMachine, self).__init__(**kwargs) + + def run(self): + """ + Function to start the relay server + """ + self.ssock = socket.socket(self.af, self.proto, 0) + self.ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.mode == ForwardMachine.MODE.TPROXY: + self.ssock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1) # TPROXY ! + self.ssock.bind((self.bind_address, self.port)) + self.ssock.listen(5) + print(self.ct.green("Relay server waiting on port %s" % self.port)) + while True: + conn, addr = self.ssock.accept() + # Calc dest + dest = conn.getsockname() + if self.mode == ForwardMachine.MODE.SERVER or ( + dest[0] in self.local_ips and self.remote_address + ): + dest = (self.remote_address,) + dest[1:] + print(self.ct.green("%s -> %s connected !" % (repr(addr), repr(dest)))) + try: + threading.Thread( + target=self.handler, + args=(conn, addr, dest), + ).start() + except Exception: + print(self.ct.red("%s errored !" % repr(addr))) + conn.close() + pass + + def xfrmcs(self, pkt, ctx): + """ + DEV: overwrite me to handle client->server + """ + raise self.FORWARD() + + def xfrmsc(self, pkt, ctx): + """ + DEV: overwrite me to handle server->client + """ + raise self.FORWARD() + + # Command Exceptions + + class DROP(Exception): + # Drop this packet. + pass + + class FORWARD(Exception): + # Forward this packet. + pass + + class FORWARD_REPLACE(Exception): + # Replace the content and forward. + def __init__(self, data): + self.data = data + + class ANSWER(Exception): + # Answer directly + def __init__(self, data): + self.data = data + + class REDIRECT_TO(Exception): + # Redirect this socket to another destination + def __init__(self, host, port, then=None, server_hostname=None): + self.dest = (host, port) + self.server_hostname = server_hostname + self.then = then or ForwardMachine.FORWARD() + + class CONTEXT: + """ + CONTEXT object kept during a session + """ + + def __init__(self, addr, dest): + self.addr = addr + self.dest = dest + self.tls_sni_name = None # Retrieved when receiving a connection + + def print_reply(self, evt, cs, req, rep): + if evt == self.FORWARD: + if cs: + print("C ==> S: %s" % req.summary()) + else: + print("S ==> C: %s" % req.summary()) + elif evt == self.FORWARD_REPLACE: + if cs: + print("C /=> S: %s -> %s" % (req.summary(), rep.summary())) + else: + print("S /=> C: %s -> %s" % (req.summary(), rep.summary())) + elif evt == self.DROP: + if cs: + print("C => 0: %s" % req.summary()) + else: + print("S => 0: %s" % req.summary()) + elif evt == self.ANSWER: + if cs: + print("C <=| : %s -> %s" % (req.summary(), rep.summary())) + else: + print("S <=| : %s -> %s" % (req.summary(), rep.summary())) + + def destalias(self, dest): + """ + Alias a destination to another destination. + A destination is the tuple (host, port) + """ + return dest + + def _getpeersock(self, dest, server_hostname=None): + """ + Get peer socket + """ + s = socket.socket(self.remote_af, self.proto) + s.settimeout(self.timeout) + ndest = self.destalias(dest) + if ndest != dest: + print("C: %s redirected to %s" % (repr(dest), repr(ndest))) + dest = ndest + s.connect(dest) + return s + + def gen_alike_chain(self, certs, privkey): + """ + Modify a real certificate chain to be served by our own privatekey + """ + c, certs = certs[0], certs[1:] + if certs: + # Recursive: if there are certificates above this one in the chain, do them + # first. + certs = self.gen_alike_chain(certs, privkey) + else: + # Last certificate of the chain. Make it self-signed + c.tbsCertificate.issuer = c.tbsCertificate.subject + # Set SubjectPublicKeyInfo to the one from our private key + c.setSubjectPublicKeyFromPrivateKey(privkey) + # Filter out extensions that would cause trouble + c.tbsCertificate.serialNumber.val = int( + RandInt() + ) # otherwise SEC_ERROR_REUSED_ISSUER_AND_SERIAL + c.tbsCertificate.extensions = [ + x + for x in c.tbsCertificate.extensions + if x.extnID + not in [ + "2.5.29.32", # CPS + "2.5.29.31", # cRLDistributionPoints + "1.3.6.1.5.5.7.1.1", # authorityInfoAccess + "1.3.6.1.4.1.11129.2.4.2", # SCT + "2.5.29.14", # subjectKeyIdentifier + "2.5.29.35", # authorityKeyIdentifier + ] + ] + # For now, we only provide a RSA private key, so we can only sign with that :/ + c.tbsCertificate.signature = X509_AlgorithmIdentifier( + algorithm=ASN1_OID("ecdsa-with-SHA384"), + ) + # Resign. + c = Cert(privkey.resignCert(c)) + # Return + return [c] + certs + + def get_key_and_alike_chain(self, cas, dest, server_name): + """ + Generate a PrivateKey and a clone of the 'cas' certificate chain signed with it, + if not already cached. + + The cache uses server_name or dest as key. + """ + ident = server_name or dest + if ident in self.cache: + return self.cache[ident] + # Parse CAs + certs = [Cert(c.public_bytes()) for c in cas] + # certs = certs[:1] + # Generate Private Key + privkey = PrivKeyECDSA() + # Iterate + certs = self.gen_alike_chain(certs, privkey) + # Build a chain object. This checks that everything is properly signed, and + # re-order the certs. + # chain = Chain(certs, cert0=certs[-1]) + self.cache[ident] = privkey, certs + return privkey, certs + + def handler(self, sock, addr, dest): + """ + Handler of a client socket + """ + ctx = self.CONTEXT(addr, dest) # we have a context object + # Initialize peer socket + ss = self._getpeersock(dest) + # Wrap both server and peer sockets in SSL + if self.tls: + # Build client SSL context + clisslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + clisslcontext.load_default_certs() + clisslcontext.check_hostname = False + clisslcontext.verify_mode = ssl.CERT_NONE + + # This acts as follows: + # - start the server-side TLS handshake + # - use the SNI callback to pop a client-side socket (using the real + # provided SNI) + # - serve the certificate + + _clisock = [ss] + + def cb_sni(sock, server_name, _): + """ + This callback occurs after the TLSClientHello is received by the server + """ + ss = _clisock[0] + ctx.tls_sni_name = server_name # the requested SNI + # Use that SNI to wrap the client socket + ss = clisslcontext.wrap_socket(ss, server_hostname=server_name) + # Get certificate chain + cas = ss._sslobj.get_unverified_chain() + if self.crtfile is None: + # SELF-SIGNED mode + # Generate private key based on the type of certificate + privkey, certs = self.get_key_and_alike_chain( + cas, dest, server_name + ) + # Load result certificate our SSL server + # (this is dumb but we need to store them on disk) + certfile = get_temp_file() + with open(certfile, "w") as fd: + for c in certs: + fd.write(c.pem) + keyfile = get_temp_file() + with open(keyfile, "wb") as fd: + password = os.urandom(32) + fd.write( + privkey.key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption( # noqa: E501 + password + ), + ) + ) + else: + # Certificate is provided + certfile = self.crtfile + keyfile = self.keyfile + sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + sslcontext.check_hostname = False + sslcontext.verify_mode = ssl.CERT_NONE # note: server side + sslcontext.load_cert_chain(certfile, keyfile, password=password) + sock.context = sslcontext + # Return success + _clisock[0] = ss + return None # Continue + + # Server SSL context + sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + sslcontext.sni_callback = cb_sni + try: + sock = sslcontext.wrap_socket(sock, server_side=True) + except Exception as ex: + print(self.ct.red("%s errored in SSL: %s" % (repr(addr), str(ex)))) + sock.close() + return + ss = _clisock[0] + # Wrap the sockets + sock = self.sockcls(sock, self.cls) + ss = self.sockcls(ss, self.cls) + sock.streamsession = ss.streamsession + try: + while True: + # Listen on both ends of the connection + for thissock in select.select([ss, sock], [], [], 0)[0]: + if thissock is ss: + cs = 0 + func = self.xfrmsc + othersock = sock + else: + cs = 1 + func = self.xfrmcs + othersock = ss + # get data + try: + data = thissock.recv(self.MTU) + except EOFError: + raise RuntimeError + if not data: + # Session needs more data + continue + try: + # And pipe everything into the processdata + try: + func(data, ctx) + # If this doesn't raise, it's a user error. + print( + self.ct.red( + "%s ERROR: you must always raise in %s !" % func + ) + ) + return + except self.REDIRECT_TO as ex: + # Replace the peer socket with a new socket + oldss = ss + ss = self._getpeersock( + ex.dest, server_hostname=ex.server_hostname + ) + ss = self.sockcls(ss, self.cls) + print( + "C: %s redirected to %s" + % (repr(ctx.dest), repr(ex.dest)) + ) + ctx.dest = ex.dest # update context + # Shut the old one. + oldss.ins.shutdown(socket.SHUT_RDWR) + oldss.close() + # Replace othersock/thissock + if oldss is thissock: + thissock = ss + else: + othersock = ss + # Raise what's next. + raise ex.then + except self.FORWARD: + # Forward the data to the other host + othersock.send(data) + self.print_reply(self.FORWARD, cs, data, None) + except self.FORWARD_REPLACE as ex: + # Forward custom data to the other host + othersock.send(ex.data) + self.print_reply(self.FORWARD_REPLACE, cs, data, ex.data) + except self.DROP: + # Drop + self.print_reply(self.DROP, cs, data, None) + except self.ANSWER as ex: + # Respond with custom data + thissock.send(ex.data) + self.print_reply(self.ANSWER, cs, data, ex.data) + except Exception as ex: + # Processing failed. forward to not break anything + print( + self.ct.orange( + "Exception happened in handling client %s ! (forward)" + % repr(addr) + ) + ) + traceback.print_exception(ex) + othersock.send(data) + self.print_reply(self.FORWARD, cs, data, None) + except RuntimeError: + print(self.ct.red("%s DISCONNECTED !" % repr(addr))) + sock.close() + ss.close() diff --git a/scapy/interfaces.py b/scapy/interfaces.py new file mode 100644 index 00000000000..70846b91be5 --- /dev/null +++ b/scapy/interfaces.py @@ -0,0 +1,451 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Interfaces management +""" + +import itertools +import uuid +from collections import defaultdict + +from scapy.config import conf +from scapy.consts import WINDOWS, LINUX +from scapy.utils import pretty_list +from scapy.utils6 import in6_isvalid + +# Typing imports +import scapy +from scapy.compat import UserDict +from typing import ( + cast, + Any, + DefaultDict, + Dict, + List, + NoReturn, + Optional, + Tuple, + Type, + Union, +) + + +class InterfaceProvider(object): + name = "Unknown" + headers: Tuple[str, ...] = ("Index", "Name", "MAC", "IPv4", "IPv6") + header_sort = 1 + libpcap = False + + def load(self): + # type: () -> Dict[str, NetworkInterface] + """Returns a dictionary of the loaded interfaces, by their + name.""" + raise NotImplementedError + + def reload(self): + # type: () -> Dict[str, NetworkInterface] + """Same than load() but for reloads. By default calls load""" + return self.load() + + def _l2socket(self, dev): + # type: (NetworkInterface) -> Type[scapy.supersocket.SuperSocket] + """Return L2 socket used by interfaces of this provider""" + return conf.L2socket + + def _l2listen(self, dev): + # type: (NetworkInterface) -> Type[scapy.supersocket.SuperSocket] + """Return L2listen socket used by interfaces of this provider""" + return conf.L2listen + + def _l3socket(self, dev, ipv6): + # type: (NetworkInterface, bool) -> Type[scapy.supersocket.SuperSocket] + """Return L3 socket used by interfaces of this provider""" + if LINUX and not self.libpcap and dev.name == conf.loopback_name: + # handle the loopback case. see troubleshooting.rst + if ipv6: + from scapy.supersocket import L3RawSocket6 + return cast(Type['scapy.supersocket.SuperSocket'], L3RawSocket6) + else: + from scapy.supersocket import L3RawSocket + return L3RawSocket + return conf.L3socket + + def _is_valid(self, dev): + # type: (NetworkInterface) -> bool + """Returns whether an interface is valid or not""" + return bool((dev.ips[4] or dev.ips[6]) and dev.mac) + + def _format(self, + dev, # type: NetworkInterface + **kwargs # type: Any + ): + # type: (...) -> Tuple[Union[str, List[str]], ...] + """Returns the elements used by show() + + If a tuple is returned, this consist of the strings that will be + inlined along with the interface. + If a list of tuples is returned, they will be appended one above the + other and should all be part of a single interface. + """ + mac = dev.mac + resolve_mac = kwargs.get("resolve_mac", True) + if resolve_mac and conf.manufdb and mac: + mac = conf.manufdb._resolve_MAC(mac) + index = str(dev.index) + return (index, dev.description, mac or "", dev.ips[4], dev.ips[6]) + + def __repr__(self) -> str: + """ + repr + """ + return "" % self.name + + +class NetworkInterface(object): + def __init__(self, + provider, # type: InterfaceProvider + data=None, # type: Optional[Dict[str, Any]] + ): + # type: (...) -> None + self.provider = provider + self.name = "" + self.description = "" + self.network_name = "" + self.index = -1 + self.ip = None # type: Optional[str] + self.ips = defaultdict(list) # type: DefaultDict[int, List[str]] + self.type = -1 + self.mac = None # type: Optional[str] + self.dummy = False + if data is not None: + self.update(data) + + def update(self, data): + # type: (Dict[str, Any]) -> None + """Update info about a network interface according + to a given dictionary. Such data is provided by providers + """ + self.name = data.get('name', "") + self.description = data.get('description', "") + self.network_name = data.get('network_name', "") + self.index = data.get('index', 0) + self.ip = data.get('ip', "") + self.type = data.get('type', -1) + self.mac = data.get('mac', "") + self.flags = data.get('flags', 0) + self.dummy = data.get('dummy', False) + + for ip in data.get('ips', []): + if in6_isvalid(ip): + self.ips[6].append(ip) + else: + self.ips[4].append(ip) + + # An interface often has multiple IPv6 so we don't store + # a "main" one, unlike IPv4. + if self.ips[4] and not self.ip: + self.ip = self.ips[4][0] + + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, str): + return other in [self.name, self.network_name, self.description] + if isinstance(other, NetworkInterface): + return self.__dict__ == other.__dict__ + return False + + def __ne__(self, other): + # type: (Any) -> bool + return not self.__eq__(other) + + def __hash__(self): + # type: () -> int + return hash(self.network_name) + + def is_valid(self): + # type: () -> bool + if self.dummy: + return False + return self.provider._is_valid(self) + + def l2socket(self): + # type: () -> Type[scapy.supersocket.SuperSocket] + return self.provider._l2socket(self) + + def l2listen(self): + # type: () -> Type[scapy.supersocket.SuperSocket] + return self.provider._l2listen(self) + + def l3socket(self, ipv6=False): + # type: (bool) -> Type[scapy.supersocket.SuperSocket] + return self.provider._l3socket(self, ipv6) + + def __repr__(self): + # type: () -> str + return "<%s %s [%s]>" % (self.__class__.__name__, + self.description, + self.dummy and "dummy" or (self.flags or "")) + + def __str__(self): + # type: () -> str + return self.network_name + + def __add__(self, other): + # type: (str) -> str + return self.network_name + other + + def __radd__(self, other): + # type: (str) -> str + return other + self.network_name + + +_GlobInterfaceType = Union[NetworkInterface, str] + + +class NetworkInterfaceDict(UserDict[str, NetworkInterface]): + """Store information about network interfaces and convert between names""" + + def __init__(self): + # type: () -> None + self.providers = {} # type: Dict[Type[InterfaceProvider], InterfaceProvider] # noqa: E501 + super(NetworkInterfaceDict, self).__init__() + + def _load(self, + dat, # type: Dict[str, NetworkInterface] + prov, # type: InterfaceProvider + ): + # type: (...) -> None + for ifname, iface in dat.items(): + if ifname in self.data: + # Handle priorities: keep except if libpcap + if prov.libpcap: + self.data[ifname] = iface + else: + self.data[ifname] = iface + + def register_provider(self, provider): + # type: (type) -> None + prov = provider() + self.providers[provider] = prov + if self.data: + # late registration + self._load(prov.reload(), prov) + + def load_confiface(self): + # type: () -> None + """ + Reload conf.iface + """ + # Can only be called after conf.route is populated + if not conf.route: + raise ValueError("Error: conf.route isn't populated !") + conf.iface = get_working_if() # type: ignore + + def _reload_provs(self): + # type: () -> None + self.clear() + for prov in self.providers.values(): + self._load(prov.reload(), prov) + + def reload(self): + # type: () -> None + self._reload_provs() + if not conf.route: + # routes are not loaded yet. + return + self.load_confiface() + + def dev_from_name(self, name): + # type: (str) -> NetworkInterface + """Return the first network device name for a given + device name. + """ + try: + return next(iface for iface in self.values() + if (iface.name == name or iface.description == name)) + except (StopIteration, RuntimeError): + raise ValueError("Unknown network interface %r" % name) + + def dev_from_networkname(self, network_name): + # type: (str) -> NoReturn + """Return interface for a given network device name.""" + try: + return next(iface for iface in self.values() # type: ignore + if iface.network_name == network_name) + except (StopIteration, RuntimeError): + raise ValueError( + "Unknown network interface %r" % + network_name) + + def dev_from_index(self, if_index): + # type: (int) -> NetworkInterface + """Return interface name from interface index""" + try: + if_index = int(if_index) # Backward compatibility + return next(iface for iface in self.values() + if iface.index == if_index) + except (StopIteration, RuntimeError): + if str(if_index) == "1": + # Test if the loopback interface is set up + return self.dev_from_networkname(conf.loopback_name) + raise ValueError("Unknown network interface index %r" % if_index) + + def _add_fake_iface(self, + ifname, + mac="00:00:00:00:00:00", + ips=["127.0.0.1", "::"]): + # type: (str, str, List[str]) -> None + """Internal function used for a testing purpose""" + data = { + 'name': ifname, + 'description': ifname, + 'network_name': ifname, + 'index': -1000, + 'dummy': True, + 'mac': mac, + 'flags': 0, + 'ips': ips, + # Windows only + 'guid': "{%s}" % uuid.uuid1(), + 'ipv4_metric': 0, + 'ipv6_metric': 0, + 'nameservers': [], + } + if WINDOWS: + from scapy.arch.windows import NetworkInterface_Win, \ + WindowsInterfacesProvider + + class FakeProv(WindowsInterfacesProvider): + name = "fake" + + self.data[ifname] = NetworkInterface_Win( + FakeProv(), + data + ) + else: + self.data[ifname] = NetworkInterface(InterfaceProvider(), data) + + def show(self, print_result=True, hidden=False, **kwargs): + # type: (bool, bool, **Any) -> Optional[str] + """ + Print list of available network interfaces in human readable form + + :param print_result: print the results if True, else return it + :param hidden: if True, also displays invalid interfaces + """ + res = defaultdict(list) + for iface_name in sorted(self.data): + dev = self.data[iface_name] + if not hidden and not dev.is_valid(): + continue + prov = dev.provider + res[(prov.headers, prov.header_sort)].append( + (prov.name,) + prov._format(dev, **kwargs) + ) + output = "" + for key in res: + hdrs, sortBy = key + output += pretty_list( + res[key], + [("Source",) + hdrs], + sortBy=sortBy + ) + "\n" + output = output[:-1] + if print_result: + print(output) + return None + else: + return output + + def __repr__(self): + # type: () -> str + return self.show(print_result=False) # type: ignore + + +conf.ifaces = IFACES = ifaces = NetworkInterfaceDict() + + +def get_if_list(): + # type: () -> List[str] + """Return a list of interface names""" + return list(conf.ifaces.keys()) + + +def get_working_if(): + # type: () -> Optional[NetworkInterface] + """Return an interface that works""" + # return the interface associated with the route with smallest + # mask (route by default if it exists) + routes = conf.route.routes[:] + routes.sort(key=lambda x: x[1]) + ifaces = (x[3] for x in routes) + # First check the routing ifaces from best to worse, + # then check all the available ifaces as backup. + for ifname in itertools.chain(ifaces, conf.ifaces.values()): + try: + iface = conf.ifaces.dev_from_networkname(ifname) # type: ignore + if iface.is_valid(): + return iface + except ValueError: + pass + # There is no hope left + try: + return conf.ifaces.dev_from_networkname(conf.loopback_name) + except ValueError: + return None + + +def get_working_ifaces(): + # type: () -> List[NetworkInterface] + """Return all interfaces that work""" + return [iface for iface in conf.ifaces.values() if iface.is_valid()] + + +def dev_from_networkname(network_name): + # type: (str) -> NetworkInterface + """Return Scapy device name for given network device name""" + return conf.ifaces.dev_from_networkname(network_name) + + +def dev_from_index(if_index): + # type: (int) -> NetworkInterface + """Return interface for a given interface index""" + return conf.ifaces.dev_from_index(if_index) + + +def resolve_iface(dev, retry=True): + # type: (_GlobInterfaceType, bool) -> NetworkInterface + """ + Resolve an interface name into the interface + """ + if isinstance(dev, NetworkInterface): + return dev + try: + return conf.ifaces.dev_from_name(dev) + except ValueError: + try: + return conf.ifaces.dev_from_networkname(dev) + except ValueError: + pass + if not retry: + raise ValueError("Interface '%s' not found !" % dev) + # Nothing found yet. Reload to detect if it was added recently + conf.ifaces.reload() + return resolve_iface(dev, retry=False) + + +def network_name(dev): + # type: (_GlobInterfaceType) -> str + """ + Resolves the device network name of a device or Scapy NetworkInterface + """ + return resolve_iface(dev).network_name + + +def show_interfaces(resolve_mac=True): + # type: (bool) -> None + """Print list of available network interfaces""" + return conf.ifaces.show(resolve_mac) # type: ignore diff --git a/scapy/layers/__init__.py b/scapy/layers/__init__.py index 326b77a23e3..74f9906a2f4 100644 --- a/scapy/layers/__init__.py +++ b/scapy/layers/__init__.py @@ -1,8 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Layer package. """ + +# Make sure config is loaded +import scapy.config # noqa: F401 diff --git a/scapy/layers/all.py b/scapy/layers/all.py index f3679c2bb4a..58b9a8bf679 100644 --- a/scapy/layers/all.py +++ b/scapy/layers/all.py @@ -1,26 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ All layers. Configurable with conf.load_layers. """ -from __future__ import absolute_import -from scapy.config import conf + +import builtins +import logging + +# We import conf from arch to make sure arch specific layers are populated +from scapy.arch import conf from scapy.error import log_loading from scapy.main import load_layer -import logging -import scapy.modules.six as six -ignored = list(six.moves.builtins.__dict__) + ["sys"] +ignored = list(builtins.__dict__) + ["sys"] log = logging.getLogger("scapy.loading") __all__ = [] for _l in conf.load_layers: - log_loading.debug("Loading layer %s" % _l) + log_loading.debug("Loading layer %s", _l) try: load_layer(_l, globals_dict=globals(), symb_list=__all__) except Exception as e: diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index aea58a9157b..2784a515e9f 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -1,9 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi # Copyright (C) Mike Ryan # Copyright (C) Michael Farrell -# This program is published under a GPLv2 license +# Copyright (C) Haram Park """ Bluetooth layers, sockets and send/receive functions. @@ -17,57 +18,67 @@ from ctypes import sizeof from scapy.config import conf -from scapy.data import DLT_BLUETOOTH_HCI_H4, DLT_BLUETOOTH_HCI_H4_WITH_PHDR +from scapy.data import ( + DLT_BLUETOOTH_HCI_H4, + DLT_BLUETOOTH_HCI_H4_WITH_PHDR, + DLT_BLUETOOTH_LINUX_MONITOR, + BLUETOOTH_CORE_COMPANY_IDENTIFIERS +) from scapy.packet import bind_layers, Packet -from scapy.fields import ByteEnumField, ByteField, Field, FieldLenField, \ - FieldListField, FlagsField, IntField, LEShortEnumField, LEShortField, \ - LenField, PacketListField, SignedByteField, StrField, StrFixedLenField, \ - StrLenField, XByteField, BitField, XLELongField, PadField, UUIDField, \ - XStrLenField, ConditionalField +from scapy.fields import ( + BitField, + XBitField, + ByteEnumField, + ByteField, + FieldLenField, + FieldListField, + FlagsField, + IntField, + LEShortEnumField, + LEShortField, + LEIntField, + LenField, + MultipleTypeField, + NBytesField, + PacketListField, + PadField, + ShortField, + SignedByteField, + StrField, + StrFixedLenField, + StrLenField, + StrNullField, + UUIDField, + XByteField, + XLE3BytesField, + XLELongField, + XStrLenField, + XLEShortField, + XLEIntField, + LEMACField, + BitEnumField, + LEThreeBytesField, + ConditionalField +) from scapy.supersocket import SuperSocket from scapy.sendrecv import sndrcv from scapy.data import MTU from scapy.consts import WINDOWS from scapy.error import warning -from scapy.utils import lhex, mac2str, str2mac -from scapy.volatile import RandMAC -from scapy.modules import six -########## -# Fields # -########## - -class XLEShortField(LEShortField): - def i2repr(self, pkt, x): - return lhex(self.i2h(pkt, x)) - - -class LEMACField(Field): - def __init__(self, name, default): - Field.__init__(self, name, default, "6s") - - def i2m(self, pkt, x): - if x is None: - return b"\0\0\0\0\0\0" - return mac2str(x)[::-1] - - def m2i(self, pkt, x): - return str2mac(x[::-1]) - - def any2i(self, pkt, x): - if isinstance(x, (six.binary_type, six.text_type)) and len(x) == 6: - x = self.m2i(pkt, x) - return x +############ +# Consts # +############ - def i2repr(self, pkt, x): - x = self.i2h(pkt, x) - if self in conf.resolve: - x = conf.manufdb._resolve_MAC(x) - return x +# From hci.h +HCI_CHANNEL_RAW = 0 +HCI_CHANNEL_USER = 1 +HCI_CHANNEL_MONITOR = 2 +HCI_CHANNEL_CONTROL = 3 +HCI_CHANNEL_LOGGING = 4 - def randval(self): - return RandMAC() +HCI_DEV_NONE = 0xffff ########## @@ -178,7 +189,7 @@ class HCI_PHDR_Hdr(Packet): 0x05: "insufficient auth", 0x06: "unsupported req", 0x07: "invalid offset", - 0x08: "insuficient author", + 0x08: "insufficient author", 0x09: "prepare queue full", 0x0a: "attr not found", 0x0b: "attr not long", @@ -190,6 +201,91 @@ class HCI_PHDR_Hdr(Packet): 0x11: "insufficient resources", } +_bluetooth_features = [ + '3_slot_packets', + '5_slot_packets', + 'encryption', + 'slot_offset', + 'timing_accuracy', + 'role_switch', + 'hold_mode', + 'sniff_mode', + 'park_mode', + 'power_control_requests', + 'channel_quality_driven_data_rate', + 'sco_link', + 'hv2_packets', + 'hv3_packets', + 'u_law_log_synchronous_data', + 'a_law_log_synchronous_data', + 'cvsd_synchronous_data', + 'paging_parameter_negotiation', + 'power_control', + 'transparent_synchronous_data', + 'flow_control_lag_4_bit0', + 'flow_control_lag_4_bit1', + 'flow_control_lag_4_bit2', + 'broadband_encryption', + 'cvsd_synchronous_data', + 'edr_acl_2_mbps_mode', + 'edr_acl_3_mbps_mode', + 'enhanced_inquiry_scan', + 'interlaced_inquiry_scan', + 'interlaced_page_scan', + 'rssi_with_inquiry_results', + 'ev3_packets', + 'ev4_packets', + 'ev5_packets', + 'reserved', + 'afh_capable_slave', + 'afh_classification_slave', + 'br_edr_not_supported', + 'le_supported_controller', + '3_slot_edr_acl_packets', + '5_slot_edr_acl_packets', + 'sniff_subrating', + 'pause_encryption', + 'afh_capable_master', + 'afh_classification_master', + 'edr_esco_2_mbps_mode', + 'edr_esco_3_mbps_mode', + '3_slot_edr_esco_packets', + 'extended_inquiry_response', + 'simultaneous_le_and_br_edr_to_same_device_capable_controller', + 'reserved2', + 'secure_simple_pairing', + 'encapsulated_pdu', + 'erroneous_data_reporting', + 'non_flushable_packet_boundary_flag', + 'reserved3', + 'link_supervision_timeout_changed_event', + 'inquiry_tx_power_level', + 'enhanced_power_control', + 'reserved4_bit0', + 'reserved4_bit1', + 'reserved4_bit2', + 'reserved4_bit3', + 'extended_features', +] + +_bluetooth_core_specification_versions = { + 0x00: '1.0b', + 0x01: '1.1', + 0x02: '1.2', + 0x03: '2.0+EDR', + 0x04: '2.1+EDR', + 0x05: '3.0+HS', + 0x06: '4.0', + 0x07: '4.1', + 0x08: '4.2', + 0x09: '5.0', + 0x0a: '5.1', + 0x0b: '5.2', + 0x0c: '5.3', + 0x0d: '5.4', + 0x0e: '6.0', +} + class HCI_Hdr(Packet): name = "HCI header" @@ -201,27 +297,16 @@ def mysummary(self): class HCI_ACL_Hdr(Packet): name = "HCI ACL header" - # NOTE: the 2-bytes entity formed by the 2 flags + handle must be LE - # This means that we must reverse those two bytes manually (we don't have - # a field that can reverse a group of fields) - fields_desc = [BitField("BC", 0, 2), # ] - BitField("PB", 0, 2), # ]=> 2 bytes - BitField("handle", 0, 12), # ] + fields_desc = [BitField("BC", 0, 2, tot_size=-2), + BitField("PB", 0, 2), + BitField("handle", 0, 12, end_tot_size=-2), LEShortField("len", None), ] - def pre_dissect(self, s): - return s[:2][::-1] + s[2:] # Reverse the 2 first bytes - - def post_dissect(self, s): - self.raw_packet_cache = None # Reset packet to allow post_build - return s - def post_build(self, p, pay): p += pay if self.len is None: p = p[:2] + struct.pack(" None if WINDOWS: warning("Not available on Windows") return - # s = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW, socket.BTPROTO_HCI) # noqa: E501 - # s.bind((0,1)) - - # yeah, if only - # thanks to Python's weak ass socket and bind implementations, we have - # to call down into libc with ctypes - + # Python socket and bind implementations do not allow us to pass down + # the correct parameters. We must call libc functions directly via + # ctypes. sockaddr_hcip = ctypes.POINTER(sockaddr_hci) - ctypes.cdll.LoadLibrary("libc.so.6") - libc = ctypes.CDLL("libc.so.6") + from ctypes.util import find_library + libc = ctypes.cdll.LoadLibrary(find_library("c")) socket_c = libc.socket socket_c.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.c_int) @@ -1503,39 +3358,24 @@ def __init__(self, adapter_index=0): ctypes.c_int) bind.restype = ctypes.c_int - ######## - # actual code - - s = socket_c(31, 3, 1) # (AF_BLUETOOTH, SOCK_RAW, HCI_CHANNEL_USER) + # Socket + s = socket_c(socket_domain, socket_type, socket_protocol) if s < 0: - raise BluetoothSocketError("Unable to open PF_BLUETOOTH socket") - - sa = sockaddr_hci() - sa.sin_family = 31 # AF_BLUETOOTH - sa.hci_dev = adapter_index # adapter index - sa.hci_channel = 1 # HCI_USER_CHANNEL + raise BluetoothSocketError( + f"Unable to open socket({socket_domain}, {socket_type}, " + f"{socket_protocol})") - r = bind(s, sockaddr_hcip(sa), sizeof(sa)) + # Bind + r = bind(s, sockaddr_hcip(sock_address), sizeof(sock_address)) if r != 0: raise BluetoothSocketError("Unable to bind") - self.ins = self.outs = socket.fromfd(s, 31, 3, 1) - - def send_command(self, cmd): - opcode = cmd.opcode - self.send(cmd) - while True: - r = self.recv() - if r.type == 0x04 and r.code == 0xe and r.opcode == opcode: - if r.status != 0: - raise BluetoothCommandError("Command %x failed with %x" % (opcode, r.status)) # noqa: E501 - return r - - def recv(self, x=MTU): - return HCI_Hdr(self.ins.recv(x)) + self.hci_fd = s + self.ins = self.outs = socket.fromfd( + s, socket_domain, socket_type, socket_protocol) def readable(self, timeout=0): - (ins, outs, foo) = select.select([self.ins], [], [], timeout) + (ins, _, _) = select.select([self.ins], [], [], timeout) return len(ins) > 0 def flush(self): @@ -1547,8 +3387,8 @@ def close(self): return # Properly close socket so we can free the device - ctypes.cdll.LoadLibrary("libc.so.6") - libc = ctypes.CDLL("libc.so.6") + from ctypes.util import find_library + libc = ctypes.cdll.LoadLibrary(find_library("c")) close = libc.close close.restype = ctypes.c_int @@ -1560,6 +3400,54 @@ def close(self): if hasattr(self, "ins"): if self.ins and (WINDOWS or self.ins.fileno() != -1): close(self.ins.fileno()) + if hasattr(self, "hci_fd"): + close(self.hci_fd) + + +class BluetoothUserSocket(_BluetoothLibcSocket): + desc = "read/write H4 over a Bluetooth user channel" + + def __init__(self, adapter_index=0): + sa = sockaddr_hci() + sa.sin_family = socket.AF_BLUETOOTH + sa.hci_dev = adapter_index + sa.hci_channel = HCI_CHANNEL_USER + super().__init__( + socket_domain=socket.AF_BLUETOOTH, + socket_type=socket.SOCK_RAW, + socket_protocol=socket.BTPROTO_HCI, + sock_address=sa) + + def send_command(self, cmd): + opcode = cmd[HCI_Command_Hdr].opcode + self.send(cmd) + while True: + r = self.recv() + if r.type == 0x04 and r.code in (0xe, 0xf) and r.opcode == opcode: + if hasattr(r, 'status') and r.status != 0: + raise BluetoothCommandError("Command %x failed with %x" % (opcode, r.status)) # noqa: E501 + return r + + def recv(self, x=MTU): + return HCI_Hdr(self.ins.recv(x)) + + +class BluetoothMonitorSocket(_BluetoothLibcSocket): + desc = "Read/write over a Bluetooth monitor channel" + + def __init__(self): + sa = sockaddr_hci() + sa.sin_family = socket.AF_BLUETOOTH + sa.hci_dev = HCI_DEV_NONE + sa.hci_channel = HCI_CHANNEL_MONITOR + super().__init__( + socket_domain=socket.AF_BLUETOOTH, + socket_type=socket.SOCK_RAW, + socket_protocol=socket.BTPROTO_HCI, + sock_address=sa) + + def recv(self, x=MTU): + return HCI_Mon_Hdr(self.ins.recv(x)) conf.BTsocket = BluetoothRFCommSocket diff --git a/scapy/layers/bluetooth4LE.py b/scapy/layers/bluetooth4LE.py index 82cee336037..49a5798a48a 100644 --- a/scapy/layers/bluetooth4LE.py +++ b/scapy/layers/bluetooth4LE.py @@ -1,8 +1,9 @@ -# This file is for use with Scapy -# See http://www.secdev.org/projects/scapy for more information +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi # Copyright (C) Airbus DS CyberSecurity # Authors: Jean-Michel Picod, Arnaud Lebrun, Jonathan Christofer Demay -# This program is published under a GPLv2 license """Bluetooth 4LE layer""" @@ -10,18 +11,38 @@ from scapy.compat import orb, chb from scapy.config import conf -from scapy.data import DLT_BLUETOOTH_LE_LL, DLT_BLUETOOTH_LE_LL_WITH_PHDR, \ - PPI_BTLE +from scapy.data import ( + DLT_BLUETOOTH_LE_LL, + DLT_BLUETOOTH_LE_LL_WITH_PHDR, + PPI_BTLE, +) from scapy.packet import Packet, bind_layers -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - Field, FlagsField, LEIntField, LEShortEnumField, LEShortField, \ - MACField, PacketListField, SignedByteField, X3BytesField, XBitField, \ - XByteField, XIntField, XShortField, XLEIntField, XLEShortField +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + Field, + FlagsField, + LEIntField, + LEShortEnumField, + LEShortField, + MACField, + PacketListField, + SignedByteField, + X3BytesField, + XByteField, + XIntField, + XLEIntField, + XLELongField, + XLEShortField, + XShortField, +) +from scapy.contrib.ethercat import LEBitEnumField, LEBitField from scapy.layers.bluetooth import EIR_Hdr, L2CAP_Hdr from scapy.layers.ppi import PPI_Element, PPI_Hdr -from scapy.modules.six.moves import range from scapy.utils import mac2str, str2mac #################### @@ -53,22 +74,47 @@ class BTLE_PPI(PPI_Element): class BTLE_RF(Packet): """Cooked BTLE link-layer pseudoheader. - http://www.whiterocker.com/bt/LINKTYPE_BLUETOOTH_LE_LL_WITH_PHDR.html + https://www.tcpdump.org/linktypes/LINKTYPE_BLUETOOTH_LE_LL_WITH_PHDR.html """ name = "BTLE RF info header" + + _TYPES = { + 0: "ADV_OR_DATA_UNKNOWN_DIR", + 1: "AUX_ADV", + 2: "DATA_M_TO_S", + 3: "DATA_S_TO_M", + 4: "CONN_ISO_M_TO_S", + 5: "CONN_ISO_S_TO_M", + 6: "BROADCAST_ISO", + 7: "RFU", + } + + _PHY = { + 0: "1M", + 1: "2M", + 2: "Coded", + 3: "RFU", + } + fields_desc = [ ByteField("rf_channel", 0), SignedByteField("signal", -128), SignedByteField("noise", -128), ByteField("access_address_offenses", 0), XLEIntField("reference_access_address", 0), - FlagsField("flags", 0, -16, [ - "dewhitened", "sig_power_valid", "noise_power_valid", - "decrypted", "reference_access_address_valid", - "access_address_offenses_valid", "channel_aliased", - "res1", "res2", "res3", "crc_checked", "crc_valid", - "mic_checked", "mic_valid", "res4", "res5" - ]) + LEBitField("dewhitened", 0, 1), + LEBitField("sig_power_valid", 0, 1), + LEBitField("noise_power_valid", 0, 1), + LEBitField("decrypted", 0, 1), + LEBitField("reference_access_address_valid", 0, 1), + LEBitField("access_address_offenses_valid", 0, 1), + LEBitField("channel_aliased", 0, 1), + LEBitEnumField("type", 0, 3, _TYPES), + LEBitField("crc_checked", 0, 1), + LEBitField("crc_valid", 0, 1), + LEBitField("mic_checked", 0, 1), + LEBitField("mic_valid", 0, 1), + LEBitEnumField("phy", 0, 2, _PHY), ] @@ -102,6 +148,61 @@ def getfield(self, pkt, s): return s[5:], self.m2i(pkt, struct.unpack(self.fmt, s[:5] + b"\x00\x00\x00")[0]) # noqa: E501 +class BTLEFeatureField(FlagsField): + def __init__(self, name, default): + super(BTLEFeatureField, self).__init__( + name, default, -64, + ['le_encryption', + 'conn_par_req_proc', + 'ext_reject_ind', + 'slave_init_feat_exch', + 'le_ping', + 'le_data_len_ext', + 'll_privacy', + 'ext_scan_filter', + 'le_2m_phy', + 'tx_mod_idx', + 'rx_mod_idx', + 'le_coded_phy', + 'le_ext_adv', + 'le_periodic_adv', + 'ch_sel_alg', + 'le_pwr_class' + 'min_used_channels', + 'conn_cte_req', + 'conn_cte_rsp', + 'connless_cte_tx', + 'connless_cte_rx', + 'antenna_switching_cte_aod_tx', + 'antenna_switching_cte_aoa_rx', + 'cte_rx', + 'periodic_adv_sync_transfer_tx', + 'periodic_adv_sync_transfer_rx', + 'sleep_clock_accuracy_updates', + 'remote_public_key_validation', + 'cis_central', + 'cis_peripheral', + 'iso_broadcaster', + 'synchronized_receiver', + 'connected_iso_host_support', + 'le_power_control_request', + 'le_power_control_request', + 'le_path_loss_monitoring', + 'periodic_adv_adi_support', + 'connection_subrating', + 'connection_subrating_host_support', + 'channel_classification'] + ) + + +class BTLEPhysField(FlagsField): + def __init__(self, name, default): + super(BTLEPhysField, self).__init__( + name, default, -8, + ['phy_1m', 'phy_2m', 'phy_coded'] + ) + + ########## # Layers # ########## @@ -167,15 +268,24 @@ def hashret(self): class BTLE_ADV(Packet): + # BT Core 5.2 - 2.3 ADVERTISING PHYSICAL CHANNEL PDU name = "BTLE advertising header" fields_desc = [ - BitEnumField("RxAdd", 0, 1, {0: "public", 1: "random"}), - BitEnumField("TxAdd", 0, 1, {0: "public", 1: "random"}), - BitField("RFU", 0, 2), # Unused - BitEnumField("PDU_type", 0, 4, {0: "ADV_IND", 1: "ADV_DIRECT_IND", 2: "ADV_NONCONN_IND", 3: "SCAN_REQ", # noqa: E501 - 4: "SCAN_RSP", 5: "CONNECT_REQ", 6: "ADV_SCAN_IND"}), # noqa: E501 - BitField("unused", 0, 2), # Unused - XBitField("Length", None, 6), + BitEnumField("RxAdd", 0, 1, {0: "public", + 1: "random"}), + BitEnumField("TxAdd", 0, 1, {0: "public", + 1: "random"}), + # 4.5.8.3.1 - LE Channel Selection Algorithm #2 + BitEnumField("ChSel", 0, 1, {1: "#2"}), + BitField("RFU", 0, 1), # Unused + BitEnumField("PDU_type", 0, 4, {0: "ADV_IND", + 1: "ADV_DIRECT_IND", + 2: "ADV_NONCONN_IND", + 3: "SCAN_REQ", + 4: "SCAN_RSP", + 5: "CONNECT_IND", + 6: "ADV_SCAN_IND"}), + XByteField("Length", None), ] def post_build(self, p, pay): @@ -185,7 +295,7 @@ def post_build(self, p, pay): l_pay = len(pay) else: l_pay = 0 - p = p[:1] + chb(l_pay & 0x3f) + p[2:] + p = p[:1] + chb(l_pay & 0xff) + p[2:] if not isinstance(self.underlayer, BTLE): self.add_underlayer(BTLE) return p @@ -274,23 +384,462 @@ class BTLE_CONNECT_REQ(Packet): BTLE_Versions = { - 7: '4.1' + 6: '4.0', + 7: '4.1', + 8: '4.2', + 9: '5.0', + 10: '5.1', + 11: '5.2', } + + BTLE_Corp_IDs = { - 0xf: 'Broadcom Corporation' + 0xf: 'Broadcom Corporation', + 0x59: 'Nordic Semiconductor ASA' +} + + +BTLE_BTLE_CTRL_opcode = { + 0x00: 'LL_CONNECTION_UPDATE_REQ', + 0x01: 'LL_CHANNEL_MAP_REQ', + 0x02: 'LL_TERMINATE_IND', + 0x03: 'LL_ENC_REQ', + 0x04: 'LL_ENC_RSP', + 0x05: 'LL_START_ENC_REQ', + 0x06: 'LL_START_ENC_RSP', + 0x07: 'LL_UNKNOWN_RSP', + 0x08: 'LL_FEATURE_REQ', + 0x09: 'LL_FEATURE_RSP', + 0x0A: 'LL_PAUSE_ENC_REQ', + 0x0B: 'LL_PAUSE_ENC_RSP', + 0x0C: 'LL_VERSION_IND', + 0x0D: 'LL_REJECT_IND', + 0x0E: 'LL_SLAVE_FEATURE_REQ', + 0x0F: 'LL_CONNECTION_PARAM_REQ', + 0x10: 'LL_CONNECTION_PARAM_RSP', + 0x14: 'LL_LENGTH_REQ', + 0x15: 'LL_LENGTH_RSP', + 0x16: 'LL_PHY_REQ', + 0x17: 'LL_PHY_RSP', + 0x18: 'LL_PHY_UPDATE_IND', + 0x19: 'LL_MIN_USED_CHANNELS', + 0x1A: 'LL_CTE_REQ', + 0x1B: 'LL_CTE_RSP', + 0x1C: 'LL_PERIODIC_SYNC_IND', + 0x1D: 'LL_CLOCK_ACCURACY_REQ', + 0x1E: 'LL_CLOCK_ACCURACY_RSP', + 0x1F: 'LL_CIS_REQ', + 0x20: 'LL_CIS_RSP', + 0x21: 'LL_CIS_IND', + 0x22: 'LL_CIS_TERMINATE_IND', + 0x23: 'LL_POWER_CONTROL_REQ', + 0x24: 'LL_POWER_CONTROL_RSP', + 0x25: 'LL_POWER_CHANGE_IND', + 0x26: 'LL_SUBRATE_REQ', + 0x27: 'LL_SUBRATE_IND', + 0x28: 'LL_CHANNEL_REPORTING_IND', + 0x29: 'LL_CHANNEL_STATUS_IND', } -class CtrlPDU(Packet): - name = "CtrlPDU" +class BTLE_EMPTY_PDU(Packet): + name = "Empty data PDU" + + +class BTLE_CTRL(Packet): + name = "BTLE_CTRL" + fields_desc = [ + ByteEnumField("opcode", 0, BTLE_BTLE_CTRL_opcode) + ] + + +class LL_CONNECTION_UPDATE_IND(Packet): + name = 'LL_CONNECTION_UPDATE_IND' + fields_desc = [ + XByteField("win_size", 0), + XLEShortField("win_offset", 0), + XLEShortField("interval", 6), + XLEShortField("latency", 0), + XLEShortField("timeout", 50), + XLEShortField("instant", 6), + ] + + +class LL_CHANNEL_MAP_IND(Packet): + name = 'LL_CHANNEL_MAP_IND' + fields_desc = [ + BTLEChanMapField("chM", 0xFFFFFFFFFE), + XLEShortField("instant", 0), + ] + + +class LL_TERMINATE_IND(Packet): + name = 'LL_TERMINATE_IND' + fields_desc = [ + XByteField("code", 0x0), + ] + + +class LL_ENC_REQ(Packet): + name = 'LL_ENC_REQ' + fields_desc = [ + XLELongField("rand", 0), + XLEShortField("ediv", 0), + XLELongField("skdm", 0), + XLEIntField("ivm", 0), + ] + + +class LL_ENC_RSP(Packet): + name = 'LL_ENC_RSP' + fields_desc = [ + XLELongField("skds", 0), + XLEIntField("ivs", 0), + ] + + +class LL_START_ENC_REQ(Packet): + name = 'LL_START_ENC_REQ' + fields_desc = [] + + +class LL_START_ENC_RSP(Packet): + name = 'LL_START_ENC_RSP' + + +class LL_UNKNOWN_RSP(Packet): + name = 'LL_UNKNOWN_RSP' + fields_desc = [ + XByteField("code", 0x0), + ] + + +class LL_FEATURE_REQ(Packet): + name = "LL_FEATURE_REQ" + fields_desc = [ + BTLEFeatureField("feature_set", 0) + ] + + +class LL_FEATURE_RSP(Packet): + name = "LL_FEATURE_RSP" + fields_desc = [ + BTLEFeatureField("feature_set", 0) + ] + + +class LL_PAUSE_ENC_REQ(Packet): + name = "LL_PAUSE_ENC_REQ" + + +class LL_PAUSE_ENC_RSP(Packet): + name = "LL_PAUSE_ENC_RSP" + + +class LL_VERSION_IND(Packet): + name = "LL_VERSION_IND" + fields_desc = [ + ByteEnumField("version", 8, BTLE_Versions), + LEShortEnumField("company", 0, BTLE_Corp_IDs), + XLEShortField("subversion", 0) + ] + + +class LL_REJECT_IND(Packet): + name = "LL_REJECT_IND" + fields_desc = [ + XByteField("code", 0x0), + ] + + +class LL_SLAVE_FEATURE_REQ(Packet): + name = "LL_SLAVE_FEATURE_REQ" + fields_desc = [ + BTLEFeatureField("feature_set", 0) + ] + + +class LL_CONNECTION_PARAM_REQ(Packet): + name = "LL_CONNECTION_PARAM_REQ" + fields_desc = [ + XShortField("interval_min", 0x6), + XShortField("interval_max", 0x6), + XShortField("latency", 0x0), + XShortField("timeout", 0x0), + XByteField("preferred_periodicity", 0x0), + XShortField("reference_conn_evt_count", 0x0), + XShortField("offset0", 0x0), + XShortField("offset1", 0x0), + XShortField("offset2", 0x0), + XShortField("offset3", 0x0), + XShortField("offset4", 0x0), + XShortField("offset5", 0x0), + ] + + +class LL_CONNECTION_PARAM_RSP(Packet): + name = "LL_CONNECTION_PARAM_RSP" + fields_desc = [ + XShortField("interval_min", 0x6), + XShortField("interval_max", 0x6), + XShortField("latency", 0x0), + XShortField("timeout", 0x0), + XByteField("preferred_periodicity", 0x0), + XShortField("reference_conn_evt_count", 0x0), + XShortField("offset0", 0x0), + XShortField("offset1", 0x0), + XShortField("offset2", 0x0), + XShortField("offset3", 0x0), + XShortField("offset4", 0x0), + XShortField("offset5", 0x0), + ] + + +class LL_REJECT_EXT_IND(Packet): + name = "LL_REJECT_EXT_IND" + fields_desc = [ + XByteField("reject_opcode", 0x0), + XByteField("error_code", 0x0), + ] + + +class LL_PING_REQ(Packet): + name = "LL_PING_REQ" + + +class LL_PING_RSP(Packet): + name = "LL_PING_RSP" + + +class LL_LENGTH_REQ(Packet): + name = ' LL_LENGTH_REQ' fields_desc = [ - XByteField("optcode", 0), - ByteEnumField("version", 0, BTLE_Versions), - LEShortEnumField("Company", 0, BTLE_Corp_IDs), - XShortField("subversion", 0) + XLEShortField("max_rx_bytes", 251), + XLEShortField("max_rx_time", 2120), + XLEShortField("max_tx_bytes", 251), + XLEShortField("max_tx_time", 2120), ] +class LL_LENGTH_RSP(Packet): + name = ' LL_LENGTH_RSP' + fields_desc = [ + XLEShortField("max_rx_bytes", 251), + XLEShortField("max_rx_time", 2120), + XLEShortField("max_tx_bytes", 251), + XLEShortField("max_tx_time", 2120), + ] + + +class LL_PHY_REQ(Packet): + name = "LL_PHY_REQ" + fields_desc = [ + BTLEPhysField('tx_phys', 0), + BTLEPhysField('rx_phys', 0), + ] + + +class LL_PHY_RSP(Packet): + name = "LL_PHY_RSP" + fields_desc = [ + BTLEPhysField('tx_phys', 0), + BTLEPhysField('rx_phys', 0), + ] + + +class LL_PHY_UPDATE_IND(Packet): + name = "LL_PHY_UPDATE_IND" + fields_desc = [ + BTLEPhysField('tx_phy', 0), + BTLEPhysField('rx_phy', 0), + XShortField("instant", 0x0), + ] + + +class LL_MIN_USED_CHANNELS_IND(Packet): + name = "LL_MIN_USED_CHANNELS_IND" + fields_desc = [ + BTLEPhysField('phys', 0), + ByteField("min_used_channels", 2), + ] + + +class LL_CTE_REQ(Packet): + name = "LL_CTE_REQ" + fields_desc = [ + LEBitField('min_cte_len_req', 0, 5), + LEBitField('rfu', 0, 1), + LEBitField("cte_type_req", 0, 2) + ] + + +class LL_CTE_RSP(Packet): + name = "LL_CTE_RSP" + fields_desc = [] + + +class LL_PERIODIC_SYNC_IND(Packet): + name = "LL_PERIODIC_SYNC_IND" + fields_desc = [ + XLEShortField("id", 251), + LEBitField("sync_info", 0, 18 * 8), + XLEShortField("conn_event_count", 0), + XLEShortField("last_pa_event_counter", 0), + LEBitField('sid', 0, 4), + LEBitField('a_type', 0, 1), + LEBitField('sca', 0, 3), + BTLEPhysField('phy', 0), + BDAddrField("AdvA", None), + XLEShortField("sync_conn_event_count", 0), + ] + + +class LL_CLOCK_ACCURACY_REQ(Packet): + name = "LL_CLOCK_ACCURACY_REQ" + fields_desc = [ + XByteField("sca", 0), + ] + + +class LL_CLOCK_ACCURACY_RSP(Packet): + name = "LL_CLOCK_ACCURACY_RSP" + fields_desc = [ + XByteField("sca", 0), + ] + + +class LL_CIS_REQ(Packet): + name = 'LL_CIS_REQ' + fields_desc = [ + XByteField("cig_id", 0), + XByteField("cis_id", 0), + BTLEPhysField('phy_c_to_p', 0), + BTLEPhysField('phy_p_to_c', 0), + LEBitField('max_sdu_c_to_p', 0, 12), + LEBitField('rfu1', 0, 3), + LEBitField('framed', 0, 1), + LEBitField('max_sdu_p_to_c', 0, 12), + LEBitField('rfu2', 0, 4), + LEBitField('sdu_interval_c_to_p', 0, 20), + LEBitField('rfu3', 0, 4), + LEBitField('sdu_interval_p_to_c', 0, 20), + LEBitField('rfu4', 0, 4), + XLEShortField("max_pdu_c_to_p", 0), + XLEShortField("max_pdu_p_to_c", 0), + XByteField("nse", 0), + X3BytesField("subinterval", 0x0), + LEBitField('bn_c_to_p', 0, 4), + LEBitField('bn_p_to_c', 0, 4), + ByteField("ft_c_to_p", 0), + ByteField("ft_p_to_c", 0), + XLEShortField("iso_interval", 0), + X3BytesField("cis_offset_min", 0x0), + X3BytesField("cis_offset_max", 0x0), + XLEShortField("conn_event_count", 0), + ] + + +class LL_CIS_RSP(Packet): + name = 'LL_CIS_RSP' + fields_desc = [ + X3BytesField("cis_offset_min", 0x0), + X3BytesField("cis_offset_max", 0x0), + XLEShortField("conn_event_count", 0), + ] + + +class LL_CIS_IND(Packet): + name = 'LL_CIS_IND' + fields_desc = [ + XIntField("AA", 0x00), + X3BytesField("cis_offset", 0x0), + X3BytesField("cig_sync_delay", 0x0), + X3BytesField("cis_sync_delay", 0x0), + XLEShortField("conn_event_count", 0), + ] + + +class LL_CIS_TERMINATE_IND(Packet): + name = 'LL_CIS_TERMINATE_IND' + fields_desc = [ + ByteField("cig_id", 0x0), + ByteField("cis_id", 0x0), + ByteField("error_code", 0x0), + ] + + +class LL_POWER_CONTROL_REQ(Packet): + name = 'LL_POWER_CONTROL_REQ' + fields_desc = [ + ByteField("phy", 0x0), + SignedByteField("delta", 0x0), + SignedByteField("tx_power", 0x0), + ] + + +class LL_POWER_CONTROL_RSP(Packet): + name = 'LL_POWER_CONTROL_RSP' + fields_desc = [ + LEBitField("min", 0, 1), + LEBitField("max", 0, 1), + LEBitField("rfu", 0, 6), + SignedByteField("delta", 0), + SignedByteField("tx_power", 0x0), + ByteField("apr", 0x0), + ] + + +class LL_POWER_CHANGE_IND(Packet): + name = 'LL_POWER_CHANGE_IND' + fields_desc = [ + ByteField("phy", 0x0), + LEBitField("min", 0, 1), + LEBitField("max", 0, 1), + LEBitField("rfu", 0, 6), + SignedByteField("delta", 0), + ByteField("tx_power", 0x0), + ] + + +class LL_SUBRATE_REQ(Packet): + name = 'LL_SUBRATE_REQ' + fields_desc = [ + LEShortField("subrate_factor_min", 0x0), + LEShortField("subrate_factor_max", 0x0), + LEShortField("max_latency", 0x0), + LEShortField("continuation_number", 0x0), + LEShortField("timeout", 0x0), + ] + + +class LL_SUBRATE_IND(Packet): + name = 'LL_SUBRATE_IND' + fields_desc = [ + LEShortField("subrate_factor", 0x0), + LEShortField("subrate_base_event", 0x0), + LEShortField("latency", 0x0), + LEShortField("continuation_number", 0x0), + LEShortField("timeout", 0x0), + ] + + +class LL_CHANNEL_REPORTING_IND(Packet): + name = 'LL_SUBRATE_IND' + fields_desc = [ + ByteField("enable", 0x0), + ByteField("min_spacing", 0x0), + ByteField("max_delay", 0x0), + ] + + +class LL_CHANNEL_STATUS_IND(Packet): + name = 'LL_CHANNEL_STATUS_IND' + fields_desc = [ + LEBitField("channel_classification", 0, 10 * 8), + ] + + +# Advertisement (37-39) channel PDUs bind_layers(BTLE, BTLE_ADV, access_addr=0x8E89BED6) bind_layers(BTLE, BTLE_DATA) bind_layers(BTLE_ADV, BTLE_ADV_IND, PDU_type=0) @@ -301,9 +850,54 @@ class CtrlPDU(Packet): bind_layers(BTLE_ADV, BTLE_CONNECT_REQ, PDU_type=5) bind_layers(BTLE_ADV, BTLE_ADV_SCAN_IND, PDU_type=6) -bind_layers(BTLE_DATA, L2CAP_Hdr, LLID=2) # BTLE_DATA / L2CAP_Hdr / ATT_Hdr +# Data channel (0-36) PDUs # LLID=1 -> Continue -bind_layers(BTLE_DATA, CtrlPDU, LLID=3) +bind_layers(BTLE_DATA, L2CAP_Hdr, LLID=2) +bind_layers(BTLE_DATA, BTLE_CTRL, LLID=3) +bind_layers(BTLE_DATA, BTLE_EMPTY_PDU, {'len': 0, 'LLID': 1}) +bind_layers(BTLE_CTRL, LL_CONNECTION_UPDATE_IND, opcode=0x00) +bind_layers(BTLE_CTRL, LL_CHANNEL_MAP_IND, opcode=0x01) +bind_layers(BTLE_CTRL, LL_TERMINATE_IND, opcode=0x02) +bind_layers(BTLE_CTRL, LL_ENC_REQ, opcode=0x03) +bind_layers(BTLE_CTRL, LL_ENC_RSP, opcode=0x04) +bind_layers(BTLE_CTRL, LL_START_ENC_REQ, opcode=0x05) +bind_layers(BTLE_CTRL, LL_START_ENC_RSP, opcode=0x06) +bind_layers(BTLE_CTRL, LL_UNKNOWN_RSP, opcode=0x07) +bind_layers(BTLE_CTRL, LL_FEATURE_REQ, opcode=0x08) +bind_layers(BTLE_CTRL, LL_FEATURE_RSP, opcode=0x09) +bind_layers(BTLE_CTRL, LL_PAUSE_ENC_REQ, opcode=0x0A) +bind_layers(BTLE_CTRL, LL_PAUSE_ENC_RSP, opcode=0x0B) +bind_layers(BTLE_CTRL, LL_VERSION_IND, opcode=0x0C) +bind_layers(BTLE_CTRL, LL_REJECT_IND, opcode=0x0D) +bind_layers(BTLE_CTRL, LL_SLAVE_FEATURE_REQ, opcode=0x0E) +bind_layers(BTLE_CTRL, LL_CONNECTION_PARAM_REQ, opcode=0x0F) +bind_layers(BTLE_CTRL, LL_CONNECTION_PARAM_RSP, opcode=0x10) +bind_layers(BTLE_CTRL, LL_REJECT_EXT_IND, opcode=0x11) +bind_layers(BTLE_CTRL, LL_PING_REQ, opcode=0x12) +bind_layers(BTLE_CTRL, LL_PING_RSP, opcode=0x13) +bind_layers(BTLE_CTRL, LL_LENGTH_REQ, opcode=0x14) +bind_layers(BTLE_CTRL, LL_LENGTH_RSP, opcode=0x15) +bind_layers(BTLE_CTRL, LL_PHY_REQ, opcode=0x16) +bind_layers(BTLE_CTRL, LL_PHY_RSP, opcode=0x17) +bind_layers(BTLE_CTRL, LL_PHY_UPDATE_IND, opcode=0x18) +bind_layers(BTLE_CTRL, LL_MIN_USED_CHANNELS_IND, opcode=0x19) +bind_layers(BTLE_CTRL, LL_CTE_REQ, opcode=0x1A) +bind_layers(BTLE_CTRL, LL_CTE_RSP, opcode=0x1B) +bind_layers(BTLE_CTRL, LL_PERIODIC_SYNC_IND, opcode=0x1C) +bind_layers(BTLE_CTRL, LL_CLOCK_ACCURACY_REQ, opcode=0x1D) +bind_layers(BTLE_CTRL, LL_CLOCK_ACCURACY_RSP, opcode=0x1E) +bind_layers(BTLE_CTRL, LL_CIS_REQ, opcode=0x1F) +bind_layers(BTLE_CTRL, LL_CIS_RSP, opcode=0x20) +bind_layers(BTLE_CTRL, LL_CIS_IND, opcode=0x21) +bind_layers(BTLE_CTRL, LL_CIS_TERMINATE_IND, opcode=0x22) +bind_layers(BTLE_CTRL, LL_POWER_CONTROL_REQ, opcode=0x23) +bind_layers(BTLE_CTRL, LL_POWER_CONTROL_RSP, opcode=0x24) +bind_layers(BTLE_CTRL, LL_POWER_CHANGE_IND, opcode=0x25) +bind_layers(BTLE_CTRL, LL_SUBRATE_REQ, opcode=0x26) +bind_layers(BTLE_CTRL, LL_SUBRATE_IND, opcode=0x27) +bind_layers(BTLE_CTRL, LL_CHANNEL_REPORTING_IND, opcode=0x28) +bind_layers(BTLE_CTRL, LL_CHANNEL_STATUS_IND, opcode=0x29) + conf.l2types.register(DLT_BLUETOOTH_LE_LL, BTLE) conf.l2types.register(DLT_BLUETOOTH_LE_LL_WITH_PHDR, BTLE_RF) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 23b0045082d..0c02c3c4496 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """A minimal implementation of the CANopen protocol, based on @@ -12,32 +12,86 @@ import os import gzip import struct -import binascii -import scapy.modules.six as six + from scapy.config import conf -from scapy.compat import orb -from scapy.data import DLT_CAN_SOCKETCAN, MTU +from scapy.compat import chb, hex_bytes +from scapy.data import DLT_CAN_SOCKETCAN from scapy.fields import FieldLenField, FlagsField, StrLenField, \ - ThreeBytesField, XBitField, ScalingField, ConditionalField, LenField + ThreeBytesField, XBitField, ScalingField, ConditionalField, LenField, ShortField from scapy.volatile import RandFloat, RandBinFloat from scapy.packet import Packet, bind_layers from scapy.layers.l2 import CookedLinux from scapy.error import Scapy_Exception from scapy.plist import PacketList +from scapy.supersocket import SuperSocket +from scapy.utils import _ByteStream + +# Typing imports +from typing import ( + Tuple, + Optional, + Type, + List, + Union, + Callable, + IO, + Any, + cast, +) __all__ = ["CAN", "SignalPacket", "SignalField", "LESignedSignalField", "LEUnsignedSignalField", "LEFloatSignalField", "BEFloatSignalField", "BESignedSignalField", "BEUnsignedSignalField", "rdcandump", - "CandumpReader", "SignalHeader"] + "CandumpReader", "SignalHeader", "CAN_MTU", "CAN_MAX_IDENTIFIER", + "CAN_MAX_DLEN", "CAN_INV_FILTER", "CANFD", "CAN_FD_MTU", + "CAN_FD_MAX_DLEN"] + +# CONSTANTS +CAN_MAX_IDENTIFIER = (1 << 29) - 1 # Maximum 29-bit identifier +CAN_MTU = 16 +CAN_MAX_DLEN = 8 +CAN_INV_FILTER = 0x20000000 +CAN_FD_MTU = 72 +CAN_FD_MAX_DLEN = 64 -# Mimics the Wireshark CAN dissector parameter 'Byte-swap the CAN ID/flags field' # noqa: E501 -# set to True when working with PF_CAN sockets -conf.contribs['CAN'] = {'swap-bytes': False} +# Mimics the Wireshark CAN dissector parameter +# 'Byte-swap the CAN ID/flags field'. +# Set to True when working with PF_CAN sockets +conf.contribs['CAN'] = {'swap-bytes': False, + 'remove-padding': True} class CAN(Packet): - """A minimal implementation of the CANopen protocol, based on - Wireshark dissectors. See https://wiki.wireshark.org/CANopen + """A implementation of CAN messages. + + Dissection of CAN messages from Wireshark captures and Linux PF_CAN sockets + are supported from protocol specification. + See https://wiki.wireshark.org/CANopen for further information on + the Wireshark dissector. Linux PF_CAN and Wireshark use different + endianness for the first 32 bit of a CAN message. This dissector can be + configured for both use cases. + + Configuration ``swap-bytes``: + Wireshark dissection: + >>> conf.contribs['CAN']['swap-bytes'] = False + + PF_CAN Socket dissection: + >>> conf.contribs['CAN']['swap-bytes'] = True + + Configuration ``remove-padding``: + Linux PF_CAN Sockets always return 16 bytes per CAN frame receive. + This implicates that CAN frames get padded from the Linux PF_CAN socket + with zeros up to 8 bytes of data. The real length from the CAN frame on + the wire is given by the length field. To obtain only the CAN frame from + the wire, this additional padding has to be removed. Nevertheless, for + corner cases, it might be useful to also get the padding. This can be + configured through the **remove-padding** configuration. + + Truncate CAN frame based on length field: + >>> conf.contribs['CAN']['remove-padding'] = True + + Show entire CAN frame received from socket: + >>> conf.contribs['CAN']['remove-padding'] = False """ fields_desc = [ @@ -47,57 +101,134 @@ class CAN(Packet): XBitField('identifier', 0, 29), FieldLenField('length', None, length_of='data', fmt='B'), ThreeBytesField('reserved', 0), - StrLenField('data', '', length_from=lambda pkt: pkt.length), + StrLenField('data', b'', length_from=lambda pkt: int(pkt.length)), ] + @classmethod + def dispatch_hook(cls, + _pkt=None, # type: Optional[bytes] + *args, # type: Any + **kargs # type: Any + ): # type: (...) -> Type[Packet] + if _pkt: + fdf_set = len(_pkt) > 5 and _pkt[5] & 0x04 and \ + not _pkt[5] & 0xf8 + if fdf_set: + return CANFD + elif len(_pkt) > 4 and _pkt[4] > 8: + return CANFD + return CAN + @staticmethod def inv_endianness(pkt): - """ Invert the order of the first four bytes of a CAN packet + # type: (bytes) -> bytes + """Invert the order of the first four bytes of a CAN packet This method is meant to be used specifically to convert a CAN packet - between the pcap format and the socketCAN format + between the pcap format and the SocketCAN format - :param pkt: str of the CAN packet - :return: packet str with the first four bytes swapped + :param pkt: bytes str of the CAN packet + :return: bytes str with the first four bytes swapped """ len_partial = len(pkt) - 4 # len of the packet, CAN ID excluded return struct.pack('I{}s'.format(len_partial), pkt)) def pre_dissect(self, s): - """ Implements the swap-bytes functionality when dissecting """ + # type: (bytes) -> bytes + """Implements the swap-bytes functionality when dissecting """ if conf.contribs['CAN']['swap-bytes']: - return CAN.inv_endianness(s) + data = CAN.inv_endianness(s) # type: bytes + return data return s def post_dissect(self, s): + # type: (bytes) -> bytes self.raw_packet_cache = None # Reset packet to allow post_build return s def post_build(self, pkt, pay): - """ Implements the swap-bytes functionality when building + # type: (bytes, bytes) -> bytes + """Implements the swap-bytes functionality for Packet build. - this is based on a copy of the Packet.self_build default method. + This is based on a copy of the Packet.self_build default method. The goal is to affect only the CAN layer data and keep - under layers (e.g LinuxCooked) unchanged + under layers (e.g CookedLinux) unchanged """ if conf.contribs['CAN']['swap-bytes']: - return CAN.inv_endianness(pkt) + pay + data = CAN.inv_endianness(pkt) # type: bytes + return data + pay return pkt + pay def extract_padding(self, p): - return b'', p + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + if conf.contribs['CAN']['remove-padding']: + return b'', None + else: + return b'', p conf.l2types.register(DLT_CAN_SOCKETCAN, CAN) bind_layers(CookedLinux, CAN, proto=12) +class CANFD(CAN): + """ + This class is used for distinction of CAN FD packets. + """ + fields_desc = [ + FlagsField('flags', 0, 3, ['error', + 'remote_transmission_request', + 'extended']), + XBitField('identifier', 0, 29), + FieldLenField('length', None, length_of='data', fmt='B'), + FlagsField('fd_flags', 4, 8, ['bit_rate_switch', + 'error_state_indicator', + 'fd_frame']), + ShortField('reserved', 0), + StrLenField('data', b'', length_from=lambda pkt: int(pkt.length)), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + + data = super(CANFD, self).post_build(pkt, pay) + + length = data[4] + + if 8 < length <= 24: + wire_length = length + (-length) % 4 + elif 24 < length <= 64: + wire_length = length + (-length) % 8 + elif length > 64: + raise NotImplementedError + else: + wire_length = length + + pad = b"\x00" * (wire_length - length) + + return data[0:4] + chb(wire_length) + data[5:] + pad + + +bind_layers(CookedLinux, CANFD, proto=13) + + class SignalField(ScalingField): + """SignalField is a base class for signal data, usually transmitted from + CAN messages in automotive applications. Most vehicle manufacturers + describe their vehicle internal signals by so called data base CAN (DBC) + files. All necessary functions to easily create Scapy dissectors similar + to signal descriptions from DBC files are provided by this base class. + + SignalField instances should only be used together with SignalPacket + classes since SignalPackets enforce length checks for CAN messages. + + """ __slots__ = ["start", "size"] def __init__(self, name, default, start, size, scaling=1, unit="", offset=0, ndigits=3, fmt="B"): + # type: (str, Union[int, float], int, int, Union[int, float], str, Union[int, float], int, str) -> None # noqa: E501 ScalingField.__init__(self, name, default, scaling, unit, offset, ndigits, fmt) self.start = start @@ -117,37 +248,53 @@ def __init__(self, name, default, start, size, scaling=1, unit="", @staticmethod def _msb_lookup(start): - return SignalField._lookup_table.index(start) + # type: (int) -> int + try: + return SignalField._lookup_table.index(start) + except ValueError: + raise Scapy_Exception("Only 64 bits for all SignalFields " + "are supported") @staticmethod def _lsb_lookup(start, size): - return SignalField._lookup_table[SignalField._msb_lookup(start) + - size - 1] + # type: (int, int) -> int + try: + return SignalField._lookup_table[SignalField._msb_lookup(start) + + size - 1] + except IndexError: + raise Scapy_Exception("Only 64 bits for all SignalFields " + "are supported") @staticmethod def _convert_to_unsigned(number, bit_length): + # type: (int, int) -> int if number & (1 << (bit_length - 1)): - mask = (2 ** bit_length) + mask = (2 ** bit_length) # type: int return mask + number return number @staticmethod def _convert_to_signed(number, bit_length): - mask = (2 ** bit_length) - 1 + # type: (int, int) -> int + mask = (2 ** bit_length) - 1 # type: int if number & (1 << (bit_length - 1)): return number | ~mask return number & mask def _is_little_endian(self): + # type: () -> bool return self.fmt[0] == "<" def _is_signed_number(self): + # type: () -> bool return self.fmt[-1].islower() def _is_float_number(self): + # type: () -> bool return self.fmt[-1] == "f" def addfield(self, pkt, s, val): + # type: (Packet, bytes, Optional[Union[int, float]]) -> bytes if not isinstance(pkt, SignalPacket): raise Scapy_Exception("Only use SignalFields in a SignalPacket") @@ -169,17 +316,20 @@ def addfield(self, pkt, s, val): s += b"\x00" * (field_len - len(s)) if self._is_float_number(): - val = struct.unpack(self.fmt[0] + "I", - struct.pack(self.fmt, val))[0] + int_val = struct.unpack(self.fmt[0] + "I", + struct.pack(self.fmt, val))[0] # type: int elif self._is_signed_number(): - val = self._convert_to_unsigned(val, self.size) + int_val = self._convert_to_unsigned(int(val), self.size) + else: + int_val = cast(int, val) pkt_val = struct.unpack(fmt, (s + b"\x00" * 8)[:8])[0] - pkt_val |= val << shift + pkt_val |= int_val << shift tmp_s = struct.pack(fmt, pkt_val) return tmp_s[:len(s)] def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, Union[int, float]] if not isinstance(pkt, SignalPacket): raise Scapy_Exception("Only use SignalFields in a SignalPacket") @@ -216,6 +366,7 @@ def getfield(self, pkt, s): return s, self.m2i(pkt, fld_val) def randval(self): + # type: () -> Union[RandBinFloat, RandFloat] if self._is_float_number(): return RandBinFloat(0, 0) @@ -232,12 +383,14 @@ def randval(self): return RandFloat(min(min_val, max_val), max(min_val, max_val)) def i2len(self, pkt, x): - return float(self.size) / 8 + # type: (Packet, Any) -> int + return int(float(self.size) / 8) class LEUnsignedSignalField(SignalField): def __init__(self, name, default, start, size, scaling=1, unit="", offset=0, ndigits=3): + # type: (str, Union[int, float], int, int, Union[int, float], str, Union[int, float], int) -> None # noqa: E501 SignalField.__init__(self, name, default, start, size, scaling, unit, offset, ndigits, " None # noqa: E501 SignalField.__init__(self, name, default, start, size, scaling, unit, offset, ndigits, " None # noqa: E501 SignalField.__init__(self, name, default, start, size, scaling, unit, offset, ndigits, ">B") @@ -259,6 +414,7 @@ def __init__(self, name, default, start, size, scaling=1, unit="", class BESignedSignalField(SignalField): def __init__(self, name, default, start, size, scaling=1, unit="", offset=0, ndigits=3): + # type: (str, Union[int, float], int, int, Union[int, float], str, Union[int, float], int) -> None # noqa: E501 SignalField.__init__(self, name, default, start, size, scaling, unit, offset, ndigits, ">b") @@ -266,6 +422,7 @@ def __init__(self, name, default, start, size, scaling=1, unit="", class LEFloatSignalField(SignalField): def __init__(self, name, default, start, scaling=1, unit="", offset=0, ndigits=3): + # type: (str, Union[int, float], int, Union[int, float], str, Union[int, float], int) -> None # noqa: E501 SignalField.__init__(self, name, default, start, 32, scaling, unit, offset, ndigits, " None # noqa: E501 SignalField.__init__(self, name, default, start, 32, scaling, unit, offset, ndigits, ">f") class SignalPacket(Packet): + """Special implementation of Packet. + + This class enforces the correct wirelen of a CAN message for + signal transmitting in automotive applications. + Furthermore, the dissection order of SignalFields in fields_desc is + deduced by the start index of a field. + """ + def pre_dissect(self, s): + # type: (bytes) -> bytes if not all(isinstance(f, SignalField) or (isinstance(f, ConditionalField) and isinstance(f.fld, SignalField)) @@ -287,12 +454,14 @@ def pre_dissect(self, s): return s def post_dissect(self, s): - """ SignalFields can be dissected on packets with unordered fields. + # type: (bytes) -> bytes + """SignalFields can be dissected on packets with unordered fields. + The order of SignalFields is defined from the start parameter. After a build, the consumed bytes of the length of all SignalFields have to be removed from the SignalPacket. """ - if self.wirelen > 8: + if self.wirelen is not None and self.wirelen > 8: raise Scapy_Exception("Only 64 bits for all SignalFields " "are supported") self.raw_packet_cache = None # Reset packet to allow post_build @@ -300,66 +469,129 @@ def post_dissect(self, s): class SignalHeader(CAN): + """Special implementation of a CAN Packet to allow dynamic binding. + + This class can be provided to CANSockets as basecls. + + Example: + >>> class floatSignals(SignalPacket): + >>> fields_desc = [ + >>> LEFloatSignalField("floatSignal2", default=0, start=32), + >>> BEFloatSignalField("floatSignal1", default=0, start=7)] + >>> + >>> bind_layers(SignalHeader, floatSignals, identifier=0x321) + >>> + >>> dbc_sock = CANSocket("can0", basecls=SignalHeader) + + All CAN messages received from this dbc_sock CANSocket will be interpreted + as SignalHeader. Through Scapys ``bind_layers`` mechanism, all CAN messages + with CAN identifier 0x321 will interpret the payload bytes of these + CAN messages as floatSignals packet. + """ fields_desc = [ FlagsField('flags', 0, 3, ['error', 'remote_transmission_request', 'extended']), XBitField('identifier', 0, 29), LenField('length', None, fmt='B'), - ThreeBytesField('reserved', 0) + FlagsField('fd_flags', 0, 8, ['bit_rate_switch', + 'error_state_indicator', + 'fd_frame']), + ShortField('reserved', 0) ] + @classmethod + def dispatch_hook(cls, + _pkt=None, # type: Optional[bytes] + *args, # type: Any + **kargs # type: Any + ): # type: (...) -> Type[Packet] + return SignalHeader + def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] return s, None def rdcandump(filename, count=-1, interface=None): - """Read a candump log file and return a packet list - - filename: file to read - count: read only packets - interfaces: return only packets from a specified interface + # type: (str, int, Optional[str]) -> PacketList + """ Read a candump log file and return a packet list. + + :param filename: Filename of the file to read from. + Also gzip files are accepted. + :param count: Read only packets. Specify -1 to read all packets. + :param interface: Return only packets from a specified interface + :return: A PacketList object containing the read files """ with CandumpReader(filename, interface) as fdesc: return fdesc.read_all(count=count) class CandumpReader: - """A stateful candump reader. Each packet is returned as a CAN packet""" + """A stateful candump reader. Each packet is returned as a CAN packet. + + Creates a CandumpReader object + + :param filename: filename of a candump logfile, compressed or + uncompressed, or a already opened file object. + :param interface: Name of a interface, if candump contains messages + of multiple interfaces and only one messages from a + specific interface are wanted. + """ - read_allowed_exceptions = () # emulate SuperSocket nonblocking_socket = True def __init__(self, filename, interface=None): + # type: (str, Optional[Union[List[str], str]]) -> None self.filename, self.f = self.open(filename) - self.ifilter = None + self.ifilter = None # type: Optional[List[str]] if interface is not None: - if isinstance(interface, six.string_types): + if isinstance(interface, str): self.ifilter = [interface] else: self.ifilter = interface def __iter__(self): + # type: () -> CandumpReader return self @staticmethod def open(filename): + # type: (Union[IO[bytes], str]) -> Tuple[str, _ByteStream] + """Open function to handle three types of input data. + + If filename of a regular candump log file is provided, this function + opens the file and returns the file object. + If filename of a gzip compressed candump log file is provided, the + required gzip open function is used to obtain the necessary file + object, which gets returned. + If a fileobject or ByteIO is provided, the filename is gathered for + internal use. No further steps are performed on this object. + + :param filename: Can be a string, specifying a candump log file or a + gzip compressed candump log file. Also already opened + file objects are allowed. + :return: A opened file object for further use. + """ """Open (if necessary) filename.""" - if isinstance(filename, six.string_types): + if isinstance(filename, str): try: - fdesc = gzip.open(filename, "rb") + fdesc = gzip.open(filename, "rb") # type: _ByteStream # try read to cause exception fdesc.read(1) fdesc.seek(0) except IOError: fdesc = open(filename, "rb") + return filename, fdesc else: - fdesc = filename - filename = getattr(fdesc, "name", "No name") - return filename, fdesc + name = getattr(filename, "name", "No name") + return name, filename def next(self): - """implement the iterator protocol on a set of packets + # type: () -> Packet + """Implements the iterator protocol on a set of packets + + :return: Next readable CAN Packet from the specified file """ try: pkt = None @@ -371,23 +603,33 @@ def next(self): return pkt __next__ = next - def read_packet(self, size=MTU): - """return a single packet read from the file or None if filters apply + def read_packet(self, size=CAN_MTU): + # type: (int) -> Optional[Packet] + """Read a packet from the specified file. + + This function will raise EOFError when no more packets are available. - raise EOFError when no more packets are available + :param size: Not used. Just here to follow the function signature for + SuperSocket emulation. + :return: A single packet read from the file or None if filters apply """ line = self.f.readline() line = line.lstrip() if len(line) < 16: raise EOFError - is_log_file_format = orb(line[0]) == orb(b"(") - + is_log_file_format = line[0] == ord(b"(") + fd_flags = None if is_log_file_format: - t, intf, f = line.split() - idn, data = f.split(b'#') + t_b, intf, f = line.split() + if b'##' in f: + idn, data = f.split(b'##') + fd_flags = data[0] + data = data[1:] + else: + idn, data = f.split(b'#') le = None - t = float(t[1:-1]) + t = float(t_b[1:-1]) # type: Optional[float] else: h, data = line.split(b']') intf, idn, le = h.split() @@ -400,7 +642,12 @@ def read_packet(self, size=MTU): data = data.replace(b' ', b'') data = data.strip() - pkt = CAN(identifier=int(idn, 16), data=binascii.unhexlify(data)) + if len(data) <= 8 and fd_flags is None: + pkt = CAN(identifier=int(idn, 16), data=hex_bytes(data)) + else: + pkt = CANFD(identifier=int(idn, 16), fd_flags=fd_flags, + data=hex_bytes(data)) + if le is not None: pkt.length = int(le[1:]) else: @@ -415,7 +662,8 @@ def read_packet(self, size=MTU): return pkt def dispatch(self, callback): - """call the specified callback routine for each packet read + # type: (Callable[[Packet], None]) -> None + """Call the specified callback routine for each packet read This is just a convenience function for the main loop that allows for easy launching of packet processing in a @@ -425,7 +673,12 @@ def dispatch(self, callback): callback(p) def read_all(self, count=-1): - """return a list of all packets in the candump file + # type: (int) -> PacketList + """Read a specific number or all packets from a candump file. + + :param count: Specify a specific number of packets to be read. + All packets can be read by count=-1. + :return: A PacketList object containing read CAN messages """ res = [] while count != 0: @@ -439,24 +692,40 @@ def read_all(self, count=-1): res.append(p) return PacketList(res, name=os.path.basename(self.filename)) - def recv(self, size=MTU): - """ Emulate a socket - """ - return self.read_packet(size=size) + def recv(self, size=CAN_MTU): + # type: (int) -> Optional[Packet] + """Emulation of SuperSocket""" + try: + return self.read_packet(size=size) + except EOFError: + return None def fileno(self): + # type: () -> int + """Emulation of SuperSocket""" return self.f.fileno() + @property + def closed(self): + # type: () -> bool + return self.f.closed + def close(self): + # type: () -> Any + """Emulation of SuperSocket""" return self.f.close() def __enter__(self): + # type: () -> CandumpReader return self def __exit__(self, exc_type, exc_value, tracback): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> None # noqa: E501 self.close() - # emulate SuperSocket @staticmethod def select(sockets, remain=None): - return sockets, None + # type: (List[SuperSocket], Optional[int]) -> List[SuperSocket] + """Emulation of SuperSocket""" + return [s for s in sockets if isinstance(s, CandumpReader) and + not s.closed] diff --git a/scapy/layers/clns.py b/scapy/layers/clns.py index 5ffbd86f638..e9121adb2a5 100644 --- a/scapy/layers/clns.py +++ b/scapy/layers/clns.py @@ -1,20 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2014, 2015 BENOCS GmbH, Berlin (Germany) + """ CLNS Extension ~~~~~~~~~~~~~~~~~~~~~ :copyright: 2014, 2015 BENOCS GmbH, Berlin (Germany) :author: Marcel Patzlaff, mpatzlaff@benocs.com - :license: GPLv2 - - This module is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This module is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. :description: diff --git a/scapy/layers/dcerpc.py b/scapy/layers/dcerpc.py new file mode 100644 index 00000000000..c8876a4a30d --- /dev/null +++ b/scapy/layers/dcerpc.py @@ -0,0 +1,3410 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +# scapy.contrib.description = DCE/RPC +# scapy.contrib.status = loads + +""" +DCE/RPC +Distributed Computing Environment / Remote Procedure Calls + +Based on [C706] - aka DCE/RPC 1.1 +https://pubs.opengroup.org/onlinepubs/9629399/toc.pdf + +And on [MS-RPCE] +https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/290c38b1-92fe-4229-91e6-4fc376610c15 + +.. note:: + Please read the documentation over + `DCE/RPC `_ +""" + +import collections +import importlib +import inspect +import struct + +from enum import IntEnum +from functools import partial +from uuid import UUID + +from scapy.base_classes import Packet_metaclass + +from scapy.config import conf +from scapy.compat import bytes_encode, plain_str +from scapy.error import log_runtime +from scapy.layers.dns import DNSStrField +from scapy.layers.ntlm import ( + NTLM_Header, + NTLMSSP_MESSAGE_SIGNATURE, +) +from scapy.packet import ( + Packet, + Raw, + bind_bottom_up, + bind_layers, + bind_top_down, + NoPayload, +) +from scapy.fields import ( + _FieldContainer, + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + EnumField, + Field, + FieldLenField, + FieldListField, + FlagsField, + IntField, + LEIntEnumField, + LEIntField, + LELongField, + LEShortEnumField, + LEShortField, + LenField, + MultipleTypeField, + PacketField, + PacketLenField, + PacketListField, + PadField, + ReversePadField, + ShortEnumField, + ShortField, + SignedByteField, + StrField, + StrFixedLenField, + StrLenField, + StrLenFieldUtf16, + StrNullField, + StrNullFieldUtf16, + TrailerField, + UUIDEnumField, + UUIDField, + XByteField, + XLEIntField, + XLELongField, + XLEShortField, + XShortField, + XStrFixedLenField, +) +from scapy.sessions import DefaultSession +from scapy.supersocket import StreamSocket + +from scapy.layers.kerberos import ( + KRB_InnerToken, + Kerberos, +) +from scapy.layers.gssapi import ( + GSSAPI_BLOB, + GSSAPI_BLOB_SIGNATURE, + GSS_S_COMPLETE, + GSS_S_FLAGS, + GSS_C_FLAGS, + SSP, +) +from scapy.layers.inet import TCP + +from scapy.contrib.rtps.common_types import ( + EField, + EPacket, + EPacketField, + EPacketListField, +) + +# Typing imports +from typing import ( + Optional, + Union, +) + +# the alignment of auth_pad +# This is 4 in [C706] 13.2.6.1 but was updated to 16 in [MS-RPCE] 2.2.2.11 +_COMMON_AUTH_PAD = 16 +# the alignment of the NDR Type 1 serialization private header +# ([MS-RPCE] sect 2.2.6.2) +_TYPE1_S_PAD = 8 + +# DCE/RPC Packet +DCE_RPC_TYPE = { + 0: "request", + 1: "ping", + 2: "response", + 3: "fault", + 4: "working", + 5: "no_call", + 6: "reject", + 7: "acknowledge", + 8: "connectionless_cancel", + 9: "frag_ack", + 10: "cancel_ack", + 11: "bind", + 12: "bind_ack", + 13: "bind_nak", + 14: "alter_context", + 15: "alter_context_resp", + 16: "auth3", + 17: "shutdown", + 18: "co_cancel", + 19: "orphaned", +} +_DCE_RPC_4_FLAGS1 = [ + "reserved_01", + "last_frag", + "frag", + "no_frag_ack", + "maybe", + "idempotent", + "broadcast", + "reserved_7", +] +_DCE_RPC_4_FLAGS2 = [ + "reserved_0", + "cancel_pending", + "reserved_2", + "reserved_3", + "reserved_4", + "reserved_5", + "reserved_6", + "reserved_7", +] +DCE_RPC_TRANSFER_SYNTAXES = { + UUID("00000000-0000-0000-0000-000000000000"): "NULL", + UUID("6cb71c2c-9812-4540-0300-000000000000"): "Bind Time Feature Negotiation", + UUID("8a885d04-1ceb-11c9-9fe8-08002b104860"): "NDR 2.0", + UUID("71710533-beba-4937-8319-b5dbef9ccc36"): "NDR64", +} +DCE_RPC_INTERFACES_NAMES = {} +DCE_RPC_INTERFACES_NAMES_rev = {} +COM_INTERFACES_NAMES = {} +COM_INTERFACES_NAMES_rev = {} + + +class DCERPC_Transport(IntEnum): + """ + Protocols identifiers currently supported by Scapy + """ + + NCACN_IP_TCP = 0x07 + NCACN_NP = 0x0F + # TODO: add more.. if people use them? + + +# [C706] Appendix I with names from Appendix B +DCE_RPC_PROTOCOL_IDENTIFIERS = { + 0x0: "OSI OID", # Special + 0x0D: "UUID", # Special + # Transports + # 0x2: "DNA Session Control", + # 0x3: "DNA Session Control V3", + # 0x4: "DNA NSP Transport", + # 0x5: "OSI TP4", + 0x06: "NCADG_OSI_CLSN", # [C706] + 0x07: "NCACN_IP_TCP", # [C706] + 0x08: "NCADG_IP_UDP", # [C706] + 0x09: "IP", # [C706] + 0x0A: "RPC connectionless protocol", # [C706] + 0x0B: "RPC connection-oriented protocol", # [C706] + 0x0C: "NCALRPC", + 0x0F: "NCACN_NP", # [MS-RPCE] + 0x11: "NCACN_NB", # [C706] + 0x12: "NCACN_NB_NB", # [MS-RPCE] + 0x13: "NCACN_SPX", # [C706] + 0x14: "NCADG_IPX", # [C706] + 0x16: "NCACN_AT_DSP", # [C706] + 0x17: "NCADG_AT_DSP", # [C706] + 0x19: "NCADG_NB", # [C706] + 0x1A: "NCACN_VNS_SPP", # [C706] + 0x1B: "NCADG_VNS_IPC", # [C706] + 0x1F: "NCACN_HTTP", # [MS-RPCE] +} + + +def _dce_rpc_endianness(pkt): + """ + Determine the right endianness sign for a given DCE/RPC packet + """ + if pkt.endian == 0: # big endian + return ">" + elif pkt.endian == 1: # little endian + return "<" + else: + return "!" + + +class _EField(EField): + def __init__(self, fld): + super(_EField, self).__init__(fld, endianness_from=_dce_rpc_endianness) + + +class DceRpc(Packet): + """DCE/RPC packet""" + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 1: + ver = ord(_pkt[0:1]) + if ver == 4: + return DceRpc4 + elif ver == 5: + return DceRpc5 + return DceRpc5 + + @classmethod + def tcp_reassemble(cls, data, metadata, session): + if data[0:1] == b"\x05": + return DceRpc5.tcp_reassemble(data, metadata, session) + return DceRpc(data) + + +bind_bottom_up(TCP, DceRpc, sport=135) +bind_layers(TCP, DceRpc, dport=135) + + +class _DceRpcPayload(Packet): + @property + def endianness(self): + if not self.underlayer: + return "!" + return _dce_rpc_endianness(self.underlayer) + + +# sect 12.5 + +_drep = [ + BitEnumField("endian", 1, 4, ["big", "little"]), + BitEnumField("encoding", 0, 4, ["ASCII", "EBCDIC"]), + ByteEnumField("float", 0, ["IEEE", "VAX", "CRAY", "IBM"]), + ByteField("reserved1", 0), +] + + +class DceRpc4(DceRpc): + """ + DCE/RPC v4 'connection-less' packet + """ + + name = "DCE/RPC v4" + fields_desc = ( + [ + ByteEnumField( + "rpc_vers", 4, {4: "4 (connection-less)", 5: "5 (connection-oriented)"} + ), + ByteEnumField("ptype", 0, DCE_RPC_TYPE), + FlagsField("flags1", 0, 8, _DCE_RPC_4_FLAGS1), + FlagsField("flags2", 0, 8, _DCE_RPC_4_FLAGS2), + ] + + _drep + + [ + XByteField("serial_hi", 0), + _EField(UUIDField("object", None)), + _EField(UUIDField("if_id", None)), + _EField(UUIDField("act_id", None)), + _EField(IntField("server_boot", 0)), + _EField(IntField("if_vers", 1)), + _EField(IntField("seqnum", 0)), + _EField(ShortField("opnum", 0)), + _EField(XShortField("ihint", 0xFFFF)), + _EField(XShortField("ahint", 0xFFFF)), + _EField(LenField("len", None, fmt="H")), + _EField(ShortField("fragnum", 0)), + ByteEnumField("auth_proto", 0, ["none", "OSF DCE Private Key"]), + XByteField("serial_lo", 0), + ] + ) + + +# Exceptionally, we define those 3 here. + + +class NL_AUTH_MESSAGE(Packet): + # [MS-NRPC] sect 2.2.1.3.1 + name = "NL_AUTH_MESSAGE" + fields_desc = [ + LEIntEnumField( + "MessageType", + 0x00000000, + { + 0x00000000: "Request", + 0x00000001: "Response", + }, + ), + FlagsField( + "Flags", + 0, + -32, + [ + "NETBIOS_DOMAIN_NAME", + "NETBIOS_COMPUTER_NAME", + "DNS_DOMAIN_NAME", + "DNS_HOST_NAME", + "NETBIOS_COMPUTER_NAME_UTF8", + ], + ), + ConditionalField( + StrNullField("NetbiosDomainName", ""), + lambda pkt: pkt.Flags.NETBIOS_DOMAIN_NAME, + ), + ConditionalField( + StrNullField("NetbiosComputerName", ""), + lambda pkt: pkt.Flags.NETBIOS_COMPUTER_NAME, + ), + ConditionalField( + DNSStrField("DnsDomainName", ""), + lambda pkt: pkt.Flags.DNS_DOMAIN_NAME, + ), + ConditionalField( + DNSStrField("DnsHostName", ""), + lambda pkt: pkt.Flags.DNS_HOST_NAME, + ), + ConditionalField( + # What the fuck? Why are they doing this + # The spec is just wrong + DNSStrField("NetbiosComputerNameUtf8", ""), + lambda pkt: pkt.Flags.NETBIOS_COMPUTER_NAME_UTF8, + ), + ] + + +class NL_AUTH_SIGNATURE(Packet): + # [MS-NRPC] sect 2.2.1.3.2/2.2.1.3.3 + name = "NL_AUTH_(SHA2_)SIGNATURE" + fields_desc = [ + LEShortEnumField( + "SignatureAlgorithm", + 0x0077, + { + 0x0077: "HMAC-MD5", + 0x0013: "HMAC-SHA256", + }, + ), + LEShortEnumField( + "SealAlgorithm", + 0xFFFF, + { + 0xFFFF: "Unencrypted", + 0x007A: "RC4", + 0x001A: "AES-128", + }, + ), + XLEShortField("Pad", 0xFFFF), + ShortField("Flags", 0), + XStrFixedLenField("SequenceNumber", b"", length=8), + XStrFixedLenField("Checksum", b"", length=8), + ConditionalField( + XStrFixedLenField("Confounder", b"", length=8), + lambda pkt: pkt.SealAlgorithm != 0xFFFF, + ), + MultipleTypeField( + [ + ( + StrFixedLenField("Reserved2", b"", length=24), + lambda pkt: pkt.SignatureAlgorithm == 0x0013, + ), + ], + StrField("Reserved2", b""), + ), + ] + + +# [MS-RPCE] sect 2.2.1.1.7 +# https://learn.microsoft.com/en-us/windows/win32/rpc/authentication-service-constants +# rpcdce.h + + +class RPC_C_AUTHN(IntEnum): + NONE = 0x00 + DCE_PRIVATE = 0x01 + DCE_PUBLIC = 0x02 + DEC_PUBLIC = 0x04 + GSS_NEGOTIATE = 0x09 + WINNT = 0x0A + GSS_SCHANNEL = 0x0E + GSS_KERBEROS = 0x10 + DPA = 0x11 + MSN = 0x12 + KERNEL = 0x14 + DIGEST = 0x15 + NEGO_EXTENDED = 0x1E + PKU2U = 0x1F + LIVE_SSP = 0x20 + LIVEXP_SSP = 0x23 + CLOUD_AP = 0x24 + NETLOGON = 0x44 + MSONLINE = 0x52 + MQ = 0x64 + DEFAULT = 0xFFFFFFFF + + +class RPC_C_AUTHN_LEVEL(IntEnum): + DEFAULT = 0x0 + NONE = 0x1 + CONNECT = 0x2 + CALL = 0x3 + PKT = 0x4 + PKT_INTEGRITY = 0x5 + PKT_PRIVACY = 0x6 + + +DCE_C_AUTHN_LEVEL = RPC_C_AUTHN_LEVEL # C706 name + + +class RPC_C_IMP_LEVEL(IntEnum): + DEFAULT = 0x0 + ANONYMOUS = 0x1 + IDENTIFY = 0x2 + IMPERSONATE = 0x3 + DELEGATE = 0x4 + + +# C706 sect 13.2.6.1 + + +class CommonAuthVerifier(Packet): + name = "Common Authentication Verifier" + fields_desc = [ + ByteEnumField( + "auth_type", + 0, + RPC_C_AUTHN, + ), + ByteEnumField("auth_level", 0, RPC_C_AUTHN_LEVEL), + ByteField("auth_pad_length", None), + ByteField("auth_reserved", 0), + XLEIntField("auth_context_id", 0), + MultipleTypeField( + [ + # SPNEGO + ( + PacketLenField( + "auth_value", + GSSAPI_BLOB(), + GSSAPI_BLOB, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type == 0x09 and pkt.parent and + # Bind/Alter + pkt.parent.ptype in [11, 12, 13, 14, 15, 16], + ), + ( + PacketLenField( + "auth_value", + GSSAPI_BLOB_SIGNATURE(), + GSSAPI_BLOB_SIGNATURE, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type == 0x09 + and pkt.parent + and ( + # Other + not pkt.parent + or pkt.parent.ptype not in [11, 12, 13, 14, 15, 16] + ), + ), + # Kerberos + ( + PacketLenField( + "auth_value", + Kerberos(), + Kerberos, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type == 0x10 and pkt.parent and + # Bind/Alter + pkt.parent.ptype in [11, 12, 13, 14, 15, 16], + ), + ( + PacketLenField( + "auth_value", + KRB_InnerToken(), + KRB_InnerToken, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type == 0x10 + and pkt.parent + and ( + # Other + not pkt.parent + or pkt.parent.ptype not in [11, 12, 13, 14, 15, 16] + ), + ), + # NTLM + ( + PacketLenField( + "auth_value", + NTLM_Header(), + NTLM_Header, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type in [0x0A, 0xFF] and pkt.parent and + # Bind/Alter + pkt.parent.ptype in [11, 12, 13, 14, 15, 16], + ), + ( + PacketLenField( + "auth_value", + NTLMSSP_MESSAGE_SIGNATURE(), + NTLMSSP_MESSAGE_SIGNATURE, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type in [0x0A, 0xFF] + and pkt.parent + and ( + # Other + not pkt.parent + or pkt.parent.ptype not in [11, 12, 13, 14, 15, 16] + ), + ), + # NetLogon + ( + PacketLenField( + "auth_value", + NL_AUTH_MESSAGE(), + NL_AUTH_MESSAGE, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type == 0x44 and pkt.parent and + # Bind/Alter + pkt.parent.ptype in [11, 12, 13, 14, 15], + ), + ( + PacketLenField( + "auth_value", + NL_AUTH_SIGNATURE(), + NL_AUTH_SIGNATURE, + length_from=lambda pkt: pkt.parent.auth_len, + ), + lambda pkt: pkt.auth_type == 0x44 + and ( + # Other + not pkt.parent + or pkt.parent.ptype not in [11, 12, 13, 14, 15] + ), + ), + ], + PacketLenField( + "auth_value", + None, + conf.raw_layer, + length_from=lambda pkt: pkt.parent and pkt.parent.auth_len or 0, + ), + ), + ] + + def is_protected(self): + if not self.auth_value: + return False + if self.parent and self.parent.ptype in [11, 12, 13, 14, 15, 16]: + return False + return True + + def is_ssp(self): + if not self.auth_value: + return False + if self.parent and self.parent.ptype not in [11, 12, 13, 14, 15, 16]: + return False + return True + + def default_payload_class(self, pkt): + return conf.padding_layer + + +# [MS-RPCE] sect 2.2.2.13 - Verification Trailer +_SECTRAILER_MAGIC = b"\x8a\xe3\x13\x71\x02\xf4\x36\x71" + + +class DceRpcSecVTCommand(Packet): + name = "Verification trailer command" + fields_desc = [ + BitField("SEC_VT_MUST_PROCESS_COMMAND", 0, 1, tot_size=-2), + BitField("SEC_VT_COMMAND_END", 0, 1), + BitEnumField( + "Command", + 0, + -14, + { + 0x0001: "SEC_VT_COMMAND_BITMASK_1", + 0x0002: "SEC_VT_COMMAND_PCONTEXT", + 0x0003: "SEC_VT_COMMAND_HEADER2", + }, + end_tot_size=-2, + ), + LenField("Length", None, fmt="> 4 + if endian not in [0, 1]: + return + length = struct.unpack(("<" if endian else ">") + "H", data[8:10])[0] + if len(data) >= length: + if conf.dcerpc_session_enable: + # If DCE/RPC sessions are enabled, use them ! + if "dcerpcsess" not in session: + session["dcerpcsess"] = dcerpcsess = DceRpcSession() + else: + dcerpcsess = session["dcerpcsess"] + return dcerpcsess.process(DceRpc5(data)) + return DceRpc5(data) + + +# sec 12.6.3.1 + + +class DceRpc5AbstractSyntax(EPacket): + name = "Presentation Syntax (p_syntax_id_t)" + fields_desc = [ + _EField( + UUIDEnumField( + "if_uuid", + None, + ( + # Those are dynamic + DCE_RPC_INTERFACES_NAMES.get, + lambda x: DCE_RPC_INTERFACES_NAMES_rev.get(x.lower()), + ), + ) + ), + _EField(IntField("if_version", 3)), + ] + + +class DceRpc5TransferSyntax(EPacket): + name = "Presentation Transfer Syntax (p_syntax_id_t)" + fields_desc = [ + _EField( + UUIDEnumField( + "if_uuid", + None, + DCE_RPC_TRANSFER_SYNTAXES, + ) + ), + _EField(IntField("if_version", 3)), + ] + + +class DceRpc5Context(EPacket): + name = "Presentation Context (p_cont_elem_t)" + fields_desc = [ + _EField(ShortField("cont_id", 0)), + FieldLenField("n_transfer_syn", None, count_of="transfer_syntaxes", fmt="B"), + ByteField("reserved", 0), + EPacketField("abstract_syntax", None, DceRpc5AbstractSyntax), + EPacketListField( + "transfer_syntaxes", + None, + DceRpc5TransferSyntax, + count_from=lambda pkt: pkt.n_transfer_syn, + endianness_from=_dce_rpc_endianness, + ), + ] + + +class DceRpc5Result(EPacket): + name = "Context negotiation Result" + fields_desc = [ + _EField( + ShortEnumField( + "result", 0, ["acceptance", "user_rejection", "provider_rejection"] + ) + ), + _EField( + ShortEnumField( + "reason", + 0, + _DCE_RPC_REJECTION_REASONS, + ) + ), + EPacketField("transfer_syntax", None, DceRpc5TransferSyntax), + ] + + +class DceRpc5PortAny(EPacket): + name = "Port Any (port_any_t)" + fields_desc = [ + _EField(FieldLenField("length", None, length_of="port_spec", fmt="H")), + _EField(StrLenField("port_spec", b"", length_from=lambda pkt: pkt.length)), + ] + + +# sec 12.6.4.3 + + +class DceRpc5Bind(_DceRpcPayload): + name = "DCE/RPC v5 - Bind" + fields_desc = [ + _EField(ShortField("max_xmit_frag", 5840)), + _EField(ShortField("max_recv_frag", 8192)), + _EField(IntField("assoc_group_id", 0)), + # p_cont_list_t + _EField( + FieldLenField("n_context_elem", None, count_of="context_elem", fmt="B") + ), + StrFixedLenField("reserved", 0, length=3), + EPacketListField( + "context_elem", + [], + DceRpc5Context, + endianness_from=_dce_rpc_endianness, + count_from=lambda pkt: pkt.n_context_elem, + ), + ] + + +bind_layers(DceRpc5, DceRpc5Bind, ptype=11) + +# sec 12.6.4.4 + + +class DceRpc5BindAck(_DceRpcPayload): + name = "DCE/RPC v5 - Bind Ack" + fields_desc = [ + _EField(ShortField("max_xmit_frag", 5840)), + _EField(ShortField("max_recv_frag", 8192)), + _EField(IntField("assoc_group_id", 0)), + PadField( + EPacketField("sec_addr", None, DceRpc5PortAny), + align=4, + ), + # p_result_list_t + _EField(FieldLenField("n_results", None, count_of="results", fmt="B")), + StrFixedLenField("reserved", 0, length=3), + EPacketListField( + "results", + [], + DceRpc5Result, + endianness_from=_dce_rpc_endianness, + count_from=lambda pkt: pkt.n_results, + ), + ] + + +bind_layers(DceRpc5, DceRpc5BindAck, ptype=12) + +# sec 12.6.4.5 + + +class DceRpc5Version(EPacket): + name = "version_t" + fields_desc = [ + ByteField("major", 0), + ByteField("minor", 0), + ] + + +class DceRpc5BindNak(_DceRpcPayload): + name = "DCE/RPC v5 - Bind Nak" + fields_desc = [ + _EField( + ShortEnumField("provider_reject_reason", 0, _DCE_RPC_REJECTION_REASONS) + ), + # p_rt_versions_supported_t + _EField(FieldLenField("n_protocols", None, count_of="protocols", fmt="B")), + EPacketListField( + "protocols", + [], + DceRpc5Version, + count_from=lambda pkt: pkt.n_protocols, + endianness_from=_dce_rpc_endianness, + ), + # [MS-RPCE] sect 2.2.2.9 + ConditionalField( + ReversePadField( + _EField( + UUIDEnumField( + "signature", + None, + { + UUID( + "90740320-fad0-11d3-82d7-009027b130ab" + ): "Extended Error", + }, + ) + ), + align=8, + ), + lambda pkt: pkt.fields.get("signature", None) + or ( + pkt.underlayer + and pkt.underlayer.frag_len >= 24 + pkt.n_protocols * 2 + 16 + ), + ), + ] + + +bind_layers(DceRpc5, DceRpc5BindNak, ptype=13) + + +# sec 12.6.4.1 + + +class DceRpc5AlterContext(_DceRpcPayload): + name = "DCE/RPC v5 - AlterContext" + fields_desc = DceRpc5Bind.fields_desc + + +bind_layers(DceRpc5, DceRpc5AlterContext, ptype=14) + + +# sec 12.6.4.2 + + +class DceRpc5AlterContextResp(_DceRpcPayload): + name = "DCE/RPC v5 - AlterContextResp" + fields_desc = DceRpc5BindAck.fields_desc + + +bind_layers(DceRpc5, DceRpc5AlterContextResp, ptype=15) + +# [MS-RPCE] sect 2.2.2.10 - rpc_auth_3 + + +class DceRpc5Auth3(Packet): + name = "DCE/RPC v5 - Auth3" + fields_desc = [StrFixedLenField("pad", b"", length=4)] + + +bind_layers(DceRpc5, DceRpc5Auth3, ptype=16) + +# sec 12.6.4.7 + + +class DceRpc5Fault(_DceRpcPayload): + name = "DCE/RPC v5 - Fault" + fields_desc = [ + _EField(IntField("alloc_hint", 0)), + _EField(ShortField("cont_id", 0)), + ByteField("cancel_count", 0), + FlagsField("reserved", 0, -8, {0x1: "RPC extended error"}), + _EField(LEIntEnumField("status", 0, _DCE_RPC_ERROR_CODES)), + IntField("reserved2", 0), + ] + + +bind_layers(DceRpc5, DceRpc5Fault, ptype=3) + + +# sec 12.6.4.9 + + +class DceRpc5Request(_DceRpcPayload): + name = "DCE/RPC v5 - Request" + fields_desc = [ + _EField(IntField("alloc_hint", 0)), + _EField(ShortField("cont_id", 0)), + _EField(ShortField("opnum", 0)), + ConditionalField( + PadField( + _EField(UUIDField("object", None)), + align=8, + ), + lambda pkt: pkt.underlayer and pkt.underlayer.pfc_flags.PFC_OBJECT_UUID, + ), + ] + + +bind_layers(DceRpc5, DceRpc5Request, ptype=0) + +# sec 12.6.4.10 + + +class DceRpc5Response(_DceRpcPayload): + name = "DCE/RPC v5 - Response" + fields_desc = [ + _EField(IntField("alloc_hint", 0)), + _EField(ShortField("cont_id", 0)), + ByteField("cancel_count", 0), + ByteField("reserved", 0), + ] + + +bind_layers(DceRpc5, DceRpc5Response, ptype=2) + +# --- API + +DceRpcOp = collections.namedtuple("DceRpcOp", ["request", "response"]) +DCE_RPC_INTERFACES = {} + + +class DceRpcInterface: + def __init__(self, name, uuid, version_tuple, if_version, opnums): + self.name = name + self.uuid = uuid + self.major_version, self.minor_version = version_tuple + self.if_version = if_version + self.opnums = opnums + + def __repr__(self): + return "" % ( + self.name, + self.major_version, + self.minor_version, + ) + + +def register_dcerpc_interface(name, uuid, version, opnums): + """ + Register a DCE/RPC interface + """ + version_tuple = tuple(map(int, version.split("."))) + assert len(version_tuple) == 2, "Version should be in format 'X.X' !" + if_version = (version_tuple[1] << 16) + version_tuple[0] + if (uuid, if_version) in DCE_RPC_INTERFACES: + # Interface is already registered. + interface = DCE_RPC_INTERFACES[(uuid, if_version)] + if interface.name == name: + if set(opnums) - set(interface.opnums): + # Interface is an extension of a previous interface + interface.opnums.update(opnums) + else: + log_runtime.warning( + "This interface is already registered: %s. Skip" % interface + ) + return + else: + raise ValueError( + "An interface with the same UUID is already registered: %s" % interface + ) + else: + # New interface + DCE_RPC_INTERFACES_NAMES[uuid] = name + DCE_RPC_INTERFACES_NAMES_rev[name.lower()] = uuid + DCE_RPC_INTERFACES[(uuid, if_version)] = DceRpcInterface( + name, + uuid, + version_tuple, + if_version, + opnums, + ) + + # bind for build + for opnum, operations in opnums.items(): + bind_top_down(DceRpc5Request, operations.request, opnum=opnum) + operations.request.opnum = opnum + operations.request.intf = uuid + + +def find_dcerpc_interface(name) -> DceRpcInterface: + """ + Find an interface object through the name in the IDL + """ + try: + return next(x for x in DCE_RPC_INTERFACES.values() if x.name == name) + except StopIteration: + raise AttributeError("Unknown interface !") + + +COM_INTERFACES = {} + + +class ComInterface: + if_version = 0 + + def __init__(self, name, uuid, opnums): + self.name = name + self.uuid = uuid + self.opnums = opnums + + def __repr__(self): + return "" % (self.name,) + + +def register_com_interface(name, uuid, opnums): + """ + Register a COM interface + """ + COM_INTERFACES[uuid] = ComInterface( + name, + uuid, + opnums, + ) + # bind for build + for opnum, operations in opnums.items(): + bind_top_down(DceRpc5Request, operations.request, opnum=opnum) + COM_INTERFACES_NAMES[uuid] = name + COM_INTERFACES_NAMES_rev[name.lower()] = uuid + + +def find_com_interface(name) -> ComInterface: + """ + Find an interface object through the name in the IDL + """ + try: + return next(x for x in COM_INTERFACES.values() if x.name == name) + except StopIteration: + raise AttributeError("Unknown interface !") + + +# --- NDR fields - [C706] chap 14 + + +def _set_ctx_on(f, obj): + if isinstance(f, _NDRPacket): + f.ndr64 = obj.ndr64 + f.ndrendian = obj.ndrendian + if isinstance(f, list): + for x in f: + if isinstance(x, _NDRPacket): + x.ndr64 = obj.ndr64 + x.ndrendian = obj.ndrendian + + +def _e(ndrendian): + return {"big": ">", "little": "<"}[ndrendian] + + +class _NDRPacket(Packet): + __slots__ = ["ndr64", "ndrendian", "deferred_pointers", "request_packet"] + + def __init__(self, *args, **kwargs): + self.ndr64 = kwargs.pop("ndr64", conf.ndr64) + self.ndrendian = kwargs.pop("ndrendian", "little") + # request_packet is used in the session, so that a response packet + # can resolve union arms if the case parameter is in the request. + self.request_packet = kwargs.pop("request_packet", None) + self.deferred_pointers = [] + super(_NDRPacket, self).__init__(*args, **kwargs) + + def do_dissect(self, s): + _up = self.parent or self.underlayer + if _up and isinstance(_up, _NDRPacket): + self.ndr64 = _up.ndr64 + self.ndrendian = _up.ndrendian + else: + # See comment above NDRConstructedType + return NDRConstructedType([]).read_deferred_pointers( + self, super(_NDRPacket, self).do_dissect(s) + ) + return super(_NDRPacket, self).do_dissect(s) + + def post_dissect(self, s): + if self.deferred_pointers: + # Can't trust the cache if there were deferred pointers + self.raw_packet_cache = None + return s + + def do_build(self): + _up = self.parent or self.underlayer + for f in self.fields.values(): + _set_ctx_on(f, self) + if not _up or not isinstance(_up, _NDRPacket): + # See comment above NDRConstructedType + return NDRConstructedType([]).add_deferred_pointers( + self, super(_NDRPacket, self).do_build() + ) + return super(_NDRPacket, self).do_build() + + def default_payload_class(self, pkt): + return conf.padding_layer + + def clone_with(self, *args, **kwargs): + pkt = super(_NDRPacket, self).clone_with(*args, **kwargs) + # We need to copy deferred_pointers to not break pointer deferral + # on build. + pkt.deferred_pointers = self.deferred_pointers + pkt.ndr64 = self.ndr64 + pkt.ndrendian = self.ndrendian + return pkt + + def copy(self): + pkt = super(_NDRPacket, self).copy() + pkt.deferred_pointers = self.deferred_pointers + pkt.ndr64 = self.ndr64 + pkt.ndrendian = self.ndrendian + return pkt + + def show2(self, dump=False, indent=3, lvl="", label_lvl=""): + return self.__class__( + bytes(self), ndr64=self.ndr64, ndrendian=self.ndrendian + ).show(dump, indent, lvl, label_lvl) + + def getfield_and_val(self, attr): + try: + return Packet.getfield_and_val(self, attr) + except ValueError: + if self.request_packet: + # Try to resolve the field from the request on failure + try: + return self.request_packet.getfield_and_val(attr) + except AttributeError: + pass + raise + + def valueof(self, request: str): + """ + Util to get the value of a NDRField, ignoring arrays, pointers, etc. + """ + val = self + for ndr_field in request.split("."): + fld, fval = val.getfield_and_val(ndr_field) + val = fld.valueof(val, fval) + return val + + +class _NDRAlign: + def padlen(self, flen, pkt): + return -flen % self._align[pkt.ndr64] + + def original_length(self, pkt): + # Find the length of the NDR frag to be able to pad properly + while pkt: + par = pkt.parent or pkt.underlayer + if par and isinstance(par, _NDRPacket): + pkt = par + else: + break + return len(pkt.original) + + +class NDRAlign(_NDRAlign, ReversePadField): + """ + ReversePadField modified to fit NDR. + + - If no align size is specified, use the one from the inner field + - Size is calculated from the beginning of the NDR stream + """ + + def __init__(self, fld, align, padwith=None): + super(NDRAlign, self).__init__(fld, align=align, padwith=padwith) + + +class _VirtualField(Field): + # Hold a value but doesn't show up when building/dissecting + def addfield(self, pkt, s, x): + return s + + def getfield(self, pkt, s): + return s, None + + +class _NDRPacketMetaclass(Packet_metaclass): + def __new__(cls, name, bases, dct): + newcls = super(_NDRPacketMetaclass, cls).__new__(cls, name, bases, dct) + conformants = dct.get("DEPORTED_CONFORMANTS", []) + if conformants: + amount = len(conformants) + if amount == 1: + newcls.fields_desc.insert( + 0, + _VirtualField("max_count", None), + ) + else: + newcls.fields_desc.insert( + 0, + FieldListField( + "max_counts", + [], + _VirtualField("", 0), + count_from=lambda _: amount, + ), + ) + return newcls # type: ignore + + +class NDRPacket(_NDRPacket, metaclass=_NDRPacketMetaclass): + """ + A NDR Packet. Handles pointer size & endianness + """ + + __slots__ = ["_align"] + + # NDR64 pad structures + # [MS-RPCE] 2.2.5.3.4.1 + ALIGNMENT = (1, 1) + # [C706] sect 14.3.7 - Conformants max_count can be added to the beginning + DEPORTED_CONFORMANTS = [] + + +# Primitive types + + +class _NDRValueOf: + def valueof(self, pkt, x): + return x + + +class _NDRLenField(_NDRValueOf, Field): + """ + Field similar to FieldLenField that takes size_of and adjust as arguments, + and take the value of a size on build. + """ + + __slots__ = ["size_of", "adjust"] + + def __init__(self, *args, **kwargs): + self.size_of = kwargs.pop("size_of", None) + self.adjust = kwargs.pop("adjust", lambda _, x: x) + super(_NDRLenField, self).__init__(*args, **kwargs) + + def i2m(self, pkt, x): + if x is None and pkt is not None and self.size_of is not None: + fld, fval = pkt.getfield_and_val(self.size_of) + f = fld.i2len(pkt, fval) + x = self.adjust(pkt, f) + elif x is None: + x = 0 + return x + + +class NDRByteField(_NDRLenField, ByteField): + pass + + +class NDRSignedByteField(_NDRLenField, SignedByteField): + pass + + +class _NDRField(_NDRLenField): + FMT = "" + ALIGN = (0, 0) + + def getfield(self, pkt, s): + return NDRAlign( + Field("", 0, fmt=_e(pkt.ndrendian) + self.FMT), align=self.ALIGN + ).getfield(pkt, s) + + def addfield(self, pkt, s, val): + return NDRAlign( + Field("", 0, fmt=_e(pkt.ndrendian) + self.FMT), align=self.ALIGN + ).addfield(pkt, s, self.i2m(pkt, val)) + + +class NDRShortField(_NDRField): + FMT = "H" + ALIGN = (2, 2) + + +class NDRSignedShortField(_NDRField): + FMT = "h" + ALIGN = (2, 2) + + +class NDRIntField(_NDRField): + FMT = "I" + ALIGN = (4, 4) + + +class NDRSignedIntField(_NDRField): + FMT = "i" + ALIGN = (4, 4) + + +class NDRLongField(_NDRField): + FMT = "Q" + ALIGN = (8, 8) + + +class NDRSignedLongField(_NDRField): + FMT = "q" + ALIGN = (8, 8) + + +class NDRIEEEFloatField(_NDRField): + FMT = "f" + ALIGN = (4, 4) + + +class NDRIEEEDoubleField(_NDRField): + FMT = "d" + ALIGN = (8, 8) + + +# Enum types + + +class _NDREnumField(_NDRValueOf, EnumField): + # [MS-RPCE] sect 2.2.5.2 - Enums are 4 octets in NDR64 + FMTS = ["H", "I"] + + def getfield(self, pkt, s): + fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] + return NDRAlign(Field("", 0, fmt=fmt), align=(2, 4)).getfield(pkt, s) + + def addfield(self, pkt, s, val): + fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] + return NDRAlign(Field("", 0, fmt=fmt), align=(2, 4)).addfield( + pkt, s, self.i2m(pkt, val) + ) + + +class NDRInt3264EnumField(NDRAlign): + def __init__(self, *args, **kwargs): + super(NDRInt3264EnumField, self).__init__( + _NDREnumField(*args, **kwargs), align=(2, 4) + ) + + +class NDRIntEnumField(_NDRValueOf, NDRAlign): + # v1_enum are always 4-octets, even in NDR32 + def __init__(self, *args, **kwargs): + super(NDRIntEnumField, self).__init__( + LEIntEnumField(*args, **kwargs), align=(4, 4) + ) + + +# Special types + + +class NDRInt3264Field(_NDRLenField): + FMTS = ["I", "Q"] + + def getfield(self, pkt, s): + fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] + return NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield(pkt, s) + + def addfield(self, pkt, s, val): + fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] + return NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield( + pkt, s, self.i2m(pkt, val) + ) + + +class NDRSignedInt3264Field(NDRInt3264Field): + FMTS = ["i", "q"] + + +# Pointer types + + +class NDRPointer(_NDRPacket): + fields_desc = [ + MultipleTypeField( + [(XLELongField("referent_id", 1), lambda pkt: pkt and pkt.ndr64)], + XLEIntField("referent_id", 1), + ), + PacketField("value", None, conf.raw_layer), + ] + + +class NDRFullPointerField(_FieldContainer): + """ + A NDR Full/Unique pointer field encapsulation. + + :param EMBEDDED: This pointer is embedded. This means that it's representation + will not appear after the pointer (pointer deferral applies). + See [C706] 14.3.12.3 - Algorithm for Deferral of Referents + """ + + EMBEDDED = False + EMBEDDED_REF = False + + def __init__(self, fld, ref=False, fmt="I"): + self.fld = fld + self.ref = ref + self.default = None + + def getfield(self, pkt, s): + fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] + remain, referent_id = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield( + pkt, s + ) + + # No value + if referent_id == 0 and not self.EMBEDDED_REF: + return remain, None + + # With value + if self.EMBEDDED: + # deferred + ptr = NDRPointer( + ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, referent_id=referent_id + ) + pkt.deferred_pointers.append((ptr, partial(self.fld.getfield, pkt))) + return remain, ptr + + remain, val = self.fld.getfield(pkt, remain) + return remain, NDRPointer( + ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, referent_id=referent_id, value=val + ) + + def addfield(self, pkt, s, val): + if val is not None and not isinstance(val, NDRPointer): + raise ValueError( + "Expected NDRPointer in %s. You are using it wrong!" % self.name + ) + fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] + fld = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)) + + # No value + if val is None and not self.EMBEDDED_REF: + return fld.addfield(pkt, s, 0) + + # With value + _set_ctx_on(val.value, pkt) + s = fld.addfield(pkt, s, val.referent_id) + if self.EMBEDDED: + # deferred + pkt.deferred_pointers.append( + ((lambda s: self.fld.addfield(pkt, s, val.value)), val) + ) + return s + + return self.fld.addfield(pkt, s, val.value) + + def any2i(self, pkt, x): + # User-friendly helper + if x is not None and not isinstance(x, NDRPointer): + return NDRPointer( + referent_id=0x20000, + value=self.fld.any2i(pkt, x), + ) + return x + + # Can't use i2repr = Field.i2repr and so on on PY2 :/ + def i2repr(self, pkt, val): + return repr(val) + + def i2h(self, pkt, x): + return x + + def h2i(self, pkt, x): + return x + + def i2len(self, pkt, x): + if x is None: + return 0 + return self.fld.i2len(pkt, x.value) + + def valueof(self, pkt, x): + if x is None: + return x + return self.fld.valueof(pkt, x.value) + + +class NDRFullEmbPointerField(NDRFullPointerField): + """ + A NDR Embedded Full pointer. + + Same as NDRFullPointerField with EMBEDDED = True. + """ + + EMBEDDED = True + + +class NDRRefEmbPointerField(NDRFullPointerField): + """ + A NDR Embedded Reference pointer. + + Same as NDRFullPointerField with EMBEDDED = True and EMBEDDED_REF = True. + """ + + EMBEDDED = True + EMBEDDED_REF = True + + +# Constructed types + + +# Note: this is utterly complex and will drive you crazy + +# If you have a NDRPacket that contains a deferred pointer on the top level +# (only happens in non DCE/RPC structures, such as in MS-PAC, where you have an NDR +# structure encapsulated in a non-NDR structure), there will be left-over deferred +# pointers when exiting dissection/build (deferred pointers are only computed when +# reaching a field that extends NDRConstructedType, which is normal: if you follow +# the DCE/RPC spec, pointers are never deferred in root structures) +# Therefore there is a special case forcing the build/dissection of any leftover +# pointers in NDRPacket, if Scapy detects that they won't be handled by any parent. + +# Implementation notes: I chose to set 'handles_deferred' inside the FIELD, rather +# than inside the PACKET. This is faster to compute because whether a constructed type +# should handle deferral or not is computed only once when loading, therefore Scapy +# knows in advance whether to handle deferred pointers or not. But it is technically +# incorrect: with this approach, a structure (packet) cannot be used in 2 code paths +# that have different pointer managements. I mean by that that if there was a +# structure that was directly embedded in a RPC request without a pointer but also +# embedded with a pointer in another RPC request, it would break. +# Fortunately this isn't the case: structures are never reused for 2 purposes. +# (or at least I never seen that... ) + + +class NDRConstructedType(object): + def __init__(self, fields): + self.handles_deferred = False + self.ndr_fields = fields + self.rec_check_deferral() + + def rec_check_deferral(self): + # We iterate through the fields within this constructed type. + # If we have a pointer, mark this field as handling deferrance + # and make all sub-constructed types not. + for f in self.ndr_fields: + if isinstance(f, NDRFullPointerField) and f.EMBEDDED: + self.handles_deferred = True + if isinstance(f, NDRConstructedType): + f.rec_check_deferral() + if f.handles_deferred: + self.handles_deferred = True + f.handles_deferred = False + + def getfield(self, pkt, s): + s, fval = super(NDRConstructedType, self).getfield(pkt, s) + if isinstance(fval, _NDRPacket): + # If a sub-packet we just dissected has deferred pointers, + # pass it to parent packet to propagate. + pkt.deferred_pointers.extend(fval.deferred_pointers) + del fval.deferred_pointers[:] + if self.handles_deferred: + # This field handles deferral ! + s = self.read_deferred_pointers(pkt, s) + return s, fval + + def read_deferred_pointers(self, pkt, s): + # Now read content of the pointers that were deferred + q = collections.deque() + q.extend(pkt.deferred_pointers) + del pkt.deferred_pointers[:] + while q: + # Recursively resolve pointers that were deferred + ptr, getfld = q.popleft() + s, val = getfld(s) + ptr.value = val + if isinstance(val, _NDRPacket): + # Pointer resolves to a packet.. that may have deferred pointers? + q.extend(val.deferred_pointers) + del val.deferred_pointers[:] + return s + + def addfield(self, pkt, s, val): + try: + s = super(NDRConstructedType, self).addfield(pkt, s, val) + except Exception as ex: + try: + ex.args = ( + "While building field '%s': " % self.name + ex.args[0], + ) + ex.args[1:] + except (AttributeError, IndexError): + pass + raise ex + if isinstance(val, _NDRPacket): + # If a sub-packet we just dissected has deferred pointers, + # pass it to parent packet to propagate. + pkt.deferred_pointers.extend(val.deferred_pointers) + del val.deferred_pointers[:] + if self.handles_deferred: + # This field handles deferral ! + s = self.add_deferred_pointers(pkt, s) + return s + + def add_deferred_pointers(self, pkt, s): + # Now add content of pointers that were deferred + q = collections.deque() + q.extend(pkt.deferred_pointers) + del pkt.deferred_pointers[:] + while q: + addfld, fval = q.popleft() + s = addfld(s) + if isinstance(fval, NDRPointer) and isinstance(fval.value, _NDRPacket): + q.extend(fval.value.deferred_pointers) + del fval.value.deferred_pointers[:] + return s + + +class _NDRPacketField(_NDRValueOf, PacketField): + def m2i(self, pkt, m): + return self.cls(m, ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, _parent=pkt) + + +class _NDRPacketPadField(PadField): + # [MS-RPCE] 2.2.5.3.4.1 Structure with Trailing Gap + # Structures have extra alignment/padding in NDR64. + def padlen(self, flen, pkt): + if pkt.ndr64: + return -flen % self._align[1] + else: + return 0 + + +class NDRPacketField(NDRConstructedType, NDRAlign): + def __init__(self, name, default, pkt_cls, **kwargs): + self.DEPORTED_CONFORMANTS = pkt_cls.DEPORTED_CONFORMANTS + self.fld = _NDRPacketField(name, default, pkt_cls=pkt_cls, **kwargs) + + # The inner _NDRPacketPadField handles NDR64's trailing gap in + # the case where there a no inner conformants (see [MS-RPCE] 2.2.5.3.4.1) + if self.DEPORTED_CONFORMANTS: + innerfld = self.fld + else: + innerfld = _NDRPacketPadField(self.fld, align=pkt_cls.ALIGNMENT) + + # C706 14.3.2 Alignment of Constructed Types is handled by the + # NDRAlign below. + NDRAlign.__init__( + self, + innerfld, + align=pkt_cls.ALIGNMENT, + ) + NDRConstructedType.__init__(self, pkt_cls.fields_desc) + + def getfield(self, pkt, x): + # Handle deformed conformants max_count here + if self.DEPORTED_CONFORMANTS: + # C706 14.3.2: "In other words, the size information precedes the + # structure and is aligned independently of the structure alignment." + fld = NDRInt3264Field("", 0) + max_counts = [] + for _ in self.DEPORTED_CONFORMANTS: + x, max_count = fld.getfield(pkt, x) + max_counts.append(max_count) + res, val = super(NDRPacketField, self).getfield(pkt, x) + if len(max_counts) == 1: + val.max_count = max_counts[0] + else: + val.max_counts = max_counts + return res, val + return super(NDRPacketField, self).getfield(pkt, x) + + def addfield(self, pkt, s, x): + # Handle deformed conformants max_count here + if self.DEPORTED_CONFORMANTS: + mcfld = NDRInt3264Field("", 0) + if len(self.DEPORTED_CONFORMANTS) == 1: + max_counts = [x.max_count] + else: + max_counts = x.max_counts + for fldname, max_count in zip(self.DEPORTED_CONFORMANTS, max_counts): + if max_count is None: + fld, val = x.getfield_and_val(fldname) + max_count = fld.i2len(x, val) + s = mcfld.addfield(pkt, s, max_count) + return super(NDRPacketField, self).addfield(pkt, s, x) + return super(NDRPacketField, self).addfield(pkt, s, x) + + +# Array types + + +class _NDRPacketListField(NDRConstructedType, PacketListField): + """ + A PacketListField for NDR that can optionally pack the packets into NDRPointers + """ + + islist = 1 + holds_packets = 1 + + __slots__ = ["ptr_lvl", "fld"] + + def __init__(self, name, default, pkt_cls, **kwargs): + self.ptr_lvl = kwargs.pop("ptr_lvl", False) + if self.ptr_lvl: + # TODO: support more than 1 level ? + self.fld = NDRFullEmbPointerField(NDRPacketField("", None, pkt_cls)) + else: + self.fld = NDRPacketField("", None, pkt_cls) + PacketListField.__init__(self, name, default, pkt_cls=pkt_cls, **kwargs) + NDRConstructedType.__init__(self, [self.fld]) + + def m2i(self, pkt, s): + remain, val = self.fld.getfield(pkt, s) + if val is None: + val = NDRNone() + # A mistake here would be to use / instead of add_payload. It adds a copy + # which breaks pointer defferal. Same applies elsewhere + val.add_payload(conf.padding_layer(remain)) + return val + + def any2i(self, pkt, x): + # User-friendly helper + if isinstance(x, list): + x = [self.fld.any2i(pkt, y) for y in x] + return super(_NDRPacketListField, self).any2i(pkt, x) + + def i2m(self, pkt, val): + return self.fld.addfield(pkt, b"", val) + + def i2len(self, pkt, x): + return len(x) + + def valueof(self, pkt, x): + return [ + self.fld.valueof(pkt, y if not isinstance(y, NDRNone) else None) for y in x + ] + + +class NDRFieldListField(NDRConstructedType, FieldListField): + """ + A FieldListField for NDR + """ + + islist = 1 + + def __init__(self, *args, **kwargs): + if "length_is" in kwargs: + kwargs["count_from"] = kwargs.pop("length_is") + elif "size_is" in kwargs: + kwargs["count_from"] = kwargs.pop("size_is") + FieldListField.__init__(self, *args, **kwargs) + NDRConstructedType.__init__(self, [self.field]) + + def i2len(self, pkt, x): + return len(x) + + def valueof(self, pkt, x): + return [self.field.valueof(pkt, y) for y in x] + + +class NDRVaryingArray(_NDRPacket): + fields_desc = [ + MultipleTypeField( + [(LELongField("offset", 0), lambda pkt: pkt and pkt.ndr64)], + LEIntField("offset", 0), + ), + MultipleTypeField( + [ + ( + LELongField("actual_count", None), + lambda pkt: pkt and pkt.ndr64, + ) + ], + LEIntField("actual_count", None), + ), + PacketField("value", None, conf.raw_layer), + ] + + +class _NDRVarField: + """ + NDR Varying Array / String field + """ + + LENGTH_FROM = False + COUNT_FROM = False + + def __init__(self, *args, **kwargs): + # We build the length_is function by taking into account both the + # actual_count (from the varying field) and a potentially provided + # length_is field. + if "length_is" in kwargs: + _length_is = kwargs.pop("length_is") + length_is = lambda pkt: (_length_is(pkt.underlayer) or pkt.actual_count) + else: + length_is = lambda pkt: pkt.actual_count + # Pass it to the sub-field (actually subclass) + if self.LENGTH_FROM: + kwargs["length_from"] = length_is + elif self.COUNT_FROM: + kwargs["count_from"] = length_is + # TODO: For now, we do nothing with max_is + if "max_is" in kwargs: + kwargs.pop("max_is") + super(_NDRVarField, self).__init__(*args, **kwargs) + + def getfield(self, pkt, s): + fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] + remain, offset = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield(pkt, s) + remain, actual_count = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield( + pkt, remain + ) + final = NDRVaryingArray( + ndr64=pkt.ndr64, + ndrendian=pkt.ndrendian, + offset=offset, + actual_count=actual_count, + _underlayer=pkt, + ) + remain, val = super(_NDRVarField, self).getfield(final, remain) + final.value = super(_NDRVarField, self).i2h(pkt, val) + return remain, final + + def addfield(self, pkt, s, val): + if not isinstance(val, NDRVaryingArray): + raise ValueError( + "Expected NDRVaryingArray in %s. You are using it wrong!" % self.name + ) + fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] + _set_ctx_on(val.value, pkt) + s = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield(pkt, s, val.offset) + s = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield( + pkt, + s, + val.actual_count is None + and super(_NDRVarField, self).i2len(pkt, val.value) + or val.actual_count, + ) + return super(_NDRVarField, self).addfield( + pkt, s, super(_NDRVarField, self).h2i(pkt, val.value) + ) + + def i2len(self, pkt, x): + return super(_NDRVarField, self).i2len(pkt, x.value) + + def any2i(self, pkt, x): + # User-friendly helper + if not isinstance(x, NDRVaryingArray): + return NDRVaryingArray( + value=super(_NDRVarField, self).any2i(pkt, x), + ) + return x + + # Can't use i2repr = Field.i2repr and so on on PY2 :/ + def i2repr(self, pkt, val): + return repr(val) + + def i2h(self, pkt, x): + return x + + def h2i(self, pkt, x): + return x + + def valueof(self, pkt, x): + return super(_NDRVarField, self).valueof(pkt, x.value) + + +class NDRConformantArray(_NDRPacket): + fields_desc = [ + MultipleTypeField( + [(LELongField("max_count", None), lambda pkt: pkt and pkt.ndr64)], + LEIntField("max_count", None), + ), + MultipleTypeField( + [ + ( + PacketListField( + "value", + [], + conf.raw_layer, + count_from=lambda pkt: pkt.max_count, + ), + ( + lambda pkt: pkt.fields.get("value", None) + and isinstance(pkt.fields["value"][0], Packet), + lambda _, val: val and isinstance(val[0], Packet), + ), + ) + ], + FieldListField( + "value", [], LEIntField("", 0), count_from=lambda pkt: pkt.max_count + ), + ), + ] + + +class NDRConformantString(_NDRPacket): + fields_desc = [ + MultipleTypeField( + [(LELongField("max_count", None), lambda pkt: pkt and pkt.ndr64)], + LEIntField("max_count", None), + ), + StrField("value", ""), + ] + + +class _NDRConfField: + """ + NDR Conformant Array / String field + """ + + CONFORMANT_STRING = False + LENGTH_FROM = False + COUNT_FROM = False + + def __init__(self, *args, **kwargs): + # when conformant_in_struct is True, we remove the level of abstraction + # provided by NDRConformantString / NDRConformantArray because max_count + # is a proper field in the parent packet. + self.conformant_in_struct = kwargs.pop("conformant_in_struct", False) + # size_is/max_is end up here, and is what defines a conformant field. + if "size_is" in kwargs: + size_is = kwargs.pop("size_is") + if self.LENGTH_FROM: + kwargs["length_from"] = size_is + elif self.COUNT_FROM: + kwargs["count_from"] = size_is + # TODO: For now, we do nothing with max_is + if "max_is" in kwargs: + kwargs.pop("max_is") + super(_NDRConfField, self).__init__(*args, **kwargs) + + def getfield(self, pkt, s): + # [C706] - 14.3.7 Structures Containing Arrays + fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] + if self.conformant_in_struct: + # [MS-RPCE] 2.2.5.3.4.2 Structure Containing a Conformant Array + # Padding is here: just before the Conformant content + return NDRAlign( + super(_NDRConfField, self), + align=pkt.ALIGNMENT, + ).getfield(pkt, s) + + # The max count is aligned as a primitive type + remain, max_count = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield( + pkt, s + ) + remain, val = super(_NDRConfField, self).getfield(pkt, remain) + return remain, ( + NDRConformantString if self.CONFORMANT_STRING else NDRConformantArray + )(ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, max_count=max_count, value=val) + + def addfield(self, pkt, s, val): + if self.conformant_in_struct: + # [MS-RPCE] 2.2.5.3.4.2 Structure Containing a Conformant Array + # Padding is here: just before the Conformant content + return NDRAlign(super(_NDRConfField, self), align=pkt.ALIGNMENT).addfield( + pkt, s, val + ) + + if self.CONFORMANT_STRING and not isinstance(val, NDRConformantString): + raise ValueError( + "Expected NDRConformantString in %s. You are using it wrong!" + % self.name + ) + elif not self.CONFORMANT_STRING and not isinstance(val, NDRConformantArray): + raise ValueError( + "Expected NDRConformantArray in %s. You are using it wrong!" % self.name + ) + fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] + _set_ctx_on(val.value, pkt) + if val.value and isinstance(val.value[0], NDRVaryingArray): + value = val.value[0] + else: + value = val.value + s = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield( + pkt, + s, + val.max_count is None + and super(_NDRConfField, self).i2len(pkt, value) + or val.max_count, + ) + return super(_NDRConfField, self).addfield(pkt, s, value) + + def _subval(self, x): + if self.conformant_in_struct: + value = x + elif ( + not self.CONFORMANT_STRING + and x.value + and isinstance(x.value[0], NDRVaryingArray) + ): + value = x.value[0] + else: + value = x.value + return value + + def i2len(self, pkt, x): + return super(_NDRConfField, self).i2len(pkt, self._subval(x)) + + def any2i(self, pkt, x): + # User-friendly helper + if self.conformant_in_struct: + return super(_NDRConfField, self).any2i(pkt, x) + if self.CONFORMANT_STRING and not isinstance(x, NDRConformantString): + return NDRConformantString( + value=super(_NDRConfField, self).any2i(pkt, x), + ) + elif not isinstance(x, NDRConformantArray): + return NDRConformantArray( + value=super(_NDRConfField, self).any2i(pkt, x), + ) + return x + + # Can't use i2repr = Field.i2repr and so on on PY2 :/ + def i2repr(self, pkt, val): + return repr(val) + + def i2h(self, pkt, x): + return x + + def h2i(self, pkt, x): + return x + + def valueof(self, pkt, x): + return super(_NDRConfField, self).valueof(pkt, self._subval(x)) + + +class NDRVarPacketListField(_NDRVarField, _NDRPacketListField): + """ + NDR Varying PacketListField. Unused + """ + + COUNT_FROM = True + + +class NDRConfPacketListField(_NDRConfField, _NDRPacketListField): + """ + NDR Conformant PacketListField + """ + + COUNT_FROM = True + + +class NDRConfVarPacketListField(_NDRConfField, _NDRVarField, _NDRPacketListField): + """ + NDR Conformant Varying PacketListField + """ + + COUNT_FROM = True + + +class NDRConfFieldListField(_NDRConfField, NDRFieldListField): + """ + NDR Conformant FieldListField + """ + + COUNT_FROM = True + + +class NDRConfVarFieldListField(_NDRConfField, _NDRVarField, NDRFieldListField): + """ + NDR Conformant Varying FieldListField + """ + + COUNT_FROM = True + + +# NDR String fields + + +class _NDRUtf16(Field): + def h2i(self, pkt, x): + encoding = {"big": "utf-16be", "little": "utf-16le"}[pkt.ndrendian] + return plain_str(x).encode(encoding) + + def i2h(self, pkt, x): + encoding = {"big": "utf-16be", "little": "utf-16le"}[pkt.ndrendian] + return bytes_encode(x).decode(encoding, errors="replace") + + +class NDRConfStrLenField(_NDRConfField, _NDRValueOf, StrLenField): + """ + NDR Conformant StrLenField. + + This is not a "string" per NDR, but an a conformant byte array + (e.g. tower_octet_string). For ease of use, we implicitly convert + it in specific cases. + """ + + CONFORMANT_STRING = True + LENGTH_FROM = True + + +class NDRConfStrLenFieldUtf16(_NDRConfField, _NDRValueOf, StrLenFieldUtf16, _NDRUtf16): + """ + NDR Conformant StrLenFieldUtf16. + + See NDRConfStrLenField for comment. + """ + + CONFORMANT_STRING = True + ON_WIRE_SIZE_UTF16 = False + LENGTH_FROM = True + + +class NDRVarStrNullField(_NDRVarField, _NDRValueOf, StrNullField): + """ + NDR Varying StrNullField + """ + + NULLFIELD = True + + +class NDRVarStrNullFieldUtf16(_NDRVarField, _NDRValueOf, StrNullFieldUtf16, _NDRUtf16): + """ + NDR Varying StrNullFieldUtf16 + """ + + NULLFIELD = True + + +class NDRVarStrLenField(_NDRVarField, StrLenField): + """ + NDR Varying StrLenField + """ + + LENGTH_FROM = True + + +class NDRVarStrLenFieldUtf16(_NDRVarField, _NDRValueOf, StrLenFieldUtf16, _NDRUtf16): + """ + NDR Varying StrLenFieldUtf16 + """ + + ON_WIRE_SIZE_UTF16 = False + LENGTH_FROM = True + + +class NDRConfVarStrLenField(_NDRConfField, _NDRVarField, _NDRValueOf, StrLenField): + """ + NDR Conformant Varying StrLenField + """ + + LENGTH_FROM = True + + +class NDRConfVarStrLenFieldUtf16( + _NDRConfField, _NDRVarField, _NDRValueOf, StrLenFieldUtf16, _NDRUtf16 +): + """ + NDR Conformant Varying StrLenFieldUtf16 + """ + + ON_WIRE_SIZE_UTF16 = False + LENGTH_FROM = True + + +class NDRConfVarStrNullField(_NDRConfField, _NDRVarField, _NDRValueOf, StrNullField): + """ + NDR Conformant Varying StrNullField + """ + + NULLFIELD = True + + +class NDRConfVarStrNullFieldUtf16( + _NDRConfField, _NDRVarField, _NDRValueOf, StrNullFieldUtf16, _NDRUtf16 +): + """ + NDR Conformant Varying StrNullFieldUtf16 + """ + + ON_WIRE_SIZE_UTF16 = False + NULLFIELD = True + + +# Union type + + +class NDRUnion(_NDRPacket): + fields_desc = [ + IntField("tag", 0), + PacketField("value", None, conf.raw_layer), + ] + + +class _NDRUnionField(MultipleTypeField): + __slots__ = ["switch_fmt", "align"] + + def __init__(self, flds, dflt, align, switch_fmt): + self.switch_fmt = switch_fmt + self.align = align + super(_NDRUnionField, self).__init__(flds, dflt) + + def getfield(self, pkt, s): + fmt = _e(pkt.ndrendian) + self.switch_fmt[pkt.ndr64] + remain, tag = NDRAlign(Field("", 0, fmt=fmt), align=self.align).getfield(pkt, s) + fld, _ = super(_NDRUnionField, self)._find_fld_pkt_val(pkt, NDRUnion(tag=tag)) + remain, val = fld.getfield(pkt, remain) + return remain, NDRUnion( + tag=tag, value=val, ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, _parent=pkt + ) + + def addfield(self, pkt, s, val): + fmt = _e(pkt.ndrendian) + self.switch_fmt[pkt.ndr64] + if not isinstance(val, NDRUnion): + raise ValueError( + "Expected NDRUnion in %s. You are using it wrong!" % self.name + ) + _set_ctx_on(val.value, pkt) + # First, align the whole tag+union against the align param + s = NDRAlign(Field("", 0, fmt=fmt), align=self.align).addfield(pkt, s, val.tag) + # Then, compute the subfield with its own alignment + return super(_NDRUnionField, self).addfield(pkt, s, val) + + def _find_fld_pkt_val(self, pkt, val): + fld, val = super(_NDRUnionField, self)._find_fld_pkt_val(pkt, val) + return fld, val.value + + # Can't use i2repr = Field.i2repr and so on on PY2 :/ + def i2repr(self, pkt, val): + return repr(val) + + def i2h(self, pkt, x): + return x + + def h2i(self, pkt, x): + return x + + def valueof(self, pkt, x): + fld, val = self._find_fld_pkt_val(pkt, x) + return fld.valueof(pkt, x.value) + + +class NDRUnionField(NDRConstructedType, _NDRUnionField): + def __init__(self, flds, dflt, align, switch_fmt): + _NDRUnionField.__init__(self, flds, dflt, align=align, switch_fmt=switch_fmt) + NDRConstructedType.__init__(self, [x[0] for x in flds] + [dflt]) + + def any2i(self, pkt, x): + # User-friendly helper + if x: + if not isinstance(x, NDRUnion): + raise ValueError("Invalid value for %s; should be NDRUnion" % self.name) + else: + x.value = _NDRUnionField.any2i(self, pkt, x) + return x + + +# Misc + + +class _ProxyArray: + # Hack for recursive fields DEPORTED_CONFORMANTS field + __slots__ = ["getfld"] + + def __init__(self, getfld): + self.getfld = getfld + + def __len__(self): + try: + return len(self.getfld()) + except AttributeError: + return 0 + + def __iter__(self): + try: + return iter(self.getfld()) + except AttributeError: + return iter([]) + + +class _ProxyTuple: + # Hack for recursive fields ALIGNMENT field + __slots__ = ["getfld"] + + def __init__(self, getfld): + self.getfld = getfld + + def __getitem__(self, name): + try: + return self.getfld()[name] + except AttributeError: + raise KeyError + + +def NDRRecursiveClass(clsname): + """ + Return a special class that is used for pointer recursion + """ + # Get module where this is called + frame = inspect.currentframe().f_back + mod = frame.f_globals["__loader__"].name + getcls = lambda: getattr(importlib.import_module(mod), clsname) + + class _REC(NDRPacket): + ALIGNMENT = _ProxyTuple(lambda: getattr(getcls(), "ALIGNMENT")) + DEPORTED_CONFORMANTS = _ProxyArray( + lambda: getattr(getcls(), "DEPORTED_CONFORMANTS") + ) + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + return getcls() + + return _REC + + +# The very few NDR-specific structures + + +class NDRContextHandle(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [ + LEIntField("attributes", 0), + StrFixedLenField("uuid", b"", length=16), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class NDRNone(NDRPacket): + # This is only used in NDRPacketListField to act as a "None" pointer, and is + # a workaround because the field doesn't support None as a value in the list. + name = "None" + ALIGNMENT = (4, 8) + fields_desc = [ + NDRInt3264Field("ptr", 0), + ] + + +# --- Type Serialization Version 1 - [MSRPCE] sect 2.2.6 + + +def _get_ndrtype1_endian(pkt): + if pkt.underlayer is None: + return "<" + return {0x00: ">", 0x10: "<"}.get(pkt.underlayer.Endianness, "<") + + +class NDRSerialization1Header(Packet): + fields_desc = [ + ByteField("Version", 1), + ByteEnumField("Endianness", 0x10, {0x00: "big", 0x10: "little"}), + LEShortField("CommonHeaderLength", 8), + XLEIntField("Filler", 0xCCCCCCCC), + ] + + # Add a bit of goo so that valueof() goes through the header + + def _ndrlayer(self): + cur = self + while cur and not isinstance(cur, _NDRPacket) and cur.payload: + cur = cur.payload + if isinstance(cur, NDRPointer): + cur = cur.value + return cur + + def getfield_and_val(self, attr): + try: + return Packet.getfield_and_val(self, attr) + except ValueError: + return self._ndrlayer().getfield_and_val(attr) + + def valueof(self, name): + return self._ndrlayer().valueof(name) + + +class NDRSerialization1PrivateHeader(Packet): + fields_desc = [ + EField( + LEIntField("ObjectBufferLength", 0), endianness_from=_get_ndrtype1_endian + ), + XLEIntField("Filler", 0), + ] + + +def ndr_deserialize1(b, cls, ptr_pack=False): + """ + Deserialize Type Serialization Version 1 + [MS-RPCE] sect 2.2.6 + + :param ptr_pack: pack in a pointer to the structure. + """ + if issubclass(cls, NDRPacket): + # We use an intermediary class because it uses NDRPacketField which handles + # deported conformant fields + if ptr_pack: + hdrlen = 20 + + class _cls(NDRPacket): + fields_desc = [NDRFullPointerField(NDRPacketField("pkt", None, cls))] + + else: + hdrlen = 16 + + class _cls(NDRPacket): + fields_desc = [NDRPacketField("pkt", None, cls)] + + hdr = NDRSerialization1Header(b[:8]) / NDRSerialization1PrivateHeader(b[8:16]) + endian = {0x00: "big", 0x10: "little"}[hdr.Endianness] + padlen = (-hdr.ObjectBufferLength) % _TYPE1_S_PAD + # padlen should be 0 (pad included in length), but some implementations + # implement apparently misread the spec + return ( + hdr + / _cls( + b[16 : hdrlen + hdr.ObjectBufferLength], + ndr64=False, # Only NDR32 is supported in Type 1 + ndrendian=endian, + ).pkt + / conf.padding_layer(b[hdrlen + padlen + hdr.ObjectBufferLength :]) + ) + return NDRSerialization1Header(b[:8]) / cls(b[8:]) + + +def ndr_serialize1(pkt, ptr_pack=False): + """ + Serialize Type Serialization Version 1 + [MS-RPCE] sect 2.2.6 + + :param ptr_pack: pack in a pointer to the structure. + """ + pkt = pkt.copy() + endian = getattr(pkt, "ndrendian", "little") + if not isinstance(pkt, NDRSerialization1Header): + if not isinstance(pkt, NDRPacket): + return bytes(NDRSerialization1Header(Endianness=endian) / pkt) + if isinstance(pkt, NDRPointer): + cls = pkt.value.__class__ + else: + cls = pkt.__class__ + val = pkt + pkt_len = len(pkt) + # ObjectBufferLength: + # > It MUST include the padding length and exclude the header itself + pkt = NDRSerialization1Header( + Endianness=endian + ) / NDRSerialization1PrivateHeader( + ObjectBufferLength=pkt_len + (-pkt_len) % _TYPE1_S_PAD + ) + else: + cls = pkt.value.__class__ + val = pkt.payload.payload + pkt.payload.remove_payload() + + # See above about why we need an intermediary class + if ptr_pack: + + class _cls(NDRPacket): + fields_desc = [NDRFullPointerField(NDRPacketField("pkt", None, cls))] + + else: + + class _cls(NDRPacket): + fields_desc = [NDRPacketField("pkt", None, cls)] + + ret = bytes(pkt / _cls(pkt=val, ndr64=False, ndrendian=endian)) + return ret + (-len(ret) % _TYPE1_S_PAD) * b"\x00" + + +class _NDRSerializeType1: + def __init__(self, *args, **kwargs): + self.ptr_pack = kwargs.pop("ptr_pack", False) + super(_NDRSerializeType1, self).__init__(*args, **kwargs) + + def i2m(self, pkt, val): + return ndr_serialize1(val, ptr_pack=self.ptr_pack) + + def m2i(self, pkt, s): + return ndr_deserialize1(s, self.cls, ptr_pack=self.ptr_pack) + + def i2len(self, pkt, val): + return len(self.i2m(pkt, val)) + + +class NDRSerializeType1PacketField(_NDRSerializeType1, PacketField): + __slots__ = ["ptr_pack"] + + +class NDRSerializeType1PacketLenField(_NDRSerializeType1, PacketLenField): + __slots__ = ["ptr_pack"] + + +class NDRSerializeType1PacketListField(_NDRSerializeType1, PacketListField): + __slots__ = ["ptr_pack"] + + def i2len(self, pkt, val): + return sum(len(self.i2m(pkt, p)) for p in val) + + +# --- DCE/RPC session + + +class DceRpcSession(DefaultSession): + """ + A DCE/RPC session within a TCP socket. + """ + + def __init__(self, *args, **kwargs): + self.rpc_bind_interface: Union[DceRpcInterface, ComInterface] = None + self.rpc_bind_is_com: bool = False + self.ndr64 = False + self.ndrendian = "little" + self.support_header_signing = kwargs.pop("support_header_signing", True) + self.header_sign = conf.dcerpc_force_header_signing + self.ssp = kwargs.pop("ssp", None) + self.sspcontext = kwargs.pop("sspcontext", None) + self.auth_level = kwargs.pop("auth_level", None) + self.sent_cont_ids = [] + self.cont_id = 0 # Currently selected context + self.auth_context_id = 0 # Currently selected authentication context + self.assoc_group_id = 0 # Currently selected association group + self.map_callid_opnum = {} + self.frags = collections.defaultdict(lambda: b"") + self.sniffsspcontexts = {} # Unfinished contexts for passive + if conf.dcerpc_session_enable and conf.winssps_passive: + for ssp in conf.winssps_passive: + self.sniffsspcontexts[ssp] = None + super(DceRpcSession, self).__init__(*args, **kwargs) + + def _up_pkt(self, pkt): + """ + Common function to handle the DCE/RPC session: what interfaces are bind, + opnums, etc. + """ + opnum = None + opts = {} + if DceRpc5Bind in pkt or DceRpc5AlterContext in pkt: + # bind => get which RPC interface + self.sent_cont_ids = [x.cont_id for x in pkt.context_elem] + for ctx in pkt.context_elem: + if_uuid = ctx.abstract_syntax.if_uuid + if_version = ctx.abstract_syntax.if_version + try: + self.rpc_bind_interface = DCE_RPC_INTERFACES[(if_uuid, if_version)] + self.rpc_bind_is_com = False + except KeyError: + try: + self.rpc_bind_interface = COM_INTERFACES[if_uuid] + self.rpc_bind_is_com = True + except KeyError: + self.rpc_bind_interface = None + log_runtime.warning( + "Unknown RPC interface %s. Try loading the IDL" % if_uuid + ) + elif DceRpc5BindAck in pkt or DceRpc5AlterContextResp in pkt: + # bind ack => is it NDR64 + for i, res in enumerate(pkt.results): + if res.result == 0: # Accepted + # Context + try: + self.cont_id = self.sent_cont_ids[i] + except IndexError: + self.cont_id = 0 + finally: + self.sent_cont_ids = [] + + self.assoc_group_id = pkt.assoc_group_id + + # Endianness + self.ndrendian = {0: "big", 1: "little"}[pkt[DceRpc5].endian] + + # Transfer syntax + if res.transfer_syntax.sprintf("%if_uuid%") == "NDR64": + self.ndr64 = True + elif DceRpc5Request in pkt: + # request => match opnum with callID + opnum = pkt.opnum + uid = (self.assoc_group_id, pkt.call_id) + if self.rpc_bind_is_com: + self.map_callid_opnum[uid] = ( + opnum, + pkt[DceRpc5Request].payload.payload, + ) + else: + self.map_callid_opnum[uid] = opnum, pkt[DceRpc5Request].payload + elif DceRpc5Response in pkt: + # response => get opnum from table + uid = (self.assoc_group_id, pkt.call_id) + try: + opnum, opts["request_packet"] = self.map_callid_opnum[uid] + del self.map_callid_opnum[uid] + except KeyError: + log_runtime.info("Unknown call_id %s in DCE/RPC session" % pkt.call_id) + # Bind / Alter request/response specific + if ( + DceRpc5Bind in pkt + or DceRpc5AlterContext in pkt + or DceRpc5BindAck in pkt + or DceRpc5AlterContextResp in pkt + ): + # Detect if "Header Signing" is in use + if pkt.pfc_flags & 0x04: # PFC_SUPPORT_HEADER_SIGN + self.header_sign = True + return opnum, opts + + # [C706] sect 12.6.2 - Fragmentation and Reassembly + # Since the connection-oriented transport guarantees sequentiality, the receiver + # will always receive the fragments in order. + + def _defragment(self, pkt, body=None): + """ + Function to defragment DCE/RPC packets. + """ + uid = (self.assoc_group_id, pkt.call_id) + if pkt.pfc_flags.PFC_FIRST_FRAG and pkt.pfc_flags.PFC_LAST_FRAG: + # Not fragmented + return body + if pkt.pfc_flags.PFC_FIRST_FRAG or uid in self.frags: + # Packet is fragmented + if body is None: + body = pkt[DceRpc5].payload.payload.original + self.frags[uid] += body + if pkt.pfc_flags.PFC_LAST_FRAG: + return self.frags[uid] + else: + # Not fragmented + return body + + # C706 sect 12.5.2.15 - PDU Body Length + # "The maximum PDU body size is 65528 bytes." + MAX_PDU_BODY_SIZE = 4176 + + def _fragment(self, pkt, body): + """ + Function to fragment DCE/RPC packets. + """ + if len(body) > self.MAX_PDU_BODY_SIZE: + # Clear any PFC_*_FRAG flag + pkt.pfc_flags &= 0xFC + + # Iterate through fragments + cur = None + while body: + # Create a fragment + pkt_frag = pkt.copy() + + if cur is None: + # It's the first one + pkt_frag.pfc_flags += "PFC_FIRST_FRAG" + + # Split + cur, body = ( + body[: self.MAX_PDU_BODY_SIZE], + body[self.MAX_PDU_BODY_SIZE :], + ) + + if not body: + # It's the last one + pkt_frag.pfc_flags += "PFC_LAST_FRAG" + yield pkt_frag, cur + else: + yield pkt, body + + # [MS-RPCE] sect 3.3.1.5.2.2 + + # The PDU header, PDU body, and sec_trailer MUST be passed in the input message, in + # this order, to GSS_WrapEx, GSS_UnwrapEx, GSS_GetMICEx, and GSS_VerifyMICEx. For + # integrity protection the sign flag for that PDU segment MUST be set to TRUE, else + # it MUST be set to FALSE. For confidentiality protection, the conf_req_flag for + # that PDU segment MUST be set to TRUE, else it MUST be set to FALSE. + + # If the authentication level is RPC_C_AUTHN_LEVEL_PKT_PRIVACY, the PDU body will + # be encrypted. + # The PDU body from the output message of GSS_UnwrapEx represents the plain text + # version of the PDU body. The PDU header and sec_trailer output from the output + # message SHOULD be ignored. + # Similarly the signature output SHOULD be ignored. + + def in_pkt(self, pkt): + # Check for encrypted payloads + body = None + if conf.raw_layer in pkt.payload: + body = bytes(pkt.payload[conf.raw_layer]) + # If we are doing passive sniffing + if conf.dcerpc_session_enable and conf.winssps_passive: + # We have Windows SSPs, and no current context + if pkt.auth_verifier and pkt.auth_verifier.is_ssp(): + # This is a bind/alter/auth3 req/resp + for ssp in self.sniffsspcontexts: + self.sniffsspcontexts[ssp], status = ssp.GSS_Passive( + self.sniffsspcontexts[ssp], + pkt.auth_verifier.auth_value, + req_flags=GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS + | GSS_C_FLAGS.GSS_C_DCE_STYLE, + ) + if status == GSS_S_COMPLETE: + self.auth_level = DCE_C_AUTHN_LEVEL( + int(pkt.auth_verifier.auth_level) + ) + self.ssp = ssp + self.sspcontext = self.sniffsspcontexts[ssp] + self.sniffsspcontexts[ssp] = None + elif ( + self.sspcontext + and pkt.auth_verifier + and pkt.auth_verifier.is_protected() + and body + ): + # This is a request/response + if self.sspcontext.passive: + self.ssp.GSS_Passive_set_Direction( + self.sspcontext, + IsAcceptor=DceRpc5Response in pkt, + ) + if pkt.auth_verifier and pkt.auth_verifier.is_protected() and body: + if self.sspcontext is None: + return pkt + if self.auth_level in ( + RPC_C_AUTHN_LEVEL.PKT_INTEGRITY, + RPC_C_AUTHN_LEVEL.PKT_PRIVACY, + ): + # note: 'vt_trailer' is included in the pdu body + # [MS-RPCE] sect 2.2.2.13 + # "The data structures MUST only appear in a request PDU, and they + # SHOULD be placed in the PDU immediately after the stub data but + # before the authentication padding octets. Therefore, for security + # purposes, the verification trailer is considered part of the PDU + # body." + if pkt.vt_trailer: + body += bytes(pkt.vt_trailer) + # Account for padding when computing checksum/encryption + if pkt.auth_padding: + body += pkt.auth_padding + + # Build pdu_header and sec_trailer + pdu_header = pkt.copy() + sec_trailer = pdu_header.auth_verifier + # sec_trailer: include the sec_trailer but not the Authentication token + authval_len = len(sec_trailer.auth_value) + # Discard everything out of the header + pdu_header.auth_padding = None + pdu_header.auth_verifier = None + pdu_header.payload.payload = NoPayload() + pdu_header.vt_trailer = None + + # [MS-RPCE] sect 2.2.2.12 + if self.auth_level == RPC_C_AUTHN_LEVEL.PKT_PRIVACY: + _msgs = self.ssp.GSS_UnwrapEx( + self.sspcontext, + [ + # "PDU header" + SSP.WRAP_MSG( + conf_req_flag=False, + sign=self.header_sign, + data=bytes(pdu_header), + ), + # "PDU body" + SSP.WRAP_MSG( + conf_req_flag=True, + sign=True, + data=body, + ), + # "sec_trailer" + SSP.WRAP_MSG( + conf_req_flag=False, + sign=self.header_sign, + data=bytes(sec_trailer)[:-authval_len], + ), + ], + pkt.auth_verifier.auth_value, + ) + body = _msgs[1].data # PDU body + elif self.auth_level == RPC_C_AUTHN_LEVEL.PKT_INTEGRITY: + self.ssp.GSS_VerifyMICEx( + self.sspcontext, + [ + # "PDU header" + SSP.MIC_MSG( + sign=self.header_sign, + data=bytes(pdu_header), + ), + # "PDU body" + SSP.MIC_MSG( + sign=True, + data=body, + ), + # "sec_trailer" + SSP.MIC_MSG( + sign=self.header_sign, + data=bytes(sec_trailer)[:-authval_len], + ), + ], + pkt.auth_verifier.auth_value, + ) + # Put padding back into the header + if pkt.auth_padding: + padlen = len(pkt.auth_padding) + body, pkt.auth_padding = body[:-padlen], body[-padlen:] + # Put back vt_trailer into the header, if present. + if _SECTRAILER_MAGIC in body: + body, pkt.vt_trailer = pkt.get_field("vt_trailer").getfield( + pkt, body + ) + # If it's a request / response, could be fragmented + if isinstance(pkt.payload, (DceRpc5Request, DceRpc5Response)) and body: + body = self._defragment(pkt, body) + if not body: + return + # Get opnum and options + opnum, opts = self._up_pkt(pkt) + # Try to parse the payload + if opnum is not None and self.rpc_bind_interface: + # use opnum to parse the payload + is_response = DceRpc5Response in pkt + try: + cls = self.rpc_bind_interface.opnums[opnum][is_response] + except KeyError: + log_runtime.warning( + "Unknown opnum %s for interface %s" + % (opnum, self.rpc_bind_interface) + ) + pkt.payload[conf.raw_layer].load = body + return pkt + if body: + orpc = None + if self.rpc_bind_is_com: + # If interface is a COM interface, start off by dissecting the + # ORPCTHIS / ORPCTHAT argument + from scapy.layers.msrpce.raw.ms_dcom import ORPCTHAT, ORPCTHIS + + # [MS-DCOM] sect 2.2.13 + # "ORPCTHIS and ORPCTHAT structures MUST be marshaled using + # the NDR (32) Transfer Syntax" + if is_response: + orpc = ORPCTHAT(body, ndr64=False) + else: + orpc = ORPCTHIS(body, ndr64=False) + body = orpc.load + orpc.remove_payload() + # Dissect payload using class + try: + payload = cls( + body, ndr64=self.ndr64, ndrendian=self.ndrendian, **opts + ) + except Exception: + if conf.debug_dissector: + log_runtime.error("%s dissector failed", cls.__name__) + if cls is not None: + raise + payload = conf.raw_layer(body, _internal=1) + pkt.payload[conf.raw_layer].underlayer.remove_payload() + if conf.padding_layer in payload: + # Most likely, dissection failed. + log_runtime.warning( + "Padding detected when dissecting %s. Looks wrong." % cls + ) + pad = payload[conf.padding_layer] + pad.underlayer.payload = conf.raw_layer(load=pad.load) + if orpc is not None: + pkt /= orpc + pkt /= payload + # If a request was encrypted, we need to re-register it once re-parsed. + if not is_response and self.auth_level == RPC_C_AUTHN_LEVEL.PKT_PRIVACY: + self._up_pkt(pkt) + elif not cls.fields_desc: + # Request class has no payload + pkt /= cls(ndr64=self.ndr64, ndrendian=self.ndrendian, **opts) + elif body: + pkt.payload[conf.raw_layer].load = body + return pkt + + def out_pkt(self, pkt): + assert DceRpc5 in pkt + # Register opnum and options + self._up_pkt(pkt) + + # If it's a request / response, we can frag it + if isinstance(pkt.payload, (DceRpc5Request, DceRpc5Response)): + # The list of packet responses + pkts = [] + # Take the body payload, and eventually split it + body = bytes(pkt.payload.payload) + + for pkt, body in self._fragment(pkt, body): + if pkt.auth_verifier is not None: + # Verifier already set + pkts.append(pkt) + continue + + # Sign / Encrypt + if self.sspcontext: + signature = None + if self.auth_level in ( + RPC_C_AUTHN_LEVEL.PKT_INTEGRITY, + RPC_C_AUTHN_LEVEL.PKT_PRIVACY, + ): + # Remember that vt_trailer is included in the PDU + if pkt.vt_trailer: + body += bytes(pkt.vt_trailer) + # Account for padding when computing checksum/encryption + if pkt.auth_padding is None: + padlen = (-len(body)) % _COMMON_AUTH_PAD # authdata padding + pkt.auth_padding = b"\x00" * padlen + else: + padlen = len(pkt.auth_padding) + # Remember that padding IS SIGNED & ENCRYPTED + body += pkt.auth_padding + # Add the auth_verifier + pkt.auth_verifier = CommonAuthVerifier( + auth_type=self.ssp.auth_type, + auth_level=self.auth_level, + auth_context_id=self.auth_context_id, + auth_pad_length=padlen, + # Note: auth_value should have the correct length because + # when using PFC_SUPPORT_HEADER_SIGN, auth_len + # (and frag_len) is included in the token.. but this + # creates a dependency loop as you'd need to know the token + # length to compute the token. Windows solves this by + # setting the 'Maximum Signature Length' (or something + # similar) beforehand, instead of the real length. + # See `gensec_sig_size` in samba. + auth_value=b"\x00" + * self.ssp.MaximumSignatureLength(self.sspcontext), + ) + # Build pdu_header and sec_trailer + pdu_header = pkt.copy() + pdu_header.auth_len = len(pdu_header.auth_verifier) - 8 + pdu_header.frag_len = len(pdu_header) + sec_trailer = pdu_header.auth_verifier + # sec_trailer: include the sec_trailer but not the + # Authentication token + authval_len = len(sec_trailer.auth_value) + # sec_trailer.auth_value = None + # Discard everything out of the header + pdu_header.auth_padding = None + pdu_header.auth_verifier = None + pdu_header.payload.payload = NoPayload() + pdu_header.vt_trailer = None + signature = None + # [MS-RPCE] sect 2.2.2.12 + if self.auth_level == RPC_C_AUTHN_LEVEL.PKT_PRIVACY: + _msgs, signature = self.ssp.GSS_WrapEx( + self.sspcontext, + [ + # "PDU header" + SSP.WRAP_MSG( + conf_req_flag=False, + sign=self.header_sign, + data=bytes(pdu_header), + ), + # "PDU body" + SSP.WRAP_MSG( + conf_req_flag=True, + sign=True, + data=body, + ), + # "sec_trailer" + SSP.WRAP_MSG( + conf_req_flag=False, + sign=self.header_sign, + data=bytes(sec_trailer)[:-authval_len], + ), + ], + ) + s = _msgs[1].data # PDU body + elif self.auth_level == RPC_C_AUTHN_LEVEL.PKT_INTEGRITY: + signature = self.ssp.GSS_GetMICEx( + self.sspcontext, + [ + # "PDU header" + SSP.MIC_MSG( + sign=self.header_sign, + data=bytes(pdu_header), + ), + # "PDU body" + SSP.MIC_MSG( + sign=True, + data=body, + ), + # "sec_trailer" + SSP.MIC_MSG( + sign=self.header_sign, + data=bytes(sec_trailer)[:-authval_len], + ), + ], + pkt.auth_verifier.auth_value, + ) + s = body + else: + raise ValueError("Impossible") + # Put padding back in the header + if padlen: + s, pkt.auth_padding = s[:-padlen], s[-padlen:] + # Put back vt_trailer into the header + if pkt.vt_trailer: + vtlen = len(pkt.vt_trailer) + s, pkt.vt_trailer = s[:-vtlen], s[-vtlen:] + else: + s = body + + # now inject the encrypted payload into the packet + pkt.payload.payload = conf.raw_layer(load=s) + # and the auth_value + if signature: + pkt.auth_verifier.auth_value = signature + else: + pkt.auth_verifier = None + # Add to the list + pkts.append(pkt) + return pkts + else: + return [pkt] + + def process(self, pkt: Packet) -> Optional[Packet]: + """ + Used when DceRpcSession is used for passive sniffing. + """ + pkt = super(DceRpcSession, self).process(pkt) + if pkt is not None and DceRpc5 in pkt: + rpkt = self.in_pkt(pkt) + if rpkt is None: + # We are passively dissecting a fragmented packet. Return + # just the header showing that it was indeed, fragmented. + pkt[DceRpc5].payload.remove_payload() + return pkt + return rpkt + return pkt + + +class DceRpcSocket(StreamSocket): + """ + A Wrapper around StreamSocket that uses a DceRpcSession + """ + + def __init__(self, *args, **kwargs): + self.transport = kwargs.pop("transport", None) + self.session = DceRpcSession( + ssp=kwargs.pop("ssp", None), + auth_level=kwargs.pop("auth_level", None), + support_header_signing=kwargs.pop("support_header_signing", True), + ) + super(DceRpcSocket, self).__init__(*args, **kwargs) + + def send(self, x, is_sr1=False, **kwargs): + for pkt in self.session.out_pkt(x): + if self.transport == DCERPC_Transport.NCACN_NP: + # In this case DceRpcSocket wraps a SMB_RPC_SOCKET, call it directly. + self.ins.send(pkt, is_sr1=is_sr1, **kwargs) + else: + super(DceRpcSocket, self).send(pkt, **kwargs) + + def sr1(self, *args, **kwargs): + # We allow to use IOCTL only when sr1() is used, as we expect an answer. + return super(DceRpcSocket, self).sr1(*args, is_sr1=True, **kwargs) + + def recv(self, x=None): + pkt = super(DceRpcSocket, self).recv(x) + if pkt is not None: + return self.session.in_pkt(pkt) + + +# --- TODO cleanup below + +# Heuristically way to find the payload class +# +# To add a possible payload to a DCE/RPC packet, one must first create the +# packet class, then instead of binding layers using bind_layers, he must +# call DceRpcPayload.register_possible_payload() with the payload class as +# parameter. +# +# To be able to decide if the payload class is capable of handling the rest of +# the dissection, the classmethod can_handle() should be implemented in the +# payload class. This method is given the rest of the string to dissect as +# first argument, and the DceRpc packet instance as second argument. Based on +# this information, the method must return True if the class is capable of +# handling the dissection, False otherwise + + +class DceRpc4Payload(Packet): + """Dummy class which use the dispatch_hook to find the payload class""" + + _payload_class = [] + + @classmethod + def dispatch_hook(cls, _pkt, _underlayer=None, *args, **kargs): + """dispatch_hook to choose among different registered payloads""" + for klass in cls._payload_class: + if hasattr(klass, "can_handle") and klass.can_handle(_pkt, _underlayer): + return klass + log_runtime.warning("DCE/RPC payload class not found or undefined (using Raw)") + return Raw + + @classmethod + def register_possible_payload(cls, pay): + """Method to call from possible DCE/RPC endpoint to register it as + possible payload""" + cls._payload_class.append(pay) + + +bind_layers(DceRpc4, DceRpc4Payload) diff --git a/scapy/layers/dhcp.py b/scapy/layers/dhcp.py index 77d9bcdef82..39250cf74a8 100644 --- a/scapy/layers/dhcp.py +++ b/scapy/layers/dhcp.py @@ -1,14 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ DHCP (Dynamic Host Configuration Protocol) and BOOTP + +Implements: +- rfc951 - BOOTSTRAP PROTOCOL (BOOTP) +- rfc1542 - Clarifications and Extensions for the Bootstrap Protocol +- rfc1533 - DHCP Options and BOOTP Vendor Extensions """ -from __future__ import absolute_import -from __future__ import print_function try: from collections.abc import Iterable except ImportError: @@ -17,44 +20,92 @@ import random import struct +import socket +import re + from scapy.ansmachine import AnsweringMachine from scapy.base_classes import Net from scapy.compat import chb, orb, bytes_encode -from scapy.fields import ByteEnumField, ByteField, Field, FieldListField, \ - FlagsField, IntField, IPField, ShortField, StrField +from scapy.fields import ( + ByteEnumField, + ByteField, + Field, + FieldListField, + FlagsField, + IntField, + IPField, + MACField, + ShortField, + StrEnumField, + StrField, + StrFixedLenField, + XIntField, +) from scapy.layers.inet import UDP, IP -from scapy.layers.l2 import Ether +from scapy.layers.l2 import Ether, HARDWARE_TYPES from scapy.packet import bind_layers, bind_bottom_up, Packet -from scapy.utils import atol, itom, ltoa, sane -from scapy.volatile import RandBin, RandField, RandNum, RandNumExpo - -from scapy.arch import get_if_raw_hwaddr -from scapy.sendrecv import srp1, sendp +from scapy.utils import atol, itom, ltoa, sane, str2mac, mac2str +from scapy.volatile import ( + RandBin, + RandByte, + RandField, + RandIP, + RandInt, + RandNum, + RandNumExpo, + VolatileValue, +) + +from scapy.arch import get_if_hwaddr +from scapy.sendrecv import srp1 from scapy.error import warning -import scapy.modules.six as six -from scapy.modules.six.moves import range from scapy.config import conf +# Typing imports +from typing import ( + List, + Optional, + Union, +) + dhcpmagic = b"c\x82Sc" +class _BOOTP_chaddr(StrFixedLenField): + def i2m(self, pkt, x): + if isinstance(x, VolatileValue): + x = x._fix() + return MACField.i2m(self, pkt, x) + + def i2repr(self, pkt, v): + if isinstance(v, VolatileValue): + return repr(v) + if pkt.htype == 1: # Ethernet + if v[6:] == b"\x00" * 10: # Default padding + return "%s (+ 10 nul pad)" % str2mac(v[:6]) + else: + return "%s (pad: %s)" % (str2mac(v[:6]), v[6:]) + return super(_BOOTP_chaddr, self).i2repr(pkt, v) + + class BOOTP(Packet): name = "BOOTP" - fields_desc = [ByteEnumField("op", 1, {1: "BOOTREQUEST", 2: "BOOTREPLY"}), - ByteField("htype", 1), - ByteField("hlen", 6), - ByteField("hops", 0), - IntField("xid", 0), - ShortField("secs", 0), - FlagsField("flags", 0, 16, "???????????????B"), - IPField("ciaddr", "0.0.0.0"), - IPField("yiaddr", "0.0.0.0"), - IPField("siaddr", "0.0.0.0"), - IPField("giaddr", "0.0.0.0"), - Field("chaddr", b"", "16s"), - Field("sname", b"", "64s"), - Field("file", b"", "128s"), - StrField("options", b"")] + fields_desc = [ + ByteEnumField("op", 1, {1: "BOOTREQUEST", 2: "BOOTREPLY"}), + ByteEnumField("htype", 1, HARDWARE_TYPES), + ByteField("hlen", 6), + ByteField("hops", 0), + XIntField("xid", 0), + ShortField("secs", 0), + FlagsField("flags", 0, 16, "???????????????B"), + IPField("ciaddr", "0.0.0.0"), + IPField("yiaddr", "0.0.0.0"), + IPField("siaddr", "0.0.0.0"), + IPField("giaddr", "0.0.0.0"), + _BOOTP_chaddr("chaddr", b"", length=16), + StrFixedLenField("sname", b"", length=64), + StrFixedLenField("file", b"", length=128), + StrEnumField("options", b"", {dhcpmagic: "DHCP magic"})] def guess_payload_class(self, payload): if self.options[:len(dhcpmagic)] == dhcpmagic: @@ -80,18 +131,95 @@ def answers(self, other): return self.xid == other.xid -class _DHCPParamReqFieldListField(FieldListField): +class _DHCPByteFieldListField(FieldListField): + def randval(self): + class _RandByteFieldList(RandField): + def _fix(self): + return [RandByte()] * int(RandByte()) + return _RandByteFieldList() + + +class RandClasslessStaticRoutesField(RandField): + """ + A RandValue for classless static routes + """ + + def _fix(self): + return "%s/%d:%s" % (RandIP(), RandNum(0, 32), RandIP()) + + +class ClasslessFieldListField(FieldListField): + def randval(self): + class _RandClasslessField(RandField): + def _fix(self): + return [RandClasslessStaticRoutesField()] * int(RandNum(1, 28)) + return _RandClasslessField() + + +class ClasslessStaticRoutesField(Field): + """ + RFC 3442 defines classless static routes as up to 9 bytes per entry: + + # Code Len Destination 1 Router 1 + +-----+---+----+-----+----+----+----+----+----+ + | 121 | n | d1 | ... | dN | r1 | r2 | r3 | r4 | + +-----+---+----+-----+----+----+----+----+----+ + + Destination first byte contains one octet describing the width followed + by all the significant octets of the subnet. + """ + + def m2i(self, pkt, x): + # type: (Packet, bytes) -> str + # b'\x20\x01\x02\x03\x04\t\x08\x07\x06' -> (1.2.3.4/32:9.8.7.6) + prefix = orb(x[0]) + + octets = (prefix + 7) // 8 + # Create the destination IP by using the number of octets + # and padding up to 4 bytes to ensure a valid IP. + dest = x[1:1 + octets] + dest = socket.inet_ntoa(dest.ljust(4, b'\x00')) + + router = x[1 + octets:5 + octets] + router = socket.inet_ntoa(router) + + return dest + "/" + str(prefix) + ":" + router + + def i2m(self, pkt, x): + # type: (Packet, str) -> bytes + # (1.2.3.4/32:9.8.7.6) -> b'\x20\x01\x02\x03\x04\t\x08\x07\x06' + if not x: + return b'' + + spx = re.split('/|:', str(x)) + prefix = int(spx[1]) + # if prefix is invalid value ( 0 > prefix > 32 ) then break + if prefix > 32 or prefix < 0: + warning("Invalid prefix value: %d (0x%x)", prefix, prefix) + return b'' + octets = (prefix + 7) // 8 + dest = socket.inet_aton(spx[0])[:octets] + router = socket.inet_aton(spx[2]) + return struct.pack('b', prefix) + dest + router + def getfield(self, pkt, s): - ret = [] - while s: - s, val = FieldListField.getfield(self, pkt, s) - ret.append(val) - return b"", [x[0] for x in ret] + prefix = orb(s[0]) + route_len = 5 + (prefix + 7) // 8 + return s[route_len:], self.m2i(pkt, s[:route_len]) + + def addfield(self, pkt, s, val): + return s + self.i2m(pkt, val) + + def randval(self): + return RandClasslessStaticRoutesField() + # DHCP_UNKNOWN, DHCP_IP, DHCP_IPLIST, DHCP_TYPE \ # = range(4) # +# DHCP Options and BOOTP Vendor Extensions + DHCPTypes = { 1: "discover", @@ -156,7 +284,7 @@ def getfield(self, pkt, s): 43: "vendor_specific", 44: IPField("NetBIOS_server", "0.0.0.0"), 45: IPField("NetBIOS_dist_server", "0.0.0.0"), - 46: ByteField("static-routes", 100), + 46: ByteField("NetBIOS_node_type", 100), 47: "netbios-scope", 48: IPField("font-servers", "0.0.0.0"), 49: IPField("x-display-manager", "0.0.0.0"), @@ -165,7 +293,9 @@ def getfield(self, pkt, s): 52: ByteField("dhcp-option-overload", 100), 53: ByteEnumField("message-type", 1, DHCPTypes), 54: IPField("server_id", "0.0.0.0"), - 55: _DHCPParamReqFieldListField("param_req_list", [], ByteField("opcode", 0), length_from=lambda x: 1), # noqa: E501 + 55: _DHCPByteFieldListField( + "param_req_list", [], + ByteField("opcode", 0)), 56: "error_message", 57: ShortField("max_dhcp_size", 1500), 58: IntField("renewal_time", 21600), @@ -175,6 +305,7 @@ def getfield(self, pkt, s): 62: "nwip-domain-name", 64: "NISplus_domain", 65: IPField("NISplus_server", "0.0.0.0"), + 66: "tftp_server_name", 67: StrField("boot-file-name", ""), 68: IPField("mobile-ip-home-agent", "0.0.0.0"), 69: IPField("SMTP_server", "0.0.0.0"), @@ -185,8 +316,10 @@ def getfield(self, pkt, s): 74: IPField("IRC_server", "0.0.0.0"), 75: IPField("StreetTalk_server", "0.0.0.0"), 76: IPField("StreetTalk_Dir_Assistance", "0.0.0.0"), + 77: "user_class", 78: "slp_service_agent", 79: "slp_service_scope", + 80: "rapid_commit", 81: "client_FQDN", 82: "relay_agent_information", 85: IPField("nds-server", "0.0.0.0"), @@ -202,21 +335,32 @@ def getfield(self, pkt, s): 98: StrField("uap-servers", ""), 100: StrField("pcode", ""), 101: StrField("tcode", ""), + 108: IntField("ipv6-only-preferred", 0), 112: IPField("netinfo-server-address", "0.0.0.0"), 113: StrField("netinfo-server-tag", ""), - 114: StrField("default-url", ""), + 114: StrField("captive-portal", ""), 116: ByteField("auto-config", 0), 117: ShortField("name-service-search", 0,), 118: IPField("subnet-selection", "0.0.0.0"), + 121: ClasslessFieldListField( + "classless_static_routes", + [], + ClasslessStaticRoutesField("route", 0)), 124: "vendor_class", 125: "vendor_specific_information", + 128: IPField("tftp_server_ip_address", "0.0.0.0"), 136: IPField("pana-agent", "0.0.0.0"), 137: "v4-lost", 138: IPField("capwap-ac-v4", "0.0.0.0"), 141: "sip_ua_service_domains", + 145: _DHCPByteFieldListField( + "forcerenew_nonce_capable", [], + ByteEnumField("algorithm", 1, {1: "HMAC-MD5"})), 146: "rdnss-selection", + 150: IPField("tftp_server_address", "0.0.0.0"), 159: "v4-portparams", 160: StrField("v4-captive-portal", ""), + 161: StrField("mud-url", ""), 208: "pxelinux_magic", 209: "pxelinux_configuration_file", 210: "pxelinux_path_prefix", @@ -228,16 +372,16 @@ def getfield(self, pkt, s): DHCPRevOptions = {} -for k, v in six.iteritems(DHCPOptions): +for k, v in DHCPOptions.items(): if isinstance(v, str): n = v v = None else: n = v.name DHCPRevOptions[n] = (k, v) -del(n) -del(v) -del(k) +del n +del v +del k class RandDHCPOptions(RandField): @@ -248,7 +392,7 @@ def __init__(self, size=None, rndstr=None): if rndstr is None: rndstr = RandBin(RandNum(0, 255)) self.rndstr = rndstr - self._opts = list(six.itervalues(DHCPOptions)) + self._opts = list(DHCPOptions.values()) self._opts.remove("pad") self._opts.remove("end") @@ -259,11 +403,23 @@ def _fix(self): if isinstance(o, str): op.append((o, self.rndstr * 1)) else: - op.append((o.name, o.randval()._fix())) + r = o.randval()._fix() + if isinstance(r, bytes): + r = r[:255] + op.append((o.name, r)) return op + def __iter__(self): + return iter(self._fix()) + class DHCPOptionsField(StrField): + """ + A field that builds and dissects DHCP options. + The internal value is a list of tuples with the format + [("option_name", ), ...] + Where expected names and values can be found using `DHCPOptions` + """ islist = 1 def i2repr(self, pkt, x): @@ -275,8 +431,7 @@ def i2repr(self, pkt, x): vv = ",".join(f.i2repr(pkt, val) for val in v[1:]) else: vv = ",".join(repr(val) for val in v[1:]) - r = "%s=%s" % (v[0], vv) - s.append(r) + s.append("%s=%s" % (v[0], vv)) else: s.append(sane(v)) return "[%s]" % (" ".join(s)) @@ -309,6 +464,16 @@ def m2i(self, pkt, x): else: olen = orb(x[1]) lval = [f.name] + + if olen == 0: + try: + _, val = f.getfield(pkt, b'') + except Exception: + opt.append(x) + break + else: + lval.append(val) + try: left = x[2:olen + 2] while left: @@ -349,8 +514,7 @@ def i2m(self, pkt, x): warning("Unknown field option %s", name) continue - s += chb(onum) - s += chb(len(oval)) + s += struct.pack("!BB", onum, len(oval)) s += oval elif (isinstance(o, str) and o in DHCPRevOptions and @@ -364,11 +528,20 @@ def i2m(self, pkt, x): warning("Malformed option %s", o) return s + def randval(self): + return RandDHCPOptions() + class DHCP(Packet): name = "DHCP options" fields_desc = [DHCPOptionsField("options", b"")] + def mysummary(self): + for id in self.options: + if isinstance(id, tuple) and id[0] == "message-type": + return "DHCP %s" % DHCPTypes.get(id[1], "").capitalize() + return super(DHCP, self).mysummary() + bind_layers(UDP, BOOTP, dport=67, sport=68) bind_layers(UDP, BOOTP, dport=68, sport=67) @@ -377,26 +550,95 @@ class DHCP(Packet): @conf.commands.register -def dhcp_request(iface=None, **kargs): - """Send a DHCP discover request and return the answer""" +def dhcp_request(hw=None, + req_type='discover', + server_id=None, + requested_addr=None, + hostname=None, + iface=None, + **kargs): + """ + Send a DHCP discover request and return the answer. + + Usage:: + + >>> dhcp_request() # send DHCP discover + >>> dhcp_request(req_type='request', + ... requested_addr='10.53.4.34') # send DHCP request + """ if conf.checkIPaddr: warning( "conf.checkIPaddr is enabled, may not be able to match the answer" ) - if iface is None: - iface = conf.iface - fam, hw = get_if_raw_hwaddr(iface) - return srp1(Ether(dst="ff:ff:ff:ff:ff:ff") / IP(src="0.0.0.0", dst="255.255.255.255") / UDP(sport=68, dport=67) / # noqa: E501 - BOOTP(chaddr=hw) / DHCP(options=[("message-type", "discover"), "end"]), iface=iface, **kargs) # noqa: E501 + if hw is None: + if iface is None: + iface = conf.iface + hw = get_if_hwaddr(iface) + dhcp_options = [ + ('message-type', req_type), + ('client_id', b'\x01' + mac2str(hw)), + ] + if requested_addr is not None: + dhcp_options.append(('requested_addr', requested_addr)) + elif req_type == 'request': + warning("DHCP Request without requested_addr will likely be ignored") + if server_id is not None: + dhcp_options.append(('server_id', server_id)) + if hostname is not None: + dhcp_options.extend([ + ('hostname', hostname), + ('client_FQDN', b'\x00\x00\x00' + bytes_encode(hostname)), + ]) + dhcp_options.extend([ + ('vendor_class_id', b'MSFT 5.0'), + ('param_req_list', [ + 1, 3, 6, 15, 31, 33, 43, 44, 46, 47, 119, 121, 249, 252 + ]), + 'end' + ]) + return srp1( + Ether(dst="ff:ff:ff:ff:ff:ff", src=hw) / + IP(src="0.0.0.0", dst="255.255.255.255") / + UDP(sport=68, dport=67) / + BOOTP(chaddr=hw, xid=RandInt(), flags="B") / + DHCP(options=dhcp_options), + iface=iface, **kargs + ) class BOOTP_am(AnsweringMachine): function_name = "bootpd" filter = "udp and port 68 and port 67" - send_function = staticmethod(sendp) - def parse_options(self, pool=Net("192.168.1.128/25"), network="192.168.1.0/24", gw="192.168.1.1", # noqa: E501 - domain="localnet", renewal_time=60, lease_time=1800): + def parse_options(self, + pool: Union[Net, List[str]] = Net("192.168.1.128/25"), + network: str = "192.168.1.0/24", + gw: str = "192.168.1.1", + nameserver: Union[str, List[str]] = None, + domain: Optional[str] = None, + renewal_time: int = 60, + lease_time: int = 1800, + **kwargs): + """ + :param pool: the range of addresses to distribute. Can be a Net, + a list of IPs or a string (always gives the same IP). + :param network: the subnet range + :param gw: the gateway IP (can be None) + :param nameserver: the DNS server IP (by default, same than gw). + This can also be a list. + :param domain: the domain to advertise (can be None) + + Other DHCP parameters can be passed as kwargs. See DHCPOptions in dhcp.py. + For instance:: + + dhcpd(pool=Net("10.0.10.0/24"), network="10.0.0.0/8", gw="10.0.10.1", + classless_static_routes=["1.2.3.4/32:9.8.7.6"]) + + Other example with different options:: + + dhcpd(pool=Net("10.0.10.0/24"), network="10.0.0.0/8", gw="10.0.10.1", + nameserver=["8.8.8.8", "4.4.4.4"], domain="DOMAIN.LOCAL") + """ self.domain = domain netw, msk = (network.split("/") + ["32"])[:2] msk = itom(int(msk)) @@ -404,10 +646,17 @@ def parse_options(self, pool=Net("192.168.1.128/25"), network="192.168.1.0/24", self.network = ltoa(atol(netw) & msk) self.broadcast = ltoa(atol(self.network) | (0xffffffff & ~msk)) self.gw = gw - if isinstance(pool, six.string_types): + if nameserver is None: + self.nameserver = (gw,) + elif isinstance(nameserver, str): + self.nameserver = (nameserver,) + else: + self.nameserver = tuple(nameserver) + + if isinstance(pool, str): pool = Net(pool) if isinstance(pool, Iterable): - pool = [k for k in pool if k not in [gw, self.network, self.broadcast]] # noqa: E501 + pool = [k for k in pool if k not in [gw, self.network, self.broadcast]] pool.reverse() if len(pool) == 1: pool, = pool @@ -415,6 +664,7 @@ def parse_options(self, pool=Net("192.168.1.128/25"), network="192.168.1.0/24", self.lease_time = lease_time self.renewal_time = renewal_time self.leases = {} + self.kwargs = kwargs def is_request(self, req): if not req.haslayer(BOOTP): @@ -424,7 +674,7 @@ def is_request(self, req): return 0 return 1 - def print_reply(self, req, reply): + def print_reply(self, _, reply): print("Reply %s to %s" % (reply.getlayer(IP).dst, reply.dst)) def make_reply(self, req): @@ -442,7 +692,7 @@ def make_reply(self, req): repb.siaddr = self.gw repb.ciaddr = self.gw repb.giaddr = self.gw - del(repb.payload) + del repb.payload rep = Ether(dst=mac) / IP(dst=ip) / UDP(sport=req.dport, dport=req.sport) / repb # noqa: E501 return rep @@ -453,18 +703,26 @@ class DHCP_am(BOOTP_am): def make_reply(self, req): resp = BOOTP_am.make_reply(self, req) if DHCP in req: - dhcp_options = [(op[0], {1: 2, 3: 5}.get(op[1], op[1])) - for op in req[DHCP].options - if isinstance(op, tuple) and op[0] == "message-type"] # noqa: E501 - dhcp_options += [("server_id", self.gw), - ("domain", self.domain), - ("router", self.gw), - ("name_server", self.gw), - ("broadcast_address", self.broadcast), - ("subnet_mask", self.netmask), - ("renewal_time", self.renewal_time), - ("lease_time", self.lease_time), - "end" - ] + dhcp_options = [ + (op[0], {1: 2, 3: 5}.get(op[1], op[1])) + for op in req[DHCP].options + if isinstance(op, tuple) and op[0] == "message-type" + ] + dhcp_options += [ + x for x in [ + ("server_id", self.gw), + ("domain", self.domain), + ("router", self.gw), + ("name_server", *self.nameserver), + ("broadcast_address", self.broadcast), + ("subnet_mask", self.netmask), + ("renewal_time", self.renewal_time), + ("lease_time", self.lease_time), + ] + if x[1] is not None + ] + if self.kwargs: + dhcp_options += self.kwargs.items() + dhcp_options.append("end") resp /= DHCP(options=dhcp_options) return resp diff --git a/scapy/layers/dhcp6.py b/scapy/layers/dhcp6.py index 4dc3257ba65..ca24bb17b25 100644 --- a/scapy/layers/dhcp6.py +++ b/scapy/layers/dhcp6.py @@ -1,32 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license - # Copyright (C) 2005 Guillaume Valadon # Arnaud Ebalard """ -DHCPv6: Dynamic Host Configuration Protocol for IPv6. [RFC 3315] +DHCPv6: Dynamic Host Configuration Protocol for IPv6. [RFC 3315,8415] """ -from __future__ import print_function import socket import struct import time from scapy.ansmachine import AnsweringMachine -from scapy.arch import get_if_raw_hwaddr, in6_getifaddr +from scapy.arch import get_if_hwaddr, in6_getifaddr from scapy.config import conf from scapy.data import EPOCH, ETHER_ANY -from scapy.compat import raw, orb, chb +from scapy.compat import raw, orb from scapy.error import warning from scapy.fields import BitField, ByteEnumField, ByteField, FieldLenField, \ - FlagsField, IntEnumField, IntField, MACField, PacketField, \ + FlagsField, IntEnumField, IntField, MACField, \ PacketListField, ShortEnumField, ShortField, StrField, StrFixedLenField, \ StrLenField, UTCTimeField, X3BytesField, XIntField, XShortEnumField, \ PacketLenField, UUIDField, FieldListField from scapy.data import IANA_ENTERPRISE_NUMBERS +from scapy.layers.dns import DNSStrField from scapy.layers.inet import UDP from scapy.layers.inet6 import DomainNameListField, IP6Field, IP6ListField, \ IPv6 @@ -35,7 +35,6 @@ from scapy.sendrecv import send from scapy.themes import Color from scapy.utils6 import in6_addrtovendor, in6_islladdr -import scapy.modules.six as six ############################################################################# # Helpers ## @@ -58,7 +57,10 @@ def get_cls(name, fallback_cls): 10: "DHCP6_Reconf", 11: "DHCP6_InfoRequest", 12: "DHCP6_RelayForward", - 13: "DHCP6_RelayReply"} + 13: "DHCP6_RelayReply", + 36: "DHCP6_AddrRegInform", + 37: "DHCP6_AddrRegReply", + } def _dhcp6_dispatcher(x, *args, **kargs): @@ -118,6 +120,7 @@ def _dhcp6_dispatcher(x, *args, **kargs): 41: "OPTION_NEW_POSIX_TIMEZONE", # RFC4833 42: "OPTION_NEW_TZDB_TIMEZONE", # RFC4833 48: "OPTION_LQ_CLIENT_LINK", # RFC5007 + 56: "OPTION_NTP_SERVER", # RFC5908 59: "OPT_BOOTFILE_URL", # RFC5970 60: "OPT_BOOTFILE_PARAM", # RFC5970 61: "OPTION_CLIENT_ARCH_TYPE", # RFC5970 @@ -125,7 +128,11 @@ def _dhcp6_dispatcher(x, *args, **kargs): 65: "OPTION_ERP_LOCAL_DOMAIN_NAME", # RFC6440 66: "OPTION_RELAY_SUPPLIED_OPTIONS", # RFC6422 68: "OPTION_VSS", # RFC6607 - 79: "OPTION_CLIENT_LINKLAYER_ADDR"} # RFC6939 + 79: "OPTION_CLIENT_LINKLAYER_ADDR", # RFC6939 + 103: "OPTION_CAPTIVE_PORTAL", # RFC8910 + 112: "OPTION_MUD_URL", # RFC8520 + 148: "OPTION_ADDR_REG_ENABLE", # RFC9686 + } dhcp6opts_by_code = {1: "DHCP6OptClientId", 2: "DHCP6OptServerId", @@ -173,6 +180,7 @@ def _dhcp6_dispatcher(x, *args, **kargs): # 46: "DHCP6OptLQClientTime", #RFC5007 # 47: "DHCP6OptLQRelayData", #RFC5007 48: "DHCP6OptLQClientLink", # RFC5007 + 56: "DHCP6OptNTPServer", # RFC5908 59: "DHCP6OptBootFileUrl", # RFC5790 60: "DHCP6OptBootFileParam", # RFC5970 61: "DHCP6OptClientArchType", # RFC5970 @@ -181,10 +189,14 @@ def _dhcp6_dispatcher(x, *args, **kargs): 66: "DHCP6OptRelaySuppliedOpt", # RFC6422 68: "DHCP6OptVSS", # RFC6607 79: "DHCP6OptClientLinkLayerAddr", # RFC6939 + 103: "DHCP6OptCaptivePortal", # RFC8910 + 112: "DHCP6OptMudUrl", # RFC8520 + 148: "DHCP6OptAddrRegEnable", # RFC9686 } -# sect 5.3 RFC 3315 : DHCP6 Messages types +# sect 7.3 RFC 8415 : DHCP6 Messages types +# also RFC 9686 dhcp6types = {1: "SOLICIT", 2: "ADVERTISE", 3: "REQUEST", @@ -197,7 +209,10 @@ def _dhcp6_dispatcher(x, *args, **kargs): 10: "RECONFIGURE", 11: "INFORMATION-REQUEST", 12: "RELAY-FORW", - 13: "RELAY-REPL"} + 13: "RELAY-REPL", + 36: "ADDR-REG-INFORM", + 37: "ADDR-REG-REPLY", + } ##################################################################### @@ -300,28 +315,38 @@ class DUID_UUID(Packet): # RFC 6355 class _DHCP6OptGuessPayload(Packet): - @classmethod + @staticmethod def _just_guess_payload_class(cls, payload): # try to guess what option is in the payload - cls = conf.raw_layer - if len(payload) > 2: - opt = struct.unpack("!H", payload[:2])[0] - cls = get_cls(dhcp6opts_by_code.get(opt, "DHCP6OptUnknown"), - DHCP6OptUnknown) - return cls + if len(payload) <= 2: + return conf.raw_layer + opt = struct.unpack("!H", payload[:2])[0] + clsname = dhcp6opts_by_code.get(opt, None) + if clsname is None: + return cls + return get_cls(clsname, cls) def guess_payload_class(self, payload): # this method is used in case of all derived classes # from _DHCP6OptGuessPayload in this file - cls = _DHCP6OptGuessPayload._just_guess_payload_class(payload) - return cls - + return _DHCP6OptGuessPayload._just_guess_payload_class( + DHCP6OptUnknown, + payload + ) + + +class _DHCP6OptGuessPayloadElt(_DHCP6OptGuessPayload): + """ + Same than _DHCP6OptGuessPayload but made for lists + in case of list of different suboptions + e.g. in ianaopts in DHCP6OptIA_NA + """ @classmethod def dispatch_hook(cls, payload=None, *args, **kargs): - # this classmethod is used in case of list of different suboptions - # e.g. in ianaopts in DHCP6OptIA_NA - cls_ = cls._just_guess_payload_class(payload) - return cls_ + return cls._just_guess_payload_class(conf.raw_layer, payload) + + def extract_padding(self, s): + return b"", s class DHCP6OptUnknown(_DHCP6OptGuessPayload): # A generic DHCPv6 Option @@ -332,61 +357,50 @@ class DHCP6OptUnknown(_DHCP6OptGuessPayload): # A generic DHCPv6 Option length_from=lambda pkt: pkt.optlen)] -class _DUIDField(PacketField): - __slots__ = ["length_from"] - - def __init__(self, name, default, length_from=None): - StrField.__init__(self, name, default) - self.length_from = length_from - - def i2m(self, pkt, i): - return raw(i) - - def m2i(self, pkt, x): - cls = conf.raw_layer - if len(x) > 4: - o = struct.unpack("!H", x[:2])[0] - cls = get_cls(duid_cls.get(o, conf.raw_layer), conf.raw_layer) - return cls(x) - - def getfield(self, pkt, s): - tmp_len = self.length_from(pkt) - return s[tmp_len:], self.m2i(pkt, s[:tmp_len]) +def _duid_dispatcher(x): + cls = conf.raw_layer + if len(x) > 4: + o = struct.unpack("!H", x[:2])[0] + cls = get_cls(duid_cls.get(o, conf.raw_layer), conf.raw_layer) + return cls(x) -class DHCP6OptClientId(_DHCP6OptGuessPayload): # RFC sect 22.2 +class DHCP6OptClientId(_DHCP6OptGuessPayload): # RFC 8415 sect 21.2 name = "DHCP6 Client Identifier Option" fields_desc = [ShortEnumField("optcode", 1, dhcp6opts), FieldLenField("optlen", None, length_of="duid", fmt="!H"), - _DUIDField("duid", "", - length_from=lambda pkt: pkt.optlen)] + PacketLenField("duid", "", _duid_dispatcher, + length_from=lambda pkt: pkt.optlen)] -class DHCP6OptServerId(DHCP6OptClientId): # RFC sect 22.3 +class DHCP6OptServerId(DHCP6OptClientId): # RFC 8415 sect 21.3 name = "DHCP6 Server Identifier Option" optcode = 2 # Should be encapsulated in the option field of IA_NA or IA_TA options # Can only appear at that location. -# TODO : last field IAaddr-options is not defined in the reference document -class DHCP6OptIAAddress(_DHCP6OptGuessPayload): # RFC sect 22.6 +class DHCP6OptIAAddress(_DHCP6OptGuessPayload): # RFC 8415 sect 21.6 name = "DHCP6 IA Address Option (IA_TA or IA_NA suboption)" fields_desc = [ShortEnumField("optcode", 5, dhcp6opts), FieldLenField("optlen", None, length_of="iaaddropts", fmt="!H", adjust=lambda pkt, x: x + 24), IP6Field("addr", "::"), - IntField("preflft", 0), - IntField("validlft", 0), - StrLenField("iaaddropts", "", - length_from=lambda pkt: pkt.optlen - 24)] + IntEnumField("preflft", 0, {0xffffffff: "infinity"}), + IntEnumField("validlft", 0, {0xffffffff: "infinity"}), + # last field IAaddr-options is not defined in the + # reference document. We copy what wireshark does: read + # more dhcp6 options and excpect failures + PacketListField("iaaddropts", [], + _DHCP6OptGuessPayloadElt, + length_from=lambda pkt: pkt.optlen - 24)] def guess_payload_class(self, payload): return conf.padding_layer -class DHCP6OptIA_NA(_DHCP6OptGuessPayload): # RFC sect 22.4 +class DHCP6OptIA_NA(_DHCP6OptGuessPayload): # RFC 8415 sect 21.4 name = "DHCP6 Identity Association for Non-temporary Addresses Option" fields_desc = [ShortEnumField("optcode", 3, dhcp6opts), FieldLenField("optlen", None, length_of="ianaopts", @@ -394,17 +408,17 @@ class DHCP6OptIA_NA(_DHCP6OptGuessPayload): # RFC sect 22.4 XIntField("iaid", None), IntField("T1", None), IntField("T2", None), - PacketListField("ianaopts", [], _DHCP6OptGuessPayload, + PacketListField("ianaopts", [], _DHCP6OptGuessPayloadElt, length_from=lambda pkt: pkt.optlen - 12)] -class DHCP6OptIA_TA(_DHCP6OptGuessPayload): # RFC sect 22.5 +class DHCP6OptIA_TA(_DHCP6OptGuessPayload): # RFC 8415 sect 21.5 name = "DHCP6 Identity Association for Temporary Addresses Option" fields_desc = [ShortEnumField("optcode", 4, dhcp6opts), FieldLenField("optlen", None, length_of="iataopts", fmt="!H", adjust=lambda pkt, x: x + 4), XIntField("iaid", None), - PacketListField("iataopts", [], _DHCP6OptGuessPayload, + PacketListField("iataopts", [], _DHCP6OptGuessPayloadElt, length_from=lambda pkt: pkt.optlen - 4)] @@ -414,7 +428,7 @@ class _OptReqListField(StrLenField): islist = 1 def i2h(self, pkt, x): - if x is None: + if not x: return [] return x @@ -450,7 +464,7 @@ def i2m(self, pkt, x): # Confirm or Information-request -class DHCP6OptOptReq(_DHCP6OptGuessPayload): # RFC sect 22.7 +class DHCP6OptOptReq(_DHCP6OptGuessPayload): # RFC 8415 sect 21.7 name = "DHCP6 Option Request Option" fields_desc = [ShortEnumField("optcode", 6, dhcp6opts), FieldLenField("optlen", None, length_of="reqopts", fmt="!H"), # noqa: E501 @@ -462,7 +476,7 @@ class DHCP6OptOptReq(_DHCP6OptGuessPayload): # RFC sect 22.7 # emise par un serveur pour affecter le choix fait par le client. Dans # les messages Advertise, a priori -class DHCP6OptPref(_DHCP6OptGuessPayload): # RFC sect 22.8 +class DHCP6OptPref(_DHCP6OptGuessPayload): # RFC 8415 sect 21.8 name = "DHCP6 Preference Option" fields_desc = [ShortEnumField("optcode", 7, dhcp6opts), ShortField("optlen", 1), @@ -478,7 +492,7 @@ def i2repr(self, pkt, x): return "%.2f sec" % (self.i2h(pkt, x) / 100.) -class DHCP6OptElapsedTime(_DHCP6OptGuessPayload): # RFC sect 22.9 +class DHCP6OptElapsedTime(_DHCP6OptGuessPayload): # RFC 8415 sect 21.9 name = "DHCP6 Elapsed Time Option" fields_desc = [ShortEnumField("optcode", 8, dhcp6opts), ShortField("optlen", 2), @@ -519,17 +533,31 @@ class DHCP6OptElapsedTime(_DHCP6OptGuessPayload): # RFC sect 22.9 # # Value Data as defined by field. - -# TODO : Decoding only at the moment -class DHCP6OptAuth(_DHCP6OptGuessPayload): # RFC sect 22.11 +# https://www.iana.org/assignments/auth-namespaces +_dhcp6_auth_proto = { + 0: "configuration token", + 1: "delayed authentication", + 2: "delayed authentication (obsolete)", + 3: "reconfigure key", +} +_dhcp6_auth_alg = { + 0: "configuration token", + 1: "HMAC-MD5", +} +_dhcp6_auth_rdm = { + 0: "use of a monotonically increasing value" +} + + +class DHCP6OptAuth(_DHCP6OptGuessPayload): # RFC 8415 sect 21.11 name = "DHCP6 Option - Authentication" fields_desc = [ShortEnumField("optcode", 11, dhcp6opts), FieldLenField("optlen", None, length_of="authinfo", - adjust=lambda pkt, x: x + 11), - ByteField("proto", 3), # TODO : XXX - ByteField("alg", 1), # TODO : XXX - ByteField("rdm", 0), # TODO : XXX - StrFixedLenField("replay", "A" * 8, 8), # TODO: XXX + fmt="!H", adjust=lambda pkt, x: x + 11), + ByteEnumField("proto", 3, _dhcp6_auth_proto), + ByteEnumField("alg", 1, _dhcp6_auth_alg), + ByteEnumField("rdm", 0, _dhcp6_auth_rdm), + StrFixedLenField("replay", b"\x00" * 8, 8), StrLenField("authinfo", "", length_from=lambda pkt: pkt.optlen - 11)] @@ -546,7 +574,7 @@ def i2m(self, pkt, x): return inet_pton(socket.AF_INET6, self.i2h(pkt, x)) -class DHCP6OptServerUnicast(_DHCP6OptGuessPayload): # RFC sect 22.12 +class DHCP6OptServerUnicast(_DHCP6OptGuessPayload): # RFC 8415 sect 21.12 name = "DHCP6 Server Unicast Option" fields_desc = [ShortEnumField("optcode", 12, dhcp6opts), ShortField("optlen", 16), @@ -555,7 +583,7 @@ class DHCP6OptServerUnicast(_DHCP6OptGuessPayload): # RFC sect 22.12 # DHCPv6 Status Code Option # -dhcp6statuscodes = {0: "Success", # sect 24.4 +dhcp6statuscodes = {0: "Success", # RFC 8415 sect 21.13 1: "UnspecFail", 2: "NoAddrsAvail", 3: "NoBinding", @@ -564,7 +592,7 @@ class DHCP6OptServerUnicast(_DHCP6OptGuessPayload): # RFC sect 22.12 6: "NoPrefixAvail"} # From RFC3633 -class DHCP6OptStatusCode(_DHCP6OptGuessPayload): # RFC sect 22.13 +class DHCP6OptStatusCode(_DHCP6OptGuessPayload): # RFC 8415 sect 21.13 name = "DHCP6 Status Code Option" fields_desc = [ShortEnumField("optcode", 13, dhcp6opts), FieldLenField("optlen", None, length_of="statusmsg", @@ -576,7 +604,7 @@ class DHCP6OptStatusCode(_DHCP6OptGuessPayload): # RFC sect 22.13 # DHCPv6 Rapid Commit Option # -class DHCP6OptRapidCommit(_DHCP6OptGuessPayload): # RFC sect 22.14 +class DHCP6OptRapidCommit(_DHCP6OptGuessPayload): # RFC 8415 sect 21.14 name = "DHCP6 Rapid Commit Option" fields_desc = [ShortEnumField("optcode", 14, dhcp6opts), ShortField("optlen", 0)] @@ -599,7 +627,7 @@ def getfield(self, pkt, s): if conf.padding_layer in p: pad = p[conf.padding_layer] remain = pad.load - del(pad.underlayer.payload) + del pad.underlayer.payload else: remain = "" lst.append(p) @@ -616,7 +644,7 @@ def guess_payload_class(self, payload): return conf.padding_layer -class DHCP6OptUserClass(_DHCP6OptGuessPayload): # RFC sect 22.15 +class DHCP6OptUserClass(_DHCP6OptGuessPayload): # RFC 8415 sect 21.15 name = "DHCP6 User Class Option" fields_desc = [ShortEnumField("optcode", 15, dhcp6opts), FieldLenField("optlen", None, fmt="!H", @@ -635,7 +663,7 @@ class VENDOR_CLASS_DATA(USER_CLASS_DATA): name = "vendor class data" -class DHCP6OptVendorClass(_DHCP6OptGuessPayload): # RFC sect 22.16 +class DHCP6OptVendorClass(_DHCP6OptGuessPayload): # RFC 8415 sect 21.16 name = "DHCP6 Vendor Class Option" fields_desc = [ShortEnumField("optcode", 16, dhcp6opts), FieldLenField("optlen", None, length_of="vcdata", fmt="!H", @@ -661,7 +689,7 @@ def guess_payload_class(self, payload): # The third one that will be used for nothing interesting -class DHCP6OptVendorSpecificInfo(_DHCP6OptGuessPayload): # RFC sect 22.17 +class DHCP6OptVendorSpecificInfo(_DHCP6OptGuessPayload): # RFC 8415 sect 21.17 name = "DHCP6 Vendor-specific Information Option" fields_desc = [ShortEnumField("optcode", 17, dhcp6opts), FieldLenField("optlen", None, length_of="vso", fmt="!H", @@ -677,7 +705,7 @@ class DHCP6OptVendorSpecificInfo(_DHCP6OptGuessPayload): # RFC sect 22.17 # masses critique. -class DHCP6OptIfaceId(_DHCP6OptGuessPayload): # RFC sect 22.18 +class DHCP6OptIfaceId(_DHCP6OptGuessPayload): # RFC 8415 sect 21.18 name = "DHCP6 Interface-Id Option" fields_desc = [ShortEnumField("optcode", 18, dhcp6opts), FieldLenField("optlen", None, fmt="!H", @@ -691,7 +719,7 @@ class DHCP6OptIfaceId(_DHCP6OptGuessPayload): # RFC sect 22.18 # A server includes a Reconfigure Message option in a Reconfigure # message to indicate to the client whether the client responds with a # renew message or an Information-request message. -class DHCP6OptReconfMsg(_DHCP6OptGuessPayload): # RFC sect 22.19 +class DHCP6OptReconfMsg(_DHCP6OptGuessPayload): # RFC 8415 sect 21.19 name = "DHCP6 Reconfigure Message Option" fields_desc = [ShortEnumField("optcode", 19, dhcp6opts), ShortField("optlen", 1), @@ -708,7 +736,7 @@ class DHCP6OptReconfMsg(_DHCP6OptGuessPayload): # RFC sect 22.19 # absence of this option, means unwillingness to accept reconfigure # messages, or instruction not to accept Reconfigure messages, for the # client and server messages, respectively. -class DHCP6OptReconfAccept(_DHCP6OptGuessPayload): # RFC sect 22.20 +class DHCP6OptReconfAccept(_DHCP6OptGuessPayload): # RFC 8415 sect 21.20 name = "DHCP6 Reconfigure Accept Option" fields_desc = [ShortEnumField("optcode", 20, dhcp6opts), ShortField("optlen", 0)] @@ -745,24 +773,28 @@ class DHCP6OptDNSDomains(_DHCP6OptGuessPayload): # RFC3646 DomainNameListField("dnsdomains", [], length_from=lambda pkt: pkt.optlen)] -# TODO: Implement iaprefopts correctly when provided with more -# information about it. - -class DHCP6OptIAPrefix(_DHCP6OptGuessPayload): # RFC3633 - name = "DHCP6 Option - IA_PD Prefix option" +class DHCP6OptIAPrefix(_DHCP6OptGuessPayload): # RFC 8415 sect 21.22 + name = "DHCP6 Option - IA Prefix option" fields_desc = [ShortEnumField("optcode", 26, dhcp6opts), FieldLenField("optlen", None, length_of="iaprefopts", adjust=lambda pkt, x: x + 25), - IntField("preflft", 0), - IntField("validlft", 0), + IntEnumField("preflft", 0, {0xffffffff: "infinity"}), + IntEnumField("validlft", 0, {0xffffffff: "infinity"}), ByteField("plen", 48), # TODO: Challenge that default value + # See RFC 8168 IP6Field("prefix", "2001:db8::"), # At least, global and won't hurt # noqa: E501 - StrLenField("iaprefopts", "", - length_from=lambda pkt: pkt.optlen - 25)] + # We copy what wireshark does: read more dhcp6 options and + # expect failures + PacketListField("iaprefopts", [], + _DHCP6OptGuessPayloadElt, + length_from=lambda pkt: pkt.optlen - 25)] + + def guess_payload_class(self, payload): + return conf.padding_layer -class DHCP6OptIA_PD(_DHCP6OptGuessPayload): # RFC3633 +class DHCP6OptIA_PD(_DHCP6OptGuessPayload): # RFC 8415 sect 21.21 name = "DHCP6 Option - Identity Association for Prefix Delegation" fields_desc = [ShortEnumField("optcode", 25, dhcp6opts), FieldLenField("optlen", None, length_of="iapdopt", @@ -770,7 +802,7 @@ class DHCP6OptIA_PD(_DHCP6OptGuessPayload): # RFC3633 XIntField("iaid", None), IntField("T1", None), IntField("T2", None), - PacketListField("iapdopt", [], _DHCP6OptGuessPayload, + PacketListField("iapdopt", [], _DHCP6OptGuessPayloadElt, length_from=lambda pkt: pkt.optlen - 12)] @@ -790,42 +822,20 @@ class DHCP6OptNISPServers(_DHCP6OptGuessPayload): # RFC3898 length_from=lambda pkt: pkt.optlen)] -class DomainNameField(StrLenField): - def getfield(self, pkt, s): - tmp_len = self.length_from(pkt) - return s[tmp_len:], self.m2i(pkt, s[:tmp_len]) - - def i2len(self, pkt, x): - return len(self.i2m(pkt, x)) - - def m2i(self, pkt, x): - cur = [] - while x: - tmp_len = orb(x[0]) - cur.append(x[1:1 + tmp_len]) - x = x[tmp_len + 1:] - return b".".join(cur) - - def i2m(self, pkt, x): - if not x: - return b"" - return b"".join(chb(len(z)) + z for z in x.split(b'.')) - - class DHCP6OptNISDomain(_DHCP6OptGuessPayload): # RFC3898 name = "DHCP6 Option - NIS Domain Name" fields_desc = [ShortEnumField("optcode", 29, dhcp6opts), FieldLenField("optlen", None, length_of="nisdomain"), - DomainNameField("nisdomain", "", - length_from=lambda pkt: pkt.optlen)] + DNSStrField("nisdomain", "", + length_from=lambda pkt: pkt.optlen)] class DHCP6OptNISPDomain(_DHCP6OptGuessPayload): # RFC3898 name = "DHCP6 Option - NIS+ Domain Name" fields_desc = [ShortEnumField("optcode", 30, dhcp6opts), FieldLenField("optlen", None, length_of="nispdomain"), - DomainNameField("nispdomain", "", - length_from=lambda pkt: pkt.optlen)] + DNSStrField("nispdomain", "", + length_from=lambda pkt: pkt.optlen)] class DHCP6OptSNTPServers(_DHCP6OptGuessPayload): # RFC4075 @@ -862,15 +872,30 @@ class DHCP6OptBCMCSServers(_DHCP6OptGuessPayload): # RFC4280 IP6ListField("bcmcsservers", [], length_from=lambda pkt: pkt.optlen)] -# TODO : Does Nothing at the moment +_dhcp6_geoconf_what = { + 0: "DHCP server", + 1: "closest network element", + 2: "client" +} -class DHCP6OptGeoConf(_DHCP6OptGuessPayload): # RFC-ietf-geopriv-dhcp-civil-09.txt # noqa: E501 - name = "" + +class DHCP6OptGeoConfElement(Packet): + fields_desc = [ByteField("CAtype", 0), + FieldLenField("CAlength", None, length_of="CAvalue"), + StrLenField("CAvalue", "", + length_from=lambda pkt: pkt.CAlength)] + + +class DHCP6OptGeoConf(_DHCP6OptGuessPayload): # RFC 4776 + name = "DHCP6 Option - Civic Location" fields_desc = [ShortEnumField("optcode", 36, dhcp6opts), - FieldLenField("optlen", None, length_of="optdata"), - StrLenField("optdata", "", - length_from=lambda pkt: pkt.optlen)] + FieldLenField("optlen", None, length_of="ca_elts", + adjust=lambda x: x + 3), + ByteEnumField("what", 2, _dhcp6_geoconf_what), + StrFixedLenField("country_code", "FR", 2), + PacketListField("ca_elts", [], DHCP6OptGeoConfElement, + length_from=lambda pkt: pkt.optlen - 3)] # TODO: see if we encounter opaque values from vendor devices @@ -885,19 +910,16 @@ class DHCP6OptRemoteID(_DHCP6OptGuessPayload): # RFC4649 StrLenField("remoteid", "", length_from=lambda pkt: pkt.optlen - 4)] -# TODO : 'subscriberid' default value should be at least 1 byte long - class DHCP6OptSubscriberID(_DHCP6OptGuessPayload): # RFC4580 name = "DHCP6 Option - Subscriber ID" fields_desc = [ShortEnumField("optcode", 38, dhcp6opts), FieldLenField("optlen", None, length_of="subscriberid"), + # subscriberid default value should be at least 1 byte long + # but we don't really care StrLenField("subscriberid", "", length_from=lambda pkt: pkt.optlen)] -# TODO : "The data in the Domain Name field MUST be encoded -# as described in Section 8 of [5]" - class DHCP6OptClientFQDN(_DHCP6OptGuessPayload): # RFC4704 name = "DHCP6 Option - Client FQDN" @@ -906,8 +928,8 @@ class DHCP6OptClientFQDN(_DHCP6OptGuessPayload): # RFC4704 adjust=lambda pkt, x: x + 1), BitField("res", 0, 5), FlagsField("flags", 0, 3, "SON"), - DomainNameField("fqdn", "", - length_from=lambda pkt: pkt.optlen - 1)] + DNSStrField("fqdn", "", + length_from=lambda pkt: pkt.optlen - 1)] class DHCP6OptPanaAuthAgent(_DHCP6OptGuessPayload): # RFC5192 @@ -951,6 +973,60 @@ class DHCP6OptLQClientLink(_DHCP6OptGuessPayload): # RFC5007 length_from=lambda pkt: pkt.optlen)] +class DHCP6NTPSubOptSrvAddr(Packet): # RFC5908 sect 4.1 + name = "DHCP6 NTP Server Address Suboption" + fields_desc = [ShortField("optcode", 1), + ShortField("optlen", 16), + IP6Field("addr", "::")] + + def extract_padding(self, s): + return b"", s + + +class DHCP6NTPSubOptMCAddr(Packet): # RFC5908 sect 4.2 + name = "DHCP6 NTP Multicast Address Suboption" + fields_desc = [ShortField("optcode", 2), + ShortField("optlen", 16), + IP6Field("addr", "::")] + + def extract_padding(self, s): + return b"", s + + +class DHCP6NTPSubOptSrvFQDN(Packet): # RFC5908 sect 4.3 + name = "DHCP6 NTP Server FQDN Suboption" + fields_desc = [ShortField("optcode", 3), + FieldLenField("optlen", None, length_of="fqdn"), + DNSStrField("fqdn", "", + length_from=lambda pkt: pkt.optlen)] + + def extract_padding(self, s): + return b"", s + + +_ntp_subopts = {1: DHCP6NTPSubOptSrvAddr, + 2: DHCP6NTPSubOptMCAddr, + 3: DHCP6NTPSubOptSrvFQDN} + + +def _ntp_subopt_dispatcher(p, **kwargs): + cls = conf.raw_layer + if len(p) >= 2: + o = struct.unpack("!H", p[:2])[0] + cls = _ntp_subopts.get(o, conf.raw_layer) + return cls(p, **kwargs) + + +class DHCP6OptNTPServer(_DHCP6OptGuessPayload): # RFC5908 + name = "DHCP6 NTP Server Option" + fields_desc = [ShortEnumField("optcode", 56, dhcp6opts), + FieldLenField("optlen", None, length_of="ntpserver", + fmt="!H"), + PacketListField("ntpserver", [], + _ntp_subopt_dispatcher, + length_from=lambda pkt: pkt.optlen)] + + class DHCP6OptBootFileUrl(_DHCP6OptGuessPayload): # RFC5970 name = "DHCP6 Boot File URL Option" fields_desc = [ShortEnumField("optcode", 59, dhcp6opts), @@ -991,7 +1067,8 @@ class DHCP6OptRelaySuppliedOpt(_DHCP6OptGuessPayload): # RFC6422 fields_desc = [ShortEnumField("optcode", 66, dhcp6opts), FieldLenField("optlen", None, length_of="relaysupplied", fmt="!H"), - PacketListField("relaysupplied", [], _DHCP6OptGuessPayload, + PacketListField("relaysupplied", [], + _DHCP6OptGuessPayloadElt, length_from=lambda pkt: pkt.optlen)] @@ -1017,6 +1094,30 @@ class DHCP6OptClientLinkLayerAddr(_DHCP6OptGuessPayload): # RFC6939 _LLAddrField("clladdr", ETHER_ANY)] +class DHCP6OptCaptivePortal(_DHCP6OptGuessPayload): # RFC8910 + name = "DHCP6 Option - Captive-Portal" + fields_desc = [ShortEnumField("optcode", 103, dhcp6opts), + FieldLenField("optlen", None, length_of="URI"), + StrLenField("URI", "", + length_from=lambda pkt: pkt.optlen)] + + +class DHCP6OptMudUrl(_DHCP6OptGuessPayload): # RFC8520 + name = "DHCP6 Option - MUD URL" + fields_desc = [ShortEnumField("optcode", 112, dhcp6opts), + FieldLenField("optlen", None, length_of="mudstring"), + StrLenField("mudstring", "", + length_from=lambda pkt: pkt.optlen, + max_length=253, + )] + + +class DHCP6OptAddrRegEnable(_DHCP6OptGuessPayload): # RFC 9686 sect 4.1 + name = "DHCP6 Address Registration Option" + fields_desc = [ShortEnumField("optcode", 148, dhcp6opts), + ShortField("optlen", 0)] + + ##################################################################### # DHCPv6 messages # ##################################################################### @@ -1067,7 +1168,7 @@ def hashret(self): # Relayed message is seen as a payload. -class DHCP6OptRelayMsg(_DHCP6OptGuessPayload): # RFC sect 22.10 +class DHCP6OptRelayMsg(_DHCP6OptGuessPayload): # RFC 8415 sect 21.10 name = "DHCP6 Relay Message Option" fields_desc = [ShortEnumField("optcode", 9, dhcp6opts), FieldLenField("optlen", None, fmt="!H", @@ -1351,6 +1452,24 @@ def answers(self, other): self.peeraddr == other.peeraddr) +##################################################################### +# Address Registration-Inform Message (RFC 9686) +# - sent by clients who generated their own address and need it registered + +class DHCP6_AddrRegInform(DHCP6): + name = "DHCPv6 Information Request Message" + msgtype = 36 + +##################################################################### +# Address Registration-Reply Message (RFC 9686) +# - sent by servers who respond to the address registration-inform message + + +class DHCP6_AddrRegReply(DHCP6): + name = "DHCPv6 Information Reply Message" + msgtype = 37 + + bind_bottom_up(UDP, _dhcp6_dispatcher, {"dport": 547}) bind_bottom_up(UDP, _dhcp6_dispatcher, {"dport": 546}) @@ -1363,7 +1482,7 @@ class DHCPv6_am(AnsweringMachine): def usage(self): msg = """ DHCPv6_am.parse_options( dns="2001:500::1035", domain="localdomain, local", - duid=None, iface=conf.iface6, advpref=255, sntpservers=None, + duid=None, iface=conf.iface, advpref=255, sntpservers=None, sipdomains=None, sipservers=None, nisdomain=None, nisservers=None, nispdomain=None, nispservers=None, @@ -1377,7 +1496,7 @@ def usage(self): answering machine. iface : the interface to listen/reply on if you do not want to use - conf.iface6. + conf.iface. advpref : Value in [0,255] given to Advertise preference field. By default, 255 is used. Be aware that this specific @@ -1446,7 +1565,7 @@ def norm_list(val, param_name): return -1 if iface is None: - iface = conf.iface6 + iface = conf.iface self.debug = debug @@ -1502,8 +1621,7 @@ def norm_list(val, param_name): timeval = time.time() - delta # Mac Address - rawmac = get_if_raw_hwaddr(iface)[1] - mac = ":".join("%.02x" % orb(x) for x in rawmac) + mac = get_if_hwaddr(iface) self.duid = DUID_LLT(timeval=timeval, lladdr=mac) @@ -1513,6 +1631,7 @@ def norm_list(val, param_name): #### # Find the source address we will use + self.src_addr = None try: addr = next(x for x in in6_getifaddr() if x[2] == iface and in6_islladdr(x[0])) # noqa: E501 except (StopIteration, RuntimeError): @@ -1621,7 +1740,7 @@ def is_request(self, p): msg += ", ".join(addrs) + n print(msg) - # See sect 18.1.7 + # See RFC 3315 sect 18.1.7 # Sent by a client to warn us she has determined # one or more addresses assigned to her is already @@ -1696,14 +1815,14 @@ def _include_options(query, answer): reqopts = [] if query.haslayer(DHCP6OptOptReq): # add only asked ones reqopts = query[DHCP6OptOptReq].reqopts - for o, opt in six.iteritems(self.dhcpv6_options): + for o, opt in self.dhcpv6_options.items(): if o in reqopts: answer /= opt else: # advertise everything we have available # Should not happen has clients MUST include # and ORO in requests (sec 18.1.1) -- arno - for o, opt in six.iteritems(self.dhcpv6_options): + for o, opt in self.dhcpv6_options.items(): answer /= opt if msgtype == 1: # SOLICIT (See Sect 17.1 and 17.2 of RFC 3315) @@ -1756,7 +1875,7 @@ def _include_options(query, answer): client_duid = p[DHCP6OptClientId].duid resp = IPv6(src=self.src_addr, dst=req_src) resp /= UDP(sport=547, dport=546) - resp /= DHCP6_Solicit(trid=trid) + resp /= DHCP6_Reply(trid=trid) resp /= DHCP6OptServerId(duid=self.duid) resp /= DHCP6OptClientId(duid=client_duid) @@ -1810,7 +1929,7 @@ def _include_options(query, answer): pass elif msgtype == 8: # RELEASE - # See section 18.1.6 + # See RFC 3315 section 18.1.6 # Message is sent to the server to indicate that # she will no longer use the addresses that was assigned @@ -1825,7 +1944,7 @@ def _include_options(query, answer): pass elif msgtype == 9: # DECLINE - # See section 18.1.7 + # See RFC 3315 section 18.1.7 pass elif msgtype == 11: # INFO-REQUEST @@ -1845,7 +1964,7 @@ def _include_options(query, answer): resp /= DHCP6OptClientId(duid=client_duid) # Stack requested options if available - for o, opt in six.iteritems(self.dhcpv6_options): + for o, opt in self.dhcpv6_options.items(): resp /= opt return resp diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py old mode 100755 new mode 100644 index fd8bd4e7e5a..a45044c5532 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -1,105 +1,211 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ -DNS: Domain Name System. +DNS: Domain Name System + +This implements: +- RFC1035: Domain Names +- RFC6762: Multicast DNS +- RFC6763: DNS-Based Service Discovery """ -from __future__ import absolute_import +import abc +import collections +import operator +import itertools +import socket import struct import time +import warnings -from scapy.config import conf -from scapy.packet import Packet, bind_layers, NoPayload -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - ConditionalField, FieldLenField, FlagsField, IntField, \ - PacketListField, ShortEnumField, ShortField, StrField, StrFixedLenField, \ - StrLenField, MultipleTypeField, UTCTimeField -from scapy.compat import orb, raw, chb, bytes_encode +from scapy.arch import ( + get_if_addr, + get_if_addr6, + read_nameservers, +) from scapy.ansmachine import AnsweringMachine -from scapy.sendrecv import sr1 +from scapy.base_classes import Net, ScopedIP +from scapy.config import conf +from scapy.compat import raw, chb, bytes_encode, plain_str +from scapy.error import log_runtime, warning, Scapy_Exception +from scapy.packet import Packet, bind_layers, Raw +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + Field, + FieldLenField, + FieldListField, + FlagsField, + I, + IP6Field, + IntField, + MACField, + MultipleTypeField, + PacketListField, + ShortEnumField, + ShortField, + StrField, + StrLenField, + UTCTimeField, + XStrFixedLenField, + XStrLenField, +) +from scapy.interfaces import resolve_iface +from scapy.sendrecv import sr1, sr +from scapy.supersocket import StreamSocket +from scapy.plist import SndRcvList, _PacketList, QueryAnswer +from scapy.pton_ntop import inet_ntop, inet_pton +from scapy.utils import pretty_list +from scapy.volatile import RandShort + +from scapy.layers.l2 import Ether from scapy.layers.inet import IP, DestIPField, IPField, UDP, TCP -from scapy.layers.inet6 import DestIP6Field, IP6Field -from scapy.error import warning, Scapy_Exception -import scapy.modules.six as six -from scapy.modules.six.moves import range +from scapy.layers.inet6 import IPv6 + +from typing import ( + Any, + List, + Optional, + Tuple, + Type, + Union, +) + + +# https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 +dnstypes = { + 0: "RESERVED", + 1: "A", 2: "NS", 3: "MD", 4: "MF", 5: "CNAME", 6: "SOA", 7: "MB", 8: "MG", + 9: "MR", 10: "NULL", 11: "WKS", 12: "PTR", 13: "HINFO", 14: "MINFO", + 15: "MX", 16: "TXT", 17: "RP", 18: "AFSDB", 19: "X25", 20: "ISDN", + 21: "RT", 22: "NSAP", 23: "NSAP-PTR", 24: "SIG", 25: "KEY", 26: "PX", + 27: "GPOS", 28: "AAAA", 29: "LOC", 30: "NXT", 31: "EID", 32: "NIMLOC", + 33: "SRV", 34: "ATMA", 35: "NAPTR", 36: "KX", 37: "CERT", 38: "A6", + 39: "DNAME", 40: "SINK", 41: "OPT", 42: "APL", 43: "DS", 44: "SSHFP", + 45: "IPSECKEY", 46: "RRSIG", 47: "NSEC", 48: "DNSKEY", 49: "DHCID", + 50: "NSEC3", 51: "NSEC3PARAM", 52: "TLSA", 53: "SMIMEA", 55: "HIP", + 56: "NINFO", 57: "RKEY", 58: "TALINK", 59: "CDS", 60: "CDNSKEY", + 61: "OPENPGPKEY", 62: "CSYNC", 63: "ZONEMD", 64: "SVCB", 65: "HTTPS", + 99: "SPF", 100: "UINFO", 101: "UID", 102: "GID", 103: "UNSPEC", 104: "NID", + 105: "L32", 106: "L64", 107: "LP", 108: "EUI48", 109: "EUI64", 249: "TKEY", + 250: "TSIG", 256: "URI", 257: "CAA", 258: "AVC", 259: "DOA", + 260: "AMTRELAY", 32768: "TA", 32769: "DLV", 65535: "RESERVED" +} + + +dnsqtypes = {251: "IXFR", 252: "AXFR", 253: "MAILB", 254: "MAILA", 255: "ALL"} +dnsqtypes.update(dnstypes) +dnsclasses = {1: 'IN', 2: 'CS', 3: 'CH', 4: 'HS', 255: 'ANY'} + + +# 12/2023 from https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml # noqa: E501 +dnssecalgotypes = {0: "Reserved", 1: "RSA/MD5", 2: "Diffie-Hellman", 3: "DSA/SHA-1", # noqa: E501 + 4: "Reserved", 5: "RSA/SHA-1", 6: "DSA-NSEC3-SHA1", + 7: "RSASHA1-NSEC3-SHA1", 8: "RSA/SHA-256", 9: "Reserved", + 10: "RSA/SHA-512", 11: "Reserved", 12: "GOST R 34.10-2001", + 13: "ECDSA Curve P-256 with SHA-256", 14: "ECDSA Curve P-384 with SHA-384", # noqa: E501 + 15: "Ed25519", 16: "Ed448", + 252: "Reserved for Indirect Keys", 253: "Private algorithms - domain name", # noqa: E501 + 254: "Private algorithms - OID", 255: "Reserved"} + +# 12/2023 from https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml +dnssecdigesttypes = {0: "Reserved", 1: "SHA-1", 2: "SHA-256", 3: "GOST R 34.11-94", 4: "SHA-384"} # noqa: E501 + +# 12/2023 from https://www.iana.org/assignments/dnssec-nsec3-parameters/dnssec-nsec3-parameters.xhtml # noqa: E501 +dnssecnsec3algotypes = {0: "Reserved", 1: "SHA-1"} -def dns_get_str(s, pointer=0, pkt=None, _fullpacket=False): +def dns_get_str(s, full=None, _ignore_compression=False): """This function decompresses a string s, starting from the given pointer. :param s: the string to decompress - :param pointer: first pointer on the string (default: 0) - :param pkt: (optional) an InheritOriginDNSStrPacket packet + :param full: (optional) the full packet (used for decompression) :returns: (decoded_string, end_index, left_string) """ - # The _fullpacket parameter is reserved for scapy. It indicates - # that the string provided is the full dns packet, and thus - # will be the same than pkt._orig_str. The "Cannot decompress" - # error will not be prompted if True. + # _ignore_compression is for internal use only max_length = len(s) # The result = the extracted name name = b"" # Will contain the index after the pointer, to be returned after_pointer = None processed_pointers = [] # Used to check for decompression loops - # Analyse given pkt - if pkt and hasattr(pkt, "_orig_s") and pkt._orig_s: - s_full = pkt._orig_s - else: - s_full = None bytes_left = None + _fullpacket = False # s = full packet + pointer = 0 while True: if abs(pointer) >= max_length: - warning("DNS RR prematured end (ofs=%i, len=%i)" % (pointer, - len(s))) + log_runtime.info( + "DNS RR prematured end (ofs=%i, len=%i)", pointer, len(s) + ) break - cur = orb(s[pointer]) # get pointer value + cur = s[pointer] # get pointer value pointer += 1 # make pointer go forward if cur & 0xc0: # Label pointer if after_pointer is None: # after_pointer points to where the remaining bytes start, # as pointer will follow the jump token after_pointer = pointer + 1 + if _ignore_compression: + # skip + pointer += 1 + continue if pointer >= max_length: - warning("DNS incomplete jump token at (ofs=%i)" % pointer) + log_runtime.info( + "DNS incomplete jump token at (ofs=%i)", pointer + ) break + if not full: + raise Scapy_Exception("DNS message can't be compressed " + + "at this point!") # Follow the pointer - pointer = ((cur & ~0xc0) << 8) + orb(s[pointer]) - 12 + pointer = ((cur & ~0xc0) << 8) + s[pointer] if pointer in processed_pointers: warning("DNS decompression loop detected") break + if len(processed_pointers) >= 20: + warning("More than 20 jumps in a single DNS decompression ! " + "Dropping (evil packet)") + break if not _fullpacket: - # Do we have access to the whole packet ? - if s_full: - # Yes -> use it to continue - bytes_left = s[after_pointer:] - s = s_full - max_length = len(s) - _fullpacket = True - else: - # No -> abort - raise Scapy_Exception("DNS message can't be compressed" + - "at this point!") + # We switch our s buffer to full, so we need to remember + # the previous context + bytes_left = s[after_pointer:] + s = full + max_length = len(s) + _fullpacket = True processed_pointers.append(pointer) continue elif cur > 0: # Label # cur = length of the string name += s[pointer:pointer + cur] + b"." pointer += cur - else: + else: # End break if after_pointer is not None: # Return the real end index (not the one we followed) pointer = after_pointer if bytes_left is None: bytes_left = s[pointer:] - # name, end_index, remaining - return name, pointer, bytes_left + # name, remaining + return name or b".", bytes_left + + +def _is_ptr(x): + """ + Heuristic to guess if bytes are an encoded DNS pointer. + """ + return ( + (x and x[-1] == 0) or + (len(x) >= 2 and (x[-2] & 0xc0) == 0xc0) + ) def dns_encode(x, check_built=False): @@ -112,9 +218,7 @@ def dns_encode(x, check_built=False): if not x or x == b".": return b"\x00" - if check_built and b"." not in x and ( - orb(x[-1]) == 0 or (orb(x[-2]) & 0xc0) == 0xc0 - ): + if check_built and _is_ptr(x): # The value has already been processed. Do not process it again return x @@ -127,8 +231,11 @@ def dns_encode(x, check_built=False): def DNSgetstr(*args, **kwargs): """Legacy function. Deprecated""" - warning("DNSgetstr deprecated. Use dns_get_str instead") - return dns_get_str(*args, **kwargs) + warnings.warn( + "DNSgetstr is deprecated. Use dns_get_str instead.", + DeprecationWarning + ) + return dns_get_str(*args, **kwargs)[:-1] def dns_compress(pkt): @@ -138,32 +245,31 @@ def dns_compress(pkt): raise Scapy_Exception("Can only compress DNS layers") pkt = pkt.copy() dns_pkt = pkt.getlayer(DNS) + dns_pkt.clear_cache() build_pkt = raw(dns_pkt) def field_gen(dns_pkt): """Iterates through all DNS strings that can be compressed""" for lay in [dns_pkt.qd, dns_pkt.an, dns_pkt.ns, dns_pkt.ar]: - if lay is None: + if not lay: continue - current = lay - while not isinstance(current, NoPayload): - if isinstance(current, InheritOriginDNSStrPacket): - for field in current.fields_desc: - if isinstance(field, DNSStrField) or \ - (isinstance(field, MultipleTypeField) and - current.type in [2, 3, 4, 5, 12, 15]): - # Get the associated data and store it accordingly # noqa: E501 - dat = current.getfieldval(field.name) - yield current, field.name, dat - current = current.payload + for current in lay: + for field in current.fields_desc: + if isinstance(field, DNSStrField) or \ + (isinstance(field, MultipleTypeField) and + current.type in [2, 3, 4, 5, 12, 15, 39, 47]): + # Get the associated data and store it accordingly # noqa: E501 + dat = current.getfieldval(field.name) + yield current, field.name, dat def possible_shortens(dat): """Iterates through all possible compression parts in a DNS string""" + if dat == b".": # we'd lose by compressing it + return yield dat for x in range(1, dat.count(b".")): yield dat.split(b".", x)[x] data = {} - burned_data = 0 for current, name, dat in field_gen(dns_pkt): for part in possible_shortens(dat): # Encode the data @@ -173,18 +279,21 @@ def possible_shortens(dat): # possible pointer for future strings. # We get the index of the encoded data index = build_pkt.index(encoded) - index -= burned_data # The following is used to build correctly the pointer fb_index = ((index >> 8) | 0xc0) sb_index = index - (256 * (fb_index - 0xc0)) pointer = chb(fb_index) + chb(sb_index) - data[part] = [(current, name, pointer)] + data[part] = [(current, name, pointer, index + 1)] else: # This string already exists, let's mark the current field # with it, so that it gets compressed data[part].append((current, name)) - # calculate spared space - burned_data += len(encoded) - 2 + _in = data[part][0][3] + build_pkt = build_pkt[:_in] + build_pkt[_in:].replace( + encoded, + b"\0\0", + 1 + ) break # Apply compression rules for ck in data: @@ -203,7 +312,7 @@ def possible_shortens(dat): new_val = kept_string + replace_pointer rep[0].setfieldval(rep[1], new_val) try: - del(rep[0].rdlen) + del rep[0].rdlen except AttributeError: pass # End of the compression algorithm @@ -214,13 +323,13 @@ def possible_shortens(dat): return dns_pkt -class InheritOriginDNSStrPacket(Packet): - __slots__ = Packet.__slots__ + ["_orig_s", "_orig_p"] - - def __init__(self, _pkt=None, _orig_s=None, _orig_p=None, *args, **kwargs): - self._orig_s = _orig_s - self._orig_p = _orig_p - Packet.__init__(self, _pkt=_pkt, *args, **kwargs) +class DNSCompressedPacket(Packet): + """ + Class to mark that a packet contains DNSStrField and supports compression + """ + @abc.abstractmethod + def get_full(self): + pass class DNSStrField(StrLenField): @@ -229,10 +338,24 @@ class DNSStrField(StrLenField): It will also handle DNS decompression. (may be StrLenField if a length_from is passed), """ + def any2i(self, pkt, x): + if x and isinstance(x, list): + return [self.h2i(pkt, y) for y in x] + return super(DNSStrField, self).any2i(pkt, x) def h2i(self, pkt, x): + # Setting a DNSStrField manually (h2i) means any current compression will break + if ( + pkt and + isinstance(pkt.parent, DNSCompressedPacket) and + pkt.parent.raw_packet_cache + ): + pkt.parent.clear_cache() if not x: return b"." + x = bytes_encode(x) + if x[-1:] != b"." and not _is_ptr(x): + return x + b"." return x def i2m(self, pkt, x): @@ -241,103 +364,23 @@ def i2m(self, pkt, x): def i2len(self, pkt, x): return len(self.i2m(pkt, x)) + def get_full(self, pkt): + while pkt and not isinstance(pkt, DNSCompressedPacket): + pkt = pkt.parent or pkt.underlayer + if not pkt: + return None + return pkt.get_full() + def getfield(self, pkt, s): remain = b"" if self.length_from: - remain, s = StrLenField.getfield(self, pkt, s) + remain, s = super(DNSStrField, self).getfield(pkt, s) # Decode the compressed DNS message - decoded, _, left = dns_get_str(s, 0, pkt) + decoded, left = dns_get_str(s, full=self.get_full(pkt)) # returns (remaining, decoded) return left + remain, decoded -class DNSRRCountField(ShortField): - __slots__ = ["rr"] - - def __init__(self, name, default, rr): - ShortField.__init__(self, name, default) - self.rr = rr - - def _countRR(self, pkt): - x = getattr(pkt, self.rr) - i = 0 - while isinstance(x, DNSRR) or isinstance(x, DNSQR) or isdnssecRR(x): - x = x.payload - i += 1 - return i - - def i2m(self, pkt, x): - if x is None: - x = self._countRR(pkt) - return x - - def i2h(self, pkt, x): - if x is None: - x = self._countRR(pkt) - return x - - -class DNSRRField(StrField): - __slots__ = ["countfld", "passon"] - holds_packets = 1 - - def __init__(self, name, countfld, passon=1): - StrField.__init__(self, name, None) - self.countfld = countfld - self.passon = passon - - def i2m(self, pkt, x): - if x is None: - return b"" - return bytes_encode(x) - - def decodeRR(self, name, s, p): - ret = s[p:p + 10] - # type, cls, ttl, rdlen - typ, cls, _, rdlen = struct.unpack("!HHIH", ret) - p += 10 - cls = DNSRR_DISPATCHER.get(typ, DNSRR) - rr = cls(b"\x00" + ret + s[p:p + rdlen], _orig_s=s, _orig_p=p) - # Will have changed because of decompression - rr.rdlen = None - rr.rrname = name - - p += rdlen - return rr, p - - def getfield(self, pkt, s): - if isinstance(s, tuple): - s, p = s - else: - p = 0 - ret = None - c = getattr(pkt, self.countfld) - if c > len(s): - warning("wrong value: DNS.%s=%i", self.countfld, c) - return s, b"" - while c: - c -= 1 - name, p, _ = dns_get_str(s, p, _fullpacket=True) - rr, p = self.decodeRR(name, s, p) - if ret is None: - ret = rr - else: - ret.add_payload(rr) - if self.passon: - return (s, p), ret - else: - return s[p:], ret - - -class DNSQRField(DNSRRField): - def decodeRR(self, name, s, p): - ret = s[p:p + 4] - p += 4 - rr = DNSQR(b"\x00" + ret, _orig_s=s, _orig_p=p) - rr.qname = name - return rr, p - - class DNSTextField(StrLenField): """ Special StrLenField that handles DNS TEXT data (16) @@ -345,15 +388,23 @@ class DNSTextField(StrLenField): islist = 1 + def i2h(self, pkt, x): + if not x: + return [] + return x + def m2i(self, pkt, s): ret_s = list() tmp_s = s # RDATA contains a list of strings, each are prepended with # a byte containing the size of the following string. while tmp_s: - tmp_len = orb(tmp_s[0]) + 1 + tmp_len = tmp_s[0] + 1 if tmp_len > len(tmp_s): - warning("DNS RR TXT prematured end of character-string (size=%i, remaining bytes=%i)" % (tmp_len, len(tmp_s))) # noqa: E501 + log_runtime.info( + "DNS RR TXT prematured end of character-string " + "(size=%i, remaining bytes=%i)", tmp_len, len(tmp_s) + ) ret_s.append(tmp_s[1:tmp_len]) tmp_s = tmp_s[tmp_len:] return ret_s @@ -369,6 +420,9 @@ def i2len(self, pkt, x): def i2m(self, pkt, s): ret_s = b"" for text in s: + if not text: + ret_s += b"\x00" + continue text = bytes_encode(text) # The initial string must be split into a list of strings # prepended with theirs sizes. @@ -381,157 +435,257 @@ def i2m(self, pkt, s): return ret_s -class DNS(Packet): - name = "DNS" - fields_desc = [ - ConditionalField(ShortField("length", None), - lambda p: isinstance(p.underlayer, TCP)), - ShortField("id", 0), - BitField("qr", 0, 1), - BitEnumField("opcode", 0, 4, {0: "QUERY", 1: "IQUERY", 2: "STATUS"}), - BitField("aa", 0, 1), - BitField("tc", 0, 1), - BitField("rd", 1, 1), - BitField("ra", 0, 1), - BitField("z", 0, 1), - # AD and CD bits are defined in RFC 2535 - BitField("ad", 0, 1), # Authentic Data - BitField("cd", 0, 1), # Checking Disabled - BitEnumField("rcode", 0, 4, {0: "ok", 1: "format-error", - 2: "server-failure", 3: "name-error", - 4: "not-implemented", 5: "refused"}), - DNSRRCountField("qdcount", None, "qd"), - DNSRRCountField("ancount", None, "an"), - DNSRRCountField("nscount", None, "ns"), - DNSRRCountField("arcount", None, "ar"), - DNSQRField("qd", "qdcount"), - DNSRRField("an", "ancount"), - DNSRRField("ns", "nscount"), - DNSRRField("ar", "arcount", 0), - ] - - def answers(self, other): - return (isinstance(other, DNS) and - self.id == other.id and - self.qr == 1 and - other.qr == 0) - - def mysummary(self): - name = "" - if self.qr: - type = "Ans" - if self.ancount > 0 and isinstance(self.an, DNSRR): - name = ' "%s"' % self.an.rdata - else: - type = "Qry" - if self.qdcount > 0 and isinstance(self.qd, DNSQR): - name = ' "%s"' % self.qd.qname - return 'DNS %s%s ' % (type, name) - - def post_build(self, pkt, pay): - if isinstance(self.underlayer, TCP) and self.length is None: - pkt = struct.pack("!H", len(pkt) - 2) + pkt[2:] - return pkt + pay - - def compress(self): - """Return the compressed DNS packet (using `dns_compress()`""" - return dns_compress(self) - - def pre_dissect(self, s): - """ - Check that a valid DNS over TCP message can be decoded - """ - if isinstance(self.underlayer, TCP): - - # Compute the length of the DNS packet - if len(s) >= 2: - dns_len = struct.unpack("!H", s[:2])[0] - else: - message = "Malformed DNS message: too small!" - warning(message) - raise Scapy_Exception(message) - - # Check if the length is valid - if dns_len < 14 or len(s) < dns_len: - message = "Malformed DNS message: invalid length!" - warning(message) - raise Scapy_Exception(message) - - return s - - -# https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 -dnstypes = { - 0: "ANY", - 1: "A", 2: "NS", 3: "MD", 4: "MF", 5: "CNAME", 6: "SOA", 7: "MB", 8: "MG", - 9: "MR", 10: "NULL", 11: "WKS", 12: "PTR", 13: "HINFO", 14: "MINFO", - 15: "MX", 16: "TXT", 17: "RP", 18: "AFSDB", 19: "X25", 20: "ISDN", 21: "RT", # noqa: E501 - 22: "NSAP", 23: "NSAP-PTR", 24: "SIG", 25: "KEY", 26: "PX", 27: "GPOS", - 28: "AAAA", 29: "LOC", 30: "NXT", 31: "EID", 32: "NIMLOC", 33: "SRV", - 34: "ATMA", 35: "NAPTR", 36: "KX", 37: "CERT", 38: "A6", 39: "DNAME", - 40: "SINK", 41: "OPT", 42: "APL", 43: "DS", 44: "SSHFP", 45: "IPSECKEY", - 46: "RRSIG", 47: "NSEC", 48: "DNSKEY", 49: "DHCID", 50: "NSEC3", - 51: "NSEC3PARAM", 52: "TLSA", 53: "SMIMEA", 55: "HIP", 56: "NINFO", 57: "RKEY", # noqa: E501 - 58: "TALINK", 59: "CDS", 60: "CDNSKEY", 61: "OPENPGPKEY", 62: "CSYNC", - 99: "SPF", 100: "UINFO", 101: "UID", 102: "GID", 103: "UNSPEC", 104: "NID", - 105: "L32", 106: "L64", 107: "LP", 108: "EUI48", 109: "EUI64", - 249: "TKEY", 250: "TSIG", 256: "URI", 257: "CAA", 258: "AVC", - 32768: "TA", 32769: "DLV", 65535: "RESERVED" -} +# RFC 2671 - Extension Mechanisms for DNS (EDNS0) -dnsqtypes = {251: "IXFR", 252: "AXFR", 253: "MAILB", 254: "MAILA", 255: "ALL"} -dnsqtypes.update(dnstypes) -dnsclasses = {1: 'IN', 2: 'CS', 3: 'CH', 4: 'HS', 255: 'ANY'} +edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Owner", + 5: "DAU", 6: "DHU", 7: "N3U", 8: "edns-client-subnet", 10: "COOKIE", + 15: "Extended DNS Error"} -class DNSQR(InheritOriginDNSStrPacket): - name = "DNS Question Record" - show_indent = 0 - fields_desc = [DNSStrField("qname", "www.example.com"), - ShortEnumField("qtype", 1, dnsqtypes), - ShortEnumField("qclass", 1, dnsclasses)] +class _EDNS0Dummy(Packet): + name = "Dummy class that implements extract_padding()" + def extract_padding(self, p): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + return "", p -# RFC 2671 - Extension Mechanisms for DNS (EDNS0) -class EDNS0TLV(Packet): +class EDNS0TLV(_EDNS0Dummy): name = "DNS EDNS0 TLV" - fields_desc = [ShortEnumField("optcode", 0, {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Reserved", 5: "PING"}), # noqa: E501 + fields_desc = [ShortEnumField("optcode", 0, edns0types), FieldLenField("optlen", None, "optdata", fmt="H"), - StrLenField("optdata", "", length_from=lambda pkt: pkt.optlen)] # noqa: E501 + StrLenField("optdata", "", + length_from=lambda pkt: pkt.optlen)] - def extract_padding(self, p): - return "", p + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + # type: (Optional[bytes], *Any, **Any) -> Type[Packet] + if _pkt is None: + return EDNS0TLV + if len(_pkt) < 2: + return Raw + edns0type = struct.unpack("!H", _pkt[:2])[0] + return EDNS0OPT_DISPATCHER.get(edns0type, EDNS0TLV) -class DNSRROPT(InheritOriginDNSStrPacket): +class DNSRROPT(Packet): name = "DNS OPT Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 41, dnstypes), - ShortField("rclass", 4096), + ShortEnumField("rclass", 4096, dnsclasses), ByteField("extrcode", 0), ByteField("version", 0), # version 0 means EDNS0 BitEnumField("z", 32768, 16, {32768: "D0"}), # D0 means DNSSEC OK from RFC 3225 FieldLenField("rdlen", None, length_of="rdata", fmt="H"), - PacketListField("rdata", [], EDNS0TLV, length_from=lambda pkt: pkt.rdlen)] # noqa: E501 + PacketListField("rdata", [], EDNS0TLV, + length_from=lambda pkt: pkt.rdlen)] + + +# draft-cheshire-edns0-owner-option-01 - EDNS0 OWNER Option + +class EDNS0OWN(_EDNS0Dummy): + name = "EDNS0 Owner (OWN)" + fields_desc = [ShortEnumField("optcode", 4, edns0types), + FieldLenField("optlen", None, count_of="primary_mac", fmt="H"), + ByteField("v", 0), + ByteField("s", 0), + MACField("primary_mac", "00:00:00:00:00:00"), + ConditionalField( + MACField("wakeup_mac", "00:00:00:00:00:00"), + lambda pkt: (pkt.optlen or 0) >= 14), + ConditionalField( + StrLenField("password", "", + length_from=lambda pkt: pkt.optlen - 14), + lambda pkt: (pkt.optlen or 0) >= 18)] -# RFC 4034 - Resource Records for the DNS Security Extensions + def post_build(self, pkt, pay): + pkt += pay + if self.optlen is None: + pkt = pkt[:2] + struct.pack("!H", len(pkt) - 4) + pkt[4:] + return pkt -# 09/2013 from http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml # noqa: E501 -dnssecalgotypes = {0: "Reserved", 1: "RSA/MD5", 2: "Diffie-Hellman", 3: "DSA/SHA-1", # noqa: E501 - 4: "Reserved", 5: "RSA/SHA-1", 6: "DSA-NSEC3-SHA1", - 7: "RSASHA1-NSEC3-SHA1", 8: "RSA/SHA-256", 9: "Reserved", - 10: "RSA/SHA-512", 11: "Reserved", 12: "GOST R 34.10-2001", - 13: "ECDSA Curve P-256 with SHA-256", 14: "ECDSA Curve P-384 with SHA-384", # noqa: E501 - 252: "Reserved for Indirect Keys", 253: "Private algorithms - domain name", # noqa: E501 - 254: "Private algorithms - OID", 255: "Reserved"} +# RFC 6975 - Signaling Cryptographic Algorithm Understanding in +# DNS Security Extensions (DNSSEC) + +class EDNS0DAU(_EDNS0Dummy): + name = "DNSSEC Algorithm Understood (DAU)" + fields_desc = [ShortEnumField("optcode", 5, edns0types), + FieldLenField("optlen", None, count_of="alg_code", fmt="H"), + FieldListField("alg_code", None, + ByteEnumField("", 0, dnssecalgotypes), + count_from=lambda pkt:pkt.optlen)] + + +class EDNS0DHU(_EDNS0Dummy): + name = "DS Hash Understood (DHU)" + fields_desc = [ShortEnumField("optcode", 6, edns0types), + FieldLenField("optlen", None, count_of="alg_code", fmt="H"), + FieldListField("alg_code", None, + ByteEnumField("", 0, dnssecdigesttypes), + count_from=lambda pkt:pkt.optlen)] -# 09/2013 from http://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml -dnssecdigesttypes = {0: "Reserved", 1: "SHA-1", 2: "SHA-256", 3: "GOST R 34.11-94", 4: "SHA-384"} # noqa: E501 +class EDNS0N3U(_EDNS0Dummy): + name = "NSEC3 Hash Understood (N3U)" + fields_desc = [ShortEnumField("optcode", 7, edns0types), + FieldLenField("optlen", None, count_of="alg_code", fmt="H"), + FieldListField("alg_code", None, + ByteEnumField("", 0, dnssecnsec3algotypes), + count_from=lambda pkt:pkt.optlen)] + + +# RFC 7871 - Client Subnet in DNS Queries + +class ClientSubnetv4(StrLenField): + af_familly = socket.AF_INET + af_length = 32 + af_default = b"\xc0" # 192.0.0.0 + + def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, I] + sz = operator.floordiv(self.length_from(pkt) + 7, 8) + sz = min(sz, operator.floordiv(self.af_length, 8)) + return s[sz:], self.m2i(pkt, s[:sz]) + + def m2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> str + padding = self.af_length - self.length_from(pkt) + if padding: + x += b"\x00" * operator.floordiv(padding, 8) + x = x[: operator.floordiv(self.af_length, 8)] + return inet_ntop(self.af_familly, x) + + def _pack_subnet(self, subnet): + # type: (bytes) -> bytes + packed_subnet = inet_pton(self.af_familly, plain_str(subnet)) + for i in list(range(operator.floordiv(self.af_length, 8)))[::-1]: + if packed_subnet[i] != 0: + i += 1 + break + return packed_subnet[:i] + + def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[Union[str, Net]]) -> bytes + if x is None: + return self.af_default + try: + return self._pack_subnet(x) + except (OSError, socket.error): + pkt.family = 2 + return ClientSubnetv6("", "")._pack_subnet(x) + + def i2len(self, pkt, x): + # type: (Packet, Any) -> int + if x is None: + return 1 + try: + return len(self._pack_subnet(x)) + except (OSError, socket.error): + pkt.family = 2 + return len(ClientSubnetv6("", "")._pack_subnet(x)) + + +class ClientSubnetv6(ClientSubnetv4): + af_familly = socket.AF_INET6 + af_length = 128 + af_default = b"\x20" # 2000:: + + +class EDNS0ClientSubnet(_EDNS0Dummy): + name = "DNS EDNS0 Client Subnet" + fields_desc = [ShortEnumField("optcode", 8, edns0types), + FieldLenField("optlen", None, "address", fmt="H", + adjust=lambda pkt, x: x + 4), + ShortField("family", 1), + FieldLenField("source_plen", None, + length_of="address", + fmt="B", + adjust=lambda pkt, x: x * 8), + ByteField("scope_plen", 0), + MultipleTypeField( + [(ClientSubnetv4("address", "192.168.0.0", + length_from=lambda p: p.source_plen), + lambda pkt: pkt.family == 1), + (ClientSubnetv6("address", "2001:db8::", + length_from=lambda p: p.source_plen), + lambda pkt: pkt.family == 2)], + ClientSubnetv4("address", "192.168.0.0", + length_from=lambda p: p.source_plen))] + + +class EDNS0COOKIE(_EDNS0Dummy): + name = "DNS EDNS0 COOKIE" + fields_desc = [ShortEnumField("optcode", 10, edns0types), + FieldLenField("optlen", None, length_of="server_cookie", fmt="!H", + adjust=lambda pkt, x: x + 8), + XStrFixedLenField("client_cookie", b"\x00" * 8, length=8), + XStrLenField("server_cookie", "", + length_from=lambda pkt: max(0, pkt.optlen - 8))] + + +# RFC 8914 - Extended DNS Errors + +# https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#extended-dns-error-codes +extended_dns_error_codes = { + 0: "Other", + 1: "Unsupported DNSKEY Algorithm", + 2: "Unsupported DS Digest Type", + 3: "Stale Answer", + 4: "Forged Answer", + 5: "DNSSEC Indeterminate", + 6: "DNSSEC Bogus", + 7: "Signature Expired", + 8: "Signature Not Yet Valid", + 9: "DNSKEY Missing", + 10: "RRSIGs Missing", + 11: "No Zone Key Bit Set", + 12: "NSEC Missing", + 13: "Cached Error", + 14: "Not Ready", + 15: "Blocked", + 16: "Censored", + 17: "Filtered", + 18: "Prohibited", + 19: "Stale NXDOMAIN Answer", + 20: "Not Authoritative", + 21: "Not Supported", + 22: "No Reachable Authority", + 23: "Network Error", + 24: "Invalid Data", + 25: "Signature Expired before Valid", + 26: "Too Early", + 27: "Unsupported NSEC3 Iterations Value", + 28: "Unable to conform to policy", + 29: "Synthesized", +} + + +# https://www.rfc-editor.org/rfc/rfc8914.html +class EDNS0ExtendedDNSError(_EDNS0Dummy): + name = "DNS EDNS0 Extended DNS Error" + fields_desc = [ShortEnumField("optcode", 15, edns0types), + FieldLenField("optlen", None, length_of="extra_text", fmt="!H", + adjust=lambda pkt, x: x + 2), + ShortEnumField("info_code", 0, extended_dns_error_codes), + StrLenField("extra_text", "", + length_from=lambda pkt: pkt.optlen - 2)] + + +EDNS0OPT_DISPATCHER = { + 4: EDNS0OWN, + 5: EDNS0DAU, + 6: EDNS0DHU, + 7: EDNS0N3U, + 8: EDNS0ClientSubnet, + 10: EDNS0COOKIE, + 15: EDNS0ExtendedDNSError, +} + + +# RFC 4034 - Resource Records for the DNS Security Extensions def bitmap2RRlist(bitmap): """ @@ -545,15 +699,15 @@ def bitmap2RRlist(bitmap): while bitmap: if len(bitmap) < 2: - warning("bitmap too short (%i)" % len(bitmap)) + log_runtime.info("bitmap too short (%i)", len(bitmap)) return - window_block = orb(bitmap[0]) # window number + window_block = bitmap[0] # window number offset = 256 * window_block # offset of the Resource Record - bitmap_len = orb(bitmap[1]) # length of the bitmap in bytes + bitmap_len = bitmap[1] # length of the bitmap in bytes if bitmap_len <= 0 or bitmap_len > 32: - warning("bitmap length is no valid (%i)" % bitmap_len) + log_runtime.info("bitmap length is no valid (%i)", bitmap_len) return tmp_bitmap = bitmap[2:2 + bitmap_len] @@ -562,7 +716,7 @@ def bitmap2RRlist(bitmap): for b in range(len(tmp_bitmap)): v = 128 for i in range(8): - if orb(tmp_bitmap[b]) & v: + if tmp_bitmap[b] & v: # each of the RR is encoded as a bit RRlist += [offset + b * 8 + i] v = v >> 1 @@ -626,18 +780,22 @@ def RRlist2bitmap(lst): class RRlistField(StrField): + islist = 1 + def h2i(self, pkt, x): - if isinstance(x, list): + if x and isinstance(x, list): return RRlist2bitmap(x) return x def i2repr(self, pkt, x): + if not x: + return "[]" x = self.i2h(pkt, x) rrlist = bitmap2RRlist(x) return [dnstypes.get(rr, rr) for rr in rrlist] if rrlist else repr(x) -class _DNSRRdummy(InheritOriginDNSStrPacket): +class _DNSRRdummy(Packet): name = "Dummy class that implements post_build() for Resource Records" def post_build(self, pkt, pay): @@ -651,12 +809,30 @@ def post_build(self, pkt, pay): return tmp_pkt + pkt + pay + def default_payload_class(self, payload): + return conf.padding_layer + + +class DNSRRHINFO(_DNSRRdummy): + name = "DNS HINFO Resource Record" + fields_desc = [DNSStrField("rrname", ""), + ShortEnumField("type", 13, dnstypes), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), + IntField("ttl", 0), + ShortField("rdlen", None), + FieldLenField("cpulen", None, fmt="!B", length_of="cpu"), + StrLenField("cpu", "", length_from=lambda x: x.cpulen), + FieldLenField("oslen", None, fmt="!B", length_of="os"), + StrLenField("os", "", length_from=lambda x: x.oslen)] + class DNSRRMX(_DNSRRdummy): name = "DNS MX Resource Record" fields_desc = [DNSStrField("rrname", ""), - ShortEnumField("type", 6, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + ShortEnumField("type", 15, dnstypes), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), ShortField("preference", 0), @@ -685,7 +861,8 @@ class DNSRRRSIG(_DNSRRdummy): name = "DNS RRSIG Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 46, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), ShortEnumField("typecovered", 1, dnstypes), @@ -704,11 +881,12 @@ class DNSRRNSEC(_DNSRRdummy): name = "DNS NSEC Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 47, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), DNSStrField("nextname", ""), - RRlistField("typebitmaps", "") + RRlistField("typebitmaps", []) ] @@ -716,7 +894,8 @@ class DNSRRDNSKEY(_DNSRRdummy): name = "DNS DNSKEY Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 48, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), FlagsField("flags", 256, 16, "S???????Z???????"), @@ -732,7 +911,8 @@ class DNSRRDS(_DNSRRdummy): name = "DNS DS Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 43, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), ShortField("keytag", 0), @@ -758,7 +938,8 @@ class DNSRRNSEC3(_DNSRRdummy): name = "DNS NSEC3 Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 50, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), ByteField("hashalg", 0), @@ -768,7 +949,7 @@ class DNSRRNSEC3(_DNSRRdummy): StrLenField("salt", "", length_from=lambda x: x.saltlength), FieldLenField("hashlength", 0, fmt="!B", length_of="nexthashedownername"), # noqa: E501 StrLenField("nexthashedownername", "", length_from=lambda x: x.hashlength), # noqa: E501 - RRlistField("typebitmaps", "") + RRlistField("typebitmaps", []) ] @@ -776,7 +957,8 @@ class DNSRRNSEC3PARAM(_DNSRRdummy): name = "DNS NSEC3PARAM Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 51, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), ByteField("hashalg", 0), @@ -786,6 +968,81 @@ class DNSRRNSEC3PARAM(_DNSRRdummy): StrLenField("salt", "", length_from=lambda pkt: pkt.saltlength) # noqa: E501 ] + +# RFC 9460 Service Binding and Parameter Specification via the DNS +# https://www.rfc-editor.org/rfc/rfc9460.html + + +# https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml +svc_param_keys = { + 0: "mandatory", + 1: "alpn", + 2: "no-default-alpn", + 3: "port", + 4: "ipv4hint", + 5: "ech", + 6: "ipv6hint", + 7: "dohpath", + 8: "ohttp", +} + + +class SvcParam(Packet): + name = "SvcParam" + fields_desc = [ShortEnumField("key", 0, svc_param_keys), + FieldLenField("len", None, length_of="value", fmt="H"), + MultipleTypeField( + [ + # mandatory + (FieldListField("value", [], + ShortEnumField("", 0, svc_param_keys), + length_from=lambda pkt: pkt.len), + lambda pkt: pkt.key == 0), + # alpn, no-default-alpn + (DNSTextField("value", [], + length_from=lambda pkt: pkt.len), + lambda pkt: pkt.key in (1, 2)), + # port + (ShortField("value", 0), + lambda pkt: pkt.key == 3), + # ipv4hint + (FieldListField("value", [], + IPField("", "0.0.0.0"), + length_from=lambda pkt: pkt.len), + lambda pkt: pkt.key == 4), + # ipv6hint + (FieldListField("value", [], + IP6Field("", "::"), + length_from=lambda pkt: pkt.len), + lambda pkt: pkt.key == 6), + ], + StrLenField("value", "", + length_from=lambda pkt:pkt.len))] + + def extract_padding(self, p): + return "", p + + +class DNSRRSVCB(_DNSRRdummy): + name = "DNS SVCB Resource Record" + fields_desc = [DNSStrField("rrname", ""), + ShortEnumField("type", 64, dnstypes), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), + IntField("ttl", 0), + ShortField("rdlen", None), + ShortField("svc_priority", 0), + DNSStrField("target_name", ""), + PacketListField("svc_params", [], SvcParam)] + + +class DNSRRHTTPS(_DNSRRdummy): + name = "DNS HTTPS Resource Record" + fields_desc = [DNSStrField("rrname", ""), + ShortEnumField("type", 65, dnstypes) + ] + DNSRRSVCB.fields_desc[2:] + + # RFC 2782 - A DNS RR for specifying the location of services (DNS SRV) @@ -793,7 +1050,8 @@ class DNSRRSRV(_DNSRRdummy): name = "DNS SRV Resource Record" fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 33, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), ShortField("rdlen", None), ShortField("priority", 0), @@ -807,9 +1065,9 @@ class DNSRRSRV(_DNSRRdummy): "hmac-sha1": 20} -class TimeSignedField(StrFixedLenField): +class TimeSignedField(Field[int, bytes]): def __init__(self, name, default): - StrFixedLenField.__init__(self, name, default, 6) + Field.__init__(self, name, default, fmt="6s") def _convert_seconds(self, packed_seconds): """Unpack the internal representation.""" @@ -817,7 +1075,7 @@ def _convert_seconds(self, packed_seconds): seconds += struct.unpack("!I", packed_seconds[2:])[0] return seconds - def h2i(self, pkt, seconds): + def i2m(self, pkt, seconds): """Convert the number of seconds since 1-Jan-70 UTC to the packed representation.""" @@ -829,7 +1087,7 @@ def h2i(self, pkt, seconds): return struct.pack("!HI", tmp_short, tmp_int) - def i2h(self, pkt, packed_seconds): + def m2i(self, pkt, packed_seconds): """Convert the internal representation to the number of seconds since 1-Jan-70 UTC.""" @@ -841,7 +1099,7 @@ def i2h(self, pkt, packed_seconds): def i2repr(self, pkt, packed_seconds): """Convert the internal representation to a nice one using the RFC format.""" - time_struct = time.gmtime(self._convert_seconds(packed_seconds)) + time_struct = time.gmtime(packed_seconds) return time.strftime("%a %b %d %H:%M:%S %Y", time_struct) @@ -864,10 +1122,33 @@ class DNSRRTSIG(_DNSRRdummy): ] +class DNSRRNAPTR(_DNSRRdummy): + name = "DNS NAPTR Resource Record" + fields_desc = [DNSStrField("rrname", ""), + ShortEnumField("type", 35, dnstypes), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), + IntField("ttl", 0), + ShortField("rdlen", None), + ShortField("order", 0), + ShortField("preference", 0), + FieldLenField("flags_len", None, fmt="!B", length_of="flags"), + StrLenField("flags", "", length_from=lambda pkt: pkt.flags_len), + FieldLenField("services_len", None, fmt="!B", length_of="services"), + StrLenField("services", "", + length_from=lambda pkt: pkt.services_len), + FieldLenField("regexp_len", None, fmt="!B", length_of="regexp"), + StrLenField("regexp", "", length_from=lambda pkt: pkt.regexp_len), + DNSStrField("replacement", ""), + ] + + DNSRR_DISPATCHER = { 6: DNSRRSOA, # RFC 1035 + 13: DNSRRHINFO, # RFC 1035 15: DNSRRMX, # RFC 1035 33: DNSRRSRV, # RFC 2782 + 35: DNSRRNAPTR, # RFC 2915 41: DNSRROPT, # RFC 1671 43: DNSRRDS, # RFC 4034 46: DNSRRRSIG, # RFC 4034 @@ -875,23 +1156,20 @@ class DNSRRTSIG(_DNSRRdummy): 48: DNSRRDNSKEY, # RFC 4034 50: DNSRRNSEC3, # RFC 5155 51: DNSRRNSEC3PARAM, # RFC 5155 + 64: DNSRRSVCB, # RFC 9460 + 65: DNSRRHTTPS, # RFC 9460 250: DNSRRTSIG, # RFC 2845 32769: DNSRRDLV, # RFC 4431 } -DNSSEC_CLASSES = tuple(six.itervalues(DNSRR_DISPATCHER)) - -def isdnssecRR(obj): - return isinstance(obj, DNSSEC_CLASSES) - - -class DNSRR(InheritOriginDNSStrPacket): +class DNSRR(Packet): name = "DNS Resource Record" show_indent = 0 fields_desc = [DNSStrField("rrname", ""), ShortEnumField("type", 1, dnstypes), - ShortEnumField("rclass", 1, dnsclasses), + BitField("cacheflush", 0, 1), # mDNS RFC 6762 + BitEnumField("rclass", 1, 15, dnsclasses), IntField("ttl", 0), FieldLenField("rdlen", None, length_of="rdata", fmt="H"), MultipleTypeField( @@ -902,12 +1180,12 @@ class DNSRR(InheritOriginDNSStrPacket): # AAAA (IP6Field("rdata", "::"), lambda pkt: pkt.type == 28), - # NS, MD, MF, CNAME, PTR + # NS, MD, MF, CNAME, PTR, DNAME (DNSStrField("rdata", "", length_from=lambda pkt: pkt.rdlen), - lambda pkt: pkt.type in [2, 3, 4, 5, 12]), + lambda pkt: pkt.type in [2, 3, 4, 5, 12, 39]), # TEXT - (DNSTextField("rdata", [], + (DNSTextField("rdata", [""], length_from=lambda pkt: pkt.rdlen), lambda pkt: pkt.type == 16), ], @@ -915,16 +1193,302 @@ class DNSRR(InheritOriginDNSStrPacket): length_from=lambda pkt:pkt.rdlen) )] + def default_payload_class(self, payload): + return conf.padding_layer + + +def _DNSRR(s, **kwargs): + """ + DNSRR dispatcher func + """ + if s: + # Try to find the type of the RR using the dispatcher + _, remain = dns_get_str(s, _ignore_compression=True) + cls = DNSRR_DISPATCHER.get( + struct.unpack("!H", remain[:2])[0], + DNSRR, + ) + rrlen = ( + len(s) - len(remain) + # rrname len + 10 + + struct.unpack("!H", remain[8:10])[0] + ) + pkt = cls(s[:rrlen], **kwargs) / conf.padding_layer(s[rrlen:]) + # drop rdlen because if rdata was compressed, it will break everything + # when rebuilding + del pkt.fields["rdlen"] + return pkt + return None + + +class DNSQR(Packet): + name = "DNS Question Record" + show_indent = 0 + fields_desc = [DNSStrField("qname", "www.example.com"), + ShortEnumField("qtype", 1, dnsqtypes), + BitField("unicastresponse", 0, 1), # mDNS RFC 6762 + BitEnumField("qclass", 1, 15, dnsclasses)] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class _DNSPacketListField(PacketListField): + # A normal PacketListField with backward-compatible hacks + def any2i(self, pkt, x): + # type: (Optional[Packet], List[Any]) -> List[Any] + if x is None: + warnings.warn( + ("The DNS fields 'qd', 'an', 'ns' and 'ar' are now " + "PacketListField(s) ! " + "Setting a null default should be [] instead of None"), + DeprecationWarning + ) + x = [] + return super(_DNSPacketListField, self).any2i(pkt, x) + + def i2h(self, pkt, x): + # type: (Optional[Packet], List[Packet]) -> Any + class _list(list): + """ + Fake list object to provide compatibility with older DNS fields + """ + def __getattr__(self, attr): + try: + ret = getattr(self[0], attr) + warnings.warn( + ("The DNS fields 'qd', 'an', 'ns' and 'ar' are now " + "PacketListField(s) ! " + "To access the first element, use pkt.an[0] instead of " + "pkt.an"), + DeprecationWarning + ) + return ret + except AttributeError: + raise + return _list(x) + + +class DNS(DNSCompressedPacket): + name = "DNS" + FORCE_TCP = False + fields_desc = [ + ConditionalField(ShortField("length", None), + lambda p: p.FORCE_TCP or isinstance(p.underlayer, TCP)), + ShortField("id", 0), + BitField("qr", 0, 1), + BitEnumField("opcode", 0, 4, {0: "QUERY", 1: "IQUERY", 2: "STATUS"}), + BitField("aa", 0, 1), + BitField("tc", 0, 1), + BitField("rd", 1, 1), + BitField("ra", 0, 1), + BitField("z", 0, 1), + # AD and CD bits are defined in RFC 2535 + BitField("ad", 0, 1), # Authentic Data + BitField("cd", 0, 1), # Checking Disabled + BitEnumField("rcode", 0, 4, {0: "ok", 1: "format-error", + 2: "server-failure", 3: "name-error", + 4: "not-implemented", 5: "refused"}), + FieldLenField("qdcount", None, count_of="qd"), + FieldLenField("ancount", None, count_of="an"), + FieldLenField("nscount", None, count_of="ns"), + FieldLenField("arcount", None, count_of="ar"), + _DNSPacketListField("qd", [DNSQR()], DNSQR, count_from=lambda pkt: pkt.qdcount), + _DNSPacketListField("an", [], _DNSRR, count_from=lambda pkt: pkt.ancount), + _DNSPacketListField("ns", [], _DNSRR, count_from=lambda pkt: pkt.nscount), + _DNSPacketListField("ar", [], _DNSRR, count_from=lambda pkt: pkt.arcount), + ] + + def get_full(self): + # Required for DNSCompressedPacket + if isinstance(self.underlayer, TCP) or self.FORCE_TCP: + return self.original[2:] + else: + return self.original + + def answers(self, other): + return (isinstance(other, DNS) and + self.id == other.id and + self.qr == 1 and + other.qr == 0) + + def mysummary(self): + name = "" + if self.qr: + type = "Ans" + if self.an and isinstance(self.an[0], DNSRR): + name = ' %s' % self.an[0].rdata + elif self.rcode != 0: + name = self.sprintf(' %rcode%') + else: + type = "Qry" + if self.qd and isinstance(self.qd[0], DNSQR): + name = ' %s' % self.qd[0].qname + return "%sDNS %s%s" % ( + "m" + if isinstance(self.underlayer, UDP) and self.underlayer.dport == 5353 + else "", + type, + name, + ) + + def post_build(self, pkt, pay): + if ( + (isinstance(self.underlayer, TCP) or self.FORCE_TCP) and + self.length is None + ): + pkt = struct.pack("!H", len(pkt) - 2) + pkt[2:] + return pkt + pay + + def compress(self): + """Return the compressed DNS packet (using `dns_compress()`)""" + return dns_compress(self) + + def pre_dissect(self, s): + """ + Check that a valid DNS over TCP message can be decoded + """ + if isinstance(self.underlayer, TCP): + + # Compute the length of the DNS packet + if len(s) >= 2: + dns_len = struct.unpack("!H", s[:2])[0] + else: + message = "Malformed DNS message: too small!" + log_runtime.info(message) + raise Scapy_Exception(message) + + # Check if the length is valid + if dns_len < 14 or len(s) < dns_len: + message = "Malformed DNS message: invalid length!" + log_runtime.info(message) + raise Scapy_Exception(message) + + return s + + +class DNSTCP(DNS): + """ + A DNS packet that is always under TCP + """ + FORCE_TCP = True + match_subclass = True + bind_layers(UDP, DNS, dport=5353) bind_layers(UDP, DNS, sport=5353) bind_layers(UDP, DNS, dport=53) bind_layers(UDP, DNS, sport=53) DestIPField.bind_addr(UDP, "224.0.0.251", dport=5353) -DestIP6Field.bind_addr(UDP, "ff02::fb", dport=5353) +if conf.ipv6_enabled: + from scapy.layers.inet6 import DestIP6Field + DestIP6Field.bind_addr(UDP, "ff02::fb", dport=5353) bind_layers(TCP, DNS, dport=53) bind_layers(TCP, DNS, sport=53) +# Nameserver config +conf.nameservers = read_nameservers() +_dns_cache = conf.netcache.new_cache("dns_cache", 300) + + +@conf.commands.register +def dns_resolve(qname, qtype="A", raw=False, tcp=False, verbose=1, timeout=3, **kwargs): + """ + Perform a simple DNS resolution using conf.nameservers with caching + + :param qname: the name to query + :param qtype: the type to query (default A) + :param raw: return the whole DNS packet (default False) + :param tcp: whether to use directly TCP instead of UDP. If truncated is received, + UDP automatically retries in TCP. (default: False) + :param verbose: show verbose errors + :param timeout: seconds until timeout (per server) + :raise TimeoutError: if no DNS servers were reached in time. + """ + # Unify types (for caching) + qtype = DNSQR.qtype.any2i_one(None, qtype) + qname = DNSQR.qname.any2i(None, qname) + # Check cache + cache_ident = b";".join( + [qname, struct.pack("!B", qtype)] + + ([b"raw"] if raw else []) + ) + result = _dns_cache.get(cache_ident) + if result: + return result + + kwargs.setdefault("timeout", timeout) + kwargs.setdefault("verbose", 0) # hide sr1() output + res = None + for nameserver in conf.nameservers: + # Try all nameservers + try: + # Spawn a socket, connect to the nameserver on port 53 + if tcp: + cls = DNSTCP + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + else: + cls = DNS + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(kwargs["timeout"]) + sock.connect((nameserver, 53)) + # Connected. Wrap it with DNS + sock = StreamSocket(sock, cls) + # I/O + res = sock.sr1( + cls(qd=[DNSQR(qname=qname, qtype=qtype)], id=RandShort()), + **kwargs, + ) + except IOError as ex: + if verbose: + log_runtime.warning(str(ex)) + continue + finally: + sock.close() + if res: + # We have a response ! Check for failure + if res[DNS].tc == 1: # truncated ! + if not tcp: + # Retry using TCP + return dns_resolve( + qname=qname, + qtype=qtype, + raw=raw, + tcp=True, + **kwargs, + ) + elif verbose: + log_runtime.info("DNS answer is truncated !") + + if res[DNS].rcode == 2: # server failure + res = None + if verbose: + log_runtime.info( + "DNS: %s answered with failure for %s" % ( + nameserver, + qname, + ) + ) + else: + break + if res is not None: + if raw: + # Raw + result = res + else: + # Find answers + result = [ + x + for x in itertools.chain(res.an, res.ns, res.ar) + if x.type == qtype + ] + if result: + # Cache it + _dns_cache[cache_ident] = result + return result + else: + raise TimeoutError + @conf.commands.register def dyndns_add(nameserver, name, rdata, type="A", ttl=10): @@ -967,24 +1531,498 @@ def dyndns_del(nameserver, name, type="ALL", ttl=10): class DNS_am(AnsweringMachine): - function_name = "dns_spoof" + function_name = "dnsd" filter = "udp port 53" + cls = DNS # We also use this automaton for llmnrd / mdnsd + + def parse_options(self, joker=None, + match=None, + srvmatch=None, + joker6=False, + send_error=False, + relay=False, + from_ip=True, + from_ip6=False, + src_ip=None, + src_ip6=None, + ttl=10, + jokerarpa=False): + """ + Simple DNS answering machine. + + :param joker: default IPv4 for unresolved domains. + Set to False to disable, None to mirror the interface's IP. + Defaults to None, unless 'match' is used, then it defaults to + False. + :param joker6: default IPv6 for unresolved domains. + Set to False to disable, None to mirror the interface's IPv6. + Defaults to False. + :param match: queries to match. + This can be a dictionary of {name: val} where name is a string + representing a domain name (A, AAAA) and val is a tuple of 2 + elements, each representing an IP or a list of IPs. If val is + a single element, (A, None) is assumed. + This can also be a list or names, in which case joker(6) are + used as a response. + :param jokerarpa: answer for .in-addr.arpa PTR requests. (Default: False) + :param relay: relay unresolved domains to conf.nameservers (Default: False). + :param send_error: send an error message when this server can't answer + (Default: False) + :param srvmatch: a dictionary of {name: (port, target)} used for SRV + :param from_ip: an source IP to filter. Can contain a netmask. True for all, + False for none. Default True + :param from_ip6: an source IPv6 to filter. Can contain a netmask. True for all, + False for none. Default False + :param ttl: the DNS time to live (in seconds) + :param src_ip: override the source IP + :param src_ip6: + + Examples: + + - Answer all 'A' and 'AAAA' requests:: + + $ sudo iptables -I OUTPUT -p icmp --icmp-type 3/3 -j DROP + >>> dnsd(joker="192.168.0.2", joker6="fe80::260:8ff:fe52:f9d8", + ... iface="eth0") + + - Answer only 'A' query for google.com with 192.168.0.2:: + + >>> dnsd(match={"google.com": "192.168.0.2"}, iface="eth0") + + - Answer DNS for a Windows domain controller ('SRV', 'A' and 'AAAA'):: + + >>> dnsd( + ... srvmatch={ + ... "_ldap._tcp.dc._msdcs.DOMAIN.LOCAL.": (389, + ... "srv1.domain.local"), + ... }, + ... match={"src1.domain.local": ("192.168.0.102", + ... "fe80::260:8ff:fe52:f9d8")}, + ... ) + + - Relay all queries to another DNS server, except some:: + + >>> conf.nameservers = ["1.1.1.1"] # server to relay to + >>> dnsd( + ... match={"test.com": "1.1.1.1"}, + ... relay=True, + ... ) + """ + from scapy.layers.inet6 import Net6 + + self.mDNS = isinstance(self, mDNS_am) + self.llmnr = self.cls != DNS + + # Add some checks (to help) + if not isinstance(joker, (str, bool)) and joker is not None: + raise ValueError("Bad 'joker': should be an IPv4 (str) or False !") + if not isinstance(joker6, (str, bool)) and joker6 is not None: + raise ValueError("Bad 'joker6': should be an IPv6 (str) or False !") + if not isinstance(jokerarpa, (str, bool)): + raise ValueError("Bad 'jokerarpa': should be a hostname or False !") + if not isinstance(from_ip, (str, Net, bool)): + raise ValueError("Bad 'from_ip': should be an IPv4 (str), Net or False !") + if not isinstance(from_ip6, (str, Net6, bool)): + raise ValueError("Bad 'from_ip6': should be an IPv6 (str), Net or False !") + if self.mDNS and src_ip: + raise ValueError("Cannot use 'src_ip' in mDNS !") + if self.mDNS and src_ip6: + raise ValueError("Cannot use 'src_ip6' in mDNS !") + + if joker is None and match is not None: + joker = False + self.joker = joker + self.joker6 = joker6 + self.jokerarpa = jokerarpa + + def normv(v): + if isinstance(v, (tuple, list)) and len(v) == 2: + return tuple(v) + elif isinstance(v, str): + return (v, joker6) + else: + raise ValueError("Bad match value: '%s'" % repr(v)) + + def normk(k): + k = bytes_encode(k).lower() + if not k.endswith(b"."): + k += b"." + return k + + self.match = collections.defaultdict(lambda: (joker, joker6)) + if match: + if isinstance(match, (list, set)): + self.match.update({normk(k): (None, None) for k in match}) + else: + self.match.update({normk(k): normv(v) for k, v in match.items()}) + if srvmatch is None: + self.srvmatch = {} + else: + self.srvmatch = {normk(k): normv(v) for k, v in srvmatch.items()} - def parse_options(self, joker="192.168.1.1", match=None): - if match is None: - self.match = {} + self.send_error = send_error + self.relay = relay + if isinstance(from_ip, str): + self.from_ip = Net(from_ip) else: - self.match = match - self.joker = joker + self.from_ip = from_ip + if isinstance(from_ip6, str): + self.from_ip6 = Net6(from_ip6) + else: + self.from_ip6 = from_ip6 + self.src_ip = src_ip + self.src_ip6 = src_ip6 + self.ttl = ttl def is_request(self, req): - return req.haslayer(DNS) and req.getlayer(DNS).qr == 0 + from scapy.layers.inet6 import IPv6 + return ( + req.haslayer(self.cls) and + req.getlayer(self.cls).qr == 0 and ( + ( + self.from_ip6 is True or + (self.from_ip6 and req[IPv6].src in self.from_ip6) + ) + if IPv6 in req else + ( + self.from_ip is True or + (self.from_ip and req[IP].src in self.from_ip) + ) + ) + ) def make_reply(self, req): - ip = req.getlayer(IP) - dns = req.getlayer(DNS) - resp = IP(dst=ip.src, src=ip.dst) / UDP(dport=ip.sport, sport=ip.dport) - rdata = self.match.get(dns.qd.qname, self.joker) - resp /= DNS(id=dns.id, qr=1, qd=dns.qd, - an=DNSRR(rrname=dns.qd.qname, ttl=10, rdata=rdata)) + # Build reply from the request + resp = req.copy() + if Ether in req: + if self.mDNS: + resp[Ether].src, resp[Ether].dst = None, None + elif self.llmnr: + resp[Ether].src, resp[Ether].dst = None, req[Ether].src + else: + resp[Ether].src, resp[Ether].dst = ( + None if req[Ether].dst == "ff:ff:ff:ff:ff:ff" else req[Ether].dst, + req[Ether].src, + ) + from scapy.layers.inet6 import IPv6 + if IPv6 in req: + resp[IPv6].underlayer.remove_payload() + if self.mDNS: + # "All Multicast DNS responses (including responses sent via unicast) + # SHOULD be sent with IP TTL set to 255." + resp /= IPv6(dst="ff02::fb", src=self.src_ip6, + fl=req[IPv6].fl, hlim=255) + elif self.llmnr: + resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6, + fl=req[IPv6].fl, hlim=req[IPv6].hlim) + else: + resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst, + fl=req[IPv6].fl, hlim=req[IPv6].hlim) + elif IP in req: + resp[IP].underlayer.remove_payload() + if self.mDNS: + # "All Multicast DNS responses (including responses sent via unicast) + # SHOULD be sent with IP TTL set to 255." + resp /= IP(dst="224.0.0.251", src=self.src_ip, + id=req[IP].id, ttl=255) + elif self.llmnr: + resp /= IP(dst=req[IP].src, src=self.src_ip, + id=req[IP].id, ttl=req[IP].ttl) + else: + resp /= IP(dst=req[IP].src, src=self.src_ip or req[IP].dst, + id=req[IP].id, ttl=req[IP].ttl) + else: + warning("No IP or IPv6 layer in %s", req.command()) + return + try: + resp /= UDP(sport=req[UDP].dport, dport=req[UDP].sport) + except IndexError: + warning("No UDP layer in %s", req.command(), exc_info=True) + return + try: + req = req[self.cls] + except IndexError: + warning( + "No %s layer in %s", + self.cls.__name__, + req.command(), + exc_info=True, + ) + return + try: + queries = req.qd + except AttributeError: + warning("No qd attribute in %s", req.command(), exc_info=True) + return + # Special case: alias 'ALL' query as 'A' + 'AAAA' + try: + allquery = next( + (x for x in queries if getattr(x, "qtype", None) == 255) + ) + queries.remove(allquery) + queries.extend([ + DNSQR( + qtype=x, + qname=allquery.qname, + unicastresponse=allquery.unicastresponse, + qclass=allquery.qclass, + ) + for x in [1, 28] + ]) + except StopIteration: + pass + # Process each query + ans = [] + ars = [] + for rq in queries: + if isinstance(rq, Raw): + warning("Cannot parse qd element %s", rq.command(), exc_info=True) + continue + rqname = rq.qname.lower() + if rq.qtype in [1, 28]: + # A or AAAA + if rq.qtype == 28: + # AAAA + rdata = self.match[rqname][1] + if rdata is None and not self.relay: + # 'None' resolves to the default IPv6 + iface = resolve_iface(self.optsniff.get("iface", conf.iface)) + if self.mDNS: + # All IPs, as per mDNS. + rdata = iface.ips[6] + else: + rdata = get_if_addr6( + iface + ) + if self.mDNS and rdata and IPv6 in resp: + # For mDNS, we must replace the IPv6 src + resp[IPv6].src = rdata + elif rq.qtype == 1: + # A + rdata = self.match[rqname][0] + if rdata is None and not self.relay: + # 'None' resolves to the default IPv4 + iface = resolve_iface(self.optsniff.get("iface", conf.iface)) + if self.mDNS: + # All IPs, as per mDNS. + rdata = iface.ips[4] + else: + rdata = get_if_addr( + iface + ) + if self.mDNS and rdata and IP in resp: + # For mDNS, we must replace the IP src + resp[IP].src = rdata + if rdata: + # Common A and AAAA + if not isinstance(rdata, list): + rdata = [rdata] + ans.extend([ + DNSRR( + rrname=rq.qname, + ttl=self.ttl, + rdata=x, + type=rq.qtype, + cacheflush=self.mDNS and rq.qtype == rq.qtype, + ) + for x in rdata + ]) + continue # next + elif rq.qtype == 33: + # SRV + try: + port, target = self.srvmatch[rqname] + ans.append(DNSRRSRV( + rrname=rq.qname, + port=port, + target=target, + weight=100, + ttl=self.ttl + )) + continue # next + except KeyError: + # No result + pass + elif rq.qtype == 12: + # PTR + if rq.qname[-14:] == b".in-addr.arpa." and self.jokerarpa: + ans.append(DNSRR( + rrname=rq.qname, + type=rq.qtype, + ttl=self.ttl, + rdata=self.jokerarpa, + )) + continue + # It it arrives here, there is currently no answer + if self.relay: + # Relay mode ? + try: + _rslv = dns_resolve(rq.qname, qtype=rq.qtype, raw=True) + if _rslv: + ans.extend(_rslv.an) + ars.extend(_rslv.ar) + continue # next + except TimeoutError: + pass + # Still no answer. + if self.mDNS: + # "Any time a responder receives a query for a name for which it + # has verified exclusive ownership, for a type for which that name + # has no records, the responder MUST respond asserting the + # nonexistence of that record using a DNS NSEC record [RFC4034]." + ans.append(DNSRRNSEC( + # RFC6762 sect 6.1 - Negative Response + ttl=self.ttl, + rrname=rq.qname, + nextname=rq.qname, + typebitmaps=RRlist2bitmap([rq.qtype]), + )) + if self.mDNS and all(x.type == 47 for x in ans): + # If mDNS answers with only NSEC, discard. + return + if not ans: + # No answer is available. + if self.send_error: + resp /= self.cls(id=req.id, qr=1, qd=req.qd, rcode=3) + return resp + log_runtime.info("No answer could be provided to: %s" % req.summary()) + return + # Handle Additional Records + if self.mDNS: + # Windows specific extension + ars.append(DNSRROPT( + z=0x1194, + rdata=[ + EDNS0OWN( + primary_mac=resp[Ether].src, + ), + ], + )) + # All rq were answered + if self.mDNS: + # in mDNS mode, don't repeat the question, set aa=1, rd=0 + dns = self.cls(id=req.id, aa=1, rd=0, qr=1, qd=[], ar=ars, an=ans) + else: + dns = self.cls(id=req.id, qr=1, qd=req.qd, ar=ars, an=ans) + # Compress DNS and mDNS + if not self.llmnr: + resp /= dns_compress(dns) + else: + resp /= dns return resp + + +class mDNS_am(DNS_am): + """ + mDNS answering machine. + + This has the same arguments as DNS_am. See help(DNS_am) + + Example:: + + - Answer for 'TEST.local' with local IPv4:: + + >>> mdnsd(match=["TEST.local"]) + + - Answer all requests with other IP:: + + >>> mdnsd(joker="192.168.0.2", joker6="fe80::260:8ff:fe52:f9d8", + ... iface="eth0") + + - Answer for multiple different mDNS names:: + + >>> mdnsd(match={"TEST.local": "192.168.0.100", + ... "BOB.local": "192.168.0.101"}) + + - Answer with both A and AAAA records:: + + >>> mdnsd(match={"TEST.local": ("192.168.0.100", + ... "fe80::260:8ff:fe52:f9d8")}) + """ + function_name = "mdnsd" + filter = "udp port 5353" + + +# DNS-SD (RFC 6763) + + +class DNSSDResult(SndRcvList): + def __init__(self, + res=None, # type: Optional[Union[_PacketList[QueryAnswer], List[QueryAnswer]]] # noqa: E501 + name="DNS-SD", # type: str + stats=None # type: Optional[List[Type[Packet]]] + ): + SndRcvList.__init__(self, res, name, stats) + + def show(self, types=['PTR', 'SRV'], alltypes=False): + # type: (List[str], bool) -> None + """ + Print the list of discovered services. + + :param types: types to show. Default ['PTR', 'SRV'] + :param alltypes: show all types. Default False + """ + if alltypes: + types = None + data = list() # type: List[Tuple[str | List[str], ...]] + + resolve_mac = ( + self.res and isinstance(self.res[0][1].underlayer, Ether) and + conf.manufdb + ) + + header = ("IP", "Service") + if resolve_mac: + header = ("Mac",) + header + + for _, r in self.res: + attrs = [] + for attr in itertools.chain(r[DNS].an, r[DNS].ar): + if types and dnstypes.get(attr.type) not in types: + continue + if isinstance(attr, DNSRRNSEC): + attrs.append(attr.sprintf("%type%=%nextname%")) + elif isinstance(attr, DNSRRSRV): + attrs.append(attr.sprintf("%type%=(%target%,%port%)")) + else: + attrs.append(attr.sprintf("%type%=%rdata%")) + ans = (r.src, attrs) + if resolve_mac: + mac = conf.manufdb._resolve_MAC(r.underlayer.src) + data.append((mac,) + ans) + else: + data.append(ans) + + print( + pretty_list( + data, + [header], + ) + ) + + +@conf.commands.register +def dnssd(service="_services._dns-sd._udp.local", + af=socket.AF_INET, + qtype="PTR", + iface=None, + verbose=2, + timeout=3): + """ + Performs a DNS-SD (RFC6763) request + + :param service: the service name to query (e.g. _spotify-connect._tcp.local) + :param af: the transport to use. socket.AF_INET or socket.AF_INET6 + :param qtype: the type to use in the mDNS. Either TXT, PTR or SRV. + :param iface: the interface to do this discovery on. + """ + if af == socket.AF_INET: + pkt = IP(dst=ScopedIP("224.0.0.251", iface), ttl=255) + elif af == socket.AF_INET6: + pkt = IPv6(dst=ScopedIP("ff02::fb", iface)) + else: + return + pkt /= UDP(sport=5353, dport=5353) + pkt /= DNS(rd=0, qd=[DNSQR(qname=service, qtype=qtype)]) + ans, _ = sr(pkt, multi=True, timeout=timeout, verbose=verbose) + return DNSSDResult(ans.res) diff --git a/scapy/layers/dot11.py b/scapy/layers/dot11.py index 965cfd0da1a..3e70571509e 100644 --- a/scapy/layers/dot11.py +++ b/scapy/layers/dot11.py @@ -1,26 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi """ Wireless LAN according to IEEE 802.11. + +This file contains bindings for 802.11 layers and some usual linklayers: + - PRISM + - RadioTap """ -from __future__ import print_function -import math import re import struct from zlib import crc32 @@ -30,32 +20,67 @@ DLT_IEEE802_11_RADIO from scapy.compat import raw, plain_str, orb, chb from scapy.packet import Packet, bind_layers, bind_top_down, NoPayload -from scapy.fields import ByteField, LEShortField, BitField, LEShortEnumField, \ - ByteEnumField, X3BytesField, FlagsField, LELongField, StrField, \ - StrLenField, IntField, XByteField, LEIntField, StrFixedLenField, \ - LESignedIntField, ReversePadField, ConditionalField, PacketListField, \ - ShortField, BitEnumField, FieldLenField, LEFieldLenField, \ - FieldListField, XStrFixedLenField, PacketField, FCSField, \ - ScalingField +from scapy.fields import ( + BitEnumField, + BitField, + BitMultiEnumField, + ByteEnumField, + ByteField, + ConditionalField, + FCSField, + FieldLenField, + FieldListField, + FlagsField, + IntField, + LEFieldLenField, + LEIntField, + LELongField, + LEShortEnumField, + LEShortField, + LESignedIntField, + MayEnd, + MultipleTypeField, + OUIField, + PacketField, + PacketListField, + ReversePadField, + ScalingField, + ShortField, + StrField, + StrFixedLenField, + StrLenField, + XByteField, + XStrFixedLenField, +) from scapy.ansmachine import AnsweringMachine from scapy.plist import PacketList from scapy.layers.l2 import Ether, LLC, MACField from scapy.layers.inet import IP, TCP from scapy.error import warning, log_loading from scapy.sendrecv import sniff, sendp -from scapy.utils import issubtype if conf.crypto_valid: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms + + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms, + ) + except ImportError: + decrepit_algorithms = algorithms else: - default_backend = Ciphers = algorithms = None - log_loading.info("Can't import python-cryptography v1.7+. Disabled WEP decryption/encryption. (Dot11)") # noqa: E501 + default_backend = Ciphers = algorithms = decrepit_algorithms = None + log_loading.info("Can't import python-cryptography v2.0+. Disabled WEP decryption/encryption. (Dot11)") # noqa: E501 -# Layers +######### +# Prism # +######### +# http://www.martin.cc/linux/prism class PrismHeader(Packet): """ iwpriv wlan0 monitor 3 """ @@ -111,58 +136,14 @@ def answers(self, other): else: return self.payload.answers(other) +############ +# RadioTap # +############ -# RadioTap - -class _RadiotapReversePadField(ReversePadField): - def __init__(self, fld): - # Quote from https://www.radiotap.org/: - # ""Radiotap requires that all fields in the radiotap header are aligned to natural boundaries. # noqa: E501 - # For radiotap, that means all 8-, 16-, 32-, and 64-bit fields must begin on 8-, 16-, 32-, and 64-bit boundaries, respectively."" # noqa: E501 - if isinstance(fld, BitField): - _align = int(math.ceil(fld.i2len(None, None))) - else: - _align = struct.calcsize(fld.fmt) - ReversePadField.__init__( - self, - fld, - _align, - padwith=b"\x00" - ) - - -def _next_radiotap_extpm(pkt, lst, cur, s): - """Generates the next RadioTapExtendedPresenceMask""" - if cur is None or (cur.present and cur.present.Ext): - st = len(lst) + (cur is not None) - return lambda *args: RadioTapExtendedPresenceMask(*args, index=st) - return None - +# https://www.radiotap.org/ -class RadioTapExtendedPresenceMask(Packet): - """RadioTapExtendedPresenceMask should be instantiated by passing an - `index=` kwarg, stating which place the item has in the list. - - Passing index will update the b[x] fields accordingly to the index. - e.g. - >>> a = RadioTapExtendedPresenceMask(present="b0+b12+b29+Ext") - >>> b = RadioTapExtendedPresenceMask(index=1, present="b33+b45+b59+b62") - >>> pkt = RadioTap(present="Ext", Ext=[a, b]) - """ - name = "RadioTap Extended presence mask" - fields_desc = [FlagsField('present', None, -32, - ["b%s" % i for i in range(0, 31)] + ["Ext"])] - - def __init__(self, _pkt=None, index=0, **kwargs): - self._restart_indentation(index) - Packet.__init__(self, _pkt, **kwargs) - - def _restart_indentation(self, index): - st = index * 32 - self.fields_desc[0].names = ["b%s" % (i + st) for i in range(0, 31)] + ["Ext"] # noqa: E501 - - def guess_payload_class(self, pay): - return conf.padding_layer +# Note: Radiotap alignment is crazy. See the doc: +# https://www.radiotap.org/#alignment-in-radiotap # RadioTap constants @@ -173,7 +154,7 @@ def guess_payload_class(self, pay): 'dB_AntSignal', 'dB_AntNoise', 'RXFlags', 'TXFlags', 'b17', 'b18', 'ChannelPlus', 'MCS', 'A_MPDU', 'VHT', 'timestamp', 'HE', 'HE_MU', 'HE_MU_other_user', - 'zero_length_psdu', 'L_SIG', 'b28', + 'zero_length_psdu', 'L_SIG', 'TLV', 'RadiotapNS', 'VendorNS', 'Ext'] # Note: Inconsistencies with wireshark @@ -191,7 +172,7 @@ def guess_payload_class(self, pay): _rt_rxflags = ["res1", "BAD_PLCP", "res2"] -_rt_txflags = ["TX_FAIL", "CTS", "RTS", "NOACK", "NOSEQ"] +_rt_txflags = ["TX_FAIL", "CTS", "RTS", "NOACK", "NOSEQ", "ORDER"] _rt_channelflags2 = ['res1', 'res2', 'res3', 'res4', 'Turbo', 'CCK', 'OFDM', '2GHz', '5GHz', 'Passive', 'Dynamic_CCK_OFDM', @@ -200,6 +181,9 @@ def guess_payload_class(self, pay): '40MHz_ext_channel_below', 'res5', 'res6', 'res7', 'res8', 'res9'] +_rt_tsflags = ['32-bit_counter', 'Accuracy', 'res1', 'res2', 'res3', + 'res4', 'res5', 'res6'] + _rt_knownmcs = ['MCS_bandwidth', 'MCS_index', 'guard_interval', 'HT_format', 'FEC_type', 'STBC_streams', 'Ness', 'Ness_MSB'] @@ -227,7 +211,7 @@ def guess_payload_class(self, pay): 'SGINsysmDis', 'LDPCextraOFDM', 'Beamformed', 'res1', 'res2'] -_rt_hemuother_per_user_known = { +_rt_hemuother_per_user_known = [ 'user field position', 'STA-ID', 'NSTS', @@ -236,11 +220,90 @@ def guess_payload_class(self, pay): 'MCS', 'DCM', 'Coding', -} +] +# Radiotap utils + +# Note: extended presence masks are dissected pretty dumbly by +# Wireshark. + +def _next_radiotap_extpm(pkt, lst, cur, s): + """Generates the next RadioTapExtendedPresenceMask""" + if cur is None or (cur.present and cur.present.Ext): + st = len(lst) + (cur is not None) + return lambda *args: RadioTapExtendedPresenceMask(*args, index=st) + return None + + +class RadioTapExtendedPresenceMask(Packet): + """RadioTapExtendedPresenceMask should be instantiated by passing an + `index=` kwarg, stating which place the item has in the list. + + Passing index will update the b[x] fields accordingly to the index. + e.g. + >>> a = RadioTapExtendedPresenceMask(present="b0+b12+b29+Ext") + >>> b = RadioTapExtendedPresenceMask(index=1, present="b33+b45+b59+b62") + >>> pkt = RadioTap(present="Ext", Ext=[a, b]) + """ + name = "RadioTap Extended presence mask" + fields_desc = [FlagsField('present', None, -32, + ["b%s" % i for i in range(0, 31)] + ["Ext"])] + + def __init__(self, _pkt=None, index=0, **kwargs): + self._restart_indentation(index) + Packet.__init__(self, _pkt, **kwargs) + + def _restart_indentation(self, index): + st = index * 32 + self.fields_desc[0].names = ["b%s" % (i + st) for i in range(0, 31)] + ["Ext"] # noqa: E501 + + def guess_payload_class(self, pay): + return conf.padding_layer + + +# This is still unimplemented in Wireshark +# https://www.radiotap.org/fields/TLV.html +class RadioTapTLV(Packet): + fields_desc = [ + LEShortEnumField("type", 0, _rt_present), + LEShortField("length", None), + ConditionalField( + OUIField("oui", 0), + lambda pkt: pkt.type == 30 # VendorNS + ), + ConditionalField( + ByteField("subtype", 0), + lambda pkt: pkt.type == 30 + ), + ConditionalField( + LEShortField("presence_type", 0), + lambda pkt: pkt.type == 30 + ), + ConditionalField( + LEShortField("reserved", 0), + lambda pkt: pkt.type == 30 + ), + StrLenField("data", b"", + length_from=lambda pkt: pkt.length), + StrLenField("pad", None, length_from=lambda pkt: -pkt.length % 4) + ] + + def post_build(self, pkt, pay): + if self.length is None: + pkt = pkt[:2] + struct.pack(" %%%s.addr1%%" % ((self.__class__.__name__,) * 4)) # noqa: E501 def guess_payload_class(self, payload): - if self.type == 0x02 and (0x08 <= self.subtype <= 0xF and self.subtype != 0xD): # noqa: E501 + if self.type == 0x02 and ( + 0x08 <= self.subtype <= 0xF and self.subtype != 0xD): return Dot11QoS - elif self.FCfield.protected: + elif hasattr(self.FCfield, "protected") and self.FCfield.protected: # When a frame is handled by encryption, the Protected Frame bit # (previously called WEP bit) is set to 1, and the Frame Body # begins with the appropriate cryptographic header. @@ -554,6 +783,33 @@ def answers(self, other): return 0 return 0 + def address_meaning(self, index): + """ + Return the meaning of the address[index] considering the context + """ + if index not in [1, 2, 3, 4]: + raise ValueError("Wrong index: should be [1, 2, 3, 4]") + index = index - 1 + if self.type == 0: # Management + return _dot11_addr_meaning[0][index] + elif self.type == 1: # Control + if (self.type, self.subtype) == (1, 6) and self.cfe == 6: + return ["RA", "NAV-SA", "NAV-DA"][index] + return _dot11_addr_meaning[1][index] + elif self.type == 2: # Data + meaning = _dot11_addr_meaning[2][index][ + self.FCfield.to_DS + ][self.FCfield.from_DS] + if meaning and index in [2, 3]: # Address 3-4 + if isinstance(self.payload, Dot11QoS): + # MSDU and Short A-MSDU + if self.payload.A_MSDU_Present: + meaning = "BSSID" + return meaning + elif self.type == 3: # Extension + return _dot11_addr_meaning[3][index] + return None + def unwep(self, key=None, warn=1): if self.FCfield & 0x40 == 0: if warn: @@ -581,17 +837,17 @@ def compute_fcs(self, s): def post_build(self, p, pay): p += pay if self.fcs is None: - p = p[:-4] + self.compute_fcs(p) + p = p[:-4] + self.compute_fcs(p[:-4]) return p class Dot11QoS(Packet): name = "802.11 QoS" - fields_desc = [BitField("Reserved", None, 1), - BitField("Ack_Policy", None, 2), - BitField("EOSP", None, 1), - BitField("TID", None, 4), - ByteField("TXOP", None)] + fields_desc = [BitField("A_MSDU_Present", 0, 1), + BitField("Ack_Policy", 0, 2), + BitField("EOSP", 0, 1), + BitField("TID", 0, 4), + ByteField("TXOP", 0)] def guess_payload_class(self, payload): if isinstance(self.underlayer, Dot11): @@ -617,25 +873,22 @@ def guess_payload_class(self, payload): 16: "timeout", 17: "AP-full", 18: "rate-unsupported"} -class _Dot11NetStats(Packet): - fields_desc = [LELongField("timestamp", 0), - LEShortField("beacon_interval", 0x0064), - FlagsField("cap", 0, 16, capability_list)] - +class _Dot11EltUtils(Packet): + """ + Contains utils for classes that have Dot11Elt as payloads + """ def network_stats(self): """Return a dictionary containing a summary of the Dot11 elements fields """ summary = {} crypto = set() - akmsuite_types = { - 0x00: "Reserved", - 0x01: "802.1X", - 0x02: "PSK" - } p = self.payload while isinstance(p, Dot11Elt): - if p.ID == 0: + # Avoid overriding already-set SSID values because it is not part + # of the standard and it protects from parsing bugs, + # see https://github.com/secdev/scapy/issues/2683 + if p.ID == 0 and "ssid" not in summary: summary["ssid"] = plain_str(p.info) elif p.ID == 3: summary["channel"] = ord(p.info) @@ -651,123 +904,175 @@ def network_stats(self): p.country_string[-1:] ) elif isinstance(p, Dot11EltRates): - summary["rates"] = p.rates + rates = [(x & 0x7f) / 2. for x in p.rates] + if "rates" in summary: + summary["rates"].extend(rates) + else: + summary["rates"] = rates elif isinstance(p, Dot11EltRSN): + wpa_version = "WPA2" + # WPA3-only: + # - AP shall at least enable AKM suite selector 00-0F-AC:8 + # - AP shall not enable AKM suite selector 00-0F-AC:2 and + # 00-0F-AC:6 + # - AP shall set MFPC and MFPR to 1 + # - AP shall not enable WEP and TKIP + # WPA3-transition: + # - AP shall at least enable AKM suite selector 00-0F-AC:2 + # and 00-0F-AC:8 + # - AP shall set MFPC to 1 and MFPR to 0 + if any(x.suite == 8 for x in p.akm_suites) and \ + all(x.suite not in [2, 6] for x in p.akm_suites) and \ + p.mfp_capable and p.mfp_required and \ + all(x.cipher not in [1, 2, 5] + for x in p.pairwise_cipher_suites): + # WPA3 only mode + wpa_version = "WPA3" + elif any(x.suite == 8 for x in p.akm_suites) and \ + any(x.suite == 2 for x in p.akm_suites) and \ + p.mfp_capable and not p.mfp_required: + # WPA3 transition mode + wpa_version = "WPA3-transition" + # Append suite if p.akm_suites: - auth = akmsuite_types.get(p.akm_suites[0].suite) - crypto.add("WPA2/%s" % auth) + auth = p.akm_suites[0].sprintf("%suite%") + crypto.add(wpa_version + "/%s" % auth) else: - crypto.add("WPA2") + crypto.add(wpa_version) elif p.ID == 221: - if isinstance(p, Dot11EltMicrosoftWPA) or \ - p.info.startswith(b'\x00P\xf2\x01\x01\x00'): + if isinstance(p, Dot11EltMicrosoftWPA): if p.akm_suites: - auth = akmsuite_types.get(p.akm_suites[0].suite) + auth = p.akm_suites[0].sprintf("%suite%") crypto.add("WPA/%s" % auth) else: crypto.add("WPA") p = p.payload - if not crypto: + if not crypto and hasattr(self, "cap"): if self.cap.privacy: crypto.add("WEP") else: crypto.add("OPN") - summary["crypto"] = crypto + if crypto: + summary["crypto"] = crypto return summary -class Dot11Beacon(_Dot11NetStats): - name = "802.11 Beacon" +############# +# 802.11 IE # +############# +# 802.11-2016 - 9.4.2 _dot11_info_elts_ids = { 0: "SSID", - 1: "Rates", + 1: "Supported Rates", 2: "FHset", - 3: "DSset", - 4: "CFset", + 3: "DSSS Set", + 4: "CF Set", 5: "TIM", - 6: "IBSSset", + 6: "IBSS Set", 7: "Country", 10: "Request", - 16: "challenge", - 33: "PowerCapability", - 36: "Channels", - 42: "ERPinfo", - 45: "HTCapabilities", - 46: "QoSCapability", - 47: "ERPinfo", - 48: "RSNinfo", - 50: "ESRates", - 52: "PowerConstraint", - 61: "HTinfo", - 68: "reserved", + 11: "BSS Load", + 12: "EDCA Set", + 13: "TSPEC", + 14: "TCLAS", + 15: "Schedule", + 16: "Challenge text", + 32: "Power Constraint", + 33: "Power Capability", + 36: "Supported Channels", + 37: "Channel Switch Announcement", + 42: "ERP", + 45: "HT Capabilities", + 46: "QoS Capability", + 48: "RSN", + 50: "Extended Supported Rates", + 52: "Neighbor Report", + 61: "HT Operation", + 74: "Overlapping BSS Scan Parameters", 107: "Interworking", - 127: "ExtendendCapatibilities", - 191: "VHTCapabilities", - 221: "vendor" + 127: "Extended Capabilities", + 191: "VHT Capabilities", + 192: "VHT Operation", + 221: "Vendor Specific" } +# Backward compatibility +_dot11_elt_deprecated_names = { + "Rates": 1, + "DSset": 3, + "CFset": 4, + "IBSSset": 6, + "challenge": 16, + "PowerCapability": 33, + "Channels": 36, + "ERPinfo": 42, + "HTinfo": 45, + "RSNinfo": 48, + "ESRates": 50, + "ExtendendCapatibilities": 127, + "VHTCapabilities": 191, + "Vendor": 221, +} + +_dot11_info_elts_ids_rev = {v: k for k, v in _dot11_info_elts_ids.items()} +_dot11_info_elts_ids_rev.update(_dot11_elt_deprecated_names) +_dot11_id_enum = ( + lambda x: _dot11_info_elts_ids.get(x, x), + lambda x: _dot11_info_elts_ids_rev.get(x, x) +) + + +# 802.11-2020 9.4.2.1 class Dot11Elt(Packet): + """ + A Generic 802.11 Element + """ __slots__ = ["info"] name = "802.11 Information Element" - fields_desc = [ByteEnumField("ID", 0, _dot11_info_elts_ids), + fields_desc = [ByteEnumField("ID", 0, _dot11_id_enum), FieldLenField("len", None, "info", "B"), StrLenField("info", "", length_from=lambda x: x.len, max_length=255)] show_indent = 0 + def __setattr__(self, attr, val): + if attr == "info": + # Will be caught by __slots__: we need an extra call + try: + self.setfieldval(attr, val) + except AttributeError: + pass + super(Dot11Elt, self).__setattr__(attr, val) + def mysummary(self): if self.ID == 0: - ssid = repr(self.info) - if ssid[:2] in ['b"', "b'"]: - ssid = ssid[1:] - return "SSID=%s" % ssid, [Dot11] + ssid = plain_str(self.info) + return "SSID='%s'" % ssid, [Dot11] else: return "" registered_ies = {} @classmethod - def register_variant(cls): - cls.registered_ies[cls.ID.default] = cls + def register_variant(cls, id=None): + id = id or cls.ID.default + if id not in cls.registered_ies: + cls.registered_ies[id] = cls @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): if _pkt: - _id = orb(_pkt[0]) - if _id == 221: - oui_a = orb(_pkt[2]) - oui_b = orb(_pkt[3]) - oui_c = orb(_pkt[4]) - if oui_a == 0x00 and oui_b == 0x50 and oui_c == 0xf2: - # MS OUI - type_ = orb(_pkt[5]) - if type_ == 0x01: - # MS WPA IE - return Dot11EltMicrosoftWPA - else: - return Dot11EltVendorSpecific - else: - return Dot11EltVendorSpecific - else: - return cls.registered_ies.get(_id, cls) + _id = ord(_pkt[:1]) + idcls = cls.registered_ies.get(_id, cls) + if idcls.dispatch_hook != cls.dispatch_hook: + # Vendor has its own dispatch_hook + return idcls.dispatch_hook(_pkt=_pkt, *args, **kargs) + cls = idcls return cls - def haslayer(self, cls): - if cls == "Dot11Elt": - if isinstance(self, Dot11Elt): - return True - elif issubtype(cls, Dot11Elt): - if isinstance(self, cls): - return True - return super(Dot11Elt, self).haslayer(cls) - - def getlayer(self, cls, nb=1, _track=None, _subclass=True, **flt): - return super(Dot11Elt, self).getlayer(cls, nb=nb, _track=_track, - _subclass=True, **flt) - def pre_dissect(self, s): # Backward compatibility: add info to all elements # This allows to introduce new Dot11Elt classes without breaking @@ -784,17 +1089,54 @@ def post_build(self, p, pay): return p + pay +# 802.11-2020 9.4.2.4 + +class Dot11EltDSSSet(Dot11Elt): + name = "802.11 DSSS Parameter Set" + match_subclass = True + fields_desc = [ + ByteEnumField("ID", 3, _dot11_id_enum), + ByteField("len", 1), + ByteField("channel", 0), + ] + + +# 802.11-2020 9.4.2.11 + +class Dot11EltERP(Dot11Elt): + name = "802.11 ERP" + match_subclass = True + fields_desc = [ + ByteEnumField("ID", 42, _dot11_id_enum), + ByteField("len", 1), + BitField("NonERP_Present", 0, 1), + BitField("Use_Protection", 0, 1), + BitField("Barker_Preamble_Mode", 0, 1), + BitField("res", 0, 5), + ] + + +# 802.11-2020 9.4.2.24.2 + class RSNCipherSuite(Packet): name = "Cipher suite" fields_desc = [ - X3BytesField("oui", 0x000fac), + OUIField("oui", 0x000fac), ByteEnumField("cipher", 0x04, { 0x00: "Use group cipher suite", 0x01: "WEP-40", 0x02: "TKIP", - 0x03: "Reserved", - 0x04: "CCMP", - 0x05: "WEP-104" + 0x03: "OCB", + 0x04: "CCMP-128", + 0x05: "WEP-104", + 0x06: "BIP-CMAC-128", + 0x07: "Group addressed traffic not allowed", + 0x08: "GCMP-128", + 0x09: "GCMP-256", + 0x0A: "CCMP-256", + 0x0B: "BIP-GMAC-128", + 0x0C: "BIP-GMAC-256", + 0x0D: "BIP-CMAC-256" }) ] @@ -802,14 +1144,32 @@ def extract_padding(self, s): return "", s +# 802.11-2020 9.4.2.24.3 + class AKMSuite(Packet): name = "AKM suite" fields_desc = [ - X3BytesField("oui", 0x000fac), + OUIField("oui", 0x000fac), ByteEnumField("suite", 0x01, { 0x00: "Reserved", - 0x01: "IEEE 802.1X / PMKSA caching", - 0x02: "PSK" + 0x01: "802.1X", + 0x02: "PSK", + 0x03: "FT-802.1X", + 0x04: "FT-PSK", + 0x05: "WPA-SHA256", + 0x06: "PSK-SHA256", + 0x07: "TDLS", + 0x08: "SAE", + 0x09: "FT-SAE", + 0x0A: "AP-PEER-KEY", + 0x0B: "WPA-SHA256-SUITE-B", + 0x0C: "WPA-SHA384-SUITE-B", + 0x0D: "FT-802.1X-SHA384", + 0x0E: "FILS-SHA256", + 0x0F: "FILS-SHA384", + 0x10: "FT-FILS-SHA256", + 0x11: "FT-FILS-SHA384", + 0x12: "OWE" }) ] @@ -817,10 +1177,12 @@ def extract_padding(self, s): return "", s +# 802.11-2020 9.4.2.24.5 + class PMKIDListPacket(Packet): name = "PMKIDs" fields_desc = [ - LEFieldLenField("nb_pmkids", 0, count_of="pmk_id_list"), + LEFieldLenField("nb_pmkids", None, count_of="pmkid_list"), FieldListField( "pmkid_list", None, @@ -833,16 +1195,19 @@ def extract_padding(self, s): return "", s +# 802.11-2020 9.4.2.24.1 + class Dot11EltRSN(Dot11Elt): name = "802.11 RSN information" + match_subclass = True fields_desc = [ - ByteField("ID", 48), + ByteEnumField("ID", 48, _dot11_id_enum), ByteField("len", None), LEShortField("version", 1), PacketField("group_cipher_suite", RSNCipherSuite(), RSNCipherSuite), LEFieldLenField( "nb_pairwise_cipher_suites", - 1, + None, count_of="pairwise_cipher_suites" ), PacketListField( @@ -853,7 +1218,7 @@ class Dot11EltRSN(Dot11Elt): ), LEFieldLenField( "nb_akm_suites", - 1, + None, count_of="akm_suites" ), PacketListField( @@ -862,19 +1227,49 @@ class Dot11EltRSN(Dot11Elt): AKMSuite, count_from=lambda p: p.nb_akm_suites ), - BitField("mfp_capable", 0, 1), - BitField("mfp_required", 0, 1), + # RSN Capabilities + # 802.11-2020 9.4.2.24.4 + BitField("mfp_capable", 1, 1), + BitField("mfp_required", 1, 1), BitField("gtksa_replay_counter", 0, 2), BitField("ptksa_replay_counter", 0, 2), BitField("no_pairwise", 0, 1), BitField("pre_auth", 0, 1), - BitField("reserved", 0, 8), + BitField("reserved", 0, 1), + BitField("ocvc", 0, 1), + BitField("extended_key_id", 0, 1), + BitField("pbac", 0, 1), + BitField("spp_a_msdu_required", 0, 1), + BitField("spp_a_msdu_capable", 0, 1), + BitField("peer_key_enabled", 0, 1), + BitField("joint_multiband_rsna", 0, 1), + # Theoretically we could use mfp_capable/mfp_required to know if those + # fields are present, but some implementations poorly implement it. + # In practice, do as wireshark: guess using offset. + ConditionalField( + PacketField("pmkids", PMKIDListPacket(), PMKIDListPacket), + lambda pkt: ( + True if pkt.len is None else + pkt.len - ( + 12 + + (pkt.nb_pairwise_cipher_suites or 0) * 4 + + (pkt.nb_akm_suites or 0) * 4 + ) >= 2 + ) + ), ConditionalField( - PacketField("pmkids", None, PMKIDListPacket), + PacketField("group_management_cipher_suite", + RSNCipherSuite(cipher=0x6), RSNCipherSuite), lambda pkt: ( - 0 if pkt.len is None else - pkt.len - (12 + (pkt.nb_pairwise_cipher_suites * 4) + - (pkt.nb_akm_suites * 4)) >= 18) + True if pkt.len is None else + pkt.len - ( + 12 + + (pkt.nb_pairwise_cipher_suites or 0) * 4 + + (pkt.nb_akm_suites or 0) * 4 + + (2 if pkt.pmkids else 0) + + (pkt.pmkids and pkt.pmkids.nb_pmkids or 0) * 16 + ) >= 4 + ) ) ] @@ -893,82 +1288,285 @@ def extract_padding(self, s): class Dot11EltCountry(Dot11Elt): name = "802.11 Country" + match_subclass = True fields_desc = [ - ByteField("ID", 7), + ByteEnumField("ID", 7, _dot11_id_enum), ByteField("len", None), StrFixedLenField("country_string", b"\0\0\0", length=3), - PacketListField( + MayEnd(PacketListField( "descriptors", [], Dot11EltCountryConstraintTriplet, length_from=lambda pkt: ( pkt.len - 3 - (pkt.len % 3) ) - ), + )), + # When this extension is last, padding appears to be omitted ConditionalField( ByteField("pad", 0), - lambda pkt: (len(pkt.descriptors) + 1) % 2 + # The length should be 3 bytes per each triplet, and 3 bytes for the + # country_string field. The standard dictates that the element length + # must be even, so if the result is odd, add a padding byte. + # Some transmitters don't comply with the standard, so instead of assuming + # the length, we test whether there is a padding byte. + # Some edge cases are still not covered, for example, if the tag length + # (pkt.len) is an arbitrary number. + lambda pkt: ((len(pkt.descriptors) + 1) % 2) if pkt.len is None else (pkt.len % 3) # noqa: E501 ) ] -class Dot11EltMicrosoftWPA(Dot11Elt): - name = "802.11 Microsoft WPA" - fields_desc = [ - ByteField("ID", 221), - ByteField("len", None), - X3BytesField("oui", 0x0050f2), - XByteField("type", 0x01), - LEShortField("version", 1), - PacketField("group_cipher_suite", RSNCipherSuite(), RSNCipherSuite), - LEFieldLenField( - "nb_pairwise_cipher_suites", - 1, - count_of="pairwise_cipher_suites" - ), - PacketListField( - "pairwise_cipher_suites", - RSNCipherSuite(), - RSNCipherSuite, - count_from=lambda p: p.nb_pairwise_cipher_suites - ), - LEFieldLenField( - "nb_akm_suites", - 1, - count_of="akm_suites" - ), - PacketListField( - "akm_suites", - AKMSuite(), - AKMSuite, - count_from=lambda p: p.nb_akm_suites - ) - ] +class _RateField(ByteField): + def i2repr(self, pkt, val): + if val is None: + return "" + s = str((val & 0x7f) / 2.) + if val & 0x80: + s += "(B)" + return s + " Mbps" class Dot11EltRates(Dot11Elt): name = "802.11 Rates" + match_subclass = True fields_desc = [ - ByteField("ID", 1), + ByteEnumField("ID", 1, _dot11_id_enum), ByteField("len", None), FieldListField( "rates", - [], - XByteField("", 0), - count_from=lambda p: p.len + [0x82], + _RateField("", 0), + length_from=lambda p: p.len ) ] +Dot11EltRates.register_variant(50) # Extended rates + + +class Dot11EltHTCapabilities(Dot11Elt): + name = "802.11 HT Capabilities" + match_subclass = True + fields_desc = [ + ByteEnumField("ID", 45, _dot11_id_enum), + ByteField("len", None), + # HT Capabilities Info: 2B + BitField("L_SIG_TXOP_Protection", 0, 1, tot_size=-2), + BitField("Forty_Mhz_Intolerant", 0, 1), + BitField("PSMP", 0, 1), + BitField("DSSS_CCK", 0, 1), + BitEnumField("Max_A_MSDU", 0, 1, {0: "3839 o", 1: "7935 o"}), + BitField("Delayed_BlockAck", 0, 1), + BitField("Rx_STBC", 0, 2), + BitField("Tx_STBC", 0, 1), + BitField("Short_GI_40Mhz", 0, 1), + BitField("Short_GI_20Mhz", 0, 1), + BitField("Green_Field", 0, 1), + BitEnumField("SM_Power_Save", 0, 2, + {0: "static SM", 1: "dynamic SM", 3: "disabled"}), + BitEnumField("Supported_Channel_Width", 0, 1, + {0: "20Mhz", 1: "20Mhz+40Mhz"}), + BitField("LDPC_Coding_Capability", 0, 1, end_tot_size=-2), + # A-MPDU Parameters: 1B + BitField("res1", 0, 3, tot_size=-1), + BitField("Min_MPDCU_Start_Spacing", 8, 3), + BitField("Max_A_MPDU_Length_Exponent", 3, 2, end_tot_size=-1), + # Supported MCS set: 16B + BitField("res2", 0, 27, tot_size=-16), + BitField("TX_Unequal_Modulation", 0, 1), + BitField("TX_Max_Spatial_Streams", 0, 2), + BitField("TX_RX_MCS_Set_Not_Equal", 0, 1), + BitField("TX_MCS_Set_Defined", 0, 1), + BitField("res3", 0, 6), + BitField("RX_Highest_Supported_Data_Rate", 0, 10), + BitField("res4", 0, 3), + BitField("RX_MSC_Bitmask", 0, 77, end_tot_size=-16), + # HT Extended capabilities: 2B + BitField("res5", 0, 4, tot_size=-2), + BitField("RD_Responder", 0, 1), + BitField("HTC_HT_Support", 0, 1), + BitField("MCS_Feedback", 0, 2), + BitField("res6", 0, 5), + BitField("PCO_Transition_Time", 0, 2), + BitField("PCO", 0, 1, end_tot_size=-2), + # TX Beamforming Capabilities TxBF: 4B + BitField("res7", 0, 3, tot_size=-4), + BitField("Channel_Estimation_Capability", 0, 2), + BitField("CSI_max_n_Rows_Beamformer_Supported", 0, 2), + BitField("Compressed_Steering_n_Beamformer_Antennas_Supported", 0, 2), + BitField("Noncompressed_Steering_n_Beamformer_Antennas_Supported", + 0, 2), + BitField("CSI_n_Beamformer_Antennas_Supported", 0, 2), + BitField("Minimal_Grouping", 0, 2), + BitField("Explicit_Compressed_Beamforming_Feedback", 0, 2), + BitField("Explicit_Noncompressed_Beamforming_Feedback", 0, 2), + BitField("Explicit_Transmit_Beamforming_CSI_Feedback", 0, 2), + BitField("Explicit_Compressed_Steering", 0, 1), + BitField("Explicit_Noncompressed_Steering", 0, 1), + BitField("Explicit_CSI_Transmit_Beamforming", 0, 1), + BitField("Calibration", 0, 2), + BitField("Implicit_Trasmit_Beamforming", 0, 1), + BitField("Transmit_NDP", 0, 1), + BitField("Receive_NDP", 0, 1), + BitField("Transmit_Staggered_Sounding", 0, 1), + BitField("Receive_Staggered_Sounding", 0, 1), + BitField("Implicit_Transmit_Beamforming_Receiving", 0, 1, + end_tot_size=-4), + # ASEL Capabilities: 1B + FlagsField("ASEL", 0, 8, [ + "res", + "Transmit_Sounding_PPDUs", + "Receive_ASEL", + "Antenna_Indices_Feedback", + "Explicit_CSI_Feedback", + "Explicit_CSI_Feedback_Based_Transmit_ASEL", + "Antenna_Selection", + ]) + ] + + class Dot11EltVendorSpecific(Dot11Elt): name = "802.11 Vendor Specific" + match_subclass = True fields_desc = [ - ByteField("ID", 221), + ByteEnumField("ID", 221, _dot11_id_enum), ByteField("len", None), - X3BytesField("oui", 0x000000), + OUIField("oui", 0x000000), StrLenField("info", "", length_from=lambda x: x.len - 3) ] + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + oui = struct.unpack("!I", b"\x00" + _pkt[2:5])[0] + ouicls = cls.registered_ouis.get(oui, cls) + if ouicls.dispatch_hook != cls.dispatch_hook: + # Sub-classes can have their own dispatch_hook + return ouicls.dispatch_hook(_pkt=_pkt, *args, **kargs) + cls = ouicls + return cls + + registered_ouis = {} + + @classmethod + def register_variant(cls): + oui = cls.oui.default + if not oui: + # This is Dot11EltVendorSpecific, register it in the super-class. + super().register_variant() + elif oui not in cls.registered_ouis: + # Sub-Vendor (e.g. Dot11EltMicrosoftWPA) + cls.registered_ouis[oui] = cls + + +class Dot11EltMicrosoftWPA(Dot11EltVendorSpecific): + name = "802.11 Microsoft WPA" + match_subclass = True + ID = 221 + oui = 0x0050f2 + # It appears many WPA implementations ignore the fact + # that this IE should only have a single cipher and auth suite + fields_desc = Dot11EltVendorSpecific.fields_desc[:3] + [ + XByteField("type", 0x01) + ] + Dot11EltRSN.fields_desc[2:8] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + type_ = orb(_pkt[5]) + if type_ == 0x01: + # MS WPA IE + return Dot11EltMicrosoftWPA + elif type_ == 0x02: + # MS WME IE TODO + # return Dot11EltMicrosoftWME + pass + elif type_ == 0x04: + # MS WPS IE TODO + # return Dot11EltWPS + pass + return Dot11EltVendorSpecific + return cls + + +# 802.11-2016 9.4.2.19 + +class Dot11EltCSA(Dot11Elt): + name = "802.11 CSA Element" + match_subclass = True + fields_desc = [ + ByteEnumField("ID", 37, _dot11_id_enum), + ByteField("len", 3), + ByteField("mode", 0), + ByteField("new_channel", 0), + ByteField("channel_switch_count", 0) + ] + + +# 802.11-2016 9.4.2.59 + +class Dot11EltOBSS(Dot11Elt): + name = "802.11 OBSS Scan Parameters Element" + match_subclass = True + fields_desc = [ + ByteEnumField("ID", 74, _dot11_id_enum), + ByteField("len", 14), + LEShortField("Passive_Dwell", 0), + LEShortField("Active_Dwell", 0), + LEShortField("Scan_Interval", 0), + LEShortField("Passive_Total_Per_Channel", 0), + LEShortField("Active_Total_Per_Channel", 0), + LEShortField("Delay", 0), + LEShortField("Activity_Threshold", 0), + ] + + +# 802.11-2016 9.4.2.159 + +class Dot11VHTOperationInfo(Packet): + name = "802.11 VHT Operation Information" + fields_desc = [ + ByteField("channel_width", 0), + ByteField("channel_center0", 36), + ByteField("channel_center1", 0), + ] + + def extract_padding(self, s): + return "", s + + +class Dot11EltVHTOperation(Dot11Elt): + name = "802.11 VHT Operation Element" + match_subclass = True + fields_desc = [ + ByteEnumField("ID", 192, _dot11_id_enum), + ByteField("len", 5), + PacketField( + "VHT_Operation_Info", + Dot11VHTOperationInfo(), + Dot11VHTOperationInfo + ), + FieldListField( + "mcs_set", + [0x00], + BitField('SS', 0x00, size=2), + count_from=lambda x: 8 + ) + ] + + +###################### +# 802.11 Frame types # +###################### + +# 802.11-2016 9.3 + +class Dot11Beacon(_Dot11EltUtils): + name = "802.11 Beacon" + fields_desc = [LELongField("timestamp", 0), + LEShortField("beacon_interval", 0x0064), + FlagsField("cap", 0, 16, capability_list)] + class Dot11ATIM(Packet): name = "802.11 ATIM" @@ -979,20 +1577,20 @@ class Dot11Disas(Packet): fields_desc = [LEShortEnumField("reason", 1, reason_code)] -class Dot11AssoReq(Packet): +class Dot11AssoReq(_Dot11EltUtils): name = "802.11 Association Request" fields_desc = [FlagsField("cap", 0, 16, capability_list), LEShortField("listen_interval", 0x00c8)] -class Dot11AssoResp(Packet): +class Dot11AssoResp(_Dot11EltUtils): name = "802.11 Association Response" fields_desc = [FlagsField("cap", 0, 16, capability_list), LEShortField("status", 0), LEShortField("AID", 0)] -class Dot11ReassoReq(Packet): +class Dot11ReassoReq(_Dot11EltUtils): name = "802.11 Reassociation Request" fields_desc = [FlagsField("cap", 0, 16, capability_list), LEShortField("listen_interval", 0x00c8), @@ -1003,22 +1601,31 @@ class Dot11ReassoResp(Dot11AssoResp): name = "802.11 Reassociation Response" -class Dot11ProbeReq(Packet): +class Dot11ProbeReq(_Dot11EltUtils): name = "802.11 Probe Request" -class Dot11ProbeResp(_Dot11NetStats): +class Dot11ProbeResp(_Dot11EltUtils): name = "802.11 Probe Response" + fields_desc = [LELongField("timestamp", 0), + LEShortField("beacon_interval", 0x0064), + FlagsField("cap", 0, 16, capability_list)] -class Dot11Auth(Packet): +class Dot11Auth(_Dot11EltUtils): name = "802.11 Authentication" fields_desc = [LEShortEnumField("algo", 0, ["open", "sharedkey"]), LEShortField("seqnum", 0), LEShortEnumField("status", 0, status_code)] def answers(self, other): - if self.seqnum == other.seqnum + 1: + if self.algo != other.algo: + return 0 + + if ( + self.seqnum == other.seqnum + 1 or + (self.algo == 3 and self.seqnum == other.seqnum) + ): return 1 return 0 @@ -1028,6 +1635,258 @@ class Dot11Deauth(Packet): fields_desc = [LEShortEnumField("reason", 1, reason_code)] +class Dot11Ack(Packet): + name = "802.11 Ack packet" + + +# 802.11-2016 9.4.1.11 + +class Dot11Action(Packet): + name = "802.11 Action" + fields_desc = [ + ByteEnumField("category", 0x00, { + 0x00: "Spectrum Management", + 0x01: "QoS", + 0x02: "DLS", + 0x03: "Block", + 0x04: "Public", + 0x05: "Radio Measurement", + 0x06: "Fast BSS Transition", + 0x07: "HT", + 0x08: "SA Query", + 0x09: "Protected Dual of Public Action", + 0x0A: "WNM", + 0x0B: "Unprotected WNM", + 0x0C: "TDLS", + 0x0D: "Mesh", + 0x0E: "Multihop", + 0x0F: "Self-protected", + 0x10: "DMG", + 0x11: "Reserved Wi-Fi Alliance", + 0x12: "Fast Session Transfer", + 0x13: "Robust AV Streaming", + 0x14: "Unprotected DMG", + 0x15: "VHT" + }) + ] + + +# 802.11-2016 9.6.14.1 + +class Dot11WNM(Packet): + name = "802.11 WNM Action" + fields_desc = [ + ByteEnumField("action", 0x00, { + 0x00: "Event Request", + 0x01: "Event Report", + 0x02: "Diagnostic Request", + 0x03: "Diagnostic Report", + 0x04: "Location Configuration Request", + 0x05: "Location Configuration Response", + 0x06: "BSS Transition Management Query", + 0x07: "BSS Transition Management Request", + 0x08: "BSS Transition Management Response", + 0x09: "FMS Request", + 0x0A: "FMS Response", + 0x0B: "Collocated Interference Request", + 0x0C: "Collocated Interference Report", + 0x0D: "TFS Request", + 0x0E: "TFS Response", + 0x0F: "TFS Notify", + 0x10: "WNM Sleep Mode Request", + 0x11: "WNM Sleep Mode Response", + 0x12: "TIM Broadcast Request", + 0x13: "TIM Broadcast Response", + 0x14: "QoS Traffic Capability Update", + 0x15: "Channel Usage Request", + 0x16: "Channel Usage Response", + 0x17: "DMS Request", + 0x18: "DMS Response", + 0x19: "Timing Measurement Request", + 0x1A: "WNM Notification Request", + 0x1B: "WNM Notification Response", + 0x1C: "WNM-Notify Response" + }) + ] + + +# 802.11-2016 9.4.2.37 + +class SubelemTLV(Packet): + fields_desc = [ + ByteField("type", 0), + LEFieldLenField("len", None, fmt="B", length_of="value"), + FieldListField( + "value", + [], + ByteField('', 0), + length_from=lambda p: p.len + ) + ] + + +class BSSTerminationDuration(Packet): + name = "BSS Termination Duration" + fields_desc = [ + ByteField("id", 4), + ByteField("len", 10), + LELongField("TSF", 0), + LEShortField("duration", 0) + ] + + def extract_padding(self, s): + return "", s + + +class NeighborReport(Packet): + name = "Neighbor Report" + fields_desc = [ + ByteField("type", 0), + ByteField("len", 13), + MACField("BSSID", ETHER_ANY), + # BSSID Information + BitField("AP_reach", 0, 2, tot_size=-4), + BitField("security", 0, 1), + BitField("key_scope", 0, 1), + BitField("capabilities", 0, 6), + BitField("mobility", 0, 1), + BitField("HT", 0, 1), + BitField("VHT", 0, 1), + BitField("FTM", 0, 1), + BitField("reserved", 0, 18, end_tot_size=-4), + # BSSID Information end + ByteField("op_class", 0), + ByteField("channel", 0), + ByteField("phy_type", 0), + ConditionalField( + PacketListField( + "subelems", + SubelemTLV(), + SubelemTLV, + length_from=lambda p: p.len - 13 + ), + lambda p: p.len > 13 + ) + ] + + +# 802.11-2016 9.6.14.9 + +btm_request_mode = [ + "Preferred_Candidate_List_Included", + "Abridged", + "Disassociation_Imminent", + "BSS_Termination_Included", + "ESS_Disassociation_Imminent" +] + + +class Dot11BSSTMRequest(Packet): + name = "BSS Transition Management Request" + fields_desc = [ + ByteField("token", 0), + FlagsField("mode", 0, 8, btm_request_mode), + LEShortField("disassociation_timer", 0), + ByteField("validity_interval", 0), + ConditionalField( + PacketField( + "termination_duration", + BSSTerminationDuration(), + BSSTerminationDuration + ), + lambda p: p.mode and p.mode.BSS_Termination_Included + ), + ConditionalField( + ByteField("url_len", 0), + lambda p: p.mode and p.mode.ESS_Disassociation_Imminent + ), + ConditionalField( + StrLenField("url", "", length_from=lambda p: p.url_len), + lambda p: p.mode and p.mode.ESS_Disassociation_Imminent != 0 + ), + ConditionalField( + PacketListField( + "neighbor_report", + NeighborReport(), + NeighborReport + ), + lambda p: p.mode and p.mode.Preferred_Candidate_List_Included + ) + ] + + +# 802.11-2016 9.6.14.10 + +btm_status_code = [ + "Accept", + "Reject-Unspecified_reject_reason", + "Reject-Insufficient_Beacon_or_Probe_Response_frames", + "Reject-Insufficient_available_capacity_from_all_candidates", + "Reject-BSS_termination_undesired", + "Reject-BSS_termination_delay_requested", + "Reject-STA_BSS_Transition_Candidate_List_provided", + "Reject-No_suitable_BSS_transition_candidates", + "Reject-Leaving_ESS" +] + + +class Dot11BSSTMResponse(Packet): + name = "BSS Transition Management Response" + fields_desc = [ + ByteField("token", 0), + ByteEnumField("status", 0, btm_status_code), + ByteField("termination_delay", 0), + ConditionalField( + MACField("target", ETHER_ANY), + lambda p: p.status == 0 + ), + ConditionalField( + PacketListField( + "neighbor_report", + NeighborReport(), + NeighborReport + ), + lambda p: p.status == 6 + ) + ] + + +# 802.11-2016 9.6.2.1 + +class Dot11SpectrumManagement(Packet): + name = "802.11 Spectrum Management Action" + fields_desc = [ + ByteEnumField("action", 0x00, { + 0x00: "Measurement Request", + 0x01: "Measurement Report", + 0x02: "TPC Request", + 0x03: "TPC Report", + 0x04: "Channel Switch Announcement", + }) + ] + + +# 802.11-2016 9.6.2.6 + +class Dot11CSA(Packet): + name = "Channel Switch Announcement Frame" + fields_desc = [ + PacketField("CSA", Dot11EltCSA(), Dot11EltCSA), + ] + + +class Dot11S1GBeacon(_Dot11EltUtils): + name = "802.11 S1G Beacon" + fields_desc = [LEIntField("timestamp", 0), + ByteField("change_seq", 0)] + + +################### +# 802.11 Security # +################### + +# 802.11-2016 12 + class Dot11Encrypted(Packet): name = "802.11 Encrypted (unknown algorithm)" fields_desc = [StrField("data", None)] @@ -1052,6 +1911,8 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return conf.raw_layer +# 802.11-2016 12.3.2 + class Dot11WEP(Dot11Encrypted): name = "802.11 WEP packet" fields_desc = [StrFixedLenField("iv", b"\0\0\0", 3), @@ -1064,7 +1925,7 @@ def decrypt(self, key=None): key = conf.wepkey if key and conf.crypto_valid: d = Cipher( - algorithms.ARC4(self.iv + key.encode("utf8")), + decrepit_algorithms.ARC4(self.iv + key.encode("utf8")), None, default_backend(), ).decryptor() @@ -1089,7 +1950,7 @@ def encrypt(self, p, pay, key=None): else: icv = p[4:8] e = Cipher( - algorithms.ARC4(self.iv + key.encode("utf8")), + decrepit_algorithms.ARC4(self.iv + key.encode("utf8")), None, default_backend(), ).encryptor() @@ -1103,10 +1964,10 @@ def post_build(self, p, pay): p = self.encrypt(p, raw(pay)) return p -# Dot11TKIP & Dot11CCMP - # we can't dissect ICV / MIC here: they are encrypted +# 802.11-2016 12.5.2.2 + class Dot11TKIP(Dot11Encrypted): name = "802.11 TKIP packet" @@ -1127,9 +1988,11 @@ class Dot11TKIP(Dot11Encrypted): StrField("data", None), ] +# 802.11-2016 12.5.3.2 + class Dot11CCMP(Dot11Encrypted): - name = "802.11 TKIP packet" + name = "802.11 CCMP packet" fields_desc = [ # iv - 8 bytes ByteField("PN0", 0), @@ -1147,15 +2010,19 @@ class Dot11CCMP(Dot11Encrypted): ] -class Dot11Ack(Packet): - name = "802.11 Ack packet" +############ +# Bindings # +############ bind_top_down(RadioTap, Dot11FCS, present=2, Flags=16) +bind_top_down(Dot11, Dot11QoS, type=2, subtype=0xc) bind_layers(PrismHeader, Dot11,) bind_layers(Dot11, LLC, type=2) bind_layers(Dot11QoS, LLC,) + +# 802.11-2016 9.2.4.1.3 Type and Subtype subfields bind_layers(Dot11, Dot11AssoReq, subtype=0, type=0) bind_layers(Dot11, Dot11AssoResp, subtype=1, type=0) bind_layers(Dot11, Dot11ReassoReq, subtype=2, type=0) @@ -1163,12 +2030,15 @@ class Dot11Ack(Packet): bind_layers(Dot11, Dot11ProbeReq, subtype=4, type=0) bind_layers(Dot11, Dot11ProbeResp, subtype=5, type=0) bind_layers(Dot11, Dot11Beacon, subtype=8, type=0) +bind_layers(Dot11, Dot11S1GBeacon, subtype=1, type=3) bind_layers(Dot11, Dot11ATIM, subtype=9, type=0) bind_layers(Dot11, Dot11Disas, subtype=10, type=0) bind_layers(Dot11, Dot11Auth, subtype=11, type=0) bind_layers(Dot11, Dot11Deauth, subtype=12, type=0) +bind_layers(Dot11, Dot11Action, subtype=13, type=0) bind_layers(Dot11, Dot11Ack, subtype=13, type=1) bind_layers(Dot11Beacon, Dot11Elt,) +bind_layers(Dot11S1GBeacon, Dot11Elt,) bind_layers(Dot11AssoReq, Dot11Elt,) bind_layers(Dot11AssoResp, Dot11Elt,) bind_layers(Dot11ReassoReq, Dot11Elt,) @@ -1179,6 +2049,11 @@ class Dot11Ack(Packet): bind_layers(Dot11Elt, Dot11Elt,) bind_layers(Dot11TKIP, conf.raw_layer) bind_layers(Dot11CCMP, conf.raw_layer) +bind_layers(Dot11Action, Dot11SpectrumManagement, category=0x00) +bind_layers(Dot11SpectrumManagement, Dot11CSA, action=4) +bind_layers(Dot11Action, Dot11WNM, category=0x0A) +bind_layers(Dot11WNM, Dot11BSSTMRequest, action=7) +bind_layers(Dot11WNM, Dot11BSSTMResponse, action=8) conf.l2types.register(DLT_IEEE802_11, Dot11) @@ -1188,6 +2063,10 @@ class Dot11Ack(Packet): conf.l2types.register(DLT_IEEE802_11_RADIO, RadioTap) conf.l2types.register_num2layer(803, RadioTap) +#################### +# Other WiFi utils # +#################### + class WiFi_am(AnsweringMachine): """Before using this, initialize "iffrom" and "ifto" interfaces: @@ -1236,8 +2115,8 @@ def make_reply(self, p): ip = p.getlayer(IP) tcp = p.getlayer(TCP) pay = raw(tcp.payload) - del(p.payload.payload.payload) - p.FCfield = "from-DS" + p[IP].underlayer.remove_payload() + p.FCfield = "from_DS" p.addr1, p.addr2 = p.addr2, p.addr1 p /= IP(src=ip.dst, dst=ip.src) p /= TCP(sport=tcp.dport, dport=tcp.sport, diff --git a/scapy/layers/dot15d4.py b/scapy/layers/dot15d4.py index 7e93b25944c..b83933de6ef 100644 --- a/scapy/layers/dot15d4.py +++ b/scapy/layers/dot15d4.py @@ -1,11 +1,11 @@ -# This program is published under a GPLv2 license +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi # Copyright (C) Ryan Speers 2011-2012 # Copyright (C) Roger Meyer : 2012-03-10 Added frames -# Copyright (C) Gabriel Potter : 2018 -# Intern at INRIA Grand Nancy Est -# This program is published under a GPLv2 license +# Copyright (C) Gabriel Potter : 2018 +# Copyright (C) Dimitrios-Georgios Akestoridis """ Wireless MAC according to IEEE 802.15.4. @@ -19,9 +19,24 @@ from scapy.data import DLT_IEEE802_15_4_WITHFCS, DLT_IEEE802_15_4_NOFCS from scapy.packet import Packet, bind_layers -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - ConditionalField, Field, LELongField, PacketField, XByteField, \ - XLEIntField, XLEShortField, FCSField, Emph +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + Emph, + FCSField, + Field, + FieldListField, + LELongField, + MultipleTypeField, + PacketField, + StrFixedLenField, + XByteField, + XLEIntField, + XLEShortField, +) # Fields # @@ -76,7 +91,7 @@ def lengthFromAddrMode(self, pkt, x): if pkttop.underlayer is None: break pkttop = pkttop.underlayer - # print "Underlayer field value of", x, "is", addrmode + # print("Underlayer field value of", x, "is", addrmode) if addrmode == 2: return 2 elif addrmode == 3: @@ -193,12 +208,14 @@ class Dot15d4AuxSecurityHeader(Packet): XLEIntField("sec_framecounter", 0x00000000), # 4 octets # Key Identifier (variable length): identifies the key that is used for cryptographic protection # noqa: E501 # Key Source : length of sec_keyid_keysource varies btwn 0, 4, and 8 bytes depending on sec_sc_keyidmode # noqa: E501 - # 4 octets when sec_sc_keyidmode == 2 - ConditionalField(XLEIntField("sec_keyid_keysource", 0x00000000), - lambda pkt: pkt.getfieldval("sec_sc_keyidmode") == 2), - # 8 octets when sec_sc_keyidmode == 3 - ConditionalField(LELongField("sec_keyid_keysource", 0x0000000000000000), # noqa: E501 - lambda pkt: pkt.getfieldval("sec_sc_keyidmode") == 3), + MultipleTypeField([ + # 4 octets when sec_sc_keyidmode == 2 + (XLEIntField("sec_keyid_keysource", 0x00000000), + lambda pkt: pkt.getfieldval("sec_sc_keyidmode") == 2), + # 8 octets when sec_sc_keyidmode == 3 + (LELongField("sec_keyid_keysource", 0x0000000000000000), + lambda pkt: pkt.getfieldval("sec_sc_keyidmode") == 3), + ], StrFixedLenField("sec_keyid_keysource", "", length=0)), # Key Index (1 octet): allows unique identification of different keys with the same originator # noqa: E501 ConditionalField(XByteField("sec_keyid_keyindex", 0xFF), lambda pkt: pkt.getfieldval("sec_sc_keyidmode") != 0), @@ -275,12 +292,17 @@ class Dot15d4Beacon(Packet): # Pending Address Fields: # Pending Address Specification (1 byte) - BitField("pa_num_short", 0, 3), # number of short addresses pending BitField("pa_reserved_1", 0, 1), BitField("pa_num_long", 0, 3), # number of long addresses pending BitField("pa_reserved_2", 0, 1), + BitField("pa_num_short", 0, 3), # number of short addresses pending # Address List (var length) - # TODO add a FieldListField of the pending short addresses, followed by the pending long addresses, with max 7 addresses # noqa: E501 + FieldListField("pa_short_addresses", [], + XLEShortField("", 0x0000), + count_from=lambda pkt: pkt.pa_num_short), + FieldListField("pa_long_addresses", [], + dot15d4AddressField("", 0, adjust=lambda pkt, x: 8), + count_from=lambda pkt: pkt.pa_num_long), # TODO beacon payload ] @@ -348,13 +370,24 @@ class Dot15d4CmdCoordRealign(Packet): ByteField("channel", 0), # Short Address (2 octets) XLEShortField("dev_address", 0xFFFF), - # Channel page (0/1 octet) TODO optional - # ByteField("channel_page", 0), ] def mysummary(self): return self.sprintf("802.15.4 Coordinator Realign Payload ( PAN ID: %Dot15dCmdCoordRealign.pan_id% : channel %Dot15d4CmdCoordRealign.channel% )") # noqa: E501 + def guess_payload_class(self, payload): + if len(payload) == 1: + return Dot15d4CmdCoordRealignPage + else: + return Packet.guess_payload_class(self, payload) + + +class Dot15d4CmdCoordRealignPage(Packet): + name = "802.15.4 Coordinator Realign Page" + fields_desc = [ + ByteField("channel_page", 0), + ] + # Utility Functions # diff --git a/scapy/layers/eap.py b/scapy/layers/eap.py index 82f6c5c1870..2894570c18a 100644 --- a/scapy/layers/eap.py +++ b/scapy/layers/eap.py @@ -1,24 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Extensible Authentication Protocol (EAP) """ -from __future__ import absolute_import -from __future__ import print_function import struct -from scapy.fields import BitField, ByteField, XByteField,\ - ShortField, IntField, XIntField, ByteEnumField, StrLenField, XStrField,\ - XStrLenField, XStrFixedLenField, LenField, FieldLenField, FieldListField,\ - PacketField, PacketListField, ConditionalField, PadField -from scapy.packet import Packet, Padding, bind_layers +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + IntField, + LenField, + LongField, + PacketField, + PacketListField, + PadField, + ShortField, + StrLenField, + XByteField, + XIntField, + XStrField, + XStrFixedLenField, + XStrLenField, +) +from scapy.packet import ( + Packet, + Padding, + bind_bottom_up, + bind_layers, + bind_top_down, +) from scapy.layers.l2 import SourceMACField, Ether, CookedLinux, GRE, SNAP -from scapy.utils import issubtype from scapy.config import conf from scapy.compat import orb, chb @@ -199,13 +220,11 @@ class EAP(Packet): ConditionalField(ByteEnumField("type", 0, eap_types), lambda pkt:pkt.code not in [ EAP.SUCCESS, EAP.FAILURE]), - ConditionalField(FieldListField( - "desired_auth_types", - [], - ByteEnumField("auth_type", 0, eap_types), - length_from=lambda pkt: pkt.len - 4 - ), - lambda pkt:pkt.code == EAP.RESPONSE and pkt.type == 3), # noqa: E501 + ConditionalField( + FieldListField("desired_auth_types", [], + ByteEnumField("auth_type", 0, eap_types), + length_from=lambda pkt: pkt.len - 4), + lambda pkt:pkt.code == EAP.RESPONSE and pkt.type == 3), ConditionalField( StrLenField("identity", '', length_from=lambda pkt: pkt.len - 5), lambda pkt: pkt.code == EAP.RESPONSE and hasattr(pkt, 'type') and pkt.type == 1), # noqa: E501 @@ -243,19 +262,6 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return cls.registered_methods.get(t, cls) return cls - def haslayer(self, cls): - if cls == "EAP": - if isinstance(self, EAP): - return True - elif issubtype(cls, EAP): - if isinstance(self, cls): - return True - return super(EAP, self).haslayer(cls) - - def getlayer(self, cls, nb=1, _track=None, _subclass=True, **flt): - return super(EAP, self).getlayer(cls, nb=nb, _track=_track, - _subclass=True, **flt) - def answers(self, other): if isinstance(other, EAP): if self.code == self.REQUEST: @@ -295,6 +301,7 @@ class EAP_MD5(EAP): """ name = "EAP-MD5" + match_subclass = True fields_desc = [ ByteEnumField("code", 1, eap_codes), ByteField("id", 0), @@ -313,6 +320,7 @@ class EAP_TLS(EAP): """ name = "EAP-TLS" + match_subclass = True fields_desc = [ ByteEnumField("code", 1, eap_codes), ByteField("id", 0), @@ -335,6 +343,7 @@ class EAP_TTLS(EAP): """ name = "EAP-TTLS" + match_subclass = True fields_desc = [ ByteEnumField("code", 1, eap_codes), ByteField("id", 0), @@ -357,6 +366,7 @@ class EAP_PEAP(EAP): """ name = "PEAP" + match_subclass = True fields_desc = [ ByteEnumField("code", 1, eap_codes), ByteField("id", 0), @@ -380,6 +390,7 @@ class EAP_FAST(EAP): """ name = "EAP-FAST" + match_subclass = True fields_desc = [ ByteEnumField("code", 1, eap_codes), ByteField("id", 0), @@ -403,6 +414,7 @@ class LEAP(EAP): """ name = "Cisco LEAP" + match_subclass = True fields_desc = [ ByteEnumField("code", 1, eap_codes), ByteField("id", 0), @@ -416,6 +428,83 @@ class LEAP(EAP): ] +############################################################################# +# IEEE 802.1X-2010 - EAPOL-Key +############################################################################# + +# sect 11.9 of 802.1X-2010 +# AND sect 12.7.2 of 802.11-2016 + + +class EAPOL_KEY(Packet): + name = "EAPOL_KEY" + deprecated_fields = { + "key": ("key_data", "2.6.0"), + "len": ("key_length", "2.6.0"), + } + fields_desc = [ + ByteEnumField("key_descriptor_type", 1, {1: "RC4", 2: "RSN"}), + # Key Information + BitField("res2", 0, 2), + BitField("smk_message", 0, 1), + BitField("encrypted_key_data", 0, 1), + BitField("request", 0, 1), + BitField("error", 0, 1), + BitField("secure", 0, 1), + BitField("has_key_mic", 1, 1), + BitField("key_ack", 0, 1), + BitField("install", 0, 1), + BitField("res", 0, 2), + BitEnumField("key_type", 0, 1, {0: "Group/SMK", 1: "Pairwise"}), + BitEnumField("key_descriptor_type_version", 0, 3, { + 1: "HMAC-MD5+ARC4", + 2: "HMAC-SHA1-128+AES-128", + 3: "AES-128-CMAC+AES-128", + }), + # + LenField("key_length", None, "H"), + LongField("key_replay_counter", 0), + XStrFixedLenField("key_nonce", "", 32), + XStrFixedLenField("key_iv", "", 16), + XStrFixedLenField("key_rsc", "", 8), + XStrFixedLenField("key_id", "", 8), + XStrFixedLenField("key_mic", "", 16), # XXX size can be 24 + FieldLenField("key_data_length", None, length_of="key_data"), + XStrLenField("key_data", "", + length_from=lambda pkt: pkt.key_data_length) + ] + + def extract_padding(self, s): + return s[:self.key_length], s[self.key_length:] + + def hashret(self): + return struct.pack("!B", self.type) + self.payload.hashret() + + def answers(self, other): + if isinstance(other, EAPOL_KEY) and \ + other.descriptor_type == self.descriptor_type: + return 1 + return 0 + + def guess_key_number(self): + """ + Determines 4-way handshake key number + + :return: key number (1-4), or 0 if it cannot be determined + """ + if self.key_type == 1: + if self.key_ack == 1: + if self.has_key_mic == 0: + return 1 + if self.install == 1: + return 3 + else: + if self.secure == 0: + return 2 + return 4 + return 0 + + ############################################################################# # IEEE 802.1X-2010 - MACsec Key Agreement (MKA) protocol ############################################################################# @@ -777,10 +866,14 @@ def extract_padding(self, s): return "", s -bind_layers(Ether, EAPOL, type=34958) -bind_layers(Ether, EAPOL, dst='01:80:c2:00:00:03', type=34958) -bind_layers(CookedLinux, EAPOL, proto=34958) -bind_layers(GRE, EAPOL, proto=34958) +# Bind EAPOL types bind_layers(EAPOL, EAP, type=0) -bind_layers(SNAP, EAPOL, code=34958) +bind_layers(EAPOL, EAPOL_KEY, type=3) bind_layers(EAPOL, MKAPDU, type=5) + +bind_bottom_up(Ether, EAPOL, type=0x888e) +# the reserved IEEE Std 802.1X PAE address +bind_top_down(Ether, EAPOL, dst='01:80:c2:00:00:03', type=0x888e) +bind_layers(CookedLinux, EAPOL, proto=0x888e) +bind_layers(SNAP, EAPOL, code=0x888e) +bind_layers(GRE, EAPOL, proto=0x888e) diff --git a/scapy/layers/gprs.py b/scapy/layers/gprs.py index 8a35efae5c6..b994cf7ac52 100644 --- a/scapy/layers/gprs.py +++ b/scapy/layers/gprs.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ GPRS (General Packet Radio Service) for mobile data communication. diff --git a/scapy/layers/gssapi.py b/scapy/layers/gssapi.py new file mode 100644 index 00000000000..5e1732ab078 --- /dev/null +++ b/scapy/layers/gssapi.py @@ -0,0 +1,752 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Generic Security Services (GSS) API + +Implements parts of: + + - GSSAPI: RFC4121 / RFC2743 + - GSSAPI C bindings: RFC2744 + - Channel Bindings for TLS: RFC5929 + +This is implemented in the following SSPs: + + - :class:`~scapy.layers.ntlm.NTLMSSP` + - :class:`~scapy.layers.kerberos.KerberosSSP` + - :class:`~scapy.layers.spnego.SPNEGOSSP` + - :class:`~scapy.layers.msrpce.msnrpc.NetlogonSSP` + +.. note:: + You will find more complete documentation for this layer over at + `GSSAPI `_ +""" + +import abc + +from dataclasses import dataclass +from enum import Enum, IntEnum, IntFlag + +from scapy.asn1.asn1 import ( + ASN1_SEQUENCE, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, +) +from scapy.asn1.ber import BERcodec_SEQUENCE, BER_id_dec +from scapy.asn1.mib import conf # loads conf.mib +from scapy.asn1fields import ( + ASN1F_OID, + ASN1F_PACKET, + ASN1F_SEQUENCE, +) +from scapy.asn1packet import ASN1_Packet +from scapy.error import log_runtime +from scapy.fields import ( + FieldLenField, + LEIntEnumField, + PacketField, + StrLenField, +) +from scapy.packet import Packet + +# Type hints +from typing import ( + Any, + List, + Optional, + Tuple, +) + +# https://datatracker.ietf.org/doc/html/rfc1508#page-48 + + +class ASN1_Class_GSSAPI(ASN1_Class_UNIVERSAL): + name = "GSSAPI" + APPLICATION = 0x60 + + +class ASN1_GSSAPI_APPLICATION(ASN1_SEQUENCE): + tag = ASN1_Class_GSSAPI.APPLICATION + + +class BERcodec_GSSAPI_APPLICATION(BERcodec_SEQUENCE): + tag = ASN1_Class_GSSAPI.APPLICATION + + +class ASN1F_GSSAPI_APPLICATION(ASN1F_SEQUENCE): + ASN1_tag = ASN1_Class_GSSAPI.APPLICATION + + +# GSS API Blob +# https://datatracker.ietf.org/doc/html/rfc4121 + +# Filled by providers +_GSSAPI_OIDS = {} +_GSSAPI_SIGNATURE_OIDS = {} + +# section 4.1 + + +class GSSAPI_BLOB(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_GSSAPI_APPLICATION( + ASN1F_OID("MechType", "1.3.6.1.5.5.2"), + ASN1F_PACKET( + "innerToken", + None, + None, + next_cls_cb=lambda pkt: _GSSAPI_OIDS.get(pkt.MechType.val, conf.raw_layer), + ), + ) + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 1: + if _pkt[0] & 0xA0 >= 0xA0: + from scapy.layers.spnego import SPNEGO_negToken + + # XXX: sometimes the token is raw, we should look from + # the session what to use here. For now: hardcode SPNEGO + # (THIS IS A VERY STRONG ASSUMPTION) + return SPNEGO_negToken + elif _pkt[:7] == b"NTLMSSP": + from scapy.layers.ntlm import NTLM_Header + + # XXX: if no mechTypes are provided during SPNEGO exchange, + # Windows falls back to a plain NTLM_Header. + return NTLM_Header.dispatch_hook(_pkt=_pkt, *args, **kargs) + elif BER_id_dec(_pkt)[0] & 0x7F > 0x60: + from scapy.layers.kerberos import Kerberos + + # XXX: Heuristic to detect raw Kerberos packets, when Windows + # fallsback or when the parent data hasn't got any mechtype specified. + return Kerberos + return cls + + +# Same but to store the signatures (e.g. DCE/RPC) + + +class GSSAPI_BLOB_SIGNATURE(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_GSSAPI_APPLICATION( + ASN1F_OID("MechType", "1.3.6.1.5.5.2"), + ASN1F_PACKET( + "innerToken", + None, + None, + next_cls_cb=lambda pkt: _GSSAPI_SIGNATURE_OIDS.get( + pkt.MechType.val, conf.raw_layer + ), # noqa: E501 + ), + ) + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 2: + # Sometimes the token is raw. Detect that with educated + # heuristics. + if _pkt[:2] in [b"\x04\x04", b"\x05\x04"]: + from scapy.layers.kerberos import KRB_InnerToken + + return KRB_InnerToken + elif len(_pkt) >= 4 and _pkt[:4] == b"\x01\x00\x00\x00": + from scapy.layers.ntlm import NTLMSSP_MESSAGE_SIGNATURE + + return NTLMSSP_MESSAGE_SIGNATURE + return cls + + +class _GSSAPI_Field(PacketField): + """ + PacketField that contains a GSSAPI_BLOB_SIGNATURE, but one that can + have a payload when not encrypted. + """ + + __slots__ = ["pay_cls"] + + def __init__(self, name, pay_cls): + self.pay_cls = pay_cls + super().__init__( + name, + None, + GSSAPI_BLOB_SIGNATURE, + ) + + def getfield(self, pkt, s): + remain, val = super().getfield(pkt, s) + if remain and val: + val.payload = self.pay_cls(remain) + return b"", val + return remain, val + + +# RFC2744 Annex A, Null values + +GSS_C_QOP_DEFAULT = 0 +GSS_C_NO_CHANNEL_BINDINGS = b"\x00" + + +# RFC2744 sect 3.9 - Status Values + +GSS_S_COMPLETE = 0 + +# These errors are encoded into the 32-bit GSS status code as follows: +# MSB LSB +# |------------------------------------------------------------| +# | Calling Error | Routine Error | Supplementary Info | +# |------------------------------------------------------------| +# Bit 31 24 23 16 15 0 + +GSS_C_CALLING_ERROR_OFFSET = 24 +GSS_C_ROUTINE_ERROR_OFFSET = 16 +GSS_C_SUPPLEMENTARY_OFFSET = 0 + +# Calling errors: + +GSS_S_CALL_INACCESSIBLE_READ = 1 << GSS_C_CALLING_ERROR_OFFSET +GSS_S_CALL_INACCESSIBLE_WRITE = 2 << GSS_C_CALLING_ERROR_OFFSET +GSS_S_CALL_BAD_STRUCTURE = 3 << GSS_C_CALLING_ERROR_OFFSET + +# Routine errors: + +GSS_S_BAD_MECH = 1 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_BAD_NAME = 2 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_BAD_NAMETYPE = 3 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_BAD_BINDINGS = 4 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_BAD_STATUS = 5 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_BAD_SIG = 6 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_BAD_MIC = GSS_S_BAD_SIG +GSS_S_NO_CRED = 7 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_NO_CONTEXT = 8 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_DEFECTIVE_TOKEN = 9 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_DEFECTIVE_CREDENTIAL = 10 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_CREDENTIALS_EXPIRED = 11 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_CONTEXT_EXPIRED = 12 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_FAILURE = 13 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_BAD_QOP = 14 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_UNAUTHORIZED = 15 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_UNAVAILABLE = 16 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_DUPLICATE_ELEMENT = 17 << GSS_C_ROUTINE_ERROR_OFFSET +GSS_S_NAME_NOT_MN = 18 << GSS_C_ROUTINE_ERROR_OFFSET + +# Supplementary info bits: + +GSS_S_CONTINUE_NEEDED = 1 << (GSS_C_SUPPLEMENTARY_OFFSET + 0) +GSS_S_DUPLICATE_TOKEN = 1 << (GSS_C_SUPPLEMENTARY_OFFSET + 1) +GSS_S_OLD_TOKEN = 1 << (GSS_C_SUPPLEMENTARY_OFFSET + 2) +GSS_S_UNSEQ_TOKEN = 1 << (GSS_C_SUPPLEMENTARY_OFFSET + 3) +GSS_S_GAP_TOKEN = 1 << (GSS_C_SUPPLEMENTARY_OFFSET + 4) + +# Address families (RFC2744 sect 3.11) + +_GSS_ADDRTYPE = { + 0: "GSS_C_AF_UNSPEC", + 1: "GSS_C_AF_LOCAL", + 2: "GSS_C_AF_INET", + 3: "GSS_C_AF_IMPLINK", + 4: "GSS_C_AF_PUP", + 5: "GSS_C_AF_CHAOS", + 6: "GSS_C_AF_NS", + 7: "GSS_C_AF_NBS", + 8: "GSS_C_AF_ECMA", + 9: "GSS_C_AF_DATAKIT", + 10: "GSS_C_AF_CCITT", + 11: "GSS_C_AF_SNA", + 12: "GSS_C_AF_DECnet", + 13: "GSS_C_AF_DLI", + 14: "GSS_C_AF_LAT", + 15: "GSS_C_AF_HYLINK", + 16: "GSS_C_AF_APPLETALK", + 17: "GSS_C_AF_BSC", + 18: "GSS_C_AF_DSS", + 19: "GSS_C_AF_OSI", + 21: "GSS_C_AF_X25", + 255: "GSS_C_AF_NULLADDR", +} + + +# GSS Structures + + +class ChannelBindingType(Enum): + """ + Channel Binding Application Data types, per: + RFC 5929 / RFC 9266 + """ + + TLS_UNIQUE = "unique" + TLS_SERVER_END_POINT = "tls-server-end-point" + TLS_UNIQUE_FOR_TELNET = "tls-unique-for-telnet" + TLS_EXPORTER = "tls-exporter" # RFC9266 + + +class GssBufferDesc(Packet): + name = "gss_buffer_desc" + fields_desc = [ + FieldLenField("length", None, length_of="value", fmt=" "GssChannelBindings": + """ + Build a GssChannelBindings struct from a socket + + :param token_type: the type from ChannelBindingType, per RFC5929 + :param sslsock: take the certificate from the the socket.socket object + :param certfile: take the certificate from a file + """ + from scapy.layers.tls.cert import Cert + from cryptography.hazmat.primitives import hashes + + if token_type == ChannelBindingType.TLS_SERVER_END_POINT: + # RFC5929 sect 4 + try: + # Parse certificate + if certfile is not None: + cert = Cert(certfile) + else: + cert = Cert(sslsock.getpeercert(binary_form=True)) + except Exception: + # We failed to parse the certificate. + log_runtime.warning("Failed to parse the SSL Certificate. CBT not used") + return GSS_C_NO_CHANNEL_BINDINGS + try: + h = cert.getCertSignatureHash() + except Exception: + # We failed to get the signature algorithm. + log_runtime.warning( + "Failed to get the Certificate signature algorithm. CBT not used" + ) + return GSS_C_NO_CHANNEL_BINDINGS + # RFC5929 sect 4.1 + if h == hashes.MD5 or h == hashes.SHA1: + h = hashes.SHA256 + # Get bytes of first certificate if there are multiple + c = cert.x509Cert.copy() + c.remove_payload() + cdata = bytes(c) + # Calc hash of certificate + digest = hashes.Hash(h) + digest.update(cdata) + cbdata = digest.finalize() + elif token_type == ChannelBindingType.TLS_UNIQUE: + # RFC5929 sect 3 + cbdata = sslsock.get_channel_binding(cb_type="tls-unique") + else: + raise NotImplementedError + # RFC5056 sect 2.1 + # "channel bindings MUST start with the channel binding unique prefix followed + # by a colon (ASCII 0x3A)." + return GssChannelBindings( + application_data=GssBufferDesc( + value=token_type.value.encode() + b":" + cbdata + ) + ) + + +# --- The base GSSAPI SSP base class + + +class GSS_C_FLAGS(IntFlag): + """ + Authenticator Flags per RFC2744 req_flags + """ + + GSS_C_DELEG_FLAG = 0x01 + GSS_C_MUTUAL_FLAG = 0x02 + GSS_C_REPLAY_FLAG = 0x04 + GSS_C_SEQUENCE_FLAG = 0x08 + GSS_C_CONF_FLAG = 0x10 # confidentiality + GSS_C_INTEG_FLAG = 0x20 # integrity + # RFC4757 + GSS_C_DCE_STYLE = 0x1000 + GSS_C_IDENTIFY_FLAG = 0x2000 + GSS_C_EXTENDED_ERROR_FLAG = 0x4000 + + +class GSS_S_FLAGS(IntFlag): + """ + Equivalent to Microsoft's ASC_REQ* Flags in AcceptSecurityContext + """ + + GSS_S_ALLOW_MISSING_BINDINGS = 0x10000000 + + +class GSS_QOP_REQ_FLAGS(IntFlag): + """ + Used for qop_flags + """ + + # Windows' API requires requesters to add an extra buffer of type + # 'SECBUFFER_PADDING' to receive the padding. The GSS_WrapEx API + # does not provide such a mechanism and always uses it. However + # some implementations like LDAP actually require NO padding, which + # therefore can't be achieved with GSS_WrapEx. + GSS_S_NO_SECBUFFER_PADDING = 0x10000000 + + +class SSP: + """ + The general SSP class + """ + + auth_type = 0x00 + + def __init__(self, **kwargs): + if kwargs: + raise ValueError("Unknown SSP parameters: " + ",".join(list(kwargs))) + + def __repr__(self): + return "<%s>" % self.__class__.__name__ + + class CONTEXT: + """ + A Security context i.e. the 'state' of the secure negotiation + """ + + __slots__ = ["state", "_flags", "passive"] + + def __init__(self, req_flags: Optional["GSS_C_FLAGS | GSS_S_FLAGS"] = None): + if req_flags is None: + # Default + req_flags = ( + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG + | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG + ) + self.flags = req_flags + self.passive = False + + def clifailure(self): + # This allows to reset the client context without discarding it. + pass + + # 'flags' is the most important attribute. Use a setter to sanitize it. + + @property + def flags(self): + return self._flags + + @flags.setter + def flags(self, x): + self._flags = GSS_C_FLAGS(int(x)) + + def __repr__(self): + return "[Default SSP]" + + class STATE(IntEnum): + """ + An Enum that contains the states of an SSP + """ + + @abc.abstractmethod + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + """ + GSS_Init_sec_context: client-side call for the SSP + """ + raise NotImplementedError + + @abc.abstractmethod + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + """ + GSS_Accept_sec_context: server-side call for the SSP + """ + raise NotImplementedError + + @abc.abstractmethod + def GSS_Inquire_names_for_mech(self) -> List[str]: + """ + Get the available OIDs for this mech, in order of preference. + """ + raise NotImplementedError + + # Passive + + @abc.abstractmethod + def GSS_Passive( + self, + Context: CONTEXT, + input_token=None, + ): + """ + GSS_Passive: client/server call for the SSP in passive mode + """ + raise NotImplementedError + + def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): + """ + GSS_Passive_set_Direction: used to swap the direction in passive mode + """ + pass + + # MS additions (*Ex functions) + + @dataclass + class WRAP_MSG: + conf_req_flag: bool + sign: bool + data: bytes + + @abc.abstractmethod + def GSS_WrapEx( + self, + Context: CONTEXT, + msgs: List[WRAP_MSG], + qop_req: int = GSS_C_QOP_DEFAULT, + ) -> Tuple[List[WRAP_MSG], Any]: + """ + GSS_WrapEx + + :param Context: the SSP context + :param qop_req: int (0 specifies default QOP) + :param msgs: list of WRAP_MSG + + :returns: (data, signature) + """ + raise NotImplementedError + + @abc.abstractmethod + def GSS_UnwrapEx( + self, Context: CONTEXT, msgs: List[WRAP_MSG], signature + ) -> List[WRAP_MSG]: + """ + :param Context: the SSP context + :param msgs: list of WRAP_MSG + :param signature: the signature + + :raises ValueError: if MIC failure. + :returns: data + """ + raise NotImplementedError + + @dataclass + class MIC_MSG: + sign: bool + data: bytes + + @abc.abstractmethod + def GSS_GetMICEx( + self, + Context: CONTEXT, + msgs: List[MIC_MSG], + qop_req: int = GSS_C_QOP_DEFAULT, + ) -> Any: + """ + GSS_GetMICEx + + :param Context: the SSP context + :param qop_req: int (0 specifies default QOP) + :param msgs: list of VERIF_MSG + + :returns: signature + """ + raise NotImplementedError + + @abc.abstractmethod + def GSS_VerifyMICEx( + self, + Context: CONTEXT, + msgs: List[MIC_MSG], + signature, + ) -> None: + """ + :param Context: the SSP context + :param msgs: list of VERIF_MSG + :param signature: the signature + + :raises ValueError: if MIC failure. + """ + raise NotImplementedError + + @abc.abstractmethod + def MaximumSignatureLength(self, Context: CONTEXT): + """ + Returns the Maximum Signature length. + + This will be used in auth_len in DceRpc5, and is necessary for + PFC_SUPPORT_HEADER_SIGN to work properly. + """ + raise NotImplementedError + + # RFC 2743 + + # sect 2.3.1 + + def GSS_GetMIC( + self, + Context: CONTEXT, + message: bytes, + qop_req: int = GSS_C_QOP_DEFAULT, + ): + """ + See GSS_GetMICEx + """ + return self.GSS_GetMICEx( + Context, + [ + self.MIC_MSG( + sign=True, + data=message, + ) + ], + qop_req=qop_req, + ) + + # sect 2.3.2 + + def GSS_VerifyMIC( + self, + Context: CONTEXT, + message: bytes, + signature, + ) -> None: + """ + See GSS_VerifyMICEx + """ + self.GSS_VerifyMICEx( + Context, + [ + self.MIC_MSG( + sign=True, + data=message, + ) + ], + signature, + ) + + # sect 2.3.3 + + def GSS_Wrap( + self, + Context: CONTEXT, + input_message: bytes, + conf_req_flag: bool, + qop_req: int = GSS_C_QOP_DEFAULT, + ): + """ + See GSS_WrapEx + """ + _msgs, signature = self.GSS_WrapEx( + Context, + [ + self.WRAP_MSG( + conf_req_flag=conf_req_flag, + sign=True, + data=input_message, + ) + ], + qop_req=qop_req, + ) + if _msgs[0].data: + signature /= _msgs[0].data + return signature + + # sect 2.3.4 + + def GSS_Unwrap( + self, + Context: CONTEXT, + signature, + ): + """ + See GSS_UnwrapEx + """ + data = b"" + if signature.payload: + # signature has a payload that is the data. Let's get that payload + # in its original form, and use it for verifying the checksum. + if signature.payload.original: + data = signature.payload.original + else: + data = bytes(signature.payload) + signature = signature.copy() + signature.remove_payload() + return self.GSS_UnwrapEx( + Context, + [ + self.WRAP_MSG( + conf_req_flag=True, + sign=True, + data=data, + ) + ], + signature, + )[0].data + + # MISC + + def NegTokenInit2(self): + """ + Server-Initiation + See [MS-SPNG] sect 3.2.5.2 + """ + return None, None + + def SupportsMechListMIC(self): + """ + Returns whether mechListMIC is supported or not + """ + return True + + def GetMechListMIC(self, Context, input): + """ + Compute mechListMIC + """ + return self.GSS_GetMIC(Context, input) + + def VerifyMechListMIC(self, Context, otherMIC, input): + """ + Verify mechListMIC + """ + return self.GSS_VerifyMIC(Context, input, otherMIC) + + def LegsAmount(self, Context: CONTEXT): + """ + Returns the amount of 'legs' (how MS calls it) of the SSP. + + i.e. 2 for Kerberos, 3 for NTLM and Netlogon + """ + return 2 diff --git a/scapy/layers/hsrp.py b/scapy/layers/hsrp.py index 791a1cfeff8..82e82606357 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -1,42 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license - -############################################################################# -# # -# hsrp.py --- HSRP protocol support for Scapy # -# # -# Copyright (C) 2010 Mathieu RENARD mathieu.renard(at)gmail.com # -# # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License version 2 as # -# published by the Free Software Foundation; version 2. # -# # -# This program is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -# # -############################################################################# -# HSRP Version 1 -# Ref. RFC 2281 -# HSRP Version 2 -# Ref. http://www.smartnetworks.jp/2006/02/hsrp_8_hsrp_version_2.html -## -# $Log: hsrp.py,v $ -# Revision 0.2 2011/05/01 15:23:34 mrenard -# Cleanup code +# See https://scapy.net/ for more information +# Copyright (C) Mathieu RENARD """ -HSRP (Hot Standby Router Protocol): proprietary redundancy protocol for Cisco routers. # noqa: E501 +HSRP (Hot Standby Router Protocol) +A proprietary redundancy protocol for Cisco routers. + +- HSRP Version 1: RFC 2281 +- HSRP Version 2: + http://www.smartnetworks.jp/2006/02/hsrp_8_hsrp_version_2.html """ +from scapy.config import conf from scapy.fields import ByteEnumField, ByteField, IPField, SourceIPField, \ StrFixedLenField, XIntField, XShortField from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.layers.inet import DestIPField, UDP -from scapy.layers.inet6 import DestIP6Field class HSRP(Packet): @@ -68,7 +48,7 @@ class HSRPmd5(Packet): ByteEnumField("algo", 0, {1: "MD5"}), ByteField("padding", 0x00), XShortField("flags", 0x00), - SourceIPField("sourceip", None), + SourceIPField("sourceip"), XIntField("keyid", 0x00), StrFixedLenField("authdigest", b"\00" * 16, 16)] @@ -86,4 +66,6 @@ def post_build(self, p, pay): bind_layers(UDP, HSRP, dport=1985, sport=1985) bind_layers(UDP, HSRP, dport=2029, sport=2029) DestIPField.bind_addr(UDP, "224.0.0.2", dport=1985) -DestIP6Field.bind_addr(UDP, "ff02::66", dport=2029) +if conf.ipv6_enabled: + from scapy.layers.inet6 import DestIP6Field + DestIP6Field.bind_addr(UDP, "ff02::66", dport=2029) diff --git a/scapy/layers/http.py b/scapy/layers/http.py index 54b3c139340..88930d569c4 100644 --- a/scapy/layers/http.py +++ b/scapy/layers/http.py @@ -1,69 +1,111 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) 2019 Gabriel Potter +# See https://scapy.net/ for more information # Copyright (C) 2012 Luca Invernizzi # Copyright (C) 2012 Steeve Barbeau - -# This program is published under a GPLv2 license +# Copyright (C) 2019 Gabriel Potter """ HTTP 1.0 layer. -Load using: +Load using:: + + from scapy.layers.http import * + +Or (console only):: >>> load_layer("http") Note that this layer ISN'T loaded by default, as quite experimental for now. To follow HTTP packets streams = group packets together to get the -whole request/answer, use `TCPSession` as: +whole request/answer, use ``TCPSession`` as:: >>> sniff(session=TCPSession) # Live on-the-flow session >>> sniff(offline="./http_chunk.pcap", session=TCPSession) # pcap -This will decode HTTP packets using `Content_Length` or chunks, +This will decode HTTP packets using ``Content_Length`` or chunks, and will also decompress the packets when needed. Note: on failure, decompression will be ignored. -You can turn auto-decompression/auto-compression off with: +You can turn auto-decompression/auto-compression off with:: + + >>> conf.contribs["http"]["auto_compression"] = False + +(Defaults to True) - >>> conf.contribs["http"]["auto_compression"] = True +You can also turn auto-chunking/dechunking off with:: + + >>> conf.contribs["http"]["auto_chunk"] = False + +(Defaults to True) """ -# This file is a modified version of the former scapy_http plugin. +# This file is a rewritten version of the former scapy_http plugin. # It was reimplemented for scapy 2.4.3+ using sessions, stream handling. # Original Authors : Steeve Barbeau, Luca Invernizzi -# Originally published under a GPLv2 license +import base64 +import datetime +import gzip +import io import os import re +import socket +import ssl import struct import subprocess -from scapy.compat import plain_str, bytes_encode, \ - gzip_compress, gzip_decompress +from enum import Enum + +from scapy.compat import plain_str, bytes_encode + +from scapy.automaton import Automaton, ATMT from scapy.config import conf from scapy.consts import WINDOWS -from scapy.error import warning, log_loading +from scapy.error import warning, log_loading, log_interactive, Scapy_Exception from scapy.fields import StrField from scapy.packet import Packet, bind_layers, bind_bottom_up, Raw +from scapy.supersocket import StreamSocket, SSLStreamSocket from scapy.utils import get_temp_file, ContextManagerSubprocess -from scapy.layers.inet import TCP, TCP_client - -from scapy.modules import six +from scapy.layers.gssapi import ( + ChannelBindingType, + GSSAPI_BLOB, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_FAILURE, + GSS_S_FLAGS, + GssChannelBindings, +) +from scapy.layers.inet import TCP try: import brotli - is_brotli_available = True + + _is_brotli_available = True +except ImportError: + _is_brotli_available = False + +try: + import lzw + + _is_lzw_available = True +except ImportError: + _is_lzw_available = False + +try: + import zstandard + + _is_zstd_available = True except ImportError: - is_brotli_available = False - log_loading.info("Can't import brotli. Won't be able to decompress " - "data streams compressed with brotli.") + _is_zstd_available = False if "http" not in conf.contribs: conf.contribs["http"] = {} conf.contribs["http"]["auto_compression"] = True + conf.contribs["http"]["auto_chunk"] = True # https://en.wikipedia.org/wiki/List_of_HTTP_header_fields @@ -79,13 +121,10 @@ "Pragma", "Upgrade", "Via", - "Warning" + "Warning", ] -COMMON_UNSTANDARD_GENERAL_HEADERS = [ - "X-Request-ID", - "X-Correlation-ID" -] +COMMON_UNSTANDARD_GENERAL_HEADERS = ["X-Request-ID", "X-Correlation-ID"] REQUEST_HEADERS = [ "A-IM", @@ -114,11 +153,10 @@ "Range", "Referer", "TE", - "User-Agent" + "User-Agent", ] COMMON_UNSTANDARD_REQUEST_HEADERS = [ - "Upgrade-Insecure-Requests", "Upgrade-Insecure-Requests", "X-Requested-With", "DNT", @@ -159,7 +197,6 @@ "Last-Modified", "Link", "Location", - "Permanent", "P3P", "Proxy-Authenticate", "Public-Key-Pins", @@ -210,7 +247,7 @@ def _parse_headers(s): headers_found = {} for header_line in headers: try: - key, value = header_line.split(b':', 1) + key, value = header_line.split(b":", 1) except ValueError: continue header_key = _strip_header_name(key).lower() @@ -219,19 +256,19 @@ def _parse_headers(s): def _parse_headers_and_body(s): - ''' Takes a HTTP packet, and returns a tuple containing: - _ the first line (e.g., "GET ...") - _ the headers in a dictionary - _ the body - ''' + """Takes a HTTP packet, and returns a tuple containing: + _ the first line (e.g., "GET ...") + _ the headers in a dictionary + _ the body + """ crlfcrlf = b"\r\n\r\n" crlfcrlfIndex = s.find(crlfcrlf) if crlfcrlfIndex != -1: - headers = s[:crlfcrlfIndex + len(crlfcrlf)] - body = s[crlfcrlfIndex + len(crlfcrlf):] + headers = s[: crlfcrlfIndex + len(crlfcrlf)] + body = s[crlfcrlfIndex + len(crlfcrlf) :] else: headers = s - body = b'' + body = b"" first_line, headers = headers.split(b"\r\n", 1) return first_line.strip(), _parse_headers(headers), body @@ -251,34 +288,38 @@ def _dissect_headers(obj, s): continue obj.setfieldval(f.name, value) if headers: - headers = {key: value for key, value in six.itervalues(headers)} - obj.setfieldval('Unknown_Headers', headers) + headers = dict(headers.values()) + obj.setfieldval("Unknown_Headers", headers) return first_line, body class _HTTPContent(Packet): + __slots__ = ["_original_len"] + # https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Transfer-Encoding def _get_encodings(self): encodings = [] if isinstance(self, HTTPResponse): if self.Transfer_Encoding: - encodings += [plain_str(x).strip().lower() for x in - plain_str(self.Transfer_Encoding).split(",")] + encodings += [ + plain_str(x).strip().lower() + for x in plain_str(self.Transfer_Encoding).split(",") + ] if self.Content_Encoding: - encodings += [plain_str(x).strip().lower() for x in - plain_str(self.Content_Encoding).split(",")] + encodings += [ + plain_str(x).strip().lower() + for x in plain_str(self.Content_Encoding).split(",") + ] return encodings def hashret(self): - # The only field both Answers and Responses have in common - return self.Http_Version + return b"HTTP1" def post_dissect(self, s): - if not conf.contribs["http"]["auto_compression"]: - return s + self._original_len = len(s) encodings = self._get_encodings() # Un-chunkify - if "chunked" in encodings: + if conf.contribs["http"]["auto_chunk"] and "chunked" in encodings: data = b"" while s: length, _, body = s.partition(b"\r\n") @@ -289,109 +330,156 @@ def post_dissect(self, s): break else: load = body[:length] - if body[length:length + 2] != b"\r\n": + if body[length : length + 2] != b"\r\n": # Invalid chunk. Ignore break - s = body[length + 2:] + s = body[length + 2 :] data += load if not s: s = data + if not conf.contribs["http"]["auto_compression"]: + return s # Decompress try: if "deflate" in encodings: import zlib + s = zlib.decompress(s) elif "gzip" in encodings: - s = gzip_decompress(s) + s = gzip.decompress(s) elif "compress" in encodings: - import lzw - s = lzw.decompress(s) - elif "br" in encodings and is_brotli_available: - s = brotli.decompress(s) + if _is_lzw_available: + s = lzw.decompress(s) + else: + log_loading.info( + "Can't import lzw. compress decompression " "will be ignored !" + ) + elif "br" in encodings: + if _is_brotli_available: + s = brotli.decompress(s) + else: + log_loading.info( + "Can't import brotli. brotli decompression " "will be ignored !" + ) + elif "zstd" in encodings: + if _is_zstd_available: + # Using its streaming API since its simple API could handle + # only cases where there is content size data embedded in + # the frame + bio = io.BytesIO(s) + reader = zstandard.ZstdDecompressor().stream_reader(bio) + s = reader.read() + else: + log_loading.info( + "Can't import zstandard. zstd decompression " + "will be ignored !" + ) except Exception: # Cannot decompress - probably incomplete data pass return s def post_build(self, pkt, pay): - if not conf.contribs["http"]["auto_compression"]: - return pkt + pay encodings = self._get_encodings() - # Compress - if "deflate" in encodings: - import zlib - pay = zlib.compress(pay) - elif "gzip" in encodings: - pay = gzip_compress(pay) - elif "compress" in encodings: - import lzw - pay = lzw.compress(pay) - elif "br" in encodings and is_brotli_available: - pay = brotli.compress(pay) + if conf.contribs["http"]["auto_compression"]: + # Compress + if "deflate" in encodings: + import zlib + + pay = zlib.compress(pay) + elif "gzip" in encodings: + pay = gzip.compress(pay) + elif "compress" in encodings: + if _is_lzw_available: + pay = lzw.compress(pay) + else: + log_loading.info( + "Can't import lzw. compress compression " "will be ignored !" + ) + elif "br" in encodings: + if _is_brotli_available: + pay = brotli.compress(pay) + else: + log_loading.info( + "Can't import brotli. brotli compression will " "be ignored !" + ) + elif "zstd" in encodings: + if _is_zstd_available: + pay = zstandard.ZstdCompressor().compress(pay) + else: + log_loading.info( + "Can't import zstandard. zstd compression will " "be ignored !" + ) + # Chunkify + if conf.contribs["http"]["auto_chunk"] and "chunked" in encodings: + # Dumb: 1 single chunk. + pay = (b"%X" % len(pay)) + b"\r\n" + pay + b"\r\n0\r\n\r\n" return pkt + pay - def self_build(self, field_pos_list=None): - ''' Takes an HTTPRequest or HTTPResponse object, and creates its - string representation.''' + def self_build(self, **kwargs): + """Takes an HTTPRequest or HTTPResponse object, and creates its + string representation.""" if not isinstance(self.underlayer, HTTP): - warning( - "An HTTPResponse/HTTPRequest should always be below an HTTP" - ) + warning("An HTTPResponse/HTTPRequest should always be below an HTTP") # Check for cache if self.raw_packet_cache is not None: return self.raw_packet_cache p = b"" + encodings = self._get_encodings() # Walk all the fields, in order - for f in self.fields_desc: + for i, f in enumerate(self.fields_desc): if f.name == "Unknown_Headers": continue # Get the field value val = self.getfieldval(f.name) if not val: - # Not specified. Skip - continue - if f.name not in ['Method', 'Path', 'Reason_Phrase', - 'Http_Version', 'Status_Code']: + if f.name == "Content_Length" and "chunked" not in encodings: + # Add Content-Length anyways + val = str(len(self.payload or b"")) + elif f.name == "Date" and isinstance(self, HTTPResponse): + val = datetime.datetime.now(datetime.timezone.utc).strftime( + "%a, %d %b %Y %H:%M:%S GMT" + ) + else: + # Not specified. Skip + continue + + if i >= 3: val = _header_line(f.real_name, val) # Fields used in the first line have a space as a separator, # whereas headers are terminated by a new line - if isinstance(self, HTTPRequest): - if f.name in ['Method', 'Path']: - separator = b' ' - else: - separator = b'\r\n' - elif isinstance(self, HTTPResponse): - if f.name in ['Http_Version', 'Status_Code']: - separator = b' ' - else: - separator = b'\r\n' + if i <= 1: + separator = b" " + else: + separator = b"\r\n" # Add the field into the packet p = f.addfield(self, p, val + separator) # Handle Unknown_Headers if self.Unknown_Headers: headers_text = b"" - for name, value in six.iteritems(self.Unknown_Headers): + for name, value in self.Unknown_Headers.items(): headers_text += _header_line(name, value) + b"\r\n" - p = self.get_field("Unknown_Headers").addfield( - self, p, headers_text - ) + p = self.get_field("Unknown_Headers").addfield(self, p, headers_text) # The packet might be empty, and in that case it should stay empty. if p: # Add an additional line after the last header - p = f.addfield(self, p, b'\r\n') + p = f.addfield(self, p, b"\r\n") return p def guess_payload_class(self, payload): - """Detect potential payloads - """ + """Detect potential payloads""" + if not hasattr(self, "Connection"): + return super(_HTTPContent, self).guess_payload_class(payload) if self.Connection and b"Upgrade" in self.Connection: from scapy.contrib.http2 import H2Frame + return H2Frame return super(_HTTPContent, self).guess_payload_class(payload) class _HTTPHeaderField(StrField): """Modified StrField to handle HTTP Header names""" + __slots__ = ["real_name"] def __init__(self, name, default): @@ -399,6 +487,11 @@ def __init__(self, name, default): name = _strip_header_name(name) StrField.__init__(self, name, default, fmt="H") + def i2repr(self, pkt, x): + if isinstance(x, bytes): + return x.decode(errors="backslashreplace") + return x + def _generate_headers(*args): """Generate the header fields based on their name""" @@ -412,94 +505,98 @@ def _generate_headers(*args): results.append(_HTTPHeaderField(h, None)) return results + # Create Request and Response packets class HTTPRequest(_HTTPContent): name = "HTTP Request" - fields_desc = [ - # First line - _HTTPHeaderField("Method", "GET"), - _HTTPHeaderField("Path", "/"), - _HTTPHeaderField("Http-Version", "HTTP/1.1"), - # Headers - ] + ( - _generate_headers( - GENERAL_HEADERS, - REQUEST_HEADERS, - COMMON_UNSTANDARD_GENERAL_HEADERS, - COMMON_UNSTANDARD_REQUEST_HEADERS + fields_desc = ( + [ + # First line + _HTTPHeaderField("Method", "GET"), + _HTTPHeaderField("Path", "/"), + _HTTPHeaderField("Http-Version", "HTTP/1.1"), + # Headers + ] + + ( + _generate_headers( + GENERAL_HEADERS, + REQUEST_HEADERS, + COMMON_UNSTANDARD_GENERAL_HEADERS, + COMMON_UNSTANDARD_REQUEST_HEADERS, + ) ) - ) + [ - _HTTPHeaderField("Unknown-Headers", None), - ] + + [ + _HTTPHeaderField("Unknown-Headers", None), + ] + ) def do_dissect(self, s): """From the HTTP packet string, populate the scapy object""" first_line, body = _dissect_headers(self, s) try: - Method, Path, HTTPVersion = re.split(br"\s+", first_line, 2) - self.setfieldval('Method', Method) - self.setfieldval('Path', Path) - self.setfieldval('Http_Version', HTTPVersion) + Method, Path, HTTPVersion = re.split(rb"\s+", first_line, maxsplit=2) + self.setfieldval("Method", Method) + self.setfieldval("Path", Path) + self.setfieldval("Http_Version", HTTPVersion) except ValueError: pass if body: - self.raw_packet_cache = s[:-len(body)] + self.raw_packet_cache = s[: -len(body)] else: self.raw_packet_cache = s return body def mysummary(self): - return self.sprintf( - "%HTTPRequest.Method% %HTTPRequest.Path% " - "%HTTPRequest.Http_Version%" - ) + return self.sprintf("%HTTPRequest.Method% '%HTTPRequest.Path%' ") class HTTPResponse(_HTTPContent): name = "HTTP Response" - fields_desc = [ - # First line - _HTTPHeaderField("Http-Version", "HTTP/1.1"), - _HTTPHeaderField("Status-Code", "200"), - _HTTPHeaderField("Reason-Phrase", "OK"), - # Headers - ] + ( - _generate_headers( - GENERAL_HEADERS, - RESPONSE_HEADERS, - COMMON_UNSTANDARD_GENERAL_HEADERS, - COMMON_UNSTANDARD_RESPONSE_HEADERS + fields_desc = ( + [ + # First line + _HTTPHeaderField("Http-Version", "HTTP/1.1"), + _HTTPHeaderField("Status-Code", "200"), + _HTTPHeaderField("Reason-Phrase", "OK"), + # Headers + ] + + ( + _generate_headers( + GENERAL_HEADERS, + RESPONSE_HEADERS, + COMMON_UNSTANDARD_GENERAL_HEADERS, + COMMON_UNSTANDARD_RESPONSE_HEADERS, + ) ) - ) + [ - _HTTPHeaderField("Unknown-Headers", None), - ] + + [ + _HTTPHeaderField("Unknown-Headers", None), + ] + ) def answers(self, other): return HTTPRequest in other def do_dissect(self, s): - ''' From the HTTP packet string, populate the scapy object ''' + """From the HTTP packet string, populate the scapy object""" first_line, body = _dissect_headers(self, s) try: - HTTPVersion, Status, Reason = re.split(br"\s+", first_line, 2) - self.setfieldval('Http_Version', HTTPVersion) - self.setfieldval('Status_Code', Status) - self.setfieldval('Reason_Phrase', Reason) + HTTPVersion, Status, Reason = re.split(rb"\s+", first_line, maxsplit=2) + self.setfieldval("Http_Version", HTTPVersion) + self.setfieldval("Status_Code", Status) + self.setfieldval("Reason_Phrase", Reason) except ValueError: pass if body: - self.raw_packet_cache = s[:-len(body)] + self.raw_packet_cache = s[: -len(body)] else: self.raw_packet_cache = s return body def mysummary(self): - return self.sprintf( - "%HTTPResponse.Http_Version% %HTTPResponse.Status_Code% " - "%HTTPResponse.Reason_Phrase%" - ) + return self.sprintf("%HTTPResponse.Status_Code% %HTTPResponse.Reason_Phrase%") + # General HTTP class + defragmentation @@ -508,11 +605,27 @@ class HTTP(Packet): name = "HTTP 1" fields_desc = [] show_indent = 0 + clsreq = HTTPRequest + clsresp = HTTPResponse + hdr = b"HTTP" + reqmethods = b"|".join( + [ + b"OPTIONS", + b"GET", + b"HEAD", + b"POST", + b"PUT", + b"DELETE", + b"TRACE", + b"CONNECT", + ] + ) @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): if _pkt and len(_pkt) >= 9: from scapy.contrib.http2 import _HTTP2_types, H2Frame + # To detect a valid HTTP2, we check that the type is correct # that the Reserved bit is set and length makes sense. while _pkt: @@ -536,35 +649,71 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): # tcp_reassemble is used by TCPSession in session.py @classmethod - def tcp_reassemble(cls, data, metadata): + def tcp_reassemble(cls, data, metadata, session): detect_end = metadata.get("detect_end", None) is_unknown = metadata.get("detect_unknown", True) + # General idea of the following is explained at + # https://datatracker.ietf.org/doc/html/rfc2616#section-4.4 if not detect_end or is_unknown: metadata["detect_unknown"] = False - http_packet = HTTP(data) + http_packet = cls(data) # Detect packing method if not isinstance(http_packet.payload, _HTTPContent): return http_packet + is_response = isinstance(http_packet.payload, cls.clsresp) + # Packets may have a Content-Length we must honnor length = http_packet.Content_Length + if length: + # Parse the length as an integer + try: + length = int(length) + except ValueError: + length = None if length is not None: # The packet provides a Content-Length attribute: let's # use it. When the total size of the frags is high enough, # we have the packet - length = int(length) + + if session.pop("head_request", False): + # Answer to a HEAD request. + detect_end = lambda dat: dat.find(b"\r\n\r\n") + # Subtract the length of the "HTTP*" layer - if http_packet.payload.payload or length == 0: - http_length = len(data) - len(http_packet.payload.payload) + elif http_packet.payload.payload or length == 0: + http_length = len(data) - http_packet.payload._original_len detect_end = lambda dat: len(dat) - http_length >= length else: # The HTTP layer isn't fully received. - detect_end = lambda dat: False - metadata["detect_unknown"] = True + if metadata.get("tcp_end", False): + # This was likely a HEAD response. Ugh + detect_end = lambda dat: True + else: + detect_end = lambda dat: False + metadata["detect_unknown"] = True else: # It's not Content-Length based. It could be chunked - encodings = http_packet[HTTP].payload._get_encodings() - chunked = ("chunked" in encodings) + encodings = http_packet[cls].payload._get_encodings() + chunked = "chunked" in encodings if chunked: + detect_end = lambda dat: dat.endswith(b"0\r\n\r\n") + # HTTP Requests that do not have any content, + # end with a double CRLF. + elif isinstance(http_packet.payload, cls.clsreq): detect_end = lambda dat: dat.endswith(b"\r\n\r\n") + # In case we are handling a HTTP Request, + # we want to continue assessing the data, + # to handle requests with a body (POST) + metadata["detect_unknown"] = True + if ( + isinstance(http_packet.payload, cls.clsreq) + and http_packet.Method == b"HEAD" + ): + session["head_request"] = True + elif is_response and http_packet.Status_Code == b"101": + # If it's an upgrade response, it may also hold a + # different protocol data. + # make sure all headers are present + detect_end = lambda dat: dat.find(b"\r\n\r\n") else: # If neither Content-Length nor chunked is specified, # it means it's the TCP packet that contains the data, @@ -576,7 +725,7 @@ def tcp_reassemble(cls, data, metadata): return http_packet else: if detect_end(data): - http_packet = HTTP(data) + http_packet = cls(data) return http_packet def guess_payload_class(self, payload): @@ -585,64 +734,279 @@ def guess_payload_class(self, payload): """ try: prog = re.compile( - br"^(?:OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) " - br"(?:.+?) " - br"HTTP/\d\.\d$" + rb"^(?:" + + self.reqmethods + + rb") " + + rb"(?:.+?) " + + self.hdr + + rb"/\d\.\d$" ) crlfIndex = payload.index(b"\r\n") req = payload[:crlfIndex] result = prog.match(req) if result: - return HTTPRequest + return self.clsreq else: - prog = re.compile(br"^HTTP/\d\.\d \d\d\d .*$") + prog = re.compile(b"^" + self.hdr + rb"/\d\.\d \d\d\d .*$") result = prog.match(req) if result: - return HTTPResponse + return self.clsresp except ValueError: # Anything that isn't HTTP but on port 80 pass return Raw -def http_request(host, path="/", port=80, timeout=3, - display=False, verbose=0, - iptables=False, **headers): - """Util to perform an HTTP request, using the TCP_client. +class HTTP_AUTH_MECHS(Enum): + NONE = "NONE" + BASIC = "Basic" + NTLM = "NTLM" + NEGOTIATE = "Negotiate" + + +class HTTP_Client(object): + """ + A basic HTTP client + + :param mech: one of HTTP_AUTH_MECHS + :param ssl: whether to use HTTPS or not + :param ssp: the SSP object to use for binding + :param no_check_certificate: with SSL, do not check the certificate + :param no_chan_bindings: force disable sending the channel bindings + """ + + def __init__( + self, + mech=HTTP_AUTH_MECHS.NONE, + verb=True, + sslcontext=None, + ssp=None, + no_check_certificate=False, + no_chan_bindings=False, + ): + self.sock = None + self._sockinfo = None + self.authmethod = mech + self.verb = verb + self.sslcontext = sslcontext + self.ssp = ssp + self.sspcontext = None + self.no_check_certificate = no_check_certificate + self.no_chan_bindings = no_chan_bindings + self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS + + def _connect_or_reuse(self, host, port=None, tls=False, timeout=5): + # Get the port + if port is None: + port = 443 if tls else 80 + # If the current socket matches, keep it. + if self._sockinfo == (host, port): + return + # A new socket is needed + if self._sockinfo: + self.close() + sock = socket.socket() + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + sock.settimeout(timeout) + if self.verb: + print( + "\u2503 Connecting to %s on port %s%s..." + % ( + host, + port, + " with SSL" if tls else "", + ) + ) + sock.connect((host, port)) + if self.verb: + print( + conf.color_theme.green( + "\u2514 Connected from %s" % repr(sock.getsockname()) + ) + ) + if tls: + if self.sslcontext is None: + if self.no_check_certificate: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + else: + context = ssl.create_default_context() + else: + context = self.sslcontext + sock = context.wrap_socket(sock, server_hostname=host) + if self.ssp and not self.no_chan_bindings: + # Compute the channel binding token (CBT) + self.chan_bindings = GssChannelBindings.fromssl( + ChannelBindingType.TLS_SERVER_END_POINT, + sslsock=sock, + ) + self.sock = SSLStreamSocket(sock, HTTP) + else: + self.sock = StreamSocket(sock, HTTP) + # Store information regarding the current socket + self._sockinfo = (host, port) + + def sr1(self, req, **kwargs): + if self.verb: + print(conf.color_theme.opening(">> %s" % req.summary())) + resp = self.sock.sr1( + HTTP() / req, + verbose=0, + **kwargs, + ) + if self.verb: + print(conf.color_theme.success("<< %s" % (resp and resp.summary()))) + return resp + + def request( + self, + url, + data=b"", + timeout=5, + follow_redirects=True, + http_headers={}, + **headers, + ): + """ + Perform a HTTP(s) request. + + :param url: the full URL to connect to. + e.g. https://google.com/test + :param data: the data to send as payload + :param follow_redirects: if True, request() will follow 302 return codes + :param http_headers: if specified, overwrites the HTTP headers + (except Host and Path). + :param headers: any additional HTTPRequest parameter to add. + e.g. Method="POST" + """ + # Parse request url + m = re.match(r"(https?)://([^/:]+)(?:\:(\d+))?(/.*)?", url) + if not m: + raise ValueError("Bad URL !") + transport, host, port, path = m.groups() + if transport == "https": + tls = True + else: + tls = False + + path = path or "/" + port = port and int(port) + + # Connect (or reuse) socket + self._connect_or_reuse(host, port=port, tls=tls, timeout=timeout) + + # Build request + if (tls and port != 443) or (not tls and port != 80): + host_hdr = "%s:%d" % (host, port) + else: + host_hdr = host + + headers.setdefault("Host", host_hdr) + headers.setdefault("Path", path) + + if not http_headers: + http_headers = { + "Accept_Encoding": b"gzip, deflate", + "Cache_Control": b"no-cache", + "Pragma": b"no-cache", + "Connection": b"keep-alive", + } + else: + http_headers = {k.replace("-", "_"): v for k, v in http_headers.items()} + http_headers.update(headers) + req = HTTP() / HTTPRequest(**http_headers) + if data: + req /= data + + while True: + # Perform the request. + try: + resp = self.sr1(req, timeout=timeout) + except Exception: + # Socket has died, restart. + self._sockinfo = None + self._connect_or_reuse(host, port=port, tls=tls, timeout=timeout) + continue + if not resp: + break + # First case: auth was required. Handle that + if resp.Status_Code in [b"401", b"407"]: + # Authentication required + if self.authmethod in [ + HTTP_AUTH_MECHS.NTLM, + HTTP_AUTH_MECHS.NEGOTIATE, + ]: + # Parse authenticate + if b" " in resp.WWW_Authenticate: + method, data = resp.WWW_Authenticate.split(b" ", 1) + try: + ssp_blob = GSSAPI_BLOB(base64.b64decode(data)) + except Exception: + raise Scapy_Exception("Invalid WWW-Authenticate") + else: + method = resp.WWW_Authenticate + ssp_blob = None + if plain_str(method) != self.authmethod.value: + raise Scapy_Exception("Invalid WWW-Authenticate") + # SPNEGO / Kerberos / NTLM + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + input_token=ssp_blob, + target_name="http/" + host, + req_flags=0, + chan_bindings=self.chan_bindings, + ) + if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + raise Scapy_Exception("Authentication failure") + req.Authorization = ( + self.authmethod.value.encode() + + b" " + + base64.b64encode(bytes(token)) + ) + continue + # Second case: follow redirection + if resp.Status_Code in [b"301", b"302"] and follow_redirects: + return self.request( + resp.Location.decode(), + data=data, + timeout=timeout, + follow_redirects=follow_redirects, + **headers, + ) + break + return resp + + def close(self): + if self.verb: + print("X Connection to server closed\n") + self.sock.close() + + +def http_request( + host, path="/", port=None, timeout=3, display=False, tls=False, verbose=0, **headers +): + """ + Util to perform an HTTP request. :param host: the host to connect to :param path: the path of the request (default /) - :param port: the port (default 80) + :param port: the port (default 80/443) :param timeout: timeout before None is returned - :param display: display the resullt in the default browser (default False) - :param iptables: temporarily prevents the kernel from - answering with a TCP RESET message. + :param display: display the result in the default browser (default False) + :param iface: interface to use. Changing this turns on "raw" :param headers: any additional headers passed to the request :returns: the HTTPResponse packet """ - http_headers = { - "Accept_Encoding": b'gzip, deflate', - "Cache_Control": b'no-cache', - "Pragma": b'no-cache', - "Connection": b'keep-alive', - "Host": host, - "Path": path, - } - http_headers.update(headers) - req = HTTP() / HTTPRequest(**http_headers) - tcp_client = TCP_client.tcplink(HTTP, host, port, debug=verbose) - ans = None - if iptables: - ip = tcp_client.atmt.dst - iptables_rule = "iptables -%c INPUT -s %s -p tcp --sport 80 -j DROP" - assert(os.system(iptables_rule % ('A', ip)) == 0) - try: - ans = tcp_client.sr1(req, timeout=timeout, verbose=verbose) - finally: - tcp_client.close() - if iptables: - assert(os.system(iptables_rule % ('D', ip)) == 0) + client = HTTP_Client(HTTP_AUTH_MECHS.NONE, verb=verbose) + if port is None: + port = 443 if tls else 80 + ans = client.request( + "http%s://%s:%s%s" % (tls and "s" or "", host, port, path), + timeout=timeout, + ) + if ans: if display: if Raw not in ans: @@ -670,3 +1034,320 @@ def http_request(host, path="/", port=80, timeout=3, bind_bottom_up(TCP, HTTP, sport=8080) bind_bottom_up(TCP, HTTP, dport=8080) + + +# Automatons + + +class HTTP_Server(Automaton): + """ + HTTP server automaton + + :param ssp: the SSP to serve. If None, unauthenticated (or basic). + :param mech: the HTTP_AUTH_MECHS to use (default: NONE) + :param require_cbt: require Channel Bindings to be valid (default: False) + :param cbt_cert: the path to the certificate used for channel bindings. + Useful if behind a reverse proxy. (default: None) + + Other parameters: + + :param BASIC_IDENTITIES: a dict that contains {"user": "password"} for Basic + authentication. + :param BASIC_REALM: the basic realm. + """ + + pkt_cls = HTTP + + def __init__( + self, + mech=HTTP_AUTH_MECHS.NONE, + verb=True, + ssp=None, + require_cbt: bool = False, + cbt_cert: str = None, + *args, + **kwargs, + ): + self.verb = verb + if "sock" not in kwargs: + raise ValueError( + "HTTP_Server cannot be started directly ! Use HTTP_Server.spawn" + ) + self.ssp = ssp + self.authmethod = mech.value + self.sspcontext = None + + # CBT settings + self.ssp_req_flags = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS + if require_cbt: + self.ssp_req_flags &= ~GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS + if cbt_cert: + self.chan_bindings = GssChannelBindings.fromssl( + ChannelBindingType.TLS_SERVER_END_POINT, + certfile=cbt_cert, + ) + else: + self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS + + # Auth settings + self.basic = False + self.BASIC_IDENTITIES = kwargs.pop("BASIC_IDENTITIES", {}) + self.BASIC_REALM = kwargs.pop("BASIC_REALM", "default") + if mech == HTTP_AUTH_MECHS.BASIC: + if not self.BASIC_IDENTITIES: + raise ValueError("Please provide 'BASIC_IDENTITIES' !") + if ssp is not None: + raise ValueError("Can't use 'BASIC_IDENTITIES' with 'ssp' !") + self.basic = True + elif mech == HTTP_AUTH_MECHS.NONE: + if ssp is not None: + raise ValueError("Cannot use ssp with mech=NONE !") + # Initialize + Automaton.__init__(self, *args, **kwargs) + + def send(self, resp): + self.sock.send(HTTP() / resp) + + def vprint(self, s=""): + """ + Verbose print (if enabled) + """ + if self.verb: + if conf.interactive: + log_interactive.info("> %s", s) + else: + print("> %s" % s) + + @ATMT.state(initial=1) + def BEGIN(self): + self.authenticated = False + self.sspcontext = None + + @ATMT.receive_condition(BEGIN, prio=1) + def should_authenticate(self, pkt): + if self.authmethod == HTTP_AUTH_MECHS.NONE.value: + raise self.SERVE(pkt) + else: + raise self.AUTH(pkt) + + @ATMT.state() + def AUTH(self, pkt=None): + if pkt is None: + return + if HTTPRequest in pkt: + self.vprint(pkt.summary()) + if pkt.Method == b"CONNECT": + # HTTP tunnel (proxy) + proxy = True + else: + # HTTP non-tunnel + proxy = False + # Get authorization + if proxy: + authorization = pkt.Proxy_Authorization + else: + authorization = pkt.Authorization + if not authorization: + # Initial ask. + data = self.authmethod + if self.basic: + data += " realm='%s'" % self.BASIC_REALM + self._ask_authorization(proxy, data) + return + # Parse authorization + method, data = authorization.split(b" ", 1) + if plain_str(method) != self.authmethod: + self.debug(3, "Bad auth method.") + raise self.AUTH_ERROR(proxy) + try: + data = base64.b64decode(data) + except Exception: + self.debug(3, "Couldn't unpack base64 of auth.") + raise self.AUTH_ERROR(proxy) + # Now process the authorization + if not self.basic: + try: + ssp_blob = GSSAPI_BLOB(data) + except Exception: + self.sspcontext = None + self._ask_authorization(proxy, self.authmethod) + self.debug(3, "Couldn't unpack GSSAPI_BLOB of auth.") + raise self.AUTH_ERROR(proxy) + # And call the SSP + self.sspcontext, tok, status = self.ssp.GSS_Accept_sec_context( + self.sspcontext, + ssp_blob, + req_flags=self.ssp_req_flags, + chan_bindings=self.chan_bindings, + ) + else: + # This is actually Basic authentication + try: + next( + True + for k, v in self.BASIC_IDENTITIES.items() + if ("%s:%s" % (k, v)).encode() == data + ) + tok, status = None, GSS_S_COMPLETE + except StopIteration: + self.debug(3, "Basic authentication failed with 'unknown user'.") + tok, status = None, GSS_S_FAILURE + # Send answer + if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + self.debug(3, "Authentication failed: %s." % status) + raise self.AUTH_ERROR(proxy) + elif status == GSS_S_CONTINUE_NEEDED: + data = self.authmethod.encode() + if tok: + data += b" " + base64.b64encode(bytes(tok)) + self._ask_authorization(proxy, data) + raise self.AUTH() + else: + # Authenticated ! + self.authenticated = True + self.vprint("AUTH OK") + raise self.SERVE(pkt) + + @ATMT.state() + def AUTH_ERROR(self, proxy): + self.sspcontext = None + self._ask_authorization(proxy, self.authmethod) + self.vprint("AUTH ERROR") + + @ATMT.condition(AUTH_ERROR) + def allow_reauth(self): + raise self.AUTH() + + def _ask_authorization(self, proxy, data): + if proxy: + self.send( + HTTPResponse( + Status_Code=b"407", + Reason_Phrase=b"Proxy Authentication Required", + Proxy_Authenticate=data, + ) + ) + else: + self.send( + HTTPResponse( + Status_Code=b"401", + Reason_Phrase=b"Unauthorized", + WWW_Authenticate=data, + ) + ) + + @ATMT.receive_condition(AUTH, prio=1) + def received_unauthenticated(self, pkt): + raise self.AUTH(pkt) + + @ATMT.eof(AUTH) + def auth_eof(self): + raise self.CLOSED() + + @ATMT.state(error=1) + def ERROR(self): + self.send( + HTTPResponse( + Status_Code="400", + Reason_Phrase="Bad Request", + ) + ) + + @ATMT.state(final=1) + def CLOSED(self): + self.vprint("CLOSED") + + # Serving + + @ATMT.state() + def SERVE(self, pkt=None): + if pkt is None: + return + answer = self.answer(pkt) + if answer: + self.send(answer) + self.vprint("%s -> %s" % (pkt.summary(), answer.summary())) + else: + self.vprint("%s" % pkt.summary()) + + @ATMT.eof(SERVE) + def serve_eof(self): + raise self.CLOSED() + + @ATMT.receive_condition(SERVE) + def new_request(self, pkt): + raise self.SERVE(pkt) + + # DEV: overwrite this function + + def answer(self, pkt): + """ + HTTP_server answer function. + + :param pkt: a HTTPRequest packet + :returns: a HTTPResponse packet + """ + if pkt.Path == b"/": + return HTTPResponse() / ( + "

OK

" + ) + else: + return HTTPResponse( + Status_Code=b"404", + Reason_Phrase=b"Not Found", + ) / ("

404 - Not Found

") + + +class HTTPS_Server(HTTP_Server): + """ + HTTPS server automaton + + This has the same arguments and attributes as HTTP_Server, with the addition of: + + :param sslcontext: an optional SSLContext object. + If used, key is ignored but cert can still be used for + channel bindings. + :param cert: path to the certificate + :param key: path to the key + :param require_cbt: require Channel Bindings to be valid + """ + + socketcls = None + + def __init__( + self, + mech=HTTP_AUTH_MECHS.NONE, + verb=True, + cert=None, + key=None, + sslcontext=None, + ssp=None, + require_cbt=False, + *args, + **kwargs, + ): + if "sock" not in kwargs: + raise ValueError( + "HTTPS_Server cannot be started directly ! Use HTTPS_Server.spawn" + ) + # wrap socket in SSL + if sslcontext is None: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(cert, key) + else: + context = sslcontext + kwargs["sock"] = SSLStreamSocket( + context.wrap_socket(kwargs["sock"], server_side=True), + self.pkt_cls, + ) + + # Call super + super(HTTPS_Server, self).__init__( + mech=mech, + verb=verb, + ssp=ssp, + cbt_cert=cert, + require_cbt=require_cbt, + *args, + **kwargs, + ) diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index 30d08517628..da98ab018c7 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -1,14 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ IPv4 (Internet Protocol v4). """ -from __future__ import absolute_import -from __future__ import print_function import time import struct import re @@ -19,33 +17,70 @@ from scapy.utils import checksum, do_graph, incremental_label, \ linehexdump, strxor, whois, colgen -from scapy.base_classes import Gen, Net -from scapy.data import ETH_P_IP, ETH_P_ALL, DLT_RAW, DLT_RAW_ALT, DLT_IPV4, \ - IP_PROTOS, TCP_SERVICES, UDP_SERVICES -from scapy.layers.l2 import Ether, Dot3, getmacbyip, CookedLinux, GRE, SNAP, \ - Loopback -from scapy.compat import raw, chb, orb, bytes_encode +from scapy.ansmachine import AnsweringMachine +from scapy.base_classes import Gen, Net, _ScopedIP +from scapy.consts import OPENBSD +from scapy.data import ( + ETH_P_IP, + ETH_P_ALL, + DLT_RAW, + DLT_RAW_ALT, + DLT_IPV4, + IP_PROTOS, + TCP_SERVICES, + UDP_SERVICES, +) +from scapy.layers.l2 import ( + CookedLinux, + Dot3, + Ether, + GRE, + Loopback, + SNAP, + arpcachepoison, + getmacbyip, +) +from scapy.compat import raw, chb, orb, bytes_encode, Optional from scapy.config import conf -from scapy.extlib import plt, MATPLOTLIB, MATPLOTLIB_INLINED, \ - MATPLOTLIB_DEFAULT_PLOT_KARGS -from scapy.fields import ConditionalField, IPField, BitField, BitEnumField, \ - FieldLenField, StrLenField, ByteField, ShortField, ByteEnumField, \ - DestField, FieldListField, FlagsField, IntField, MultiEnumField, \ - PacketListField, ShortEnumField, SourceIPField, StrField, \ - StrFixedLenField, XByteField, XShortField, Emph +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + DestField, + Emph, + FieldLenField, + FieldListField, + FlagsField, + IPField, + IP6Field, + IntField, + MayEnd, + MultiEnumField, + MultipleTypeField, + PacketField, + PacketListField, + ShortEnumField, + ShortField, + SourceIPField, + StrField, + StrFixedLenField, + StrLenField, + TrailerField, + XByteField, + XShortField, +) from scapy.packet import Packet, bind_layers, bind_bottom_up, NoPayload from scapy.volatile import RandShort, RandInt, RandBin, RandNum, VolatileValue from scapy.sendrecv import sr, sr1 -from scapy.plist import PacketList, SndRcvList +from scapy.plist import _PacketList, PacketList, SndRcvList from scapy.automaton import Automaton, ATMT -from scapy.error import warning +from scapy.error import log_runtime, warning from scapy.pton_ntop import inet_pton import scapy.as_resolvers -import scapy.modules.six as six -from scapy.modules.six.moves import range - #################### # IP Tools class # #################### @@ -216,6 +251,28 @@ class IPOption_Traceroute(IPOption): IPField("originator_ip", "0.0.0.0")] +class IPOption_Timestamp(IPOption): + name = "IP Option Timestamp" + optclass = 2 + option = 4 + fields_desc = [_IPOption_HDR, + ByteField("length", None), + ByteField("pointer", 9), + BitField("oflw", 0, 4), + BitEnumField("flg", 1, 4, + {0: "timestamp_only", + 1: "timestamp_and_ip_addr", + 3: "prespecified_ip_addr"}), + ConditionalField(IPField("internet_address", "0.0.0.0"), + lambda pkt: pkt.flg != 0), + IntField('timestamp', 0)] + + def post_build(self, p, pay): + if self.length is None: + p = p[:1] + struct.pack("!B", len(p)) + p[2:] + return p + pay + + class IPOption_Address_Extension(IPOption): name = "IP Option Address Extension" copy_flag = 1 @@ -257,8 +314,10 @@ class IPOption_SDBM(IPOption): 8: ("Timestamp", "!II"), 14: ("AltChkSum", "!BH"), 15: ("AltChkSumOpt", None), + 19: ("MD5", "16s"), 25: ("Mood", "!p"), 28: ("UTO", "!H"), + 29: ("AO", None), 34: ("TFO", "!II"), # RFC 3692 # 253: ("Experiment", "!HHHH"), @@ -273,12 +332,32 @@ class IPOption_SDBM(IPOption): "Timestamp": 8, "AltChkSum": 14, "AltChkSumOpt": 15, + "MD5": 19, "Mood": 25, "UTO": 28, + "AO": 29, "TFO": 34, }) +class TCPAOValue(Packet): + """Value of TCP-AO option""" + fields_desc = [ + ByteField("keyid", None), + ByteField("rnextkeyid", None), + StrLenField("mac", "", length_from=lambda p:len(p.original) - 2), + ] + + +def get_tcpao(tcphdr): + # type: (TCP) -> Optional[TCPAOValue] + """Get the TCP-AO option from the header""" + for optid, optval in tcphdr.options: + if optid == 'AO': + return optval + return None + + class RandTCPOptions(VolatileValue): def __init__(self, size=None): if size is None: @@ -290,7 +369,7 @@ def _fix(self): # Random ("NAME", fmt) rand_patterns = [ random.choice(list( - (opt, fmt) for opt, fmt in six.itervalues(TCPOptions[0]) + (opt, fmt) for opt, fmt in TCPOptions[0].values() if opt != 'EOL' )) for _ in range(self.size) @@ -301,7 +380,7 @@ def _fix(self): rand_vals.append((oname, b'')) else: # Process the fmt arguments 1 by 1 - structs = fmt[1:] if fmt[0] == "!" else fmt + structs = re.findall(r"!?([bBhHiIlLqQfdpP]|\d+[spx])", fmt) rval = [] for stru in structs: stru = "!" + stru @@ -324,7 +403,9 @@ class TCPOptionsField(StrField): def getfield(self, pkt, s): opsz = (pkt.dataofs - 5) * 4 if opsz < 0: - warning("bad dataofs (%i). Assuming dataofs=5" % pkt.dataofs) + log_runtime.info( + "bad dataofs (%i). Assuming dataofs=5", pkt.dataofs + ) opsz = 0 return s[opsz:], self.m2i(pkt, s[:opsz]) @@ -344,13 +425,17 @@ def m2i(self, pkt, x): except IndexError: olen = 0 if olen < 2: - warning("Malformed TCP option (announced length is %i)" % olen) + log_runtime.info( + "Malformed TCP option (announced length is %i)", olen + ) olen = 2 oval = x[2:olen] if onum in TCPOptions[0]: oname, ofmt = TCPOptions[0][onum] if onum == 5: # SAck ofmt += "%iI" % (len(oval) // 4) + if onum == 29: # AO + oval = TCPAOValue(oval) if ofmt and struct.calcsize(ofmt) == len(oval): oval = struct.unpack(ofmt, oval) if len(oval) == 1: @@ -388,8 +473,10 @@ def i2m(self, pkt, x): if not isinstance(oval, tuple): oval = (oval,) oval = struct.pack(ofmt, *oval) + if onum == 29: # AO + oval = bytes(oval) else: - warning("option [%s] unknown. Skipped.", oname) + warning("Option [%s] unknown. Skipped.", oname) continue else: onum = oname @@ -397,7 +484,7 @@ def i2m(self, pkt, x): warning("Invalid option number [%i]" % onum) continue if not isinstance(oval, (bytes, str)): - warning("option [%i] is not bytes." % onum) + warning("Option [%i] is not bytes." % onum) continue if isinstance(oval, str): oval = bytes_encode(oval) @@ -453,7 +540,6 @@ def i2h(self, pkt, x): class IP(Packet, IPTools): - __slots__ = ["_defrag_pos"] name = "IP" fields_desc = [BitField("version", 4, 4), BitField("ihl", None, 4), @@ -466,7 +552,7 @@ class IP(Packet, IPTools): ByteEnumField("proto", 0, IP_PROTOS), XShortField("chksum", None), # IPField("src", "127.0.0.1"), - Emph(SourceIPField("src", "dst")), + Emph(SourceIPField("src")), Emph(DestIPField("dst", "127.0.0.1")), PacketListField("options", [], IPOption, length_from=lambda p:p.ihl * 4 - 20)] # noqa: E501 @@ -492,12 +578,17 @@ def extract_padding(self, s): def route(self): dst = self.dst - if isinstance(dst, Gen): + scope = None + if isinstance(dst, (Net, _ScopedIP)): + scope = dst.scope + if isinstance(dst, (Gen, list)): dst = next(iter(dst)) if conf.route is None: # unused import, only to initialize conf.route import scapy.route # noqa: F401 - return conf.route.route(dst) + if not isinstance(dst, (str, bytes, int)): + dst = str(dst) + return conf.route.route(dst, dev=scope) def hashret(self): if ((self.proto == socket.IPPROTO_ICMP) and @@ -551,44 +642,18 @@ def mysummary(self): def fragment(self, fragsize=1480): """Fragment IP datagrams""" - fragsize = (fragsize + 7) // 8 * 8 - lst = [] - fnb = 0 - fl = self - while fl.underlayer is not None: - fnb += 1 - fl = fl.underlayer - - for p in fl: - s = raw(p[fnb].payload) - nb = (len(s) + fragsize - 1) // fragsize - for i in range(nb): - q = p.copy() - del(q[fnb].payload) - del(q[fnb].chksum) - del(q[fnb].len) - if i != nb - 1: - q[fnb].flags |= 1 - q[fnb].frag += i * fragsize // 8 - r = conf.raw_layer(load=s[i * fragsize:(i + 1) * fragsize]) - r.overload_fields = p[fnb].payload.overload_fields.copy() - q.add_payload(r) - lst.append(q) - return lst + return fragment(self, fragsize=fragsize) -def in4_chksum(proto, u, p): - """ - As Specified in RFC 2460 - 8.1 Upper-Layer Checksums +def in4_pseudoheader(proto, u, plen): + # type: (int, IP, int) -> bytes + """IPv4 Pseudo Header as defined in RFC793 as bytes - Performs IPv4 Upper Layer checksum computation. Provided parameters are: - - 'proto' : value of upper layer protocol - - 'u' : IP upper layer instance - - 'p' : the payload of the upper layer provided as a string + :param proto: value of upper layer protocol + :param u: IP layer instance + :param plen: the length of the upper layer and payload """ - if not isinstance(u, IP): - warning("No IP underlayer to compute checksum. Leaving null.") - return 0 + u = u.copy() if u.len is not None: if u.ihl is None: olen = sum(len(x) for x in u.options) @@ -597,15 +662,89 @@ def in4_chksum(proto, u, p): ihl = u.ihl ln = max(u.len - 4 * ihl, 0) else: - ln = len(p) - psdhdr = struct.pack("!4s4sHH", - inet_pton(socket.AF_INET, u.src), - inet_pton(socket.AF_INET, u.dst), - proto, - ln) + ln = plen + + # Filter out IPOption_LSRR and IPOption_SSRR + sr_options = [opt for opt in u.options if isinstance(opt, IPOption_LSRR) or + isinstance(opt, IPOption_SSRR)] + len_sr_options = len(sr_options) + if len_sr_options == 1 and len(sr_options[0].routers): + # The checksum must be computed using the final + # destination address + u.dst = sr_options[0].routers[-1] + elif len_sr_options > 1: + message = "Found %d Source Routing Options! " + message += "Falling back to IP.dst for checksum computation." + warning(message, len_sr_options) + + return struct.pack("!4s4sHH", + inet_pton(socket.AF_INET, u.src), + inet_pton(socket.AF_INET, u.dst), + proto, + ln) + + +def in4_chksum(proto, u, p): + # type: (int, IP, bytes) -> int + """IPv4 Pseudo Header checksum as defined in RFC793 + + :param proto: value of upper layer protocol + :param u: upper layer instance + :param p: the payload of the upper layer provided as a string + """ + if not isinstance(u, IP): + warning("No IP underlayer to compute checksum. Leaving null.") + return 0 + psdhdr = in4_pseudoheader(proto, u, len(p)) return checksum(psdhdr + p) +def _is_ipv6_layer(p): + # type: (Packet) -> bytes + return (isinstance(p, scapy.layers.inet6.IPv6) or + isinstance(p, scapy.layers.inet6._IPv6ExtHdr)) + + +def tcp_pseudoheader(tcp): + # type: (TCP) -> bytes + """Pseudoheader of a TCP packet as bytes + + Requires underlayer to be either IP or IPv6 + """ + if isinstance(tcp.underlayer, IP): + plen = len(bytes(tcp)) + return in4_pseudoheader(socket.IPPROTO_TCP, tcp.underlayer, plen) + elif conf.ipv6_enabled and _is_ipv6_layer(tcp.underlayer): + plen = len(bytes(tcp)) + return raw(scapy.layers.inet6.in6_pseudoheader( + socket.IPPROTO_TCP, tcp.underlayer, plen)) + else: + raise ValueError("TCP packet does not have IP or IPv6 underlayer") + + +def calc_tcp_md5_hash(tcp, key): + # type: (TCP, bytes) -> bytes + """Calculate TCP-MD5 hash from packet and return a 16-byte string""" + import hashlib + + h = hashlib.md5() # nosec + tcp_bytes = bytes(tcp) + h.update(tcp_pseudoheader(tcp)) + h.update(tcp_bytes[:16]) + h.update(b"\x00\x00") + h.update(tcp_bytes[18:]) + h.update(key) + + return h.digest() + + +def sign_tcp_md5(tcp, key): + # type: (TCP, bytes) -> None + """Append TCP-MD5 signature to tcp packet""" + sig = calc_tcp_md5_hash(tcp, key) + tcp.options = tcp.options + [('MD5', sig)] + + class TCP(Packet): name = "TCP" fields_desc = [ShortEnumField("sport", 20, TCP_SERVICES), @@ -636,7 +775,9 @@ def post_build(self, p, pay): ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_TCP, self.underlayer, p) # noqa: E501 p = p[:16] + struct.pack("!H", ck) + p[18:] else: - warning("No IP underlayer to compute checksum. Leaving null.") + log_runtime.info( + "No IP underlayer to compute checksum. Leaving null." + ) return p def hashret(self): @@ -712,7 +853,9 @@ def post_build(self, p, pay): ck = 0xFFFF p = p[:6] + struct.pack("!H", ck) + p[8:] else: - warning("No IP underlayer to compute checksum. Leaving null.") + log_runtime.info( + "No IP underlayer to compute checksum. Leaving null." + ) return p def extract_padding(self, s): @@ -739,6 +882,185 @@ def mysummary(self): return self.sprintf("UDP %UDP.sport% > %UDP.dport%") +# RFC 4884 ICMP extensions +_ICMP_classnums = { + # https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-ext-classes + 1: "MPLS", + 2: "Interface Information", + 3: "Interface Identification", + 4: "Extended Information", +} + + +class ICMPExtension_Object(Packet): + name = "ICMP Extension Object" + show_indent = 0 + fields_desc = [ + ShortField("len", None), + ByteEnumField("classnum", 0, _ICMP_classnums), + ByteField("classtype", 0), + ] + + def post_build(self, p, pay): + if self.len is None: + tmp_len = len(p) + len(pay) + p = struct.pack("!H", tmp_len) + p[2:] + return p + pay + + registered_icmp_exts = {} + + @classmethod + def register_variant(cls): + cls.registered_icmp_exts[cls.classnum.default] = cls + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 4: + classnum = _pkt[2] + if classnum in cls.registered_icmp_exts: + return cls.registered_icmp_exts[classnum] + return cls + + +class ICMPExtension_InterfaceInformation(ICMPExtension_Object): + name = "ICMP Extension Object - Interface Information Object (RFC5837)" + + fields_desc = [ + ShortField("len", None), + ByteEnumField("classnum", 2, _ICMP_classnums), + BitField("classtype", 0, 2), + BitField("reserved", 0, 2), + BitField("has_ifindex", 0, 1), + BitField("has_ipaddr", 0, 1), + BitField("has_ifname", 0, 1), + BitField("has_mtu", 0, 1), + ConditionalField(IntField("ifindex", None), lambda pkt: pkt.has_ifindex == 1), + ConditionalField(ShortField("afi", None), lambda pkt: pkt.has_ipaddr == 1), + ConditionalField(ShortField("reserved2", 0), lambda pkt: pkt.has_ipaddr == 1), + ConditionalField(IPField("ip4", None), lambda pkt: pkt.afi == 1), + ConditionalField(IP6Field("ip6", None), lambda pkt: pkt.afi == 2), + ConditionalField( + FieldLenField("ifname_len", None, fmt="B", length_of="ifname"), + lambda pkt: pkt.has_ifname == 1, + ), + ConditionalField( + StrLenField("ifname", None, length_from=lambda pkt: pkt.ifname_len), + lambda pkt: pkt.has_ifname == 1, + ), + ConditionalField(IntField("mtu", None), lambda pkt: pkt.has_mtu == 1), + ] + + def self_build(self, **kwargs): + if self.afi is None: + if self.ip4 is not None: + self.afi = 1 + elif self.ip6 is not None: + self.afi = 2 + return ICMPExtension_Object.self_build(self, **kwargs) + + +class ICMPExtension_Header(Packet): + r""" + ICMP Extension per RFC4884. + + Example:: + + pkt = IP(dst="127.0.0.1", src="127.0.0.1") / ICMP( + type="time-exceeded", + code="ttl-zero-during-transit", + ext=ICMPExtension_Header() / ICMPExtension_InterfaceInformation( + has_ifindex=1, + has_ipaddr=1, + has_ifname=1, + ip4="10.10.10.10", + ifname="hey", + ) + ) / IPerror(src="12.4.4.4", dst="12.1.1.1") / \ + UDPerror(sport=42315, dport=33440) / \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + """ + + name = "ICMP Extension Header (RFC4884)" + show_indent = 0 + fields_desc = [ + BitField("version", 2, 4), + BitField("reserved", 0, 12), + XShortField("chksum", None), + ] + + _min_ieo_len = len(ICMPExtension_Object()) + + def post_build(self, p, pay): + p += pay + if self.chksum is None: + ck = checksum(p) + p = p[:2] + chb(ck >> 8) + chb(ck & 0xFF) + p[4:] + return p + + def guess_payload_class(self, payload): + if len(payload) < self._min_ieo_len: + return Packet.guess_payload_class(self, payload) + return ICMPExtension_Object + + +class _ICMPExtensionField(TrailerField): + # We use a TrailerField for building only. Dissection is normal. + + def __init__(self): + super(_ICMPExtensionField, self).__init__( + PacketField( + "ext", + None, + ICMPExtension_Header, + ), + ) + + def getfield(self, pkt, s): + # RFC4884 section 5.2 says if the ICMP packet length + # is >144 then ICMP extensions start at byte 137. + if len(pkt.original) < 144: + return s, None + offset = 136 + len(s) - len(pkt.original) + data = s[offset:] + # Validate checksum + if checksum(data) == data[3:5]: + return s, None # failed + # Dissect + return s[:offset], ICMPExtension_Header(data) + + def addfield(self, pkt, s, val): + if val is None: + return s + data = bytes(val) + # Calc how much padding we need, not how much we deserve + pad = 136 - len(pkt.payload) - len(s) + if pad < 0: + warning("ICMPExtension_Header is after the 136th octet of ICMP.") + return data + return super(_ICMPExtensionField, self).addfield(pkt, s, b"\x00" * pad + data) + + +class _ICMPExtensionPadField(TrailerField): + def __init__(self): + super(_ICMPExtensionPadField, self).__init__( + StrFixedLenField("extpad", "", length=0) + ) + + def i2repr(self, pkt, s): + if s and s == b"\x00" * len(s): + return "b'' (%s octets)" % len(s) + return self.fld.i2repr(pkt, s) + + +def _ICMP_extpad_post_dissection(self, pkt): + # If we have padding, put it in 'extpad' for re-build + if pkt.ext: + pad = pkt.lastlayer() + if isinstance(pad, conf.padding_layer): + pad.underlayer.remove_payload() + pkt.extpad = pad.load + + icmptypes = {0: "echo-reply", 3: "dest-unreach", 4: "source-quench", @@ -798,25 +1120,99 @@ def mysummary(self): 5: "need-authorization", }, } +_icmp_answers = [ + (8, 0), + (13, 14), + (15, 16), + (17, 18), + (33, 34), + (35, 36), + (37, 38), +] + +icmp_id_seq_types = [0, 8, 13, 14, 15, 16, 17, 18, 37, 38] + + class ICMP(Packet): name = "ICMP" - fields_desc = [ByteEnumField("type", 8, icmptypes), - MultiEnumField("code", 0, icmpcodes, depends_on=lambda pkt:pkt.type, fmt="B"), # noqa: E501 - XShortField("chksum", None), - ConditionalField(XShortField("id", 0), lambda pkt:pkt.type in [0, 8, 13, 14, 15, 16, 17, 18]), # noqa: E501 - ConditionalField(XShortField("seq", 0), lambda pkt:pkt.type in [0, 8, 13, 14, 15, 16, 17, 18]), # noqa: E501 - ConditionalField(ICMPTimeStampField("ts_ori", None), lambda pkt:pkt.type in [13, 14]), # noqa: E501 - ConditionalField(ICMPTimeStampField("ts_rx", None), lambda pkt:pkt.type in [13, 14]), # noqa: E501 - ConditionalField(ICMPTimeStampField("ts_tx", None), lambda pkt:pkt.type in [13, 14]), # noqa: E501 - ConditionalField(IPField("gw", "0.0.0.0"), lambda pkt:pkt.type == 5), # noqa: E501 - ConditionalField(ByteField("ptr", 0), lambda pkt:pkt.type == 12), # noqa: E501 - ConditionalField(ByteField("reserved", 0), lambda pkt:pkt.type in [3, 11]), # noqa: E501 - ConditionalField(ByteField("length", 0), lambda pkt:pkt.type in [3, 11, 12]), # noqa: E501 - ConditionalField(IPField("addr_mask", "0.0.0.0"), lambda pkt:pkt.type in [17, 18]), # noqa: E501 - ConditionalField(ShortField("nexthopmtu", 0), lambda pkt:pkt.type == 3), # noqa: E501 - ConditionalField(ShortField("unused", 0), lambda pkt:pkt.type in [11, 12]), # noqa: E501 - ConditionalField(IntField("unused", 0), lambda pkt:pkt.type not in [0, 3, 5, 8, 11, 12, 13, 14, 15, 16, 17, 18]) # noqa: E501 - ] + fields_desc = [ + ByteEnumField("type", 8, icmptypes), + MultiEnumField("code", 0, icmpcodes, + depends_on=lambda pkt:pkt.type, fmt="B"), + XShortField("chksum", None), + ConditionalField( + XShortField("id", 0), + lambda pkt: pkt.type in icmp_id_seq_types + ), + ConditionalField( + XShortField("seq", 0), + lambda pkt: pkt.type in icmp_id_seq_types + ), + ConditionalField( + # Timestamp only (RFC792) + ICMPTimeStampField("ts_ori", None), + lambda pkt: pkt.type in [13, 14] + ), + ConditionalField( + # Timestamp only (RFC792) + ICMPTimeStampField("ts_rx", None), + lambda pkt: pkt.type in [13, 14] + ), + ConditionalField( + # Timestamp only (RFC792) + ICMPTimeStampField("ts_tx", None), + lambda pkt: pkt.type in [13, 14] + ), + ConditionalField( + # Redirect only (RFC792) + IPField("gw", "0.0.0.0"), + lambda pkt: pkt.type == 5 + ), + ConditionalField( + # Parameter problem only (RFC792) + ByteField("ptr", 0), + lambda pkt: pkt.type == 12 + ), + ConditionalField( + ByteField("reserved", 0), + lambda pkt: pkt.type in [3, 11] + ), + ConditionalField( + ByteField("length", 0), + lambda pkt: pkt.type in [3, 11, 12] + ), + ConditionalField( + IPField("addr_mask", "0.0.0.0"), + lambda pkt: pkt.type in [17, 18] + ), + ConditionalField( + ShortField("nexthopmtu", 0), + lambda pkt: pkt.type == 3 + ), + MultipleTypeField( + [ + (ShortField("unused", 0), + lambda pkt:pkt.type in [11, 12]), + (IntField("unused", 0), + lambda pkt:pkt.type not in [0, 3, 5, 8, 11, 12, + 13, 14, 15, 16, 17, + 18]) + ], + StrFixedLenField("unused", "", length=0), + ), + # RFC4884 ICMP extension + ConditionalField( + _ICMPExtensionPadField(), + lambda pkt: pkt.type in [3, 11, 12], + ), + ConditionalField( + _ICMPExtensionField(), + lambda pkt: pkt.type in [3, 11, 12], + ), + ] + + # To handle extpad + post_dissection = _ICMP_extpad_post_dissection def post_build(self, p, pay): p += pay @@ -826,14 +1222,14 @@ def post_build(self, p, pay): return p def hashret(self): - if self.type in [0, 8, 13, 14, 15, 16, 17, 18, 33, 34, 35, 36, 37, 38]: + if self.type in icmp_id_seq_types: return struct.pack("HH", self.id, self.seq) + self.payload.hashret() # noqa: E501 return self.payload.hashret() def answers(self, other): if not isinstance(other, ICMP): return 0 - if ((other.type, self.type) in [(8, 0), (13, 14), (15, 16), (17, 18), (33, 34), (35, 36), (37, 38)] and # noqa: E501 + if ((other.type, self.type) in _icmp_answers and self.id == other.id and self.seq == other.seq): return 1 @@ -846,11 +1242,18 @@ def guess_payload_class(self, payload): return None def mysummary(self): + extra = "" + if self.ext: + extra = self.ext.payload.sprintf(" ext:%classnum%") if isinstance(self.underlayer, IP): - return self.underlayer.sprintf("ICMP %IP.src% > %IP.dst% %ICMP.type% %ICMP.code%") # noqa: E501 + return self.underlayer.sprintf( + "ICMP %IP.src% > %IP.dst% %ICMP.type% %ICMP.code%" + ) + extra else: - return self.sprintf("ICMP %ICMP.type% %ICMP.code%") + return self.sprintf("ICMP %ICMP.type% %ICMP.code%") + extra + +# IP / TCP / UDP error packets class IPerror(IP): name = "IP in ICMP" @@ -881,6 +1284,12 @@ def mysummary(self): class TCPerror(TCP): name = "TCP in ICMP" + fields_desc = ( + TCP.fields_desc[:2] + + # MayEnd after the 8 first octets. + [MayEnd(TCP.fields_desc[2])] + + TCP.fields_desc[3:] + ) def answers(self, other): if not isinstance(other, TCP): @@ -955,16 +1364,22 @@ def mysummary(self): bind_layers(IP, TCP, frag=0, proto=6) bind_layers(IP, UDP, frag=0, proto=17) bind_layers(IP, GRE, frag=0, proto=47) +bind_layers(UDP, GRE, dport=4754) conf.l2types.register(DLT_RAW, IP) conf.l2types.register_num2layer(DLT_RAW_ALT, IP) conf.l2types.register(DLT_IPV4, IP) +if OPENBSD: + conf.l2types.register_num2layer(228, IP) conf.l3types.register(ETH_P_IP, IP) conf.l3types.register_num2layer(ETH_P_ALL, IP) def inet_register_l3(l2, l3): + """ + Resolves the default L2 destination address when IP is used. + """ return getmacbyip(l3.dst) @@ -979,20 +1394,27 @@ def inet_register_l3(l2, l3): @conf.commands.register def fragment(pkt, fragsize=1480): """Fragment a big IP datagram""" - fragsize = (fragsize + 7) // 8 * 8 + if fragsize < 8: + warning("fragsize cannot be lower than 8") + fragsize = max(fragsize, 8) + lastfragsz = fragsize + fragsize -= fragsize % 8 lst = [] for p in pkt: s = raw(p[IP].payload) - nb = (len(s) + fragsize - 1) // fragsize + nb = (len(s) - lastfragsz + fragsize - 1) // fragsize + 1 for i in range(nb): q = p.copy() - del(q[IP].payload) - del(q[IP].chksum) - del(q[IP].len) + del q[IP].payload + del q[IP].chksum + del q[IP].len if i != nb - 1: q[IP].flags |= 1 + fragend = (i + 1) * fragsize + else: + fragend = i * fragsize + lastfragsz q[IP].frag += i * fragsize // 8 - r = conf.raw_layer(load=s[i * fragsize:(i + 1) * fragsize]) + r = conf.raw_layer(load=s[i * fragsize:fragend]) r.overload_fields = p[IP].payload.overload_fields.copy() q.add_payload(r) lst.append(q) @@ -1011,7 +1433,7 @@ def overlap_frag(p, overlap, fragsize=8, overlap_fragsize=None): if overlap_fragsize is None: overlap_fragsize = fragsize q = p.copy() - del(q[IP].payload) + del q[IP].payload q[IP].add_payload(overlap) qfrag = fragment(q, overlap_fragsize) @@ -1019,86 +1441,115 @@ def overlap_frag(p, overlap, fragsize=8, overlap_fragsize=None): return qfrag + fragment(p, fragsize) -def _defrag_list(lst, defrag, missfrag): - """Internal usage only. Part of the _defrag_logic""" - p = lst[0] - lastp = lst[-1] - if p.frag > 0 or lastp.flags.MF: # first or last fragment missing - missfrag.append(lst) - return - p = p.copy() - if conf.padding_layer in p: - del(p[conf.padding_layer].underlayer.payload) - ip = p[IP] - if ip.len is None or ip.ihl is None: - clen = len(ip.payload) - else: - clen = ip.len - (ip.ihl << 2) - txt = conf.raw_layer() - for q in lst[1:]: - if clen != q.frag << 3: # Wrong fragmentation offset - if clen > q.frag << 3: - warning("Fragment overlap (%i > %i) %r || %r || %r" % (clen, q.frag << 3, p, txt, q)) # noqa: E501 - missfrag.append(lst) - break - if q[IP].len is None or q[IP].ihl is None: - clen += len(q[IP].payload) +class BadFragments(ValueError): + def __init__(self, *args, **kwargs): + self.frags = kwargs.pop("frags", None) + super(BadFragments, self).__init__(*args, **kwargs) + + +def _defrag_iter_and_check_offsets(frags): + """ + Internal generator used in _defrag_ip_pkt + """ + offset = 0 + for pkt, o, length in frags: + if offset != o: + if offset > o: + op = ">" + else: + op = "<" + warning("Fragment overlap (%i %s %i) on %r" % (offset, op, o, pkt)) + raise BadFragments + offset += length + yield bytes(pkt[IP].payload) + + +def _defrag_ip_pkt(pkt, frags): + """ + Defragment a single IP packet. + + :param pkt: the new pkt + :param frags: a defaultdict(list) used for storage + :return: a tuple (fragmented, defragmented_value) + """ + ip = pkt[IP] + if pkt.frag != 0 or ip.flags.MF: + # fragmented ! + uid = (ip.id, ip.src, ip.dst, ip.proto) + if ip.len is None or ip.ihl is None: + fraglen = len(ip.payload) else: - clen += q[IP].len - (q[IP].ihl << 2) - if conf.padding_layer in q: - del(q[conf.padding_layer].underlayer.payload) - txt.add_payload(q[IP].payload.copy()) - if q.time > p.time: - p.time = q.time - else: - ip.flags.MF = False - del(ip.chksum) - del(ip.len) - p = p / txt - p._defrag_pos = max(x._defrag_pos for x in lst) - defrag.append(p) + fraglen = ip.len - (ip.ihl << 2) + # (pkt, frag offset, frag len) + frags[uid].append((pkt, ip.frag << 3, fraglen)) + if not ip.flags.MF: # no more fragments = last fragment + curfrags = sorted(frags[uid], key=lambda x: x[1]) # sort by offset + try: + data = b"".join(_defrag_iter_and_check_offsets(curfrags)) + except ValueError: + # bad fragment + badfrags = frags[uid] + del frags[uid] + raise BadFragments(frags=badfrags) + # re-build initial packet without fragmentation + p = curfrags[0][0].copy() + pay_class = p[IP].payload.__class__ + p[IP].flags.MF = False + p[IP].remove_payload() + p[IP].len = None + p[IP].chksum = None + # append defragmented payload + p /= pay_class(data) + # cleanup + del frags[uid] + return True, p + return True, None + return False, pkt def _defrag_logic(plist, complete=False): - """Internal function used to defragment a list of packets. + """ + Internal function used to defragment a list of packets. It contains the logic behind the defrag() and defragment() functions """ - frags = defaultdict(lambda: []) + frags = defaultdict(list) final = [] - pos = 0 - for p in plist: - p._defrag_pos = pos - pos += 1 - if IP in p: - ip = p[IP] - if ip.frag != 0 or ip.flags.MF: - uniq = (ip.id, ip.src, ip.dst, ip.proto) - frags[uniq].append(p) - continue - final.append(p) - - defrag = [] - missfrag = [] - for lst in six.itervalues(frags): - lst.sort(key=lambda x: x.frag) - _defrag_list(lst, defrag, missfrag) - defrag2 = [] - for p in defrag: - q = p.__class__(raw(p)) - q._defrag_pos = p._defrag_pos - q.time = p.time - defrag2.append(q) + notfrag = [] + badfrag = [] + # Defrag + for i, pkt in enumerate(plist): + if IP not in pkt: + # no IP layer + if complete: + final.append(pkt) + continue + try: + fragmented, defragmented_value = _defrag_ip_pkt( + pkt, + frags, + ) + except BadFragments as ex: + if complete: + final.extend(ex.frags) + else: + badfrag.extend(ex.frags) + continue + if complete and defragmented_value: + final.append(defragmented_value) + elif defragmented_value: + if fragmented: + final.append(defragmented_value) + else: + notfrag.append(defragmented_value) + # Return if complete: - final.extend(defrag2) - final.extend(missfrag) - final.sort(key=lambda x: x._defrag_pos) if hasattr(plist, "listname"): name = "Defragmented %s" % plist.listname else: name = "Defragmented" return PacketList(final, name=name) else: - return PacketList(final), PacketList(defrag2), PacketList(missfrag) + return PacketList(notfrag), PacketList(final), PacketList(badfrag) @conf.commands.register @@ -1117,6 +1568,13 @@ def defragment(plist): # Add timeskew_graph() method to PacketList def _packetlist_timeskew_graph(self, ip, **kargs): """Tries to graph the timeskew between the timestamps and real time for a given ip""" # noqa: E501 + # Defer imports of matplotlib until its needed + # because it has a heavy dep chain + from scapy.libs.matplot import ( + plt, + MATPLOTLIB_INLINED, + MATPLOTLIB_DEFAULT_PLOT_KARGS + ) # Filter TCP segments which source address is 'ip' tmp = (self._elt2pkt(x) for x in self.res) @@ -1163,7 +1621,7 @@ def _wrap_data(ts_tuple, wrap_seconds=2000): return lines -PacketList.timeskew_graph = _packetlist_timeskew_graph +_PacketList.timeskew_graph = _packetlist_timeskew_graph # Create a new packet list @@ -1193,14 +1651,14 @@ def get_trace(self): if d not in trace: trace[d] = {} trace[d][s[IP].ttl] = r[IP].src, ICMP not in r - for k in six.itervalues(trace): + for k in trace.values(): try: - m = min(x for x, y in six.iteritems(k) if y[1]) + m = min(x for x, y in k.items() if y[1]) except ValueError: continue - for l in list(k): # use list(): k is modified in the loop - if l > m: - del k[l] + for li in list(k): # use list(): k is modified in the loop + if li > m: + del k[li] return trace def trace3D(self, join=True): @@ -1219,7 +1677,7 @@ def trace3D(self, join=True): p.join() def trace3D_notebook(self): - """Same than trace3D, used when ran from Jupyther notebooks""" + """Same than trace3D, used when ran from Jupyter notebooks""" trace = self.get_trace() import vpython @@ -1326,12 +1784,12 @@ def action(self): s = IPsphere(pos=vpython.vec((tmp_len - 1) * vpython.cos(2 * i * vpython.pi / tmp_len), (tmp_len - 1) * vpython.sin(2 * i * vpython.pi / tmp_len), 2 * t), # noqa: E501 ip=r[i][0], color=col) - for trlst in six.itervalues(tr3d): + for trlst in tr3d.values(): if t <= len(trlst): if trlst[t - 1] == i: trlst[t - 1] = s forecol = colgen(0.625, 0.4375, 0.25, 0.125) - for trlst in six.itervalues(tr3d): + for trlst in tr3d.values(): col = vpython.vec(*next(forecol)) start = vpython.vec(0, 0, 0) for ip in trlst: @@ -1369,35 +1827,48 @@ def world_trace(self): # Check that the geoip2 module can be imported # Doc: http://geoip2.readthedocs.io/en/latest/ + from scapy.libs.matplot import plt, MATPLOTLIB, MATPLOTLIB_INLINED + try: # GeoIP2 modules need to be imported as below import geoip2.database import geoip2.errors except ImportError: - warning("Cannot import geoip2. Won't be able to plot the world.") + log_runtime.error( + "Cannot import geoip2. Won't be able to plot the world." + ) return [] # Check availability of database if not conf.geoip_city: - warning("Cannot import the geolite2 CITY database.\n" - "Download it from http://dev.maxmind.com/geoip/geoip2/geolite2/" # noqa: E501 - " then set its path to conf.geoip_city") + log_runtime.error( + "Cannot import the geolite2 CITY database.\n" + "Download it from http://dev.maxmind.com/geoip/geoip2/geolite2/" # noqa: E501 + " then set its path to conf.geoip_city" + ) return [] # Check availability of plotting devices try: import cartopy.crs as ccrs except ImportError: - warning("Cannot import cartopy.\n" - "More infos on http://scitools.org.uk/cartopy/docs/latest/installing.html") # noqa: E501 + log_runtime.error( + "Cannot import cartopy.\n" + "More infos on http://scitools.org.uk/cartopy/docs/latest/installing.html" # noqa: E501 + ) return [] if not MATPLOTLIB: - warning("Matplotlib is not installed. Won't be able to plot the world.") # noqa: E501 + log_runtime.error( + "Matplotlib is not installed. Won't be able to plot the world." + ) return [] # Open & read the GeoListIP2 database try: db = geoip2.database.Reader(conf.geoip_city) except Exception: - warning("Cannot open geoip2 database at %s", conf.geoip_city) + log_runtime.error( + "Cannot open geoip2 database at %s", + conf.geoip_city + ) return [] # Regroup results per trace @@ -1455,7 +1926,7 @@ def world_trace(self): lines = [] # Split traceroute measurement - for key, trc in six.iteritems(trt): + for key, trc in trt.items(): # Get next color color = next(colors_cycle) # Gather mesurments data @@ -1703,18 +2174,29 @@ class TCP_client(Automaton): >>> a = TCP_client.tcplink(HTTP, "www.google.com", 80) >>> a.send(HTTPRequest()) >>> a.recv() + + :param ip: the ip to connect to + :param port: + :param src: (optional) use another source IP + :param sport: (optional) the TCP source port (default: random) + :param seq: (optional) initial TCP sequence number (default: random) """ - def parse_args(self, ip, port, *args, **kargs): + + def parse_args(self, ip, port, srcip=None, sport=None, seq=None, ack=0, **kargs): from scapy.sessions import TCPSession self.dst = str(Net(ip)) self.dport = port - self.sport = random.randrange(0, 2**16) - self.l4 = IP(dst=ip) / TCP(sport=self.sport, dport=self.dport, flags=0, - seq=random.randrange(0, 2**32)) + self.sport = sport if sport is not None else random.randrange(0, 2**16) + self.l4 = IP(dst=ip, src=srcip) / TCP( + sport=self.sport, dport=self.dport, + flags=0, + seq=seq if seq is not None else random.randrange(0, 2**32), + ack=ack, + ) self.src = self.l4.src self.sack = self.l4[TCP].ack self.rel_seq = None - self.rcvbuf = TCPSession(self._transmit_packet, False) + self.rcvbuf = TCPSession() bpf = "host %s and host %s and port %i and port %i" % (self.src, self.dst, self.sport, @@ -1755,6 +2237,14 @@ def LAST_ACK(self): def CLOSED(self): pass + @ATMT.state(stop=1) + def STOP(self): + pass + + @ATMT.state() + def STOP_SENT_FIN_ACK(self): + pass + @ATMT.condition(START) def connect(self): raise self.SYN_SENT() @@ -1791,7 +2281,9 @@ def receive_data(self, pkt): # Answer with an Ack self.send(self.l4) # Process data - will be sent to the SuperSocket through this - self.rcvbuf.on_packet_received(pkt) + pkt = self.rcvbuf.process(pkt) + if pkt: + self._transmit_packet(pkt) @ATMT.ioevent(ESTABLISHED, name="tcp", as_supersocket="tcplink") def outgoing_data_received(self, fd): @@ -1825,6 +2317,35 @@ def ack_of_fin_received(self, pkt): if pkt[TCP].flags.A: raise self.CLOSED() + @ATMT.condition(STOP) + def stop_requested(self): + raise self.STOP_SENT_FIN_ACK() + + @ATMT.action(stop_requested) + def stop_send_finack(self): + self.l4[TCP].flags = "FA" + self.send(self.l4) + self.l4[TCP].seq += 1 + + @ATMT.receive_condition(STOP_SENT_FIN_ACK) + def stop_fin_received(self, pkt): + if pkt[TCP].flags.F: + raise self.CLOSED().action_parameters(pkt) + + @ATMT.action(stop_fin_received) + def stop_send_ack(self, pkt): + self.l4[TCP].flags = "A" + self.l4[TCP].ack = pkt[TCP].seq + 1 + self.send(self.l4) + + @ATMT.timeout(SYN_SENT, 1) + def syn_ack_timeout(self): + raise self.CLOSED() + + @ATMT.timeout(STOP_SENT_FIN_ACK, 1) + def stop_ack_timeout(self): + raise self.CLOSED() + ##################### # Reporting stuff # @@ -1867,7 +2388,7 @@ def IPID_count(lst, funcID=lambda x: x[1].id, funcpres=lambda x: x[1].summary()) classes += [t[1] for t in zip(idlst[:-1], idlst[1:]) if abs(t[0] - t[1]) > 50] # noqa: E501 lst = [(funcID(x), funcpres(x)) for x in lst] lst.sort() - print("Probably %i classes:" % len(classes), classes) + print("Probably %i classes: %s" % (len(classes), classes)) for id, pr in lst: print("%5i" % id, pr) @@ -1900,7 +2421,7 @@ def fragleak(target, sport=123, dport=123, timeout=0.2, onlyasc=0, count=None): if ans.payload.payload.dst != target: continue if ans.src != target: - print("leak from", ans.src, end=' ') + print("leak from", ans.src) if not ans.haslayer(conf.padding_layer): continue leak = ans.getlayer(conf.padding_layer).load @@ -1937,6 +2458,99 @@ def fragleak2(target, timeout=0.4, onlyasc=0, count=None): pass +@conf.commands.register +class connect_from_ip: + """ + Open a TCP socket to a host:port while spoofing another IP. + + :param host: the host to connect to + :param port: the port to connect to + :param srcip: the IP to spoof. the cache of the gateway will + be poisonned with this IP. + :param poison: (optional, default True) ARP poison the gateway (or next hop), + so that it answers us (only one packet). + :param timeout: (optional) the socket timeout. + + Example - Connect to 192.168.0.1:80 spoofing 192.168.0.2:: + + from scapy.layers.http import HTTP, HTTPRequest + client = connect_from_ip("192.168.0.1", 80, "192.168.0.2") + sock = SSLStreamSocket(client.sock, HTTP) + resp = sock.sr1(HTTP() / HTTPRequest(Path="/")) + + Example - Connect to 192.168.0.1:443 with TLS wrapping spoofing 192.168.0.2:: + + import ssl + from scapy.layers.http import HTTP, HTTPRequest + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + client = connect_from_ip("192.168.0.1", 443, "192.168.0.2") + sock = context.wrap_socket(client.sock) + sock = SSLStreamSocket(client.sock, HTTP) + resp = sock.sr1(HTTP() / HTTPRequest(Path="/")) + """ + + def __init__(self, host, port, srcip, poison=True, timeout=1, debug=0): + host = str(Net(host)) + if poison: + # poison the next hop + gateway = conf.route.route(host)[2] + if gateway == "0.0.0.0": + # on lan + gateway = host + getmacbyip(gateway) # cache real gateway before poisoning + arpcachepoison(gateway, srcip, count=1, interval=0, verbose=0) + # create a socket pair + self._sock, self.sock = socket.socketpair() + self.sock.settimeout(timeout) + self.client = TCP_client( + host, port, + srcip=srcip, + external_fd={"tcp": self._sock}, + debug=debug, + ) + # start the TCP_client + self.client.runbg() + + def close(self): + self.client.stop() + self.client.destroy() + self.sock.close() + self._sock.close() + + +class ICMPEcho_am(AnsweringMachine): + """Responds to ICMP Echo-Requests (ping)""" + function_name = "icmpechod" + + def is_request(self, req): + if req.haslayer(ICMP): + icmp_req = req.getlayer(ICMP) + if icmp_req.type == 8: # echo-request + return True + + return False + + def print_reply(self, req, reply): + print("Replying %s to %s" % (reply[IP].dst, req[IP].dst)) + + def make_reply(self, req): + reply = req.copy() + reply[ICMP].type = 0 # echo-reply + # Force re-generation of the checksum + reply[ICMP].chksum = None + if req.haslayer(IP): + reply[IP].src, reply[IP].dst = req[IP].dst, req[IP].src + reply[IP].chksum = None + if req.haslayer(Ether): + reply[Ether].src, reply[Ether].dst = ( + None if req[Ether].dst == "ff:ff:ff:ff:ff:ff" else req[Ether].dst, + req[Ether].src, + ) + return reply + + conf.stats_classic_protocols += [TCP, UDP, ICMP] conf.stats_dot11_protocols += [TCP, UDP, ICMP] diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index 6b50d1fd6be..4645a5e1344 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -1,31 +1,17 @@ -############################################################################# -# # -# inet6.py --- IPv6 support for Scapy # -# see http://natisbad.org/IPv6/ # -# for more information # -# # -# Copyright (C) 2005 Guillaume Valadon # -# Arnaud Ebalard # -# # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License version 2 as # -# published by the Free Software Foundation. # -# # -# This program is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -# # -############################################################################# +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Guillaume Valadon +# Copyright (C) Arnaud Ebalard + +# Cool history about this file: http://natisbad.org/scapy/index.html + """ IPv6 (Internet Protocol v6). """ -from __future__ import absolute_import -from __future__ import print_function - from hashlib import md5 import random import socket @@ -34,25 +20,70 @@ from scapy.arch import get_if_hwaddr from scapy.as_resolvers import AS_resolver_riswhois -from scapy.base_classes import Gen +from scapy.base_classes import Gen, _ScopedIP from scapy.compat import chb, orb, raw, plain_str, bytes_encode +from scapy.consts import WINDOWS, OPENBSD from scapy.config import conf -import scapy.consts -from scapy.data import DLT_IPV6, DLT_RAW, DLT_RAW_ALT, ETHER_ANY, ETH_P_IPV6, \ - MTU -from scapy.error import warning -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - DestIP6Field, FieldLenField, FlagsField, IntField, IP6Field, \ - LongField, MACField, PacketLenField, PacketListField, ShortEnumField, \ - ShortField, SourceIP6Field, StrField, StrFixedLenField, StrLenField, \ - X3BytesField, XBitField, XIntField, XShortField -from scapy.layers.inet import IP, IPTools, TCP, TCPerror, TracerouteResult, \ - UDP, UDPerror -from scapy.layers.l2 import CookedLinux, Ether, GRE, Loopback, SNAP -import scapy.modules.six as six +from scapy.data import ( + DLT_IPV6, + DLT_RAW, + DLT_RAW_ALT, + ETHER_ANY, + ETH_P_ALL, + ETH_P_IPV6, + MTU, +) +from scapy.error import log_runtime, warning +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + DestIP6Field, + FieldLenField, + FlagsField, + IntField, + IP6Field, + LongField, + MACField, + MayEnd, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + SourceIP6Field, + StrField, + StrFixedLenField, + StrLenField, + X3BytesField, + XBitField, + XByteField, + XIntField, + XShortField, +) +from scapy.layers.inet import ( + _ICMPExtensionField, + _ICMPExtensionPadField, + _ICMP_extpad_post_dissection, + IP, + IPTools, + TCP, + TCPerror, + TracerouteResult, + UDP, + UDPerror, +) +from scapy.layers.l2 import ( + CookedLinux, + Ether, + GRE, + Loopback, + SNAP, + SourceMACField, +) from scapy.packet import bind_layers, Packet, Raw from scapy.sendrecv import sendp, sniff, sr, srp1 -from scapy.supersocket import SuperSocket, L3RawSocket +from scapy.supersocket import SuperSocket from scapy.utils import checksum, strxor from scapy.pton_ntop import inet_pton, inet_ntop from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_isaddr6to4, \ @@ -60,6 +91,11 @@ in6_isllsnmaddr, in6_ismaddr, Net6, teredoAddrExtractInfo from scapy.volatile import RandInt, RandShort +# Typing +from typing import ( + Optional, +) + if not socket.has_ipv6: raise socket.error("can't use AF_INET6, IPv6 is disabled") if not hasattr(socket, "IPPROTO_IPV6"): @@ -71,7 +107,7 @@ if conf.route6 is None: # unused import, only to initialize conf.route6 - import scapy.route6 + import scapy.route6 # noqa: F401 ########################## # Neighbor cache stuff # @@ -87,7 +123,9 @@ def neighsol(addr, src, iface, timeout=1, chainCC=0): This function sends an ICMPv6 Neighbor Solicitation message to get the MAC address of the neighbor with specified IPv6 address address. - 'src' address is used as source of the message. Message is sent on iface. + 'src' address is used as the source IPv6 address of the message. Message + is sent on 'iface'. The source MAC address is retrieved accordingly. + By default, timeout waiting for an answer is 1 second. If no answer is gathered, None is returned. Else, the answer is @@ -97,10 +135,11 @@ def neighsol(addr, src, iface, timeout=1, chainCC=0): nsma = in6_getnsma(inet_pton(socket.AF_INET6, addr)) d = inet_ntop(socket.AF_INET6, nsma) dm = in6_getnsmac(nsma) - p = Ether(dst=dm) / IPv6(dst=d, src=src, hlim=255) + sm = get_if_hwaddr(iface) + p = Ether(dst=dm, src=sm) / IPv6(dst=d, src=src, hlim=255) p /= ICMPv6ND_NS(tgt=addr) - p /= ICMPv6NDOptSrcLLAddr(lladdr=get_if_hwaddr(iface)) - res = srp1(p, type=ETH_P_IPV6, iface=iface, timeout=1, verbose=0, + p /= ICMPv6NDOptSrcLLAddr(lladdr=sm) + res = srp1(p, type=ETH_P_IPV6, iface=iface, timeout=timeout, verbose=0, chainCC=chainCC) return res @@ -108,25 +147,30 @@ def neighsol(addr, src, iface, timeout=1, chainCC=0): @conf.commands.register def getmacbyip6(ip6, chainCC=0): - """Returns the MAC address corresponding to an IPv6 address + # type: (str, int) -> Optional[str] + """ + Returns the MAC address of the next hop used to reach a given IPv6 address. neighborCache.get() method is used on instantiated neighbor cache. Resolution mechanism is described in associated doc string. (chainCC parameter value ends up being passed to sending function used to perform the resolution, if needed) - """ + .. seealso:: :func:`~scapy.layers.l2.getmacbyip` for IPv4. + """ + # Sanitize the IP if isinstance(ip6, Net6): ip6 = str(ip6) - if in6_ismaddr(ip6): # Multicast + # Multicast + if in6_ismaddr(ip6): # mcast @ mac = in6_getnsmac(inet_pton(socket.AF_INET6, ip6)) return mac iff, a, nh = conf.route6.route(ip6) - if iff == scapy.consts.LOOPBACK_INTERFACE: + if iff == conf.loopback_name: return "ff:ff:ff:ff:ff:ff" if nh != '::': @@ -270,20 +314,23 @@ def default_payload_class(self, p): class IPv6(_IPv6GuessPayload, Packet, IPTools): name = "IPv6" fields_desc = [BitField("version", 6, 4), - BitField("tc", 0, 8), # TODO: IPv6, ByteField ? + BitField("tc", 0, 8), BitField("fl", 0, 20), ShortField("plen", None), ByteEnumField("nh", 59, ipv6nh), ByteField("hlim", 64), - SourceIP6Field("src", "dst"), # dst is for src @ selection + SourceIP6Field("src"), DestIP6Field("dst", "::1")] def route(self): """Used to select the L2 address""" dst = self.dst - if isinstance(dst, Gen): + scope = None + if isinstance(dst, (Net6, _ScopedIP)): + scope = dst.scope + if isinstance(dst, (Gen, list)): dst = next(iter(dst)) - return conf.route6.route(dst) + return conf.route6.route(dst, dev=scope) def mysummary(self): return "%s > %s (%i)" % (self.src, self.dst, self.nh) @@ -318,7 +365,7 @@ def extract_padding(self, data): idx += 1 if jumbo_len is None: - warning("Scapy did not find a Jumbo option") + log_runtime.info("Scapy did not find a Jumbo option") jumbo_len = 0 tmp_len = hbh_len + jumbo_len @@ -377,8 +424,8 @@ def hashret(self): if isinstance(o, HAO): foundhao = o if foundhao: - nh = self.payload.nh # XXX what if another extension follows ? ss = foundhao.hoa + nh = self.payload.nh # XXX what if another extension follows ? if conf.checkIPsrc and conf.checkIPaddr and not in6_ismaddr(sd): sd = inet_pton(socket.AF_INET6, sd) @@ -434,7 +481,7 @@ def answers(self, other): elif other.nh == 43 and isinstance(other.payload, IPv6ExtHdrSegmentRouting): # noqa: E501 return self.payload.answers(other.payload.payload) # Buggy if self.payload is a IPv6ExtHdrRouting # noqa: E501 elif other.nh == 60 and isinstance(other.payload, IPv6ExtHdrDestOpt): - return self.payload.payload.answers(other.payload.payload) + return self.payload.answers(other.payload.payload) elif self.nh == 60 and isinstance(self.payload, IPv6ExtHdrDestOpt): # BU in reply to BRR, for instance # noqa: E501 return self.payload.payload.answers(other.payload) else: @@ -443,11 +490,13 @@ def answers(self, other): return self.payload.answers(other.payload) -class _IPv46(IP): +class IPv46(IP, IPv6): """ This class implements a dispatcher that is used to detect the IP version while parsing Raw IP pcap files. """ + name = "IPv4/6" + @classmethod def dispatch_hook(cls, _pkt=None, *_, **kargs): if _pkt: @@ -459,6 +508,9 @@ def dispatch_hook(cls, _pkt=None, *_, **kargs): def inet6_register_l3(l2, l3): + """ + Resolves the default L2 destination address when IPv6 is used. + """ return getmacbyip6(l3.dst) @@ -554,16 +606,15 @@ class PseudoIPv6(Packet): # IPv6 Pseudo-header for checksum computation name = "Pseudo IPv6 Header" fields_desc = [IP6Field("src", "::"), IP6Field("dst", "::"), - ShortField("uplen", None), + IntField("uplen", None), BitField("zero", 0, 24), ByteField("nh", 0)] -def in6_chksum(nh, u, p): +def in6_pseudoheader(nh, u, plen): + # type: (int, IP, int) -> PseudoIPv6 """ - As Specified in RFC 2460 - 8.1 Upper-Layer Checksums - - Performs IPv6 Upper Layer checksum computation. + Build an PseudoIPv6 instance as specified in RFC 2460 8.1 This function operates by filling a pseudo header class instance (PseudoIPv6) with: @@ -580,9 +631,8 @@ def in6_chksum(nh, u, p): :param u: upper layer instance (TCP, UDP, ICMPv6*, ). Instance must be provided with all under layers (IPv6 and all extension headers, for example) - :param p: the payload of the upper layer provided as a string + :param plen: the length of the upper layer and payload """ - ph6 = PseudoIPv6() ph6.nh = nh rthdr = 0 @@ -605,7 +655,7 @@ def in6_chksum(nh, u, p): u = u.underlayer if u is None: warning("No IPv6 underlayer to compute checksum. Leaving null.") - return 0 + return None if hahdr: ph6.src = hahdr else: @@ -614,7 +664,25 @@ def in6_chksum(nh, u, p): ph6.dst = rthdr else: ph6.dst = u.dst - ph6.uplen = len(p) + ph6.uplen = plen + return ph6 + + +def in6_chksum(nh, u, p): + """ + As Specified in RFC 2460 - 8.1 Upper-Layer Checksums + + See also `.in6_pseudoheader` + + :param nh: value of upper layer protocol + :param u: upper layer instance (TCP, UDP, ICMPv6*, ). Instance must be + provided with all under layers (IPv6 and all extension headers, + for example) + :param p: the payload of the upper layer provided as a string + """ + ph6 = in6_pseudoheader(nh, u, len(p)) + if ph6 is None: + return 0 ph6s = raw(ph6) return checksum(ph6s + p) @@ -625,11 +693,23 @@ def in6_chksum(nh, u, p): ############################################################################# ############################################################################# +nh_clserror = {socket.IPPROTO_TCP: TCPerror, + socket.IPPROTO_UDP: UDPerror} + # Inherited by all extension header classes class _IPv6ExtHdr(_IPv6GuessPayload, Packet): name = 'Abstract IPv6 Option Header' - aliastypes = [IPv6, IPerror6] # TODO ... + aliastypes = [IPv6] + + def guess_payload_class(self, payload): + if self.nh in nh_clserror: + underlayer = self.underlayer + while underlayer: + if isinstance(underlayer, IPerror6): + return nh_clserror[self.nh] + underlayer = underlayer.underlayer + return super(_IPv6ExtHdr, self).guess_payload_class(payload) # IPv6 options for Extension Headers # @@ -677,8 +757,8 @@ def alignment_delta(self, curpos): # By default, no alignment requirement """ As specified in section 4.2 of RFC 2460, every options has an alignment requirement usually expressed xn+y, meaning - the Option Type must appear at an integer multiple of x octest - from the start of the header, plus y octet. + the Option Type must appear at an integer multiple of x octets + from the start of the header, plus y octets. That function is provided the current position from the start of the header and returns required padding length. @@ -748,6 +828,27 @@ def extract_padding(self, p): return b"", p +class RplOption(Packet): # RFC 6553 - RPL Option + name = "RPL Option" + fields_desc = [_OTypeField("otype", 0x63, _hbhopts), + ByteField("optlen", 4), + BitField("Down", 0, 1), + BitField("RankError", 0, 1), + BitField("ForwardError", 0, 1), + BitField("unused", 0, 5), + XByteField("RplInstanceId", 0), + XShortField("SenderRank", 0)] + + def alignment_delta(self, curpos): # alignment requirement : 2n+0 + x = 2 + y = 0 + delta = x * ((curpos - y + x - 1) // x) + y - curpos + return delta + + def extract_padding(self, p): + return b"", p + + class Jumbo(Packet): # IPv6 Hop-By-Hop Option name = "Jumbo Payload" fields_desc = [_OTypeField("otype", 0xC2, _hbhopts), @@ -783,6 +884,7 @@ def extract_padding(self, p): _hbhoptcls = {0x00: Pad1, 0x01: PadN, 0x05: RouterAlert, + 0x63: RplOption, 0xC2: Jumbo, 0xC9: HAO} @@ -807,7 +909,7 @@ def i2m(self, pkt, x): autopad = 1 if not autopad: - return b"".join(map(str, x)) + return b"".join(map(bytes, x)) curpos = self.curpos s = b"" @@ -859,7 +961,7 @@ class IPv6ExtHdrHopByHop(_IPv6ExtHdr): adjust=lambda pkt, x: (x + 2 + 7) // 8 - 1), _PhantomAutoPadField("autopad", 1), # autopad activated by default # noqa: E501 _OptionsField("options", [], HBHOptUnknown, 2, - length_from=lambda pkt: (8 * (pkt.len + 1)) - 2)] # noqa: E501 + length_from=lambda pkt: (8 * (pkt.len + 1)) - 2)] # noqa: E501 overload_fields = {IPv6: {"nh": 0}} @@ -872,7 +974,7 @@ class IPv6ExtHdrDestOpt(_IPv6ExtHdr): adjust=lambda pkt, x: (x + 2 + 7) // 8 - 1), _PhantomAutoPadField("autopad", 1), # autopad activated by default # noqa: E501 _OptionsField("options", [], HBHOptUnknown, 2, - length_from=lambda pkt: (8 * (pkt.len + 1)) - 2)] # noqa: E501 + length_from=lambda pkt: (8 * (pkt.len + 1)) - 2)] # noqa: E501 overload_fields = {IPv6: {"nh": 60}} @@ -898,15 +1000,24 @@ def post_build(self, pkt, pay): # Segment Routing Header # -# This implementation is based on draft 06, available at: +# This implementation is based on RFC8754, but some older snippets come from: # https://tools.ietf.org/html/draft-ietf-6man-segment-routing-header-06 +_segment_routing_header_tlvs = { + # RFC 8754 sect 8.2 + 0: "Pad1 TLV", + 1: "Ingress Node TLV", # draft 06 + 2: "Egress Node TLV", # draft 06 + 4: "PadN TLV", + 5: "HMAC TLV", +} + + class IPv6ExtHdrSegmentRoutingTLV(Packet): name = "IPv6 Option Header Segment Routing - Generic TLV" - fields_desc = [ByteField("type", 0), + # RFC 8754 sect 2.1 + fields_desc = [ByteEnumField("type", None, _segment_routing_header_tlvs), ByteField("len", 0), - ByteField("reserved", 0), - ByteField("flags", 0), StrLenField("value", "", length_from=lambda pkt: pkt.len)] def extract_padding(self, p): @@ -921,14 +1032,15 @@ def register_variant(cls): @classmethod def dispatch_hook(cls, pkt=None, *args, **kargs): if pkt: - tmp_type = orb(pkt[0]) + tmp_type = ord(pkt[:1]) return cls.registered_sr_tlv.get(tmp_type, cls) return cls class IPv6ExtHdrSegmentRoutingTLVIngressNode(IPv6ExtHdrSegmentRoutingTLV): name = "IPv6 Option Header Segment Routing - Ingress Node TLV" - fields_desc = [ByteField("type", 1), + # draft-ietf-6man-segment-routing-header-06 3.1.1 + fields_desc = [ByteEnumField("type", 1, _segment_routing_header_tlvs), ByteField("len", 18), ByteField("reserved", 0), ByteField("flags", 0), @@ -937,22 +1049,44 @@ class IPv6ExtHdrSegmentRoutingTLVIngressNode(IPv6ExtHdrSegmentRoutingTLV): class IPv6ExtHdrSegmentRoutingTLVEgressNode(IPv6ExtHdrSegmentRoutingTLV): name = "IPv6 Option Header Segment Routing - Egress Node TLV" - fields_desc = [ByteField("type", 2), + # draft-ietf-6man-segment-routing-header-06 3.1.2 + fields_desc = [ByteEnumField("type", 2, _segment_routing_header_tlvs), ByteField("len", 18), ByteField("reserved", 0), ByteField("flags", 0), IP6Field("egress_node", "::1")] -class IPv6ExtHdrSegmentRoutingTLVPadding(IPv6ExtHdrSegmentRoutingTLV): - name = "IPv6 Option Header Segment Routing - Padding TLV" - fields_desc = [ByteField("type", 4), +class IPv6ExtHdrSegmentRoutingTLVPad1(IPv6ExtHdrSegmentRoutingTLV): + name = "IPv6 Option Header Segment Routing - Pad1 TLV" + # RFC8754 sect 2.1.1.1, Pad1 is a single byte + fields_desc = [ByteEnumField("type", 0, _segment_routing_header_tlvs)] + + +class IPv6ExtHdrSegmentRoutingTLVPadN(IPv6ExtHdrSegmentRoutingTLV): + name = "IPv6 Option Header Segment Routing - PadN TLV" + # RFC8754 sect 2.1.1.2 + fields_desc = [ByteEnumField("type", 4, _segment_routing_header_tlvs), FieldLenField("len", None, length_of="padding", fmt="B"), StrLenField("padding", b"\x00", length_from=lambda pkt: pkt.len)] # noqa: E501 +class IPv6ExtHdrSegmentRoutingTLVHMAC(IPv6ExtHdrSegmentRoutingTLV): + name = "IPv6 Option Header Segment Routing - HMAC TLV" + # RFC8754 sect 2.1.2 + fields_desc = [ByteEnumField("type", 5, _segment_routing_header_tlvs), + FieldLenField("len", None, length_of="hmac", + adjust=lambda _, x: x + 48), + BitField("D", 0, 1), + BitField("reserved", 0, 15), + IntField("hmackeyid", 0), + StrLenField("hmac", "", + length_from=lambda pkt: pkt.len - 48)] + + class IPv6ExtHdrSegmentRouting(_IPv6ExtHdr): name = "IPv6 Option Header Segment Routing" + # RFC8754 sect 2. + flag bits from draft 06 fields_desc = [ByteEnumField("nh", 59, ipv6nh), ByteField("len", None), ByteField("type", 4), @@ -980,13 +1114,14 @@ def post_build(self, pkt, pay): if self.len is None: # The extension must be align on 8 bytes - tmp_mod = (len(pkt) - 8) % 8 + tmp_mod = (-len(pkt) + 8) % 8 if tmp_mod == 1: - warning("IPv6ExtHdrSegmentRouting(): can't pad 1 byte!") + tlv = IPv6ExtHdrSegmentRoutingTLVPad1() + pkt += raw(tlv) elif tmp_mod >= 2: # Add the padding extension tmp_pad = b"\x00" * (tmp_mod - 2) - tlv = IPv6ExtHdrSegmentRoutingTLVPadding(padding=tmp_pad) + tlv = IPv6ExtHdrSegmentRoutingTLVPadN(padding=tmp_pad) pkt += raw(tlv) tmp_len = (len(pkt) - 8) // 8 @@ -1023,6 +1158,12 @@ class IPv6ExtHdrFragment(_IPv6ExtHdr): IntField("id", None)] overload_fields = {IPv6: {"nh": 44}} + def guess_payload_class(self, p): + if self.offset > 0: + return Raw + else: + return super(IPv6ExtHdrFragment, self).guess_payload_class(p) + def defragment6(packets): """ @@ -1053,47 +1194,65 @@ def defragment6(packets): min_pos = 0 min_offset = cur_offset res.append(lst[min_pos]) - del(lst[min_pos]) + del lst[min_pos] # regenerate the fragmentable part fragmentable = b"" + frag_hdr_len = 8 for p in res: q = p[IPv6ExtHdrFragment] offset = 8 * q.offset if offset != len(fragmentable): warning("Expected an offset of %d. Found %d. Padding with XXXX" % (len(fragmentable), offset)) # noqa: E501 + frag_data_len = p[IPv6].plen + if frag_data_len is not None: + frag_data_len -= frag_hdr_len fragmentable += b"X" * (offset - len(fragmentable)) - fragmentable += raw(q.payload) + fragmentable += raw(q.payload)[:frag_data_len] # Regenerate the unfragmentable part. - q = res[0] + q = res[0].copy() nh = q[IPv6ExtHdrFragment].nh q[IPv6ExtHdrFragment].underlayer.nh = nh q[IPv6ExtHdrFragment].underlayer.plen = len(fragmentable) del q[IPv6ExtHdrFragment].underlayer.payload q /= conf.raw_layer(load=fragmentable) - del(q.plen) + del q.plen - return IPv6(raw(q)) + if q[IPv6].underlayer: + q[IPv6] = IPv6(raw(q[IPv6])) + else: + q = IPv6(raw(q)) + return q def fragment6(pkt, fragSize): """ - Performs fragmentation of an IPv6 packet. Provided packet ('pkt') must - already contain an IPv6ExtHdrFragment() class. 'fragSize' argument is the - expected maximum size of fragments (MTU). The list of packets is returned. + Performs fragmentation of an IPv6 packet. 'fragSize' argument is the + expected maximum size of fragment data (MTU). The list of packets is + returned. - If packet does not contain an IPv6ExtHdrFragment class, it is returned in - result list. + If packet does not contain an IPv6ExtHdrFragment class, it is added to + first IPv6 layer found. If no IPv6 layer exists packet is returned in + result list unmodified. """ pkt = pkt.copy() if IPv6ExtHdrFragment not in pkt: - # TODO : automatically add a fragment before upper Layer - # at the moment, we do nothing and return initial packet - # as single element of a list - return [pkt] + if IPv6 not in pkt: + return [pkt] + + layer3 = pkt[IPv6] + data = layer3.payload + frag = IPv6ExtHdrFragment(nh=layer3.nh) + + layer3.remove_payload() + del layer3.nh + del layer3.plen + + frag.add_payload(data) + layer3.add_payload(frag) # If the payload is bigger than 65535, a Jumbo payload must be used, as # an IPv6 packet can't be bigger than 65535 bytes. @@ -1203,6 +1362,8 @@ def fragment6(pkt, fragSize): 151: "ICMPv6MRD_Advertisement", 152: "ICMPv6MRD_Solicitation", 153: "ICMPv6MRD_Termination", + # 154: Do Me - FMIPv6 Messages - RFC 5568 + 155: "ICMPv6RPL", # RFC 6550 } icmp6typesminhdrlen = {1: 8, @@ -1230,7 +1391,8 @@ def fragment6(pkt, fragSize): 147: 8, 151: 8, 152: 4, - 153: 4 + 153: 4, + 155: 4 } icmp6types = {1: "Destination unreachable", @@ -1264,6 +1426,7 @@ def fragment6(pkt, fragSize): 151: "Multicast Router Advertisement", 152: "Multicast Router Solicitation", 153: "Multicast Router Termination", + 155: "RPL Control Message", 200: "Private Experimentation", 201: "Private Experimentation"} @@ -1321,7 +1484,10 @@ class ICMPv6DestUnreach(_ICMPv6Error): 4: "Port unreachable"}), XShortField("cksum", None), ByteField("length", 0), - X3BytesField("unused", 0)] + X3BytesField("unused", 0), + _ICMPExtensionPadField(), + _ICMPExtensionField()] + post_dissection = _ICMP_extpad_post_dissection class ICMPv6PacketTooBig(_ICMPv6Error): @@ -1339,7 +1505,11 @@ class ICMPv6TimeExceeded(_ICMPv6Error): 1: "fragment reassembly time exceeded"}), # noqa: E501 XShortField("cksum", None), ByteField("length", 0), - X3BytesField("unused", 0)] + X3BytesField("unused", 0), + _ICMPExtensionPadField(), + _ICMPExtensionField()] + post_dissection = _ICMP_extpad_post_dissection + # The default pointer value is set to the next header field of # the encapsulated IPv6 packet @@ -1627,7 +1797,9 @@ def extract_padding(self, s): 24: "ICMPv6NDOptRouteInfo", 25: "ICMPv6NDOptRDNSS", 26: "ICMPv6NDOptEFA", - 31: "ICMPv6NDOptDNSSL" + 31: "ICMPv6NDOptDNSSL", + 37: "ICMPv6NDOptCaptivePortal", + 38: "ICMPv6NDOptPREF64", } icmp6ndraprefs = {0: "Medium (default)", @@ -1641,18 +1813,41 @@ class _ICMPv6NDGuessPayload: def guess_payload_class(self, p): if len(p) > 1: - return icmp6ndoptscls.get(orb(p[0]), Raw) # s/Raw/ICMPv6NDOptUnknown/g ? # noqa: E501 + return icmp6ndoptscls.get(orb(p[0]), ICMPv6NDOptUnknown) # Beginning of ICMPv6 Neighbor Discovery Options. +class ICMPv6NDOptDataField(StrLenField): + __slots__ = ["strip_zeros"] + + def __init__(self, name, default, strip_zeros=False, **kwargs): + super().__init__(name, default, **kwargs) + self.strip_zeros = strip_zeros + + def i2len(self, pkt, x): + return len(self.i2m(pkt, x)) + + def i2m(self, pkt, x): + r = (len(x) + 2) % 8 + if r: + x += b"\x00" * (8 - r) + return x + + def m2i(self, pkt, x): + if self.strip_zeros: + x = x.rstrip(b"\x00") + return x + + class ICMPv6NDOptUnknown(_ICMPv6NDGuessPayload, Packet): name = "ICMPv6 Neighbor Discovery Option - Scapy Unimplemented" - fields_desc = [ByteField("type", None), + fields_desc = [ByteField("type", 0), FieldLenField("len", None, length_of="data", fmt="B", - adjust=lambda pkt, x: x + 2), - StrLenField("data", "", - length_from=lambda pkt: pkt.len - 2)] + adjust=lambda pkt, x: (2 + x) // 8), + ICMPv6NDOptDataField("data", "", strip_zeros=False, + length_from=lambda pkt: + 8 * max(pkt.len, 1) - 2)] # NOTE: len includes type and len field. Expressed in unit of 8 bytes # TODO: Revoir le coup du ETHER_ANY @@ -1662,7 +1857,7 @@ class ICMPv6NDOptSrcLLAddr(_ICMPv6NDGuessPayload, Packet): name = "ICMPv6 Neighbor Discovery Option - Source Link-Layer Address" fields_desc = [ByteField("type", 1), ByteField("len", 1), - MACField("lladdr", ETHER_ANY)] + SourceMACField("lladdr")] def mysummary(self): return self.sprintf("%name% %lladdr%") @@ -1677,7 +1872,7 @@ class ICMPv6NDOptPrefixInfo(_ICMPv6NDGuessPayload, Packet): name = "ICMPv6 Neighbor Discovery Option - Prefix Information" fields_desc = [ByteField("type", 3), ByteField("len", 4), - ByteField("prefixlen", None), + ByteField("prefixlen", 64), BitField("L", 1, 1), BitField("A", 1, 1), BitField("R", 0, 1), @@ -1697,44 +1892,22 @@ def mysummary(self): class TruncPktLenField(PacketLenField): - __slots__ = ["cur_shift"] - - def __init__(self, name, default, cls, cur_shift, length_from=None, shift=0): # noqa: E501 - PacketLenField.__init__(self, name, default, cls, length_from=length_from) # noqa: E501 - self.cur_shift = cur_shift - - def getfield(self, pkt, s): - tmp_len = self.length_from(pkt) - i = self.m2i(pkt, s[:tmp_len]) - return s[tmp_len:], i - - def m2i(self, pkt, m): - s = None - try: # It can happen we have sth shorter than 40 bytes - s = self.cls(m) - except Exception: - return conf.raw_layer(m) - return s - def i2m(self, pkt, x): - s = raw(x) + s = bytes(x) tmp_len = len(s) - r = (tmp_len + self.cur_shift) % 8 - tmp_len = tmp_len - r - return s[:tmp_len] + return s[:tmp_len - (tmp_len % 8)] def i2len(self, pkt, i): return len(self.i2m(pkt, i)) -# Faire un post_build pour le recalcul de la taille (en multiple de 8 octets) class ICMPv6NDOptRedirectedHdr(_ICMPv6NDGuessPayload, Packet): name = "ICMPv6 Neighbor Discovery Option - Redirected Header" fields_desc = [ByteField("type", 4), FieldLenField("len", None, length_of="pkt", fmt="B", - adjust=lambda pkt, x:(x + 8) // 8), - StrFixedLenField("res", b"\x00" * 6, 6), - TruncPktLenField("pkt", b"", IPv6, 8, + adjust=lambda pkt, x: (x + 8) // 8), + MayEnd(StrFixedLenField("res", b"\x00" * 6, 6)), + TruncPktLenField("pkt", b"", IPv6, length_from=lambda pkt: 8 * pkt.len - 8)] # See which value should be used for default MTU instead of 1280 @@ -1905,7 +2078,7 @@ class ICMPv6NDOptRDNSS(_ICMPv6NDGuessPayload, Packet): # RFC 5006 length_from=lambda pkt: 8 * (pkt.len - 1))] def mysummary(self): - return self.sprintf("%name% " + ", ".join(self.dns)) + return self.sprintf("%name% ") + ", ".join(self.dns) class ICMPv6NDOptEFA(_ICMPv6NDGuessPayload, Packet): # RFC 5175 (prev. 5075) @@ -1925,13 +2098,18 @@ class DomainNameListField(StrLenField): islist = 1 padded_unit = 8 - def __init__(self, name, default, fld=None, length_from=None, padded=False): # noqa: E501 + def __init__(self, name, default, length_from=None, padded=False): # noqa: E501 self.padded = padded - StrLenField.__init__(self, name, default, fld, length_from) + StrLenField.__init__(self, name, default, length_from=length_from) def i2len(self, pkt, x): return len(self.i2m(pkt, x)) + def i2h(self, pkt, x): + if not x: + return [] + return x + def m2i(self, pkt, x): x = plain_str(x) # Decode bytes to string res = [] @@ -1982,7 +2160,42 @@ class ICMPv6NDOptDNSSL(_ICMPv6NDGuessPayload, Packet): # RFC 6106 ] def mysummary(self): - return self.sprintf("%name% " + ", ".join(self.searchlist)) + return self.sprintf("%name% ") + ", ".join(self.searchlist) + + +class ICMPv6NDOptCaptivePortal(_ICMPv6NDGuessPayload, Packet): # RFC 8910 + name = "ICMPv6 Neighbor Discovery Option - Captive-Portal Option" + fields_desc = [ByteField("type", 37), + FieldLenField("len", None, length_of="URI", fmt="B", + adjust=lambda pkt, x: (2 + x) // 8), + ICMPv6NDOptDataField("URI", "", strip_zeros=True, + length_from=lambda pkt: + 8 * max(pkt.len, 1) - 2)] + + def mysummary(self): + return self.sprintf("%name% %URI%") + + +class _PREF64(IP6Field): + def addfield(self, pkt, s, val): + return s + self.i2m(pkt, val)[:12] + + def getfield(self, pkt, s): + return s[12:], self.m2i(pkt, s[:12] + b"\x00" * 4) + + +class ICMPv6NDOptPREF64(_ICMPv6NDGuessPayload, Packet): # RFC 8781 + name = "ICMPv6 Neighbor Discovery Option - PREF64 Option" + fields_desc = [ByteField("type", 38), + ByteField("len", 2), + BitField("scaledlifetime", 0, 13), + BitEnumField("plc", 0, 3, + ["/96", "/64", "/56", "/48", "/40", "/32"]), + _PREF64("prefix", "::")] + + def mysummary(self): + plc = self.sprintf("%plc%") if self.plc < 6 else f"[invalid PLC({self.plc})]" + return self.sprintf("%name% %prefix%") + plc # End of ICMPv6 Neighbor Discovery Options. @@ -2442,7 +2655,7 @@ def h2i(self, pkt, x): x = [x] if isinstance(x, list): x = [val.encode() if isinstance(val, str) else val for val in x] # noqa: E501 - if x and isinstance(x[0], six.integer_types): + if x and isinstance(x[0], int): ttl = x[0] names = x[1:] else: @@ -2460,7 +2673,7 @@ def fixvalue(x): if not isinstance(x, tuple): x = (0, x) # Decode bytes - if six.PY3 and isinstance(x[1], bytes): + if isinstance(x[1], bytes): x = (x[0], x[1].decode()) return x @@ -2601,6 +2814,32 @@ def _niquery_guesser(p): return cls +############################################################################# +############################################################################# +# Routing Protocol for Low Power and Lossy Networks RPL (RFC 6550) # +############################################################################# +############################################################################# + +# https://www.iana.org/assignments/rpl/rpl.xhtml#control-codes +rplcodes = {0: "DIS", + 1: "DIO", + 2: "DAO", + 3: "DAO-ACK", + # 4: "P2P-DRO", + # 5: "P2P-DRO-ACK", + # 6: "Measurement", + 7: "DCO", + 8: "DCO-ACK"} + + +class ICMPv6RPL(_ICMPv6): # RFC 6550 + name = 'RPL' + fields_desc = [ByteEnumField("type", 155, icmp6types), + ByteEnumField("code", 0, rplcodes), + XShortField("cksum", None)] + overload_fields = {IPv6: {"nh": 58, "dst": "ff02::1a"}} + + ############################################################################# ############################################################################# # Mobile IPv6 (RFC 3775) and Nemo (RFC 3963) # @@ -3006,7 +3245,7 @@ class MIP6MH_BRR(_MobilityHeader): ShortField("res2", None), _PhantomAutoPadField("autopad", 1), # autopad activated by default # noqa: E501 _OptionsField("options", [], MIP6OptUnknown, 8, - length_from=lambda pkt: 8 * pkt.len)] + length_from=lambda pkt: 8 * pkt.len)] overload_fields = {IPv6: {"nh": 135}} def hashret(self): @@ -3027,7 +3266,7 @@ class MIP6MH_HoTI(_MobilityHeader): StrFixedLenField("cookie", b"\x00" * 8, 8), _PhantomAutoPadField("autopad", 1), # autopad activated by default # noqa: E501 _OptionsField("options", [], MIP6OptUnknown, 16, - length_from=lambda pkt: 8 * (pkt.len - 1))] # noqa: E501 + length_from=lambda pkt: 8 * (pkt.len - 1))] overload_fields = {IPv6: {"nh": 135}} def hashret(self): @@ -3054,7 +3293,7 @@ class MIP6MH_HoT(_MobilityHeader): StrFixedLenField("token", b"\x00" * 8, 8), _PhantomAutoPadField("autopad", 1), # autopad activated by default # noqa: E501 _OptionsField("options", [], MIP6OptUnknown, 24, - length_from=lambda pkt: 8 * (pkt.len - 2))] # noqa: E501 + length_from=lambda pkt: 8 * (pkt.len - 2))] overload_fields = {IPv6: {"nh": 135}} def hashret(self): @@ -3099,7 +3338,7 @@ class MIP6MH_BU(_MobilityHeader): LifetimeField("mhtime", 3), # unit == 4 seconds _PhantomAutoPadField("autopad", 1), # autopad activated by default # noqa: E501 _OptionsField("options", [], MIP6OptUnknown, 12, - length_from=lambda pkt: 8 * pkt.len - 4)] # noqa: E501 + length_from=lambda pkt: 8 * pkt.len - 4)] overload_fields = {IPv6: {"nh": 135}} def hashret(self): # Hack: see comment in MIP6MH_BRR.hashret() @@ -3125,7 +3364,7 @@ class MIP6MH_BA(_MobilityHeader): XShortField("mhtime", 0), # unit == 4 seconds _PhantomAutoPadField("autopad", 1), # autopad activated by default # noqa: E501 _OptionsField("options", [], MIP6OptUnknown, 12, - length_from=lambda pkt: 8 * pkt.len - 4)] # noqa: E501 + length_from=lambda pkt: 8 * pkt.len - 4)] overload_fields = {IPv6: {"nh": 135}} def hashret(self): # Hack: see comment in MIP6MH_BRR.hashret() @@ -3158,7 +3397,7 @@ class MIP6MH_BE(_MobilityHeader): ByteField("reserved", 0), IP6Field("ha", "::"), _OptionsField("options", [], MIP6OptUnknown, 24, - length_from=lambda pkt: 8 * (pkt.len - 2))] # noqa: E501 + length_from=lambda pkt: 8 * (pkt.len - 2))] overload_fields = {IPv6: {"nh": 135}} @@ -3233,14 +3472,14 @@ def get_trace(self): trace[d][s[IPv6].hlim] = r[IPv6].src, t - for k in six.itervalues(trace): + for k in trace.values(): try: - m = min(x for x, y in six.iteritems(k) if y[1]) + m = min(x for x, y in k.items() if y[1]) except ValueError: continue - for l in list(k): # use list(): k is modified in the loop - if l > m: - del k[l] + for li in list(k): # use list(): k is modified in the loop + if li > m: + del k[li] return trace @@ -3267,7 +3506,7 @@ def traceroute6(target, dport=80, minttl=1, maxttl=30, sport=RandShort(), a = TracerouteResult6(a.res) if verbose: - a.display() + a.show() return a, b @@ -3278,12 +3517,15 @@ def traceroute6(target, dport=80, minttl=1, maxttl=30, sport=RandShort(), ############################################################################# -class L3RawSocket6(L3RawSocket): - def __init__(self, type=ETH_P_IPV6, filter=None, iface=None, promisc=None, nofilter=0): # noqa: E501 - L3RawSocket.__init__(self, type, filter, iface, promisc) - # NOTE: if fragmentation is needed, it will be done by the kernel (RFC 2292) # noqa: E501 - self.outs = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_RAW) # noqa: E501 - self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type)) # noqa: E501 +if not WINDOWS: + from scapy.supersocket import L3RawSocket + + class L3RawSocket6(L3RawSocket): + def __init__(self, type=ETH_P_IPV6, filter=None, iface=None, promisc=None, nofilter=0): # noqa: E501 + # NOTE: if fragmentation is needed, it will be done by the kernel (RFC 2292) # noqa: E501 + self.outs = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_RAW) # noqa: E501 + self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type)) # noqa: E501 + self.iface = iface def IPv6inIP(dst='203.178.135.36', src=None): @@ -3292,7 +3534,7 @@ def IPv6inIP(dst='203.178.135.36', src=None): if not conf.L3socket == _IPv6inIP: _IPv6inIP.cls = conf.L3socket else: - del(conf.L3socket) + del conf.L3socket return _IPv6inIP @@ -3635,7 +3877,7 @@ def reply_callback(req, reply_mac, router, iface): reply_mac = get_if_hwaddr(iface) sniff_filter = "icmp6 and not ether src %s" % reply_mac - router = (router and 1) or 0 # Value of the R flags in NA + router = 1 if router else 0 # Value of the R flags in NA sniff(store=0, filter=sniff_filter, @@ -3845,7 +4087,7 @@ def ra_reply_callback(req, reply_mac, tgt_mac, iface): while ICMPv6NDOptPrefixInfo in tmp: pio = tmp[ICMPv6NDOptPrefixInfo] tmp = pio.payload - del(pio.payload) + del pio.payload rep /= pio # ... and source link layer address option @@ -3978,16 +4220,24 @@ def _load_dict(d): ############################################################################# conf.l3types.register(ETH_P_IPV6, IPv6) +conf.l3types.register_num2layer(ETH_P_ALL, IPv46) conf.l2types.register(31, IPv6) conf.l2types.register(DLT_IPV6, IPv6) -conf.l2types.register(DLT_RAW, _IPv46) -conf.l2types.register_num2layer(DLT_RAW_ALT, _IPv46) +conf.l2types.register(DLT_RAW, IPv46) +conf.l2types.register_num2layer(DLT_RAW_ALT, IPv46) +if OPENBSD: + conf.l2types.register_num2layer(229, IPv6) bind_layers(Ether, IPv6, type=0x86dd) bind_layers(CookedLinux, IPv6, proto=0x86dd) bind_layers(GRE, IPv6, proto=0x86dd) bind_layers(SNAP, IPv6, code=0x86dd) -bind_layers(Loopback, IPv6, type=socket.AF_INET6) +# AF_INET6 values are platform-dependent. For a detailed explaination, read +# https://github.com/the-tcpdump-group/libpcap/blob/f98637ad7f086a34c4027339c9639ae1ef842df3/gencode.c#L3333-L3354 # noqa: E501 +if WINDOWS: + bind_layers(Loopback, IPv6, type=0x18) +else: + bind_layers(Loopback, IPv6, type=socket.AF_INET6) bind_layers(IPerror6, TCPerror, nh=socket.IPPROTO_TCP) bind_layers(IPerror6, UDPerror, nh=socket.IPPROTO_UDP) bind_layers(IPv6, TCP, nh=socket.IPPROTO_TCP) diff --git a/scapy/layers/ipsec.py b/scapy/layers/ipsec.py index 369d6d99795..8cff919102a 100644 --- a/scapy/layers/ipsec.py +++ b/scapy/layers/ipsec.py @@ -1,17 +1,8 @@ -############################################################################# -# ipsec.py --- IPsec support for Scapy # -# # -# Copyright (C) 2014 6WIND # -# # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License version 2 as # -# published by the Free Software Foundation. # -# # -# This program is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -############################################################################# +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2014 6WIND + r""" IPsec layer =========== @@ -19,7 +10,7 @@ Example of use: >>> sa = SecurityAssociation(ESP, spi=0xdeadbeef, crypt_algo='AES-CBC', -... crypt_key='sixteenbytes key') +... crypt_key=b'sixteenbytes key') >>> p = IP(src='1.1.1.1', dst='2.2.2.2') >>> p /= TCP(sport=45012, dport=80) >>> p /= Raw('testdata') @@ -39,7 +30,6 @@ True """ -from __future__ import absolute_import try: from math import gcd except ImportError: @@ -47,17 +37,32 @@ import os import socket import struct +import warnings from scapy.config import conf, crypto_validator from scapy.compat import orb, raw from scapy.data import IP_PROTOS from scapy.error import log_loading -from scapy.fields import ByteEnumField, ByteField, IntField, PacketField, \ - ShortField, StrField, XIntField, XStrField, XStrLenField -from scapy.packet import Packet, bind_layers, Raw +from scapy.fields import ( + ByteEnumField, + ByteField, + IntField, + PacketField, + ShortField, + StrField, + XByteField, + XIntField, + XStrField, + XStrLenField, +) +from scapy.packet import ( + Packet, + Raw, + bind_bottom_up, + bind_layers, + bind_top_down, +) from scapy.layers.inet import IP, UDP -import scapy.modules.six as six -from scapy.modules.six.moves import range from scapy.layers.inet6 import IPv6, IPv6ExtHdrHopByHop, IPv6ExtHdrDestOpt, \ IPv6ExtHdrRouting @@ -87,7 +92,7 @@ def __get_icv_len(self): ByteEnumField('nh', None, IP_PROTOS), ByteField('payloadlen', None), ShortField('reserved', None), - XIntField('spi', 0x0), + XIntField('spi', 0x00000001), IntField('seq', 0), XStrLenField('icv', None, length_from=__get_icv_len), # Padding len can only be known with the SecurityAssociation.auth_algo @@ -120,11 +125,22 @@ class ESP(Packet): name = 'ESP' fields_desc = [ - XIntField('spi', 0x0), + XIntField('spi', 0x00000001), IntField('seq', 0), XStrField('data', None), ] + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + if len(_pkt) >= 4 and struct.unpack("!I", _pkt[0:4])[0] == 0x00: + return NON_ESP + elif len(_pkt) == 1 and struct.unpack("!B", _pkt)[0] == 0xff: + return NAT_KEEPALIVE + else: + return ESP + return cls + overload_fields = { IP: {'proto': socket.IPPROTO_ESP}, IPv6: {'nh': socket.IPPROTO_ESP}, @@ -134,10 +150,27 @@ class ESP(Packet): } +class NON_ESP(Packet): # RFC 3948, section 2.2 + fields_desc = [ + XIntField("non_esp", 0x0) + ] + + +class NAT_KEEPALIVE(Packet): # RFC 3948, section 2.2 + fields_desc = [ + XByteField("nat_keepalive", 0xFF) + ] + + bind_layers(IP, ESP, proto=socket.IPPROTO_ESP) bind_layers(IPv6, ESP, nh=socket.IPPROTO_ESP) -bind_layers(UDP, ESP, dport=4500) # NAT-Traversal encapsulation -bind_layers(UDP, ESP, sport=4500) # NAT-Traversal encapsulation + +# NAT-Traversal encapsulation +bind_bottom_up(UDP, ESP, dport=4500) +bind_bottom_up(UDP, ESP, sport=4500) +bind_top_down(UDP, ESP, dport=4500, sport=4500) +bind_top_down(UDP, NON_ESP, dport=4500, sport=4500) +bind_top_down(UDP, NAT_KEEPALIVE, dport=4500, sport=4500) ############################################################################### @@ -170,16 +203,29 @@ def data_for_encryption(self): from cryptography.exceptions import InvalidTag from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import ( + aead, Cipher, algorithms, modes, ) + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms + ) + except ImportError: + decrepit_algorithms = algorithms + + # cryptography's TripleDES can be used to simulate DES behavior + DES = lambda key: decrepit_algorithms.TripleDES(key * 3) + DES.key_sizes = decrepit_algorithms.TripleDES.key_sizes + DES.block_size = decrepit_algorithms.TripleDES.block_size else: - log_loading.info("Can't import python-cryptography v1.7+. " + log_loading.info("Can't import python-cryptography v2.0+. " "Disabled IPsec encryption/authentication.") default_backend = None InvalidTag = Exception - Cipher = algorithms = modes = None + Cipher = algorithms = modes = DES = None ############################################################################### @@ -225,11 +271,18 @@ def __init__(self, name, cipher, mode, block_size=None, iv_size=None, self.mode = mode self.icv_size = icv_size - if modes and self.mode is not None: - self.is_aead = issubclass(self.mode, - modes.ModeWithAuthenticationTag) - else: - self.is_aead = False + self.is_aead = False + # If using cryptography.hazmat.primitives.cipher.aead + self.ciphers_aead_api = False + + if modes: + if self.mode is not None: + self.is_aead = issubclass(self.mode, + modes.ModeWithAuthenticationTag) + elif self.cipher in (aead.AESGCM, aead.AESCCM, + aead.ChaCha20Poly1305): + self.is_aead = True + self.ciphers_aead_api = True if block_size is not None: self.block_size = block_size @@ -339,36 +392,53 @@ def pad(self, esp): return esp - def encrypt(self, sa, esp, key, esn_en=False, esn=0): + def encrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): """ Encrypt an ESP packet :param sa: the SecurityAssociation associated with the ESP packet. :param esp: an unencrypted _ESPPlain packet with valid padding :param key: the secret key used for encryption + :param icv_size: the length of the icv used for integrity check :esn_en: extended sequence number enable which allows to use 64-bit sequence number instead of 32-bit when using an AEAD algorithm :esn: extended sequence number (32 MSB) :return: a valid ESP packet encrypted with this algorithm """ + if icv_size is None: + icv_size = self.icv_size if self.is_aead else 0 data = esp.data_for_encryption() if self.cipher: mode_iv = self._format_mode_iv(algo=self, sa=sa, iv=esp.iv) - cipher = self.new_cipher(key, mode_iv) - encryptor = cipher.encryptor() - + aad = None if self.is_aead: if esn_en: aad = struct.pack('!LLL', esp.spi, esn, esp.seq) else: aad = struct.pack('!LL', esp.spi, esp.seq) - encryptor.authenticate_additional_data(aad) - data = encryptor.update(data) + encryptor.finalize() - data += encryptor.tag[:self.icv_size] + if self.ciphers_aead_api: + # New API + if self.cipher == aead.AESCCM: + cipher = self.cipher(key, tag_length=icv_size) + else: + cipher = self.cipher(key) + if self.name == 'AES-NULL-GMAC': + # Special case for GMAC (rfc 4543 sect 3) + data = data + cipher.encrypt(mode_iv, b"", aad + esp.iv + data) + else: + data = cipher.encrypt(mode_iv, data, aad) else: - data = encryptor.update(data) + encryptor.finalize() + cipher = self.new_cipher(key, mode_iv) + encryptor = cipher.encryptor() + + if self.is_aead: + encryptor.authenticate_additional_data(aad) + data = encryptor.update(data) + encryptor.finalize() + data += encryptor.tag[:icv_size] + else: + data = encryptor.update(data) + encryptor.finalize() return ESP(spi=esp.spi, seq=esp.seq, data=esp.iv + data) @@ -397,29 +467,45 @@ def decrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): if self.cipher: mode_iv = self._format_mode_iv(sa=sa, iv=iv) - cipher = self.new_cipher(key, mode_iv, icv) - decryptor = cipher.decryptor() - + aad = None if self.is_aead: - # Tag value check is done during the finalize method if esn_en: - decryptor.authenticate_additional_data( - struct.pack('!LLL', esp.spi, esn, esp.seq)) + aad = struct.pack('!LLL', esp.spi, esn, esp.seq) + else: + aad = struct.pack('!LL', esp.spi, esp.seq) + if self.ciphers_aead_api: + # New API + if self.cipher == aead.AESCCM: + cipher = self.cipher(key, tag_length=icv_size) else: - decryptor.authenticate_additional_data( - struct.pack('!LL', esp.spi, esp.seq)) - try: - data = decryptor.update(data) + decryptor.finalize() - except InvalidTag as err: - raise IPSecIntegrityError(err) + cipher = self.cipher(key) + try: + if self.name == 'AES-NULL-GMAC': + # Special case for GMAC (rfc 4543 sect 3) + data = data + cipher.decrypt(mode_iv, icv, aad + iv + data) + else: + data = cipher.decrypt(mode_iv, data + icv, aad) + except InvalidTag as err: + raise IPSecIntegrityError(err) + else: + cipher = self.new_cipher(key, mode_iv, icv) + decryptor = cipher.decryptor() + + if self.is_aead: + # Tag value check is done during the finalize method + decryptor.authenticate_additional_data(aad) + try: + data = decryptor.update(data) + decryptor.finalize() + except InvalidTag as err: + raise IPSecIntegrityError(err) # extract padlen and nh padlen = orb(data[-2]) nh = orb(data[-1]) # then use padlen to determine data and padding - data = data[:len(data) - padlen - 2] padding = data[len(data) - padlen - 2: len(data) - 2] + data = data[:len(data) - padlen - 2] return _ESPPlain(spi=esp.spi, seq=esp.seq, @@ -447,43 +533,80 @@ def decrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): CRYPT_ALGOS['AES-CTR'] = CryptAlgo('AES-CTR', cipher=algorithms.AES, mode=modes.CTR, + block_size=1, iv_size=8, salt_size=4, format_mode_iv=_aes_ctr_format_mode_iv) _salt_format_mode_iv = lambda sa, iv, **kw: sa.crypt_salt + iv CRYPT_ALGOS['AES-GCM'] = CryptAlgo('AES-GCM', - cipher=algorithms.AES, - mode=modes.GCM, + cipher=aead.AESGCM, + key_size=(16, 24, 32), + mode=None, salt_size=4, + block_size=1, iv_size=8, icv_size=16, format_mode_iv=_salt_format_mode_iv) - if hasattr(modes, 'CCM'): - CRYPT_ALGOS['AES-CCM'] = CryptAlgo('AES-CCM', - cipher=algorithms.AES, - mode=modes.CCM, - iv_size=8, - salt_size=3, - icv_size=16, - format_mode_iv=_salt_format_mode_iv) - # XXX: Flagged as weak by 'cryptography'. Kept for backward compatibility - CRYPT_ALGOS['Blowfish'] = CryptAlgo('Blowfish', - cipher=algorithms.Blowfish, - mode=modes.CBC) - # XXX: RFC7321 states that DES *MUST NOT* be implemented. - # XXX: Keep for backward compatibility? + # GMAC: rfc 4543, "companion to the AES Galois/Counter Mode ESP" + # This is defined as a crypt_algo by rfc, but has the role of an auth_algo + CRYPT_ALGOS['AES-NULL-GMAC'] = CryptAlgo('AES-NULL-GMAC', + cipher=aead.AESGCM, + key_size=(16, 24, 32), + mode=None, + salt_size=4, + block_size=1, + iv_size=8, + icv_size=16, + format_mode_iv=_salt_format_mode_iv) + CRYPT_ALGOS['AES-CCM'] = CryptAlgo('AES-CCM', + cipher=aead.AESCCM, + mode=None, + key_size=(16, 24, 32), + block_size=1, + iv_size=8, + salt_size=3, + icv_size=16, + format_mode_iv=_salt_format_mode_iv) + CRYPT_ALGOS['CHACHA20-POLY1305'] = CryptAlgo('CHACHA20-POLY1305', + cipher=aead.ChaCha20Poly1305, + mode=None, + key_size=32, + block_size=1, + iv_size=8, + salt_size=4, + icv_size=16, + format_mode_iv=_salt_format_mode_iv) # noqa: E501 + # Using a TripleDES cipher algorithm for DES is done by using the same 64 - # bits key 3 times (done by cryptography when given a 64 bits key) + # bits key 3 times CRYPT_ALGOS['DES'] = CryptAlgo('DES', - cipher=algorithms.TripleDES, + cipher=DES, mode=modes.CBC, key_size=(8,)) CRYPT_ALGOS['3DES'] = CryptAlgo('3DES', - cipher=algorithms.TripleDES, - mode=modes.CBC) - CRYPT_ALGOS['CAST'] = CryptAlgo('CAST', - cipher=algorithms.CAST5, + cipher=decrepit_algorithms.TripleDES, mode=modes.CBC) + if decrepit_algorithms is algorithms: + # cryptography < 43 raises a DeprecationWarning + from cryptography.utils import CryptographyDeprecationWarning + with warnings.catch_warnings(): + # Hide deprecation warnings + warnings.filterwarnings("ignore", + category=CryptographyDeprecationWarning) + CRYPT_ALGOS['CAST'] = CryptAlgo('CAST', + cipher=decrepit_algorithms.CAST5, + mode=modes.CBC) + CRYPT_ALGOS['Blowfish'] = CryptAlgo('Blowfish', + cipher=decrepit_algorithms.Blowfish, + mode=modes.CBC) + else: + CRYPT_ALGOS['CAST'] = CryptAlgo('CAST', + cipher=decrepit_algorithms.CAST5, + mode=modes.CBC) + CRYPT_ALGOS['Blowfish'] = CryptAlgo('Blowfish', + cipher=decrepit_algorithms.Blowfish, + mode=modes.CBC) + ############################################################################### if conf.crypto_valid: @@ -564,16 +687,17 @@ def sign(self, pkt, key, esn_en=False, esn=0): mac = self.new_mac(key) if pkt.haslayer(ESP): - mac.update(raw(pkt[ESP])) + mac.update(bytes(pkt[ESP])) + if esn_en: + # RFC4303 sect 2.2.1 + mac.update(struct.pack('!L', esn)) pkt[ESP].data += mac.finalize()[:self.icv_size] elif pkt.haslayer(AH): - clone = zero_mutable_fields(pkt.copy(), sending=True) + mac.update(bytes(zero_mutable_fields(pkt.copy(), sending=True))) if esn_en: - temp = raw(clone) + struct.pack('!L', esn) - else: - temp = raw(clone) - mac.update(temp) + # RFC4302 sect 2.5.1 + mac.update(struct.pack('!L', esn)) pkt[AH].icv = mac.finalize()[:self.icv_size] return pkt @@ -602,7 +726,10 @@ def verify(self, pkt, key, esn_en=False, esn=0): pkt_icv = pkt.data[len(pkt.data) - self.icv_size:] clone = pkt.copy() clone.data = clone.data[:len(clone.data) - self.icv_size] - temp = raw(clone) + mac.update(bytes(clone)) + if esn_en: + # RFC4303 sect 2.2.1 + mac.update(struct.pack('!L', esn)) elif pkt.haslayer(AH): if len(pkt[AH].icv) != self.icv_size: @@ -611,12 +738,11 @@ def verify(self, pkt, key, esn_en=False, esn=0): pkt[AH].icv = pkt[AH].icv[:self.icv_size] pkt_icv = pkt[AH].icv clone = zero_mutable_fields(pkt.copy(), sending=False) + mac.update(bytes(clone)) if esn_en: - temp = raw(clone) + struct.pack('!L', esn) - else: - temp = raw(clone) + # RFC4302 sect 2.5.1 + mac.update(struct.pack('!L', esn)) - mac.update(temp) computed_icv = mac.finalize()[:self.icv_size] # XXX: Cannot use mac.verify because the ICV can be truncated @@ -808,7 +934,9 @@ class SecurityAssociation(object): SUPPORTED_PROTOS = (IP, IPv6) def __init__(self, proto, spi, seq_num=1, crypt_algo=None, crypt_key=None, - auth_algo=None, auth_key=None, tunnel_header=None, nat_t_header=None, esn_en=False, esn=0): # noqa: E501 + crypt_icv_size=None, + auth_algo=None, auth_key=None, + tunnel_header=None, nat_t_header=None, esn_en=False, esn=0): """ :param proto: the IPsec proto to use (ESP or AH) :param spi: the Security Parameters Index of this SA @@ -816,6 +944,8 @@ def __init__(self, proto, spi, seq_num=1, crypt_algo=None, crypt_key=None, packets :param crypt_algo: the encryption algorithm name (only used with ESP) :param crypt_key: the encryption key (only used with ESP) + :param crypt_icv_size: change the default size of the crypt_algo + (only used with ESP) :param auth_algo: the integrity algorithm name :param auth_key: the integrity key :param tunnel_header: an instance of a IP(v6) header that will be used @@ -828,10 +958,10 @@ def __init__(self, proto, spi, seq_num=1, crypt_algo=None, crypt_key=None, :param esn: extended sequence number (32 MSB) """ - if proto not in (ESP, AH, ESP.name, AH.name): + if proto not in {ESP, AH, ESP.name, AH.name}: raise ValueError("proto must be either ESP or AH") - if isinstance(proto, six.string_types): - self.proto = eval(proto) + if isinstance(proto, str): + self.proto = {ESP.name: ESP, AH.name: AH}[proto] else: self.proto = proto @@ -857,6 +987,8 @@ def __init__(self, proto, spi, seq_num=1, crypt_algo=None, crypt_key=None, else: self.crypt_algo = CRYPT_ALGOS['NULL'] self.crypt_key = None + self.crypt_salt = None + self.crypt_icv_size = crypt_icv_size if auth_algo: if auth_algo not in AUTH_ALGOS: @@ -913,10 +1045,14 @@ def _encrypt_esp(self, pkt, seq_num=None, iv=None, esn_en=None, esn=None): esp = self.crypt_algo.pad(esp) esp = self.crypt_algo.encrypt(self, esp, self.crypt_key, + self.crypt_icv_size, esn_en=esn_en or self.esn_en, esn=esn or self.esn) - self.auth_algo.sign(esp, self.auth_key) + self.auth_algo.sign(esp, + self.auth_key, + esn_en=esn_en or self.esn_en, + esn=esn or self.esn) if self.nat_t_header: nat_t_header = self.nat_t_header.copy() @@ -929,17 +1065,16 @@ def _encrypt_esp(self, pkt, seq_num=None, iv=None, esn_en=None, esn=None): ip_header /= nat_t_header if ip_header.version == 4: - ip_header.len = len(ip_header) + len(esp) + del ip_header.len del ip_header.chksum - ip_header = ip_header.__class__(raw(ip_header)) else: - ip_header.plen = len(ip_header.payload) + len(esp) + del ip_header.plen # sequence number must always change, unless specified by the user if seq_num is None: self.seq_num += 1 - return ip_header / esp + return ip_header.__class__(raw(ip_header / esp)) def _encrypt_ah(self, pkt, seq_num=None, esn_en=False, esn=0): @@ -1028,9 +1163,12 @@ def _decrypt_esp(self, pkt, verify=True, esn_en=None, esn=None): if verify: self.check_spi(pkt) - self.auth_algo.verify(encrypted, self.auth_key) + self.auth_algo.verify(encrypted, self.auth_key, + esn_en=esn_en or self.esn_en, + esn=esn or self.esn) esp = self.crypt_algo.decrypt(self, encrypted, self.crypt_key, + self.crypt_icv_size or self.crypt_algo.icv_size or self.auth_algo.icv_size, esn_en=esn_en or self.esn_en, @@ -1058,8 +1196,13 @@ def _decrypt_esp(self, pkt, verify=True, esn_en=None, esn=None): # recompute checksum ip_header = ip_header.__class__(raw(ip_header)) else: - encrypted.underlayer.nh = esp.nh - encrypted.underlayer.remove_payload() + if self.nat_t_header: + # drop the UDP header and return the payload untouched + ip_header.nh = esp.nh + ip_header.remove_payload() + else: + encrypted.underlayer.nh = esp.nh + encrypted.underlayer.remove_payload() ip_header.plen = len(ip_header.payload) + len(esp.data) cls = ip_header.guess_payload_class(esp.data) diff --git a/scapy/layers/ir.py b/scapy/layers/ir.py index c25fc0852c5..0206808ddc2 100644 --- a/scapy/layers/ir.py +++ b/scapy/layers/ir.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ IrDA infrared data communication. @@ -25,19 +25,19 @@ class IrLAPHead(Packet): class IrLAPCommand(Packet): name = "IrDA Link Access Protocol Command" fields_desc = [XByteField("Control", 0), - XByteField("Format identifier", 0), - XIntField("Source address", 0), - XIntField("Destination address", 0xffffffff), - XByteField("Discovery flags", 0x1), - ByteEnumField("Slot number", 255, {"final": 255}), + XByteField("Format_identifier", 0), + XIntField("Source_address", 0), + XIntField("Destination_address", 0xffffffff), + XByteField("Discovery_flags", 0x1), + ByteEnumField("Slot_number", 255, {"final": 255}), XByteField("Version", 0)] class IrLMP(Packet): name = "IrDA Link Management Protocol" - fields_desc = [XShortField("Service hints", 0), - XByteField("Character set", 0), - StrField("Device name", "")] + fields_desc = [XShortField("Service_hints", 0), + XByteField("Character_set", 0), + StrField("Device_name", "")] bind_layers(CookedLinux, IrLAPHead, proto=23) diff --git a/scapy/layers/isakmp.py b/scapy/layers/isakmp.py index 4267e22b9b7..b909e66a2ce 100644 --- a/scapy/layers/isakmp.py +++ b/scapy/layers/isakmp.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ ISAKMP (Internet Security Association and Key Management Protocol). @@ -9,15 +9,31 @@ # Mostly based on https://tools.ietf.org/html/rfc2408 -from __future__ import absolute_import import struct from scapy.config import conf from scapy.packet import Packet, bind_bottom_up, bind_top_down, bind_layers from scapy.compat import chb -from scapy.fields import ByteEnumField, ByteField, FieldLenField, FlagsField, \ - IntEnumField, IntField, PacketLenField, ShortEnumField, ShortField, \ - StrFixedLenField, StrLenField, XByteField +from scapy.fields import ( + ByteEnumField, + ByteField, + FieldLenField, + FieldListField, + FlagsField, + IPField, + IntEnumField, + IntField, + MultipleTypeField, + PacketLenField, + ShortEnumField, + ShortField, + StrLenEnumField, + StrLenField, + XByteField, + XStrFixedLenField, + XStrLenField, +) from scapy.layers.inet import IP, UDP +from scapy.layers.ipsec import NON_ESP from scapy.sendrecv import sr from scapy.volatile import RandString from scapy.error import warning @@ -27,96 +43,133 @@ # and inherit a default ISAKMP_payload -# see http://www.iana.org/assignments/ipsec-registry for details -ISAKMPAttributeTypes = {"Encryption": (1, {"DES-CBC": 1, - "IDEA-CBC": 2, - "Blowfish-CBC": 3, - "RC5-R16-B64-CBC": 4, - "3DES-CBC": 5, - "CAST-CBC": 6, - "AES-CBC": 7, - "CAMELLIA-CBC": 8, }, 0), - "Hash": (2, {"MD5": 1, - "SHA": 2, - "Tiger": 3, - "SHA2-256": 4, - "SHA2-384": 5, - "SHA2-512": 6, }, 0), - "Authentication": (3, {"PSK": 1, - "DSS": 2, - "RSA Sig": 3, - "RSA Encryption": 4, - "RSA Encryption Revised": 5, - "ElGamal Encryption": 6, - "ElGamal Encryption Revised": 7, - "ECDSA Sig": 8, - "HybridInitRSA": 64221, - "HybridRespRSA": 64222, - "HybridInitDSS": 64223, - "HybridRespDSS": 64224, - "XAUTHInitPreShared": 65001, - "XAUTHRespPreShared": 65002, - "XAUTHInitDSS": 65003, - "XAUTHRespDSS": 65004, - "XAUTHInitRSA": 65005, - "XAUTHRespRSA": 65006, - "XAUTHInitRSAEncryption": 65007, - "XAUTHRespRSAEncryption": 65008, - "XAUTHInitRSARevisedEncryption": 65009, # noqa: E501 - "XAUTHRespRSARevisedEncryptio": 65010, }, 0), # noqa: E501 - "GroupDesc": (4, {"768MODPgr": 1, - "1024MODPgr": 2, - "EC2Ngr155": 3, - "EC2Ngr185": 4, - "1536MODPgr": 5, - "2048MODPgr": 14, - "3072MODPgr": 15, - "4096MODPgr": 16, - "6144MODPgr": 17, - "8192MODPgr": 18, }, 0), - "GroupType": (5, {"MODP": 1, - "ECP": 2, - "EC2N": 3}, 0), - "GroupPrime": (6, {}, 1), - "GroupGenerator1": (7, {}, 1), - "GroupGenerator2": (8, {}, 1), - "GroupCurveA": (9, {}, 1), - "GroupCurveB": (10, {}, 1), - "LifeType": (11, {"Seconds": 1, - "Kilobytes": 2}, 0), - "LifeDuration": (12, {}, 1), - "PRF": (13, {}, 0), - "KeyLength": (14, {}, 0), - "FieldSize": (15, {}, 0), - "GroupOrder": (16, {}, 1), - } - -# the name 'ISAKMPTransformTypes' is actually a misnomer (since the table -# holds info for all ISAKMP Attribute types, not just transforms, but we'll -# keep it for backwards compatibility... for now at least -ISAKMPTransformTypes = ISAKMPAttributeTypes - -ISAKMPTransformNum = {} -for n in ISAKMPTransformTypes: - val = ISAKMPTransformTypes[n] - tmp = {} - for e in val[1]: - tmp[val[1][e]] = e - ISAKMPTransformNum[val[0]] = (n, tmp, val[2]) -del(n) -del(e) -del(tmp) -del(val) +# see https://www.iana.org/assignments/ipsec-registry/ipsec-registry.xhtml#ipsec-registry-2 for details # noqa: E501 +ISAKMPAttributeTypes = { + "Encryption": (1, {"DES-CBC": 1, + "IDEA-CBC": 2, + "Blowfish-CBC": 3, + "RC5-R16-B64-CBC": 4, + "3DES-CBC": 5, + "CAST-CBC": 6, + "AES-CBC": 7, + "CAMELLIA-CBC": 8, }, 0), + "Hash": (2, {"MD5": 1, + "SHA": 2, + "Tiger": 3, + "SHA2-256": 4, + "SHA2-384": 5, + "SHA2-512": 6, }, 0), + "Authentication": (3, {"PSK": 1, + "DSS": 2, + "RSA Sig": 3, + "RSA Encryption": 4, + "RSA Encryption Revised": 5, + "ElGamal Encryption": 6, + "ElGamal Encryption Revised": 7, + "ECDSA Sig": 8, + "HybridInitRSA": 64221, + "HybridRespRSA": 64222, + "HybridInitDSS": 64223, + "HybridRespDSS": 64224, + "XAUTHInitPreShared": 65001, + "XAUTHRespPreShared": 65002, + "XAUTHInitDSS": 65003, + "XAUTHRespDSS": 65004, + "XAUTHInitRSA": 65005, + "XAUTHRespRSA": 65006, + "XAUTHInitRSAEncryption": 65007, + "XAUTHRespRSAEncryption": 65008, + "XAUTHInitRSARevisedEncryption": 65009, # noqa: E501 + "XAUTHRespRSARevisedEncryptio": 65010, }, 0), # noqa: E501 + "GroupDesc": (4, {"768MODPgr": 1, + "1024MODPgr": 2, + "EC2Ngr155": 3, + "EC2Ngr185": 4, + "1536MODPgr": 5, + "2048MODPgr": 14, + "3072MODPgr": 15, + "4096MODPgr": 16, + "6144MODPgr": 17, + "8192MODPgr": 18, }, 0), + "GroupType": (5, {"MODP": 1, + "ECP": 2, + "EC2N": 3}, 0), + "GroupPrime": (6, {}, 1), + "GroupGenerator1": (7, {}, 1), + "GroupGenerator2": (8, {}, 1), + "GroupCurveA": (9, {}, 1), + "GroupCurveB": (10, {}, 1), + "LifeType": (11, {"Seconds": 1, + "Kilobytes": 2}, 0), + "LifeDuration": (12, {}, 1), + "PRF": (13, {}, 0), + "KeyLength": (14, {}, 0), + "FieldSize": (15, {}, 0), + "GroupOrder": (16, {}, 1), +} + +# see https://www.iana.org/assignments/isakmp-registry/isakmp-registry.xhtml#isakmp-registry-13 for details # noqa: E501 +IPSECAttributeTypes = { + "LifeType": (1, {"Reserved": 0, + "seconds": 1, + "kilobytes": 2}, 0), + "LifeDuration": (2, {}, 1), + "GroupDesc": (3, ISAKMPAttributeTypes["GroupDesc"][1], 0), + "EncapsulationMode": (4, {"Reserved": 0, + "Tunnel": 1, + "Transport": 2, + "UDP-Encapsulated-Tunnel": 3, + "UDP-Encapsulated-Transport": 4}, 0), + "AuthenticationAlgorithm": (5, {"HMAC-MD5": 1, + "HMAC-SHA": 2, + "DES-MAC": 3, + "KPDK": 4, + "HMAC-SHA2-256": 5, + "HMAC-SHA2-384": 6, + "HMAC-SHA2-512": 7, + "HMAC-RIPEMD": 8, + "AES-XCBC-MAC": 9, + "SIG-RSA": 10, + "AES-128-GMAC": 11, + "AES-192-GMAC": 12, + "AES-256-GMAC": 13}, 0), + "KeyLength": (6, {}, 0), + "KeyRounds": (7, {}, 0), + "CompressDictionarySize": (8, {}, 0), + "CompressPrivateAlgorithm": (9, {}, 1), +} + +_rev = lambda x: { + v[0]: (k, {vv: kk for kk, vv in v[1].items()}, v[2]) + for k, v in x.items() +} +ISAKMPTransformNum = _rev(ISAKMPAttributeTypes) +IPSECTransformNum = _rev(IPSECAttributeTypes) + +# See IPSEC Security Protocol Identifiers entry in +# https://www.iana.org/assignments/isakmp-registry/isakmp-registry.xhtml#isakmp-registry-3 +PROTO_ISAKMP = 1 +PROTO_IPSEC_AH = 2 +PROTO_IPSEC_ESP = 3 +PROTO_IPCOMP = 4 +PROTO_GIGABEAM_RADIO = 5 class ISAKMPTransformSetField(StrLenField): islist = 1 @staticmethod - def type2num(type_val_tuple): + def type2num(type_val_tuple, proto=0): typ, val = type_val_tuple - type_val, enc_dict, tlv = ISAKMPTransformTypes.get(typ, (typ, {}, 0)) + if proto == PROTO_ISAKMP: + type_val, enc_dict, tlv = ISAKMPAttributeTypes.get(typ, (typ, {}, 0)) + elif proto == PROTO_IPSEC_ESP: + type_val, enc_dict, tlv = IPSECAttributeTypes.get(typ, (typ, {}, 0)) + else: + type_val, enc_dict, tlv = (typ, {}, 0) val = enc_dict.get(val, val) + if isinstance(val, str): + raise ValueError("Unknown attribute '%s'" % val) s = b"" if (val & ~0xffff): if not tlv: @@ -132,15 +185,30 @@ def type2num(type_val_tuple): return struct.pack("!HH", type_val, val) + s @staticmethod - def num2type(typ, enc): - val = ISAKMPTransformNum.get(typ, (typ, {})) + def num2type(typ, enc, proto=0): + if proto == PROTO_ISAKMP: + val = ISAKMPTransformNum.get(typ, (typ, {})) + elif proto == PROTO_IPSEC_ESP: + val = IPSECTransformNum.get(typ, (typ, {})) + else: + val = (typ, {}) enc = val[1].get(enc, enc) return (val[0], enc) + def _get_proto(self, pkt): + # Ugh + cur = pkt + while cur and getattr(cur, "proto", None) is None: + cur = cur.parent or cur.underlayer + if cur is None: + return PROTO_ISAKMP + return cur.proto + def i2m(self, pkt, i): if i is None: return b"" - i = [ISAKMPTransformSetField.type2num(e) for e in i] + proto = self._get_proto(pkt) + i = [ISAKMPTransformSetField.type2num(e, proto=proto) for e in i] return b"".join(i) def m2i(self, pkt, m): @@ -150,6 +218,7 @@ def m2i(self, pkt, m): # worst case that should result in broken attributes (which would # be expected). (wam) lst = [] + proto = self._get_proto(pkt) while len(m) >= 4: trans_type, = struct.unpack("!H", m[:2]) is_tlv = not (trans_type & 0x8000) @@ -167,41 +236,73 @@ def m2i(self, pkt, m): value_len = 0 value, = struct.unpack("!H", m[2:4]) m = m[4 + value_len:] - lst.append(ISAKMPTransformSetField.num2type(trans_type, value)) + lst.append(ISAKMPTransformSetField.num2type(trans_type, value, proto=proto)) if len(m) > 0: warning("Extra bytes after ISAKMP transform dissection [%r]" % m) return lst -ISAKMP_payload_type = ["None", "SA", "Proposal", "Transform", "KE", "ID", - "CERT", "CR", "Hash", "SIG", "Nonce", "Notification", - "Delete", "VendorID"] - -ISAKMP_exchange_type = ["None", "base", "identity prot.", - "auth only", "aggressive", "info"] - - -class ISAKMP_class(Packet): - def guess_payload_class(self, payload): - np = self.next_payload - if np == 0: +ISAKMP_payload_type = { + 0: "None", + 1: "SA", + 2: "Proposal", + 3: "Transform", + 4: "KE", + 5: "ID", + 6: "CERT", + 7: "CR", + 8: "Hash", + 9: "SIG", + 10: "Nonce", + 11: "Notification", + 12: "Delete", + 13: "VendorID", +} + +ISAKMP_exchange_type = { + 0: "None", + 1: "base", + 2: "identity protection", + 3: "authentication only", + 4: "aggressive", + 5: "informational", + 32: "quick mode", +} + +# https://www.iana.org/assignments/isakmp-registry/isakmp-registry.xhtml#isakmp-registry-3 +# IPSEC Security Protocol Identifiers +ISAKMP_protos = { + 1: "ISAKMP", + 2: "IPSEC_AH", + 3: "IPSEC_ESP", + 4: "IPCOMP", + 5: "GIGABEAM_RADIO" +} + +ISAKMP_doi = { + 0: "ISAKMP", + 1: "IPSEC", +} + + +class _ISAKMP_class(Packet): + def default_payload_class(self, payload): + if self.next_payload == 0: return conf.raw_layer - elif np < len(ISAKMP_payload_type): - pt = ISAKMP_payload_type[np] - return globals().get("ISAKMP_payload_%s" % pt, ISAKMP_payload) - else: - return ISAKMP_payload + return ISAKMP_payload + +# -- ISAKMP -class ISAKMP(ISAKMP_class): # rfc2408 +class ISAKMP(_ISAKMP_class): # rfc2408 name = "ISAKMP" fields_desc = [ - StrFixedLenField("init_cookie", "", 8), - StrFixedLenField("resp_cookie", "", 8), + XStrFixedLenField("init_cookie", "", 8), + XStrFixedLenField("resp_cookie", "", 8), ByteEnumField("next_payload", 0, ISAKMP_payload_type), XByteField("version", 0x10), ByteEnumField("exch_type", 0, ISAKMP_exchange_type), - FlagsField("flags", 0, 8, ["encryption", "commit", "auth_only", "res3", "res4", "res5", "res6", "res7"]), # XXX use a Flag field # noqa: E501 + FlagsField("flags", 0, 8, ["encryption", "commit", "auth_only"]), IntField("id", 0), IntField("length", None) ] @@ -209,7 +310,7 @@ class ISAKMP(ISAKMP_class): # rfc2408 def guess_payload_class(self, payload): if self.flags & 1: return conf.raw_layer - return ISAKMP_class.guess_payload_class(self, payload) + return _ISAKMP_class.guess_payload_class(self, payload) def answers(self, other): if isinstance(other, ISAKMP): @@ -224,15 +325,33 @@ def post_build(self, p, pay): return p -class ISAKMP_payload_Transform(ISAKMP_class): - name = "IKE Transform" +# -- ISAKMP payloads + +class ISAKMP_payload(_ISAKMP_class): + name = "ISAKMP payload" + show_indent = 0 fields_desc = [ ByteEnumField("next_payload", None, ISAKMP_payload_type), ByteField("res", 0), - # ShortField("len",None), ShortField("length", None), - ByteField("num", None), - ByteEnumField("id", 1, {1: "KEY_IKE"}), + XStrLenField("load", "", length_from=lambda x:x.length - 4), + ] + + def post_build(self, pkt, pay): + if self.length is None: + pkt = pkt[:2] + struct.pack("!H", len(pkt)) + pkt[4:] + return pkt + pay + + +class ISAKMP_payload_Transform(ISAKMP_payload): + name = "IKE Transform" + deprecated_fields = { + "num": ("transform_count", ("2.5.0")), + "id": ("transform_id", ("2.5.0")), + } + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + ByteField("transform_count", None), + ByteEnumField("transform_id", 1, {1: "KEY_IKE"}), ShortField("res2", 0), ISAKMPTransformSetField("transforms", None, length_from=lambda x: x.length - 8) # noqa: E501 # XIntField("enc",0x80010005L), @@ -244,25 +363,13 @@ class ISAKMP_payload_Transform(ISAKMP_class): # XIntField("durationl",0x00007080L), ] - def post_build(self, p, pay): - if self.length is None: - tmp_len = len(p) - tmp_pay = p[:2] + chb((tmp_len >> 8) & 0xff) - p = tmp_pay + chb(tmp_len & 0xff) + p[4:] - p += pay - return p - # https://tools.ietf.org/html/rfc2408#section-3.5 -class ISAKMP_payload_Proposal(ISAKMP_class): +class ISAKMP_payload_Proposal(ISAKMP_payload): name = "IKE proposal" -# ISAKMP_payload_type = 0 - fields_desc = [ - ByteEnumField("next_payload", None, ISAKMP_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "trans", "H", adjust=lambda pkt, x:x + 8), # noqa: E501 + fields_desc = ISAKMP_payload.fields_desc[:3] + [ ByteField("proposal", 1), - ByteEnumField("proto", 1, {1: "ISAKMP"}), + ByteEnumField("proto", 1, ISAKMP_protos), FieldLenField("SPIsize", None, "SPI", "B"), ByteField("trans_nb", None), StrLenField("SPI", "", length_from=lambda x: x.SPIsize), @@ -270,27 +377,31 @@ class ISAKMP_payload_Proposal(ISAKMP_class): ] -class ISAKMP_payload(ISAKMP_class): - name = "ISAKMP payload" - fields_desc = [ - ByteEnumField("next_payload", None, ISAKMP_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 4), - StrLenField("load", "", length_from=lambda x:x.length - 4), - ] +# VendorID: https://www.rfc-editor.org/rfc/rfc2408#section-3.16 + +# packet-isakmp.c from wireshark +ISAKMP_VENDOR_IDS = { + b"\x09\x00\x26\x89\xdf\xd6\xb7\x12": "XAUTH", + b"\xaf\xca\xd7\x13h\xa1\xf1\xc9k\x86\x96\xfcwW\x01\x00": "RFC 3706 DPD", + b"@H\xb7\xd5n\xbc\xe8\x85%\xe7\xde\x7f\x00\xd6\xc2\xd3\x80": "Cisco Fragmentation", + b"J\x13\x1c\x81\x07\x03XE\\W(\xf2\x0e\x95E/": "RFC 3947 Negotiation of NAT-Transversal", # noqa: E501 + b"\x90\xcb\x80\x91>\xbbin\x08c\x81\xb5\xecB{\x1f": "draft-ietf-ipsec-nat-t-ike-02", +} class ISAKMP_payload_VendorID(ISAKMP_payload): name = "ISAKMP Vendor ID" + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + StrLenEnumField("VendorID", b"", + ISAKMP_VENDOR_IDS, + length_from=lambda x: x.length - 4) + ] -class ISAKMP_payload_SA(ISAKMP_class): +class ISAKMP_payload_SA(ISAKMP_payload): name = "ISAKMP SA" - fields_desc = [ - ByteEnumField("next_payload", None, ISAKMP_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "prop", "H", adjust=lambda pkt, x:x + 12), # noqa: E501 - IntEnumField("DOI", 1, {1: "IPSEC"}), + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + IntEnumField("doi", 1, ISAKMP_doi), IntEnumField("situation", 1, {1: "identity"}), PacketLenField("prop", conf.raw_layer(), ISAKMP_payload_Proposal, length_from=lambda x: x.length - 12), # noqa: E501 ] @@ -298,50 +409,144 @@ class ISAKMP_payload_SA(ISAKMP_class): class ISAKMP_payload_Nonce(ISAKMP_payload): name = "ISAKMP Nonce" + deprecated_fields = {"load": ("nonce", "2.6.2")} + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + StrLenField("nonce", "", length_from=lambda x: x.length - 4) + ] class ISAKMP_payload_KE(ISAKMP_payload): name = "ISAKMP Key Exchange" + deprecated_fields = {"load": ("ke", "2.6.2")} + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + StrLenField("ke", "", length_from=lambda x: x.length - 4) + ] -class ISAKMP_payload_ID(ISAKMP_class): +class ISAKMP_payload_ID(ISAKMP_payload): name = "ISAKMP Identification" - fields_desc = [ - ByteEnumField("next_payload", None, ISAKMP_payload_type), - ByteField("res", 0), - FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 8), - ByteEnumField("IDtype", 1, {1: "IPv4_addr", 11: "Key"}), + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + ByteEnumField("IDtype", 1, { + # Beware, apparently in-the-wild the values used + # appear to be the ones from IKEv2 (RFC4306 sect 3.5) + # and not ISAKMP (RFC2408 sect A.4) + 1: "IPv4_addr", + 11: "Key" + }), ByteEnumField("ProtoID", 0, {0: "Unused"}), ShortEnumField("Port", 0, {0: "Unused"}), - # IPField("IdentData","127.0.0.1"), - StrLenField("load", "", length_from=lambda x: x.length - 8), + MultipleTypeField( + [ + (IPField("IdentData", "127.0.0.1"), + lambda pkt: pkt.IDtype == 1), + ], + StrLenField("IdentData", "", length_from=lambda x: x.length - 8), + ) ] class ISAKMP_payload_Hash(ISAKMP_payload): name = "ISAKMP Hash" + deprecated_fields = {"load": ("hash", "2.6.2")} + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + StrLenField("hash", "", length_from=lambda x: x.length - 4) + ] + + +class ISAKMP_payload_SIG(ISAKMP_payload): + name = "ISAKMP Signature" + deprecated_fields = {"load": ("sig", "2.6.2")} + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + StrLenField("sig", "", length_from=lambda x: x.length - 4) + ] + + +NotifyMessageType = { + 1: "INVALID-PAYLOAD-TYPE", + 2: "DOI-NOT-SUPPORTED", + 3: "SITUATION-NOT-SUPPORTED", + 4: "INVALID-COOKIE", + 5: "INVALID-MAJOR-VERSION", + 6: "INVALID-MINOR-VERSION", + 7: "INVALID-EXCHANGE-TYPE", + 8: "INVALID-FLAGS", + 9: "INVALID-MESSAGE-ID", + 10: "INVALID-PROTOCOL-ID", + 11: "INVALID-SPI", + 12: "INVALID-TRANSFORM-ID", + 13: "ATTRIBUTES-NOT-SUPPORTED", + 14: "NO-PROPOSAL-CHOSEN", + 15: "BAD-PROPOSAL-SYNTAX", + 16: "PAYLOAD-MALFORMED", + 17: "INVALID-KEY-INFORMATION", + 18: "INVALID-ID-INFORMATION", + 19: "INVALID-CERT-ENCODING", + 20: "INVALID-CERTIFICATE", + 21: "CERT-TYPE-UNSUPPORTED", + 22: "INVALID-CERT-AUTHORITY", + 23: "INVALID-HASH-INFORMATION", + 24: "AUTHENTICATION-FAILED", + 25: "INVALID-SIGNATURE", + 26: "ADDRESS-NOTIFICATION", + 27: "NOTIFY-SA-LIFETIME", + 28: "CERTIFICATE-UNAVAILABLE", + 29: "UNSUPPORTED-EXCHANGE-TYPE", + 30: "UNEQUAL-PAYLOAD-LENGTHS", + 16384: "CONNECTED", + # RFC 3706 + 36136: "R-U-THERE", + 36137: "R-U-THERE-ACK", +} + + +class ISAKMP_payload_Notify(ISAKMP_payload): + name = "ISAKMP Notify (Notification)" + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + IntEnumField("doi", 0, ISAKMP_doi), + ByteEnumField("proto", 1, ISAKMP_protos), + FieldLenField("SPIsize", None, "SPI", "B"), + ShortEnumField("notify_msg_type", None, NotifyMessageType), + StrLenField("SPI", "", length_from=lambda x: x.SPIsize), + StrLenField("notify_data", "", + length_from=lambda x: x.length - x.SPIsize - 12) + ] + + +class ISAKMP_payload_Delete(ISAKMP_payload): + name = "ISAKMP Delete" + fields_desc = ISAKMP_payload.fields_desc[:3] + [ + IntEnumField("doi", 0, ISAKMP_doi), + ByteEnumField("proto", 1, ISAKMP_protos), + FieldLenField("SPIsize", None, length_of="SPIs", fmt="B", + adjust=lambda pkt, x: x and x // len(pkt.SPIs)), + FieldLenField("SPIcount", None, count_of="SPIs", fmt="H"), + FieldListField("SPIs", [], + StrLenField("", "", length_from=lambda pkt: pkt.SPIsize), + count_from=lambda pkt: pkt.SPIcount), + ] bind_bottom_up(UDP, ISAKMP, dport=500) bind_bottom_up(UDP, ISAKMP, sport=500) -bind_layers(UDP, ISAKMP, dport=500, sport=500) - -# Add building bindings -# (Dissection bindings are located in ISAKMP_class.guess_payload_class) -bind_top_down(ISAKMP_class, ISAKMP_payload, next_payload=0) -bind_top_down(ISAKMP_class, ISAKMP_payload_SA, next_payload=1) -bind_top_down(ISAKMP_class, ISAKMP_payload_Proposal, next_payload=2) -bind_top_down(ISAKMP_class, ISAKMP_payload_Transform, next_payload=3) -bind_top_down(ISAKMP_class, ISAKMP_payload_KE, next_payload=4) -bind_top_down(ISAKMP_class, ISAKMP_payload_ID, next_payload=5) -# bind_top_down(ISAKMP_class, ISAKMP_payload_CERT, next_payload=6) -# bind_top_down(ISAKMP_class, ISAKMP_payload_CR, next_payload=7) -bind_top_down(ISAKMP_class, ISAKMP_payload_Hash, next_payload=8) -# bind_top_down(ISAKMP_class, ISAKMP_payload_SIG, next_payload=9) -bind_top_down(ISAKMP_class, ISAKMP_payload_Nonce, next_payload=10) -# bind_top_down(ISAKMP_class, ISAKMP_payload_Notification, next_payload=11) -# bind_top_down(ISAKMP_class, ISAKMP_payload_Delete, next_payload=12) -bind_top_down(ISAKMP_class, ISAKMP_payload_VendorID, next_payload=13) +bind_top_down(UDP, ISAKMP, dport=500, sport=500) + +bind_bottom_up(NON_ESP, ISAKMP) + +# Add bindings +bind_top_down(_ISAKMP_class, ISAKMP_payload, next_payload=0) +bind_layers(_ISAKMP_class, ISAKMP_payload_SA, next_payload=1) +bind_layers(_ISAKMP_class, ISAKMP_payload_Proposal, next_payload=2) +bind_layers(_ISAKMP_class, ISAKMP_payload_Transform, next_payload=3) +bind_layers(_ISAKMP_class, ISAKMP_payload_KE, next_payload=4) +bind_layers(_ISAKMP_class, ISAKMP_payload_ID, next_payload=5) +# bind_layers(_ISAKMP_class, ISAKMP_payload_CERT, next_payload=6) +# bind_layers(_ISAKMP_class, ISAKMP_payload_CR, next_payload=7) +bind_layers(_ISAKMP_class, ISAKMP_payload_Hash, next_payload=8) +bind_layers(_ISAKMP_class, ISAKMP_payload_SIG, next_payload=9) +bind_layers(_ISAKMP_class, ISAKMP_payload_Nonce, next_payload=10) +bind_layers(_ISAKMP_class, ISAKMP_payload_Notify, next_payload=11) +bind_layers(_ISAKMP_class, ISAKMP_payload_Delete, next_payload=12) +bind_layers(_ISAKMP_class, ISAKMP_payload_VendorID, next_payload=13) def ikescan(ip): diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py new file mode 100644 index 00000000000..f8e18ce64c2 --- /dev/null +++ b/scapy/layers/kerberos.py @@ -0,0 +1,5765 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +r""" +Kerberos V5 + +Implements parts of: + +- Kerberos Network Authentication Service (V5): RFC4120 +- Kerberos Version 5 GSS-API: RFC1964, RFC4121 +- Kerberos Pre-Authentication: RFC6113 (FAST) +- Kerberos Principal Name Canonicalization and Cross-Realm Referrals: RFC6806 +- Microsoft Windows 2000 Kerberos Change Password and Set Password Protocols: RFC3244 +- PKINIT and its extensions: RFC4556, RFC8070, RFC8636 and [MS-PKCA] +- User to User Kerberos Authentication: draft-ietf-cat-user2user-03 +- Public Key Cryptography Based User-to-User Authentication (PKU2U): draft-zhu-pku2u-09 +- Initial and Pass Through Authentication Using Kerberos V5 (IAKERB): + draft-ietf-kitten-iakerb-03 +- Kerberos Protocol Extensions: [MS-KILE] +- Kerberos Protocol Extensions: Service for User: [MS-SFU] +- Kerberos Key Distribution Center Proxy Protocol: [MS-KKDCP] + + +.. note:: + You will find more complete documentation for this layer over at + `Kerberos `_ + +Example decryption:: + + >>> from scapy.libs.rfc3961 import Key, EncryptionType + >>> pkt = Ether(hex_bytes("525400695813525400216c2b08004500015da71840008006dc\ + 83c0a87a9cc0a87a11c209005854f6ab2392c25bd650182014b6e00000000001316a8201\ + 2d30820129a103020105a20302010aa3633061304ca103020102a24504433041a0030201\ + 12a23a043848484decb01c9b62a1cabfbc3f2d1ed85aa5e093ba8358a8cea34d4393af93\ + bf211e274fa58e814878db9f0d7a28d94e7327660db4f3704b3011a10402020080a20904\ + 073005a0030101ffa481b73081b4a00703050040810010a1123010a003020101a1093007\ + 1b0577696e3124a20e1b0c444f4d41494e2e4c4f43414ca321301fa003020102a1183016\ + 1b066b72627467741b0c444f4d41494e2e4c4f43414ca511180f32303337303931333032\ + 343830355aa611180f32303337303931333032343830355aa7060204701cc5d1a8153013\ + 0201120201110201170201180202ff79020103a91d301b3019a003020114a11204105749\ + 4e31202020202020202020202020")) + >>> enc = pkt[Kerberos].root.padata[0].padataValue + >>> k = Key(enc.etype.val, key=hex_bytes("7fada4e566ae4fb270e2800a23a\ + e87127a819d42e69b5e22de0ddc63da80096d")) + >>> enc.decrypt(k) +""" + +from collections import namedtuple, deque +from datetime import datetime, timedelta, timezone +from enum import IntEnum + +import os +import re +import socket +import struct + +from scapy.error import warning +import scapy.asn1.mib # noqa: F401 +from scapy.asn1.ber import BER_id_dec, BER_Decoding_Error +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_BOOLEAN, + ASN1_Class, + ASN1_Codecs, + ASN1_GENERAL_STRING, + ASN1_GENERALIZED_TIME, + ASN1_INTEGER, + ASN1_OID, + ASN1_STRING, +) +from scapy.asn1fields import ( + ASN1F_BIT_STRING_ENCAPS, + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_enum_INTEGER, + ASN1F_FLAGS, + ASN1F_GENERAL_STRING, + ASN1F_GENERALIZED_TIME, + ASN1F_INTEGER, + ASN1F_OID, + ASN1F_optional, + ASN1F_PACKET, + ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_STRING_ENCAPS, + ASN1F_STRING_PacketField, + ASN1F_STRING, +) +from scapy.asn1packet import ASN1_Packet +from scapy.automaton import Automaton, ATMT +from scapy.config import conf +from scapy.compat import bytes_encode +from scapy.error import log_runtime +from scapy.fields import ( + ConditionalField, + FieldLenField, + FlagsField, + IntEnumField, + LEIntEnumField, + LenField, + LEShortEnumField, + LEShortField, + LongField, + MayEnd, + MultipleTypeField, + PacketField, + PacketLenField, + PacketListField, + PadField, + ShortEnumField, + ShortField, + StrField, + StrFieldUtf16, + StrFixedLenEnumField, + XByteField, + XLEIntEnumField, + XLEIntField, + XLEShortField, + XStrField, + XStrFixedLenField, + XStrLenField, +) +from scapy.packet import Packet, bind_bottom_up, bind_top_down, bind_layers +from scapy.supersocket import StreamSocket, SuperSocket +from scapy.utils import strrot, strxor +from scapy.volatile import GeneralizedTime, RandNum, RandBin + +from scapy.layers.gssapi import ( + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, + GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_QOP_REQ_FLAGS, + GSS_S_BAD_BINDINGS, + GSS_S_BAD_MECH, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_DEFECTIVE_CREDENTIAL, + GSS_S_DEFECTIVE_TOKEN, + GSS_S_FAILURE, + GSS_S_FLAGS, + GSSAPI_BLOB, + GssChannelBindings, + SSP, +) +from scapy.layers.inet import TCP, UDP +from scapy.layers.smb import _NV_VERSION +from scapy.layers.tls.cert import ( + Cert, + CertList, + CertTree, + CMS_Engine, + PrivKey, +) +from scapy.layers.tls.crypto.hash import ( + Hash_SHA, + Hash_SHA256, + Hash_SHA384, + Hash_SHA512, +) +from scapy.layers.tls.crypto.groups import _ffdh_groups +from scapy.layers.windows.erref import STATUS_ERREF +from scapy.layers.x509 import ( + _CMS_ENCAPSULATED, + CMS_ContentInfo, + CMS_IssuerAndSerialNumber, + DHPublicKey, + X509_AlgorithmIdentifier, + X509_DirectoryName, + X509_SubjectPublicKeyInfo, + DomainParameters, +) + +# Redirect exports from RFC3961 +try: + from scapy.libs.rfc3961 import * # noqa: F401,F403 + from scapy.libs.rfc3961 import ( + _rfc1964pad, + ChecksumType, + Cipher, + decrepit_algorithms, + EncryptionType, + Hmac_MD5, + Key, + KRB_FX_CF2, + octetstring2key, + ) +except ImportError: + pass + + +# Crypto imports +if conf.crypto_valid: + from cryptography.hazmat.primitives.serialization import pkcs12 + from cryptography.hazmat.primitives.asymmetric import dh + +# Typing imports +from typing import ( + List, + Optional, + Union, +) + +# kerberos APPLICATION + + +class ASN1_Class_KRB(ASN1_Class): + name = "Kerberos" + # APPLICATION + CONSTRUCTED = 0x40 | 0x20 + Token = 0x60 | 0 # GSSAPI + Ticket = 0x60 | 1 + Authenticator = 0x60 | 2 + EncTicketPart = 0x60 | 3 + AS_REQ = 0x60 | 10 + AS_REP = 0x60 | 11 + TGS_REQ = 0x60 | 12 + TGS_REP = 0x60 | 13 + AP_REQ = 0x60 | 14 + AP_REP = 0x60 | 15 + PRIV = 0x60 | 21 + CRED = 0x60 | 22 + EncASRepPart = 0x60 | 25 + EncTGSRepPart = 0x60 | 26 + EncAPRepPart = 0x60 | 27 + EncKrbPrivPart = 0x60 | 28 + EncKrbCredPart = 0x60 | 29 + ERROR = 0x60 | 30 + + +# RFC4120 sect 5.2 + + +KerberosString = ASN1F_GENERAL_STRING +Realm = KerberosString +Int32 = ASN1F_INTEGER +UInt32 = ASN1F_INTEGER + +_PRINCIPAL_NAME_TYPES = { + 0: "NT-UNKNOWN", + 1: "NT-PRINCIPAL", + 2: "NT-SRV-INST", + 3: "NT-SRV-HST", + 4: "NT-SRV-XHST", + 5: "NT-UID", + 6: "NT-X500-PRINCIPAL", + 7: "NT-SMTP-NAME", + 10: "NT-ENTERPRISE", +} + + +class PrincipalName(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "nameType", + 0, + _PRINCIPAL_NAME_TYPES, + explicit_tag=0xA0, + ), + ASN1F_SEQUENCE_OF("nameString", [], KerberosString, explicit_tag=0xA1), + ) + + def toString(self): + """ + Convert a PrincipalName back into its string representation. + """ + return "/".join(x.val.decode() for x in self.nameString) + + @staticmethod + def fromUPN(upn: str, canonicalize: bool = False): + """ + Create a PrincipalName from a UPN string. + """ + if canonicalize: + return PrincipalName( + nameString=[ASN1_GENERAL_STRING(upn)], + nameType=ASN1_INTEGER(10), # NT-ENTERPRISE + ) + else: + user, _ = _parse_upn(upn) + return PrincipalName( + nameString=[ASN1_GENERAL_STRING(user)], + nameType=ASN1_INTEGER(1), # NT-PRINCIPAL + ) + + @staticmethod + def fromSPN(spn: str): + """ + Create a PrincipalName from a SPN string. + """ + spn, _ = _parse_spn(spn) + if spn.startswith("krbtgt"): + return PrincipalName( + nameString=[ASN1_GENERAL_STRING(x) for x in spn.split("/")], + nameType=ASN1_INTEGER(2), # NT-SRV-INST + ) + elif "/" in spn: + return PrincipalName( + nameString=[ASN1_GENERAL_STRING(x) for x in spn.split("/")], + nameType=ASN1_INTEGER(3), # NT-SRV-HST + ) + else: + # In case of U2U + return PrincipalName( + nameString=[ASN1_GENERAL_STRING(spn)], + nameType=ASN1_INTEGER(1), # NT-PRINCIPAL + ) + + +KerberosTime = ASN1F_GENERALIZED_TIME +Microseconds = ASN1F_INTEGER + + +# https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml#kerberos-parameters-1 + +_KRB_E_TYPES = { + 1: "DES-CBC-CRC", + 2: "DES-CBC-MD4", + 3: "DES-CBC-MD5", + 5: "DES3-CBC-MD5", + 7: "DES3-CBC-SHA1", + 9: "DSAWITHSHA1-CMSOID", + 10: "MD5WITHRSAENCRYPTION-CMSOID", + 11: "SHA1WITHRSAENCRYPTION-CMSOID", + 12: "RC2CBC-ENVOID", + 13: "RSAENCRYPTION-ENVOID", + 14: "RSAES-OAEP-ENV-OID", + 15: "DES-EDE3-CBC-ENV-OID", + 16: "DES3-CBC-SHA1-KD", + 17: "AES128-CTS-HMAC-SHA1-96", + 18: "AES256-CTS-HMAC-SHA1-96", + 19: "AES128-CTS-HMAC-SHA256-128", + 20: "AES256-CTS-HMAC-SHA384-192", + 23: "RC4-HMAC", + 24: "RC4-HMAC-EXP", + 25: "CAMELLIA128-CTS-CMAC", + 26: "CAMELLIA256-CTS-CMAC", +} + +# https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml#kerberos-parameters-2 + +_KRB_S_TYPES = { + 1: "CRC32", + 2: "RSA-MD4", + 3: "RSA-MD4-DES", + 4: "DES-MAC", + 5: "DES-MAC-K", + 6: "RSA-MD4-DES-K", + 7: "RSA-MD5", + 8: "RSA-MD5-DES", + 9: "RSA-MD5-DES3", + 10: "SHA1", + 12: "HMAC-SHA1-DES3-KD", + 13: "HMAC-SHA1-DES3", + 14: "SHA1", + 15: "HMAC-SHA1-96-AES128", + 16: "HMAC-SHA1-96-AES256", + 17: "CMAC-CAMELLIA128", + 18: "CMAC-CAMELLIA256", + 19: "HMAC-SHA256-128-AES128", + 20: "HMAC-SHA384-192-AES256", + # RFC 4121 + 0x8003: "KRB-AUTHENTICATOR", + # [MS-KILE] + 0xFFFFFF76: "MD5", + -138: "MD5", +} + + +class EncryptedData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("etype", 0x17, _KRB_E_TYPES, explicit_tag=0xA0), + ASN1F_optional(UInt32("kvno", None, explicit_tag=0xA1)), + ASN1F_STRING("cipher", "", explicit_tag=0xA2), + ) + + def get_usage(self): + """ + Get current key usage number and encrypted class + """ + # RFC 4120 sect 7.5.1 + if self.underlayer: + if isinstance(self.underlayer, PADATA): + patype = self.underlayer.padataType + if patype == 2: + # AS-REQ PA-ENC-TIMESTAMP padata timestamp + return 1, PA_ENC_TS_ENC + elif patype == 138: + # RFC6113 PA-ENC-TS-ENC + return 54, PA_ENC_TS_ENC + elif isinstance(self.underlayer, KRB_Ticket): + # AS-REP Ticket and TGS-REP Ticket + return 2, EncTicketPart + elif isinstance(self.underlayer, KRB_AS_REP): + # AS-REP encrypted part + return 3, EncASRepPart + elif isinstance(self.underlayer, KRB_KDC_REQ_BODY): + # KDC-REQ enc-authorization-data + return 4, AuthorizationData + elif isinstance(self.underlayer, KRB_AP_REQ) and isinstance( + self.underlayer.underlayer, PADATA + ): + # TGS-REQ PA-TGS-REQ Authenticator + return 7, KRB_Authenticator + elif isinstance(self.underlayer, KRB_TGS_REP): + # TGS-REP encrypted part + return 8, EncTGSRepPart + elif isinstance(self.underlayer, KRB_AP_REQ): + # AP-REQ Authenticator + return 11, KRB_Authenticator + elif isinstance(self.underlayer, KRB_AP_REP): + # AP-REP encrypted part + return 12, EncAPRepPart + elif isinstance(self.underlayer, KRB_PRIV): + # KRB-PRIV encrypted part + return 13, EncKrbPrivPart + elif isinstance(self.underlayer, KRB_CRED): + # KRB-CRED encrypted part + return 14, EncKrbCredPart + elif isinstance(self.underlayer, KrbFastArmoredReq): + # KEY_USAGE_FAST_ENC + return 51, KrbFastReq + elif isinstance(self.underlayer, KrbFastArmoredRep): + # KEY_USAGE_FAST_REP + return 52, KrbFastResponse + raise ValueError( + "Could not guess key usage number. Please specify key_usage_number" + ) + + def decrypt(self, key, key_usage_number=None, cls=None): + """ + Decrypt and return the data contained in cipher. + + :param key: the key to use for decryption + :param key_usage_number: (optional) specify the key usage number. + Guessed otherwise + :param cls: (optional) the class of the decrypted payload + Guessed otherwise (or bytes) + """ + if key_usage_number is None: + key_usage_number, cls = self.get_usage() + d = key.decrypt(key_usage_number, self.cipher.val) + if cls: + try: + return cls(d) + except BER_Decoding_Error: + if cls == EncASRepPart: + # https://datatracker.ietf.org/doc/html/rfc4120#section-5.4.2 + # "Compatibility note: Some implementations unconditionally send an + # encrypted EncTGSRepPart (application tag number 26) in this field + # regardless of whether the reply is a AS-REP or a TGS-REP. In the + # interest of compatibility, implementors MAY relax the check on the + # tag number of the decrypted ENC-PART." + try: + res = EncTGSRepPart(d) + # https://github.com/krb5/krb5/blob/48ccd81656381522d1f9ccb8705c13f0266a46ab/src/lib/krb5/asn.1/asn1_k_encode.c#L1128 + # This is a bug because as the RFC clearly says above, we're + # perfectly in our right to be strict on this. (MAY) + log_runtime.warning( + "Implementation bug detected. This looks like MIT Kerberos." + ) + return res + except BER_Decoding_Error: + pass + raise + return d + + def encrypt(self, key, text, confounder=None, key_usage_number=None): + """ + Encrypt text and set it into cipher. + + :param key: the key to use for encryption + :param text: the bytes value to encode + :param confounder: (optional) specify the confounder bytes. Random otherwise + :param key_usage_number: (optional) specify the key usage number. + Guessed otherwise + """ + if key_usage_number is None: + key_usage_number = self.get_usage()[0] + self.etype = key.etype + self.cipher = ASN1_STRING( + key.encrypt(key_usage_number, text, confounder=confounder) + ) + + +class EncryptionKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("keytype", 0, _KRB_E_TYPES, explicit_tag=0xA0), + ASN1F_STRING("keyvalue", "", explicit_tag=0xA1), + ) + + def toKey(self): + return Key( + etype=self.keytype.val, + key=self.keyvalue.val, + ) + + @classmethod + def fromKey(self, key): + return EncryptionKey( + keytype=key.etype, + keyvalue=key.key, + ) + + +class _Checksum_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_Checksum_Field, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.cksumtype.val == 0x8003: + # Special case per RFC 4121 + return KRB_AuthenticatorChecksum(val[0].val, _underlayer=pkt), val[1] + return val + + +class Checksum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "cksumtype", + 0, + _KRB_S_TYPES, + explicit_tag=0xA0, + ), + _Checksum_Field("checksum", "", explicit_tag=0xA1), + ) + + def get_usage(self): + """ + Get current key usage number + """ + # RFC 4120 sect 7.5.1 + if self.underlayer: + if isinstance(self.underlayer, KRB_Authenticator): + # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator cksum + # (n°10 should never happen as we use RFC4121) + return 6 + elif isinstance(self.underlayer, PA_FOR_USER): + # [MS-SFU] sect 2.2.1 + return 17 + elif isinstance(self.underlayer, PA_S4U_X509_USER): + # [MS-SFU] sect 2.2.2 + return 26 + elif isinstance(self.underlayer, AD_KDCIssued): + # AD-KDC-ISSUED checksum + return 19 + elif isinstance(self.underlayer, KrbFastArmoredReq): + # KEY_USAGE_FAST_REQ_CHKSUM + return 50 + elif isinstance(self.underlayer, KrbFastFinished): + # KEY_USAGE_FAST_FINISHED + return 53 + raise ValueError( + "Could not guess key usage number. Please specify key_usage_number" + ) + + def verify(self, key, text, key_usage_number=None): + """ + Verify a signature of text using a key. + + :param key: the key to use to check the checksum + :param text: the bytes to verify + :param key_usage_number: (optional) specify the key usage number. + Guessed otherwise + """ + if key_usage_number is None: + key_usage_number = self.get_usage() + key.verify_checksum(key_usage_number, text, self.checksum.val) + + def make(self, key, text, key_usage_number=None, cksumtype=None): + """ + Make a signature. + + :param key: the key to use to make the checksum + :param text: the bytes to make a checksum of + :param key_usage_number: (optional) specify the key usage number. + Guessed otherwise + """ + if key_usage_number is None: + key_usage_number = self.get_usage() + self.cksumtype = cksumtype or key.cksumtype + self.checksum = ASN1_STRING( + key.make_checksum( + keyusage=key_usage_number, + text=text, + cksumtype=self.cksumtype, + ) + ) + + +KerberosFlags = ASN1F_FLAGS + +_ADDR_TYPES = { + # RFC4120 sect 7.5.3 + 0x02: "IPv4", + 0x03: "Directional", + 0x05: "ChaosNet", + 0x06: "XNS", + 0x07: "ISO", + 0x0C: "DECNET Phase IV", + 0x10: "AppleTalk DDP", + 0x14: "NetBios", + 0x18: "IPv6", +} + + +class HostAddress(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "addrType", + 0, + _ADDR_TYPES, + explicit_tag=0xA0, + ), + ASN1F_STRING("address", "", explicit_tag=0xA1), + ) + + +HostAddresses = lambda name, **kwargs: ASN1F_SEQUENCE_OF( + name, [], HostAddress, **kwargs +) + + +_AUTHORIZATIONDATA_VALUES = { + # Filled below +} + + +class _AuthorizationData_value_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_AuthorizationData_value_Field, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.adType.val in _AUTHORIZATIONDATA_VALUES: + return ( + _AUTHORIZATIONDATA_VALUES[pkt.adType.val](val[0].val, _underlayer=pkt), + val[1], + ) + return val + + +_AD_TYPES = { + # RFC4120 sect 7.5.4 + 1: "AD-IF-RELEVANT", + 2: "AD-INTENDED-FOR-SERVER", + 3: "AD-INTENDED-FOR-APPLICATION-CLASS", + 4: "AD-KDC-ISSUED", + 5: "AD-AND-OR", + 6: "AD-MANDATORY-TICKET-EXTENSIONS", + 7: "AD-IN-TICKET-EXTENSIONS", + 8: "AD-MANDATORY-FOR-KDC", + 64: "OSF-DCE", + 65: "SESAME", + 66: "AD-OSD-DCE-PKI-CERTID", + 128: "AD-WIN2K-PAC", + 129: "AD-ETYPE-NEGOTIATION", + # [MS-KILE] additions + 141: "KERB-AUTH-DATA-TOKEN-RESTRICTIONS", + 142: "KERB-LOCAL", + 143: "AD-AUTH-DATA-AP-OPTIONS", + 144: "KERB-AUTH-DATA-CLIENT-TARGET", +} + + +class AuthorizationDataItem(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "adType", + 0, + _AD_TYPES, + explicit_tag=0xA0, + ), + _AuthorizationData_value_Field("adData", "", explicit_tag=0xA1), + ) + + +class AuthorizationData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "seq", [AuthorizationDataItem()], AuthorizationDataItem + ) + + def getAuthData(self, adType): + return next((x.adData for x in self.seq if x.adType == adType), None) + + +AD_IF_RELEVANT = AuthorizationData +_AUTHORIZATIONDATA_VALUES[1] = AD_IF_RELEVANT + + +class AD_KDCIssued(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("adChecksum", Checksum(), Checksum, explicit_tag=0xA0), + ASN1F_optional( + Realm("iRealm", "", explicit_tag=0xA1), + ), + ASN1F_optional(ASN1F_PACKET("iSname", None, PrincipalName, explicit_tag=0xA2)), + ASN1F_PACKET("elements", None, AuthorizationData, explicit_tag=0xA3), + ) + + +_AUTHORIZATIONDATA_VALUES[4] = AD_KDCIssued + + +class AD_AND_OR(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + Int32("conditionCount", 0, explicit_tag=0xA0), + ASN1F_PACKET("elements", None, AuthorizationData, explicit_tag=0xA1), + ) + + +_AUTHORIZATIONDATA_VALUES[5] = AD_AND_OR + +ADMANDATORYFORKDC = AuthorizationData +_AUTHORIZATIONDATA_VALUES[8] = ADMANDATORYFORKDC + + +# https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xml +_PADATA_TYPES = { + 1: "PA-TGS-REQ", + 2: "PA-ENC-TIMESTAMP", + 3: "PA-PW-SALT", + 11: "PA-ETYPE-INFO", + 14: "PA-PK-AS-REQ-OLD", + 15: "PA-PK-AS-REP-OLD", + 16: "PA-PK-AS-REQ", + 17: "PA-PK-AS-REP", + 18: "PA-PK-OCSP-RESPONSE", + 19: "PA-ETYPE-INFO2", + 20: "PA-SVR-REFERRAL-INFO", + 111: "TD-CMS-DIGEST-ALGORITHMS", + 128: "PA-PAC-REQUEST", + 129: "PA-FOR-USER", + 130: "PA-FOR-X509-USER", + 131: "PA-FOR-CHECK_DUPS", + 132: "PA-AS-CHECKSUM", + 133: "PA-FX-COOKIE", + 134: "PA-AUTHENTICATION-SET", + 135: "PA-AUTH-SET-SELECTED", + 136: "PA-FX-FAST", + 137: "PA-FX-ERROR", + 138: "PA-ENCRYPTED-CHALLENGE", + 141: "PA-OTP-CHALLENGE", + 142: "PA-OTP-REQUEST", + 143: "PA-OTP-CONFIRM", + 144: "PA-OTP-PIN-CHANGE", + 145: "PA-EPAK-AS-REQ", + 146: "PA-EPAK-AS-REP", + 147: "PA-PKINIT-KX", + 148: "PA-PKU2U-NAME", + 149: "PA-REQ-ENC-PA-REP", + 150: "PA-AS-FRESHNESS", + 151: "PA-SPAKE", + 161: "KERB-KEY-LIST-REQ", + 162: "KERB-KEY-LIST-REP", + 165: "PA-SUPPORTED-ENCTYPES", + 166: "PA-EXTENDED-ERROR", + 167: "PA-PAC-OPTIONS", + 170: "KERB-SUPERSEDED-BY-USER", + 171: "KERB-DMSA-KEY-PACKAGE", +} + +_PADATA_CLASSES = { + # Filled elsewhere in this file +} + + +# RFC4120 + + +class _PADATA_value_Field(ASN1F_STRING_PacketField): + """ + A special field that properly dispatches PA-DATA values according to + padata-type and if the paquet is a request or a response. + """ + + def m2i(self, pkt, s): + val = super(_PADATA_value_Field, self).m2i(pkt, s) + if pkt.padataType.val in _PADATA_CLASSES: + cls = _PADATA_CLASSES[pkt.padataType.val] + if isinstance(cls, tuple): + parent = pkt.underlayer or pkt.parent + is_reply = False + if parent is not None: + if isinstance(parent, (KRB_AS_REP, KRB_TGS_REP)): + is_reply = True + else: + parent = parent.underlayer or parent.parent + is_reply = isinstance(parent, KRB_ERROR) + cls = cls[is_reply] + if not val[0].val: + return val + return cls(val[0].val, _underlayer=pkt), val[1] + return val + + +class PADATA(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("padataType", 0, _PADATA_TYPES, explicit_tag=0xA1), + _PADATA_value_Field( + "padataValue", + "", + explicit_tag=0xA2, + ), + ) + + +# RFC 4120 sect 5.2.7.2 + + +class PA_ENC_TS_ENC(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KerberosTime("patimestamp", GeneralizedTime(), explicit_tag=0xA0), + ASN1F_optional(Microseconds("pausec", 0, explicit_tag=0xA1)), + ) + + +_PADATA_CLASSES[2] = EncryptedData # PA-ENC-TIMESTAMP +_PADATA_CLASSES[138] = EncryptedData # PA-ENCRYPTED-CHALLENGE + + +# RFC 4120 sect 5.2.7.4 + + +class ETYPE_INFO_ENTRY(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("etype", 0x1, _KRB_E_TYPES, explicit_tag=0xA0), + ASN1F_optional( + ASN1F_STRING("salt", "", explicit_tag=0xA1), + ), + ) + + +class ETYPE_INFO(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("seq", [ETYPE_INFO_ENTRY()], ETYPE_INFO_ENTRY) + + +_PADATA_CLASSES[11] = ETYPE_INFO + +# RFC 4120 sect 5.2.7.5 + + +class ETYPE_INFO_ENTRY2(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("etype", 0x1, _KRB_E_TYPES, explicit_tag=0xA0), + ASN1F_optional( + KerberosString("salt", "", explicit_tag=0xA1), + ), + ASN1F_optional( + ASN1F_STRING("s2kparams", "", explicit_tag=0xA2), + ), + ) + + +class ETYPE_INFO2(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("seq", [ETYPE_INFO_ENTRY2()], ETYPE_INFO_ENTRY2) + + +_PADATA_CLASSES[19] = ETYPE_INFO2 + + +# RFC8636 - PKINIT Algorithm Agility + + +class TD_CMS_DIGEST_ALGORITHMS(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("seq", [], X509_AlgorithmIdentifier) + + +_PADATA_CLASSES[111] = TD_CMS_DIGEST_ALGORITHMS + + +# PADATA Extended with RFC6113 + + +class PA_AUTHENTICATION_SET_ELEM(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + Int32("paType", 0, explicit_tag=0xA0), + ASN1F_optional( + ASN1F_STRING("paHint", "", explicit_tag=0xA1), + ), + ASN1F_optional( + ASN1F_STRING("paValue", "", explicit_tag=0xA2), + ), + ) + + +class PA_AUTHENTICATION_SET(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "elems", [PA_AUTHENTICATION_SET_ELEM()], PA_AUTHENTICATION_SET_ELEM + ) + + +_PADATA_CLASSES[134] = PA_AUTHENTICATION_SET + + +# [MS-KILE] sect 2.2.3 + + +class PA_PAC_REQUEST(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("includePac", True, explicit_tag=0xA0), + ) + + +_PADATA_CLASSES[128] = PA_PAC_REQUEST + + +# [MS-KILE] sect 2.2.5 + + +class LSAP_TOKEN_INFO_INTEGRITY(Packet): + fields_desc = [ + FlagsField( + "Flags", + 0, + -32, + { + 0x00000001: "UAC-Restricted", + }, + ), + LEIntEnumField( + "TokenIL", + 0x00002000, + { + 0x00000000: "Untrusted", + 0x00001000: "Low", + 0x00002000: "Medium", + 0x00003000: "High", + 0x00004000: "System", + 0x00005000: "Protected process", + }, + ), + MayEnd(XStrFixedLenField("MachineID", b"", length=32)), + # KB 5068222 - still waiting for [MS-KILE] update (oct. 2025) + XStrFixedLenField("PermanentMachineID", b"", length=32), + ] + + +# [MS-KILE] sect 2.2.6 + + +class _KerbAdRestrictionEntry_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_KerbAdRestrictionEntry_Field, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.restrictionType.val == 0x0000: # LSAP_TOKEN_INFO_INTEGRITY + return LSAP_TOKEN_INFO_INTEGRITY(val[0].val, _underlayer=pkt), val[1] + return val + + +class KERB_AD_RESTRICTION_ENTRY(ASN1_Packet): + name = "KERB-AD-RESTRICTION-ENTRY" + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "restrictionType", + 0, + {0: "LSAP_TOKEN_INFO_INTEGRITY"}, + explicit_tag=0xA0, + ), + _KerbAdRestrictionEntry_Field("restriction", b"", explicit_tag=0xA1), + ) + ) + + +_AUTHORIZATIONDATA_VALUES[141] = KERB_AD_RESTRICTION_ENTRY + + +# [MS-KILE] sect 3.2.5.8 + + +class KERB_AUTH_DATA_AP_OPTIONS(Packet): + name = "KERB-AUTH-DATA-AP-OPTIONS" + fields_desc = [ + FlagsField( + "apOptions", + 0x4000, + -32, + { + 0x4000: "KERB_AP_OPTIONS_CBT", + 0x8000: "KERB_AP_OPTIONS_UNVERIFIED_TARGET_NAME", + }, + ), + ] + + +_AUTHORIZATIONDATA_VALUES[143] = KERB_AUTH_DATA_AP_OPTIONS + + +# This has no doc..? [MS-KILE] only mentions its name. + + +class KERB_AUTH_DATA_CLIENT_TARGET(Packet): + name = "KERB-AD-TARGET-PRINCIPAL" + fields_desc = [ + StrFieldUtf16("spn", ""), + ] + + +_AUTHORIZATIONDATA_VALUES[144] = KERB_AUTH_DATA_CLIENT_TARGET + + +# RFC6806 sect 6 + + +class KERB_AD_LOGIN_ALIAS(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE(ASN1F_SEQUENCE_OF("loginAliases", [], PrincipalName)) + + +_AUTHORIZATIONDATA_VALUES[80] = KERB_AD_LOGIN_ALIAS + + +# [MS-KILE] sect 2.2.8 + + +class PA_SUPPORTED_ENCTYPES(Packet): + fields_desc = [ + FlagsField( + "flags", + 0, + -32, + [ + "DES-CBC-CRC", + "DES-CBC-MD5", + "RC4-HMAC", + "AES128-CTS-HMAC-SHA1-96", + "AES256-CTS-HMAC-SHA1-96", + ] + + ["bit_%d" % i for i in range(11)] + + [ + "FAST-supported", + "Compount-identity-supported", + "Claims-supported", + "Resource-SID-compression-disabled", + ], + ) + ] + + +_PADATA_CLASSES[165] = PA_SUPPORTED_ENCTYPES + +# [MS-KILE] sect 2.2.10 + + +class PA_PAC_OPTIONS(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KerberosFlags( + "options", + "", + [ + "Claims", + "Branch-Aware", + "Forward-to-Full-DC", + "Resource-based-constrained-delegation", # [MS-SFU] 2.2.5 + ], + explicit_tag=0xA0, + ) + ) + + +_PADATA_CLASSES[167] = PA_PAC_OPTIONS + +# [MS-KILE] sect 2.2.11 + + +class KERB_KEY_LIST_REQ(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "keytypes", + [], + ASN1F_enum_INTEGER("", 0, _KRB_E_TYPES), + ) + + +_PADATA_CLASSES[161] = KERB_KEY_LIST_REQ + +# [MS-KILE] sect 2.2.12 + + +class KERB_KEY_LIST_REP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "keys", + [], + ASN1F_PACKET("", None, EncryptionKey), + ) + + +_PADATA_CLASSES[162] = KERB_KEY_LIST_REP + +# [MS-KILE] sect 2.2.13 + + +class KERB_SUPERSEDED_BY_USER(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("name", None, PrincipalName, explicit_tag=0xA0), + Realm("realm", None, explicit_tag=0xA1), + ) + + +_PADATA_CLASSES[170] = KERB_SUPERSEDED_BY_USER + + +# [MS-KILE] sect 2.2.14 + + +class KERB_DMSA_KEY_PACKAGE(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF( + "currentKeys", + [], + ASN1F_PACKET("", None, EncryptionKey), + explicit_tag=0xA0, + ), + ASN1F_optional( + ASN1F_SEQUENCE_OF( + "previousKeys", + [], + ASN1F_PACKET("", None, EncryptionKey), + explicit_tag=0xA1, + ), + ), + KerberosTime("expirationInterval", GeneralizedTime(), explicit_tag=0xA2), + KerberosTime("fetchInterval", GeneralizedTime(), explicit_tag=0xA4), + ) + + +_PADATA_CLASSES[171] = KERB_DMSA_KEY_PACKAGE + + +# RFC6113 sect 5.4.1 + + +class _KrbFastArmor_value_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_KrbFastArmor_value_Field, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.armorType.val == 1: # FX_FAST_ARMOR_AP_REQUEST + return KRB_AP_REQ(val[0].val, _underlayer=pkt), val[1] + return val + + +class KrbFastArmor(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "armorType", 1, {1: "FX_FAST_ARMOR_AP_REQUEST"}, explicit_tag=0xA0 + ), + _KrbFastArmor_value_Field("armorValue", "", explicit_tag=0xA1), + ) + + +# RFC6113 sect 5.4.2 + + +class KrbFastArmoredReq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_optional( + ASN1F_PACKET("armor", None, KrbFastArmor, explicit_tag=0xA0) + ), + ASN1F_PACKET("reqChecksum", Checksum(), Checksum, explicit_tag=0xA1), + ASN1F_PACKET("encFastReq", None, EncryptedData, explicit_tag=0xA2), + ) + ) + + +class PA_FX_FAST_REQUEST(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "armoredData", + ASN1_STRING(""), + ASN1F_PACKET("req", KrbFastArmoredReq, KrbFastArmoredReq, implicit_tag=0xA0), + ) + + +# RFC6113 sect 5.4.3 + + +class KrbFastArmoredRep(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_PACKET("encFastRep", None, EncryptedData, explicit_tag=0xA0), + ) + ) + + +class PA_FX_FAST_REPLY(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "armoredData", + ASN1_STRING(""), + ASN1F_PACKET("req", KrbFastArmoredRep, KrbFastArmoredRep, implicit_tag=0xA0), + ) + + +class KrbFastFinished(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KerberosTime("timestamp", GeneralizedTime(), explicit_tag=0xA0), + Microseconds("usec", 0, explicit_tag=0xA1), + Realm("crealm", "", explicit_tag=0xA2), + ASN1F_PACKET("cname", None, PrincipalName, explicit_tag=0xA3), + ASN1F_PACKET("ticketChecksum", Checksum(), Checksum, explicit_tag=0xA4), + ) + + +class KrbFastResponse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("padata", [PADATA()], PADATA, explicit_tag=0xA0), + ASN1F_optional( + ASN1F_PACKET("strengthenKey", None, EncryptionKey, explicit_tag=0xA1) + ), + ASN1F_optional( + ASN1F_PACKET( + "finished", KrbFastFinished(), KrbFastFinished, explicit_tag=0xA2 + ) + ), + UInt32("nonce", 0, explicit_tag=0xA3), + ) + + +_PADATA_CLASSES[136] = (PA_FX_FAST_REQUEST, PA_FX_FAST_REPLY) + + +# RFC 4556 - PKINIT + + +# sect 3.2.1 + + +class ExternalPrincipalIdentifier(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional( + ASN1F_STRING_ENCAPS( + "subjectName", None, X509_DirectoryName, implicit_tag=0x80 + ), + ), + ASN1F_optional( + ASN1F_STRING_ENCAPS( + "issuerAndSerialNumber", + None, + CMS_IssuerAndSerialNumber, + implicit_tag=0x81, + ), + ), + ASN1F_optional( + ASN1F_STRING("subjectKeyIdentifier", "", implicit_tag=0x82), + ), + ) + + +class PA_PK_AS_REQ(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS( + "signedAuthpack", + CMS_ContentInfo(), + CMS_ContentInfo, + implicit_tag=0x80, + ), + ASN1F_optional( + ASN1F_SEQUENCE_OF( + "trustedCertifiers", + None, + ExternalPrincipalIdentifier, + explicit_tag=0xA1, + ), + ), + ASN1F_optional( + ASN1F_STRING("kdcPkId", "", implicit_tag=0xA2), + ), + ) + + +_PADATA_CLASSES[16] = PA_PK_AS_REQ + + +# [MS-PKCA] sect 2.2.3 + + +class PAChecksum2(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING("checksum", "", explicit_tag=0xA0), + ASN1F_PACKET( + "algorithmIdentifier", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier, + explicit_tag=0xA1, + ), + ) + + def verify(self, text): + """ + Verify a checksum of text. + + :param text: the bytes to verify + """ + # [MS-PKCA] 2.2.3 - PAChecksum2 + + # Only some OIDs are supported. Dumb but readable code. + oid = self.algorithmIdentifier.algorithm.val + if oid == "1.3.14.3.2.26": + hashcls = Hash_SHA + elif oid == "2.16.840.1.101.3.4.2.1": + hashcls = Hash_SHA256 + elif oid == "2.16.840.1.101.3.4.2.2": + hashcls = Hash_SHA384 + elif oid == "2.16.840.1.101.3.4.2.3": + hashcls = Hash_SHA512 + else: + raise ValueError("Bad PAChecksum2 checksum !") + + if hashcls().digest(text) != self.checksum.val: + raise ValueError("Bad PAChecksum2 checksum !") + + def make(self, text, h="sha256"): + """ + Make a checksum. + + :param text: the bytes to make a checksum of + """ + # Only some OIDs are supported. Dumb but readable code. + if h == "sha1": + hashcls = Hash_SHA + self.algorithmIdentifier.algorithm = ASN1_OID("1.3.14.3.2.26") + elif h == "sha256": + hashcls = Hash_SHA256 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.1") + elif h == "sha384": + hashcls = Hash_SHA384 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.2") + elif h == "sha512": + hashcls = Hash_SHA512 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.3") + else: + raise ValueError("Bad PAChecksum2 checksum !") + + self.checksum = ASN1_STRING(hashcls().digest(text)) + + +# still RFC 4556 sect 3.2.1 + + +class KRB_PKAuthenticator(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + Microseconds("cusec", 0, explicit_tag=0xA0), + KerberosTime("ctime", GeneralizedTime(), explicit_tag=0xA1), + UInt32("nonce", 0, explicit_tag=0xA2), + ASN1F_optional( + ASN1F_STRING("paChecksum", "", explicit_tag=0xA3), + ), + # RFC8070 extension + ASN1F_optional( + ASN1F_STRING("freshnessToken", None, explicit_tag=0xA4), + ), + # [MS-PKCA] sect 2.2.3 + ASN1F_optional( + ASN1F_PACKET("paChecksum2", None, PAChecksum2, explicit_tag=0xA5), + ), + ) + + def make_checksum(self, text, h: str = "sha256"): + """ + Populate paChecksum + """ + # paChecksum (always sha-1) + self.paChecksum = ASN1_STRING(Hash_SHA().digest(text)) + + # paChecksum2 + if h != "sha1": + self.paChecksum2 = PAChecksum2() + self.paChecksum2.make(text, h=h) + + def verify_checksum(self, text): + """ + Verify paChecksum and paChecksum2 + """ + if self.paChecksum.val != Hash_SHA().digest(text): + raise ValueError("Bad paChecksum checksum !") + + if self.paChecksum2 is not None: + self.paChecksum2.verify(text) + + +# RFC8636 sect 6 + + +class KDFAlgorithmId(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("kdfId", "", explicit_tag=0xA0), + ) + + +# still RFC 4556 sect 3.2.1 + + +class KRB_AuthPack(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET( + "pkAuthenticator", + KRB_PKAuthenticator(), + KRB_PKAuthenticator, + explicit_tag=0xA0, + ), + ASN1F_optional( + ASN1F_PACKET( + "clientPublicValue", + X509_SubjectPublicKeyInfo(), + X509_SubjectPublicKeyInfo, + explicit_tag=0xA1, + ), + ), + ASN1F_optional( + ASN1F_SEQUENCE_OF( + "supportedCMSTypes", + None, + X509_AlgorithmIdentifier, + explicit_tag=0xA2, + ), + ), + ASN1F_optional( + ASN1F_STRING("clientDHNonce", None, explicit_tag=0xA3), + ), + # RFC8636 extension + ASN1F_optional( + ASN1F_SEQUENCE_OF("supportedKDFs", None, KDFAlgorithmId, explicit_tag=0xA4), + ), + ) + + +_CMS_ENCAPSULATED["1.3.6.1.5.2.3.1"] = KRB_AuthPack + +# sect 3.2.3 + + +class DHRepInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS( + "dhSignedData", + CMS_ContentInfo(), + CMS_ContentInfo, + implicit_tag=0x80, + ), + ASN1F_optional( + ASN1F_STRING("serverDHNonce", "", explicit_tag=0xA1), + ), + # RFC8636 extension + ASN1F_optional( + ASN1F_PACKET("kdf", None, KDFAlgorithmId, explicit_tag=0xA2), + ), + ) + + +class EncKeyPack(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_STRING("encKeyPack", "") + + +class PA_PK_AS_REP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "rep", + ASN1_STRING(""), + ASN1F_PACKET("dhInfo", DHRepInfo(), DHRepInfo, explicit_tag=0xA0), + ASN1F_PACKET("encKeyPack", EncKeyPack(), EncKeyPack, explicit_tag=0xA1), + ) + + +_PADATA_CLASSES[17] = PA_PK_AS_REP + + +class KDCDHKeyInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS( + "subjectPublicKey", DHPublicKey(), DHPublicKey, explicit_tag=0xA0 + ), + UInt32("nonce", 0, explicit_tag=0xA1), + ASN1F_optional( + KerberosTime("dhKeyExpiration", None, explicit_tag=0xA2), + ), + ) + + +_CMS_ENCAPSULATED["1.3.6.1.5.2.3.2"] = KDCDHKeyInfo + +# [MS-SFU] + + +# sect 2.2.1 +class PA_FOR_USER(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("userName", PrincipalName(), PrincipalName, explicit_tag=0xA0), + Realm("userRealm", "", explicit_tag=0xA1), + ASN1F_PACKET("cksum", Checksum(), Checksum, explicit_tag=0xA2), + KerberosString("authPackage", "Kerberos", explicit_tag=0xA3), + ) + + +_PADATA_CLASSES[129] = PA_FOR_USER + + +# sect 2.2.2 + + +class S4UUserID(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + UInt32("nonce", 0, explicit_tag=0xA0), + ASN1F_optional( + ASN1F_PACKET("cname", None, PrincipalName, explicit_tag=0xA1), + ), + Realm("crealm", "", explicit_tag=0xA2), + ASN1F_optional( + ASN1F_STRING("subjectCertificate", None, explicit_tag=0xA3), + ), + ASN1F_optional( + ASN1F_FLAGS( + "options", + "", + [ + "reserved", + "KDC_CHECK_LOGON_HOUR_RESTRICTIONS", + "USE_REPLY_KEY_USAGE", + "NT_AUTH_POLICY_NOT_REQUIRED", + "UNCONDITIONAL_DELEGATION", + ], + explicit_tag=0xA4, + ) + ), + ) + + +class PA_S4U_X509_USER(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("userId", S4UUserID(), S4UUserID, explicit_tag=0xA0), + ASN1F_PACKET("checksum", Checksum(), Checksum, explicit_tag=0xA1), + ) + + +_PADATA_CLASSES[130] = PA_S4U_X509_USER + + +# Back to RFC4120 + +# sect 5.10 +KRB_MSG_TYPES = { + 1: "Ticket", + 2: "Authenticator", + 3: "EncTicketPart", + 10: "AS-REQ", + 11: "AS-REP", + 12: "TGS-REQ", + 13: "TGS-REP", + 14: "AP-REQ", + 15: "AP-REP", + 16: "KRB-TGT-REQ", # U2U + 17: "KRB-TGT-REP", # U2U + 20: "KRB-SAFE", + 21: "KRB-PRIV", + 22: "KRB-CRED", + 25: "EncASRepPart", + 26: "EncTGSRepPart", + 27: "EncAPRepPart", + 28: "EncKrbPrivPart", + 29: "EnvKrbCredPart", + 30: "KRB-ERROR", +} + +# sect 5.3 + + +class KRB_Ticket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("tktVno", 5, explicit_tag=0xA0), + Realm("realm", "", explicit_tag=0xA1), + ASN1F_PACKET("sname", PrincipalName(), PrincipalName, explicit_tag=0xA2), + ASN1F_PACKET("encPart", EncryptedData(), EncryptedData, explicit_tag=0xA3), + ), + implicit_tag=ASN1_Class_KRB.Ticket, + ) + + def getSPN(self): + return "%s@%s" % ( + self.sname.toString(), + self.realm.val.decode(), + ) + + +class TransitedEncoding(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + Int32("trType", 0, explicit_tag=0xA0), + ASN1F_STRING("contents", "", explicit_tag=0xA1), + ) + + +_TICKET_FLAGS = [ + "reserved", + "forwardable", + "forwarded", + "proxiable", + "proxy", + "may-postdate", + "postdated", + "invalid", + "renewable", + "initial", + "pre-authent", + "hw-authent", + "transited-since-policy-checked", + "ok-as-delegate", + "unused", + "canonicalize", # RFC6806 + "anonymous", # RFC6112 + RFC8129 +] + + +class EncTicketPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + KerberosFlags( + "flags", + "", + _TICKET_FLAGS, + explicit_tag=0xA0, + ), + ASN1F_PACKET("key", EncryptionKey(), EncryptionKey, explicit_tag=0xA1), + Realm("crealm", "", explicit_tag=0xA2), + ASN1F_PACKET("cname", PrincipalName(), PrincipalName, explicit_tag=0xA3), + ASN1F_PACKET( + "transited", TransitedEncoding(), TransitedEncoding, explicit_tag=0xA4 + ), + KerberosTime("authtime", GeneralizedTime(), explicit_tag=0xA5), + ASN1F_optional( + KerberosTime("starttime", GeneralizedTime(), explicit_tag=0xA6) + ), + KerberosTime("endtime", GeneralizedTime(), explicit_tag=0xA7), + ASN1F_optional( + KerberosTime("renewTill", GeneralizedTime(), explicit_tag=0xA8), + ), + ASN1F_optional( + HostAddresses("addresses", explicit_tag=0xA9), + ), + ASN1F_optional( + ASN1F_PACKET( + "authorizationData", None, AuthorizationData, explicit_tag=0xAA + ), + ), + ), + implicit_tag=ASN1_Class_KRB.EncTicketPart, + ) + + +# sect 5.4.1 + + +class KRB_KDC_REQ_BODY(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KerberosFlags( + "kdcOptions", + "", + [ + "reserved", + "forwardable", + "forwarded", + "proxiable", + "proxy", + "allow-postdate", + "postdated", + "unused7", + "renewable", + "unused9", + "unused10", + "opt-hardware-auth", + "unused12", + "unused13", + "cname-in-addl-tkt", # [MS-SFU] sect 2.2.3 + "canonicalize", # RFC6806 + "request-anonymous", # RFC6112 + RFC8129 + ] + + ["unused%d" % i for i in range(17, 26)] + + [ + "disable-transited-check", + "renewable-ok", + "enc-tkt-in-skey", + "unused29", + "renew", + "validate", + ], + explicit_tag=0xA0, + ), + ASN1F_optional(ASN1F_PACKET("cname", None, PrincipalName, explicit_tag=0xA1)), + Realm("realm", "", explicit_tag=0xA2), + ASN1F_optional( + ASN1F_PACKET("sname", None, PrincipalName, explicit_tag=0xA3), + ), + ASN1F_optional(KerberosTime("from_", None, explicit_tag=0xA4)), + KerberosTime("till", GeneralizedTime(), explicit_tag=0xA5), + ASN1F_optional(KerberosTime("rtime", GeneralizedTime(), explicit_tag=0xA6)), + UInt32("nonce", 0, explicit_tag=0xA7), + ASN1F_SEQUENCE_OF("etype", [], Int32, explicit_tag=0xA8), + ASN1F_optional( + HostAddresses("addresses", explicit_tag=0xA9), + ), + ASN1F_optional( + ASN1F_PACKET( + "encAuthorizationData", None, EncryptedData, explicit_tag=0xAA + ), + ), + ASN1F_optional( + ASN1F_SEQUENCE_OF("additionalTickets", [], KRB_Ticket, explicit_tag=0xAB) + ), + ) + + +KRB_KDC_REQ = ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA1), + ASN1F_enum_INTEGER("msgType", 10, KRB_MSG_TYPES, explicit_tag=0xA2), + ASN1F_optional(ASN1F_SEQUENCE_OF("padata", [], PADATA, explicit_tag=0xA3)), + ASN1F_PACKET("reqBody", KRB_KDC_REQ_BODY(), KRB_KDC_REQ_BODY, explicit_tag=0xA4), +) + + +class KrbFastReq(ASN1_Packet): + # RFC6113 sect 5.4.2 + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KerberosFlags( + "fastOptions", + "", + [ + "RESERVED", + "hide-client-names", + ] + + ["res%d" % i for i in range(2, 16)] + + ["kdc-follow-referrals"], + explicit_tag=0xA0, + ), + ASN1F_SEQUENCE_OF("padata", [PADATA()], PADATA, explicit_tag=0xA1), + ASN1F_PACKET("reqBody", None, KRB_KDC_REQ_BODY, explicit_tag=0xA2), + ) + + +class KRB_AS_REQ(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KRB_KDC_REQ, + implicit_tag=ASN1_Class_KRB.AS_REQ, + ) + + +class KRB_TGS_REQ(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KRB_KDC_REQ, + implicit_tag=ASN1_Class_KRB.TGS_REQ, + ) + msgType = ASN1_INTEGER(12) + + +# sect 5.4.2 + +KRB_KDC_REP = ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 11, KRB_MSG_TYPES, explicit_tag=0xA1), + ASN1F_optional( + ASN1F_SEQUENCE_OF("padata", [], PADATA, explicit_tag=0xA2), + ), + Realm("crealm", "", explicit_tag=0xA3), + ASN1F_PACKET("cname", None, PrincipalName, explicit_tag=0xA4), + ASN1F_PACKET("ticket", None, KRB_Ticket, explicit_tag=0xA5), + ASN1F_PACKET("encPart", None, EncryptedData, explicit_tag=0xA6), +) + + +class KRB_AS_REP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KRB_KDC_REP, + implicit_tag=ASN1_Class_KRB.AS_REP, + ) + + def getUPN(self): + return "%s@%s" % ( + self.cname.toString(), + self.crealm.val.decode(), + ) + + +class KRB_TGS_REP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + KRB_KDC_REP, + implicit_tag=ASN1_Class_KRB.TGS_REP, + ) + + def getUPN(self): + return "%s@%s" % ( + self.cname.toString(), + self.crealm.val.decode(), + ) + + +class LastReqItem(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + Int32("lrType", 0, explicit_tag=0xA0), + KerberosTime("lrValue", GeneralizedTime(), explicit_tag=0xA1), + ) + + +EncKDCRepPart = ASN1F_SEQUENCE( + ASN1F_PACKET("key", None, EncryptionKey, explicit_tag=0xA0), + ASN1F_SEQUENCE_OF("lastReq", [], LastReqItem, explicit_tag=0xA1), + UInt32("nonce", 0, explicit_tag=0xA2), + ASN1F_optional( + KerberosTime("keyExpiration", GeneralizedTime(), explicit_tag=0xA3), + ), + KerberosFlags( + "flags", + "", + _TICKET_FLAGS, + explicit_tag=0xA4, + ), + KerberosTime("authtime", GeneralizedTime(), explicit_tag=0xA5), + ASN1F_optional( + KerberosTime("starttime", GeneralizedTime(), explicit_tag=0xA6), + ), + KerberosTime("endtime", GeneralizedTime(), explicit_tag=0xA7), + ASN1F_optional( + KerberosTime("renewTill", GeneralizedTime(), explicit_tag=0xA8), + ), + Realm("srealm", "", explicit_tag=0xA9), + ASN1F_PACKET("sname", PrincipalName(), PrincipalName, explicit_tag=0xAA), + ASN1F_optional( + HostAddresses("caddr", explicit_tag=0xAB), + ), + # RFC6806 sect 11 + ASN1F_optional( + ASN1F_SEQUENCE_OF("encryptedPaData", [], PADATA, explicit_tag=0xAC), + ), +) + + +class EncASRepPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + EncKDCRepPart, + implicit_tag=ASN1_Class_KRB.EncASRepPart, + ) + + +class EncTGSRepPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + EncKDCRepPart, + implicit_tag=ASN1_Class_KRB.EncTGSRepPart, + ) + + +# sect 5.5.1 + + +class KRB_AP_REQ(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 14, KRB_MSG_TYPES, explicit_tag=0xA1), + KerberosFlags( + "apOptions", + "", + [ + "reserved", + "use-session-key", + "mutual-required", + ], + explicit_tag=0xA2, + ), + ASN1F_PACKET("ticket", None, KRB_Ticket, explicit_tag=0xA3), + ASN1F_PACKET("authenticator", None, EncryptedData, explicit_tag=0xA4), + ), + implicit_tag=ASN1_Class_KRB.AP_REQ, + ) + + +_PADATA_CLASSES[1] = KRB_AP_REQ + + +class KRB_Authenticator(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("authenticatorPvno", 5, explicit_tag=0xA0), + Realm("crealm", "", explicit_tag=0xA1), + ASN1F_PACKET("cname", None, PrincipalName, explicit_tag=0xA2), + ASN1F_optional( + ASN1F_PACKET("cksum", None, Checksum, explicit_tag=0xA3), + ), + Microseconds("cusec", 0, explicit_tag=0xA4), + KerberosTime("ctime", GeneralizedTime(), explicit_tag=0xA5), + ASN1F_optional( + ASN1F_PACKET("subkey", None, EncryptionKey, explicit_tag=0xA6), + ), + ASN1F_optional( + UInt32("seqNumber", 0, explicit_tag=0xA7), + ), + ASN1F_optional( + ASN1F_PACKET( + "encAuthorizationData", None, AuthorizationData, explicit_tag=0xA8 + ), + ), + ), + implicit_tag=ASN1_Class_KRB.Authenticator, + ) + + +# sect 5.5.2 + + +class KRB_AP_REP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 15, KRB_MSG_TYPES, explicit_tag=0xA1), + ASN1F_PACKET("encPart", None, EncryptedData, explicit_tag=0xA2), + ), + implicit_tag=ASN1_Class_KRB.AP_REP, + ) + + +class EncAPRepPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + KerberosTime("ctime", GeneralizedTime(), explicit_tag=0xA0), + Microseconds("cusec", 0, explicit_tag=0xA1), + ASN1F_optional( + ASN1F_PACKET("subkey", None, EncryptionKey, explicit_tag=0xA2), + ), + ASN1F_optional( + UInt32("seqNumber", 0, explicit_tag=0xA3), + ), + ), + implicit_tag=ASN1_Class_KRB.EncAPRepPart, + ) + + +# sect 5.7 + + +class KRB_PRIV(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 21, KRB_MSG_TYPES, explicit_tag=0xA1), + ASN1F_PACKET("encPart", None, EncryptedData, explicit_tag=0xA3), + ), + implicit_tag=ASN1_Class_KRB.PRIV, + ) + + +class EncKrbPrivPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_STRING("userData", ASN1_STRING(""), explicit_tag=0xA0), + ASN1F_optional( + KerberosTime("timestamp", None, explicit_tag=0xA1), + ), + ASN1F_optional( + Microseconds("usec", None, explicit_tag=0xA2), + ), + ASN1F_optional( + UInt32("seqNumber", None, explicit_tag=0xA3), + ), + ASN1F_PACKET("sAddress", None, HostAddress, explicit_tag=0xA4), + ASN1F_optional( + ASN1F_PACKET("cAddress", None, HostAddress, explicit_tag=0xA5), + ), + ), + implicit_tag=ASN1_Class_KRB.EncKrbPrivPart, + ) + + +# sect 5.8 + + +class KRB_CRED(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 22, KRB_MSG_TYPES, explicit_tag=0xA1), + ASN1F_SEQUENCE_OF("tickets", [KRB_Ticket()], KRB_Ticket, explicit_tag=0xA2), + ASN1F_PACKET("encPart", None, EncryptedData, explicit_tag=0xA3), + ), + implicit_tag=ASN1_Class_KRB.CRED, + ) + + +class KrbCredInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("key", EncryptionKey(), EncryptionKey, explicit_tag=0xA0), + ASN1F_optional( + Realm("prealm", None, explicit_tag=0xA1), + ), + ASN1F_optional( + ASN1F_PACKET("pname", None, PrincipalName, explicit_tag=0xA2), + ), + ASN1F_optional( + KerberosFlags( + "flags", + None, + _TICKET_FLAGS, + explicit_tag=0xA3, + ), + ), + ASN1F_optional( + KerberosTime("authtime", None, explicit_tag=0xA4), + ), + ASN1F_optional(KerberosTime("starttime", None, explicit_tag=0xA5)), + ASN1F_optional( + KerberosTime("endtime", None, explicit_tag=0xA6), + ), + ASN1F_optional( + KerberosTime("renewTill", None, explicit_tag=0xA7), + ), + ASN1F_optional( + Realm("srealm", None, explicit_tag=0xA8), + ), + ASN1F_optional( + ASN1F_PACKET("sname", None, PrincipalName, explicit_tag=0xA9), + ), + ASN1F_optional( + HostAddresses("caddr", explicit_tag=0xAA), + ), + ) + + +class EncKrbCredPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF( + "ticketInfo", + [KrbCredInfo()], + KrbCredInfo, + explicit_tag=0xA0, + ), + ASN1F_optional( + UInt32("nonce", None, explicit_tag=0xA1), + ), + ASN1F_optional( + KerberosTime("timestamp", None, explicit_tag=0xA2), + ), + ASN1F_optional( + Microseconds("usec", None, explicit_tag=0xA3), + ), + ASN1F_optional( + ASN1F_PACKET("sAddress", None, HostAddress, explicit_tag=0xA4), + ), + ASN1F_optional( + ASN1F_PACKET("cAddress", None, HostAddress, explicit_tag=0xA5), + ), + ), + implicit_tag=ASN1_Class_KRB.EncKrbCredPart, + ) + + +# sect 5.9.1 + + +class MethodData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("seq", [PADATA()], PADATA) + + +class _KRBERROR_data_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_KRBERROR_data_Field, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.errorCode.val in [14, 24, 25, 36, 80]: + # 14: KDC_ERR_ETYPE_NOSUPP + # 24: KDC_ERR_PREAUTH_FAILED + # 25: KDC_ERR_PREAUTH_REQUIRED + # 36: KRB_AP_ERR_BADMATCH + # 80: KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED + return MethodData(val[0].val, _underlayer=pkt), val[1] + elif pkt.errorCode.val in [6, 7, 12, 13, 18, 29, 32, 41, 60, 62]: + # 6: KDC_ERR_C_PRINCIPAL_UNKNOWN + # 7: KDC_ERR_S_PRINCIPAL_UNKNOWN + # 12: KDC_ERR_POLICY + # 13: KDC_ERR_BADOPTION + # 18: KDC_ERR_CLIENT_REVOKED + # 29: KDC_ERR_SVC_UNAVAILABLE + # 32: KRB_AP_ERR_TKT_EXPIRED + # 41: KRB_AP_ERR_MODIFIED + # 60: KRB_ERR_GENERIC + # 62: KERB_ERR_TYPE_EXTENDED + try: + return KERB_ERROR_DATA(val[0].val, _underlayer=pkt), val[1] + except BER_Decoding_Error: + if pkt.errorCode.val in [18, 12]: + # Some types can also happen in FAST sessions + # 18: KDC_ERR_CLIENT_REVOKED + return MethodData(val[0].val, _underlayer=pkt), val[1] + elif pkt.errorCode.val == 7: + # This looks like an undocumented structure. + # 7: KDC_ERR_S_PRINCIPAL_UNKNOWN + return KERB_ERROR_UNK(val[0].val, _underlayer=pkt), val[1] + raise + elif pkt.errorCode.val == 69: + # KRB_AP_ERR_USER_TO_USER_REQUIRED + return KRB_TGT_REP(val[0].val, _underlayer=pkt), val[1] + return val + + +class KRB_ERROR(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 30, KRB_MSG_TYPES, explicit_tag=0xA1), + ASN1F_optional( + KerberosTime("ctime", None, explicit_tag=0xA2), + ), + ASN1F_optional( + Microseconds("cusec", None, explicit_tag=0xA3), + ), + KerberosTime("stime", GeneralizedTime(), explicit_tag=0xA4), + Microseconds("susec", 0, explicit_tag=0xA5), + ASN1F_enum_INTEGER( + "errorCode", + 0, + { + # RFC4120 sect 7.5.9 + 0: "KDC_ERR_NONE", + 1: "KDC_ERR_NAME_EXP", + 2: "KDC_ERR_SERVICE_EXP", + 3: "KDC_ERR_BAD_PVNO", + 4: "KDC_ERR_C_OLD_MAST_KVNO", + 5: "KDC_ERR_S_OLD_MAST_KVNO", + 6: "KDC_ERR_C_PRINCIPAL_UNKNOWN", + 7: "KDC_ERR_S_PRINCIPAL_UNKNOWN", + 8: "KDC_ERR_PRINCIPAL_NOT_UNIQUE", + 9: "KDC_ERR_NULL_KEY", + 10: "KDC_ERR_CANNOT_POSTDATE", + 11: "KDC_ERR_NEVER_VALID", + 12: "KDC_ERR_POLICY", + 13: "KDC_ERR_BADOPTION", + 14: "KDC_ERR_ETYPE_NOSUPP", + 15: "KDC_ERR_SUMTYPE_NOSUPP", + 16: "KDC_ERR_PADATA_TYPE_NOSUPP", + 17: "KDC_ERR_TRTYPE_NOSUPP", + 18: "KDC_ERR_CLIENT_REVOKED", + 19: "KDC_ERR_SERVICE_REVOKED", + 20: "KDC_ERR_TGT_REVOKED", + 21: "KDC_ERR_CLIENT_NOTYET", + 22: "KDC_ERR_SERVICE_NOTYET", + 23: "KDC_ERR_KEY_EXPIRED", + 24: "KDC_ERR_PREAUTH_FAILED", + 25: "KDC_ERR_PREAUTH_REQUIRED", + 26: "KDC_ERR_SERVER_NOMATCH", + 27: "KDC_ERR_MUST_USE_USER2USER", + 28: "KDC_ERR_PATH_NOT_ACCEPTED", + 29: "KDC_ERR_SVC_UNAVAILABLE", + 31: "KRB_AP_ERR_BAD_INTEGRITY", + 32: "KRB_AP_ERR_TKT_EXPIRED", + 33: "KRB_AP_ERR_TKT_NYV", + 34: "KRB_AP_ERR_REPEAT", + 35: "KRB_AP_ERR_NOT_US", + 36: "KRB_AP_ERR_BADMATCH", + 37: "KRB_AP_ERR_SKEW", + 38: "KRB_AP_ERR_BADADDR", + 39: "KRB_AP_ERR_BADVERSION", + 40: "KRB_AP_ERR_MSG_TYPE", + 41: "KRB_AP_ERR_MODIFIED", + 42: "KRB_AP_ERR_BADORDER", + 44: "KRB_AP_ERR_BADKEYVER", + 45: "KRB_AP_ERR_NOKEY", + 46: "KRB_AP_ERR_MUT_FAIL", + 47: "KRB_AP_ERR_BADDIRECTION", + 48: "KRB_AP_ERR_METHOD", + 49: "KRB_AP_ERR_BADSEQ", + 50: "KRB_AP_ERR_INAPP_CKSUM", + 51: "KRB_AP_PATH_NOT_ACCEPTED", + 52: "KRB_ERR_RESPONSE_TOO_BIG", + 60: "KRB_ERR_GENERIC", + 61: "KRB_ERR_FIELD_TOOLONG", + # RFC4556 + 62: "KDC_ERR_CLIENT_NOT_TRUSTED", + 63: "KDC_ERR_KDC_NOT_TRUSTED", + 64: "KDC_ERR_INVALID_SIG", + 65: "KDC_ERR_KEY_TOO_WEAK", + 66: "KDC_ERR_CERTIFICATE_MISMATCH", + 67: "KRB_AP_ERR_NO_TGT", + 68: "KDC_ERR_WRONG_REALM", + 69: "KRB_AP_ERR_USER_TO_USER_REQUIRED", + 70: "KDC_ERR_CANT_VERIFY_CERTIFICATE", + 71: "KDC_ERR_INVALID_CERTIFICATE", + 72: "KDC_ERR_REVOKED_CERTIFICATE", + 73: "KDC_ERR_REVOCATION_STATUS_UNKNOWN", + 74: "KDC_ERR_REVOCATION_STATUS_UNAVAILABLE", + 75: "KDC_ERR_CLIENT_NAME_MISMATCH", + 76: "KDC_ERR_KDC_NAME_MISMATCH", + 77: "KDC_ERR_INCONSISTENT_KEY_PURPOSE", + 78: "KDC_ERR_DIGEST_IN_CERT_NOT_ACCEPTED", + 79: "KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED", + 80: "KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED", + 81: "KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED", + # draft-ietf-kitten-iakerb + 85: "KRB_AP_ERR_IAKERB_KDC_NOT_FOUND", + 86: "KRB_AP_ERR_IAKERB_KDC_NO_RESPONSE", + # RFC6113 + 90: "KDC_ERR_PREAUTH_EXPIRED", + 91: "KDC_ERR_MORE_PREAUTH_DATA_REQUIRED", + 92: "KDC_ERR_PREAUTH_BAD_AUTHENTICATION_SET", + 93: "KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS", + # RFC8636 + 100: "KDC_ERR_NO_ACCEPTABLE_KDF", + }, + explicit_tag=0xA6, + ), + ASN1F_optional(Realm("crealm", None, explicit_tag=0xA7)), + ASN1F_optional( + ASN1F_PACKET("cname", None, PrincipalName, explicit_tag=0xA8), + ), + Realm("realm", "", explicit_tag=0xA9), + ASN1F_PACKET("sname", PrincipalName(), PrincipalName, explicit_tag=0xAA), + ASN1F_optional(KerberosString("eText", "", explicit_tag=0xAB)), + ASN1F_optional(_KRBERROR_data_Field("eData", "", explicit_tag=0xAC)), + ), + implicit_tag=ASN1_Class_KRB.ERROR, + ) + + def getSPN(self): + return "%s@%s" % ( + self.sname.toString(), + self.realm.val.decode(), + ) + + +# PA-FX-ERROR +_PADATA_CLASSES[137] = KRB_ERROR + + +# [MS-KILE] sect 2.2.1 + + +class KERB_EXT_ERROR(Packet): + fields_desc = [ + XLEIntEnumField("status", 0, STATUS_ERREF), + XLEIntField("reserved", 0), + XLEIntField("flags", 0x00000001), + ] + + +# [MS-KILE] sect 2.2.2 + + +class _Error_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_Error_Field, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.dataType.val == 3: # KERB_ERR_TYPE_EXTENDED + return KERB_EXT_ERROR(val[0].val, _underlayer=pkt), val[1] + return val + + +class KERB_ERROR_DATA(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "dataType", + 2, + { + 1: "KERB_AP_ERR_TYPE_NTSTATUS", # from the wdk + 2: "KERB_AP_ERR_TYPE_SKEW_RECOVERY", + 3: "KERB_ERR_TYPE_EXTENDED", + }, + explicit_tag=0xA1, + ), + ASN1F_optional(_Error_Field("dataValue", None, explicit_tag=0xA2)), + ) + + +# This looks like an undocumented structure. + + +class KERB_ERROR_UNK(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_enum_INTEGER( + "dataType", + 0, + { + -128: "KDC_ERR_MUST_USE_USER2USER", + }, + explicit_tag=0xA0, + ), + ASN1F_STRING("dataValue", None, explicit_tag=0xA1), + ) + ) + + +# Kerberos U2U - draft-ietf-cat-user2user-03 + + +class KRB_TGT_REQ(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 16, KRB_MSG_TYPES, explicit_tag=0xA1), + ASN1F_optional( + ASN1F_PACKET("sname", None, PrincipalName, explicit_tag=0xA2), + ), + ASN1F_optional( + Realm("realm", None, explicit_tag=0xA3), + ), + ) + + +class KRB_TGT_REP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), + ASN1F_enum_INTEGER("msgType", 17, KRB_MSG_TYPES, explicit_tag=0xA1), + ASN1F_PACKET("ticket", None, KRB_Ticket, explicit_tag=0xA2), + ) + + +# draft-ietf-kitten-iakerb-03 sect 4 + + +class KRB_FINISHED(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("gssMic", Checksum(), Checksum, explicit_tag=0xA1), + ) + + +# RFC 6542 sect 3.1 + + +class KRB_GSS_EXT(Packet): + fields_desc = [ + IntEnumField( + "type", + 0, + { + # https://www.iana.org/assignments/kerberos-v-gss-api/kerberos-v-gss-api.xhtml + 0x00000000: "GSS_EXTS_CHANNEL_BINDING", # RFC 6542 sect 3.2 + 0x00000001: "GSS_EXTS_IAKERB_FINISHED", # not standard + 0x00000002: "GSS_EXTS_FINISHED", # PKU2U / IAKERB + }, + ), + FieldLenField("length", None, length_of="data", fmt="!I"), + MultipleTypeField( + [ + ( + PacketField("data", KRB_FINISHED(), KRB_FINISHED), + lambda pkt: pkt.type == 0x00000002, + ), + ], + XStrLenField("data", b"", length_from=lambda pkt: pkt.length), + ), + ] + + +# RFC 4121 sect 4.1.1 + + +class KRB_AuthenticatorChecksum(Packet): + fields_desc = [ + FieldLenField("Lgth", None, length_of="Bnd", fmt="= 13: + # Older RFC1964 variants of the token have KRB_GSSAPI_Token wrapper + if _pkt[2:13] == b"\x06\t*\x86H\x86\xf7\x12\x01\x02\x02": + return KRB_GSSAPI_Token + return cls + + +# RFC 4121 - sect 4.1 + + +class KRB_GSSAPI_Token(GSSAPI_BLOB): + name = "Kerberos GSSAPI-Token" + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("MechType", "1.2.840.113554.1.2.2"), + ASN1F_PACKET( + "innerToken", + KRB_InnerToken(), + KRB_InnerToken, + implicit_tag=0x0, + ), + implicit_tag=ASN1_Class_KRB.Token, + ) + + +# RFC 1964 - sect 1.2.1 + + +class KRB_GSS_MIC_RFC1964(Packet): + name = "Kerberos v5 MIC Token (RFC1964)" + fields_desc = [ + LEShortEnumField("SGN_ALG", 0, _SGN_ALGS), + XLEIntField("Filler", 0xFFFFFFFF), + XStrFixedLenField("SND_SEQ", b"", length=8), + PadField( # sect 1.2.2.3 + XStrFixedLenField("SGN_CKSUM", b"", length=8), + align=8, + padwith=b"\x04", + ), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_InitialContextTokens[b"\x01\x01"] = KRB_GSS_MIC_RFC1964 + +# RFC 1964 - sect 1.2.2 + + +class KRB_GSS_Wrap_RFC1964(Packet): + name = "Kerberos v5 GSS_Wrap (RFC1964)" + fields_desc = [ + LEShortEnumField("SGN_ALG", 0, _SGN_ALGS), + LEShortEnumField("SEAL_ALG", 0, _SEAL_ALGS), + XLEShortField("Filler", 0xFFFF), + XStrFixedLenField("SND_SEQ", b"", length=8), + PadField( # sect 1.2.2.3 + XStrFixedLenField("SGN_CKSUM", b"", length=8), + align=8, + padwith=b"\x04", + ), + # sect 1.2.2.3 + XStrFixedLenField("CONFOUNDER", b"", length=8), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_InitialContextTokens[b"\x02\x01"] = KRB_GSS_Wrap_RFC1964 + + +# RFC 1964 - sect 1.2.2 + + +class KRB_GSS_Delete_sec_context_RFC1964(Packet): + name = "Kerberos v5 GSS_Delete_sec_context (RFC1964)" + fields_desc = KRB_GSS_MIC_RFC1964.fields_desc + + +_InitialContextTokens[b"\x01\x02"] = KRB_GSS_Delete_sec_context_RFC1964 + + +# RFC 4121 - sect 4.2.2 +_KRB5_GSS_Flags = [ + "SentByAcceptor", + "Sealed", + "AcceptorSubkey", +] + + +# RFC 4121 - sect 4.2.6.1 + + +class KRB_GSS_MIC(Packet): + name = "Kerberos v5 MIC Token" + fields_desc = [ + FlagsField("Flags", 0, 8, _KRB5_GSS_Flags), + XStrFixedLenField("Filler", b"\xff\xff\xff\xff\xff", length=5), + LongField("SND_SEQ", 0), # Big endian + XStrField("SGN_CKSUM", b"\x00" * 12), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_InitialContextTokens[b"\x04\x04"] = KRB_GSS_MIC + + +# RFC 4121 - sect 4.2.6.2 + + +class KRB_GSS_Wrap(Packet): + name = "Kerberos v5 Wrap Token" + fields_desc = [ + FlagsField("Flags", 0, 8, _KRB5_GSS_Flags), + XByteField("Filler", 0xFF), + ShortField("EC", 0), # Big endian + ShortField("RRC", 0), # Big endian + LongField("SND_SEQ", 0), # Big endian + MultipleTypeField( + [ + ( + XStrField("Data", b""), + lambda pkt: pkt.Flags.Sealed, + ) + ], + XStrLenField("Data", b"", length_from=lambda pkt: pkt.EC), + ), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_InitialContextTokens[b"\x05\x04"] = KRB_GSS_Wrap + + +# Kerberos IAKERB - draft-ietf-kitten-iakerb-03 + + +class IAKERB_HEADER(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + Realm("targetRealm", "", explicit_tag=0xA1), + ASN1F_optional( + ASN1F_STRING("cookie", None, explicit_tag=0xA2), + ), + ) + + +_InitialContextTokens[b"\x05\x01"] = IAKERB_HEADER + + +# Register for GSSAPI + +# Kerberos 5 +_GSSAPI_OIDS["1.2.840.113554.1.2.2"] = KRB_InnerToken +_GSSAPI_SIGNATURE_OIDS["1.2.840.113554.1.2.2"] = KRB_InnerToken +# Kerberos 5 - U2U +_GSSAPI_OIDS["1.2.840.113554.1.2.2.3"] = KRB_InnerToken +# Kerberos 5 - IAKERB +_GSSAPI_OIDS["1.3.6.1.5.2.5"] = KRB_InnerToken + + +# Entry class + +# RFC4120 sect 5.10 + + +class Kerberos(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "root", + None, + # RFC4120 + KRB_GSSAPI_Token, # [APPLICATION 0] + KRB_Ticket, # [APPLICATION 1] + KRB_Authenticator, # [APPLICATION 2] + KRB_AS_REQ, # [APPLICATION 10] + KRB_AS_REP, # [APPLICATION 11] + KRB_TGS_REQ, # [APPLICATION 12] + KRB_TGS_REP, # [APPLICATION 13] + KRB_AP_REQ, # [APPLICATION 14] + KRB_AP_REP, # [APPLICATION 15] + # RFC4120 + KRB_ERROR, # [APPLICATION 30] + ) + + def mysummary(self): + return self.root.summary() + + +bind_bottom_up(UDP, Kerberos, sport=88) +bind_bottom_up(UDP, Kerberos, dport=88) +bind_layers(UDP, Kerberos, sport=88, dport=88) + +_InitialContextTokens[b"\x01\x00"] = KRB_AP_REQ +_InitialContextTokens[b"\x02\x00"] = KRB_AP_REP +_InitialContextTokens[b"\x03\x00"] = KRB_ERROR +_InitialContextTokens[b"\x04\x00"] = KRB_TGT_REQ +_InitialContextTokens[b"\x04\x01"] = KRB_TGT_REP + + +# RFC4120 sect 7.2.2 + + +class KerberosTCPHeader(Packet): + # According to RFC 5021, first bit to 1 has a special meaning and + # negotiates Kerberos TCP extensions... But apart from rfc6251 no one used that + # https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml#kerberos-parameters-4 + fields_desc = [LenField("len", None, fmt="!I")] + + @classmethod + def tcp_reassemble(cls, data, *args, **kwargs): + if len(data) < 4: + return None + length = struct.unpack("!I", data[:4])[0] + if len(data) == length + 4: + return cls(data) + + +bind_layers(KerberosTCPHeader, Kerberos) + +bind_bottom_up(TCP, KerberosTCPHeader, sport=88) +bind_layers(TCP, KerberosTCPHeader, dport=88) + + +# RFC3244 sect 2 + + +class KPASSWD_REQ(Packet): + fields_desc = [ + ShortField("len", None), + ShortField("pvno", 0xFF80), + ShortField("apreqlen", None), + PacketLenField( + "apreq", KRB_AP_REQ(), KRB_AP_REQ, length_from=lambda pkt: pkt.apreqlen + ), + ConditionalField( + PacketLenField( + "krbpriv", + KRB_PRIV(), + KRB_PRIV, + length_from=lambda pkt: pkt.len - 6 - pkt.apreqlen, + ), + lambda pkt: pkt.apreqlen != 0, + ), + ConditionalField( + PacketLenField( + "error", KRB_ERROR(), KRB_ERROR, length_from=lambda pkt: pkt.len - 6 + ), + lambda pkt: pkt.apreqlen == 0, + ), + ] + + def post_build(self, p, pay): + if self.len is None: + p = struct.pack("!H", len(p)) + p[2:] + if self.apreqlen is None and self.krbpriv is not None: + p = p[:4] + struct.pack("!H", len(self.apreq)) + p[6:] + return p + pay + + +class ChangePasswdData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING("newpasswd", ASN1_STRING(""), explicit_tag=0xA0), + ASN1F_optional( + ASN1F_PACKET("targname", None, PrincipalName, explicit_tag=0xA1) + ), + ASN1F_optional(Realm("targrealm", None, explicit_tag=0xA2)), + ) + + +class KPASSWD_REP(Packet): + fields_desc = [ + ShortField("len", None), + ShortField("pvno", 0x0001), + ShortField("apreplen", None), + PacketLenField( + "aprep", KRB_AP_REP(), KRB_AP_REP, length_from=lambda pkt: pkt.apreplen + ), + ConditionalField( + PacketLenField( + "krbpriv", + KRB_PRIV(), + KRB_PRIV, + length_from=lambda pkt: pkt.len - 6 - pkt.apreplen, + ), + lambda pkt: pkt.apreplen != 0, + ), + ConditionalField( + PacketLenField( + "error", KRB_ERROR(), KRB_ERROR, length_from=lambda pkt: pkt.len - 6 + ), + lambda pkt: pkt.apreplen == 0, + ), + ] + + def post_build(self, p, pay): + if self.len is None: + p = struct.pack("!H", len(p)) + p[2:] + if self.apreplen is None and self.krbpriv is not None: + p = p[:4] + struct.pack("!H", len(self.aprep)) + p[6:] + return p + pay + + def answers(self, other): + return isinstance(other, KPASSWD_REQ) + + +KPASSWD_RESULTS = { + 0: "KRB5_KPASSWD_SUCCESS", + 1: "KRB5_KPASSWD_MALFORMED", + 2: "KRB5_KPASSWD_HARDERROR", + 3: "KRB5_KPASSWD_AUTHERROR", + 4: "KRB5_KPASSWD_SOFTERROR", + 5: "KRB5_KPASSWD_ACCESSDENIED", + 6: "KRB5_KPASSWD_BAD_VERSION", + 7: "KRB5_KPASSWD_INITIAL_FLAG_NEEDED", +} + + +class KPasswdRepData(Packet): + fields_desc = [ + ShortEnumField("resultCode", 0, KPASSWD_RESULTS), + StrField("resultString", ""), + ] + + +class Kpasswd(Packet): + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 4: + if _pkt[2:4] == b"\xff\x80": + return KPASSWD_REQ + elif _pkt[2:4] == b"\x00\x01": + asn1_tag = BER_id_dec(_pkt[6:8])[0] & 0x1F + if asn1_tag == 14: + return KPASSWD_REQ + elif asn1_tag == 15: + return KPASSWD_REP + return KPASSWD_REQ + + +bind_bottom_up(UDP, Kpasswd, sport=464) +bind_bottom_up(UDP, Kpasswd, dport=464) +bind_top_down(UDP, KPASSWD_REQ, sport=464, dport=464) +bind_top_down(UDP, KPASSWD_REP, sport=464, dport=464) + + +class KpasswdTCPHeader(Packet): + fields_desc = [LenField("len", None, fmt="!I")] + + @classmethod + def tcp_reassemble(cls, data, *args, **kwargs): + if len(data) < 4: + return None + length = struct.unpack("!I", data[:4])[0] + if len(data) == length + 4: + return cls(data) + + +bind_layers(KpasswdTCPHeader, Kpasswd) + +bind_bottom_up(TCP, KpasswdTCPHeader, sport=464) +bind_layers(TCP, KpasswdTCPHeader, dport=464) + +# [MS-KKDCP] + + +class _KerbMessage_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_KerbMessage_Field, self).m2i(pkt, s) + if not val[0].val: + return val + return KerberosTCPHeader(val[0].val, _underlayer=pkt), val[1] + + +class KDC_PROXY_MESSAGE(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + _KerbMessage_Field("kerbMessage", "", explicit_tag=0xA0), + ASN1F_optional(Realm("targetDomain", None, explicit_tag=0xA1)), + ASN1F_optional( + ASN1F_FLAGS( + "dclocatorHint", + None, + FlagsField("", 0, -32, _NV_VERSION).names, + explicit_tag=0xA2, + ) + ), + ) + + +class KdcProxySocket(SuperSocket): + """ + This is a wrapper of a HTTP_Client that does KKDCP proxying, + disguised as a SuperSocket to be compatible with the rest of the KerberosClient. + """ + + def __init__( + self, + url, + targetDomain, + dclocatorHint=None, + no_check_certificate=False, + **kwargs, + ): + self.url = url + self.targetDomain = targetDomain + self.dclocatorHint = dclocatorHint + self.no_check_certificate = no_check_certificate + self.queue = deque() + super(KdcProxySocket, self).__init__(**kwargs) + + def recv(self, x=None): + return self.queue.popleft() + + def send(self, x, **kwargs): + from scapy.layers.http import HTTP_Client + + cli = HTTP_Client(no_check_certificate=self.no_check_certificate) + try: + # sr it via the web client + resp = cli.request( + self.url, + Method="POST", + data=bytes( + # Wrap request in KDC_PROXY_MESSAGE + KDC_PROXY_MESSAGE( + kerbMessage=bytes(x), + targetDomain=ASN1_GENERAL_STRING(self.targetDomain.encode()), + # dclocatorHint is optional + dclocatorHint=self.dclocatorHint, + ) + ), + http_headers={ + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "User-Agent": "kerberos/1.0", + }, + ) + if resp and conf.raw_layer in resp: + # Parse the payload + resp = KDC_PROXY_MESSAGE(resp.load).kerbMessage + # We have an answer, queue it. + self.queue.append(resp) + else: + raise EOFError + finally: + cli.close() + + @staticmethod + def select(sockets, remain=None): + return [x for x in sockets if isinstance(x, KdcProxySocket) and x.queue] + + +# Util functions + + +class PKINIT_KEX_METHOD(IntEnum): + DIFFIE_HELLMAN = 1 + PUBLIC_KEY = 2 + + +class KerberosClient(Automaton): + """ + Implementation of a Kerberos client. + + Prefer to use the ``krb_as_req`` and ``krb_tgs_req`` functions which + wrap this client. + + Common parameters: + + :param mode: the mode to use for the client (default: AS_REQ). + :param ip: the IP of the DC (default: discovered by dclocator) + :param upn: the UPN of the client. + :param canonicalize: request the UPN to be canonicalized. + :param password: the password of the client. + :param key: the Key of the client (instead of the password) + :param realm: the realm of the domain. (default: from the UPN) + :param host: the name of the host doing the request + :param port: the Kerberos port (default 88) + :param timeout: timeout of each request (default 5) + + Advanced common parameters: + + :param kdc_proxy: specify a KDC proxy url + :param kdc_proxy_no_check_certificate: do not check the KDC proxy certificate + :param fast: use FAST armoring + :param armor_ticket: an external ticket to use for armoring + :param armor_ticket_upn: the UPN of the client of the armoring ticket + :param armor_ticket_skey: the session Key object of the armoring ticket + :param etypes: specify the list of encryption types to support + :param dhashes: specify the list of supported digest algorithms for PKINIT + (defaults to ["sha1", "sha256", "sha384", "sha512"]) + + AS-REQ only: + + :param x509: a X509 certificate to use for PKINIT AS_REQ or S4U2Proxy + :param x509key: the private key of the X509 certificate (in an AS_REQ) + :param ca: the CA list that verifies the peer (KDC) certificate. Typically + only the ROOT CA is required. + :param p12: (optional) use a pfx/p12 instead of x509 and x509key. In this case, + 'password' is the password of the p12. + :param pkinit_kex_method: (advanced) whether to use the DIFFIE-HELLMAN method or the + Certificate based one for PKINIT. + + TGS-REQ only: + + :param spn: the SPN to request in a TGS-REQ + :param ticket: the existing ticket to use in a TGS-REQ + :param renew: sets the Renew flag in a TGS-REQ + :param additional_tickets: in U2U or S4U2Proxy, the additional tickets + :param u2u: sets the U2U flag + :param for_user: the UPN of another user in TGS-REQ, to do a S4U2Self + :param s4u2proxy: sets the S4U2Proxy flag + :param dmsa: sets the 'unconditional delegation' mode for DMSA TGT retrieval + """ + + RES_AS_MODE = namedtuple( + "AS_Result", ["asrep", "sessionkey", "kdcrep", "upn", "pa_type"] + ) + RES_TGS_MODE = namedtuple("TGS_Result", ["tgsrep", "sessionkey", "kdcrep", "upn"]) + + class MODE(IntEnum): + AS_REQ = 0 + TGS_REQ = 1 + GET_SALT = 2 + + def __init__( + self, + mode=MODE.AS_REQ, + ip: Optional[str] = None, + upn: Optional[str] = None, + canonicalize: bool = False, + password: Optional[str] = None, + key: Optional["Key"] = None, + realm: Optional[str] = None, + x509: Optional[Union[Cert, str]] = None, + x509key: Optional[Union[PrivKey, str]] = None, + ca: Optional[Union[CertTree, str]] = None, + no_verify_cert: bool = False, + p12: Optional[str] = None, + spn: Optional[str] = None, + ticket: Optional[KRB_Ticket] = None, + host: Optional[str] = None, + renew: bool = False, + additional_tickets: List[KRB_Ticket] = [], + u2u: bool = False, + for_user: Optional[str] = None, + s4u2proxy: bool = False, + dmsa: bool = False, + kdc_proxy: Optional[str] = None, + kdc_proxy_no_check_certificate: bool = False, + fast: bool = False, + armor_ticket: KRB_Ticket = None, + armor_ticket_upn: Optional[str] = None, + armor_ticket_skey: Optional["Key"] = None, + key_list_req: List["EncryptionType"] = [], + etypes: Optional[List["EncryptionType"]] = None, + dhashes: Optional[List[str]] = None, + pkinit_kex_method: PKINIT_KEX_METHOD = PKINIT_KEX_METHOD.DIFFIE_HELLMAN, + port: int = 88, + timeout: int = 5, + verbose: bool = True, + **kwargs, + ): + import scapy.libs.rfc3961 # Trigger error if any # noqa: F401 + from scapy.layers.ldap import dclocator + + # PKINIT checks + if p12 is not None: + # password should be None or bytes + if isinstance(password, str): + password = password.encode() + + # Read p12/pfx. If it fails and no password was provided, prompt and + # retry once. + while True: + try: + with open(p12, "rb") as fd: + x509key, x509, _ = pkcs12.load_key_and_certificates( + fd.read(), + password=password, + ) + break + except ValueError as ex: + if password is None: + # We don't have a password. Prompt and retry. + try: + from prompt_toolkit import prompt + + password = prompt( + "Enter PKCS12 password: ", is_password=True + ) + except ImportError: + password = input("Enter PKCS12 password: ") + password = password.encode() + else: + raise ex + + x509 = Cert(cryptography_obj=x509) + x509key = PrivKey(cryptography_obj=x509key) + elif x509 and x509key: + if not isinstance(x509, Cert): + x509 = Cert(x509) + if not isinstance(x509key, PrivKey): + x509key = PrivKey(x509key) + if ca and not isinstance(ca, CertList): + ca = CertList(ca) + if upn is None and x509: + # For PKINIT, get the UPN from the SAN, if possible and present + if realm is None: + raise ValueError( + "When using PKINIT, you must at least specify the realm= !" + ) + for ext in x509.extensions: + if ext.extnID.val == "2.5.29.17": # subjectAltName + generalName = ext.extnValue.subjectAltName[0].generalName + upn = generalName.value.val.decode("utf-8") + break + if upn is None: + raise ValueError( + "Could not find subjectAltName in certificate !" + " Please provide a UPN." + ) + canonicalize = True + + # UPN, SPN and realm calculation + if not upn: + raise ValueError("Invalid upn") + if realm is None: + if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: + _, realm = _parse_upn(upn) + elif mode == self.MODE.TGS_REQ: + _, realm = _parse_spn(spn) + if not realm and ticket: + # if no realm is specified, but there's a ticket, take the realm + # of the ticket. + realm = ticket.realm.val.decode() + else: + raise ValueError("Invalid realm") + if not spn and mode == self.MODE.AS_REQ and realm: + spn = "krbtgt/" + realm + elif not spn: + raise ValueError("Invalid spn") + + # Extra checks for specific requests + if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: + if not host: + raise ValueError("Invalid host") + if x509 is not None: + if x509key and not ca: + if not no_verify_cert: + raise ValueError( + "Using PKINIT without specifying the remote CA is unsafe !" + " Set no_verify_cert=True to bypass this check." + ) + else: + ca = [] + elif not x509key or not ca: + raise ValueError("Must provide both 'x509', 'x509key' and 'ca' !") + elif mode == self.MODE.TGS_REQ: + if not ticket: + raise ValueError("Invalid ticket") + + if not ip and not kdc_proxy: + # No KDC IP provided. Find it by querying the DNS + ip = dclocator( + realm, + timeout=timeout, + # Use connect mode instead of ldap for compatibility + # with MIT kerberos servers + mode="connect", + port=port, + debug=kwargs.get("debug", 0), + ).ip + + # Armoring checks + if fast: + if mode == self.MODE.AS_REQ: + # Requires an external ticket + if not armor_ticket or not armor_ticket_upn or not armor_ticket_skey: + raise ValueError( + "Implicit armoring is not possible on AS-REQ: " + "please provide the 3 required armor arguments" + ) + elif mode == self.MODE.TGS_REQ: + if armor_ticket and (not armor_ticket_upn or not armor_ticket_skey): + raise ValueError( + "Cannot specify armor_ticket without armor_ticket_{upn,skey}" + ) + + # Provide default supported encryption types. For SALT mode, we discard + # the encryption types that don't have a salt. + if mode == self.MODE.GET_SALT: + if etypes is not None: + raise ValueError("Cannot specify etypes in GET_SALT mode !") + + etypes = [ + EncryptionType.AES256_CTS_HMAC_SHA1_96, + EncryptionType.AES128_CTS_HMAC_SHA1_96, + ] + elif etypes is None: + etypes = [ + EncryptionType.AES256_CTS_HMAC_SHA1_96, + EncryptionType.AES128_CTS_HMAC_SHA1_96, + EncryptionType.RC4_HMAC, + EncryptionType.DES_CBC_MD5, + ] + self.etypes = etypes + + self.mode = mode + + self.result = None # Result + + self._timeout = timeout + self._verbose = verbose + self._ip = ip + self._port = port + self.kdc_proxy = kdc_proxy + self.kdc_proxy_no_check_certificate = kdc_proxy_no_check_certificate + + if self.mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: + self.host = host.upper() + self.password = password and bytes_encode(password) + self.spn = spn + self.upn = upn + self.canonicalize = canonicalize # Whether we request canonicalization + self.realm = realm.upper() + self.x509 = x509 + self.x509key = x509key + self.pkinit_kex_method = pkinit_kex_method + self.ticket = ticket + self.fast = fast + self.armor_ticket = armor_ticket + self.armor_ticket_upn = armor_ticket_upn + self.armor_ticket_skey = armor_ticket_skey + self.key_list_req = key_list_req + self.renew = renew + self.additional_tickets = additional_tickets # U2U + S4U2Proxy + self.u2u = u2u # U2U + self.for_user = for_user # FOR-USER + self.s4u2proxy = s4u2proxy # S4U2Proxy + self.dmsa = dmsa # DMSA + self.key = key + self.subkey = None # In the AP-REQ authenticator + self.replykey = None # Key used for reply + # See RFC4120 - sect 7.2.2 + # This marks whether we should follow-up after an EOF + self.should_followup = False + # This marks that we sent a FAST-req and are awaiting for an answer + self.fast_req_sent = False + # Session parameters + if self.x509: + # Windows only assumes it needs a pre-auth when PKINIT is used, + # otherwise it waits to have a PREAUTH_REQUIRED error first. + self.pre_auth = True + else: + self.pre_auth = False + self.pa_type = None # preauth-type that's used + self.fast_rep = None + self.fast_error = None + self.fast_skey = None # The random subkey used for fast + self.fast_armorkey = None # The armor key + self.fxcookie = None + self.pkinit_dh_key = None + self.no_verify_cert = no_verify_cert + if ca is not None: + self.pkinit_cms = CMS_Engine(ca) + else: + self.pkinit_cms = None + if dhashes is None: + self.dhashes = ["sha1", "sha256", "sha384", "sha512"] + else: + self.dhashes = dhashes + + # Launch the client + sock = self._connect() + super(KerberosClient, self).__init__( + sock=sock, + **kwargs, + ) + + def _connect(self): + """ + Internal function to bind a socket to the DC. + This also takes care of an eventual KDC proxy. + """ + if self.kdc_proxy: + # If we are using a KDC Proxy, wrap the socket with the KdcProxySocket, + # that takes our messages and transport them over HTTP. + sock = KdcProxySocket( + url=self.kdc_proxy, + targetDomain=self.realm, + no_check_certificate=self.kdc_proxy_no_check_certificate, + ) + else: + sock = socket.socket() + sock.settimeout(self._timeout) + sock.connect((self._ip, self._port)) + sock = StreamSocket(sock, KerberosTCPHeader) + return sock + + def send(self, pkt): + """ + Sends a wrapped Kerberos packet + """ + super(KerberosClient, self).send(KerberosTCPHeader() / pkt) + + def _show_krb_error(self, error): + """ + Displays a Kerberos error + """ + if error.root.errorCode == 0x07: + # KDC_ERR_S_PRINCIPAL_UNKNOWN + if ( + isinstance(error.root.eData, KERB_ERROR_UNK) + and error.root.eData.dataType == -128 + ): + log_runtime.error( + "KerberosSSP: KDC requires U2U for SPN '%s' !" % error.root.getSPN() + ) + else: + log_runtime.error( + "KerberosSSP: KDC_ERR_S_PRINCIPAL_UNKNOWN for SPN '%s'" + % error.root.getSPN() + ) + else: + log_runtime.error(error.root.sprintf("KerberosSSP: Received %errorCode% !")) + if self._verbose: + error.show() + + def _base_kdc_req(self, now_time): + """ + Return the KRB_KDC_REQ_BODY used in both AS-REQ and TGS-REQ + """ + kdcreq = KRB_KDC_REQ_BODY( + etype=[ASN1_INTEGER(x) for x in self.etypes], + additionalTickets=None, + # Windows default + kdcOptions="forwardable+renewable+canonicalize+renewable-ok", + cname=None, + realm=ASN1_GENERAL_STRING(self.realm), + till=ASN1_GENERALIZED_TIME(now_time + timedelta(hours=10)), + rtime=ASN1_GENERALIZED_TIME(now_time + timedelta(hours=10)), + nonce=ASN1_INTEGER(RandNum(0, 0x7FFFFFFF)._fix()), + ) + if self.renew: + kdcreq.kdcOptions.set(30, 1) # set 'renew' (bit 30) + return kdcreq + + def calc_fast_armorkey(self): + """ + Calculate and return the FAST armorkey + """ + # Generate a random key of the same type than ticket_skey + if self.mode == self.MODE.AS_REQ: + # AS-REQ mode + self.fast_skey = Key.new_random_key(self.armor_ticket_skey.etype) + + self.fast_armorkey = KRB_FX_CF2( + self.fast_skey, + self.armor_ticket_skey, + b"subkeyarmor", + b"ticketarmor", + ) + elif self.mode == self.MODE.TGS_REQ: + # TGS-REQ: 2 cases + + self.subkey = Key.new_random_key(self.key.etype) + + if not self.armor_ticket: + # Case 1: Implicit armoring + self.fast_armorkey = KRB_FX_CF2( + self.subkey, + self.key, + b"subkeyarmor", + b"ticketarmor", + ) + else: + # Case 2: Explicit armoring, in "Compounded Identity mode". + # This is a Microsoft extension: see [MS-KILE] sect 3.3.5.7.4 + + self.fast_skey = Key.new_random_key(self.armor_ticket_skey.etype) + + explicit_armor_key = KRB_FX_CF2( + self.fast_skey, + self.armor_ticket_skey, + b"subkeyarmor", + b"ticketarmor", + ) + + self.fast_armorkey = KRB_FX_CF2( + explicit_armor_key, + self.subkey, + b"explicitarmor", + b"tgsarmor", + ) + + def _fast_wrap(self, kdc_req, padata, now_time, pa_tgsreq_ap=None): + """ + :param kdc_req: the KDC_REQ_BODY to wrap + :param padata: the list of PADATA to wrap + :param now_time: the current timestamp used by the client + """ + + # Create the PA Fast request wrapper + pafastreq = PA_FX_FAST_REQUEST( + armoredData=KrbFastArmoredReq( + reqChecksum=Checksum(), + encFastReq=EncryptedData(), + ) + ) + + if self.armor_ticket is not None: + # EXPLICIT mode only (AS-REQ or TGS-REQ) + + pafastreq.armoredData.armor = KrbFastArmor( + armorType=1, # FX_FAST_ARMOR_AP_REQUEST + armorValue=KRB_AP_REQ( + ticket=self.armor_ticket, + authenticator=EncryptedData(), + ), + ) + + # Populate the authenticator. Note the client is the wrapper + _, crealm = _parse_upn(self.armor_ticket_upn) + authenticator = KRB_Authenticator( + crealm=ASN1_GENERAL_STRING(crealm), + cname=PrincipalName.fromUPN(self.armor_ticket_upn), + cksum=None, + ctime=ASN1_GENERALIZED_TIME(now_time), + cusec=ASN1_INTEGER(0), + subkey=EncryptionKey.fromKey(self.fast_skey), + seqNumber=ASN1_INTEGER(0), + encAuthorizationData=None, + ) + pafastreq.armoredData.armor.armorValue.authenticator.encrypt( + self.armor_ticket_skey, + authenticator, + ) + + # Sign the fast request wrapper + if self.mode == self.MODE.TGS_REQ: + # "for a TGS-REQ, it is performed over the type AP- + # REQ in the PA-TGS-REQ padata of the TGS request" + pafastreq.armoredData.reqChecksum.make( + self.fast_armorkey, + bytes(pa_tgsreq_ap), + ) + else: + # "For an AS-REQ, it is performed over the type KDC-REQ- + # BODY for the req-body field of the KDC-REQ structure of the + # containing message" + pafastreq.armoredData.reqChecksum.make( + self.fast_armorkey, + bytes(kdc_req), + ) + + # Build and encrypt the Fast request + fastreq = KrbFastReq( + padata=padata, + reqBody=kdc_req, + ) + pafastreq.armoredData.encFastReq.encrypt( + self.fast_armorkey, + fastreq, + ) + + # Return the PADATA + return PADATA( + padataType=ASN1_INTEGER(136), # PA-FX-FAST + padataValue=pafastreq, + ) + + def as_req(self): + now_time = datetime.now(timezone.utc).replace(microsecond=0) + + # 1. Build and populate KDC-REQ + kdc_req = self._base_kdc_req(now_time=now_time) + kdc_req.addresses = [ + HostAddress( + addrType=ASN1_INTEGER(20), # Netbios + address=ASN1_STRING(self.host.ljust(16, " ")), + ) + ] + kdc_req.addresses = None + kdc_req.cname = PrincipalName.fromUPN(self.upn, canonicalize=self.canonicalize) + kdc_req.sname = PrincipalName.fromSPN(self.spn) + + # 2. Build the list of PADATA + padata = [ + PADATA( + padataType=ASN1_INTEGER(128), # PA-PAC-REQUEST + padataValue=PA_PAC_REQUEST(includePac=ASN1_BOOLEAN(-1)), + ) + ] + + # Cookie support + if self.fxcookie: + padata.insert( + 0, + PADATA( + padataType=133, # PA-FX-COOKIE + padataValue=self.fxcookie, + ), + ) + + # FAST + if self.fast: + # Calculate the armor key + self.calc_fast_armorkey() + + # [MS-KILE] sect 3.2.5.5 + # "When sending the AS-REQ, add a PA-PAC-OPTIONS [167]" + padata.append( + PADATA( + padataType=ASN1_INTEGER(167), # PA-PAC-OPTIONS + padataValue=PA_PAC_OPTIONS( + options="Claims", + ), + ) + ) + + # Pre-auth is requested + if self.pre_auth: + if self.x509: + # Special PKINIT (RFC4556) factor + + # RFC4556 - 3.2.1. Generation of Client Request + + # RFC4556 - 3.2.1 - (5) AuthPack + authpack = KRB_AuthPack( + pkAuthenticator=KRB_PKAuthenticator( + ctime=ASN1_GENERALIZED_TIME(now_time), + cusec=ASN1_INTEGER(0), + nonce=ASN1_INTEGER(RandNum(0, 0x7FFFFFFF)._fix()), + ), + clientPublicValue=None, # Used only in DH mode + supportedCMSTypes=[], + clientDHNonce=None, + supportedKDFs=None, + ) + + if self.pkinit_kex_method == PKINIT_KEX_METHOD.DIFFIE_HELLMAN: + # RFC4556 - 3.2.3.1. Diffie-Hellman Key Exchange + + # We (and Windows) use modp2048 + dh_parameters = _ffdh_groups["modp2048"][0] + self.pkinit_dh_key = dh_parameters.generate_private_key() + numbers = dh_parameters.parameter_numbers() + + # We can't use 'public_bytes' because it's the PKCS#3 format, + # and we want the DomainParameters format. + authpack.clientPublicValue = X509_SubjectPublicKeyInfo( + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID("dhpublicnumber"), + parameters=DomainParameters( + p=ASN1_INTEGER(numbers.p), + g=ASN1_INTEGER(numbers.g), + # q: see ERRATA 1 of RFC4556 + q=ASN1_INTEGER(numbers.q or (numbers.p - 1) // 2), + j=None, + ), + ), + subjectPublicKey=DHPublicKey( + y=ASN1_INTEGER( + self.pkinit_dh_key.public_key().public_numbers().y + ), + ), + ) + elif self.pkinit_kex_method == PKINIT_KEX_METHOD.PUBLIC_KEY: + # RFC4556 - 3.2.3.2. - Public Key Encryption + + # Set supportedCMSTypes, supportedKDFs + authpack.supportedCMSTypes = [ + X509_AlgorithmIdentifier(algorithm=ASN1_OID(x)) + for x in [ + "ecdsa-with-SHA512", + "ecdsa-with-SHA256", + "sha512WithRSAEncryption", + "sha256WithRSAEncryption", + ] + ] + authpack.supportedKDFs = [ + KDFAlgorithmId(kdfId=ASN1_OID(x)) + for x in [ + "id-pkinit-kdf-sha256", + "id-pkinit-kdf-sha1", + "id-pkinit-kdf-sha512", + ] + ] + + # XXX UNFINISHED + raise NotImplementedError + else: + raise ValueError + + # Find a supported digest hash. Windows 25H2 still defaults + # to SHA1 unless a client policy has been applied. + dhash = next(iter(self.dhashes)) + + # Populate paChecksum + authpack.pkAuthenticator.make_checksum( + bytes(kdc_req), + h=dhash, + ) + + # Sign the AuthPack + signedAuthpack = self.pkinit_cms.sign( + authpack, + ASN1_OID("id-pkinit-authData"), + self.x509, + self.x509key, + dhash=dhash, + ) + + # Build PA-DATA + self.pa_type = 16 # PA-PK-AS-REQ + pafactor = PADATA( + padataType=self.pa_type, + padataValue=PA_PK_AS_REQ( + signedAuthpack=signedAuthpack, + trustedCertifiers=None, + kdcPkId=None, + ), + ) + + # RFC 4557 extension - OCSP + padata.insert( + 0, + PADATA( + padataType=18, # PA-PK-OCSP-RESPONSE + ), + ) + else: + # Key-based factor + + if self.fast: + # Special FAST factor + # RFC6113 sect 5.4.6 + + # Calculate the 'challenge key' + ts_key = KRB_FX_CF2( + self.fast_armorkey, + self.key, + b"clientchallengearmor", + b"challengelongterm", + ) + self.pa_type = 138 # PA-ENCRYPTED-CHALLENGE + pafactor = PADATA( + padataType=self.pa_type, + padataValue=EncryptedData(), + ) + else: + # Usual 'timestamp' factor + ts_key = self.key + self.pa_type = 2 # PA-ENC-TIMESTAMP + pafactor = PADATA( + padataType=self.pa_type, + padataValue=EncryptedData(), + ) + pafactor.padataValue.encrypt( + ts_key, + PA_ENC_TS_ENC(patimestamp=ASN1_GENERALIZED_TIME(now_time)), + ) + + # Insert Pre-Authentication data + padata.insert( + 0, + pafactor, + ) + + # FAST support + if self.fast: + # We are using RFC6113's FAST armoring. The PADATA's are therefore + # hidden inside the encrypted section. + padata = [ + self._fast_wrap( + kdc_req=kdc_req, + padata=padata, + now_time=now_time, + ) + ] + + # 3. Build the request + asreq = Kerberos( + root=KRB_AS_REQ( + padata=padata, + reqBody=kdc_req, + ) + ) + + # Note the reply key + self.replykey = self.key + + return asreq + + def tgs_req(self): + now_time = datetime.now(timezone.utc).replace(microsecond=0) + + # Compute armor key for FAST + if self.fast: + self.calc_fast_armorkey() + + # 1. Build and populate KDC-REQ + kdc_req = self._base_kdc_req(now_time=now_time) + kdc_req.sname = PrincipalName.fromSPN(self.spn) + + # Additional tickets + if self.additional_tickets: + kdc_req.additionalTickets = self.additional_tickets + + # U2U + if self.u2u: + kdc_req.kdcOptions.set(28, 1) # set 'enc-tkt-in-skey' (bit 28) + + # 2. Build the list of PADATA + padata = [] + + # [MS-SFU] FOR-USER extension + if self.for_user is not None: + # [MS-SFU] note 4: + # "Windows Vista, Windows Server 2008, Windows 7, and Windows Server + # 2008 R2 send the PA-S4U-X509-USER padata type alone if the user's + # certificate is available. + # If the user's certificate is not available, it sends both the + # PA-S4U-X509-USER padata type and the PA-FOR-USER padata type. + # When the PA-S4U-X509-USER padata type is used without the user's + # certificate, the certificate field is not present." + + # 1. Add PA_S4U_X509_USER + pasfux509 = PA_S4U_X509_USER( + userId=S4UUserID( + nonce=kdc_req.nonce, + # [MS-SFU] note 5: + # "Windows S4U clients always set this option." + options="USE_REPLY_KEY_USAGE", + cname=PrincipalName.fromUPN(self.for_user), + crealm=ASN1_GENERAL_STRING(_parse_upn(self.for_user)[1]), + subjectCertificate=None, # TODO + ), + checksum=Checksum(), + ) + + if self.dmsa: + # DMSA = set UNCONDITIONAL_DELEGATION to 1 + pasfux509.userId.options.set(4, 1) + + if self.key.etype in [EncryptionType.RC4_HMAC, EncryptionType.RC4_HMAC_EXP]: + # "if the key's encryption type is RC4_HMAC_NT (23) the checksum type + # is rsa-md4 (2) as defined in section 6.2.6 of [RFC3961]." + pasfux509.checksum.make( + self.key, + bytes(pasfux509.userId), + cksumtype=ChecksumType.RSA_MD4, + ) + else: + pasfux509.checksum.make( + self.key, + bytes(pasfux509.userId), + ) + padata.append( + PADATA( + padataType=ASN1_INTEGER(130), # PA-FOR-X509-USER + padataValue=pasfux509, + ) + ) + + # 2. Add PA_FOR_USER + if True: # XXX user's certificate is not available. + paforuser = PA_FOR_USER( + userName=PrincipalName.fromUPN(self.for_user), + userRealm=ASN1_GENERAL_STRING(_parse_upn(self.for_user)[1]), + cksum=Checksum(), + ) + S4UByteArray = struct.pack( # [MS-SFU] sect 2.2.1 + "" + :param ip: the KDC ip. (optional. If not provided, Scapy will query the DNS for + _kerberos._tcp.dc._msdcs.domain.local). + :param key: (optional) pass the Key object. + :param password: (optional) otherwise, pass the user's password + :param x509: (optional) pass a x509 certificate for PKINIT. + :param x509key: (optional) pass the private key of the x509 certificate for PKINIT. + :param p12: (optional) use a pfx/p12 instead of x509 and x509key. In this case, + 'password' is the password of the p12. + :param realm: (optional) the realm to use. Otherwise use the one from UPN. + :param host: (optional) the host performing the AS-Req. WIN10 by default. + + :return: returns a named tuple (asrep=<...>, sessionkey=<...>) + + Example:: + + >>> # The KDC is found via DC Locator, we ask a TGT for user1 + >>> krb_as_req("user1@DOMAIN.LOCAL", password="Password1") + + Equivalent:: + + >>> from scapy.libs.rfc3961 import Key, EncryptionType + >>> key = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=hex_bytes("6d0748c546 + ...: f4e99205e78f8da7681d4ec5520ae4815543720c2a647c1ae814c9")) + >>> krb_as_req("user1@DOMAIN.LOCAL", ip="192.168.122.17", key=key) + + Example using PKINIT with a p12 ("password" is the password of the p12):: + + >>> krb_as_req(p12="./store.p12", realm="DOMAIN.LOCAL", password="password") + """ + if key is None and p12 is None and x509 is None: + if password is None: + try: + from prompt_toolkit import prompt + + password = prompt("Enter password: ", is_password=True) + except ImportError: + password = input("Enter password: ") + cli = KerberosClient( + mode=KerberosClient.MODE.AS_REQ, + realm=realm, + ip=ip, + spn=spn, + host=host, + upn=upn, + password=password, + key=key, + p12=p12, + x509=x509, + x509key=x509key, + **kwargs, + ) + cli.run() + cli.stop() + return cli.result + + +def krb_tgs_req( + upn, + spn, + sessionkey, + ticket, + ip=None, + renew=False, + realm=None, + additional_tickets=[], + u2u=False, + etypes=None, + for_user=None, + s4u2proxy=False, + **kwargs, +): + r""" + Kerberos TGS-Req + + :param upn: the user principal name formatted as "DOMAIN\user", "DOMAIN/user" + or "user@DOMAIN" + :param spn: the full service principal name (e.g. "cifs/srv1") + :param sessionkey: the session key retrieved from the tgt + :param ticket: the tgt ticket + :param ip: the KDC ip. (optional. If not provided, Scapy will query the DNS for + _kerberos._tcp.dc._msdcs.domain.local). + :param renew: ask for renewal + :param realm: (optional) the realm to use. Otherwise use the one from SPN. + :param additional_tickets: (optional) a list of additional tickets to pass. + :param u2u: (optional) if specified, enable U2U and request the ticket to be + signed using the session key from the first additional ticket. + :param etypes: array of EncryptionType values. + By default: AES128, AES256, RC4, DES_MD5 + :param for_user: a user principal name to request the ticket for. This is the + S4U2Self extension. + + :return: returns a named tuple (tgsrep=<...>, sessionkey=<...>) + + Example:: + + >>> # The KDC is on 192.168.122.17, we ask a TGT for user1 + >>> krb_as_req("user1@DOMAIN.LOCAL", "192.168.122.17", password="Password1") + + Equivalent:: + + >>> from scapy.libs.rfc3961 import Key, EncryptionType + >>> key = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=hex_bytes("6d0748c546 + ...: f4e99205e78f8da7681d4ec5520ae4815543720c2a647c1ae814c9")) + >>> krb_as_req("user1@DOMAIN.LOCAL", "192.168.122.17", key=key) + """ + cli = KerberosClient( + mode=KerberosClient.MODE.TGS_REQ, + realm=realm, + upn=upn, + ip=ip, + spn=spn, + key=sessionkey, + ticket=ticket, + renew=renew, + additional_tickets=additional_tickets, + u2u=u2u, + etypes=etypes, + for_user=for_user, + s4u2proxy=s4u2proxy, + **kwargs, + ) + cli.run() + cli.stop() + return cli.result + + +def krb_as_and_tgs(upn, spn, ip=None, key=None, password=None, **kwargs): + """ + Kerberos AS-Req then TGS-Req + """ + res = krb_as_req(upn=upn, ip=ip, key=key, password=password, **kwargs) + if not res: + return + + return krb_tgs_req( + upn=res.upn, # UPN might get canonicalized + spn=spn, + sessionkey=res.sessionkey, + ticket=res.asrep.ticket, + ip=ip, + **kwargs, + ) + + +def krb_get_salt(upn, ip=None, realm=None, host="WIN10", **kwargs): + """ + Kerberos AS-Req only to get the salt associated with the UPN. + """ + if realm is None: + _, realm = _parse_upn(upn) + cli = KerberosClient( + mode=KerberosClient.MODE.GET_SALT, + realm=realm, + ip=ip, + spn="krbtgt/" + realm, + upn=upn, + host=host, + **kwargs, + ) + cli.run() + cli.stop() + return cli.result + + +def kpasswd( + upn, + targetupn=None, + ip=None, + password=None, + newpassword=None, + key=None, + ticket=None, + realm=None, + ssp=None, + setpassword=None, + timeout=3, + port=464, + debug=0, + **kwargs, +): + """ + Change a password using RFC3244's Kerberos Set / Change Password. + + :param upn: the UPN to use for authentication + :param targetupn: (optional) the UPN to change the password of. If not specified, + same as upn. + :param ip: the KDC ip. (optional. If not provided, Scapy will query the DNS for + _kerberos._tcp.dc._msdcs.domain.local). + :param key: (optional) pass the Key object. + :param ticket: (optional) a ticket to use. Either a TGT or ST for kadmin/changepw. + :param password: (optional) otherwise, pass the user's password + :param realm: (optional) the realm to use. Otherwise use the one from UPN. + :param setpassword: (optional) use "Set Password" mechanism. + :param ssp: (optional) a Kerberos SSP for the service kadmin/changepw@REALM. + If provided, you probably don't need anything else. Otherwise built. + """ + from scapy.layers.ldap import dclocator + + if not realm: + _, realm = _parse_upn(upn) + spn = "kadmin/changepw@%s" % realm + if ip is None: + ip = dclocator( + realm, + timeout=timeout, + # Use connect mode instead of ldap for compatibility + # with MIT kerberos servers + mode="connect", + port=port, + debug=debug, + ).ip + if ssp is None and ticket is not None: + tktspn = ticket.getSPN().split("/")[0] + assert tktspn in ["krbtgt", "kadmin"], "Unexpected ticket type ! %s" % tktspn + if tktspn == "krbtgt": + log_runtime.info( + "Using 'Set Password' mode. This only works with admin privileges." + ) + setpassword = True + resp = krb_tgs_req( + upn=upn, + spn=spn, + ticket=ticket, + sessionkey=key, + ip=ip, + debug=debug, + ) + if resp is None: + return + ticket = resp.tgsrep.ticket + key = resp.sessionkey + if setpassword is None: + setpassword = bool(targetupn) + elif setpassword and targetupn is None: + targetupn = upn + assert setpassword or not targetupn, "Cannot use targetupn in changepassword mode !" + # Get a ticket for kadmin/changepw + if ssp is None: + if ticket is None: + # Get a ticket for kadmin/changepw through AS-REQ + resp = krb_as_req( + upn=upn, + spn=spn, + key=key, + ip=ip, + password=password, + debug=debug, + ) + if resp is None: + return + ticket = resp.asrep.ticket + key = resp.sessionkey + ssp = KerberosSSP( + UPN=upn, + SPN=spn, + ST=ticket, + KEY=key, + DC_IP=ip, + debug=debug, + **kwargs, + ) + Context, tok, status = ssp.GSS_Init_sec_context( + None, + req_flags=0, # No GSS_C_MUTUAL_FLAG + ) + if status != GSS_S_CONTINUE_NEEDED: + warning("SSP failed on initial GSS_Init_sec_context !") + if tok: + tok.show() + return + apreq = tok.innerToken.root + # Connect + sock = socket.socket() + sock.settimeout(timeout) + sock.connect((ip, port)) + sock = StreamSocket(sock, KpasswdTCPHeader) + # Do KPASSWD request + if newpassword is None: + try: + from prompt_toolkit import prompt + + newpassword = prompt("Enter NEW password: ", is_password=True) + except ImportError: + newpassword = input("Enter NEW password: ") + krbpriv = KRB_PRIV(encPart=EncryptedData()) + krbpriv.encPart.encrypt( + Context.KrbSessionKey, + EncKrbPrivPart( + sAddress=HostAddress( + addrType=ASN1_INTEGER(2), # IPv4 + address=ASN1_STRING(b"\xc0\xa8\x00e"), + ), + userData=ASN1_STRING( + bytes( + ChangePasswdData( + newpasswd=newpassword, + targname=PrincipalName.fromUPN(targetupn), + targrealm=realm, + ) + ) + if setpassword + else newpassword + ), + timestamp=None, + usec=None, + seqNumber=Context.SendSeqNum, + ), + ) + resp = sock.sr1( + KpasswdTCPHeader() + / KPASSWD_REQ( + pvno=0xFF80 if setpassword else 1, + apreq=apreq, + krbpriv=krbpriv, + ), + timeout=timeout, + verbose=0, + ) + # Verify KPASSWD response + if not resp: + raise TimeoutError("KPASSWD_REQ timed out !") + if KPASSWD_REP not in resp: + resp.show() + raise ValueError("Invalid response to KPASSWD_REQ !") + Context, tok, status = ssp.GSS_Init_sec_context( + Context, + input_token=resp.aprep, + ) + if status != GSS_S_COMPLETE: + warning("SSP failed on subsequent GSS_Init_sec_context !") + if tok: + tok.show() + return + # Parse answer KRB_PRIV + krbanswer = resp.krbpriv.encPart.decrypt(Context.KrbSessionKey) + userRep = KPasswdRepData(krbanswer.userData.val) + if userRep.resultCode != 0: + warning(userRep.sprintf("KPASSWD failed !")) + userRep.show() + return + print(userRep.sprintf("%resultCode%")) + + +# SSP + + +class KerberosSSP(SSP): + """ + The KerberosSSP + + Client settings: + + :param ST: the service ticket to use for access. + If not provided, will be retrieved + :param SPN: the SPN of the service to use. If not provided, will use the + target_name provided in the GSS_Init_sec_context + :param UPN: The client UPN + :param DC_IP: (optional) is ST+KEY are not provided, will need to contact + the KDC at this IP. If not provided, will perform dc locator. + :param TGT: (optional) pass a TGT to use to get the ST. + :param KEY: the session key associated with the ST if it is provided, + OR the session key associated with the TGT + OR the kerberos key associated with the UPN + :param PASSWORD: (optional) if a UPN is provided and not a KEY, this is the + password of the UPN. + :param U2U: (optional) use U2U when requesting the ST. + + Server settings: + + :param SPN: the SPN of the service to use. + :param KEY: the kerberos key to use to decrypt the AP-req + :param UPN: (optional) the UPN, if used in U2U mode. + :param TGT: (optional) pass a TGT to use for U2U. + :param DC_IP: (optional) if TGT is not provided, request one on the KDC at + this IP using using the KEY when using U2U. + """ + + auth_type = 0x10 + + class STATE(SSP.STATE): + INIT = 1 + CLI_SENT_TGTREQ = 2 + CLI_SENT_APREQ = 3 + CLI_RCVD_APREP = 4 + SRV_SENT_APREP = 5 + FAILED = -1 + + class CONTEXT(SSP.CONTEXT): + __slots__ = [ + "SessionKey", + "ServerHostname", + "U2U", + "KrbSessionKey", # raw Key object + "ST", # the service ticket + "STSessionKey", # raw ST Key object (for DCE_STYLE) + "SeqNum", # for AP + "SendSeqNum", # for MIC + "RecvSeqNum", # for MIC + "IsAcceptor", + "SendSealKeyUsage", + "SendSignKeyUsage", + "RecvSealKeyUsage", + "RecvSignKeyUsage", + # server-only + "UPN", + "PAC", + ] + + def __init__(self, IsAcceptor, req_flags=None): + self.state = KerberosSSP.STATE.INIT + self.SessionKey = None + self.ServerHostname = None + self.U2U = False + self.SendSeqNum = 0 + self.RecvSeqNum = 0 + self.KrbSessionKey = None + self.ST = None + self.STSessionKey = None + self.IsAcceptor = IsAcceptor + self.UPN = None + self.PAC = None + # [RFC 4121] sect 2 + if IsAcceptor: + self.SendSealKeyUsage = 22 + self.SendSignKeyUsage = 23 + self.RecvSealKeyUsage = 24 + self.RecvSignKeyUsage = 25 + else: + self.SendSealKeyUsage = 24 + self.SendSignKeyUsage = 25 + self.RecvSealKeyUsage = 22 + self.RecvSignKeyUsage = 23 + super(KerberosSSP.CONTEXT, self).__init__(req_flags=req_flags) + + def clifailure(self): + self.__init__(self.IsAcceptor, req_flags=self.flags) + + def __repr__(self): + if self.U2U: + return "KerberosSSP-U2U" + return "KerberosSSP" + + def __init__( + self, + ST=None, + UPN=None, + PASSWORD=None, + U2U=False, + KEY=None, + SPN=None, + TGT=None, + DC_IP=None, + SKEY_TYPE=None, + debug=0, + **kwargs, + ): + import scapy.libs.rfc3961 # Trigger error if any # noqa: F401 + + self.ST = ST + self.UPN = UPN + self.KEY = KEY + self.SPN = SPN + self.TGT = TGT + self.TGTSessionKey = None + self.PASSWORD = PASSWORD + self.U2U = U2U + self.DC_IP = DC_IP + self.debug = debug + if SKEY_TYPE is None: + SKEY_TYPE = EncryptionType.AES128_CTS_HMAC_SHA1_96 + self.SKEY_TYPE = SKEY_TYPE + super(KerberosSSP, self).__init__(**kwargs) + + def GSS_Inquire_names_for_mech(self): + mechs = [ + "1.2.840.48018.1.2.2", # MS KRB5 - Microsoft Kerberos 5 + "1.2.840.113554.1.2.2", # Kerberos 5 + ] + if self.U2U: + mechs.append("1.2.840.113554.1.2.2.3") # Kerberos 5 - User to User + return mechs + + def GSS_GetMICEx(self, Context, msgs, qop_req=0): + """ + [MS-KILE] sect 3.4.5.6 + + - AES: RFC4121 sect 4.2.6.1 + """ + if Context.KrbSessionKey.etype in [17, 18]: # AES + # Concatenate the ToSign + ToSign = b"".join(x.data for x in msgs if x.sign) + sig = KRB_InnerToken( + TOK_ID=b"\x04\x04", + root=KRB_GSS_MIC( + Flags="AcceptorSubkey" + + ("+SentByAcceptor" if Context.IsAcceptor else ""), + SND_SEQ=Context.SendSeqNum, + ), + ) + ToSign += bytes(sig)[:16] + sig.root.SGN_CKSUM = Context.KrbSessionKey.make_checksum( + keyusage=Context.SendSignKeyUsage, + text=ToSign, + ) + else: + raise NotImplementedError + Context.SendSeqNum += 1 + return sig + + def GSS_VerifyMICEx(self, Context, msgs, signature): + """ + [MS-KILE] sect 3.4.5.7 + + - AES: RFC4121 sect 4.2.6.1 + """ + Context.RecvSeqNum = signature.root.SND_SEQ + if Context.KrbSessionKey.etype in [17, 18]: # AES + # Concatenate the ToSign + ToSign = b"".join(x.data for x in msgs if x.sign) + ToSign += bytes(signature)[:16] + sig = Context.KrbSessionKey.make_checksum( + keyusage=Context.RecvSignKeyUsage, + text=ToSign, + ) + else: + raise NotImplementedError + if sig != signature.root.SGN_CKSUM: + raise ValueError("ERROR: Checksums don't match") + + def GSS_WrapEx(self, Context, msgs, qop_req: GSS_QOP_REQ_FLAGS = 0): + """ + [MS-KILE] sect 3.4.5.4 + + - AES: RFC4121 sect 4.2.6.2 and [MS-KILE] sect 3.4.5.4.1 + - HMAC-RC4: RFC4757 sect 7.3 and [MS-KILE] sect 3.4.5.4.1 + """ + # Is confidentiality in use? + confidentiality = (Context.flags & GSS_C_FLAGS.GSS_C_CONF_FLAG) and any( + x.conf_req_flag for x in msgs + ) + if Context.KrbSessionKey.etype in [17, 18]: # AES + # Build token + tok = KRB_InnerToken( + TOK_ID=b"\x05\x04", + root=KRB_GSS_Wrap( + Flags="AcceptorSubkey" + + ("+SentByAcceptor" if Context.IsAcceptor else "") + + ("+Sealed" if confidentiality else ""), + SND_SEQ=Context.SendSeqNum, + RRC=0, + ), + ) + Context.SendSeqNum += 1 + # Real separation starts now: RFC4121 sect 4.2.4 + if confidentiality: + # Confidentiality is requested (see RFC4121 sect 4.3) + # {"header" | encrypt(plaintext-data | filler | "header")} + # 0. Roll confounder + Confounder = os.urandom(Context.KrbSessionKey.ep.blocksize) + # 1. Concatenate the data to be encrypted + Data = b"".join(x.data for x in msgs if x.conf_req_flag) + DataLen = len(Data) + # 2. Add filler + if qop_req & GSS_QOP_REQ_FLAGS.GSS_S_NO_SECBUFFER_PADDING: + # Special case for compatibility with Windows API. See + # GSS_QOP_REQ_FLAGS. + tok.root.EC = 0 + else: + # [MS-KILE] sect 3.4.5.4.1 - "For AES-SHA1 ciphers, the EC must not + # be zero" + tok.root.EC = ( + (-DataLen) % Context.KrbSessionKey.ep.blocksize + ) or 16 + Filler = b"\x00" * tok.root.EC + Data += Filler + # 3. Add first 16 octets of the Wrap token "header" + PlainHeader = bytes(tok)[:16] + Data += PlainHeader + # 4. Build 'ToSign', exclusively used for checksum + ToSign = Confounder + ToSign += b"".join(x.data for x in msgs if x.sign) + ToSign += Filler + ToSign += PlainHeader + # 5. Finalize token for signing + # "The RRC field is [...] 28 if encryption is requested." + tok.root.RRC = 28 + # 6. encrypt() is the encryption operation (which provides for + # integrity protection) + Data = Context.KrbSessionKey.encrypt( + keyusage=Context.SendSealKeyUsage, + plaintext=Data, + confounder=Confounder, + signtext=ToSign, + ) + # 7. Rotate + Data = strrot(Data, tok.root.RRC + tok.root.EC) + # 8. Split (token and encrypted messages) + toklen = len(Data) - DataLen + tok.root.Data = Data[:toklen] + offset = toklen + for msg in msgs: + msglen = len(msg.data) + if msg.conf_req_flag: + msg.data = Data[offset : offset + msglen] + offset += msglen + return msgs, tok + else: + # No confidentiality is requested + # {"header" | plaintext-data | get_mic(plaintext-data | "header")} + # 0. Concatenate the data + Data = b"".join(x.data for x in msgs if x.sign) + DataLen = len(Data) + # 1. Add first 16 octets of the Wrap token "header" + ToSign = Data + ToSign += bytes(tok)[:16] + # 2. get_mic() is the checksum operation for the required + # checksum mechanism + Mic = Context.KrbSessionKey.make_checksum( + keyusage=Context.SendSealKeyUsage, + text=ToSign, + ) + # In Wrap tokens without confidentiality, the EC field SHALL be used + # to encode the number of octets in the trailing checksum + tok.root.EC = 12 # len(tok.root.Data) == 12 for AES + # "The RRC field ([RFC4121] section 4.2.5) is 12 if no encryption + # is requested" + tok.root.RRC = 12 + # 3. Concat and pack + for msg in msgs: + if msg.sign: + msg.data = b"" + Data = Data + Mic + # 4. Rotate + tok.root.Data = strrot(Data, tok.root.RRC) + return msgs, tok + elif Context.KrbSessionKey.etype in [23, 24]: # RC4 + # Build token + seq = struct.pack(">I", Context.SendSeqNum) + tok = KRB_InnerToken( + TOK_ID=b"\x02\x01", + root=KRB_GSS_Wrap_RFC1964( + SGN_ALG="HMAC", + SEAL_ALG="RC4" if confidentiality else "none", + SND_SEQ=seq + + ( + # See errata + b"\xff\xff\xff\xff" + if Context.IsAcceptor + else b"\x00\x00\x00\x00" + ), + ), + ) + Context.SendSeqNum += 1 + # 0. Concatenate data + ToSign = _rfc1964pad(b"".join(x.data for x in msgs if x.sign)) + ToEncrypt = b"".join(x.data for x in msgs if x.conf_req_flag) + Kss = Context.KrbSessionKey.key + # 1. Roll confounder + Confounder = os.urandom(8) + # 2. Compute the 'Kseq' key + Klocal = strxor(Kss, len(Kss) * b"\xf0") + if Context.KrbSessionKey.etype == 24: # EXP + Kcrypt = Hmac_MD5(Klocal).digest(b"fortybits\x00" + b"\x00\x00\x00\x00") + Kcrypt = Kcrypt[:7] + b"\xab" * 9 + else: + Kcrypt = Hmac_MD5(Klocal).digest(b"\x00\x00\x00\x00") + Kcrypt = Hmac_MD5(Kcrypt).digest(seq) + # 3. Build SGN_CKSUM + tok.root.SGN_CKSUM = Context.KrbSessionKey.make_checksum( + keyusage=13, # See errata + text=bytes(tok)[:8] + Confounder + ToSign, + )[:8] + # 4. Populate token + encrypt + if confidentiality: + # 'encrypt' is requested + rc4 = Cipher(decrepit_algorithms.ARC4(Kcrypt), mode=None).encryptor() + tok.root.CONFOUNDER = rc4.update(Confounder) + Data = rc4.update(ToEncrypt) + # Split encrypted data + offset = 0 + for msg in msgs: + msglen = len(msg.data) + if msg.conf_req_flag: + msg.data = Data[offset : offset + msglen] + offset += msglen + else: + # 'encrypt' is not requested + tok.root.CONFOUNDER = Confounder + # 5. Compute the 'Kseq' key + if Context.KrbSessionKey.etype == 24: # EXP + Kseq = Hmac_MD5(Kss).digest(b"fortybits\x00" + b"\x00\x00\x00\x00") + Kseq = Kseq[:7] + b"\xab" * 9 + else: + Kseq = Hmac_MD5(Kss).digest(b"\x00\x00\x00\x00") + Kseq = Hmac_MD5(Kseq).digest(tok.root.SGN_CKSUM) + # 6. Encrypt 'SND_SEQ' + rc4 = Cipher(decrepit_algorithms.ARC4(Kseq), mode=None).encryptor() + tok.root.SND_SEQ = rc4.update(tok.root.SND_SEQ) + # 7. Include 'InitialContextToken pseudo ASN.1 header' + tok = KRB_GSSAPI_Token( + MechType="1.2.840.113554.1.2.2", # Kerberos 5 + innerToken=tok, + ) + return msgs, tok + else: + raise NotImplementedError + + def GSS_UnwrapEx(self, Context, msgs, signature): + """ + [MS-KILE] sect 3.4.5.5 + + - AES: RFC4121 sect 4.2.6.2 + - HMAC-RC4: RFC4757 sect 7.3 + """ + if Context.KrbSessionKey.etype in [17, 18]: # AES + confidentiality = signature.root.Flags.Sealed + # Real separation starts now: RFC4121 sect 4.2.4 + if confidentiality: + # 0. Concatenate the data + Data = signature.root.Data + Data += b"".join(x.data for x in msgs if x.conf_req_flag) + # 1. Un-Rotate + Data = strrot(Data, signature.root.RRC + signature.root.EC, right=False) + + # 2. Function to build 'ToSign', exclusively used for checksum + def MakeToSign(Confounder, DecText): + offset = 0 + # 2.a Confounder + ToSign = Confounder + # 2.b Messages + for msg in msgs: + msglen = len(msg.data) + if msg.conf_req_flag: + ToSign += DecText[offset : offset + msglen] + offset += msglen + elif msg.sign: + ToSign += msg.data + # 2.c Filler & Padding + ToSign += DecText[offset:] + return ToSign + + # 3. Decrypt + Data = Context.KrbSessionKey.decrypt( + keyusage=Context.RecvSealKeyUsage, + ciphertext=Data, + presignfunc=MakeToSign, + ) + # 4. Split + Data, f16header = ( + Data[:-16], + Data[-16:], + ) + # 5. Check header + hdr = signature.copy() + hdr.root.RRC = 0 + if f16header != bytes(hdr)[:16]: + raise ValueError("ERROR: Headers don't match") + # 6. Split (and ignore filler) + offset = 0 + for msg in msgs: + msglen = len(msg.data) + if msg.conf_req_flag: + msg.data = Data[offset : offset + msglen] + offset += msglen + # Case without msgs + if len(msgs) == 1 and not msgs[0].data: + msgs[0].data = Data + return msgs + else: + # No confidentiality is requested + # 0. Concatenate the data + Data = signature.root.Data + Data += b"".join(x.data for x in msgs if x.sign) + # 1. Un-Rotate + Data = strrot(Data, signature.root.RRC, right=False) + # 2. Split + Data, Mic = Data[: -signature.root.EC], Data[-signature.root.EC :] + # "Both the EC field and the RRC field in + # the token header SHALL be filled with zeroes for the purpose of + # calculating the checksum." + ToSign = Data + hdr = signature.copy() + hdr.root.RRC = 0 + hdr.root.EC = 0 + # Concatenate the data + ToSign += bytes(hdr)[:16] + # 3. Calculate the signature + sig = Context.KrbSessionKey.make_checksum( + keyusage=Context.RecvSealKeyUsage, + text=ToSign, + ) + # 4. Compare + if sig != Mic: + raise ValueError("ERROR: Checksums don't match") + # Case without msgs + if len(msgs) == 1 and not msgs[0].data: + msgs[0].data = Data + return msgs + elif Context.KrbSessionKey.etype in [23, 24]: # RC4 + # Drop wrapping + tok = signature.innerToken + + # Detect confidentiality + confidentiality = tok.root.SEAL_ALG != 0xFFFF + + # 0. Concatenate data + ToDecrypt = b"".join(x.data for x in msgs if x.conf_req_flag) + Kss = Context.KrbSessionKey.key + # 1. Compute the 'Kseq' key + if Context.KrbSessionKey.etype == 24: # EXP + Kseq = Hmac_MD5(Kss).digest(b"fortybits\x00" + b"\x00\x00\x00\x00") + Kseq = Kseq[:7] + b"\xab" * 9 + else: + Kseq = Hmac_MD5(Kss).digest(b"\x00\x00\x00\x00") + Kseq = Hmac_MD5(Kseq).digest(tok.root.SGN_CKSUM) + # 2. Decrypt 'SND_SEQ' + rc4 = Cipher(decrepit_algorithms.ARC4(Kseq), mode=None).encryptor() + seq = rc4.update(tok.root.SND_SEQ)[:4] + # 3. Compute the 'Kcrypt' key + Klocal = strxor(Kss, len(Kss) * b"\xf0") + if Context.KrbSessionKey.etype == 24: # EXP + Kcrypt = Hmac_MD5(Klocal).digest(b"fortybits\x00" + b"\x00\x00\x00\x00") + Kcrypt = Kcrypt[:7] + b"\xab" * 9 + else: + Kcrypt = Hmac_MD5(Klocal).digest(b"\x00\x00\x00\x00") + Kcrypt = Hmac_MD5(Kcrypt).digest(seq) + # 4. Decrypt + if confidentiality: + # 'encrypt' was requested + rc4 = Cipher(decrepit_algorithms.ARC4(Kcrypt), mode=None).encryptor() + Confounder = rc4.update(tok.root.CONFOUNDER) + Data = rc4.update(ToDecrypt) + # Split encrypted data + offset = 0 + for msg in msgs: + msglen = len(msg.data) + if msg.conf_req_flag: + msg.data = Data[offset : offset + msglen] + offset += msglen + else: + # 'encrypt' was not requested + Confounder = tok.root.CONFOUNDER + # 5. Verify SGN_CKSUM + ToSign = _rfc1964pad(b"".join(x.data for x in msgs if x.sign)) + Context.KrbSessionKey.verify_checksum( + keyusage=13, # See errata + text=bytes(tok)[:8] + Confounder + ToSign, + cksum=tok.root.SGN_CKSUM, + ) + return msgs + else: + raise NotImplementedError + + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + # New context + Context = self.CONTEXT(IsAcceptor=False, req_flags=req_flags) + + if Context.state == self.STATE.INIT and self.U2U: + # U2U - Get TGT + Context.state = self.STATE.CLI_SENT_TGTREQ + return ( + Context, + KRB_GSSAPI_Token( + MechType="1.2.840.113554.1.2.2.3", # U2U + innerToken=KRB_InnerToken( + TOK_ID=b"\x04\x00", + root=KRB_TGT_REQ(), + ), + ), + GSS_S_CONTINUE_NEEDED, + ) + + if Context.state in [self.STATE.INIT, self.STATE.CLI_SENT_TGTREQ]: + if not self.UPN: + raise ValueError("Missing UPN attribute") + + # Do we have a ST? + if self.ST is None: + # Client sends an AP-req + if not self.SPN and not target_name: + raise ValueError("Missing SPN/target_name attribute") + additional_tickets = [] + + if self.U2U: + try: + # GSSAPI / Kerberos + tgt_rep = input_token.root.innerToken.root + except AttributeError: + try: + # Kerberos + tgt_rep = input_token.innerToken.root + except AttributeError: + return Context, None, GSS_S_DEFECTIVE_TOKEN + if not isinstance(tgt_rep, KRB_TGT_REP): + tgt_rep.show() + raise ValueError("KerberosSSP: Unexpected input_token !") + additional_tickets = [tgt_rep.ticket] + + if self.TGT is None: + # Get TGT. We were passed a kerberos key + res = krb_as_req( + upn=self.UPN, + ip=self.DC_IP, + key=self.KEY, + password=self.PASSWORD, + debug=self.debug, + verbose=bool(self.debug), + ) + if res is None: + # Failed to retrieve the ticket + return Context, None, GSS_S_FAILURE + + # Update UPN (could have been canonicalized) + self.UPN = res.upn + + # Store TGT, + self.TGT = res.asrep.ticket + self.TGTSessionKey = res.sessionkey + elif self.TGTSessionKey is None: + # We have a TGT and were passed its key + self.TGTSessionKey = self.KEY + + # Get ST + if not self.TGTSessionKey: + raise ValueError("Cannot use TGT without the KEY") + + res = krb_tgs_req( + upn=self.UPN, + spn=self.SPN or target_name, + ip=self.DC_IP, + sessionkey=self.TGTSessionKey, + ticket=self.TGT, + additional_tickets=additional_tickets, + u2u=self.U2U, + debug=self.debug, + verbose=bool(self.debug), + ) + if not res: + # Failed to retrieve the ticket + return Context, None, GSS_S_FAILURE + + # Store the service ticket and associated key + Context.ST, Context.STSessionKey = res.tgsrep.ticket, res.sessionkey + elif not self.KEY: + raise ValueError("Must provide KEY with ST") + else: + # We were passed a ST and its key + Context.ST = self.ST + Context.STSessionKey = self.KEY + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + raise ValueError( + "Cannot use GSS_C_DELEG_FLAG when passed a service ticket !" + ) + + # Save ServerHostname + if len(Context.ST.sname.nameString) == 2: + Context.ServerHostname = Context.ST.sname.nameString[1].val.decode() + + # Build the KRB-AP + apOptions = ASN1_BIT_STRING("000") + if Context.flags & GSS_C_FLAGS.GSS_C_MUTUAL_FLAG: + apOptions.set(2, "1") # mutual-required + if self.U2U: + apOptions.set(1, "1") # use-session-key + Context.U2U = True + ap_req = KRB_AP_REQ( + apOptions=apOptions, + ticket=Context.ST, + authenticator=EncryptedData(), + ) + + # Get the current time + now_time = datetime.now(timezone.utc).replace(microsecond=0) + # Pick a random session key + Context.KrbSessionKey = Key.new_random_key( + self.SKEY_TYPE, + ) + + # We use a random SendSeqNum + Context.SendSeqNum = RandNum(0, 0x7FFFFFFF)._fix() + + # Get the realm of the client + _, crealm = _parse_upn(self.UPN) + + # Build the RFC4121 authenticator checksum + authenticator_checksum = KRB_AuthenticatorChecksum( + # RFC 4121 sect 4.1.1.2 + # "The Bnd field contains the MD5 hash of channel bindings" + Bnd=( + chan_bindings.digestMD5() + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + else (b"\x00" * 16) + ), + Flags=int(Context.flags), + ) + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + # Delegate TGT + raise NotImplementedError("GSS_C_DELEG_FLAG is not implemented !") + # authenticator_checksum.Deleg = KRB_CRED( + # tickets=[self.TGT], + # encPart=EncryptedData() + # ) + # authenticator_checksum.encPart.encrypt( + # Context.STSessionKey, + # EncKrbCredPart( + # ticketInfo=KrbCredInfo( + # key=EncryptionKey.fromKey(self.TGTSessionKey), + # prealm=ASN1_GENERAL_STRING(crealm), + # pname=PrincipalName.fromUPN(self.UPN), + # # TODO: rework API to pass starttime... here. + # sreralm=self.TGT.realm, + # sname=self.TGT.sname, + # ) + # ) + # ) + + # Build and encrypt the full KRB_Authenticator + ap_req.authenticator.encrypt( + Context.STSessionKey, + KRB_Authenticator( + crealm=crealm, + cname=PrincipalName.fromUPN(self.UPN), + cksum=Checksum( + cksumtype="KRB-AUTHENTICATOR", checksum=authenticator_checksum + ), + ctime=ASN1_GENERALIZED_TIME(now_time), + cusec=ASN1_INTEGER(0), + subkey=EncryptionKey.fromKey(Context.KrbSessionKey), + seqNumber=Context.SendSeqNum, + encAuthorizationData=AuthorizationData( + seq=[ + AuthorizationDataItem( + adType="AD-IF-RELEVANT", + adData=AuthorizationData( + seq=[ + AuthorizationDataItem( + adType="KERB-AUTH-DATA-TOKEN-RESTRICTIONS", + adData=KERB_AD_RESTRICTION_ENTRY( + restriction=LSAP_TOKEN_INFO_INTEGRITY( + MachineID=bytes(RandBin(32)), + PermanentMachineID=bytes( + RandBin(32) + ), + ) + ), + ), + # This isn't documented, but sent on Windows :/ + AuthorizationDataItem( + adType="KERB-LOCAL", + adData=b"\x00" * 16, + ), + ] + + ( + # Channel bindings + [ + AuthorizationDataItem( + adType="AD-AUTH-DATA-AP-OPTIONS", + adData=KERB_AUTH_DATA_AP_OPTIONS( + apOptions="KERB_AP_OPTIONS_CBT" + ), + ) + ] + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + else [] + ) + ), + ) + ] + ), + ), + ) + Context.state = self.STATE.CLI_SENT_APREQ + if Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + # Raw kerberos DCE-STYLE + return Context, ap_req, GSS_S_CONTINUE_NEEDED + else: + # Kerberos wrapper + return ( + Context, + KRB_GSSAPI_Token( + innerToken=KRB_InnerToken( + root=ap_req, + ) + ), + GSS_S_CONTINUE_NEEDED, + ) + + elif Context.state == self.STATE.CLI_SENT_APREQ: + if isinstance(input_token, KRB_AP_REP): + # Raw AP_REP was passed + ap_rep = input_token + else: + try: + # GSSAPI / Kerberos + ap_rep = input_token.root.innerToken.root + except AttributeError: + try: + # Kerberos + ap_rep = input_token.innerToken.root + except AttributeError: + try: + # Raw kerberos DCE-STYLE + ap_rep = input_token.root + except AttributeError: + return Context, None, GSS_S_DEFECTIVE_TOKEN + if not isinstance(ap_rep, KRB_AP_REP): + return Context, None, GSS_S_DEFECTIVE_TOKEN + + # Retrieve SessionKey + repPart = ap_rep.encPart.decrypt(Context.STSessionKey) + if repPart.subkey is not None: + Context.SessionKey = repPart.subkey.keyvalue.val + Context.KrbSessionKey = repPart.subkey.toKey() + + # OK ! + Context.state = self.STATE.CLI_RCVD_APREP + if Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + # [MS-KILE] sect 3.4.5.1 + # The client MUST generate an additional AP exchange reply message + # exactly as the server would as the final message to send to the + # server. + now_time = datetime.now(timezone.utc).replace(microsecond=0) + cli_ap_rep = KRB_AP_REP(encPart=EncryptedData()) + cli_ap_rep.encPart.encrypt( + Context.STSessionKey, + EncAPRepPart( + ctime=ASN1_GENERALIZED_TIME(now_time), + seqNumber=repPart.seqNumber, + subkey=None, + ), + ) + return Context, cli_ap_rep, GSS_S_COMPLETE + return Context, None, GSS_S_COMPLETE + elif ( + Context.state == self.STATE.CLI_RCVD_APREP + and Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE + ): + # DCE_STYLE with SPNEGOSSP + return Context, None, GSS_S_COMPLETE + else: + raise ValueError("KerberosSSP: Unknown state") + + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + # New context + Context = self.CONTEXT(IsAcceptor=True, req_flags=req_flags) + + import scapy.layers.msrpce.mspac # noqa: F401 + + if Context.state == self.STATE.INIT: + if self.UPN and self.SPN: + raise ValueError("Cannot use SPN and UPN at the same time !") + if self.SPN and self.TGT: + raise ValueError("Cannot use TGT with SPN.") + if self.UPN and not self.TGT: + # UPN is provided: use U2U + res = krb_as_req( + self.UPN, + self.DC_IP, + key=self.KEY, + password=self.PASSWORD, + ) + self.TGT, self.TGTSessionKey = res.asrep.ticket, res.sessionkey + + # Server receives AP-req, sends AP-rep + if isinstance(input_token, KRB_AP_REQ): + # Raw AP_REQ was passed + ap_req = input_token + else: + try: + # GSSAPI/Kerberos + ap_req = input_token.root.innerToken.root + except AttributeError: + try: + # Kerberos + ap_req = input_token.innerToken.root + except AttributeError: + try: + # Raw kerberos + ap_req = input_token.root + except AttributeError: + return Context, None, GSS_S_DEFECTIVE_TOKEN + + if isinstance(ap_req, KRB_TGT_REQ): + # Special U2U case + Context.U2U = True + return ( + None, + KRB_GSSAPI_Token( + MechType="1.2.840.113554.1.2.2.3", # U2U + innerToken=KRB_InnerToken( + TOK_ID=b"\x04\x01", + root=KRB_TGT_REP( + ticket=self.TGT, + ), + ), + ), + GSS_S_CONTINUE_NEEDED, + ) + elif not isinstance(ap_req, KRB_AP_REQ): + ap_req.show() + raise ValueError("Unexpected type in KerberosSSP") + if not self.KEY: + raise ValueError("Missing KEY attribute") + + now_time = datetime.now(timezone.utc).replace(microsecond=0) + + # If using a UPN, require U2U + if self.UPN and ap_req.apOptions.val[1] != "1": # use-session-key + # Required but not provided. Return an error + Context.U2U = True + err = KRB_GSSAPI_Token( + innerToken=KRB_InnerToken( + TOK_ID=b"\x03\x00", + root=KRB_ERROR( + errorCode="KRB_AP_ERR_USER_TO_USER_REQUIRED", + stime=ASN1_GENERALIZED_TIME(now_time), + realm=ap_req.ticket.realm, + sname=ap_req.ticket.sname, + eData=KRB_TGT_REP( + ticket=self.TGT, + ), + ), + ) + ) + return Context, err, GSS_S_CONTINUE_NEEDED + + # Validate the 'serverName' of the ticket. + sname = ap_req.ticket.getSPN() + our_sname = self.SPN or self.UPN + if not _spn_are_equal(our_sname, sname): + warning("KerberosSSP: bad server name: %s != %s" % (sname, our_sname)) + err = KRB_GSSAPI_Token( + innerToken=KRB_InnerToken( + TOK_ID=b"\x03\x00", + root=KRB_ERROR( + errorCode="KRB_AP_ERR_BADMATCH", + stime=ASN1_GENERALIZED_TIME(now_time), + realm=ap_req.ticket.realm, + sname=ap_req.ticket.sname, + eData=None, + ), + ) + ) + return Context, err, GSS_S_BAD_MECH + + # Decrypt the ticket + try: + tkt = ap_req.ticket.encPart.decrypt(self.KEY) + except ValueError as ex: + warning("KerberosSSP: %s (bad KEY?)" % ex) + err = KRB_GSSAPI_Token( + innerToken=KRB_InnerToken( + TOK_ID=b"\x03\x00", + root=KRB_ERROR( + errorCode="KRB_AP_ERR_MODIFIED", + stime=ASN1_GENERALIZED_TIME(now_time), + realm=ap_req.ticket.realm, + sname=ap_req.ticket.sname, + eData=None, + ), + ) + ) + return Context, err, GSS_S_DEFECTIVE_CREDENTIAL + + # Store information about the user in the Context + if tkt.authorizationData and tkt.authorizationData.seq: + # Get AD-IF-RELEVANT + adIfRelevant = tkt.authorizationData.getAuthData(0x1) + if adIfRelevant: + # Get AD-WIN2K-PAC + Context.PAC = adIfRelevant.getAuthData(0x80) + + # Get AP-REQ session key + Context.STSessionKey = tkt.key.toKey() + authenticator = ap_req.authenticator.decrypt(Context.STSessionKey) + + # Compute an application session key ([MS-KILE] sect 3.1.1.2) + subkey = None + if ap_req.apOptions.val[2] == "1": # mutual-required + appkey = Key.new_random_key( + self.SKEY_TYPE, + ) + Context.KrbSessionKey = appkey + Context.SessionKey = appkey.key + subkey = EncryptionKey.fromKey(appkey) + else: + Context.KrbSessionKey = self.KEY + Context.SessionKey = self.KEY.key + + # Eventually process the "checksum" + if authenticator.cksum and authenticator.cksum.cksumtype == 0x8003: + # KRB-Authenticator + authcksum = authenticator.cksum.checksum + Context.flags = authcksum.Flags + # Check channel bindings + if ( + chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + and chan_bindings.digestMD5() != authcksum.Bnd + and not ( + GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS in req_flags + and authcksum.Bnd == GSS_C_NO_CHANNEL_BINDINGS + ) + ): + # Channel binding checks failed. + return Context, None, GSS_S_BAD_BINDINGS + elif ( + chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + and GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS not in req_flags + ): + # Uhoh, we required channel bindings + return Context, None, GSS_S_BAD_BINDINGS + + # Build response (RFC4120 sect 3.2.4) + ap_rep = KRB_AP_REP(encPart=EncryptedData()) + ap_rep.encPart.encrypt( + Context.STSessionKey, + EncAPRepPart( + ctime=authenticator.ctime, + cusec=authenticator.cusec, + seqNumber=None, + subkey=subkey, + ), + ) + Context.state = self.STATE.SRV_SENT_APREP + if Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + # [MS-KILE] sect 3.4.5.1 + return Context, ap_rep, GSS_S_CONTINUE_NEEDED + return Context, ap_rep, GSS_S_COMPLETE # success + elif ( + Context.state == self.STATE.SRV_SENT_APREP + and Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE + ): + # [MS-KILE] sect 3.4.5.1 + # The server MUST receive the additional AP exchange reply message and + # verify that the message is constructed correctly. + if not input_token: + return Context, None, GSS_S_DEFECTIVE_TOKEN + # Server receives AP-req, sends AP-rep + if isinstance(input_token, KRB_AP_REP): + # Raw AP_REP was passed + ap_rep = input_token + else: + try: + # GSSAPI/Kerberos + ap_rep = input_token.root.innerToken.root + except AttributeError: + try: + # Raw Kerberos + ap_rep = input_token.root + except AttributeError: + return Context, None, GSS_S_DEFECTIVE_TOKEN + # Decrypt the AP-REP + try: + ap_rep.encPart.decrypt(Context.STSessionKey) + except ValueError as ex: + warning("KerberosSSP: %s (bad KEY?)" % ex) + return Context, None, GSS_S_DEFECTIVE_TOKEN + return Context, None, GSS_S_COMPLETE # success + else: + raise ValueError("KerberosSSP: Unknown state %s" % repr(Context.state)) + + def GSS_Passive( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + ): + if Context is None: + Context = self.CONTEXT(True) + Context.passive = True + + if Context.state == self.STATE.INIT or ( + # In DCE/RPC, there's an extra AP-REP sent from the client. + Context.state == self.STATE.SRV_SENT_APREP + and req_flags & GSS_C_FLAGS.GSS_C_DCE_STYLE + ): + Context, _, status = self.GSS_Accept_sec_context( + Context, + input_token=input_token, + req_flags=req_flags, + ) + if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: + Context.state = self.STATE.CLI_SENT_APREQ + else: + Context.state = self.STATE.FAILED + elif Context.state == self.STATE.CLI_SENT_APREQ: + Context, _, status = self.GSS_Init_sec_context( + Context, + input_token=input_token, + req_flags=req_flags, + ) + if status == GSS_S_COMPLETE: + if req_flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + status = GSS_S_CONTINUE_NEEDED + Context.state = self.STATE.SRV_SENT_APREP + else: + Context.state == self.STATE.FAILED + else: + # Unknown state. Don't crash though. + status = GSS_S_FAILURE + + return Context, status + + def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): + if Context.IsAcceptor is not IsAcceptor: + return + # Swap everything + Context.SendSealKeyUsage, Context.RecvSealKeyUsage = ( + Context.RecvSealKeyUsage, + Context.SendSealKeyUsage, + ) + Context.SendSignKeyUsage, Context.RecvSignKeyUsage = ( + Context.RecvSignKeyUsage, + Context.SendSignKeyUsage, + ) + Context.IsAcceptor = not Context.IsAcceptor + + def LegsAmount(self, Context: CONTEXT): + if Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + return 4 + else: + return 2 + + def MaximumSignatureLength(self, Context: CONTEXT): + if Context.flags & GSS_C_FLAGS.GSS_C_CONF_FLAG: + # TODO: support DES + if Context.KrbSessionKey.etype in [17, 18]: # AES + return 76 + elif Context.KrbSessionKey.etype in [23, 24]: # RC4_HMAC + return 45 + else: + raise NotImplementedError + else: + return 28 diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index 962648e838f..05bb6ba0c2b 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -1,48 +1,107 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Classes and functions for layer 2 protocols. """ -from __future__ import absolute_import -from __future__ import print_function -import os +import itertools +import socket import struct import time -import socket from scapy.ansmachine import AnsweringMachine from scapy.arch import get_if_addr, get_if_hwaddr -from scapy.base_classes import Gen, Net -from scapy.compat import chb, orb +from scapy.base_classes import Gen, Net, _ScopedIP +from scapy.compat import chb from scapy.config import conf from scapy import consts from scapy.data import ARPHDR_ETHER, ARPHDR_LOOPBACK, ARPHDR_METRICOM, \ - DLT_ETHERNET_MPACKET, DLT_LINUX_IRDA, DLT_LINUX_SLL, DLT_LOOP, \ - DLT_NULL, ETHER_ANY, ETHER_BROADCAST, ETHER_TYPES, ETH_P_ARP, \ - ETH_P_MACSEC -from scapy.error import warning, ScapyNoDstMacException -from scapy.fields import BCDFloatField, BitField, ByteField, \ - ConditionalField, FieldLenField, FCSField, \ - IntEnumField, IntField, IP6Field, IPField, \ - LenField, MACField, MultipleTypeField, \ - ShortEnumField, ShortField, SourceIP6Field, SourceIPField, \ - StrFixedLenField, StrLenField, X3BytesField, XByteField, XIntField, \ - XShortEnumField, XShortField -from scapy.modules.six import viewitems + DLT_ETHERNET_MPACKET, DLT_LINUX_IRDA, DLT_LINUX_SLL, DLT_LINUX_SLL2, \ + DLT_LOOP, DLT_NULL, ETHER_ANY, ETHER_BROADCAST, ETHER_TYPES, ETH_P_ARP, ETH_P_MACSEC +from scapy.error import ( + ScapyNoDstMacException, + log_runtime, + warning, +) +from scapy.fields import ( + BCDFloatField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + FCSField, + FieldLenField, + IP6Field, + IPField, + IntEnumField, + IntField, + LenField, + MACField, + MultipleTypeField, + OUIField, + ShortEnumField, + ShortField, + SourceIP6Field, + SourceIPField, + StrFixedLenField, + StrLenField, + ThreeBytesField, + XByteField, + XIntField, + XShortEnumField, + XShortField, +) +from scapy.interfaces import _GlobInterfaceType, resolve_iface from scapy.packet import bind_layers, Packet -from scapy.plist import PacketList, SndRcvList -from scapy.sendrecv import sendp, srp, srp1 -from scapy.utils import checksum, hexdump, hexstr, inet_ntoa, inet_aton, \ - mac2str, valid_mac, valid_net, valid_net6 +from scapy.plist import ( + PacketList, + QueryAnswer, + SndRcvList, + _PacketList, +) +from scapy.sendrecv import sendp, srp, srp1, srploop +from scapy.utils import ( + checksum, + hexdump, + hexstr, + in4_getnsmac, + in4_ismaddr, + inet_aton, + inet_ntoa, + mac2str, + pretty_list, + valid_mac, + valid_net, + valid_net6, +) + +# Typing imports +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Tuple, + Type, + Union, + cast, +) +from scapy.interfaces import NetworkInterface + + if conf.route is None: # unused import, only to initialize conf.route import scapy.route # noqa: F401 +# type definitions +_ResolverCallable = Callable[[Packet, Packet], Optional[str]] + ################# # Tools # ################# @@ -50,41 +109,69 @@ class Neighbor: def __init__(self): - self.resolvers = {} + # type: () -> None + self.resolvers = {} # type: Dict[Tuple[Type[Packet], Type[Packet]], _ResolverCallable] # noqa: E501 def register_l3(self, l2, l3, resolve_method): + # type: (Type[Packet], Type[Packet], _ResolverCallable) -> None self.resolvers[l2, l3] = resolve_method def resolve(self, l2inst, l3inst): + # type: (Packet, Packet) -> Optional[str] k = l2inst.__class__, l3inst.__class__ if k in self.resolvers: return self.resolvers[k](l2inst, l3inst) + return None def __repr__(self): + # type: () -> str return "\n".join("%-15s -> %-15s" % (l2.__name__, l3.__name__) for l2, l3 in self.resolvers) # noqa: E501 conf.neighbor = Neighbor() -conf.netcache.new_cache("arp_cache", 120) # cache entries expire after 120s +# cache entries expire after 120s +_arp_cache = conf.netcache.new_cache("arp_cache", 120) @conf.commands.register def getmacbyip(ip, chainCC=0): - """Return MAC address corresponding to a given IP address""" + # type: (str, int) -> Optional[str] + """ + Returns the destination MAC address used to reach a given IP address. + + This will follow the routing table and will issue an ARP request if + necessary. Special cases (multicast, etc.) are also handled. + + .. seealso:: :func:`~scapy.layers.inet6.getmacbyip6` for IPv6. + """ + # Sanitize the IP if isinstance(ip, Net): ip = next(iter(ip)) ip = inet_ntoa(inet_aton(ip or "0.0.0.0")) - tmp = [orb(e) for e in inet_aton(ip)] - if (tmp[0] & 0xf0) == 0xe0: # mcast @ - return "01:00:5e:%.2x:%.2x:%.2x" % (tmp[1] & 0x7f, tmp[2], tmp[3]) + + # Multicast + if in4_ismaddr(ip): # mcast @ + mac = in4_getnsmac(inet_aton(ip)) + return mac + + # Check the routing table iff, _, gw = conf.route.route(ip) - if ((iff == consts.LOOPBACK_INTERFACE) or (ip == conf.route.get_if_bcast(iff))): # noqa: E501 + + # Limited broadcast + if ip == "255.255.255.255": return "ff:ff:ff:ff:ff:ff" + + # Directed broadcast + if (iff == conf.loopback_name) or (ip in conf.route.get_if_bcast(iff)): + return "ff:ff:ff:ff:ff:ff" + + # An ARP request is necessary if gw != "0.0.0.0": ip = gw - mac = conf.netcache.arp_cache.get(ip) + # Check the cache + mac = _arp_cache.get(ip) if mac: return mac @@ -97,11 +184,11 @@ def getmacbyip(ip, chainCC=0): chainCC=chainCC, nofilter=1) except Exception as ex: - warning("getmacbyip failed on %s" % ex) + warning("getmacbyip failed on %s", ex) return None if res is not None: mac = res.payload.hwsrc - conf.netcache.arp_cache[ip] = mac + _arp_cache[ip] = mac return mac return None @@ -110,10 +197,18 @@ def getmacbyip(ip, chainCC=0): class DestMACField(MACField): def __init__(self, name): + # type: (str) -> None MACField.__init__(self, name, None) def i2h(self, pkt, x): - if x is None: + # type: (Optional[Packet], Optional[str]) -> str + if x is None and pkt is not None: + x = None + return super(DestMACField, self).i2h(pkt, x) + + def i2m(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> bytes + if x is None and pkt is not None: try: x = conf.neighbor.resolve(pkt, pkt.payload) except socket.error: @@ -123,42 +218,64 @@ def i2h(self, pkt, x): raise ScapyNoDstMacException() else: x = "ff:ff:ff:ff:ff:ff" - warning("Mac address to reach destination not found. Using broadcast.") # noqa: E501 - return MACField.i2h(self, pkt, x) - - def i2m(self, pkt, x): - return MACField.i2m(self, pkt, self.i2h(pkt, x)) + warning( + "MAC address to reach destination not found. Using broadcast." + ) + return super(DestMACField, self).i2m(pkt, x) class SourceMACField(MACField): __slots__ = ["getif"] def __init__(self, name, getif=None): + # type: (str, Optional[Any]) -> None MACField.__init__(self, name, None) self.getif = (lambda pkt: pkt.route()[0]) if getif is None else getif def i2h(self, pkt, x): + # type: (Optional[Packet], Optional[str]) -> str if x is None: iff = self.getif(pkt) - if iff is None: - iff = conf.iface if iff: - try: - x = get_if_hwaddr(iff) - except Exception as e: - warning("Could not get the source MAC: %s" % e) + x = resolve_iface(iff).mac if x is None: x = "00:00:00:00:00:00" - return MACField.i2h(self, pkt, x) + return super(SourceMACField, self).i2h(pkt, x) def i2m(self, pkt, x): - return MACField.i2m(self, pkt, self.i2h(pkt, x)) + # type: (Optional[Packet], Optional[Any]) -> bytes + return super(SourceMACField, self).i2m(pkt, self.i2h(pkt, x)) # Layers -ETHER_TYPES['802_AD'] = 0x88a8 -ETHER_TYPES['802_1AE'] = ETH_P_MACSEC +HARDWARE_TYPES = { + 1: "Ethernet (10Mb)", + 2: "Ethernet (3Mb)", + 3: "AX.25", + 4: "Proteon ProNET Token Ring", + 5: "Chaos", + 6: "IEEE 802 Networks", + 7: "ARCNET", + 8: "Hyperchannel", + 9: "Lanstar", + 10: "Autonet Short Address", + 11: "LocalTalk", + 12: "LocalNet", + 13: "Ultra link", + 14: "SMDS", + 15: "Frame relay", + 16: "ATM", + 17: "HDLC", + 18: "Fibre Channel", + 19: "ATM", + 20: "Serial Line", + 21: "ATM", +} + +ETHER_TYPES[0x88a8] = '802_1AD' +ETHER_TYPES[0x88e7] = '802_1AH' +ETHER_TYPES[ETH_P_MACSEC] = '802_1AE' class Ether(Packet): @@ -169,19 +286,23 @@ class Ether(Packet): __slots__ = ["_defrag_pos"] def hashret(self): + # type: () -> bytes return struct.pack("H", self.type) + self.payload.hashret() def answers(self, other): + # type: (Packet) -> int if isinstance(other, Ether): if self.type == other.type: return self.payload.answers(other.payload) return 0 def mysummary(self): + # type: () -> str return self.sprintf("%src% > %dst% (%type%)") @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): + # type: (Optional[bytes], *Any, **Any) -> Type[Packet] if _pkt and len(_pkt) >= 14: if struct.unpack("!H", _pkt[12:14])[0] <= 1500: return Dot3 @@ -195,19 +316,23 @@ class Dot3(Packet): LenField("len", None, "H")] def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, bytes] tmp_len = self.len return s[:tmp_len], s[tmp_len:] def answers(self, other): + # type: (Packet) -> int if isinstance(other, Dot3): return self.payload.answers(other.payload) return 0 def mysummary(self): + # type: () -> str return "802.3 %s > %s" % (self.src, self.dst) @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): + # type: (Optional[Any], *Any, **Any) -> Type[Packet] if _pkt and len(_pkt) >= 14: if struct.unpack("!H", _pkt[12:14])[0] > 1500: return Ether @@ -221,29 +346,50 @@ class LLC(Packet): ByteField("ctrl", 0)] -def l2_register_l3(l2, l3): - return conf.neighbor.resolve(l2, l3.payload) +def l2_register_l3(l2: Packet, l3: Packet) -> Optional[str]: + """ + Delegates resolving the default L2 destination address to the payload of L3. + """ + neighbor = conf.neighbor # type: Neighbor + return neighbor.resolve(l2, l3.payload) conf.neighbor.register_l3(Ether, LLC, l2_register_l3) conf.neighbor.register_l3(Dot3, LLC, l2_register_l3) +COOKED_LINUX_PACKET_TYPES = { + 0: "unicast", + 1: "broadcast", + 2: "multicast", + 3: "unicast-to-another-host", + 4: "sent-by-us" +} + + class CookedLinux(Packet): # Documentation: http://www.tcpdump.org/linktypes/LINKTYPE_LINUX_SLL.html name = "cooked linux" # from wireshark's database - fields_desc = [ShortEnumField("pkttype", 0, {0: "unicast", - 1: "broadcast", - 2: "multicast", - 3: "unicast-to-another-host", - 4: "sent-by-us"}), + fields_desc = [ShortEnumField("pkttype", 0, COOKED_LINUX_PACKET_TYPES), XShortField("lladdrtype", 512), ShortField("lladdrlen", 0), - StrFixedLenField("src", "", 8), + StrFixedLenField("src", b"", 8), XShortEnumField("proto", 0x800, ETHER_TYPES)] +class CookedLinuxV2(CookedLinux): + # Documentation: https://www.tcpdump.org/linktypes/LINKTYPE_LINUX_SLL2.html + name = "cooked linux v2" + fields_desc = [XShortEnumField("proto", 0x800, ETHER_TYPES), + ShortField("reserved", 0), + IntField("ifindex", 0), + XShortField("lladdrtype", 512), + ByteEnumField("pkttype", 0, COOKED_LINUX_PACKET_TYPES), + ByteField("lladdrlen", 0), + StrFixedLenField("src", b"", 8)] + + class MPacketPreamble(Packet): # IEEE 802.3br Figure 99-3 name = "MPacket Preamble" @@ -253,7 +399,7 @@ class MPacketPreamble(Packet): class SNAP(Packet): name = "SNAP" - fields_desc = [X3BytesField("OUI", 0x000000), + fields_desc = [OUIField("OUI", 0x000000), XShortEnumField("code", 0x000, ETHER_TYPES)] @@ -264,11 +410,15 @@ class Dot1Q(Packet): name = "802.1Q" aliastypes = [Ether] fields_desc = [BitField("prio", 0, 3), - BitField("id", 0, 1), + BitField("dei", 0, 1), BitField("vlan", 1, 12), XShortEnumField("type", 0x0000, ETHER_TYPES)] + deprecated_fields = { + "id": ("dei", "2.5.0"), + } def answers(self, other): + # type: (Packet) -> int if isinstance(other, Dot1Q): if ((self.type == other.type) and (self.vlan == other.vlan)): @@ -278,16 +428,19 @@ def answers(self, other): return 0 def default_payload_class(self, pay): + # type: (bytes) -> Type[Packet] if self.type <= 1500: return LLC return conf.raw_layer def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] if self.type <= 1500: return s[:self.type], s[self.type:] return s, None def mysummary(self): + # type: () -> str if isinstance(self.underlayer, Ether): return self.underlayer.sprintf("802.1q %Ether.src% > %Ether.dst% (%Dot1Q.type%) vlan %Dot1Q.vlan%") # noqa: E501 else: @@ -318,7 +471,7 @@ class STP(Packet): class ARP(Packet): name = "ARP" fields_desc = [ - XShortField("hwtype", 0x0001), + XShortEnumField("hwtype", 0x0001, HARDWARE_TYPES), XShortEnumField("ptype", 0x0800, ETHER_TYPES), FieldLenField("hwlen", None, fmt="B", length_of="hwsrc"), FieldLenField("plen", None, fmt="B", length_of="psrc"), @@ -347,13 +500,13 @@ class ARP(Packet): ), MultipleTypeField( [ - (SourceIPField("psrc", "pdst"), + (SourceIPField("psrc"), (lambda pkt: pkt.ptype == 0x0800 and pkt.plen == 4, lambda pkt, val: pkt.ptype == 0x0800 and ( pkt.plen == 4 or (pkt.plen is None and (val is None or valid_net(val))) ))), - (SourceIP6Field("psrc", "pdst"), + (SourceIP6Field("psrc"), (lambda pkt: pkt.ptype == 0x86dd and pkt.plen == 16, lambda pkt, val: pkt.ptype == 0x86dd and ( pkt.plen == 16 or (pkt.plen is None and @@ -394,36 +547,46 @@ class ARP(Packet): ] def hashret(self): + # type: () -> bytes return struct.pack(">HHH", self.hwtype, self.ptype, ((self.op + 1) // 2)) + self.payload.hashret() def answers(self, other): + # type: (Packet) -> int if not isinstance(other, ARP): return False if self.op != other.op + 1: return False # We use a loose comparison on psrc vs pdst to catch answers # with ARP leaks - self_psrc = self.get_field('psrc').i2m(self, self.psrc) - other_pdst = other.get_field('pdst').i2m(other, other.pdst) + self_psrc = self.get_field('psrc').i2m(self, self.psrc) # type: bytes + other_pdst = other.get_field('pdst').i2m(other, other.pdst) \ + # type: bytes return self_psrc[:len(other_pdst)] == other_pdst[:len(self_psrc)] def route(self): - fld, dst = self.getfield_and_val("pdst") - fld, dst = fld._find_fld_pkt_val(self, dst) + # type: () -> Tuple[Optional[str], Optional[str], Optional[str]] + fld, dst = cast(Tuple[MultipleTypeField, str], + self.getfield_and_val("pdst")) + fld_inner, dst = fld._find_fld_pkt_val(self, dst) + scope = None + if isinstance(dst, (Net, _ScopedIP)): + scope = dst.scope if isinstance(dst, Gen): dst = next(iter(dst)) - if isinstance(fld, IP6Field): - return conf.route6.route(dst) - elif isinstance(fld, IPField): - return conf.route.route(dst) + if isinstance(fld_inner, IP6Field): + return conf.route6.route(dst, dev=scope) + elif isinstance(fld_inner, IPField): + return conf.route.route(dst, dev=scope) else: return None, None, None def extract_padding(self, s): - return "", s + # type: (bytes) -> Tuple[bytes, bytes] + return b"", s def mysummary(self): + # type: () -> str if self.op == 1: return self.sprintf("ARP who has %pdst% says %psrc%") if self.op == 2: @@ -431,32 +594,42 @@ def mysummary(self): return self.sprintf("ARP %op% %psrc% > %pdst%") -def l2_register_l3_arp(l2, l3): - return getmacbyip(l3.pdst) +def l2_register_l3_arp(l2: Packet, l3: Packet) -> Optional[str]: + """ + Resolves the default L2 destination address when ARP is used. + """ + if l3.op == 1: # who-has + return "ff:ff:ff:ff:ff:ff" + elif l3.op == 2: # is-at + log_runtime.warning( + "You should be providing the Ethernet destination MAC address when " + "sending an is-at ARP." + ) + # Need ARP request to send ARP request... + plen = l3.get_field("pdst").i2len(l3, l3.pdst) + if plen == 4: + return getmacbyip(l3.pdst) + elif plen == 32: + from scapy.layers.inet6 import getmacbyip6 + return getmacbyip6(l3.pdst) + # Can't even do that + log_runtime.warning( + "You should be providing the Ethernet destination mac when sending this " + "kind of ARP packets." + ) + return None conf.neighbor.register_l3(Ether, ARP, l2_register_l3_arp) -class ERSPAN(Packet): - name = "ERSPAN" - fields_desc = [BitField("ver", 0, 4), - BitField("vlan", 0, 12), - BitField("cos", 0, 3), - BitField("en", 0, 2), - BitField("t", 0, 1), - BitField("session_id", 0, 10), - BitField("reserved", 0, 12), - BitField("index", 0, 20), - ] - - class GRErouting(Packet): name = "GRE routing information" fields_desc = [ShortField("address_family", 0), ByteField("SRE_offset", 0), FieldLenField("SRE_len", None, "routing_info", "B"), - StrLenField("routing_info", "", "SRE_len"), + StrLenField("routing_info", b"", + length_from=lambda pkt: pkt.SRE_len), ] @@ -482,11 +655,13 @@ class GRE(Packet): @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): + # type: (Optional[Any], *Any, **Any) -> Type[Packet] if _pkt and struct.unpack("!H", _pkt[2:4])[0] == 0x880b: return GRE_PPTP return cls def post_build(self, p, pay): + # type: (bytes, bytes) -> bytes p += pay if self.chksum_present and self.chksum is None: c = checksum(p) @@ -521,6 +696,7 @@ class GRE_PPTP(GRE): ConditionalField(XIntField("ack_number", None), lambda pkt: pkt.acknum_present == 1)] # noqa: E501 def post_build(self, p, pay): + # type: (bytes, bytes) -> bytes p += pay if self.payload_len is None: pay_len = len(pay) @@ -533,10 +709,12 @@ def post_build(self, p, pay): class LoIntEnumField(IntEnumField): def m2i(self, pkt, x): + # type: (Optional[Packet], int) -> int return x >> 24 def i2m(self, pkt, x): - return x << 24 + # type: (Optional[Packet], Union[List[int], int, None]) -> int + return cast(int, x) << 24 # https://github.com/wireshark/wireshark/blob/fe219637a6748130266a0b0278166046e60a2d68/epan/dissectors/packet-null.c @@ -548,44 +726,84 @@ def i2m(self, pkt, x): 0x18: "IPv6", 0x1c: "IPv6", 0x1e: "IPv6"} -class Loopback(Packet): - r"""\*BSD loopback layer""" +# On OpenBSD, Loopback = LoopbackOpenBSD. On other platforms, the 2 are available. +# This is to be compatible with both tcpdump and tshark +class Loopback(Packet): + r""" + \*BSD loopback layer + """ + __slots__ = ["_defrag_pos"] name = "Loopback" if consts.OPENBSD: fields_desc = [IntEnumField("type", 0x2, LOOPBACK_TYPES)] else: fields_desc = [LoIntEnumField("type", 0x2, LOOPBACK_TYPES)] - __slots__ = ["_defrag_pos"] + + +if consts.OPENBSD: + LoopbackOpenBSD = Loopback +else: + class LoopbackOpenBSD(Loopback): + name = "OpenBSD Loopback" + fields_desc = [IntEnumField("type", 0x2, LOOPBACK_TYPES)] class Dot1AD(Dot1Q): name = '802_1AD' +class Dot1AH(Packet): + name = "802_1AH" + fields_desc = [BitField("prio", 0, 3), + BitField("dei", 0, 1), + BitField("nca", 0, 1), + BitField("res1", 0, 1), + BitField("res2", 0, 2), + ThreeBytesField("isid", 0)] + + def answers(self, other): + # type: (Packet) -> int + if isinstance(other, Dot1AH): + if self.isid == other.isid: + return self.payload.answers(other.payload) + return 0 + + def mysummary(self): + # type: () -> str + return self.sprintf("802.1ah (isid=%Dot1AH.isid%") + + +conf.neighbor.register_l3(Ether, Dot1AH, l2_register_l3) + + bind_layers(Dot3, LLC) bind_layers(Ether, LLC, type=122) bind_layers(Ether, LLC, type=34928) bind_layers(Ether, Dot1Q, type=33024) bind_layers(Ether, Dot1AD, type=0x88a8) +bind_layers(Ether, Dot1AH, type=0x88e7) bind_layers(Dot1AD, Dot1AD, type=0x88a8) bind_layers(Dot1AD, Dot1Q, type=0x8100) +bind_layers(Dot1AD, Dot1AH, type=0x88e7) bind_layers(Dot1Q, Dot1AD, type=0x88a8) -bind_layers(ERSPAN, Ether) +bind_layers(Dot1Q, Dot1AH, type=0x88e7) +bind_layers(Dot1AH, Ether) bind_layers(Ether, Ether, type=1) bind_layers(Ether, ARP, type=2054) bind_layers(CookedLinux, LLC, proto=122) bind_layers(CookedLinux, Dot1Q, proto=33024) bind_layers(CookedLinux, Dot1AD, type=0x88a8) +bind_layers(CookedLinux, Dot1AH, type=0x88e7) bind_layers(CookedLinux, Ether, proto=1) bind_layers(CookedLinux, ARP, proto=2054) bind_layers(MPacketPreamble, Ether) bind_layers(GRE, LLC, proto=122) bind_layers(GRE, Dot1Q, proto=33024) bind_layers(GRE, Dot1AD, type=0x88a8) +bind_layers(GRE, Dot1AH, type=0x88e7) bind_layers(GRE, Ether, proto=0x6558) bind_layers(GRE, ARP, proto=2054) -bind_layers(GRE, ERSPAN, proto=0x88be, seqnum_present=1) bind_layers(GRE, GRErouting, {"routing_present": 1}) bind_layers(GRErouting, conf.raw_layer, {"address_family": 0, "SRE_len": 0}) bind_layers(GRErouting, GRErouting) @@ -593,6 +811,7 @@ class Dot1AD(Dot1Q): bind_layers(LLC, SNAP, dsap=170, ssap=170, ctrl=3) bind_layers(SNAP, Dot1Q, code=33024) bind_layers(SNAP, Dot1AD, type=0x88a8) +bind_layers(SNAP, Dot1AH, type=0x88e7) bind_layers(SNAP, Ether, code=1) bind_layers(SNAP, ARP, code=2054) bind_layers(SNAP, STP, code=267) @@ -602,10 +821,11 @@ class Dot1AD(Dot1Q): conf.l2types.register_num2layer(ARPHDR_LOOPBACK, Ether) conf.l2types.register_layer2num(ARPHDR_ETHER, Dot3) conf.l2types.register(DLT_LINUX_SLL, CookedLinux) +conf.l2types.register(DLT_LINUX_SLL2, CookedLinuxV2) conf.l2types.register(DLT_ETHERNET_MPACKET, MPacketPreamble) conf.l2types.register_num2layer(DLT_LINUX_IRDA, CookedLinux) -conf.l2types.register(DLT_LOOP, Loopback) -conf.l2types.register_num2layer(DLT_NULL, Loopback) +conf.l2types.register(DLT_NULL, Loopback) +conf.l2types.register(DLT_LOOP, LoopbackOpenBSD) conf.l3types.register(ETH_P_ARP, ARP) @@ -614,58 +834,273 @@ class Dot1AD(Dot1Q): @conf.commands.register -def arpcachepoison(target, victim, interval=60): - """Poison target's cache with (your MAC,victim's IP) couple -arpcachepoison(target, victim, [interval=60]) -> None -""" - tmac = getmacbyip(target) - p = Ether(dst=tmac) / ARP(op="who-has", psrc=victim, pdst=target) +def arpcachepoison( + target, # type: Union[str, List[str]] + addresses, # type: Union[str, Tuple[str, str], List[Tuple[str, str]]] + broadcast=False, # type: bool + count=None, # type: Optional[int] + interval=15, # type: int + **kwargs, # type: Any +): + # type: (...) -> None + """Poison targets' ARP cache + + :param target: Can be an IP, subnet (string) or a list of IPs. This lists the IPs + or the subnet that will be poisoned. + :param addresses: Can be either a string, a tuple of a list of tuples. + If it's a string, it's the IP to advertise to the victim, + with the local interface's MAC. If it's a tuple, + it's ("IP", "MAC"). It it's a list, it's [("IP", "MAC")]. + "IP" can be a subnet of course. + :param broadcast: Use broadcast ethernet + + Examples for target "192.168.0.2":: + + >>> arpcachepoison("192.168.0.2", "192.168.0.1") + >>> arpcachepoison("192.168.0.1/24", "192.168.0.1") + >>> arpcachepoison(["192.168.0.2", "192.168.0.3"], "192.168.0.1") + >>> arpcachepoison("192.168.0.2", ("192.168.0.1", get_if_hwaddr("virbr0"))) + >>> arpcachepoison("192.168.0.2", [("192.168.0.1", get_if_hwaddr("virbr0"), + ... ("192.168.0.2", "aa:aa:aa:aa:aa:aa")]) + + """ + if isinstance(target, str): + targets = Net(target) # type: Union[Net, List[str]] + str_target = target + else: + targets = target + str_target = target[0] + if isinstance(addresses, str): + couple_list = [(addresses, get_if_hwaddr(conf.route.route(str_target)[0]))] + elif isinstance(addresses, tuple): + couple_list = [addresses] + else: + couple_list = addresses + p: List[Packet] = [ + Ether(src=y, dst="ff:ff:ff:ff:ff:ff" if broadcast else None) / + ARP(op="who-has", psrc=x, pdst=targets, + hwsrc=y, hwdst="00:00:00:00:00:00") + for x, y in couple_list + ] + if count is not None: + sendp(p, iface_hint=str_target, count=count, inter=interval, **kwargs) + return try: while True: - sendp(p, iface_hint=target) - if conf.verb > 1: - os.write(1, b".") + sendp(p, iface_hint=str_target, **kwargs) time.sleep(interval) except KeyboardInterrupt: pass +@conf.commands.register +def arp_mitm( + ip1, # type: str + ip2, # type: str + mac1=None, # type: Optional[Union[str, List[str]]] + mac2=None, # type: Optional[Union[str, List[str]]] + broadcast=False, # type: bool + target_mac=None, # type: Optional[str] + iface=None, # type: Optional[_GlobInterfaceType] + inter=3, # type: int +): + # type: (...) -> None + r"""ARP MitM: poison 2 target's ARP cache + + :param ip1: IPv4 of the first machine + :param ip2: IPv4 of the second machine + :param mac1: MAC of the first machine (optional: will ARP otherwise) + :param mac2: MAC of the second machine (optional: will ARP otherwise) + :param broadcast: if True, will use broadcast mac for MitM by default + :param target_mac: MAC of the attacker (optional: default to the interface's one) + :param iface: the network interface. (optional: default, route for ip1) + + Example usage:: + + $ sysctl net.ipv4.conf.virbr0.send_redirects=0 # virbr0 = interface + $ sysctl net.ipv4.ip_forward=1 + $ sudo iptables -t mangle -A PREROUTING -j TTL --ttl-inc 1 + $ sudo scapy + >>> arp_mitm("192.168.122.156", "192.168.122.17") + + Alternative usages: + >>> arp_mitm("10.0.0.1", "10.1.1.0/21", iface="eth1") + >>> arp_mitm("10.0.0.1", "10.1.1.2", + ... target_mac="aa:aa:aa:aa:aa:aa", + ... mac2="00:1e:eb:bf:c1:ab") + + .. warning:: + If using a subnet, this will first perform an arping, unless broadcast is on! + + Remember to change the sysctl settings back.. + """ + if not iface: + iface = conf.route.route(ip1)[0] + if not target_mac: + target_mac = get_if_hwaddr(iface) + + def _tups(ip, mac): + # type: (str, Optional[Union[str, List[str]]]) -> Iterable[Tuple[str, str]] + if mac is None: + if broadcast: + # ip can be a Net/list/etc and will be iterated upon while sending + return [(ip, "ff:ff:ff:ff:ff:ff")] + return [(x.query.pdst, x.answer.hwsrc) + for x in arping(ip, verbose=0, iface=iface)[0]] + elif isinstance(mac, list): + return [(ip, x) for x in mac] + else: + return [(ip, mac)] + + tup1 = _tups(ip1, mac1) + if not tup1: + raise OSError(f"Could not resolve {ip1}") + tup2 = _tups(ip2, mac2) + if not tup2: + raise OSError(f"Could not resolve {ip2}") + print(f"MITM on {iface}: %s <--> {target_mac} <--> %s" % ( + [x[1] for x in tup1], + [x[1] for x in tup2], + )) + # We loop who-has requests + srploop( + list(itertools.chain( + (x + for ipa, maca in tup1 + for ipb, _ in tup2 + if ipb != ipa + for x in + Ether(dst=maca, src=target_mac) / + ARP(op="who-has", psrc=ipb, pdst=ipa, + hwsrc=target_mac, hwdst="00:00:00:00:00:00") + ), + (x + for ipb, macb in tup2 + for ipa, _ in tup1 + if ipb != ipa + for x in + Ether(dst=macb, src=target_mac) / + ARP(op="who-has", psrc=ipa, pdst=ipb, + hwsrc=target_mac, hwdst="00:00:00:00:00:00") + ), + )), + filter="arp and arp[7] = 2", + inter=inter, + iface=iface, + timeout=0.5, + verbose=1, + store=0, + ) + print("Restoring...") + sendp( + list(itertools.chain( + (x + for ipa, maca in tup1 + for ipb, macb in tup2 + if ipb != ipa + for x in + Ether(dst="ff:ff:ff:ff:ff:ff", src=macb) / + ARP(op="who-has", psrc=ipb, pdst=ipa, + hwsrc=macb, hwdst="00:00:00:00:00:00") + ), + (x + for ipb, macb in tup2 + for ipa, maca in tup1 + if ipb != ipa + for x in + Ether(dst="ff:ff:ff:ff:ff:ff", src=maca) / + ARP(op="who-has", psrc=ipa, pdst=ipb, + hwsrc=maca, hwdst="00:00:00:00:00:00") + ), + )), + iface=iface + ) + + class ARPingResult(SndRcvList): - def __init__(self, res=None, name="ARPing", stats=None): + def __init__(self, + res=None, # type: Optional[Union[_PacketList[QueryAnswer], List[QueryAnswer]]] # noqa: E501 + name="ARPing", # type: str + stats=None # type: Optional[List[Type[Packet]]] + ): SndRcvList.__init__(self, res, name, stats) - def show(self): + def show(self, *args, **kwargs): + # type: (*Any, **Any) -> None """ Print the list of discovered MAC addresses. """ - - data = list() - padding = 0 + data = list() # type: List[Tuple[str | List[str], ...]] for s, r in self.res: manuf = conf.manufdb._get_short_manuf(r.src) manuf = "unknown" if manuf == r.src else manuf - padding = max(padding, len(manuf)) data.append((r[Ether].src, manuf, r[ARP].psrc)) - for src, manuf, psrc in data: - print(" %-17s %-*s %s" % (src, padding, manuf, psrc)) + print( + pretty_list( + data, + [("src", "manuf", "psrc")], + sortBy=2, + ) + ) @conf.commands.register -def arping(net, timeout=2, cache=0, verbose=None, **kargs): - """Send ARP who-has requests to determine which hosts are up -arping(net, [cache=0,] [iface=conf.iface,] [verbose=conf.verb]) -> None -Set cache=True if you want arping to modify internal ARP-Cache""" +def arping(net: str, + timeout: int = 2, + cache: int = 0, + verbose: Optional[int] = None, + threaded: bool = True, + **kargs: Any, + ) -> Tuple[ARPingResult, PacketList]: + """ + Send ARP who-has requests to determine which hosts are up:: + + arping(net, [cache=0,] [iface=conf.iface,] [verbose=conf.verb]) -> None + + Set cache=True if you want arping to modify internal ARP-Cache + """ if verbose is None: verbose = conf.verb - ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=net), verbose=verbose, # noqa: E501 - filter="arp and arp[7] = 2", timeout=timeout, iface_hint=net, **kargs) # noqa: E501 + + hwaddr = None + if "iface" in kargs: + hwaddr = get_if_hwaddr(kargs["iface"]) + if isinstance(net, list): + hint = net[0] + else: + hint = str(net) + psrc = conf.route.route( + hint, + dev=kargs.get("iface", None), + verbose=False, + _internal=True, # Do not follow default routes. + )[1] + if psrc == "0.0.0.0" and "iface" not in kargs: + warning( + "Could not find the interface for destination %s based on the routes. " + "Using conf.iface. Please provide an 'iface' !" % hint + ) + + ans, unans = srp( + Ether(dst="ff:ff:ff:ff:ff:ff", src=hwaddr) / ARP( + pdst=net, + psrc=psrc, + hwsrc=hwaddr + ), + verbose=verbose, + filter="arp and arp[7] = 2", + timeout=timeout, + threaded=threaded, + iface_hint=hint, + **kargs, + ) ans = ARPingResult(ans.res) if cache and ans is not None: for pair in ans: - conf.netcache.arp_cache[pair[1].psrc] = (pair[1].hwsrc, time.time()) # noqa: E501 + _arp_cache[pair[1].psrc] = pair[1].hwsrc if ans is not None and verbose: ans.show() return ans, unans @@ -673,6 +1108,7 @@ def arping(net, timeout=2, cache=0, verbose=None, **kargs): @conf.commands.register def is_promisc(ip, fake_bcast="ff:ff:00:00:00:00", **kargs): + # type: (str, str, **Any) -> bool """Try to guess if target is in Promisc mode. The target is provided by its ip.""" # noqa: E501 responses = srp1(Ether(dst=fake_bcast) / ARP(op="who-has", pdst=ip), type=ETH_P_ARP, iface_hint=ip, timeout=1, verbose=0, **kargs) # noqa: E501 @@ -682,17 +1118,18 @@ def is_promisc(ip, fake_bcast="ff:ff:00:00:00:00", **kargs): @conf.commands.register def promiscping(net, timeout=2, fake_bcast="ff:ff:ff:ff:ff:fe", **kargs): + # type: (str, int, str, **Any) -> Tuple[ARPingResult, PacketList] """Send ARP who-has requests to determine which hosts are in promiscuous mode promiscping(net, iface=conf.iface)""" ans, unans = srp(Ether(dst=fake_bcast) / ARP(pdst=net), filter="arp and arp[7] = 2", timeout=timeout, iface_hint=net, **kargs) # noqa: E501 ans = ARPingResult(ans.res, name="PROMISCPing") - ans.display() + ans.show() return ans, unans -class ARP_am(AnsweringMachine): +class ARP_am(AnsweringMachine[Packet]): """Fake ARP Relay Daemon (farpd) example: @@ -723,21 +1160,36 @@ class ARP_am(AnsweringMachine): filter = "arp" send_function = staticmethod(sendp) - def parse_options(self, IP_addr=None, ARP_addr=None): - self.IP_addr = IP_addr + def parse_options(self, IP_addr=None, ARP_addr=None, from_ip=None): + # type: (Optional[str], Optional[str], Optional[str]) -> None + if isinstance(IP_addr, str): + self.IP_addr = Net(IP_addr) # type: Optional[Net] + else: + self.IP_addr = IP_addr + if isinstance(from_ip, str): + self.from_ip = Net(from_ip) # type: Optional[Net] + else: + self.from_ip = from_ip self.ARP_addr = ARP_addr def is_request(self, req): - return (req.haslayer(ARP) and - req.getlayer(ARP).op == 1 and - (self.IP_addr is None or self.IP_addr == req.getlayer(ARP).pdst)) # noqa: E501 + # type: (Packet) -> bool + if not req.haslayer(ARP): + return False + arp = req[ARP] + return ( + arp.op == 1 and + (self.IP_addr is None or arp.pdst in self.IP_addr) and + (self.from_ip is None or arp.psrc in self.from_ip) + ) def make_reply(self, req): - ether = req.getlayer(Ether) - arp = req.getlayer(ARP) + # type: (Packet) -> Packet + ether = req[Ether] + arp = req[ARP] if 'iface' in self.optsend: - iff = self.optsend.get('iface') + iff = cast(Union[NetworkInterface, str], self.optsend.get('iface')) else: iff, a, gw = conf.route.route(arp.psrc) self.iff = iff @@ -756,18 +1208,21 @@ def make_reply(self, req): pdst=arp.psrc) return resp - def send_reply(self, reply): + def send_reply(self, reply, send_function=None): + # type: (Packet, Any) -> None if 'iface' in self.optsend: self.send_function(reply, **self.optsend) else: self.send_function(reply, iface=self.iff, **self.optsend) def print_reply(self, req, reply): + # type: (Packet, Packet) -> None print("%s ==> %s on %s" % (req.summary(), reply.summary(), self.iff)) @conf.commands.register def etherleak(target, **kargs): + # type: (str, **Any) -> Tuple[SndRcvList, PacketList] """Exploit Etherleak flaw""" return srp(Ether() / ARP(pdst=target), prn=lambda s_r: conf.padding_layer in s_r[1] and hexstr(s_r[1][conf.padding_layer].load), # noqa: E501 @@ -776,13 +1231,14 @@ def etherleak(target, **kargs): @conf.commands.register def arpleak(target, plen=255, hwlen=255, **kargs): + # type: (str, int, int, **Any) -> Tuple[SndRcvList, PacketList] """Exploit ARP leak flaws, like NetBSD-SA2017-002. https://ftp.netbsd.org/pub/NetBSD/security/advisories/NetBSD-SA2017-002.txt.asc """ # We want explicit packets - pkts_iface = {} + pkts_iface = {} # type: Dict[str, List[Packet]] for pkt in ARP(pdst=target): # We have to do some of Scapy's work since we mess with # important values @@ -804,7 +1260,7 @@ def arpleak(target, plen=255, hwlen=255, **kargs): Ether(src=hwsrc, dst=ETHER_BROADCAST) / pkt ) ans, unans = SndRcvList(), PacketList(name="Unanswered") - for iface, pkts in viewitems(pkts_iface): + for iface, pkts in pkts_iface.items(): ans_new, unans_new = srp(pkts, iface=iface, filter="arp", **kargs) ans += ans_new unans += unans_new diff --git a/scapy/layers/l2tp.py b/scapy/layers/l2tp.py index dc79de393a6..45e8d1c907f 100644 --- a/scapy/layers/l2tp.py +++ b/scapy/layers/l2tp.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ L2TP (Layer 2 Tunneling Protocol) for VPNs. @@ -25,7 +25,7 @@ class L2TP(Packet): 'res06', 'sequence', 'res08', 'res09', 'length', 'control']), # noqa: E501 BitEnumField("version", 2, 4, {2: 'L2TPv2'}), - ConditionalField(ShortField("len", 0), + ConditionalField(ShortField("len", None), lambda pkt: pkt.hdr & 'control+length'), ShortField("tunnel_id", 0), ShortField("session_id", 0), @@ -40,7 +40,7 @@ class L2TP(Packet): ] def post_build(self, pkt, pay): - if self.len is None: + if self.len is None and self.hdr & 'control+length': tmp_len = len(pkt) + len(pay) pkt = pkt[:2] + struct.pack("!H", tmp_len) + pkt[4:] return pkt + pay diff --git a/scapy/layers/ldap.py b/scapy/layers/ldap.py new file mode 100644 index 00000000000..d1acbc7ad06 --- /dev/null +++ b/scapy/layers/ldap.py @@ -0,0 +1,2519 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +LDAP + +RFC 1777 - LDAP v2 +RFC 4511 - LDAP v3 + +Note: to mimic Microsoft Windows LDAP packets, you must set:: + + conf.ASN1_default_long_size = 4 + +.. note:: + You will find more complete documentation for this layer over at + `LDAP `_ +""" + +import collections +import re +import socket +import ssl +import string +import struct +import uuid + +from scapy.arch import get_if_addr +from scapy.ansmachine import AnsweringMachine +from scapy.asn1.asn1 import ( + ASN1_BOOLEAN, + ASN1_Class, + ASN1_Codecs, + ASN1_ENUMERATED, + ASN1_INTEGER, + ASN1_STRING, +) +from scapy.asn1.ber import ( + BER_Decoding_Error, + BER_id_dec, + BER_len_dec, + BERcodec_STRING, +) +from scapy.asn1fields import ( + ASN1F_badsequence, + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_ENUMERATED, + ASN1F_FLAGS, + ASN1F_INTEGER, + ASN1F_NULL, + ASN1F_optional, + ASN1F_PACKET, + ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_SET_OF, + ASN1F_STRING_PacketField, + ASN1F_STRING, +) +from scapy.asn1packet import ASN1_Packet +from scapy.config import conf +from scapy.compat import StrEnum +from scapy.consts import WINDOWS +from scapy.error import log_runtime +from scapy.fields import ( + FieldLenField, + FlagsField, + ThreeBytesField, +) +from scapy.packet import ( + Packet, + bind_bottom_up, + bind_layers, +) +from scapy.sendrecv import send +from scapy.supersocket import ( + SimpleSocket, + StreamSocket, + SSLStreamSocket, +) + +from scapy.layers.dns import dns_resolve +from scapy.layers.inet import IP, TCP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.gssapi import ( + _GSSAPI_Field, + ChannelBindingType, + GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_QOP_REQ_FLAGS, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSSAPI_BLOB_SIGNATURE, + GSSAPI_BLOB, + GssChannelBindings, + SSP, +) +from scapy.layers.netbios import NBTDatagram +from scapy.layers.smb import ( + NETLOGON, + NETLOGON_SAM_LOGON_RESPONSE_EX, +) +from scapy.layers.windows.erref import STATUS_ERREF + +# Typing imports +from typing import ( + Any, + Dict, + List, + Optional, + Union, +) + +# Elements of protocol +# https://datatracker.ietf.org/doc/html/rfc1777#section-4 + +LDAPString = ASN1F_STRING +LDAPOID = ASN1F_STRING +LDAPDN = LDAPString +RelativeLDAPDN = LDAPString +AttributeType = LDAPString +AttributeValue = ASN1F_STRING +URI = LDAPString + + +class AttributeValueAssertion(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + AttributeType("attributeType", "organizationName"), + AttributeValue("attributeValue", ""), + ) + + +class LDAPReferral(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = LDAPString("uri", "") + + +LDAPResult = ( + ASN1F_ENUMERATED( + "resultCode", + 0, + { + 0: "success", + 1: "operationsError", + 2: "protocolError", + 3: "timeLimitExceeded", + 4: "sizeLimitExceeded", + 5: "compareFalse", + 6: "compareTrue", + 7: "authMethodNotSupported", + 8: "strongAuthRequired", + 10: "referral", + 11: "adminLimitExceeded", + 14: "saslBindInProgress", + 16: "noSuchAttribute", + 17: "undefinedAttributeType", + 18: "inappropriateMatching", + 19: "constraintViolation", + 20: "attributeOrValueExists", + 21: "invalidAttributeSyntax", + 32: "noSuchObject", + 33: "aliasProblem", + 34: "invalidDNSyntax", + 35: "isLeaf", + 36: "aliasDereferencingProblem", + 48: "inappropriateAuthentication", + 49: "invalidCredentials", + 50: "insufficientAccessRights", + 51: "busy", + 52: "unavailable", + 53: "unwillingToPerform", + 54: "loopDetect", + 64: "namingViolation", + 65: "objectClassViolation", + 66: "notAllowedOnNonLeaf", + 67: "notAllowedOnRDN", + 68: "entryAlreadyExists", + 69: "objectClassModsProhibited", + 70: "resultsTooLarge", # CLDAP + 80: "other", + }, + ), + LDAPDN("matchedDN", ""), + LDAPString("diagnosticMessage", ""), + # LDAP v3 only + ASN1F_optional(ASN1F_SEQUENCE_OF("referral", [], LDAPReferral, implicit_tag=0xA3)), +) + + +# ldap APPLICATION + + +class ASN1_Class_LDAP(ASN1_Class): + name = "LDAP" + # APPLICATION + CONSTRUCTED = 0x40 | 0x20 + BindRequest = 0x60 + BindResponse = 0x61 + UnbindRequest = 0x42 # not constructed + SearchRequest = 0x63 + SearchResultEntry = 0x64 + SearchResultDone = 0x65 + ModifyRequest = 0x66 + ModifyResponse = 0x67 + AddRequest = 0x68 + AddResponse = 0x69 + DelRequest = 0x4A # not constructed + DelResponse = 0x6B + ModifyDNRequest = 0x6C + ModifyDNResponse = 0x6D + CompareRequest = 0x6E + CompareResponse = 0x7F + AbandonRequest = 0x50 # application + primitive + SearchResultReference = 0x73 + ExtendedRequest = 0x77 + ExtendedResponse = 0x78 + + +# Bind operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.2 + + +class ASN1_Class_LDAP_Authentication(ASN1_Class): + name = "LDAP Authentication" + # CONTEXT-SPECIFIC = 0x80 + simple = 0x80 + krbv42LDAP = 0x81 + krbv42DSA = 0x82 + sasl = 0xA3 # CONTEXT-SPECIFIC | CONSTRUCTED + # [MS-ADTS] sect 5.1.1.1 + sicilyPackageDiscovery = 0x89 + sicilyNegotiate = 0x8A + sicilyResponse = 0x8B + + +# simple +class LDAP_Authentication_simple(ASN1_STRING): + tag = ASN1_Class_LDAP_Authentication.simple + + +class BERcodec_LDAP_Authentication_simple(BERcodec_STRING): + tag = ASN1_Class_LDAP_Authentication.simple + + +class ASN1F_LDAP_Authentication_simple(ASN1F_STRING): + ASN1_tag = ASN1_Class_LDAP_Authentication.simple + + +# krbv42LDAP +class LDAP_Authentication_krbv42LDAP(ASN1_STRING): + tag = ASN1_Class_LDAP_Authentication.krbv42LDAP + + +class BERcodec_LDAP_Authentication_krbv42LDAP(BERcodec_STRING): + tag = ASN1_Class_LDAP_Authentication.krbv42LDAP + + +class ASN1F_LDAP_Authentication_krbv42LDAP(ASN1F_STRING): + ASN1_tag = ASN1_Class_LDAP_Authentication.krbv42LDAP + + +# krbv42DSA +class LDAP_Authentication_krbv42DSA(ASN1_STRING): + tag = ASN1_Class_LDAP_Authentication.krbv42DSA + + +class BERcodec_LDAP_Authentication_krbv42DSA(BERcodec_STRING): + tag = ASN1_Class_LDAP_Authentication.krbv42DSA + + +class ASN1F_LDAP_Authentication_krbv42DSA(ASN1F_STRING): + ASN1_tag = ASN1_Class_LDAP_Authentication.krbv42DSA + + +# sicilyPackageDiscovery +class LDAP_Authentication_sicilyPackageDiscovery(ASN1_STRING): + tag = ASN1_Class_LDAP_Authentication.sicilyPackageDiscovery + + +class BERcodec_LDAP_Authentication_sicilyPackageDiscovery(BERcodec_STRING): + tag = ASN1_Class_LDAP_Authentication.sicilyPackageDiscovery + + +class ASN1F_LDAP_Authentication_sicilyPackageDiscovery(ASN1F_STRING): + ASN1_tag = ASN1_Class_LDAP_Authentication.sicilyPackageDiscovery + + +# sicilyNegotiate +class LDAP_Authentication_sicilyNegotiate(ASN1_STRING): + tag = ASN1_Class_LDAP_Authentication.sicilyNegotiate + + +class BERcodec_LDAP_Authentication_sicilyNegotiate(BERcodec_STRING): + tag = ASN1_Class_LDAP_Authentication.sicilyNegotiate + + +class ASN1F_LDAP_Authentication_sicilyNegotiate(ASN1F_STRING): + ASN1_tag = ASN1_Class_LDAP_Authentication.sicilyNegotiate + + +# sicilyResponse +class LDAP_Authentication_sicilyResponse(ASN1_STRING): + tag = ASN1_Class_LDAP_Authentication.sicilyResponse + + +class BERcodec_LDAP_Authentication_sicilyResponse(BERcodec_STRING): + tag = ASN1_Class_LDAP_Authentication.sicilyResponse + + +class ASN1F_LDAP_Authentication_sicilyResponse(ASN1F_STRING): + ASN1_tag = ASN1_Class_LDAP_Authentication.sicilyResponse + + +_SASL_MECHANISMS = {b"GSS-SPNEGO": GSSAPI_BLOB, b"GSSAPI": GSSAPI_BLOB} + + +class _SaslCredentialsField(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_SaslCredentialsField, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.mechanism.val in _SASL_MECHANISMS: + return ( + _SASL_MECHANISMS[pkt.mechanism.val](val[0].val, _underlayer=pkt), + val[1], + ) + return val + + +class LDAP_Authentication_SaslCredentials(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAPString("mechanism", ""), + ASN1F_optional( + _SaslCredentialsField("credentials", ""), + ), + implicit_tag=ASN1_Class_LDAP_Authentication.sasl, + ) + + +class LDAP_BindRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("version", 3), + LDAPDN("bind_name", ""), + ASN1F_CHOICE( + "authentication", + None, + ASN1F_LDAP_Authentication_simple, + ASN1F_LDAP_Authentication_krbv42LDAP, + ASN1F_LDAP_Authentication_krbv42DSA, + LDAP_Authentication_SaslCredentials, + ), + implicit_tag=ASN1_Class_LDAP.BindRequest, + ) + + +class LDAP_BindResponse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + *( + LDAPResult + + ( + ASN1F_optional( + # For GSSAPI, the response is wrapped in + # LDAP_Authentication_SaslCredentials + ASN1F_STRING("serverSaslCredsWrap", "", implicit_tag=0xA7), + ), + ASN1F_optional( + ASN1F_STRING("serverSaslCreds", "", implicit_tag=0x87), + ), + ) + ), + implicit_tag=ASN1_Class_LDAP.BindResponse, + ) + + @property + def serverCreds(self): + """ + serverCreds field in SicilyBindResponse + """ + return self.matchedDN.val + + @serverCreds.setter + def serverCreds(self, val): + """ + serverCreds field in SicilyBindResponse + """ + self.matchedDN = ASN1_STRING(val) + + @property + def serverSaslCredsData(self): + """ + Get serverSaslCreds or serverSaslCredsWrap depending on what's available + """ + if self.serverSaslCredsWrap and self.serverSaslCredsWrap.val: + wrap = LDAP_Authentication_SaslCredentials(self.serverSaslCredsWrap.val) + val = wrap.credentials + if isinstance(val, ASN1_STRING): + return val.val + return bytes(val) + elif self.serverSaslCreds and self.serverSaslCreds.val: + return self.serverSaslCreds.val + else: + return None + + +# Unbind operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.3 + + +class LDAP_UnbindRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_NULL("info", 0), + implicit_tag=ASN1_Class_LDAP.UnbindRequest, + ) + + +# Search operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.5 + + +class LDAP_SubstringFilterInitial(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = LDAPString("val", "") + + +class LDAP_SubstringFilterAny(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = LDAPString("val", "") + + +class LDAP_SubstringFilterFinal(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = LDAPString("val", "") + + +class LDAP_SubstringFilterStr(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "str", + ASN1_STRING(""), + ASN1F_PACKET( + "initial", + LDAP_SubstringFilterInitial(), + LDAP_SubstringFilterInitial, + implicit_tag=0x80, + ), + ASN1F_PACKET( + "any", LDAP_SubstringFilterAny(), LDAP_SubstringFilterAny, implicit_tag=0x81 + ), + ASN1F_PACKET( + "final", + LDAP_SubstringFilterFinal(), + LDAP_SubstringFilterFinal, + implicit_tag=0x82, + ), + ) + + +class LDAP_SubstringFilter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + AttributeType("type", ""), + ASN1F_SEQUENCE_OF("filters", [], LDAP_SubstringFilterStr), + ) + + +_LDAP_Filter = lambda *args, **kwargs: LDAP_Filter(*args, **kwargs) + + +class LDAP_FilterAnd(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("vals", [], _LDAP_Filter) + + +class LDAP_FilterOr(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("vals", [], _LDAP_Filter) + + +class LDAP_FilterNot(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("val", None, None, next_cls_cb=lambda *args, **kwargs: LDAP_Filter) + ) + + +class LDAP_FilterPresent(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = AttributeType("present", "objectClass") + + +class LDAP_FilterEqual(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = AttributeValueAssertion.ASN1_root + + +class LDAP_FilterGreaterOrEqual(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = AttributeValueAssertion.ASN1_root + + +class LDAP_FilterLessOrEqual(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = AttributeValueAssertion.ASN1_root + + +class LDAP_FilterApproxMatch(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = AttributeValueAssertion.ASN1_root + + +class LDAP_FilterExtensibleMatch(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional( + LDAPString("matchingRule", "", implicit_tag=0x81), + ), + ASN1F_optional( + LDAPString("type", "", implicit_tag=0x81), + ), + AttributeValue("matchValue", "", implicit_tag=0x82), + ASN1F_BOOLEAN("dnAttributes", False, implicit_tag=0x84), + ) + + +class ASN1_Class_LDAP_Filter(ASN1_Class): + name = "LDAP Filter" + # CONTEXT-SPECIFIC + CONSTRUCTED = 0x80 | 0x20 + And = 0xA0 + Or = 0xA1 + Not = 0xA2 + EqualityMatch = 0xA3 + Substrings = 0xA4 + GreaterOrEqual = 0xA5 + LessOrEqual = 0xA6 + Present = 0x87 # not constructed + ApproxMatch = 0xA8 + ExtensibleMatch = 0xA9 + + +class LDAP_Filter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "filter", + LDAP_FilterPresent(), + ASN1F_PACKET( + "and_", None, LDAP_FilterAnd, implicit_tag=ASN1_Class_LDAP_Filter.And + ), + ASN1F_PACKET( + "or_", None, LDAP_FilterOr, implicit_tag=ASN1_Class_LDAP_Filter.Or + ), + ASN1F_PACKET( + "not_", None, LDAP_FilterNot, implicit_tag=ASN1_Class_LDAP_Filter.Not + ), + ASN1F_PACKET( + "equalityMatch", + None, + LDAP_FilterEqual, + implicit_tag=ASN1_Class_LDAP_Filter.EqualityMatch, + ), + ASN1F_PACKET( + "substrings", + None, + LDAP_SubstringFilter, + implicit_tag=ASN1_Class_LDAP_Filter.Substrings, + ), + ASN1F_PACKET( + "greaterOrEqual", + None, + LDAP_FilterGreaterOrEqual, + implicit_tag=ASN1_Class_LDAP_Filter.GreaterOrEqual, + ), + ASN1F_PACKET( + "lessOrEqual", + None, + LDAP_FilterLessOrEqual, + implicit_tag=ASN1_Class_LDAP_Filter.LessOrEqual, + ), + ASN1F_PACKET( + "present", + None, + LDAP_FilterPresent, + implicit_tag=ASN1_Class_LDAP_Filter.Present, + ), + ASN1F_PACKET( + "approxMatch", + None, + LDAP_FilterApproxMatch, + implicit_tag=ASN1_Class_LDAP_Filter.ApproxMatch, + ), + ASN1F_PACKET( + "extensibleMatch", + None, + LDAP_FilterExtensibleMatch, + implicit_tag=ASN1_Class_LDAP_Filter.ExtensibleMatch, + ), + ) + + @staticmethod + def from_rfc2254_string(filter: str): + """ + Convert a RFC-2254 filter to LDAP_Filter + """ + # Note: this code is very dumb to be readable. + _lerr = "Invalid LDAP filter string: " + if filter.lstrip()[0] != "(": + filter = "(%s)" % filter + + # 1. Cheap lexer. + tokens = [] + cur = tokens + backtrack = [] + filterlen = len(filter) + i = 0 + while i < filterlen: + c = filter[i] + i += 1 + if c in [" ", "\t", "\n"]: + # skip spaces + continue + elif c == "(": + # enclosure + cur.append([]) + backtrack.append(cur) + cur = cur[-1] + elif c == ")": + # end of enclosure + if not backtrack: + raise ValueError(_lerr + "parenthesis unmatched.") + cur = backtrack.pop(-1) + elif c in "&|!": + # and / or / not + cur.append(c) + elif c in "=": + # filtertype + if cur[-1] in "~><:": + cur[-1] += c + continue + cur.append(c) + elif c in "~><": + # comparisons + cur.append(c) + elif c == ":": + # extensible + cur.append(c) + elif c == "*": + # substring + cur.append(c) + else: + # value + v = "" + for x in filter[i - 1 :]: + if x in "():!|&~<>=*": + break + v += x + if not v: + raise ValueError(_lerr + "critical failure (impossible).") + i += len(v) - 1 + cur.append(v) + + # Check that parenthesis were closed + if backtrack: + raise ValueError(_lerr + "parenthesis unmatched.") + + # LDAP filters must have an empty enclosure () + tokens = tokens[0] + + # 2. Cheap grammar parser. + # Doing it recursively is trivial. + def _getfld(x): + if not x: + raise ValueError(_lerr + "empty enclosure.") + elif len(x) == 1 and isinstance(x[0], list): + # useless enclosure + return _getfld(x[0]) + elif x[0] in "&|": + # multinary operator + if len(x) < 3: + raise ValueError(_lerr + "bad use of multinary operator.") + return (LDAP_FilterAnd if x[0] == "&" else LDAP_FilterOr)( + vals=[LDAP_Filter(filter=_getfld(y)) for y in x[1:]] + ) + elif x[0] == "!": + # unary operator + if len(x) != 2: + raise ValueError(_lerr + "bad use of unary operator.") + return LDAP_FilterNot( + val=LDAP_Filter(filter=_getfld(x[1])), + ) + elif "=" in x and "*" in x: + # substring + if len(x) < 3 or x[1] != "=": + raise ValueError(_lerr + "bad use of substring.") + return LDAP_SubstringFilter( + type=ASN1_STRING(x[0].strip()), + filters=[ + LDAP_SubstringFilterStr( + str=( + LDAP_SubstringFilterFinal + if i == (len(x) - 3) + else ( + LDAP_SubstringFilterInitial + if i == 0 + else LDAP_SubstringFilterAny + ) + )(val=ASN1_STRING(y)) + ) + for i, y in enumerate(x[2:]) + if y != "*" + ], + ) + elif ":=" in x: + # extensible + raise NotImplementedError("Extensible not implemented.") + elif any(y in ["<=", ">=", "~=", "="] for y in x): + # simple + if len(x) != 3 or "=" not in x[1]: + raise ValueError(_lerr + "bad use of comparison.") + if x[2] == "*": + return LDAP_FilterPresent(present=ASN1_STRING(x[0])) + return ( + LDAP_FilterLessOrEqual + if "<=" in x + else ( + LDAP_FilterGreaterOrEqual + if ">=" in x + else LDAP_FilterApproxMatch if "~=" in x else LDAP_FilterEqual + ) + )( + attributeType=ASN1_STRING(x[0].strip()), + attributeValue=ASN1_STRING(x[2]), + ) + else: + raise ValueError(_lerr + "invalid filter.") + + return LDAP_Filter(filter=_getfld(tokens)) + + +class LDAP_SearchRequestAttribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = AttributeType("type", "") + + +class LDAP_SearchRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAPDN("baseObject", ""), + ASN1F_ENUMERATED( + "scope", 0, {0: "baseObject", 1: "singleLevel", 2: "wholeSubtree"} + ), + ASN1F_ENUMERATED( + "derefAliases", + 0, + { + 0: "neverDerefAliases", + 1: "derefInSearching", + 2: "derefFindingBaseObj", + 3: "derefAlways", + }, + ), + ASN1F_INTEGER("sizeLimit", 0), + ASN1F_INTEGER("timeLimit", 0), + ASN1F_BOOLEAN("attrsOnly", False), + ASN1F_PACKET("filter", LDAP_Filter(), LDAP_Filter), + ASN1F_SEQUENCE_OF("attributes", [], LDAP_SearchRequestAttribute), + implicit_tag=ASN1_Class_LDAP.SearchRequest, + ) + + +class LDAP_AttributeValue(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = AttributeValue("value", "") + + +class LDAP_PartialAttribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + AttributeType("type", ""), + ASN1F_SET_OF("values", [], LDAP_AttributeValue), + ) + + +class LDAP_SearchResponseEntry(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAPDN("objectName", ""), + ASN1F_SEQUENCE_OF( + "attributes", + LDAP_PartialAttribute(), + LDAP_PartialAttribute, + ), + implicit_tag=ASN1_Class_LDAP.SearchResultEntry, + ) + + +class LDAP_SearchResponseResultDone(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + *LDAPResult, + implicit_tag=ASN1_Class_LDAP.SearchResultDone, + ) + + +class LDAP_SearchResponseReference(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "uris", + [], + URI, + implicit_tag=ASN1_Class_LDAP.SearchResultReference, + ) + + +# Modify Operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.6 + + +class LDAP_ModifyRequestChange(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED( + "operation", + 0, + { + 0: "add", + 1: "delete", + 2: "replace", + }, + ), + ASN1F_PACKET("modification", LDAP_PartialAttribute(), LDAP_PartialAttribute), + ) + + +class LDAP_ModifyRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAPDN("object", ""), + ASN1F_SEQUENCE_OF("changes", [], LDAP_ModifyRequestChange), + implicit_tag=ASN1_Class_LDAP.ModifyRequest, + ) + + +class LDAP_ModifyResponse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + *LDAPResult, + implicit_tag=ASN1_Class_LDAP.ModifyResponse, + ) + + +# Add Operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.7 + + +class LDAP_Attribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = LDAP_PartialAttribute.ASN1_root + + +class LDAP_AddRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAPDN("entry", ""), + ASN1F_SEQUENCE_OF( + "attributes", + LDAP_Attribute(), + LDAP_Attribute, + ), + implicit_tag=ASN1_Class_LDAP.AddRequest, + ) + + +class LDAP_AddResponse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + *LDAPResult, + implicit_tag=ASN1_Class_LDAP.AddResponse, + ) + + +# Delete Operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.8 + + +class LDAP_DelRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = LDAPDN( + "entry", + "", + implicit_tag=ASN1_Class_LDAP.DelRequest, + ) + + +class LDAP_DelResponse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + *LDAPResult, + implicit_tag=ASN1_Class_LDAP.DelResponse, + ) + + +# Modify DN Operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.9 + + +class LDAP_ModifyDNRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAPDN("entry", ""), + LDAPDN("newrdn", ""), + ASN1F_BOOLEAN("deleteoldrdn", ASN1_BOOLEAN(False)), + ASN1F_optional(LDAPDN("newSuperior", None, implicit_tag=0xA0)), + implicit_tag=ASN1_Class_LDAP.ModifyDNRequest, + ) + + +class LDAP_ModifyDNResponse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + *LDAPResult, + implicit_tag=ASN1_Class_LDAP.ModifyDNResponse, + ) + + +# Abandon Operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.11 + + +class LDAP_AbandonRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("messageID", 0), + implicit_tag=ASN1_Class_LDAP.AbandonRequest, + ) + + +# LDAP v3 + +# RFC 4511 sect 4.12 - Extended Operation + + +class LDAP_ExtendedResponse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + *( + LDAPResult + + ( + ASN1F_optional(LDAPOID("responseName", None, implicit_tag=0x8A)), + ASN1F_optional(ASN1F_STRING("responseValue", None, implicit_tag=0x8B)), + ) + ), + implicit_tag=ASN1_Class_LDAP.ExtendedResponse, + ) + + def do_dissect(self, x): + # Note: Windows builds this packet with a buggy sequence size, that does not + # include the optional fields. Do another pass of dissection on the optionals. + s = super(LDAP_ExtendedResponse, self).do_dissect(x) + if not s: + return s + for obj in self.ASN1_root.seq[-2:]: # only on the 2 optional fields + try: + s = obj.dissect(self, s) + except ASN1F_badsequence: + break + return s + + +# RFC 4511 sect 4.1.11 + +_LDAP_CONTROLS = {} + + +class _ControlValue_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_ControlValue_Field, self).m2i(pkt, s) + if not val[0].val: + return val + controlType = pkt.controlType.val.decode() + if controlType in _LDAP_CONTROLS: + return ( + _LDAP_CONTROLS[controlType](val[0].val, _underlayer=pkt), + val[1], + ) + return val + + +class LDAP_Control(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAPOID("controlType", ""), + ASN1F_optional( + ASN1F_BOOLEAN("criticality", False), + ), + ASN1F_optional(_ControlValue_Field("controlValue", "")), + ) + + +# RFC 2696 - LDAP Control Extension for Simple Paged Results Manipulation + + +class LDAP_realSearchControlValue(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("size", 0), + ASN1F_STRING("cookie", ""), + ) + + +_LDAP_CONTROLS["1.2.840.113556.1.4.319"] = LDAP_realSearchControlValue + + +# [MS-ADTS] + + +class LDAP_serverSDFlagsControl(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS( + "flags", + None, + [ + "OWNER", + "GROUP", + "DACL", + "SACL", + ], + ) + ) + + +_LDAP_CONTROLS["1.2.840.113556.1.4.801"] = LDAP_serverSDFlagsControl + + +# LDAP main class + + +class LDAP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("messageID", 0), + ASN1F_CHOICE( + "protocolOp", + LDAP_SearchRequest(), + LDAP_BindRequest, + LDAP_BindResponse, + LDAP_SearchRequest, + LDAP_SearchResponseEntry, + LDAP_SearchResponseResultDone, + LDAP_AbandonRequest, + LDAP_SearchResponseReference, + LDAP_ModifyRequest, + LDAP_ModifyResponse, + LDAP_AddRequest, + LDAP_AddResponse, + LDAP_DelRequest, + LDAP_DelResponse, + LDAP_ModifyDNRequest, + LDAP_ModifyDNResponse, + LDAP_UnbindRequest, + LDAP_ExtendedResponse, + ), + # LDAP v3 only + ASN1F_optional( + ASN1F_SEQUENCE_OF("Controls", None, LDAP_Control, implicit_tag=0xA0) + ), + ) + + show_indent = 0 + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 4: + # Heuristic to detect SASL_Buffer + if _pkt[0] != 0x30: + if struct.unpack("!I", _pkt[:4])[0] + 4 == len(_pkt): + return LDAP_SASL_Buffer + return conf.raw_layer + return cls + + @classmethod + def tcp_reassemble(cls, data, *args, **kwargs): + if len(data) < 4: + return None + # For LDAP, we would prefer to have the entire LDAP response + # (multiple LDAP concatenated) in one go, to stay consistent with + # what you get when using SASL. + remaining = data + while remaining: + try: + length, x = BER_len_dec(BER_id_dec(remaining)[1]) + except (BER_Decoding_Error, IndexError): + return None + if length and len(x) >= length: + remaining = x[length:] + if not remaining: + pkt = cls(data) + # Packet can be a whole response yet still miss some content. + if ( + LDAP_SearchResponseEntry in pkt + and LDAP_SearchResponseResultDone not in pkt + ): + return None + return pkt + else: + return None + return None + + def hashret(self): + return b"ldap" + + @property + def unsolicited(self): + # RFC4511 sect 4.4. - Unsolicited Notification + return self.messageID == 0 and isinstance( + self.protocolOp, LDAP_ExtendedResponse + ) + + def answers(self, other): + if self.unsolicited: + return True + return isinstance(other, LDAP) and other.messageID == self.messageID + + def mysummary(self): + if not self.protocolOp or not self.messageID: + return "" + return ( + "%s(%s)" + % ( + self.protocolOp.__class__.__name__.replace("_", " "), + self.messageID.val, + ), + [LDAP], + ) + + +bind_layers(LDAP, LDAP) + +bind_bottom_up(TCP, LDAP, dport=389) +bind_bottom_up(TCP, LDAP, sport=389) +bind_bottom_up(TCP, LDAP, dport=3268) +bind_bottom_up(TCP, LDAP, sport=3268) +bind_layers(TCP, LDAP, sport=389, dport=389) + +# CLDAP - rfc1798 + + +class CLDAP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + LDAP.ASN1_root.seq[0], # messageID + ASN1F_optional( + LDAPDN("user", ""), + ), + LDAP.ASN1_root.seq[1], # protocolOp + ) + + def answers(self, other): + return isinstance(other, CLDAP) and other.messageID == self.messageID + + +bind_layers(CLDAP, CLDAP) + +bind_bottom_up(UDP, CLDAP, dport=389) +bind_bottom_up(UDP, CLDAP, sport=389) +bind_layers(UDP, CLDAP, sport=389, dport=389) + +# [MS-ADTS] sect 3.1.1.2.3.3 + +LDAP_PROPERTY_SET = { + uuid.UUID( + "C7407360-20BF-11D0-A768-00AA006E0529" + ): "Domain Password & Lockout Policies", + uuid.UUID("59BA2F42-79A2-11D0-9020-00C04FC2D3CF"): "General Information", + uuid.UUID("4C164200-20C0-11D0-A768-00AA006E0529"): "Account Restrictions", + uuid.UUID("5F202010-79A5-11D0-9020-00C04FC2D4CF"): "Logon Information", + uuid.UUID("BC0AC240-79A9-11D0-9020-00C04FC2D4CF"): "Group Membership", + uuid.UUID("E45795B2-9455-11D1-AEBD-0000F80367C1"): "Phone and Mail Options", + uuid.UUID("77B5B886-944A-11D1-AEBD-0000F80367C1"): "Personal Information", + uuid.UUID("E45795B3-9455-11D1-AEBD-0000F80367C1"): "Web Information", + uuid.UUID("E48D0154-BCF8-11D1-8702-00C04FB96050"): "Public Information", + uuid.UUID("037088F8-0AE1-11D2-B422-00A0C968F939"): "Remote Access Information", + uuid.UUID("B8119FD0-04F6-4762-AB7A-4986C76B3F9A"): "Other Domain Parameters", + uuid.UUID("72E39547-7B18-11D1-ADEF-00C04FD8D5CD"): "DNS Host Name Attributes", + uuid.UUID("FFA6F046-CA4B-4FEB-B40D-04DFEE722543"): "MS-TS-GatewayAccess", + uuid.UUID("91E647DE-D96F-4B70-9557-D63FF4F3CCD8"): "Private Information", + uuid.UUID("5805BC62-BDC9-4428-A5E2-856A0F4C185E"): "Terminal Server License Server", +} + +# [MS-ADTS] sect 5.1.3.2.1 + +LDAP_CONTROL_ACCESS_RIGHTS = { + uuid.UUID("ee914b82-0a98-11d1-adbb-00c04fd8d5cd"): "Abandon-Replication", + uuid.UUID("440820ad-65b4-11d1-a3da-0000f875ae0d"): "Add-GUID", + uuid.UUID("1abd7cf8-0a99-11d1-adbb-00c04fd8d5cd"): "Allocate-Rids", + uuid.UUID("68b1d179-0d15-4d4f-ab71-46152e79a7bc"): "Allowed-To-Authenticate", + uuid.UUID("edacfd8f-ffb3-11d1-b41d-00a0c968f939"): "Apply-Group-Policy", + uuid.UUID("0e10c968-78fb-11d2-90d4-00c04f79dc55"): "Certificate-Enrollment", + uuid.UUID("a05b8cc2-17bc-4802-a710-e7c15ab866a2"): "Certificate-AutoEnrollment", + uuid.UUID("014bf69c-7b3b-11d1-85f6-08002be74fab"): "Change-Domain-Master", + uuid.UUID("cc17b1fb-33d9-11d2-97d4-00c04fd8d5cd"): "Change-Infrastructure-Master", + uuid.UUID("bae50096-4752-11d1-9052-00c04fc2d4cf"): "Change-PDC", + uuid.UUID("d58d5f36-0a98-11d1-adbb-00c04fd8d5cd"): "Change-Rid-Master", + uuid.UUID("e12b56b6-0a95-11d1-adbb-00c04fd8d5cd"): "Change-Schema-Master", + uuid.UUID("e2a36dc9-ae17-47c3-b58b-be34c55ba633"): "Create-Inbound-Forest-Trust", + uuid.UUID("fec364e0-0a98-11d1-adbb-00c04fd8d5cd"): "Do-Garbage-Collection", + uuid.UUID("ab721a52-1e2f-11d0-9819-00aa0040529b"): "Domain-Administer-Server", + uuid.UUID("69ae6200-7f46-11d2-b9ad-00c04f79f805"): "DS-Check-Stale-Phantoms", + uuid.UUID("2f16c4a5-b98e-432c-952a-cb388ba33f2e"): "DS-Execute-Intentions-Script", + uuid.UUID("9923a32a-3607-11d2-b9be-0000f87a36b2"): "DS-Install-Replica", + uuid.UUID("4ecc03fe-ffc0-4947-b630-eb672a8a9dbc"): "DS-Query-Self-Quota", + uuid.UUID("1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Get-Changes", + uuid.UUID("1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Get-Changes-All", + uuid.UUID( + "89e95b76-444d-4c62-991a-0facbeda640c" + ): "DS-Replication-Get-Changes-In-Filtered-Set", + uuid.UUID("1131f6ac-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Manage-Topology", + uuid.UUID( + "f98340fb-7c5b-4cdb-a00b-2ebdfa115a96" + ): "DS-Replication-Monitor-Topology", + uuid.UUID("1131f6ab-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Synchronize", + uuid.UUID( + "05c74c5e-4deb-43b4-bd9f-86664c2a7fd5" + ): "Enable-Per-User-Reversibly-Encrypted-Password", + uuid.UUID("b7b1b3de-ab09-4242-9e30-9980e5d322f7"): "Generate-RSoP-Logging", + uuid.UUID("b7b1b3dd-ab09-4242-9e30-9980e5d322f7"): "Generate-RSoP-Planning", + uuid.UUID("7c0e2a7c-a419-48e4-a995-10180aad54dd"): "Manage-Optional-Features", + uuid.UUID("ba33815a-4f93-4c76-87f3-57574bff8109"): "Migrate-SID-History", + uuid.UUID("b4e60130-df3f-11d1-9c86-006008764d0e"): "msmq-Open-Connector", + uuid.UUID("06bd3201-df3e-11d1-9c86-006008764d0e"): "msmq-Peek", + uuid.UUID("4b6e08c3-df3c-11d1-9c86-006008764d0e"): "msmq-Peek-computer-Journal", + uuid.UUID("4b6e08c1-df3c-11d1-9c86-006008764d0e"): "msmq-Peek-Dead-Letter", + uuid.UUID("06bd3200-df3e-11d1-9c86-006008764d0e"): "msmq-Receive", + uuid.UUID("4b6e08c2-df3c-11d1-9c86-006008764d0e"): "msmq-Receive-computer-Journal", + uuid.UUID("4b6e08c0-df3c-11d1-9c86-006008764d0e"): "msmq-Receive-Dead-Letter", + uuid.UUID("06bd3203-df3e-11d1-9c86-006008764d0e"): "msmq-Receive-journal", + uuid.UUID("06bd3202-df3e-11d1-9c86-006008764d0e"): "msmq-Send", + uuid.UUID("a1990816-4298-11d1-ade2-00c04fd8d5cd"): "Open-Address-Book", + uuid.UUID( + "1131f6ae-9c07-11d1-f79f-00c04fc2dcd2" + ): "Read-Only-Replication-Secret-Synchronization", + uuid.UUID("45ec5156-db7e-47bb-b53f-dbeb2d03c40f"): "Reanimate-Tombstones", + uuid.UUID("0bc1554e-0a99-11d1-adbb-00c04fd8d5cd"): "Recalculate-Hierarchy", + uuid.UUID( + "62dd28a8-7f46-11d2-b9ad-00c04f79f805" + ): "Recalculate-Security-Inheritance", + uuid.UUID("ab721a56-1e2f-11d0-9819-00aa0040529b"): "Receive-As", + uuid.UUID("9432c620-033c-4db7-8b58-14ef6d0bf477"): "Refresh-Group-Cache", + uuid.UUID("1a60ea8d-58a6-4b20-bcdc-fb71eb8a9ff8"): "Reload-SSL-Certificate", + uuid.UUID("7726b9d5-a4b4-4288-a6b2-dce952e80a7f"): "Run-Protect_Admin_Groups-Task", + uuid.UUID("91d67418-0135-4acc-8d79-c08e857cfbec"): "SAM-Enumerate-Entire-Domain", + uuid.UUID("ab721a54-1e2f-11d0-9819-00aa0040529b"): "Send-As", + uuid.UUID("ab721a55-1e2f-11d0-9819-00aa0040529b"): "Send-To", + uuid.UUID("ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501"): "Unexpire-Password", + uuid.UUID( + "280f369c-67c7-438e-ae98-1d46f3c6f541" + ): "Update-Password-Not-Required-Bit", + uuid.UUID("be2bb760-7f46-11d2-b9ad-00c04f79f805"): "Update-Schema-Cache", + uuid.UUID("ab721a53-1e2f-11d0-9819-00aa0040529b"): "User-Change-Password", + uuid.UUID("00299570-246d-11d0-a768-00aa006e0529"): "User-Force-Change-Password", + uuid.UUID("3e0f7e18-2c7a-4c10-ba82-4d926db99a3e"): "DS-Clone-Domain-Controller", + uuid.UUID("084c93a2-620d-4879-a836-f0ae47de0e89"): "DS-Read-Partition-Secrets", + uuid.UUID("94825a8d-b171-4116-8146-1e34d8f54401"): "DS-Write-Partition-Secrets", + uuid.UUID("4125c71f-7fac-4ff0-bcb7-f09a41325286"): "DS-Set-Owner", + uuid.UUID("88a9933e-e5c8-4f2a-9dd7-2527416b8092"): "DS-Bypass-Quota", + uuid.UUID("9b026da6-0d3c-465c-8bee-5199d7165cba"): "DS-Validated-Write-Computer", +} + +# [MS-ADTS] sect 5.1.3.2 and +# https://learn.microsoft.com/en-us/windows/win32/secauthz/directory-services-access-rights + +LDAP_DS_ACCESS_RIGHTS = { + 0x00000001: "CREATE_CHILD", + 0x00000002: "DELETE_CHILD", + 0x00000004: "LIST_CONTENTS", + 0x00000008: "WRITE_PROPERTY_EXTENDED", + 0x00000010: "READ_PROP", + 0x00000020: "WRITE_PROP", + 0x00000040: "DELETE_TREE", + 0x00000080: "LIST_OBJECT", + 0x00000100: "CONTROL_ACCESS", + 0x00010000: "DELETE", + 0x00020000: "READ_CONTROL", + 0x00040000: "WRITE_DAC", + 0x00080000: "WRITE_OWNER", + 0x00100000: "SYNCHRONIZE", + 0x01000000: "ACCESS_SYSTEM_SECURITY", + 0x80000000: "GENERIC_READ", + 0x40000000: "GENERIC_WRITE", + 0x20000000: "GENERIC_EXECUTE", + 0x10000000: "GENERIC_ALL", +} + + +# Small CLDAP Answering machine: [MS-ADTS] 6.3.3 - Ldap ping + + +class LdapPing_am(AnsweringMachine): + function_name = "ldappingd" + filter = "udp port 389 or 138" + send_function = staticmethod(send) + + def parse_options( + self, + NetbiosDomainName="DOMAIN", + DomainGuid=uuid.UUID("192bc4b3-0085-4521-83fe-062913ef59f2"), + DcSiteName="Default-First-Site-Name", + NetbiosComputerName="SRV1", + DnsForestName=None, + DnsHostName=None, + src_ip=None, + src_ip6=None, + ): + self.NetbiosDomainName = NetbiosDomainName + self.DnsForestName = DnsForestName or (NetbiosDomainName + ".LOCAL") + self.DomainGuid = DomainGuid + self.DcSiteName = DcSiteName + self.NetbiosComputerName = NetbiosComputerName + self.DnsHostName = DnsHostName or ( + NetbiosComputerName + "." + self.DnsForestName + ) + self.src_ip = src_ip + self.src_ip6 = src_ip6 + + def is_request(self, req): + # [MS-ADTS] 6.3.3 - Example: + # (&(DnsDomain=abcde.corp.microsoft.com)(Host=abcdefgh-dev)(User=abcdefgh- + # dev$)(AAC=\80\00\00\00)(DomainGuid=\3b\b0\21\ca\d3\6d\d1\11\8a\7d\b8\df\b1\56\87\1f)(NtVer + # =\06\00\00\00)) + if NBTDatagram in req: + # special case: mailslot ping + from scapy.layers.smb import SMBMailslot_Write, NETLOGON_SAM_LOGON_REQUEST + + try: + return ( + SMBMailslot_Write in req and NETLOGON_SAM_LOGON_REQUEST in req.Data + ) + except AttributeError: + return False + if CLDAP not in req or not isinstance(req.protocolOp, LDAP_SearchRequest): + return False + req = req.protocolOp + return ( + req.attributes + and req.attributes[0].type.val.lower() == b"netlogon" + and req.filter + and isinstance(req.filter.filter, LDAP_FilterAnd) + and any( + x.filter.attributeType.val == b"NtVer" for x in req.filter.filter.vals + ) + ) + + def make_reply(self, req): + if NBTDatagram in req: + # Special case + return self.make_mailslot_ping_reply(req) + if IPv6 in req: + resp = IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst) + else: + resp = IP(dst=req[IP].src, src=self.src_ip or req[IP].dst) + resp /= UDP(sport=req.dport, dport=req.sport) + # get the DnsDomainName from the request + try: + DnsDomainName = next( + x.filter.attributeValue.val + for x in req.protocolOp.filter.filter.vals + if x.filter.attributeType.val == b"DnsDomain" + ) + except StopIteration: + return + return ( + resp + / CLDAP( + protocolOp=LDAP_SearchResponseEntry( + attributes=[ + LDAP_PartialAttribute( + values=[ + LDAP_AttributeValue( + value=ASN1_STRING( + val=bytes( + NETLOGON_SAM_LOGON_RESPONSE_EX( + # Mandatory fields + DnsDomainName=DnsDomainName, + NtVersion="V1+V5", + LmNtToken=65535, + Lm20Token=65535, + # Below can be customized + Flags=0x3F3FD, + DomainGuid=self.DomainGuid, + DnsForestName=self.DnsForestName, + DnsHostName=self.DnsHostName, + NetbiosDomainName=self.NetbiosDomainName, # noqa: E501 + NetbiosComputerName=self.NetbiosComputerName, # noqa: E501 + UserName=b".", + DcSiteName=self.DcSiteName, + ClientSiteName=self.DcSiteName, + ) + ) + ) + ) + ], + type=ASN1_STRING(b"Netlogon"), + ) + ], + ), + messageID=req.messageID, + user=None, + ) + / CLDAP( + protocolOp=LDAP_SearchResponseResultDone( + referral=None, + resultCode=0, + ), + messageID=req.messageID, + user=None, + ) + ) + + def make_mailslot_ping_reply(self, req): + # type: (Packet) -> Packet + from scapy.layers.smb import ( + SMBMailslot_Write, + SMB_Header, + DcSockAddr, + NETLOGON_SAM_LOGON_RESPONSE_EX, + ) + + resp = IP(dst=req[IP].src) / UDP( + sport=req.dport, + dport=req.sport, + ) + address = self.src_ip or get_if_addr(self.optsniff.get("iface", conf.iface)) + resp /= ( + NBTDatagram( + SourceName=req.DestinationName, + SUFFIX1=req.SUFFIX2, + DestinationName=req.SourceName, + SUFFIX2=req.SUFFIX1, + SourceIP=address, + ) + / SMB_Header() + / SMBMailslot_Write( + Name=req.Data.MailslotName, + ) + ) + NetbiosDomainName = req.DestinationName.strip() + resp.Data = NETLOGON_SAM_LOGON_RESPONSE_EX( + # Mandatory fields + NetbiosDomainName=NetbiosDomainName, + DcSockAddr=DcSockAddr( + sin_addr=address, + ), + NtVersion="V1+V5EX+V5EX_WITH_IP", + LmNtToken=65535, + Lm20Token=65535, + # Below can be customized + Flags=0x3F3FD, + DomainGuid=self.DomainGuid, + DnsForestName=self.DnsForestName, + DnsDomainName=self.DnsForestName, + DnsHostName=self.DnsHostName, + NetbiosComputerName=self.NetbiosComputerName, + DcSiteName=self.DcSiteName, + ClientSiteName=self.DcSiteName, + ) + return resp + + +_located_dc = collections.namedtuple("LocatedDC", ["ip", "samlogon"]) +_dclocatorcache = conf.netcache.new_cache("dclocator", 600) + + +@conf.commands.register +def dclocator( + realm, qtype="A", mode="ldap", port=None, timeout=1, NtVersion=None, debug=0 +): + """ + Perform a DC Locator as per [MS-ADTS] sect 6.3.6 or RFC4120. + + :param realm: the kerberos realm to locate + :param mode: Detect if a server is up and joinable thanks to one of: + + - 'nocheck': Do not check that servers are online. + - 'ldap': Use the LDAP ping (CLDAP) per [MS-ADTS]. Default. + This will however not work with MIT Kerberos servers. + - 'connect': connect to specified port to test the connection. + + :param mode: in connect mode, the port to connect to. (e.g. 88) + :param debug: print debug logs + + This is cached in conf.netcache.dclocator. + """ + if NtVersion is None: + # Windows' default + NtVersion = ( + 0x00000002 # V5 + | 0x00000004 # V5EX + | 0x00000010 # V5EX_WITH_CLOSEST_SITE + | 0x01000000 # AVOID_NT4EMUL + | 0x20000000 # IP + ) + # Check cache + cache_ident = ";".join([realm, qtype, mode, str(NtVersion)]).lower() + if cache_ident in _dclocatorcache: + return _dclocatorcache[cache_ident] + # Perform DNS-Based discovery (6.3.6.1) + # 1. SRV records + qname = "_kerberos._tcp.dc._msdcs.%s" % realm.lower() + if debug: + log_runtime.info("DC Locator: requesting SRV for '%s' ..." % qname) + try: + hosts = [ + x.target + for x in dns_resolve( + qname=qname, + qtype="SRV", + timeout=timeout, + ) + ] + except TimeoutError: + raise TimeoutError("Resolution of %s timed out" % qname) + if not hosts: + raise ValueError("No DNS record found for %s" % qname) + elif debug: + log_runtime.info( + "DC Locator: got %s. Resolving %s records ..." % (hosts, qtype) + ) + # 2. A records + ips = [] + for host in hosts: + arec = dns_resolve( + qname=host, + qtype=qtype, + timeout=timeout, + ) + if arec: + ips.extend(x.rdata for x in arec) + if not ips: + raise ValueError("Could not get any %s records for %s" % (qtype, hosts)) + elif debug: + log_runtime.info("DC Locator: got %s . Mode: %s" % (ips, mode)) + # Pick first online host. We have three options + if mode == "nocheck": + # Don't check anything. Not recommended + return _located_dc(ips[0], None) + elif mode == "connect": + assert port is not None, "Must provide a port in connect mode !" + # Compatibility with MIT Kerberos servers + for ip in ips: # TODO: "addresses in weighted random order [RFC2782]" + if debug: + log_runtime.info("DC Locator: connecting to %s on %s ..." % (ip, port)) + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect((ip, port)) + # Success + result = _located_dc(ip, None) + # Cache + _dclocatorcache[cache_ident] = result + return result + except OSError: + # Host timed out, No route to host, etc. + if debug: + log_runtime.info("DC Locator: %s timed out." % ip) + continue + finally: + sock.close() + raise ValueError("No host was reachable on port %s among %s" % (port, ips)) + elif mode == "ldap": + # Real 'LDAP Ping' per [MS-ADTS] + for ip in ips: # TODO: "addresses in weighted random order [RFC2782]" + if debug: + log_runtime.info("DC Locator: LDAP Ping %s on ..." % ip) + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + sock.connect((ip, 389)) + sock = SimpleSocket(sock, CLDAP) + pkt = sock.sr1( + CLDAP( + protocolOp=LDAP_SearchRequest( + filter=LDAP_Filter( + filter=LDAP_FilterAnd( + vals=[ + LDAP_Filter( + filter=LDAP_FilterEqual( + attributeType=ASN1_STRING(b"DnsDomain"), + attributeValue=ASN1_STRING(realm), + ) + ), + LDAP_Filter( + filter=LDAP_FilterEqual( + attributeType=ASN1_STRING(b"NtVer"), + attributeValue=ASN1_STRING( + struct.pack("= length: + return cls(data) + + +class LDAP_Exception(RuntimeError): + __slots__ = ["resultCode", "diagnosticMessage"] + + def __init__(self, *args, **kwargs): + resp = kwargs.pop("resp", None) + if resp: + self.resultCode = resp.protocolOp.sprintf("%resultCode%") + self.diagnosticMessage = resp.protocolOp.diagnosticMessage.val.rstrip( + b"\x00" + ).decode(errors="backslashreplace") + else: + self.resultCode = kwargs.pop("resultCode", None) + self.diagnosticMessage = kwargs.pop("diagnosticMessage", None) + super(LDAP_Exception, self).__init__(*args, **kwargs) + # If there's a 'data' string argument, attempt to parse the error code. + try: + m = re.match(r"(\d+): LdapErr.*", self.diagnosticMessage) + if m: + errstr = m.group(1) + err = int(errstr, 16) + if err in STATUS_ERREF: + self.diagnosticMessage = self.diagnosticMessage.replace( + errstr, errstr + " (%s)" % STATUS_ERREF[err], 1 + ) + except ValueError: + pass + # Add note if this exception is raised + self.add_note(self.diagnosticMessage) + + +class LDAP_Client(object): + """ + A basic LDAP client + + The complete documentation is available at + https://scapy.readthedocs.io/en/latest/layers/ldap.html + + Example 1 - SICILY - NTLM (with encryption):: + + client = LDAP_Client() + client.connect("192.168.0.100") + ssp = NTLMSSP(UPN="Administrator", PASSWORD="Password1!") + client.bind( + LDAP_BIND_MECHS.SICILY, + ssp=ssp, + encrypt=True, + ) + + Example 2 - SASL_GSSAPI - Kerberos (with signing):: + + client = LDAP_Client() + client.connect("192.168.0.100") + ssp = KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!", + SPN="ldap/dc1.domain.local") + client.bind( + LDAP_BIND_MECHS.SASL_GSSAPI, + ssp=ssp, + sign=True, + ) + + Example 3 - SASL_GSS_SPNEGO - NTLM / Kerberos:: + + client = LDAP_Client() + client.connect("192.168.0.100") + ssp = SPNEGOSSP([ + NTLMSSP(UPN="Administrator", PASSWORD="Password1!"), + KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!", + SPN="ldap/dc1.domain.local"), + ]) + client.bind( + LDAP_BIND_MECHS.SASL_GSS_SPNEGO, + ssp=ssp, + ) + + Example 4 - Simple bind over TLS:: + + client = LDAP_Client() + client.connect("192.168.0.100", use_ssl=True) + client.bind( + LDAP_BIND_MECHS.SIMPLE, + simple_username="Administrator", + simple_password="Password1!", + ) + """ + + def __init__( + self, + verb=True, + ): + self.sock = None + self.host = None + self.verb = verb + self.ssl = False + self.sslcontext = None + self.ssp = None + self.sspcontext = None + self.encrypt = False + self.sign = False + # Session status + self.sasl_wrap = False + self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS + self.bound = False + self.messageID = 0 + + def connect( + self, + host, + port=None, + use_ssl=False, + sslcontext=None, + sni=None, + no_check_certificate=False, + timeout=5, + ): + """ + Initiate a connection + + :param host: the IP or hostname to connect to. + :param port: the port to connect to. (Default: 389 or 636) + + :param use_ssl: whether to use LDAPS or not. (Default: False) + :param sslcontext: an optional SSLContext to use. + :param sni: (optional) specify the SNI to use if LDAPS, otherwise use ip. + :param no_check_certificate: with SSL, do not check the certificate + """ + self.ssl = use_ssl + self.sslcontext = sslcontext + self.timeout = timeout + self.host = host + + if port is None: + if self.ssl: + port = 636 + else: + port = 389 + + # Create and configure socket + sock = socket.socket() + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + sock.settimeout(timeout) + + # Connect + if self.verb: + print( + "\u2503 Connecting to %s on port %s%s..." + % ( + host, + port, + " with SSL" if self.ssl else "", + ) + ) + sock.connect((host, port)) + if self.verb: + print( + conf.color_theme.green( + "\u2514 Connected from %s" % repr(sock.getsockname()) + ) + ) + + # For SSL, build and apply SSLContext + if self.ssl: + if self.sslcontext is None: + if no_check_certificate: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + else: + context = ssl.create_default_context() + else: + context = self.sslcontext + sock = context.wrap_socket(sock, server_hostname=sni or host) + + # Wrap the socket in a Scapy socket + if self.ssl: + # Compute the channel binding token (CBT) + self.chan_bindings = GssChannelBindings.fromssl( + ChannelBindingType.TLS_SERVER_END_POINT, + sslsock=sock, + ) + + self.sock = SSLStreamSocket(sock, LDAP) + else: + self.sock = StreamSocket(sock, LDAP) + + def sr1(self, protocolOp, controls: List[LDAP_Control] = None, **kwargs): + self.messageID += 1 + if self.verb: + print(conf.color_theme.opening(">> %s" % protocolOp.__class__.__name__)) + + # Build packet + pkt = LDAP( + messageID=self.messageID, + protocolOp=protocolOp, + Controls=controls, + ) + + # If signing / encryption is used, apply + if self.sasl_wrap: + pkt = LDAP_SASL_Buffer( + Buffer=self.ssp.GSS_Wrap( + self.sspcontext, + bytes(pkt), + conf_req_flag=self.encrypt, + # LDAP on Windows doesn't use SECBUFFER_PADDING, which + # isn't supported by GSS_WrapEx. We add our own flag to + # tell it. + qop_req=GSS_QOP_REQ_FLAGS.GSS_S_NO_SECBUFFER_PADDING, + ) + ) + + # Send / Receive + resp = self.sock.sr1( + pkt, + verbose=0, + **kwargs, + ) + # Check for unsolicited notification + if resp and LDAP in resp and resp[LDAP].unsolicited: + if self.verb: + resp.show() + print(conf.color_theme.fail("! Got unsolicited notification.")) + return resp + + # If signing / encryption is used, unpack + if self.sasl_wrap: + if resp.Buffer: + resp = LDAP( + self.ssp.GSS_Unwrap( + self.sspcontext, + resp.Buffer, + ) + ) + else: + resp = None + + # Verbose display + if self.verb: + if not resp: + print(conf.color_theme.fail("! Bad response.")) + return + else: + print( + conf.color_theme.success( + "<< %s" + % ( + resp.protocolOp.__class__.__name__ + if LDAP in resp + else resp.__class__.__name__ + ) + ) + ) + return resp + + def bind( + self, + mech, + ssp=None, + sign: Optional[bool] = None, + encrypt: Optional[bool] = None, + simple_username=None, + simple_password=None, + ): + """ + Send Bind request. + + :param mech: one of LDAP_BIND_MECHS + :param ssp: the SSP object to use for binding + + :param sign: request signing when binding + :param encrypt: request encryption when binding + + : + This acts differently based on the :mech: provided during initialization. + """ + # Bind default values: if NTLM then encrypt, else sign unless anonymous/simple + if encrypt is None: + encrypt = mech == LDAP_BIND_MECHS.SICILY + if sign is None and not encrypt: + sign = mech not in [LDAP_BIND_MECHS.NONE, LDAP_BIND_MECHS.SIMPLE] + + # Store and check consistency + self.mech = mech + self.ssp = ssp # type: SSP + self.sign = sign + self.encrypt = encrypt + self.sspcontext = None + + if mech is None or not isinstance(mech, LDAP_BIND_MECHS): + raise ValueError( + "'mech' attribute is required and must be one of LDAP_BIND_MECHS." + ) + + if mech == LDAP_BIND_MECHS.SASL_GSSAPI: + from scapy.layers.kerberos import KerberosSSP + + if not isinstance(self.ssp, KerberosSSP): + raise ValueError("Only raw KerberosSSP is supported with SASL_GSSAPI !") + elif mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO: + from scapy.layers.spnego import SPNEGOSSP + + if not isinstance(self.ssp, SPNEGOSSP): + if WINDOWS: + from scapy.arch.windows.sspi import WinSSP + + if not isinstance(self.ssp, WinSSP): + raise ValueError( + "Only SPNEGOSSP is supported with SASL_GSS_SPNEGO !" + ) + else: + raise ValueError( + "Only SPNEGOSSP is supported with SASL_GSS_SPNEGO !" + ) + elif mech == LDAP_BIND_MECHS.SICILY: + from scapy.layers.ntlm import NTLMSSP + + if not isinstance(self.ssp, NTLMSSP): + raise ValueError("Only raw NTLMSSP is supported with SICILY !") + if self.sign and not self.encrypt: + raise ValueError( + "NTLM on LDAP does not support signing without encryption !" + ) + elif mech in [LDAP_BIND_MECHS.NONE, LDAP_BIND_MECHS.SIMPLE]: + if self.sign or self.encrypt: + raise ValueError("Cannot use 'sign' or 'encrypt' with NONE or SIMPLE !") + else: + raise ValueError("Mech %s is still unimplemented !" % mech) + + if self.ssp is not None and mech in [ + LDAP_BIND_MECHS.NONE, + LDAP_BIND_MECHS.SIMPLE, + ]: + raise ValueError("%s cannot be used with a ssp !" % mech.value) + + # Now perform the bind, depending on the mech + if self.mech == LDAP_BIND_MECHS.SIMPLE: + # Simple binding + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(simple_username or ""), + authentication=LDAP_Authentication_simple( + simple_password or "", + ), + ) + ) + if ( + LDAP not in resp + or not isinstance(resp.protocolOp, LDAP_BindResponse) + or resp.protocolOp.resultCode != 0 + ): + raise LDAP_Exception( + "LDAP simple bind failed !", + resp=resp, + ) + status = GSS_S_COMPLETE + elif self.mech == LDAP_BIND_MECHS.SICILY: + # [MS-ADTS] sect 5.1.1.1.3 + # 1. Package Discovery + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(b""), + authentication=LDAP_Authentication_sicilyPackageDiscovery(b""), + ) + ) + if resp.protocolOp.resultCode != 0: + raise LDAP_Exception( + "Sicily package discovery failed !", + resp=resp, + ) + # 2. First exchange: Negotiate + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + target_name="ldap/" + self.host, + req_flags=( + GSS_C_FLAGS.GSS_C_REPLAY_FLAG + | GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG + | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG + | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.sign else 0) + | (GSS_C_FLAGS.GSS_C_CONF_FLAG if self.encrypt else 0) + ), + ) + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(b"NTLM"), + authentication=LDAP_Authentication_sicilyNegotiate( + bytes(token), + ), + ) + ) + val = resp.protocolOp.serverCreds + if not val: + raise LDAP_Exception( + "Sicily negotiate failed !", + resp=resp, + ) + # 3. Second exchange: Response + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + input_token=GSSAPI_BLOB(val), + target_name="ldap/" + self.host, + chan_bindings=self.chan_bindings, + ) + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(b"NTLM"), + authentication=LDAP_Authentication_sicilyResponse( + bytes(token), + ), + ) + ) + if resp.protocolOp.resultCode != 0: + raise LDAP_Exception( + "Sicily response failed !", + resp=resp, + ) + elif self.mech in [ + LDAP_BIND_MECHS.SASL_GSS_SPNEGO, + LDAP_BIND_MECHS.SASL_GSSAPI, + ]: + # GSSAPI or SPNEGO + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + target_name="ldap/" + self.host, + req_flags=( + # Required flags for GSSAPI: RFC4752 sect 3.1 + GSS_C_FLAGS.GSS_C_REPLAY_FLAG + | GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG + | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG + | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.sign else 0) + | (GSS_C_FLAGS.GSS_C_CONF_FLAG if self.encrypt else 0) + ), + chan_bindings=self.chan_bindings, + ) + if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + raise RuntimeError( + "%s: GSS_Init_sec_context failed with %s !" + % (self.mech.name, repr(status)), + ) + while token: + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(b""), + authentication=LDAP_Authentication_SaslCredentials( + mechanism=ASN1_STRING(self.mech.value), + credentials=ASN1_STRING(bytes(token)), + ), + ) + ) + if not isinstance(resp.protocolOp, LDAP_BindResponse): + raise LDAP_Exception( + "%s bind failed !" % self.mech.name, + resp=resp, + ) + val = resp.protocolOp.serverSaslCredsData + if resp.protocolOp.resultCode not in [0, 14]: + raise LDAP_Exception( + "SASL authentication failed !", + resp=resp, + ) + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + input_token=GSSAPI_BLOB(val), + target_name="ldap/" + self.host, + chan_bindings=self.chan_bindings, + ) + else: + status = GSS_S_COMPLETE + if status != GSS_S_COMPLETE: + raise RuntimeError( + "%s: GSS_Init_sec_context failed with %s !" + % (self.mech.name, repr(status)), + ) + elif self.mech == LDAP_BIND_MECHS.SASL_GSSAPI: + # GSSAPI has 2 extra exchanges + # https://datatracker.ietf.org/doc/html/rfc2222#section-7.2.1 + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(b""), + authentication=LDAP_Authentication_SaslCredentials( + mechanism=ASN1_STRING(self.mech.value), + credentials=None, + ), + ) + ) + # Parse server-supported layers + saslOptions = LDAP_SASL_GSSAPI_SsfCap( + self.ssp.GSS_Unwrap( + self.sspcontext, + GSSAPI_BLOB_SIGNATURE(resp.protocolOp.serverSaslCredsData), + ) + ) + if self.sign and not saslOptions.supported_security_layers.INTEGRITY: + raise RuntimeError("GSSAPI SASL failed to negotiate INTEGRITY !") + if ( + self.encrypt + and not saslOptions.supported_security_layers.CONFIDENTIALITY + ): + raise RuntimeError("GSSAPI SASL failed to negotiate CONFIDENTIALITY !") + # Announce client-supported layers + saslOptions = LDAP_SASL_GSSAPI_SsfCap( + supported_security_layers=( + "+".join( + (["INTEGRITY"] if self.sign else []) + + (["CONFIDENTIALITY"] if self.encrypt else []) + ) + if (self.sign or self.encrypt) + else "NONE" + ), + # Same as server + max_output_token_size=saslOptions.max_output_token_size, + ) + resp = self.sr1( + LDAP_BindRequest( + bind_name=ASN1_STRING(b""), + authentication=LDAP_Authentication_SaslCredentials( + mechanism=ASN1_STRING(self.mech.value), + credentials=self.ssp.GSS_Wrap( + self.sspcontext, + bytes(saslOptions), + # We still haven't finished negotiating + conf_req_flag=False, + ), + ), + ) + ) + if resp.protocolOp.resultCode != 0: + raise LDAP_Exception( + "GSSAPI SASL failed to negotiate client security flags !", + resp=resp, + ) + + # If we use SPNEGO and NTLMSSP was used, understand we can't use sign + if self.mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO: + from scapy.layers.ntlm import NTLMSSP + + if isinstance(self.sspcontext.ssp, NTLMSSP): + self.sign = False + + # SASL wrapping is now available. + self.sasl_wrap = self.encrypt or self.sign + if self.sasl_wrap: + self.sock.closed = True # prevent closing by marking it as already closed. + self.sock = StreamSocket(self.sock.ins, LDAP_SASL_Buffer) + + # Success. + if self.verb: + print("%s bind succeeded !" % self.mech.name) + self.bound = True + + _TEXT_REG = re.compile(b"^[%s]*$" % re.escape(string.printable.encode())) + + def search( + self, + baseObject: str = "", + filter: str = "", + scope=0, + derefAliases=0, + sizeLimit=300000, + timeLimit=3000, + attrsOnly=0, + attributes: List[str] = [], + controls: List[LDAP_Control] = [], + ) -> Dict[str, List[Any]]: + """ + Perform a LDAP search. + + :param baseObject: the dn of the base object to search in. + :param filter: the filter to apply to the search (currently unsupported) + :param scope: 0=baseObject, 1=singleLevel, 2=wholeSubtree + """ + if baseObject == "rootDSE": + baseObject = "" + if filter: + filter = LDAP_Filter.from_rfc2254_string(filter) + else: + # Default filter: (objectClass=*) + filter = LDAP_Filter( + filter=LDAP_FilterPresent( + present=ASN1_STRING(b"objectClass"), + ) + ) + # we loop as we might need more than one packet thanks to paging + cookie = b"" + entries = {} + while True: + resp = self.sr1( + LDAP_SearchRequest( + filter=filter, + attributes=[ + LDAP_SearchRequestAttribute(type=ASN1_STRING(attr)) + for attr in attributes + ], + baseObject=ASN1_STRING(baseObject), + scope=ASN1_ENUMERATED(scope), + derefAliases=ASN1_ENUMERATED(derefAliases), + sizeLimit=ASN1_INTEGER(sizeLimit), + timeLimit=ASN1_INTEGER(timeLimit), + attrsOnly=ASN1_BOOLEAN(attrsOnly), + ), + controls=( + controls + + ( + [ + # This control is only usable when bound. + LDAP_Control( + controlType="1.2.840.113556.1.4.319", + criticality=True, + controlValue=LDAP_realSearchControlValue( + size=100, # paging to 100 per 100 + cookie=cookie, + ), + ) + ] + if self.bound + else [] + ) + ), + timeout=self.timeout, + ) + if LDAP_SearchResponseResultDone not in resp: + resp.show() + raise TimeoutError("Search timed out.") + # Now, reassemble the results + + def _s(x): + try: + return x.decode() + except UnicodeDecodeError: + return x + + def _ssafe(x): + if self._TEXT_REG.match(x): + return x.decode() + else: + return x + + # For each individual packet response + while resp: + # Find all 'LDAP' layers + if LDAP not in resp: + log_runtime.warning("Invalid response: %s", repr(resp)) + break + if LDAP_SearchResponseEntry in resp.protocolOp: + attrs = { + _s(attr.type.val): [_ssafe(x.value.val) for x in attr.values] + for attr in resp.protocolOp.attributes + } + entries[_s(resp.protocolOp.objectName.val)] = attrs + elif LDAP_SearchResponseResultDone in resp.protocolOp: + resultCode = resp.protocolOp.resultCode + if resultCode != 0x0: # != success + log_runtime.warning( + resp.protocolOp.sprintf("Got response: %resultCode%") + ) + raise LDAP_Exception( + "LDAP search failed !", + resp=resp, + ) + else: + # success + if resp.Controls: + # We have controls back + realSearchControlValue = next( + ( + c.controlValue + for c in resp.Controls + if isinstance( + c.controlValue, LDAP_realSearchControlValue + ) + ), + None, + ) + if realSearchControlValue is not None: + # has paging ! + cookie = realSearchControlValue.cookie.val + break + break + resp = resp.payload + # If we have a cookie, continue + if not cookie: + break + return entries + + def modify( + self, + object: str, + changes: List[LDAP_ModifyRequestChange], + controls: List[LDAP_Control] = [], + ) -> None: + """ + Perform a LDAP modify request. + + :returns: + """ + resp = self.sr1( + LDAP_ModifyRequest( + object=object, + changes=changes, + ), + controls=controls, + timeout=self.timeout, + ) + if ( + LDAP_ModifyResponse not in resp.protocolOp + or resp.protocolOp.resultCode != 0 + ): + raise LDAP_Exception( + "LDAP modify failed !", + resp=resp, + ) + + def add( + self, + entry: str, + attributes: Union[Dict[str, List[Any]], List[ASN1_Packet]], + controls: List[LDAP_Control] = [], + ): + """ + Perform a LDAP add request. + + :param attributes: the attributes to add. We support two formats: + - a list of LDAP_Attribute (or LDAP_PartialAttribute) + - a dict following {attribute: [list of values]} + + :returns: + """ + # We handle the two cases in the type of attributes + if isinstance(attributes, dict): + attributes = [ + LDAP_Attribute( + type=ASN1_STRING(k), + values=[ + LDAP_AttributeValue( + value=ASN1_STRING(x), + ) + for x in v + ], + ) + for k, v in attributes.items() + ] + + resp = self.sr1( + LDAP_AddRequest( + entry=ASN1_STRING(entry), + attributes=attributes, + ), + controls=controls, + timeout=self.timeout, + ) + if LDAP_AddResponse not in resp.protocolOp or resp.protocolOp.resultCode != 0: + raise LDAP_Exception( + "LDAP add failed !", + resp=resp, + ) + + def modifydn( + self, + entry: str, + newdn: str, + deleteoldrdn=True, + controls: List[LDAP_Control] = [], + ): + """ + Perform a LDAP modify DN request. + + ..note:: This functions calculates the relative DN and superior required for + LDAP ModifyDN automatically. + + :param entry: the DN of the entry to rename. + :param newdn: the new FULL DN of the entry. + :returns: + """ + # RFC4511 sect 4.9 + # Calculate the newrdn (relative DN) and superior + newrdn, newSuperior = newdn.split(",", 1) + _, cur_superior = entry.split(",", 1) + # If the superior hasn't changed, don't update it. + if cur_superior == newSuperior: + newSuperior = None + # Send the request + resp = self.sr1( + LDAP_ModifyDNRequest( + entry=entry, + newrdn=newrdn, + newSuperior=newSuperior, + deleteoldrdn=deleteoldrdn, + ), + controls=controls, + timeout=self.timeout, + ) + if ( + LDAP_ModifyDNResponse not in resp.protocolOp + or resp.protocolOp.resultCode != 0 + ): + raise LDAP_Exception( + "LDAP modify failed !", + resp=resp, + ) + + def close(self): + if self.verb: + print("X Connection closed\n") + self.sock.close() + self.bound = False + self.sspcontext = None diff --git a/scapy/layers/llmnr.py b/scapy/layers/llmnr.py index b606f3717e2..1f3282879d5 100644 --- a/scapy/layers/llmnr.py +++ b/scapy/layers/llmnr.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ LLMNR (Link Local Multicast Node Resolution). @@ -14,39 +14,68 @@ import struct -from scapy.fields import BitEnumField, BitField, ShortField +from scapy.fields import ( + BitEnumField, + BitField, + DestField, + DestIP6Field, + ShortField, +) from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.compat import orb from scapy.layers.inet import UDP -from scapy.layers.dns import DNSQRField, DNSRRField, DNSRRCountField +from scapy.layers.dns import ( + DNSCompressedPacket, + DNS_am, + DNS, + DNSQR, + DNSRR, +) _LLMNR_IPv6_mcast_Addr = "FF02:0:0:0:0:0:1:3" _LLMNR_IPv4_mcast_addr = "224.0.0.252" -class LLMNRQuery(Packet): +class LLMNRQuery(DNSCompressedPacket): name = "Link Local Multicast Node Resolution - Query" - fields_desc = [ShortField("id", 0), - BitField("qr", 0, 1), - BitEnumField("opcode", 0, 4, {0: "QUERY"}), - BitField("c", 0, 1), - BitField("tc", 0, 2), - BitField("z", 0, 4), - BitEnumField("rcode", 0, 4, {0: "ok"}), - DNSRRCountField("qdcount", None, "qd"), - DNSRRCountField("ancount", None, "an"), - DNSRRCountField("nscount", None, "ns"), - DNSRRCountField("arcount", None, "ar"), - DNSQRField("qd", "qdcount"), - DNSRRField("an", "ancount"), - DNSRRField("ns", "nscount"), - DNSRRField("ar", "arcount", 0)] + qd = [] + fields_desc = [ + ShortField("id", 0), + BitField("qr", 0, 1), + BitEnumField("opcode", 0, 4, {0: "QUERY"}), + BitField("c", 0, 1), + BitField("tc", 0, 1), + BitField("t", 0, 1), + BitField("z", 0, 4) + ] + DNS.fields_desc[-9:] overload_fields = {UDP: {"sport": 5355, "dport": 5355}} + def get_full(self): + # Required for DNSCompressedPacket + return self.original + def hashret(self): return struct.pack("!H", self.id) + def mysummary(self): + s = self.__class__.__name__ + if self.qr: + if self.an and isinstance(self.an[0], DNSRR): + s += " '%s' is at '%s'" % ( + self.an[0].rrname.decode(errors="backslashreplace"), + self.an[0].rdata, + ) + else: + s += " [malformed]" + elif self.qd and isinstance(self.qd[0], DNSQR): + s += " who has '%s'" % ( + self.qd[0].qname.decode(errors="backslashreplace"), + ) + else: + s += " [malformed]" + return s, [UDP] + class LLMNRResponse(LLMNRQuery): name = "Link Local Multicast Node Resolution - Response" @@ -74,4 +103,26 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): bind_bottom_up(UDP, _LLMNR, sport=5355) bind_layers(UDP, _LLMNR, sport=5355, dport=5355) +DestField.bind_addr(LLMNRQuery, _LLMNR_IPv4_mcast_addr, dport=5355) +DestField.bind_addr(LLMNRResponse, _LLMNR_IPv4_mcast_addr, dport=5355) +DestIP6Field.bind_addr(LLMNRQuery, _LLMNR_IPv6_mcast_Addr, dport=5355) +DestIP6Field.bind_addr(LLMNRResponse, _LLMNR_IPv6_mcast_Addr, dport=5355) + + +class LLMNR_am(DNS_am): + """ + LLMNR answering machine. + + This has the same arguments as DNS_am. See help(DNS_am) + + Example:: + + >>> llmnrd(joker="192.168.0.2", iface="eth0") + >>> llmnrd(match={"TEST": "192.168.0.2"}) + """ + function_name = "llmnrd" + filter = "udp port 5355" + cls = LLMNRQuery + + # LLMNRQuery(id=RandShort(), qd=DNSQR(qname="vista."))) diff --git a/scapy/layers/lltd.py b/scapy/layers/lltd.py index c9ce0c0adb6..e37aeef4872 100644 --- a/scapy/layers/lltd.py +++ b/scapy/layers/lltd.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """LLTD Protocol @@ -9,7 +9,6 @@ """ -from __future__ import absolute_import from array import array from scapy.fields import BitField, FlagsField, ByteField, ByteEnumField, \ @@ -22,7 +21,6 @@ from scapy.layers.inet import IPField from scapy.layers.inet6 import IP6Field from scapy.data import ETHER_ANY -import scapy.modules.six as six from scapy.compat import orb, chb @@ -111,8 +109,8 @@ def mysummary(self): def hashret(self): tos, function = self.tos, self.function - return "%c%c" % self.answer_hashret.get((tos, function), - (tos, function)) + return b"%c%c" % self.answer_hashret.get((tos, function), + (tos, function)) def answers(self, other): if not isinstance(other, LLTD): @@ -299,7 +297,7 @@ def dispatch_hook(cls, _pkt=None, *_, **kargs): cmd = orb(_pkt[0]) elif "type" in kargs: cmd = kargs["type"] - if isinstance(cmd, six.string_types): + if isinstance(cmd, str): cmd = cls.fields_desc[0].s2i[cmd] else: return cls @@ -717,7 +715,7 @@ class LLTDAttributeMachineName(LLTDAttribute): ] def mysummary(self): - return (self.sprintf("Hostname: %r" % self.hostname), + return (f"Hostname: {self.hostname!r}", [LLTD, LLTDAttributeHostID]) @@ -842,4 +840,4 @@ def get_data(self): """ return {key: "".join(chr(byte) for byte in data) - for key, data in six.iteritems(self.data)} + for key, data in self.data.items()} diff --git a/scapy/layers/mgcp.py b/scapy/layers/mgcp.py index 34d020275a4..f813f47aa36 100644 --- a/scapy/layers/mgcp.py +++ b/scapy/layers/mgcp.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ MGCP (Media Gateway Control Protocol) diff --git a/scapy/layers/mobileip.py b/scapy/layers/mobileip.py index 67c2ce2059d..bf36c5a1cce 100644 --- a/scapy/layers/mobileip.py +++ b/scapy/layers/mobileip.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Mobile IP. diff --git a/scapy/layers/ms_nrtp.py b/scapy/layers/ms_nrtp.py new file mode 100644 index 00000000000..2af3c6b1d05 --- /dev/null +++ b/scapy/layers/ms_nrtp.py @@ -0,0 +1,1038 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +.NET RemoTing Protocol + +This implements: +- [MS-NRTP] - .NET Remoting Core Protocol +- [MS-NRBF] - .NET Remoting Binary Format +""" + +import enum +import functools +import struct + +from scapy.automaton import Automaton, ATMT +from scapy.config import conf +from scapy.main import interact +from scapy.fields import ( + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + FlagsField, + LEIntField, + LELongField, + LEShortEnumField, + LEShortField, + LESignedIntField, + LESignedLongField, + LESignedShortField, + LenField, + MSBExtendedField, + MultipleTypeField, + PacketField, + PacketListField, + SignedByteField, + StrField, + StrFixedLenField, + StrLenField, + StrLenFieldUtf16, +) +from scapy.packet import Packet +from scapy.supersocket import StreamSocket + + +# [MS-NRTP] sect 2.2.3.2.1 + + +class CountedString(Packet): + fields_desc = [ + ByteEnumField( + "StringEncoding", + 0, + { + 0: "Unicode", + 1: "UTF8", + }, + ), + FieldLenField("Length", None, fmt="= 2: + return cls.registered_headers.get( + struct.unpack("= 14: + cd = struct.unpack("= length: + # Get content-type + try: + content_type = next( + x.ContentTypeValue.StringData + for x in pkt.Headers + if x.HeaderToken == 6 + ) + session["content_type"] = content_type + except StopIteration: + # Not in this packet. Do we know it from the session? + content_type = session.get("content_type", None) + if not content_type: + return pkt + # We have a content-type. Parse it. + if content_type == b"application/octet-stream": + # pkt.payload is NRBF. + pkt.payload = NRBF(bytes(pkt.payload)) + return pkt + return None + + +# [MS-NRBF] .NET Remoting Binary Format + + +class MSBExtendedFieldLen(MSBExtendedField): + __slots__ = FieldLenField.__slots__ + + def __init__(self, name, default, length_of=None): + FieldLenField.__init__(self, name, default, length_of=length_of) + super(MSBExtendedFieldLen, self).__init__(name, default) + + i2m = FieldLenField.i2m + + +# [MS-NRBF] sect 2.1.1.6 + + +class NRBFLengthPrefixedString(Packet): + fields_desc = [ + MSBExtendedFieldLen("Length", None, length_of="String"), + StrLenField("String", b"", length_from=lambda pkt: pkt.Length), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# [MS-NRBF] sect 2.1.1.8 + + +class NRBFClassTypeInfo(Packet): + fields_desc = [ + PacketField("TypeName", NRBFLengthPrefixedString(), NRBFLengthPrefixedString), + LESignedIntField("LibraryId", 0), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# [MS-NRBF] sect 2.1.2.3 + + +class PrimitiveTypeEnum(enum.IntEnum): + Boolean = 1 + Byte = 2 + Char = 2 + Decimal = 5 + Double = 6 + Int16 = 7 + Int32 = 8 + Int64 = 9 + SByte = 10 + Single = 11 + TimeSpan = 12 + DateTime = 13 + UInt16 = 14 + UInt32 = 15 + UInt64 = 16 + Null = 17 + String = 18 + + +# [MS-NRBF] sect 2.1.2.2 + + +class BinaryTypeEnum(enum.IntEnum): + Primitive = 0 + String = 1 + Object = 2 + SystemClass = 3 + Class = 4 + ObjectArray = 5 + StringArray = 6 + PrimitiveArray = 7 + + +# [MS-NRBF] sect 2.2.2.1 + + +class NRBFValueWithCode(Packet): + fields_desc = [ + ByteEnumField("PrimitiveType", 0, PrimitiveTypeEnum), + MultipleTypeField( + [ + (ByteField("Value", 0), lambda pkt: pkt.PrimitiveType in [1, 2, 3, 4]), + (LESignedShortField("Value", 0), lambda pkt: pkt.PrimitiveType == 7), + (LESignedIntField("Value", 0), lambda pkt: pkt.PrimitiveType == 8), + (LESignedLongField("Value", 0), lambda pkt: pkt.PrimitiveType == 9), + (SignedByteField("Value", 0), lambda pkt: pkt.PrimitiveType == 10), + (LEShortField("Value", 0), lambda pkt: pkt.PrimitiveType == 14), + (LEIntField("Value", 0), lambda pkt: pkt.PrimitiveType == 15), + (LELongField("Value", 0), lambda pkt: pkt.PrimitiveType == 16), + ( + PacketField( + "Value", NRBFLengthPrefixedString(), NRBFLengthPrefixedString + ), + lambda pkt: pkt.PrimitiveType == 18, + ), + ], + StrFixedLenField("Value", b"", length=0), + ), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# [MS-NRBF] sect 2.2.2.2 + + +class NRBFStringValueWithCode(NRBFValueWithCode): + PrimitiveType = 18 + + +StringValueWithCode = lambda name: PacketField( + name, NRBFStringValueWithCode(), NRBFStringValueWithCode +) + + +# [MS-NRBF] sect 2.2.2.3 + + +class NRBFArrayOfValueWithCode(Packet): + fields_desc = [ + FieldLenField("Length", None, fmt="= pkt.MemberCount: + return None + if hasattr(pkt, "BinaryTypeEnums"): + if index < len(pkt.BinaryTypeEnums): + typeEnum = pkt.BinaryTypeEnums[index] + if typeEnum == BinaryTypeEnum.Primitive: + # Get AdditionalInfo to get the matching primitive type. + primitiveType = pkt.AdditionalInfos[ + sum( + 1 + for x in pkt.BinaryTypeEnums[:index] + if x + not in [ + BinaryTypeEnum.String, + BinaryTypeEnum.Object, + BinaryTypeEnum.ObjectArray, + BinaryTypeEnum.StringArray, + ] + ) + ].Value + return functools.partial( + NRBFMemberPrimitiveUnTyped, + primtype=PrimitiveTypeEnum(primitiveType), + ) + return NRBFRecord + + +class _NRBFMembers(Packet): + fields_desc = [ + PacketListField( + "Members", + [], + None, + next_cls_cb=_members_cb, + ) + ] + + +# [MS-NRBF] sect 2.3.1.1 + + +class NRBFClassInfo(Packet): + fields_desc = [ + LESignedIntField("ObjectId", 0), + PacketField("Name", NRBFLengthPrefixedString(), NRBFLengthPrefixedString), + FieldLenField("MemberCount", None, fmt="= index + ) + except StopIteration: + return None + typeEnum = BinaryTypeEnum(typeEnum) + # Return BinaryTypeEnum tainted with a preselected type. + return functools.partial( + NRBFAdditionalInfo, + bintype=typeEnum, + ) + + +class NRBFMemberTypeInfo(Packet): + fields_desc = [ + FieldListField( + "BinaryTypeEnums", + [], + ByteEnumField("", 0, BinaryTypeEnum), + count_from=lambda pkt: pkt.MemberCount, + ), + PacketListField( + "AdditionalInfos", + [], + None, + next_cls_cb=_member_type_infos_cb, + ), + ] + + +# [MS-NRBF] 2.3.2.5 + + +class NRBFClassWithId(NRBFRecord): + RecordTypeEnum = 1 + fields_desc = [ + NRBFRecord, + LESignedIntField("ObjectId", 0), + LESignedIntField("MetadataId", 0), + ] + + +# [MS-NRBF] sect 2.5.2 + + +class NRBFMemberPrimitiveUnTyped(Packet): + __slots__ = ["primtype"] + + fields_desc = [ + NRBFValueWithCode.fields_desc[1], + ] + + def __init__(self, _pkt=None, **kwargs): + self.primtype = kwargs.pop("primtype", PrimitiveTypeEnum.Byte) + assert isinstance(self.primtype, PrimitiveTypeEnum) + super(NRBFMemberPrimitiveUnTyped, self).__init__(_pkt, **kwargs) + + def clone_with(self, *args, **kwargs): + pkt = super(NRBFMemberPrimitiveUnTyped, self).clone_with(*args, **kwargs) + pkt.primtype = self.primtype + return pkt + + def copy(self): + pkt = super(NRBFMemberPrimitiveUnTyped, self).copy() + pkt.primtype = self.primtype + return pkt + + @property + def PrimitiveType(self): + return self.primtype + + def default_payload_class(self, payload): + return conf.padding_layer + + +# [MS-NRBF] sect 2.3.2.1 + + +class NRBFClassWithMembersAndTypes(NRBFRecord): + RecordTypeEnum = 5 + fields_desc = [ + NRBFRecord, + NRBFClassInfo, + NRBFMemberTypeInfo, + LESignedIntField("LibraryId", 0), + _NRBFMembers, + ] + + +# [MS-NRBF] sect 2.3.2.3 + + +class NRBFSystemClassWithMembersAndTypes(NRBFRecord): + RecordTypeEnum = 4 + fields_desc = [ + NRBFRecord, + NRBFClassInfo, + NRBFMemberTypeInfo, + _NRBFMembers, + ] + + +# [MS-NRBF] sect 2.3.2.4 + + +class NRBFSystemClassWithMembers(NRBFRecord): + RecordTypeEnum = 2 + fields_desc = [ + NRBFRecord, + NRBFClassInfo, + _NRBFMembers, + ] + + +# [MS-NRBF] sect 2.4.2.1 + + +class ArrayInfo(Packet): + fields_desc = [LEIntField("ObjectId", 0), LEIntField("Length", None)] + + +# [MS-NRBF] sect 2.4.3.2 + + +class NRBFArraySingleObject(NRBFRecord): + RecordTypeEnum = 16 + Length = 1 + fields_desc = [ + NRBFRecord, + ArrayInfo, + ] + + +# [MS-NRBF] sect 2.4.3.3 + + +def _values_singleprim_cb(pkt, lst, cur, remain): + index = len(lst) + (1 if cur is not None else 0) + if index >= pkt.Length: + return None + return functools.partial( + NRBFMemberPrimitiveUnTyped, + primtype=PrimitiveTypeEnum(pkt.PrimitiveTypeEnum), + ) + + +class NRBFArraySinglePrimitive(NRBFRecord): + RecordTypeEnum = 15 + fields_desc = [ + NRBFRecord, + ArrayInfo, + ByteEnumField("PrimitiveTypeEnum", 0, PrimitiveTypeEnum), + MultipleTypeField( + [ + ( + StrLenField("Values", [], length_from=lambda pkt: pkt.Length), + lambda pkt: pkt.PrimitiveTypeEnum == PrimitiveTypeEnum.Byte, + ) + ], + PacketListField( + "Values", + [], + next_cls_cb=_values_singleprim_cb, + max_count=1000, + ), + ), + ] + + def post_build(self, p, pay): + if self.Length is None: + p = p[:5] + struct.pack(" + +""" +All MSRPCE layers +""" + +import uuid + +from scapy.error import log_loading +from scapy.main import load_layer + +from scapy.layers.dcerpc import ( + DCE_RPC_INTERFACES_NAMES, + DCE_RPC_INTERFACES_NAMES_rev, +) + +__all__ = [] + + +# Load all layers bundled with Scapy +_LAYERS = [ + # High-level classes + "msrpce.msdcom", + "msrpce.mseerr", + "msrpce.msnrpc", + "msrpce.mspac", + # Client / Server + "msrpce.rpcclient", + "msrpce.rpcserver", + # Low-level RPC definitions + "msrpce.raw.ept", + "msrpce.raw.ms_dcom", + "msrpce.raw.ms_drsr", + "msrpce.raw.ms_nrpc", + "msrpce.raw.ms_samr", + "msrpce.raw.ms_srvs", + "msrpce.raw.ms_wkst", +] + +for _l in _LAYERS: + log_loading.debug("Loading MSRPCE layer %s", _l) + try: + load_layer(_l, globals_dict=globals(), symb_list=__all__) + except Exception as e: + log_loading.warning("can't import layer %s: %s", _l, e) + + +# Populate DCE_RPC_INTERFACES_NAMES for some well-known interfaces + +# Well-Known = from MSDN +_DCE_RPC_WELL_KNOWN_UUIDS = [ + (uuid.UUID("00000000-0000-0000-c000-000000000046"), "IUnknown"), + (uuid.UUID("00000131-0000-0000-c000-000000000046"), "IRemUnknown"), + (uuid.UUID("00000143-0000-0000-c000-000000000046"), "IRemUnknown2"), + (uuid.UUID("000001a0-0000-0000-c000-000000000046"), "IRemoteSCMActivator"), + (uuid.UUID("00020400-0000-0000-c000-000000000046"), "IDispatch"), + (uuid.UUID("00020401-0000-0000-c000-000000000046"), "ITypeInfo"), + (uuid.UUID("00020402-0000-0000-c000-000000000046"), "ITypeLib"), + (uuid.UUID("00020403-0000-0000-c000-000000000046"), "ITypeComp"), + (uuid.UUID("00020404-0000-0000-c000-000000000046"), "IEnumVARIANT"), + (uuid.UUID("00020411-0000-0000-c000-000000000046"), "ITypeLib2"), + (uuid.UUID("00020412-0000-0000-c000-000000000046"), "ITypeInfo2"), + (uuid.UUID("004c6a2b-0c19-4c69-9f5c-a269b2560db9"), "IWindowsDriverUpdate4"), + (uuid.UUID("0191775e-bcff-445a-b4f4-3bdda54e2816"), "IAppHostPropertyCollection"), + (uuid.UUID("01954e6b-9254-4e6e-808c-c9e05d007696"), "IVssEnumMgmtObject"), + (uuid.UUID("027947e1-d731-11ce-a357-000000000001"), "IEnumWbemClassObject"), + (uuid.UUID("0316560b-5db4-4ed9-bbb5-213436ddc0d9"), "IVdsRemovable"), + ( + uuid.UUID("0344cdda-151e-4cbf-82da-66ae61e97754"), + "IAppHostElementSchemaCollection", + ), + (uuid.UUID("034634fd-ba3f-11d1-856a-00a0c944138c"), "IManageTelnetSessions"), + (uuid.UUID("038374ff-098b-11d8-9414-505054503030"), "IDataCollector"), + (uuid.UUID("03837502-098b-11d8-9414-505054503030"), "IDataCollectorCollection"), + ( + uuid.UUID("03837506-098b-11d8-9414-505054503030"), + "IPerformanceCounterDataCollector", + ), + (uuid.UUID("0383750b-098b-11d8-9414-505054503030"), "ITraceDataCollector"), + (uuid.UUID("03837510-098b-11d8-9414-505054503030"), "ITraceDataProviderCollection"), + (uuid.UUID("03837512-098b-11d8-9414-505054503030"), "ITraceDataProvider"), + (uuid.UUID("03837514-098b-11d8-9414-505054503030"), "IConfigurationDataCollector"), + (uuid.UUID("03837516-098b-11d8-9414-505054503030"), "IAlertDataCollector"), + (uuid.UUID("0383751a-098b-11d8-9414-505054503030"), "IApiTracingDataCollector"), + (uuid.UUID("03837520-098b-11d8-9414-505054503030"), "IDataCollectorSet"), + (uuid.UUID("03837524-098b-11d8-9414-505054503030"), "IDataCollectorSetCollection"), + (uuid.UUID("03837533-098b-11d8-9414-505054503030"), "IValueMapItem"), + (uuid.UUID("03837534-098b-11d8-9414-505054503030"), "IValueMap"), + (uuid.UUID("0383753a-098b-11d8-9414-505054503030"), "ISchedule"), + (uuid.UUID("0383753d-098b-11d8-9414-505054503030"), "IScheduleCollection"), + (uuid.UUID("03837541-098b-11d8-9414-505054503030"), "IDataManager"), + (uuid.UUID("03837543-098b-11d8-9414-505054503030"), "IFolderAction"), + (uuid.UUID("03837544-098b-11d8-9414-505054503030"), "IFolderActionCollection"), + (uuid.UUID("04c6895d-eaf2-4034-97f3-311de9be413a"), "IUpdateSearcher3"), + (uuid.UUID("070669eb-b52f-11d1-9270-00c04fbbbfb3"), "IDataFactory2"), + (uuid.UUID("0716caf8-7d05-4a46-8099-77594be91394"), "IAppHostConstantValue"), + (uuid.UUID("0770687e-9f36-4d6f-8778-599d188461c9"), "IFsrmFileManagementJob"), + (uuid.UUID("07e5c822-f00c-47a1-8fce-b244da56fd06"), "IVdsDisk"), + (uuid.UUID("07f7438c-7709-4ca5-b518-91279288134e"), "IUpdateCollection"), + (uuid.UUID("0818a8ef-9ba9-40d8-a6f9-e22833cc771e"), "IVdsService"), + (uuid.UUID("081e7188-c080-4ff3-9238-29f66d6cabfd"), "IMessenger"), + ( + uuid.UUID("08a90f5f-0702-48d6-b45f-02a9885a9768"), + "IAppHostChildElementCollection", + ), + (uuid.UUID("09829352-87c2-418d-8d79-4133969a489d"), "IAppHostChangeHandler"), + (uuid.UUID("0ac13689-3134-47c6-a17c-4669216801be"), "IVdsServiceHba"), + (uuid.UUID("0b1c2170-5732-4e0e-8cd3-d9b16f3b84d7"), "authzr"), + (uuid.UUID("0bb8531d-7e8d-424f-986c-a0b8f60a3e7b"), "IUpdateServiceManager2"), + ( + uuid.UUID("0d521700-a372-4bef-828b-3d00c10adebd"), + "IWindowsDriverUpdateEntryCollection", + ), + (uuid.UUID("0dd8a158-ebe6-4008-a1d9-b7ecc8f1104b"), "IAppHostSectionGroup"), + (uuid.UUID("0e3d6630-b46b-11d1-9d2d-006008b0e5ca"), "ICatalogTableRead"), + (uuid.UUID("0e3d6631-b46b-11d1-9d2d-006008b0e5ca"), "ICatalogTableWrite"), + (uuid.UUID("0eac4842-8763-11cf-a743-00aa00a3f00d"), "IDataFactory"), + (uuid.UUID("0fb15084-af41-11ce-bd2b-204c4f4f5020"), "ITransaction"), + (uuid.UUID("1088a980-eae5-11d0-8d9b-00a02453c337"), "qm2qm"), + (uuid.UUID("10c5e575-7984-4e81-a56b-431f5f92ae42"), "IVdsProvider"), + (uuid.UUID("112eda6b-95b3-476f-9d90-aee82c6b8181"), "IUpdate3"), + (uuid.UUID("118610b7-8d94-4030-b5b8-500889788e4e"), "IEnumVdsObject"), + (uuid.UUID("11899a43-2b68-4a76-92e3-a3d6ad8c26ce"), "TermSrvNotification"), + (uuid.UUID("11942d87-a1de-4e7f-83fb-a840d9c5928d"), "IClusterStorage3"), + (uuid.UUID("12345678-1234-abcd-ef00-0123456789ab"), "winspool"), + (uuid.UUID("12345678-1234-abcd-ef00-01234567cffb"), "logon"), + (uuid.UUID("12345778-1234-abcd-ef00-0123456789ab"), "lsarpc"), + (uuid.UUID("12345778-1234-abcd-ef00-0123456789ac"), "samr"), + (uuid.UUID("1257b580-ce2f-4109-82d6-a9459d0bf6bc"), "SessEnvPublicRpc"), + (uuid.UUID("12937789-e247-4917-9c20-f3ee9c7ee783"), "IFsrmActionCommand"), + (uuid.UUID("135698d2-3a37-4d26-99df-e2bb6ae3ac61"), "IVolumeClient3"), + (uuid.UUID("13b50bff-290a-47dd-8558-b7c58db1a71a"), "IVdsPack2"), + (uuid.UUID("144fe9b0-d23d-4a8b-8634-fb4457533b7a"), "IUpdate2"), + (uuid.UUID("14a8831c-bc82-11d2-8a64-0008c7457e5d"), "ExtendedError"), + (uuid.UUID("14fbe036-3ed7-4e10-90e9-a5ff991aff01"), "IVdsServiceIscsi"), + (uuid.UUID("1518b460-6518-4172-940f-c75883b24ceb"), "IUpdateService2"), + (uuid.UUID("1544f5e0-613c-11d1-93df-00c04fd7bd09"), "rfri"), + (uuid.UUID("1568a795-3924-4118-b74b-68d8f0fa5daf"), "IFsrmQuotaBase"), + (uuid.UUID("15a81350-497d-4aba-80e9-d4dbcc5521fe"), "IFsrmStorageModuleDefinition"), + (uuid.UUID("15fc031c-0652-4306-b2c3-f558b8f837e2"), "IVdsServiceSw"), + (uuid.UUID("17fdd703-1827-4e34-79d4-24a55c53bb37"), "msgsvc"), + (uuid.UUID("182c40fa-32e4-11d0-818b-00a0c9231c29"), "ICatalogSession"), + (uuid.UUID("1a9134dd-7b39-45ba-ad88-44d01ca47f28"), "RemoteRead"), + (uuid.UUID("1a927394-352e-4553-ae3f-7cf4aafca620"), "WdsRpcInterface"), + (uuid.UUID("1bb617b8-3886-49dc-af82-a6c90fa35dda"), "IFsrmMutableCollection"), + (uuid.UUID("1be2275a-b315-4f70-9e44-879b3a2a53f2"), "IVdsVolumeOnline"), + (uuid.UUID("1c1c45ee-4395-11d2-b60b-00104b703efd"), "IWbemFetchSmartEnum"), + (uuid.UUID("1d118904-94b3-4a64-9fa6-ed432666a7b9"), "ICatalog64BitSupport"), + (uuid.UUID("1e062b84-e5e6-4b4b-8a25-67b81e8f13e8"), "IVdsVDisk"), + (uuid.UUID("1f7b1697-ecb2-4cbb-8a0e-75c427f4a6f0"), "IImport2"), + (uuid.UUID("1ff70682-0a51-30e8-076d-740be8cee98b"), "atsvc"), + (uuid.UUID("205bebf8-dd93-452a-95a6-32b566b35828"), "IFsrmFileScreenTemplate"), + (uuid.UUID("20610036-fa22-11cf-9823-00a0c911e5df"), "rasrpc"), + (uuid.UUID("20d15747-6c48-4254-a358-65039fd8c63c"), "IServerHealthReport2"), + ( + uuid.UUID("214a0f28-b737-4026-b847-4f9e37d79529"), + "IVssDifferentialSoftwareSnapshotMgmt", + ), + (uuid.UUID("21546ae8-4da5-445e-987f-627fea39c5e8"), "IWRMConfig"), + (uuid.UUID("22bcef93-4a3f-4183-89f9-2f8b8a628aee"), "IFsrmObject"), + (uuid.UUID("22e5386d-8b12-4bf0-b0ec-6a1ea419e366"), "NetEventForwarder"), + (uuid.UUID("23857e3c-02ba-44a3-9423-b1c900805f37"), "IUpdateServiceManager"), + (uuid.UUID("23c9dd26-2355-4fe2-84de-f779a238adbd"), "IProcessDump"), + (uuid.UUID("27b899fe-6ffa-4481-a184-d3daade8a02b"), "IFsrmReportManager"), + (uuid.UUID("27e94b0d-5139-49a2-9a61-93522dc54652"), "IUpdate4"), + (uuid.UUID("29822ab7-f302-11d0-9953-00c04fd919c1"), "IWamAdmin"), + (uuid.UUID("29822ab8-f302-11d0-9953-00c04fd919c1"), "IWamAdmin2"), + (uuid.UUID("2a3eb639-d134-422d-90d8-aaa1b5216202"), "IResourceManager2"), + (uuid.UUID("2abd757f-2851-4997-9a13-47d2a885d6ca"), "IVdsHbaPort"), + (uuid.UUID("2c9273e0-1dc3-11d3-b364-00105a1f8177"), "IWbemRefreshingServices"), + (uuid.UUID("2d9915fb-9d42-4328-b782-1b46819fab9e"), "IAppHostMethodSchema"), + (uuid.UUID("2dbe63c4-b340-48a0-a5b0-158e07fc567e"), "IFsrmActionReport"), + (uuid.UUID("300f3532-38cc-11d0-a3f0-0020af6b0add"), "trkwks"), + (uuid.UUID("31a83ea0-c0e4-4a2c-8a01-353cc2a4c60a"), "IAppHostMappingExtension"), + (uuid.UUID("326af66f-2ac0-4f68-bf8c-4759f054fa29"), "IFsrmPropertyCondition"), + (uuid.UUID("338cd001-2244-31f1-aaaa-900038001003"), "winreg"), + (uuid.UUID("367abb81-9844-35f1-ad32-98f038001003"), "svcctl"), + (uuid.UUID("370af178-7758-4dad-8146-7391f6e18585"), "IAppHostConfigLocation"), + (uuid.UUID("377f739d-9647-4b8e-97d2-5ffce6d759cd"), "IFsrmQuota"), + (uuid.UUID("378e52b0-c0a9-11cf-822d-00aa0051e40f"), "sasec"), + (uuid.UUID("3858c0d5-0f35-4bf5-9714-69874963bc36"), "IVdsAdvancedDisk3"), + (uuid.UUID("38a0a9ab-7cc8-4693-ac07-1f28bd03c3da"), "IVdsIscsiInitiatorPortal"), + (uuid.UUID("38e87280-715c-4c7d-a280-ea1651a19fef"), "IFsrmReportJob"), + (uuid.UUID("3919286a-b10c-11d0-9ba8-00c04fd92ef5"), "dssetup"), + (uuid.UUID("39322a2d-38ee-4d0d-8095-421a80849a82"), "IFsrmDerivedObjectsResult"), + (uuid.UUID("3a410f21-553f-11d1-8e5e-00a0c92c9d5d"), "IDMRemoteServer"), + (uuid.UUID("3a56bfb8-576c-43f7-9335-fe4838fd7e37"), "ICategoryCollection"), + (uuid.UUID("3b69d7f5-9d94-4648-91ca-79939ba263bf"), "IVdsPack"), + (uuid.UUID("3bbed8d9-2c9a-4b21-8936-acb2f995be6c"), "INtmsObjectManagement3"), + (uuid.UUID("3dde7c30-165d-11d1-ab8f-00805f14db40"), "BackupKey"), + (uuid.UUID("3f3b1b86-dbbe-11d1-9da6-00805f85cfe3"), "IContainerControl"), + (uuid.UUID("40f73c8b-687d-4a13-8d96-3d7f2e683936"), "IVdsDisk2"), + (uuid.UUID("41208ee0-e970-11d1-9b9e-00e02c064c39"), "qmmgmt"), + (uuid.UUID("4173ac41-172d-4d52-963c-fdc7e415f717"), "IFsrmQuotaTemplateManager"), + (uuid.UUID("423ec01e-2e35-11d2-b604-00104b703efd"), "IWbemWCOSmartEnum"), + (uuid.UUID("426677d5-018c-485c-8a51-20b86d00bdc4"), "IFsrmFileGroupManager"), + (uuid.UUID("42dc3511-61d5-48ae-b6dc-59fc00c0a8d6"), "IFsrmQuotaObject"), + (uuid.UUID("44aca674-e8fc-11d0-a07c-00c04fb68820"), "IWbemContext"), + (uuid.UUID("44aca675-e8fc-11d0-a07c-00c04fb68820"), "IWbemCallResult"), + (uuid.UUID("44e265dd-7daf-42cd-8560-3cdb6e7a2729"), "TsProxyRpcInterface"), + (uuid.UUID("450386db-7409-4667-935e-384dbbee2a9e"), "IAppHostPropertySchema"), + (uuid.UUID("456129e2-1078-11d2-b0f9-00805fc73204"), "ICatalogUtils"), + (uuid.UUID("45f52c28-7f9f-101a-b52b-08002b2efabe"), "winsif"), + (uuid.UUID("46297823-9940-4c09-aed9-cd3ea6d05968"), "IUpdateIdentity"), + (uuid.UUID("4639db2a-bfc5-11d2-9318-00c04fbbbfb3"), "IDataFactory3"), + (uuid.UUID("47782152-d16c-4229-b4e1-0ddfe308b9f6"), "IFsrmPropertyDefinition2"), + (uuid.UUID("47cde9a1-0bf6-11d2-8016-00c04fb9988e"), "ICapabilitySupport"), + (uuid.UUID("481e06cf-ab04-4498-8ffe-124a0a34296d"), "IWRMCalendar"), + (uuid.UUID("4846cb01-d430-494f-abb4-b1054999fb09"), "IFsrmQuotaManagerEx"), + (uuid.UUID("484809d6-4239-471b-b5bc-61df8c23ac48"), "TermSrvSession"), + (uuid.UUID("497d95a6-2d27-4bf5-9bbd-a6046957133c"), "RCMListener"), + (uuid.UUID("49ebd502-4a96-41bd-9e3e-4c5057f4250c"), "IWindowsDriverUpdate3"), + (uuid.UUID("4a2f5c31-cfd9-410e-b7fb-29a653973a0f"), "IAutomaticUpdates2"), + (uuid.UUID("4a6b0e15-2e38-11d1-9965-00c04fbbb345"), "IEventSubscription"), + (uuid.UUID("4a6b0e16-2e38-11d1-9965-00c04fbbb345"), "IEventSubscription2"), + (uuid.UUID("4a73fee4-4102-4fcc-9ffb-38614f9ee768"), "IFsrmProperty"), + (uuid.UUID("4afc3636-db01-4052-80c3-03bbcb8d3c69"), "IVdsServiceInitialization"), + (uuid.UUID("4b324fc8-1670-01d3-1278-5a47bf6ee188"), "srvsvc"), + (uuid.UUID("4bb8ab1d-9ef9-4100-8eb6-dd4b4e418b72"), "IADProxy"), + (uuid.UUID("4bdafc52-fe6a-11d2-93f8-00105a11164a"), "IVolumeClient2"), + (uuid.UUID("4c8f96c3-5d94-4f37-a4f4-f56ab463546f"), "IFsrmActionEventLog"), + (uuid.UUID("4cbdcb2d-1589-4beb-bd1c-3e582ff0add0"), "IUpdateSearcher2"), + (uuid.UUID("4d9f4ab8-7d1c-11cf-861e-0020af6e7c57"), "IActivation"), + (uuid.UUID("4da1c422-943d-11d1-acae-00c04fc2aa3f"), "trksvr"), + (uuid.UUID("4daa0135-e1d1-40f1-aaa5-3cc1e53221c3"), "IVdsVolumePlex"), + (uuid.UUID("4dbcee9a-6343-4651-b85f-5e75d74d983c"), "IVdsVolumeMF2"), + (uuid.UUID("4dfa1df3-8900-4bc7-bbb5-d1a458c52410"), "IAppHostConfigException"), + (uuid.UUID("4e14fb9f-2e22-11d1-9964-00c04fbbb345"), "IEventSystem"), + (uuid.UUID("4e6cdcc9-fb25-4fd5-9cc5-c9f4b6559cec"), "IComTrackingInfoEvents"), + (uuid.UUID("4e934f30-341a-11d1-8fb1-00a024cb6019"), "INtmsLibraryControl1"), + (uuid.UUID("4f7ca01c-a9e5-45b6-b142-2332a1339c1d"), "IWRMAccounting"), + (uuid.UUID("4fc742e0-4a10-11cf-8273-00aa004ae673"), "netdfs"), + (uuid.UUID("503626a3-8e14-4729-9355-0fe664bd2321"), "IUpdateExceptionCollection"), + (uuid.UUID("50abc2a4-574d-40b3-9d66-ee4fd5fba076"), "DnsServer"), + ( + uuid.UUID("515c1277-2c81-440e-8fcf-367921ed4f59"), + "IFsrmPipelineModuleDefinition", + ), + (uuid.UUID("5261574a-4572-206e-b268-6b199213b4e4"), "asyncemsmdb"), + (uuid.UUID("52c80b95-c1ad-4240-8d89-72e9fa84025e"), "IClusCfgAsyncEvictCleanup"), + (uuid.UUID("538684e0-ba3d-4bc0-aca9-164aff85c2a9"), "IVdsDiskPartitionMF"), + (uuid.UUID("53b46b02-c73b-4a3e-8dee-b16b80672fc0"), "TSVIPPublic"), + (uuid.UUID("541679ab-2e5f-11d3-b34e-00104bcc4b4a"), "IWbemLoginHelper"), + (uuid.UUID("5422fd3a-d4b8-4cef-a12e-e87d4ca22e90"), "ICertRequestD2"), + (uuid.UUID("54a2cb2d-9a0c-48b6-8a50-9abb69ee2d02"), "IUpdateDownloadContent"), + (uuid.UUID("59602eb6-57b0-4fd8-aa4b-ebf06971fe15"), "IWRMPolicy"), + (uuid.UUID("5a7b91f8-ff00-11d0-a9b2-00c04fb6e6fc"), "msgsvcsend"), + ( + uuid.UUID("5b5a68e6-8b9f-45e1-8199-a95ffccdffff"), + "IAppHostConstantValueCollection", + ), + (uuid.UUID("5b821720-f63b-11d0-aad2-00c04fc324db"), "dhcpsrv2"), + (uuid.UUID("5ca4a760-ebb1-11cf-8611-00a0245420ed"), "IcaApi"), + (uuid.UUID("5f6325d3-ce88-4733-84c1-2d6aefc5ea07"), "IFsrmFileScreen"), + (uuid.UUID("5ff9bdf6-bd91-4d8b-a614-d6317acc8dd8"), "IRemoteSstpCertCheck"), + (uuid.UUID("6099fc12-3eff-11d0-abd0-00c04fd91a4e"), "faxclient"), + (uuid.UUID("6139d8a4-e508-4ebb-bac7-d7f275145897"), "IRemoteIPV6Config"), + (uuid.UUID("615c4269-7a48-43bd-96b7-bf6ca27d6c3e"), "IWindowsDriverUpdate2"), + (uuid.UUID("64ff8ccc-b287-4dae-b08a-a72cbf45f453"), "IAppHostElement"), + (uuid.UUID("6619a740-8154-43be-a186-0319578e02db"), "IRemoteDispatch"), + (uuid.UUID("66a2db1b-d706-11d0-a37b-00c04fc9da04"), "IRemoteNetworkConfig"), + (uuid.UUID("66a2db20-d706-11d0-a37b-00c04fc9da04"), "IRemoteRouterRestart"), + (uuid.UUID("66a2db21-d706-11d0-a37b-00c04fc9da04"), "IRemoteSetDnsConfig"), + (uuid.UUID("66a2db22-d706-11d0-a37b-00c04fc9da04"), "IRemoteICFICSConfig"), + (uuid.UUID("673425bf-c082-4c7c-bdfd-569464b8e0ce"), "IAutomaticUpdates"), + (uuid.UUID("6788faf9-214e-4b85-ba59-266953616e09"), "IVdsVolumeMF3"), + (uuid.UUID("67e08fc2-2984-4b62-b92e-fc1aae64bbbb"), "IRemoteStringIdConfig"), + (uuid.UUID("6879caf9-6617-4484-8719-71c3d8645f94"), "IFsrmReportScheduler"), + (uuid.UUID("69ab7050-3059-11d1-8faf-00a024cb6019"), "INtmsObjectInfo1"), + (uuid.UUID("6a92b07a-d821-4682-b423-5c805022cc4d"), "IUpdate"), + (uuid.UUID("6b5bdd1e-528c-422c-af8c-a4079be4fe48"), "RemoteFW"), + (uuid.UUID("6bffd098-a112-3610-9833-012892020162"), "browser"), + (uuid.UUID("6bffd098-a112-3610-9833-46c3f874532d"), "dhcpsrv"), + (uuid.UUID("6bffd098-a112-3610-9833-46c3f87e345a"), "wkssvc"), + (uuid.UUID("6c935649-30a6-4211-8687-c4c83e5fe1c7"), "IContainerControl2"), + (uuid.UUID("6cd6408a-ae60-463b-9ef1-e117534d69dc"), "IFsrmAction"), + (uuid.UUID("6e6f6b40-977c-4069-bddd-ac710059f8c0"), "IVdsAdvancedDisk"), + (uuid.UUID("6f4dbfff-6920-4821-a6c3-b7e94c1fd60c"), "IFsrmPathMapper"), + (uuid.UUID("708cca10-9569-11d1-b2a5-0060977d8118"), "dscomm2"), + (uuid.UUID("70b51430-b6ca-11d0-b9b9-00a0c922e750"), "IMSAdminBaseW"), + (uuid.UUID("70cf5c82-8642-42bb-9dbc-0cfd263c6c4f"), "IWindowsDriverUpdate5"), + (uuid.UUID("72ae6713-dcbb-4a03-b36b-371f6ac6b53d"), "IVdsVolume2"), + (uuid.UUID("75c8f324-f715-4fe3-a28e-f9011b61a4a1"), "IVdsOpenVDisk"), + (uuid.UUID("76b3b17e-aed6-4da5-85f0-83587f81abe3"), "IUpdateService"), + (uuid.UUID("76d12b80-3467-11d3-91ff-0090272f9ea3"), "qmcomm2"), + (uuid.UUID("76f03f96-cdfd-44fc-a22c-64950a001209"), "IRemoteWinspool"), + (uuid.UUID("77df7a80-f298-11d0-8358-00a024c480a8"), "dscomm"), + (uuid.UUID("784b693d-95f3-420b-8126-365c098659f2"), "IOCSPAdminD"), + (uuid.UUID("7883ca1c-1112-4447-84c3-52fbeb38069d"), "IAppHostMethod"), + (uuid.UUID("7c44d7d4-31d5-424c-bd5e-2b3e1f323d22"), "dsaop"), + (uuid.UUID("7c4e1804-e342-483d-a43e-a850cfcc8d18"), "IIISApplicationAdmin"), + (uuid.UUID("7c857801-7381-11cf-884d-00aa004b2e24"), "IWbemObjectSink"), + (uuid.UUID("7c907864-346c-4aeb-8f3f-57da289f969f"), "IImageInformation"), + (uuid.UUID("7d07f313-a53f-459a-bb12-012c15b1846e"), "IRobustNtmsMediaServices1"), + (uuid.UUID("7f43b400-1a0e-4d57-bbc9-6b0c65f7a889"), "IAlternateLaunch"), + (uuid.UUID("7fb7ea43-2d76-4ea8-8cd9-3decc270295e"), "IEventClass3"), + (uuid.UUID("7fe0d935-dda6-443f-85d0-1cfb58fe41dd"), "ICertAdminD2"), + (uuid.UUID("811109bf-a4e1-11d1-ab54-00a0c91e9b45"), "winsi2"), + (uuid.UUID("8165b19e-8d3a-4d0b-80c8-97de310db583"), "IServicedComponentInfo"), + (uuid.UUID("816858a4-260d-4260-933a-2585f1abc76b"), "IUpdateSession"), + (uuid.UUID("81ddc1b8-9d35-47a6-b471-5b80f519223b"), "ICategory"), + (uuid.UUID("82273fdc-e32a-18c3-3f78-827929dc23ea"), "eventlog"), + (uuid.UUID("8276702f-2532-4839-89bf-4872609a2ea4"), "IFsrmActionEmail2"), + (uuid.UUID("8298d101-f992-43b7-8eca-5052d885b995"), "IMSAdminBase2W"), + (uuid.UUID("82ad4280-036b-11cf-972c-00aa006887b0"), "inetinfo"), + (uuid.UUID("8326cd1d-cf59-4936-b786-5efc08798e25"), "IVdsAdviseSink"), + ( + uuid.UUID("832a32f7-b3ea-4b8c-b260-9a2923001184"), + "IAppHostConfigLocationCollection", + ), + (uuid.UUID("833e4100-aff7-4ac3-aac2-9f24c1457bce"), "IPCHCollection"), + (uuid.UUID("833e41aa-aff7-4ac3-aac2-9f24c1457bce"), "ISAFSession"), + (uuid.UUID("83bfb87f-43fb-4903-baa6-127f01029eec"), "IVdsSubSystemImportTarget"), + (uuid.UUID("85713fa1-7796-4fa2-be3b-e2d6124dd373"), "IWindowsUpdateAgentInfo"), + (uuid.UUID("86d35949-83c9-4044-b424-db363231fd0c"), "ITaskSchedulerService"), + (uuid.UUID("879c8bbe-41b0-11d1-be11-00c04fb6bf70"), "IClientSink"), + (uuid.UUID("88143fd0-c28d-4b2b-8fef-8d882f6a9390"), "TermSrvEnumeration"), + (uuid.UUID("88306bb2-e71f-478c-86a2-79da200a0f11"), "IVdsVolume"), + (uuid.UUID("894de0c0-0d55-11d3-a322-00c04fa321a1"), "InitShutdown"), + (uuid.UUID("895a2c86-270d-489d-a6c0-dc2a9b35280e"), "INtmsObjectManagement2"), + (uuid.UUID("897e2e5f-93f3-4376-9c9c-fd2277495c27"), "FrsTransport"), + (uuid.UUID("8bb68c7d-19d8-4ffb-809e-be4fc1734014"), "IFsrmQuotaManager"), + ( + uuid.UUID("8bed2c68-a5fb-4b28-8581-a0dc5267419f"), + "IAppHostPropertySchemaCollection", + ), + (uuid.UUID("8db2180e-bd29-11d1-8b7e-00c04fd7a924"), "IRegister"), + (uuid.UUID("8dd04909-0e34-4d55-afaa-89e1f1a1bbb9"), "IFsrmFileGroup"), + (uuid.UUID("8f09f000-b7ed-11ce-bbd2-00001a181cad"), "dimsvc"), + (uuid.UUID("8f45abf1-f9ae-4b95-a933-f0f66e5056ea"), "IUpdateSearcher"), + (uuid.UUID("8f4b2f5d-ec15-4357-992f-473ef10975b9"), "IVdsDisk3"), + (uuid.UUID("8f6d760f-f0cb-4d69-b5f6-848b33e9bdc6"), "IAppHostConfigManager"), + (uuid.UUID("8fb6d884-2388-11d0-8c35-00c04fda2795"), "W32Time"), + (uuid.UUID("90681b1d-6a7f-48e8-9061-31b7aa125322"), "IVdsDiskOnline"), + (uuid.UUID("906b0ce0-c70b-1067-b317-00dd010662da"), "IXnRemote"), + (uuid.UUID("918efd1e-b5d8-4c90-8540-aeb9bdc56f9d"), "IUpdateSession3"), + (uuid.UUID("91ae6020-9e3c-11cf-8d7c-00aa00c091be"), "ICertPassage"), + (uuid.UUID("91caf7b0-eb23-49ed-9937-c52d817f46f7"), "IUpdateSession2"), + (uuid.UUID("943991a5-b3fe-41fa-9696-7f7b656ee34b"), "IWRMMachineGroup"), + (uuid.UUID("9556dc99-828c-11cf-a37e-00aa003240c7"), "IWbemServices"), + (uuid.UUID("96deb3b5-8b91-4a2a-9d93-80a35d8aa847"), "IFsrmCommittableCollection"), + (uuid.UUID("971668dc-c3fe-4ea1-9643-0c7230f494a1"), "IRegister2"), + (uuid.UUID("97199110-db2e-11d1-a251-0000f805ca53"), "ITransactionStream"), + (uuid.UUID("9723f420-9355-42de-ab66-e31bb15beeac"), "IVdsAdvancedDisk2"), + (uuid.UUID("98315903-7be5-11d2-adc1-00a02463d6e7"), "IReplicationUtil"), + (uuid.UUID("9882f547-cfc3-420b-9750-00dfbec50662"), "IVdsCreatePartitionEx"), + (uuid.UUID("99cc098f-a48a-4e9c-8e58-965c0afc19d5"), "IEventSystem2"), + (uuid.UUID("99fcfec4-5260-101b-bbcb-00aa0021347a"), "IObjectExporter"), + (uuid.UUID("9a2bf113-a329-44cc-809a-5c00fce8da40"), "IFsrmQuotaTemplateImported"), + (uuid.UUID("9aa58360-ce33-4f92-b658-ed24b14425b8"), "IVdsSwProvider"), + (uuid.UUID("9b0353aa-0e52-44ff-b8b0-1f7fa0437f88"), "IUpdateServiceCollection"), + (uuid.UUID("9be77978-73ed-4a9a-87fd-13f09fec1b13"), "IAppHostAdminManager"), + (uuid.UUID("9cbe50ca-f2d2-4bf4-ace1-96896b729625"), "IVdsDiskPartitionMF2"), + ( + uuid.UUID("9d07ca0d-8f02-4ed5-b727-acf37fea5bbc"), + "ISingleSignonRemoteMasterSecret", + ), + (uuid.UUID("a0e8f27a-888c-11d1-b763-00c04fb926af"), "IEventSystemInitialize"), + (uuid.UUID("a2efab31-295e-46bb-b976-e86d58b52e8b"), "IFsrmQuotaTemplate"), + (uuid.UUID("a359dec5-e813-4834-8a2a-ba7f1d777d76"), "IWbemBackupRestoreEx"), + (uuid.UUID("a35af600-9cf4-11cd-a076-08002b2bd711"), "type_scard_pack"), + (uuid.UUID("a376dd5e-09d4-427f-af7c-fed5b6e1c1d6"), "IUpdateException"), + (uuid.UUID("a4f1db00-ca47-1067-b31f-00dd010662da"), "emsmdb"), + ( + uuid.UUID("a7f04f3c-a290-435b-aadf-a116c3357a5c"), + "IUpdateHistoryEntryCollection", + ), + (uuid.UUID("a8927a41-d3ce-11d1-8472-006008b0e5ca"), "ICatalogTableInfo"), + (uuid.UUID("a8e0653c-2744-4389-a61d-7373df8b2292"), "FileServerVssAgent"), + (uuid.UUID("ad55f10b-5f11-4be7-94ef-d9ee2e470ded"), "IFsrmFileGroupImported"), + (uuid.UUID("ada4e6fb-e025-401e-a5d0-c3134a281f07"), "IAppHostConfigFile"), + (uuid.UUID("ae1c7110-2f60-11d3-8a39-00c04f72d8e3"), "IVssEnumObject"), + (uuid.UUID("afa8bd80-7d8a-11c9-bef4-08002b102989"), "mgmt"), + (uuid.UUID("afc052c2-5315-45ab-841b-c6db0e120148"), "IFsrmClassificationRule"), + (uuid.UUID("afc07e2e-311c-4435-808c-c483ffeec7c9"), "lsacap"), + (uuid.UUID("b057dc50-3059-11d1-8faf-00a024cb6019"), "INtmsObjectManagement1"), + (uuid.UUID("b07fedd4-1682-4440-9189-a39b55194dc5"), "IVdsIscsiInitiatorAdapter"), + (uuid.UUID("b196b284-bab4-101a-b69c-00aa00341d07"), "IConnectionPointContainer"), + (uuid.UUID("b196b285-bab4-101a-b69c-00aa00341d07"), "IEnumConnectionPoints"), + (uuid.UUID("b196b286-bab4-101a-b69c-00aa00341d07"), "IConnectionPoint"), + (uuid.UUID("b196b287-bab4-101a-b69c-00aa00341d07"), "IEnumConnections"), + (uuid.UUID("b383cd1a-5ce9-4504-9f63-764b1236f191"), "IWindowsDriverUpdate"), + (uuid.UUID("b481498c-8354-45f9-84a0-0bdd2832a91f"), "IVdsVdProvider"), + (uuid.UUID("b60040e0-bcf3-11d1-861d-0080c729264d"), "IGetTrackingData"), + (uuid.UUID("b6b22da8-f903-4be7-b492-c09d875ac9da"), "IVdsServiceUninstallDisk"), + ( + uuid.UUID("b7d381ee-8860-47a1-8af4-1f33b2b1f325"), + "IAppHostSectionDefinitionCollection", + ), + (uuid.UUID("b80f3c42-60e0-4ae0-9007-f52852d3dbed"), "IAppHostMethodInstance"), + (uuid.UUID("b9785960-524f-11df-8b6d-83dcded72085"), "ISDKey"), + (uuid.UUID("b97db8b2-4c63-11cf-bff6-08002be23f2f"), "clusapi"), + (uuid.UUID("b97db8b2-4c63-11cf-bff6-08002be23f2f"), "clusapi"), + ( + uuid.UUID("bb36ea26-6318-4b8c-8592-f72dd602e7a5"), + "IFsrmClassifierModuleDefinition", + ), + (uuid.UUID("bb39332c-bfee-4380-ad8a-badc8aff5bb6"), "INtmsNotifySink"), + (uuid.UUID("bba9cb76-eb0c-462c-aa1b-5d8c34415701"), "Claims"), + ( + uuid.UUID("bc5513c8-b3b8-4bf7-a4d4-361c0d8c88ba"), + "IUpdateDownloadContentCollection", + ), + (uuid.UUID("bc681469-9dd9-4bf4-9b3d-709f69efe431"), "IWRMResourceGroup"), + (uuid.UUID("bd0c73bc-805b-4043-9c30-9a28d64dd7d2"), "IIISCertObj"), + (uuid.UUID("bd7c23c2-c805-457c-8f86-d17fe6b9d19f"), "IClusterLogEx"), + (uuid.UUID("bde95fdf-eee0-45de-9e12-e5a61cd0d4fe"), "RCMPublic"), + (uuid.UUID("be56a644-af0e-4e0e-a311-c1d8e695cbff"), "IUpdateHistoryEntry"), + (uuid.UUID("bee7ce02-df77-4515-9389-78f01c5afc1a"), "IFsrmFileScreenException"), + (uuid.UUID("c1c2f21a-d2f4-4902-b5c6-8a081c19a890"), "IUpdate5"), + (uuid.UUID("c2be6970-df9e-11d1-8b87-00c04fd7a924"), "IImport"), + (uuid.UUID("c2bfb780-4539-4132-ab8c-0a8772013ab6"), "IUpdateHistoryEntry2"), + (uuid.UUID("c3fcc19e-a970-11d2-8b5a-00a0c9b7c9c4"), "IManagedObject"), + (uuid.UUID("c49e32c7-bc8b-11d2-85d4-00105a1f8304"), "IWbemBackupRestore"), + (uuid.UUID("c4b0c7d9-abe0-4733-a1e1-9fdedf260c7a"), "IADProxy2"), + (uuid.UUID("c5c04795-321c-4014-8fd6-d44658799393"), "IAppHostSectionDefinition"), + (uuid.UUID("c5cebee2-9df5-4cdd-a08c-c2471bc144b4"), "IResourceManager"), + (uuid.UUID("c681d488-d850-11d0-8c52-00c04fd90f7e"), "efsrpc"), + (uuid.UUID("c726744e-5735-4f08-8286-c510ee638fb6"), "ICatalogUtils2"), + (uuid.UUID("c8550bff-5281-4b1e-ac34-99b6fa38464d"), "IAppHostElementCollection"), + (uuid.UUID("c97ad11b-f257-420b-9d9f-377f733f6f68"), "IUpdateDownloadContent2"), + (uuid.UUID("cb0df960-16f5-4495-9079-3f9360d831df"), "IFsrmRule"), + (uuid.UUID("ccd8c074-d0e5-4a40-92b4-d074faa6ba28"), "Witness"), + (uuid.UUID("cfadac84-e12c-11d1-b34c-00c04f990d54"), "IExport"), + ( + uuid.UUID("cfe36cba-1949-4e74-a14f-f1d580ceaf13"), + "IFsrmFileScreenTemplateManager", + ), + (uuid.UUID("d02e4be0-3419-11d1-8fb1-00a024cb6019"), "INtmsMediaServices1"), + (uuid.UUID("d049b186-814f-11d1-9a3c-00c04fc9b232"), "NtFrsApi"), + (uuid.UUID("d2d79df5-3400-11d0-b40b-00aa005ff586"), "IVolumeClient"), + (uuid.UUID("d2d79df7-3400-11d0-b40b-00aa005ff586"), "IDMNotify"), + (uuid.UUID("d2dc89da-ee91-48a0-85d8-cc72a56f7d04"), "IFsrmClassificationManager"), + (uuid.UUID("d40cff62-e08c-4498-941a-01e25f0fd33c"), "ISearchResult"), + (uuid.UUID("d4781cd6-e5d3-44df-ad94-930efe48a887"), "IWbemLoginClientID"), + (uuid.UUID("d5d23b6d-5a55-4492-9889-397a3c2d2dbc"), "IVdsAsync"), + (uuid.UUID("d646567d-26ae-4caa-9f84-4e0aad207fca"), "IFsrmActionEmail"), + (uuid.UUID("d68168c9-82a2-4f85-b6e9-74707c49a58f"), "IVdsVolumeShrink"), + (uuid.UUID("d6c7cd8f-bb8d-4f96-b591-d3a5f1320269"), "IAppHostMethodCollection"), + (uuid.UUID("d8cc81d9-46b8-4fa4-bfa5-4aa9dec9b638"), "IFsrmReport"), + (uuid.UUID("d95afe70-a6d5-4259-822e-2c84da1ddb0d"), "WindowsShutdown"), + (uuid.UUID("d99bdaae-b13a-4178-9fdb-e27f16b4603e"), "IVdsHwProvider"), + (uuid.UUID("d99e6e70-fc88-11d0-b498-00a0c90312f3"), "ICertRequestD"), + (uuid.UUID("d99e6e71-fc88-11d0-b498-00a0c90312f3"), "ICertAdminD"), + (uuid.UUID("d9a59339-e245-4dbd-9686-4d5763e39624"), "IInstallationBehavior"), + (uuid.UUID("da5a86c5-12c2-4943-ab30-7f74a813d853"), "PerflibV2"), + (uuid.UUID("db90832f-6910-4d46-9f5e-9fd6bfa73903"), "INtmsLibraryControl2"), + (uuid.UUID("dc12a681-737f-11cf-884d-00aa004b2e24"), "IWbemClassObject"), + (uuid.UUID("dde02280-12b3-4e0b-937b-6747f6acb286"), "IUpdateServiceRegistration"), + (uuid.UUID("de095db1-5368-4d11-81f6-efef619b7bcf"), "IAppHostCollectionSchema"), + (uuid.UUID("deb01010-3a37-4d26-99df-e2bb6ae3ac61"), "IVolumeClient4"), + (uuid.UUID("e0393303-90d4-4a97-ab71-e9b671ee2729"), "IVdsServiceLoader"), + ( + uuid.UUID("e1010359-3e5d-4ecd-9fe4-ef48622fdf30"), + "IFsrmFileScreenTemplateImported", + ), + (uuid.UUID("e1af8308-5d1f-11c9-91a4-08002b14a0fa"), "ept"), + (uuid.UUID("e33c0cc4-0482-101a-bc0c-02608c6ba218"), "LocToLoc"), + (uuid.UUID("e3514235-4b06-11d1-ab04-00c04fc2dcd2"), "drsuapi"), + (uuid.UUID("e3d0d746-d2af-40fd-8a7a-0d7078bb7092"), "BitsPeerAuth"), + (uuid.UUID("e65e8028-83e8-491b-9af7-aaf6bd51a0ce"), "IServerHealthReport"), + (uuid.UUID("e7927575-5cc3-403b-822e-328a6b904bee"), "IAppHostPathMapper"), + (uuid.UUID("e7a4d634-7942-4dd9-a111-82228ba33901"), "IAutomaticUpdatesResults"), + (uuid.UUID("e8fb8620-588f-11d2-9d61-00c04f79c5fe"), "IIisServiceControl"), + (uuid.UUID("e946d148-bd67-4178-8e22-1c44925ed710"), "IFsrmPropertyDefinitionValue"), + (uuid.UUID("ea0a3165-4834-11d2-a6f8-00c04fa346cc"), "fax"), + (uuid.UUID("eafe4895-a929-41ea-b14d-613e23f62b71"), "IAppHostPropertyException"), + (uuid.UUID("ed35f7a1-5024-4e7b-a44d-07ddaf4b524d"), "IAppHostProperty"), + (uuid.UUID("ed8bfe40-a60b-42ea-9652-817dfcfa23ec"), "IWindowsDriverUpdateEntry"), + (uuid.UUID("ede0150f-e9a3-419c-877c-01fe5d24c5d3"), "IFsrmPropertyDefinition"), + (uuid.UUID("ee2d5ded-6236-4169-931d-b9778ce03dc6"), "IVdsVolumeMF"), + ( + uuid.UUID("ee321ecb-d95e-48e9-907c-c7685a013235"), + "IFsrmFileManagementJobManager", + ), + (uuid.UUID("ef13d885-642c-4709-99ec-b89561c6bc69"), "IAppHostElementSchema"), + (uuid.UUID("eff90582-2ddc-480f-a06d-60f3fbc362c3"), "IStringCollection"), + (uuid.UUID("f131ea3e-b7be-480e-a60d-51cb2785779e"), "IExport2"), + (uuid.UUID("f1e9c5b2-f59b-11d2-b362-00105a1f8177"), "IWbemRemoteRefresher"), + (uuid.UUID("f309ad18-d86a-11d0-a075-00c04fb68820"), "IWbemLevel1Login"), + (uuid.UUID("f31931a9-832d-481c-9503-887a0e6a79f0"), "IWRMProtocol"), + (uuid.UUID("f3637e80-5b22-4a2b-a637-bbb642b41cfc"), "IFsrmFileScreenBase"), + (uuid.UUID("f411d4fd-14be-4260-8c40-03b7c95e608a"), "IFsrmSetting"), + (uuid.UUID("f4a07d63-2e25-11d1-9964-00c04fbbb345"), "IEnumEventObject"), + (uuid.UUID("f5cc59b4-4264-101a-8c59-08002b2f8426"), "frsrpc"), + (uuid.UUID("f5cc5a18-4264-101a-8c59-08002b2f8426"), "nspi"), + (uuid.UUID("f612954d-3b0b-4c56-9563-227b7be624b4"), "IMSAdminBase3W"), + (uuid.UUID("f6beaff7-1e19-4fbb-9f8f-b89e2018337c"), "IEventService"), + (uuid.UUID("f76fbf3b-8ddd-4b42-b05a-cb1c3ff1fee8"), "IFsrmCollection"), + (uuid.UUID("f82e5729-6aba-4740-bfc7-c7f58f75fb7b"), "IFsrmAutoApplyQuota"), + (uuid.UUID("f89ac270-d4eb-11d1-b682-00805fc79216"), "IEventObjectCollection"), + (uuid.UUID("fa7660f6-7b3f-4237-a8bf-ed0ad0dcbbd9"), "IAppHostWritableAdminManager"), + (uuid.UUID("fa7df749-66e7-4986-a27f-e2f04ae53772"), "IVssSnapshotMgmt"), + (uuid.UUID("fb2b72a0-7a68-11d1-88f9-0080c7d771bf"), "IEventClass"), + (uuid.UUID("fb2b72a1-7a68-11d1-88f9-0080c7d771bf"), "IEventClass2"), + (uuid.UUID("fbc1d17d-c498-43a0-81af-423ddd530af6"), "IEventSubscription3"), + (uuid.UUID("fc5d23e8-a88b-41a5-8de0-2d2f73c5a630"), "IVdsServiceSAN"), + (uuid.UUID("fc910418-55ca-45ef-b264-83d4ce7d30e0"), "IWRMRemoteSessionMgmt"), + (uuid.UUID("fdb3a030-065f-11d1-bb9b-00a024ea5525"), "qmcomm"), + (uuid.UUID("ff4fa04e-5a94-4bda-a3a0-d5b4d3c52eba"), "IFsrmFileScreenManager"), +] + +for uid, name in _DCE_RPC_WELL_KNOWN_UUIDS: + DCE_RPC_INTERFACES_NAMES[uid] = name + DCE_RPC_INTERFACES_NAMES_rev[name.lower()] = uid diff --git a/scapy/layers/msrpce/ept.py b/scapy/layers/msrpce/ept.py new file mode 100644 index 00000000000..1f51993f311 --- /dev/null +++ b/scapy/layers/msrpce/ept.py @@ -0,0 +1,193 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +EPT map (EndPoinT mapper) +""" + +import uuid + +from scapy.config import conf +from scapy.fields import ( + ByteEnumField, + ConditionalField, + FieldLenField, + IPField, + LEShortField, + MultipleTypeField, + PacketListField, + ShortField, + StrLenField, + UUIDEnumField, +) +from scapy.packet import Packet +from scapy.layers.dcerpc import ( + DCE_RPC_INTERFACES_NAMES_rev, + DCE_RPC_INTERFACES_NAMES, + DCE_RPC_PROTOCOL_IDENTIFIERS, + DCE_RPC_TRANSFER_SYNTAXES, +) + +from scapy.layers.msrpce.raw.ept import * # noqa: F401, F403 + + +# [C706] Appendix L + +# "For historical reasons, this cannot be done using the standard +# NDR encoding rules for marshalling and unmarshalling. +# A special encoding is required." - Appendix L + + +class octet_string_t(Packet): + fields_desc = [ + FieldLenField("count", None, fmt=" + NDRShortField("cRequestedProtseqs", None, size_of="pRequestedProtseqs"), + NDRFullEmbPointerField( + NDRConfFieldListField( + "pRequestedProtseqs", + [], + NDRShortField("", 0), + size_is=lambda pkt: pkt.cRequestedProtseqs, + ), + ), + ] + + +class ScmRequestInfoData(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRIntField("pdwReserved", 0)), + NDRFullEmbPointerField( + NDRPacketField( + "remoteRequest", + customREMOTE_REQUEST_SCM_INFO(), + customREMOTE_REQUEST_SCM_INFO, + ), + ), + ] + + +# [MS-DCOM] 2.2.22.2.5 + + +class ActivationContextInfoData(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRSignedIntField("clientOK", 0), + NDRSignedIntField("bReserved1", 0), + NDRIntField("dwReserved1", 0), + NDRIntField("dwReserved2", 0), + NDRFullEmbPointerField( + NDRPacketField("pIFDClientCtx", MInterfacePointer(), MInterfacePointer), + ), + NDRFullEmbPointerField( + NDRPacketField("pIFDPrototypeCtx", MInterfacePointer(), MInterfacePointer), + ), + ] + + +# [MS-DCOM] 2.2.22.2.6 + + +class LocationInfoData(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField( + NDRConfVarStrNullFieldUtf16("machineName", None), + ), + NDRIntField("processId", 0), + NDRIntField("apartmentId", 0), + NDRIntField("contextId", 0), + ] + + +# [MS-DCOM] 2.2.22.2.7 + + +class COSERVERINFO(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("dwReserved1", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("pwszName", "")), + NDRFullEmbPointerField(NDRIntField("pdwReserved", 0)), + NDRIntField("dwReserved2", 0), + ] + + +class SecurityInfoData(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("dwAuthnFlags", 0), + NDRFullEmbPointerField( + NDRPacketField("pServerInfo", COSERVERINFO(), COSERVERINFO), + ), + NDRFullPointerField(NDRIntField("pdwReserved", None)), + ] + + +class customREMOTE_REPLY_SCM_INFO(NDRPacket): + ALIGNMENT = (8, 8) + fields_desc = [ + NDRLongField("Oxid", 0), + NDRFullEmbPointerField( + NDRPacketField("pdsaOxidBindings", DUALSTRINGARRAY(), DUALSTRINGARRAY), + ), + NDRPacketField("ipidRemUnknown", GUID(), GUID), + NDRIntField("authnHint", 0), + NDRPacketField("serverVersion", COMVERSION(), COMVERSION), + ] + + +class ScmReplyInfoData(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRIntField("pdwReserved", 0)), + NDRFullEmbPointerField( + NDRPacketField( + "remoteReply", + customREMOTE_REPLY_SCM_INFO(), + customREMOTE_REPLY_SCM_INFO, + ), + ), + ] + + +# [MS-DCOM] 2.2.22.2.9 + + +class PropsOutInfo(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("cIfs", None, size_of="ppIntfData"), + NDRFullEmbPointerField( + NDRConfPacketListField("piid", [], GUID, size_is=lambda pkt: pkt.cIfs) + ), + NDRFullEmbPointerField( + NDRConfFieldListField( + "phresults", + [], + NDRSignedIntField("phresults", 0), + size_is=lambda pkt: pkt.cIfs, + ) + ), + NDRFullEmbPointerField( + NDRConfPacketListField( + "ppIntfData", + [], + MInterfacePointer, + size_is=lambda pkt: pkt.cIfs, + ptr_lvl=1, + ) + ), + ] + + +# [MS-DCOM] 2.2.22.1 + + +class CustomHeader(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("totalSize", 0), + NDRIntField("headerSize", 0), + NDRIntField("dwReserved", 0), + NDRIntEnumField("destCtx", 2, {2: "MSHCTX_DIFFERENTMACHINE"}), + NDRIntField("cIfs", None, size_of="pSizes"), + NDRPacketField("classInfoClsid", GUID(), GUID), + NDRFullEmbPointerField( + NDRConfPacketListField( + "pclsid", [GUID()], GUID, count_from=lambda pkt: pkt.cIfs + ), + ), + NDRFullEmbPointerField( + NDRConfFieldListField( + "pSizes", None, NDRIntField("", 0), count_from=lambda pkt: pkt.cIfs + ), + ), + NDRFullEmbPointerField(NDRIntField("pdwReserved", None)), + ] + + +class _ActivationPropertiesField(NDRSerializeType1PacketListField): + def __init__(self, *args, **kwargs): + kwargs["next_cls_cb"] = self._get_cls_activation + super(_ActivationPropertiesField, self).__init__(*args, **kwargs) + + def _get_cls_activation(self, pkt, lst, cur, remain): + # Get all the pcslsid + pclsid = pkt.CustomHeader[CustomHeader].valueof("pclsid") + ndrendian = pkt.CustomHeader[CustomHeader].ndrendian + i = len(lst) + int(bool(cur)) + if i >= len(pclsid): + return + # Get the next pclsid we need to process + next_uid = _uid_from_bytes(bytes(pclsid[i]), ndrendian=ndrendian) + # [MS-DCOM] 1.9 + cls = { + CLSID_ActivationContextInfo: ActivationContextInfoData, + CLSID_InstanceInfo: InstanceInfoData, + CLSID_InstantiationInfo: InstantiationInfoData, + CLSID_PropsOutInfo: PropsOutInfo, + CLSID_ScmReplyInfo: ScmReplyInfoData, + CLSID_ScmRequestInfo: ScmRequestInfoData, + CLSID_SecurityInfo: SecurityInfoData, + CLSID_ServerLocationInfo: LocationInfoData, + CLSID_SpecialSystemProperties: SpecialPropertiesData, + }[next_uid] + return lambda x: ndr_deserialize1(x, cls) + + +class ActivationPropertiesBlob(Packet): + fields_desc = [ + FieldLenField( + "dwSize", + None, + fmt=" Tuple[List[STRINGBINDING], List[SECURITYBINDING]]: + """ + Process aStringArray in a DUALSTRINGARRAY to extract string bindings and + security bindings. + """ + str_fld = PacketListField("", [], STRINGBINDING) + sec_fld = PacketListField("", [], SECURITYBINDING) + string = str_fld.getfield(dual, dual.aStringArray[: dual.wSecurityOffset * 2])[1] + secs = sec_fld.getfield(dual, dual.aStringArray[dual.wSecurityOffset * 2 :])[1] + if string[-1].wTowerId != 0 or secs[-1].wAuthnSvc != 0: + raise ValueError("Invalid DUALSTRINGARRAY !") + return string[:-1], secs[:-1] + + +def _HashStringBinding(strings: List[STRINGBINDING]): + """ + Hash a STRINGBINDING list + """ + return hashlib.sha256(b"".join(bytes(x) for x in strings)).digest() + + +# Entries. + + +class IPID_Entry: + """ + An entry in the IPID table + [MS-DCOM] 3.1.1.1 Abstract Data Model + """ + + def __init__(self): + self.ipid: Optional[uuid.UUID] = None + self.iid: Optional[uuid.UUID] = None + self.oid: Optional[int] = None + self.oxid: Optional[int] = None + self.cPublicRefs: int = 0 + self.cPrivateRefs: int = 0 + self.state: Any = None + # Additions + self.iface: Optional[ComInterface] = None + + +class OID_Entry: + """ + An entry in the OID table + [MS-DCOM] 3.1.1.1 Abstract Data Model + """ + + def __init__(self): + self.oid: Optional[int] = None + self.oxid: Optional[int] = None + self.ipids: List[uuid.UUID] = [] + self.hash: Optional[bytes] = None + self.last_orpc: int = None + self.garbage_collection: bool = True + self.state = None + + +class Resolver_Entry: + """ + An entry in the Resolver table. + [MS-DCOM] 3.2.1 Abstract Data Model + """ + + def __init__(self): + self.hash: Optional[bytes] = None + self.binds: List[STRINGBINDING] = [] + self.secs: List[SECURITYBINDING] = [] + self.setid: Optional[int] = None + self.client: Optional[DCERPC_Client] = None + + +class SETID_Entry: + """ + An entry in the SETID table. + [MS-DCOM] 3.2.1 Abstract Data Model + """ + + def __init__(self): + self.setid: Optional[int] = None + self.oids: List[int] = [] + self.seq: Optional[int] = None + + +class OXID_Entry: + """ + An entry in the OXID table. + [MS-DCOM] 3.2.1 Abstract Data Model + """ + + def __init__(self): + self.oxid: Optional[int] = None + self.bindingInfo: Optional[Tuple[str, int]] = None + self.target_name: str = None + self.authnHint: DCE_C_AUTHN_LEVEL = DCE_C_AUTHN_LEVEL.CONNECT + self.version: Optional[COMVERSION] = None + self.ipid_IRemUnknown: Optional[uuid.UUID] = None + + def __repr__(self): + return f"" + + +class ObjectInstance: + """ + An reference to an instantiated object. + + This is a helper to manipulate this object and perform calls over it. + """ + + def __init__(self, client: "DCOM_Client", oid: int): + self.client = client + self.oid = oid + + def __repr__(self): + return f"" + + @property + def valid(self): + """ + Returns whether the current object still exists + """ + return self.oid in self.client.OID_table + + @property + def ndr64(self): + """ + Whether NDR64 is required to talk to this object + """ + return self.client.ndr64 + + def sr1_req( + self, + pkt: NDRPacket, + iface: ComInterface, + ssp=None, + auth_level=None, + impersonation_type=None, + timeout=None, + **kwargs, + ): + """ + Make an ORPC call on this object instance. + + :param iface: the ComInterface to call. + :param pkt: the request to make. + + :param ssp: (optional) non default SSP to use to connect to the object exporter + :param auth_level: (optional) non default authn level to use + :param impersonation_type: (optional) non default impersonation type to use + :param timeout: (optional) timeout for the connection + """ + # Look for this object's entry + try: + oid_entry = self.client.OID_table[self.oid] + except KeyError: + raise ValueError("This object has been released.") + + # Look for the ipid matching the interface required by the user + ipid = None + for ipid in oid_entry.ipids: + ipid_entry = self.client.IPID_table[ipid] + if ipid_entry.iid == iface.uuid: + break + else: + # Acquire interface on the object + self.client.AcquireInterface( + ipid=oid_entry.ipids[0], + iids=[ + iface, + ], + cPublicRefs=1, + ) + + return self.client.sr1_orpc_req( + ipid=ipid, + pkt=pkt, + ssp=ssp, + auth_level=auth_level, + impersonation_type=impersonation_type, + timeout=timeout, + **kwargs, + ) + + def release(self): + """ + Call IRemUnknown2::RemRelease to release counts on an object reference. + """ + for ipid in self.client.OID_table[self.oid].ipids: + self.client.RemRelease(ipid) + + +class DCOM_Client(DCERPC_Client): + """ + A wrapper of DCERPC_Client that adds functions to use COM interfaces. + + :param cid: the client identifier + """ + + IREMUNKNOWN = find_com_interface("IRemUnknown2") + + def __init__(self, cid: GUID = None, verb=True, **kwargs): + # Pick a random cid to identify this client + self.cid = cid or GUID(RandUUID().bytes_le) + + # The OXID table kept up-to-date by the client + self.OXID_table: Dict[int, OXID_Entry] = {} + + # The IPID table kept up-to-date by the client + self.IPID_table: Dict[int, IPID_Entry] = {} + + # The OID table kept up-to-date by the client + self.OID_table: Dict[int, OID_Entry] = {} + + # The Resolver table kept up-to-date by the client + self.Resolver_table: Dict[STRINGBINDING, Resolver_Entry] = {} + + # DCOM defaults to at least PKT_INTEGRITY + if "auth_level" not in kwargs and "ssp" in kwargs: + kwargs["auth_level"] = DCE_C_AUTHN_LEVEL.PKT_INTEGRITY + + # DCOM_Client handles the activations. + # [MS-RPCE] sect 3.2.4.1.1.2 : "it MUST specify a default impersonation + # level of at leastRPC_C_IMPL_LEVEL_IMPERSONATE" + if "impersonation_type" not in kwargs and "ssp" in kwargs: + kwargs["impersonation_type"] = RPC_C_IMP_LEVEL.IMPERSONATE + + super(DCOM_Client, self).__init__( + DCERPC_Transport.NCACN_IP_TCP, + ndr64=False, + verb=verb, + **kwargs, + ) + + def connect(self, host: str, timeout=5): + """ + Initiate a connection to the object resolver. + + :param host: the host to connect to + :param timeout: (optional) the connection timeout (default 5) + """ + # [MS-DCOM] 3.2.4.1.2.1 Determining RPC Binding Information + binds, _ = ServerAlive2(host) + host, port = self._ChoseRPCBinding(binds) + + super(DCOM_Client, self).connect( + host=host, + port=port, + timeout=timeout, + ) + + def sr1_req(self, pkt, **kwargs): + raise NotImplementedError("Cannot use sr1_req on DCOM_Client !") + + def _GetObjectInstance(self, oid: int): + """ + Internal function to get an ObjectInstance from an oid + """ + return ObjectInstance( + client=self, + oid=oid, + ) + + def _RemoteCreateInstanceOrGetClassObject( + self, + clsreq, + clsresp, + clsid: uuid.UUID, + iids: List[ComInterface], + ) -> ObjectInstance: + """ + Internal function common to RemoteCreateInstance and RemoteGetClassObject + """ + if not iids: + raise ValueError("Must specify at least one interface !") + + # Bind IObjectExporter if not already + self.bind_or_alter( + find_dcerpc_interface("IRemoteSCMActivator"), + target_name="rpcss/" + self.host, + ) + + # [MS-DCOM] sect 3.1.2.5.2.3.3 - Issuing the Activation Request + + # Build the activation properties + ActivationProperties = [ + SpecialPropertiesData( + # Same as windows + dwDefaultAuthnLvl=self.auth_level, + dwOrigClsctx=16, + dwFlags=2, # ??? + ndr64=False, + ), + InstantiationInfoData( + classId=GUID(_uid_to_bytes(clsid)), + classCtx=16, + actvflags=0, + fIsSurrogate=0, + clientCOMVersion=COMVERSION( + MajorVersion=5, + MinorVersion=7, + ), + pIID=[GUID(_uid_to_bytes(x.uuid)) for x in iids], + ndr64=False, + ), + ActivationContextInfoData( + pIFDClientCtx=MInterfacePointer( + abData=OBJREF(iid=IID_IContext) + / OBJREF_CUSTOM( + clsid=CLSID_ContextMarshaler, + pObjectData=Context( + ContextId=uuid.UUID("53394e9f-e973-4bf0-a341-154519534fe1"), + Flags="CTXMSHLFLAGS_BYVAL", + ), + ), + ), + ndr64=False, + ), + SecurityInfoData( + pServerInfo=COSERVERINFO( + pwszName=self.host, + ), + ndr64=False, + ), + LocationInfoData(ndr64=False), + ScmRequestInfoData( + remoteRequest=customREMOTE_REQUEST_SCM_INFO( + pRequestedProtseqs=[ + # Note <51> for Windows Vista and later + int(DCERPC_Transport.NCACN_IP_TCP), + ] + ), + ndr64=False, + ), + ] + + # Build CustomHeader + hdr = CustomHeader( + pclsid=[ + GUID(_uid_to_bytes(CLSID_SpecialSystemProperties)), + GUID(_uid_to_bytes(CLSID_InstantiationInfo)), + GUID(_uid_to_bytes(CLSID_ActivationContextInfo)), + GUID(_uid_to_bytes(CLSID_SecurityInfo)), + GUID(_uid_to_bytes(CLSID_ServerLocationInfo)), + GUID(_uid_to_bytes(CLSID_ScmRequestInfo)), + ], + pSizes=[ + # Account for the size of the Type1 header + padding + len(x) + 16 + (-len(x) % 8) + for x in ActivationProperties + ], + ndr64=False, + ) + hdr.headerSize = len(hdr) + 16 # 16: size of the Type1 serialization header + hdr.totalSize = hdr.headerSize + sum(hdr.valueof("pSizes")) + + # Build final request + pkt = clsreq( + orpcthis=ORPCTHIS( + version=COMVERSION( + MajorVersion=5, + MinorVersion=7, + ), + flags=tagCPFLAGS.CPFLAG_PROPAGATE, + cid=self.cid, + ), + pActProperties=MInterfacePointer( + abData=OBJREF(iid=IID_IActivationPropertiesIn) + / OBJREF_CUSTOM( + clsid=CLSID_ActivationPropertiesIn, + pObjectData=ActivationPropertiesBlob( + CustomHeader=hdr, + Property=ActivationProperties, + ), + ), + ), + ndr64=False, + ) + + if isinstance(pkt, RemoteCreateInstance_Request): + pkt.pUnkOuter = None + + # Send and receive + resp = super(DCOM_Client, self).sr1_req(pkt) + if not resp or resp.status != 0: + raise ValueError("%s failed." % clsreq.__name__) + + entry = OXID_Entry() + objrefs = [] + + # [MS-DCOM] sect 3.2.4.1.1.3 - Updating the Client OXID Table after Activation + abData = OBJREF(resp.valueof("ppActProperties").abData) + for prop in abData.pObjectData.Property: + if ScmReplyInfoData in prop: + # Information about the object exporter the server found for us + remoteReply = prop[ScmReplyInfoData].valueof("remoteReply") + + # Get OXID, IPID, COMVERSION, authentication level hint + entry.oxid = remoteReply.Oxid + entry.version = remoteReply.serverVersion + entry.authnHint = DCE_C_AUTHN_LEVEL(remoteReply.authnHint) + entry.ipid_IRemUnknown = _uid_from_bytes( + bytes(remoteReply.ipidRemUnknown), ndrendian=remoteReply.ndrendian + ) + + # Set RPC bindings from the activation request + binds, secs = _ParseStringArray(remoteReply.valueof("pdsaOxidBindings")) + entry.bindingInfo = self._ChoseRPCBinding(binds) + entry.target_name = self._CalculateTargetName(secs) + + if PropsOutInfo in prop: + # Information about the interfaces that the client requested + info = prop[PropsOutInfo] + + # Check that all interfaces were obtained + phresults = info.valueof("phresults") + if any(x > 0 for x in phresults): + raise ValueError( + "Interfaces %s were not obtained !" + % [iids[i] for i, x in enumerate(phresults) if x > 0] + ) + + # Now store the object references for each interface + for i, ptr in enumerate(info.valueof("ppIntfData")): + if phresults[i] == 0: + objrefs.append(OBJREF(ptr.abData)) + else: + objrefs.append(None) + + # Update the OXID table + if entry.oxid not in self.OXID_table: + self.OXID_table[entry.oxid] = entry + + # Get oid + oid = objrefs[0].std.oid + + # Add an entry to the IPID table for the RemUnknown + if entry.ipid_IRemUnknown not in self.IPID_table: + ipid_entry = IPID_Entry() + ipid_entry.iface = self.IREMUNKNOWN + ipid_entry.iid = self.IREMUNKNOWN.uuid + ipid_entry.oxid = entry.oxid + ipid_entry.oid = oid + self.IPID_table[entry.ipid_IRemUnknown] = ipid_entry + + # "For each object reference returned from the activation request for + # which the corresponding status code indicates success, the client MUST + # unmarshal the object reference" + for i, obj in enumerate(objrefs): + if obj is None: + continue + # Unmarshall + self._UnmarshallObjref(obj, iid=iids[i]) + + return self._GetObjectInstance(oid=oid) + + def _UnmarshallObjref( + self, + obj: OBJREF, + iid: Optional[ComInterface] = None, + ) -> int: + """ + [MS-DCOM] sect 3.2.4.1.2 - Unmarshaling an Object Reference + + :param iid: "IID specified by the application when unmarshalling the object + reference" (see [MS-DCOM] sect 4.5) + """ + # "If the OBJREF_STANDARD flag is set" + if OBJREF_STANDARD in obj and iid: + # "the client MUST look up the OXID entry in the OXID + # table using the OXID from the STDOBJREF" + try: + ox = self.OXID_table[obj.std.oxid] + except KeyError: + # "If the table entry is not found" + + # "determine the RPC binding information to be used" + binds, _ = _ParseStringArray(obj.saResAddr) + host, port = self._ChoseRPCBinding(binds) + + # "issue OXID resolution" + ox = self.ResolveOxid2(oxid=obj.std.oxid, host=host, port=port) + + # "Next, the client MUST update its tables" + self._UpdateTables(iid, ox, obj, obj.std) + + # "Finally, the client MUST compare the IID in the OBJREF with the + # IID specified by the application" + if obj.iid != iid.uuid: + # "First, the client SHOULD acquire an object reference of the IID + # specified by the application" + self.AcquireInterface( + ipid=obj.std.ipid, + iids=[ + iid, + ], + cPublicRefs=1, + ) + + # "Next, the client MUST release the object reference unmarshaled + # from the OBJREF" + self.RemRelease(obj.std.ipid) + + return obj.std.oid + else: + obj.show() + raise NotImplementedError("Non OBJREF_STANDARD ! Please report.") + + def _UpdateTables( + self, + iface: ComInterface, + ox: OXID_Entry, + obj: OBJREF, + std: STDOBJREF, + ) -> None: + """ + [MS-DCOM] 3.2.4.1.2.3 Updating Client Tables After Unmarshaling + """ + # [MS-DCOM] 3.2.4.1.2.3.1 Updating the OXID + if std.oxid not in self.OXID_table: + self.OXID_table[std.oxid] = ox + + # [MS-DCOM] 3.2.4.1.2.3.2 Updating the OID/IPID/Resolver + if std.ipid in self.IPID_table: + self.IPID_table[std.ipid].cPublicRefs += std.cPublicRefs + else: + entry = IPID_Entry() + entry.ipid = std.ipid + entry.oxid = std.oxid + entry.oid = std.oid + entry.iid = obj.iid + entry.iface = iface + entry.cPublicRefs = std.cPublicRefs + if entry.cPublicRefs == 0: + # "If the STDOBJREF contains a public reference count of zero, + # the client MUST obtain additional references on the interface" + raise NotImplementedError("Should acquire additional references !") + entry.cPrivateRefs = 0 + self.IPID_table[std.ipid] = entry + + if std.oid in self.OID_table: + oid_entry = self.OID_table[std.oid] + if std.ipid not in oid_entry.ipids: + oid_entry.ipids.append(std.ipid) + else: + binds, secs = _ParseStringArray(obj.saResAddr) + + oid_entry = OID_Entry() + oid_entry.oid = std.oid + oid_entry.oxid = std.oxid + oid_entry.ipids.append(std.ipid) + oid_entry.garbage_collection = not std.flags.SORF_NOPING + oid_entry.hash = _HashStringBinding(binds) + self.OID_table[std.oid] = oid_entry + + if oid_entry.hash not in self.Resolver_table: + resolver_entry = Resolver_Entry() + resolver_entry.setid = 0 + resolver_entry.hash = oid_entry.hash + resolver_entry.binds = binds + resolver_entry.secs = secs + self.Resolver_table[oid_entry.hash] = resolver_entry + + def _ChoseRPCBinding(self, bindings: List[STRINGBINDING]): + """ + [MS-DCOM] 3.2.4.1.2.1 - Determining RPC Binding Information for OXID Resolution + """ + # We don't try security bindings, only string ones (connection). + # We take the first valid one. + for binding in bindings: + # Only NCACN_IP_TCP is supported by DCOM + if binding.wTowerId == DCERPC_Transport.NCACN_IP_TCP: + # [MS-DCOM] 2.2.19.3 + m = re.match(r"(.*)\[(.*)\]", binding.aNetworkAddr) + if m: + host, port = m.group(1), int(m.group(2)) + else: + host, port = binding.aNetworkAddr, 135 + + # Check validity of the host/port tuple + if valid_ip6(host): + # IPv6 + pass + elif valid_ip(host): + # IPv4 + pass + else: + # Netbios/FQDN + try: + socket.gethostbyname(host) + except Exception: + # Resolution failed. Skip. + log_runtime.warning( + "Resolution of '%s' failed, check your DNS and default " + "DNS prefix. Kerberos authentication will likely not work." + % host + ) + continue + + # Success + return host, port + raise ValueError("No valid bindings available !") + + def _CalculateTargetName(self, secs: List[SECURITYBINDING]): + """ + 3.2.4.2 ORPC Invocations - Find SPN from aPrincName + """ + if self.ssp is None or not secs: + return None + + for sec in secs: + # "if the aPrincName field is nonempty" + if sec.wAuthnSvc == self.ssp.auth_type and sec.aPrincName: + return sec.aPrincName + + # "if the aPrincName field is empty, the client MUST NOT specify an SPN" + return None + + def UnmarshallObjectReference( + self, mifaceptr: MInterfacePointer, iid: ComInterface + ): + """ + [MS-DCOM] 3.2.4.3 Marshaling an Object Reference + + Unmarshall a MInterfacePointer received by the applicative layer. + """ + oid = self._UnmarshallObjref(obj=OBJREF(mifaceptr.abData), iid=iid) + return self._GetObjectInstance(oid) + + def ResolveOxid2( + self, oxid: int, host: Optional[str] = None, port: Optional[int] = None + ): + """ + [MS-DCOM] 3.2.4.1.2.2 Issuing the OXID Resolution Request + + :param oxid: the OXID to resolve + :param host: (optional) connect to a different host + :param port: (optional) connect to a different port + """ + + if host == self.host and port == self.port: + host = self.host + port = self.port + client = self + else: + # Create and connect client + client = DCOM_Client( + # Note <85>: Windows uses INTEGRITY + auth_level=DCE_C_AUTHN_LEVEL.PKT_INTEGRITY, + ssp=self.ssp, + ) + client.connect(host, port=port) + + # Bind IObjectExporter if not already + client.bind_or_alter( + find_dcerpc_interface("IObjectExporter"), + target_name="rpcss/" + self.host, + ) + + try: + # Perform ResolveOxid2 + resp = super(DCOM_Client, client).sr1_req( + ResolveOxid2_Request( + pOxid=oxid, + arRequestedProtseqs=[ + DCERPC_Transport.NCACN_IP_TCP, + ], + ndr64=self.ndr64, + ) + ) + finally: + if host != self.host or port != self.port: + client.close() + + # Entry + if oxid in self.OXID_table: + entry = self.OXID_table[oxid] + else: + entry = OXID_Entry() + + # Get OXID, IPID, COMVERSION, authentication level hint + entry.oxid = oxid + entry.version = resp.pComVersion + entry.authnHint = DCE_C_AUTHN_LEVEL(resp.pAuthnHint) + entry.ipid_IRemUnknown = _uid_from_bytes( + bytes(resp.pipidRemUnknown), ndrendian=resp.ndrendian + ) + + # Set RPC bindings from the oxid request + binds, secs = _ParseStringArray(resp.valueof("ppdsaOxidBindings")) + entry.bindingInfo = self._ChoseRPCBinding(binds) + entry.target_name = self._CalculateTargetName(secs) + + # Update the OXID table + if entry.oxid not in self.OXID_table: + self.OXID_table[entry.oxid] = entry + + return entry + + def RemoteCreateInstance( + self, clsid: uuid.UUID, iids: List[ComInterface] + ) -> ObjectInstance: + """ + Calls IRemoteSCMActivator::RemoteCreateInstance and returns a OXID_Entry + that points to an instance of the provided class. + + :param clsid: the class ID to initialize + :param iids: the IDs of the interfaces to request + """ + return self._RemoteCreateInstanceOrGetClassObject( + RemoteCreateInstance_Request, + RemoteCreateInstance_Response, + clsid, + iids, + ) + + def RemoteGetClassObject( + self, clsid: uuid.UUID, iids: List[ComInterface] + ) -> ObjectInstance: + """ + Calls IRemoteSCMActivator::RemoteGetClassObject and returns a OXID_Entry + that points to the factory. + + :param clsid: the class ID to initialize + :param iids: the IDs of the interfaces to request + """ + return self._RemoteCreateInstanceOrGetClassObject( + RemoteGetClassObject_Request, + RemoteGetClassObject_Response, + clsid, + iids, + ) + + def sr1_orpc_req( + self, + pkt: NDRPacket, + ipid: uuid.UUID, + ssp=None, + auth_level=None, + impersonation_type=None, + timeout=5, + **kwargs, + ): + """ + Make an ORPC call. + + :param ipid: the reference to a specific interface on an object. + :param pkt: the request to make. + + :param ssp: (optional) non default SSP to use to connect to the object exporter + :param auth_level: (optional) non default authn level to use + :param impersonation_type: (optional) non default impersonation type to use + :param timeout: (optional) timeout for the connection + """ + # [MS-DCOM] sect 3.2.4.2 + + # 1. look up the object exporter information in the client tables + + try: + # "The client MUST use the IPID specified by the client application to + # look up the IPID entry in the IPID table." + ipid_entry = self.IPID_table[ipid] + except KeyError: + raise ValueError("The IPID that was passed is unknown.") + + # "The client MUST then look up the OXID entry" + oxid_entry = self.OXID_table[ipid_entry.oxid] + oid_entry = self.OID_table[ipid_entry.oid] + resolver_entry = self.Resolver_table[oid_entry.hash] + + # Get opnum + try: + opnum = pkt.overload_fields[DceRpc5Request]["opnum"] + except KeyError: + raise ValueError("This packet is not part of a registered COM interface !") + + # Build ORPC request + + if resolver_entry.client is None: + # We don't have a client ready, make one. + resolver_entry.client = DCERPC_Client( + DCERPC_Transport.NCACN_IP_TCP, + ssp=ssp or self.ssp, + auth_level=auth_level or oxid_entry.authnHint, + impersonation_type=impersonation_type or self.impersonation_type, + verb=self.verb, + ) + + resolver_entry.client.connect( + host=oxid_entry.bindingInfo[0], + port=oxid_entry.bindingInfo[1], + timeout=timeout, + ) + + # Bind the COM interface + resolver_entry.client.bind_or_alter( + ipid_entry.iface, + target_name=oxid_entry.target_name, + ) + + # We need to set the NDR very late, after the bind + pkt.ndr64 = resolver_entry.client.ndr64 + + # "The ORPCTHIS and ORPCTHAT structures MUST be marshaled using + # the NDR [2.0] Transfer Syntax" + pkt = ( + ORPCTHIS( + version=oxid_entry.version, + cid=self.cid, + ndr64=False, + ) + / pkt + ) + + # Send/Receive ! + resp = resolver_entry.client.sr1_req( + pkt, + opnum=opnum, + objectuuid=ipid, + **kwargs, + ) + + return resp[ORPCTHAT].payload + + def AcquireInterface( + self, + ipid: uuid.UUID, + iids: List[ComInterface], + cPublicRefs: int, + ): + """ + [MS-DCOM] 3.2.4.4.3 - Acquiring Additional Interfaces on the Object + """ + # 1. Look up the OID entry + ipid_entry = self.IPID_table[ipid] + oxid_entry = self.OXID_table[ipid_entry.oxid] + + # 2. Perform call + resp = self.sr1_orpc_req( + ipid=oxid_entry.ipid_IRemUnknown, + pkt=RemQueryInterface_Request( + ripid=GUID(_uid_to_bytes(ipid)), + cRefs=cPublicRefs, + cIids=len(iids), + iids=[GUID(_uid_to_bytes(x.uuid)) for x in iids], + ), + ) + + # 3. Process answer + if not resp or resp.status != 0: + raise ValueError + + # "When the call returns successfully..." + for i, remqir in enumerate(resp.valueof("ppQIResults")): + self._UnmarshallObjref( + OBJREF(iid=iids[i].uuid) + / OBJREF_STANDARD(std=STDOBJREF(bytes(remqir.std))), + iid=iids[i], + ) + + def RemRelease(self, ipid: uuid.UUID): + """ + 3.2.4.4.2 Releasing Reference Counts on an Interface + """ + + # 1. Look up the OID entry + ipid_entry = self.IPID_table[ipid] + oxid_entry = self.OXID_table[ipid_entry.oxid] + oid_entry = self.OID_table[ipid_entry.oid] + + # 2. Perform call + resp = self.sr1_orpc_req( + ipid=oxid_entry.ipid_IRemUnknown, + pkt=RemRelease_Request( + InterfaceRefs=[ + REMINTERFACEREF( + ipid=GUID(_uid_to_bytes(ipid)), + cPublicRefs=ipid_entry.cPublicRefs, + cPrivateRefs=ipid_entry.cPrivateRefs, + ) + ], + ), + ) + + # 3. Process answer + if resp and resp.status == 0: + # "When the call returns successfully..." + # "It MUST remove the IPID entry from the IPID table." + del self.IPID_table[ipid] + + # "It MUST remove the IPID from the IPID list in the OID entry." + oid_entry.ipids.remove(ipid) + + # "If the IPID list of the OID entry is empty, it MUST remove the + # OID entry from the OID table." + if not oid_entry.ipids: + del self.OID_table[ipid_entry.oid] + + +def ServerAlive2(host, timeout=5) -> Tuple[List[STRINGBINDING], List[SECURITYBINDING]]: + """ + Call IObjectExporter::ServerAlive2 + """ + client = DCERPC_Client( + transport=DCERPC_Transport.NCACN_IP_TCP, + verb=False, + ndr64=False, + # "The client MUST NOT specify security on the call" + auth_level=DCE_C_AUTHN_LEVEL.NONE, + ) + client.connect(host, port=135, timeout=timeout) + + # Bind IObjectExporter if not already + client.bind_or_alter(find_dcerpc_interface("IObjectExporter")) + + # Send ServerAlive2 request + resp = client.sr1_req(ServerAlive2_Request(ndr64=False), timeout=timeout) + if not resp or resp.status != 0: + raise ValueError("ServerAlive2 failed !") + + # Parse bindings and security options + return _ParseStringArray(resp.ppdsaOrBindings.value) diff --git a/scapy/layers/msrpce/msdrsr.py b/scapy/layers/msrpce/msdrsr.py new file mode 100644 index 00000000000..d447c6bdecb --- /dev/null +++ b/scapy/layers/msrpce/msdrsr.py @@ -0,0 +1,131 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +[MS-DRSR] Directory Replication Service (DRS) Remote Protocol +""" + +import uuid +from dataclasses import dataclass + +from scapy.packet import Packet +from scapy.fields import LEIntField, FlagsField, UUIDField, UTCTimeField +from scapy.volatile import RandShort + +from scapy.asn1.asn1 import ASN1_OID +from scapy.layers.msrpce.raw.ms_drsr import UUID +from scapy.layers.msrpce.raw.ms_drsr import * # noqa: F403,F401 + +# [MS-DRSR] sect 5.16.4 ATTRTYP-to-OID Conversion + + +@dataclass +class Prefix: + prefixString: str + prefixIndex: int + + +def MakeAttid(t, o): + """ + MakeAttid per [MS-DRSR] sect 5.16.4 + """ + ToBinary = lambda x: bytes(ASN1_OID(x)) + + lastValue = int(o.split(".")[-1]) + + # "convert the dotted form of OID into a BER encoded binary" + binaryOID = ToBinary(o) + + # "get the prefix of the OID" + if lastValue < 128: + oidPrefix = binaryOID[:-1] + else: + oidPrefix = binaryOID[:-2] + + lowerWord = lastValue % 16384 + if lastValue >= 16384: + lowerWord += 32768 + try: + upperWord = next(x.prefixIndex for x in t if x.prefixString == oidPrefix) + except StopIteration: + # AddPrefixTableEntry + upperWord = int(RandShort()) + t.append( + Prefix( + prefixString=oidPrefix, + prefixIndex=upperWord, + ) + ) + + return upperWord * 65536 + lowerWord + + +# [MS-DRSR] sect 5.39 DRS_EXTENSIONS_INT + + +class DRS_EXTENSIONS_INT(Packet): + fields_desc = [ + FlagsField( + "dwFlags", + 0, + -32, + { + 0x00000001: "BASE", + 0x00000002: "ASYNCREPL", + 0x00000004: "REMOVEAPI", + 0x00000008: "MOVEREQ_V2", + 0x00000010: "GETCHG_DEFLATE", + 0x00000020: "DCINFO_V1", + 0x00000040: "RESTORE_USN_OPTIMIZATION", + 0x00000080: "ADDENTRY", + 0x00000100: "KCC_EXECUTE", + 0x00000200: "ADDENTRY_V2", + 0x00000400: "LINKED_VALUE_REPLICATION", + 0x00000800: "DCINFO_V2", + 0x00001000: "INSTANCE_TYPE_NOT_REQ_ON_MOD", + 0x00002000: "CRYPTO_BIND", + 0x00004000: "GET_REPL_INFO", + 0x00008000: "STRONG_ENCRYPTION", + 0x00010000: "DCINFO_VFFFFFFFF", + 0x00020000: "TRANSITIVE_MEMBERSHIP", + 0x00040000: "ADD_SID_HISTORY", + 0x00080000: "POST_BETA3", + 0x00100000: "GETCHGREQ_V5", + 0x00200000: "GETMEMBERSHIPS2", + 0x00400000: "GETCHGREQ_V6", + 0x00800000: "NONDOMAIN_NCS", + 0x01000000: "GETCHGREQ_V8", + 0x02000000: "GETCHGREPLY_V5", + 0x04000000: "GETCHGREPLY_V6", + 0x08000000: "WHISTLER_BETA3", + 0x10000000: "W2K3_DEFLATE", + 0x20000000: "GETCHGREQ_V10", + 0x40000000: "R2", + 0x80000000: "R3", + }, + ), + UUIDField("SiteObjGuid", None, uuid_fmt=UUIDField.FORMAT_LE), + LEIntField("Pid", 0), + UTCTimeField("dwReplEpoch", None, fmt=" None: + """ + Print stacktrace + """ + # Get a list of ErrorInfo + cur = self.extended_error + errors = [cur] + while cur and cur.Next: + cur = cur.Next.value + errors.append(cur) + # Concatenate the ErrorInfos + timefld = UTCTimeField( + "", + None, + fmt=" 47.0 + from cryptography.hazmat.decrepit.ciphers.modes import CFB8 + except ImportError: + from cryptography.hazmat.primitives.ciphers.modes import CFB8 +else: + hashes = hmac = Cipher = algorithms = modes = DES = CFB8 = None + + +# Typing imports +from typing import ( + Optional, +) + +# --- RFC + +# [MS-NRPC] sect 3.1.4.2 +_negotiateFlags = { + # Not used. MUST be ignored on receipt. + 0x00000001: "A", + # B: BDCs persistently try to update their database to the PDC's + # version after they get a notification indicating that their + # database is out-of-date. + 0x00000002: "BDCContinuousUpdate", + # C: Supports RC4 encryption. + 0x00000004: "RC4", + # Not used. MUST be ignored on receipt. + 0x00000008: "D", + # E: Supports BDCs handling CHANGELOGs. + 0x00000010: "BDCChangelog", + # F: Supports restarting of full synchronization between DCs. + 0x00000020: "RestartingDCSync", + # G: Does not require ValidationLevel 2 fornongeneric passthrough. + 0x00000040: "NoValidationLevel2", + # H: Supports the NetrDatabaseRedo (Opnum 17) functionality + 0x00000080: "DatabaseRedo", + # I: Supports refusal of password changes. + 0x00000100: "RefusalPasswordChange", + # J: Supports the NetrLogonSendToSam (Opnum 32) functionality. + 0x00000200: "SendToSam", + # K: Supports generic pass-through authentication. + 0x00000400: "Generic-passthrough", + # L: Supports concurrent RPC calls. + 0x00000800: "ConcurrentRPC", + # M: Supports avoiding of user account database replication. + 0x00001000: "AvoidRepliAccountDB", + # N: Supports avoiding of Security Authority database replication. + 0x00002000: "AvoidRepliAuthorityDB", + # O: Supports strong keys. + 0x00004000: "StrongKeys", + # P: Supports transitive trusts. + 0x00008000: "TransitiveTrust", + # Not used. MUST be ignored on receipt. + 0x00010000: "Q", + # R: Supports the NetrServerPasswordSet2 functionality. + 0x00020000: "ServerPasswordSet2", + # S: Supports the NetrLogonGetDomainInfo functionality. + 0x00040000: "GetDomainInfo", + # T: Supports cross-forest trusts. + 0x00080000: "CrossForestTrust", + # U: The server ignores the NT4Emulator ADM element. + 0x00100000: "NoNT4Emul", + # V: Supports RODC pass-through to different domains. + 0x00200000: "RODC-passthrough", + # W: Supports Advanced Encryption Standard (AES) encryption and SHA2 hashing. + 0x01000000: "AES", + # Not used. MUST be ignored on receipt. + 0x20000000: "X", + # Y: Supports Secure RPC. + 0x40000000: "SecureRPC", + # Supports Kerberos as the security support provider for secure channel setup. + 0x80000000: "Kerberos", +} +_negotiateFlags = FlagsField("", 0, -32, _negotiateFlags).names + +# -- CRYPTO + + +# [MS-NRPC] sect 3.1.4.3.1 +@crypto_validator +def ComputeSessionKeyAES(HashNt, ClientChallenge, ServerChallenge): + M4SS = HashNt + h = hmac.HMAC(M4SS, hashes.SHA256()) + h.update(ClientChallenge) + h.update(ServerChallenge) + return h.finalize()[:16] + + +# [MS-NRPC] sect 3.1.4.3.2 +@crypto_validator +def ComputeSessionKeyStrongKey(HashNt, ClientChallenge, ServerChallenge): + M4SS = HashNt + digest = hashes.Hash(hashes.MD5()) + digest.update(b"\x00\x00\x00\x00") + digest.update(ClientChallenge) + digest.update(ServerChallenge) + h = hmac.HMAC(M4SS, hashes.MD5()) + h.update(digest.finalize()) + return h.finalize() + + +# [MS-NRPC] sect 3.1.4.4.1 +@crypto_validator +def ComputeNetlogonCredentialAES(Input, Sk): + cipher = Cipher(algorithms.AES(Sk), mode=CFB8(b"\x00" * 16)) + encryptor = cipher.encryptor() + return encryptor.update(Input) + + +# [MS-NRPC] sect 3.1.4.4.2 +def InitLMKey(KeyIn): + KeyOut = bytearray(b"\x00" * 8) + KeyOut[0] = KeyIn[0] >> 0x01 + KeyOut[1] = ((KeyIn[0] & 0x01) << 6) | (KeyIn[1] >> 2) + KeyOut[2] = ((KeyIn[1] & 0x03) << 5) | (KeyIn[2] >> 3) + KeyOut[3] = ((KeyIn[2] & 0x07) << 4) | (KeyIn[3] >> 4) + KeyOut[4] = ((KeyIn[3] & 0x0F) << 3) | (KeyIn[4] >> 5) + KeyOut[5] = ((KeyIn[4] & 0x1F) << 2) | (KeyIn[5] >> 6) + KeyOut[6] = ((KeyIn[5] & 0x3F) << 1) | (KeyIn[6] >> 7) + KeyOut[7] = KeyIn[6] & 0x7F + for i in range(8): + KeyOut[i] = (KeyOut[i] << 1) & 0xFE + return KeyOut + + +@crypto_validator +def ComputeNetlogonCredentialDES(Input, Sk): + k3 = InitLMKey(Sk[0:7]) + k4 = InitLMKey(Sk[7:14]) + output1 = Cipher(DES(k3), modes.ECB()).encryptor().update(Input) + return Cipher(DES(k4), modes.ECB()).encryptor().update(output1) + + +# [MS-NRPC] sect 3.1.4.5 +def _credentialAddition(cred, i): + return ( + struct.pack( + "L", ClientSequenceNumber & 0xFFFFFFFF) + high = struct.pack( + ">L", + ((ClientSequenceNumber >> 32) & 0xFFFFFFFF) | (0x80000000 if client else 0), + ) + return low + high + + +@crypto_validator +def ComputeNetlogonChecksumAES(nl_auth_sig, message, SessionKey, Confounder=None): + h = hmac.HMAC(SessionKey, hashes.SHA256()) + h.update(nl_auth_sig[:8]) + if Confounder: + h.update(Confounder) + h.update(message) + return h.finalize() + + +@crypto_validator +def ComputeNetlogonChecksumMD5(nl_auth_sig, message, SessionKey, Confounder=None): + digest = hashes.Hash(hashes.MD5()) + digest.update(b"\x00\x00\x00\x00") + digest.update(nl_auth_sig[:8]) + if Confounder: + digest.update(Confounder) + digest.update(message) + h = hmac.HMAC(SessionKey, hashes.MD5()) + h.update(digest.finalize()) + return h.finalize() + + +@crypto_validator +def ComputeNetlogonSealingKeyAES(SessionKey): + return bytes(bytearray((x ^ 0xF0) for x in bytearray(SessionKey))) + + +@crypto_validator +def ComputeNetlogonSealingKeyRC4(SessionKey, CopySeqNumber): + XorKey = bytes(bytearray((x ^ 0xF0) for x in bytearray(SessionKey))) + h = hmac.HMAC(XorKey, hashes.MD5()) + h.update(b"\x00\x00\x00\x00") + h = hmac.HMAC(h.finalize(), hashes.MD5()) + h.update(CopySeqNumber) + return h.finalize() + + +@crypto_validator +def ComputeNetlogonSequenceNumberKeyMD5(SessionKey, Checksum): + h = hmac.HMAC(SessionKey, hashes.MD5()) + h.update(b"\x00\x00\x00\x00") + h = hmac.HMAC(h.finalize(), hashes.MD5()) + h.update(Checksum) + return h.finalize() + + +# --- SSP + + +class NetlogonSSP(SSP): + auth_type = 0x44 # Netlogon + + class STATE(SSP.STATE): + INIT = 1 + CLI_SENT_NL = 2 + SRV_SENT_NL = 3 + + class CONTEXT(SSP.CONTEXT): + __slots__ = [ + "ClientSequenceNumber", + "IsClient", + "AES", + ] + + def __init__(self, IsClient, req_flags=None, AES=True): + self.state = NetlogonSSP.STATE.INIT + self.IsClient = IsClient + self.ClientSequenceNumber = 0 + self.AES = AES + super(NetlogonSSP.CONTEXT, self).__init__(req_flags=req_flags) + + def __init__(self, SessionKey, computername, domainname, AES=True, **kwargs): + self.SessionKey = SessionKey + self.AES = AES + self.computername = computername + self.domainname = domainname + super(NetlogonSSP, self).__init__(**kwargs) + + def GSS_Inquire_names_for_mech(self): + raise NotImplementedError("Netlogon cannot be used with SPNEGO !") + + def _secure(self, Context, msgs, Seal): + """ + Internal function used by GSS_WrapEx and GSS_GetMICEx + + [MS-NRPC] 3.3.4.2.1 + """ + # Concatenate the ToSign + ToSign = b"".join(x.data for x in msgs if x.sign) + + Confounder = None + if Seal: + Confounder = os.urandom(8) + + if Context.AES: + # 1. If AES is negotiated + signature = NL_AUTH_SIGNATURE( + SignatureAlgorithm=0x0013, + SealAlgorithm=0x001A if Seal else 0xFFFF, + ) + else: + # 2. If AES is not negotiated + signature = NL_AUTH_SIGNATURE( + SignatureAlgorithm=0x0077, + SealAlgorithm=0x007A if Seal else 0xFFFF, + ) + # 3. Pad filled with 0xff (OK) + # 4. Flags with 0x00 (OK) + # 5. SequenceNumber + SequenceNumber = ComputeCopySeqNumber( + Context.ClientSequenceNumber, Context.IsClient + ) + # 6. The ClientSequenceNumber MUST be incremented by 1 + Context.ClientSequenceNumber += 1 + # 7. Signature + if Context.AES: + signature.Checksum = ComputeNetlogonChecksumAES( + bytes(signature), ToSign, self.SessionKey, Confounder + )[:8] + else: + signature.Checksum = ComputeNetlogonChecksumMD5( + bytes(signature), ToSign, self.SessionKey, Confounder + )[:8] + # 8. If the Confidentiality option is requested, the Confounder field and + # the data MUST be encrypted + if Seal: + if Context.AES: + EncryptionKey = ComputeNetlogonSealingKeyAES(self.SessionKey) + else: + EncryptionKey = ComputeNetlogonSealingKeyRC4( + self.SessionKey, SequenceNumber + ) + # Encrypt Confounder and data + if Context.AES: + IV = SequenceNumber * 2 + encryptor = Cipher( + algorithms.AES(EncryptionKey), mode=CFB8(IV) + ).encryptor() + # Confounder + signature.Confounder = encryptor.update(Confounder) + # data + for msg in msgs: + if msg.conf_req_flag: + msg.data = encryptor.update(msg.data) + else: + handle = RC4Init(EncryptionKey) + # Confounder + signature.Confounder = RC4(handle, Confounder) + # DOC IS WRONG ! + # > The server MUST initialize RC4 only once, before encrypting + # > the Confounder field. + # But, this fails ! as Samba put it: + # > For RC4, Windows resets the cipherstate after encrypting + # > the confounder, thus defeating the purpose of the confounder + handle = RC4Init(EncryptionKey) + # data + for msg in msgs: + if msg.conf_req_flag: + msg.data = RC4(handle, msg.data) + # 9. The SequenceNumber MUST be encrypted. + if Context.AES: + EncryptionKey = self.SessionKey + IV = signature.Checksum * 2 + cipher = Cipher(algorithms.AES(EncryptionKey), mode=CFB8(IV)) + encryptor = cipher.encryptor() + signature.SequenceNumber = encryptor.update(SequenceNumber) + else: + EncryptionKey = ComputeNetlogonSequenceNumberKeyMD5( + self.SessionKey, signature.Checksum + ) + signature.SequenceNumber = RC4K(EncryptionKey, SequenceNumber) + + return ( + msgs, + signature, + ) + + def _unsecure(self, Context, msgs, signature, Seal): + """ + Internal function used by GSS_UnwrapEx and GSS_VerifyMICEx + + [MS-NRPC] 3.3.4.2.2 + """ + assert isinstance(signature, NL_AUTH_SIGNATURE) + + # 1. The SignatureAlgorithm bytes MUST be verified + if (Context.AES and signature.SignatureAlgorithm != 0x0013) or ( + not Context.AES and signature.SignatureAlgorithm != 0x0077 + ): + raise ValueError("Invalid SignatureAlgorithm !") + + # 5. The SequenceNumber MUST be decrypted. + if Context.AES: + EncryptionKey = self.SessionKey + IV = signature.Checksum * 2 + cipher = Cipher(algorithms.AES(EncryptionKey), mode=CFB8(IV)) + decryptor = cipher.decryptor() + SequenceNumber = decryptor.update(signature.SequenceNumber) + else: + EncryptionKey = ComputeNetlogonSequenceNumberKeyMD5( + self.SessionKey, signature.Checksum + ) + SequenceNumber = RC4K(EncryptionKey, signature.SequenceNumber) + # 6. A local copy of SequenceNumber MUST be computed + CopySeqNumber = ComputeCopySeqNumber( + Context.ClientSequenceNumber, not Context.IsClient + ) + # 7. The SequenceNumber MUST be compared to CopySeqNumber + if SequenceNumber != CopySeqNumber: + raise ValueError("ERROR: SequenceNumber don't match") + # 8. ClientSequenceNumber MUST be incremented. + Context.ClientSequenceNumber += 1 + # 9. If the Confidentiality option is requested, the Confounder and the + # data MUST be decrypted. + Confounder = None + if Seal: + if Context.AES: + EncryptionKey = ComputeNetlogonSealingKeyAES(self.SessionKey) + else: + EncryptionKey = ComputeNetlogonSealingKeyRC4( + self.SessionKey, SequenceNumber + ) + # Decrypt Confounder and data + if Context.AES: + IV = SequenceNumber * 2 + decryptor = Cipher( + algorithms.AES(EncryptionKey), mode=CFB8(IV) + ).decryptor() + # Confounder + Confounder = decryptor.update(signature.Confounder) + # data + for msg in msgs: + if msg.conf_req_flag: + msg.data = decryptor.update(msg.data) + else: + # Confounder + EncryptionKey = ComputeNetlogonSealingKeyRC4( + self.SessionKey, SequenceNumber + ) + Confounder = RC4K(EncryptionKey, signature.Confounder) + # data + handle = RC4Init(EncryptionKey) + for msg in msgs: + if msg.conf_req_flag: + msg.data = RC4(handle, msg.data) + + # Concatenate the ToSign + ToSign = b"".join(x.data for x in msgs if x.sign) + + # 10/11. Signature + if Context.AES: + Checksum = ComputeNetlogonChecksumAES( + bytes(signature), ToSign, self.SessionKey, Confounder + )[:8] + else: + Checksum = ComputeNetlogonChecksumMD5( + bytes(signature), ToSign, self.SessionKey, Confounder + )[:8] + if signature.Checksum != Checksum: + raise ValueError("ERROR: Checksum don't match") + return msgs + + def GSS_WrapEx(self, Context, msgs, qop_req=0): + return self._secure(Context, msgs, True) + + def GSS_GetMICEx(self, Context, msgs, qop_req=0): + return self._secure(Context, msgs, False)[1] + + def GSS_UnwrapEx(self, Context, msgs, signature): + return self._unsecure(Context, msgs, signature, True) + + def GSS_VerifyMICEx(self, Context, msgs, signature): + self._unsecure(Context, msgs, signature, False) + + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: bytes = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + Context = self.CONTEXT(True, req_flags=req_flags, AES=self.AES) + + if Context.state == self.STATE.INIT: + Context.state = self.STATE.CLI_SENT_NL + return ( + Context, + NL_AUTH_MESSAGE( + MessageType=0, + Flags=3, + NetbiosDomainName=self.domainname, + NetbiosComputerName=self.computername, + ), + GSS_S_CONTINUE_NEEDED, + ) + else: + return Context, None, GSS_S_COMPLETE + + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: bytes = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + Context = self.CONTEXT(False, req_flags=req_flags, AES=self.AES) + + if Context.state == self.STATE.INIT: + Context.state = self.STATE.SRV_SENT_NL + return ( + Context, + NL_AUTH_MESSAGE( + MessageType=1, + Flags=0, + ), + GSS_S_COMPLETE, + ) + else: + # Invalid state + return Context, None, GSS_S_FAILURE + + def MaximumSignatureLength(self, Context: CONTEXT): + """ + Returns the Maximum Signature length. + + This will be used in auth_len in DceRpc5, and is necessary for + PFC_SUPPORT_HEADER_SIGN to work properly. + """ + # len(NL_AUTH_SIGNATURE()) + if Context.flags & GSS_C_FLAGS.GSS_C_CONF_FLAG: + if Context.AES: + return 56 + else: + return 32 + else: + if Context.AES: + return 48 + else: + return 24 + + +# --- Utils + + +class NETLOGON_SECURE_CHANNEL_METHOD(enum.Enum): + NetrServerAuthenticate3 = 1 + NetrServerAuthenticateKerberos = 2 + + +class NetlogonClient(DCERPC_Client): + """ + A subclass of DCERPC_Client that supports establishing a Netlogon secure channel + using the Netlogon SSP, and handling Netlogon authenticators. + + This class therefore only supports the 'logon' rpc. + + :param auth_level: one of DCE_C_AUTHN_LEVEL + + :param verb: verbosity control. + :param supportAES: advertise AES support in the Netlogon session. + + Example:: + + >>> cli = NetlogonClient() + >>> cli.connect_and_bind("192.168.0.100") + >>> cli.establish_secure_channel( + ... UPN="WIN10@DOMAIN", + ... HASHNT=bytes.fromhex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + ... ) + """ + + def __init__( + self, + # Default to PRIVACY: see KB5021130 + auth_level=DCE_C_AUTHN_LEVEL.PKT_PRIVACY, + verb=True, + supportAES=True, + **kwargs, + ): + self.interface = find_dcerpc_interface("logon") + self.SessionKey = None + self.ClientStoredCredential = None + self.supportAES = supportAES + super(NetlogonClient, self).__init__( + DCERPC_Transport.NCACN_IP_TCP, + auth_level=auth_level, + verb=verb, + **kwargs, + ) + + def connect(self, host, **kwargs): + """ + This calls DCERPC_Client's connect to bind the 'logon' interface. + """ + super(NetlogonClient, self).connect( + host=host, + interface=self.interface, + **kwargs, + ) + + def create_authenticator(self): + """ + Create a NETLOGON_AUTHENTICATOR + """ + if isinstance(self.ssp, NetlogonSSP): + # [MS-NRPC] sect 3.1.4.5 + ts = int(time.time()) + self.ClientStoredCredential = _credentialAddition( + self.ClientStoredCredential, ts + ) + return PNETLOGON_AUTHENTICATOR( + Credential=PNETLOGON_CREDENTIAL( + data=( + ComputeNetlogonCredentialAES( + self.ClientStoredCredential, + self.SessionKey, + ) + if self.supportAES + else ComputeNetlogonCredentialDES( + self.ClientStoredCredential, + self.SessionKey, + ) + ), + ), + Timestamp=ts, + ) + elif isinstance(self.ssp, KerberosSSP): + # Kerberos. This is off spec :( + return PNETLOGON_AUTHENTICATOR() + else: + raise ValueError("Invalid ssp case !") + + def validate_authenticator(self, auth): + """ + Validate a NETLOGON_AUTHENTICATOR + + :param auth: the NETLOGON_AUTHENTICATOR object + """ + if isinstance(self.ssp, NetlogonSSP): + # [MS-NRPC] sect 3.1.4.5 + self.ClientStoredCredential = _credentialAddition( + self.ClientStoredCredential, 1 + ) + if self.supportAES: + tempcred = ComputeNetlogonCredentialAES( + self.ClientStoredCredential, self.SessionKey + ) + else: + tempcred = ComputeNetlogonCredentialDES( + self.ClientStoredCredential, self.SessionKey + ) + if tempcred != auth.Credential.data: + raise ValueError("Server netlogon authenticator is wrong !") + elif isinstance(self.ssp, KerberosSSP): + # Kerberos. This is off spec :( + if bytes(auth) != b"\x00" * 12: + raise ValueError("Server netlogon authenticator is wrong !") + else: + raise ValueError("Invalid ssp case !") + + def establish_secure_channel( + self, + UPN: str, + DC_FQDN: str, + HASHNT: Optional[bytes] = None, + PASSWORD: Optional[str] = None, + KEY=None, + ssp: Optional[KerberosSSP] = None, + mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, + secureChannelType=NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, + ): + """ + Function to establish the Netlogon Secure Channel. + + This uses NetrServerAuthenticate3 or NetrServerAuthenticateKerberos to + negotiate the session key, then creates a NetlogonSSP that uses that session + key and alters the DCE/RPC session to use it. + + :param mode: one of NETLOGON_SECURE_CHANNEL_METHOD. This defines which method + to use to establish the secure channel. + :param UPN: the UPN of the computer account name that is used to establish + the secure channel. (e.g. WIN10$@domain.local) + :param DC_FQDN: the FQDN name of the DC. + + The function then requires one of the following: + + :param HASHNT: the HashNT of the computer account (in Authenticate3 mode). + :param KEY: a Kerberos key to use (in Kerberos mode) + :param PASSWORD: the password of the computer account (any mode). + :param ssp: a KerberosSSP to use (in Kerberos mode) + """ + computername, domainname = _parse_upn(UPN) + # We need to normalize here, since the functions require both the accountname + # and the normal (no dollar) computer name. + if computername.endswith("$"): + computername = computername[:-1] + + if mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3: + if ssp or KEY: + raise ValueError("Cannot use 'ssp' on 'KEY' in Authenticate3 mode !") + if not HASHNT: + if PASSWORD: + HASHNT = MD4le(PASSWORD) + else: + raise ValueError("Missing either 'PASSWORD' or 'HASHNT' !") + if "." in domainname: + raise ValueError( + "The UPN in Authenticate3 must have a NETBIOS domain name !" + ) + else: + if HASHNT: + raise ValueError("Cannot use 'HASHNT' in Kerberos mode !") + + # Calc NegotiateFlags + NegotiateFlags = FlagValue( + 0x602FFFFF, # sensible default (Windows) + names=_negotiateFlags, + ) + if self.supportAES: + NegotiateFlags += "AES" + + # We are either using NetrServerAuthenticate3 or NetrServerAuthenticateKerberos + if mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3: + # We use the legacy NetrServerAuthenticate3 function (NetlogonSSP) + + # Make sure the interface is bound + if not self.bind_or_alter(self.interface): + raise ValueError("Bind failed !") + + # Flow documented in 3.1.4 Session-Key Negotiation + # and sect 3.4.5.2 for specific calls + clientChall = os.urandom(8) + + # Perform NetrServerReqChallenge request + netr_server_req_chall_response = self.sr1_req( + NetrServerReqChallenge_Request( + PrimaryName=None, + ComputerName=computername, + ClientChallenge=PNETLOGON_CREDENTIAL( + data=clientChall, + ), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerReqChallenge_Response not in netr_server_req_chall_response + or netr_server_req_chall_response.status != 0 + ): + print( + conf.color_theme.fail( + "! %s" + % STATUS_ERREF.get( + netr_server_req_chall_response.status, "Failure" + ) + ) + ) + netr_server_req_chall_response.show() + raise ValueError("NetrServerReqChallenge failed !") + + # Build the session key + serverChall = netr_server_req_chall_response.ServerChallenge.data + if self.supportAES: + SessionKey = ComputeSessionKeyAES(HASHNT, clientChall, serverChall) + self.ClientStoredCredential = ComputeNetlogonCredentialAES( + clientChall, SessionKey + ) + else: + SessionKey = ComputeSessionKeyStrongKey( + HASHNT, clientChall, serverChall + ) + self.ClientStoredCredential = ComputeNetlogonCredentialDES( + clientChall, SessionKey + ) + + # Perform Authenticate3 request + netr_server_auth3_response = self.sr1_req( + NetrServerAuthenticate3_Request( + PrimaryName="\\\\" + DC_FQDN, + AccountName=computername + "$", + SecureChannelType=secureChannelType, + ComputerName=computername, + ClientCredential=PNETLOGON_CREDENTIAL( + data=self.ClientStoredCredential, + ), + NegotiateFlags=int(NegotiateFlags), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if netr_server_auth3_response.status != 0: + # An error occurred. + NegotiatedFlags = None + if NetrServerAuthenticate3_Response in netr_server_auth3_response: + NegotiatedFlags = FlagValue( + netr_server_auth3_response.NegotiateFlags, + names=_negotiateFlags, + ) + if NegotiateFlags != NegotiatedFlags: + print( + conf.color_theme.fail( + "! Unsupported server flags: %s" + % (NegotiatedFlags ^ NegotiateFlags) + ) + ) + raise ValueError("NetrServerAuthenticate3 failed !") + + # Check Server Credential + if self.supportAES: + if ( + netr_server_auth3_response.ServerCredential.data + != ComputeNetlogonCredentialAES(serverChall, SessionKey) + ): + print(conf.color_theme.fail("! Invalid ServerCredential.")) + raise ValueError + else: + if ( + netr_server_auth3_response.ServerCredential.data + != ComputeNetlogonCredentialDES(serverChall, SessionKey) + ): + print(conf.color_theme.fail("! Invalid ServerCredential.")) + raise ValueError + + # SessionKey negotiated ! + self.SessionKey = SessionKey + + # Create the NetlogonSSP and assign it to the local client + self.ssp = self.sock.session.ssp = NetlogonSSP( + SessionKey=self.SessionKey, + AES=self.supportAES, + domainname=domainname, + computername=computername, + ) + + # Finally alter context (to use the SSP) + if not self.alter_context(self.interface): + raise ValueError("Bind failed !") + + elif mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos: + # We use the brand new NetrServerAuthenticateKerberos function + NegotiateFlags += "Kerberos" + + # Set KerberosSSP and alter context + if ssp: + self.ssp = self.sock.session.ssp = ssp + else: + self.ssp = self.sock.session.ssp = KerberosSSP( + UPN=UPN, + PASSWORD=PASSWORD, + KEY=KEY, + ) + # [MS-NRPC] note <185> "Windows uses netlogon/" + target_name = "netlogon/" + DC_FQDN + if not self.bind_or_alter(self.interface, target_name=target_name): + raise ValueError("Bind failed !") + + # Send AuthenticateKerberos request + netr_server_authkerb_response = self.sr1_req( + NetrServerAuthenticateKerberos_Request( + PrimaryName="\\\\" + DC_FQDN, + AccountName=computername + "$", + AccountType=secureChannelType, + ComputerName=computername, + NegotiateFlags=int(NegotiateFlags), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerAuthenticateKerberos_Response + not in netr_server_authkerb_response + or netr_server_authkerb_response.status != 0 + ): + # An error occurred + netr_server_authkerb_response.show() + raise ValueError("NetrServerAuthenticateKerberos failed !") diff --git a/scapy/layers/msrpce/mspac.py b/scapy/layers/msrpce/mspac.py new file mode 100644 index 00000000000..1a96a9afd13 --- /dev/null +++ b/scapy/layers/msrpce/mspac.py @@ -0,0 +1,874 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +[MS-PAC] + +https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/166d8064-c863-41e1-9c23-edaaa5f36962 +Up to date with version: 23.0 +""" + +import struct + +from scapy.config import conf +from scapy.error import log_runtime +from scapy.fields import ( + ConditionalField, + FieldLenField, + FieldListField, + FlagsField, + LEIntEnumField, + LEIntField, + LELongField, + LEShortField, + MultipleTypeField, + PacketField, + PacketListField, + StrField, + StrFieldUtf16, + StrFixedLenField, + StrLenFieldUtf16, + UTCTimeField, + UUIDField, + XStrField, + XStrLenField, +) +from scapy.packet import Packet +from scapy.layers.kerberos import ( + _AUTHORIZATIONDATA_VALUES, + _KRB_S_TYPES, +) +from scapy.layers.dcerpc import ( + NDRByteField, + NDRConfFieldListField, + NDRConfPacketListField, + NDRConfStrLenField, + NDRConfVarStrLenFieldUtf16, + NDRConfVarStrNullFieldUtf16, + NDRConformantString, + NDRFieldListField, + NDRFullEmbPointerField, + NDRInt3264EnumField, + NDRIntField, + NDRLongField, + NDRPacket, + NDRPacketField, + NDRSerialization1Header, + NDRSerializeType1PacketLenField, + NDRShortField, + NDRSignedLongField, + NDRUnionField, + _NDRConfField, + ndr_deserialize1, + ndr_serialize1, +) +from scapy.layers.ntlm import ( + _NTLMPayloadField, + _NTLMPayloadPacket, +) +from scapy.layers.windows.security import WINNT_SID + +# sect 2.4 + + +class PAC_INFO_BUFFER(Packet): + fields_desc = [ + LEIntEnumField( + "ulType", + 0x00000001, + { + 0x00000001: "Logon information", + 0x00000002: "Credentials information", + 0x00000006: "Server Signature", + 0x00000007: "KDC Signature", + 0x0000000A: "Client name and ticket information", + 0x0000000B: "Constrained delegation information", + 0x0000000C: "UPN and DNS information", + 0x0000000D: "Client claims information", + 0x0000000E: "Device information", + 0x0000000F: "Device claims information", + 0x00000010: "Ticket Signature", + 0x00000011: "PAC Attributes", + 0x00000012: "PAC Requestor", + 0x00000013: "Extended KDC Signature", + }, + ), + LEIntField("cbBufferSize", None), + LELongField("Offset", None), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_PACTYPES = {} + + +# sect 2.5 - NDR PACKETS + + +class RPC_UNICODE_STRING(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRShortField("Length", None, size_of="Buffer", adjust=lambda _, x: (x * 2)), + NDRShortField( + "MaximumLength", None, size_of="Buffer", adjust=lambda _, x: (x * 2) + ), + NDRFullEmbPointerField( + NDRConfVarStrLenFieldUtf16( + "Buffer", + "", + size_is=lambda pkt: (pkt.MaximumLength // 2), + length_is=lambda pkt: (pkt.Length // 2), + ), + ), + ] + + +class FILETIME(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("dwLowDateTime", 0), NDRIntField("dwHighDateTime", 0)] + + +class GROUP_MEMBERSHIP(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("RelativeId", 0), NDRIntField("Attributes", 0)] + + +class CYPHER_BLOCK(NDRPacket): + fields_desc = [StrFixedLenField("data", "", length=8)] + + +class USER_SESSION_KEY(NDRPacket): + fields_desc = [PacketListField("data", [], CYPHER_BLOCK, count_from=lambda _: 2)] + + +class RPC_SID_IDENTIFIER_AUTHORITY(NDRPacket): + fields_desc = [StrFixedLenField("Value", "", length=6)] + + +class SID(NDRPacket): + ALIGNMENT = (4, 8) + DEPORTED_CONFORMANTS = ["SubAuthority"] + fields_desc = [ + NDRByteField("Revision", 0), + NDRByteField("SubAuthorityCount", None, size_of="SubAuthority"), + NDRPacketField( + "IdentifierAuthority", + RPC_SID_IDENTIFIER_AUTHORITY(), + RPC_SID_IDENTIFIER_AUTHORITY, + ), + NDRConfFieldListField( + "SubAuthority", + [], + NDRIntField("", 0), + size_is=lambda pkt: pkt.SubAuthorityCount, + conformant_in_struct=True, + ), + ] + + def summary(self): + return WINNT_SID.summary(self) + + +class KERB_SID_AND_ATTRIBUTES(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRPacketField("Sid", SID(), SID)), + NDRIntField("Attributes", 0), + ] + + +class KERB_VALIDATION_INFO(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRPacketField("LogonTime", FILETIME(), FILETIME), + NDRPacketField("LogoffTime", FILETIME(), FILETIME), + NDRPacketField("KickOffTime", FILETIME(), FILETIME), + NDRPacketField("PasswordLastSet", FILETIME(), FILETIME), + NDRPacketField("PasswordCanChange", FILETIME(), FILETIME), + NDRPacketField("PasswordMustChange", FILETIME(), FILETIME), + NDRPacketField("EffectiveName", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRPacketField("FullName", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRPacketField("LogonScript", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRPacketField("ProfilePath", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRPacketField("HomeDirectory", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRPacketField("HomeDirectoryDrive", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRShortField("LogonCount", 0), + NDRShortField("BadPasswordCount", 0), + NDRIntField("UserId", 0), + NDRIntField("PrimaryGroupId", 0), + NDRIntField("GroupCount", None, size_of="GroupIds"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "GroupIds", + [GROUP_MEMBERSHIP()], + GROUP_MEMBERSHIP, + size_is=lambda pkt: pkt.GroupCount, + ), + ), + NDRIntField("UserFlags", 0), + NDRPacketField("UserSessionKey", USER_SESSION_KEY(), USER_SESSION_KEY), + NDRPacketField("LogonServer", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRPacketField("LogonDomainName", RPC_UNICODE_STRING(), RPC_UNICODE_STRING), + NDRFullEmbPointerField(NDRPacketField("LogonDomainId", SID(), SID)), + NDRFieldListField("Reserved1", [], NDRIntField("", 0), length_is=lambda _: 2), + NDRIntField("UserAccountControl", 0), + NDRFieldListField("Reserved3", [], NDRIntField("", 0), length_is=lambda _: 7), + NDRIntField("SidCount", None, size_of="ExtraSids"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "ExtraSids", + [KERB_SID_AND_ATTRIBUTES()], + KERB_SID_AND_ATTRIBUTES, + size_is=lambda pkt: pkt.SidCount, + ), + ), + NDRFullEmbPointerField( + NDRPacketField("ResourceGroupDomainSid", SID(), SID), + ), + NDRIntField("ResourceGroupCount", None, size_of="ResourceGroupIds"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "ResourceGroupIds", + [GROUP_MEMBERSHIP()], + GROUP_MEMBERSHIP, + size_is=lambda pkt: pkt.ResourceGroupCount, + ), + ), + ] + + +_PACTYPES[1] = KERB_VALIDATION_INFO + +# sect 2.6 + + +class PAC_CREDENTIAL_INFO(Packet): + fields_desc = [ + LEIntField("Version", 0), + LEIntEnumField( + "EncryptionType", + 1, + { + 0x00000001: "DES-CBC-CRC", + 0x00000003: "DES-CBC-MD5", + 0x00000011: "AES128_CTS_HMAC_SHA1_96", + 0x00000012: "AES256_CTS_HMAC_SHA1_96", + 0x00000017: "RC4-HMAC", + }, + ), + XStrField("SerializedData", b""), + ] + + +_PACTYPES[2] = PAC_CREDENTIAL_INFO + +# sect 2.7 + + +class PAC_CLIENT_INFO(Packet): + fields_desc = [ + UTCTimeField( + "ClientId", None, fmt=" bytes + offset = 12 + fields = { + "Upn": 0, + "DnsDomainName": 4, + } + if self.Flags.S: + offset = 20 + fields["SamName"] = 12 + fields["Sid"] = 16 + return ( + _pac_post_build( + self, + pkt, + offset, + fields, + ) + + pay + ) + + +_PACTYPES[0xC] = UPN_DNS_INFO + +# sect 2.11 - NDR PACKETS + +try: + from enum import IntEnum +except ImportError: + IntEnum = object + + +class CLAIM_TYPE(IntEnum): + CLAIM_TYPE_INT64 = 1 + CLAIM_TYPE_UINT64 = 2 + CLAIM_TYPE_STRING = 3 + CLAIM_TYPE_BOOLEAN = 6 + + +class CLAIMS_SOURCE_TYPE(IntEnum): + CLAIMS_SOURCE_TYPE_AD = 1 + CLAIMS_SOURCE_TYPE_CERTIFICATE = 2 + + +class CLAIMS_COMPRESSION_FORMAT(IntEnum): + COMPRESSION_FORMAT_NONE = 0 + COMPRESSION_FORMAT_LZNT1 = 2 + COMPRESSION_FORMAT_XPRESS = 3 + COMPRESSION_FORMAT_XPRESS_HUFF = 4 + + +class CLAIM_ENTRY_sub0(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("ValueCount", None, size_of="Int64Values"), + NDRFullEmbPointerField( + NDRConfFieldListField( + "Int64Values", + [], + NDRSignedLongField, + size_is=lambda pkt: pkt.ValueCount, + ), + ), + ] + + +class CLAIM_ENTRY_sub1(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("ValueCount", None, size_of="Uint64Values"), + NDRFullEmbPointerField( + NDRConfFieldListField( + "Uint64Values", [], NDRLongField, size_is=lambda pkt: pkt.ValueCount + ), + ), + ] + + +class CLAIM_ENTRY_sub2(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("ValueCount", None, size_of="StringValues"), + NDRFullEmbPointerField( + NDRConfFieldListField( + "StringValues", + [], + NDRFullEmbPointerField( + NDRConfVarStrNullFieldUtf16("StringVal", ""), + ), + size_is=lambda pkt: pkt.ValueCount, + ), + ), + ] + + +class CLAIM_ENTRY_sub3(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("ValueCount", None, size_of="BooleanValues"), + NDRFullEmbPointerField( + NDRConfFieldListField( + "BooleanValues", [], NDRLongField, size_is=lambda pkt: pkt.ValueCount + ), + ), + ] + + +class CLAIM_ENTRY(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("Id", "")), + NDRInt3264EnumField("Type", 0, CLAIM_TYPE), + NDRUnionField( + [ + ( + NDRPacketField("Values", CLAIM_ENTRY_sub0(), CLAIM_ENTRY_sub0), + ( + ( + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_INT64 + ), + (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_INT64), + ), + ), + ( + NDRPacketField("Values", CLAIM_ENTRY_sub1(), CLAIM_ENTRY_sub1), + ( + ( + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_UINT64 + ), + (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_UINT64), + ), + ), + ( + NDRPacketField("Values", CLAIM_ENTRY_sub2(), CLAIM_ENTRY_sub2), + ( + ( + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_STRING + ), + (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_STRING), + ), + ), + ( + NDRPacketField("Values", CLAIM_ENTRY_sub3(), CLAIM_ENTRY_sub3), + ( + ( + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_BOOLEAN + ), + (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_BOOLEAN), + ), + ), + ], + StrFixedLenField("Values", "", length=0), + align=(2, 8), + switch_fmt=("H", "I"), + ), + ] + + +class CLAIMS_ARRAY(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRInt3264EnumField("usClaimsSourceType", 0, CLAIMS_SOURCE_TYPE), + NDRIntField("ulClaimsCount", None, size_of="ClaimEntries"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "ClaimEntries", + [CLAIM_ENTRY()], + CLAIM_ENTRY, + size_is=lambda pkt: pkt.ulClaimsCount, + ), + ), + ] + + +class CLAIMS_SET(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("ulClaimsArrayCount", None, size_of="ClaimsArrays"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "ClaimsArrays", + [CLAIMS_ARRAY()], + CLAIMS_ARRAY, + size_is=lambda pkt: pkt.ulClaimsArrayCount, + ), + ), + NDRShortField("usReservedType", 0), + NDRIntField("ulReservedFieldSize", None, size_of="ReservedField"), + NDRFullEmbPointerField( + NDRConfStrLenField( + "ReservedField", "", size_is=lambda pkt: pkt.ulReservedFieldSize + ), + ), + ] + + +class _CLAIMSClaimSet(_NDRConfField, NDRSerializeType1PacketLenField): + CONFORMANT_STRING = True + LENGTH_FROM = True + + def m2i(self, pkt, s): + if pkt.usCompressionFormat == CLAIMS_COMPRESSION_FORMAT.COMPRESSION_FORMAT_NONE: + return ndr_deserialize1(s, CLAIMS_SET, ptr_pack=True) + else: + # TODO: There are 3 funky compression formats... see sect 2.2.18.4 + return NDRConformantString(value=s) + + def i2m(self, pkt, val): + val = val[0] + if pkt.usCompressionFormat == CLAIMS_COMPRESSION_FORMAT.COMPRESSION_FORMAT_NONE: + return ndr_serialize1(val, ptr_pack=True) + else: + # funky + return bytes(val) + + def valueof(self, pkt, x): + if pkt.usCompressionFormat == CLAIMS_COMPRESSION_FORMAT.COMPRESSION_FORMAT_NONE: + return self._subval(x)[0] + else: + return x + + +class CLAIMS_SET_METADATA(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("ulClaimsSetSize", None, size_of="ClaimsSet"), + NDRFullEmbPointerField( + _CLAIMSClaimSet( + "ClaimsSet", None, None, size_is=lambda pkt: pkt.ulClaimsSetSize + ), + ), + NDRInt3264EnumField( + "usCompressionFormat", + 0, + CLAIMS_COMPRESSION_FORMAT, + ), + # this size_of is technically wrong. we just assume it's uncompressed... + NDRIntField("ulUncompressedClaimsSetSize", None, size_of="ClaimsSet"), + NDRShortField("usReservedType", 0), + NDRIntField("ulReservedFieldSize", None, size_of="ReservedField"), + NDRFullEmbPointerField( + NDRConfStrLenField( + "ReservedField", "", size_is=lambda pkt: pkt.ulReservedFieldSize + ), + ), + ] + + +class PAC_CLIENT_CLAIMS_INFO(NDRPacket): + fields_desc = [NDRPacketField("Claims", CLAIMS_SET_METADATA(), CLAIMS_SET_METADATA)] + + +if IntEnum != object: + # If not available, ignore. I can't be bothered + _PACTYPES[0xD] = PAC_CLIENT_CLAIMS_INFO + + +# sect 2.12 - NDR PACKETS + + +class DOMAIN_GROUP_MEMBERSHIP(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRPacketField("DomainId", SID(), SID)), + NDRIntField("GroupCount", 0), + NDRFullEmbPointerField( + NDRConfPacketListField( + "GroupIds", + [GROUP_MEMBERSHIP()], + GROUP_MEMBERSHIP, + size_is=lambda pkt: pkt.GroupCount, + ), + ), + ] + + +class PAC_DEVICE_INFO(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("UserId", 0), + NDRIntField("PrimaryGroupId", 0), + NDRFullEmbPointerField( + NDRPacketField("AccountDomainId", SID(), SID), + ), + NDRIntField("AccountGroupCount", 0), + NDRFullEmbPointerField( + NDRConfPacketListField( + "AccountGroupIds", + [GROUP_MEMBERSHIP()], + GROUP_MEMBERSHIP, + size_is=lambda pkt: pkt.AccountGroupCount, + ), + ), + NDRIntField("SidCount", 0), + NDRFullEmbPointerField( + NDRConfPacketListField( + "ExtraSids", + [KERB_SID_AND_ATTRIBUTES()], + KERB_SID_AND_ATTRIBUTES, + size_is=lambda pkt: pkt.SidCount, + ), + ), + NDRIntField("DomainGroupCount", 0), + NDRFullEmbPointerField( + NDRConfPacketListField( + "DomainGroup", + [DOMAIN_GROUP_MEMBERSHIP()], + DOMAIN_GROUP_MEMBERSHIP, + size_is=lambda pkt: pkt.DomainGroupCount, + ), + ), + ] + + +_PACTYPES[0xE] = PAC_DEVICE_INFO + +# sect 2.14 - PAC_ATTRIBUTES_INFO + + +class PAC_ATTRIBUTES_INFO(Packet): + fields_desc = [ + LEIntField("FlagsLength", 2), + FieldListField( + "Flags", + ["PAC_WAS_REQUESTED"], + FlagsField( + "", + 0, + -32, + { + 0x00000001: "PAC_WAS_REQUESTED", + 0x00000002: "PAC_WAS_GIVEN_IMPLICITLY", + }, + ), + count_from=lambda pkt: (pkt.FlagsLength + 7) // 8, + ), + ] + + +_PACTYPES[0x11] = PAC_ATTRIBUTES_INFO + +# sect 2.15 - PAC_REQUESTOR_SID + + +class PAC_REQUESTOR_SID(Packet): + fields_desc = [ + PacketField("Sid", WINNT_SID(), WINNT_SID), + ] + + +_PACTYPES[0x12] = PAC_REQUESTOR_SID + +# sect 2.16 - PAC_REQUESTOR_GUID + + +class PAC_REQUESTOR_GUID(Packet): + fields_desc = [ + UUIDField("Guid", None), + ] + + +_PACTYPES[0x14] = PAC_REQUESTOR_GUID + +# sect 2.3 + + +class _PACTYPEBuffers(PacketListField): + def addfield(self, pkt, s, val): + # we use this field to set Offset and cbBufferSize + res = b"" + if len(val) != len(pkt.Payloads): + log_runtime.warning("Size of 'Buffers' does not match size of 'Payloads' !") + return super(_PACTYPEBuffers, self).addfield(pkt, s, val) + offset = 16 * len(pkt.Payloads) + 8 + for i, v in enumerate(val): + x = self.i2m(pkt, v) + pay = pkt.Payloads[i] + if isinstance(pay, NDRPacket) or isinstance(pay, NDRSerialization1Header): + lgth = len(ndr_serialize1(pay, ptr_pack=True)) + else: + lgth = len(pay) + if v.cbBufferSize is None: + x = x[:4] + struct.pack(" DceRpcSession: + try: + return self.sock.session + except AttributeError: + raise ValueError("Client is not connected ! Please connect()") + + def connect( + self, + host, + endpoint: Union[int, str] = None, + port: Optional[int] = None, + interface=None, + timeout=5, + smb_kwargs={}, + ): + """ + Initiate a connection. + + :param host: the host to connect to + :param endpoint: (optional) the port/smb pipe to connect to + :param interface: (optional) if endpoint isn't provided, uses the endpoint + mapper to find the appropriate endpoint for that interface. + :param timeout: (optional) the connection timeout (default 5) + :param port: (optional) the port to connect to. (useful for SMB) + """ + smb_kwargs.setdefault("HOST", host) + if endpoint is None and interface is not None: + # Figure out the endpoint using the endpoint mapper + + if self.transport == DCERPC_Transport.NCACN_IP_TCP and port is None: + # IP/TCP + # ask the endpoint mapper (port 135) for the IP:PORT + endpoints = get_endpoint( + host, + interface, + ndrendian=self.ndrendian, + verb=self.verb, + ) + if endpoints: + _, endpoint = endpoints[0] + else: + raise ValueError( + "Could not find an available endpoint for that interface !" + ) + elif self.transport == DCERPC_Transport.NCACN_NP: + # SMB + # ask the endpoint mapper (over SMB) for the namedpipe + endpoints = get_endpoint( + host, + interface, + transport=self.transport, + ndrendian=self.ndrendian, + verb=self.verb, + ssp=self.ssp, + smb_kwargs=smb_kwargs, + ) + if endpoints: + endpoint = endpoints[0].lstrip("\\pipe\\") + else: + return + + # Assign the default port if no port is provided + if port is None: + if self.transport == DCERPC_Transport.NCACN_IP_TCP: # IP/TCP + port = endpoint or 135 + elif self.transport == DCERPC_Transport.NCACN_NP: # SMB + port = 445 + else: + raise ValueError( + "Can't guess the port for transport: %s" % self.transport + ) + + # Start socket and connect + self.host = host + self.port = port + sock = socket.socket() + sock.settimeout(timeout) + if self.verb: + print( + "\u2503 Connecting to %s on port %s via %s..." + % (host, port, repr(self.transport)) + ) + sock.connect((host, port)) + if self.verb: + print( + conf.color_theme.green( + "\u2514 Connected from %s" % repr(sock.getsockname()) + ) + ) + + if self.transport == DCERPC_Transport.NCACN_NP: # SMB + if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_PRIVACY: + smb_kwargs.setdefault("REQUIRE_ENCRYPTION", True) + elif self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_INTEGRITY: + smb_kwargs.setdefault("REQUIRE_SIGNATURE", True) + + # We pack the socket into a SMB_RPC_SOCKET + sock = self.smbrpcsock = SMB_RPC_SOCKET.from_tcpsock( + sock, + ssp=self.ssp, + **smb_kwargs, + ) + + # If the endpoint is provided, connect to it. + if endpoint is not None: + self.open_smbpipe(endpoint) + + self.sock = DceRpcSocket( + sock, + DceRpc5, + **self.dcesockargs, + ) + elif self.transport == DCERPC_Transport.NCACN_IP_TCP: + self.sock = DceRpcSocket( + sock, + DceRpc5, + ssp=self.ssp, + auth_level=self.auth_level, + **self.dcesockargs, + ) + + def close(self): + """ + Close the DCE/RPC client. + """ + if self.verb: + print("X Connection closed\n") + self.sock.close() + + def sr1(self, pkt, **kwargs): + """ + Send/Receive a DCE/RPC message. + + The DCE/RPC header is added automatically. + """ + self.call_ids[self.session.assoc_group_id] += 1 + pkt = ( + DceRpc5( + call_id=self.call_ids[self.session.assoc_group_id], + pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG", + endian=self.ndrendian, + auth_verifier=kwargs.pop("auth_verifier", None), + vt_trailer=kwargs.pop("vt_trailer", None), + ) + / pkt + ) + if "pfc_flags" in kwargs: + pkt.pfc_flags = kwargs.pop("pfc_flags") + if "objectuuid" in kwargs: + pkt.pfc_flags += "PFC_OBJECT_UUID" + pkt.object = kwargs.pop("objectuuid") + return self.sock.sr1(pkt, verbose=0, **kwargs) + + def send(self, pkt, **kwargs): + """ + Send a DCE/RPC message. + + The DCE/RPC header is added automatically. + """ + self.call_ids[self.session.assoc_group_id] += 1 + pkt = ( + DceRpc5( + call_id=self.call_ids[self.session.assoc_group_id], + pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG", + endian=self.ndrendian, + auth_verifier=kwargs.pop("auth_verifier", None), + vt_trailer=kwargs.pop("vt_trailer", None), + ) + / pkt + ) + if "pfc_flags" in kwargs: + pkt.pfc_flags = kwargs.pop("pfc_flags") + if "objectuuid" in kwargs: + pkt.pfc_flags += "PFC_OBJECT_UUID" + pkt.object = kwargs.pop("objectuuid") + return self.sock.send(pkt, **kwargs) + + def sr1_req(self, pkt, **kwargs): + """ + Send/Receive a DCE/RPC request. + + :param pkt: the inner DCE/RPC message, without any header. + """ + if self.verb: + if "objectuuid" in kwargs: + # COM + print( + conf.color_theme.opening( + ">> REQUEST (COM): %s" % pkt.payload.__class__.__name__ + ) + ) + else: + print( + conf.color_theme.opening(">> REQUEST: %s" % pkt.__class__.__name__) + ) + + # Add sectrailer if first time talking on this interface + vt_trailer = b"" + if ( + self._first_time_on_interface + and self.transport != DCERPC_Transport.NCACN_NP + ): + # In the first request after a bind, Windows sends a trailer to verify + # that the negotiated transfer/interface wasn't altered. + self._first_time_on_interface = False + vt_trailer = DceRpcSecVT( + commands=[ + DceRpcSecVTCommand(SEC_VT_COMMAND_END=1) + / DceRpcSecVTPcontext( + InterfaceId=self.session.rpc_bind_interface.uuid, + Version=self.session.rpc_bind_interface.if_version, + TransferSyntax="NDR64" if self.ndr64 else "NDR 2.0", + TransferVersion=1 if self.ndr64 else 2, + ) + ] + ) + + # Optional: force opnum + opnum = {} + if "opnum" in kwargs: + opnum["opnum"] = kwargs.pop("opnum") + + # Set NDR64 + pkt.ndr64 = self.ndr64 + + # Send/receive + resp = self.sr1( + DceRpc5Request( + cont_id=self.session.cont_id, + alloc_hint=len(pkt) + len(vt_trailer), + **opnum, + ) + / pkt, + vt_trailer=vt_trailer, + **kwargs, + ) + + # Parse result + result = None + if DceRpc5Response in resp: + if self.verb: + if "objectuuid" in kwargs: + # COM + print( + conf.color_theme.success( + "<< RESPONSE (COM): %s" + % (resp[DceRpc5Response].payload.payload.__class__.__name__) + ) + ) + else: + print( + conf.color_theme.success( + "<< RESPONSE: %s" + % (resp[DceRpc5Response].payload.__class__.__name__) + ) + ) + result = resp[DceRpc5Response].payload + elif DceRpc5Fault in resp: + if self.verb: + print(conf.color_theme.success("<< FAULT")) + # If [MS-EERR] is loaded, show the extended info + if resp[DceRpc5Fault].payload and not isinstance( + resp[DceRpc5Fault].payload, conf.raw_layer + ): + resp[DceRpc5Fault].payload.show() + result = resp + + if self.verb and getattr(resp, "status", 0) != 0: + if resp.status in _DCE_RPC_ERROR_CODES: + print(conf.color_theme.fail(f"! {_DCE_RPC_ERROR_CODES[resp.status]}")) + elif resp.status in STATUS_ERREF: + print(conf.color_theme.fail(f"! {STATUS_ERREF[resp.status]}")) + else: + print(conf.color_theme.fail("! Failure")) + resp.show() + return result + + def _get_bind_context(self, interface): + """ + Internal: get the bind DCE/RPC context. + """ + if interface in self.contexts: + # We have already found acceptable contexts for this interface, + # reuse that. + return self.contexts[interface] + + # NDR 2.0 + contexts = [ + DceRpc5Context( + cont_id=self.next_cont_id, + abstract_syntax=DceRpc5AbstractSyntax( + if_uuid=interface.uuid, + if_version=interface.if_version, + ), + transfer_syntaxes=[ + DceRpc5TransferSyntax( + # NDR 2.0 32-bit + if_uuid="NDR 2.0", + if_version=2, + ) + ], + ), + ] + self.next_cont_id += 1 + + # NDR64 + if self.ndr64: + contexts.append( + DceRpc5Context( + cont_id=self.next_cont_id, + abstract_syntax=DceRpc5AbstractSyntax( + if_uuid=interface.uuid, + if_version=interface.if_version, + ), + transfer_syntaxes=[ + DceRpc5TransferSyntax( + # NDR64 + if_uuid="NDR64", + if_version=1, + ) + ], + ) + ) + self.next_cont_id += 1 + + # BindTimeFeatureNegotiationBitmask + contexts.append( + DceRpc5Context( + cont_id=self.next_cont_id, + abstract_syntax=DceRpc5AbstractSyntax( + if_uuid=interface.uuid, + if_version=interface.if_version, + ), + transfer_syntaxes=[ + DceRpc5TransferSyntax( + if_uuid=uuid.UUID("6cb71c2c-9812-4540-0300-000000000000"), + if_version=1, + ) + ], + ) + ) + self.next_cont_id += 1 + + # Store contexts for this interface + self.contexts[interface] = contexts + + return contexts + + def _check_bind_context(self, interface, contexts) -> bool: + """ + Internal: check the answer DCE/RPC bind context, and update them. + """ + for i, ctx in enumerate(contexts): + if ctx.result == 0: + # Context was accepted. Remove all others from cache + if len(self.contexts[interface]) != 1: + self.contexts[interface] = [self.contexts[interface][i]] + return True + + return False + + def _bind( + self, + interface: Union[DceRpcInterface, ComInterface], + reqcls, + respcls, + target_name: Optional[str] = None, + ) -> bool: + """ + Internal: used to send a bind/alter request + """ + # Build a security context: [MS-RPCE] 3.3.1.5.2 + if self.verb: + print( + conf.color_theme.opening( + ">> %s on %s" % (reqcls.__name__, interface) + + (" (with %s)" % self.ssp.__class__.__name__ if self.ssp else "") + ) + ) + + # Do we need an authenticated bind + if not self.ssp or ( + self.sspcontext is not None + or self.transport == DCERPC_Transport.NCACN_NP + and self.auth_level < DCE_C_AUTHN_LEVEL.PKT_INTEGRITY + ): + # NCACN_NP = SMB without INTEGRITY/PRIVACY does not bind the RPC securely, + # again as it has already authenticated during the SMB Session Setup + resp = self.sr1( + reqcls(context_elem=self._get_bind_context(interface)), + auth_verifier=None, + ) + status = GSS_S_COMPLETE + else: + # Perform authentication + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + req_flags=( + # SSPs need to be instantiated with some special flags + # for DCE/RPC usages. + GSS_C_FLAGS.GSS_C_DCE_STYLE + | GSS_C_FLAGS.GSS_C_REPLAY_FLAG + | GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG + | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG + | ( + GSS_C_FLAGS.GSS_C_INTEG_FLAG + if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_INTEGRITY + else 0 + ) + | ( + GSS_C_FLAGS.GSS_C_CONF_FLAG + if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_PRIVACY + else 0 + ) + | ( + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG + if self.impersonation_type <= RPC_C_IMP_LEVEL.IDENTIFY + else 0 + ) + | ( + GSS_C_FLAGS.GSS_C_DELEG_FLAG + if self.impersonation_type == RPC_C_IMP_LEVEL.DELEGATE + else 0 + ) + ), + target_name=target_name or ("host/" + self.host), + ) + + if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: + # Authentication failed. + self.sspcontext.clifailure() + return False + + resp = self.sr1( + reqcls(context_elem=self._get_bind_context(interface)), + auth_verifier=( + None + if not self.sspcontext + else CommonAuthVerifier( + auth_type=self.ssp.auth_type, + auth_level=self.auth_level, + auth_context_id=self.session.auth_context_id, + auth_value=token, + ) + ), + pfc_flags=( + "PFC_FIRST_FRAG+PFC_LAST_FRAG" + + ( + # If the SSP supports "Header Signing", advertise it + "+PFC_SUPPORT_HEADER_SIGN" + if self.ssp is not None and self.session.support_header_signing + else "" + ) + ), + ) + + # Check that the answer looks valid and contexts were accepted + if respcls not in resp or not self._check_bind_context( + interface, resp.results + ): + token = None + status = GSS_S_FAILURE + else: + # Call the underlying SSP + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + input_token=resp.auth_verifier.auth_value, + target_name=target_name or ("host/" + self.host), + ) + + if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: + # Authentication should continue, in two ways: + # - through DceRpc5Auth3 (e.g. NTLM) + # - through DceRpc5AlterContext (e.g. Kerberos) + if token and self.ssp.LegsAmount(self.sspcontext) % 2 == 1: + # AUTH 3 for certain SSPs (e.g. NTLM) + # "The server MUST NOT respond to an rpc_auth_3 PDU" + self.send( + DceRpc5Auth3(), + auth_verifier=CommonAuthVerifier( + auth_type=self.ssp.auth_type, + auth_level=self.auth_level, + auth_context_id=self.session.auth_context_id, + auth_value=token, + ), + ) + status = GSS_S_COMPLETE + else: + while token: + respcls = DceRpc5AlterContextResp + resp = self.sr1( + DceRpc5AlterContext( + context_elem=self._get_bind_context(interface) + ), + auth_verifier=CommonAuthVerifier( + auth_type=self.ssp.auth_type, + auth_level=self.auth_level, + auth_context_id=self.session.auth_context_id, + auth_value=token, + ), + ) + if respcls not in resp: + status = GSS_S_FAILURE + break + if resp.auth_verifier is None: + status = GSS_S_COMPLETE + break + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + input_token=resp.auth_verifier.auth_value, + target_name=target_name or ("host/" + self.host), + ) + else: + log_runtime.error("GSS_Init_sec_context failed with %s !" % status) + + # Check context acceptance + if ( + status == GSS_S_COMPLETE + and respcls in resp + and self._check_bind_context(interface, resp.results) + ): + port = resp.sec_addr.port_spec.decode() + ndr = self.session.ndr64 and "NDR64" or "NDR32" + self.ndr64 = self.session.ndr64 + if self.verb: + print( + conf.color_theme.success( + f"<< {respcls.__name__} port '{port}' using {ndr}" + ) + ) + self.session.sspcontext = self.sspcontext + self._first_time_on_interface = True + return True + else: + if self.verb: + if DceRpc5BindNak in resp: + err_msg = resp.sprintf( + "reject_reason: %DceRpc5BindNak.provider_reject_reason%" + ) + print(conf.color_theme.fail("! Bind_nak (%s)" % err_msg)) + if DceRpc5BindNak in resp: + if resp[DceRpc5BindNak].payload and not isinstance( + resp[DceRpc5BindNak].payload, conf.raw_layer + ): + resp[DceRpc5BindNak].payload.show() + elif DceRpc5Fault in resp: + if getattr(resp, "status", 0) != 0: + if resp.status in _DCE_RPC_ERROR_CODES: + print( + conf.color_theme.fail( + f"! {_DCE_RPC_ERROR_CODES[resp.status]}" + ) + ) + elif resp.status in STATUS_ERREF: + print( + conf.color_theme.fail(f"! {STATUS_ERREF[resp.status]}") + ) + else: + print(conf.color_theme.fail("! Failure")) + resp.show() + if resp[DceRpc5Fault].payload and not isinstance( + resp[DceRpc5Fault].payload, conf.raw_layer + ): + resp[DceRpc5Fault].payload.show() + else: + print(conf.color_theme.fail("! Failure")) + resp.show() + return False + + def bind( + self, + interface: Union[DceRpcInterface, ComInterface], + target_name: Optional[str] = None, + ) -> bool: + """ + Bind the client to an interface + + :param interface: the DceRpcInterface object + """ + return self._bind( + interface, + DceRpc5Bind, + DceRpc5BindAck, + target_name=target_name, + ) + + def alter_context( + self, + interface: Union[DceRpcInterface, ComInterface], + target_name: Optional[str] = None, + ) -> bool: + """ + Alter context: post-bind context negotiation + + :param interface: the DceRpcInterface object + """ + return self._bind( + interface, + DceRpc5AlterContext, + DceRpc5AlterContextResp, + target_name=target_name, + ) + + def bind_or_alter( + self, + interface: Union[DceRpcInterface, ComInterface], + target_name: Optional[str] = None, + ) -> bool: + """ + Bind the client to an interface or alter the context if already bound + + :param interface: the DceRpcInterface object + """ + if not self.session.rpc_bind_interface: + # No interface is bound + return self.bind(interface, target_name=target_name) + elif self.session.rpc_bind_interface != interface: + # An interface is already bound + return self.alter_context(interface, target_name=target_name) + return True + + def open_smbpipe(self, name: str): + """ + Open a certain filehandle with the SMB automaton. + + :param name: the name of the pipe + """ + self.ipc_tid = self.smbrpcsock.tree_connect("IPC$") + self.smbrpcsock.open_pipe(name) + + def close_smbpipe(self): + """ + Close the previously opened pipe + """ + self.smbrpcsock.set_TID(self.ipc_tid) + self.smbrpcsock.close_pipe() + self.smbrpcsock.tree_disconnect() + + def connect_and_bind( + self, + host: str, + interface: DceRpcInterface, + port: Optional[int] = None, + timeout: int = 5, + smb_kwargs={}, + ): + """ + Asks the Endpoint Mapper what address to use to connect to the interface, + then uses connect() followed by a bind() + + :param host: the host to connect to + :param interface: the DceRpcInterface object + :param port: (optional, NCACN_NP only) the port to connect to + :param timeout: (optional) the connection timeout (default 5) + """ + # Connect to the interface using the endpoint mapper + self.connect( + host=host, + interface=interface, + port=port, + timeout=timeout, + smb_kwargs=smb_kwargs, + ) + + # Bind in RPC + self.bind(interface) + + def epm_map(self, interface): + """ + Calls ept_map (the EndPoint Manager) + """ + if self.ndr64: + ndr_uuid = "NDR64" + ndr_version = 1 + else: + ndr_uuid = "NDR 2.0" + ndr_version = 2 + pkt = self.sr1_req( + ept_map_Request( + obj=NDRPointer( + referent_id=1, + value=UUID( + Data1=0, + Data2=0, + Data3=0, + Data4=None, + ), + ), + map_tower=NDRPointer( + referent_id=2, + value=twr_p_t( + tower_octet_string=bytes( + protocol_tower_t( + floors=[ + prot_and_addr_t( + lhs_length=19, + protocol_identifier=0xD, + uuid=interface.uuid, + version=interface.major_version, + rhs_length=2, + rhs=interface.minor_version, + ), + prot_and_addr_t( + lhs_length=19, + protocol_identifier=0xD, + uuid=ndr_uuid, + version=ndr_version, + rhs_length=2, + rhs=0, + ), + prot_and_addr_t( + lhs_length=1, + protocol_identifier="RPC connection-oriented protocol", # noqa: E501 + rhs_length=2, + rhs=0, + ), + { + DCERPC_Transport.NCACN_IP_TCP: ( + prot_and_addr_t( + lhs_length=1, + protocol_identifier="NCACN_IP_TCP", + rhs_length=2, + rhs=135, + ) + ), + DCERPC_Transport.NCACN_NP: ( + prot_and_addr_t( + lhs_length=1, + protocol_identifier="NCACN_NP", + rhs_length=2, + rhs=b"0\x00", + ) + ), + }[self.transport], + { + DCERPC_Transport.NCACN_IP_TCP: ( + prot_and_addr_t( + lhs_length=1, + protocol_identifier="IP", + rhs_length=4, + rhs="0.0.0.0", + ) + ), + DCERPC_Transport.NCACN_NP: ( + prot_and_addr_t( + lhs_length=1, + protocol_identifier="NCACN_NB", + rhs_length=10, + rhs=b"127.0.0.1\x00", + ) + ), + }[self.transport], + ], + ) + ), + ), + ), + entry_handle=NDRContextHandle( + attributes=0, + uuid=b"\x00" * 16, + ), + max_towers=500, + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if pkt and ept_map_Response in pkt: + status = pkt[ept_map_Response].status + # [MS-RPCE] sect 2.2.1.2.5 + if status == 0x00000000: + towers = [ + protocol_tower_t(x.value.tower_octet_string) + for x in pkt[ept_map_Response].ITowers.value[0].value + ] + # Let's do some checks to know we know what we're doing + endpoints = [] + for t in towers: + if t.floors[0].uuid != interface.uuid: + if self.verb: + print( + conf.color_theme.fail( + "! Server answered with a different interface." + ) + ) + raise ValueError + if t.floors[1].sprintf("%uuid%") != ndr_uuid: + if self.verb: + print( + conf.color_theme.fail( + "! Server answered with a different NDR version." + ) + ) + raise ValueError + if self.transport == DCERPC_Transport.NCACN_IP_TCP: + endpoints.append((t.floors[4].rhs, t.floors[3].rhs)) + elif self.transport == DCERPC_Transport.NCACN_NP: + endpoints.append(t.floors[3].rhs.rstrip(b"\x00").decode()) + return endpoints + elif status == 0x16C9A0D6: + if self.verb: + print( + conf.color_theme.fail( + "! Server errored: 'There are no elements that satisfy" + " the specified search criteria'." + ) + ) + raise ValueError + print(conf.color_theme.fail("! Failure.")) + if pkt: + pkt.show() + raise ValueError("EPM Map failed") + + +def get_endpoint( + ip, + interface, + transport=DCERPC_Transport.NCACN_IP_TCP, + ndrendian="little", + verb=True, + ssp=None, + smb_kwargs={}, +): + """ + Call the endpoint mapper on a remote IP to find an interface + + :param ip: + :param interface: + :param mode: + :param verb: + :param ssp: + + :return: a list of connection tuples for this interface + """ + client = DCERPC_Client( + transport, + # EPM only works with NDR32 + ndr64=False, + ndrendian=ndrendian, + verb=verb, + ssp=ssp, + ) + + if transport == DCERPC_Transport.NCACN_IP_TCP: + endpoint = 135 + elif transport == DCERPC_Transport.NCACN_NP: + endpoint = "epmapper" + else: + raise ValueError("Unknown transport value !") + + client.connect(ip, endpoint=endpoint, smb_kwargs=smb_kwargs) + + client.bind(find_dcerpc_interface("ept")) + try: + endpoints = client.epm_map(interface) + finally: + client.close() + + return endpoints diff --git a/scapy/layers/msrpce/rpcserver.py b/scapy/layers/msrpce/rpcserver.py new file mode 100644 index 00000000000..8b65164c2cc --- /dev/null +++ b/scapy/layers/msrpce/rpcserver.py @@ -0,0 +1,517 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +DCE/RPC server as per [MS-RPCE] +""" + +import socket +import uuid +import threading +from collections import deque + +from scapy.arch import get_if_addr +from scapy.config import conf +from scapy.data import MTU +from scapy.volatile import RandShort + +from scapy.layers.dcerpc import ( + CommonAuthVerifier, + DCE_RPC_INTERFACES, + DCERPC_Transport, + DceRpc5, + DceRpc5AlterContext, + DceRpc5AlterContextResp, + DceRpc5Auth3, + DceRpc5Bind, + DceRpc5BindAck, + DceRpc5BindNak, + DceRpc5Fault, + DceRpc5PortAny, + DceRpc5Request, + DceRpc5Response, + DceRpc5Result, + DceRpc5TransferSyntax, + DceRpcInterface, + DceRpcSession, + NDRPacket, + RPC_C_AUTHN_LEVEL, +) + +# RPC +from scapy.layers.msrpce.ept import ( + ept_map_Request, + ept_map_Response, + twr_p_t, + protocol_tower_t, + prot_and_addr_t, +) + +# Typing +from typing import ( + Dict, + Callable, + Optional, + Tuple, +) + + +class _DCERPC_Server_metaclass(type): + # This value is calculated for each DCE/RPC server, and contains + # the callables sorted by interface+opnum + dcerpc_commands: Dict[Tuple[uuid.UUID, int], Callable] = {} + + def __new__(cls, name, bases, dct): + dct.setdefault( + "dcerpc_commands", + {x.dcerpc_command: x for x in dct.values() if hasattr(x, "dcerpc_command")}, + ) + return type.__new__(cls, name, bases, dct) + + +class DCERPC_Server(metaclass=_DCERPC_Server_metaclass): + def __init__( + self, + transport: DCERPC_Transport, + ndr64: Optional[bool] = None, + verb: bool = True, + local_ip: str = None, + port: int = None, + portmap: Dict[DceRpcInterface, int] = None, + **kwargs, + ): + self.transport = transport + self.session = DceRpcSession(**kwargs) + self.queue = deque() + if ndr64 is None: + ndr64 = conf.ndr64 + self.ndr64 = ndr64 + # For endpoint mapper. TODO: improve separation/handling of SMB/IP etc + self.local_ip = local_ip + self.port = port + self.portmap = portmap or {} + self.verb = verb + + def loop(self, sock): + while True: + pkt = sock.recv(MTU) + if not pkt: + break + self.recv(pkt) + # send all possible responses + while True: + resp = self.get_response() + if not resp: + break + sock.send(bytes(resp)) + + @staticmethod + def answer(reqcls): + """ + A decorator that registers a DCE/RPC responder to a command. + See the DCE/RPC documentation. + + :param reqcls: the DCE/RPC packet class to respond to + """ + + def deco(func): + if not issubclass(reqcls, NDRPacket): + raise ValueError("Cannot answer a non NDRPacket class !") + try: + func.dcerpc_command = reqcls.intf, reqcls.opnum + except AttributeError: + raise ValueError( + "NDRPacket class isn't registered or isn't a request !" + ) + return func + + return deco + + def extend(self, server_cls): + """ + Extend a DCE/RPC server into another + """ + self.dcerpc_commands.update(server_cls.dcerpc_commands) + + def make_reply(self, req): + """ + Make a response to the DCE/RPC request. + + This finds whether a callback has been registered for this particular packet, + and call it if available. + """ + opnum = req[DceRpc5Request].opnum + intf = self.session.rpc_bind_interface.uuid + if (intf, opnum) in self.dcerpc_commands: + # call handler + return self.dcerpc_commands[(intf, opnum)](self, req) + return None + + @classmethod + def spawn(cls, transport, iface=None, port=135, bg=False, **kwargs): + """ + Spawn a DCE/RPC server + + :param transport: one of DCERPC_Transport + :param iface: the interface to spawn it on (default: conf.iface) + :param port: the port to spawn it on (for IP_TCP or the SMB server) + :param bg: background mode? (default: False) + :param ndr64: whether NDR64 is supported or not (default: conf.ndr64). + This attribute will be overwritten if the client doesn't support it. + """ + if transport == DCERPC_Transport.NCACN_IP_TCP: + # IP/TCP case + ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + local_ip = get_if_addr(iface or conf.iface) + try: + ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except OSError: + pass + ssock.bind((local_ip, port)) + ssock.listen(5) + sockets = [] + if kwargs.get("verb", True): + print( + conf.color_theme.green( + "Server %s started. Waiting..." % cls.__name__ + ) + ) + + def _run(): + # Wait for clients forever + try: + while True: + clientsocket, address = ssock.accept() + sockets.append(clientsocket) + print( + conf.color_theme.gold( + "\u2503 Connection received from %s" % repr(address) + ) + ) + server = cls( + DCERPC_Transport.NCACN_IP_TCP, + local_ip=local_ip, + port=port, + **kwargs, + ) + threading.Thread( + target=server.loop, args=(clientsocket,) + ).start() + except KeyboardInterrupt: + print("X Exiting.") + ssock.shutdown(socket.SHUT_RDWR) + except OSError: + print("X Server closed.") + finally: + for sock in sockets: + try: + sock.shutdown(socket.SHUT_RDWR) + sock.close() + except Exception: + pass + ssock.close() + + if bg: + # Background + threading.Thread(target=_run).start() + return ssock + else: + # Non-background + _run() + elif transport == DCERPC_Transport.NCACN_NP: + # SMB case + from scapy.layers.smbserver import SMB_Server + + kwargs.setdefault("shares", []) # do not expose files by default + return SMB_Server.spawn( + iface=iface or conf.iface, + port=port, + bg=bg, + # Important: pass the DCE/RPC server + DCERPC_SERVER_CLS=cls, + # SMB parameters + **kwargs, + ) + else: + raise ValueError("Unsupported transport :(") + + def recv(self, data): + if isinstance(data, bytes): + req = DceRpc5(data) + else: + req = data + # If the packet has padding, it contains several fragments + pad = None + if conf.padding_layer in req: + pad = req[conf.padding_layer].load + req[conf.padding_layer].underlayer.remove_payload() + # Ask the DCE/RPC session to process it (match interface, etc.) + req = self.session.in_pkt(req) + hdr = DceRpc5( + endian=req.endian, + encoding=req.encoding, + float=req.float, + call_id=req.call_id, + ) + # Now process the packet based on the DCE/RPC type + if DceRpc5Bind in req or DceRpc5AlterContext in req or DceRpc5Auth3 in req: + # Log + if self.verb: + print( + conf.color_theme.opening( + "<< %s" % req.payload.__class__.__name__ + + ( + " (with %s%s)" + % ( + self.session.ssp.__class__.__name__, + ( + f" - {self.session.auth_level.name}" + if self.session.auth_level is not None + else "" + ), + ) + if self.session.ssp + else "" + ) + ) + ) + if not self.session.rpc_bind_interface: + # The session did not find a matching interface ! + self.queue.extend(self.session.out_pkt(hdr / DceRpc5BindNak())) + if self.verb: + print(conf.color_theme.fail("! DceRpc5BindNak (unknown interface)")) + else: + auth_value, status = None, 0 + if ( + self.session.ssp + and req.auth_verifier + and req.auth_verifier.auth_value + ): + ( + self.session.sspcontext, + auth_value, + status, + ) = self.session.ssp.GSS_Accept_sec_context( + self.session.sspcontext, req.auth_verifier.auth_value + ) + self.session.auth_level = RPC_C_AUTHN_LEVEL( + req.auth_verifier.auth_level + ) + self.session.auth_context_id = req.auth_verifier.auth_context_id + if DceRpc5Auth3 in req: + # Auth 3 stops here (no server response) ! + if status != 0: + print(conf.color_theme.fail("! DceRpc5Auth3 failed")) + if pad is not None: + self.recv(pad) + return + # auth_verifier here contains the SSP nego packets + # (whereas it usually contains the verifiers) + if auth_value is not None: + hdr.auth_verifier = CommonAuthVerifier( + auth_type=req.auth_verifier.auth_type, + auth_level=req.auth_verifier.auth_level, + auth_context_id=req.auth_verifier.auth_context_id, + auth_value=auth_value, + ) + + # Detect if the client requested NDR64 and the server agrees + self.ndr64 = self.ndr64 and any( + ctx.transfer_syntaxes[0].sprintf("%if_uuid%") == "NDR64" + for ctx in req.context_elem + ) + + # Process bind contexts and answer to them + results = [] + for ctx in req.context_elem: + # Get name + name = ctx.transfer_syntaxes[0].sprintf("%if_uuid%") + if ( + # NDR64 + (name == "NDR64" and self.ndr64) + or + # NDR 2.0 + (name == "NDR 2.0" and not self.ndr64) + ): + # Acceptance + results.append( + DceRpc5Result( + result=0, + reason=0, + transfer_syntax=DceRpc5TransferSyntax( + if_uuid=ctx.transfer_syntaxes[0].if_uuid, + if_version=ctx.transfer_syntaxes[0].if_version, + ), + ) + ) + elif name == "Bind Time Feature Negotiation": + # Handle Bind Time Feature + results.append( + DceRpc5Result( + result=3, + reason=3, + transfer_syntax=DceRpc5TransferSyntax( + if_uuid="NULL", + if_version=0, + ), + ) + ) + else: + # Reject + results.append( + DceRpc5Result( + result=2, + reason=2, + transfer_syntax=DceRpc5TransferSyntax( + if_uuid="NULL", + if_version=0, + ), + ) + ) + + if self.port is None: + # Piped + port_spec = ( + b"\\\\PIPE\\\\%s\0" + % self.session.rpc_bind_interface.name.encode() + ) + else: + # IP + port_spec = str(self.port).encode() + b"\x00" + if DceRpc5Bind in req: + cls = DceRpc5BindAck + else: + cls = DceRpc5AlterContextResp + self.queue.extend( + self.session.out_pkt( + hdr + / cls( + assoc_group_id=int(RandShort()), + sec_addr=DceRpc5PortAny( + port_spec=port_spec, + ), + results=results, + ), + ) + ) + if self.verb: + print( + conf.color_theme.success( + f">> {cls.__name__} {self.session.rpc_bind_interface.name}" + f" is on port '{port_spec.decode()}' using " + + ("NDR64" if self.ndr64 else "NDR32") + ) + ) + elif DceRpc5Request in req: + if self.verb: + print( + conf.color_theme.opening( + "<< REQUEST: %s" + % req[DceRpc5Request].payload.__class__.__name__ + ) + ) + # Can be any RPC request ! + resp = self.make_reply(req) + if resp: + self.queue.extend( + self.session.out_pkt( + hdr + / DceRpc5Response( + alloc_hint=len(resp), + cont_id=req.cont_id, + ) + / resp, + ) + ) + if self.verb: + print( + conf.color_theme.success( + ">> RESPONSE: %s" % (resp.__class__.__name__) + ) + ) + else: + # Unimplemented request ! + if self.verb: + print( + conf.color_theme.fail( + "! RPC request not implemented by server." + ) + ) + req.show() + + # Return a Fault + hdr.pfc_flags += "PFC_DID_NOT_EXECUTE" + self.queue.extend( + hdr + / DceRpc5Fault( + # nca_s_op_rng_error + status=0x1C010002, + cont_id=req.cont_id, + ) + ) + # If there was padding, process the second frag + if pad is not None: + self.recv(pad) + + def get_response(self): + try: + return self.queue.popleft() + except IndexError: + return None + + # Endpoint mapper + + @answer.__func__(ept_map_Request) # hack for Python <= 3.9 + def ept_map(self, req): + """ + Answer to ept_map_Request. + """ + if self.transport != DCERPC_Transport.NCACN_IP_TCP: + raise ValueError("Unimplemented") + + tower = protocol_tower_t( + req[ept_map_Request].valueof("map_tower.tower_octet_string") + ) + uuid = tower.floors[0].uuid + if_version = (tower.floors[0].rhs << 16) | tower.floors[0].version + + # Check for results in our portmap + port = None + if (uuid, if_version) in DCE_RPC_INTERFACES: + interface = DCE_RPC_INTERFACES[(uuid, if_version)] + if interface in self.portmap: + port = self.portmap[interface] + + if port is not None: + # Found result + resp_tower = twr_p_t( + tower_octet_string=bytes( + protocol_tower_t( + floors=[ + tower.floors[0], # UUID + tower.floors[1], # NDR version + tower.floors[2], # RPC version + prot_and_addr_t( + lhs_length=1, + protocol_identifier="NCACN_IP_TCP", + rhs_length=2, + rhs=port, + ), + prot_and_addr_t( + lhs_length=1, + protocol_identifier="IP", + rhs_length=4, + rhs=self.local_ip or "0.0.0.0", + ), + ] + ) + ) + ) + resp = ept_map_Response(ITowers=[resp_tower], ndr64=self.ndr64) + resp.ITowers.max_count = req.max_towers # ugh + else: + # No result found + pass + return resp diff --git a/scapy/layers/netbios.py b/scapy/layers/netbios.py index 2c5d54dc375..3d20a0ae65b 100644 --- a/scapy/layers/netbios.py +++ b/scapy/layers/netbios.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ NetBIOS over TCP/IP @@ -10,13 +10,40 @@ """ import struct - -from scapy.packet import Packet, bind_layers -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - IPField, IntField, NetBIOSNameField, ShortEnumField, ShortField, \ - StrFixedLenField, XShortField -from scapy.layers.inet import UDP, TCP -from scapy.layers.l2 import SourceMACField +from scapy.arch import get_if_addr +from scapy.base_classes import Net +from scapy.ansmachine import AnsweringMachine +from scapy.compat import bytes_encode +from scapy.config import conf + +from scapy.packet import Packet, bind_bottom_up, bind_layers, bind_top_down +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + FieldLenField, + FlagsField, + IPField, + IntField, + NetBIOSNameField, + PacketListField, + ShortEnumField, + ShortField, + StrFixedLenField, + XShortField, + XStrFixedLenField +) +from scapy.interfaces import _GlobInterfaceType +from scapy.sendrecv import sr1 +from scapy.layers.inet import IP, UDP, TCP +from scapy.layers.l2 import Ether, SourceMACField + +# Typing imports +from typing import ( + List, + Union, +) class NetBIOS_DS(Packet): @@ -90,123 +117,125 @@ def post_build(self, p, pay): 1: "Group name" } + +class NBNSHeader(Packet): + name = "NBNS Header" + fields_desc = [ + ShortField("NAME_TRN_ID", 0), + BitField("RESPONSE", 0, 1), + BitField("OPCODE", 0, 4), + FlagsField("NM_FLAGS", 0, 7, ["B", + "res1", + "res0", + "RA", + "RD", + "TC", + "AA"]), + BitField("RCODE", 0, 4), + ShortField("QDCOUNT", 0), + ShortField("ANCOUNT", 0), + ShortField("NSCOUNT", 0), + ShortField("ARCOUNT", 0), + ] + + def hashret(self): + return b"NBNS" + struct.pack("!B", self.OPCODE) + + # Name Query Request -# Node Status Request +# RFC1002 sect 4.2.12 class NBNSQueryRequest(Packet): name = "NBNS query request" - fields_desc = [ShortField("NAME_TRN_ID", 0), - ShortField("FLAGS", 0x0110), - ShortField("QDCOUNT", 1), - ShortField("ANCOUNT", 0), - ShortField("NSCOUNT", 0), - ShortField("ARCOUNT", 0), - NetBIOSNameField("QUESTION_NAME", "windows"), + fields_desc = [NetBIOSNameField("QUESTION_NAME", "windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("QUESTION_TYPE", 0x20, _NETBIOS_QRTYPES), ShortEnumField("QUESTION_CLASS", 1, _NETBIOS_QRCLASS)] -# Name Registration Request -# Name Refresh Request -# Name Release Request or Demand - - -class NBNSRequest(Packet): - name = "NBNS request" - fields_desc = [ShortField("NAME_TRN_ID", 0), - ShortField("FLAGS", 0x2910), - ShortField("QDCOUNT", 1), - ShortField("ANCOUNT", 0), - ShortField("NSCOUNT", 0), - ShortField("ARCOUNT", 1), - NetBIOSNameField("QUESTION_NAME", "windows"), - ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), - ByteField("NULL", 0), - ShortEnumField("QUESTION_TYPE", 0x20, _NETBIOS_QRTYPES), - ShortEnumField("QUESTION_CLASS", 1, _NETBIOS_QRCLASS), - ShortEnumField("RR_NAME", 0xC00C, _NETBIOS_RNAMES), - ShortEnumField("RR_TYPE", 0x20, _NETBIOS_QRTYPES), - ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), - IntField("TTL", 0), - ShortField("RDLENGTH", 6), - BitEnumField("G", 0, 1, _NETBIOS_GNAMES), - BitEnumField("OWNER_NODE_TYPE", 00, 2, - _NETBIOS_OWNER_MODE_TYPES), - BitEnumField("UNUSED", 0, 13, {0: "Unused"}), - IPField("NB_ADDRESS", "127.0.0.1")] + def mysummary(self): + return "NBNSQueryRequest who has '\\\\%s'" % ( + self.QUESTION_NAME.decode(errors="backslashreplace") + ) + + +bind_layers(NBNSHeader, NBNSQueryRequest, + OPCODE=0x0, NM_FLAGS=0x11, QDCOUNT=1) + # Name Query Response -# Name Registration Response +# RFC1002 sect 4.2.13 + + +class NBNS_ADD_ENTRY(Packet): + fields_desc = [ + BitEnumField("G", 0, 1, _NETBIOS_GNAMES), + BitEnumField("OWNER_NODE_TYPE", 00, 2, + _NETBIOS_OWNER_MODE_TYPES), + BitEnumField("UNUSED", 0, 13, {0: "Unused"}), + IPField("NB_ADDRESS", "127.0.0.1") + ] class NBNSQueryResponse(Packet): name = "NBNS query response" - fields_desc = [ShortField("NAME_TRN_ID", 0), - ShortField("FLAGS", 0x8500), - ShortField("QDCOUNT", 0), - ShortField("ANCOUNT", 1), - ShortField("NSCOUNT", 0), - ShortField("ARCOUNT", 0), - NetBIOSNameField("RR_NAME", "windows"), + fields_desc = [NetBIOSNameField("RR_NAME", "windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("QUESTION_TYPE", 0x20, _NETBIOS_QRTYPES), ShortEnumField("QUESTION_CLASS", 1, _NETBIOS_QRCLASS), IntField("TTL", 0x493e0), - ShortField("RDLENGTH", 6), - ShortField("NB_FLAGS", 0), - IPField("NB_ADDRESS", "127.0.0.1")] - -# Name Query Response (negative) -# Name Release Response - - -class NBNSQueryResponseNegative(Packet): - name = "NBNS query response (negative)" - fields_desc = [ShortField("NAME_TRN_ID", 0), - ShortField("FLAGS", 0x8506), - ShortField("QDCOUNT", 0), - ShortField("ANCOUNT", 1), - ShortField("NSCOUNT", 0), - ShortField("ARCOUNT", 0), - NetBIOSNameField("RR_NAME", "windows"), - ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), - ByteField("NULL", 0), - ShortEnumField("RR_TYPE", 0x20, _NETBIOS_QRTYPES), - ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), - IntField("TTL", 0), - ShortField("RDLENGTH", 6), - BitEnumField("G", 0, 1, _NETBIOS_GNAMES), - BitEnumField("OWNER_NODE_TYPE", 00, 2, - _NETBIOS_OWNER_MODE_TYPES), - BitEnumField("UNUSED", 0, 13, {0: "Unused"}), - IPField("NB_ADDRESS", "127.0.0.1")] + FieldLenField("RDLENGTH", None, length_of="ADDR_ENTRY"), + PacketListField("ADDR_ENTRY", + [NBNS_ADD_ENTRY()], NBNS_ADD_ENTRY, + length_from=lambda pkt: pkt.RDLENGTH) + ] + + def mysummary(self): + if not self.ADDR_ENTRY or \ + not isinstance(self.ADDR_ENTRY[0], NBNS_ADD_ENTRY): + return "NBNSQueryResponse" + return "NBNSQueryResponse '\\\\%s' is at %s" % ( + self.RR_NAME.decode(errors="backslashreplace"), + self.ADDR_ENTRY[0].NB_ADDRESS + ) + + def answers(self, other): + return ( + isinstance(other, NBNSQueryRequest) and + other.QUESTION_NAME == self.RR_NAME + ) + + +bind_layers(NBNSHeader, NBNSQueryResponse, # RD+AA + OPCODE=0x0, NM_FLAGS=0x50, RESPONSE=1, ANCOUNT=1) +for _flg in [0x58, 0x70, 0x78]: + bind_bottom_up(NBNSHeader, NBNSQueryResponse, + OPCODE=0x0, NM_FLAGS=_flg, RESPONSE=1, ANCOUNT=1) -# Node Status Response +# Node Status Request +# RFC1002 sect 4.2.17 + +class NBNSNodeStatusRequest(NBNSQueryRequest): + name = "NBNS status request" + QUESTION_NAME = b"*" + b"\x00" * 14 + QUESTION_TYPE = 0x21 + + def mysummary(self): + return "NBNSNodeStatusRequest who has '\\\\%s'" % ( + self.QUESTION_NAME.decode(errors="backslashreplace") + ) -class NBNSNodeStatusResponse(Packet): - name = "NBNS Node Status Response" - fields_desc = [ShortField("NAME_TRN_ID", 0), - ShortField("FLAGS", 0x8500), - ShortField("QDCOUNT", 0), - ShortField("ANCOUNT", 1), - ShortField("NSCOUNT", 0), - ShortField("ARCOUNT", 0), - NetBIOSNameField("RR_NAME", "windows"), - ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), - ByteField("NULL", 0), - ShortEnumField("RR_TYPE", 0x21, _NETBIOS_QRTYPES), - ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), - IntField("TTL", 0), - ShortField("RDLENGTH", 83), - ByteField("NUM_NAMES", 1)] -# Service for Node Status Response +bind_bottom_up(NBNSHeader, NBNSNodeStatusRequest, OPCODE=0x0, NM_FLAGS=0, QDCOUNT=1) +bind_layers(NBNSHeader, NBNSNodeStatusRequest, OPCODE=0x0, NM_FLAGS=1, QDCOUNT=1) +# Node Status Response +# RFC1002 sect 4.2.18 + class NBNSNodeStatusResponseService(Packet): name = "NBNS Node Status Response Service" fields_desc = [StrFixedLenField("NETBIOS_NAME", "WINDOWS ", 15), @@ -220,26 +249,77 @@ class NBNSNodeStatusResponseService(Packet): ByteField("NAME_FLAGS", 0x4), ByteEnumField("UNUSED", 0, {0: "unused"})] -# End of Node Status Response packet + def default_payload_class(self, payload): + return conf.padding_layer -class NBNSNodeStatusResponseEnd(Packet): +class NBNSNodeStatusResponse(Packet): name = "NBNS Node Status Response" - fields_desc = [SourceMACField("MAC_ADDRESS"), - BitField("STATISTICS", 0, 57 * 8)] + fields_desc = [NetBIOSNameField("RR_NAME", "windows"), + ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), + ByteField("NULL", 0), + ShortEnumField("RR_TYPE", 0x21, _NETBIOS_QRTYPES), + ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), + IntField("TTL", 0), + ShortField("RDLENGTH", 83), + FieldLenField("NUM_NAMES", None, fmt="B", + count_of="NODE_NAME"), + PacketListField("NODE_NAME", + [NBNSNodeStatusResponseService()], + NBNSNodeStatusResponseService, + count_from=lambda pkt: pkt.NUM_NAMES), + SourceMACField("MAC_ADDRESS"), + XStrFixedLenField("STATISTICS", b"", 46)] + + def answers(self, other): + return ( + isinstance(other, NBNSNodeStatusRequest) and + other.QUESTION_NAME == self.RR_NAME + ) -# Wait for Acknowledgement Response +bind_layers(NBNSHeader, NBNSNodeStatusResponse, + OPCODE=0x0, NM_FLAGS=0x40, RESPONSE=1, ANCOUNT=1) + + +# Name Registration Request +# RFC1002 sect 4.2.2 + +class NBNSRegistrationRequest(Packet): + name = "NBNS registration request" + fields_desc = [ + NetBIOSNameField("QUESTION_NAME", "Windows"), + ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), + ByteField("NULL", 0), + ShortEnumField("QUESTION_TYPE", 0x20, _NETBIOS_QRTYPES), + ShortEnumField("QUESTION_CLASS", 1, _NETBIOS_QRCLASS), + ShortEnumField("RR_NAME", 0xC00C, _NETBIOS_RNAMES), + ShortEnumField("RR_TYPE", 0x20, _NETBIOS_QRTYPES), + ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), + IntField("TTL", 0), + ShortField("RDLENGTH", 6), + BitEnumField("G", 0, 1, _NETBIOS_GNAMES), + BitEnumField("OWNER_NODE_TYPE", 00, 2, + _NETBIOS_OWNER_MODE_TYPES), + BitEnumField("UNUSED", 0, 13, {0: "Unused"}), + IPField("NB_ADDRESS", "127.0.0.1") + ] + + def mysummary(self): + return self.sprintf("Register %G% %QUESTION_NAME% at %NB_ADDRESS%") + + +bind_bottom_up(NBNSHeader, NBNSRegistrationRequest, OPCODE=0x5) +bind_layers(NBNSHeader, NBNSRegistrationRequest, + OPCODE=0x5, NM_FLAGS=0x11, QDCOUNT=1, ARCOUNT=1) + + +# Wait for Acknowledgement Response +# RFC1002 sect 4.2.16 class NBNSWackResponse(Packet): name = "NBNS Wait for Acknowledgement Response" - fields_desc = [ShortField("NAME_TRN_ID", 0), - ShortField("FLAGS", 0xBC07), - ShortField("QDCOUNT", 0), - ShortField("ANCOUNT", 1), - ShortField("NSCOUNT", 0), - ShortField("ARCOUNT", 0), - NetBIOSNameField("RR_NAME", "windows"), + fields_desc = [NetBIOSNameField("RR_NAME", "windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("RR_TYPE", 0x20, _NETBIOS_QRTYPES), @@ -249,6 +329,12 @@ class NBNSWackResponse(Packet): BitField("RDATA", 10512, 16)] # 10512=0010100100010000 +bind_layers(NBNSHeader, NBNSWackResponse, + OPCODE=0x7, NM_FLAGS=0x40, RESPONSE=1, ANCOUNT=1) + +# NetBIOS DATAGRAM HEADER + + class NBTDatagram(Packet): name = "NBT Datagram Packet" fields_desc = [ByteField("Type", 0x10), @@ -256,18 +342,28 @@ class NBTDatagram(Packet): ShortField("ID", 0), IPField("SourceIP", "127.0.0.1"), ShortField("SourcePort", 138), - ShortField("Length", 272), + ShortField("Length", None), ShortField("Offset", 0), NetBIOSNameField("SourceName", "windows"), ShortEnumField("SUFFIX1", 0x4141, _NETBIOS_SUFFIXES), - ByteField("NULL", 0), + ByteField("NULL1", 0), NetBIOSNameField("DestinationName", "windows"), ShortEnumField("SUFFIX2", 0x4141, _NETBIOS_SUFFIXES), - ByteField("NULL", 0)] + ByteField("NULL2", 0)] + + def post_build(self, pkt, pay): + if self.Length is None: + length = len(pay) + 68 + pkt = pkt[:10] + struct.pack("!H", length) + pkt[12:] + return pkt + pay + + +# SESSION SERVICE PACKETS class NBTSession(Packet): name = "NBT Session Packet" + MAXLENGTH = 0x3ffff fields_desc = [ByteEnumField("TYPE", 0, {0x00: "Session Message", 0x81: "Session Request", 0x82: "Positive Session Response", @@ -275,18 +371,170 @@ class NBTSession(Packet): 0x84: "Retarget Session Response", 0x85: "Session Keepalive"}), BitField("RESERVED", 0x00, 7), - BitField("LENGTH", 0, 17)] - - -bind_layers(UDP, NBNSQueryRequest, dport=137) -bind_layers(UDP, NBNSRequest, dport=137) -bind_layers(UDP, NBNSQueryResponse, sport=137) -bind_layers(UDP, NBNSQueryResponseNegative, sport=137) -bind_layers(UDP, NBNSNodeStatusResponse, sport=137) -bind_layers(NBNSNodeStatusResponse, NBNSNodeStatusResponseService, ) -bind_layers(NBNSNodeStatusResponse, NBNSNodeStatusResponseService, ) -bind_layers(NBNSNodeStatusResponseService, NBNSNodeStatusResponseService, ) -bind_layers(NBNSNodeStatusResponseService, NBNSNodeStatusResponseEnd, ) -bind_layers(UDP, NBNSWackResponse, sport=137) -bind_layers(UDP, NBTDatagram, dport=138) -bind_layers(TCP, NBTSession, dport=139) + BitField("LENGTH", None, 17)] + + def post_build(self, pkt, pay): + if self.LENGTH is None: + length = len(pay) & self.MAXLENGTH + pkt = pkt[:1] + struct.pack("!I", length)[1:] + return pkt + pay + + def extract_padding(self, s): + return s[:self.LENGTH], s[self.LENGTH:] + + @classmethod + def tcp_reassemble(cls, data, *args, **kwargs): + if len(data) < 4: + return None + length = struct.unpack("!I", data[:4])[0] & cls.MAXLENGTH + if len(data) >= length + 4: + return cls(data) + + +bind_bottom_up(UDP, NBNSHeader, dport=137) +bind_bottom_up(UDP, NBNSHeader, sport=137) +bind_top_down(UDP, NBNSHeader, sport=137, dport=137) + +bind_bottom_up(UDP, NBTDatagram, dport=138) +bind_bottom_up(UDP, NBTDatagram, sport=138) +bind_top_down(UDP, NBTDatagram, sport=138, dport=138) + +bind_bottom_up(TCP, NBTSession, dport=445) +bind_bottom_up(TCP, NBTSession, sport=445) +bind_bottom_up(TCP, NBTSession, dport=139) +bind_bottom_up(TCP, NBTSession, sport=139) +bind_layers(TCP, NBTSession, dport=139, sport=139) + + +_nbns_cache = conf.netcache.new_cache("nbns_cache", 300) + + +@conf.commands.register +def nbns_resolve( + qname: str, + iface: Union[_GlobInterfaceType, List[_GlobInterfaceType]] = None, + raw: bool = False, + timeout: int = 3, + **kwargs, +) -> List[str]: + """ + Perform a simple NBNS (NetBios Name Services) resolution with caching + + :param qname: the name to query + :param iface: the interfaces to use. (default: all) + :param raw: return the whole netbios packet (default False) + :param timeout: seconds until timeout (per server) + :raise TimeoutError: if no DNS servers were reached in time. + """ + kwargs.setdefault("verbose", 0) + + # Unify types (for caching) + qname = NBNSQueryRequest.QUESTION_NAME.any2i(None, qname) + + # Check cache + cache_ident = qname + b"raw" if raw else b"" + result = _nbns_cache.get(cache_ident) + if result: + return result + + if iface is None: + ifaces = [ + x + for name, x in conf.ifaces.items() + if x.is_valid() and name != conf.loopback_name + ] + elif isinstance(iface, list): + ifaces = iface + else: + ifaces = [iface] + + # Builds a request for each broadcast address of each interface + requests = [] + for iface in ifaces: + for bdcst in conf.route.get_if_bcast(iface): + if bdcst == "255.255.255.255": + continue + requests.append( + IP(dst=bdcst) / + UDP() / + NBNSHeader() / + NBNSQueryRequest(QUESTION_NAME=qname) + ) + + if not requests: + return None + + # Perform requests, get the first response + try: + old_checkIPAddr = conf.checkIPaddr + conf.checkIPaddr = False + + res = sr1( + requests, + timeout=timeout, + first=True, + **kwargs, + ) + finally: + conf.checkIPaddr = old_checkIPAddr + + if res is not None: + if raw: + # Raw + result = res + else: + # Get IP + result = [x.NB_ADDRESS for x in res.ADDR_ENTRY] + if result: + # Cache it + _nbns_cache[cache_ident] = result + return result + else: + raise TimeoutError + + +class NBNS_am(AnsweringMachine): + function_name = "nbnsd" + filter = "udp port 137" + sniff_options = {"store": 0} + + def parse_options(self, server_name=None, from_ip=None, ip=None): + """ + NBNS answering machine + + :param server_name: the netbios server name to match + :param from_ip: an IP (can have a netmask) to filter on + :param ip: the IP to answer with + """ + self.ServerName = bytes_encode(server_name or "") + self.ip = ip + if isinstance(from_ip, str): + self.from_ip = Net(from_ip) + else: + self.from_ip = from_ip + + def is_request(self, req): + if self.from_ip and IP in req and req[IP].src not in self.from_ip: + return False + return NBNSQueryRequest in req and ( + not self.ServerName or + req[NBNSQueryRequest].QUESTION_NAME.strip() == self.ServerName + ) + + def make_reply(self, req): + # type: (Packet) -> Packet + resp = Ether( + dst=req[Ether].src, + src=None if req[Ether].dst == "ff:ff:ff:ff:ff:ff" else req[Ether].dst, + ) / IP(dst=req[IP].src) / UDP( + sport=req.dport, + dport=req.sport, + ) + address = self.ip or get_if_addr(self.optsniff.get("iface", conf.iface)) + resp /= NBNSHeader() / NBNSQueryResponse( + RR_NAME=self.ServerName or req.QUESTION_NAME, + SUFFIX=req.SUFFIX, + ADDR_ENTRY=[NBNS_ADD_ENTRY(NB_ADDRESS=address)] + ) + resp.NAME_TRN_ID = req.NAME_TRN_ID + return resp diff --git a/scapy/layers/netflow.py b/scapy/layers/netflow.py index 3f4740eb417..04d8e9fbac2 100644 --- a/scapy/layers/netflow.py +++ b/scapy/layers/netflow.py @@ -1,9 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license + # Netflow V5 appended by spaceB0x and Guillaume Valadon -# Netflow V9/10 appended ny Gabriel Potter +# Netflow V9/10 appended by Gabriel Potter """ Cisco NetFlow protocol v1, v5, v9 and v10 (IPFix) @@ -27,27 +28,59 @@ >>> sniff(session=NetflowSession, prn=[...]) +.. note:: You will find more examples over + https://scapy.readthedocs.io/en/latest/layers/netflow.html """ +import dataclasses import socket import struct +from collections import Counter + from scapy.config import conf from scapy.data import IP_PROTOS from scapy.error import warning, Scapy_Exception -from scapy.fields import ByteEnumField, ByteField, Field, FieldLenField, \ - FlagsField, IPField, IntField, MACField, \ - PacketListField, PadField, SecondsIntField, ShortEnumField, ShortField, \ - StrField, StrFixedLenField, ThreeBytesField, UTCTimeField, XByteField, \ - XShortField, LongField, BitField, ConditionalField, BitEnumField, \ - StrLenField +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + Field, + FieldLenField, + FlagsField, + IntField, + IPField, + LongField, + MACField, + NBytesField, + PacketListField, + SecondsIntField, + ShortEnumField, + ShortField, + StrField, + StrFixedLenField, + StrLenField, + ThreeBytesField, + UTCTimeField, + XByteField, + XShortField, +) from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.plist import PacketList -from scapy.sessions import IPSession, DefaultSession +from scapy.sessions import IPSession from scapy.layers.inet import UDP from scapy.layers.inet6 import IP6Field +# Typing imports +from typing import ( + Any, + Dict, + Optional, +) + class NetflowHeader(Packet): name = "Netflow Header" @@ -67,11 +100,17 @@ class NetflowHeader(Packet): class NetflowHeaderV1(Packet): name = "Netflow Header v1" - fields_desc = [ShortField("count", 0), + fields_desc = [ShortField("count", None), IntField("sysUptime", 0), UTCTimeField("unixSecs", 0), UTCTimeField("unixNanoSeconds", 0, use_nano=True)] + def post_build(self, pkt, pay): + if self.count is None: + count = len(self.layers()) - 1 + pkt = struct.pack("!H", count) + pkt[2:] + return pkt + pay + class NetflowRecordV1(Packet): name = "Netflow Record v1" @@ -105,7 +144,7 @@ class NetflowRecordV1(Packet): class NetflowHeaderV5(Packet): name = "Netflow Header v5" - fields_desc = [ShortField("count", 0), + fields_desc = [ShortField("count", None), IntField("sysUptime", 0), UTCTimeField("unixSecs", 0), UTCTimeField("unixNanoSeconds", 0, use_nano=True), @@ -114,6 +153,12 @@ class NetflowHeaderV5(Packet): ByteField("engineID", 0), ShortField("samplingInterval", 0)] + def post_build(self, pkt, pay): + if self.count is None: + count = len(self.layers()) - 1 + pkt = struct.pack("!H", count) + pkt[2:] + return pkt + pay + class NetflowRecordV5(Packet): name = "Netflow Record v5" @@ -154,916 +199,18 @@ class NetflowRecordV5(Packet): # https://tools.ietf.org/html/rfc5101 # https://tools.ietf.org/html/rfc5655 -# This is v9_v10_template_types (with names from the rfc for the first 79) -# https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-netflow.c # noqa: E501 -# (it has all values external to the RFC) -NTOP_BASE = 57472 -NetflowV910TemplateFieldTypes = { - 1: "IN_BYTES", - 2: "IN_PKTS", - 3: "FLOWS", - 4: "PROTOCOL", - 5: "TOS", - 6: "TCP_FLAGS", - 7: "L4_SRC_PORT", - 8: "IPV4_SRC_ADDR", - 9: "SRC_MASK", - 10: "INPUT_SNMP", - 11: "L4_DST_PORT", - 12: "IPV4_DST_ADDR", - 13: "DST_MASK", - 14: "OUTPUT_SNMP", - 15: "IPV4_NEXT_HOP", - 16: "SRC_AS", - 17: "DST_AS", - 18: "BGP_IPV4_NEXT_HOP", - 19: "MUL_DST_PKTS", - 20: "MUL_DST_BYTES", - 21: "LAST_SWITCHED", - 22: "FIRST_SWITCHED", - 23: "OUT_BYTES", - 24: "OUT_PKTS", - 25: "IP_LENGTH_MINIMUM", - 26: "IP_LENGTH_MAXIMUM", - 27: "IPV6_SRC_ADDR", - 28: "IPV6_DST_ADDR", - 29: "IPV6_SRC_MASK", - 30: "IPV6_DST_MASK", - 31: "IPV6_FLOW_LABEL", - 32: "ICMP_TYPE", - 33: "MUL_IGMP_TYPE", - 34: "SAMPLING_INTERVAL", - 35: "SAMPLING_ALGORITHM", - 36: "FLOW_ACTIVE_TIMEOUT", - 37: "FLOW_INACTIVE_TIMEOUT", - 38: "ENGINE_TYPE", - 39: "ENGINE_ID", - 40: "TOTAL_BYTES_EXP", - 41: "TOTAL_PKTS_EXP", - 42: "TOTAL_FLOWS_EXP", - 43: "IPV4_ROUTER_SC", - 44: "IP_SRC_PREFIX", - 45: "IP_DST_PREFIX", - 46: "MPLS_TOP_LABEL_TYPE", - 47: "MPLS_TOP_LABEL_IP_ADDR", - 48: "FLOW_SAMPLER_ID", - 49: "FLOW_SAMPLER_MODE", - 50: "FLOW_SAMPLER_RANDOM_INTERVAL", - 51: "FLOW_CLASS", - 52: "IP TTL MINIMUM", - 53: "IP TTL MAXIMUM", - 54: "IPv4 ID", - 55: "DST_TOS", - 56: "SRC_MAC", - 57: "DST_MAC", - 58: "SRC_VLAN", - 59: "DST_VLAN", - 60: "IP_PROTOCOL_VERSION", - 61: "DIRECTION", - 62: "IPV6_NEXT_HOP", - 63: "BGP_IPV6_NEXT_HOP", - 64: "IPV6_OPTION_HEADERS", - 70: "MPLS_LABEL_1", - 71: "MPLS_LABEL_2", - 72: "MPLS_LABEL_3", - 73: "MPLS_LABEL_4", - 74: "MPLS_LABEL_5", - 75: "MPLS_LABEL_6", - 76: "MPLS_LABEL_7", - 77: "MPLS_LABEL_8", - 78: "MPLS_LABEL_9", - 79: "MPLS_LABEL_10", - 80: "DESTINATION_MAC", - 81: "SOURCE_MAC", - 82: "IF_NAME", - 83: "IF_DESC", - 84: "SAMPLER_NAME", - 85: "BYTES_TOTAL", - 86: "PACKETS_TOTAL", - 88: "FRAGMENT_OFFSET", - 89: "FORWARDING_STATUS", - 90: "VPN_ROUTE_DISTINGUISHER", - 91: "mplsTopLabelPrefixLength", - 92: "SRC_TRAFFIC_INDEX", - 93: "DST_TRAFFIC_INDEX", - 94: "APPLICATION_DESC", - 95: "APPLICATION_ID", - 96: "APPLICATION_NAME", - 98: "postIpDiffServCodePoint", - 99: "multicastReplicationFactor", - 101: "classificationEngineId", - 128: "DST_AS_PEER", - 129: "SRC_AS_PEER", - 130: "exporterIPv4Address", - 131: "exporterIPv6Address", - 132: "DROPPED_BYTES", - 133: "DROPPED_PACKETS", - 134: "DROPPED_BYTES_TOTAL", - 135: "DROPPED_PACKETS_TOTAL", - 136: "flowEndReason", - 137: "commonPropertiesId", - 138: "observationPointId", - 139: "icmpTypeCodeIPv6", - 140: "MPLS_TOP_LABEL_IPv6_ADDRESS", - 141: "lineCardId", - 142: "portId", - 143: "meteringProcessId", - 144: "FLOW_EXPORTER", - 145: "templateId", - 146: "wlanChannelId", - 147: "wlanSSID", - 148: "flowId", - 149: "observationDomainId", - 150: "flowStartSeconds", - 151: "flowEndSeconds", - 152: "flowStartMilliseconds", - 153: "flowEndMilliseconds", - 154: "flowStartMicroseconds", - 155: "flowEndMicroseconds", - 156: "flowStartNanoseconds", - 157: "flowEndNanoseconds", - 158: "flowStartDeltaMicroseconds", - 159: "flowEndDeltaMicroseconds", - 160: "systemInitTimeMilliseconds", - 161: "flowDurationMilliseconds", - 162: "flowDurationMicroseconds", - 163: "observedFlowTotalCount", - 164: "ignoredPacketTotalCount", - 165: "ignoredOctetTotalCount", - 166: "notSentFlowTotalCount", - 167: "notSentPacketTotalCount", - 168: "notSentOctetTotalCount", - 169: "destinationIPv6Prefix", - 170: "sourceIPv6Prefix", - 171: "postOctetTotalCount", - 172: "postPacketTotalCount", - 173: "flowKeyIndicator", - 174: "postMCastPacketTotalCount", - 175: "postMCastOctetTotalCount", - 176: "ICMP_IPv4_TYPE", - 177: "ICMP_IPv4_CODE", - 178: "ICMP_IPv6_TYPE", - 179: "ICMP_IPv6_CODE", - 180: "UDP_SRC_PORT", - 181: "UDP_DST_PORT", - 182: "TCP_SRC_PORT", - 183: "TCP_DST_PORT", - 184: "TCP_SEQ_NUM", - 185: "TCP_ACK_NUM", - 186: "TCP_WINDOW_SIZE", - 187: "TCP_URGENT_PTR", - 188: "TCP_HEADER_LEN", - 189: "IP_HEADER_LEN", - 190: "IP_TOTAL_LEN", - 191: "payloadLengthIPv6", - 192: "IP_TTL", - 193: "nextHeaderIPv6", - 194: "mplsPayloadLength", - 195: "IP_DSCP", - 196: "IP_PRECEDENCE", - 197: "IP_FRAGMENT_FLAGS", - 198: "DELTA_BYTES_SQUARED", - 199: "TOTAL_BYTES_SQUARED", - 200: "MPLS_TOP_LABEL_TTL", - 201: "MPLS_LABEL_STACK_OCTETS", - 202: "MPLS_LABEL_STACK_DEPTH", - 203: "MPLS_TOP_LABEL_EXP", - 204: "IP_PAYLOAD_LENGTH", - 205: "UDP_LENGTH", - 206: "IS_MULTICAST", - 207: "IP_HEADER_WORDS", - 208: "IP_OPTION_MAP", - 209: "TCP_OPTION_MAP", - 210: "paddingOctets", - 211: "collectorIPv4Address", - 212: "collectorIPv6Address", - 213: "collectorInterface", - 214: "collectorProtocolVersion", - 215: "collectorTransportProtocol", - 216: "collectorTransportPort", - 217: "exporterTransportPort", - 218: "tcpSynTotalCount", - 219: "tcpFinTotalCount", - 220: "tcpRstTotalCount", - 221: "tcpPshTotalCount", - 222: "tcpAckTotalCount", - 223: "tcpUrgTotalCount", - 224: "ipTotalLength", - 225: "postNATSourceIPv4Address", - 226: "postNATDestinationIPv4Address", - 227: "postNAPTSourceTransportPort", - 228: "postNAPTDestinationTransportPort", - 229: "natOriginatingAddressRealm", - 230: "natEvent", - 231: "initiatorOctets", - 232: "responderOctets", - 233: "firewallEvent", - 234: "ingressVRFID", - 235: "egressVRFID", - 236: "VRFname", - 237: "postMplsTopLabelExp", - 238: "tcpWindowScale", - 239: "biflowDirection", - 240: "ethernetHeaderLength", - 241: "ethernetPayloadLength", - 242: "ethernetTotalLength", - 243: "dot1qVlanId", - 244: "dot1qPriority", - 245: "dot1qCustomerVlanId", - 246: "dot1qCustomerPriority", - 247: "metroEvcId", - 248: "metroEvcType", - 249: "pseudoWireId", - 250: "pseudoWireType", - 251: "pseudoWireControlWord", - 252: "ingressPhysicalInterface", - 253: "egressPhysicalInterface", - 254: "postDot1qVlanId", - 255: "postDot1qCustomerVlanId", - 256: "ethernetType", - 257: "postIpPrecedence", - 258: "collectionTimeMilliseconds", - 259: "exportSctpStreamId", - 260: "maxExportSeconds", - 261: "maxFlowEndSeconds", - 262: "messageMD5Checksum", - 263: "messageScope", - 264: "minExportSeconds", - 265: "minFlowStartSeconds", - 266: "opaqueOctets", - 267: "sessionScope", - 268: "maxFlowEndMicroseconds", - 269: "maxFlowEndMilliseconds", - 270: "maxFlowEndNanoseconds", - 271: "minFlowStartMicroseconds", - 272: "minFlowStartMilliseconds", - 273: "minFlowStartNanoseconds", - 274: "collectorCertificate", - 275: "exporterCertificate", - 276: "dataRecordsReliability", - 277: "observationPointType", - 278: "newConnectionDeltaCount", - 279: "connectionSumDurationSeconds", - 280: "connectionTransactionId", - 281: "postNATSourceIPv6Address", - 282: "postNATDestinationIPv6Address", - 283: "natPoolId", - 284: "natPoolName", - 285: "anonymizationFlags", - 286: "anonymizationTechnique", - 287: "informationElementIndex", - 288: "p2pTechnology", - 289: "tunnelTechnology", - 290: "encryptedTechnology", - 291: "basicList", - 292: "subTemplateList", - 293: "subTemplateMultiList", - 294: "bgpValidityState", - 295: "IPSecSPI", - 296: "greKey", - 297: "natType", - 298: "initiatorPackets", - 299: "responderPackets", - 300: "observationDomainName", - 301: "selectionSequenceId", - 302: "selectorId", - 303: "informationElementId", - 304: "selectorAlgorithm", - 305: "samplingPacketInterval", - 306: "samplingPacketSpace", - 307: "samplingTimeInterval", - 308: "samplingTimeSpace", - 309: "samplingSize", - 310: "samplingPopulation", - 311: "samplingProbability", - 312: "dataLinkFrameSize", - 313: "IP_SECTION HEADER", - 314: "IP_SECTION PAYLOAD", - 315: "dataLinkFrameSection", - 316: "mplsLabelStackSection", - 317: "mplsPayloadPacketSection", - 318: "selectorIdTotalPktsObserved", - 319: "selectorIdTotalPktsSelected", - 320: "absoluteError", - 321: "relativeError", - 322: "observationTimeSeconds", - 323: "observationTimeMilliseconds", - 324: "observationTimeMicroseconds", - 325: "observationTimeNanoseconds", - 326: "digestHashValue", - 327: "hashIPPayloadOffset", - 328: "hashIPPayloadSize", - 329: "hashOutputRangeMin", - 330: "hashOutputRangeMax", - 331: "hashSelectedRangeMin", - 332: "hashSelectedRangeMax", - 333: "hashDigestOutput", - 334: "hashInitialiserValue", - 335: "selectorName", - 336: "upperCILimit", - 337: "lowerCILimit", - 338: "confidenceLevel", - 339: "informationElementDataType", - 340: "informationElementDescription", - 341: "informationElementName", - 342: "informationElementRangeBegin", - 343: "informationElementRangeEnd", - 344: "informationElementSemantics", - 345: "informationElementUnits", - 346: "privateEnterpriseNumber", - 347: "virtualStationInterfaceId", - 348: "virtualStationInterfaceName", - 349: "virtualStationUUID", - 350: "virtualStationName", - 351: "layer2SegmentId", - 352: "layer2OctetDeltaCount", - 353: "layer2OctetTotalCount", - 354: "ingressUnicastPacketTotalCount", - 355: "ingressMulticastPacketTotalCount", - 356: "ingressBroadcastPacketTotalCount", - 357: "egressUnicastPacketTotalCount", - 358: "egressBroadcastPacketTotalCount", - 359: "monitoringIntervalStartMilliSeconds", - 360: "monitoringIntervalEndMilliSeconds", - 361: "portRangeStart", - 362: "portRangeEnd", - 363: "portRangeStepSize", - 364: "portRangeNumPorts", - 365: "staMacAddress", - 366: "staIPv4Address", - 367: "wtpMacAddress", - 368: "ingressInterfaceType", - 369: "egressInterfaceType", - 370: "rtpSequenceNumber", - 371: "userName", - 372: "applicationCategoryName", - 373: "applicationSubCategoryName", - 374: "applicationGroupName", - 375: "originalFlowsPresent", - 376: "originalFlowsInitiated", - 377: "originalFlowsCompleted", - 378: "distinctCountOfSourceIPAddress", - 379: "distinctCountOfDestinationIPAddress", - 380: "distinctCountOfSourceIPv4Address", - 381: "distinctCountOfDestinationIPv4Address", - 382: "distinctCountOfSourceIPv6Address", - 383: "distinctCountOfDestinationIPv6Address", - 384: "valueDistributionMethod", - 385: "rfc3550JitterMilliseconds", - 386: "rfc3550JitterMicroseconds", - 387: "rfc3550JitterNanoseconds", - 388: "dot1qDEI", - 389: "dot1qCustomerDEI", - 390: "flowSelectorAlgorithm", - 391: "flowSelectedOctetDeltaCount", - 392: "flowSelectedPacketDeltaCount", - 393: "flowSelectedFlowDeltaCount", - 394: "selectorIDTotalFlowsObserved", - 395: "selectorIDTotalFlowsSelected", - 396: "samplingFlowInterval", - 397: "samplingFlowSpacing", - 398: "flowSamplingTimeInterval", - 399: "flowSamplingTimeSpacing", - 400: "hashFlowDomain", - 401: "transportOctetDeltaCount", - 402: "transportPacketDeltaCount", - 403: "originalExporterIPv4Address", - 404: "originalExporterIPv6Address", - 405: "originalObservationDomainId", - 406: "intermediateProcessId", - 407: "ignoredDataRecordTotalCount", - 408: "dataLinkFrameType", - 409: "sectionOffset", - 410: "sectionExportedOctets", - 411: "dot1qServiceInstanceTag", - 412: "dot1qServiceInstanceId", - 413: "dot1qServiceInstancePriority", - 414: "dot1qCustomerSourceMacAddress", - 415: "dot1qCustomerDestinationMacAddress", - 416: "deprecated [dup of layer2OctetDeltaCount]", - 417: "postLayer2OctetDeltaCount", - 418: "postMCastLayer2OctetDeltaCount", - 419: "deprecated [dup of layer2OctetTotalCount", - 420: "postLayer2OctetTotalCount", - 421: "postMCastLayer2OctetTotalCount", - 422: "minimumLayer2TotalLength", - 423: "maximumLayer2TotalLength", - 424: "droppedLayer2OctetDeltaCount", - 425: "droppedLayer2OctetTotalCount", - 426: "ignoredLayer2OctetTotalCount", - 427: "notSentLayer2OctetTotalCount", - 428: "layer2OctetDeltaSumOfSquares", - 429: "layer2OctetTotalSumOfSquares", - 430: "layer2FrameDeltaCount", - 431: "layer2FrameTotalCount", - 432: "pseudoWireDestinationIPv4Address", - 433: "ignoredLayer2FrameTotalCount", - 434: "mibObjectValueInteger", - 435: "mibObjectValueOctetString", - 436: "mibObjectValueOID", - 437: "mibObjectValueBits", - 438: "mibObjectValueIPAddress", - 439: "mibObjectValueCounter", - 440: "mibObjectValueGauge", - 441: "mibObjectValueTimeTicks", - 442: "mibObjectValueUnsigned", - 443: "mibObjectValueTable", - 444: "mibObjectValueRow", - 445: "mibObjectIdentifier", - 446: "mibSubIdentifier", - 447: "mibIndexIndicator", - 448: "mibCaptureTimeSemantics", - 449: "mibContextEngineID", - 450: "mibContextName", - 451: "mibObjectName", - 452: "mibObjectDescription", - 453: "mibObjectSyntax", - 454: "mibModuleName", - 455: "mobileIMSI", - 456: "mobileMSISDN", - 457: "httpStatusCode", - 458: "sourceTransportPortsLimit", - 459: "httpRequestMethod", - 460: "httpRequestHost", - 461: "httpRequestTarget", - 462: "httpMessageVersion", - 463: "natInstanceID", - 464: "internalAddressRealm", - 465: "externalAddressRealm", - 466: "natQuotaExceededEvent", - 467: "natThresholdEvent", - 468: "httpUserAgent", - 469: "httpContentType", - 470: "httpReasonPhrase", - 471: "maxSessionEntries", - 472: "maxBIBEntries", - 473: "maxEntriesPerUser", - 474: "maxSubscribers", - 475: "maxFragmentsPendingReassembly", - 476: "addressPoolHighThreshold", - 477: "addressPoolLowThreshold", - 478: "addressPortMappingHighThreshold", - 479: "addressPortMappingLowThreshold", - 480: "addressPortMappingPerUserHighThreshold", - 481: "globalAddressMappingHighThreshold", - - # Ericsson NAT Logging - 24628: "NAT_LOG_FIELD_IDX_CONTEXT_ID", - 24629: "NAT_LOG_FIELD_IDX_CONTEXT_NAME", - 24630: "NAT_LOG_FIELD_IDX_ASSIGN_TS_SEC", - 24631: "NAT_LOG_FIELD_IDX_UNASSIGN_TS_SEC", - 24632: "NAT_LOG_FIELD_IDX_IPV4_INT_ADDR", - 24633: "NAT_LOG_FIELD_IDX_IPV4_EXT_ADDR", - 24634: "NAT_LOG_FIELD_IDX_EXT_PORT_FIRST", - 24635: "NAT_LOG_FIELD_IDX_EXT_PORT_LAST", - # Cisco ASA5500 Series NetFlow - 33000: "INGRESS_ACL_ID", - 33001: "EGRESS_ACL_ID", - 33002: "FW_EXT_EVENT", - # Cisco TrustSec - 34000: "SGT_SOURCE_TAG", - 34001: "SGT_DESTINATION_TAG", - 34002: "SGT_SOURCE_NAME", - 34003: "SGT_DESTINATION_NAME", - # medianet performance monitor - 37000: "PACKETS_DROPPED", - 37003: "BYTE_RATE", - 37004: "APPLICATION_MEDIA_BYTES", - 37006: "APPLICATION_MEDIA_BYTE_RATE", - 37007: "APPLICATION_MEDIA_PACKETS", - 37009: "APPLICATION_MEDIA_PACKET_RATE", - 37011: "APPLICATION_MEDIA_EVENT", - 37012: "MONITOR_EVENT", - 37013: "TIMESTAMP_INTERVAL", - 37014: "TRANSPORT_PACKETS_EXPECTED", - 37016: "TRANSPORT_ROUND_TRIP_TIME", - 37017: "TRANSPORT_EVENT_PACKET_LOSS", - 37019: "TRANSPORT_PACKETS_LOST", - 37021: "TRANSPORT_PACKETS_LOST_RATE", - 37022: "TRANSPORT_RTP_SSRC", - 37023: "TRANSPORT_RTP_JITTER_MEAN", - 37024: "TRANSPORT_RTP_JITTER_MIN", - 37025: "TRANSPORT_RTP_JITTER_MAX", - 37041: "TRANSPORT_RTP_PAYLOAD_TYPE", - 37071: "TRANSPORT_BYTES_OUT_OF_ORDER", - 37074: "TRANSPORT_PACKETS_OUT_OF_ORDER", - 37083: "TRANSPORT_TCP_WINDOWS_SIZE_MIN", - 37084: "TRANSPORT_TCP_WINDOWS_SIZE_MAX", - 37085: "TRANSPORT_TCP_WINDOWS_SIZE_MEAN", - 37086: "TRANSPORT_TCP_MAXIMUM_SEGMENT_SIZE", - # Cisco ASA 5500 - 40000: "AAA_USERNAME", - 40001: "XLATE_SRC_ADDR_IPV4", - 40002: "XLATE_DST_ADDR_IPV4", - 40003: "XLATE_SRC_PORT", - 40004: "XLATE_DST_PORT", - 40005: "FW_EVENT", - # v9 nTop extensions - 80 + NTOP_BASE: "SRC_FRAGMENTS", - 81 + NTOP_BASE: "DST_FRAGMENTS", - 82 + NTOP_BASE: "SRC_TO_DST_MAX_THROUGHPUT", - 83 + NTOP_BASE: "SRC_TO_DST_MIN_THROUGHPUT", - 84 + NTOP_BASE: "SRC_TO_DST_AVG_THROUGHPUT", - 85 + NTOP_BASE: "SRC_TO_SRC_MAX_THROUGHPUT", - 86 + NTOP_BASE: "SRC_TO_SRC_MIN_THROUGHPUT", - 87 + NTOP_BASE: "SRC_TO_SRC_AVG_THROUGHPUT", - 88 + NTOP_BASE: "NUM_PKTS_UP_TO_128_BYTES", - 89 + NTOP_BASE: "NUM_PKTS_128_TO_256_BYTES", - 90 + NTOP_BASE: "NUM_PKTS_256_TO_512_BYTES", - 91 + NTOP_BASE: "NUM_PKTS_512_TO_1024_BYTES", - 92 + NTOP_BASE: "NUM_PKTS_1024_TO_1514_BYTES", - 93 + NTOP_BASE: "NUM_PKTS_OVER_1514_BYTES", - 98 + NTOP_BASE: "CUMULATIVE_ICMP_TYPE", - 101 + NTOP_BASE: "SRC_IP_COUNTRY", - 102 + NTOP_BASE: "SRC_IP_CITY", - 103 + NTOP_BASE: "DST_IP_COUNTRY", - 104 + NTOP_BASE: "DST_IP_CITY", - 105 + NTOP_BASE: "FLOW_PROTO_PORT", - 106 + NTOP_BASE: "UPSTREAM_TUNNEL_ID", - 107 + NTOP_BASE: "LONGEST_FLOW_PKT", - 108 + NTOP_BASE: "SHORTEST_FLOW_PKT", - 109 + NTOP_BASE: "RETRANSMITTED_IN_PKTS", - 110 + NTOP_BASE: "RETRANSMITTED_OUT_PKTS", - 111 + NTOP_BASE: "OOORDER_IN_PKTS", - 112 + NTOP_BASE: "OOORDER_OUT_PKTS", - 113 + NTOP_BASE: "UNTUNNELED_PROTOCOL", - 114 + NTOP_BASE: "UNTUNNELED_IPV4_SRC_ADDR", - 115 + NTOP_BASE: "UNTUNNELED_L4_SRC_PORT", - 116 + NTOP_BASE: "UNTUNNELED_IPV4_DST_ADDR", - 117 + NTOP_BASE: "UNTUNNELED_L4_DST_PORT", - 118 + NTOP_BASE: "L7_PROTO", - 119 + NTOP_BASE: "L7_PROTO_NAME", - 120 + NTOP_BASE: "DOWNSTREAM_TUNNEL_ID", - 121 + NTOP_BASE: "FLOW_USER_NAME", - 122 + NTOP_BASE: "FLOW_SERVER_NAME", - 123 + NTOP_BASE: "CLIENT_NW_LATENCY_MS", - 124 + NTOP_BASE: "SERVER_NW_LATENCY_MS", - 125 + NTOP_BASE: "APPL_LATENCY_MS", - 126 + NTOP_BASE: "PLUGIN_NAME", - 127 + NTOP_BASE: "RETRANSMITTED_IN_BYTES", - 128 + NTOP_BASE: "RETRANSMITTED_OUT_BYTES", - 130 + NTOP_BASE: "SIP_CALL_ID", - 131 + NTOP_BASE: "SIP_CALLING_PARTY", - 132 + NTOP_BASE: "SIP_CALLED_PARTY", - 133 + NTOP_BASE: "SIP_RTP_CODECS", - 134 + NTOP_BASE: "SIP_INVITE_TIME", - 135 + NTOP_BASE: "SIP_TRYING_TIME", - 136 + NTOP_BASE: "SIP_RINGING_TIME", - 137 + NTOP_BASE: "SIP_INVITE_OK_TIME", - 138 + NTOP_BASE: "SIP_INVITE_FAILURE_TIME", - 139 + NTOP_BASE: "SIP_BYE_TIME", - 140 + NTOP_BASE: "SIP_BYE_OK_TIME", - 141 + NTOP_BASE: "SIP_CANCEL_TIME", - 142 + NTOP_BASE: "SIP_CANCEL_OK_TIME", - 143 + NTOP_BASE: "SIP_RTP_IPV4_SRC_ADDR", - 144 + NTOP_BASE: "SIP_RTP_L4_SRC_PORT", - 145 + NTOP_BASE: "SIP_RTP_IPV4_DST_ADDR", - 146 + NTOP_BASE: "SIP_RTP_L4_DST_PORT", - 147 + NTOP_BASE: "SIP_RESPONSE_CODE", - 148 + NTOP_BASE: "SIP_REASON_CAUSE", - 150 + NTOP_BASE: "RTP_FIRST_SEQ", - 151 + NTOP_BASE: "RTP_FIRST_TS", - 152 + NTOP_BASE: "RTP_LAST_SEQ", - 153 + NTOP_BASE: "RTP_LAST_TS", - 154 + NTOP_BASE: "RTP_IN_JITTER", - 155 + NTOP_BASE: "RTP_OUT_JITTER", - 156 + NTOP_BASE: "RTP_IN_PKT_LOST", - 157 + NTOP_BASE: "RTP_OUT_PKT_LOST", - 158 + NTOP_BASE: "RTP_OUT_PAYLOAD_TYPE", - 159 + NTOP_BASE: "RTP_IN_MAX_DELTA", - 160 + NTOP_BASE: "RTP_OUT_MAX_DELTA", - 161 + NTOP_BASE: "RTP_IN_PAYLOAD_TYPE", - 168 + NTOP_BASE: "SRC_PROC_PID", - 169 + NTOP_BASE: "SRC_PROC_NAME", - 180 + NTOP_BASE: "HTTP_URL", - 181 + NTOP_BASE: "HTTP_RET_CODE", - 182 + NTOP_BASE: "HTTP_REFERER", - 183 + NTOP_BASE: "HTTP_UA", - 184 + NTOP_BASE: "HTTP_MIME", - 185 + NTOP_BASE: "SMTP_MAIL_FROM", - 186 + NTOP_BASE: "SMTP_RCPT_TO", - 187 + NTOP_BASE: "HTTP_HOST", - 188 + NTOP_BASE: "SSL_SERVER_NAME", - 189 + NTOP_BASE: "BITTORRENT_HASH", - 195 + NTOP_BASE: "MYSQL_SRV_VERSION", - 196 + NTOP_BASE: "MYSQL_USERNAME", - 197 + NTOP_BASE: "MYSQL_DB", - 198 + NTOP_BASE: "MYSQL_QUERY", - 199 + NTOP_BASE: "MYSQL_RESPONSE", - 200 + NTOP_BASE: "ORACLE_USERNAME", - 201 + NTOP_BASE: "ORACLE_QUERY", - 202 + NTOP_BASE: "ORACLE_RSP_CODE", - 203 + NTOP_BASE: "ORACLE_RSP_STRING", - 204 + NTOP_BASE: "ORACLE_QUERY_DURATION", - 205 + NTOP_BASE: "DNS_QUERY", - 206 + NTOP_BASE: "DNS_QUERY_ID", - 207 + NTOP_BASE: "DNS_QUERY_TYPE", - 208 + NTOP_BASE: "DNS_RET_CODE", - 209 + NTOP_BASE: "DNS_NUM_ANSWERS", - 210 + NTOP_BASE: "POP_USER", - 220 + NTOP_BASE: "GTPV1_REQ_MSG_TYPE", - 221 + NTOP_BASE: "GTPV1_RSP_MSG_TYPE", - 222 + NTOP_BASE: "GTPV1_C2S_TEID_DATA", - 223 + NTOP_BASE: "GTPV1_C2S_TEID_CTRL", - 224 + NTOP_BASE: "GTPV1_S2C_TEID_DATA", - 225 + NTOP_BASE: "GTPV1_S2C_TEID_CTRL", - 226 + NTOP_BASE: "GTPV1_END_USER_IP", - 227 + NTOP_BASE: "GTPV1_END_USER_IMSI", - 228 + NTOP_BASE: "GTPV1_END_USER_MSISDN", - 229 + NTOP_BASE: "GTPV1_END_USER_IMEI", - 230 + NTOP_BASE: "GTPV1_APN_NAME", - 231 + NTOP_BASE: "GTPV1_RAI_MCC", - 232 + NTOP_BASE: "GTPV1_RAI_MNC", - 233 + NTOP_BASE: "GTPV1_ULI_CELL_LAC", - 234 + NTOP_BASE: "GTPV1_ULI_CELL_CI", - 235 + NTOP_BASE: "GTPV1_ULI_SAC", - 236 + NTOP_BASE: "GTPV1_RAT_TYPE", - 240 + NTOP_BASE: "RADIUS_REQ_MSG_TYPE", - 241 + NTOP_BASE: "RADIUS_RSP_MSG_TYPE", - 242 + NTOP_BASE: "RADIUS_USER_NAME", - 243 + NTOP_BASE: "RADIUS_CALLING_STATION_ID", - 244 + NTOP_BASE: "RADIUS_CALLED_STATION_ID", - 245 + NTOP_BASE: "RADIUS_NAS_IP_ADDR", - 246 + NTOP_BASE: "RADIUS_NAS_IDENTIFIER", - 247 + NTOP_BASE: "RADIUS_USER_IMSI", - 248 + NTOP_BASE: "RADIUS_USER_IMEI", - 249 + NTOP_BASE: "RADIUS_FRAMED_IP_ADDR", - 250 + NTOP_BASE: "RADIUS_ACCT_SESSION_ID", - 251 + NTOP_BASE: "RADIUS_ACCT_STATUS_TYPE", - 252 + NTOP_BASE: "RADIUS_ACCT_IN_OCTETS", - 253 + NTOP_BASE: "RADIUS_ACCT_OUT_OCTETS", - 254 + NTOP_BASE: "RADIUS_ACCT_IN_PKTS", - 255 + NTOP_BASE: "RADIUS_ACCT_OUT_PKTS", - 260 + NTOP_BASE: "IMAP_LOGIN", - 270 + NTOP_BASE: "GTPV2_REQ_MSG_TYPE", - 271 + NTOP_BASE: "GTPV2_RSP_MSG_TYPE", - 272 + NTOP_BASE: "GTPV2_C2S_S1U_GTPU_TEID", - 273 + NTOP_BASE: "GTPV2_C2S_S1U_GTPU_IP", - 274 + NTOP_BASE: "GTPV2_S2C_S1U_GTPU_TEID", - 275 + NTOP_BASE: "GTPV2_S2C_S1U_GTPU_IP", - 276 + NTOP_BASE: "GTPV2_END_USER_IMSI", - 277 + NTOP_BASE: "GTPV2_END_USER_MSISDN", - 278 + NTOP_BASE: "GTPV2_APN_NAME", - 279 + NTOP_BASE: "GTPV2_ULI_MCC", - 280 + NTOP_BASE: "GTPV2_ULI_MNC", - 281 + NTOP_BASE: "GTPV2_ULI_CELL_TAC", - 282 + NTOP_BASE: "GTPV2_ULI_CELL_ID", - 283 + NTOP_BASE: "GTPV2_RAT_TYPE", - 284 + NTOP_BASE: "GTPV2_PDN_IP", - 285 + NTOP_BASE: "GTPV2_END_USER_IMEI", - 290 + NTOP_BASE: "SRC_AS_PATH_1", - 291 + NTOP_BASE: "SRC_AS_PATH_2", - 292 + NTOP_BASE: "SRC_AS_PATH_3", - 293 + NTOP_BASE: "SRC_AS_PATH_4", - 294 + NTOP_BASE: "SRC_AS_PATH_5", - 295 + NTOP_BASE: "SRC_AS_PATH_6", - 296 + NTOP_BASE: "SRC_AS_PATH_7", - 297 + NTOP_BASE: "SRC_AS_PATH_8", - 298 + NTOP_BASE: "SRC_AS_PATH_9", - 299 + NTOP_BASE: "SRC_AS_PATH_10", - 300 + NTOP_BASE: "DST_AS_PATH_1", - 301 + NTOP_BASE: "DST_AS_PATH_2", - 302 + NTOP_BASE: "DST_AS_PATH_3", - 303 + NTOP_BASE: "DST_AS_PATH_4", - 304 + NTOP_BASE: "DST_AS_PATH_5", - 305 + NTOP_BASE: "DST_AS_PATH_6", - 306 + NTOP_BASE: "DST_AS_PATH_7", - 307 + NTOP_BASE: "DST_AS_PATH_8", - 308 + NTOP_BASE: "DST_AS_PATH_9", - 309 + NTOP_BASE: "DST_AS_PATH_10", - 320 + NTOP_BASE: "MYSQL_APPL_LATENCY_USEC", - 321 + NTOP_BASE: "GTPV0_REQ_MSG_TYPE", - 322 + NTOP_BASE: "GTPV0_RSP_MSG_TYPE", - 323 + NTOP_BASE: "GTPV0_TID", - 324 + NTOP_BASE: "GTPV0_END_USER_IP", - 325 + NTOP_BASE: "GTPV0_END_USER_MSISDN", - 326 + NTOP_BASE: "GTPV0_APN_NAME", - 327 + NTOP_BASE: "GTPV0_RAI_MCC", - 328 + NTOP_BASE: "GTPV0_RAI_MNC", - 329 + NTOP_BASE: "GTPV0_RAI_CELL_LAC", - 330 + NTOP_BASE: "GTPV0_RAI_CELL_RAC", - 331 + NTOP_BASE: "GTPV0_RESPONSE_CAUSE", - 332 + NTOP_BASE: "GTPV1_RESPONSE_CAUSE", - 333 + NTOP_BASE: "GTPV2_RESPONSE_CAUSE", - 334 + NTOP_BASE: "NUM_PKTS_TTL_5_32", - 335 + NTOP_BASE: "NUM_PKTS_TTL_32_64", - 336 + NTOP_BASE: "NUM_PKTS_TTL_64_96", - 337 + NTOP_BASE: "NUM_PKTS_TTL_96_128", - 338 + NTOP_BASE: "NUM_PKTS_TTL_128_160", - 339 + NTOP_BASE: "NUM_PKTS_TTL_160_192", - 340 + NTOP_BASE: "NUM_PKTS_TTL_192_224", - 341 + NTOP_BASE: "NUM_PKTS_TTL_224_255", - 342 + NTOP_BASE: "GTPV1_RAI_LAC", - 343 + NTOP_BASE: "GTPV1_RAI_RAC", - 344 + NTOP_BASE: "GTPV1_ULI_MCC", - 345 + NTOP_BASE: "GTPV1_ULI_MNC", - 346 + NTOP_BASE: "NUM_PKTS_TTL_2_5", - 347 + NTOP_BASE: "NUM_PKTS_TTL_EQ_1", - 348 + NTOP_BASE: "RTP_SIP_CALL_ID", - 349 + NTOP_BASE: "IN_SRC_OSI_SAP", - 350 + NTOP_BASE: "OUT_DST_OSI_SAP", - 351 + NTOP_BASE: "WHOIS_DAS_DOMAIN", - 352 + NTOP_BASE: "DNS_TTL_ANSWER", - 353 + NTOP_BASE: "DHCP_CLIENT_MAC", - 354 + NTOP_BASE: "DHCP_CLIENT_IP", - 355 + NTOP_BASE: "DHCP_CLIENT_NAME", - 356 + NTOP_BASE: "FTP_LOGIN", - 357 + NTOP_BASE: "FTP_PASSWORD", - 358 + NTOP_BASE: "FTP_COMMAND", - 359 + NTOP_BASE: "FTP_COMMAND_RET_CODE", - 360 + NTOP_BASE: "HTTP_METHOD", - 361 + NTOP_BASE: "HTTP_SITE", - 362 + NTOP_BASE: "SIP_C_IP", - 363 + NTOP_BASE: "SIP_CALL_STATE", - 364 + NTOP_BASE: "EPP_REGISTRAR_NAME", - 365 + NTOP_BASE: "EPP_CMD", - 366 + NTOP_BASE: "EPP_CMD_ARGS", - 367 + NTOP_BASE: "EPP_RSP_CODE", - 368 + NTOP_BASE: "EPP_REASON_STR", - 369 + NTOP_BASE: "EPP_SERVER_NAME", - 370 + NTOP_BASE: "RTP_IN_MOS", - 371 + NTOP_BASE: "RTP_IN_R_FACTOR", - 372 + NTOP_BASE: "SRC_PROC_USER_NAME", - 373 + NTOP_BASE: "SRC_FATHER_PROC_PID", - 374 + NTOP_BASE: "SRC_FATHER_PROC_NAME", - 375 + NTOP_BASE: "DST_PROC_PID", - 376 + NTOP_BASE: "DST_PROC_NAME", - 377 + NTOP_BASE: "DST_PROC_USER_NAME", - 378 + NTOP_BASE: "DST_FATHER_PROC_PID", - 379 + NTOP_BASE: "DST_FATHER_PROC_NAME", - 380 + NTOP_BASE: "RTP_RTT", - 381 + NTOP_BASE: "RTP_IN_TRANSIT", - 382 + NTOP_BASE: "RTP_OUT_TRANSIT", - 383 + NTOP_BASE: "SRC_PROC_ACTUAL_MEMORY", - 384 + NTOP_BASE: "SRC_PROC_PEAK_MEMORY", - 385 + NTOP_BASE: "SRC_PROC_AVERAGE_CPU_LOAD", - 386 + NTOP_BASE: "SRC_PROC_NUM_PAGE_FAULTS", - 387 + NTOP_BASE: "DST_PROC_ACTUAL_MEMORY", - 388 + NTOP_BASE: "DST_PROC_PEAK_MEMORY", - 389 + NTOP_BASE: "DST_PROC_AVERAGE_CPU_LOAD", - 390 + NTOP_BASE: "DST_PROC_NUM_PAGE_FAULTS", - 391 + NTOP_BASE: "DURATION_IN", - 392 + NTOP_BASE: "DURATION_OUT", - 393 + NTOP_BASE: "SRC_PROC_PCTG_IOWAIT", - 394 + NTOP_BASE: "DST_PROC_PCTG_IOWAIT", - 395 + NTOP_BASE: "RTP_DTMF_TONES", - 396 + NTOP_BASE: "UNTUNNELED_IPV6_SRC_ADDR", - 397 + NTOP_BASE: "UNTUNNELED_IPV6_DST_ADDR", - 398 + NTOP_BASE: "DNS_RESPONSE", - 399 + NTOP_BASE: "DIAMETER_REQ_MSG_TYPE", - 400 + NTOP_BASE: "DIAMETER_RSP_MSG_TYPE", - 401 + NTOP_BASE: "DIAMETER_REQ_ORIGIN_HOST", - 402 + NTOP_BASE: "DIAMETER_RSP_ORIGIN_HOST", - 403 + NTOP_BASE: "DIAMETER_REQ_USER_NAME", - 404 + NTOP_BASE: "DIAMETER_RSP_RESULT_CODE", - 405 + NTOP_BASE: "DIAMETER_EXP_RES_VENDOR_ID", - 406 + NTOP_BASE: "DIAMETER_EXP_RES_RESULT_CODE", - 407 + NTOP_BASE: "S1AP_ENB_UE_S1AP_ID", - 408 + NTOP_BASE: "S1AP_MME_UE_S1AP_ID", - 409 + NTOP_BASE: "S1AP_MSG_EMM_TYPE_MME_TO_ENB", - 410 + NTOP_BASE: "S1AP_MSG_ESM_TYPE_MME_TO_ENB", - 411 + NTOP_BASE: "S1AP_MSG_EMM_TYPE_ENB_TO_MME", - 412 + NTOP_BASE: "S1AP_MSG_ESM_TYPE_ENB_TO_MME", - 413 + NTOP_BASE: "S1AP_CAUSE_ENB_TO_MME", - 414 + NTOP_BASE: "S1AP_DETAILED_CAUSE_ENB_TO_MME", - 415 + NTOP_BASE: "TCP_WIN_MIN_IN", - 416 + NTOP_BASE: "TCP_WIN_MAX_IN", - 417 + NTOP_BASE: "TCP_WIN_MSS_IN", - 418 + NTOP_BASE: "TCP_WIN_SCALE_IN", - 419 + NTOP_BASE: "TCP_WIN_MIN_OUT", - 420 + NTOP_BASE: "TCP_WIN_MAX_OUT", - 421 + NTOP_BASE: "TCP_WIN_MSS_OUT", - 422 + NTOP_BASE: "TCP_WIN_SCALE_OUT", - 423 + NTOP_BASE: "DHCP_REMOTE_ID", - 424 + NTOP_BASE: "DHCP_SUBSCRIBER_ID", - 425 + NTOP_BASE: "SRC_PROC_UID", - 426 + NTOP_BASE: "DST_PROC_UID", - 427 + NTOP_BASE: "APPLICATION_NAME", - 428 + NTOP_BASE: "USER_NAME", - 429 + NTOP_BASE: "DHCP_MESSAGE_TYPE", - 430 + NTOP_BASE: "RTP_IN_PKT_DROP", - 431 + NTOP_BASE: "RTP_OUT_PKT_DROP", - 432 + NTOP_BASE: "RTP_OUT_MOS", - 433 + NTOP_BASE: "RTP_OUT_R_FACTOR", - 434 + NTOP_BASE: "RTP_MOS", - 435 + NTOP_BASE: "GTPV2_S5_S8_GTPC_TEID", - 436 + NTOP_BASE: "RTP_R_FACTOR", - 437 + NTOP_BASE: "RTP_SSRC", - 438 + NTOP_BASE: "PAYLOAD_HASH", - 439 + NTOP_BASE: "GTPV2_C2S_S5_S8_GTPU_TEID", - 440 + NTOP_BASE: "GTPV2_S2C_S5_S8_GTPU_TEID", - 441 + NTOP_BASE: "GTPV2_C2S_S5_S8_GTPU_IP", - 442 + NTOP_BASE: "GTPV2_S2C_S5_S8_GTPU_IP", - 443 + NTOP_BASE: "SRC_AS_MAP", - 444 + NTOP_BASE: "DST_AS_MAP", - 445 + NTOP_BASE: "DIAMETER_HOP_BY_HOP_ID", - 446 + NTOP_BASE: "UPSTREAM_SESSION_ID", - 447 + NTOP_BASE: "DOWNSTREAM_SESSION_ID", - 448 + NTOP_BASE: "SRC_IP_LONG", - 449 + NTOP_BASE: "SRC_IP_LAT", - 450 + NTOP_BASE: "DST_IP_LONG", - 451 + NTOP_BASE: "DST_IP_LAT", - 452 + NTOP_BASE: "DIAMETER_CLR_CANCEL_TYPE", - 453 + NTOP_BASE: "DIAMETER_CLR_FLAGS", - 454 + NTOP_BASE: "GTPV2_C2S_S5_S8_GTPC_IP", - 455 + NTOP_BASE: "GTPV2_S2C_S5_S8_GTPC_IP", - 456 + NTOP_BASE: "GTPV2_C2S_S5_S8_SGW_GTPU_TEID", - 457 + NTOP_BASE: "GTPV2_S2C_S5_S8_SGW_GTPU_TEID", - 458 + NTOP_BASE: "GTPV2_C2S_S5_S8_SGW_GTPU_IP", - 459 + NTOP_BASE: "GTPV2_S2C_S5_S8_SGW_GTPU_IP", - 460 + NTOP_BASE: "HTTP_X_FORWARDED_FOR", - 461 + NTOP_BASE: "HTTP_VIA", - 462 + NTOP_BASE: "SSDP_HOST", - 463 + NTOP_BASE: "SSDP_USN", - 464 + NTOP_BASE: "NETBIOS_QUERY_NAME", - 465 + NTOP_BASE: "NETBIOS_QUERY_TYPE", - 466 + NTOP_BASE: "NETBIOS_RESPONSE", - 467 + NTOP_BASE: "NETBIOS_QUERY_OS", - 468 + NTOP_BASE: "SSDP_SERVER", - 469 + NTOP_BASE: "SSDP_TYPE", - 470 + NTOP_BASE: "SSDP_METHOD", - 471 + NTOP_BASE: "NPROBE_IPV4_ADDRESS", -} -ScopeFieldTypes = { - 1: "System", - 2: "Interface", - 3: "Line card", - 4: "Cache", - 5: "Template", -} +@dataclasses.dataclass +class _N910F: + name: str + length: int = 0 + field: Field = None + kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + isint: bool = False -NetflowV9TemplateFieldDefaultLengths = { - 1: 4, - 2: 4, - 3: 4, - 4: 1, - 5: 1, - 6: 1, - 7: 2, - 8: 4, - 9: 1, - 10: 2, - 11: 2, - 12: 4, - 13: 1, - 14: 2, - 15: 4, - 16: 2, - 17: 2, - 18: 4, - 19: 4, - 20: 4, - 21: 4, - 22: 4, - 23: 4, - 24: 4, - 27: 16, - 28: 16, - 29: 1, - 30: 1, - 31: 3, - 32: 2, - 33: 1, - 34: 4, - 35: 1, - 36: 2, - 37: 2, - 38: 1, - 39: 1, - 40: 4, - 41: 4, - 42: 4, - 46: 1, - 47: 4, - 48: 4, # from ERRATA - 49: 1, - 50: 4, - 55: 1, - 56: 6, - 57: 6, - 58: 2, - 59: 2, - 60: 1, - 61: 1, - 62: 16, - 63: 16, - 64: 4, - 70: 3, - 71: 3, - 72: 3, - 73: 3, - 74: 3, - 75: 3, - 76: 3, - 77: 3, - 78: 3, - 79: 3, -} # NetflowV9 Ready-made fields - class ShortOrInt(IntField): def getfield(self, pkt, x): if len(x) == 2: @@ -1103,118 +250,1012 @@ def __init__(self, name, default, *args, **kargs): self, name, default, length ) - -# TODO: There are hundreds of entries to add to the following :( -# https://tools.ietf.org/html/rfc5655 +# TODO: There are hundreds of entries to add to the following list :( +# it's thus incomplete. +# https://www.iana.org/assignments/ipfix/ipfix.xml # ==> feel free to contribute :D -NetflowV9TemplateFieldDecoders = { - # Only contains fields that have a fixed length - # ID: Field, - # or - # ID: (Field, [*optional_parameters]), - 4: (ByteEnumField, [IP_PROTOS]), # PROTOCOL - 5: XByteField, # TOS - 6: ByteField, # TCP_FLAGS - 7: ShortField, # L4_SRC_PORT - 8: IPField, # IPV4_SRC_ADDR - 9: ByteField, # SRC_MASK - 11: ShortField, # L4_DST_PORT - 12: IPField, # IPV4_DST_PORT - 13: ByteField, # DST_MASK - 15: IPField, # IPv4_NEXT_HOP - 16: ShortOrInt, # SRC_AS - 17: ShortOrInt, # DST_AS - 18: IPField, # BGP_IPv4_NEXT_HOP - 21: (SecondsIntField, [True]), # LAST_SWITCHED - 22: (SecondsIntField, [True]), # FIRST_SWITCHED - 27: IP6Field, # IPV6_SRC_ADDR - 28: IP6Field, # IPV6_DST_ADDR - 29: ByteField, # IPV6_SRC_MASK - 30: ByteField, # IPV6_DST_MASK - 31: ThreeBytesField, # IPV6_FLOW_LABEL - 32: XShortField, # ICMP_TYPE - 33: ByteField, # MUL_IGMP_TYPE - 34: IntField, # SAMPLING_INTERVAL - 35: XByteField, # SAMPLING_ALGORITHM - 36: ShortField, # FLOW_ACTIVE_TIMEOUT - 37: ShortField, # FLOW_ACTIVE_TIMEOUT - 38: ByteField, # ENGINE_TYPE - 39: ByteField, # ENGINE_ID - 46: (ByteEnumField, [{0x00: "UNKNOWN", 0x01: "TE-MIDPT", 0x02: "ATOM", 0x03: "VPN", 0x04: "BGP", 0x05: "LDP"}]), # MPLS_TOP_LABEL_TYPE # noqa: E501 - 47: IPField, # MPLS_TOP_LABEL_IP_ADDR - 48: ByteField, # FLOW_SAMPLER_ID - 49: ByteField, # FLOW_SAMPLER_MODE - 50: IntField, # FLOW_SAMPLER_RANDOM_INTERVAL - 55: XByteField, # DST_TOS - 56: MACField, # SRC_MAC - 57: MACField, # DST_MAC - 58: ShortField, # SRC_VLAN - 59: ShortField, # DST_VLAN - 60: ByteField, # IP_PROTOCOL_VERSION - 61: (ByteEnumField, [{0x00: "Ingress flow", 0x01: "Egress flow"}]), # DIRECTION # noqa: E501 - 62: IP6Field, # IPV6_NEXT_HOP - 63: IP6Field, # BGP_IPV6_NEXT_HOP - 130: IPField, # exporterIPv4Address - 131: IP6Field, # exporterIPv6Address - 150: N9UTCTimeField, # flowStartSeconds - 151: N9UTCTimeField, # flowEndSeconds - 152: (N9UTCTimeField, [True]), # flowStartMilliseconds - 153: (N9UTCTimeField, [True]), # flowEndMilliseconds - 154: (N9UTCTimeField, [False, True]), # flowStartMicroseconds - 155: (N9UTCTimeField, [False, True]), # flowEndMicroseconds - 156: (N9UTCTimeField, [False, False, True]), # flowStartNanoseconds - 157: (N9UTCTimeField, [False, False, True]), # flowEndNanoseconds - 158: (N9SecondsIntField, [False, True]), # flowStartDeltaMicroseconds - 159: (N9SecondsIntField, [False, True]), # flowEndDeltaMicroseconds - 160: (N9UTCTimeField, [True]), # systemInitTimeMilliseconds - 161: (N9SecondsIntField, [True]), # flowDurationMilliseconds - 162: (N9SecondsIntField, [False, True]), # flowDurationMicroseconds - 211: IPField, # collectorIPv4Address - 212: IP6Field, # collectorIPv6Address - 225: IPField, # postNATSourceIPv4Address - 226: IPField, # postNATDestinationIPv4Address - 258: (N9SecondsIntField, [True]), # collectionTimeMilliseconds - 260: N9SecondsIntField, # maxExportSeconds - 261: N9SecondsIntField, # maxFlowEndSeconds - 264: N9SecondsIntField, # minExportSeconds - 265: N9SecondsIntField, # minFlowStartSeconds - 268: (N9UTCTimeField, [False, True]), # maxFlowEndMicroseconds - 269: (N9UTCTimeField, [True]), # maxFlowEndMilliseconds - 270: (N9UTCTimeField, [False, False, True]), # maxFlowEndNanoseconds - 271: (N9UTCTimeField, [False, True]), # minFlowStartMicroseconds - 272: (N9UTCTimeField, [True]), # minFlowStartMilliseconds - 273: (N9UTCTimeField, [False, False, True]), # minFlowStartNanoseconds - 279: N9SecondsIntField, # connectionSumDurationSeconds - 281: IP6Field, # postNATSourceIPv6Address - 282: IP6Field, # postNATDestinationIPv6Address - 322: N9UTCTimeField, # observationTimeSeconds - 323: (N9UTCTimeField, [True]), # observationTimeMilliseconds - 324: (N9UTCTimeField, [False, True]), # observationTimeMicroseconds - 325: (N9UTCTimeField, [False, False, True]), # observationTimeNanoseconds - 365: MACField, # staMacAddress - 366: IPField, # staIPv4Address - 367: MACField, # wtpMacAddress - 380: IPField, # distinctCountOfSourceIPv4Address - 381: IPField, # distinctCountOfDestinationIPv4Address - 382: IP6Field, # distinctCountOfSourceIPv6Address - 383: IP6Field, # distinctCountOfDestinationIPv6Address - 403: IPField, # originalExporterIPv4Address - 404: IP6Field, # originalExporterIPv6Address - 414: MACField, # dot1qCustomerSourceMacAddress - 415: MACField, # dot1qCustomerDestinationMacAddress - 432: IPField, # pseudoWireDestinationIPv4Address - 24632: IPField, # NAT_LOG_FIELD_IDX_IPV4_INT_ADDR - 24633: IPField, # NAT_LOG_FIELD_IDX_IPV4_EXT_ADDR - 40001: IPField, # XLATE_SRC_ADDR_IPV4 - 40002: IPField, # XLATE_DST_ADDR_IPV4 - 114 + NTOP_BASE: IPField, # UNTUNNELED_IPV4_SRC_ADDR - 116 + NTOP_BASE: IPField, # UNTUNNELED_IPV4_DST_ADDR - 143 + NTOP_BASE: IPField, # SIP_RTP_IPV4_SRC_ADDR - 145 + NTOP_BASE: IPField, # SIP_RTP_IPV4_DST_ADDR - 353 + NTOP_BASE: MACField, # DHCP_CLIENT_MAC - 396 + NTOP_BASE: IP6Field, # UNTUNNELED_IPV6_SRC_ADDR - 397 + NTOP_BASE: IP6Field, # UNTUNNELED_IPV6_DST_ADDR - 471 + NTOP_BASE: IPField, # NPROBE_IPV4_ADDRESS + +# XXX: we should probably switch the names below to IANA normalized ones. + +# This is v9_v10_template_types (with names from the rfc for the first 79) +# https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-netflow.c # noqa: E501 +# (it has all values external to the RFC) + + +NTOP_BASE = 57472 +NetflowV910TemplateFields = { + 1: _N910F("IN_BYTES", length=4, + isint=True), + 2: _N910F("IN_PKTS", length=4, + isint=True), + 3: _N910F("FLOWS", length=4), + 4: _N910F("PROTOCOL", length=1, + field=ByteEnumField, kwargs={"enum": IP_PROTOS}), + 5: _N910F("TOS", length=1, + field=XByteField), + 6: _N910F("TCP_FLAGS", length=1, + field=ByteField), + 7: _N910F("L4_SRC_PORT", length=2, + field=ShortField), + 8: _N910F("IPV4_SRC_ADDR", length=4, + field=IPField), + 9: _N910F("SRC_MASK", length=1, + field=ByteField), + 10: _N910F("INPUT_SNMP", + isint=True), + 11: _N910F("L4_DST_PORT", length=2, + field=ShortField), + 12: _N910F("IPV4_DST_ADDR", length=4, + field=IPField), + 13: _N910F("DST_MASK", length=1, + field=ByteField), + 14: _N910F("OUTPUT_SNMP", + isint=True), + 15: _N910F("IPV4_NEXT_HOP", length=4, + field=IPField), + 16: _N910F("SRC_AS", length=2, + field=ShortOrInt), + 17: _N910F("DST_AS", length=2, + field=ShortOrInt), + 18: _N910F("BGP_IPV4_NEXT_HOP", length=4, + field=IPField), + 19: _N910F("MUL_DST_PKTS", length=4, + isint=True), + 20: _N910F("MUL_DST_BYTES", length=4, + isint=True), + 21: _N910F("LAST_SWITCHED", length=4, + field=SecondsIntField, + kwargs={"use_msec": True}), + 22: _N910F("FIRST_SWITCHED", length=4, + field=SecondsIntField, + kwargs={"use_msec": True}), + 23: _N910F("OUT_BYTES", length=4, + isint=True), + 24: _N910F("OUT_PKTS", length=4, + isint=True), + 25: _N910F("IP_LENGTH_MINIMUM"), + 26: _N910F("IP_LENGTH_MAXIMUM"), + 27: _N910F("IPV6_SRC_ADDR", length=16, + field=IP6Field), + 28: _N910F("IPV6_DST_ADDR", length=16, + field=IP6Field), + 29: _N910F("IPV6_SRC_MASK", length=1, + field=ByteField), + 30: _N910F("IPV6_DST_MASK", length=1, + field=ByteField), + 31: _N910F("IPV6_FLOW_LABEL", length=3, + field=ThreeBytesField), + 32: _N910F("ICMP_TYPE", length=2, + field=XShortField), + 33: _N910F("MUL_IGMP_TYPE", length=1, + field=ByteField), + 34: _N910F("SAMPLING_INTERVAL", length=4, + field=IntField), + 35: _N910F("SAMPLING_ALGORITHM", length=1, + field=XByteField), + 36: _N910F("FLOW_ACTIVE_TIMEOUT", length=2, + field=ShortField), + 37: _N910F("FLOW_INACTIVE_TIMEOUT", length=2, + field=ShortField), + 38: _N910F("ENGINE_TYPE", length=1, + field=ByteField), + 39: _N910F("ENGINE_ID", length=1, + field=ByteField), + 40: _N910F("TOTAL_BYTES_EXP", length=4, + isint=True), + 41: _N910F("TOTAL_PKTS_EXP", length=4, + isint=True), + 42: _N910F("TOTAL_FLOWS_EXP", length=4, + isint=True), + 43: _N910F("IPV4_ROUTER_SC"), + 44: _N910F("IP_SRC_PREFIX"), + 45: _N910F("IP_DST_PREFIX"), + 46: _N910F("MPLS_TOP_LABEL_TYPE", length=1, + field=ByteEnumField, + kwargs={"enum": { + 0x00: "UNKNOWN", + 0x01: "TE-MIDPT", + 0x02: "ATOM", + 0x03: "VPN", + 0x04: "BGP", + 0x05: "LDP", + }}), + 47: _N910F("MPLS_TOP_LABEL_IP_ADDR", length=4, + field=IPField), + 48: _N910F("FLOW_SAMPLER_ID", length=4), # from ERRATA + 49: _N910F("FLOW_SAMPLER_MODE", length=1, + field=ByteField), + 50: _N910F("FLOW_SAMPLER_RANDOM_INTERVAL", length=4, + field=IntField), + 51: _N910F("FLOW_CLASS"), + 52: _N910F("MIN_TTL"), + 53: _N910F("MAX_TTL"), + 54: _N910F("IPV4_IDENT"), + 55: _N910F("DST_TOS", length=1, + field=XByteField), + 56: _N910F("SRC_MAC", length=6, + field=MACField), + 57: _N910F("DST_MAC", length=6, + field=MACField), + 58: _N910F("SRC_VLAN", length=2, + field=ShortField), + 59: _N910F("DST_VLAN", length=2, + field=ShortField), + 60: _N910F("IP_PROTOCOL_VERSION", length=1, + field=ByteField), + 61: _N910F("DIRECTION", length=1, + field=ByteEnumField, + kwargs={"enum": {0x00: "Ingress flow", 0x01: "Egress flow"}}), + 62: _N910F("IPV6_NEXT_HOP", length=16, + field=IP6Field), + 63: _N910F("BGP_IPV6_NEXT_HOP", length=16, + field=IP6Field), + 64: _N910F("IPV6_OPTION_HEADERS", length=4), + 70: _N910F("MPLS_LABEL_1", length=3, + field=ThreeBytesField), + 71: _N910F("MPLS_LABEL_2", length=3, + field=ThreeBytesField), + 72: _N910F("MPLS_LABEL_3", length=3, + field=ThreeBytesField), + 73: _N910F("MPLS_LABEL_4", length=3, + field=ThreeBytesField), + 74: _N910F("MPLS_LABEL_5", length=3, + field=ThreeBytesField), + 75: _N910F("MPLS_LABEL_6", length=3, + field=ThreeBytesField), + 76: _N910F("MPLS_LABEL_7", length=3, + field=ThreeBytesField), + 77: _N910F("MPLS_LABEL_8", length=3, + field=ThreeBytesField), + 78: _N910F("MPLS_LABEL_9", length=3, + field=ThreeBytesField), + 79: _N910F("MPLS_LABEL_10", length=3, + field=ThreeBytesField), + 80: _N910F("DESTINATION_MAC"), + 81: _N910F("SOURCE_MAC"), + 82: _N910F("IF_NAME"), + 83: _N910F("IF_DESC"), + 84: _N910F("SAMPLER_NAME"), + 85: _N910F("BYTES_TOTAL"), + 86: _N910F("PACKETS_TOTAL"), + 88: _N910F("FRAGMENT_OFFSET"), + 89: _N910F("FORWARDING_STATUS"), + 90: _N910F("VPN_ROUTE_DISTINGUISHER"), + 91: _N910F("mplsTopLabelPrefixLength"), + 92: _N910F("SRC_TRAFFIC_INDEX"), + 93: _N910F("DST_TRAFFIC_INDEX"), + 94: _N910F("APPLICATION_DESC"), + 95: _N910F("APPLICATION_ID"), + 96: _N910F("APPLICATION_NAME"), + 98: _N910F("postIpDiffServCodePoint"), + 99: _N910F("multicastReplicationFactor"), + 101: _N910F("classificationEngineId"), + 128: _N910F("DST_AS_PEER"), + 129: _N910F("SRC_AS_PEER"), + 130: _N910F("exporterIPv4Address", length=4, + field=IPField), + 131: _N910F("exporterIPv6Address", length=16, + field=IP6Field), + 132: _N910F("DROPPED_BYTES"), + 133: _N910F("DROPPED_PACKETS"), + 134: _N910F("DROPPED_BYTES_TOTAL"), + 135: _N910F("DROPPED_PACKETS_TOTAL"), + 136: _N910F("flowEndReason"), + 137: _N910F("commonPropertiesId"), + 138: _N910F("observationPointId"), + 139: _N910F("icmpTypeCodeIPv6"), + 140: _N910F("MPLS_TOP_LABEL_IPv6_ADDRESS"), + 141: _N910F("lineCardId"), + 142: _N910F("portId"), + 143: _N910F("meteringProcessId"), + 144: _N910F("FLOW_EXPORTER"), + 145: _N910F("templateId"), + 146: _N910F("wlanChannelId"), + 147: _N910F("wlanSSID"), + 148: _N910F("flowId"), + 149: _N910F("observationDomainId"), + 150: _N910F("flowStartSeconds", length=8, + field=N9UTCTimeField), + 151: _N910F("flowEndSeconds", length=8, + field=N9UTCTimeField), + 152: _N910F("flowStartMilliseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_msec": True}), + 153: _N910F("flowEndMilliseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_msec": True}), + 154: _N910F("flowStartMicroseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_micro": True}), + 155: _N910F("flowEndMicroseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_micro": True}), + 156: _N910F("flowStartNanoseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_nano": True}), + 157: _N910F("flowEndNanoseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_nano": True}), + 158: _N910F("flowStartDeltaMicroseconds", length=8, + field=N9SecondsIntField, + kwargs={"use_micro": True}), + 159: _N910F("flowEndDeltaMicroseconds", length=8, + field=N9SecondsIntField, + kwargs={"use_micro": True}), + 160: _N910F("systemInitTimeMilliseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_msec": True}), + 161: _N910F("flowDurationMilliseconds", length=8, + field=N9SecondsIntField, + kwargs={"use_msec": True}), + 162: _N910F("flowDurationMicroseconds", length=8, + field=N9SecondsIntField, + kwargs={"use_micro": True}), + 163: _N910F("observedFlowTotalCount"), + 164: _N910F("ignoredPacketTotalCount"), + 165: _N910F("ignoredOctetTotalCount"), + 166: _N910F("notSentFlowTotalCount"), + 167: _N910F("notSentPacketTotalCount"), + 168: _N910F("notSentOctetTotalCount"), + 169: _N910F("destinationIPv6Prefix"), + 170: _N910F("sourceIPv6Prefix"), + 171: _N910F("postOctetTotalCount"), + 172: _N910F("postPacketTotalCount"), + 173: _N910F("flowKeyIndicator"), + 174: _N910F("postMCastPacketTotalCount"), + 175: _N910F("postMCastOctetTotalCount"), + 176: _N910F("ICMP_IPv4_TYPE"), + 177: _N910F("ICMP_IPv4_CODE"), + 178: _N910F("ICMP_IPv6_TYPE"), + 179: _N910F("ICMP_IPv6_CODE"), + 180: _N910F("UDP_SRC_PORT"), + 181: _N910F("UDP_DST_PORT"), + 182: _N910F("TCP_SRC_PORT"), + 183: _N910F("TCP_DST_PORT"), + 184: _N910F("TCP_SEQ_NUM"), + 185: _N910F("TCP_ACK_NUM"), + 186: _N910F("TCP_WINDOW_SIZE"), + 187: _N910F("TCP_URGENT_PTR"), + 188: _N910F("TCP_HEADER_LEN"), + 189: _N910F("IP_HEADER_LEN"), + 190: _N910F("IP_TOTAL_LEN"), + 191: _N910F("payloadLengthIPv6"), + 192: _N910F("IP_TTL"), + 193: _N910F("nextHeaderIPv6"), + 194: _N910F("mplsPayloadLength"), + 195: _N910F("IP_DSCP", length=1, + field=XByteField), + 196: _N910F("IP_PRECEDENCE"), + 197: _N910F("IP_FRAGMENT_FLAGS"), + 198: _N910F("DELTA_BYTES_SQUARED"), + 199: _N910F("TOTAL_BYTES_SQUARED"), + 200: _N910F("MPLS_TOP_LABEL_TTL"), + 201: _N910F("MPLS_LABEL_STACK_OCTETS"), + 202: _N910F("MPLS_LABEL_STACK_DEPTH"), + 203: _N910F("MPLS_TOP_LABEL_EXP"), + 204: _N910F("IP_PAYLOAD_LENGTH"), + 205: _N910F("UDP_LENGTH"), + 206: _N910F("IS_MULTICAST"), + 207: _N910F("IP_HEADER_WORDS"), + 208: _N910F("IP_OPTION_MAP"), + 209: _N910F("TCP_OPTION_MAP"), + 210: _N910F("paddingOctets"), + 211: _N910F("collectorIPv4Address", length=4, + field=IPField), + 212: _N910F("collectorIPv6Address", length=16, + field=IP6Field), + 213: _N910F("collectorInterface"), + 214: _N910F("collectorProtocolVersion"), + 215: _N910F("collectorTransportProtocol"), + 216: _N910F("collectorTransportPort"), + 217: _N910F("exporterTransportPort"), + 218: _N910F("tcpSynTotalCount"), + 219: _N910F("tcpFinTotalCount"), + 220: _N910F("tcpRstTotalCount"), + 221: _N910F("tcpPshTotalCount"), + 222: _N910F("tcpAckTotalCount"), + 223: _N910F("tcpUrgTotalCount"), + 224: _N910F("ipTotalLength"), + 225: _N910F("postNATSourceIPv4Address", length=4, + field=IPField), + 226: _N910F("postNATDestinationIPv4Address", length=4, + field=IPField), + 227: _N910F("postNAPTSourceTransportPort"), + 228: _N910F("postNAPTDestinationTransportPort"), + 229: _N910F("natOriginatingAddressRealm"), + 230: _N910F("natEvent"), + 231: _N910F("initiatorOctets"), + 232: _N910F("responderOctets"), + 233: _N910F("firewallEvent"), + 234: _N910F("ingressVRFID"), + 235: _N910F("egressVRFID"), + 236: _N910F("VRFname"), + 237: _N910F("postMplsTopLabelExp"), + 238: _N910F("tcpWindowScale"), + 239: _N910F("biflowDirection"), + 240: _N910F("ethernetHeaderLength"), + 241: _N910F("ethernetPayloadLength"), + 242: _N910F("ethernetTotalLength"), + 243: _N910F("dot1qVlanId"), + 244: _N910F("dot1qPriority"), + 245: _N910F("dot1qCustomerVlanId"), + 246: _N910F("dot1qCustomerPriority"), + 247: _N910F("metroEvcId"), + 248: _N910F("metroEvcType"), + 249: _N910F("pseudoWireId"), + 250: _N910F("pseudoWireType"), + 251: _N910F("pseudoWireControlWord"), + 252: _N910F("ingressPhysicalInterface"), + 253: _N910F("egressPhysicalInterface"), + 254: _N910F("postDot1qVlanId"), + 255: _N910F("postDot1qCustomerVlanId"), + 256: _N910F("ethernetType"), + 257: _N910F("postIpPrecedence"), + 258: _N910F("collectionTimeMilliseconds", length=8, + field=N9SecondsIntField, + kwargs={"use_msec": True}), + 259: _N910F("exportSctpStreamId"), + 260: _N910F("maxExportSeconds", length=8, + field=N9SecondsIntField), + 261: _N910F("maxFlowEndSeconds", length=8, + field=N9SecondsIntField), + 262: _N910F("messageMD5Checksum"), + 263: _N910F("messageScope"), + 264: _N910F("minExportSeconds", length=8, + field=N9SecondsIntField), + 265: _N910F("minFlowStartSeconds", length=8, + field=N9SecondsIntField), + 266: _N910F("opaqueOctets"), + 267: _N910F("sessionScope"), + 268: _N910F("maxFlowEndMicroseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_micro": True}), + 269: _N910F("maxFlowEndMilliseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_msec": True}), + 270: _N910F("maxFlowEndNanoseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_nano": True}), + 271: _N910F("minFlowStartMicroseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_micro": True}), + 272: _N910F("minFlowStartMilliseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_msec": True}), + 273: _N910F("minFlowStartNanoseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_nano": True}), + 274: _N910F("collectorCertificate"), + 275: _N910F("exporterCertificate"), + 276: _N910F("dataRecordsReliability"), + 277: _N910F("observationPointType"), + 278: _N910F("newConnectionDeltaCount"), + 279: _N910F("connectionSumDurationSeconds", length=8, + field=N9SecondsIntField), + 280: _N910F("connectionTransactionId"), + 281: _N910F("postNATSourceIPv6Address", length=16, + field=IP6Field), + 282: _N910F("postNATDestinationIPv6Address", length=16, + field=IP6Field), + 283: _N910F("natPoolId"), + 284: _N910F("natPoolName"), + 285: _N910F("anonymizationFlags"), + 286: _N910F("anonymizationTechnique"), + 287: _N910F("informationElementIndex"), + 288: _N910F("p2pTechnology"), + 289: _N910F("tunnelTechnology"), + 290: _N910F("encryptedTechnology"), + 291: _N910F("basicList"), + 292: _N910F("subTemplateList"), + 293: _N910F("subTemplateMultiList"), + 294: _N910F("bgpValidityState"), + 295: _N910F("IPSecSPI"), + 296: _N910F("greKey"), + 297: _N910F("natType"), + 298: _N910F("initiatorPackets"), + 299: _N910F("responderPackets"), + 300: _N910F("observationDomainName"), + 301: _N910F("selectionSequenceId"), + 302: _N910F("selectorId"), + 303: _N910F("informationElementId"), + 304: _N910F("selectorAlgorithm"), + 305: _N910F("samplingPacketInterval"), + 306: _N910F("samplingPacketSpace"), + 307: _N910F("samplingTimeInterval"), + 308: _N910F("samplingTimeSpace"), + 309: _N910F("samplingSize"), + 310: _N910F("samplingPopulation"), + 311: _N910F("samplingProbability"), + 312: _N910F("dataLinkFrameSize"), + 313: _N910F("IP_SECTION_HEADER"), + 314: _N910F("IP_SECTION_PAYLOAD"), + 315: _N910F("dataLinkFrameSection"), + 316: _N910F("mplsLabelStackSection"), + 317: _N910F("mplsPayloadPacketSection"), + 318: _N910F("selectorIdTotalPktsObserved"), + 319: _N910F("selectorIdTotalPktsSelected"), + 320: _N910F("absoluteError"), + 321: _N910F("relativeError"), + 322: _N910F("observationTimeSeconds", length=8, + field=N9UTCTimeField), + 323: _N910F("observationTimeMilliseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_msec": True}), + 324: _N910F("observationTimeMicroseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_micro": True}), + 325: _N910F("observationTimeNanoseconds", length=8, + field=N9UTCTimeField, + kwargs={"use_nano": True}), + 326: _N910F("digestHashValue"), + 327: _N910F("hashIPPayloadOffset"), + 328: _N910F("hashIPPayloadSize"), + 329: _N910F("hashOutputRangeMin"), + 330: _N910F("hashOutputRangeMax"), + 331: _N910F("hashSelectedRangeMin"), + 332: _N910F("hashSelectedRangeMax"), + 333: _N910F("hashDigestOutput"), + 334: _N910F("hashInitialiserValue"), + 335: _N910F("selectorName"), + 336: _N910F("upperCILimit"), + 337: _N910F("lowerCILimit"), + 338: _N910F("confidenceLevel"), + 339: _N910F("informationElementDataType"), + 340: _N910F("informationElementDescription"), + 341: _N910F("informationElementName"), + 342: _N910F("informationElementRangeBegin"), + 343: _N910F("informationElementRangeEnd"), + 344: _N910F("informationElementSemantics"), + 345: _N910F("informationElementUnits"), + 346: _N910F("privateEnterpriseNumber"), + 347: _N910F("virtualStationInterfaceId"), + 348: _N910F("virtualStationInterfaceName"), + 349: _N910F("virtualStationUUID"), + 350: _N910F("virtualStationName"), + 351: _N910F("layer2SegmentId"), + 352: _N910F("layer2OctetDeltaCount"), + 353: _N910F("layer2OctetTotalCount"), + 354: _N910F("ingressUnicastPacketTotalCount"), + 355: _N910F("ingressMulticastPacketTotalCount"), + 356: _N910F("ingressBroadcastPacketTotalCount"), + 357: _N910F("egressUnicastPacketTotalCount"), + 358: _N910F("egressBroadcastPacketTotalCount"), + 359: _N910F("monitoringIntervalStartMilliSeconds"), + 360: _N910F("monitoringIntervalEndMilliSeconds"), + 361: _N910F("portRangeStart"), + 362: _N910F("portRangeEnd"), + 363: _N910F("portRangeStepSize"), + 364: _N910F("portRangeNumPorts"), + 365: _N910F("staMacAddress", length=6, + field=MACField), + 366: _N910F("staIPv4Address", length=4, + field=IPField), + 367: _N910F("wtpMacAddress", length=6, + field=MACField), + 368: _N910F("ingressInterfaceType"), + 369: _N910F("egressInterfaceType"), + 370: _N910F("rtpSequenceNumber"), + 371: _N910F("userName"), + 372: _N910F("applicationCategoryName"), + 373: _N910F("applicationSubCategoryName"), + 374: _N910F("applicationGroupName"), + 375: _N910F("originalFlowsPresent"), + 376: _N910F("originalFlowsInitiated"), + 377: _N910F("originalFlowsCompleted"), + 378: _N910F("distinctCountOfSourceIPAddress"), + 379: _N910F("distinctCountOfDestinationIPAddress"), + 380: _N910F("distinctCountOfSourceIPv4Address", length=4, + field=IPField), + 381: _N910F("distinctCountOfDestinationIPv4Address", length=4, + field=IPField), + 382: _N910F("distinctCountOfSourceIPv6Address", length=16, + field=IP6Field), + 383: _N910F("distinctCountOfDestinationIPv6Address", length=16, + field=IP6Field), + 384: _N910F("valueDistributionMethod"), + 385: _N910F("rfc3550JitterMilliseconds"), + 386: _N910F("rfc3550JitterMicroseconds"), + 387: _N910F("rfc3550JitterNanoseconds"), + 388: _N910F("dot1qDEI"), + 389: _N910F("dot1qCustomerDEI"), + 390: _N910F("flowSelectorAlgorithm"), + 391: _N910F("flowSelectedOctetDeltaCount"), + 392: _N910F("flowSelectedPacketDeltaCount"), + 393: _N910F("flowSelectedFlowDeltaCount"), + 394: _N910F("selectorIDTotalFlowsObserved"), + 395: _N910F("selectorIDTotalFlowsSelected"), + 396: _N910F("samplingFlowInterval"), + 397: _N910F("samplingFlowSpacing"), + 398: _N910F("flowSamplingTimeInterval"), + 399: _N910F("flowSamplingTimeSpacing"), + 400: _N910F("hashFlowDomain"), + 401: _N910F("transportOctetDeltaCount"), + 402: _N910F("transportPacketDeltaCount"), + 403: _N910F("originalExporterIPv4Address", length=4, + field=IPField), + 404: _N910F("originalExporterIPv6Address", length=16, + field=IP6Field), + 405: _N910F("originalObservationDomainId"), + 406: _N910F("intermediateProcessId"), + 407: _N910F("ignoredDataRecordTotalCount"), + 408: _N910F("dataLinkFrameType"), + 409: _N910F("sectionOffset"), + 410: _N910F("sectionExportedOctets"), + 411: _N910F("dot1qServiceInstanceTag"), + 412: _N910F("dot1qServiceInstanceId"), + 413: _N910F("dot1qServiceInstancePriority"), + 414: _N910F("dot1qCustomerSourceMacAddress", length=6, + field=MACField), + 415: _N910F("dot1qCustomerDestinationMacAddress", length=6, + field=MACField), + 416: _N910F("deprecated [dup of layer2OctetDeltaCount]"), + 417: _N910F("postLayer2OctetDeltaCount"), + 418: _N910F("postMCastLayer2OctetDeltaCount"), + 419: _N910F("deprecated [dup of layer2OctetTotalCount"), + 420: _N910F("postLayer2OctetTotalCount"), + 421: _N910F("postMCastLayer2OctetTotalCount"), + 422: _N910F("minimumLayer2TotalLength"), + 423: _N910F("maximumLayer2TotalLength"), + 424: _N910F("droppedLayer2OctetDeltaCount"), + 425: _N910F("droppedLayer2OctetTotalCount"), + 426: _N910F("ignoredLayer2OctetTotalCount"), + 427: _N910F("notSentLayer2OctetTotalCount"), + 428: _N910F("layer2OctetDeltaSumOfSquares"), + 429: _N910F("layer2OctetTotalSumOfSquares"), + 430: _N910F("layer2FrameDeltaCount"), + 431: _N910F("layer2FrameTotalCount"), + 432: _N910F("pseudoWireDestinationIPv4Address", length=4, + field=IPField), + 433: _N910F("ignoredLayer2FrameTotalCount"), + 434: _N910F("mibObjectValueInteger"), + 435: _N910F("mibObjectValueOctetString"), + 436: _N910F("mibObjectValueOID"), + 437: _N910F("mibObjectValueBits"), + 438: _N910F("mibObjectValueIPAddress"), + 439: _N910F("mibObjectValueCounter"), + 440: _N910F("mibObjectValueGauge"), + 441: _N910F("mibObjectValueTimeTicks"), + 442: _N910F("mibObjectValueUnsigned"), + 443: _N910F("mibObjectValueTable"), + 444: _N910F("mibObjectValueRow"), + 445: _N910F("mibObjectIdentifier"), + 446: _N910F("mibSubIdentifier"), + 447: _N910F("mibIndexIndicator"), + 448: _N910F("mibCaptureTimeSemantics"), + 449: _N910F("mibContextEngineID"), + 450: _N910F("mibContextName"), + 451: _N910F("mibObjectName"), + 452: _N910F("mibObjectDescription"), + 453: _N910F("mibObjectSyntax"), + 454: _N910F("mibModuleName"), + 455: _N910F("mobileIMSI"), + 456: _N910F("mobileMSISDN"), + 457: _N910F("httpStatusCode"), + 458: _N910F("sourceTransportPortsLimit"), + 459: _N910F("httpRequestMethod"), + 460: _N910F("httpRequestHost"), + 461: _N910F("httpRequestTarget"), + 462: _N910F("httpMessageVersion"), + 463: _N910F("natInstanceID"), + 464: _N910F("internalAddressRealm"), + 465: _N910F("externalAddressRealm"), + 466: _N910F("natQuotaExceededEvent"), + 467: _N910F("natThresholdEvent"), + 468: _N910F("httpUserAgent"), + 469: _N910F("httpContentType"), + 470: _N910F("httpReasonPhrase"), + 471: _N910F("maxSessionEntries"), + 472: _N910F("maxBIBEntries"), + 473: _N910F("maxEntriesPerUser"), + 474: _N910F("maxSubscribers"), + 475: _N910F("maxFragmentsPendingReassembly"), + 476: _N910F("addressPoolHighThreshold"), + 477: _N910F("addressPoolLowThreshold"), + 478: _N910F("addressPortMappingHighThreshold"), + 479: _N910F("addressPortMappingLowThreshold"), + 480: _N910F("addressPortMappingPerUserHighThreshold"), + 481: _N910F("globalAddressMappingHighThreshold"), + + # Ericsson NAT Logging + 24628: _N910F("NAT_LOG_FIELD_IDX_CONTEXT_ID"), + 24629: _N910F("NAT_LOG_FIELD_IDX_CONTEXT_NAME"), + 24630: _N910F("NAT_LOG_FIELD_IDX_ASSIGN_TS_SEC"), + 24631: _N910F("NAT_LOG_FIELD_IDX_UNASSIGN_TS_SEC"), + 24632: _N910F("NAT_LOG_FIELD_IDX_IPV4_INT_ADDR", length=4, + field=IPField), + 24633: _N910F("NAT_LOG_FIELD_IDX_IPV4_EXT_ADDR", length=4, + field=IPField), + 24634: _N910F("NAT_LOG_FIELD_IDX_EXT_PORT_FIRST"), + 24635: _N910F("NAT_LOG_FIELD_IDX_EXT_PORT_LAST"), + # Cisco ASA5500 Series NetFlow + 33000: _N910F("INGRESS_ACL_ID"), + 33001: _N910F("EGRESS_ACL_ID"), + 33002: _N910F("FW_EXT_EVENT"), + # Cisco TrustSec + 34000: _N910F("SGT_SOURCE_TAG"), + 34001: _N910F("SGT_DESTINATION_TAG"), + 34002: _N910F("SGT_SOURCE_NAME"), + 34003: _N910F("SGT_DESTINATION_NAME"), + # medianet performance monitor + 37000: _N910F("PACKETS_DROPPED"), + 37003: _N910F("BYTE_RATE"), + 37004: _N910F("APPLICATION_MEDIA_BYTES"), + 37006: _N910F("APPLICATION_MEDIA_BYTE_RATE"), + 37007: _N910F("APPLICATION_MEDIA_PACKETS"), + 37009: _N910F("APPLICATION_MEDIA_PACKET_RATE"), + 37011: _N910F("APPLICATION_MEDIA_EVENT"), + 37012: _N910F("MONITOR_EVENT"), + 37013: _N910F("TIMESTAMP_INTERVAL"), + 37014: _N910F("TRANSPORT_PACKETS_EXPECTED"), + 37016: _N910F("TRANSPORT_ROUND_TRIP_TIME"), + 37017: _N910F("TRANSPORT_EVENT_PACKET_LOSS"), + 37019: _N910F("TRANSPORT_PACKETS_LOST"), + 37021: _N910F("TRANSPORT_PACKETS_LOST_RATE"), + 37022: _N910F("TRANSPORT_RTP_SSRC"), + 37023: _N910F("TRANSPORT_RTP_JITTER_MEAN"), + 37024: _N910F("TRANSPORT_RTP_JITTER_MIN"), + 37025: _N910F("TRANSPORT_RTP_JITTER_MAX"), + 37041: _N910F("TRANSPORT_RTP_PAYLOAD_TYPE"), + 37071: _N910F("TRANSPORT_BYTES_OUT_OF_ORDER"), + 37074: _N910F("TRANSPORT_PACKETS_OUT_OF_ORDER"), + 37083: _N910F("TRANSPORT_TCP_WINDOWS_SIZE_MIN"), + 37084: _N910F("TRANSPORT_TCP_WINDOWS_SIZE_MAX"), + 37085: _N910F("TRANSPORT_TCP_WINDOWS_SIZE_MEAN"), + 37086: _N910F("TRANSPORT_TCP_MAXIMUM_SEGMENT_SIZE"), + # Cisco ASA 5500 + 40000: _N910F("AAA_USERNAME"), + 40001: _N910F("XLATE_SRC_ADDR_IPV4", length=4, + field=IPField), + 40002: _N910F("XLATE_DST_ADDR_IPV4", length=4, + field=IPField), + 40003: _N910F("XLATE_SRC_PORT"), + 40004: _N910F("XLATE_DST_PORT"), + 40005: _N910F("FW_EVENT"), + # v9 nTop extensions + 80 + NTOP_BASE: _N910F("SRC_FRAGMENTS"), + 81 + NTOP_BASE: _N910F("DST_FRAGMENTS"), + 82 + NTOP_BASE: _N910F("SRC_TO_DST_MAX_THROUGHPUT"), + 83 + NTOP_BASE: _N910F("SRC_TO_DST_MIN_THROUGHPUT"), + 84 + NTOP_BASE: _N910F("SRC_TO_DST_AVG_THROUGHPUT"), + 85 + NTOP_BASE: _N910F("SRC_TO_SRC_MAX_THROUGHPUT"), + 86 + NTOP_BASE: _N910F("SRC_TO_SRC_MIN_THROUGHPUT"), + 87 + NTOP_BASE: _N910F("SRC_TO_SRC_AVG_THROUGHPUT"), + 88 + NTOP_BASE: _N910F("NUM_PKTS_UP_TO_128_BYTES"), + 89 + NTOP_BASE: _N910F("NUM_PKTS_128_TO_256_BYTES"), + 90 + NTOP_BASE: _N910F("NUM_PKTS_256_TO_512_BYTES"), + 91 + NTOP_BASE: _N910F("NUM_PKTS_512_TO_1024_BYTES"), + 92 + NTOP_BASE: _N910F("NUM_PKTS_1024_TO_1514_BYTES"), + 93 + NTOP_BASE: _N910F("NUM_PKTS_OVER_1514_BYTES"), + 98 + NTOP_BASE: _N910F("CUMULATIVE_ICMP_TYPE"), + 101 + NTOP_BASE: _N910F("SRC_IP_COUNTRY"), + 102 + NTOP_BASE: _N910F("SRC_IP_CITY"), + 103 + NTOP_BASE: _N910F("DST_IP_COUNTRY"), + 104 + NTOP_BASE: _N910F("DST_IP_CITY"), + 105 + NTOP_BASE: _N910F("FLOW_PROTO_PORT"), + 106 + NTOP_BASE: _N910F("UPSTREAM_TUNNEL_ID"), + 107 + NTOP_BASE: _N910F("LONGEST_FLOW_PKT"), + 108 + NTOP_BASE: _N910F("SHORTEST_FLOW_PKT"), + 109 + NTOP_BASE: _N910F("RETRANSMITTED_IN_PKTS"), + 110 + NTOP_BASE: _N910F("RETRANSMITTED_OUT_PKTS"), + 111 + NTOP_BASE: _N910F("OOORDER_IN_PKTS"), + 112 + NTOP_BASE: _N910F("OOORDER_OUT_PKTS"), + 113 + NTOP_BASE: _N910F("UNTUNNELED_PROTOCOL"), + 114 + NTOP_BASE: _N910F("UNTUNNELED_IPV4_SRC_ADDR", length=4, + field=IPField), + 115 + NTOP_BASE: _N910F("UNTUNNELED_L4_SRC_PORT"), + 116 + NTOP_BASE: _N910F("UNTUNNELED_IPV4_DST_ADDR", length=4, + field=IPField), + 117 + NTOP_BASE: _N910F("UNTUNNELED_L4_DST_PORT"), + 118 + NTOP_BASE: _N910F("L7_PROTO"), + 119 + NTOP_BASE: _N910F("L7_PROTO_NAME"), + 120 + NTOP_BASE: _N910F("DOWNSTREAM_TUNNEL_ID"), + 121 + NTOP_BASE: _N910F("FLOW_USER_NAME"), + 122 + NTOP_BASE: _N910F("FLOW_SERVER_NAME"), + 123 + NTOP_BASE: _N910F("CLIENT_NW_LATENCY_MS"), + 124 + NTOP_BASE: _N910F("SERVER_NW_LATENCY_MS"), + 125 + NTOP_BASE: _N910F("APPL_LATENCY_MS"), + 126 + NTOP_BASE: _N910F("PLUGIN_NAME"), + 127 + NTOP_BASE: _N910F("RETRANSMITTED_IN_BYTES"), + 128 + NTOP_BASE: _N910F("RETRANSMITTED_OUT_BYTES"), + 130 + NTOP_BASE: _N910F("SIP_CALL_ID"), + 131 + NTOP_BASE: _N910F("SIP_CALLING_PARTY"), + 132 + NTOP_BASE: _N910F("SIP_CALLED_PARTY"), + 133 + NTOP_BASE: _N910F("SIP_RTP_CODECS"), + 134 + NTOP_BASE: _N910F("SIP_INVITE_TIME"), + 135 + NTOP_BASE: _N910F("SIP_TRYING_TIME"), + 136 + NTOP_BASE: _N910F("SIP_RINGING_TIME"), + 137 + NTOP_BASE: _N910F("SIP_INVITE_OK_TIME"), + 138 + NTOP_BASE: _N910F("SIP_INVITE_FAILURE_TIME"), + 139 + NTOP_BASE: _N910F("SIP_BYE_TIME"), + 140 + NTOP_BASE: _N910F("SIP_BYE_OK_TIME"), + 141 + NTOP_BASE: _N910F("SIP_CANCEL_TIME"), + 142 + NTOP_BASE: _N910F("SIP_CANCEL_OK_TIME"), + 143 + NTOP_BASE: _N910F("SIP_RTP_IPV4_SRC_ADDR", length=4, + field=IPField), + 144 + NTOP_BASE: _N910F("SIP_RTP_L4_SRC_PORT"), + 145 + NTOP_BASE: _N910F("SIP_RTP_IPV4_DST_ADDR", length=4, + field=IPField), + 146 + NTOP_BASE: _N910F("SIP_RTP_L4_DST_PORT"), + 147 + NTOP_BASE: _N910F("SIP_RESPONSE_CODE"), + 148 + NTOP_BASE: _N910F("SIP_REASON_CAUSE"), + 150 + NTOP_BASE: _N910F("RTP_FIRST_SEQ"), + 151 + NTOP_BASE: _N910F("RTP_FIRST_TS"), + 152 + NTOP_BASE: _N910F("RTP_LAST_SEQ"), + 153 + NTOP_BASE: _N910F("RTP_LAST_TS"), + 154 + NTOP_BASE: _N910F("RTP_IN_JITTER"), + 155 + NTOP_BASE: _N910F("RTP_OUT_JITTER"), + 156 + NTOP_BASE: _N910F("RTP_IN_PKT_LOST"), + 157 + NTOP_BASE: _N910F("RTP_OUT_PKT_LOST"), + 158 + NTOP_BASE: _N910F("RTP_OUT_PAYLOAD_TYPE"), + 159 + NTOP_BASE: _N910F("RTP_IN_MAX_DELTA"), + 160 + NTOP_BASE: _N910F("RTP_OUT_MAX_DELTA"), + 161 + NTOP_BASE: _N910F("RTP_IN_PAYLOAD_TYPE"), + 168 + NTOP_BASE: _N910F("SRC_PROC_PID"), + 169 + NTOP_BASE: _N910F("SRC_PROC_NAME"), + 180 + NTOP_BASE: _N910F("HTTP_URL"), + 181 + NTOP_BASE: _N910F("HTTP_RET_CODE"), + 182 + NTOP_BASE: _N910F("HTTP_REFERER"), + 183 + NTOP_BASE: _N910F("HTTP_UA"), + 184 + NTOP_BASE: _N910F("HTTP_MIME"), + 185 + NTOP_BASE: _N910F("SMTP_MAIL_FROM"), + 186 + NTOP_BASE: _N910F("SMTP_RCPT_TO"), + 187 + NTOP_BASE: _N910F("HTTP_HOST"), + 188 + NTOP_BASE: _N910F("SSL_SERVER_NAME"), + 189 + NTOP_BASE: _N910F("BITTORRENT_HASH"), + 195 + NTOP_BASE: _N910F("MYSQL_SRV_VERSION"), + 196 + NTOP_BASE: _N910F("MYSQL_USERNAME"), + 197 + NTOP_BASE: _N910F("MYSQL_DB"), + 198 + NTOP_BASE: _N910F("MYSQL_QUERY"), + 199 + NTOP_BASE: _N910F("MYSQL_RESPONSE"), + 200 + NTOP_BASE: _N910F("ORACLE_USERNAME"), + 201 + NTOP_BASE: _N910F("ORACLE_QUERY"), + 202 + NTOP_BASE: _N910F("ORACLE_RSP_CODE"), + 203 + NTOP_BASE: _N910F("ORACLE_RSP_STRING"), + 204 + NTOP_BASE: _N910F("ORACLE_QUERY_DURATION"), + 205 + NTOP_BASE: _N910F("DNS_QUERY"), + 206 + NTOP_BASE: _N910F("DNS_QUERY_ID"), + 207 + NTOP_BASE: _N910F("DNS_QUERY_TYPE"), + 208 + NTOP_BASE: _N910F("DNS_RET_CODE"), + 209 + NTOP_BASE: _N910F("DNS_NUM_ANSWERS"), + 210 + NTOP_BASE: _N910F("POP_USER"), + 220 + NTOP_BASE: _N910F("GTPV1_REQ_MSG_TYPE"), + 221 + NTOP_BASE: _N910F("GTPV1_RSP_MSG_TYPE"), + 222 + NTOP_BASE: _N910F("GTPV1_C2S_TEID_DATA"), + 223 + NTOP_BASE: _N910F("GTPV1_C2S_TEID_CTRL"), + 224 + NTOP_BASE: _N910F("GTPV1_S2C_TEID_DATA"), + 225 + NTOP_BASE: _N910F("GTPV1_S2C_TEID_CTRL"), + 226 + NTOP_BASE: _N910F("GTPV1_END_USER_IP"), + 227 + NTOP_BASE: _N910F("GTPV1_END_USER_IMSI"), + 228 + NTOP_BASE: _N910F("GTPV1_END_USER_MSISDN"), + 229 + NTOP_BASE: _N910F("GTPV1_END_USER_IMEI"), + 230 + NTOP_BASE: _N910F("GTPV1_APN_NAME"), + 231 + NTOP_BASE: _N910F("GTPV1_RAI_MCC"), + 232 + NTOP_BASE: _N910F("GTPV1_RAI_MNC"), + 233 + NTOP_BASE: _N910F("GTPV1_ULI_CELL_LAC"), + 234 + NTOP_BASE: _N910F("GTPV1_ULI_CELL_CI"), + 235 + NTOP_BASE: _N910F("GTPV1_ULI_SAC"), + 236 + NTOP_BASE: _N910F("GTPV1_RAT_TYPE"), + 240 + NTOP_BASE: _N910F("RADIUS_REQ_MSG_TYPE"), + 241 + NTOP_BASE: _N910F("RADIUS_RSP_MSG_TYPE"), + 242 + NTOP_BASE: _N910F("RADIUS_USER_NAME"), + 243 + NTOP_BASE: _N910F("RADIUS_CALLING_STATION_ID"), + 244 + NTOP_BASE: _N910F("RADIUS_CALLED_STATION_ID"), + 245 + NTOP_BASE: _N910F("RADIUS_NAS_IP_ADDR"), + 246 + NTOP_BASE: _N910F("RADIUS_NAS_IDENTIFIER"), + 247 + NTOP_BASE: _N910F("RADIUS_USER_IMSI"), + 248 + NTOP_BASE: _N910F("RADIUS_USER_IMEI"), + 249 + NTOP_BASE: _N910F("RADIUS_FRAMED_IP_ADDR"), + 250 + NTOP_BASE: _N910F("RADIUS_ACCT_SESSION_ID"), + 251 + NTOP_BASE: _N910F("RADIUS_ACCT_STATUS_TYPE"), + 252 + NTOP_BASE: _N910F("RADIUS_ACCT_IN_OCTETS"), + 253 + NTOP_BASE: _N910F("RADIUS_ACCT_OUT_OCTETS"), + 254 + NTOP_BASE: _N910F("RADIUS_ACCT_IN_PKTS"), + 255 + NTOP_BASE: _N910F("RADIUS_ACCT_OUT_PKTS"), + 260 + NTOP_BASE: _N910F("IMAP_LOGIN"), + 270 + NTOP_BASE: _N910F("GTPV2_REQ_MSG_TYPE"), + 271 + NTOP_BASE: _N910F("GTPV2_RSP_MSG_TYPE"), + 272 + NTOP_BASE: _N910F("GTPV2_C2S_S1U_GTPU_TEID"), + 273 + NTOP_BASE: _N910F("GTPV2_C2S_S1U_GTPU_IP"), + 274 + NTOP_BASE: _N910F("GTPV2_S2C_S1U_GTPU_TEID"), + 275 + NTOP_BASE: _N910F("GTPV2_S2C_S1U_GTPU_IP"), + 276 + NTOP_BASE: _N910F("GTPV2_END_USER_IMSI"), + 277 + NTOP_BASE: _N910F("GTPV2_END_USER_MSISDN"), + 278 + NTOP_BASE: _N910F("GTPV2_APN_NAME"), + 279 + NTOP_BASE: _N910F("GTPV2_ULI_MCC"), + 280 + NTOP_BASE: _N910F("GTPV2_ULI_MNC"), + 281 + NTOP_BASE: _N910F("GTPV2_ULI_CELL_TAC"), + 282 + NTOP_BASE: _N910F("GTPV2_ULI_CELL_ID"), + 283 + NTOP_BASE: _N910F("GTPV2_RAT_TYPE"), + 284 + NTOP_BASE: _N910F("GTPV2_PDN_IP"), + 285 + NTOP_BASE: _N910F("GTPV2_END_USER_IMEI"), + 290 + NTOP_BASE: _N910F("SRC_AS_PATH_1"), + 291 + NTOP_BASE: _N910F("SRC_AS_PATH_2"), + 292 + NTOP_BASE: _N910F("SRC_AS_PATH_3"), + 293 + NTOP_BASE: _N910F("SRC_AS_PATH_4"), + 294 + NTOP_BASE: _N910F("SRC_AS_PATH_5"), + 295 + NTOP_BASE: _N910F("SRC_AS_PATH_6"), + 296 + NTOP_BASE: _N910F("SRC_AS_PATH_7"), + 297 + NTOP_BASE: _N910F("SRC_AS_PATH_8"), + 298 + NTOP_BASE: _N910F("SRC_AS_PATH_9"), + 299 + NTOP_BASE: _N910F("SRC_AS_PATH_10"), + 300 + NTOP_BASE: _N910F("DST_AS_PATH_1"), + 301 + NTOP_BASE: _N910F("DST_AS_PATH_2"), + 302 + NTOP_BASE: _N910F("DST_AS_PATH_3"), + 303 + NTOP_BASE: _N910F("DST_AS_PATH_4"), + 304 + NTOP_BASE: _N910F("DST_AS_PATH_5"), + 305 + NTOP_BASE: _N910F("DST_AS_PATH_6"), + 306 + NTOP_BASE: _N910F("DST_AS_PATH_7"), + 307 + NTOP_BASE: _N910F("DST_AS_PATH_8"), + 308 + NTOP_BASE: _N910F("DST_AS_PATH_9"), + 309 + NTOP_BASE: _N910F("DST_AS_PATH_10"), + 320 + NTOP_BASE: _N910F("MYSQL_APPL_LATENCY_USEC"), + 321 + NTOP_BASE: _N910F("GTPV0_REQ_MSG_TYPE"), + 322 + NTOP_BASE: _N910F("GTPV0_RSP_MSG_TYPE"), + 323 + NTOP_BASE: _N910F("GTPV0_TID"), + 324 + NTOP_BASE: _N910F("GTPV0_END_USER_IP"), + 325 + NTOP_BASE: _N910F("GTPV0_END_USER_MSISDN"), + 326 + NTOP_BASE: _N910F("GTPV0_APN_NAME"), + 327 + NTOP_BASE: _N910F("GTPV0_RAI_MCC"), + 328 + NTOP_BASE: _N910F("GTPV0_RAI_MNC"), + 329 + NTOP_BASE: _N910F("GTPV0_RAI_CELL_LAC"), + 330 + NTOP_BASE: _N910F("GTPV0_RAI_CELL_RAC"), + 331 + NTOP_BASE: _N910F("GTPV0_RESPONSE_CAUSE"), + 332 + NTOP_BASE: _N910F("GTPV1_RESPONSE_CAUSE"), + 333 + NTOP_BASE: _N910F("GTPV2_RESPONSE_CAUSE"), + 334 + NTOP_BASE: _N910F("NUM_PKTS_TTL_5_32"), + 335 + NTOP_BASE: _N910F("NUM_PKTS_TTL_32_64"), + 336 + NTOP_BASE: _N910F("NUM_PKTS_TTL_64_96"), + 337 + NTOP_BASE: _N910F("NUM_PKTS_TTL_96_128"), + 338 + NTOP_BASE: _N910F("NUM_PKTS_TTL_128_160"), + 339 + NTOP_BASE: _N910F("NUM_PKTS_TTL_160_192"), + 340 + NTOP_BASE: _N910F("NUM_PKTS_TTL_192_224"), + 341 + NTOP_BASE: _N910F("NUM_PKTS_TTL_224_255"), + 342 + NTOP_BASE: _N910F("GTPV1_RAI_LAC"), + 343 + NTOP_BASE: _N910F("GTPV1_RAI_RAC"), + 344 + NTOP_BASE: _N910F("GTPV1_ULI_MCC"), + 345 + NTOP_BASE: _N910F("GTPV1_ULI_MNC"), + 346 + NTOP_BASE: _N910F("NUM_PKTS_TTL_2_5"), + 347 + NTOP_BASE: _N910F("NUM_PKTS_TTL_EQ_1"), + 348 + NTOP_BASE: _N910F("RTP_SIP_CALL_ID"), + 349 + NTOP_BASE: _N910F("IN_SRC_OSI_SAP"), + 350 + NTOP_BASE: _N910F("OUT_DST_OSI_SAP"), + 351 + NTOP_BASE: _N910F("WHOIS_DAS_DOMAIN"), + 352 + NTOP_BASE: _N910F("DNS_TTL_ANSWER"), + 353 + NTOP_BASE: _N910F("DHCP_CLIENT_MAC", length=6, + field=MACField), + 354 + NTOP_BASE: _N910F("DHCP_CLIENT_IP", length=4, + field=IPField), + 355 + NTOP_BASE: _N910F("DHCP_CLIENT_NAME"), + 356 + NTOP_BASE: _N910F("FTP_LOGIN"), + 357 + NTOP_BASE: _N910F("FTP_PASSWORD"), + 358 + NTOP_BASE: _N910F("FTP_COMMAND"), + 359 + NTOP_BASE: _N910F("FTP_COMMAND_RET_CODE"), + 360 + NTOP_BASE: _N910F("HTTP_METHOD"), + 361 + NTOP_BASE: _N910F("HTTP_SITE"), + 362 + NTOP_BASE: _N910F("SIP_C_IP"), + 363 + NTOP_BASE: _N910F("SIP_CALL_STATE"), + 364 + NTOP_BASE: _N910F("EPP_REGISTRAR_NAME"), + 365 + NTOP_BASE: _N910F("EPP_CMD"), + 366 + NTOP_BASE: _N910F("EPP_CMD_ARGS"), + 367 + NTOP_BASE: _N910F("EPP_RSP_CODE"), + 368 + NTOP_BASE: _N910F("EPP_REASON_STR"), + 369 + NTOP_BASE: _N910F("EPP_SERVER_NAME"), + 370 + NTOP_BASE: _N910F("RTP_IN_MOS"), + 371 + NTOP_BASE: _N910F("RTP_IN_R_FACTOR"), + 372 + NTOP_BASE: _N910F("SRC_PROC_USER_NAME"), + 373 + NTOP_BASE: _N910F("SRC_FATHER_PROC_PID"), + 374 + NTOP_BASE: _N910F("SRC_FATHER_PROC_NAME"), + 375 + NTOP_BASE: _N910F("DST_PROC_PID"), + 376 + NTOP_BASE: _N910F("DST_PROC_NAME"), + 377 + NTOP_BASE: _N910F("DST_PROC_USER_NAME"), + 378 + NTOP_BASE: _N910F("DST_FATHER_PROC_PID"), + 379 + NTOP_BASE: _N910F("DST_FATHER_PROC_NAME"), + 380 + NTOP_BASE: _N910F("RTP_RTT"), + 381 + NTOP_BASE: _N910F("RTP_IN_TRANSIT"), + 382 + NTOP_BASE: _N910F("RTP_OUT_TRANSIT"), + 383 + NTOP_BASE: _N910F("SRC_PROC_ACTUAL_MEMORY"), + 384 + NTOP_BASE: _N910F("SRC_PROC_PEAK_MEMORY"), + 385 + NTOP_BASE: _N910F("SRC_PROC_AVERAGE_CPU_LOAD"), + 386 + NTOP_BASE: _N910F("SRC_PROC_NUM_PAGE_FAULTS"), + 387 + NTOP_BASE: _N910F("DST_PROC_ACTUAL_MEMORY"), + 388 + NTOP_BASE: _N910F("DST_PROC_PEAK_MEMORY"), + 389 + NTOP_BASE: _N910F("DST_PROC_AVERAGE_CPU_LOAD"), + 390 + NTOP_BASE: _N910F("DST_PROC_NUM_PAGE_FAULTS"), + 391 + NTOP_BASE: _N910F("DURATION_IN"), + 392 + NTOP_BASE: _N910F("DURATION_OUT"), + 393 + NTOP_BASE: _N910F("SRC_PROC_PCTG_IOWAIT"), + 394 + NTOP_BASE: _N910F("DST_PROC_PCTG_IOWAIT"), + 395 + NTOP_BASE: _N910F("RTP_DTMF_TONES"), + 396 + NTOP_BASE: _N910F("UNTUNNELED_IPV6_SRC_ADDR", length=16, + field=IP6Field), + 397 + NTOP_BASE: _N910F("UNTUNNELED_IPV6_DST_ADDR", length=16, + field=IP6Field), + 398 + NTOP_BASE: _N910F("DNS_RESPONSE"), + 399 + NTOP_BASE: _N910F("DIAMETER_REQ_MSG_TYPE"), + 400 + NTOP_BASE: _N910F("DIAMETER_RSP_MSG_TYPE"), + 401 + NTOP_BASE: _N910F("DIAMETER_REQ_ORIGIN_HOST"), + 402 + NTOP_BASE: _N910F("DIAMETER_RSP_ORIGIN_HOST"), + 403 + NTOP_BASE: _N910F("DIAMETER_REQ_USER_NAME"), + 404 + NTOP_BASE: _N910F("DIAMETER_RSP_RESULT_CODE"), + 405 + NTOP_BASE: _N910F("DIAMETER_EXP_RES_VENDOR_ID"), + 406 + NTOP_BASE: _N910F("DIAMETER_EXP_RES_RESULT_CODE"), + 407 + NTOP_BASE: _N910F("S1AP_ENB_UE_S1AP_ID"), + 408 + NTOP_BASE: _N910F("S1AP_MME_UE_S1AP_ID"), + 409 + NTOP_BASE: _N910F("S1AP_MSG_EMM_TYPE_MME_TO_ENB"), + 410 + NTOP_BASE: _N910F("S1AP_MSG_ESM_TYPE_MME_TO_ENB"), + 411 + NTOP_BASE: _N910F("S1AP_MSG_EMM_TYPE_ENB_TO_MME"), + 412 + NTOP_BASE: _N910F("S1AP_MSG_ESM_TYPE_ENB_TO_MME"), + 413 + NTOP_BASE: _N910F("S1AP_CAUSE_ENB_TO_MME"), + 414 + NTOP_BASE: _N910F("S1AP_DETAILED_CAUSE_ENB_TO_MME"), + 415 + NTOP_BASE: _N910F("TCP_WIN_MIN_IN"), + 416 + NTOP_BASE: _N910F("TCP_WIN_MAX_IN"), + 417 + NTOP_BASE: _N910F("TCP_WIN_MSS_IN"), + 418 + NTOP_BASE: _N910F("TCP_WIN_SCALE_IN"), + 419 + NTOP_BASE: _N910F("TCP_WIN_MIN_OUT"), + 420 + NTOP_BASE: _N910F("TCP_WIN_MAX_OUT"), + 421 + NTOP_BASE: _N910F("TCP_WIN_MSS_OUT"), + 422 + NTOP_BASE: _N910F("TCP_WIN_SCALE_OUT"), + 423 + NTOP_BASE: _N910F("DHCP_REMOTE_ID"), + 424 + NTOP_BASE: _N910F("DHCP_SUBSCRIBER_ID"), + 425 + NTOP_BASE: _N910F("SRC_PROC_UID"), + 426 + NTOP_BASE: _N910F("DST_PROC_UID"), + 427 + NTOP_BASE: _N910F("APPLICATION_NAME"), + 428 + NTOP_BASE: _N910F("USER_NAME"), + 429 + NTOP_BASE: _N910F("DHCP_MESSAGE_TYPE"), + 430 + NTOP_BASE: _N910F("RTP_IN_PKT_DROP"), + 431 + NTOP_BASE: _N910F("RTP_OUT_PKT_DROP"), + 432 + NTOP_BASE: _N910F("RTP_OUT_MOS"), + 433 + NTOP_BASE: _N910F("RTP_OUT_R_FACTOR"), + 434 + NTOP_BASE: _N910F("RTP_MOS"), + 435 + NTOP_BASE: _N910F("GTPV2_S5_S8_GTPC_TEID"), + 436 + NTOP_BASE: _N910F("RTP_R_FACTOR"), + 437 + NTOP_BASE: _N910F("RTP_SSRC"), + 438 + NTOP_BASE: _N910F("PAYLOAD_HASH"), + 439 + NTOP_BASE: _N910F("GTPV2_C2S_S5_S8_GTPU_TEID"), + 440 + NTOP_BASE: _N910F("GTPV2_S2C_S5_S8_GTPU_TEID"), + 441 + NTOP_BASE: _N910F("GTPV2_C2S_S5_S8_GTPU_IP"), + 442 + NTOP_BASE: _N910F("GTPV2_S2C_S5_S8_GTPU_IP"), + 443 + NTOP_BASE: _N910F("SRC_AS_MAP"), + 444 + NTOP_BASE: _N910F("DST_AS_MAP"), + 445 + NTOP_BASE: _N910F("DIAMETER_HOP_BY_HOP_ID"), + 446 + NTOP_BASE: _N910F("UPSTREAM_SESSION_ID"), + 447 + NTOP_BASE: _N910F("DOWNSTREAM_SESSION_ID"), + 448 + NTOP_BASE: _N910F("SRC_IP_LONG"), + 449 + NTOP_BASE: _N910F("SRC_IP_LAT"), + 450 + NTOP_BASE: _N910F("DST_IP_LONG"), + 451 + NTOP_BASE: _N910F("DST_IP_LAT"), + 452 + NTOP_BASE: _N910F("DIAMETER_CLR_CANCEL_TYPE"), + 453 + NTOP_BASE: _N910F("DIAMETER_CLR_FLAGS"), + 454 + NTOP_BASE: _N910F("GTPV2_C2S_S5_S8_GTPC_IP"), + 455 + NTOP_BASE: _N910F("GTPV2_S2C_S5_S8_GTPC_IP"), + 456 + NTOP_BASE: _N910F("GTPV2_C2S_S5_S8_SGW_GTPU_TEID"), + 457 + NTOP_BASE: _N910F("GTPV2_S2C_S5_S8_SGW_GTPU_TEID"), + 458 + NTOP_BASE: _N910F("GTPV2_C2S_S5_S8_SGW_GTPU_IP"), + 459 + NTOP_BASE: _N910F("GTPV2_S2C_S5_S8_SGW_GTPU_IP"), + 460 + NTOP_BASE: _N910F("HTTP_X_FORWARDED_FOR"), + 461 + NTOP_BASE: _N910F("HTTP_VIA"), + 462 + NTOP_BASE: _N910F("SSDP_HOST"), + 463 + NTOP_BASE: _N910F("SSDP_USN"), + 464 + NTOP_BASE: _N910F("NETBIOS_QUERY_NAME"), + 465 + NTOP_BASE: _N910F("NETBIOS_QUERY_TYPE"), + 466 + NTOP_BASE: _N910F("NETBIOS_RESPONSE"), + 467 + NTOP_BASE: _N910F("NETBIOS_QUERY_OS"), + 468 + NTOP_BASE: _N910F("SSDP_SERVER"), + 469 + NTOP_BASE: _N910F("SSDP_TYPE"), + 470 + NTOP_BASE: _N910F("SSDP_METHOD"), + 471 + NTOP_BASE: _N910F("NPROBE_IPV4_ADDRESS", length=4, + field=IPField), +} +NetflowV910TemplateFieldTypes = { + k: v.name for k, v in NetflowV910TemplateFields.items() +} + +ScopeFieldTypes = { + 1: "System", + 2: "Interface", + 3: "Line card", + 4: "Cache", + 5: "Template", } @@ -1227,11 +1268,23 @@ class NetflowHeaderV9(Packet): IntField("SourceID", 0)] def post_build(self, pkt, pay): + + def count_by_layer(layer): + if type(layer) == NetflowFlowsetV9: + return len(layer.templates) + elif type(layer) == NetflowDataflowsetV9: + return len(layer.records) + elif type(layer) == NetflowOptionsFlowsetV9: + return 1 + else: + return 0 + if self.count is None: - count = sum(1 for x in self.layers() if x in [ - NetflowFlowsetV9, - NetflowDataflowsetV9, - NetflowOptionsFlowsetV9] + # https://www.rfc-editor.org/rfc/rfc3954#section-5.1 + count = sum( + sum(count_by_layer(self.getlayer(layer_cls, nth)) + for nth in range(1, n + 1)) + for layer_cls, n in Counter(self.layers()).items() ) pkt = struct.pack("!H", count) + pkt[2:] return pkt + pay @@ -1246,20 +1299,30 @@ class NetflowHeaderV10(Packet): IntField("flowSequence", 0), IntField("ObservationDomainID", 0)] + def post_build(self, pkt, pay): + if self.length is None: + length = len(pkt) + len(pay) + pkt = struct.pack("!H", length) + pkt[2:] + return pkt + pay + class NetflowTemplateFieldV9(Packet): name = "Netflow Flowset Template Field V9/10" fields_desc = [BitField("enterpriseBit", 0, 1), BitEnumField("fieldType", None, 15, NetflowV910TemplateFieldTypes), - ShortField("fieldLength", 0), + ShortField("fieldLength", None), ConditionalField(IntField("enterpriseNumber", 0), lambda p: p.enterpriseBit)] def __init__(self, *args, **kwargs): Packet.__init__(self, *args, **kwargs) - if self.fieldType is not None and not self.fieldLength and self.fieldType in NetflowV9TemplateFieldDefaultLengths: # noqa: E501 - self.fieldLength = NetflowV9TemplateFieldDefaultLengths[self.fieldType] # noqa: E501 + if (self.fieldType is not None and + self.fieldLength is None and + self.fieldType in NetflowV910TemplateFields): + self.fieldLength = NetflowV910TemplateFields[ + self.fieldType + ].length or None def default_payload_class(self, p): return conf.padding_layer @@ -1291,23 +1354,38 @@ def i2repr(self, pkt, v): def _GenNetflowRecordV9(cls, lengths_list): - """Internal function used to generate the Records from + """ + Internal function used to generate the Records from their template. """ _fields_desc = [] for j, k in lengths_list: - _f_data = NetflowV9TemplateFieldDecoders.get(k, None) - _f_type, _f_args = ( - _f_data if isinstance(_f_data, tuple) else (_f_data, []) - ) + # For each field, if it's known in our template list, + # try to make a nice field for it. Otherwise use an integer + # or a string default. + _f_type = None _f_kwargs = {} + _f_isint = False + if k in NetflowV910TemplateFields: + _f = NetflowV910TemplateFields[k] + _f_type = _f.field + _f_kwargs = _f.kwargs + _f_isint = _f.isint + if _f_type: if issubclass(_f_type, _AdjustableNetflowField): _f_kwargs["length"] = j _fields_desc.append( _f_type( NetflowV910TemplateFieldTypes.get(k, "unknown_data"), - 0, *_f_args, **_f_kwargs + 0, **_f_kwargs + ) + ) + elif _f_isint: + _fields_desc.append( + NBytesField( + NetflowV910TemplateFieldTypes.get(k, "unknown_data"), + 0, sz=j ) ) else: @@ -1366,14 +1444,11 @@ def default_payload_class(self, p): class NetflowDataflowsetV9(Packet): name = "Netflow DataFlowSet V9/10" fields_desc = [ShortField("templateID", 255), - FieldLenField("length", None, length_of="records", - adjust=lambda pkt, x: x + 4 + (-x % 4)), - PadField( - PacketListField( - "records", [], - NetflowRecordV9, - length_from=lambda pkt: pkt.length - 4 - ), 4, padwith=b"\x00")] + ShortField("length", None), + PacketListField( + "records", [], + NetflowRecordV9, + length_from=lambda pkt: pkt.length - 4)] @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): @@ -1391,6 +1466,15 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return NetflowOptionsFlowset10 return cls + def post_build(self, pkt, pay): + if self.length is None: + # Padding is optional, let's apply it on build + length = len(pkt) + pad = (-length) % 4 + pkt = pkt[:2] + struct.pack("!H", length + pad) + pkt[4:] + pkt += b"\x00" * pad + return pkt + pay + def _netflowv9_defragment_packet(pkt, definitions, definitions_opts, ignored): """Used internally to process a single packet during defragmenting""" @@ -1445,53 +1529,56 @@ def _netflowv9_defragment_packet(pkt, definitions, definitions_opts, ignored): current = current.payload # Dissect flowsets if NetflowDataflowsetV9 in pkt: - datafl = pkt[NetflowDataflowsetV9] - tid = datafl.templateID - if tid not in definitions and tid not in definitions_opts: - ignored.add(tid) - return - # All data is stored in one record, awaiting to be split - # If fieldValue is available, the record has not been - # defragmented: pop it - try: - data = datafl.records[0].fieldValue - datafl.records.pop(0) - except (IndexError, AttributeError): - return - res = [] - # Flowset record - # Now, according to the flow/option data, - # let's re-dissect NetflowDataflowsetV9 - if tid in definitions: - tot_len, cls = definitions[tid] - while len(data) >= tot_len: - res.append(cls(data[:tot_len])) - data = data[tot_len:] - # Inject dissected data - datafl.records = res - if data: - if len(data) <= 4: - datafl.add_payload(conf.padding_layer(data)) - else: - datafl.do_dissect_payload(data) - # Options - elif tid in definitions_opts: - (scope_len, scope_cls, - option_len, option_cls) = definitions_opts[tid] - # Dissect scopes - if scope_len: - res.append(scope_cls(data[:scope_len])) - if option_len: - res.append( - option_cls(data[scope_len:scope_len + option_len]) - ) - if len(data) > scope_len + option_len: - res.append( - conf.padding_layer(data[scope_len + option_len:]) - ) - # Inject dissected data - datafl.records = res - datafl.name = "Netflow DataFlowSet V9/10 - OPTIONS" + current = pkt + while NetflowDataflowsetV9 in current: + datafl = current[NetflowDataflowsetV9] + tid = datafl.templateID + if tid not in definitions and tid not in definitions_opts: + ignored.add(tid) + return + # All data is stored in one record, awaiting to be split + # If fieldValue is available, the record has not been + # defragmented: pop it + try: + data = datafl.records[0].fieldValue + datafl.records.pop(0) + except (IndexError, AttributeError): + return + res = [] + # Flowset record + # Now, according to the flow/option data, + # let's re-dissect NetflowDataflowsetV9 + if tid in definitions: + tot_len, cls = definitions[tid] + while len(data) >= tot_len: + res.append(cls(data[:tot_len])) + data = data[tot_len:] + # Inject dissected data + datafl.records = res + if data: + if len(data) <= 4: + datafl.add_payload(conf.padding_layer(data)) + else: + datafl.do_dissect_payload(data) + # Options + elif tid in definitions_opts: + (scope_len, scope_cls, + option_len, option_cls) = definitions_opts[tid] + # Dissect scopes + if scope_len: + res.append(scope_cls(data[:scope_len])) + if option_len: + res.append( + option_cls(data[scope_len:scope_len + option_len]) + ) + if len(data) > scope_len + option_len: + res.append( + conf.padding_layer(data[scope_len + option_len:]) + ) + # Inject dissected data + datafl.records = res + datafl.name = "Netflow DataFlowSet V9/10 - OPTIONS" + current = datafl.payload def netflowv9_defragment(plist, verb=1): @@ -1529,26 +1616,22 @@ class NetflowSession(IPSession): """Session used to defragment NetflowV9/10 packets on the flow. See help(scapy.layers.netflow) for more infos. """ - def __init__(self, *args): - IPSession.__init__(self, *args) + def __init__(self, *args, **kwargs): self.definitions = {} self.definitions_opts = {} self.ignored = set() + super(NetflowSession, self).__init__(*args, **kwargs) - def _process_packet(self, pkt): + def process(self, pkt: Packet) -> Optional[Packet]: + pkt = super(NetflowSession, self).process(pkt) + if not pkt: + return _netflowv9_defragment_packet(pkt, self.definitions, self.definitions_opts, self.ignored) return pkt - def on_packet_received(self, pkt): - # First, defragment IP if necessary - pkt = self._ip_process_packet(pkt) - # Now handle NetflowV9 defragmentation - pkt = self._process_packet(pkt) - DefaultSession.on_packet_received(self, pkt) - class NetflowOptionsRecordScopeV9(NetflowRecordV9): name = "Netflow Options Template Record V9/10 - Scope" @@ -1608,12 +1691,12 @@ def default_payload_class(self, p): return conf.padding_layer def post_build(self, pkt, pay): - if self.length is None: - pkt = pkt[:2] + struct.pack("!H", len(pkt)) + pkt[4:] if self.pad is None: # Padding 4-bytes with b"\x00" start = 10 + self.option_scope_length + self.option_field_length pkt = pkt[:start] + (-len(pkt) % 4) * b"\x00" + if self.length is None: + pkt = pkt[:2] + struct.pack("!H", len(pkt)) + pkt[4:] return pkt + pay diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py new file mode 100644 index 00000000000..fd94cc6f2ec --- /dev/null +++ b/scapy/layers/ntlm.py @@ -0,0 +1,2323 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +NTLM + +This is documented in [MS-NLMP] + +.. note:: + You will find more complete documentation for this layer over at + `GSSAPI `_ +""" + +import copy +import time +import os +import struct + +from enum import IntEnum + +from scapy.asn1.asn1 import ASN1_Codecs +from scapy.asn1.mib import conf # loads conf.mib +from scapy.asn1fields import ( + ASN1F_OID, + ASN1F_PRINTABLE_STRING, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, +) +from scapy.asn1packet import ASN1_Packet +from scapy.config import crypto_validator +from scapy.compat import bytes_base64 +from scapy.error import log_runtime +from scapy.fields import ( + ByteEnumField, + ByteField, + ConditionalField, + Field, + FieldLenField, + FlagsField, + LEIntEnumField, + LEIntField, + LEShortEnumField, + LEShortField, + LEThreeBytesField, + MultipleTypeField, + PacketField, + PacketListField, + StrField, + StrFieldUtf16, + StrFixedLenField, + StrLenFieldUtf16, + UTCTimeField, + XStrField, + XStrFixedLenField, + XStrLenField, + _StrField, +) +from scapy.packet import Packet +from scapy.sessions import StringBuffer + +from scapy.layers.gssapi import ( + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, + GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_BAD_BINDINGS, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_DEFECTIVE_CREDENTIAL, + GSS_S_DEFECTIVE_TOKEN, + GSS_S_FLAGS, + GssChannelBindings, + SSP, +) + +# Typing imports +from typing import ( + Any, + Callable, + List, + Optional, + Tuple, + Union, +) + +# Crypto imports + +from scapy.layers.tls.crypto.hash import Hash_MD4, Hash_MD5 +from scapy.layers.tls.crypto.h_mac import Hmac_MD5 + +########## +# Fields # +########## + + +# NTLM structures are all in all very complicated. Many fields don't have a fixed +# position, but are rather referred to with an offset (from the beginning of the +# structure) and a length. In addition to that, there are variants of the structure +# with missing fields when running old versions of Windows (sometimes also seen when +# talking to products that reimplement NTLM, most notably backup applications). + +# We add `_NTLMPayloadField` and `_NTLMPayloadPacket` to parse fields that use an +# offset, and `_NTLM_post_build` to be able to rebuild those offsets. +# In addition, the `NTLM_VARIANT*` allows to select what flavor of NTLM to use +# (NT, XP, or Recent). But in real world use only Recent should be used. + + +class _NTLMPayloadField(_StrField[List[Tuple[str, Any]]]): + """Special field used to dissect NTLM payloads. + This isn't trivial because the offsets are variable.""" + + __slots__ = [ + "fields", + "fields_map", + "offset", + "length_from", + "force_order", + "offset_name", + ] + islist = True + + def __init__( + self, + name, # type: str + offset, # type: Union[int, Callable[[Packet], int]] + fields, # type: List[Field[Any, Any]] + length_from=None, # type: Optional[Callable[[Packet], int]] + force_order=None, # type: Optional[List[str]] + offset_name="BufferOffset", # type: str + ): + # type: (...) -> None + self.offset = offset + self.fields = fields + self.fields_map = {field.name: field for field in fields} + self.length_from = length_from + self.force_order = force_order # whether the order of fields is fixed + self.offset_name = offset_name + super(_NTLMPayloadField, self).__init__( + name, + [ + (field.name, field.default) + for field in fields + if field.default is not None + ], + ) + + def _on_payload(self, pkt, x, func): + # type: (Optional[Packet], bytes, str) -> List[Tuple[str, Any]] + if not pkt or not x: + return [] + results = [] + for field_name, value in x: + if field_name not in self.fields_map: + continue + if not isinstance( + self.fields_map[field_name], PacketListField + ) and not isinstance(value, Packet): + value = getattr(self.fields_map[field_name], func)(pkt, value) + results.append((field_name, value)) + return results + + def i2h(self, pkt, x): + # type: (Optional[Packet], bytes) -> List[Tuple[str, str]] + return self._on_payload(pkt, x, "i2h") + + def h2i(self, pkt, x): + # type: (Optional[Packet], bytes) -> List[Tuple[str, str]] + return self._on_payload(pkt, x, "h2i") + + def i2repr(self, pkt, x): + # type: (Optional[Packet], bytes) -> str + return repr(self._on_payload(pkt, x, "i2repr")) + + def _o_pkt(self, pkt): + # type: (Optional[Packet]) -> int + if callable(self.offset): + return self.offset(pkt) + return self.offset + + def addfield(self, pkt, s, val): + # type: (Optional[Packet], bytes, Optional[List[Tuple[str, str]]]) -> bytes + # Create string buffer + buf = StringBuffer() + buf.append(s, 1) + # Calc relative offset + r_off = self._o_pkt(pkt) - len(s) + if self.force_order: + val.sort(key=lambda x: self.force_order.index(x[0])) + for field_name, value in val: + if field_name not in self.fields_map: + continue + field = self.fields_map[field_name] + offset = pkt.getfieldval(field_name + self.offset_name) + if offset is None: + # No offset specified: calc + offset = len(buf) + else: + # Calc relative offset + offset -= r_off + pad = offset + 1 - len(buf) + # Add padding if necessary + if pad > 0: + buf.append(pad * b"\x00", len(buf)) + buf.append(field.addfield(pkt, bytes(buf), value)[len(buf) :], offset + 1) + return bytes(buf) + + def getfield(self, pkt, s): + # type: (Packet, bytes) -> Tuple[bytes, List[Tuple[str, str]]] + if self.length_from is None: + ret, remain = b"", s + else: + len_pkt = self.length_from(pkt) + ret, remain = s[len_pkt:], s[:len_pkt] + if not pkt or not remain: + return s, [] + results = [] + max_offset = 0 + o_pkt = self._o_pkt(pkt) + offsets = [ + pkt.getfieldval(x.name + self.offset_name) - o_pkt for x in self.fields + ] + for i, field in enumerate(self.fields): + offset = offsets[i] + try: + length = pkt.getfieldval(field.name + "Len") + except AttributeError: + length = len(remain) - offset + # length can't be greater than the difference with the next offset + try: + length = min(length, min(x - offset for x in offsets if x > offset)) + except ValueError: + pass + if offset < 0: + continue + max_offset = max(offset + length, max_offset) + if remain[offset : offset + length]: + results.append( + ( + offset, + field.name, + field.getfield(pkt, remain[offset : offset + length])[1], + ) + ) + ret += remain[max_offset:] + results.sort(key=lambda x: x[0]) + return ret, [x[1:] for x in results] + + +class _NTLMPayloadPacket(Packet): + _NTLM_PAYLOAD_FIELD_NAME = "Payload" + + def __init__( + self, + _pkt=b"", # type: Union[bytes, bytearray] + post_transform=None, # type: Any + _internal=0, # type: int + _underlayer=None, # type: Optional[Packet] + _parent=None, # type: Optional[Packet] + **fields, # type: Any + ): + # pop unknown fields. We can't process them until the packet is initialized + unknown = { + k: fields.pop(k) + for k in list(fields) + if not any(k == f.name for f in self.fields_desc) + } + super(_NTLMPayloadPacket, self).__init__( + _pkt=_pkt, + post_transform=post_transform, + _internal=_internal, + _underlayer=_underlayer, + _parent=_parent, + **fields, + ) + # check unknown fields for implicit ones + local_fields = next( + [y.name for y in x.fields] + for x in self.fields_desc + if x.name == self._NTLM_PAYLOAD_FIELD_NAME + ) + implicit_fields = {k: v for k, v in unknown.items() if k in local_fields} + for k, value in implicit_fields.items(): + self.setfieldval(k, value) + + def getfieldval(self, attr): + # Ease compatibility with _NTLMPayloadField + try: + return super(_NTLMPayloadPacket, self).getfieldval(attr) + except AttributeError: + try: + return next( + x[1] + for x in super(_NTLMPayloadPacket, self).getfieldval( + self._NTLM_PAYLOAD_FIELD_NAME + ) + if x[0] == attr + ) + except StopIteration: + raise AttributeError(attr) + + def getfield_and_val(self, attr): + # Ease compatibility with _NTLMPayloadField + try: + return super(_NTLMPayloadPacket, self).getfield_and_val(attr) + except ValueError: + PayFields = self.get_field(self._NTLM_PAYLOAD_FIELD_NAME).fields_map + try: + return ( + PayFields[attr], + PayFields[attr].h2i( # cancel out the i2h.. it's dumb i know + self, + next( + x[1] + for x in super(_NTLMPayloadPacket, self).__getattr__( + self._NTLM_PAYLOAD_FIELD_NAME + ) + if x[0] == attr + ), + ), + ) + except (StopIteration, KeyError): + raise ValueError(attr) + + def setfieldval(self, attr, val): + # Ease compatibility with _NTLMPayloadField + try: + return super(_NTLMPayloadPacket, self).setfieldval(attr, val) + except AttributeError: + Payload = super(_NTLMPayloadPacket, self).__getattr__( + self._NTLM_PAYLOAD_FIELD_NAME + ) + if attr not in self.get_field(self._NTLM_PAYLOAD_FIELD_NAME).fields_map: + raise AttributeError(attr) + try: + Payload.pop( + next( + i + for i, x in enumerate( + super(_NTLMPayloadPacket, self).__getattr__( + self._NTLM_PAYLOAD_FIELD_NAME + ) + ) + if x[0] == attr + ) + ) + except StopIteration: + pass + Payload.append([attr, val]) + super(_NTLMPayloadPacket, self).setfieldval( + self._NTLM_PAYLOAD_FIELD_NAME, Payload + ) + + +class _NTLM_ENUM(IntEnum): + LEN = 0x0001 + MAXLEN = 0x0002 + OFFSET = 0x0004 + COUNT = 0x0008 + PAD8 = 0x1000 + + +_NTLM_CONFIG = [ + ("Len", _NTLM_ENUM.LEN), + ("MaxLen", _NTLM_ENUM.MAXLEN), + ("BufferOffset", _NTLM_ENUM.OFFSET), +] + + +def _NTLM_post_build(self, p, pay_offset, fields, config=_NTLM_CONFIG): + """Util function to build the offset and populate the lengths""" + for field_name, value in self.fields[self._NTLM_PAYLOAD_FIELD_NAME]: + fld = self.get_field(self._NTLM_PAYLOAD_FIELD_NAME).fields_map[field_name] + length = fld.i2len(self, value) + count = fld.i2count(self, value) + offset = fields[field_name] + i = 0 + r = lambda y: {2: "H", 4: "I", 8: "Q"}[y] + for fname, ftype in config: + if isinstance(ftype, dict): + ftype = ftype[field_name] + if ftype & _NTLM_ENUM.LEN: + fval = length + elif ftype & _NTLM_ENUM.OFFSET: + fval = pay_offset + elif ftype & _NTLM_ENUM.MAXLEN: + fval = length + elif ftype & _NTLM_ENUM.COUNT: + fval = count + else: + raise ValueError + if ftype & _NTLM_ENUM.PAD8: + fval += (-fval) % 8 + sz = self.get_field(field_name + fname).sz + if self.getfieldval(field_name + fname) is None: + p = ( + p[: offset + i] + + struct.pack("<%s" % r(sz), fval) + + p[offset + i + sz :] + ) + i += sz + pay_offset += length + return p + + +############## +# Structures # +############## + + +# -- Util: VARIANT class + + +class NTLM_VARIANT(IntEnum): + """ + The message variant to use for NTLM. + """ + + NT_OR_2000 = 0 + XP_OR_2003 = 1 + RECENT = 2 + + +class _NTLM_VARIANT_Packet(_NTLMPayloadPacket): + def __init__(self, *args, **kwargs): + self.VARIANT = kwargs.pop("VARIANT", NTLM_VARIANT.RECENT) + super(_NTLM_VARIANT_Packet, self).__init__(*args, **kwargs) + + def clone_with(self, *args, **kwargs): + pkt = super(_NTLM_VARIANT_Packet, self).clone_with(*args, **kwargs) + pkt.VARIANT = self.VARIANT + return pkt + + def copy(self): + pkt = super(_NTLM_VARIANT_Packet, self).copy() + pkt.VARIANT = self.VARIANT + + return pkt + + def show2(self, dump=False, indent=3, lvl="", label_lvl=""): + return self.__class__(bytes(self), VARIANT=self.VARIANT).show( + dump, indent, lvl, label_lvl + ) + + +# Sect 2.2 + + +class NTLM_Header(Packet): + name = "NTLM Header" + fields_desc = [ + StrFixedLenField("Signature", b"NTLMSSP\0", length=8), + LEIntEnumField( + "MessageType", + 3, + { + 1: "NEGOTIATE_MESSAGE", + 2: "CHALLENGE_MESSAGE", + 3: "AUTHENTICATE_MESSAGE", + }, + ), + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if cls is NTLM_Header and _pkt and len(_pkt) >= 10: + MessageType = struct.unpack("= NTLM_VARIANT.XP_OR_2003 + and ( + ( + ( + 40 + if pkt.DomainNameBufferOffset is None + else pkt.DomainNameBufferOffset or len(pkt.original or b"") + ) + > 32 + ) + or pkt.fields.get(x.name, b"") + ), + ) + for x in _NTLM_Version.fields_desc + ] + + [ + # Payload + _NTLMPayloadField( + "Payload", + OFFSET, + [ + # "MUST be encoded using the OEM character set" + StrField("DomainName", b""), + StrField("WorkstationName", b""), + ], + ), + ] + ) + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _NTLM_post_build( + self, + pkt, + self.OFFSET(), + { + "DomainName": 16, + "WorkstationName": 24, + }, + ) + + pay + ) + + +# Challenge + + +class Single_Host_Data(Packet): + fields_desc = [ + LEIntField("Size", None), + LEIntField("Z4", 0), + # "CustomData" guessed using LSAP_TOKEN_INFO_INTEGRITY. + FlagsField( + "Flags", + 0, + -32, + { + 0x00000001: "UAC-Restricted", + }, + ), + LEIntEnumField( + "TokenIL", + 0x00002000, + { + 0x00000000: "Untrusted", + 0x00001000: "Low", + 0x00002000: "Medium", + 0x00003000: "High", + 0x00004000: "System", + 0x00005000: "Protected process", + }, + ), + XStrFixedLenField("MachineID", b"", length=32), + # KB 5068222 - still waiting for [MS-KILE] update (oct. 2025) + ConditionalField( + XStrFixedLenField("PermanentMachineID", None, length=32), + lambda pkt: pkt.Size is None or pkt.Size > 48, + ), + ] + + def post_build(self, pkt, pay): + if self.Size is None: + pkt = struct.pack("= NTLM_VARIANT.XP_OR_2003 + and ( + ((pkt.TargetInfoBufferOffset or 56) > 40) + or pkt.fields.get(x.name, b"") + ), + ) + for x in _NTLM_Version.fields_desc + ] + + [ + # Payload + _NTLMPayloadField( + "Payload", + OFFSET, + [ + _NTLMStrField("TargetName", b""), + PacketListField("TargetInfo", [AV_PAIR()], AV_PAIR), + ], + ), + ] + ) + + def getAv(self, AvId): + try: + return next(x for x in self.TargetInfo if x.AvId == AvId) + except (StopIteration, AttributeError): + raise IndexError + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _NTLM_post_build( + self, + pkt, + self.OFFSET(), + { + "TargetName": 12, + "TargetInfo": 40, + }, + ) + + pay + ) + + +# Authenticate + + +class LM_RESPONSE(Packet): + fields_desc = [ + StrFixedLenField("Response", b"", length=24), + ] + + +class LMv2_RESPONSE(Packet): + fields_desc = [ + StrFixedLenField("Response", b"", length=16), + StrFixedLenField("ChallengeFromClient", b"", length=8), + ] + + +class NTLM_RESPONSE(Packet): + fields_desc = [ + StrFixedLenField("Response", b"", length=24), + ] + + +class NTLMv2_CLIENT_CHALLENGE(Packet): + fields_desc = [ + ByteField("RespType", 1), + ByteField("HiRespType", 1), + LEShortField("Reserved1", 0), + LEIntField("Reserved2", 0), + UTCTimeField( + "TimeStamp", None, fmt="= NTLM_VARIANT.XP_OR_2003 + and ( + ((pkt.DomainNameBufferOffset or 88) > 64) + or pkt.fields.get(x.name, b"") + ), + ) + for x in _NTLM_Version.fields_desc + ] + + [ + # MIC + ConditionalField( + # (not present on some old Windows versions. We use a heuristic) + XStrFixedLenField("MIC", b"", length=16), + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.RECENT + and ( + ((pkt.DomainNameBufferOffset or 88) > 72) + or pkt.fields.get("MIC", b"") + ), + ), + # Payload + _NTLMPayloadField( + "Payload", + OFFSET, + [ + MultipleTypeField( + [ + ( + PacketField( + "LmChallengeResponse", + LMv2_RESPONSE(), + LMv2_RESPONSE, + ), + lambda pkt: pkt.NTLM_VERSION == 2, + ) + ], + PacketField("LmChallengeResponse", LM_RESPONSE(), LM_RESPONSE), + ), + MultipleTypeField( + [ + ( + PacketField( + "NtChallengeResponse", + NTLMv2_RESPONSE(), + NTLMv2_RESPONSE, + ), + lambda pkt: pkt.NTLM_VERSION == 2, + ) + ], + PacketField( + "NtChallengeResponse", NTLM_RESPONSE(), NTLM_RESPONSE + ), + ), + _NTLMStrField("DomainName", b""), + _NTLMStrField("UserName", b""), + _NTLMStrField("Workstation", b""), + XStrField("EncryptedRandomSessionKey", b""), + ], + ), + ] + ) + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _NTLM_post_build( + self, + pkt, + self.OFFSET(), + { + "LmChallengeResponse": 12, + "NtChallengeResponse": 20, + "DomainName": 28, + "UserName": 36, + "Workstation": 44, + "EncryptedRandomSessionKey": 52, + }, + ) + + pay + ) + + def compute_mic(self, ExportedSessionKey, negotiate, challenge): + self.MIC = b"\x00" * 16 + self.MIC = HMAC_MD5( + ExportedSessionKey, bytes(negotiate) + bytes(challenge) + bytes(self) + ) + + +class NTLM_AUTHENTICATE_V2(NTLM_AUTHENTICATE): + NTLM_VERSION = 2 + + +def HTTP_ntlm_negotiate(ntlm_negotiate): + """Create an HTTP NTLM negotiate packet from an NTLM_NEGOTIATE message""" + assert isinstance(ntlm_negotiate, NTLM_NEGOTIATE) + from scapy.layers.http import HTTP, HTTPRequest + + return HTTP() / HTTPRequest( + Authorization=b"NTLM " + bytes_base64(bytes(ntlm_negotiate)) + ) + + +# Experimental - Reversed stuff + +# This is the GSSAPI NegoEX Exchange metadata blob. This is not documented +# but described as an "opaque blob": this was reversed and everything is a +# placeholder. + + +class NEGOEX_EXCHANGE_NTLM_ITEM(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_OID("oid", ""), + ASN1F_PRINTABLE_STRING("token", ""), + explicit_tag=0x31, + ), + explicit_tag=0x80, + ) + ) + + +class NEGOEX_EXCHANGE_NTLM(ASN1_Packet): + """ + GSSAPI NegoEX Exchange metadata blob + This was reversed and may be meaningless + """ + + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("items", [], NEGOEX_EXCHANGE_NTLM_ITEM), implicit_tag=0xA0 + ), + ) + + +# Crypto - [MS-NLMP] + + +def HMAC_MD5(key, data): + return Hmac_MD5(key=key).digest(data) + + +def MD4le(x): + """ + MD4 over a string encoded as utf-16le + """ + return Hash_MD4().digest(x.encode("utf-16le")) + + +def RC4Init(key): + """Alleged RC4""" + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms + + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms, + ) + except ImportError: + decrepit_algorithms = algorithms + + algorithm = decrepit_algorithms.ARC4(key) + cipher = Cipher(algorithm, mode=None) + encryptor = cipher.encryptor() + return encryptor + + +def RC4(handle, data): + """The RC4 Encryption Algorithm""" + return handle.update(data) + + +def RC4K(key, data): + """Indicates the encryption of data item D with the key K using the + RC4 algorithm. + """ + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms + + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms, + ) + except ImportError: + decrepit_algorithms = algorithms + + algorithm = decrepit_algorithms.ARC4(key) + cipher = Cipher(algorithm, mode=None) + encryptor = cipher.encryptor() + return encryptor.update(data) + encryptor.finalize() + + +# sect 2.2.2.9 - With Extended Session Security + + +class NTLMSSP_MESSAGE_SIGNATURE(Packet): + # [MS-RPCE] sect 2.2.2.9.1/2.2.2.9.2 + fields_desc = [ + LEIntField("Version", 0x00000001), + XStrFixedLenField("Checksum", b"", length=8), + LEIntField("SeqNum", 0x00000000), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_GSSAPI_OIDS["1.3.6.1.4.1.311.2.2.10"] = NTLM_Header +_GSSAPI_SIGNATURE_OIDS["1.3.6.1.4.1.311.2.2.10"] = NTLMSSP_MESSAGE_SIGNATURE + + +# sect 3.3.2 + + +def NTOWFv2(Passwd, User, UserDom, HashNt=None): + """ + Computes the ResponseKeyNT (per [MS-NLMP] sect 3.3.2) + + :param Passwd: the plain password + :param User: the username + :param UserDom: the domain name + :param HashNt: (out of spec) if you have the HashNt, use this and set + Passwd to None + """ + if HashNt is None: + HashNt = MD4le(Passwd) + return HMAC_MD5(HashNt, (User.upper() + UserDom).encode("utf-16le")) + + +def NTLMv2_ComputeSessionBaseKey(ResponseKeyNT, NTProofStr): + return HMAC_MD5(ResponseKeyNT, NTProofStr) + + +# sect 3.4.4.2 - With Extended Session Security + + +def MAC(Handle, SigningKey, SeqNum, Message): + chksum = HMAC_MD5(SigningKey, struct.pack(".) + :param IDENTITIES: a dict {"username": } + Setting this value enables signature computation and + authenticates inbound users. + """ + + auth_type = 0x0A + + class STATE(SSP.STATE): + INIT = 1 + CLI_SENT_NEGO = 2 + CLI_SENT_AUTH = 3 + SRV_SENT_CHAL = 4 + + class CONTEXT(SSP.CONTEXT): + __slots__ = [ + "SessionKey", + "ExportedSessionKey", + "IsAcceptor", + "SendSignKey", + "SendSealKey", + "RecvSignKey", + "RecvSealKey", + "SendSealHandle", + "RecvSealHandle", + "SendSeqNum", + "RecvSeqNum", + "neg_tok", + "chall_tok", + "ServerHostname", + "ServerDomain", + ] + + @crypto_validator + def __init__(self, IsAcceptor, req_flags=None): + self.state = NTLMSSP.STATE.INIT + self.SessionKey = None + self.ExportedSessionKey = None + self.SendSignKey = None + self.SendSealKey = None + self.SendSealHandle = None + self.RecvSignKey = None + self.RecvSealKey = None + self.RecvSealHandle = None + self.SendSeqNum = 0 + self.RecvSeqNum = 0 + self.neg_tok = None + self.chall_tok = None + self.ServerHostname = None + self.ServerDomain = None + self.IsAcceptor = IsAcceptor + super(NTLMSSP.CONTEXT, self).__init__(req_flags=req_flags) + + def clifailure(self): + self.__init__(self.IsAcceptor, req_flags=self.flags) + + def __repr__(self): + return "NTLMSSP" + + # [MS-NLMP] note <36>: "the maximum lifetime is 36 hours" (lol, Kerberos has 5min) + NTLM_MaxLifetime = 36 * 3600 + + def __init__( + self, + UPN=None, + HASHNT=None, + PASSWORD=None, + USE_MIC=True, + VARIANT: NTLM_VARIANT = NTLM_VARIANT.RECENT, + NTLM_VALUES={}, + DOMAIN_FQDN=None, + DOMAIN_NB_NAME=None, + COMPUTER_NB_NAME=None, + COMPUTER_FQDN=None, + IDENTITIES=None, + DO_NOT_CHECK_LOGIN=False, + SERVER_CHALLENGE=None, + **kwargs, + ): + self.UPN = UPN + if HASHNT is None and PASSWORD is not None: + HASHNT = MD4le(PASSWORD) + self.HASHNT = HASHNT + self.VARIANT = VARIANT + if self.VARIANT != NTLM_VARIANT.RECENT: + log_runtime.warning( + "VARIANT != NTLM_VARIANT.RECENT. You shouldn't touch this !" + ) + self.USE_MIC = False + else: + self.USE_MIC = USE_MIC + + if UPN is not None: + # Populate values used only in server mode. + from scapy.layers.kerberos import _parse_upn + + try: + user, realm = _parse_upn(UPN) + if DOMAIN_FQDN is None: + DOMAIN_FQDN = realm + if COMPUTER_NB_NAME is None: + COMPUTER_NB_NAME = user + except ValueError: + pass + + # Compute various netbios/fqdn names + self.DOMAIN_FQDN = DOMAIN_FQDN or "WORKGROUP" + self.DOMAIN_NB_NAME = ( + DOMAIN_NB_NAME or self.DOMAIN_FQDN.split(".")[0].upper()[:15] + ) + self.COMPUTER_NB_NAME = COMPUTER_NB_NAME or "WIN10" + self.COMPUTER_FQDN = COMPUTER_FQDN or ( + self.COMPUTER_NB_NAME.lower() + "." + self.DOMAIN_FQDN + ) + self.NTLM_VALUES = NTLM_VALUES + + if IDENTITIES: + self.IDENTITIES = { + # Windows usernames are case insensitive + user.upper(): hashnt + for user, hashnt in IDENTITIES.items() + } + else: + self.IDENTITIES = IDENTITIES + + self.DO_NOT_CHECK_LOGIN = DO_NOT_CHECK_LOGIN + self.SERVER_CHALLENGE = SERVER_CHALLENGE + super(NTLMSSP, self).__init__(**kwargs) + + def LegsAmount(self, Context: CONTEXT): + return 3 + + def GSS_Inquire_names_for_mech(self): + return ["1.3.6.1.4.1.311.2.2.10"] + + def GSS_GetMICEx(self, Context, msgs, qop_req=0): + """ + [MS-NLMP] sect 3.4.8 + """ + # Concatenate the ToSign + ToSign = b"".join(x.data for x in msgs if x.sign) + sig = MAC( + Context.SendSealHandle, + Context.SendSignKey, + Context.SendSeqNum, + ToSign, + ) + Context.SendSeqNum += 1 + return sig + + def GSS_VerifyMICEx(self, Context, msgs, signature): + """ + [MS-NLMP] sect 3.4.9 + """ + Context.RecvSeqNum = signature.SeqNum + # Concatenate the ToSign + ToSign = b"".join(x.data for x in msgs if x.sign) + sig = MAC( + Context.RecvSealHandle, + Context.RecvSignKey, + Context.RecvSeqNum, + ToSign, + ) + if sig.Checksum != signature.Checksum: + raise ValueError("ERROR: Checksums don't match") + + def GSS_WrapEx(self, Context, msgs, qop_req=0): + """ + [MS-NLMP] sect 3.4.6 + """ + msgs_cpy = copy.deepcopy(msgs) # Keep copy for signature + # Encrypt + for msg in msgs: + if msg.conf_req_flag: + msg.data = RC4(Context.SendSealHandle, msg.data) + # Sign + sig = self.GSS_GetMICEx(Context, msgs_cpy, qop_req=qop_req) + return ( + msgs, + sig, + ) + + def GSS_UnwrapEx(self, Context, msgs, signature): + """ + [MS-NLMP] sect 3.4.7 + """ + # Decrypt + for msg in msgs: + if msg.conf_req_flag: + msg.data = RC4(Context.RecvSealHandle, msg.data) + # Check signature + self.GSS_VerifyMICEx(Context, msgs, signature) + return msgs + + def SupportsMechListMIC(self): + if not self.USE_MIC: + # RFC 4178 + # "If the mechanism selected by the negotiation does not support integrity + # protection, then no mechlistMIC token is used." + return False + if self.DO_NOT_CHECK_LOGIN: + # In this mode, we won't negotiate any credentials. + return False + return True + + def GetMechListMIC(self, Context, input): + # [MS-SPNG] + # "When NTLM is negotiated, the SPNG server MUST set OriginalHandle to + # ServerHandle before generating the mechListMIC, then set ServerHandle to + # OriginalHandle after generating the mechListMIC." + OriginalHandle = Context.SendSealHandle + Context.SendSealHandle = RC4Init(Context.SendSealKey) + try: + return super(NTLMSSP, self).GetMechListMIC(Context, input) + finally: + Context.SendSealHandle = OriginalHandle + + def VerifyMechListMIC(self, Context, otherMIC, input): + # [MS-SPNG] + # "the SPNEGO Extension server MUST set OriginalHandle to ClientHandle before + # validating the mechListMIC and then set ClientHandle to OriginalHandle after + # validating the mechListMIC." + OriginalHandle = Context.RecvSealHandle + Context.RecvSealHandle = RC4Init(Context.RecvSealKey) + try: + return super(NTLMSSP, self).VerifyMechListMIC(Context, otherMIC, input) + finally: + Context.RecvSealHandle = OriginalHandle + + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + Context = self.CONTEXT(False, req_flags=req_flags) + + if Context.state == self.STATE.INIT: + # Client: negotiate + + # Create a default token + tok = NTLM_NEGOTIATE( + VARIANT=self.VARIANT, + NegotiateFlags="+".join( + [ + "NEGOTIATE_UNICODE", + "REQUEST_TARGET", + "NEGOTIATE_NTLM", + "NEGOTIATE_ALWAYS_SIGN", + "TARGET_TYPE_DOMAIN", + "NEGOTIATE_EXTENDED_SESSIONSECURITY", + "NEGOTIATE_TARGET_INFO", + "NEGOTIATE_128", + "NEGOTIATE_56", + ] + + ( + ["NEGOTIATE_VERSION"] + if self.VARIANT >= NTLM_VARIANT.XP_OR_2003 + else [] + ) + + ( + [ + "NEGOTIATE_KEY_EXCH", + ] + if Context.flags + & (GSS_C_FLAGS.GSS_C_INTEG_FLAG | GSS_C_FLAGS.GSS_C_CONF_FLAG) + else [] + ) + + ( + [ + "NEGOTIATE_SIGN", + ] + if Context.flags & GSS_C_FLAGS.GSS_C_INTEG_FLAG + else [] + ) + + ( + [ + "NEGOTIATE_SEAL", + ] + if Context.flags & GSS_C_FLAGS.GSS_C_CONF_FLAG + else [] + ) + + ( + [ + "NEGOTIATE_IDENTIFY", + ] + if Context.flags & GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG + else [] + ) + ), + ProductMajorVersion=10, + ProductMinorVersion=0, + ProductBuild=26100, + ) + + # Update that token with the customs one + if self.NTLM_VALUES: + for key in [ + "NegotiateFlags", + "ProductMajorVersion", + "ProductMinorVersion", + "ProductBuild", + "DomainName", + "WorkstationName", + ]: + if key in self.NTLM_VALUES: + setattr(tok, key, self.NTLM_VALUES[key]) + Context.neg_tok = tok + Context.SessionKey = None # Reset signing (if previous auth failed) + Context.state = self.STATE.CLI_SENT_NEGO + return Context, tok, GSS_S_CONTINUE_NEEDED + elif Context.state == self.STATE.CLI_SENT_NEGO: + # Client: auth (token=challenge) + chall_tok = input_token + + if self.UPN is None or self.HASHNT is None: + raise ValueError( + "Must provide a 'UPN' and a 'HASHNT' or 'PASSWORD' when " + "running in standalone !" + ) + + from scapy.layers.kerberos import _parse_upn + + # Check token sanity + if not chall_tok or NTLM_CHALLENGE not in chall_tok: + log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Challenge") + return Context, None, GSS_S_DEFECTIVE_TOKEN + + # Some information from the CHALLENGE are stored + try: + Context.ServerHostname = chall_tok.getAv(0x0001).Value + except IndexError: + pass + try: + Context.ServerDomain = chall_tok.getAv(0x0002).Value + except IndexError: + pass + try: + # the server SHOULD set the timestamp in the CHALLENGE_MESSAGE + ServerTimestamp = chall_tok.getAv(0x0007).Value + ServerTime = (ServerTimestamp / 1e7) - 11644473600 + + if abs(ServerTime - time.time()) >= NTLMSSP.NTLM_MaxLifetime: + log_runtime.warning( + "Server and Client times are off by more than 36h !" + ) + # We could error here, but we don't. + except IndexError: + pass + + # Initialize a default token + tok = NTLM_AUTHENTICATE_V2( + VARIANT=self.VARIANT, + NegotiateFlags=chall_tok.NegotiateFlags, + ProductMajorVersion=10, + ProductMinorVersion=0, + ProductBuild=26100, + ) + + # Populate the token + tok.LmChallengeResponse = LMv2_RESPONSE() + + # 1. Set username + try: + tok.UserName, realm = _parse_upn(self.UPN) + except ValueError: + tok.UserName, realm = self.UPN, Context.ServerDomain + + # 2. Set domain name + if realm is None: + log_runtime.warning( + "No realm specified in UPN, nor provided by server." + ) + tok.DomainName = self.DOMAIN_FQDN + else: + tok.DomainName = realm + + # 3. Set workstation name + tok.Workstation = self.COMPUTER_NB_NAME + + # 4. Create and calculate the ChallengeResponse + # 4.1 Build the payload + cr = tok.NtChallengeResponse = NTLMv2_RESPONSE( + ChallengeFromClient=os.urandom(8), + ) + cr.TimeStamp = int((time.time() + 11644473600) * 1e7) + cr.AvPairs = ( + # Repeat AvPairs from the server + chall_tok.TargetInfo[:-1] + + ( + [ + AV_PAIR(AvId="MsvAvFlags", Value="MIC integrity"), + ] + if self.USE_MIC + else [] + ) + + [ + AV_PAIR( + AvId="MsvAvSingleHost", + Value=Single_Host_Data( + MachineID=os.urandom(32), + PermanentMachineID=os.urandom(32), + ), + ), + ] + + ( + [ + AV_PAIR( + # [MS-NLMP] sect 2.2.2.1 refers to RFC 4121 sect 4.1.1.2 + # "The Bnd field contains the MD5 hash of channel bindings" + AvId="MsvAvChannelBindings", + Value=chan_bindings.digestMD5(), + ), + ] + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + else [] + ) + + [ + AV_PAIR( + AvId="MsvAvTargetName", + Value=target_name or ("host/" + Context.ServerHostname), + ), + AV_PAIR(AvId="MsvAvEOL"), + ] + ) + if self.NTLM_VALUES: + # Update that token with the customs one + for key in [ + "NegotiateFlags", + "ProductMajorVersion", + "ProductMinorVersion", + "ProductBuild", + ]: + if key in self.NTLM_VALUES: + setattr(tok, key, self.NTLM_VALUES[key]) + + # 4.2 Compute the ResponseKeyNT + ResponseKeyNT = NTOWFv2( + None, + tok.UserName, + tok.DomainName, + HashNt=self.HASHNT, + ) + + # 4.3 Compute the NTProofStr + cr.NTProofStr = cr.computeNTProofStr( + ResponseKeyNT, + chall_tok.ServerChallenge, + ) + + # 4.4 Compute the Session Key + SessionBaseKey = NTLMv2_ComputeSessionBaseKey(ResponseKeyNT, cr.NTProofStr) + KeyExchangeKey = SessionBaseKey # Only true for NTLMv2 + if chall_tok.NegotiateFlags.NEGOTIATE_KEY_EXCH: + ExportedSessionKey = os.urandom(16) + tok.EncryptedRandomSessionKey = RC4K( + KeyExchangeKey, + ExportedSessionKey, + ) + else: + ExportedSessionKey = KeyExchangeKey + + # 4.5 Compute the MIC + if self.USE_MIC: + tok.compute_mic(ExportedSessionKey, Context.neg_tok, chall_tok) + + # 5. Perform key computations + Context.ExportedSessionKey = ExportedSessionKey + # [MS-SMB] 3.2.5.3 + Context.SessionKey = Context.ExportedSessionKey + # Compute NTLM keys + Context.SendSignKey = SIGNKEY( + tok.NegotiateFlags, ExportedSessionKey, "Client" + ) + Context.SendSealKey = SEALKEY( + tok.NegotiateFlags, ExportedSessionKey, "Client" + ) + Context.SendSealHandle = RC4Init(Context.SendSealKey) + Context.RecvSignKey = SIGNKEY( + tok.NegotiateFlags, ExportedSessionKey, "Server" + ) + Context.RecvSealKey = SEALKEY( + tok.NegotiateFlags, ExportedSessionKey, "Server" + ) + Context.RecvSealHandle = RC4Init(Context.RecvSealKey) + + # Update the state + Context.state = self.STATE.CLI_SENT_AUTH + + return Context, tok, GSS_S_COMPLETE + elif Context.state == self.STATE.CLI_SENT_AUTH: + if input_token: + # what is that? + status = GSS_S_DEFECTIVE_TOKEN + else: + status = GSS_S_COMPLETE + return Context, None, status + else: + raise ValueError("NTLMSSP: unexpected state %s" % repr(Context.state)) + + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + Context = self.CONTEXT(IsAcceptor=True, req_flags=req_flags) + + if Context.state == self.STATE.INIT: + # Server: challenge (input_token=negotiate) + nego_tok = input_token + if not nego_tok or NTLM_NEGOTIATE not in nego_tok: + log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Negotiate") + return Context, None, GSS_S_DEFECTIVE_TOKEN + + # Build the challenge token + currentTime = (time.time() + 11644473600) * 1e7 + tok = NTLM_CHALLENGE( + VARIANT=self.VARIANT, + ServerChallenge=self.SERVER_CHALLENGE or os.urandom(8), + NegotiateFlags="+".join( + [ + "NEGOTIATE_UNICODE", + "REQUEST_TARGET", + "NEGOTIATE_NTLM", + "NEGOTIATE_ALWAYS_SIGN", + "NEGOTIATE_EXTENDED_SESSIONSECURITY", + "NEGOTIATE_TARGET_INFO", + "TARGET_TYPE_DOMAIN", + "NEGOTIATE_128", + "NEGOTIATE_KEY_EXCH", + "NEGOTIATE_56", + ] + + ( + ["NEGOTIATE_VERSION"] + if self.VARIANT >= NTLM_VARIANT.XP_OR_2003 + else [] + ) + + ( + ["NEGOTIATE_SIGN"] + if nego_tok.NegotiateFlags.NEGOTIATE_SIGN + else [] + ) + + ( + ["NEGOTIATE_SEAL"] + if nego_tok.NegotiateFlags.NEGOTIATE_SEAL + else [] + ) + ), + ProductMajorVersion=10, + ProductMinorVersion=0, + Payload=[ + ("TargetName", ""), + ( + "TargetInfo", + [ + # MsvAvNbComputerName + AV_PAIR(AvId=1, Value=self.COMPUTER_NB_NAME), + # MsvAvNbDomainName + AV_PAIR(AvId=2, Value=self.DOMAIN_NB_NAME), + # MsvAvDnsComputerName + AV_PAIR(AvId=3, Value=self.COMPUTER_FQDN), + # MsvAvDnsDomainName + AV_PAIR(AvId=4, Value=self.DOMAIN_FQDN), + # MsvAvDnsTreeName + AV_PAIR(AvId=5, Value=self.DOMAIN_FQDN), + # MsvAvTimestamp + AV_PAIR(AvId=7, Value=currentTime), + # MsvAvEOL + AV_PAIR(AvId=0), + ], + ), + ], + ) + if self.NTLM_VALUES: + # Update that token with the customs one + for key in [ + "ServerChallenge", + "NegotiateFlags", + "ProductMajorVersion", + "ProductMinorVersion", + "TargetName", + ]: + if key in self.NTLM_VALUES: + setattr(tok, key, self.NTLM_VALUES[key]) + avpairs = {x.AvId: x.Value for x in tok.TargetInfo} + tok.TargetInfo = [ + AV_PAIR(AvId=i, Value=self.NTLM_VALUES.get(x, avpairs[i])) + for (i, x) in [ + (2, "NetbiosDomainName"), + (1, "NetbiosComputerName"), + (4, "DnsDomainName"), + (3, "DnsComputerName"), + (5, "DnsTreeName"), + (6, "Flags"), + (7, "Timestamp"), + (0, None), + ] + if ((x in self.NTLM_VALUES) or (i in avpairs)) + and self.NTLM_VALUES.get(x, True) is not None + ] + + # Store for next step + Context.chall_tok = tok + + # Update the state + Context.state = self.STATE.SRV_SENT_CHAL + + return Context, tok, GSS_S_CONTINUE_NEEDED + elif Context.state == self.STATE.SRV_SENT_CHAL: + # server: OK or challenge again (input_token=auth) + auth_tok = input_token + + if not auth_tok or NTLM_AUTHENTICATE_V2 not in auth_tok: + log_runtime.debug( + "NTLMSSP: Unexpected token. Expected NTLM Authenticate v2" + ) + return Context, None, GSS_S_DEFECTIVE_TOKEN + + if self.DO_NOT_CHECK_LOGIN: + # Just trust me bro. Typically used in "guest" mode. + return Context, None, GSS_S_COMPLETE + + # Compute the session key + SessionBaseKey = self._getSessionBaseKey(Context, auth_tok) + if SessionBaseKey: + # [MS-NLMP] sect 3.2.5.1.2 + KeyExchangeKey = SessionBaseKey # Only true for NTLMv2 + if auth_tok.NegotiateFlags.NEGOTIATE_KEY_EXCH: + try: + EncryptedRandomSessionKey = auth_tok.EncryptedRandomSessionKey + except AttributeError: + # No EncryptedRandomSessionKey. libcurl for instance + # hmm. this looks bad + EncryptedRandomSessionKey = b"\x00" * 16 + ExportedSessionKey = RC4K(KeyExchangeKey, EncryptedRandomSessionKey) + else: + ExportedSessionKey = KeyExchangeKey + Context.ExportedSessionKey = ExportedSessionKey + # [MS-SMB] 3.2.5.3 + Context.SessionKey = Context.ExportedSessionKey + + # Check the timestamp + try: + ClientTimestamp = auth_tok.NtChallengeResponse.getAv(0x0007).Value + ClientTime = (ClientTimestamp / 1e7) - 11644473600 + + if abs(ClientTime - time.time()) >= NTLMSSP.NTLM_MaxLifetime: + log_runtime.warning( + "Server and Client times are off by more than 36h !" + ) + # We could error here, but we don't. + except IndexError: + pass + + # Check the channel bindings + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + try: + Bnd = auth_tok.NtChallengeResponse.getAv(0x000A).Value + if Bnd != chan_bindings.digestMD5(): + # Bad channel bindings + return Context, None, GSS_S_BAD_BINDINGS + except IndexError: + if GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS not in req_flags: + # Uhoh, we required channel bindings + return Context, None, GSS_S_BAD_BINDINGS + + if Context.SessionKey: + # Compute NTLM keys + Context.SendSignKey = SIGNKEY( + auth_tok.NegotiateFlags, ExportedSessionKey, "Server" + ) + Context.SendSealKey = SEALKEY( + auth_tok.NegotiateFlags, ExportedSessionKey, "Server" + ) + Context.SendSealHandle = RC4Init(Context.SendSealKey) + Context.RecvSignKey = SIGNKEY( + auth_tok.NegotiateFlags, ExportedSessionKey, "Client" + ) + Context.RecvSealKey = SEALKEY( + auth_tok.NegotiateFlags, ExportedSessionKey, "Client" + ) + Context.RecvSealHandle = RC4Init(Context.RecvSealKey) + + # Check the NTProofStr + if self._checkLogin(Context, auth_tok): + # Set negotiated flags + if auth_tok.NegotiateFlags.NEGOTIATE_SIGN: + Context.flags |= GSS_C_FLAGS.GSS_C_INTEG_FLAG + if auth_tok.NegotiateFlags.NEGOTIATE_SEAL: + Context.flags |= GSS_C_FLAGS.GSS_C_CONF_FLAG + return Context, None, GSS_S_COMPLETE + + # Bad NTProofStr or unknown user + Context.SessionKey = None + Context.state = self.STATE.INIT + return Context, None, GSS_S_DEFECTIVE_CREDENTIAL + else: + raise ValueError("NTLMSSP: unexpected state %s" % repr(Context.state)) + + def MaximumSignatureLength(self, Context: CONTEXT): + """ + Returns the Maximum Signature length. + + This will be used in auth_len in DceRpc5, and is necessary for + PFC_SUPPORT_HEADER_SIGN to work properly. + """ + return 16 # len(NTLMSSP_MESSAGE_SIGNATURE()) + + def GSS_Passive(self, Context: CONTEXT, token=None, req_flags=None): + if Context is None: + Context = self.CONTEXT(True) + Context.passive = True + + # We capture the Negotiate, Challenge, then call the server's auth handling + # and discard the output. + + if Context.state == self.STATE.INIT: + if not token or NTLM_NEGOTIATE not in token: + log_runtime.warning("NTLMSSP: Expected NTLM Negotiate") + return None, GSS_S_DEFECTIVE_TOKEN + Context.neg_tok = token + Context.state = self.STATE.CLI_SENT_NEGO + return Context, GSS_S_CONTINUE_NEEDED + elif Context.state == self.STATE.CLI_SENT_NEGO: + if not token or NTLM_CHALLENGE not in token: + log_runtime.warning("NTLMSSP: Expected NTLM Challenge") + return None, GSS_S_DEFECTIVE_TOKEN + Context.chall_tok = token + Context.state = self.STATE.SRV_SENT_CHAL + return Context, GSS_S_CONTINUE_NEEDED + elif Context.state == self.STATE.SRV_SENT_CHAL: + if not token or NTLM_AUTHENTICATE_V2 not in token: + log_runtime.warning("NTLMSSP: Expected NTLM Authenticate") + return None, GSS_S_DEFECTIVE_TOKEN + Context, _, status = self.GSS_Accept_sec_context(Context, token) + if status != GSS_S_COMPLETE: + log_runtime.info("NTLMSSP: auth failed.") + Context.state = self.STATE.INIT + return Context, status + else: + raise ValueError("NTLMSSP: unexpected state %s" % repr(Context.state)) + + def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): + if Context.IsAcceptor is not IsAcceptor: + return + # Swap everything + Context.SendSignKey, Context.RecvSignKey = ( + Context.RecvSignKey, + Context.SendSignKey, + ) + Context.SendSealKey, Context.RecvSealKey = ( + Context.RecvSealKey, + Context.SendSealKey, + ) + Context.SendSealHandle, Context.RecvSealHandle = ( + Context.RecvSealHandle, + Context.SendSealHandle, + ) + Context.SendSeqNum, Context.RecvSeqNum = Context.RecvSeqNum, Context.SendSeqNum + Context.IsAcceptor = not Context.IsAcceptor + + def _getSessionBaseKey(self, Context, auth_tok): + """ + Function that returns the SessionBaseKey from the ntlm Authenticate. + """ + try: + # Windows usernames are case insensitive + username = auth_tok.UserName.upper() + except AttributeError: + username = None + try: + domain = auth_tok.DomainName + except AttributeError: + domain = "" + if self.IDENTITIES and username in self.IDENTITIES: + ResponseKeyNT = NTOWFv2( + None, + username, + domain, + HashNt=self.IDENTITIES[username], + ) + return NTLMv2_ComputeSessionBaseKey( + ResponseKeyNT, + auth_tok.NtChallengeResponse.NTProofStr, + ) + elif self.IDENTITIES: + log_runtime.debug("NTLMSSP: Bad credentials for %s" % username) + return None + + def _checkLogin(self, Context, auth_tok): + """ + Function that checks the validity of an authentication. + + Overwrite and return True to bypass. + """ + try: + # Windows usernames are case insensitive + username = auth_tok.UserName.upper() + except AttributeError: + username = None + try: + domain = auth_tok.DomainName + except AttributeError: + domain = "" + if username in self.IDENTITIES: + ResponseKeyNT = NTOWFv2( + None, + username, + domain, + HashNt=self.IDENTITIES[username], + ) + NTProofStr = auth_tok.NtChallengeResponse.computeNTProofStr( + ResponseKeyNT, + Context.chall_tok.ServerChallenge, + ) + if NTProofStr == auth_tok.NtChallengeResponse.NTProofStr: + return True + return False + + +class NTLMSSP_DOMAIN(NTLMSSP): + """ + A variant of the NTLMSSP to be used in server mode that gets the session + keys from the domain using a Netlogon channel. + + This has the same arguments as NTLMSSP, but supports the following in server + mode: + + :param UPN: the UPN of the machine account to login for Netlogon. + :param HASHNT: the HASHNT of the machine account (use Netlogon secure channel). + :param ssp: a KerberosSSP to use (use Kerberos secure channel). + :param PASSWORD: the PASSWORD of the machine account to use for Netlogon. + :param DC_FQDN: (optional) specify the FQDN of the DC. + + Netlogon example:: + + >>> mySSP = NTLMSSP_DOMAIN( + ... UPN="Server1@domain.local", + ... HASHNT=bytes.fromhex("8846f7eaee8fb117ad06bdd830b7586c"), + ... ) + + Kerberos example:: + + >>> mySSP = NTLMSSP_DOMAIN( + ... UPN="Server1@domain.local", + ... KEY=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, + ... key=bytes.fromhex( + ... "85abb9b61dc2fa49d4cc04317bbd108f8f79df28" + ... "239155ed7b144c5d2ebcf016" + ... ) + ... ), + ... ) + """ + + def __init__(self, UPN=None, *args, timeout=3, ssp=None, **kwargs): + from scapy.layers.kerberos import KerberosSSP + + # Either PASSWORD or HASHNT or ssp + if ( + "HASHNT" not in kwargs + and "PASSWORD" not in kwargs + and "KEY" not in kwargs + and ssp is None + ): + raise ValueError( + "Must specify either 'HASHNT', 'PASSWORD' or " + "provide a ssp=KerberosSSP()" + ) + elif ssp is not None and not isinstance(ssp, KerberosSSP): + raise ValueError("'ssp' can only be None or a KerberosSSP !") + + self.KEY = kwargs.pop("KEY", None) + self.PASSWORD = kwargs.get("PASSWORD", None) + + # UPN is mandatory + if UPN is None and ssp is not None and ssp.UPN: + UPN = ssp.UPN + elif UPN is None: + raise ValueError("Must specify a 'UPN' !") + kwargs["UPN"] = UPN + + # Call parent + super(NTLMSSP_DOMAIN, self).__init__( + *args, + **kwargs, + ) + + # Treat specific parameters + self.DC_FQDN = kwargs.pop("DC_FQDN", None) + if self.DC_FQDN is None: + # Get DC_FQDN from dclocator + from scapy.layers.ldap import dclocator + + dc = dclocator( + self.DOMAIN_FQDN, + timeout=timeout, + debug=kwargs.get("debug", 0), + ) + self.DC_FQDN = dc.samlogon.DnsHostName.decode().rstrip(".") + + # If logging in via Kerberos + self.ssp = ssp + + def _getSessionBaseKey(self, Context, ntlm): + """ + Return the Session Key by asking the DC. + """ + # No user / no domain: skip. + if not ntlm.UserNameLen or not ntlm.DomainNameLen: + return super(NTLMSSP_DOMAIN, self)._getSessionBaseKey(Context, ntlm) + + # Import RPC stuff + from scapy.layers.dcerpc import NDRUnion + from scapy.layers.msrpce.msnrpc import ( + NETLOGON_SECURE_CHANNEL_METHOD, + NetlogonClient, + ) + from scapy.layers.msrpce.raw.ms_nrpc import ( + NETLOGON_LOGON_IDENTITY_INFO, + NetrLogonSamLogonWithFlags_Request, + PNETLOGON_AUTHENTICATOR, + PNETLOGON_NETWORK_INFO, + STRING, + UNICODE_STRING, + ) + + # Create NetlogonClient with PRIVACY + client = NetlogonClient() + client.connect(self.DC_FQDN) + + # Establish the Netlogon secure channel (this will bind) + try: + if self.ssp is None and self.KEY is None: + # Login via classic NetlogonSSP + client.establish_secure_channel( + mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, + UPN=f"{self.COMPUTER_NB_NAME}@{self.DOMAIN_NB_NAME}", + DC_FQDN=self.DC_FQDN, + HASHNT=self.HASHNT, + ) + else: + # Login via KerberosSSP (Windows 2025) + client.establish_secure_channel( + mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos, + UPN=self.UPN, + DC_FQDN=self.DC_FQDN, + PASSWORD=self.PASSWORD, + KEY=self.KEY, + ssp=self.ssp, + ) + except ValueError: + log_runtime.warning( + "Couldn't establish the Netlogon secure channel. " + "Check the credentials for '%s' !" % self.COMPUTER_NB_NAME + ) + return super(NTLMSSP_DOMAIN, self)._getSessionBaseKey(Context, ntlm) + + # Request validation of the NTLM request + req = NetrLogonSamLogonWithFlags_Request( + LogonServer="", + ComputerName=self.COMPUTER_NB_NAME, + Authenticator=client.create_authenticator(), + ReturnAuthenticator=PNETLOGON_AUTHENTICATOR(), + LogonLevel=6, # NetlogonNetworkTransitiveInformation + LogonInformation=NDRUnion( + tag=6, + value=PNETLOGON_NETWORK_INFO( + Identity=NETLOGON_LOGON_IDENTITY_INFO( + LogonDomainName=UNICODE_STRING( + Buffer=ntlm.DomainName, + ), + ParameterControl=0x00002AE0, + UserName=UNICODE_STRING( + Buffer=ntlm.UserName, + ), + Workstation=UNICODE_STRING( + Buffer=ntlm.Workstation, + ), + ), + LmChallenge=Context.chall_tok.ServerChallenge, + NtChallengeResponse=STRING( + Buffer=bytes(ntlm.NtChallengeResponse), + ), + LmChallengeResponse=STRING( + Buffer=bytes(ntlm.LmChallengeResponse), + ), + ), + ), + ValidationLevel=6, + ExtraFlags=0, + ndr64=client.ndr64, + ) + + # Get response + resp = client.sr1_req(req) + if resp and resp.status == 0: + # Success + + # Validate DC authenticator + client.validate_authenticator(resp.ReturnAuthenticator.value) + + # Get and return the SessionKey + UserSessionKey = resp.ValidationInformation.value.value.UserSessionKey + return bytes(UserSessionKey) + else: + # Failed + return super(NTLMSSP_DOMAIN, self)._getSessionBaseKey(Context, ntlm) + + def _checkLogin(self, Context, auth_tok): + # Always OK if we got the session key + return True diff --git a/scapy/layers/ntp.py b/scapy/layers/ntp.py index a294b606b3b..e8739006f14 100644 --- a/scapy/layers/ntp.py +++ b/scapy/layers/ntp.py @@ -1,32 +1,51 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ NTP (Network Time Protocol). References : RFC 5905, RC 1305, ntpd source code """ -from __future__ import absolute_import import struct import time import datetime from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, BitEnumField, ByteField, ByteEnumField, \ - XByteField, SignedByteField, FlagsField, ShortField, LEShortField, \ - IntField, LEIntField, FixedPointField, IPField, StrField, \ - StrFixedLenField, StrFixedLenEnumField, XStrFixedLenField, PacketField, \ - PacketLenField, PacketListField, FieldListField, ConditionalField, \ - PadField -from scapy.layers.inet6 import IP6Field +from scapy.fields import ( + BitEnumField, + BitField, + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + FixedPointField, + FlagsField, + IP6Field, + IPField, + IntField, + LEIntField, + LEShortField, + MayEnd, + MultipleTypeField, + PacketField, + PacketListField, + PadField, + ShortField, + SignedByteField, + StrField, + StrFixedLenEnumField, + StrFixedLenField, + StrLenField, + XByteField, + XStrFixedLenField, +) from scapy.layers.inet import UDP -from scapy.utils import issubtype, lhex +from scapy.utils import lhex from scapy.compat import orb from scapy.config import conf -import scapy.modules.six as six -from scapy.modules.six.moves import range ############################################################################# @@ -79,11 +98,14 @@ def i2repr(self, pkt, val): return "--" val = self.i2h(pkt, val) if val < _NTP_BASETIME: - return val - return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(val - _NTP_BASETIME)) # noqa: E501 + return str(val) + return time.strftime( + "%a, %d %b %Y %H:%M:%S +0000", + time.gmtime(int(val - _NTP_BASETIME)) + ) def any2i(self, pkt, val): - if isinstance(val, six.string_types): + if isinstance(val, str): val = int(time.mktime(time.strptime(val))) + _NTP_BASETIME elif isinstance(val, datetime.datetime): val = int(val.strftime("%s")) + _NTP_BASETIME @@ -123,25 +145,25 @@ def i2m(self, pkt, val): # RFC 5905 / Section 7.3 _reference_identifiers = { - "GOES": "Geosynchronous Orbit Environment Satellite", - "GPS ": "Global Position System", - "GAL ": "Galileo Positioning System", - "PPS ": "Generic pulse-per-second", - "IRIG": "Inter-Range Instrumentation Group", - "WWVB": "LF Radio WWVB Ft. Collins, CO 60 kHz", - "DCF ": "LF Radio DCF77 Mainflingen, DE 77.5 kHz", - "HBG ": "LF Radio HBG Prangins, HB 75 kHz", - "MSF ": "LF Radio MSF Anthorn, UK 60 kHz", - "JJY ": "LF Radio JJY Fukushima, JP 40 kHz, Saga, JP 60 kHz", - "LORC": "MF Radio LORAN C station, 100 kHz", - "TDF ": "MF Radio Allouis, FR 162 kHz", - "CHU ": "HF Radio CHU Ottawa, Ontario", - "WWV ": "HF Radio WWV Ft. Collins, CO", - "WWVH": "HF Radio WWVH Kauai, HI", - "NIST": "NIST telephone modem", - "ACTS": "NIST telephone modem", - "USNO": "USNO telephone modem", - "PTB ": "European telephone modem", + b"GOES": "Geosynchronous Orbit Environment Satellite", + b"GPS ": "Global Position System", + b"GAL ": "Galileo Positioning System", + b"PPS ": "Generic pulse-per-second", + b"IRIG": "Inter-Range Instrumentation Group", + b"WWVB": "LF Radio WWVB Ft. Collins, CO 60 kHz", + b"DCF ": "LF Radio DCF77 Mainflingen, DE 77.5 kHz", + b"HBG ": "LF Radio HBG Prangins, HB 75 kHz", + b"MSF ": "LF Radio MSF Anthorn, UK 60 kHz", + b"JJY ": "LF Radio JJY Fukushima, JP 40 kHz, Saga, JP 60 kHz", + b"LORC": "MF Radio LORAN C station, 100 kHz", + b"TDF ": "MF Radio Allouis, FR 162 kHz", + b"CHU ": "HF Radio CHU Ottawa, Ontario", + b"WWV ": "HF Radio WWV Ft. Collins, CO", + b"WWVH": "HF Radio WWVH Kauai, HI", + b"NIST": "NIST telephone modem", + b"ACTS": "NIST telephone modem", + b"USNO": "USNO telephone modem", + b"PTB ": "European telephone modem", } @@ -207,24 +229,10 @@ def pre_dissect(self, s): raise _NTPInvalidDataException(err) return s - # NTPHeader, NTPControl and NTPPrivate are NTP packets. - # This might help, for example when reading a pcap file. - def haslayer(self, cls): - """Specific: NTPHeader().haslayer(NTP) should return True.""" - if cls == "NTP": - if isinstance(self, NTP): - return True - elif issubtype(cls, NTP): - if isinstance(self, cls): - return True - return super(NTP, self).haslayer(cls) - - def getlayer(self, cls, nb=1, _track=None, _subclass=True, **flt): - return super(NTP, self).getlayer(cls, nb=nb, _track=_track, - _subclass=True, **flt) - def mysummary(self): - return self.sprintf("NTP v%ir,NTP.version%, %NTP.mode%") + return self.sprintf( + "NTP v%ir,{0}.version%, %{0}.mode%".format(self.__class__.__name__) + ) class _NTPAuthenticatorPaddingField(StrField): @@ -436,13 +444,14 @@ class NTPHeader(NTP): # name = "NTPHeader" + match_subclass = True fields_desc = [ BitEnumField("leap", 0, 2, _leap_indicator), BitField("version", 4, 3), BitEnumField("mode", 3, 3, _ntp_modes), BitField("stratum", 2, 8), - BitField("poll", 0xa, 8), - BitField("precision", 0, 8), + SignedByteField("poll", 0xa), + SignedByteField("precision", 0), FixedPointField("delay", 0, size=32, frac_bits=16), FixedPointField("dispersion", 0, size=32, frac_bits=16), ConditionalField(IPField("id", "127.0.0.1"), lambda p: p.stratum > 1), @@ -467,10 +476,10 @@ def guess_payload_class(self, payload): """ plen = len(payload) - if plen > _NTP_AUTH_MD5_TAIL_SIZE: - return NTPExtensions - elif plen == _NTP_AUTH_MD5_TAIL_SIZE: + if plen - 4 in [16, 20, 32, 64]: # length of MD5, SHA1, SHA256, SHA512 return NTPAuthenticator + elif plen > _NTP_AUTH_MD5_TAIL_SIZE: + return NTPExtensions return Packet.guess_payload_class(self, payload) @@ -603,18 +612,6 @@ def __init__(self, details): } -class NTPStatusPacket(Packet): - """ - Packet handling a non specific status word. - """ - - name = "status" - fields_desc = [ShortField("status", 0)] - - def extract_padding(self, s): - return b"", s - - class NTPSystemStatusPacket(Packet): """ @@ -684,55 +681,6 @@ def extract_padding(self, s): return b"", s -class NTPControlStatusField(PacketField): - """ - This field provides better readability for the "status" field. - """ - - ######################################################################### - # - # RFC 1305 - ######################################################################### - # - # Appendix B.3. Commands // ntpd source code: ntp_control.h - ######################################################################### - # - - def m2i(self, pkt, m): - ret = None - association_id = struct.unpack("!H", m[2:4])[0] - - if pkt.err == 1: - ret = NTPErrorStatusPacket(m) - - # op_code == CTL_OP_READSTAT - elif pkt.op_code == 1: - if association_id != 0: - ret = NTPPeerStatusPacket(m) - else: - ret = NTPSystemStatusPacket(m) - - # op_code == CTL_OP_READVAR - elif pkt.op_code == 2: - if association_id != 0: - ret = NTPPeerStatusPacket(m) - else: - ret = NTPSystemStatusPacket(m) - - # op_code == CTL_OP_WRITEVAR - elif pkt.op_code == 3: - ret = NTPStatusPacket(m) - - # op_code == CTL_OP_READCLOCK or op_code == CTL_OP_WRITECLOCK - elif pkt.op_code == 4 or pkt.op_code == 5: - ret = NTPClockStatusPacket(m) - - else: - ret = NTPStatusPacket(m) - - return ret - - class NTPPeerStatusDataPacket(Packet): """ Packet handling the data field when op_code is CTL_OP_READSTAT @@ -745,94 +693,87 @@ class NTPPeerStatusDataPacket(Packet): PacketField("peer_status", NTPPeerStatusPacket(), NTPPeerStatusPacket), ] + def extract_padding(self, s): + return b"", s -class NTPControlDataPacketLenField(PacketLenField): +class NTPControlStatusField(PacketField): """ - PacketField handling the "data" field of NTP control messages. + The various types of the "status" field. """ - + # RFC 9327 sect 3 def m2i(self, pkt, m): - ret = None + association_id = struct.unpack("!H", m[2:4])[0] - # op_code == CTL_OP_READSTAT - if pkt.op_code == 1: - if pkt.association_id == 0: - # Data contains association ID and peer status - ret = NTPPeerStatusDataPacket(m) - else: - ret = conf.raw_layer(m) + if pkt.err == 1: + return NTPErrorStatusPacket(m) + elif pkt.op_code in [4, 5]: # Read/write clock + return NTPClockStatusPacket(m) else: - ret = conf.raw_layer(m) - - return ret - - def getfield(self, pkt, s): - length = self.length_from(pkt) - i = None - if length > 0: - # RFC 1305 - # The maximum number of data octets is 468. - # - # include/ntp_control.h - # u_char data[480 + MAX_MAC_LEN]; /* data + auth */ - # - # Set the minimum length to 480 - 468 - length = max(12, length) - if length % 4: - length += (4 - length % 4) - try: - i = self.m2i(pkt, s[:length]) - except Exception: - if conf.debug_dissector: - raise - i = conf.raw_layer(load=s[:length]) - return s[length:], i + if association_id != 0: + return NTPPeerStatusPacket(m) + else: + return NTPSystemStatusPacket(m) class NTPControl(NTP): """ Packet handling NTP mode 6 / "Control" messages. """ - - ######################################################################### - # - # RFC 1305 - ######################################################################### - # - # Appendix B.3. Commands // ntpd source code: ntp_control.h - ######################################################################### - # - - name = "Control message" + deprecated_fields = { + "status_word": ("status", "2.6.2"), + } + # RFC 9327 sect 2 + name = "NTP Control message" + match_subclass = True fields_desc = [ - BitField("zeros", 0, 2), + BitEnumField("leap", 0, 2, _leap_indicator), BitField("version", 2, 3), - BitField("mode", 6, 3), + BitEnumField("mode", 6, 3, _ntp_modes), BitField("response", 0, 1), BitField("err", 0, 1), BitField("more", 0, 1), BitEnumField("op_code", 0, 5, _op_codes), ShortField("sequence", 0), - ConditionalField(NTPControlStatusField( - "status_word", "", Packet), lambda p: p.response == 1), - ConditionalField(ShortField("status", 0), lambda p: p.response == 0), + MultipleTypeField( + [ + ( + ShortField("status", 0), + lambda pkt: pkt.response == 0 or pkt.op_code in [6, 7] + ) + ], + NTPControlStatusField("status", NTPSystemStatusPacket(), None), + ), ShortField("association_id", 0), ShortField("offset", 0), - ShortField("count", None), - NTPControlDataPacketLenField( - "data", "", Packet, length_from=lambda p: p.count), + FieldLenField("count", None, length_of="data"), + MayEnd( + PadField( + MultipleTypeField( + # RFC 1305 + [ + ( + PacketListField( + "data", + "", + NTPPeerStatusDataPacket, + length_from=lambda p: p.count, + ), + lambda pkt: ( + pkt.response and + pkt.op_code == 1 and + pkt.association_id == 0 + ) + ), + ], + StrLenField("data", "", length_from=lambda pkt: pkt.count), + ), + align=4 + ) + ), PacketField("authenticator", "", NTPAuthenticator), ] - def post_build(self, p, pay): - if self.count is None: - length = 0 - if self.data: - length = len(self.data) - p = p[:11] + struct.pack("!H", length) + p[13:] - return p + pay - ############################################################################## # Private (mode 7) @@ -1110,7 +1051,7 @@ class NTPInfoSys(Packet): ByteField("peer_mode", 0), ByteField("leap", 0), ByteField("stratum", 0), - ByteField("precision", 0), + SignedByteField("precision", 0), FixedPointField("rootdelay", 0, size=32, frac_bits=16), FixedPointField("rootdispersion", 0, size=32, frac_bits=16), IPField("refid", 0), @@ -1168,7 +1109,8 @@ class NTPInfoMemStats(Packet): "hashcount", [0.0 for i in range(0, _NTP_HASH_SIZE)], ByteField("", 0), - count_from=lambda p: _NTP_HASH_SIZE + count_from=lambda p: _NTP_HASH_SIZE, + max_count=_NTP_HASH_SIZE ) ] @@ -1785,11 +1727,12 @@ class NTPPrivate(NTP): # name = "Private (mode 7)" + match_subclass = True fields_desc = [ BitField("response", 0, 1), BitField("more", 0, 1), BitField("version", 2, 3), - BitField("mode", 0, 3), + BitEnumField("mode", 7, 3, _ntp_modes), BitField("auth", 0, 1), BitField("seq", 0, 7), ByteEnumField("implementation", 0, _implementations), diff --git a/scapy/layers/pflog.py b/scapy/layers/pflog.py index 65e3e517c58..1069f8c4448 100644 --- a/scapy/layers/pflog.py +++ b/scapy/layers/pflog.py @@ -1,30 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ PFLog: OpenBSD PF packet filter logging. """ -import socket - from scapy.data import DLT_PFLOG from scapy.packet import Packet, bind_layers -from scapy.fields import ByteEnumField, ByteField, IntField, SignedIntField, \ - StrFixedLenField +from scapy.fields import ByteEnumField, ByteField, IntField, \ + IPField, IP6Field, MultipleTypeField, PadField, ShortField, \ + SignedIntField, StrFixedLenField, YesNoByteField from scapy.layers.inet import IP from scapy.config import conf if conf.ipv6_enabled: from scapy.layers.inet6 import IPv6 +# from OpenBSD src/sys/sys/socket.h +# define AF_INET 2 +# define AF_INET6 24 +OPENBSD_AF_INET = 2 +OPENBSD_AF_INET6 = 24 + +# from OpenBSD src/sys/net/if_pflog.h +# define PFLOG_HDRLEN sizeof(struct pfloghdr) +PFLOG_HDRLEN = 100 + class PFLog(Packet): + """ + Class for handling PFLog headers + """ name = "PFLog" - # from OpenBSD src/sys/net/pfvar.h and src/sys/net/if_pflog.h - fields_desc = [ByteField("hdrlen", 0), - ByteEnumField("addrfamily", 2, {socket.AF_INET: "IPv4", - socket.AF_INET6: "IPv6"}), + # from OpenBSD src/sys/net/pfvar.h + # and src/sys/net/if_pflog.h (struct pfloghdr) + fields_desc = [ByteField("hdrlen", PFLOG_HDRLEN), + ByteEnumField("addrfamily", 2, {OPENBSD_AF_INET: "IPv4", + OPENBSD_AF_INET6: "IPv6"}), ByteEnumField("action", 1, {0: "pass", 1: "drop", 2: "scrub", 3: "no-scrub", 4: "nat", 5: "no-nat", @@ -53,14 +66,38 @@ class PFLog(Packet): IntField("rulepid", 0), ByteEnumField("direction", 255, {0: "inout", 1: "in", 2: "out", 255: "unknown"}), - StrFixedLenField("pad", b"\x00\x00\x00", 3)] + YesNoByteField("rewritten", 0), + ByteEnumField("naddrfamily", 2, {OPENBSD_AF_INET: "IPv4", + OPENBSD_AF_INET6: "IPv6"}), + StrFixedLenField("pad", b"\x00", 1), + MultipleTypeField( + [ + (PadField(IPField("saddr", "127.0.0.1"), + 16, padwith=b"\x00"), + lambda pkt: pkt.addrfamily == OPENBSD_AF_INET), + (IP6Field("saddr", "::1"), + lambda pkt: pkt.addrfamily == OPENBSD_AF_INET6), + ], + PadField(IPField("saddr", "127.0.0.1"), + 16, padwith=b"\x00"),), + MultipleTypeField( + [ + (PadField(IPField("daddr", "127.0.0.1"), + 16, padwith=b"\x00"), + lambda pkt: pkt.addrfamily == OPENBSD_AF_INET), + (IP6Field("daddr", "::1"), + lambda pkt: pkt.addrfamily == OPENBSD_AF_INET6), + ], + PadField(IPField("daddr", "127.0.0.1"), + 16, padwith=b"\x00"),), + ShortField("sport", 0), + ShortField("dport", 0), ] def mysummary(self): return self.sprintf("%PFLog.addrfamily% %PFLog.action% on %PFLog.iface% by rule %PFLog.rulenumber%") # noqa: E501 -bind_layers(PFLog, IP, addrfamily=socket.AF_INET) -if conf.ipv6_enabled: - bind_layers(PFLog, IPv6, addrfamily=socket.AF_INET6) +bind_layers(PFLog, IP, addrfamily=OPENBSD_AF_INET) +bind_layers(PFLog, IPv6, addrfamily=OPENBSD_AF_INET6) conf.l2types.register(DLT_PFLOG, PFLog) diff --git a/scapy/layers/ppi.py b/scapy/layers/ppi.py index 3b11a20e6d5..5b7e2b665d3 100644 --- a/scapy/layers/ppi.py +++ b/scapy/layers/ppi.py @@ -1,19 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Scapy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# any later version. -# -# Scapy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Scapy. If not, see . - +# See https://scapy.net/ for more information # Original PPI author: + # scapy.contrib.description = CACE Per-Packet Information (PPI) header # scapy.contrib.status = loads @@ -22,7 +11,7 @@ A method for adding metadata to link-layer packets. -For example, one can tag an 802.11 packet with GPS co-ordinates of where it +For example, one can tag an 802.11 packet with GPS coordinates of where it was captured, and include it in the PCAP file. New PPI types should: diff --git a/scapy/layers/ppp.py b/scapy/layers/ppp.py index 3957930c605..e0f4c593d0c 100644 --- a/scapy/layers/ppp.py +++ b/scapy/layers/ppp.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ PPP (Point to Point Protocol) @@ -19,11 +19,25 @@ from scapy.layers.l2 import Ether, CookedLinux, GRE_PPTP from scapy.layers.inet import IP from scapy.layers.inet6 import IPv6 -from scapy.fields import BitField, ByteEnumField, ByteField, \ - ConditionalField, EnumField, FieldLenField, IntField, IPField, \ - PacketListField, PacketField, ShortEnumField, ShortField, \ - StrFixedLenField, StrLenField, XByteField, XShortField, XStrLenField -from scapy.modules import six +from scapy.fields import ( + BitField, + ByteEnumField, + ByteField, + ConditionalField, + EnumField, + FieldLenField, + IPField, + IntField, + OUIField, + PacketField, + PacketListField, + ShortEnumField, + ShortField, + StrLenField, + XByteField, + XShortField, + XStrLenField, +) class PPPoE(Packet): @@ -62,6 +76,12 @@ class PPPoED(PPPoE): XShortField("sessionid", 0x0), ShortField("len", None)] + def extract_padding(self, s): + return s[:self.len], s[self.len:] + + def mysummary(self): + return self.sprintf("%code%") + # PPPoE Tag types (RFC2516, RFC4638, RFC5578) class PPPoETag(Packet): @@ -97,6 +117,11 @@ class PPPoED_Tags(Packet): name = "PPPoE Tag List" fields_desc = [PacketListField('tag_list', None, PPPoETag)] + def mysummary(self): + return "PPPoE Tags" + ", ".join( + x.sprintf("%tag_type%") for x in self.tag_list + ), [PPPoED] + _PPP_PROTOCOLS = { 0x0001: "Padding Protocol", @@ -267,6 +292,14 @@ class _PPPProtoField(EnumField): See RFC 1661 section 2 + + The generated proto field is two bytes when not specified, or when specified + as an integer or a string: + PPP() + PPP(proto=0x21) + PPP(proto="Internet Protocol version 4") + To explicitly forge a one byte proto field, use the bytes representation: + PPP(proto=b'\x21') """ def getfield(self, pkt, s): if ord(s[:1]) & 0x01: @@ -279,12 +312,18 @@ def getfield(self, pkt, s): return super(_PPPProtoField, self).getfield(pkt, s) def addfield(self, pkt, s, val): - if val < 0x100: - self.fmt = "!B" - self.sz = 1 + if isinstance(val, bytes): + if len(val) == 1: + fmt, sz = "!B", 1 + elif len(val) == 2: + fmt, sz = "!H", 2 + else: + raise TypeError('Invalid length for PPP proto') + val = struct.Struct(fmt).unpack(val)[0] else: - self.fmt = "!H" - self.sz = 2 + fmt, sz = "!H", 2 + self.fmt = fmt + self.sz = sz self.struct = struct.Struct(self.fmt) return super(_PPPProtoField, self).addfield(pkt, s, val) @@ -435,7 +474,7 @@ class PPP_ECP_Option_OUI(PPP_ECP_Option): ByteEnumField("type", 0, _PPP_ecpopttypes), FieldLenField("len", None, length_of="data", fmt="B", adjust=lambda _, val: val + 6), - StrFixedLenField("oui", "", 3), + OUIField("oui", 0), ByteField("subtype", 0), StrLenField("data", "", length_from=lambda pkt: pkt.len - 6), ] @@ -727,7 +766,7 @@ def dispatch_hook(cls, _pkt=None, *_, **kargs): code = orb(_pkt[0]) elif "code" in kargs: code = kargs["code"] - if isinstance(code, six.string_types): + if isinstance(code, str): code = cls.fields_desc[0].s2i[code] if code == 1: @@ -808,7 +847,7 @@ def dispatch_hook(cls, _pkt=None, *_, **kargs): code = orb(_pkt[0]) elif "code" in kargs: code = kargs["code"] - if isinstance(code, six.string_types): + if isinstance(code, str): code = cls.fields_desc[0].s2i[code] if code in (1, 2): diff --git a/scapy/layers/pptp.py b/scapy/layers/pptp.py index c43959ec177..6d228d36310 100644 --- a/scapy/layers/pptp.py +++ b/scapy/layers/pptp.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Jan Sebechlebsky -# This program is published under a GPLv2 license """ PPTP (Point to Point Tunneling Protocol) @@ -88,7 +88,7 @@ class PPTPStartControlConnectionRequest(PPTP): XIntField("magic_cookie", _PPTP_MAGIC_COOKIE), ShortEnumField("ctrl_msg_type", 1, _PPTP_ctrl_msg_type), XShortField("reserved_0", 0x0000), - ShortField("protocol_version", 1), + ShortField("protocol_version", 0x0100), XShortField("reserved_1", 0x0000), FlagsField("framing_capabilities", 0, 32, _PPTP_FRAMING_CAPABILITIES_FLAGS), @@ -114,7 +114,7 @@ class PPTPStartControlConnectionReply(PPTP): XIntField("magic_cookie", _PPTP_MAGIC_COOKIE), ShortEnumField("ctrl_msg_type", 2, _PPTP_ctrl_msg_type), XShortField("reserved_0", 0x0000), - ShortField("protocol_version", 1), + ShortField("protocol_version", 0x0100), ByteEnumField("result_code", 1, _PPTP_start_control_connection_result), ByteEnumField("error_code", 0, _PPTP_general_error_code), diff --git a/scapy/layers/quic.py b/scapy/layers/quic.py new file mode 100644 index 00000000000..03d2b25ec94 --- /dev/null +++ b/scapy/layers/quic.py @@ -0,0 +1,342 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +QUIC + +The draft of a very basic implementation of the structures from [RFC 9000]. +This isn't binded to UDP by default as currently too incomplete. + +TODO: +- payloads. +- encryption. +- automaton. +- etc. +""" + +import struct + +from scapy.packet import ( + Packet, +) +from scapy.fields import ( + _EnumField, + BitEnumField, + BitField, + ByteEnumField, + ByteField, + EnumField, + Field, + FieldLenField, + FieldListField, + IntField, + MultipleTypeField, + ShortField, + StrLenField, + ThreeBytesField, +) + +# Typing imports +from typing import ( + Any, + Optional, + Tuple, +) + +# RFC9000 table 3 +_quic_payloads = { + 0x00: "PADDING", + 0x01: "PING", + 0x02: "ACK", + 0x04: "RESET_STREAM", + 0x05: "STOP_SENDING", + 0x06: "CRYPTO", + 0x07: "NEW_TOKEN", + 0x08: "STREAM", + 0x10: "MAX_DATA", + 0x11: "MAX_STREAM_DATA", + 0x12: "MAX_STREAMS", + 0x14: "DATA_BLOCKED", + 0x15: "STREAM_DATA_BLOCKED", + 0x16: "STREAMS_BLOCKED", + 0x18: "NEW_CONNECTION_ID", + 0x19: "RETIRE_CONNECTION_ID", + 0x1A: "PATH_CHALLENGE", + 0x1B: "PATH_RESPONSE", + 0x1C: "CONNECTION_CLOSE", + 0x1E: "HANDSHAKE_DONE", +} + + +# RFC9000 sect 16 +class QuicVarIntField(Field[int, int]): + def addfield(self, pkt: Packet, s: bytes, val: Optional[int]): + val = self.i2m(pkt, val) + if val < 0 or val > 0x3FFFFFFFFFFFFFFF: + raise struct.error("requires 0 <= number <= 4611686018427387903") + if val < 0x40: + return s + struct.pack("!B", val) + elif val < 0x4000: + return s + struct.pack("!H", val | 0x4000) + elif val < 0x40000000: + return s + struct.pack("!I", val | 0x80000000) + else: + return s + struct.pack("!Q", val | 0xC000000000000000) + + def getfield(self, pkt: Packet, s: bytes) -> Tuple[bytes, int]: + length = (s[0] & 0xC0) >> 6 + if length == 0: + return s[1:], struct.unpack("!B", s[:1])[0] & 0x3F + elif length == 1: + return s[2:], struct.unpack("!H", s[:2])[0] & 0x3FFF + elif length == 2: + return s[4:], struct.unpack("!I", s[:4])[0] & 0x3FFFFFFF + elif length == 3: + return s[8:], struct.unpack("!Q", s[:8])[0] & 0x3FFFFFFFFFFFFFFF + else: + raise Exception("Impossible.") + + +class QuicVarLenField(FieldLenField, QuicVarIntField): + pass + + +class QuicVarEnumField(QuicVarIntField, _EnumField[int]): + __slots__ = EnumField.__slots__ + + def __init__(self, name, default, enum): + # type: (str, Optional[int], Any, int) -> None + _EnumField.__init__(self, name, default, enum) # type: ignore + QuicVarIntField.__init__(self, name, default) + + def any2i(self, pkt, x): + # type: (Optional[Packet], Any) -> int + return _EnumField.any2i(self, pkt, x) # type: ignore + + def i2repr( + self, + pkt, # type: Optional[Packet] + x, # type: int + ): + # type: (...) -> Any + return _EnumField.i2repr(self, pkt, x) + + +# -- Headers -- + + +# RFC9000 sect 17.2 +_quic_long_hdr = { + 0: "Short", + 1: "Long", +} + +_quic_long_pkttyp = { + # RFC9000 table 5 + 0x00: "Initial", + 0x01: "0-RTT", + 0x02: "Handshake", + 0x03: "Retry", +} + +# RFC9000 sect 17 abstraction + + +class QUIC(Packet): + match_subclass = True + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + """ + Returns the right class for the given data. + """ + if _pkt: + hdr = _pkt[0] + if hdr & 0x80: + # Long Header packets + if hdr & 0x40 == 0: + return QUIC_Version + else: + typ = (hdr & 0x30) >> 4 + return { + 0: QUIC_Initial, + 1: QUIC_0RTT, + 2: QUIC_Handshake, + 3: QUIC_Retry, + }[typ] + else: + # Short Header packets + return QUIC_1RTT + return QUIC_Initial + + def mysummary(self): + return self.name + + +# RFC9000 sect 17.2.1 + + +class QUIC_Version(QUIC): + name = "QUIC - Version Negotiation" + fields_desc = [ + BitEnumField("HeaderForm", 1, 1, _quic_long_hdr), + BitField("Unused", 0, 7), + IntField("Version", 0), + FieldLenField("DstConnIDLen", None, length_of="DstConnID", fmt="B"), + StrLenField("DstConnID", "", length_from=lambda pkt: pkt.DstConnIDLen), + FieldLenField("SrcConnIDLen", None, length_of="SrcConnID", fmt="B"), + StrLenField("SrcConnID", "", length_from=lambda pkt: pkt.SrcConnIDLen), + FieldListField("SupportedVersions", [], IntField("", 0)), + ] + + +# RFC9000 sect 17.2.2 + +QuicPacketNumberField = lambda name, default: MultipleTypeField( + [ + ( + ByteField(name, default), + ( + lambda pkt: pkt.PacketNumberLen == 0, + lambda _, val: val < 0x100, + ), + ), + ( + ShortField(name, default), + ( + lambda pkt: pkt.PacketNumberLen == 1, + lambda _, val: val < 0x10000, + ), + ), + ( + ThreeBytesField(name, default), + ( + lambda pkt: pkt.PacketNumberLen == 2, + lambda _, val: val < 0x1000000, + ), + ), + ( + IntField(name, default), + ( + lambda pkt: pkt.PacketNumberLen == 3, + lambda _, val: val < 0x100000000, + ), + ), + ], + ByteField(name, default), +) + + +class QuicPacketNumberBitFieldLenField(BitField): + def i2m(self, pkt, x): + if x is None and pkt is not None: + PacketNumber = pkt.PacketNumber or 0 + if PacketNumber < 0 or PacketNumber > 0xFFFFFFFF: + raise struct.error("requires 0 <= number <= 0xFFFFFFFF") + if PacketNumber < 0x100: + return 0 + elif PacketNumber < 0x10000: + return 1 + elif PacketNumber < 0x1000000: + return 2 + else: + return 3 + elif x is None: + return 0 + return x + + +class QUIC_Initial(QUIC): + name = "QUIC - Initial" + Version = 0x00000001 + fields_desc = ( + [ + BitEnumField("HeaderForm", 1, 1, _quic_long_hdr), + BitField("FixedBit", 1, 1), + BitEnumField("LongPacketType", 0, 2, _quic_long_pkttyp), + BitField("Reserved", 0, 2), + QuicPacketNumberBitFieldLenField("PacketNumberLen", None, 2), + ] + + QUIC_Version.fields_desc[2:7] + + [ + QuicVarLenField("TokenLen", None, length_of="Token"), + StrLenField("Token", "", length_from=lambda pkt: pkt.TokenLen), + QuicVarIntField("Length", 0), + QuicPacketNumberField("PacketNumber", 0), + ] + ) + + +# RFC9000 sect 17.2.3 +class QUIC_0RTT(QUIC): + name = "QUIC - 0-RTT" + LongPacketType = 1 + fields_desc = QUIC_Initial.fields_desc[:10] + [ + QuicVarIntField("Length", 0), + QuicPacketNumberField("PacketNumber", 0), + ] + + +# RFC9000 sect 17.2.4 +class QUIC_Handshake(QUIC): + name = "QUIC - Handshake" + LongPacketType = 2 + fields_desc = QUIC_0RTT.fields_desc + + +# RFC9000 sect 17.2.5 +class QUIC_Retry(QUIC): + name = "QUIC - Retry" + LongPacketType = 3 + Version = 0x00000001 + fields_desc = ( + QUIC_Initial.fields_desc[:3] + + [ + BitField("Unused", 0, 4), + ] + + QUIC_Version.fields_desc[2:7] + ) + + +# RFC9000 sect 17.3 +class QUIC_1RTT(QUIC): + name = "QUIC - 1-RTT" + fields_desc = [ + BitEnumField("HeaderForm", 0, 1, _quic_long_hdr), + BitField("FixedBit", 1, 1), + BitField("SpinBit", 0, 1), + BitField("Reserved", 0, 2), + BitField("KeyPhase", 0, 1), + QuicPacketNumberBitFieldLenField("PacketNumberLen", None, 2), + # FIXME - Destination Connection ID + QuicPacketNumberField("PacketNumber", 0), + ] + + +# RFC9000 sect 19.1 +class QUIC_PADDING(Packet): + fields_desc = [ + ByteEnumField("Type", 0x00, _quic_payloads), + ] + + +# RFC9000 sect 19.2 +class QUIC_PING(Packet): + fields_desc = [ + ByteEnumField("Type", 0x01, _quic_payloads), + ] + + +# RFC9000 sect 19.3 +class QUIC_ACK(Packet): + fields_desc = [ + ByteEnumField("Type", 0x02, _quic_payloads), + ] + + +# Bindings +# bind_bottom_up(UDP, QUIC, dport=443) +# bind_bottom_up(UDP, QUIC, sport=443) +# bind_layers(UDP, QUIC, dport=443, sport=443) diff --git a/scapy/layers/radius.py b/scapy/layers/radius.py index c1e8a1844df..14a1fd8787e 100644 --- a/scapy/layers/radius.py +++ b/scapy/layers/radius.py @@ -1,26 +1,66 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Vincent Mauge -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Acknowledgment: Vincent Mauge """ RADIUS (Remote Authentication Dial In User Service) + +To disable Radius-EAP defragmentation (True by default), you can use:: + + conf.contribs.setdefault("radius", {}).setdefault("auto-defrag", False) """ -import struct +import collections +import enum import hashlib import hmac -from scapy.compat import orb, raw -from scapy.packet import Packet, Padding, bind_layers, bind_bottom_up -from scapy.fields import ByteField, ByteEnumField, IntField, StrLenField,\ - XStrLenField, XStrFixedLenField, FieldLenField, PacketLenField,\ - PacketListField, IPField, MultiEnumField -from scapy.layers.inet import UDP +import struct + +from scapy.ansmachine import AnsweringMachine +from scapy.compat import bytes_encode +from scapy.config import conf, crypto_validator +from scapy.error import log_runtime, Scapy_Exception +from scapy.packet import ( + Packet, + Padding, + bind_layers, + bind_bottom_up, +) +from scapy.fields import ( + ByteEnumField, + ByteField, + FieldLenField, + IPField, + IntEnumField, + IntField, + MultiEnumField, + MultipleTypeField, + PacketLenField, + PacketListField, + StrField, + StrFixedLenField, + StrLenField, + XStrFixedLenField, + XStrLenField, +) +from scapy.sendrecv import send +from scapy.utils import strxor + from scapy.layers.eap import EAP -from scapy.utils import issubtype -from scapy.config import conf -from scapy.error import Scapy_Exception +from scapy.layers.inet import UDP, IP +from scapy.layers.ntlm import MD4le + +if conf.crypto_valid: + from scapy.layers.tls.crypto.cipher_block import Cipher_DES_ECB + from scapy.layers.tls.crypto.hash import ( + Hash_MD4, + Hash_MD5, + Hash_SHA, + ) +else: + Cipher_DES_ECB = None + Hash_MD4 = Hash_MD5 = Hash_SHA = None # https://www.iana.org/assignments/radius-types/radius-types.xhtml @@ -253,23 +293,10 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): """ if _pkt: - attr_type = orb(_pkt[0]) + attr_type = _pkt[0] return cls.registered_attributes.get(attr_type, cls) return cls - def haslayer(self, cls): - if cls == "RadiusAttribute": - if isinstance(self, RadiusAttribute): - return True - elif issubtype(cls, RadiusAttribute): - if isinstance(self, cls): - return True - return super(RadiusAttribute, self).haslayer(cls) - - def getlayer(self, cls, nb=1, _track=None, _subclass=True, **flt): - return super(RadiusAttribute, self).getlayer(cls, nb=nb, _track=_track, - _subclass=True, **flt) - def post_build(self, p, pay): length = self.len if length is None: @@ -288,6 +315,7 @@ class _SpecificRadiusAttr(RadiusAttribute): """ __slots__ = ["val"] + match_subclass = True def __init__(self, _pkt="", post_transform=None, _internal=0, _underlayer=None, **fields): # noqa: E501 super(_SpecificRadiusAttr, self).__init__( @@ -407,6 +435,11 @@ class RadiusAttr_Acct_Output_Gigawords(_RadiusAttrIntValue): val = 53 +class RadiusAttr_Event_Timestamp(_RadiusAttrIntValue): + """RFC 2869""" + val = 55 + + class RadiusAttr_Egress_VLANID(_RadiusAttrIntValue): """RFC 4675""" val = 56 @@ -537,26 +570,56 @@ class RadiusAttr_User_Password(_RadiusAttrHexStringVal): """RFC 2865""" val = 2 + def decrypt(self, radius_packet, secret): + """ + Return the decrypted value of the User-Password field + RFC2865 sect 5.2 + """ + password = b"" + + encrypted = self.value + ci = radius_packet.authenticator + while encrypted: + bi = Hash_MD5().digest(secret + ci) + ci, encrypted = encrypted[:16], encrypted[16:] + password += strxor(ci, bi) + + return password.rstrip(b"\x00") + + @staticmethod + def encrypt(radius_packet, password, secret): + """ + Create a User-Password attribute from a secret + RFC2865 sect 5.2 + """ + password = bytes_encode(password) + + # Pad to 16 octets boundary + password += (-len(password) % 16) * b"\x00" + + encrypted = b"" + ci = radius_packet.authenticator + while password: + bi = Hash_MD5().digest(secret + ci) + ci = strxor(password[:16], bi) + password = password[16:] + + return RadiusAttr_User_Password(value=encrypted) + class RadiusAttr_State(_RadiusAttrHexStringVal): """RFC 2865""" val = 24 -def prepare_packed_data(radius_packet, packed_req_authenticator): +def prepare_packed_data(radius_packet, RequestAuth): """ Pack RADIUS data prior computing the authentication MAC """ - - packed_hdr = struct.pack("!B", radius_packet.code) - packed_hdr += struct.pack("!B", radius_packet.id) - packed_hdr += struct.pack("!H", radius_packet.len) - - packed_attrs = b'' - for attr in radius_packet.attributes: - packed_attrs += raw(attr) - - return packed_hdr + packed_req_authenticator + packed_attrs + s = bytes(radius_packet) + Code_Id_Length = s[:4] + Attributes = s[4 + 16:] + return Code_Id_Length + RequestAuth + Attributes class RadiusAttr_Message_Authenticator(_RadiusAttrHexStringVal): @@ -582,8 +645,10 @@ def compute_message_authenticator(radius_packet, packed_req_authenticator, (RFC 2869 - Page 33) """ + # Make sure the current auth is empty attr = radius_packet[RadiusAttr_Message_Authenticator] - attr.value = bytearray(attr.len - 2) + attr.value = b"\x00" * (attr.len - 2) + data = prepare_packed_data(radius_packet, packed_req_authenticator) radius_hmac = hmac.new(shared_secret, data, hashlib.md5) @@ -1002,13 +1067,31 @@ class RadiusAttr_Framed_Protocol(_RadiusAttrIntEnumVal): val = 7 +class RadiusAttr_Acct_Status_Type(_RadiusAttrIntEnumVal): + """RFC 2866""" + val = 40 + + +class RadiusAttr_Acct_Authentic(_RadiusAttrIntEnumVal): + """RFC 2866""" + val = 45 + + +class RadiusAttr_Acct_Terminate_Cause(_RadiusAttrIntEnumVal): + """RFC 2866""" + val = 49 + + class RadiusAttr_NAS_Port_Type(_RadiusAttrIntEnumVal): """RFC 2865""" val = 61 +# +# RADIUS attributes that are complex structures +# + class _EAPPacketField(PacketLenField): - """ Handles EAP-Message attribute value (the actual EAP packet). """ @@ -1029,8 +1112,8 @@ class RadiusAttr_EAP_Message(RadiusAttribute): """ Implements the "EAP-Message" attribute (RFC 3579). """ - name = "EAP-Message" + match_subclass = True fields_desc = [ ByteEnumField("type", 79, _radius_attribute_types), FieldLenField( @@ -1038,11 +1121,98 @@ class RadiusAttr_EAP_Message(RadiusAttribute): None, "value", "B", - adjust=lambda pkt, x: len(pkt.value) + 2 + adjust=lambda pkt, x: x + 2 ), _EAPPacketField("value", "", EAP, length_from=lambda p: p.len - 2) ] + def post_dissect(self, s): + if not conf.contribs.get("radius", {}).get("auto-defrag", True): + return s + if isinstance(self.value, conf.raw_layer): + # Defragment + x = s + buf = self.value.load + while x and struct.unpack("!B", x[:1])[0] == 79: + # Let's carefully avoid the infinite loop + length = struct.unpack("!B", x[1:2])[0] + if not length: + return s + buf, x = buf + x[2:length], x[length:] + if length < 254: + self.value = EAP(buf) + return x + return s + + +_radius_vendor_types = { + # Microsoft (RFC 2548) + 311: { + 1: "MS-CHAP-Response", + 2: "MS-CHAP-Error", + 3: "MS-CHAP-CPW-1", + 4: "MS-CHAP-CPW-2", + 5: "MS-CHAP-LM-Enc-PW", + 6: "MS-CHAP-NT-Enc-PW", + 7: "MS-MPPE-Encryption-Policy", + 8: "MS-MPPE-Encryption-Type", + 9: "MS-RAS-Vendor", + 10: "MS-CHAP-Domain", + 11: "MS-CHAP-Challenge", + 12: "MS-CHAP-MPPE-Keys", + 13: "MS-BAP-Usage", + 14: "MS-Link-Utilization-Threshold", + 15: "MS-Link-Drop-Time-Limit", + 16: "MS-MPPE-Send-Key", + 17: "MS-MPPE-Recv-Key", + 18: "MS-RAS-Version", + 19: "MS-Old-ARAP-Password", + 20: "MS-New-ARAP-Password", + 21: "MS-ARAP-PW-Change-Reason", + 22: "MS-Filter", + 23: "MS-Acct-Auth-Type", + 24: "MS-Acct-EAP-Type", + 25: "MS-CHAP2-Response", + 26: "MS-CHAP2-Success", + 27: "MS-CHAP2-CPW", + 28: "MS-Primary-DNS-Server", + 29: "MS-Secondary-DNS-Server", + 30: "MS-Primary-NBNS-Server", + 31: "MS-Secondary-NBNS-Server", + 33: "MS-ARAP-Challenge", + } +} + + +class _RadiusAttrVendorValue(Packet): + """ + Used to register a 'value' vendor-specific + """ + registered_vendor_value = collections.defaultdict(dict) + VENDOR_ID = 0 + VENDOR_TYPE = 0 + + @classmethod + def register_variant(cls): + cls.registered_vendor_value[cls.VENDOR_ID][cls.VENDOR_TYPE] = cls + + +def _radius_vendor_cls(pkt): + """ + Return the class that makes for a 'value' in the vendor attribute, or None. + """ + if pkt.vendor_id not in _RadiusAttrVendorValue.registered_vendor_value: + return None + return _RadiusAttrVendorValue.registered_vendor_value[pkt.vendor_id].get( + pkt.vendor_type, + None, + ) + + +class _RadiusVendorValueField(PacketLenField): + def m2i(self, pkt, s): + return _radius_vendor_cls(pkt)(s, _parent=pkt) + class RadiusAttr_Vendor_Specific(RadiusAttribute): """ @@ -1050,6 +1220,7 @@ class RadiusAttr_Vendor_Specific(RadiusAttribute): """ name = "Vendor-Specific" + match_subclass = True fields_desc = [ ByteEnumField("type", 26, _radius_attribute_types), FieldLenField( @@ -1059,8 +1230,16 @@ class RadiusAttr_Vendor_Specific(RadiusAttribute): "B", adjust=lambda pkt, x: len(pkt.value) + 8 ), - IntField("vendor_id", 0), - ByteField("vendor_type", 0), + IntEnumField("vendor_id", 0, { + 311: "Microsoft", + }), + MultiEnumField( + "vendor_type", + 0, + _radius_vendor_types, + depends_on=lambda p: p.vendor_id, + fmt="B" + ), FieldLenField( "vendor_len", None, @@ -1068,7 +1247,16 @@ class RadiusAttr_Vendor_Specific(RadiusAttribute): "B", adjust=lambda p, x: len(p.value) + 2 ), - StrLenField("value", "", length_from=lambda p: p.vendor_len - 2) + MultipleTypeField( + [ + ( + _RadiusVendorValueField("value", None, None, + length_from=lambda p: p.vendor_len - 2), + lambda pkt: _radius_vendor_cls(pkt) is not None + ) + ], + StrLenField("value", "", length_from=lambda p: p.vendor_len - 2), + ) ] @@ -1161,6 +1349,37 @@ def post_build(self, p, pay): p = p[:2] + struct.pack("!H", length) + p[4:] return p + def mysummary(self): + extra = "" + if self.code == 1: + # Access-Request + attrs = { + ( + (x.vendor_id, x.vendor_type) + if RadiusAttr_Vendor_Specific in x else + x.type + ): x + for x in self.attributes + if isinstance(x, RadiusAttribute) + } + # Log additional attributes + if 1 in attrs: + extra += "User:'%s' " % attrs[1].value.decode(errors="ignore") + # Try to detect the logon algo + if 2 in attrs: + extra += "PAP" + elif 3 in attrs: + extra += "CHAP" + elif 79 in attrs: + extra += "EAP" + elif (311, 1) in attrs: + extra += "MS-CHAP" + elif (311, 25) in attrs: + extra += "MS-CHAP2" + if extra: + extra = " (%s)" % extra.strip() + return self.sprintf("RADIUS %code%") + extra + bind_bottom_up(UDP, Radius, sport=1812) bind_bottom_up(UDP, Radius, dport=1812) @@ -1169,3 +1388,353 @@ def post_build(self, p, pay): bind_bottom_up(UDP, Radius, sport=3799) bind_bottom_up(UDP, Radius, dport=3799) bind_layers(UDP, Radius, sport=1812, dport=1812) + + +# MS-CHAP2 + +# RFC 2548 sect 2.3.2 + +class MS_CHAP2_Response(_RadiusAttrVendorValue): + VENDOR_ID = 311 + VENDOR_TYPE = 25 + fields_desc = [ + ByteField("Ident", 0), + ByteField("Flags", 0), + XStrFixedLenField("PeerChallenge", b"", length=16), + XStrFixedLenField("Reserved", b"", length=8), + XStrFixedLenField("Response", b"", length=24), + ] + + +# RFC 2548 sect 2.3.3 + +class MS_CHAP2_Success(_RadiusAttrVendorValue): + VENDOR_ID = 311 + VENDOR_TYPE = 26 + fields_desc = [ + ByteField("Ident", 0), + StrFixedLenField("String", b"", length=42), + ] + + +# RFC 2548 sect 2.1.5 + +class MS_CHAP_Error(_RadiusAttrVendorValue): + VENDOR_ID = 311 + VENDOR_TYPE = 2 + fields_desc = [ + ByteField("Ident", 0), + StrField("String", b""), + ] + + +# RFC 2548 sect 2.1.4 + +class MS_CHAP_Domain(_RadiusAttrVendorValue): + VENDOR_ID = 311 + VENDOR_TYPE = 10 + fields_desc = [ + ByteField("Ident", 0), + StrField("String", b""), + ] + + +def MS_CHAP2_GenerateNTResponse(AuthenticatorChallenge, PeerChallenge, + UserName, HashNT): + """ + RFC2759 sect 8.1 + """ + Challenge = MS_CHAP2_ChallengeHash(PeerChallenge, AuthenticatorChallenge, UserName) + PasswordHash = HashNT + return MS_CHAP2_ChallengeResponse(Challenge, PasswordHash) + + +def MS_CHAP2_ChallengeHash(PeerChallenge, AuthenticatorChallenge, UserName): + """ + rfc 2759 sect 8.2 + """ + UserName = UserName.split(b"\\")[-1] # Strip domain if present + return Hash_SHA().digest(PeerChallenge + AuthenticatorChallenge + UserName)[:8] + + +def MS_CHAP2_ChallengeResponse(Challenge, PasswordHash): + """ + rfc 2759 sect 8.5 + """ + ZPasswordHash = int.from_bytes( + PasswordHash + b"\x00" * (-len(PasswordHash) % 21), + "big", + ) + + # Add !FAKE! DES parity bits because cryptography requires them (then drops them) + ZPasswordHashParity = b"" + for _ in range(24): + val, ZPasswordHash = (ZPasswordHash & 0x7F), (ZPasswordHash >> 7) + ZPasswordHashParity = struct.pack("B", val << 1) + ZPasswordHashParity + + return ( + Cipher_DES_ECB(ZPasswordHashParity[0:8]).encrypt(Challenge) + + Cipher_DES_ECB(ZPasswordHashParity[8:16]).encrypt(Challenge) + + Cipher_DES_ECB(ZPasswordHashParity[16:24]).encrypt(Challenge) + ) + + +def MS_CHAP2_GenerateAuthenticatorResponse(HashNT, + NTResponse, + PeerChallenge, + AuthenticatorChallenge, + UserName): + """ + rfc 2759 sect 8.7 + """ + Magic1 = bytes(bytearray([ + 0x4D, 0x61, 0x67, 0x69, 0x63, 0x20, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x20, 0x74, 0x6F, 0x20, 0x63, 0x6C, 0x69, 0x65, + 0x6E, 0x74, 0x20, 0x73, 0x69, 0x67, 0x6E, 0x69, 0x6E, 0x67, + 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x74, + ])) + Magic2 = bytes(bytearray([ + 0x50, 0x61, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x6D, 0x61, 0x6B, + 0x65, 0x20, 0x69, 0x74, 0x20, 0x64, 0x6F, 0x20, 0x6D, 0x6F, + 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E, 0x20, 0x6F, 0x6E, + 0x65, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, + 0x6E + ])) + PasswordHash = HashNT + PasswordHashHash = Hash_MD4().digest(PasswordHash) + + Digest = Hash_SHA().digest(PasswordHashHash + NTResponse + Magic1) + + Challenge = MS_CHAP2_ChallengeHash( + PeerChallenge, + AuthenticatorChallenge, + UserName, + ) + + return Hash_SHA().digest(Digest + Challenge + Magic2) + + +# Answering machine + +class RadiusAuthType(enum.Enum): + MS_CHAP_V2 = enum.auto() + EAP = enum.auto() + + +@crypto_validator +class Radius_am(AnsweringMachine): + function_name = "radiusd" + filter = "udp and port 1812" + send_function = staticmethod(send) + send_options_list = ["inter", "verbose"] + + def parse_options(self, + secret, + IDENTITIES=None, + IDENTITIES_MSCHAPv2=None, + servicetype=None, + mschapdomain=None, + extra_attributes=[]): + """ + This provides a tiny RADIUS daemon that answers Access-Request messages. + This can be used while setting up a Cisco switch for instance. + + Demo:: + + >>> radiusd(secret="SECRET", iface="lo", IDENTITIES={"user": "password"}) + $ echo "Message-Authenticator=0x00,User-Name=user,\\ + User-Password=password" | radclient -P udp 127.0.0.1 auth -F SECRET + + :param secret: the server's secret + :param IDENTITIES: the identities in format {"username": b"password"} + :param IDENTITIES_MSCHAPv2: the MsCHAPv2 identities in format + {"username": b"HashNT"}. The HashNT can be obtained + using MD4le(). If IDENTITIES is provided, this will be calculated. + :param servicetype: the Service-Type to answer. + :param mschapdomain: the MS-CHAP-DOMAIN to answer if MS-CHAP* is used. + :param extra_attributes: a list of extra Radius attributes + """ + self.secret = bytes_encode(secret) + self.servicetype = servicetype + self.mschapdomain = mschapdomain + self.extra_attributes = extra_attributes + if not IDENTITIES: + IDENTITIES = {} + if IDENTITIES_MSCHAPv2 is None and IDENTITIES: + IDENTITIES_MSCHAPv2 = { + user: MD4le(pwd) + for user, pwd in IDENTITIES.items() + } + self.IDENTITIES = { + user: bytes_encode(pwd) + for user, pwd in IDENTITIES.items() + } + self.IDENTITIES_MSCHAPv2 = IDENTITIES_MSCHAPv2 + + def is_request(self, req): + # Only match Access-Request + return Radius in req and req[Radius].code == 1 + + def print_reply(self, req, reply): + print("%s / %s -> %s" % ( + reply[IP].dst, + req[Radius].summary(), + ( + conf.color_theme.fail + if reply.code != 2 else + conf.color_theme.success + )(reply.sprintf("%Radius.code%")), + )) + + def make_reply(self, req): + resp = req + + # Basic response + resp = ( + IP(src=req[IP].dst, dst=req[IP].src) / + UDP(sport=req[UDP].dport, dport=req[UDP].sport) + ) + + # Sort attributes for quick access + attrs = { + ( + (x.vendor_id, x.vendor_type) + if RadiusAttr_Vendor_Specific in x else + x.type + ): x + for x in req.attributes + } + + # Build Radius response + rad = Radius(code=2, id=req[Radius].id) + + # Process various authentication methods + try: + if 2 in attrs: + # PAP + if not self.IDENTITIES: + raise Scapy_Exception( + "Missing IDENTITIES for User-Password auth ! Assuming OK." + ) + + UserName = attrs[1].value + KnownPassword = self.IDENTITIES.get(UserName.decode(), None) + UserPassword = attrs[2].decrypt( + req, + self.secret, + ) + + if KnownPassword is None: + log_runtime.warning("Couldn't find user '%s'" % UserName.decode()) + rad.code = 3 + elif UserPassword != KnownPassword: + log_runtime.warning( + "Bad password for user '%s'" % UserName.decode() + ) + rad.code = 3 + elif 79 in attrs: + # EAP-Message is used + raise Scapy_Exception( + "EAP as a Radius auth method is not implemented !" + ) + elif (311, 25) in attrs: + # MS-CHAP2 + if not self.IDENTITIES_MSCHAPv2: + raise Scapy_Exception("Missing IDENTITIES_MSCHAPv2 for MsChapV2 !") + + response = attrs[(311, 25)].value + try: + AuthenticatorChallenge = attrs[(311, 11)].value # CHAP-Challenge + except KeyError: + raise Scapy_Exception("Missing CHAP-Challenge !") + + UserName = attrs[1].value + HashNT = self.IDENTITIES_MSCHAPv2.get(UserName.decode(), None) + + # 1. Check the client-provided NTResponse + if HashNT is None: + log_runtime.warning("Couldn't find user '%s'" % UserName.decode()) + rad.code = 3 + elif MS_CHAP2_GenerateNTResponse( + AuthenticatorChallenge, + response.PeerChallenge, + UserName, + HashNT) != response.Response: + log_runtime.warning( + "Bad MS-CHAP2-NTResponse for user '%s' !" % UserName.decode() + ) + rad.code = 3 + + # Did the auth failed? + if rad.code == 3: + rad.attributes.append( + RadiusAttr_Vendor_Specific( + vendor_id="Microsoft", + vendor_type=2, + value=MS_CHAP_Error( + Ident=response.Ident, + String="E=691 R=0 V=3", + ), + ) + ) + else: + # 2. Build the response 'success' response + auth_string = MS_CHAP2_GenerateAuthenticatorResponse( + HashNT, + response.Response, + response.PeerChallenge, + AuthenticatorChallenge, + UserName, + ) + rad.attributes.append( + RadiusAttr_Vendor_Specific( + vendor_id=311, + vendor_type="MS-CHAP2-Success", + value=MS_CHAP2_Success( + Ident=response.Ident, + String="S=%s" % auth_string.hex().upper() + ) + ) + ) + if self.mschapdomain is not None: + rad.attributes.append( + RadiusAttr_Vendor_Specific( + vendor_id=311, + vendor_type="MS-CHAP-Domain", + value=MS_CHAP_Domain( + Ident=response.Ident, + String=self.mschapdomain, + ) + ) + ) + else: + raise Scapy_Exception( + "Authentication method not provided or unsupported !" + ) + except Scapy_Exception as ex: + # display a warning + log_runtime.warning(str(ex)) + + # Add additional records if it's an accept + if rad.code == 2: + if self.servicetype is not None: + rad.attributes.append( + RadiusAttr_Service_Type(value=self.servicetype) + ) + + rad.attributes.extend(self.extra_attributes) + + # Add and compute message authenticator + mauth = RadiusAttr_Message_Authenticator() + rad.attributes.insert(0, mauth) + mauth.value = mauth.compute_message_authenticator( + rad, + req.authenticator, + self.secret, + ) + + # Add global authenticator + rad.authenticator = rad.compute_authenticator(req.authenticator, self.secret) + + # Final packet + return resp / rad diff --git a/scapy/layers/rip.py b/scapy/layers/rip.py index 27ba712532e..fb6e701abd4 100644 --- a/scapy/layers/rip.py +++ b/scapy/layers/rip.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ RIP (Routing Information Protocol). diff --git a/scapy/layers/rtp.py b/scapy/layers/rtp.py index ee8f0b9c9c4..861debd6a82 100644 --- a/scapy/layers/rtp.py +++ b/scapy/layers/rtp.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ RTP (Real-time Transport Protocol). diff --git a/scapy/layers/sctp.py b/scapy/layers/sctp.py index e6634b90c12..1d002dcc5b0 100644 --- a/scapy/layers/sctp.py +++ b/scapy/layers/sctp.py @@ -1,27 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi # Copyright (C) 6WIND -# This program is published under a GPLv2 license """ SCTP (Stream Control Transmission Protocol). """ -from __future__ import absolute_import import struct from scapy.compat import orb, raw from scapy.volatile import RandBin from scapy.config import conf from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, ByteEnumField, ConditionalField, Field, \ - FieldLenField, FieldListField, IPField, IntEnumField, IntField, \ - PacketListField, PadField, ShortEnumField, ShortField, StrLenField, \ - XByteField, XIntField, XShortField -from scapy.layers.inet import IP -from scapy.layers.inet6 import IP6Field -from scapy.layers.inet6 import IPv6 +from scapy.fields import ( + BitField, + ByteEnumField, + Field, + FieldLenField, + FieldListField, + IPField, + IntEnumField, + IntField, + MultipleTypeField, + PacketLenField, + PacketListField, + PadField, + ShortEnumField, + ShortField, + StrFixedLenField, + StrLenField, + XByteField, + XIntField, + XShortField, +) +from scapy.data import SCTP_SERVICES +from scapy.layers.inet import IP, IPerror +from scapy.layers.inet6 import IP6Field, IPv6, IPerror6, nh_clserror IPPROTO_SCTP = 132 @@ -109,13 +125,13 @@ def crc32c(buf): def update_adler32(adler, buf): s1 = adler & 0xffff s2 = (adler >> 16) & 0xffff - print s1,s2 + print(s1, s2) for c in buf: - print orb(c) + print(orb(c)) s1 = (s1 + orb(c)) % BASE s2 = (s2 + s1) % BASE - print s1,s2 + print(s1, s2) return (s2 << 16) + s1 def sctp_checksum(buf): @@ -144,8 +160,13 @@ def sctp_checksum(buf): 11: "SCTPChunkCookieAck", 14: "SCTPChunkShutdownComplete", 15: "SCTPChunkAuthentication", + 64: "SCTPChunkIData", + 130: "SCTPChunkReConfig", + 132: "SCTPChunkPad", 0x80: "SCTPChunkAddressConfAck", + 192: "SCTPChunkForwardTSN", 0xc1: "SCTPChunkAddressConf", + 194: "SCTPChunkIForwardTSN", } sctpchunktypes = { @@ -163,12 +184,17 @@ def sctp_checksum(buf): 11: "cookie-ack", 14: "shutdown-complete", 15: "authentication", + 64: "i-data", + 130: "re-config", + 132: "pad", 0x80: "address-configuration-ack", + 192: "forward-tsn", 0xc1: "address-configuration", + 194: "i-forward-tsn", } sctpchunkparamtypescls = { - 1: "SCTPChunkParamHearbeatInfo", + 1: "SCTPChunkParamHeartbeatInfo", 5: "SCTPChunkParamIPv4Addr", 6: "SCTPChunkParamIPv6Addr", 7: "SCTPChunkParamStateCookie", @@ -176,6 +202,12 @@ def sctp_checksum(buf): 9: "SCTPChunkParamCookiePreservative", 11: "SCTPChunkParamHostname", 12: "SCTPChunkParamSupportedAddrTypes", + 13: "SCTPChunkParamOutgoingSSNResetRequest", + 14: "SCTPChunkParamIncomingSSNResetRequest", + 15: "SCTPChunkParamSSNTSNResetRequest", + 16: "SCTPChunkParamReConfigurationResponse", + 17: "SCTPChunkParamAddOutgoingStreamRequest", + 18: "SCTPChunkParamAddIncomingStreamRequest", 0x8000: "SCTPChunkParamECNCapable", 0x8002: "SCTPChunkParamRandom", 0x8003: "SCTPChunkParamChunkList", @@ -199,6 +231,12 @@ def sctp_checksum(buf): 9: "cookie-preservative", 11: "hostname", 12: "addrtypes", + 13: "out-ssn-reset-req", + 14: "in-ssn-reset-req", + 15: "ssn-tsn-reset-req", + 16: "re-configuration-response", + 17: "add-outgoing-stream-req", + 18: "add-incoming-stream-req", 0x8000: "ecn-capable", 0x8002: "random", 0x8003: "chunk-list", @@ -228,9 +266,9 @@ def default_payload_class(self, p): class SCTP(_SCTPChunkGuessPayload, Packet): - fields_desc = [ShortField("sport", None), - ShortField("dport", None), - XIntField("tag", None), + fields_desc = [ShortEnumField("sport", 0, SCTP_SERVICES), + ShortEnumField("dport", 0, SCTP_SERVICES), + XIntField("tag", 0), XIntField("chksum", None), ] def answers(self, other): @@ -249,9 +287,39 @@ def post_build(self, p, pay): p = p[:8] + struct.pack(">I", crc) + p[12:] return p + +class SCTPerror(SCTP): + name = "SCTP in ICMP" + + def answers(self, other): + if not isinstance(other, SCTP): + return 0 + if conf.checkIPsrc: + if not ((self.sport == other.sport) and + (self.dport == other.dport)): + return 0 + return 1 + + def mysummary(self): + return Packet.mysummary(self) + + +nh_clserror[IPPROTO_SCTP] = SCTPerror + # SCTP Chunk variable params +resultcode = { + 0: "Success - Nothing to do", + 1: "Success - Performed", + 2: "Denied", + 3: "Error - Wrong SSN", + 4: "Error - Request already in progress", + 5: "Error - Bad Sequence Number", + 6: "In Progress" +} + + class ChunkParamField(PacketListField): def __init__(self, name, default, count_from=None, length_from=None): PacketListField.__init__(self, name, default, conf.raw_layer, count_from=count_from, length_from=length_from) # noqa: E501 @@ -271,7 +339,7 @@ def extract_padding(self, s): return b"", s[:] -class SCTPChunkParamHearbeatInfo(_SCTPChunkParam, Packet): +class SCTPChunkParamHeartbeatInfo(_SCTPChunkParam, Packet): fields_desc = [ShortEnumField("type", 1, sctpchunkparamtypes), FieldLenField("len", None, length_of="data", adjust=lambda pkt, x:x + 4), @@ -335,6 +403,62 @@ class SCTPChunkParamSupportedAddrTypes(_SCTPChunkParam, Packet): 4, padwith=b"\x00"), ] +class SCTPChunkParamOutSSNResetReq(_SCTPChunkParam, Packet): + fields_desc = [ShortEnumField("type", 13, sctpchunkparamtypes), + FieldLenField("len", None, length_of="stream_num_list", + adjust=lambda pkt, x:x + 16), + XIntField("re_conf_req_seq_num", None), + XIntField("re_conf_res_seq_num", None), + XIntField("tsn", None), + PadField(FieldListField("stream_num_list", [], + XShortField("stream_num", None), + length_from=lambda pkt: pkt.len - 16), + 4, padwith=b"\x00"), + ] + + +class SCTPChunkParamInSSNResetReq(_SCTPChunkParam, Packet): + fields_desc = [ShortEnumField("type", 14, sctpchunkparamtypes), + FieldLenField("len", None, length_of="stream_num_list", + adjust=lambda pkt, x:x + 8), + XIntField("re_conf_req_seq_num", None), + PadField(FieldListField("stream_num_list", [], + XShortField("stream_num", None), + length_from=lambda pkt: pkt.len - 8), + 4, padwith=b"\x00"), + ] + + +class SCTPChunkParamSSNTSNResetReq(_SCTPChunkParam, Packet): + fields_desc = [ShortEnumField("type", 15, sctpchunkparamtypes), + XShortField("len", 8), + XIntField("re_conf_req_seq_num", None), + ] + + +class SCTPChunkParamReConfigRes(_SCTPChunkParam, Packet): + fields_desc = [ShortEnumField("type", 16, sctpchunkparamtypes), + XShortField("len", 12), + XIntField("re_conf_res_seq_num", None), + IntEnumField("result", None, resultcode), + XIntField("sender_next_tsn", None), + XIntField("receiver_next_tsn", None), + ] + + +class SCTPChunkParamAddOutgoingStreamReq(_SCTPChunkParam, Packet): + fields_desc = [ShortEnumField("type", 17, sctpchunkparamtypes), + XShortField("len", 12), + XIntField("re_conf_req_seq_num", None), + XShortField("num_new_stream", None), + XShortField("reserved", None), + ] + + +class SCTPChunkParamAddIncomingStreamReq(SCTPChunkParamAddOutgoingStreamReq): + type = 18 + + class SCTPChunkParamECNCapable(_SCTPChunkParam, Packet): fields_desc = [ShortEnumField("type", 0x8000, sctpchunkparamtypes), ShortField("len", 4), ] @@ -396,12 +520,16 @@ class SCTPChunkParamAddIPAddr(_SCTPChunkParam, Packet): ShortEnumField("addr_type", 5, sctpchunkparamtypes), FieldLenField("addr_len", None, length_of="addr", adjust=lambda pkt, x:x + 4), - ConditionalField( - IPField("addr", "127.0.0.1"), - lambda p: p.addr_type == 5), - ConditionalField( - IP6Field("addr", "::1"), - lambda p: p.addr_type == 6), ] + MultipleTypeField( + [ + (IPField("addr", "127.0.0.1"), + lambda p: p.addr_type == 5), + (IP6Field("addr", "::1"), + lambda p: p.addr_type == 6), + ], + StrFixedLenField("addr", "", + length_from=lambda pkt: pkt.addr_len)) + ] class SCTPChunkParamDelIPAddr(SCTPChunkParamAddIPAddr): @@ -499,6 +627,23 @@ class SCTPChunkParamAdaptationLayer(_SCTPChunkParam, Packet): } +class _SCTPChunkDataField(PacketLenField): + """PacketLenField that dispatches using bind_layers bindings.""" + + def m2i(self, pkt, m): + # Only dissect complete messages + if pkt.beginning != 1 or pkt.ending != 1: + return conf.raw_layer(load=m) + # Check bind_layers bindings + for fval, cls in pkt.payload_guess: + if all( + hasattr(pkt, k) and v == pkt.getfieldval(k) + for k, v in fval.items() + ): + return cls(m) + return conf.raw_layer(load=m) + + class SCTPChunkData(_SCTPChunkGuessPayload, Packet): # TODO : add a padding function in post build if this layer is used to generate SCTP chunk data # noqa: E501 fields_desc = [ByteEnumField("type", 0, sctpchunktypes), @@ -512,11 +657,69 @@ class SCTPChunkData(_SCTPChunkGuessPayload, Packet): XShortField("stream_id", None), XShortField("stream_seq", None), IntEnumField("proto_id", None, SCTP_PAYLOAD_PROTOCOL_INDENTIFIERS), # noqa: E501 - PadField(StrLenField("data", None, length_from=lambda pkt: pkt.len - 16), # noqa: E501 + PadField(_SCTPChunkDataField("data", None, conf.raw_layer, + length_from=lambda pkt: pkt.len - 16), # noqa: E501 4, padwith=b"\x00"), ] +class SCTPChunkIData(_SCTPChunkGuessPayload, Packet): + fields_desc = [ByteEnumField("type", 64, sctpchunktypes), + BitField("reserved", None, 4), + BitField("delay_sack", 0, 1), # immediate bit + BitField("unordered", 0, 1), + BitField("beginning", 0, 1), + BitField("ending", 0, 1), + FieldLenField("len", None, length_of="data", + adjust=lambda pkt, x:x + 20), + XIntField("tsn", None), + XShortField("stream_id", None), + XShortField("reserved_16", None), + XIntField("message_id", None), + MultipleTypeField( + [ + (IntEnumField("ppid_fsn", None, + SCTP_PAYLOAD_PROTOCOL_INDENTIFIERS), + lambda pkt: pkt.beginning == 1), + (XIntField("ppid_fsn", None), + lambda pkt: pkt.beginning == 0), + ], + XIntField("ppid_fsn", None)), + PadField(StrLenField("data", None, + length_from=lambda pkt: pkt.len - 20), + 4, padwith=b"\x00"), + ] + + +class SCTPForwardSkip(_SCTPChunkParam, Packet): + fields_desc = [ShortField("stream_id", None), + ShortField("stream_seq", None) + ] + + +class SCTPChunkForwardTSN(_SCTPChunkGuessPayload, Packet): + fields_desc = [ByteEnumField("type", 192, sctpchunktypes), + XByteField("flags", None), + FieldLenField("len", None, length_of="skips", + adjust=lambda pkt, x:x + 8), + IntField("new_tsn", None), + ChunkParamField("skips", None, + length_from=lambda pkt: pkt.len - 8) + ] + + +class SCTPIForwardSkip(_SCTPChunkParam, Packet): + fields_desc = [ShortField("stream_id", None), + BitField("reserved", None, 15), + BitField("unordered", None, 1), + IntField("message_id", None) + ] + + +class SCTPChunkIForwardTSN(SCTPChunkForwardTSN): + type = 194 + + class SCTPChunkInit(_SCTPChunkGuessPayload, Packet): fields_desc = [ByteEnumField("type", 1, sctpchunktypes), XByteField("flags", None), @@ -664,9 +867,31 @@ class SCTPChunkAddressConf(_SCTPChunkGuessPayload, Packet): ] +class SCTPChunkReConfig(_SCTPChunkGuessPayload, Packet): + fields_desc = [ByteEnumField("type", 130, sctpchunktypes), + XByteField("flags", None), + FieldLenField("len", None, length_of="params", + adjust=lambda pkt, x:x + 4), + ChunkParamField("params", None, length_from=lambda pkt: pkt.len - 4), + ] + + +class SCTPChunkPad(_SCTPChunkGuessPayload, Packet): + fields_desc = [ByteEnumField("type", 132, sctpchunktypes), + XByteField("flags", None), + FieldLenField("len", None, length_of="padding", + adjust=lambda pkt, x:x + 8), + PadField(StrLenField("padding", None, + length_from=lambda pkt: pkt.len - 8), + 4, padwith=b"\x00") + ] + + class SCTPChunkAddressConfAck(SCTPChunkAddressConf): type = 0x80 bind_layers(IP, SCTP, proto=IPPROTO_SCTP) +bind_layers(IPerror, SCTPerror, proto=IPPROTO_SCTP) bind_layers(IPv6, SCTP, nh=IPPROTO_SCTP) +bind_layers(IPerror6, SCTPerror, proto=IPPROTO_SCTP) diff --git a/scapy/layers/sixlowpan.py b/scapy/layers/sixlowpan.py index 27a40cb89a3..cdc7d829409 100644 --- a/scapy/layers/sixlowpan.py +++ b/scapy/layers/sixlowpan.py @@ -1,18 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Cesar A. Bernardini # Intern at INRIA Grand Nancy Est -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# Copyright (C) Gabriel Potter """ 6LoWPAN Protocol Stack ====================== This implementation follows the next documents: -- Transmission of IPv6 Packets over IEEE 802.15.4 Networks +- Transmission of IPv6 Packets over IEEE 802.15.4 Networks: RFC 4944 - Compression Format for IPv6 Datagrams in Low Power and Lossy - networks (6LoWPAN): draft-ietf-6lowpan-hc-15 + networks (6LoWPAN): RFC 6282 - RFC 4291 +----------------------------+-----------------------+ @@ -45,25 +45,45 @@ Known Issues: * Unimplemented context information - * Next header compression techniques - * Unimplemented LoWPANBroadcast - + * Unimplemented IPv6 extensions fields """ import socket import struct from scapy.compat import chb, orb, raw - -from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, ByteField, BitEnumField, BitFieldLenField, \ - XShortField, FlagsField, ConditionalField, FieldLenField +from scapy.data import ETHER_TYPES + +from scapy.packet import Packet, bind_layers, bind_top_down +from scapy.fields import ( + BitEnumField, + BitField, + BitLenField, + BitScalingField, + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + MultipleTypeField, + PacketField, + PacketListField, + StrFixedLenField, + XBitField, + XLongField, + XShortField, +) from scapy.layers.dot15d4 import Dot15d4Data -from scapy.layers.inet6 import IPv6, IP6Field +from scapy.layers.inet6 import ( + IP6Field, + IPv6, + _IPv6ExtHdr, + ipv6nh, +) from scapy.layers.inet import UDP +from scapy.layers.l2 import Ether -from scapy.utils import lhex +from scapy.utils import mac2str from scapy.config import conf from scapy.error import warning @@ -71,13 +91,20 @@ from scapy.pton_ntop import inet_pton, inet_ntop from scapy.volatile import RandShort +ETHER_TYPES[0xA0ED] = "6LoWPAN" + LINK_LOCAL_PREFIX = b"\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # noqa: E501 +########## +# Fields # +########## + + class IP6FieldLenField(IP6Field): __slots__ = ["length_of"] - def __init__(self, name, default, size, length_of=None): + def __init__(self, name, default, length_of=None): IP6Field.__init__(self, name, default) self.length_of = length_of @@ -98,96 +125,256 @@ def getfield(self, pkt, s): self.m2i(pkt, b"\x00" * (16 - tmp_len) + s[:tmp_len])) -class BitVarSizeField(BitField): - __slots__ = ["length_f"] - - def __init__(self, name, default, calculate_length=None): - BitField.__init__(self, name, default, 0) - self.length_f = calculate_length +################# +# Basic 6LoWPAN # +################# +# https://tools.ietf.org/html/rfc4944 - def addfield(self, pkt, s, val): - self.size = self.length_f(pkt) - return BitField.addfield(self, pkt, s, val) - def getfield(self, pkt, s): - self.size = self.length_f(pkt) - return BitField.getfield(self, pkt, s) +class LoWPANUncompressedIPv6(Packet): + name = "6LoWPAN Uncompressed IPv6" + fields_desc = [ + BitField("_type", 0x41, 8) + ] + def default_payload_class(self, pay): + return IPv6 -class SixLoWPANAddrField(FieldLenField): - """Special field to store 6LoWPAN addresses +# https://tools.ietf.org/html/rfc4944#section-5.2 - 6LoWPAN Addresses have a variable length depending on other parameters. - This special field allows to save them, and encode/decode no matter which - encoding parameters they have. - """ - def i2repr(self, pkt, x): - return lhex(self.i2h(pkt, x)) +class LoWPANMesh(Packet): + name = "6LoWPAN Mesh Packet" + deprecated_fields = { + "_v": ("v", "2.4.4"), + "_f": ("f", "2.4.4"), + "_sourceAddr": ("src", "2.4.4"), + "_destinyAddr": ("dst", "2.4.4"), + } + fields_desc = [ + BitField("reserved", 0x2, 2), + BitEnumField("v", 0x0, 1, ["EUI-64", "Short"]), + BitEnumField("f", 0x0, 1, ["EUI-64", "Short"]), + BitField("hopsLeft", 0x0, 4), + MultipleTypeField( + [(XShortField("src", 0x0), lambda pkt: pkt.v == 1)], + XLongField("src", 0x0) + ), + MultipleTypeField( + [(XShortField("dst", 0x0), lambda pkt: pkt.v == 1)], + XLongField("dst", 0x0) + ) + ] - def addfield(self, pkt, s, val): - """Add an internal value to a string""" - if self.length_of(pkt) == 8: - return s + struct.pack(self.fmt[0] + "B", val) - if self.length_of(pkt) == 16: - return s + struct.pack(self.fmt[0] + "H", val) - if self.length_of(pkt) == 32: - return s + struct.pack(self.fmt[0] + "2H", val) # TODO: fix! - if self.length_of(pkt) == 48: - return s + struct.pack(self.fmt[0] + "3H", val) # TODO: fix! - elif self.length_of(pkt) == 64: - return s + struct.pack(self.fmt[0] + "Q", val) - elif self.length_of(pkt) == 128: - # TODO: FIX THE PACKING!! - return s + struct.pack(self.fmt[0] + "16s", raw(val)) - else: - return s - def getfield(self, pkt, s): - if self.length_of(pkt) == 8: - return s[1:], self.m2i(pkt, struct.unpack(self.fmt[0] + "B", s[:1])[0]) # noqa: E501 - elif self.length_of(pkt) == 16: - return s[2:], self.m2i(pkt, struct.unpack(self.fmt[0] + "H", s[:2])[0]) # noqa: E501 - elif self.length_of(pkt) == 32: - return s[4:], self.m2i(pkt, struct.unpack(self.fmt[0] + "2H", s[:2], s[2:4])[0]) # noqa: E501 - elif self.length_of(pkt) == 48: - return s[6:], self.m2i(pkt, struct.unpack(self.fmt[0] + "3H", s[:2], s[2:4], s[4:6])[0]) # noqa: E501 - elif self.length_of(pkt) == 64: - return s[8:], self.m2i(pkt, struct.unpack(self.fmt[0] + "Q", s[:8])[0]) # noqa: E501 - elif self.length_of(pkt) == 128: - return s[16:], self.m2i(pkt, struct.unpack(self.fmt[0] + "16s", s[:16])[0]) # noqa: E501 +# https://tools.ietf.org/html/rfc4944#section-10.1 +# This implementation is NOT RECOMMENDED according to RFC 6282 -class LoWPANUncompressedIPv6(Packet): - name = "6LoWPAN Uncompressed IPv6" +class LoWPAN_HC2_UDP(Packet): + name = "6LoWPAN HC1 UDP encoding" fields_desc = [ - BitField("_type", 0x0, 8) + BitEnumField("sc", 0, 1, ["In-line", "Compressed"]), + BitEnumField("dc", 0, 1, ["In-line", "Compressed"]), + BitEnumField("lc", 0, 1, ["In-line", "Compressed"]), + BitField("res", 0, 5), ] - def default_payload_class(self, pay): - return IPv6 + def default_payload_class(self, payload): + return conf.padding_layer -class LoWPANMesh(Packet): - name = "6LoWPAN Mesh Packet" +def _get_hc1_pad(pkt): + """ + Get LoWPAN_HC1 padding + + LoWPAN_HC1 is not recommended for several reasons, one + of them being that padding is a mess (not 8-bit regular) + We therefore add padding bits that are not in the spec to restore + 8-bit parity. Wireshark seems to agree + """ + length = 0 # in bits, of the fields that are not //8 + if not pkt.tc_fl: + length += 20 + if pkt.hc2: + if pkt.nh == 1: + length += pkt.hc2Field.sc * 4 + length += pkt.hc2Field.dc * 4 + return (-length) % 8 + + +class LoWPAN_HC1(Packet): + name = "LoWPAN_HC1 Compressed IPv6" fields_desc = [ - BitField("reserved", 0x2, 2), - BitEnumField("_v", 0x0, 1, [False, True]), - BitEnumField("_f", 0x0, 1, [False, True]), - BitField("_hopsLeft", 0x0, 4), - SixLoWPANAddrField("_sourceAddr", 0x0, length_of=lambda pkt: pkt._v and 2 or 8), # noqa: E501 - SixLoWPANAddrField("_destinyAddr", 0x0, length_of=lambda pkt: pkt._f and 2 or 8), # noqa: E501 + # https://tools.ietf.org/html/rfc4944#section-10.1 + ByteField("reserved", 0x42), + BitEnumField("sp", 0, 1, ["In-line", "Compressed"]), + BitEnumField("si", 0, 1, ["In-line", "Elided"]), + BitEnumField("dp", 0, 1, ["In-line", "Compressed"]), + BitEnumField("di", 0, 1, ["In-line", "Elided"]), + BitEnumField("tc_fl", 0, 1, ["Not compressed", "zero"]), + BitEnumField("nh", 0, 2, {0: "not compressed", + 1: "UDP", + 2: "ICMP", + 3: "TCP"}), + BitEnumField("hc2", 0, 1, ["No more header compression bits", + "HC2 Present"]), + # https://tools.ietf.org/html/rfc4944#section-10.2 + ConditionalField( + MultipleTypeField( + [ + (PacketField("hc2Field", LoWPAN_HC2_UDP(), + LoWPAN_HC2_UDP), + lambda pkt: pkt.nh == 1), + # TODO: ICMP & TCP not implemented yet for HC1 + # (PacketField("hc2Field", LoWPAN_HC2_ICMP(), + # LoWPAN_HC2_ICMP), + # lambda pkt: pkt.nh == 2), + # (PacketField("hc2Field", LoWPAN_HC2_TCP(), + # LoWPAN_HC2_TCP), + # lambda pkt: pkt.nh == 3), + ], + StrFixedLenField("hc2Field", b"", 0), + ), + lambda pkt: pkt.hc2 + ), + # IPv6 header fields + # https://tools.ietf.org/html/rfc4944#section-10.3.1 + ByteField("hopLimit", 0x0), + IP6FieldLenField("src", "::", + lambda pkt: (0 if pkt.sp else 8) + + (0 if pkt.si else 8)), + IP6FieldLenField("dst", "::", + lambda pkt: (0 if pkt.dp else 8) + + (0 if pkt.di else 8)), + ConditionalField( + ByteField("traffic_class", 0), + lambda pkt: not pkt.tc_fl + ), + ConditionalField( + BitField("flow_label", 0, 20), + lambda pkt: not pkt.tc_fl + ), + # Other fields + # https://tools.ietf.org/html/rfc4944#section-10.3.2 + ConditionalField( + MultipleTypeField( + [(BitScalingField("udpSourcePort", 0, 4, offset=0xF0B0), + lambda pkt: getattr(pkt.hc2Field, "sc", 0))], + BitField("udpSourcePort", 0, 16) + ), + lambda pkt: pkt.nh == 1 and pkt.hc2 + ), + ConditionalField( + MultipleTypeField( + [(BitScalingField("udpDestPort", 0, 4, offset=0xF0B0), + lambda pkt: getattr(pkt.hc2Field, "dc", 0))], + BitField("udpDestPort", 0, 16) + ), + lambda pkt: pkt.nh == 1 and pkt.hc2 + ), + ConditionalField( + BitField("udpLength", 0, 16), + lambda pkt: pkt.nh == 1 and pkt.hc2 and not pkt.hc2Field.lc + ), + ConditionalField( + XBitField("udpChecksum", 0, 16), + lambda pkt: pkt.nh == 1 and pkt.hc2 + ), + # Out of spec + BitLenField("pad", 0, _get_hc1_pad) ] - def guess_payload_class(self, payload): - # check first 2 bytes if they are ZERO it's not a 6LoWPAN packet - pass + def post_dissect(self, data): + # uncompress payload + packet = IPv6() + packet.version = IPHC_DEFAULT_VERSION + packet.tc = self.traffic_class + packet.fl = self.flow_label + nh_match = { + 1: socket.IPPROTO_UDP, + 2: socket.IPPROTO_ICMP, + 3: socket.IPPROTO_TCP + } + if self.nh: + packet.nh = nh_match.get(self.nh) + packet.hlim = self.hopLimit + + packet.src = self.decompressSourceAddr() + packet.dst = self.decompressDestAddr() + + if self.hc2 and self.nh == 1: # UDP + udp = UDP() + udp.sport = self.udpSourcePort + udp.dport = self.udpDestPort + udp.len = self.udpLength or None + udp.chksum = self.udpChecksum + udp.add_payload(data) + packet.add_payload(udp) + else: + packet.add_payload(data) + data = raw(packet) + return Packet.post_dissect(self, data) + + def decompressSourceAddr(self): + if not self.sp and not self.si: + # Prefix & Interface + return self.src + elif not self.si: + # Only interface + addr = inet_pton(socket.AF_INET6, self.src)[-8:] + addr = LINK_LOCAL_PREFIX[:8] + addr + else: + # Interface not provided + addr = _extract_upperaddress(self, source=True) + self.src = inet_ntop(socket.AF_INET6, addr) + return self.src + + def decompressDestAddr(self): + if not self.dp and not self.di: + # Prefix & Interface + return self.dst + elif not self.di: + # Only interface + addr = inet_pton(socket.AF_INET6, self.dst)[-8:] + addr = LINK_LOCAL_PREFIX[:8] + addr + else: + # Interface not provided + addr = _extract_upperaddress(self, source=False) + self.dst = inet_ntop(socket.AF_INET6, addr) + return self.dst -############################################################################### -# Fragmentation -# -# Section 5.3 - September 2007 -############################################################################### + def do_build(self): + if not isinstance(self.payload, IPv6): + return Packet.do_build(self) + # IPv6 + ipv6 = self.payload + self.src = ipv6.src + self.dst = ipv6.dst + self.flow_label = ipv6.fl + self.traffic_class = ipv6.tc + self.hopLimit = ipv6.hlim + if isinstance(ipv6.payload, UDP): + self.nh = 1 + self.hc2 = 1 + udp = ipv6.payload + self.udpSourcePort = udp.sport + self.udpDestPort = udp.dport + if not udp.len or not udp.chksum: + udp = UDP(raw(udp)) + self.udpLength = udp.len + self.udpChecksum = udp.chksum + return Packet.do_build(self) + + def do_build_payload(self): + # Elide the IPv6 and UDP payload + if isinstance(self.payload, IPv6): + if isinstance(self.payload.payload, UDP): + return raw(self.payload.payload.payload) + return raw(self.payload.payload) + return Packet.do_build_payload(self) + +# https://tools.ietf.org/html/rfc4944#section-5.3 class LoWPANFragmentationFirst(Packet): @@ -209,13 +396,28 @@ class LoWPANFragmentationSubsequent(Packet): ] +# https://tools.ietf.org/html/rfc4944#section-11.1 + +class LoWPANBroadcast(Packet): + name = "6LoWPAN Broadcast" + fields_desc = [ + ByteField("reserved", 0x50), + ByteField("seq", 0) + ] + + +######################### +# LoWPAN_IPHC (RFC6282) # +######################### + + IPHC_DEFAULT_VERSION = 6 IPHC_DEFAULT_TF = 0 IPHC_DEFAULT_FL = 0 -def source_addr_mode2(pkt): - """source_addr_mode +def source_addr_size(pkt): + """Source address size This function depending on the arguments returns the amount of bits to be used by the source address. @@ -243,11 +445,11 @@ def source_addr_mode2(pkt): return 0 -def destiny_addr_mode(pkt): - """destiny_addr_mode +def dest_addr_size(pkt): + """Destination address size This function depending on the arguments returns the amount of bits to be - used by the destiny address. + used by the destination address. Keyword arguments: pkt -- packet object instance @@ -263,7 +465,8 @@ def destiny_addr_mode(pkt): return 0 elif pkt.m == 0 and pkt.dac == 1: if pkt.dam == 0x0: - raise Exception('reserved') + # reserved + return 0 elif pkt.dam == 0x1: return 8 elif pkt.dam == 0x2: @@ -283,163 +486,140 @@ def destiny_addr_mode(pkt): if pkt.dam == 0x0: return 6 elif pkt.dam == 0x1: - raise Exception('reserved') + # reserved + return 0 elif pkt.dam == 0x2: - raise Exception('reserved') + # reserved + return 0 elif pkt.dam == 0x3: - raise Exception('reserved') - - -def nhc_port(pkt): - if not pkt.nh: - return 0, 0 - if pkt.header_compression & 0x3 == 0x3: - return 4, 4 - elif pkt.header_compression & 0x2 == 0x2: - return 8, 16 - elif pkt.header_compression & 0x1 == 0x1: - return 16, 8 - else: - return 16, 16 - - -def pad_trafficclass(pkt): - """ - This function depending on the arguments returns the amount of bits to be - used by the padding of the traffic class. - - Keyword arguments: - pkt -- packet object instance - """ - if pkt.tf == 0x0: - return 4 - elif pkt.tf == 0x1: - return 2 - elif pkt.tf == 0x2: - return 0 - else: - return 0 - - -def flowlabel_len(pkt): - """ - This function depending on the arguments returns the amount of bits to be - used by the padding of the traffic class. - - Keyword arguments: - pkt -- packet object instance - """ - if pkt.tf == 0x0: - return 20 - elif pkt.tf == 0x1: - return 20 - else: - return 0 - - -def _tf_last_attempt(pkt): - if pkt.tf == 0: - return 2, 6, 4, 20 - elif pkt.tf == 1: - return 2, 0, 2, 20 - elif pkt.tf == 2: - return 2, 6, 0, 0 - else: - return 0, 0, 0, 0 + # reserved + return 0 -def _extract_dot15d4address(pkt, source=True): +def _extract_upperaddress(pkt, source=True): """This function extracts the source/destination address of a 6LoWPAN - from its upper Dot15d4Data (802.15.4 data) layer. + from its upper layer. + + (Upper layer could be 802.15.4 data, Ethernet...) params: - source: if True, the address is the source one. Otherwise, it is the destination. - returns: the packed & processed address + returns: (upper_address, ipv6_address) """ + # https://tools.ietf.org/html/rfc6282#section-3.2.2 + SUPPORTED_LAYERS = (Ether, Dot15d4Data) underlayer = pkt.underlayer - while underlayer is not None and not isinstance(underlayer, Dot15d4Data): # noqa: E501 + while underlayer and not isinstance(underlayer, SUPPORTED_LAYERS): underlayer = underlayer.underlayer - if type(underlayer) == Dot15d4Data: - addr = underlayer.src_addr if source else underlayer.dest_addr - if underlayer.underlayer.fcf_destaddrmode == 3: - tmp_ip = LINK_LOCAL_PREFIX[0:8] + struct.pack(">Q", addr) # noqa: E501 + # Extract and process address + if type(underlayer) == Ether: + addr = mac2str(underlayer.src if source else underlayer.dst) + # https://tools.ietf.org/html/rfc2464#section-4 + return LINK_LOCAL_PREFIX[:8] + addr[:3] + b"\xff\xfe" + addr[3:] + elif type(underlayer) == Dot15d4Data: + if source: + addr = underlayer.src_addr + addrmode = underlayer.underlayer.fcf_srcaddrmode + else: + addr = underlayer.dest_addr + addrmode = underlayer.underlayer.fcf_destaddrmode + addr = struct.pack(">Q", addr) + if addrmode == 3: # Extended/long + tmp_ip = LINK_LOCAL_PREFIX[0:8] + addr # Turn off the bit 7. - tmp_ip = tmp_ip[0:8] + struct.pack("B", (orb(tmp_ip[8]) ^ 0x2)) + tmp_ip[9:16] # noqa: E501 - elif underlayer.underlayer.fcf_destaddrmode == 2: - tmp_ip = LINK_LOCAL_PREFIX[0:8] + \ - b"\x00\x00\x00\xff\xfe\x00" + \ - struct.pack(">Q", addr)[6:] - return tmp_ip + return tmp_ip[0:8] + struct.pack("B", (orb(tmp_ip[8]) ^ 0x2)) + tmp_ip[9:16] # noqa: E501 + elif addrmode == 2: # Short + return ( + LINK_LOCAL_PREFIX[0:8] + + b"\x00\x00\x00\xff\xfe\x00" + + addr[6:] + ) else: - # Most of the times, it's necessary the IEEE 802.15.4 data to extract this address # noqa: E501 - raise Exception('Unimplemented: IP Header is contained into IEEE 802.15.4 frame, in this case it\'s not available.') # noqa: E501 + # Most of the times, it's necessary the IEEE 802.15.4 data to extract + # this address, sometimes another layer. + warning( + 'Unimplemented: Unsupported upper layer: %s' % type(underlayer) + ) + return b"\x00" * 16 class LoWPAN_IPHC(Packet): """6LoWPAN IPv6 header compressed packets - It follows the implementation of draft-ietf-6lowpan-hc-15. + It follows the implementation of RFC6282 """ + __slots__ = ["_ipv6"] # the LOWPAN_IPHC encoding utilizes 13 bits, 5 dispatch type name = "LoWPAN IP Header Compression Packet" - _address_modes = ["Unspecified", "1", "16-bits inline", "Compressed"] - _state_mode = ["Stateless", "Stateful"] + _address_modes = ["Unspecified (0)", "1", "16-bits inline (3)", + "Compressed (3)"] + _state_mode = ["Stateless (0)", "Stateful (1)"] + deprecated_fields = { + "_nhField": ("nhField", "2.4.4"), + "_hopLimit": ("hopLimit", "2.4.4"), + "sourceAddr": ("src", "2.4.4"), + "destinyAddr": ("dst", "2.4.4"), + "udpDestinyPort": ("udpDestPort", "2.4.4"), + } fields_desc = [ - # dispatch + # Base Format https://tools.ietf.org/html/rfc6282#section-3.1.2 BitField("_reserved", 0x03, 3), BitField("tf", 0x0, 2), BitEnumField("nh", 0x0, 1, ["Inline", "Compressed"]), - BitField("hlim", 0x0, 2), - BitEnumField("cid", 0x0, 1, [False, True]), + BitEnumField("hlim", 0x0, 2, {0: "Inline", + 1: "Compressed/HL1", + 2: "Compressed/HL64", + 3: "Compressed/HL255"}), + BitEnumField("cid", 0x0, 1, {1: "Present (1)"}), BitEnumField("sac", 0x0, 1, _state_mode), BitEnumField("sam", 0x0, 2, _address_modes), - BitEnumField("m", 0x0, 1, [False, True]), + BitEnumField("m", 0x0, 1, {1: "multicast (1)"}), BitEnumField("dac", 0x0, 1, _state_mode), BitEnumField("dam", 0x0, 2, _address_modes), + # https://tools.ietf.org/html/rfc6282#section-3.1.2 + # Context Identifier Extension ConditionalField( - ByteField("_contextIdentifierExtension", 0x0), + BitField("sci", 0, 4), lambda pkt: pkt.cid == 0x1 ), - # TODO: THIS IS WRONG!!!!! - BitVarSizeField("tc_ecn", 0, calculate_length=lambda pkt: _tf_last_attempt(pkt)[0]), # noqa: E501 - BitVarSizeField("tc_dscp", 0, calculate_length=lambda pkt: _tf_last_attempt(pkt)[1]), # noqa: E501 - BitVarSizeField("_padd", 0, calculate_length=lambda pkt: _tf_last_attempt(pkt)[2]), # noqa: E501 - BitVarSizeField("flowlabel", 0, calculate_length=lambda pkt: _tf_last_attempt(pkt)[3]), # noqa: E501 - - # NH ConditionalField( - ByteField("_nhField", 0x0), - lambda pkt: not pkt.nh + BitField("dci", 0, 4), + lambda pkt: pkt.cid == 0x1 ), - # HLIM: Hop Limit: if it's 0 + # https://tools.ietf.org/html/rfc6282#section-3.2.1 ConditionalField( - ByteField("_hopLimit", 0x0), - lambda pkt: pkt.hlim == 0x0 + BitField("tc_ecn", 0, 2), + lambda pkt: pkt.tf in [0, 1, 2] ), - IP6FieldLenField("sourceAddr", "::", 0, length_of=source_addr_mode2), - IP6FieldLenField("destinyAddr", "::", 0, length_of=destiny_addr_mode), # problem when it's 0 # noqa: E501 - - # LoWPAN_UDP Header Compression ######################################## # noqa: E501 - # TODO: IMPROVE!!!!! ConditionalField( - FlagsField("header_compression", 0, 8, ["A", "B", "C", "D", "E", "C", "PS", "PD"]), # noqa: E501 - lambda pkt: pkt.nh + BitField("tc_dscp", 0, 6), + lambda pkt: pkt.tf in [0, 2], ), ConditionalField( - BitFieldLenField("udpSourcePort", 0x0, 16, length_of=lambda pkt: nhc_port(pkt)[0]), # noqa: E501 - # ShortField("udpSourcePort", 0x0), - lambda pkt: pkt.nh and pkt.header_compression & 0x2 == 0x0 + MultipleTypeField( + [(BitField("rsv", 0, 4), lambda pkt: pkt.tf == 0)], + BitField("rsv", 0, 2), + ), + lambda pkt: pkt.tf in [0, 1] ), ConditionalField( - BitFieldLenField("udpDestinyPort", 0x0, 16, length_of=lambda pkt: nhc_port(pkt)[1]), # noqa: E501 - lambda pkt: pkt.nh and pkt.header_compression & 0x1 == 0x0 + BitField("flowlabel", 0, 20), + lambda pkt: pkt.tf in [0, 1] ), + # Inline fields https://tools.ietf.org/html/rfc6282#section-3.1.1 ConditionalField( - XShortField("udpChecksum", 0x0), - lambda pkt: pkt.nh and pkt.header_compression & 0x4 == 0x0 + ByteEnumField("nhField", 0x0, ipv6nh), + lambda pkt: pkt.nh == 0x0 ), - + ConditionalField( + ByteField("hopLimit", 0x0), + lambda pkt: pkt.hlim == 0x0 + ), + # The src and dst fields are filled up or removed in the + # pre_dissect and post_build, depending on the other options. + IP6FieldLenField("src", "::", length_of=source_addr_size), + IP6FieldLenField("dst", "::", length_of=dest_addr_size), # problem when it's 0 # noqa: E501 ] def post_dissect(self, data): @@ -451,108 +631,89 @@ def post_dissect(self, data): # uncompress payload packet = IPv6() - packet.version = IPHC_DEFAULT_VERSION packet.tc, packet.fl = self._getTrafficClassAndFlowLabel() if not self.nh: - packet.nh = self._nhField + packet.nh = self.nhField # HLIM: Hop Limit if self.hlim == 0: - packet.hlim = self._hopLimit + packet.hlim = self.hopLimit elif self.hlim == 0x1: packet.hlim = 1 elif self.hlim == 0x2: packet.hlim = 64 else: packet.hlim = 255 - # TODO: Payload length can be inferred from lower layers from either the # noqa: E501 - # 6LoWPAN Fragmentation header or the IEEE802.15.4 header packet.src = self.decompressSourceAddr(packet) - packet.dst = self.decompressDestinyAddr(packet) + packet.dst = self.decompressDestAddr(packet) - if self.nh == 1: - # The Next Header field is compressed and the next header is - # encoded using LOWPAN_NHC - - packet.nh = 0x11 # UDP - udp = UDP() - if self.header_compression and \ - self.header_compression & 0x4 == 0x0: - udp.chksum = self.udpChecksum - - s, d = nhc_port(self) - if s == 16: - udp.sport = self.udpSourcePort - elif s == 8: - udp.sport = 0xF000 + s - elif s == 4: - udp.sport = 0xF0B0 + s - if d == 16: - udp.dport = self.udpDestinyPort - elif d == 8: - udp.dport = 0xF000 + d - elif d == 4: - udp.dport = 0xF0B0 + d - - packet.payload = udp / data - data = raw(packet) - # else self.nh == 0 not necessary - elif self._nhField & 0xE0 == 0xE0: # IPv6 Extension Header Decompression # noqa: E501 - warning('Unimplemented: IPv6 Extension Header decompression') # noqa: E501 - packet.payload = conf.raw_layer(data) - data = raw(packet) - else: - packet.payload = conf.raw_layer(data) + pay_cls = self.guess_payload_class(data) + if pay_cls == IPv6: + packet.add_payload(data) data = raw(packet) - + elif pay_cls == LoWPAN_NHC: + self._ipv6 = packet return Packet.post_dissect(self, data) - def decompressDestinyAddr(self, packet): + def decompressDestAddr(self, packet): + # https://tools.ietf.org/html/rfc6282#section-3.1.1 try: - tmp_ip = inet_pton(socket.AF_INET6, self.destinyAddr) + tmp_ip = inet_pton(socket.AF_INET6, self.dst) except socket.error: tmp_ip = b"\x00" * 16 if self.m == 0 and self.dac == 0: if self.dam == 0: + # Address fully carried pass elif self.dam == 1: tmp_ip = LINK_LOCAL_PREFIX[0:8] + tmp_ip[-8:] elif self.dam == 2: tmp_ip = LINK_LOCAL_PREFIX[0:8] + b"\x00\x00\x00\xff\xfe\x00" + tmp_ip[-2:] # noqa: E501 elif self.dam == 3: - # TODO May need some extra changes, we are copying - # (self.m == 0 and self.dac == 1) - tmp_ip = _extract_dot15d4address(self, source=False) + tmp_ip = _extract_upperaddress(self, source=False) elif self.m == 0 and self.dac == 1: if self.dam == 0: - raise Exception('Reserved') + # reserved + pass elif self.dam == 0x3: - tmp_ip = _extract_dot15d4address(self, source=False) + # should use context IID + encapsulating header + tmp_ip = _extract_upperaddress(self, source=False) elif self.dam not in [0x1, 0x2]: - warning("Unknown destiny address compression mode !") + # https://tools.ietf.org/html/rfc6282#page-9 + # Should use context information: unimplemented + pass elif self.m == 1 and self.dac == 0: if self.dam == 0: - raise Exception("unimplemented") + # Address fully carried + pass elif self.dam == 1: - tmp = b"\xff" + chb(tmp_ip[16 - destiny_addr_mode(self)]) + tmp = b"\xff" + chb(tmp_ip[16 - dest_addr_size(self)]) tmp_ip = tmp + b"\x00" * 9 + tmp_ip[-5:] elif self.dam == 2: - tmp = b"\xff" + chb(tmp_ip[16 - destiny_addr_mode(self)]) + tmp = b"\xff" + chb(tmp_ip[16 - dest_addr_size(self)]) tmp_ip = tmp + b"\x00" * 11 + tmp_ip[-3:] else: # self.dam == 3: tmp_ip = b"\xff\x02" + b"\x00" * 13 + tmp_ip[-1:] elif self.m == 1 and self.dac == 1: if self.dam == 0x0: - raise Exception("Unimplemented: I didn't understand the 6lowpan specification") # noqa: E501 - else: # all the others values - raise Exception("Reserved value by specification.") + # https://tools.ietf.org/html/rfc6282#page-10 + # https://github.com/wireshark/wireshark/blob/f54611d1104d85a425e52c7318c522ed249916b6/epan/dissectors/packet-6lowpan.c#L2149-L2166 + # Format: ffXX:XXLL:PPPP:PPPP:PPPP:PPPP:XXXX:XXXX + # P and L should be retrieved from context + P = b"\x00" * 16 + L = b"\x00" + X = tmp_ip[-6:] + tmp_ip = b"\xff" + X[:2] + L + P[:8] + X[2:6] + else: # all the others values: reserved + pass - self.destinyAddr = inet_ntop(socket.AF_INET6, tmp_ip) - return self.destinyAddr + self.dst = inet_ntop(socket.AF_INET6, tmp_ip) + return self.dst def compressSourceAddr(self, ipv6): + # https://tools.ietf.org/html/rfc6282#section-3.1.1 tmp_ip = inet_pton(socket.AF_INET6, ipv6.src) if self.sac == 0: @@ -572,10 +733,11 @@ def compressSourceAddr(self, ipv6): elif self.sam == 0x2: tmp_ip = tmp_ip[14:16] - self.sourceAddr = inet_ntop(socket.AF_INET6, b"\x00" * (16 - len(tmp_ip)) + tmp_ip) # noqa: E501 - return self.sourceAddr + self.src = inet_ntop(socket.AF_INET6, b"\x00" * (16 - len(tmp_ip)) + tmp_ip) # noqa: E501 + return self.src - def compressDestinyAddr(self, ipv6): + def compressDestAddr(self, ipv6): + # https://tools.ietf.org/html/rfc6282#section-3.1.1 tmp_ip = inet_pton(socket.AF_INET6, ipv6.dst) if self.m == 0 and self.dac == 0: @@ -591,6 +753,8 @@ def compressDestinyAddr(self, ipv6): elif self.dam == 0x2: tmp_ip = b"\x00" * 14 + tmp_ip[14:16] elif self.m == 1 and self.dac == 0: + if self.dam == 0x0: + pass if self.dam == 0x1: tmp_ip = b"\x00" * 10 + tmp_ip[1:2] + tmp_ip[11:16] elif self.dam == 0x2: @@ -598,51 +762,63 @@ def compressDestinyAddr(self, ipv6): elif self.dam == 0x3: tmp_ip = b"\x00" * 15 + tmp_ip[15:16] elif self.m == 1 and self.dac == 1: - raise Exception('Unimplemented') + if self.dam == 0: + tmp_ip = b"\x00" * 10 + tmp_ip[1:3] + tmp_ip[12:16] - self.destinyAddr = inet_ntop(socket.AF_INET6, tmp_ip) + self.dst = inet_ntop(socket.AF_INET6, tmp_ip) def decompressSourceAddr(self, packet): + # https://tools.ietf.org/html/rfc6282#section-3.1.1 try: - tmp_ip = inet_pton(socket.AF_INET6, self.sourceAddr) + tmp_ip = inet_pton(socket.AF_INET6, self.src) except socket.error: tmp_ip = b"\x00" * 16 if self.sac == 0: if self.sam == 0x0: + # Full address is carried in-line pass elif self.sam == 0x1: - tmp_ip = LINK_LOCAL_PREFIX[0:8] + tmp_ip[16 - source_addr_mode2(self):16] # noqa: E501 + tmp_ip = LINK_LOCAL_PREFIX[0:8] + tmp_ip[16 - source_addr_size(self):16] # noqa: E501 elif self.sam == 0x2: tmp = LINK_LOCAL_PREFIX[0:8] + b"\x00\x00\x00\xff\xfe\x00" - tmp_ip = tmp + tmp_ip[16 - source_addr_mode2(self):16] - elif self.sam == 0x3: # EXTRACT ADDRESS FROM Dot15d4 - tmp_ip = _extract_dot15d4address(self, source=True) - else: - warning("Unknown source address compression mode !") + tmp_ip = tmp + tmp_ip[16 - source_addr_size(self):16] + elif self.sam == 0x3: + # Taken from encapsulating header + tmp_ip = _extract_upperaddress(self, source=True) else: # self.sac == 1: if self.sam == 0x0: + # Unspecified address :: + pass + elif self.sam == 0x1: + # should use context IID pass elif self.sam == 0x2: - # TODO: take context IID + # should use context IID tmp = LINK_LOCAL_PREFIX[0:8] + b"\x00\x00\x00\xff\xfe\x00" - tmp_ip = tmp + tmp_ip[16 - source_addr_mode2(self):16] + tmp_ip = tmp + tmp_ip[16 - source_addr_size(self):16] elif self.sam == 0x3: - tmp_ip = LINK_LOCAL_PREFIX[0:8] + b"\x00" * 8 # TODO: CONTEXT ID # noqa: E501 - else: - raise Exception('Unimplemented') - self.sourceAddr = inet_ntop(socket.AF_INET6, tmp_ip) - return self.sourceAddr + # should use context IID + tmp_ip = LINK_LOCAL_PREFIX[0:8] + b"\x00" * 8 + self.src = inet_ntop(socket.AF_INET6, tmp_ip) + return self.src def guess_payload_class(self, payload): - if self.underlayer and isinstance(self.underlayer, (LoWPANFragmentationFirst, LoWPANFragmentationSubsequent)): # noqa: E501 + if self.nh: + return LoWPAN_NHC + u = self.underlayer + if u and isinstance(u, (LoWPANFragmentationFirst, + LoWPANFragmentationSubsequent)): return Raw return IPv6 def do_build(self): - if not isinstance(self.payload, IPv6): + _cur = self + if isinstance(_cur.payload, LoWPAN_NHC): + _cur = _cur.payload + if not isinstance(_cur.payload, IPv6): return Packet.do_build(self) - ipv6 = self.payload + ipv6 = _cur.payload self._reserved = 0x03 @@ -665,15 +841,14 @@ def do_build(self): # 2. Next Header if self.nh == 0x0: - self.nh = 0 # ipv6.nh - elif self.nh == 0x1: - self.nh = 0 # disable compression - # The Next Header field is compressed and the next header is encoded using LOWPAN_NHC, which is discussed in Section 4.1. # noqa: E501 - warning('Next header compression is not implemented yet ! Will be ignored') # noqa: E501 + self.nhField = ipv6.nh + elif self.nh == 1: + # This will be handled in LoWPAN_NHC + pass # 3. HLim if self.hlim == 0x0: - self._hopLimit = ipv6.hlim + self.hopLimit = ipv6.hlim else: # if hlim is 1, 2 or 3, there are nothing to do! pass @@ -681,21 +856,20 @@ def do_build(self): if self.cid == 0x0: pass else: - # TODO: Context Unimplemented yet in my class - self._contextIdentifierExtension = 0 + # TODO: Context Unimplemented yet + pass # 5. Compress Source Addr self.compressSourceAddr(ipv6) - self.compressDestinyAddr(ipv6) + self.compressDestAddr(ipv6) return Packet.do_build(self) def do_build_payload(self): - if self.header_compression and\ - self.header_compression & 240 == 240: # TODO: UDP header IMPROVE - return raw(self.payload)[40 + 16:] - else: - return raw(self.payload)[40:] + # Elide the IPv6 payload + if isinstance(self.payload, IPv6): + return raw(self.payload.payload) + return Packet.do_build_payload(self) def _getTrafficClassAndFlowLabel(self): """Page 6, draft feb 2011 """ @@ -708,35 +882,250 @@ def _getTrafficClassAndFlowLabel(self): else: return 0, 0 -# Old compression (deprecated) +############## +# LOWPAN_NHC # +############## + +# https://tools.ietf.org/html/rfc6282#section-4 + +class LoWPAN_NHC_Hdr(Packet): + @classmethod + def get_next_cls(cls, s): + if s and len(s) >= 2: + fb = ord(s[:1]) + if fb >> 3 == 0x1e: + return LoWPAN_NHC_UDP + if fb >> 4 == 0xe: + return LoWPAN_NHC_IPv6Ext + return None + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kargs): + return LoWPAN_NHC_Hdr.get_next_cls(_pkt) or LoWPAN_NHC_Hdr -class LoWPAN_HC1(Raw): - name = "LoWPAN_HC1 Compressed IPv6 (Not supported)" + def extract_padding(self, s): + return b"", s + + +class LoWPAN_NHC_UDP(LoWPAN_NHC_Hdr): + fields_desc = [ + BitField("res", 0x1e, 5), + BitField("C", 0, 1), + BitField("P", 0, 2), + MultipleTypeField( + [(BitField("udpSourcePort", 0, 16), + lambda pkt: pkt.P in [0, 1]), + (BitField("udpSourcePort", 0, 8), + lambda pkt: pkt.P == 2), + (BitField("udpSourcePort", 0, 4), + lambda pkt: pkt.P == 3)], + BitField("udpSourcePort", 0x0, 16), + ), + MultipleTypeField( + [(BitField("udpDestPort", 0, 16), + lambda pkt: pkt.P in [0, 2]), + (BitField("udpDestPort", 0, 8), + lambda pkt: pkt.P == 1), + (BitField("udpDestPort", 0, 4), + lambda pkt: pkt.P == 3)], + BitField("udpDestPort", 0x0, 16), + ), + ConditionalField( + XShortField("udpChecksum", 0x0), + lambda pkt: pkt.C == 0 + ), + ] + + +_lowpan_nhc_ipv6ext_eid = { + 0: "Hop-by-hop Options Header", + 1: "IPv6 Routing Header", + 2: "IPv6 Fragment Header", + 3: "IPv6 Destination Options Header", + 4: "IPv6 Mobility Header", + 7: "IPv6 Header", +} + + +class LoWPAN_NHC_IPv6Ext(LoWPAN_NHC_Hdr): + fields_desc = [ + BitField("res", 0xe, 4), + BitEnumField("eid", 0, 3, _lowpan_nhc_ipv6ext_eid), + BitField("nh", 0, 1), + ConditionalField( + ByteField("nhField", 0), + lambda pkt: pkt.nh == 0 + ), + FieldLenField("len", None, length_of="data", fmt="B"), + StrFixedLenField("data", b"", length_from=lambda pkt: pkt.len) + ] + + def post_build(self, p, pay): + if self.len is None: + offs = (not self.nh) + 1 + p = p[:offs] + struct.pack("!B", len(p) - offs) + p[offs + 1:] + return p + pay + + +class LoWPAN_NHC(Packet): + name = "LOWPAN_NHC" + fields_desc = [ + PacketListField( + "exts", [], pkt_cls=LoWPAN_NHC_Hdr, + next_cls_cb=lambda *s: LoWPAN_NHC_Hdr.get_next_cls(s[3]) + ) + ] + + def post_dissect(self, data): + if not self.underlayer or not hasattr(self.underlayer, "_ipv6"): + return data + if self.guess_payload_class(data) != IPv6: + return data + # Underlayer is LoWPAN_IPHC + packet = self.underlayer._ipv6 + try: + ipv6_hdr = next( + x for x in self.exts if isinstance(x, LoWPAN_NHC_IPv6Ext) + ) + except StopIteration: + ipv6_hdr = None + if ipv6_hdr: + # XXX todo: implement: append the IPv6 extension + # packet = packet / ipv6extension + pass + try: + udp_hdr = next( + x for x in self.exts if isinstance(x, LoWPAN_NHC_UDP) + ) + except StopIteration: + udp_hdr = None + if udp_hdr: + packet.nh = 0x11 # UDP + udp = UDP() + # https://tools.ietf.org/html/rfc6282#section-4.3.3 + if udp_hdr.C == 0: + udp.chksum = udp_hdr.udpChecksum + if udp_hdr.P == 0: + udp.sport = udp_hdr.udpSourcePort + udp.dport = udp_hdr.udpDestPort + elif udp_hdr.P == 1: + udp.sport = udp_hdr.udpSourcePort + udp.dport = 0xF000 + udp_hdr.udpDestPort + elif udp_hdr.P == 2: + udp.sport = 0xF000 + udp_hdr.udpSourcePort + udp.dport = udp_hdr.udpDestPort + elif udp_hdr.P == 3: + udp.sport = 0xF0B0 + udp_hdr.udpSourcePort + udp.dport = 0xF0B0 + udp_hdr.udpDestPort + packet.lastlayer().add_payload(udp / data) + else: + packet.lastlayer().add_payload(data) + data = raw(packet) + return Packet.post_dissect(self, data) + + def do_build(self): + if not isinstance(self.payload, IPv6): + return Packet.do_build(self) + pay = self.payload.payload + while pay and isinstance(pay.payload, _IPv6ExtHdr): + # XXX todo: populate a LoWPAN_NHC_IPv6Ext + pay = pay.payload + if isinstance(pay, UDP): + try: + udp_hdr = next( + x for x in self.exts if isinstance(x, LoWPAN_NHC_UDP) + ) + except StopIteration: + udp_hdr = LoWPAN_NHC_UDP() + # Guess best compression + if pay.sport >> 4 == 0xf0b and pay.dport >> 4 == 0xf0b: + udp_hdr.P = 3 + elif pay.sport >> 8 == 0xf0: + udp_hdr.P = 2 + elif pay.dport >> 8 == 0xf0: + udp_hdr.P = 1 + self.exts.insert(0, udp_hdr) + # https://tools.ietf.org/html/rfc6282#section-4.3.3 + if udp_hdr.P == 0: + udp_hdr.udpSourcePort = pay.sport + udp_hdr.udpDestPort = pay.dport + elif udp_hdr.P == 1: + udp_hdr.udpSourcePort = pay.sport + udp_hdr.udpDestPort = pay.dport & 255 + elif udp_hdr.P == 2: + udp_hdr.udpSourcePort = pay.sport & 255 + udp_hdr.udpDestPort = pay.dport + elif udp_hdr.P == 3: + udp_hdr.udpSourcePort = pay.sport & 15 + udp_hdr.udpDestPort = pay.dport & 15 + if udp_hdr.C == 0: + if pay.chksum: + udp_hdr.udpChecksum = pay.chksum + else: + udp_hdr.udpChecksum = UDP(raw(pay)).chksum + return Packet.do_build(self) + + def do_build_payload(self): + # Elide IPv6 payload, extensions and UDP + if isinstance(self.payload, IPv6): + cur = self.payload + while cur and isinstance(cur, (IPv6, UDP)): + cur = cur.payload + return raw(cur) + return Packet.do_build_payload(self) + + def guess_payload_class(self, payload): + if self.underlayer: + u = self.underlayer.underlayer + if isinstance(u, (LoWPANFragmentationFirst, + LoWPANFragmentationSubsequent)): + return Raw + return IPv6 + + +###################### +# 6LowPan Dispatcher # +###################### + +# https://tools.ietf.org/html/rfc4944#section-5.1 + +class SixLoWPAN_ESC(Packet): + name = "SixLoWPAN Dispatcher ESC" + fields_desc = [ByteField("dispatch", 0)] class SixLoWPAN(Packet): - name = "SixLoWPAN(Packet)" + name = "SixLoWPAN Dispatcher" @classmethod def dispatch_hook(cls, _pkt=b"", *args, **kargs): - """Depending on the payload content, the frame type we should interpretate""" # noqa: E501 + """Depending on the payload content, the frame type we should interpret""" if _pkt and len(_pkt) >= 1: - if orb(_pkt[0]) == 0x41: + fb = ord(_pkt[:1]) + if fb == 0x41: return LoWPANUncompressedIPv6 - if orb(_pkt[0]) == 0x42: + if fb == 0x42: return LoWPAN_HC1 - if orb(_pkt[0]) >> 3 == 0x18: + if fb == 0x50: + return LoWPANBroadcast + if fb == 0x7f: + return SixLoWPAN_ESC + if fb >> 3 == 0x18: return LoWPANFragmentationFirst - elif orb(_pkt[0]) >> 3 == 0x1C: + if fb >> 3 == 0x1C: return LoWPANFragmentationSubsequent - elif orb(_pkt[0]) >> 6 == 0x02: + if fb >> 6 == 0x02: return LoWPANMesh - elif orb(_pkt[0]) >> 6 == 0x01: + if fb >> 6 == 0x01: return LoWPAN_IPHC return cls +################# +# Fragmentation # +################# + # fragmentate IPv6 MAX_SIZE = 96 @@ -758,8 +1147,8 @@ def sixlowpan_fragment(packet, datagram_tag=1): if len(str_packet) <= MAX_SIZE: return [packet] - def chunks(l, n): - return [l[i:i + n] for i in range(0, len(l), n)] + def chunks(li, n): + return [li[i:i + n] for i in range(0, len(li), n)] new_packet = chunks(str_packet, MAX_SIZE) @@ -785,20 +1174,16 @@ def sixlowpan_defragment(packet_list): results[tag] = results.get(tag, b"") + p[cls].payload.load # noqa: E501 return {tag: SixLoWPAN(x) for tag, x in results.items()} +############ +# Bindings # +############ + + +bind_layers(LoWPAN_HC1, IPv6) + +bind_top_down(LoWPAN_IPHC, LoWPAN_NHC, nh=1) +bind_layers(LoWPANFragmentationFirst, SixLoWPAN) +bind_layers(LoWPANMesh, SixLoWPAN) +bind_layers(LoWPANBroadcast, SixLoWPAN) -bind_layers(SixLoWPAN, LoWPANFragmentationFirst,) -bind_layers(SixLoWPAN, LoWPANFragmentationSubsequent,) -bind_layers(SixLoWPAN, LoWPANMesh,) -bind_layers(SixLoWPAN, LoWPAN_IPHC,) -bind_layers(LoWPANMesh, LoWPANFragmentationFirst,) -bind_layers(LoWPANMesh, LoWPANFragmentationSubsequent,) -# TODO: I have several doubts about the Broadcast LoWPAN -# bind_layers( LoWPANBroadcast, LoWPANHC1CompressedIPv6, ) -# bind_layers( SixLoWPAN, LoWPANBroadcast, ) -# bind_layers( LoWPANMesh, LoWPANBroadcast, ) -# bind_layers( LoWPANBroadcast, LoWPANFragmentationFirst, ) -# bind_layers( LoWPANBroadcast, LoWPANFragmentationSubsequent, ) - -# TODO: find a way to chose between ZigbeeNWK and SixLoWPAN (cf. dot15d4.py) -# Currently: use conf.dot15d4_protocol value -# bind_layers(Dot15d4Data, SixLoWPAN) +bind_layers(Ether, SixLoWPAN, type=0xA0ED) diff --git a/scapy/layers/skinny.py b/scapy/layers/skinny.py index 7f8f074a894..322cb47d89f 100644 --- a/scapy/layers/skinny.py +++ b/scapy/layers/skinny.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Skinny Call Control Protocol (SCCP) diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py index 4dc42f1229a..cf2ee2e868a 100644 --- a/scapy/layers/smb.py +++ b/scapy/layers/smb.py @@ -1,377 +1,1213 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license +# Copyright (C) Gabriel Potter """ -SMB (Server Message Block), also known as CIFS. +SMB 1.0 (Server Message Block), also known as CIFS. + +.. note:: + You will find more complete documentation for this layer over at + `SMB `_ + +Specs: + +- [MS-CIFS] (base) +- [MS-SMB] (extension of CIFS - SMB v1) """ -from scapy.packet import Packet, bind_layers -from scapy.fields import BitField, ByteEnumField, ByteField, FlagsField, \ - LEFieldLenField, LEIntField, LELongField, LEShortField, ShortField, \ - StrFixedLenField, StrLenField, StrNullField -from scapy.layers.netbios import NBTSession - - -# SMB NetLogon Response Header -class SMBNetlogon_Protocol_Response_Header(Packet): - name = "SMBNetlogon Protocol Response Header" - fields_desc = [StrFixedLenField("Start", b"\xffSMB", 4), - ByteEnumField("Command", 0x25, {0x25: "Trans"}), - ByteField("Error_Class", 0x02), - ByteField("Reserved", 0), - LEShortField("Error_code", 4), - ByteField("Flags", 0), - LEShortField("Flags2", 0x0000), - LEShortField("PIDHigh", 0x0000), - LELongField("Signature", 0x0), - LEShortField("Unused", 0x0), - LEShortField("TID", 0), - LEShortField("PID", 0), - LEShortField("UID", 0), - LEShortField("MID", 0), - ByteField("WordCount", 17), - LEShortField("TotalParamCount", 0), - LEShortField("TotalDataCount", 112), - LEShortField("MaxParamCount", 0), - LEShortField("MaxDataCount", 0), - ByteField("MaxSetupCount", 0), - ByteField("unused2", 0), - LEShortField("Flags3", 0), - ByteField("TimeOut1", 0xe8), - ByteField("TimeOut2", 0x03), - LEShortField("unused3", 0), - LEShortField("unused4", 0), - LEShortField("ParamCount2", 0), - LEShortField("ParamOffset", 0), - LEShortField("DataCount", 112), - LEShortField("DataOffset", 92), - ByteField("SetupCount", 3), - ByteField("unused5", 0)] - -# SMB MailSlot Protocol - - -class SMBMailSlot(Packet): - name = "SMB Mail Slot Protocol" - fields_desc = [LEShortField("opcode", 1), - LEShortField("priority", 1), - LEShortField("class", 2), - LEShortField("size", 135), - StrNullField("name", "\\MAILSLOT\\NET\\GETDC660")] - -# SMB NetLogon Protocol Response Tail SAM - - -class SMBNetlogon_Protocol_Response_Tail_SAM(Packet): - name = "SMB Netlogon Protocol Response Tail SAM" - fields_desc = [ByteEnumField("Command", 0x17, {0x12: "SAM logon request", 0x17: "SAM Active directory Response"}), # noqa: E501 - ByteField("unused", 0), - ShortField("Data1", 0), - ShortField("Data2", 0xfd01), - ShortField("Data3", 0), - ShortField("Data4", 0xacde), - ShortField("Data5", 0x0fe5), - ShortField("Data6", 0xd10a), - ShortField("Data7", 0x374c), - ShortField("Data8", 0x83e2), - ShortField("Data9", 0x7dd9), - ShortField("Data10", 0x3a16), - ShortField("Data11", 0x73ff), - ByteField("Data12", 0x04), - StrFixedLenField("Data13", "rmff", 4), - ByteField("Data14", 0x0), - ShortField("Data16", 0xc018), - ByteField("Data18", 0x0a), - StrFixedLenField("Data20", "rmff-win2k", 10), - ByteField("Data21", 0xc0), - ShortField("Data22", 0x18c0), - ShortField("Data23", 0x180a), - StrFixedLenField("Data24", "RMFF-WIN2K", 10), - ShortField("Data25", 0), - ByteField("Data26", 0x17), - StrFixedLenField("Data27", "Default-First-Site-Name", 23), - ShortField("Data28", 0x00c0), - ShortField("Data29", 0x3c10), - ShortField("Data30", 0x00c0), - ShortField("Data31", 0x0200), - ShortField("Data32", 0x0), - ShortField("Data33", 0xac14), - ShortField("Data34", 0x0064), - ShortField("Data35", 0x0), - ShortField("Data36", 0x0), - ShortField("Data37", 0x0), - ShortField("Data38", 0x0), - ShortField("Data39", 0x0d00), - ShortField("Data40", 0x0), - ShortField("Data41", 0xffff)] - -# SMB NetLogon Protocol Response Tail LM2.0 - - -class SMBNetlogon_Protocol_Response_Tail_LM20(Packet): - name = "SMB Netlogon Protocol Response Tail LM20" - fields_desc = [ByteEnumField("Command", 0x06, {0x06: "LM 2.0 Response to logon request"}), # noqa: E501 - ByteField("unused", 0), - StrFixedLenField("DblSlash", "\\\\", 2), - StrNullField("ServerName", "WIN"), - LEShortField("LM20Token", 0xffff)] - -# SMBNegociate Protocol Request Header - - -class SMBNegociate_Protocol_Request_Header(Packet): - name = "SMBNegociate Protocol Request Header" - fields_desc = [StrFixedLenField("Start", b"\xffSMB", 4), - ByteEnumField("Command", 0x72, {0x72: "SMB_COM_NEGOTIATE"}), - ByteField("Error_Class", 0), - ByteField("Reserved", 0), - LEShortField("Error_code", 0), - ByteField("Flags", 0x18), - LEShortField("Flags2", 0x0000), - LEShortField("PIDHigh", 0x0000), - LELongField("Signature", 0x0), - LEShortField("Unused", 0x0), - LEShortField("TID", 0), - LEShortField("PID", 1), - LEShortField("UID", 0), - LEShortField("MID", 2), - ByteField("WordCount", 0), - LEShortField("ByteCount", 12)] - -# SMB Negotiate Protocol Request Tail - - -class SMBNegociate_Protocol_Request_Tail(Packet): - name = "SMB Negotiate Protocol Request Tail" - fields_desc = [ByteField("BufferFormat", 0x02), - StrNullField("BufferData", "NT LM 0.12")] - -# SMBNegociate Protocol Response Advanced Security - - -class SMBNegociate_Protocol_Response_Advanced_Security(Packet): - name = "SMBNegociate Protocol Response Advanced Security" - fields_desc = [StrFixedLenField("Start", b"\xffSMB", 4), - ByteEnumField("Command", 0x72, {0x72: "SMB_COM_NEGOTIATE"}), - ByteField("Error_Class", 0), - ByteField("Reserved", 0), - LEShortField("Error_Code", 0), - ByteField("Flags", 0x98), - LEShortField("Flags2", 0x0000), - LEShortField("PIDHigh", 0x0000), - LELongField("Signature", 0x0), - LEShortField("Unused", 0x0), - LEShortField("TID", 0), - LEShortField("PID", 1), - LEShortField("UID", 0), - LEShortField("MID", 2), - ByteField("WordCount", 17), - LEShortField("DialectIndex", 7), - ByteField("SecurityMode", 0x03), - LEShortField("MaxMpxCount", 50), - LEShortField("MaxNumberVC", 1), - LEIntField("MaxBufferSize", 16144), - LEIntField("MaxRawSize", 65536), - LEIntField("SessionKey", 0x0000), - LEShortField("ServerCapabilities", 0xf3f9), - BitField("UnixExtensions", 0, 1), - BitField("Reserved2", 0, 7), - BitField("ExtendedSecurity", 1, 1), - BitField("CompBulk", 0, 2), - BitField("Reserved3", 0, 5), - # There have been 127490112000000000 tenths of micro-seconds between 1st january 1601 and 1st january 2005. 127490112000000000=0x1C4EF94D6228000, so ServerTimeHigh=0xD6228000 and ServerTimeLow=0x1C4EF94. # noqa: E501 - LEIntField("ServerTimeHigh", 0xD6228000), - LEIntField("ServerTimeLow", 0x1C4EF94), - LEShortField("ServerTimeZone", 0x3c), - ByteField("EncryptionKeyLength", 0), - LEFieldLenField("ByteCount", None, "SecurityBlob", adjust=lambda pkt, x: x - 16), # noqa: E501 - BitField("GUID", 0, 128), - StrLenField("SecurityBlob", "", length_from=lambda x: x.ByteCount + 16)] # noqa: E501 - -# SMBNegociate Protocol Response No Security -# When using no security, with EncryptionKeyLength=8, you must have an EncryptionKey before the DomainName # noqa: E501 - - -class SMBNegociate_Protocol_Response_No_Security(Packet): - name = "SMBNegociate Protocol Response No Security" - fields_desc = [StrFixedLenField("Start", b"\xffSMB", 4), - ByteEnumField("Command", 0x72, {0x72: "SMB_COM_NEGOTIATE"}), - ByteField("Error_Class", 0), - ByteField("Reserved", 0), - LEShortField("Error_Code", 0), - ByteField("Flags", 0x98), - LEShortField("Flags2", 0x0000), - LEShortField("PIDHigh", 0x0000), - LELongField("Signature", 0x0), - LEShortField("Unused", 0x0), - LEShortField("TID", 0), - LEShortField("PID", 1), - LEShortField("UID", 0), - LEShortField("MID", 2), - ByteField("WordCount", 17), - LEShortField("DialectIndex", 7), - ByteField("SecurityMode", 0x03), - LEShortField("MaxMpxCount", 50), - LEShortField("MaxNumberVC", 1), - LEIntField("MaxBufferSize", 16144), - LEIntField("MaxRawSize", 65536), - LEIntField("SessionKey", 0x0000), - LEShortField("ServerCapabilities", 0xf3f9), - BitField("UnixExtensions", 0, 1), - BitField("Reserved2", 0, 7), - BitField("ExtendedSecurity", 0, 1), - FlagsField("CompBulk", 0, 2, "CB"), - BitField("Reserved3", 0, 5), - # There have been 127490112000000000 tenths of micro-seconds between 1st january 1601 and 1st january 2005. 127490112000000000=0x1C4EF94D6228000, so ServerTimeHigh=0xD6228000 and ServerTimeLow=0x1C4EF94. # noqa: E501 - LEIntField("ServerTimeHigh", 0xD6228000), - LEIntField("ServerTimeLow", 0x1C4EF94), - LEShortField("ServerTimeZone", 0x3c), - ByteField("EncryptionKeyLength", 8), - LEShortField("ByteCount", 24), - BitField("EncryptionKey", 0, 64), - StrNullField("DomainName", "WORKGROUP"), - StrNullField("ServerName", "RMFF1")] - -# SMBNegociate Protocol Response No Security No Key - - -class SMBNegociate_Protocol_Response_No_Security_No_Key(Packet): - namez = "SMBNegociate Protocol Response No Security No Key" - fields_desc = [StrFixedLenField("Start", b"\xffSMB", 4), - ByteEnumField("Command", 0x72, {0x72: "SMB_COM_NEGOTIATE"}), - ByteField("Error_Class", 0), - ByteField("Reserved", 0), - LEShortField("Error_Code", 0), - ByteField("Flags", 0x98), - LEShortField("Flags2", 0x0000), - LEShortField("PIDHigh", 0x0000), - LELongField("Signature", 0x0), - LEShortField("Unused", 0x0), - LEShortField("TID", 0), - LEShortField("PID", 1), - LEShortField("UID", 0), - LEShortField("MID", 2), - ByteField("WordCount", 17), - LEShortField("DialectIndex", 7), - ByteField("SecurityMode", 0x03), - LEShortField("MaxMpxCount", 50), - LEShortField("MaxNumberVC", 1), - LEIntField("MaxBufferSize", 16144), - LEIntField("MaxRawSize", 65536), - LEIntField("SessionKey", 0x0000), - LEShortField("ServerCapabilities", 0xf3f9), - BitField("UnixExtensions", 0, 1), - BitField("Reserved2", 0, 7), - BitField("ExtendedSecurity", 0, 1), - FlagsField("CompBulk", 0, 2, "CB"), - BitField("Reserved3", 0, 5), - # There have been 127490112000000000 tenths of micro-seconds between 1st january 1601 and 1st january 2005. 127490112000000000=0x1C4EF94D6228000, so ServerTimeHigh=0xD6228000 and ServerTimeLow=0x1C4EF94. # noqa: E501 - LEIntField("ServerTimeHigh", 0xD6228000), - LEIntField("ServerTimeLow", 0x1C4EF94), - LEShortField("ServerTimeZone", 0x3c), - ByteField("EncryptionKeyLength", 0), - LEShortField("ByteCount", 16), - StrNullField("DomainName", "WORKGROUP"), - StrNullField("ServerName", "RMFF1")] +import struct + +from scapy.config import conf +from scapy.packet import Packet, bind_layers, bind_top_down +from scapy.fields import ( + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + FlagsField, + IPField, + LEFieldLenField, + LEIntEnumField, + LEIntField, + LELongField, + LEShortEnumField, + LEShortField, + MultipleTypeField, + PacketField, + PacketLenField, + PacketListField, + ReversePadField, + ScalingField, + ShortField, + StrFixedLenField, + StrNullField, + StrNullFieldUtf16, + UTCTimeField, + UUIDField, + XLEShortField, + XStrLenField, +) + +from scapy.layers.dns import ( + DNSStrField, + DNSCompressedPacket, +) +from scapy.layers.ntlm import ( + _NTLMPayloadPacket, + _NTLMPayloadField, + _NTLM_ENUM, + _NTLM_post_build, +) +from scapy.layers.netbios import NBTSession, NBTDatagram +from scapy.layers.gssapi import ( + GSSAPI_BLOB, +) +from scapy.layers.smb2 import ( + SMB2_Compression_Transform_Header, + SMB2_Header, + SMB2_Transform_Header, +) +from scapy.layers.windows.erref import STATUS_ERREF + + +SMB_COM = { + 0x00: "SMB_COM_CREATE_DIRECTORY", + 0x01: "SMB_COM_DELETE_DIRECTORY", + 0x02: "SMB_COM_OPEN", + 0x03: "SMB_COM_CREATE", + 0x04: "SMB_COM_CLOSE", + 0x05: "SMB_COM_FLUSH", + 0x06: "SMB_COM_DELETE", + 0x07: "SMB_COM_RENAME", + 0x08: "SMB_COM_QUERY_INFORMATION", + 0x09: "SMB_COM_SET_INFORMATION", + 0x0A: "SMB_COM_READ", + 0x0B: "SMB_COM_WRITE", + 0x0C: "SMB_COM_LOCK_BYTE_RANGE", + 0x0D: "SMB_COM_UNLOCK_BYTE_RANGE", + 0x0E: "SMB_COM_CREATE_TEMPORARY", + 0x0F: "SMB_COM_CREATE_NEW", + 0x10: "SMB_COM_CHECK_DIRECTORY", + 0x11: "SMB_COM_PROCESS_EXIT", + 0x12: "SMB_COM_SEEK", + 0x13: "SMB_COM_LOCK_AND_READ", + 0x14: "SMB_COM_WRITE_AND_UNLOCK", + 0x1A: "SMB_COM_READ_RAW", + 0x1B: "SMB_COM_READ_MPX", + 0x1C: "SMB_COM_READ_MPX_SECONDARY", + 0x1D: "SMB_COM_WRITE_RAW", + 0x1E: "SMB_COM_WRITE_MPX", + 0x1F: "SMB_COM_WRITE_MPX_SECONDARY", + 0x20: "SMB_COM_WRITE_COMPLETE", + 0x21: "SMB_COM_QUERY_SERVER", + 0x22: "SMB_COM_SET_INFORMATION2", + 0x23: "SMB_COM_QUERY_INFORMATION2", + 0x24: "SMB_COM_LOCKING_ANDX", + 0x25: "SMB_COM_TRANSACTION", + 0x26: "SMB_COM_TRANSACTION_SECONDARY", + 0x27: "SMB_COM_IOCTL", + 0x28: "SMB_COM_IOCTL_SECONDARY", + 0x29: "SMB_COM_COPY", + 0x2A: "SMB_COM_MOVE", + 0x2B: "SMB_COM_ECHO", + 0x2C: "SMB_COM_WRITE_AND_CLOSE", + 0x2D: "SMB_COM_OPEN_ANDX", + 0x2E: "SMB_COM_READ_ANDX", + 0x2F: "SMB_COM_WRITE_ANDX", + 0x30: "SMB_COM_NEW_FILE_SIZE", + 0x31: "SMB_COM_CLOSE_AND_TREE_DISC", + 0x32: "SMB_COM_TRANSACTION2", + 0x33: "SMB_COM_TRANSACTION2_SECONDARY", + 0x34: "SMB_COM_FIND_CLOSE2", + 0x35: "SMB_COM_FIND_NOTIFY_CLOSE", + 0x70: "SMB_COM_TREE_CONNECT", + 0x71: "SMB_COM_TREE_DISCONNECT", + 0x72: "SMB_COM_NEGOTIATE", + 0x73: "SMB_COM_SESSION_SETUP_ANDX", + 0x74: "SMB_COM_LOGOFF_ANDX", + 0x75: "SMB_COM_TREE_CONNECT_ANDX", + 0x7E: "SMB_COM_SECURITY_PACKAGE_ANDX", + 0x80: "SMB_COM_QUERY_INFORMATION_DISK", + 0x81: "SMB_COM_SEARCH", + 0x82: "SMB_COM_FIND", + 0x83: "SMB_COM_FIND_UNIQUE", + 0x84: "SMB_COM_FIND_CLOSE", + 0xA0: "SMB_COM_NT_TRANSACT", + 0xA1: "SMB_COM_NT_TRANSACT_SECONDARY", + 0xA2: "SMB_COM_NT_CREATE_ANDX", + 0xA4: "SMB_COM_NT_CANCEL", + 0xA5: "SMB_COM_NT_RENAME", + 0xC0: "SMB_COM_OPEN_PRINT_FILE", + 0xC1: "SMB_COM_WRITE_PRINT_FILE", + 0xC2: "SMB_COM_CLOSE_PRINT_FILE", + 0xC3: "SMB_COM_GET_PRINT_QUEUE", + 0xD8: "SMB_COM_READ_BULK", + 0xD9: "SMB_COM_WRITE_BULK", + 0xDA: "SMB_COM_WRITE_BULK_DATA", + 0xFE: "SMB_COM_INVALID", + 0xFF: "SMB_COM_NO_ANDX_COMMAND", +} + + +class SMB_Header(Packet): + name = "SMB 1 Protocol Request Header" + fields_desc = [ + StrFixedLenField("Start", b"\xffSMB", 4), + ByteEnumField("Command", 0x72, SMB_COM), + LEIntEnumField("Status", 0, STATUS_ERREF), + FlagsField( + "Flags", + 0x18, + 8, + [ + "LOCK_AND_READ_OK", + "BUF_AVAIL", + "res", + "CASE_INSENSITIVE", + "CANONICALIZED_PATHS", + "OPLOCK", + "OPBATCH", + "REPLY", + ], + ), + FlagsField( + "Flags2", + 0x0000, + -16, + [ + "LONG_NAMES", + "EAS", + "SMB_SECURITY_SIGNATURE", + "COMPRESSED", + "SMB_SECURITY_SIGNATURE_REQUIRED", + "res", + "IS_LONG_NAME", + "res", + "res", + "res", + "REPARSE_PATH", + "EXTENDED_SECURITY", + "DFS", + "PAGING_IO", + "NT_STATUS", + "UNICODE", + ], + ), + LEShortField("PIDHigh", 0x0000), + StrFixedLenField("SecuritySignature", b"", length=8), + LEShortField("Reserved", 0x0), + LEShortField("TID", 0), + LEShortField("PIDLow", 0), + LEShortField("UID", 0), + LEShortField("MID", 0), + ] + + def guess_payload_class(self, payload): + # type: (bytes) -> Packet + if not payload: + return super(SMB_Header, self).guess_payload_class(payload) + WordCount = ord(payload[:1]) + if self.Command == 0x72: + if self.Flags.REPLY: + if self.Flags2.EXTENDED_SECURITY: + return SMBNegotiate_Response_Extended_Security + else: + return SMBNegotiate_Response_Security + else: + return SMBNegotiate_Request + elif self.Command == 0x73: + if WordCount == 0: + return SMBSession_Null + if self.Flags.REPLY: + if WordCount == 0x04: + return SMBSession_Setup_AndX_Response_Extended_Security + elif WordCount == 0x03: + return SMBSession_Setup_AndX_Response + if self.Flags2.EXTENDED_SECURITY: + return SMBSession_Setup_AndX_Response_Extended_Security + else: + return SMBSession_Setup_AndX_Response + else: + if WordCount == 0x0C: + return SMBSession_Setup_AndX_Request_Extended_Security + elif WordCount == 0x0D: + return SMBSession_Setup_AndX_Request + if self.Flags2.EXTENDED_SECURITY: + return SMBSession_Setup_AndX_Request_Extended_Security + else: + return SMBSession_Setup_AndX_Request + elif self.Command == 0x25: + if self.Flags.REPLY: + if WordCount == 0x11: + return SMBMailslot_Write + else: + return SMBTransaction_Response + else: + if WordCount == 0x11: + return SMBMailslot_Write + else: + return SMBTransaction_Request + return super(SMB_Header, self).guess_payload_class(payload) + + def answers(self, pkt): + return SMB_Header in pkt + + +# SMB Negotiate Request + + +class SMB_Dialect(Packet): + name = "SMB Dialect" + fields_desc = [ + ByteField("BufferFormat", 0x02), + StrNullField("DialectString", "NT LM 0.12"), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SMBNegotiate_Request(Packet): + name = "SMB Negotiate Request" + fields_desc = [ + ByteField("WordCount", 0), + LEFieldLenField("ByteCount", None, length_of="Dialects"), + PacketListField( + "Dialects", + [SMB_Dialect()], + SMB_Dialect, + length_from=lambda pkt: pkt.ByteCount, + ), + ] + + +bind_layers(SMB_Header, SMBNegotiate_Request, Command=0x72) + +# SMBNegotiate Protocol Response + + +def _SMBStrNullField(name, default): + """ + Returns a StrNullField that is either normal or UTF-16 depending + on the SMB headers. + """ + + def _isUTF16(pkt): + while not hasattr(pkt, "Flags2") and pkt.underlayer: + pkt = pkt.underlayer + return hasattr(pkt, "Flags2") and pkt.Flags2.UNICODE + + return MultipleTypeField( + [(StrNullFieldUtf16(name, default), _isUTF16)], + StrNullField(name, default), + ) + + +def _len(pkt, name): + """ + Returns the length of a field, works with Unicode strings. + """ + fld, v = pkt.getfield_and_val(name) + return len(fld.addfield(pkt, v, b"")) + + +class _SMBNegotiate_Response(Packet): + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 2: + # Yes this is inspired by + # https://github.com/wireshark/wireshark/blob/925e01b23fd5aad2fa929fafd894128a88832e74/epan/dissectors/packet-smb.c#L2902 + wc = struct.unpack(" bytes + return ( + _NTLM_post_build( + self, + pkt, + 32 + 31 + len(self.Setup) * 2 + len(self.Name) + 1, + { + "Parameter": 19, + "Data": 23, + }, + config=_SMB_CONFIG, + ) + + pay + ) + + def mysummary(self): + if getattr(self, "Data", None) is not None: + return self.sprintf("Tran %Name% ") + self.Data.mysummary() + return self.sprintf("Tran %Name%") + + +bind_top_down(SMB_Header, SMBTransaction_Request, Command=0x25) + + +class SMBMailslot_Write(SMBTransaction_Request): + WordCount = 0x11 + + +# [MS-CIFS] sect 2.2.4.33.2 + + +class SMBTransaction_Response(_NTLMPayloadPacket): + name = "SMB COM Transaction Response" + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + FieldLenField( + "WordCount", + None, + length_of="SetupCount", + adjust=lambda pkt, x: x + 0x0A, + fmt="B", + ), + FieldLenField( + "TotalParamCount", + None, + length_of="Buffer", + fmt=" bytes + return ( + _NTLM_post_build( + self, + pkt, + 32 + 22 + len(self.Setup) * 2, + { + "Parameter": 7, + "Data": 13, + }, + config=_SMB_CONFIG, + ) + + pay + ) + + +bind_top_down(SMB_Header, SMBTransaction_Response, Command=0x25, Flags=0x80) + + +# [MS-ADTS] sect 6.3.1.4 + +_NETLOGON_opcodes = { + 0x7: "LOGON_PRIMARY_QUERY", + 0x12: "LOGON_SAM_LOGON_REQUEST", + 0x13: "LOGON_SAM_LOGON_RESPONSE", + 0x15: "LOGON_SAM_USER_UNKNOWN", + 0x17: "LOGON_SAM_LOGON_RESPONSE_EX", + 0x19: "LOGON_SAM_USER_UNKNOWN_EX", +} + +_NV_VERSION = { + 0x00000001: "V1", + 0x00000002: "V5", + 0x00000004: "V5EX", + 0x00000008: "V5EX_WITH_IP", + 0x00000010: "V5EX_WITH_CLOSEST_SITE", + 0x01000000: "AVOID_NT4EMUL", + 0x10000000: "PDC", + 0x20000000: "IP", + 0x40000000: "LOCAL", + 0x80000000: "GC", +} + + +class NETLOGON(Packet): + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + if _pkt[0] == 0x07: # LOGON_PRIMARY_QUERY + return NETLOGON_LOGON_QUERY + elif _pkt[0] == 0x12: # LOGON_SAM_LOGON_REQUEST + return NETLOGON_SAM_LOGON_REQUEST + elif _pkt[0] == 0x13: # LOGON_SAM_USER_RESPONSE + try: + i = _pkt.index(b"\xff\xff\xff\xff") + NtVersion = NETLOGON_SAM_LOGON_RESPONSE_NT40.fields_desc[ + -3 + ].getfield(None, _pkt[i - 4 : i])[1] + if NtVersion.V1 and not NtVersion.V5: + return NETLOGON_SAM_LOGON_RESPONSE_NT40 + except Exception: + pass + return NETLOGON_SAM_LOGON_RESPONSE + elif _pkt[0] == 0x15: # LOGON_SAM_USER_UNKNOWN + return NETLOGON_SAM_LOGON_RESPONSE + elif _pkt[0] == 0x17: # LOGON_SAM_LOGON_RESPONSE_EX + return NETLOGON_SAM_LOGON_RESPONSE_EX + elif _pkt[0] == 0x19: # LOGON_SAM_USER_UNKNOWN_EX + return NETLOGON_SAM_LOGON_RESPONSE + return cls + + +class NETLOGON_LOGON_QUERY(NETLOGON): + fields_desc = [ + LEShortEnumField("OpCode", 0x7, _NETLOGON_opcodes), + StrNullField("ComputerName", ""), + StrNullField("MailslotName", ""), + StrNullFieldUtf16("UnicodeComputerName", ""), + FlagsField("NtVersion", 0xB, -32, _NV_VERSION), + XLEShortField("LmNtToken", 0xFFFF), + XLEShortField("Lm20Token", 0xFFFF), + ] + + +# [MS-ADTS] sect 6.3.1.6 + + +class NETLOGON_SAM_LOGON_REQUEST(NETLOGON): + fields_desc = [ + LEShortEnumField("OpCode", 0x12, _NETLOGON_opcodes), + LEShortField("RequestCount", 0), + StrNullFieldUtf16("UnicodeComputerName", ""), + StrNullFieldUtf16("UnicodeUserName", ""), + StrNullField("MailslotName", "\\MAILSLOT\\NET\\GETDC701253F9"), + LEIntField("AllowableAccountControlBits", 0), + FieldLenField("DomainSidSize", None, fmt="=2008R2 + 0x00008000: "DS_9", # >=2012 + 0x00010000: "DS_10", # >=2016 + 0x00020000: "DS_11", # >=2019 + 0x00040000: "DS_12", # >=2025 + 0x20000000: "DNS_CONTROLLER", + 0x40000000: "DNS_DOMAIN", + 0x80000000: "DNS_FOREST", +} + + +# [MS-ADTS] sect 6.3.1.8 + + +class NETLOGON_SAM_LOGON_RESPONSE(NETLOGON, DNSCompressedPacket): + fields_desc = [ + LEShortEnumField("OpCode", 0x17, _NETLOGON_opcodes), + StrNullFieldUtf16("UnicodeLogonServer", ""), + StrNullFieldUtf16("UnicodeUserName", ""), + StrNullFieldUtf16("UnicodeDomainName", ""), + UUIDField("DomainGuid", None, uuid_fmt=UUIDField.FORMAT_LE), + UUIDField("NullGuid", None, uuid_fmt=UUIDField.FORMAT_LE), + DNSStrField("DnsForestName", ""), + DNSStrField("DnsDomainName", ""), + DNSStrField("DnsHostName", ""), + IPField("DcIpAddress", "0.0.0.0"), + FlagsField("Flags", 0, -32, _NETLOGON_FLAGS), + FlagsField("NtVersion", 0x1, -32, _NV_VERSION), + XLEShortField("LmNtToken", 0xFFFF), + XLEShortField("Lm20Token", 0xFFFF), + ] + + def get_full(self): + return self.original + + +# [MS-ADTS] sect 6.3.1.9 + + +class DcSockAddr(Packet): + fields_desc = [ + LEShortField("sin_family", 2), + LEShortField("sin_port", 0), + IPField("sin_addr", None), + LELongField("sin_zero", 0), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class NETLOGON_SAM_LOGON_RESPONSE_EX(NETLOGON, DNSCompressedPacket): + fields_desc = [ + LEShortEnumField("OpCode", 0x17, _NETLOGON_opcodes), + LEShortField("Sbz", 0), + FlagsField("Flags", 0, -32, _NETLOGON_FLAGS), + UUIDField("DomainGuid", None, uuid_fmt=UUIDField.FORMAT_LE), + DNSStrField("DnsForestName", ""), + DNSStrField("DnsDomainName", ""), + DNSStrField("DnsHostName", ""), + DNSStrField("NetbiosDomainName", ""), + DNSStrField("NetbiosComputerName", ""), + DNSStrField("UserName", ""), + DNSStrField("DcSiteName", "Default-First-Site-Name"), + DNSStrField("ClientSiteName", "Default-First-Site-Name"), + ConditionalField( + ByteField("DcSockAddrSize", 0x10), + lambda pkt: pkt.NtVersion.V5EX_WITH_IP, + ), + ConditionalField( + PacketField("DcSockAddr", DcSockAddr(), DcSockAddr), + lambda pkt: pkt.NtVersion.V5EX_WITH_IP, + ), + ConditionalField( + DNSStrField("NextClosestSiteName", ""), + lambda pkt: pkt.NtVersion.V5EX_WITH_CLOSEST_SITE, + ), + FlagsField("NtVersion", 0xB, -32, _NV_VERSION), + XLEShortField("LmNtToken", 0xFFFF), + XLEShortField("Lm20Token", 0xFFFF), + ] + + def pre_dissect(self, s): + try: + i = s.index(b"\xff\xff\xff\xff") + self.fields["NtVersion"] = self.fields_desc[-3].getfield( + self, s[i - 4 : i] + )[1] + except Exception: + self.NtVersion = 0xB + return s + + def get_full(self): + return self.original + + +# [MS-BRWS] sect 2.2 + + +class BRWS(Packet): + fields_desc = [ + ByteEnumField( + "OpCode", + 0x00, + { + 0x01: "HostAnnouncement", + 0x02: "AnnouncementRequest", + 0x08: "RequestElection", + 0x09: "GetBackupListRequest", + 0x0A: "GetBackupListResponse", + 0x0B: "BecomeBackup", + 0x0C: "DomainAnnouncement", + 0x0D: "MasterAnnouncement", + 0x0E: "ResetStateRequest", + 0x0F: "LocalMasterAnnouncement", + }, + ), + ] + + def mysummary(self): + return self.sprintf("%OpCode%") + + registered_opcodes = {} + + @classmethod + def register_variant(cls): + cls.registered_opcodes[cls.OpCode.default] = cls + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + return cls.registered_opcodes.get(_pkt[0], cls) + return cls + + def default_payload_class(self, payload): + return conf.padding_layer + + +# [MS-BRWS] sect 2.2.1 + + +class BRWS_HostAnnouncement(BRWS): + OpCode = 0x01 + fields_desc = [ + BRWS, + ByteField("UpdateCount", 0), + LEIntField("Periodicity", 128000), + StrFixedLenField("ServerName", b"", length=16), + ByteField("OSVersionMajor", 6), + ByteField("OSVersionMinor", 1), + LEIntField("ServerType", 4611), + ByteField("BrowserConfigVersionMajor", 21), + ByteField("BrowserConfigVersionMinor", 1), + XLEShortField("Signature", 0xAA55), + StrNullField("Comment", ""), + ] + + def mysummary(self): + return self.sprintf("%OpCode% for %ServerName%") + + +# [MS-BRWS] sect 2.2.6 + + +class BRWS_BecomeBackup(BRWS): + OpCode = 0x0B + fields_desc = [ + BRWS, + StrNullField("BrowserToPromote", b""), + ] + + def mysummary(self): + return self.sprintf("%OpCode% from %BrowserToPromote%") + + +# [MS-BRWS] sect 2.2.10 + + +class BRWS_LocalMasterAnnouncement(BRWS_HostAnnouncement): + OpCode = 0x0F + + +# SMB dispatcher + + +class _SMBGeneric(Packet): + name = "SMB Generic dispatcher" + fields_desc = [StrFixedLenField("Start", b"\xffSMB", 4)] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + """ + Depending on the first 4 bytes of the packet, + dispatch to the correct version of Header + (either SMB or SMB2) + """ + if _pkt and len(_pkt) >= 4: + if _pkt[:4] == b"\xffSMB": + return SMB_Header + if _pkt[:4] == b"\xfeSMB": + return SMB2_Header + if _pkt[:4] == b"\xfdSMB": + return SMB2_Transform_Header + if _pkt[:4] == b"\xfcSMB": + return SMB2_Compression_Transform_Header + return cls + + +bind_layers(NBTSession, _SMBGeneric) +bind_layers(NBTDatagram, _SMBGeneric) diff --git a/scapy/layers/smb2.py b/scapy/layers/smb2.py new file mode 100644 index 00000000000..ba6a9292e19 --- /dev/null +++ b/scapy/layers/smb2.py @@ -0,0 +1,4128 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +SMB (Server Message Block), also known as CIFS - version 2 + +.. note:: + You will find more complete documentation for this layer over at + `SMB `_ +""" + +import collections +import functools +import hashlib +import os +import struct + +from scapy.automaton import select_objects +from scapy.config import conf, crypto_validator +from scapy.error import log_runtime +from scapy.packet import Packet, bind_layers, bind_top_down +from scapy.fields import ( + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + FlagsField, + IP6Field, + IPField, + IntField, + LEIntField, + LEIntEnumField, + LELongField, + LenField, + LEShortEnumField, + LEShortField, + MultipleTypeField, + PadField, + PacketField, + PacketLenField, + PacketListField, + ReversePadField, + ScalingField, + ShortEnumField, + ShortField, + StrFieldUtf16, + StrFixedLenField, + StrLenField, + StrLenFieldUtf16, + StrNullFieldUtf16, + ThreeBytesField, + UTCTimeField, + UUIDField, + XLEIntField, + XLELongField, + XLEShortField, + XStrLenField, + XStrFixedLenField, + YesNoByteField, +) +from scapy.sessions import DefaultSession +from scapy.supersocket import StreamSocket + +if conf.crypto_valid: + from scapy.libs.rfc3961 import SP800108_KDFCTR + +from scapy.layers.gssapi import GSSAPI_BLOB +from scapy.layers.netbios import NBTSession +from scapy.layers.ntlm import ( + _NTLMPayloadField, + _NTLMPayloadPacket, + _NTLM_ENUM, + _NTLM_post_build, +) +from scapy.layers.windows.erref import STATUS_ERREF + + +# EnumField +SMB_DIALECTS = { + 0x0202: "SMB 2.002", + 0x0210: "SMB 2.1", + 0x02FF: "SMB 2.???", + 0x0300: "SMB 3.0", + 0x0302: "SMB 3.0.2", + 0x0311: "SMB 3.1.1", +} + +# SMB2 sect 2.1.2.1 +REPARSE_TAGS = { + 0x00000000: "IO_REPARSE_TAG_RESERVED_ZERO", + 0x00000001: "IO_REPARSE_TAG_RESERVED_ONE", + 0x00000002: "IO_REPARSE_TAG_RESERVED_TWO", + 0xA0000003: "IO_REPARSE_TAG_MOUNT_POINT", + 0xC0000004: "IO_REPARSE_TAG_HSM", + 0x80000005: "IO_REPARSE_TAG_DRIVE_EXTENDER", + 0x80000006: "IO_REPARSE_TAG_HSM2", + 0x80000007: "IO_REPARSE_TAG_SIS", + 0x80000008: "IO_REPARSE_TAG_WIM", + 0x80000009: "IO_REPARSE_TAG_CSV", + 0x8000000A: "IO_REPARSE_TAG_DFS", + 0x8000000B: "IO_REPARSE_TAG_FILTER_MANAGER", + 0xA000000C: "IO_REPARSE_TAG_SYMLINK", + 0xA0000010: "IO_REPARSE_TAG_IIS_CACHE", + 0x80000012: "IO_REPARSE_TAG_DFSR", + 0x80000013: "IO_REPARSE_TAG_DEDUP", + 0xC0000014: "IO_REPARSE_TAG_APPXSTRM", + 0x80000014: "IO_REPARSE_TAG_NFS", + 0x80000015: "IO_REPARSE_TAG_FILE_PLACEHOLDER", + 0x80000016: "IO_REPARSE_TAG_DFM", + 0x80000017: "IO_REPARSE_TAG_WOF", + 0x80000018: "IO_REPARSE_TAG_WCI", + 0x90001018: "IO_REPARSE_TAG_WCI_1", + 0xA0000019: "IO_REPARSE_TAG_GLOBAL_REPARSE", + 0x9000001A: "IO_REPARSE_TAG_CLOUD", + 0x9000101A: "IO_REPARSE_TAG_CLOUD_1", + 0x9000201A: "IO_REPARSE_TAG_CLOUD_2", + 0x9000301A: "IO_REPARSE_TAG_CLOUD_3", + 0x9000401A: "IO_REPARSE_TAG_CLOUD_4", + 0x9000501A: "IO_REPARSE_TAG_CLOUD_5", + 0x9000601A: "IO_REPARSE_TAG_CLOUD_6", + 0x9000701A: "IO_REPARSE_TAG_CLOUD_7", + 0x9000801A: "IO_REPARSE_TAG_CLOUD_8", + 0x9000901A: "IO_REPARSE_TAG_CLOUD_9", + 0x9000A01A: "IO_REPARSE_TAG_CLOUD_A", + 0x9000B01A: "IO_REPARSE_TAG_CLOUD_B", + 0x9000C01A: "IO_REPARSE_TAG_CLOUD_C", + 0x9000D01A: "IO_REPARSE_TAG_CLOUD_D", + 0x9000E01A: "IO_REPARSE_TAG_CLOUD_E", + 0x9000F01A: "IO_REPARSE_TAG_CLOUD_F", + 0x8000001B: "IO_REPARSE_TAG_APPEXECLINK", + 0x9000001C: "IO_REPARSE_TAG_PROJFS", + 0xA000001D: "IO_REPARSE_TAG_LX_SYMLINK", + 0x8000001E: "IO_REPARSE_TAG_STORAGE_SYNC", + 0xA000001F: "IO_REPARSE_TAG_WCI_TOMBSTONE", + 0x80000020: "IO_REPARSE_TAG_UNHANDLED", + 0x80000021: "IO_REPARSE_TAG_ONEDRIVE", + 0xA0000022: "IO_REPARSE_TAG_PROJFS_TOMBSTONE", + 0x80000023: "IO_REPARSE_TAG_AF_UNIX", + 0x80000024: "IO_REPARSE_TAG_LX_FIFO", + 0x80000025: "IO_REPARSE_TAG_LX_CHR", + 0x80000026: "IO_REPARSE_TAG_LX_BLK", + 0xA0000027: "IO_REPARSE_TAG_WCI_LINK", + 0xA0001027: "IO_REPARSE_TAG_WCI_LINK_1", +} + +# SMB2 sect 2.2.1.1 +SMB2_COM = { + 0x0000: "SMB2_NEGOTIATE", + 0x0001: "SMB2_SESSION_SETUP", + 0x0002: "SMB2_LOGOFF", + 0x0003: "SMB2_TREE_CONNECT", + 0x0004: "SMB2_TREE_DISCONNECT", + 0x0005: "SMB2_CREATE", + 0x0006: "SMB2_CLOSE", + 0x0007: "SMB2_FLUSH", + 0x0008: "SMB2_READ", + 0x0009: "SMB2_WRITE", + 0x000A: "SMB2_LOCK", + 0x000B: "SMB2_IOCTL", + 0x000C: "SMB2_CANCEL", + 0x000D: "SMB2_ECHO", + 0x000E: "SMB2_QUERY_DIRECTORY", + 0x000F: "SMB2_CHANGE_NOTIFY", + 0x0010: "SMB2_QUERY_INFO", + 0x0011: "SMB2_SET_INFO", + 0x0012: "SMB2_OPLOCK_BREAK", +} + +# EnumField +SMB2_NEGOTIATE_CONTEXT_TYPES = { + 0x0001: "SMB2_PREAUTH_INTEGRITY_CAPABILITIES", + 0x0002: "SMB2_ENCRYPTION_CAPABILITIES", + 0x0003: "SMB2_COMPRESSION_CAPABILITIES", + 0x0005: "SMB2_NETNAME_NEGOTIATE_CONTEXT_ID", + 0x0006: "SMB2_TRANSPORT_CAPABILITIES", + 0x0007: "SMB2_RDMA_TRANSFORM_CAPABILITIES", + 0x0008: "SMB2_SIGNING_CAPABILITIES", +} + +# FlagField +SMB2_CAPABILITIES = { + 0x00000001: "DFS", + 0x00000002: "LEASING", + 0x00000004: "LARGE_MTU", + 0x00000008: "MULTI_CHANNEL", + 0x00000010: "PERSISTENT_HANDLES", + 0x00000020: "DIRECTORY_LEASING", + 0x00000040: "ENCRYPTION", +} +SMB2_SECURITY_MODE = { + 0x01: "SIGNING_ENABLED", + 0x02: "SIGNING_REQUIRED", +} + +# [MS-SMB2] 2.2.3.1.3 +SMB2_COMPRESSION_ALGORITHMS = { + 0x0000: "None", + 0x0001: "LZNT1", + 0x0002: "LZ77", + 0x0003: "LZ77 + Huffman", + 0x0004: "Pattern_V1", +} + +# [MS-SMB2] sect 2.2.3.1.2 +SMB2_ENCRYPTION_CIPHERS = { + 0x0001: "AES-128-CCM", + 0x0002: "AES-128-GCM", + 0x0003: "AES-256-CCM", + 0x0004: "AES-256-GCM", +} + +# [MS-SMB2] sect 2.2.3.1.7 +SMB2_SIGNING_ALGORITHMS = { + 0x0000: "HMAC-SHA256", + 0x0001: "AES-CMAC", + 0x0002: "AES-GMAC", +} + +# [MS-SMB2] sect 2.2.3.1.1 +SMB2_HASH_ALGORITHMS = { + 0x0001: "SHA-512", +} + +# sect [MS-SMB2] 2.2.13.1.1 +SMB2_ACCESS_FLAGS_FILE = { + 0x00000001: "FILE_READ_DATA", + 0x00000002: "FILE_WRITE_DATA", + 0x00000004: "FILE_APPEND_DATA", + 0x00000008: "FILE_READ_EA", + 0x00000010: "FILE_WRITE_EA", + 0x00000040: "FILE_DELETE_CHILD", + 0x00000020: "FILE_EXECUTE", + 0x00000080: "FILE_READ_ATTRIBUTES", + 0x00000100: "FILE_WRITE_ATTRIBUTES", + 0x00010000: "DELETE", + 0x00020000: "READ_CONTROL", + 0x00040000: "WRITE_DAC", + 0x00080000: "WRITE_OWNER", + 0x00100000: "SYNCHRONIZE", + 0x01000000: "ACCESS_SYSTEM_SECURITY", + 0x02000000: "MAXIMUM_ALLOWED", + 0x10000000: "GENERIC_ALL", + 0x20000000: "GENERIC_EXECUTE", + 0x40000000: "GENERIC_WRITE", + 0x80000000: "GENERIC_READ", +} + +# sect [MS-SMB2] 2.2.13.1.2 +SMB2_ACCESS_FLAGS_DIRECTORY = { + 0x00000001: "FILE_LIST_DIRECTORY", + 0x00000002: "FILE_ADD_FILE", + 0x00000004: "FILE_ADD_SUBDIRECTORY", + 0x00000008: "FILE_READ_EA", + 0x00000010: "FILE_WRITE_EA", + 0x00000020: "FILE_TRAVERSE", + 0x00000040: "FILE_DELETE_CHILD", + 0x00000080: "FILE_READ_ATTRIBUTES", + 0x00000100: "FILE_WRITE_ATTRIBUTES", + 0x00010000: "DELETE", + 0x00020000: "READ_CONTROL", + 0x00040000: "WRITE_DAC", + 0x00080000: "WRITE_OWNER", + 0x00100000: "SYNCHRONIZE", + 0x01000000: "ACCESS_SYSTEM_SECURITY", + 0x02000000: "MAXIMUM_ALLOWED", + 0x10000000: "GENERIC_ALL", + 0x20000000: "GENERIC_EXECUTE", + 0x40000000: "GENERIC_WRITE", + 0x80000000: "GENERIC_READ", +} + +# [MS-SRVS] sec 2.2.2.4 +SRVSVC_SHARE_TYPES = { + 0x00000000: "DISKTREE", + 0x00000001: "PRINTQ", + 0x00000002: "DEVICE", + 0x00000003: "IPC", + 0x02000000: "CLUSTER_FS", + 0x04000000: "CLUSTER_SOFS", + 0x08000000: "CLUSTER_DFS", +} + + +# [MS-FSCC] sec 2.6 +FileAttributes = { + 0x00000001: "FILE_ATTRIBUTE_READONLY", + 0x00000002: "FILE_ATTRIBUTE_HIDDEN", + 0x00000004: "FILE_ATTRIBUTE_SYSTEM", + 0x00000010: "FILE_ATTRIBUTE_DIRECTORY", + 0x00000020: "FILE_ATTRIBUTE_ARCHIVE", + 0x00000080: "FILE_ATTRIBUTE_NORMAL", + 0x00000100: "FILE_ATTRIBUTE_TEMPORARY", + 0x00000200: "FILE_ATTRIBUTE_SPARSE_FILE", + 0x00000400: "FILE_ATTRIBUTE_REPARSE_POINT", + 0x00000800: "FILE_ATTRIBUTE_COMPRESSED", + 0x00001000: "FILE_ATTRIBUTE_OFFLINE", + 0x00002000: "FILE_ATTRIBUTE_NOT_CONTENT_INDEXED", + 0x00004000: "FILE_ATTRIBUTE_ENCRYPTED", + 0x00008000: "FILE_ATTRIBUTE_INTEGRITY_STREAM", + 0x00020000: "FILE_ATTRIBUTE_NO_SCRUB_DATA", + 0x00040000: "FILE_ATTRIBUTE_RECALL_ON_OPEN", + 0x00080000: "FILE_ATTRIBUTE_PINNED", + 0x00100000: "FILE_ATTRIBUTE_UNPINNED", + 0x00400000: "FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS", +} + + +# [MS-FSCC] sect 2.4 +FileInformationClasses = { + 0x01: "FileDirectoryInformation", + 0x02: "FileFullDirectoryInformation", + 0x03: "FileBothDirectoryInformation", + 0x04: "FileBasicInformation", + 0x05: "FileStandardInformation", + 0x06: "FileInternalInformation", + 0x07: "FileEaInformation", + 0x08: "FileAccessInformation", + 0x0A: "FileRenameInformation", + 0x0E: "FilePositionInformation", + 0x10: "FileModeInformation", + 0x11: "FileAlignmentInformation", + 0x12: "FileAllInformation", + 0x22: "FileNetworkOpenInformation", + 0x25: "FileIdBothDirectoryInformation", + 0x26: "FileIdFullDirectoryInformation", + 0x0C: "FileNamesInformation", + 0x30: "FileNormalizedNameInformation", + 0x3C: "FileIdExtdDirectoryInformation", +} +_FileInformationClasses = {} + + +# [MS-FSCC] 2.1.7 FILE_NAME_INFORMATION + + +class FILE_NAME_INFORMATION(Packet): + fields_desc = [ + FieldLenField("FileNameLength", None, length_of="FileName", fmt=" 65535 / len(FILE_ID_BOTH_DIR_INFORMATION()) + ), + ] + + +# [MS-FSCC] 2.4.22 FileInternalInformation + + +class FileInternalInformation(Packet): + fields_desc = [ + LELongField("IndexNumber", 0), + ] + + def default_payload_class(self, s): + return conf.padding_layer + + +# [MS-FSCC] 2.4.26 FileModeInformation + + +class FileModeInformation(Packet): + fields_desc = [ + FlagsField( + "Mode", + 0, + -32, + { + 0x00000002: "FILE_WRITE_TROUGH", + 0x00000004: "FILE_SEQUENTIAL_ONLY", + 0x00000008: "FILE_NO_INTERMEDIATE_BUFFERING", + 0x00000010: "FILE_SYNCHRONOUS_IO_ALERT", + 0x00000020: "FILE_SYNCHRONOUS_IO_NONALERT", + 0x00001000: "FILE_DELETE_ON_CLOSE", + }, + ) + ] + + def default_payload_class(self, s): + return conf.padding_layer + + +# [MS-FSCC] 2.4.35 FilePositionInformation + + +class FilePositionInformation(Packet): + fields_desc = [ + LELongField("CurrentByteOffset", 0), + ] + + def default_payload_class(self, s): + return conf.padding_layer + + +# [MS-FSCC] 2.4.37 FileRenameInformation + + +class FileRenameInformation(Packet): + fields_desc = [ + YesNoByteField("ReplaceIfExists", False), + XStrFixedLenField("Reserved", b"", length=7), + LELongField("RootDirectory", 0), + FieldLenField("FileNameLength", 0, length_of="FileName", fmt=" bytes + if len(pkt) < 24: + # 'Length of this field MUST be the number of bytes required to make the + # size of this structure at least 24.' + pkt += (24 - len(pkt)) * b"\x00" + return pkt + pay + + def default_payload_class(self, s): + return conf.padding_layer + + +_FileInformationClasses[0x0A] = FileRenameInformation + + +# [MS-FSCC] 2.4.41 FileStandardInformation + + +class FileStandardInformation(Packet): + fields_desc = [ + LELongField("AllocationSize", 4096), + LELongField("EndOfFile", 0), + LEIntField("NumberOfLinks", 1), + ByteField("DeletePending", 0), + ByteField("Directory", 0), + ShortField("Reserved", 0), + ] + + def default_payload_class(self, s): + return conf.padding_layer + + +# [MS-FSCC] 2.4.43 FileStreamInformation + + +class FileStreamInformation(Packet): + fields_desc = [ + LEIntField("Next", 0), + FieldLenField("StreamNameLength", None, length_of="StreamName", fmt=" bytes + return ( + _SMB2_post_build( + self, + pkt, + 24 + len(self.IPAddrMoveList) * 24, + { + "ResourceName": 8, + }, + ) + + pay + ) + + +# sect 2.2.2.1 + + +class SMB2_Error_ContextResponse(Packet): + fields_desc = [ + FieldLenField("ErrorDatalength", None, fmt=" bytes + return ( + _NTLM_post_build( + self, + pkt, + 64 + 36 + len(self.Dialects) * 2, + { + "NegotiateContexts": 28, + }, + config=[ + ("BufferOffset", _NTLM_ENUM.OFFSET | _NTLM_ENUM.PAD8), + ("Count", _NTLM_ENUM.COUNT), + ], + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Negotiate_Protocol_Request, + Command=0x0000, +) + +# sect 2.2.3.1.1 + + +class SMB2_Preauth_Integrity_Capabilities(Packet): + name = "SMB2 Preauth Integrity Capabilities" + fields_desc = [ + # According to the spec, this field value must be greater than 0 + # (cf Section 2.2.3.1.1 of MS-SMB2.pdf) + FieldLenField("HashAlgorithmCount", None, fmt=" bytes + pkt = _NTLM_post_build( + self, + pkt, + self.OFFSET, + { + "SecurityBlob": 56, + "NegotiateContexts": 60, + }, + config=[ + ( + "BufferOffset", + { + "SecurityBlob": _NTLM_ENUM.OFFSET, + "NegotiateContexts": _NTLM_ENUM.OFFSET | _NTLM_ENUM.PAD8, + }, + ), + ], + ) + if getattr(self, "SecurityBlob", None): + if self.SecurityBlobLen is None: + pkt = pkt[:58] + struct.pack(" bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "SecurityBlob": 12, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Session_Setup_Request, + Command=0x0001, +) + +# sect 2.2.6 + + +class SMB2_Session_Setup_Response(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 Session Setup Response" + Command = 0x0001 + OFFSET = 8 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x9), + FlagsField( + "SessionFlags", + 0, + -16, + { + 0x0001: "IS_GUEST", + 0x0002: "IS_NULL", + 0x0004: "ENCRYPT_DATA", + }, + ), + XLEShortField("SecurityBufferOffset", None), + LEShortField("SecurityLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + PacketField("Security", None, GSSAPI_BLOB), + ], + ), + ] + + def __getattr__(self, attr): + # Ease SMB1 backward compatibility + if attr == "SecurityBlob": + return ( + super(SMB2_Session_Setup_Response, self).__getattr__("Buffer") + or [(None, None)] + )[0][1] + return super(SMB2_Session_Setup_Response, self).__getattr__(attr) + + def setfieldval(self, attr, val): + if attr == "SecurityBlob": + return super(SMB2_Session_Setup_Response, self).setfieldval( + "Buffer", [("Security", val)] + ) + return super(SMB2_Session_Setup_Response, self).setfieldval(attr, val) + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Security": 4, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Session_Setup_Response, + Command=0x0001, + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR +) + +# sect 2.2.7 + + +class SMB2_Session_Logoff_Request(_SMB2_Payload): + name = "SMB2 LOGOFF Request" + Command = 0x0002 + fields_desc = [ + XLEShortField("StructureSize", 0x4), + ShortField("reserved", 0), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Session_Logoff_Request, + Command=0x0002, +) + +# sect 2.2.8 + + +class SMB2_Session_Logoff_Response(_SMB2_Payload): + name = "SMB2 LOGOFF Request" + Command = 0x0002 + fields_desc = [ + XLEShortField("StructureSize", 0x4), + ShortField("reserved", 0), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Session_Logoff_Response, + Command=0x0002, + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR +) + +# sect 2.2.9 + + +class SMB2_Tree_Connect_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 TREE_CONNECT Request" + Command = 0x0003 + OFFSET = 8 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x9), + FlagsField( + "Flags", + 0, + -16, + ["CLUSTER_RECONNECT", "REDIRECT_TO_OWNER", "EXTENSION_PRESENT"], + ), + XLEShortField("PathBufferOffset", None), + LEShortField("PathLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + StrFieldUtf16("Path", b""), + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Path": 4, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Tree_Connect_Request, + Command=0x0003, +) + +# sect 2.2.10 + + +class SMB2_Tree_Connect_Response(_SMB2_Payload): + name = "SMB2 TREE_CONNECT Response" + Command = 0x0003 + fields_desc = [ + XLEShortField("StructureSize", 0x10), + ByteEnumField("ShareType", 0, {0x01: "DISK", 0x02: "PIPE", 0x03: "PRINT"}), + ByteField("Reserved", 0), + FlagsField( + "ShareFlags", + 0x30, + -32, + { + 0x00000010: "AUTO_CACHING", + 0x00000020: "VDO_CACHING", + 0x00000030: "NO_CACHING", + 0x00000001: "DFS", + 0x00000002: "DFS_ROOT", + 0x00000100: "RESTRICT_EXCLUSIVE_OPENS", + 0x00000200: "FORCE_SHARED_DELETE", + 0x00000400: "ALLOW_NAMESPACE_CACHING", + 0x00000800: "ACCESS_BASED_DIRECTORY_ENUM", + 0x00001000: "FORCE_LEVELII_OPLOCK", + 0x00002000: "ENABLE_HASH_V1", + 0x00004000: "ENABLE_HASH_V2", + 0x00008000: "ENCRYPT_DATA", + 0x00040000: "IDENTITY_REMOTING", + 0x00100000: "COMPRESS_DATA", + }, + ), + FlagsField( + "Capabilities", + 0, + -32, + { + 0x00000008: "DFS", + 0x00000010: "CONTINUOUS_AVAILABILITY", + 0x00000020: "SCALEOUT", + 0x00000040: "CLUSTER", + 0x00000080: "ASYMMETRIC", + 0x00000100: "REDIRECT_TO_OWNER", + }, + ), + FlagsField("MaximalAccess", 0, -32, SMB2_ACCESS_FLAGS_FILE), + ] + + +bind_top_down(SMB2_Header, SMB2_Tree_Connect_Response, Command=0x0003, Flags=1) + +# sect 2.2.11 + + +class SMB2_Tree_Disconnect_Request(_SMB2_Payload): + name = "SMB2 TREE_DISCONNECT Request" + Command = 0x0004 + fields_desc = [ + XLEShortField("StructureSize", 0x4), + XLEShortField("Reserved", 0), + ] + + +bind_top_down(SMB2_Header, SMB2_Tree_Disconnect_Request, Command=0x0004) + +# sect 2.2.12 + + +class SMB2_Tree_Disconnect_Response(_SMB2_Payload): + name = "SMB2 TREE_DISCONNECT Response" + Command = 0x0004 + fields_desc = [ + XLEShortField("StructureSize", 0x4), + XLEShortField("Reserved", 0), + ] + + +bind_top_down(SMB2_Header, SMB2_Tree_Disconnect_Response, Command=0x0004, Flags=1) + + +# sect 2.2.14.1 + + +class SMB2_FILEID(Packet): + fields_desc = [XLELongField("Persistent", 0), XLELongField("Volatile", 0)] + + def __hash__(self): + return self.Persistent + self.Volatile << 64 + + def default_payload_class(self, payload): + return conf.padding_layer + + +# sect 2.2.14.2 + + +class SMB2_CREATE_DURABLE_HANDLE_RESPONSE(Packet): + fields_desc = [ + XStrFixedLenField("Reserved", b"\x00" * 8, length=8), + ] + + +class SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE(Packet): + fields_desc = [ + LEIntEnumField("QueryStatus", 0, STATUS_ERREF), + FlagsField("MaximalAccess", 0, -32, SMB2_ACCESS_FLAGS_FILE), + ] + + +class SMB2_CREATE_QUERY_ON_DISK_ID(Packet): + fields_desc = [ + XLELongField("DiskFileId", 0), + XLELongField("VolumeId", 0), + XStrFixedLenField("Reserved", b"", length=16), + ] + + +class SMB2_CREATE_RESPONSE_LEASE(Packet): + fields_desc = [ + UUIDField("LeaseKey", None), + FlagsField( + "LeaseState", + 0x7, + -32, + { + 0x01: "SMB2_LEASE_READ_CACHING", + 0x02: "SMB2_LEASE_HANDLE_CACHING", + 0x04: "SMB2_LEASE_WRITE_CACHING", + }, + ), + FlagsField( + "LeaseFlags", + 0, + -32, + { + 0x02: "SMB2_LEASE_FLAG_BREAK_IN_PROGRESS", + 0x04: "SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET", + }, + ), + LELongField("LeaseDuration", 0), + ] + + +class SMB2_CREATE_RESPONSE_LEASE_V2(Packet): + fields_desc = [ + SMB2_CREATE_RESPONSE_LEASE, + UUIDField("ParentLeaseKey", None), + LEShortField("Epoch", 0), + LEShortField("Reserved", 0), + ] + + +class SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2(Packet): + fields_desc = [ + LEIntField("Timeout", 0), + FlagsField( + "Flags", + 0, + -32, + { + 0x02: "SMB2_DHANDLE_FLAG_PERSISTENT", + }, + ), + ] + + +# sect 2.2.13 + + +class SMB2_CREATE_DURABLE_HANDLE_REQUEST(Packet): + fields_desc = [ + XStrFixedLenField("DurableRequest", b"", length=16), + ] + + +class SMB2_CREATE_DURABLE_HANDLE_RECONNECT(Packet): + fields_desc = [ + PacketField("Data", SMB2_FILEID(), SMB2_FILEID), + ] + + +class SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST(Packet): + fields_desc = [ + LELongField("Timestamp", 0), + ] + + +class SMB2_CREATE_ALLOCATION_SIZE(Packet): + fields_desc = [ + LELongField("AllocationSize", 0), + ] + + +class SMB2_CREATE_TIMEWARP_TOKEN(Packet): + fields_desc = [ + LELongField("Timestamp", 0), + ] + + +class SMB2_CREATE_REQUEST_LEASE(Packet): + fields_desc = [ + SMB2_CREATE_RESPONSE_LEASE, + ] + + +class SMB2_CREATE_REQUEST_LEASE_V2(Packet): + fields_desc = [ + SMB2_CREATE_RESPONSE_LEASE_V2, + ] + + +class SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2(Packet): + fields_desc = [ + SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2, + XStrFixedLenField("Reserved", b"", length=8), + UUIDField("CreateGuid", 0x0, uuid_fmt=UUIDField.FORMAT_LE), + ] + + +class SMB2_CREATE_DURABLE_HANDLE_RECONNECT_V2(Packet): + fields_desc = [ + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + UUIDField("CreateGuid", 0x0, uuid_fmt=UUIDField.FORMAT_LE), + FlagsField( + "Flags", + 0, + -32, + { + 0x02: "SMB2_DHANDLE_FLAG_PERSISTENT", + }, + ), + ] + + +class SMB2_CREATE_APP_INSTANCE_ID(Packet): + fields_desc = [ + XLEShortField("StructureSize", 0x14), + LEShortField("Reserved", 0), + XStrFixedLenField("AppInstanceId", b"", length=16), + ] + + +class SMB2_CREATE_APP_INSTANCE_VERSION(Packet): + fields_desc = [ + XLEShortField("StructureSize", 0x18), + LEShortField("Reserved", 0), + LEIntField("Padding", 0), + LELongField("AppInstanceVersionHigh", 0), + LELongField("AppInstanceVersionLow", 0), + ] + + +class SMB2_Create_Context(_NTLMPayloadPacket): + name = "SMB2 CREATE CONTEXT" + OFFSET = 16 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + LEIntField("Next", None), + XLEShortField("NameBufferOffset", None), + LEShortField("NameLen", None), + ShortField("Reserved", 0), + XLEShortField("DataBufferOffset", None), + LEIntField("DataLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + PadField( + StrLenField("Name", b"", length_from=lambda pkt: pkt.NameLen), + 8, + ), + # Must be padded on 8-octet alignment + PacketLenField( + "Data", None, conf.raw_layer, length_from=lambda pkt: pkt.DataLen + ), + ], + force_order=["Name", "Data"], + ), + StrLenField( + "pad", + b"", + length_from=lambda x: ( + ( + x.Next + - max( + x.DataBufferOffset + x.DataLen, x.NameBufferOffset + x.NameLen + ) + ) + if x.Next + else 0 + ), + ), + ] + + def post_dissect(self, s): + if not self.DataLen: + return s + try: + if isinstance(self.parent, SMB2_Create_Request): + data_cls = { + b"DHnQ": SMB2_CREATE_DURABLE_HANDLE_REQUEST, + b"DHnC": SMB2_CREATE_DURABLE_HANDLE_RECONNECT, + b"AISi": SMB2_CREATE_ALLOCATION_SIZE, + b"MxAc": SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST, + b"TWrp": SMB2_CREATE_TIMEWARP_TOKEN, + b"QFid": SMB2_CREATE_QUERY_ON_DISK_ID, + b"RqLs": SMB2_CREATE_REQUEST_LEASE, + b"DH2Q": SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2, + b"DH2C": SMB2_CREATE_DURABLE_HANDLE_RECONNECT_V2, + # 3.1.1 only + b"E\xbc\xa6j\xef\xa7\xf7J\x90\x08\xfaF.\x14Mt": SMB2_CREATE_APP_INSTANCE_ID, # noqa: E501 + b"\xb9\x82\xd0\xb7;V\x07O\xa0{RJ\x81\x16\xa0\x10": SMB2_CREATE_APP_INSTANCE_VERSION, # noqa: E501 + }[self.Name] + if self.Name == b"RqLs" and self.DataLen > 32: + data_cls = SMB2_CREATE_REQUEST_LEASE_V2 + elif isinstance(self.parent, SMB2_Create_Response): + data_cls = { + b"DHnQ": SMB2_CREATE_DURABLE_HANDLE_RESPONSE, + b"MxAc": SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE, + b"QFid": SMB2_CREATE_QUERY_ON_DISK_ID, + b"RqLs": SMB2_CREATE_RESPONSE_LEASE, + b"DH2Q": SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2, + }[self.Name] + if self.Name == b"RqLs" and self.DataLen > 32: + data_cls = SMB2_CREATE_RESPONSE_LEASE_V2 + else: + return s + except KeyError: + return s + self.Data = data_cls(self.Data.load) + return s + + def default_payload_class(self, _): + return conf.padding_layer + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _NTLM_post_build( + self, + pkt, + self.OFFSET, + { + "Name": 4, + "Data": 10, + }, + config=[ + ( + "BufferOffset", + { + "Name": _NTLM_ENUM.OFFSET, + "Data": _NTLM_ENUM.OFFSET | _NTLM_ENUM.PAD8, + }, + ), + ("Len", _NTLM_ENUM.LEN), + ], + ) + + pay + ) + + +# sect 2.2.13 + +SMB2_OPLOCK_LEVELS = { + 0x00: "SMB2_OPLOCK_LEVEL_NONE", + 0x01: "SMB2_OPLOCK_LEVEL_II", + 0x08: "SMB2_OPLOCK_LEVEL_EXCLUSIVE", + 0x09: "SMB2_OPLOCK_LEVEL_BATCH", + 0xFF: "SMB2_OPLOCK_LEVEL_LEASE", +} + + +class SMB2_Create_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 CREATE Request" + Command = 0x0005 + OFFSET = 56 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x39), + ByteField("ShareType", 0), + ByteEnumField("RequestedOplockLevel", 0, SMB2_OPLOCK_LEVELS), + LEIntEnumField( + "ImpersonationLevel", + 0, + { + 0x00000000: "Anonymous", + 0x00000001: "Identification", + 0x00000002: "Impersonation", + 0x00000003: "Delegate", + }, + ), + LELongField("SmbCreateFlags", 0), + LELongField("Reserved", 0), + FlagsField("DesiredAccess", 0, -32, SMB2_ACCESS_FLAGS_FILE), + FlagsField("FileAttributes", 0x00000080, -32, FileAttributes), + FlagsField( + "ShareAccess", + 0, + -32, + { + 0x00000001: "FILE_SHARE_READ", + 0x00000002: "FILE_SHARE_WRITE", + 0x00000004: "FILE_SHARE_DELETE", + }, + ), + LEIntEnumField( + "CreateDisposition", + 1, + { + 0x00000000: "FILE_SUPERSEDE", + 0x00000001: "FILE_OPEN", + 0x00000002: "FILE_CREATE", + 0x00000003: "FILE_OPEN_IF", + 0x00000004: "FILE_OVERWRITE", + 0x00000005: "FILE_OVERWRITE_IF", + }, + ), + FlagsField( + "CreateOptions", + 0, + -32, + { + 0x00000001: "FILE_DIRECTORY_FILE", + 0x00000002: "FILE_WRITE_THROUGH", + 0x00000004: "FILE_SEQUENTIAL_ONLY", + 0x00000008: "FILE_NO_INTERMEDIATE_BUFFERING", + 0x00000010: "FILE_SYNCHRONOUS_IO_ALERT", + 0x00000020: "FILE_SYNCHRONOUS_IO_NONALERT", + 0x00000040: "FILE_NON_DIRECTORY_FILE", + 0x00000100: "FILE_COMPLETE_IF_OPLOCKED", + 0x00000200: "FILE_RANDOM_ACCESS", + 0x00001000: "FILE_DELETE_ON_CLOSE", + 0x00002000: "FILE_OPEN_BY_FILE_ID", + 0x00004000: "FILE_OPEN_FOR_BACKUP_INTENT", + 0x00008000: "FILE_NO_COMPRESSION", + 0x00000400: "FILE_OPEN_REMOTE_INSTANCE", + 0x00010000: "FILE_OPEN_REQUIRING_OPLOCK", + 0x00020000: "FILE_DISALLOW_EXCLUSIVE", + 0x00100000: "FILE_RESERVE_OPFILTER", + 0x00200000: "FILE_OPEN_REPARSE_POINT", + 0x00400000: "FILE_OPEN_NO_RECALL", + 0x00800000: "FILE_OPEN_FOR_FREE_SPACE_QUERY", + }, + ), + XLEShortField("NameBufferOffset", None), + LEShortField("NameLen", None), + XLEIntField("CreateContextsBufferOffset", None), + LEIntField("CreateContextsLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + StrFieldUtf16("Name", b""), + _NextPacketListField( + "CreateContexts", + [], + SMB2_Create_Context, + length_from=lambda pkt: pkt.CreateContextsLen, + ), + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + if len(pkt) == 0x38: + # 'In the request, the Buffer field MUST be at least one byte in length.' + pkt += b"\x00" + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Name": 44, + "CreateContexts": 48, + }, + ) + + pay + ) + + +bind_top_down(SMB2_Header, SMB2_Create_Request, Command=0x0005) + + +# sect 2.2.14 + + +class SMB2_Create_Response(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 CREATE Response" + Command = 0x0005 + OFFSET = 88 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x59), + ByteEnumField("OplockLevel", 0, SMB2_OPLOCK_LEVELS), + FlagsField("Flags", 0, -8, {0x01: "SMB2_CREATE_FLAG_REPARSEPOINT"}), + LEIntEnumField( + "CreateAction", + 1, + { + 0x00000000: "FILE_SUPERSEDED", + 0x00000001: "FILE_OPENED", + 0x00000002: "FILE_CREATED", + 0x00000003: "FILE_OVERWRITEN", + }, + ), + FileNetworkOpenInformation, + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + XLEIntField("CreateContextsBufferOffset", None), + LEIntField("CreateContextsLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + _NextPacketListField( + "CreateContexts", + [], + SMB2_Create_Context, + length_from=lambda pkt: pkt.CreateContextsLen, + ), + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "CreateContexts": 80, + }, + ) + + pay + ) + + +bind_top_down(SMB2_Header, SMB2_Create_Response, Command=0x0005, Flags=1) + +# sect 2.2.15 + + +class SMB2_Close_Request(_SMB2_Payload): + name = "SMB2 CLOSE Request" + Command = 0x0006 + fields_desc = [ + XLEShortField("StructureSize", 0x18), + FlagsField("Flags", 0, -16, ["SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB"]), + LEIntField("Reserved", 0), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Close_Request, + Command=0x0006, +) + +# sect 2.2.16 + + +class SMB2_Close_Response(_SMB2_Payload): + name = "SMB2 CLOSE Response" + Command = 0x0006 + FileAttributes = 0 + CreationTime = 0 + LastAccessTime = 0 + LastWriteTime = 0 + ChangeTime = 0 + fields_desc = [ + XLEShortField("StructureSize", 0x3C), + FlagsField("Flags", 0, -16, ["SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB"]), + LEIntField("Reserved", 0), + ] + FileNetworkOpenInformation.fields_desc[:7] + + +bind_top_down( + SMB2_Header, + SMB2_Close_Response, + Command=0x0006, + Flags=1, +) + +# sect 2.2.19 + + +class SMB2_Read_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 READ Request" + Command = 0x0008 + OFFSET = 48 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x31), + ByteField("Padding", 0x00), + FlagsField( + "Flags", + 0, + -8, + { + 0x01: "SMB2_READFLAG_READ_UNBUFFERED", + 0x02: "SMB2_READFLAG_REQUEST_COMPRESSED", + }, + ), + LEIntField("Length", 4280), + LELongField("Offset", 0), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + LEIntField("MinimumCount", 0), + LEIntEnumField( + "Channel", + 0, + { + 0x00000000: "SMB2_CHANNEL_NONE", + 0x00000001: "SMB2_CHANNEL_RDMA_V1", + 0x00000002: "SMB2_CHANNEL_RDMA_V1_INVALIDATE", + 0x00000003: "SMB2_CHANNEL_RDMA_TRANSFORM", + }, + ), + LEIntField("RemainingBytes", 0), + LEShortField("ReadChannelInfoBufferOffset", None), + LEShortField("ReadChannelInfoLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + StrLenField( + "ReadChannelInfo", + b"", + length_from=lambda pkt: pkt.ReadChannelInfoLen, + ) + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + if len(pkt) == 0x30: + # 'The first byte of the Buffer field MUST be set to 0.' + pkt += b"\x00" + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "ReadChannelInfo": 44, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Read_Request, + Command=0x0008, +) + +# sect 2.2.20 + + +class SMB2_Read_Response(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 READ Response" + Command = 0x0008 + OFFSET = 16 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x11), + LEShortField("DataBufferOffset", None), + LEIntField("DataLen", None), + LEIntField("DataRemaining", 0), + FlagsField( + "Flags", + 0, + -32, + { + 0x01: "SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM", + }, + ), + _NTLMPayloadField( + "Buffer", + OFFSET, + [StrLenField("Data", b"", length_from=lambda pkt: pkt.DataLen)], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Data": 2, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Read_Response, + Command=0x0008, + Flags=1, +) + + +# sect 2.2.21 + + +class SMB2_Write_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 WRITE Request" + Command = 0x0009 + OFFSET = 48 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x31), + LEShortField("DataBufferOffset", None), + LEIntField("DataLen", None), + LELongField("Offset", 0), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + LEIntEnumField( + "Channel", + 0, + { + 0x00000000: "SMB2_CHANNEL_NONE", + 0x00000001: "SMB2_CHANNEL_RDMA_V1", + 0x00000002: "SMB2_CHANNEL_RDMA_V1_INVALIDATE", + 0x00000003: "SMB2_CHANNEL_RDMA_TRANSFORM", + }, + ), + LEIntField("RemainingBytes", 0), + LEShortField("WriteChannelInfoBufferOffset", None), + LEShortField("WriteChannelInfoLen", None), + FlagsField( + "Flags", + 0, + -32, + { + 0x00000001: "SMB2_WRITEFLAG_WRITE_THROUGH", + 0x00000002: "SMB2_WRITEFLAG_WRITE_UNBUFFERED", + }, + ), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + StrLenField("Data", b"", length_from=lambda pkt: pkt.DataLen), + StrLenField( + "WriteChannelInfo", + b"", + length_from=lambda pkt: pkt.WriteChannelInfoLen, + ), + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Data": 2, + "WriteChannelInfo": 40, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Write_Request, + Command=0x0009, +) + +# sect 2.2.22 + + +class SMB2_Write_Response(_SMB2_Payload): + name = "SMB2 WRITE Response" + Command = 0x0009 + fields_desc = [ + XLEShortField("StructureSize", 0x11), + LEShortField("Reserved", 0), + LEIntField("Count", 0), + LEIntField("Remaining", 0), + LEShortField("WriteChannelInfoBufferOffset", 0), + LEShortField("WriteChannelInfoLen", 0), + ] + + +bind_top_down(SMB2_Header, SMB2_Write_Response, Command=0x0009, Flags=1) + +# sect 2.2.28 + + +class SMB2_Echo_Request(_SMB2_Payload): + name = "SMB2 ECHO Request" + Command = 0x000D + fields_desc = [ + XLEShortField("StructureSize", 0x4), + LEShortField("Reserved", 0), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Echo_Request, + Command=0x000D, +) + +# sect 2.2.29 + + +class SMB2_Echo_Response(_SMB2_Payload): + name = "SMB2 ECHO Response" + Command = 0x000D + fields_desc = [ + XLEShortField("StructureSize", 0x4), + LEShortField("Reserved", 0), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Echo_Response, + Command=0x000D, + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR +) + +# sect 2.2.30 + + +class SMB2_Cancel_Request(_SMB2_Payload): + name = "SMB2 CANCEL Request" + fields_desc = [ + XLEShortField("StructureSize", 0x4), + LEShortField("Reserved", 0), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Cancel_Request, + Command=0x0009, +) + +# sect 2.2.31.4 + + +class SMB2_IOCTL_Validate_Negotiate_Info_Request(Packet): + name = "SMB2 IOCTL Validate Negotiate Info" + fields_desc = ( + SMB2_Negotiate_Protocol_Request.fields_desc[4:6] + + SMB2_Negotiate_Protocol_Request.fields_desc[1:3][::-1] # Cap/GUID + + [SMB2_Negotiate_Protocol_Request.fields_desc[9]] # SecMod/DC # Dialects + ) + + +# sect 2.2.31 + + +class _SMB2_IOCTL_Request_PacketLenField(PacketLenField): + def m2i(self, pkt, m): + if pkt.CtlCode == 0x00140204: # FSCTL_VALIDATE_NEGOTIATE_INFO + return SMB2_IOCTL_Validate_Negotiate_Info_Request(m) + elif pkt.CtlCode == 0x00060194: # FSCTL_DFS_GET_REFERRALS + return SMB2_IOCTL_REQ_GET_DFS_Referral(m) + elif pkt.CtlCode == 0x00094264: # FSCTL_OFFLOAD_READ + return SMB2_IOCTL_OFFLOAD_READ_Request(m) + return conf.raw_layer(m) + + +class SMB2_IOCTL_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 IOCTL Request" + Command = 0x000B + OFFSET = 56 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + deprecated_fields = { + "IntputCount": ("InputLen", "alias"), + "OutputCount": ("OutputLen", "alias"), + } + fields_desc = [ + XLEShortField("StructureSize", 0x39), + LEShortField("Reserved", 0), + LEIntEnumField( + "CtlCode", + 0, + { + 0x00060194: "FSCTL_DFS_GET_REFERRALS", + 0x0011400C: "FSCTL_PIPE_PEEK", + 0x00110018: "FSCTL_PIPE_WAIT", + 0x0011C017: "FSCTL_PIPE_TRANSCEIVE", + 0x001440F2: "FSCTL_SRV_COPYCHUNK", + 0x00144064: "FSCTL_SRV_ENUMERATE_SNAPSHOTS", + 0x00140078: "FSCTL_SRV_REQUEST_RESUME_KEY", + 0x001441BB: "FSCTL_SRV_READ_HASH", + 0x001480F2: "FSCTL_SRV_COPYCHUNK_WRITE", + 0x001401D4: "FSCTL_LMR_REQUEST_RESILIENCY", + 0x001401FC: "FSCTL_QUERY_NETWORK_INTERFACE_INFO", + 0x000900A4: "FSCTL_SET_REPARSE_POINT", + 0x000601B0: "FSCTL_DFS_GET_REFERRALS_EX", + 0x00098208: "FSCTL_FILE_LEVEL_TRIM", + 0x00140204: "FSCTL_VALIDATE_NEGOTIATE_INFO", + 0x00094264: "FSCTL_OFFLOAD_READ", + }, + ), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + LEIntField("InputBufferOffset", None), + LEIntField("InputLen", None), # Called InputCount but it's a length + LEIntField("MaxInputResponse", 0), + LEIntField("OutputBufferOffset", None), + LEIntField("OutputLen", None), # Called OutputCount. + LEIntField("MaxOutputResponse", 65535), + FlagsField("Flags", 0, -32, {0x00000001: "SMB2_0_IOCTL_IS_FSCTL"}), + LEIntField("Reserved2", 0), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + _SMB2_IOCTL_Request_PacketLenField( + "Input", None, conf.raw_layer, length_from=lambda pkt: pkt.InputLen + ), + _SMB2_IOCTL_Request_PacketLenField( + "Output", + None, + conf.raw_layer, + length_from=lambda pkt: pkt.OutputLen, + ), + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Input": 24, + "Output": 36, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_IOCTL_Request, + Command=0x000B, +) + +# sect 2.2.32.5 + + +class SOCKADDR_STORAGE(Packet): + fields_desc = [ + LEShortEnumField("Family", 0x0002, {0x0002: "IPv4", 0x0017: "IPv6"}), + ShortField("Port", 0), + # IPv4 + ConditionalField( + IPField("IPv4Adddress", None), + lambda pkt: pkt.Family == 0x0002, + ), + ConditionalField( + StrFixedLenField("Reserved", b"", length=8), + lambda pkt: pkt.Family == 0x0002, + ), + # IPv6 + ConditionalField( + LEIntField("FlowInfo", 0), + lambda pkt: pkt.Family == 0x00017, + ), + ConditionalField( + IP6Field("IPv6Address", None), + lambda pkt: pkt.Family == 0x00017, + ), + ConditionalField( + LEIntField("ScopeId", 0), + lambda pkt: pkt.Family == 0x00017, + ), + ] + + def default_payload_class(self, _): + return conf.padding_layer + + +class NETWORK_INTERFACE_INFO(Packet): + fields_desc = [ + LEIntField("Next", None), # 0 = no next entry + LEIntField("IfIndex", 1), + FlagsField( + "Capability", + 1, + -32, + { + 0x00000001: "RSS_CAPABLE", + 0x00000002: "RDMA_CAPABLE", + }, + ), + LEIntField("Reserved", 0), + ScalingField("LinkSpeed", 10000000000, fmt=" bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Input": 24, + "Output": 32, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_IOCTL_Response, + Command=0x000B, + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR +) + +# sect 2.2.33 + + +class SMB2_Query_Directory_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 QUERY DIRECTORY Request" + Command = 0x000E + OFFSET = 32 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x21), + ByteEnumField("FileInformationClass", 0x1, FileInformationClasses), + FlagsField( + "Flags", + 0, + -8, + { + 0x01: "SMB2_RESTART_SCANS", + 0x02: "SMB2_RETURN_SINGLE_ENTRY", + 0x04: "SMB2_INDEX_SPECIFIED", + 0x10: "SMB2_REOPEN", + }, + ), + LEIntField("FileIndex", 0), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + LEShortField("FileNameBufferOffset", None), + LEShortField("FileNameLen", None), + LEIntField("OutputBufferLength", 65535), + _NTLMPayloadField("Buffer", OFFSET, [StrFieldUtf16("FileName", b"")]), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "FileName": 24, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Query_Directory_Request, + Command=0x000E, +) + +# sect 2.2.34 + + +class SMB2_Query_Directory_Response(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 QUERY DIRECTORY Response" + Command = 0x000E + OFFSET = 8 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x9), + LEShortField("OutputBufferOffset", None), + LEIntField("OutputLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + # TODO + StrFixedLenField("Output", b"", length_from=lambda pkt: pkt.OutputLen) + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Output": 2, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Query_Directory_Response, + Command=0x000E, + Flags=1, +) + +# sect 2.2.35 + + +class SMB2_Change_Notify_Request(_SMB2_Payload): + name = "SMB2 CHANGE NOTIFY Request" + Command = 0x000F + fields_desc = [ + XLEShortField("StructureSize", 0x20), + FlagsField( + "Flags", + 0, + -16, + { + 0x0001: "SMB2_WATCH_TREE", + }, + ), + LEIntField("OutputBufferLength", 2048), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + FlagsField( + "CompletionFilter", + 0, + -32, + { + 0x00000001: "FILE_NOTIFY_CHANGE_FILE_NAME", + 0x00000002: "FILE_NOTIFY_CHANGE_DIR_NAME", + 0x00000004: "FILE_NOTIFY_CHANGE_ATTRIBUTES", + 0x00000008: "FILE_NOTIFY_CHANGE_SIZE", + 0x00000010: "FILE_NOTIFY_CHANGE_LAST_WRITE", + 0x00000020: "FILE_NOTIFY_CHANGE_LAST_ACCESS", + 0x00000040: "FILE_NOTIFY_CHANGE_CREATION", + 0x00000080: "FILE_NOTIFY_CHANGE_EA", + 0x00000100: "FILE_NOTIFY_CHANGE_SECURITY", + 0x00000200: "FILE_NOTIFY_CHANGE_STREAM_NAME", + 0x00000400: "FILE_NOTIFY_CHANGE_STREAM_SIZE", + 0x00000800: "FILE_NOTIFY_CHANGE_STREAM_WRITE", + }, + ), + LEIntField("Reserved", 0), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Change_Notify_Request, + Command=0x000F, +) + +# sect 2.2.36 + + +class SMB2_Change_Notify_Response(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 CHANGE NOTIFY Response" + Command = 0x000F + OFFSET = 8 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x9), + LEShortField("OutputBufferOffset", None), + LEIntField("OutputLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + _NextPacketListField( + "Output", + [], + FILE_NOTIFY_INFORMATION, + length_from=lambda pkt: pkt.OutputLen, + max_count=1000, + ) + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Output": 2, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Change_Notify_Response, + Command=0x000F, + Flags=1, +) + +# sect 2.2.37 + + +class FILE_GET_QUOTA_INFORMATION(Packet): + fields_desc = [ + IntField("NextEntryOffset", 0), + FieldLenField("SidLength", None, length_of="Sid"), + StrLenField("Sid", b"", length_from=lambda x: x.SidLength), + StrLenField( + "pad", + b"", + length_from=lambda x: ( + (x.NextEntryOffset - x.SidLength) if x.NextEntryOffset else 0 + ), + ), + ] + + +class SMB2_Query_Quota_Info(Packet): + fields_desc = [ + ByteField("ReturnSingle", 0), + ByteField("ReturnBoolean", 0), + ShortField("Reserved", 0), + LEIntField("SidListLength", 0), + LEIntField("StartSidLength", 0), + LEIntField("StartSidOffset", 0), + StrLenField("pad", b"", length_from=lambda x: x.StartSidOffset), + MultipleTypeField( + [ + ( + PacketListField( + "SidBuffer", + [], + FILE_GET_QUOTA_INFORMATION, + length_from=lambda x: x.SidListLength, + ), + lambda x: x.SidListLength, + ), + ( + StrLenField( + "SidBuffer", b"", length_from=lambda x: x.StartSidLength + ), + lambda x: x.StartSidLength, + ), + ], + StrFixedLenField("SidBuffer", b"", length=0), + ), + ] + + +SMB2_INFO_TYPE = { + 0x01: "SMB2_0_INFO_FILE", + 0x02: "SMB2_0_INFO_FILESYSTEM", + 0x03: "SMB2_0_INFO_SECURITY", + 0x04: "SMB2_0_INFO_QUOTA", +} + +SMB2_ADDITIONAL_INFORMATION = { + 0x00000001: "OWNER_SECURITY_INFORMATION", + 0x00000002: "GROUP_SECURITY_INFORMATION", + 0x00000004: "DACL_SECURITY_INFORMATION", + 0x00000008: "SACL_SECURITY_INFORMATION", + 0x00000010: "LABEL_SECURITY_INFORMATION", + 0x00000020: "ATTRIBUTE_SECURITY_INFORMATION", + 0x00000040: "SCOPE_SECURITY_INFORMATION", + 0x00010000: "BACKUP_SECURITY_INFORMATION", +} + + +class SMB2_Query_Info_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 QUERY INFO Request" + Command = 0x0010 + OFFSET = 40 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x29), + ByteEnumField( + "InfoType", + 0, + SMB2_INFO_TYPE, + ), + ByteEnumField("FileInfoClass", 0, FileInformationClasses), + LEIntField("OutputBufferLength", 0), + XLEIntField("InputBufferOffset", None), # Short + Reserved = Int + LEIntField("InputLen", None), + FlagsField( + "AdditionalInformation", + 0, + -32, + SMB2_ADDITIONAL_INFORMATION, + ), + FlagsField( + "Flags", + 0, + -32, + { + 0x00000001: "SL_RESTART_SCAN", + 0x00000002: "SL_RETURN_SINGLE_ENTRY", + 0x00000004: "SL_INDEX_SPECIFIED", + }, + ), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + MultipleTypeField( + [ + ( + # QUOTA + PacketListField( + "Input", + None, + SMB2_Query_Quota_Info, + length_from=lambda pkt: pkt.InputLen, + ), + lambda pkt: pkt.InfoType == 0x04, + ), + ], + StrLenField("Input", b"", length_from=lambda pkt: pkt.InputLen), + ), + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Input": 4, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Query_Info_Request, + Command=0x00010, +) + + +class SMB2_Query_Info_Response(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 QUERY INFO Response" + Command = 0x0010 + OFFSET = 8 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x9), + LEShortField("OutputBufferOffset", None), + LEIntField("OutputLen", None), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + # TODO + StrFixedLenField("Output", b"", length_from=lambda pkt: pkt.OutputLen) + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Output": 2, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Query_Info_Response, + Command=0x00010, + Flags=1, +) + + +# sect 2.2.39 + + +class SMB2_Set_Info_Request(_SMB2_Payload, _NTLMPayloadPacket): + name = "SMB2 SET INFO Request" + Command = 0x0011 + OFFSET = 32 + 64 + _NTLM_PAYLOAD_FIELD_NAME = "Buffer" + fields_desc = [ + XLEShortField("StructureSize", 0x21), + ByteEnumField( + "InfoType", + 0, + SMB2_INFO_TYPE, + ), + ByteEnumField("FileInfoClass", 0, FileInformationClasses), + LEIntField("DataLen", None), + XLEIntField("DataBufferOffset", None), # Short + Reserved = Int + FlagsField( + "AdditionalInformation", + 0, + -32, + SMB2_ADDITIONAL_INFORMATION, + ), + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), + _NTLMPayloadField( + "Buffer", + OFFSET, + [ + MultipleTypeField( + [ + ( + # FILE + PacketLenField( + "Data", + None, + lambda x, _parent: _FileInformationClasses.get( + _parent.FileInfoClass, conf.raw_layer + )(x), + length_from=lambda pkt: pkt.DataLen, + ), + lambda pkt: pkt.InfoType == 0x01, + ), + ( + # QUOTA + PacketListField( + "Data", + None, + SMB2_Query_Quota_Info, + length_from=lambda pkt: pkt.DataLen, + ), + lambda pkt: pkt.InfoType == 0x04, + ), + ], + StrLenField("Data", b"", length_from=lambda pkt: pkt.DataLen), + ), + ], + ), + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Data": 4, + }, + ) + + pay + ) + + +bind_top_down( + SMB2_Header, + SMB2_Set_Info_Request, + Command=0x00011, +) + + +class SMB2_Set_Info_Response(_SMB2_Payload): + name = "SMB2 SET INFO Request" + Command = 0x0011 + fields_desc = [ + XLEShortField("StructureSize", 0x02), + ] + + +bind_top_down( + SMB2_Header, + SMB2_Set_Info_Response, + Command=0x00011, + Flags=1, +) + + +# sect 2.2.41 + + +class SMB2_Transform_Header(Packet): + name = "SMB2 Transform Header" + fields_desc = [ + StrFixedLenField("Start", b"\xfdSMB", 4), + XStrFixedLenField("Signature", 0, length=16), + XStrFixedLenField("Nonce", b"", length=16), + LEIntField("OriginalMessageSize", 0x0), + LEShortField("Reserved", 0), + LEShortEnumField( + "Flags", + 0x1, + { + 0x0001: "ENCRYPTED", + }, + ), + LELongField("SessionId", 0), + ] + + def decrypt(self, dialect, DecryptionKey, CipherId): + """ + [MS-SMB2] sect 3.2.5.1.1.1 - Decrypting the Message + """ + if not isinstance(self.payload, conf.raw_layer): + raise Exception("No payload to decrypt !") + + if "GCM" in CipherId: + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + + nonce = self.Nonce[:12] + cipher = AESGCM(DecryptionKey) + elif "CCM" in CipherId: + from cryptography.hazmat.primitives.ciphers.aead import AESCCM + + nonce = self.Nonce[:11] + cipher = AESCCM(DecryptionKey) + else: + raise Exception("Unknown CipherId !") + + # Decrypt the data + aad = self.self_build()[20:] + data = cipher.decrypt( + nonce, + self.payload.load + self.Signature, + aad, + ) + return SMB2_Header(data, _decrypted=True) + + +bind_layers(SMB2_Transform_Header, conf.raw_layer) + + +# sect 2.2.42.1 + + +class SMB2_Compression_Transform_Header(Packet): + name = "SMB2 Compression Transform Header" + fields_desc = [ + StrFixedLenField("Start", b"\xfcSMB", 4), + LEIntField("OriginalCompressedSegmentSize", 0x0), + LEShortEnumField("CompressionAlgorithm", 0, SMB2_COMPRESSION_ALGORITHMS), + LEShortEnumField( + "Flags", + 0x0, + { + 0x0000: "SMB2_COMPRESSION_FLAG_NONE", + 0x0001: "SMB2_COMPRESSION_FLAG_CHAINED", + }, + ), + XLEIntField("Offset_or_Length", 0), + ] + + +# [MS-DFSC] sect 2.2 + + +class SMB2_IOCTL_REQ_GET_DFS_Referral(Packet): + fields_desc = [ + LEShortField("MaxReferralLevel", 0), + StrNullFieldUtf16("RequestFileName", ""), + ] + + +class DFS_REFERRAL(Packet): + fields_desc = [ + LEShortField("Version", 1), + FieldLenField( + "Size", None, fmt="= 2: + version = struct.unpack(" bytes + if self.Size is None: + pkt = pkt[:2] + struct.pack(" bytes + # Note: Windows is smart and uses some sort of compression in the sense + # that it reuses fields that are used several times across ReferralBuffer. + # But we just do the dumb thing because it's 'easier', and do no compression. + offsets = { + # DFS_REFERRAL_ENTRY0 + "DFSPath": 12, + "DFSAlternatePath": 14, + "NetworkAddress": 16, + # DFS_REFERRAL_ENTRY1 + "SpecialName": 12, + "ExpandedName": 16, + } + # dataoffset = pointer in the ReferralBuffer + # entryoffset = pointer in the ReferralEntries + dataoffset = sum(len(x) for x in self.ReferralEntries) + entryoffset = 8 + for ref, buf in zip(self.ReferralEntries, self.ReferralBuffer): + for fld in buf.fields_desc: + off = entryoffset + offsets[fld.name] + if ref.getfieldval(fld.name + "Offset") is None and buf.getfieldval( + fld.name + ): + pkt = pkt[:off] + struct.pack("= 0x0300: + if self.Dialect == 0x0311: + label = b"SMBSigningKey\x00" + context = self.SessionPreauthIntegrityHashValue + else: + label = b"SMB2AESCMAC\x00" + context = b"SmbSign\x00" + # [MS-SMB2] sect 3.1.4.2 + if "256" in self.CipherId: + L = 256 + elif "128" in self.CipherId: + L = 128 + else: + raise ValueError + self.SigningKey = SP800108_KDFCTR( + self.sspcontext.SessionKey[:16], + label, + context, + L, + ) + # EncryptionKey / DecryptionKey + if self.Dialect == 0x0311: + if IsClient: + label_out = b"SMBC2SCipherKey\x00" + label_in = b"SMBS2CCipherKey\x00" + else: + label_out = b"SMBS2CCipherKey\x00" + label_in = b"SMBC2SCipherKey\x00" + context_out = context_in = self.SessionPreauthIntegrityHashValue + else: + label_out = label_in = b"SMB2AESCCM\x00" + if IsClient: + context_out = b"ServerIn \x00" # extra space per spec + context_in = b"ServerOut\x00" + else: + context_out = b"ServerOut\x00" + context_in = b"ServerIn \x00" + self.EncryptionKey = SP800108_KDFCTR( + self.sspcontext.SessionKey[: L // 8], + label_out, + context_out, + L, + ) + self.DecryptionKey = SP800108_KDFCTR( + self.sspcontext.SessionKey[: L // 8], + label_in, + context_in, + L, + ) + elif self.Dialect <= 0x0210: + self.SigningKey = self.sspcontext.SessionKey[:16] + else: + raise ValueError("Hmmm ? >:(") + + def computeSMBConnectionPreauth(self, *negopkts): + if self.Dialect and self.Dialect >= 0x0311: # SMB 3.1.1 only + # [MS-SMB2] 3.3.5.4 + # TODO: handle SMB2_SESSION_FLAG_BINDING + if self.ConnectionPreauthIntegrityHashValue is None: + # New auth or failure + self.ConnectionPreauthIntegrityHashValue = b"\x00" * 64 + # Calculate the *Connection* PreauthIntegrityHashValue + for negopkt in negopkts: + self.ConnectionPreauthIntegrityHashValue = ( + SMB2computePreauthIntegrityHashValue( + self.ConnectionPreauthIntegrityHashValue, + negopkt, + HashId=self.PreauthIntegrityHashId, + ) + ) + + def computeSMBSessionPreauth(self, *sesspkts): + if self.Dialect and self.Dialect >= 0x0311: # SMB 3.1.1 only + # [MS-SMB2] 3.3.5.5.3 + if self.SessionPreauthIntegrityHashValue is None: + # New auth or failure + self.SessionPreauthIntegrityHashValue = ( + self.ConnectionPreauthIntegrityHashValue + ) + # Calculate the *Session* PreauthIntegrityHashValue + for sesspkt in sesspkts: + self.SessionPreauthIntegrityHashValue = ( + SMB2computePreauthIntegrityHashValue( + self.SessionPreauthIntegrityHashValue, + sesspkt, + HashId=self.PreauthIntegrityHashId, + ) + ) + + # I/O + + def in_pkt(self, pkt): + """ + Incoming SMB packet + """ + if SMB2_Transform_Header in pkt: + # Packet is encrypted + pkt = pkt[SMB2_Transform_Header].decrypt( + self.Dialect, + self.DecryptionKey, + CipherId=self.CipherId, + ) + # Signature is verified in SMBStreamSocket + return pkt + + def out_pkt(self, pkt, Compounded=False, ForceSign=False, ForceEncrypt=False): + """ + Outgoing SMB packet + + :param pkt: the packet to send + :param Compound: if True, will be stack to be send with the next + un-compounded packet + :param ForceSign: if True, force to sign the packet. + :param ForceEncrypt: if True, force to encrypt the packet. + + Handles: + - handle compounded requests (if any): [MS-SMB2] 3.3.5.2.7 + - handles signing and encryption (if required) + """ + # Note: impacket and wireshark get crazy on compounded+signature, but + # windows+samba tells we're right :D + if SMB2_Header in pkt: + if self.CompoundQueue: + # this is a subsequent compound: only keep the SMB2 + pkt = pkt[SMB2_Header] + if Compounded: + # [MS-SMB2] 3.2.4.1.4 + # "Compounded requests MUST be aligned on 8-byte boundaries; the + # last request of the compounded requests does not need to be padded to + # an 8-byte boundary." + # [MS-SMB2] 3.1.4.1 + # "If the message is part of a compounded chain, any + # padding at the end of the message MUST be used in the hash + # computation." + length = len(pkt[SMB2_Header]) + padlen = (-length) % 8 + if padlen: + pkt.add_payload(b"\x00" * padlen) + pkt[SMB2_Header].NextCommand = length + padlen + if ( + self.Dialect + and self.SigningKey + and (ForceSign or self.SigningRequired and not ForceEncrypt) + ): + # [MS-SMB2] sect 3.2.4.1.1 - Signing + smb = pkt[SMB2_Header] + smb.Flags += "SMB2_FLAGS_SIGNED" + smb.sign( + self.Dialect, + self.SigningKey, + # SMB 3.1.1 parameters: + SigningAlgorithmId=self.SigningAlgorithmId, + IsClient=False, + ) + if Compounded: + # There IS a next compound. Store in queue + self.CompoundQueue.append(pkt) + return [] + else: + # If there are any compounded responses in store, sum them + if self.CompoundQueue: + pkt = functools.reduce(lambda x, y: x / y, self.CompoundQueue) / pkt + self.CompoundQueue.clear() + if self.EncryptionKey and ( + ForceEncrypt or self.EncryptData or self.TreeEncryptData + ): + # [MS-SMB2] sect 3.1.4.3 - Encrypting the message + smb = pkt[SMB2_Header] + assert not smb.Flags.SMB2_FLAGS_SIGNED + smbt = smb.encrypt( + self.Dialect, + self.EncryptionKey, + CipherId=self.CipherId, + ) + if smb.underlayer: + # If there's an underlayer, replace current SMB header + smb.underlayer.payload = smbt + else: + smb = smbt + return [pkt] + + def process(self, pkt: Packet): + # Called when passively sniffing + pkt = super(SMBSession, self).process(pkt) + if pkt is not None and SMB2_Header in pkt: + return self.in_pkt(pkt) + return pkt diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py new file mode 100644 index 00000000000..36a3c1fd451 --- /dev/null +++ b/scapy/layers/smbclient.py @@ -0,0 +1,1913 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +SMB 1 / 2 Client Automaton + + +.. note:: + You will find more complete documentation for this layer over at + `SMB `_ +""" + +import io +import os +import pathlib +import socket +import time +import threading + +from scapy.automaton import ATMT, Automaton, ObjectPipe +from scapy.config import conf +from scapy.error import Scapy_Exception +from scapy.fields import UTCTimeField +from scapy.supersocket import SuperSocket +from scapy.utils import ( + CLIUtil, + pretty_list, + human_size, +) +from scapy.volatile import RandUUID + +from scapy.layers.dcerpc import NDRUnion, find_dcerpc_interface +from scapy.layers.gssapi import ( + GSS_C_FLAGS, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_DEFECTIVE_TOKEN, +) +from scapy.layers.msrpce.raw.ms_srvs import ( + LPSHARE_ENUM_STRUCT, + NetrShareEnum_Request, + SHARE_INFO_1_CONTAINER, +) +from scapy.layers.ntlm import ( + NTLMSSP, +) +from scapy.layers.smb import ( + SMBNegotiate_Request, + SMBNegotiate_Response_Extended_Security, + SMBNegotiate_Response_Security, + SMBSession_Null, + SMBSession_Setup_AndX_Request, + SMBSession_Setup_AndX_Request_Extended_Security, + SMBSession_Setup_AndX_Response, + SMBSession_Setup_AndX_Response_Extended_Security, + SMB_Dialect, + SMB_Header, +) +from scapy.layers.windows.security import SECURITY_DESCRIPTOR +from scapy.layers.smb2 import ( + DirectTCP, + FileAllInformation, + FileIdBothDirectoryInformation, + SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2, + SMB2_CREATE_REQUEST_LEASE, + SMB2_CREATE_REQUEST_LEASE_V2, + SMB2_Change_Notify_Request, + SMB2_Change_Notify_Response, + SMB2_Close_Request, + SMB2_Close_Response, + SMB2_Create_Context, + SMB2_Create_Request, + SMB2_Create_Response, + SMB2_ENCRYPTION_CIPHERS, + SMB2_Encryption_Capabilities, + SMB2_Error_Response, + SMB2_Header, + SMB2_IOCTL_Request, + SMB2_IOCTL_Response, + SMB2_Negotiate_Context, + SMB2_Negotiate_Protocol_Request, + SMB2_Negotiate_Protocol_Response, + SMB2_Netname_Negotiate_Context_ID, + SMB2_Preauth_Integrity_Capabilities, + SMB2_Query_Directory_Request, + SMB2_Query_Directory_Response, + SMB2_Query_Info_Request, + SMB2_Query_Info_Response, + SMB2_Read_Request, + SMB2_Read_Response, + SMB2_SIGNING_ALGORITHMS, + SMB2_Session_Setup_Request, + SMB2_Session_Setup_Response, + SMB2_Signing_Capabilities, + SMB2_Tree_Connect_Request, + SMB2_Tree_Connect_Response, + SMB2_Tree_Disconnect_Request, + SMB2_Tree_Disconnect_Response, + SMB2_Write_Request, + SMB2_Write_Response, + SMBStreamSocket, + SMB_DIALECTS, + SRVSVC_SHARE_TYPES, + STATUS_ERREF, +) +from scapy.layers.spnego import SPNEGOSSP + + +class SMB_Client(Automaton): + """ + SMB client automaton + + :param sock: the SMBStreamSocket to use + :param ssp: the SSP to use + + All other options (in caps) are optional, and SMB specific: + + :param REQUIRE_SIGNATURE: set 'Require Signature' + :param REQUIRE_ENCRYPTION: set 'Requite Encryption' + :param MIN_DIALECT: minimum SMB dialect. Defaults to 0x0202 (2.0.2) + :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1) + :param DIALECTS: list of supported SMB2 dialects. + Constructed from MIN_DIALECT, MAX_DIALECT otherwise. + """ + + port = 445 + cls = DirectTCP + + def __init__(self, sock, ssp=None, *args, **kwargs): + # Various SMB client arguments + self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) + self.USE_SMB1 = kwargs.pop("USE_SMB1", False) + self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", None) + self.REQUIRE_ENCRYPTION = kwargs.pop("REQUIRE_ENCRYPTION", False) + self.RETRY = kwargs.pop("RETRY", 0) # optionally: retry n times session setup + self.SMB2 = kwargs.pop("SMB2", False) # optionally: start directly in SMB2 + self.HOST = kwargs.pop("HOST", "") + # Store supported dialects + if "DIALECTS" in kwargs: + self.DIALECTS = kwargs.pop("DIALECTS") + else: + MIN_DIALECT = kwargs.pop("MIN_DIALECT", 0x0202) + self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311) + self.DIALECTS = sorted( + [ + x + for x in [0x0202, 0x0210, 0x0300, 0x0302, 0x0311] + if x >= MIN_DIALECT and x <= self.MAX_DIALECT + ] + ) + # Internal Session information + self.ErrorStatus = None + self.NegotiateCapabilities = None + self.GUID = RandUUID()._fix() + self.SequenceWindow = (0, 0) # keep track of allowed MIDs + self.CurrentCreditCount = 0 + self.MaxCreditCount = 128 + if ssp is None: + # We got no SSP. Assuming the server allows anonymous + ssp = SPNEGOSSP( + [ + NTLMSSP( + UPN="guest", + HASHNT=b"", + ) + ] + ) + # Initialize + kwargs["sock"] = sock + Automaton.__init__( + self, + *args, + **kwargs, + ) + if self.is_atmt_socket: + self.smb_sock_ready = threading.Event() + # Set session options + self.session.ssp = ssp + self.session.SigningRequired = ( + self.REQUIRE_SIGNATURE if self.REQUIRE_SIGNATURE is not None else bool(ssp) + ) + self.session.Dialect = self.MAX_DIALECT + + @classmethod + def from_tcpsock(cls, sock, **kwargs): + return cls.smblink( + None, + SMBStreamSocket(sock, DirectTCP), + **kwargs, + ) + + @property + def session(self): + # session shorthand + return self.sock.session + + def send(self, pkt): + # Calculate what CreditCharge to send. + if self.session.Dialect > 0x0202 and isinstance(pkt.payload, SMB2_Header): + # [MS-SMB2] sect 3.2.4.1.5 + typ = type(pkt.payload.payload) + if typ is SMB2_Negotiate_Protocol_Request: + # See [MS-SMB2] 3.2.4.1.2 note + pkt.CreditCharge = 0 + elif typ in [ + SMB2_Read_Request, + SMB2_Write_Request, + SMB2_IOCTL_Request, + SMB2_Query_Directory_Request, + SMB2_Change_Notify_Request, + SMB2_Query_Info_Request, + ]: + # [MS-SMB2] 3.1.5.2 + # "For READ, WRITE, IOCTL, and QUERY_DIRECTORY requests" + # "CHANGE_NOTIFY, QUERY_INFO, or SET_INFO" + if typ == SMB2_Read_Request: + Length = pkt.payload.Length + elif typ == SMB2_Write_Request: + Length = len(pkt.payload.Data) + elif typ == SMB2_IOCTL_Request: + # [MS-SMB2] 3.3.5.15 + Length = max(len(pkt.payload.Input), pkt.payload.MaxOutputResponse) + elif typ in [ + SMB2_Query_Directory_Request, + SMB2_Change_Notify_Request, + SMB2_Query_Info_Request, + ]: + Length = pkt.payload.OutputBufferLength + else: + raise RuntimeError("impossible case") + pkt.CreditCharge = 1 + (Length - 1) // 65536 + else: + # "For all other requests, the client MUST set CreditCharge to 1" + pkt.CreditCharge = 1 + # Keep track of our credits + self.CurrentCreditCount -= pkt.CreditCharge + # [MS-SMB2] note <110> + # "The Windows-based client will request credits up to a configurable + # maximum of 128 by default." + pkt.CreditRequest = self.MaxCreditCount - self.CurrentCreditCount + # Get first available message ID: [MS-SMB2] 3.2.4.1.3 and 3.2.4.1.5 + pkt.MID = self.SequenceWindow[0] + return super(SMB_Client, self).send(pkt) + + @ATMT.state(initial=1) + def BEGIN(self): + pass + + @ATMT.condition(BEGIN) + def continue_smb2(self): + if self.SMB2: # Directly started in SMB2 + self.smb_header = DirectTCP() / SMB2_Header(PID=0xFEFF) + raise self.SMB2_NEGOTIATE() + + @ATMT.condition(BEGIN, prio=1) + def send_negotiate(self): + raise self.SENT_NEGOTIATE() + + @ATMT.action(send_negotiate) + def on_negotiate(self): + # [MS-SMB2] sect 3.2.4.2.2.1 - Multi-Protocol Negotiate + self.smb_header = DirectTCP() / SMB_Header( + Flags2=( + "LONG_NAMES+EAS+NT_STATUS+UNICODE+" + "SMB_SECURITY_SIGNATURE+EXTENDED_SECURITY" + ), + TID=0xFFFF, + PIDLow=0xFEFF, + UID=0, + MID=0, + ) + if self.EXTENDED_SECURITY: + self.smb_header.Flags2 += "EXTENDED_SECURITY" + pkt = self.smb_header.copy() / SMBNegotiate_Request( + Dialects=[ + SMB_Dialect(DialectString=x) + for x in [ + "PC NETWORK PROGRAM 1.0", + "LANMAN1.0", + "Windows for Workgroups 3.1a", + "LM1.2X002", + "LANMAN2.1", + "NT LM 0.12", + ] + + (["SMB 2.002", "SMB 2.???"] if not self.USE_SMB1 else []) + ], + ) + if not self.EXTENDED_SECURITY: + pkt.Flags2 -= "EXTENDED_SECURITY" + pkt[SMB_Header].Flags2 = ( + pkt[SMB_Header].Flags2 + - "SMB_SECURITY_SIGNATURE" + + "SMB_SECURITY_SIGNATURE_REQUIRED+IS_LONG_NAME" + ) + self.send(pkt) + + @ATMT.state() + def SENT_NEGOTIATE(self): + pass + + @ATMT.state() + def SMB2_NEGOTIATE(self): + pass + + @ATMT.condition(SMB2_NEGOTIATE) + def send_negotiate_smb2(self): + raise self.SENT_NEGOTIATE() + + @ATMT.action(send_negotiate_smb2) + def on_negotiate_smb2(self): + # [MS-SMB2] sect 3.2.4.2.2.2 - SMB2-Only Negotiate + pkt = self.smb_header.copy() / SMB2_Negotiate_Protocol_Request( + Dialects=self.DIALECTS, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), + ) + if self.MAX_DIALECT >= 0x0210: + # "If the client implements the SMB 2.1 or SMB 3.x dialect, ClientGuid + # MUST be set to the global ClientGuid value" + pkt.ClientGUID = self.GUID + # Capabilities: same as [MS-SMB2] 3.3.5.4 + self.NegotiateCapabilities = "+".join( + [ + "DFS", + "LEASING", + "LARGE_MTU", + ] + ) + if self.MAX_DIALECT >= 0x0300: + # "if Connection.Dialect belongs to the SMB 3.x dialect family ..." + self.NegotiateCapabilities += "+" + "+".join( + [ + "MULTI_CHANNEL", + "PERSISTENT_HANDLES", + "DIRECTORY_LEASING", + "ENCRYPTION", + ] + ) + if self.MAX_DIALECT >= 0x0311: + # "If the client implements the SMB 3.1.1 dialect, it MUST do" + pkt.NegotiateContexts = [ + SMB2_Negotiate_Context() + / SMB2_Preauth_Integrity_Capabilities( + # As for today, no other hash algorithm is described by the spec + HashAlgorithms=["SHA-512"], + Salt=self.session.Salt, + ), + SMB2_Negotiate_Context() + / SMB2_Encryption_Capabilities( + Ciphers=self.session.SupportedCipherIds, + ), + # TODO support compression and RDMA + SMB2_Negotiate_Context() + / SMB2_Netname_Negotiate_Context_ID( + NetName=self.HOST, + ), + SMB2_Negotiate_Context() + / SMB2_Signing_Capabilities( + SigningAlgorithms=self.session.SupportedSigningAlgorithmIds, + ), + ] + pkt.Capabilities = self.NegotiateCapabilities + # Send + self.send(pkt) + # If required, compute sessions + self.session.computeSMBConnectionPreauth( + bytes(pkt[SMB2_Header]), # nego request + ) + + @ATMT.receive_condition(SENT_NEGOTIATE) + def receive_negotiate_response(self, pkt): + if ( + SMBNegotiate_Response_Extended_Security in pkt + or SMB2_Negotiate_Protocol_Response in pkt + ): + # Extended SMB1 / SMB2 + try: + ssp_blob = pkt.SecurityBlob # eventually SPNEGO server initiation + except AttributeError: + ssp_blob = None + if ( + SMB2_Negotiate_Protocol_Response in pkt + and pkt.DialectRevision & 0xFF == 0xFF + ): + # Version is SMB X.??? + # [MS-SMB2] 3.2.5.2 + # If the DialectRevision field in the SMB2 NEGOTIATE Response is + # 0x02FF ... the client MUST allocate sequence number 1 from + # Connection.SequenceWindow, and MUST set MessageId field of the + # SMB2 header to 1. + self.SequenceWindow = (1, 1) + self.smb_header = DirectTCP() / SMB2_Header(PID=0xFEFF, MID=1) + self.SMB2 = True # We're now using SMB2 to talk to the server + raise self.SMB2_NEGOTIATE() + else: + if SMB2_Negotiate_Protocol_Response in pkt: + # SMB2 was negotiated ! + self.session.Dialect = pkt.DialectRevision + # If required, compute sessions + self.session.computeSMBConnectionPreauth( + bytes(pkt[SMB2_Header]), # nego response + ) + # Process max sizes + self.session.MaxReadSize = pkt.MaxReadSize + self.session.MaxTransactionSize = pkt.MaxTransactionSize + self.session.MaxWriteSize = pkt.MaxWriteSize + # Process SecurityMode + if pkt.SecurityMode.SIGNING_REQUIRED: + self.session.SigningRequired = True + # Process capabilities + if self.session.Dialect >= 0x0300: + self.session.SupportsEncryption = pkt.Capabilities.ENCRYPTION + # Process NegotiateContext + if self.session.Dialect >= 0x0311 and pkt.NegotiateContextsCount: + for ngctx in pkt.NegotiateContexts: + if ngctx.ContextType == 0x0002: + # SMB2_ENCRYPTION_CAPABILITIES + if ngctx.Ciphers[0] != 0: + self.session.CipherId = SMB2_ENCRYPTION_CIPHERS[ + ngctx.Ciphers[0] + ] + self.session.SupportsEncryption = True + elif ngctx.ContextType == 0x0008: + # SMB2_SIGNING_CAPABILITIES + self.session.SigningAlgorithmId = ( + SMB2_SIGNING_ALGORITHMS[ngctx.SigningAlgorithms[0]] + ) + if self.REQUIRE_ENCRYPTION and not self.session.SupportsEncryption: + self.ErrorStatus = "NEGOTIATE FAILURE: encryption." + raise self.NEGO_FAILED() + self.update_smbheader(pkt) + raise self.NEGOTIATED(ssp_blob) + elif SMBNegotiate_Response_Security in pkt: + # Non-extended SMB1 + # Never tested. FIXME. probably broken + raise self.NEGOTIATED(pkt.Challenge) + + @ATMT.state(final=1) + def NEGO_FAILED(self): + self.smb_sock_ready.set() + + @ATMT.state() + def NEGOTIATED(self, ssp_blob=None): + # Negotiated ! We now know the Dialect + if self.session.Dialect > 0x0202: + # [MS-SMB2] sect 3.2.5.1.4 + self.smb_header.CreditRequest = 1 + # Begin session establishment + ssp_tuple = self.session.ssp.GSS_Init_sec_context( + self.session.sspcontext, + input_token=ssp_blob, + target_name="cifs/" + self.HOST if self.HOST else None, + req_flags=( + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG + | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.session.SigningRequired else 0) + ), + ) + return ssp_tuple + + def update_smbheader(self, pkt): + """ + Called when receiving a SMB2 packet to update the current smb_header + """ + # Some values should not be updated when ASYNC + if not pkt.Flags.SMB2_FLAGS_ASYNC_COMMAND: + # Update IDs + self.smb_header.SessionId = pkt.SessionId + self.smb_header.TID = pkt.TID + self.smb_header.PID = pkt.PID + # Update credits + self.CurrentCreditCount += pkt.CreditRequest + # [MS-SMB2] 3.2.5.1.4 + self.SequenceWindow = ( + self.SequenceWindow[0] + max(pkt.CreditCharge, 1), + self.SequenceWindow[1] + pkt.CreditRequest, + ) + + # DEV: add a condition on NEGOTIATED with prio=0 + + @ATMT.condition(NEGOTIATED, prio=1) + def should_retry_without_blob(self, ssp_tuple): + _, _, status = ssp_tuple + if status == GSS_S_DEFECTIVE_TOKEN: + # Token was defective. This could be that we passed a SPNEGO initial token + # to a NTLM SSP (not using SPNEGO). Retry using no input blob + raise self.NEGOTIATED() + + @ATMT.condition(NEGOTIATED, prio=2) + def should_send_session_setup_request(self, ssp_tuple): + _, _, status = ssp_tuple + if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + raise ValueError( + "Internal error: the SSP completed with error: %s" % status + ) + raise self.SENT_SESSION_REQUEST().action_parameters(ssp_tuple) + + @ATMT.state() + def SENT_SESSION_REQUEST(self): + pass + + @ATMT.action(should_send_session_setup_request) + def send_setup_session_request(self, ssp_tuple): + self.session.sspcontext, token, status = ssp_tuple + if self.SMB2 and status == GSS_S_CONTINUE_NEEDED: + # New session: force 0 + self.SessionId = 0 + if self.SMB2 or self.EXTENDED_SECURITY: + # SMB1 extended / SMB2 + if self.SMB2: + # SMB2 + pkt = self.smb_header.copy() / SMB2_Session_Setup_Request( + Capabilities="DFS", + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), + ) + else: + # SMB1 extended + pkt = ( + self.smb_header.copy() + / SMBSession_Setup_AndX_Request_Extended_Security( + ServerCapabilities=( + "UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS+" + "DYNAMIC_REAUTH+EXTENDED_SECURITY" + ), + NativeOS=b"", + NativeLanMan=b"", + ) + ) + pkt.SecurityBlob = token + else: + # Non-extended security. + pkt = self.smb_header.copy() / SMBSession_Setup_AndX_Request( + ServerCapabilities="UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS", + NativeOS=b"", + NativeLanMan=b"", + OEMPassword=b"\0" * 24, + UnicodePassword=token, + ) + self.send(pkt) + if self.SMB2: + # If required, compute sessions + self.session.computeSMBSessionPreauth( + bytes(pkt[SMB2_Header]), # session request + ) + + @ATMT.receive_condition(SENT_SESSION_REQUEST) + def receive_session_setup_response(self, pkt): + if ( + SMBSession_Null in pkt + or SMBSession_Setup_AndX_Response_Extended_Security in pkt + or SMBSession_Setup_AndX_Response in pkt + ): + # SMB1 + if SMBSession_Null in pkt: + # Likely an error + raise self.NEGOTIATED() + # Logging + if pkt.Status != 0 and pkt.Status != 0xC0000016: + # Not SUCCESS nor MORE_PROCESSING_REQUIRED: log + self.ErrorStatus = pkt.sprintf("%SMB2_Header.Status%") + self.debug( + lvl=1, + msg=conf.color_theme.red( + pkt.sprintf("SMB Session Setup Response: %SMB2_Header.Status%") + ), + ) + if self.SMB2: + self.update_smbheader(pkt) + # Cases depending on the response packet + if ( + SMBSession_Setup_AndX_Response_Extended_Security in pkt + or SMB2_Session_Setup_Response in pkt + ): + # The server assigns us a SessionId + self.smb_header.SessionId = pkt.SessionId + # SMB1 extended / SMB2 + if pkt.Status == 0: # Authenticated + if SMB2_Session_Setup_Response in pkt: + # [MS-SMB2] sect 3.2.5.3.1 + if pkt.SessionFlags.IS_GUEST: + # "If the security subsystem indicates that the session + # was established by a guest user, Session.SigningRequired + # MUST be set to FALSE and Session.IsGuest MUST be set to TRUE." + self.session.IsGuest = True + self.session.SigningRequired = False + elif self.session.Dialect >= 0x0300: + if pkt.SessionFlags.ENCRYPT_DATA or self.REQUIRE_ENCRYPTION: + self.session.EncryptData = True + self.session.SigningRequired = False + raise self.AUTHENTICATED(pkt.SecurityBlob) + else: + if SMB2_Header in pkt: + # If required, compute sessions + self.session.computeSMBSessionPreauth( + bytes(pkt[SMB2_Header]), # session response + ) + # Ongoing auth + raise self.NEGOTIATED(pkt.SecurityBlob) + elif SMBSession_Setup_AndX_Response_Extended_Security in pkt: + # SMB1 non-extended + pass + elif SMB2_Error_Response in pkt: + # Authentication failure + self.session.sspcontext.clifailure() + # Reset Session preauth (SMB 3.1.1) + self.session.SessionPreauthIntegrityHashValue = None + if not self.RETRY: + raise self.AUTH_FAILED() + self.debug(lvl=2, msg="RETRY: %s" % self.RETRY) + self.RETRY -= 1 + raise self.NEGOTIATED() + + @ATMT.state(final=1) + def AUTH_FAILED(self): + self.smb_sock_ready.set() + + @ATMT.state() + def AUTHENTICATED(self, ssp_blob=None): + self.session.sspcontext, _, status = self.session.ssp.GSS_Init_sec_context( + self.session.sspcontext, + input_token=ssp_blob, + target_name="cifs/" + self.HOST if self.HOST else None, + ) + if status != GSS_S_COMPLETE: + raise ValueError( + "Internal error: the SSP completed with error: %s" % status + ) + # Authentication was successful + self.session.computeSMBSessionKeys(IsClient=True) + + # DEV: add a condition on AUTHENTICATED with prio=0 + + @ATMT.condition(AUTHENTICATED, prio=1) + def authenticated_post_actions(self): + raise self.SOCKET_BIND() + + # Plain SMB Socket + + @ATMT.state() + def SOCKET_BIND(self): + self.smb_sock_ready.set() + + @ATMT.condition(SOCKET_BIND) + def start_smb_socket(self): + raise self.SOCKET_MODE_SMB() + + @ATMT.state() + def SOCKET_MODE_SMB(self): + pass + + @ATMT.receive_condition(SOCKET_MODE_SMB) + def incoming_data_received_smb(self, pkt): + raise self.SOCKET_MODE_SMB().action_parameters(pkt) + + @ATMT.action(incoming_data_received_smb) + def receive_data_smb(self, pkt): + resp = pkt[SMB2_Header].payload + if isinstance(resp, SMB2_Error_Response): + if pkt.Status == 0x00000103: # STATUS_PENDING + # answer is coming later.. just wait... + return + if pkt.Status == 0x0000010B: # STATUS_NOTIFY_CLEANUP + # this is a notify cleanup. ignore + return + self.update_smbheader(pkt) + # Add the status to the response as metadata + resp.NTStatus = pkt.sprintf("%SMB2_Header.Status%") + self.oi.smbpipe.send(resp) + + @ATMT.ioevent(SOCKET_MODE_SMB, name="smbpipe", as_supersocket="smblink") + def outgoing_data_received_smb(self, fd): + raise self.SOCKET_MODE_SMB().action_parameters(fd.recv()) + + @ATMT.action(outgoing_data_received_smb) + def send_data(self, d): + self.send(self.smb_header.copy() / d) + + +class SMB_SOCKET(SuperSocket): + """ + Mid-level wrapper over SMB_Client.smblink that provides some basic SMB + client functions, such as tree connect, directory query, etc. + """ + + def __init__(self, smbsock, use_ioctl=True, timeout=3): + self.ins = smbsock + self.timeout = timeout + if not self.ins.atmt.smb_sock_ready.wait(timeout=timeout): + # If we have a SSP, tell it we failed. + if self.session.sspcontext: + self.session.sspcontext.clifailure() + raise TimeoutError( + "The SMB handshake timed out ! (enable debug=1 for logs)" + ) + if self.ins.atmt.ErrorStatus: + raise Scapy_Exception( + "SMB Session Setup failed: %s" % self.ins.atmt.ErrorStatus + ) + + @classmethod + def from_tcpsock(cls, sock, **kwargs): + """ + Wraps the tcp socket in a SMB_Client.smblink first, then into the + SMB_SOCKET/SMB_RPC_SOCKET + """ + return cls( + use_ioctl=kwargs.pop("use_ioctl", True), + timeout=kwargs.pop("timeout", 3), + smbsock=SMB_Client.from_tcpsock(sock, **kwargs), + ) + + @property + def session(self): + return self.ins.atmt.session + + def set_TID(self, TID): + """ + Set the TID (Tree ID). + This can be called before sending a packet + """ + self.ins.atmt.smb_header.TID = TID + + def get_TID(self): + """ + Get the current TID from the underlying socket + """ + return self.ins.atmt.smb_header.TID + + def tree_connect(self, name): + """ + Send a TreeConnect request + """ + resp = self.ins.sr1( + SMB2_Tree_Connect_Request( + Buffer=[ + ( + "Path", + "\\\\%s\\%s" + % ( + self.session.sspcontext.ServerHostname, + name, + ), + ) + ] + ), + verbose=False, + timeout=self.timeout, + ) + if not resp: + raise ValueError("TreeConnect timed out !") + if SMB2_Tree_Connect_Response not in resp: + raise ValueError("Failed TreeConnect ! %s" % resp.NTStatus) + # [MS-SMB2] sect 3.2.5.5 + if self.session.Dialect >= 0x0300: + if resp.ShareFlags.ENCRYPT_DATA and self.session.SupportsEncryption: + self.session.TreeEncryptData = True + else: + self.session.TreeEncryptData = False + return self.get_TID() + + def tree_disconnect(self): + """ + Send a TreeDisconnect request + """ + resp = self.ins.sr1( + SMB2_Tree_Disconnect_Request(), + verbose=False, + timeout=self.timeout, + ) + if not resp: + raise ValueError("TreeDisconnect timed out !") + if SMB2_Tree_Disconnect_Response not in resp: + raise ValueError("Failed TreeDisconnect ! %s" % resp.NTStatus) + + def create_request( + self, + name, + mode="r", + type="pipe", + extra_create_options=[], + extra_desired_access=[], + ): + """ + Open a file/pipe by its name + + :param name: the name of the file or named pipe. e.g. 'srvsvc' + """ + ShareAccess = [] + DesiredAccess = [] + # Common params depending on the access + if "r" in mode: + ShareAccess.append("FILE_SHARE_READ") + DesiredAccess.extend(["FILE_READ_DATA", "FILE_READ_ATTRIBUTES"]) + if "w" in mode: + ShareAccess.append("FILE_SHARE_WRITE") + DesiredAccess.extend(["FILE_WRITE_DATA", "FILE_WRITE_ATTRIBUTES"]) + if "d" in mode: + ShareAccess.append("FILE_SHARE_DELETE") + # Params depending on the type + FileAttributes = [] + CreateOptions = [] + CreateContexts = [] + CreateDisposition = "FILE_OPEN" + if type == "folder": + FileAttributes.append("FILE_ATTRIBUTE_DIRECTORY") + CreateOptions.append("FILE_DIRECTORY_FILE") + elif type in ["file", "pipe"]: + CreateOptions = ["FILE_NON_DIRECTORY_FILE"] + if "r" in mode: + DesiredAccess.extend(["FILE_READ_EA", "READ_CONTROL", "SYNCHRONIZE"]) + if "w" in mode: + CreateDisposition = "FILE_OVERWRITE_IF" + DesiredAccess.append("FILE_WRITE_EA") + if "d" in mode: + DesiredAccess.append("DELETE") + CreateOptions.append("FILE_DELETE_ON_CLOSE") + if type == "file": + FileAttributes.append("FILE_ATTRIBUTE_NORMAL") + elif type: + raise ValueError("Unknown type: %s" % type) + # [MS-SMB2] 3.2.4.3.8 + RequestedOplockLevel = 0 + if self.session.Dialect >= 0x0300: + RequestedOplockLevel = "SMB2_OPLOCK_LEVEL_LEASE" + elif self.session.Dialect >= 0x0210 and type == "file": + RequestedOplockLevel = "SMB2_OPLOCK_LEVEL_LEASE" + # SMB 3.X + if self.session.Dialect >= 0x0300 and type in ["file", "folder"]: + CreateContexts.extend( + [ + # [SMB2] sect 3.2.4.3.5 + SMB2_Create_Context( + Name=b"DH2Q", + Data=SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2( + CreateGuid=RandUUID()._fix() + ), + ), + # [SMB2] sect 3.2.4.3.9 + SMB2_Create_Context( + Name=b"MxAc", + ), + # [SMB2] sect 3.2.4.3.10 + SMB2_Create_Context( + Name=b"QFid", + ), + # [SMB2] sect 3.2.4.3.8 + SMB2_Create_Context( + Name=b"RqLs", + Data=SMB2_CREATE_REQUEST_LEASE_V2(LeaseKey=RandUUID()._fix()), + ), + ] + ) + elif self.session.Dialect == 0x0210 and type == "file": + CreateContexts.extend( + [ + # [SMB2] sect 3.2.4.3.8 + SMB2_Create_Context( + Name=b"RqLs", + Data=SMB2_CREATE_REQUEST_LEASE(LeaseKey=RandUUID()._fix()), + ), + ] + ) + # Extra options + if extra_create_options: + CreateOptions.extend(extra_create_options) + if extra_desired_access: + DesiredAccess.extend(extra_desired_access) + # Request + resp = self.ins.sr1( + SMB2_Create_Request( + ImpersonationLevel="Impersonation", + DesiredAccess="+".join(DesiredAccess), + CreateDisposition=CreateDisposition, + CreateOptions="+".join(CreateOptions), + ShareAccess="+".join(ShareAccess), + FileAttributes="+".join(FileAttributes), + CreateContexts=CreateContexts, + RequestedOplockLevel=RequestedOplockLevel, + Name=name, + ), + verbose=0, + timeout=self.timeout, + ) + if not resp: + raise ValueError("CreateRequest timed out !") + if SMB2_Create_Response not in resp: + raise ValueError("Failed CreateRequest ! %s" % resp.NTStatus) + return resp[SMB2_Create_Response].FileId + + def close_request(self, FileId): + """ + Close the FileId + """ + pkt = SMB2_Close_Request(FileId=FileId) + resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) + if not resp: + raise ValueError("CloseRequest timed out !") + if SMB2_Close_Response not in resp: + raise ValueError("Failed CloseRequest ! %s" % resp.NTStatus) + + def read_request(self, FileId, Length, Offset=0): + """ + Read request + """ + resp = self.ins.sr1( + SMB2_Read_Request( + FileId=FileId, + Length=Length, + Offset=Offset, + ), + verbose=0, + timeout=self.timeout * 10, + ) + if not resp: + raise ValueError("ReadRequest timed out !") + if SMB2_Read_Response not in resp: + raise ValueError("Failed ReadRequest ! %s" % resp.NTStatus) + return resp.Data + + def write_request(self, Data, FileId, Offset=0): + """ + Write request + """ + resp = self.ins.sr1( + SMB2_Write_Request( + FileId=FileId, + Data=Data, + Offset=Offset, + ), + verbose=0, + timeout=self.timeout * 10, + ) + if not resp: + raise ValueError("WriteRequest timed out !") + if SMB2_Write_Response not in resp: + raise ValueError("Failed WriteRequest ! %s" % resp.NTStatus) + return resp.Count + + def query_directory(self, FileId, FileName="*"): + """ + Query the Directory with FileId + """ + results = [] + Flags = "SMB2_RESTART_SCANS" + while True: + pkt = SMB2_Query_Directory_Request( + FileInformationClass="FileIdBothDirectoryInformation", + FileId=FileId, + FileName=FileName, + Flags=Flags, + ) + resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) + Flags = 0 # only the first one is RESTART_SCANS + if not resp: + raise ValueError("QueryDirectory timed out !") + if SMB2_Error_Response in resp: + break + elif SMB2_Query_Directory_Response not in resp: + raise ValueError("Failed QueryDirectory ! %s" % resp.NTStatus) + res = FileIdBothDirectoryInformation(resp.Output) + results.extend( + [ + ( + x.FileName, + x.FileAttributes, + x.EndOfFile, + x.LastWriteTime, + ) + for x in res.files + ] + ) + return results + + def query_info(self, FileId, InfoType, FileInfoClass, AdditionalInformation=0): + """ + Query the Info + """ + pkt = SMB2_Query_Info_Request( + InfoType=InfoType, + FileInfoClass=FileInfoClass, + OutputBufferLength=65535, + FileId=FileId, + AdditionalInformation=AdditionalInformation, + ) + resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) + if not resp: + raise ValueError("QueryInfo timed out !") + if SMB2_Query_Info_Response not in resp: + raise ValueError("Failed QueryInfo ! %s" % resp.NTStatus) + return resp.Output + + def changenotify(self, FileId): + """ + Register change notify + """ + pkt = SMB2_Change_Notify_Request( + Flags="SMB2_WATCH_TREE", + OutputBufferLength=65535, + FileId=FileId, + CompletionFilter=0x0FFF, + ) + # we can wait forever, not a problem in this one + resp = self.ins.sr1(pkt, verbose=0, chainCC=True) + if SMB2_Change_Notify_Response not in resp: + raise ValueError("Failed ChangeNotify ! %s" % resp.NTStatus) + return resp.Output + + +class SMB_RPC_SOCKET(ObjectPipe, SMB_SOCKET): + """ + Extends SMB_SOCKET (which is a wrapper over SMB_Client.smblink) to send + DCE/RPC messages (bind, reqs, etc.) + + This is usable as a normal SuperSocket (sr1, etc.) and performs the + wrapping of the DCE/RPC messages into SMB2_Write/Read packets. + """ + + def __init__(self, smbsock, use_ioctl=True, timeout=3): + self.use_ioctl = use_ioctl + ObjectPipe.__init__(self, "SMB_RPC_SOCKET") + SMB_SOCKET.__init__(self, smbsock, timeout=timeout) + + def open_pipe(self, name): + self.PipeFileId = self.create_request(name, mode="rw", type="pipe") + + def close_pipe(self): + self.close_request(self.PipeFileId) + self.PipeFileId = None + + def send(self, x, is_sr1=True): + # Reminder: this class is an ObjectPipe ! It doesn't act as a real socket + # but just a queue. When someone calls the "send" function, they pipe + # some data that we must send, and tell us if they expect an answer through + # the is_sr1 flag. + + # Detect if DCE/RPC is fragmented. Then we must use Read/Write + is_frag = x.pfc_flags & 3 != 3 + + if self.use_ioctl and is_sr1 and not is_frag and self.session.Dialect >= 0x0210: + # Use IOCTLRequest + pkt = SMB2_IOCTL_Request( + FileId=self.PipeFileId, + Flags="SMB2_0_IOCTL_IS_FSCTL", + CtlCode="FSCTL_PIPE_TRANSCEIVE", + ) + pkt.Input = bytes(x) + resp = self.ins.sr1(pkt, verbose=0) + if SMB2_IOCTL_Response not in resp: + raise ValueError("Failed reading IOCTL_Response ! %s" % resp.NTStatus) + data = bytes(resp.Output) + super(SMB_RPC_SOCKET, self).send(data) + + # Handle BUFFER_OVERFLOW (big DCE/RPC response) + while resp.NTStatus == "STATUS_BUFFER_OVERFLOW" or data[3] & 2 != 2: + # Retrieve DCE/RPC full size + resp = self.ins.sr1( + SMB2_Read_Request( + FileId=self.PipeFileId, + ), + verbose=0, + ) + data = resp.Data + super(SMB_RPC_SOCKET, self).send(data) + else: + # Use WriteRequest/ReadRequest + pkt = SMB2_Write_Request( + FileId=self.PipeFileId, + ) + pkt.Data = bytes(x) + # We send the Write Request + resp = self.ins.sr1(pkt, verbose=0) + if SMB2_Write_Response not in resp: + raise ValueError("Failed sending WriteResponse ! %s" % resp.NTStatus) + + # We may not be expecting an answer + if not is_sr1: + return + + # If fragmented, only read if it's the last. + if is_frag and not x.pfc_flags.PFC_LAST_FRAG: + return + + # We send a Read Request afterwards + resp = self.ins.sr1( + SMB2_Read_Request( + FileId=self.PipeFileId, + ), + verbose=0, + ) + if SMB2_Read_Response not in resp: + raise ValueError("Failed reading ReadResponse ! %s" % resp.NTStatus) + super(SMB_RPC_SOCKET, self).send(resp.Data) + # Handle fragmented response + while resp.Data[3] & 2 != 2: # PFC_LAST_FRAG not set + # Retrieve DCE/RPC full size + resp = self.ins.sr1( + SMB2_Read_Request( + FileId=self.PipeFileId, + ), + verbose=0, + ) + super(SMB_RPC_SOCKET, self).send(resp.Data) + + def close(self): + SMB_SOCKET.close(self) + ObjectPipe.close(self) + + +@conf.commands.register +class smbclient(CLIUtil): + r""" + A simple SMB client CLI powered by Scapy + + :param target: can be a hostname, the IPv4 or the IPv6 to connect to + :param UPN: the upn to use (DOMAIN/USER, DOMAIN\USER, USER@DOMAIN or USER) + :param guest: use guest mode (over NTLM) + :param ssp: if provided, use this SSP for auth. + :param kerberos_required: require kerberos + :param port: the TCP port. default 445 + :param password: if provided, used for auth + :param HashNt: if provided, used for auth (NTLM) + :param HashAes256Sha96: if provided, used for auth (Kerberos) + :param HashAes128Sha96: if provided, used for auth (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. + :param use_winssp: (bool) (only works on Windows). Use implicit authentication + through WinSSP. + :param ST: if provided, the service ticket to use (Kerberos) + :param KEY: if provided, the session key associated to the ticket (Kerberos) + :param cli: CLI mode (default True). False to use for scripting + + Some additional SMB parameters are available under help(SMB_Client). Some of + them include the following: + + :param REQUIRE_ENCRYPTION: requires encryption. + """ + + def __init__( + self, + target: str, + UPN: str = None, + password: str = None, + guest: bool = False, + kerberos_required: bool = False, + HashNt: bytes = None, + HashAes256Sha96: bytes = None, + HashAes128Sha96: bytes = None, + use_krb5ccname: bool = False, + use_winssp: bool = False, + port: int = 445, + timeout: int = 5, + debug: int = 0, + ssp=None, + ST=None, + KEY=None, + cli=True, + # SMB arguments + REQUIRE_ENCRYPTION=False, + **kwargs, + ): + if cli: + self._depcheck() + assert ( + UPN or ssp or guest or use_winssp + ), "Either UPN, ssp or guest must be provided !" + # Do we need to build a SSP? + if ssp is None: + # Create the SSP (only if not guest mode) + if not guest: + ssp = SPNEGOSSP.from_cli_arguments( + UPN=UPN, + target=target, + password=password, + HashNt=HashNt, + HashAes256Sha96=HashAes256Sha96, + HashAes128Sha96=HashAes128Sha96, + ST=ST, + KEY=KEY, + kerberos_required=kerberos_required, + use_krb5ccname=use_krb5ccname, + use_winssp=use_winssp, + ) + else: + # Guest mode + ssp = None + # Check if target is IPv4 or IPv6 + if ":" in target: + family = socket.AF_INET6 + else: + family = socket.AF_INET + # Open socket + sock = socket.socket(family, socket.SOCK_STREAM) + # Configure socket for SMB: + # - TCP KEEPALIVE, TCP_KEEPIDLE and TCP_KEEPINTVL. Against a Windows server this + # isn't necessary, but samba kills the socket VERY fast otherwise. + # - set TCP_NODELAY to disable Nagle's algorithm (we're streaming data) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) + # Timeout & connect + sock.settimeout(timeout) + if debug: + print("Connecting to %s:%s" % (target, port)) + sock.connect((target, port)) + self.extra_create_options = [] + # Wrap with the automaton + self.timeout = timeout + kwargs.setdefault("HOST", target) + self.sock = SMB_Client.from_tcpsock( + sock, + ssp=ssp, + debug=debug, + REQUIRE_ENCRYPTION=REQUIRE_ENCRYPTION, + timeout=timeout, + **kwargs, + ) + try: + # Wrap with SMB_SOCKET + self.smbsock = SMB_SOCKET(self.sock, timeout=self.timeout) + # Wait for either the atmt to fail, or the smb_sock_ready to timeout + _t = time.time() + while True: + if self.sock.atmt.smb_sock_ready.is_set(): + # yay + break + if not self.sock.atmt.isrunning(): + status = self.sock.atmt.get("Status") + raise Scapy_Exception( + "%s with status %s" + % ( + self.sock.atmt.state.state, + STATUS_ERREF.get(status, hex(status)), + ) + ) + if time.time() - _t > timeout: + self.sock.close() + raise TimeoutError("The SMB handshake timed out.") + time.sleep(0.1) + except Exception: + # Something bad happened, end the socket/automaton + self.sock.close() + raise + + # For some usages, we will also need the RPC wrapper + from scapy.layers.msrpce.rpcclient import DCERPC_Client + + self.rpcclient = DCERPC_Client.from_smblink( + self.sock, + ndr64=False, + verb=bool(debug), + ) + # We have a valid smb connection ! + print( + "%s authentication successful using %s%s !" + % ( + SMB_DIALECTS.get( + self.smbsock.session.Dialect, + "SMB %s" % self.smbsock.session.Dialect, + ), + repr(self.smbsock.session.sspcontext), + " as GUEST" if self.smbsock.session.IsGuest else "", + ) + ) + # Now define some variables for our CLI + self.pwd = pathlib.PureWindowsPath("/") + self.localpwd = pathlib.Path(".").resolve() + self.current_tree = None + self.ls_cache = {} # cache the listing of the current directory + self.sh_cache = [] # cache the shares + # Start CLI + if cli: + self.loop(debug=debug) + + def ps1(self): + return r"smb: \%s> " % self.normalize_path(self.pwd) + + def close(self): + print("Connection closed") + self.smbsock.close() + + def _require_share(self, silent=False): + if self.current_tree is None: + if not silent: + print("No share selected ! Try 'shares' then 'use'.") + return True + + def collapse_path(self, path): + # the amount of pathlib.wtf you need to do to resolve .. on all platforms + # is ridiculous + return pathlib.PureWindowsPath(os.path.normpath(path.as_posix())) + + def normalize_path(self, path): + """ + Normalize path for CIFS usage + """ + return str(self.collapse_path(path)).lstrip("\\") + + @CLIUtil.addcommand() + def shares(self): + """ + List the shares available + """ + # Poll cache + if self.sh_cache: + return self.sh_cache + # It's an RPC + self.rpcclient.open_smbpipe("srvsvc") + self.rpcclient.bind(find_dcerpc_interface("srvsvc")) + req = NetrShareEnum_Request( + InfoStruct=LPSHARE_ENUM_STRUCT( + Level=1, + ShareInfo=NDRUnion( + tag=1, + value=SHARE_INFO_1_CONTAINER(Buffer=None), + ), + ), + PreferedMaximumLength=0xFFFFFFFF, + ndr64=self.rpcclient.ndr64, + ) + resp = self.rpcclient.sr1_req(req, timeout=self.timeout) + self.rpcclient.close_smbpipe() + if resp.status != 0: + resp.show() + raise ValueError("NetrShareEnum_Request failed !") + results = [] + for share in resp.valueof("InfoStruct.ShareInfo.Buffer"): + shi1_type = share.valueof("shi1_type") & 0x0FFFFFFF + results.append( + ( + share.valueof("shi1_netname").decode(), + SRVSVC_SHARE_TYPES.get(shi1_type, shi1_type), + share.valueof("shi1_remark").decode(), + ) + ) + self.sh_cache = results # cache + return results + + @CLIUtil.addoutput(shares) + def shares_output(self, results): + """ + Print the output of 'shares' + """ + print(pretty_list(results, [("ShareName", "ShareType", "Comment")])) + + @CLIUtil.addcommand(mono=True) + def use(self, share): + """ + Open a share + """ + self.current_tree = self.smbsock.tree_connect(share) + self.pwd = pathlib.PureWindowsPath("/") + self.ls_cache.clear() + + @CLIUtil.addcomplete(use) + def use_complete(self, share): + """ + Auto-complete 'use' + """ + return [ + x[0] for x in self.shares() if x[0].startswith(share) and x[0] != "IPC$" + ] + + def _parsepath(self, arg, remote=True): + """ + Parse a path. Returns the parent folder and file name + """ + # Find parent directory if it exists + elt = (pathlib.PureWindowsPath if remote else pathlib.Path)(arg) + eltpar = (pathlib.PureWindowsPath if remote else pathlib.Path)(".") + eltname = elt.name + if arg.endswith("/") or arg.endswith("\\"): + eltpar = elt + eltname = "" + elif elt.parent and elt.parent.name or elt.is_absolute(): + eltpar = elt.parent + return eltpar, eltname + + def _fs_complete(self, arg, cond=None): + """ + Return a listing of the remote files for completion purposes + """ + if cond is None: + cond = lambda _: True + eltpar, eltname = self._parsepath(arg) + # ls in that directory + try: + files = self.ls(parent=eltpar) + except ValueError: + return [] + return [ + str(eltpar / x[0]) + for x in files + if ( + x[0].lower().startswith(eltname.lower()) + and x[0] not in [".", ".."] + and cond(x[1]) + ) + ] + + def _dir_complete(self, arg): + """ + Return a directories of remote files for completion purposes + """ + results = self._fs_complete( + arg, + cond=lambda x: x.FILE_ATTRIBUTE_DIRECTORY, + ) + if len(results) == 1 and results[0].startswith(arg): + # skip through folders + return [results[0] + "\\"] + return results + + @CLIUtil.addcommand(mono=True) + def ls(self, parent=None): + """ + List the files in the remote directory + -t: sort by timestamp + -S: sort by size + -r: reverse while sorting + """ + if self._require_share(): + return + # Get pwd of the ls + pwd = self.pwd + if parent is not None: + pwd /= parent + pwd = self.normalize_path(pwd) + # Poll the cache + if self.ls_cache and pwd in self.ls_cache: + return self.ls_cache[pwd] + self.smbsock.set_TID(self.current_tree) + # Open folder + fileId = self.smbsock.create_request( + pwd, + type="folder", + extra_create_options=self.extra_create_options, + ) + # Query the folder + files = self.smbsock.query_directory(fileId) + # Close the folder + self.smbsock.close_request(fileId) + self.ls_cache[pwd] = files # Store cache + return files + + @CLIUtil.addoutput(ls) + def ls_output(self, results, *, t=False, S=False, r=False): + """ + Print the output of 'ls' + """ + fld = UTCTimeField( + "", None, fmt=" works + str(eltpar / x.name) + for x in eltpar.resolve().glob("*") + if (x.name.lower().startswith(eltname.lower()) and cond(x)) + ] + + @CLIUtil.addoutput(cd) + def cd_output(self, result): + """ + Print the output of 'cd' + """ + if result: + print(result) + + @CLIUtil.addcommand() + def lls(self): + """ + List the files in the local directory + """ + return list(self.localpwd.glob("*")) + + @CLIUtil.addoutput(lls) + def lls_output(self, results): + """ + Print the output of 'lls' + """ + results = [ + ( + x.name, + human_size(stat.st_size), + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime)), + ) + for x, stat in ((x, x.stat()) for x in results) + ] + print( + pretty_list(results, [("FileName", "File Size", "Last Modification Time")]) + ) + + @CLIUtil.addcommand(mono=True) + def lcd(self, folder): + """ + Change the local current directory + """ + if not folder: + # show mode + return str(self.localpwd) + self.localpwd /= folder + self.localpwd = self.localpwd.resolve() + + @CLIUtil.addcomplete(lcd) + def lcd_complete(self, folder): + """ + Auto-complete lcd + """ + return self._lfs_complete(folder, lambda x: x.is_dir()) + + @CLIUtil.addoutput(lcd) + def lcd_output(self, result): + """ + Print the output of 'lcd' + """ + if result: + print(result) + + def _get_file(self, file, fd): + """ + Gets the file bytes from a remote host + """ + # Get pwd of the ls + fpath = self.pwd / file + self.smbsock.set_TID(self.current_tree) + + # Open file + fileId = self.smbsock.create_request( + self.normalize_path(fpath), + type="file", + extra_create_options=[ + "FILE_SEQUENTIAL_ONLY", + ] + + self.extra_create_options, + ) + + # Get the file size + info = FileAllInformation( + self.smbsock.query_info( + FileId=fileId, + InfoType="SMB2_0_INFO_FILE", + FileInfoClass="FileAllInformation", + ) + ) + length = info.StandardInformation.EndOfFile + offset = 0 + + # Read the file + while length: + lengthRead = min(self.smbsock.session.MaxReadSize, length) + fd.write( + self.smbsock.read_request(fileId, Length=lengthRead, Offset=offset) + ) + offset += lengthRead + length -= lengthRead + + # Close the file + self.smbsock.close_request(fileId) + return offset + + def _send_file(self, fname, fd): + """ + Send the file bytes to a remote host + """ + # Get destination file + fpath = self.pwd / fname + self.smbsock.set_TID(self.current_tree) + # Open file + fileId = self.smbsock.create_request( + self.normalize_path(fpath), + type="file", + mode="w", + extra_create_options=self.extra_create_options, + ) + # Send the file + offset = 0 + while True: + data = fd.read(self.smbsock.session.MaxWriteSize) + if not data: + # end of file + break + offset += self.smbsock.write_request( + Data=data, + FileId=fileId, + Offset=offset, + ) + # Close the file + self.smbsock.close_request(fileId) + return offset + + def _getr(self, directory, _root, _verb=True): + """ + Internal recursive function to get a directory + + :param directory: the remote directory to get + :param _root: locally, the directory to store any found files + """ + size = 0 + if not _root.exists(): + _root.mkdir() + # ls the directory + for x in self.ls(parent=directory): + if x[0] in [".", ".."]: + # Discard . and .. + continue + remote = directory / x[0] + local = _root / x[0] + try: + if x[1].FILE_ATTRIBUTE_DIRECTORY: + # Sub-directory + size += self._getr(remote, local) + else: + # Sub-file + size += self.get(remote, local)[1] + if _verb: + print(remote) + except ValueError as ex: + if _verb: + print(conf.color_theme.red(remote), "->", str(ex)) + return size + + @CLIUtil.addcommand(mono=True, globsupport=True) + def get(self, file, _dest=None, _verb=True, *, r=False): + """ + Retrieve a file + -r: recursively download a directory + """ + if self._require_share(): + return + if r: + dirpar, dirname = self._parsepath(file) + return file, self._getr( + dirpar / dirname, # Remotely + _root=self.localpwd / dirname, # Locally + _verb=_verb, + ) + else: + fname = pathlib.PureWindowsPath(file).name + # Write the buffer + if _dest is None: + _dest = self.localpwd / fname + with _dest.open("wb") as fd: + size = self._get_file(file, fd) + return fname, size + + @CLIUtil.addoutput(get) + def get_output(self, info): + """ + Print the output of 'get' + """ + print("Retrieved '%s' of size %s" % (info[0], human_size(info[1]))) + + @CLIUtil.addcomplete(get) + def get_complete(self, file): + """ + Auto-complete get + """ + if self._require_share(silent=True): + return [] + return self._fs_complete(file) + + @CLIUtil.addcommand(mono=True, globsupport=True) + def cat(self, file): + """ + Print a file + """ + if self._require_share(): + return + # Write the buffer to buffer + buf = io.BytesIO() + self._get_file(file, buf) + return buf.getvalue() + + @CLIUtil.addoutput(cat) + def cat_output(self, result): + """ + Print the output of 'cat' + """ + print(result.decode(errors="backslashreplace")) + + @CLIUtil.addcomplete(cat) + def cat_complete(self, file): + """ + Auto-complete cat + """ + if self._require_share(silent=True): + return [] + return self._fs_complete(file) + + @CLIUtil.addcommand(mono=True, globsupport=True) + def put(self, file): + """ + Upload a file + """ + if self._require_share(): + return + local_file = self.localpwd / file + if local_file.is_dir(): + # Directory + raise ValueError("put on dir not impl") + else: + fname = pathlib.Path(file).name + with local_file.open("rb") as fd: + size = self._send_file(fname, fd) + self.ls_cache.clear() + return fname, size + + @CLIUtil.addcomplete(put) + def put_complete(self, folder): + """ + Auto-complete put + """ + return self._lfs_complete(folder, lambda x: not x.is_dir()) + + @CLIUtil.addcommand(mono=True) + def rm(self, file): + """ + Delete a file + """ + if self._require_share(): + return + # Get pwd of the ls + fpath = self.pwd / file + self.smbsock.set_TID(self.current_tree) + # Open file + fileId = self.smbsock.create_request( + self.normalize_path(fpath), + type="file", + mode="d", + extra_create_options=self.extra_create_options, + ) + # Close the file + self.smbsock.close_request(fileId) + self.ls_cache.clear() + return fpath.name + + @CLIUtil.addcomplete(rm) + def rm_complete(self, file): + """ + Auto-complete rm + """ + if self._require_share(silent=True): + return [] + return self._fs_complete(file) + + @CLIUtil.addcommand() + def backup(self): + """ + Turn on or off backup intent + """ + if "FILE_OPEN_FOR_BACKUP_INTENT" in self.extra_create_options: + print("Backup Intent: Off") + self.extra_create_options.remove("FILE_OPEN_FOR_BACKUP_INTENT") + else: + print("Backup Intent: On") + self.extra_create_options.append("FILE_OPEN_FOR_BACKUP_INTENT") + + @CLIUtil.addcommand(mono=True) + def watch(self, folder): + """ + Watch file changes in folder (recursively) + """ + if self._require_share(): + return + # Get pwd of the ls + fpath = self.pwd / folder + self.smbsock.set_TID(self.current_tree) + # Open file + fileId = self.smbsock.create_request( + self.normalize_path(fpath), + type="folder", + extra_create_options=self.extra_create_options, + ) + print("Watching '%s'" % fpath) + # Watch for changes + try: + while True: + changes = self.smbsock.changenotify(fileId) + for chg in changes: + print(chg.sprintf("%.time%: %Action% %FileName%")) + except KeyboardInterrupt: + pass + print("Cancelled.") + + @CLIUtil.addcommand(mono=True) + def getsd(self, file): + """ + Get the Security Descriptor + """ + if self._require_share(): + return + fpath = self.pwd / file + self.smbsock.set_TID(self.current_tree) + # Open file + fileId = self.smbsock.create_request( + self.normalize_path(fpath), + type="", + mode="", + extra_desired_access=["READ_CONTROL", "ACCESS_SYSTEM_SECURITY"], + ) + # Get the file size + info = self.smbsock.query_info( + FileId=fileId, + InfoType="SMB2_0_INFO_SECURITY", + FileInfoClass=0, + AdditionalInformation=( + 0x00000001 + | 0x00000002 + | 0x00000004 + | 0x00000008 + | 0x00000010 + | 0x00000020 + | 0x00000040 + | 0x00010000 + ), + ) + self.smbsock.close_request(fileId) + return info + + @CLIUtil.addcomplete(getsd) + def getsd_complete(self, file): + """ + Auto-complete getsd + """ + if self._require_share(silent=True): + return [] + return self._fs_complete(file) + + @CLIUtil.addoutput(getsd) + def getsd_output(self, results): + """ + Print the output of 'getsd' + """ + sd = SECURITY_DESCRIPTOR(results) + sd.show_print() + + +if __name__ == "__main__": + from scapy.utils import AutoArgparse + + AutoArgparse(smbclient) diff --git a/scapy/layers/smbserver.py b/scapy/layers/smbserver.py new file mode 100644 index 00000000000..784f791b5ec --- /dev/null +++ b/scapy/layers/smbserver.py @@ -0,0 +1,1892 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +SMB 2 Server Automaton + +This provides a [MS-SMB2] server that can: +- serve files +- host a DCE/RPC server + +This is a Scapy Automaton that is supposedly easily extendable. + +.. note:: + You will find more complete documentation for this layer over at + `SMB `_ +""" + +import hashlib +import pathlib +import socket +import struct +import time + +from scapy.arch import get_if_addr +from scapy.automaton import ATMT, Automaton +from scapy.config import conf +from scapy.consts import WINDOWS +from scapy.error import log_runtime, log_interactive +from scapy.volatile import RandUUID + +from scapy.layers.dcerpc import ( + DCERPC_Transport, + NDRUnion, + NDRPointer, +) +from scapy.layers.gssapi import ( + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_CREDENTIALS_EXPIRED, +) +from scapy.layers.msrpce.rpcserver import DCERPC_Server +from scapy.layers.ntlm import ( + NTLMSSP, +) +from scapy.layers.smb import ( + SMBNegotiate_Request, + SMBNegotiate_Response_Extended_Security, + SMBNegotiate_Response_Security, + SMBSession_Null, + SMBSession_Setup_AndX_Request, + SMBSession_Setup_AndX_Request_Extended_Security, + SMBSession_Setup_AndX_Response, + SMBSession_Setup_AndX_Response_Extended_Security, + SMBTree_Connect_AndX, + SMB_Header, +) +from scapy.layers.windows.security import SECURITY_DESCRIPTOR +from scapy.layers.smb2 import ( + DFS_REFERRAL_ENTRY1, + DFS_REFERRAL_V3, + DirectTCP, + FILE_BOTH_DIR_INFORMATION, + FILE_FULL_DIR_INFORMATION, + FILE_ID_BOTH_DIR_INFORMATION, + FILE_NAME_INFORMATION, + FileAllInformation, + FileAlternateNameInformation, + FileBasicInformation, + FileEaInformation, + FileFsAttributeInformation, + FileFsSizeInformation, + FileFsVolumeInformation, + FileIdBothDirectoryInformation, + FileInternalInformation, + FileNetworkOpenInformation, + FileStandardInformation, + FileStreamInformation, + NETWORK_INTERFACE_INFO, + SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2, + SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE, + SMB2_CREATE_QUERY_ON_DISK_ID, + SMB2_Cancel_Request, + SMB2_Change_Notify_Request, + SMB2_Change_Notify_Response, + SMB2_Close_Request, + SMB2_Close_Response, + SMB2_Create_Context, + SMB2_Create_Request, + SMB2_Create_Response, + SMB2_ENCRYPTION_CIPHERS, + SMB2_Echo_Request, + SMB2_Echo_Response, + SMB2_Encryption_Capabilities, + SMB2_Error_Response, + SMB2_FILEID, + SMB2_Header, + SMB2_IOCTL_Network_Interface_Info, + SMB2_IOCTL_RESP_GET_DFS_Referral, + SMB2_IOCTL_Request, + SMB2_IOCTL_Response, + SMB2_IOCTL_Validate_Negotiate_Info_Response, + SMB2_Negotiate_Context, + SMB2_Negotiate_Protocol_Request, + SMB2_Negotiate_Protocol_Response, + SMB2_Preauth_Integrity_Capabilities, + SMB2_Query_Directory_Request, + SMB2_Query_Directory_Response, + SMB2_Query_Info_Request, + SMB2_Query_Info_Response, + SMB2_Read_Request, + SMB2_Read_Response, + SMB2_SIGNING_ALGORITHMS, + SMB2_Session_Logoff_Request, + SMB2_Session_Logoff_Response, + SMB2_Session_Setup_Request, + SMB2_Session_Setup_Response, + SMB2_Set_Info_Request, + SMB2_Set_Info_Response, + SMB2_Signing_Capabilities, + SMB2_Tree_Connect_Request, + SMB2_Tree_Connect_Response, + SMB2_Tree_Disconnect_Request, + SMB2_Tree_Disconnect_Response, + SMB2_Write_Request, + SMB2_Write_Response, + SMBStreamSocket, + SOCKADDR_STORAGE, + SRVSVC_SHARE_TYPES, +) +from scapy.layers.spnego import SPNEGOSSP + +# Import DCE/RPC +from scapy.layers.msrpce.raw.ms_srvs import ( + LPSERVER_INFO_101, + LPSHARE_ENUM_STRUCT, + LPSHARE_INFO_1, + NetrServerGetInfo_Request, + NetrServerGetInfo_Response, + NetrShareEnum_Request, + NetrShareEnum_Response, + NetrShareGetInfo_Request, + NetrShareGetInfo_Response, + SHARE_INFO_1_CONTAINER, +) +from scapy.layers.msrpce.raw.ms_wkst import ( + LPWKSTA_INFO_100, + NetrWkstaGetInfo_Request, + NetrWkstaGetInfo_Response, +) + + +class SMBShare: + """ + A class used to define a share, used by SMB_Server + + :param name: the share name + :param path: the path the the folder hosted by the share + :param type: (optional) share type per [MS-SRVS] sect 2.2.2.4 + :param remark: (optional) a description of the share + :param encryptdata: (optional) whether encryption should be used for this + share. This only applies to SMB 3.1.1. + """ + + def __init__(self, name, path=".", type=None, remark="", encryptdata=False): + # Set the default type + if type is None: + type = 0 # DISKTREE + if name.endswith("$"): + type &= 0x80000000 # SPECIAL + # Lower case the name for resolution + self._name = name.lower() + # Resolve path + self.path = pathlib.Path(path).resolve() + # props + self.name = name + self.type = type + self.remark = remark + self.encryptdata = encryptdata + + def __repr__(self): + type = SRVSVC_SHARE_TYPES[self.type & 0x0FFFFFFF] + if self.type & 0x80000000: + type = "SPECIAL+" + type + if self.type & 0x40000000: + type = "TEMPORARY+" + type + return "" % ( + self.name, + type, + self.remark and (" '%s'" % self.remark) or "", + str(self.path), + ) + + +# The SMB Automaton + + +class SMB_Server(Automaton): + """ + SMB server automaton + + :param shares: the shares to serve. By default, share nothing. + Note that IPC$ is appended. + :param ssp: the SSP to use + + All other options (in caps) are optional, and SMB specific: + + :param ANONYMOUS_LOGIN: mark the clients as anonymous + :param GUEST_LOGIN: mark the clients as guest + :param REQUIRE_SIGNATURE: set 'Require Signature' + :param REQUIRE_ENCRYPTION: globally require encryption. + You could also make it share-specific on 3.1.1. + :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1) + :param TREE_SHARE_FLAGS: flags to announce on Tree_Connect_Response + :param TREE_CAPABILITIES: capabilities to announce on Tree_Connect_Response + :param TREE_MAXIMAL_ACCESS: maximal access to announce on Tree_Connect_Response + :param FILE_MAXIMAL_ACCESS: maximal access to announce in MxAc Create Context + """ + + pkt_cls = DirectTCP + socketcls = SMBStreamSocket + + def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwargs): + self.verb = verb + if "sock" not in kwargs: + raise ValueError( + "SMB_Server cannot be started directly ! Use SMB_Server.spawn" + ) + # Various SMB server arguments + self.ANONYMOUS_LOGIN = kwargs.pop("ANONYMOUS_LOGIN", False) + self.GUEST_LOGIN = kwargs.pop("GUEST_LOGIN", None) + self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) + self.USE_SMB1 = kwargs.pop("USE_SMB1", False) + self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", None) + self.REQUIRE_ENCRYPTION = kwargs.pop("REQUIRE_ENCRYPTION", False) + self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311) + self.TREE_SHARE_FLAGS = kwargs.pop( + "TREE_SHARE_FLAGS", "FORCE_LEVELII_OPLOCK+RESTRICT_EXCLUSIVE_OPENS" + ) + self.TREE_CAPABILITIES = kwargs.pop("TREE_CAPABILITIES", 0) + self.TREE_MAXIMAL_ACCESS = kwargs.pop( + "TREE_MAXIMAL_ACCESS", + "+".join( + [ + "FILE_READ_DATA", + "FILE_WRITE_DATA", + "FILE_APPEND_DATA", + "FILE_READ_EA", + "FILE_WRITE_EA", + "FILE_EXECUTE", + "FILE_DELETE_CHILD", + "FILE_READ_ATTRIBUTES", + "FILE_WRITE_ATTRIBUTES", + "DELETE", + "READ_CONTROL", + "WRITE_DAC", + "WRITE_OWNER", + "SYNCHRONIZE", + ] + ), + ) + self.FILE_MAXIMAL_ACCESS = kwargs.pop( + # Read-only + "FILE_MAXIMAL_ACCESS", + "+".join( + [ + "FILE_READ_DATA", + "FILE_READ_EA", + "FILE_EXECUTE", + "FILE_READ_ATTRIBUTES", + "READ_CONTROL", + "SYNCHRONIZE", + ] + ), + ) + self.LOCAL_IPS = kwargs.pop( + "LOCAL_IPS", [get_if_addr(kwargs.get("iface", conf.iface) or conf.iface)] + ) + self.DOMAIN_REFERRALS = kwargs.pop("DOMAIN_REFERRALS", []) + if self.USE_SMB1: + log_runtime.warning("Serving SMB1 is not supported :/") + self.readonly = readonly + # We don't want to update the parent shares argument + self.shares = shares.copy() + # Append the IPC$ share + self.shares.append( + SMBShare( + name="IPC$", + type=0x80000003, # SPECIAL+IPC + remark="Remote IPC", + ) + ) + # Initialize the DCE/RPC server for SMB + self.rpc_server = SMB_DCERPC_Server( + DCERPC_Transport.NCACN_NP, + shares=self.shares, + verb=self.verb, + ) + # Extend it if another DCE/RPC server is provided + if "DCERPC_SERVER_CLS" in kwargs: + self.rpc_server.extend(kwargs.pop("DCERPC_SERVER_CLS")) + # Internal Session information + self.SMB2 = False + self.NegotiateCapabilities = None + self.GUID = RandUUID()._fix() + self.NextForceSign = False + self.NextForceEncrypt = False + # Compounds are handled on receiving by the StreamSocket, + # and on aggregated in a CompoundQueue to be sent in one go + self.NextCompound = False + self.CompoundedHandle = None + # SSP provider + if ssp is None: + # No SSP => fallback on NTLM with guest + ssp = SPNEGOSSP( + [ + NTLMSSP( + USE_MIC=False, + DO_NOT_CHECK_LOGIN=True, + ), + ] + ) + if self.GUEST_LOGIN is None: + self.GUEST_LOGIN = True + # Initialize + Automaton.__init__(self, *args, **kwargs) + # Set session options + self.session.ssp = ssp + self.session.SigningRequired = ( + self.REQUIRE_SIGNATURE if self.REQUIRE_SIGNATURE is not None else bool(ssp) + ) + + @property + def session(self): + # session shorthand + return self.sock.session + + def vprint(self, s=""): + """ + Verbose print (if enabled) + """ + if self.verb: + if conf.interactive: + log_interactive.info("> %s", s) + else: + print("> %s" % s) + + def send(self, pkt): + ForceSign, ForceEncrypt = self.NextForceSign, self.NextForceEncrypt + self.NextForceSign = self.NextForceEncrypt = False + return super(SMB_Server, self).send( + pkt, + Compounded=self.NextCompound, + ForceSign=ForceSign, + ForceEncrypt=ForceEncrypt, + ) + + @ATMT.state(initial=1) + def BEGIN(self): + self.authenticated = False + + @ATMT.receive_condition(BEGIN) + def received_negotiate(self, pkt): + if SMBNegotiate_Request in pkt: + raise self.NEGOTIATED().action_parameters(pkt) + + @ATMT.receive_condition(BEGIN) + def received_negotiate_smb2_begin(self, pkt): + if SMB2_Negotiate_Protocol_Request in pkt: + self.SMB2 = True + raise self.NEGOTIATED().action_parameters(pkt) + + @ATMT.action(received_negotiate_smb2_begin) + def on_negotiate_smb2_begin(self, pkt): + self.on_negotiate(pkt) + + @ATMT.action(received_negotiate) + def on_negotiate(self, pkt): + self.session.sspcontext, spnego_token = self.session.ssp.NegTokenInit2() + # Build negotiate response + DialectIndex = None + DialectRevision = None + if SMB2_Negotiate_Protocol_Request in pkt: + # SMB2 + DialectRevisions = pkt[SMB2_Negotiate_Protocol_Request].Dialects + DialectRevisions = [x for x in DialectRevisions if x <= self.MAX_DIALECT] + DialectRevisions.sort(reverse=True) + if DialectRevisions: + DialectRevision = DialectRevisions[0] + else: + # SMB1 + DialectIndexes = [ + x.DialectString for x in pkt[SMBNegotiate_Request].Dialects + ] + if self.USE_SMB1: + # Enforce SMB1 + DialectIndex = DialectIndexes.index(b"NT LM 0.12") + else: + # Find a value matching SMB2, fallback to SMB1 + for key, rev in [(b"SMB 2.???", 0x02FF), (b"SMB 2.002", 0x0202)]: + try: + DialectIndex = DialectIndexes.index(key) + DialectRevision = rev + self.SMB2 = True + break + except ValueError: + pass + else: + DialectIndex = DialectIndexes.index(b"NT LM 0.12") + if DialectRevision and DialectRevision & 0xFF != 0xFF: + # Version isn't SMB X.??? + self.session.Dialect = DialectRevision + cls = None + if self.SMB2: + # SMB2 + cls = SMB2_Negotiate_Protocol_Response + self.smb_header = DirectTCP() / SMB2_Header( + Flags="SMB2_FLAGS_SERVER_TO_REDIR", + CreditRequest=1, + CreditCharge=1, + ) + if SMB2_Negotiate_Protocol_Request in pkt: + self.update_smbheader(pkt) + else: + # SMB1 + self.smb_header = DirectTCP() / SMB_Header( + Flags="REPLY+CASE_INSENSITIVE+CANONICALIZED_PATHS", + Flags2=( + "LONG_NAMES+EAS+NT_STATUS+SMB_SECURITY_SIGNATURE+" + "UNICODE+EXTENDED_SECURITY" + ), + TID=pkt.TID, + MID=pkt.MID, + UID=pkt.UID, + PIDLow=pkt.PIDLow, + ) + if self.EXTENDED_SECURITY: + cls = SMBNegotiate_Response_Extended_Security + else: + cls = SMBNegotiate_Response_Security + if DialectRevision is None and DialectIndex is None: + # No common dialect found. + if self.SMB2: + resp = self.smb_header.copy() / SMB2_Error_Response() + resp.Command = "SMB2_NEGOTIATE" + else: + resp = self.smb_header.copy() / SMBSession_Null() + resp.Command = "SMB_COM_NEGOTIATE" + resp.Status = "STATUS_NOT_SUPPORTED" + self.send(resp) + return + if self.SMB2: # SMB2 + # SecurityMode + if SMB2_Header in pkt and pkt.SecurityMode.SIGNING_REQUIRED: + self.session.SigningRequired = True + # Capabilities: [MS-SMB2] 3.3.5.4 + self.NegotiateCapabilities = "+".join( + [ + "DFS", + "LEASING", + "LARGE_MTU", + ] + ) + if DialectRevision >= 0x0300: + # "if Connection.Dialect belongs to the SMB 3.x dialect family, + # the server supports..." + self.NegotiateCapabilities += "+" + "+".join( + [ + "MULTI_CHANNEL", + "PERSISTENT_HANDLES", + "DIRECTORY_LEASING", + "ENCRYPTION", + ] + ) + # Build response + resp = self.smb_header.copy() / cls( + DialectRevision=DialectRevision, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), + ServerTime=(time.time() + 11644473600) * 1e7, + ServerStartTime=0, + MaxTransactionSize=65536, + MaxReadSize=65536, + MaxWriteSize=65536, + Capabilities=self.NegotiateCapabilities, + ) + # SMB >= 3.0.0 + if DialectRevision >= 0x0300: + # [MS-SMB2] sect 3.3.5.3.1 note 253 + resp.MaxTransactionSize = 0x800000 + resp.MaxReadSize = 0x800000 + resp.MaxWriteSize = 0x800000 + # SMB 3.1.1 + if DialectRevision >= 0x0311 and pkt.NegotiateContextsCount: + # Negotiate context-capabilities + for ngctx in pkt.NegotiateContexts: + if ngctx.ContextType == 0x0002: + # SMB2_ENCRYPTION_CAPABILITIES + for ciph in ngctx.Ciphers: + tciph = SMB2_ENCRYPTION_CIPHERS.get(ciph, None) + if tciph in self.session.SupportedCipherIds: + # Common ! + self.session.CipherId = tciph + self.session.SupportsEncryption = True + break + elif ngctx.ContextType == 0x0008: + # SMB2_SIGNING_CAPABILITIES + for signalg in ngctx.SigningAlgorithms: + tsignalg = SMB2_SIGNING_ALGORITHMS.get(signalg, None) + if tsignalg in self.session.SupportedSigningAlgorithmIds: + # Common ! + self.session.SigningAlgorithmId = tsignalg + break + # Send back the negotiated algorithms + resp.NegotiateContexts = [ + # Preauth capabilities + SMB2_Negotiate_Context() + / SMB2_Preauth_Integrity_Capabilities( + # SHA-512 by default + HashAlgorithms=[self.session.PreauthIntegrityHashId], + Salt=self.session.Salt, + ), + # Encryption capabilities + SMB2_Negotiate_Context() + / SMB2_Encryption_Capabilities( + # AES-128-CCM by default + Ciphers=[self.session.CipherId], + ), + # Signing capabilities + SMB2_Negotiate_Context() + / SMB2_Signing_Capabilities( + # AES-128-CCM by default + SigningAlgorithms=[self.session.SigningAlgorithmId], + ), + ] + else: + # SMB1 + resp = self.smb_header.copy() / cls( + DialectIndex=DialectIndex, + ServerCapabilities=( + "UNICODE+LARGE_FILES+NT_SMBS+RPC_REMOTE_APIS+STATUS32+" + "LEVEL_II_OPLOCKS+LOCK_AND_READ+NT_FIND+" + "LWIO+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX" + ), + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), + ServerTime=(time.time() + 11644473600) * 1e7, + ServerTimeZone=0x3C, + ) + if self.EXTENDED_SECURITY: + resp.ServerCapabilities += "EXTENDED_SECURITY" + if self.EXTENDED_SECURITY or self.SMB2: + # Extended SMB1 / SMB2 + resp.GUID = self.GUID + # Add security blob + resp.SecurityBlob = spnego_token + else: + # Non-extended SMB1 + # FIXME never tested. + resp.SecurityBlob = spnego_token + resp.Flags2 -= "EXTENDED_SECURITY" + if not self.SMB2: + resp[SMB_Header].Flags2 = ( + resp[SMB_Header].Flags2 + - "SMB_SECURITY_SIGNATURE" + + "SMB_SECURITY_SIGNATURE_REQUIRED+IS_LONG_NAME" + ) + if SMB2_Header in pkt: + # If required, compute sessions + self.session.computeSMBConnectionPreauth( + bytes(pkt[SMB2_Header]), # nego request + bytes(resp[SMB2_Header]), # nego response + ) + self.send(resp) + + @ATMT.state(final=1) + def NEGO_FAILED(self): + self.vprint("SMB Negotiate failed: encryption was not negotiated.") + self.end() + + @ATMT.state() + def NEGOTIATED(self): + pass + + def update_smbheader(self, pkt): + """ + Called when receiving a SMB2 packet to update the current smb_header + """ + # [MS-SMB2] sect 3.2.5.1.4 - always grant client its credits + self.smb_header.CreditRequest = pkt.CreditRequest + # [MS-SMB2] sect 3.3.4.1 + # "the server SHOULD set the CreditCharge field in the SMB2 header + # of the response to the CreditCharge value in the SMB2 header of the request." + self.smb_header.CreditCharge = pkt.CreditCharge + # If the packet has a NextCommand, set NextCompound to True + self.NextCompound = bool(pkt.NextCommand) + # [MS-SMB2] sect 3.3.4.1.1 - "If the request was signed by the client..." + # If the packet was signed, note we must answer with a signed packet. + if ( + not self.session.SigningRequired + and pkt.SecuritySignature != b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + ): + self.NextForceSign = True + # [MS-SMB2] sect 3.3.4.1.4 - "If the message being sent is any response to a + # client request for which Request.IsEncrypted is TRUE" + if pkt[SMB2_Header]._decrypted: + self.NextForceEncrypt = True + # [MS-SMB2] sect 3.3.5.2.7.2 + # Add SMB2_FLAGS_RELATED_OPERATIONS to the response if present + if pkt.Flags.SMB2_FLAGS_RELATED_OPERATIONS: + self.smb_header.Flags += "SMB2_FLAGS_RELATED_OPERATIONS" + else: + self.smb_header.Flags -= "SMB2_FLAGS_RELATED_OPERATIONS" + # [MS-SMB2] sect 2.2.1.2 - Priority + if (self.session.Dialect or 0) >= 0x0311: + self.smb_header.Flags &= 0xFF8F + self.smb_header.Flags |= int(pkt.Flags) & 0x70 + # Update IDs + self.smb_header.SessionId = pkt.SessionId + self.smb_header.TID = pkt.TID + self.smb_header.MID = pkt.MID + self.smb_header.PID = pkt.PID + + @ATMT.receive_condition(NEGOTIATED) + def received_negotiate_smb2(self, pkt): + if SMB2_Negotiate_Protocol_Request in pkt: + raise self.NEGOTIATED().action_parameters(pkt) + + @ATMT.action(received_negotiate_smb2) + def on_negotiate_smb2(self, pkt): + self.on_negotiate(pkt) + + @ATMT.receive_condition(NEGOTIATED) + def receive_setup_andx_request(self, pkt): + if ( + SMBSession_Setup_AndX_Request_Extended_Security in pkt + or SMBSession_Setup_AndX_Request in pkt + ): + # SMB1 + if SMBSession_Setup_AndX_Request_Extended_Security in pkt: + # Extended + ssp_blob = pkt.SecurityBlob + else: + # Non-extended + ssp_blob = pkt[SMBSession_Setup_AndX_Request].UnicodePassword + raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt, ssp_blob) + elif SMB2_Session_Setup_Request in pkt: + # SMB2 + ssp_blob = pkt.SecurityBlob + raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt, ssp_blob) + + @ATMT.state() + def RECEIVED_SETUP_ANDX_REQUEST(self): + pass + + @ATMT.action(receive_setup_andx_request) + def on_setup_andx_request(self, pkt, ssp_blob): + self.session.sspcontext, tok, status = self.session.ssp.GSS_Accept_sec_context( + self.session.sspcontext, + ssp_blob, + ) + self.update_smbheader(pkt) + if SMB2_Session_Setup_Request in pkt: + # SMB2 + self.smb_header.SessionId = 0x0001000000000015 + if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: + # Error + if SMB2_Session_Setup_Request in pkt: + # SMB2 + resp = self.smb_header.copy() / SMB2_Session_Setup_Response() + # Set security blob (if any) + resp.SecurityBlob = tok + else: + # SMB1 + resp = self.smb_header.copy() / SMBSession_Null() + # Map some GSS return codes to NTStatus + if status == GSS_S_CREDENTIALS_EXPIRED: + resp.Status = "STATUS_PASSWORD_EXPIRED" + else: + resp.Status = "STATUS_LOGON_FAILURE" + # Reset Session preauth (SMB 3.1.1) + self.session.SessionPreauthIntegrityHashValue = None + else: + # Negotiation + if ( + SMBSession_Setup_AndX_Request_Extended_Security in pkt + or SMB2_Session_Setup_Request in pkt + ): + # SMB1 extended / SMB2 + if SMB2_Session_Setup_Request in pkt: + resp = self.smb_header.copy() / SMB2_Session_Setup_Response() + if self.GUEST_LOGIN: + # "If the security subsystem indicates that the session + # was established by a guest user, Session.SigningRequired + # MUST be set to FALSE and Session.IsGuest MUST be set to TRUE." + resp.SessionFlags = "IS_GUEST" + self.session.IsGuest = True + self.session.SigningRequired = False + if self.ANONYMOUS_LOGIN: + resp.SessionFlags = "IS_NULL" + # [MS-SMB2] sect 3.3.5.5.3 + if self.session.Dialect >= 0x0300 and self.REQUIRE_ENCRYPTION: + resp.SessionFlags += "ENCRYPT_DATA" + else: + # SMB1 extended + resp = ( + self.smb_header.copy() + / SMBSession_Setup_AndX_Response_Extended_Security( + NativeOS="Windows 4.0", + NativeLanMan="Windows 4.0", + ) + ) + if self.GUEST_LOGIN: + resp.Action = "SMB_SETUP_GUEST" + # Set security blob + resp.SecurityBlob = tok + elif SMBSession_Setup_AndX_Request in pkt: + # Non-extended + resp = self.smb_header.copy() / SMBSession_Setup_AndX_Response( + NativeOS="Windows 4.0", + NativeLanMan="Windows 4.0", + ) + resp.Status = 0x0 if (status == GSS_S_COMPLETE) else 0xC0000016 + # We have a response. If required, compute sessions + if status == GSS_S_CONTINUE_NEEDED: + # the setup session response is used in hash + self.session.computeSMBSessionPreauth( + bytes(pkt[SMB2_Header]), # session setup request + bytes(resp[SMB2_Header]), # session setup response + ) + else: + # the setup session response is not used in hash + self.session.computeSMBSessionPreauth( + bytes(pkt[SMB2_Header]), # session setup request + ) + if status == GSS_S_COMPLETE: + # Authentication was successful + self.session.computeSMBSessionKeys(IsClient=False) + self.authenticated = True + # [MS-SMB2] Note: "Windows-based servers always sign the final session setup + # response when the user is neither anonymous nor guest." + # If not available, it will still be ignored. + self.NextForceSign = True + self.send(resp) + # Check whether we must enable encryption from now on + if ( + self.authenticated + and not self.session.IsGuest + and self.session.Dialect >= 0x0300 + and self.REQUIRE_ENCRYPTION + ): + # [MS-SMB2] sect 3.3.5.5.3: from now on, turn encryption on ! + self.session.EncryptData = True + self.session.SigningRequired = False + + @ATMT.condition(RECEIVED_SETUP_ANDX_REQUEST) + def wait_for_next_request(self): + if self.authenticated: + self.vprint( + "User authenticated %s!" % (self.GUEST_LOGIN and " as guest" or "") + ) + raise self.AUTHENTICATED() + else: + raise self.NEGOTIATED() + + @ATMT.state() + def AUTHENTICATED(self): + """Dev: overload this""" + pass + + # DEV: add a condition on AUTHENTICATED with prio=0 + + @ATMT.condition(AUTHENTICATED, prio=1) + def should_serve(self): + # Serve files + self.current_trees = {} + self.current_handles = {} + self.enumerate_index = {} # used for query directory enumeration + self.tree_id = 0 + self.base_time_t = self.current_smb_time() + raise self.SERVING() + + def _ioctl_error(self, Status="STATUS_NOT_SUPPORTED"): + pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff") + pkt.Status = Status + pkt.Command = "SMB2_IOCTL" + self.send(pkt) + + @ATMT.state(final=1) + def END(self): + self.end() + + # SERVE FILES + + def current_tree(self): + """ + Return the current tree name + """ + return self.current_trees[self.smb_header.TID] + + def root_path(self): + """ + Return the root path of the current tree + """ + curtree = self.current_tree() + try: + share_path = next(x.path for x in self.shares if x._name == curtree.lower()) + except StopIteration: + return None + return pathlib.Path(share_path).resolve() + + @ATMT.state() + def SERVING(self): + """ + Main state when serving files + """ + pass + + @ATMT.receive_condition(SERVING) + def receive_logoff_request(self, pkt): + if SMB2_Session_Logoff_Request in pkt: + raise self.NEGOTIATED().action_parameters(pkt) + + @ATMT.action(receive_logoff_request) + def send_logoff_response(self, pkt): + self.update_smbheader(pkt) + self.send(self.smb_header.copy() / SMB2_Session_Logoff_Response()) + + @ATMT.receive_condition(SERVING) + def receive_setup_andx_request_in_serving(self, pkt): + self.receive_setup_andx_request(pkt) + + @ATMT.receive_condition(SERVING) + def is_smb1_tree(self, pkt): + if SMBTree_Connect_AndX in pkt: + # Unsupported + log_runtime.warning("Tree request in SMB1: unimplemented. Quit") + raise self.END() + + @ATMT.receive_condition(SERVING) + def receive_tree_connect(self, pkt): + if SMB2_Tree_Connect_Request in pkt: + tree_name = pkt[SMB2_Tree_Connect_Request].Path.split("\\")[-1] + raise self.SERVING().action_parameters(pkt, tree_name) + + @ATMT.action(receive_tree_connect) + def send_tree_connect_response(self, pkt, tree_name): + self.update_smbheader(pkt) + # Check the tree name against the shares we're serving + try: + share = next(x for x in self.shares if x._name == tree_name.lower()) + except StopIteration: + # Unknown tree + resp = self.smb_header.copy() / SMB2_Error_Response() + resp.Command = "SMB2_TREE_CONNECT" + resp.Status = "STATUS_BAD_NETWORK_NAME" + self.send(resp) + return + # Add tree to current trees + if tree_name not in self.current_trees: + self.tree_id += 1 + self.smb_header.TID = self.tree_id + self.current_trees[self.smb_header.TID] = tree_name + + # Construct ShareFlags + ShareFlags = ( + "AUTO_CACHING+NO_CACHING" + if self.current_tree() == "IPC$" + else self.TREE_SHARE_FLAGS + ) + # [MS-SMB2] sect 3.3.5.7 + if ( + self.session.Dialect >= 0x0311 + and not self.session.EncryptData + and share.encryptdata + ): + if not self.session.SupportsEncryption: + raise Exception("Peer asked for encryption but doesn't support it !") + ShareFlags += "+ENCRYPT_DATA" + + self.vprint("Tree Connect on: %s" % tree_name) + self.send( + self.smb_header.copy() + / SMB2_Tree_Connect_Response( + ShareType="PIPE" if self.current_tree() == "IPC$" else "DISK", + ShareFlags=ShareFlags, + Capabilities=( + 0 if self.current_tree() == "IPC$" else self.TREE_CAPABILITIES + ), + MaximalAccess=self.TREE_MAXIMAL_ACCESS, + ) + ) + + @ATMT.receive_condition(SERVING) + def receive_ioctl(self, pkt): + if SMB2_IOCTL_Request in pkt: + raise self.SERVING().action_parameters(pkt) + + @ATMT.action(receive_ioctl) + def send_ioctl_response(self, pkt): + self.update_smbheader(pkt) + if pkt.CtlCode == 0x11C017: + # FSCTL_PIPE_TRANSCEIVE + self.rpc_server.recv(pkt.Input.load) + self.send( + self.smb_header.copy() + / SMB2_IOCTL_Response( + CtlCode=0x11C017, + FileId=pkt[SMB2_IOCTL_Request].FileId, + Buffer=[("Output", self.rpc_server.get_response())], + ) + ) + elif pkt.CtlCode == 0x00140204 and self.session.sspcontext.SessionKey: + # FSCTL_VALIDATE_NEGOTIATE_INFO + # This is a security measure asking the server to validate + # what flags were negotiated during the SMBNegotiate exchange. + # This packet is ALWAYS signed, and expects a signed response. + + # https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation + # > "Down-level servers (pre-Windows 2012) will return + # > STATUS_NOT_SUPPORTED or STATUS_INVALID_DEVICE_REQUEST + # > since they do not allow or implement + # > FSCTL_VALIDATE_NEGOTIATE_INFO. + # > The client should accept the + # > response provided it's properly signed". + + if (self.session.Dialect or 0) < 0x0300: + # SMB < 3 isn't supposed to support FSCTL_VALIDATE_NEGOTIATE_INFO + self._ioctl_error(Status="STATUS_FILE_CLOSED") + return + + # SMB3 + self.send( + self.smb_header.copy() + / SMB2_IOCTL_Response( + CtlCode=0x00140204, + FileId=pkt[SMB2_IOCTL_Request].FileId, + Buffer=[ + ( + "Output", + SMB2_IOCTL_Validate_Negotiate_Info_Response( + GUID=self.GUID, + DialectRevision=self.session.Dialect, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), + Capabilities=self.NegotiateCapabilities, + ), + ) + ], + ) + ) + elif pkt.CtlCode == 0x001401FC: + # FSCTL_QUERY_NETWORK_INTERFACE_INFO + self.send( + self.smb_header.copy() + / SMB2_IOCTL_Response( + CtlCode=0x001401FC, + FileId=pkt[SMB2_IOCTL_Request].FileId, + Output=SMB2_IOCTL_Network_Interface_Info( + interfaces=[ + NETWORK_INTERFACE_INFO( + SockAddr_Storage=SOCKADDR_STORAGE( + Family=0x0002, + IPv4Adddress=x, + ) + ) + for x in self.LOCAL_IPS + ] + ), + ) + ) + elif pkt.CtlCode == 0x00060194: + # FSCTL_DFS_GET_REFERRALS + if ( + self.DOMAIN_REFERRALS + and not pkt[SMB2_IOCTL_Request].Input.RequestFileName + ): + # Requesting domain referrals + self.send( + self.smb_header.copy() + / SMB2_IOCTL_Response( + CtlCode=0x00060194, + FileId=pkt[SMB2_IOCTL_Request].FileId, + Output=SMB2_IOCTL_RESP_GET_DFS_Referral( + ReferralEntries=[ + DFS_REFERRAL_V3( + ReferralEntryFlags="NameListReferral", + TimeToLive=600, + ) + for _ in self.DOMAIN_REFERRALS + ], + ReferralBuffer=[ + DFS_REFERRAL_ENTRY1(SpecialName=name) + for name in self.DOMAIN_REFERRALS + ], + ), + ) + ) + return + resp = self.smb_header.copy() / SMB2_Error_Response() + resp.Command = "SMB2_IOCTL" + resp.Status = "STATUS_FS_DRIVER_REQUIRED" + self.send(resp) + else: + # Among other things, FSCTL_VALIDATE_NEGOTIATE_INFO + self._ioctl_error(Status="STATUS_NOT_SUPPORTED") + + @ATMT.receive_condition(SERVING) + def receive_create_file(self, pkt): + if SMB2_Create_Request in pkt: + raise self.SERVING().action_parameters(pkt) + + PIPES_TABLE = { + "srvsvc": SMB2_FILEID(Persistent=0x4000000012, Volatile=0x4000000001), + "wkssvc": SMB2_FILEID(Persistent=0x4000000013, Volatile=0x4000000002), + "NETLOGON": SMB2_FILEID(Persistent=0x4000000014, Volatile=0x4000000003), + } + + # special handle in case of compounded requests ([MS-SMB2] 3.2.4.1.4) + # that points to the chained opened file handle + LAST_HANDLE = SMB2_FILEID( + Persistent=0xFFFFFFFFFFFFFFFF, Volatile=0xFFFFFFFFFFFFFFFF + ) + + def current_smb_time(self): + return ( + FileNetworkOpenInformation().get_field("CreationTime").i2m(None, None) + - 864000000000 # one day ago + ) + + def make_file_id(self, fname): + """ + Generate deterministic FileId based on the fname + """ + hash = hashlib.md5((fname or "").encode()).digest() + return 0x4000000000 | struct.unpack("= 2: + log_runtime.info("-- Scapy %s SMB Server --" % conf.version) + log_runtime.info( + "SSP: %s. Read-Only: %s. Serving %s shares:" + % ( + conf.color_theme.yellow(ssp or "NTLM (guest)"), + ( + conf.color_theme.yellow("YES") + if readonly + else conf.color_theme.format("NO", "bg_red+white") + ), + conf.color_theme.red(len(shares)), + ) + ) + for share in shares: + log_runtime.info(" * %s" % share) + # Start SMB Server + self.srv = SMB_Server.spawn( + # TCP server + port=port, + iface=iface or conf.loopback_name, + verb=verb, + # SMB server + ssp=ssp, + shares=shares, + readonly=readonly, + # SMB arguments + **kwargs, + ) + + def close(self): + """ + Close the smbserver if started in background mode (bg=True) + """ + if self.srv: + try: + self.srv.shutdown(socket.SHUT_RDWR) + except OSError: + pass + self.srv.close() + + +if __name__ == "__main__": + from scapy.utils import AutoArgparse + + AutoArgparse(smbserver) diff --git a/scapy/layers/snmp.py b/scapy/layers/snmp.py index 6505995db5c..d6a8bf69e56 100644 --- a/scapy/layers/snmp.py +++ b/scapy/layers/snmp.py @@ -1,18 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ SNMP (Simple Network Management Protocol). """ -from __future__ import print_function from scapy.packet import bind_layers, bind_bottom_up from scapy.asn1packet import ASN1_Packet from scapy.asn1fields import ASN1F_INTEGER, ASN1F_IPADDRESS, ASN1F_OID, \ ASN1F_SEQUENCE, ASN1F_SEQUENCE_OF, ASN1F_STRING, ASN1F_TIME_TICKS, \ - ASN1F_enum_INTEGER, ASN1F_field, ASN1F_CHOICE + ASN1F_enum_INTEGER, ASN1F_field, ASN1F_CHOICE, ASN1F_optional, ASN1F_NULL from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Codecs, ASN1_NULL, \ ASN1_SEQUENCE from scapy.asn1.ber import BERcodec_SEQUENCE @@ -178,9 +177,17 @@ class ASN1F_SNMP_PDU_TRAPv2(ASN1F_SEQUENCE): class SNMPvarbind(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE(ASN1F_OID("oid", "1.3"), - ASN1F_field("value", ASN1_NULL(0)) - ) + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("oid", "1.3"), + ASN1F_optional( + ASN1F_field("value", ASN1_NULL(0)) + ), + + # exceptions in responses + ASN1F_optional(ASN1F_NULL("noSuchObject", None, implicit_tag=0x80)), + ASN1F_optional(ASN1F_NULL("noSuchInstance", None, implicit_tag=0x81)), + ASN1F_optional(ASN1F_NULL("endOfMibView", None, implicit_tag=0x82)), + ) class SNMPget(ASN1_Packet): @@ -269,9 +276,7 @@ class SNMP(ASN1_Packet): def answers(self, other): return (isinstance(self.PDU, SNMPresponse) and - (isinstance(other.PDU, SNMPget) or - isinstance(other.PDU, SNMPnext) or - isinstance(other.PDU, SNMPset)) and + isinstance(other.PDU, (SNMPget, SNMPnext, SNMPset)) and self.PDU.id == other.PDU.id) diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py new file mode 100644 index 00000000000..7073d907295 --- /dev/null +++ b/scapy/layers/spnego.py @@ -0,0 +1,1326 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +SPNEGO + +Implements parts of: + +- GSSAPI SPNEGO: RFC4178 > RFC2478 +- GSSAPI SPNEGO NEGOEX: [MS-NEGOEX] + +.. note:: + You will find more complete documentation for this layer over at + `GSSAPI `_ +""" + +import os +import struct +from uuid import UUID + +from scapy.asn1.asn1 import ( + ASN1_Codecs, + ASN1_OID, + ASN1_GENERAL_STRING, +) +from scapy.asn1.mib import conf # loads conf.mib +from scapy.asn1fields import ( + ASN1F_CHOICE, + ASN1F_ENUMERATED, + ASN1F_FLAGS, + ASN1F_GENERAL_STRING, + ASN1F_OID, + ASN1F_optional, + ASN1F_PACKET, + ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_STRING_ENCAPS, + ASN1F_STRING, +) +from scapy.asn1packet import ASN1_Packet +from scapy.consts import WINDOWS +from scapy.fields import ( + FieldListField, + LEIntEnumField, + LEIntField, + LELongEnumField, + LELongField, + LEShortField, + MultipleTypeField, + PacketField, + PacketListField, + StrField, + StrFixedLenField, + UUIDEnumField, + UUIDField, + XStrFixedLenField, + XStrLenField, +) +from scapy.error import log_runtime +from scapy.packet import Packet, bind_layers +from scapy.utils import ( + valid_ip, + valid_ip6, +) + +from scapy.layers.gssapi import ( + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, + GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_BAD_MECH, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_FAILURE, + GSS_S_FLAGS, + GSSAPI_BLOB_SIGNATURE, + GSSAPI_BLOB, + GssChannelBindings, + SSP, +) + +# SSP Providers +from scapy.layers.kerberos import ( + Kerberos, + KerberosSSP, + _parse_spn, + _parse_upn, +) +from scapy.layers.ntlm import ( + NTLMSSP, + MD4le, + NEGOEX_EXCHANGE_NTLM, + NTLM_Header, + _NTLMPayloadField, + _NTLMPayloadPacket, +) + +# Typing imports +from typing import ( + Dict, + List, + Optional, + Tuple, +) + +# SPNEGO negTokenInit +# https://datatracker.ietf.org/doc/html/rfc4178#section-4.2.1 + + +class SPNEGO_MechType(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_OID("oid", None) + + +class SPNEGO_MechTypes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("mechTypes", None, SPNEGO_MechType) + + +class SPNEGO_MechListMIC(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_STRING_ENCAPS("value", "", GSSAPI_BLOB_SIGNATURE) + + +_mechDissector = { + "1.3.6.1.4.1.311.2.2.10": NTLM_Header, # NTLM + "1.2.840.48018.1.2.2": Kerberos, # MS KRB5 - Microsoft Kerberos 5 + "1.2.840.113554.1.2.2": Kerberos, # Kerberos 5 + "1.2.840.113554.1.2.2.3": Kerberos, # Kerberos 5 - User to User +} + + +class _SPNEGO_Token_Field(ASN1F_STRING): + def i2m(self, pkt, x): + if x is None: + x = b"" + return super(_SPNEGO_Token_Field, self).i2m(pkt, bytes(x)) + + def m2i(self, pkt, s): + dat, r = super(_SPNEGO_Token_Field, self).m2i(pkt, s) + types = None + if isinstance(pkt.underlayer, SPNEGO_negTokenInit): + types = pkt.underlayer.mechTypes + elif isinstance(pkt.underlayer, SPNEGO_negTokenResp): + types = [pkt.underlayer.supportedMech] + if types and types[0] and types[0].oid.val in _mechDissector: + return _mechDissector[types[0].oid.val](dat.val), r + else: + # Use heuristics + return GSSAPI_BLOB(dat.val), r + + +class SPNEGO_Token(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = _SPNEGO_Token_Field("value", None) + + +_ContextFlags = [ + "delegFlag", + "mutualFlag", + "replayFlag", + "sequenceFlag", + "superseded", + "anonFlag", + "confFlag", + "integFlag", +] + + +class SPNEGO_negHints(ASN1_Packet): + # [MS-SPNG] 2.2.1 + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional( + ASN1F_GENERAL_STRING( + "hintName", "not_defined_in_RFC4178@please_ignore", explicit_tag=0xA0 + ), + ), + ASN1F_optional( + ASN1F_GENERAL_STRING("hintAddress", None, explicit_tag=0xA1), + ), + ) + + +class SPNEGO_negTokenInit(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional( + ASN1F_SEQUENCE_OF("mechTypes", None, SPNEGO_MechType, explicit_tag=0xA0) + ), + ASN1F_optional(ASN1F_FLAGS("reqFlags", None, _ContextFlags, implicit_tag=0x81)), + ASN1F_optional( + ASN1F_PACKET("mechToken", None, SPNEGO_Token, explicit_tag=0xA2) + ), + # [MS-SPNG] flavor ! + ASN1F_optional( + ASN1F_PACKET("negHints", None, SPNEGO_negHints, explicit_tag=0xA3) + ), + ASN1F_optional( + ASN1F_PACKET("mechListMIC", None, SPNEGO_MechListMIC, explicit_tag=0xA4) + ), + # Compat with RFC 4178's SPNEGO_negTokenInit + ASN1F_optional( + ASN1F_PACKET("_mechListMIC", None, SPNEGO_MechListMIC, explicit_tag=0xA3) + ), + ) + + +# SPNEGO negTokenTarg +# https://datatracker.ietf.org/doc/html/rfc4178#section-4.2.2 + + +class SPNEGO_negTokenResp(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional( + ASN1F_ENUMERATED( + "negState", + 0, + { + 0: "accept-completed", + 1: "accept-incomplete", + 2: "reject", + 3: "request-mic", + }, + explicit_tag=0xA0, + ), + ), + ASN1F_optional( + ASN1F_PACKET( + "supportedMech", SPNEGO_MechType(), SPNEGO_MechType, explicit_tag=0xA1 + ), + ), + ASN1F_optional( + ASN1F_PACKET("responseToken", None, SPNEGO_Token, explicit_tag=0xA2) + ), + ASN1F_optional( + ASN1F_PACKET("mechListMIC", None, SPNEGO_MechListMIC, explicit_tag=0xA3) + ), + ) + + +class SPNEGO_negToken(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "token", + SPNEGO_negTokenInit(), + ASN1F_PACKET( + "negTokenInit", + SPNEGO_negTokenInit(), + SPNEGO_negTokenInit, + explicit_tag=0xA0, + ), + ASN1F_PACKET( + "negTokenResp", + SPNEGO_negTokenResp(), + SPNEGO_negTokenResp, + explicit_tag=0xA1, + ), + ) + + +# Register for the GSS API Blob + +_GSSAPI_OIDS["1.3.6.1.5.5.2"] = SPNEGO_negToken +_GSSAPI_SIGNATURE_OIDS["1.3.6.1.5.5.2"] = SPNEGO_negToken + + +def mechListMIC(oids): + """ + Implementation of RFC 4178 - Appendix D. mechListMIC Computation + + NOTE: The documentation on mechListMIC isn't super clear, so note that: + + - The mechListMIC that the client sends is computed over the + list of mechanisms that it requests. + - the mechListMIC that the server sends is computed over the + list of mechanisms that the client requested. + + This also means that NegTokenInit2 added by [MS-SPNG] is NOT protected. + That's not necessarily an issue, since it was optional in most cases, + but it's something to keep in mind. + """ + return bytes(SPNEGO_MechTypes(mechTypes=oids)) + + +# NEGOEX +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-negoex/0ad7a003-ab56-4839-a204-b555ca6759a2 + + +_NEGOEX_AUTH_SCHEMES = { + # Reversed. Is there any doc related to this? + # The NEGOEX doc is very ellusive + UUID("5c33530d-eaf9-0d4d-b2ec-4ae3786ec308"): "UUID('[NTLM-UUID]')", +} + + +class NEGOEX_MESSAGE_HEADER(Packet): + fields_desc = [ + StrFixedLenField("Signature", "NEGOEXTS", length=8), + LEIntEnumField( + "MessageType", + 0, + { + 0x0: "INITIATOR_NEGO", + 0x01: "ACCEPTOR_NEGO", + 0x02: "INITIATOR_META_DATA", + 0x03: "ACCEPTOR_META_DATA", + 0x04: "CHALLENGE", + 0x05: "AP_REQUEST", + 0x06: "VERIFY", + 0x07: "ALERT", + }, + ), + LEIntField("SequenceNum", 0), + LEIntField("cbHeaderLength", None), + LEIntField("cbMessageLength", None), + UUIDField("ConversationId", None), + ] + + def post_build(self, pkt, pay): + if self.cbHeaderLength is None: + pkt = pkt[16:] + struct.pack(" bytes + """Util function to build the offset and populate the lengths""" + for field_name, value in self.fields["Payload"]: + length = self.get_field("Payload").fields_map[field_name].i2len(self, value) + count = self.get_field("Payload").fields_map[field_name].i2count(self, value) + offset = fields[field_name] + # Offset + if self.getfieldval(field_name + "BufferOffset") is None: + p = p[:offset] + struct.pack(" bytes + return ( + _NEGOEX_post_build( + self, + pkt, + self.OFFSET, + { + "AuthScheme": 96, + "Extension": 102, + }, + ) + + pay + ) + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 12: + MessageType = struct.unpack(" None: + """ + Perform SSP negotiation. + + This updates our context and sets it with the first SSP that is + common to both client and server. This also applies rules from + [MS-SPNG] and RFC4178 to determine if mechListMIC is required. + """ + if self.other_mechtypes is None: + # We don't have any information about the peer's preferred SSPs. + # This typically happens on client side, when NegTokenInit2 isn't used. + self.ssp = self.ssps[0] + ssp_oid = self.ssp.GSS_Inquire_names_for_mech()[0] + else: + # Get first common SSP between us and our peer + other_oids = [x.oid.val for x in self.other_mechtypes] + try: + self.ssp, ssp_oid = next( + (ssp, requested_oid) + for requested_oid in other_oids + for ssp in self.ssps + if requested_oid in ssp.GSS_Inquire_names_for_mech() + ) + except StopIteration: + raise ValueError( + "Could not find a common SSP with the remote peer !" + ) + + # Check whether the selected SSP was the one preferred by the client + self.first_choice = ssp_oid == other_oids[0] + + # Check whether mechListMIC is mandatory for this exchange + if not self.first_choice: + # RFC4178 rules for mechListMIC: mandatory if not the first choice. + self.require_mic = True + elif ssp_oid == "1.3.6.1.4.1.311.2.2.10" and self.ssp.SupportsMechListMIC(): + # [MS-SPNG] note 8: "If NTLM authentication is most preferred by + # the client and the server, and the client includes a MIC in + # AUTHENTICATE_MESSAGE, then the mechListMIC field becomes + # mandatory" + self.require_mic = True + + # Get the associated ssp dissection class and mechtype + self.ssp_mechtype = SPNEGO_MechType(oid=ASN1_OID(ssp_oid)) + + # Reset the ssp context + self.ssp_context = None + + # Passthrough attributes and functions + + def clifailure(self): + if self.ssp_context is not None: + self.ssp_context.clifailure() + + def __getattr__(self, attr): + try: + return object.__getattribute__(self, attr) + except AttributeError: + return getattr(self.ssp_context, attr) + + def __setattr__(self, attr, val): + try: + return object.__setattr__(self, attr, val) + except AttributeError: + return setattr(self.ssp_context, attr, val) + + # Passthrough the flags property + + @property + def flags(self): + if self.ssp_context: + return self.ssp_context.flags + return GSS_C_FLAGS(0) + + @flags.setter + def flags(self, x): + if not self.ssp_context: + return + self.ssp_context.flags = x + + def __repr__(self): + return "SPNEGOSSP[%s]" % repr(self.ssp_context) + + def __init__(self, ssps: List[SSP], **kwargs): + self.ssps = ssps + super(SPNEGOSSP, self).__init__(**kwargs) + + @classmethod + def from_cli_arguments( + cls, + UPN: str, + target: str, + password: str = None, + HashNt: bytes = None, + HashAes256Sha96: bytes = None, + HashAes128Sha96: bytes = None, + kerberos_required: bool = False, + ST=None, + TGT=None, + KEY=None, + ccache: str = None, + debug: int = 0, + use_krb5ccname: bool = False, + use_winssp: bool = False, + ): + """ + Initialize a SPNEGOSSP from a list of many arguments. + + This is useful in a CLI, as it will try to build the best SPNEGOSSP + with NTLM and Kerberos based on the various parameters. + + :param UPN: the UPN of the user to use. + :param target: the target IP/hostname entered by the user. + :param kerberos_required: require kerberos + :param password: (string) if provided, used for auth + :param HashNt: (bytes) if provided, used for auth (NTLM) + :param HashAes256Sha96: (bytes) if provided, used for auth (Kerberos) + :param HashAes128Sha96: (bytes) if provided, used for auth (Kerberos) + :param ST: if provided, the service ticket to use (Kerberos) + :param TGT: if provided, the TGT to use (Kerberos) + :param KEY: if ST provided, the session key associated to the ticket (Kerberos). + This can be either for the ST or TGT. Else, the user secret key. + :param ccache: (str) if provided, a path to a CCACHE (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. + :param use_winssp: (bool) (only works on Windows). Use implicit authentication + through WinSSP. + """ + kerberos = True + hostname = None + # Check if target is a hostname / Check IP + if target and ":" in target: + if not valid_ip6(target): + hostname = target + else: + if not valid_ip(target): + hostname = target + + # If using WinSSP, this goes fast. + if use_winssp: + if not WINDOWS: + raise OSError("Cannot use WinSSP on a non-Windows computer !") + from scapy.arch.windows.sspi import WinSSP + + return WinSSP() + + # Check UPN + try: + _, realm = _parse_upn(UPN) + if realm == ".": + # Local + kerberos = False + except ValueError: + # not a UPN: NTLM only + kerberos = False + + # If we're asked, check the environment for KRB5CCNAME + if use_krb5ccname and ccache is None and "KRB5CCNAME" in os.environ: + ccache = os.environ["KRB5CCNAME"] + + # Do we need to ask the password? + if all( + x is None + for x in [ + ST, + password, + HashNt, + HashAes256Sha96, + HashAes128Sha96, + ccache, + ] + ): + # yes. + from prompt_toolkit import prompt + + password = prompt("Password: ", is_password=True) + + ssps = [] + # Kerberos + if kerberos and hostname: + # Get ticket if we don't already have one. + if ST is None and TGT is None and ccache is not None: + # In this case, load the KerberosSSP from ccache + from scapy.modules.ticketer import Ticketer + + # Import into a Ticketer object + t = Ticketer() + t.open_ccache(ccache) + + # Look for the ticket that we'll use. We chose: + # - either a ST if the UPN and SPN matches our target + # - or a ST that matches the UPN + # - else a TGT if we got nothing better + tgts = [] + sts = [] + for i, (tkt, key, upn, spn) in t.enumerate_tickets(): + spn, _ = _parse_spn(spn) + spn_host = spn.split("/")[-1] + # Check that it's for the correct user + if upn.lower() == UPN.lower(): + # Check that it's either a TGT or a ST to the correct service + if spn.lower().startswith("krbtgt/"): + # TGT. Keep it, and see if we don't have a better ST. + tgts.append(t.ssp(i)) + elif hostname.lower() == spn_host.lower(): + # ST. UPN and SPN match. We're done ! + ssps.append(t.ssp(i)) + break + else: + # ST. UPN matches, Keep it + sts.append(t.ssp(i)) + else: + # No perfect ticket found + if tgts: + # Using a TGT ! + ssps.append(tgts[0]) + elif sts: + # Using a ST where at least the UPN matched ! + ssps.append(sts[0]) + else: + # Nothing found + t.show() + raise ValueError( + f"Could not find a ticket for {upn}, either a " + f"TGT or towards {hostname}" + ) + elif ST is None and TGT is None: + # In this case, KEY is supposed to be the user's key. + from scapy.libs.rfc3961 import Key, EncryptionType + + if KEY is None and HashAes256Sha96: + KEY = Key( + EncryptionType.AES256_CTS_HMAC_SHA1_96, + HashAes256Sha96, + ) + elif KEY is None and HashAes128Sha96: + KEY = Key( + EncryptionType.AES128_CTS_HMAC_SHA1_96, + HashAes128Sha96, + ) + elif KEY is None and HashNt: + KEY = Key( + EncryptionType.RC4_HMAC, + HashNt, + ) + # Make a SSP that only has a UPN and secret. + ssps.append( + KerberosSSP( + UPN=UPN, + PASSWORD=password, + KEY=KEY, + debug=debug, + ) + ) + else: + # We have a ST, use it with the key. + ssps.append( + KerberosSSP( + UPN=UPN, + ST=ST, + TGT=TGT, + KEY=KEY, + debug=debug, + ) + ) + elif kerberos_required: + raise ValueError( + "Kerberos required but domain not specified in the UPN, " + "or target isn't a hostname !" + ) + + # NTLM + if not kerberos_required: + if HashNt is None and password is not None: + HashNt = MD4le(password) + if HashNt is not None: + ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) + + if not ssps: + raise ValueError("Unexpected case ! Please report.") + + # Build the SSP + return cls(ssps) + + def NegTokenInit2(self): + """ + Server-Initiation of GSSAPI/SPNEGO. + See [MS-SPNG] sect 3.2.5.2 + """ + Context = SPNEGOSSP.CONTEXT(list(self.ssps)) + return ( + Context, + GSSAPI_BLOB( + innerToken=SPNEGO_negToken( + token=SPNEGO_negTokenInit( + mechTypes=Context.get_supported_mechtypes(), + negHints=SPNEGO_negHints( + hintName=ASN1_GENERAL_STRING( + "not_defined_in_RFC4178@please_ignore" + ), + ), + ) + ) + ), + ) + + # NOTE: NegoEX has an effect on how the SecurityContext is + # initialized, as detailed in [MS-AUTHSOD] sect 3.3.2 + # But the format that the Exchange token uses appears not to + # be documented :/ + + # resp.SecurityBlob.innerToken.token.mechTypes.insert( + # 0, + # # NEGOEX + # SPNEGO_MechType(oid="1.3.6.1.4.1.311.2.2.30"), + # ) + # resp.SecurityBlob.innerToken.token.mechToken = SPNEGO_Token( + # value=negoex_token + # ) # noqa: E501 + + def GSS_WrapEx(self, Context, *args, **kwargs): + # Passthrough + return Context.ssp.GSS_WrapEx(Context.ssp_context, *args, **kwargs) + + def GSS_UnwrapEx(self, Context, *args, **kwargs): + # Passthrough + return Context.ssp.GSS_UnwrapEx(Context.ssp_context, *args, **kwargs) + + def GSS_GetMICEx(self, Context, *args, **kwargs): + # Passthrough + return Context.ssp.GSS_GetMICEx(Context.ssp_context, *args, **kwargs) + + def GSS_VerifyMICEx(self, Context, *args, **kwargs): + # Passthrough + return Context.ssp.GSS_VerifyMICEx(Context.ssp_context, *args, **kwargs) + + def LegsAmount(self, Context: CONTEXT): + return 4 + + def MapStatusToNegState(self, status: int) -> int: + """ + Map a GSSAPI return code to SPNEGO negState codes + """ + if status == GSS_S_COMPLETE: + return 0 # accept_completed + elif status == GSS_S_CONTINUE_NEEDED: + return 1 # accept_incomplete + else: + return 2 # reject + + def GuessOtherMechtypes(self, Context: CONTEXT, input_token): + """ + Guesses the mechtype of the peer when the "raw" fallback is used. + """ + if isinstance(input_token, NTLM_Header): + Context.other_mechtypes = [ + SPNEGO_MechType(oid=ASN1_OID("1.3.6.1.4.1.311.2.2.10")) + ] + elif isinstance(input_token, Kerberos): + Context.other_mechtypes = [ + SPNEGO_MechType(oid=ASN1_OID("1.2.840.48018.1.2.2")) + ] + else: + Context.other_mechtypes = [] + + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + # New Context + Context = SPNEGOSSP.CONTEXT( + list(self.ssps), + req_flags=req_flags, + ) + + input_token_inner = None + negState = None + + # Extract values from GSSAPI token, if present + if input_token is not None: + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + # We are handling a NegTokenInit2 request ! + # Populate context with values from the server's request + Context.other_mechtypes = input_token.mechTypes + elif isinstance(input_token, SPNEGO_negTokenResp): + # Extract token and state from the client request + if input_token.responseToken is not None: + input_token_inner = input_token.responseToken.value + if input_token.negState is not None: + negState = input_token.negState + else: + # The blob is a raw token. We aren't using SPNEGO here. + Context.raw = True + input_token_inner = input_token + self.GuessOtherMechtypes(Context, input_token) + + # Perform SSP negotiation + if Context.ssp is None: + try: + Context.negotiate_ssp() + except ValueError as ex: + # Couldn't find common SSP + log_runtime.warning("SPNEGOSSP: %s" % ex) + return Context, None, GSS_S_BAD_MECH + + # Call inner-SSP + Context.ssp_context, output_token_inner, status = ( + Context.ssp.GSS_Init_sec_context( + Context.ssp_context, + input_token=input_token_inner, + target_name=target_name, + req_flags=Context.req_flags, + chan_bindings=chan_bindings, + ) + ) + + if negState == 2 or status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + # SSP failed. Remove it from the list of SSPs we're currently running + Context.ssps.remove(Context.ssp) + log_runtime.warning( + "SPNEGOSSP: %s failed. Retrying with next in queue." % repr(Context.ssp) + ) + + if Context.ssps: + # We have other SSPs remaining. Retry using another one. + Context.ssp = None + return self.GSS_Init_sec_context( + Context, + None, # No input for retry. + target_name=target_name, + req_flags=req_flags, + chan_bindings=chan_bindings, + ) + else: + # We don't have anything left + return Context, None, status + + # Raw processing ends here. + if Context.raw: + return Context, output_token_inner, status + + # Verify MIC if present. + if status == GSS_S_COMPLETE and input_token and input_token.mechListMIC: + # NOTE: the mechListMIC that the server sends is computed over the list of + # mechanisms that the **client requested**. + Context.ssp.VerifyMechListMIC( + Context.ssp_context, + input_token.mechListMIC.value, + mechListMIC(Context.sent_mechtypes), + ) + Context.verified_mic = True + + if negState == 0 and status == GSS_S_COMPLETE: + # We are done. + return Context, None, status + elif Context.state == SPNEGOSSP.STATE.FIRST: + # First freeze the list of available mechtypes on the first message + Context.sent_mechtypes = Context.get_supported_mechtypes() + + # Now build the token + spnego_tok = GSSAPI_BLOB( + innerToken=SPNEGO_negToken( + token=SPNEGO_negTokenInit(mechTypes=Context.sent_mechtypes) + ) + ) + + # Add the output token if provided + if output_token_inner is not None: + spnego_tok.innerToken.token.mechToken = SPNEGO_Token( + value=output_token_inner, + ) + elif Context.state == SPNEGOSSP.STATE.SUBSEQUENT: + # Build subsequent client tokens: without the list of supported mechtypes + # NOTE: GSSAPI_BLOB is stripped. + spnego_tok = SPNEGO_negToken( + token=SPNEGO_negTokenResp( + supportedMech=None, + negState=None, + ) + ) + + # Add the MIC if required and the exchange is finished. + if status == GSS_S_COMPLETE and Context.require_mic: + spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( + value=Context.ssp.GetMechListMIC( + Context.ssp_context, + mechListMIC(Context.sent_mechtypes), + ), + ) + + # If we still haven't verified the MIC, we aren't done. + if not Context.verified_mic: + status = GSS_S_CONTINUE_NEEDED + + # Add the output token if provided + if output_token_inner: + spnego_tok.token.responseToken = SPNEGO_Token( + value=output_token_inner, + ) + + # Update the state + Context.state = SPNEGOSSP.STATE.SUBSEQUENT + + return Context, spnego_tok, status + + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + if Context is None: + # New Context + Context = SPNEGOSSP.CONTEXT( + list(self.ssps), + req_flags=req_flags, + ) + + input_token_inner = None + _mechListMIC = None + + # Extract values from GSSAPI token + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + # Populate context with values from the client's request + if input_token.mechTypes: + Context.other_mechtypes = input_token.mechTypes + if input_token.mechToken: + input_token_inner = input_token.mechToken.value + _mechListMIC = input_token.mechListMIC or input_token._mechListMIC + elif isinstance(input_token, SPNEGO_negTokenResp): + if input_token.responseToken: + input_token_inner = input_token.responseToken.value + _mechListMIC = input_token.mechListMIC + else: + # The blob is a raw token. We aren't using SPNEGO here. + Context.raw = True + input_token_inner = input_token + self.GuessOtherMechtypes(Context, input_token) + + if Context.other_mechtypes is None: + # At this point, we should have already gotten the mechtypes from a current + # or former request. + return Context, None, GSS_S_FAILURE + + # Perform SSP negotiation + if Context.ssp is None: + try: + Context.negotiate_ssp() + except ValueError as ex: + # Couldn't find common SSP + log_runtime.warning("SPNEGOSSP: %s" % ex) + return Context, None, GSS_S_FAILURE + + output_token_inner = None + status = GSS_S_CONTINUE_NEEDED + + # If we didn't pick the client's first choice, the token we were passed + # isn't usable. + if not Context.first_choice: + # Typically a client opportunistically starts with Kerberos, including + # its APREQ, and we want to use NTLM. Here we add one round trip + Context.first_choice = True # Do not enter here again. + else: + # Send it to the negotiated SSP + Context.ssp_context, output_token_inner, status = ( + Context.ssp.GSS_Accept_sec_context( + Context.ssp_context, + input_token=input_token_inner, + req_flags=Context.req_flags, + chan_bindings=chan_bindings, + ) + ) + + # Verify MIC if context succeeded + if status == GSS_S_COMPLETE and _mechListMIC: + # NOTE: the mechListMIC that the client sends is computed over the + # **list of mechanisms that it requests**. + if Context.ssp.SupportsMechListMIC(): + # We need to check we support checking the MIC. The only case where + # this is needed is NTLM in guest mode: the client will send a mic + # but we don't check it... + Context.ssp.VerifyMechListMIC( + Context.ssp_context, + _mechListMIC.value, + mechListMIC(Context.other_mechtypes), + ) + Context.verified_mic = True + Context.require_mic = True + + # Raw processing ends here. + if Context.raw: + return Context, output_token_inner, status + + # 0. Build the template response token + spnego_tok = SPNEGO_negToken( + token=SPNEGO_negTokenResp( + supportedMech=None, + ) + ) + if Context.state == SPNEGOSSP.STATE.FIRST: + # Include the supportedMech list if this is the first message we send + # or a renegotiation. + spnego_tok.token.supportedMech = Context.ssp_mechtype + + # Add the output token if provided + if output_token_inner: + spnego_tok.token.responseToken = SPNEGO_Token(value=output_token_inner) + + # Update the state + Context.state = SPNEGOSSP.STATE.SUBSEQUENT + + # Add the MIC if required and the exchange is finished. + if status == GSS_S_COMPLETE and Context.require_mic: + spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( + value=Context.ssp.GetMechListMIC( + Context.ssp_context, + mechListMIC(Context.other_mechtypes), + ), + ) + + # If we still haven't verified the MIC, we aren't done. + if not Context.verified_mic: + status = GSS_S_CONTINUE_NEEDED + + # Set negState + spnego_tok.token.negState = self.MapStatusToNegState(status) + + return Context, spnego_tok, status + + def GSS_Passive( + self, + Context: CONTEXT, + input_token=None, + req_flags=None, + ): + if Context is None: + # New Context + Context = SPNEGOSSP.CONTEXT(list(self.ssps)) + Context.passive = True + + input_token_inner = None + + # Extract values from GSSAPI token + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + if input_token.mechTypes is not None: + Context.other_mechtypes = input_token.mechTypes + if input_token.mechToken: + input_token_inner = input_token.mechToken.value + elif isinstance(input_token, SPNEGO_negTokenResp): + if input_token.supportedMech is not None: + Context.other_mechtypes = [input_token.supportedMech] + if input_token.responseToken: + input_token_inner = input_token.responseToken.value + else: + # Raw. + input_token_inner = input_token + + if Context.other_mechtypes is None: + self.GuessOtherMechtypes(Context, input_token) + + # Uninitialized OR allowed mechtypes have changed + if Context.ssp is None or Context.ssp_mechtype not in Context.other_mechtypes: + try: + Context.negotiate_ssp() + except ValueError: + # Couldn't find common SSP + return Context, GSS_S_FAILURE + + # Passthrough + Context.ssp_context, status = Context.ssp.GSS_Passive( + Context.ssp_context, + input_token_inner, + req_flags=req_flags, + ) + + return Context, status + + def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): + Context.ssp.GSS_Passive_set_Direction( + Context.ssp_context, IsAcceptor=IsAcceptor + ) + + def MaximumSignatureLength(self, Context: CONTEXT): + return Context.ssp.MaximumSignatureLength(Context.ssp_context) diff --git a/scapy/layers/ssh.py b/scapy/layers/ssh.py new file mode 100644 index 00000000000..a7fbbd52714 --- /dev/null +++ b/scapy/layers/ssh.py @@ -0,0 +1,496 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Secure Shell (SSH) Transport Layer Protocol + +RFC 4250, 4251, 4252, 4253 and 4254 +""" + +from scapy.config import conf +from scapy.compat import plain_str +from scapy.fields import ( + BitLenField, + ByteField, + ByteEnumField, + IntEnumField, + IntField, + PacketField, + PacketListField, + PacketLenField, + FieldLenField, + FieldListField, + StrLenField, + StrFixedLenField, + StrNullField, + YesNoByteField, +) +from scapy.packet import Packet, bind_bottom_up, bind_layers + +from scapy.layers.inet import TCP + + +class StrCRLFField(StrNullField): + DELIMITER = b"\r\n" + + +class _SSHHeaderField(FieldListField): + def getfield(self, pkt, s): + val = [] + while s: + s, v = self.field.getfield(pkt, s) + val.append(v) + if v[:4] == b"SSH-": + return s, val + return s, val + + +# RFC 4251 - SSH Architecture +# This RFC defines some types + +# RFC 4251 - sect 5 + + +class _ComaStrField(StrLenField): + islist = 1 + + def m2i(self, pkt, x): + return super(_ComaStrField, self).m2i(pkt, x).split(b",") + + def i2m(self, pkt, x): + return super(_ComaStrField, self).i2m(pkt, b",".join(x)) + + +class SSHString(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="value", fmt="!I"), + StrLenField("value", 0, length_from=lambda pkt: pkt.length), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHPacketStringField(PacketField): + __slots__ = ["sub_cls"] + + def __init__(self, name, sub_cls): + self.sub_cls = sub_cls + super(SSHPacketStringField, self).__init__(name, SSHString(), SSHString) + + def m2i(self, pkt, x): + x = super(SSHPacketStringField, self).m2i(pkt, x) + x.value = self.sub_cls(x.value) + return x + + +class NameList(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="names", fmt="!I"), + _ComaStrField("names", [], length_from=lambda pkt: pkt.length), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class Mpint(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="value", fmt="!I"), + BitLenField("value", 0, length_from=lambda pkt: pkt.length * 8), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# RFC4250 - sect 4.1.2 + +_SSH_message_numbers = { + # RFC4253 - SSH-TRANS + 1: "SSH_MSG_DISCONNECT", + 2: "SSH_MSG_IGNORE", + 3: "SSH_MSG_UNIMPLEMENTED", + 4: "SSH_MSG_DEBUG", + 5: "SSH_MSG_SERVICE_REQUEST", + 6: "SSH_MSG_SERVICE_ACCEPT", + 7: "SSH_MSG_EXT_INFO", # RFC 8308 + 8: "SSH_MSG_NEWCOMPRESS", + 20: "SSH_MSG_KEXINIT", + 21: "SSH_MSG_NEWKEYS", + # Errata 152 of RFC4253 + 30: "SSH_MSG_KEXDH_INIT", + 31: "SSH_MSG_KEXDH_REPLY", + # RFC4252 - SSH-USERAUTH + 50: "SSH_MSG_USERAUTH_REQUEST", + 51: "SSH_MSG_USERAUTH_FAILURE", + 52: "SSH_MSG_USERAUTH_SUCCESS", + 53: "SSH_MSG_USERAUTH_BANNER", + # RFC4254 - SSH-CONNECT + 80: "SSH_MSG_GLOBAL_REQUEST", + 81: "SSH_MSG_REQUEST_SUCCESS", + 82: "SSH_MSG_REQUEST_FAILURE", + 90: "SSH_MSG_CHANNEL_OPEN", + 91: "SSH_MSG_CHANNEL_OPEN_CONFIRMATION", + 92: "SSH_MSG_CHANNEL_OPEN_FAILURE", + 93: "SSH_MSG_CHANNEL_WINDOW_ADJUST", + 94: "SSH_MSG_CHANNEL_DATA", + 95: "SSH_MSG_CHANNEL_EXTENDED_DATA", + 96: "SSH_MSG_CHANNEL_EOF", + 97: "SSH_MSG_CHANNEL_CLOSE", + 98: "SSH_MSG_CHANNEL_REQUEST", + 99: "SSH_MSG_CHANNEL_SUCCESS", + 100: "SSH_MSG_CHANNEL_FAILURE", +} + +# RFC4253 - sect 6 + +_SSH_messages = {} + + +def _SSHPayload(x, **kwargs): + return _SSH_messages.get(x and x[0], conf.raw_layer)(x) + + +class SSH(Packet): + name = "SSH - Binary Packet" + fields_desc = [ + IntField("packet_length", None), + ByteField("padding_length", None), + PacketLenField( + "pay", + None, + _SSHPayload, + length_from=lambda pkt: pkt.packet_length - pkt.padding_length - 1, + ), + StrLenField("random_padding", b"", length_from=lambda pkt: pkt.padding_length), + # StrField("mac", b""), + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 4 and _pkt[:4] == b"SSH-": + return SSHVersionExchange + return cls + + def mysummary(self): + if self.pay: + if isinstance(self.pay, conf.raw_layer): + return "SSH type " + str(self.pay.load[0]), [TCP, SSH] + return "SSH " + self.pay.sprintf("%type%"), [TCP, SSH] + return "SSH", [TCP, SSH] + + +# RFC4253 - sect 4.2 + + +class SSHVersionExchange(Packet): + name = "SSH - Protocol Version Exchange" + fields_desc = [ + _SSHHeaderField( + "lines", + [], + StrCRLFField("", b""), + ) + ] + + def mysummary(self): + return "SSH - Version Exchange %s" % plain_str(self.lines[-1]), [TCP] + + +# RFC4253 - sect 6.6 + +_SSH_certificates = {} +_SSH_publickeys = {} +_SSH_signatures = {} + + +class _SSHCertificate(PacketField): + def m2i(self, pkt, x): + return _SSH_certificates.get(pkt.format_identifier.value, self.cls)(x) + + +class _SSHPublicKey(PacketField): + def m2i(self, pkt, x): + return _SSH_publickeys.get(pkt.format_identifier.value, self.cls)(x) + + +class _SSHSignature(PacketField): + def m2i(self, pkt, x): + return _SSH_signatures.get(pkt.format_identifier.value, self.cls)(x) + + +class SSHCertificate(Packet): + fields_desc = [ + PacketField("format_identifier", SSHString(), SSHString), + _SSHCertificate("data", None, conf.raw_layer), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHPublicKey(Packet): + fields_desc = [ + PacketField("format_identifier", SSHString(), SSHString), + _SSHPublicKey("data", None, conf.raw_layer), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHSignature(Packet): + fields_desc = [ + PacketField("format_identifier", SSHString(), SSHString), + _SSHSignature("data", None, conf.raw_layer), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# RFC4253 - sect 7.1 + + +class SSHKexInit(Packet): + fields_desc = [ + ByteEnumField("type", 20, _SSH_message_numbers), + StrFixedLenField("cookie", b"", length=16), + PacketField("kex_algorithms", NameList(), NameList), + PacketField("server_host_key_algorithms", NameList(), NameList), + PacketField("encryption_algorithms_client_to_server", NameList(), NameList), + PacketField("encryption_algorithms_server_to_client", NameList(), NameList), + PacketField("mac_algorithms_client_to_server", NameList(), NameList), + PacketField("mac_algorithms_server_to_client", NameList(), NameList), + PacketField("compression_algorithms_client_to_server", NameList(), NameList), + PacketField("compression_algorithms_server_to_client", NameList(), NameList), + PacketField("languages_client_to_server", NameList(), NameList), + PacketField("languages_server_to_client", NameList(), NameList), + YesNoByteField("first_kex_packet_follows", 0), + IntField("reserved", 0), + ] + + +_SSH_messages[20] = SSHKexInit + +# RFC4253 - sect 7.3 + + +class SSHNewKeys(Packet): + fields_desc = [ + ByteEnumField("type", 21, _SSH_message_numbers), + ] + + +_SSH_messages[21] = SSHNewKeys + + +# RFC4253 - sect 8 + + +class SSHKexDHInit(Packet): + fields_desc = [ + ByteEnumField("type", 30, _SSH_message_numbers), + PacketField("e", Mpint(), Mpint), + ] + + +_SSH_messages[30] = SSHKexDHInit + + +class SSHKexDHReply(Packet): + fields_desc = [ + ByteEnumField("type", 31, _SSH_message_numbers), + SSHPacketStringField("K_S", SSHPublicKey), + PacketField("f", Mpint(), Mpint), + SSHPacketStringField("H_hash", SSHSignature), + ] + + +_SSH_messages[31] = SSHKexDHReply + +# RFC4253 - sect 10 + + +class SSHServiceRequest(Packet): + fields_desc = [ + ByteEnumField("type", 5, _SSH_message_numbers), + PacketField("service_name", SSHString(), SSHString), + ] + + +_SSH_messages[5] = SSHServiceRequest + + +class SSHServiceAccept(Packet): + fields_desc = [ + ByteEnumField("type", 6, _SSH_message_numbers), + PacketField("service_name", SSHString(), SSHString), + ] + + +_SSH_messages[6] = SSHServiceAccept + +# RFC4253 - sect 11.1 + + +class SSHDisconnect(Packet): + fields_desc = [ + ByteEnumField("type", 1, _SSH_message_numbers), + IntEnumField( + "reason_code", + 0, + { + 1: "SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT", + 2: "SSH_DISCONNECT_PROTOCOL_ERROR", + 3: "SSH_DISCONNECT_KEY_EXCHANGE_FAILED", + 4: "SSH_DISCONNECT_RESERVED", + 5: "SSH_DISCONNECT_MAC_ERROR", + 6: "SSH_DISCONNECT_COMPRESSION_ERROR", + 7: "SSH_DISCONNECT_SERVICE_NOT_AVAILABLE", + 8: "SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED", + 9: "SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE", + 10: "SSH_DISCONNECT_CONNECTION_LOST", + 11: "SSH_DISCONNECT_BY_APPLICATION", + 12: "SSH_DISCONNECT_TOO_MANY_CONNECTIONS", + 13: "SSH_DISCONNECT_AUTH_CANCELLED_BY_USER", + 14: "SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE", + 15: "SSH_DISCONNECT_ILLEGAL_USER_NAME", + }, + ), + PacketField("description", SSHString(), SSHString), + PacketField("language_tag", SSHString(), SSHString), + ] + + +_SSH_messages[1] = SSHDisconnect + +# RFC4253 - sect 11.2 + + +class SSHIgnore(Packet): + fields_desc = [ + ByteEnumField("type", 2, _SSH_message_numbers), + PacketField("data", SSHString(), SSHString), + ] + + +_SSH_messages[2] = SSHIgnore + +# RFC4253 - sect 11.3 + + +class SSHServiceDebug(Packet): + fields_desc = [ + ByteEnumField("type", 4, _SSH_message_numbers), + YesNoByteField("always_display", 0), + PacketField("message", SSHString(), SSHString), + PacketField("language_tag", SSHString(), SSHString), + ] + + +_SSH_messages[4] = SSHServiceDebug + +# RFC4253 - sect 11.4 + + +class SSHUnimplemented(Packet): + fields_desc = [ + ByteEnumField("type", 3, _SSH_message_numbers), + IntField("seq_num", 0), + ] + + +_SSH_messages[3] = SSHUnimplemented + +# RFC8308 - sect 2.3 + + +class SSHExtension(Packet): + fields_desc = [ + PacketField("extension_name", SSHString(), SSHString), + PacketField("extension_value", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class SSHExtInfo(Packet): + fields_desc = [ + ByteEnumField("type", 7, _SSH_message_numbers), + FieldLenField("nr_extensions", None, length_of="extensions"), + PacketListField("extensions", [], SSHExtension), + ] + + +_SSH_messages[7] = SSHExtInfo + +# RFC8308 - sect 3.2 + + +class SSHNewCompress(Packet): + fields_desc = [ + ByteEnumField("type", 3, _SSH_message_numbers), + ] + + +_SSH_messages[8] = SSHNewCompress + +# RFC8709 + + +class SSHPublicKeyEd25519(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_publickeys[b"ssh-ed25519"] = SSHPublicKeyEd25519 + + +class SSHPublicKeyEd448(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_publickeys[b"ssh-ed448"] = SSHPublicKeyEd448 + + +class SSHSignatureEd25519(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_signatures[b"ssh-ed25519"] = SSHSignatureEd25519 + + +class SSHSignatureEd448(Packet): + fields_desc = [ + PacketField("key", SSHString(), SSHString), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +_SSH_signatures[b"ssh-ed448"] = SSHSignatureEd448 + +bind_layers(SSH, SSH) + +bind_bottom_up(TCP, SSH, sport=22) +bind_layers(TCP, SSH, dport=22) diff --git a/scapy/layers/tftp.py b/scapy/layers/tftp.py index 97d841b4b31..fcd925c3f57 100644 --- a/scapy/layers/tftp.py +++ b/scapy/layers/tftp.py @@ -1,25 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ TFTP (Trivial File Transfer Protocol). + +This provides TFTP implementation and 4 small automata: + - TFTP_read: read a remote file + - TFTP_RRQ_server: server that answers to read requests + - TFTP_write: write a remote file + - TFTP_WRQ_server: server than accepts write requests """ -from __future__ import absolute_import import os import random from scapy.packet import Packet, bind_layers, split_bottom_up, bind_bottom_up -from scapy.fields import PacketListField, ShortEnumField, ShortField, \ - StrNullField +from scapy.fields import ( + PacketListField, + ShortEnumField, + ShortField, + StrNullField, +) from scapy.automaton import ATMT, Automaton -from scapy.layers.inet import UDP, IP -from scapy.modules.six.moves import range +from scapy.base_classes import Net from scapy.config import conf +from scapy.sessions import IPSession from scapy.volatile import RandShort +from scapy.layers.inet import UDP, IP TFTP_operations = {1: "RRQ", 2: "WRQ", 3: "DATA", 4: "ACK", 5: "ERROR", 6: "OACK"} # noqa: E501 @@ -83,7 +93,7 @@ class TFTP_ACK(Packet): def answers(self, other): if isinstance(other, TFTP_DATA): return self.block == other.block - elif isinstance(other, TFTP_RRQ) or isinstance(other, TFTP_WRQ) or isinstance(other, TFTP_OACK): # noqa: E501 + elif isinstance(other, (TFTP_RRQ, TFTP_WRQ, TFTP_OACK)): # noqa: E501 return self.block == 0 return 0 @@ -109,10 +119,7 @@ class TFTP_ERROR(Packet): StrNullField("errormsg", "")] def answers(self, other): - return (isinstance(other, TFTP_DATA) or - isinstance(other, TFTP_RRQ) or - isinstance(other, TFTP_WRQ) or - isinstance(other, TFTP_ACK)) + return isinstance(other, (TFTP_DATA, TFTP_RRQ, TFTP_WRQ, TFTP_ACK)) def mysummary(self): return self.sprintf("ERROR %errorcode%: %errormsg%"), [UDP] @@ -123,7 +130,7 @@ class TFTP_OACK(Packet): fields_desc = [] def answers(self, other): - return isinstance(other, TFTP_WRQ) or isinstance(other, TFTP_RRQ) + return isinstance(other, (TFTP_WRQ, TFTP_RRQ)) bind_layers(UDP, TFTP, dport=69) @@ -138,8 +145,22 @@ def answers(self, other): bind_layers(TFTP_OACK, TFTP_Options) +# Automatons + class TFTP_read(Automaton): + """ + TFTP automaton to read a remote file on a TFTP server. + + :param filename: the name of the remote file to read. + :param server: the host on which to read (IP or name). + :param sport: (optional) the source port to use. (default: random) + :param port: (optional) the TFTP port (default: 69) + """ + def parse_args(self, filename, server, sport=None, port=69, **kargs): + if "iface" not in kargs: + server = str(Net(server)) + kargs["iface"] = conf.route.route(server)[0] Automaton.parse_args(self, **kargs) self.filename = filename self.server = server @@ -226,7 +247,20 @@ def END(self): class TFTP_write(Automaton): + """ + TFTP automaton to write a local file onto a TFTP server. + + :param filename: the name of the remote file to write. + :param data: the bytes data to write. + :param server: the host on which to read (IP or name). + :param sport: (optional) the source port to use. (default: random) + :param port: (optional) the TFTP port (default: 69) + """ + def parse_args(self, filename, data, server, sport=None, port=69, **kargs): + if "iface" not in kargs: + server = str(Net(server)) + kargs["iface"] = conf.route.route(server)[0] Automaton.parse_args(self, **kargs) self.filename = filename self.server = server @@ -306,8 +340,18 @@ def END(self): class TFTP_WRQ_server(Automaton): + """ + TFTP automaton to wait for incoming files + + :param ip: (optional) the local IP to listen on. + :param sport: (optional) the local port (by default: random) + """ def parse_args(self, ip=None, sport=None, *args, **kargs): + if "iface" not in kargs and ip: + ip = str(Net(ip)) + kargs["iface"] = conf.route.route(ip)[0] + kargs.setdefault("session", IPSession()) Automaton.parse_args(self, *args, **kargs) self.ip = ip self.sport = sport @@ -340,7 +384,7 @@ def ack_WRQ(self, pkt): self.last_packet = self.l3 / TFTP_ACK(block=0) self.send(self.last_packet) else: - opt = [x for x in options.options if x.oname.upper() == "BLKSIZE"] + opt = [x for x in options.options if x.oname.upper() == b"BLKSIZE"] if opt: self.blksize = int(opt[0].value) self.debug(2, "Negotiated new blksize at %i" % self.blksize) @@ -365,7 +409,7 @@ def receive_data(self, pkt): @ATMT.action(receive_data) def ack_data(self): - self.last_packet = self.l3 / TFTP_ACK(block=self.blk) + self.last_packet = self.l3 / TFTP_ACK(block=self.blk % 65536) self.send(self.last_packet) @ATMT.state() @@ -383,7 +427,25 @@ def END(self): class TFTP_RRQ_server(Automaton): + """ + TFTP automaton to serve local files + + You can't use 'store' and 'dir' at the same time. + + :param store: (optional) a dictionary that contains the file data, like + {"thefile": b"data"}. + :param dir: (optional) a folder that contains the data file data. + :param joker: (optional) data to return when no file/data is found. + :param ip: (optional) the local IP to listen on. + :param sport: (optional) the local port (by default: random) + :param serve_one: (optional) close after serving one client (default: False) + """ + def parse_args(self, store=None, joker=None, dir=None, ip=None, sport=None, serve_one=False, **kargs): # noqa: E501 + if "iface" not in kargs and ip: + ip = str(Net(ip)) + kargs["iface"] = conf.route.route(ip)[0] + kargs.setdefault("session", IPSession()) Automaton.parse_args(self, **kargs) if store is None: store = {} @@ -415,7 +477,7 @@ def receive_rrq(self, pkt): @ATMT.state() def RECEIVED_RRQ(self, pkt): ip = pkt[IP] - options = pkt[TFTP_Options] + options = pkt.getlayer(TFTP_Options) self.l3 = IP(src=ip.dst, dst=ip.src) / UDP(sport=self.my_tid, dport=ip.sport) / TFTP() # noqa: E501 self.filename = pkt[TFTP_RRQ].filename.decode("utf-8", "ignore") self.blk = 1 @@ -434,7 +496,7 @@ def RECEIVED_RRQ(self, pkt): self.data = self.joker if options: - opt = [x for x in options.options if x.oname.upper() == "BLKSIZE"] + opt = [x for x in options.options if x.oname.upper() == b"BLKSIZE"] if opt: self.blksize = int(opt[0].value) self.debug(2, "Negotiated new blksize at %i" % self.blksize) @@ -454,11 +516,17 @@ def file_not_found(self): @ATMT.action(file_not_found) def send_error(self): - self.send(self.l3 / TFTP_ERROR(errorcode=1, errormsg=TFTP_Error_Codes[1])) # noqa: E501 + self.send(self.l3 / TFTP_ERROR( + errorcode=1, + errormsg=TFTP_Error_Codes[1], + )) @ATMT.state() def SEND_FILE(self): - self.send(self.l3 / TFTP_DATA(block=self.blk) / self.data[(self.blk - 1) * self.blksize:self.blk * self.blksize]) # noqa: E501 + self.send( + self.l3 / TFTP_DATA(block=self.blk % 65536) / + self.data[(self.blk - 1) * self.blksize:self.blk * self.blksize] + ) @ATMT.timeout(SEND_FILE, 3) def timeout_waiting_ack(self): diff --git a/scapy/layers/tls/__init__.py b/scapy/layers/tls/__init__.py index 60b48ef4664..80e213fbbef 100644 --- a/scapy/layers/tls/__init__.py +++ b/scapy/layers/tls/__init__.py @@ -1,7 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ Tools for handling TLS sessions and digital certificates. @@ -19,7 +21,7 @@ - RSA & ECDSA keys sign/verify methods. - TLS records and sublayers (handshake...) parsing/building. Works with - versions SSLv2 to TLS 1.2. This may be enhanced by a TLS context. For + versions SSLv2 to TLS 1.3. This may be enhanced by a TLS context. For instance, if Scapy reads a ServerHello with version TLS 1.2 and a cipher suite using AES, it will assume the presence of IVs prepending the data. See test/tls.uts for real examples. @@ -44,7 +46,7 @@ - Reading a TLS handshake between a Firefox client and a GitHub server. - - Reading TLS 1.3 handshakes from test vectors of a draft RFC. + - Reading TLS 1.3 handshakes from test vectors of the 8448 RFC. - Reading a SSLv2 handshake between s_client and s_server, without PFS. @@ -52,19 +54,20 @@ - Test our TLS client against our TLS server (s_server is unscriptable). + - Test our TLS client against python's SSL Socket wrapper (for TLS 1.3) + TODO list (may it be carved away by good souls): - Features to add (or wait for) in the cryptography library: - - X448 from RFC 7748 (no support in openssl yet); - - the compressed EC point format. - - About the automatons: - - Add resumption support, through session IDs or session tickets. + - Allow upgrade from TLS 1.2 to TLS 1.3 in the Automaton client. + Currently we'll use TLS 1.3 only if the automaton client was given + version="tls13". - Add various checks for discrepancies between client and server. Is the ServerHello ciphersuite ok? What about the SKE params? Etc. @@ -75,18 +78,11 @@ - Allow the server to store both one RSA key and one ECDSA key, and select the right one to use according to the ClientHello suites. - - Find a way to shutdown the automatons sockets properly without - simultaneously breaking the unit tests. - - Miscellaneous: - - Enhance PSK and session ticket support. - - Define several Certificate Transparency objects. - - Add the extended master secret and encrypt-then-mac logic. - - Mostly unused features : DSS, fixed DH, SRP, char2 curves... """ @@ -95,5 +91,5 @@ if not conf.crypto_valid: import logging log_loading = logging.getLogger("scapy.loading") - log_loading.info("Can't import python-cryptography v1.7+. " + log_loading.info("Can't import python-cryptography v2.0+. " "Disabled PKI & TLS crypto-related features.") diff --git a/scapy/layers/tls/all.py b/scapy/layers/tls/all.py index f226104154e..e757da815ef 100644 --- a/scapy/layers/tls/all.py +++ b/scapy/layers/tls/all.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ Aggregate top level objects from all TLS modules. diff --git a/scapy/layers/tls/automaton.py b/scapy/layers/tls/automaton.py index 3b951fe42ee..d1dbe149d66 100644 --- a/scapy/layers/tls/automaton.py +++ b/scapy/layers/tls/automaton.py @@ -1,12 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ The _TLSAutomaton class provides methods common to both TLS client and server. """ +import select +import socket import struct from scapy.automaton import Automaton @@ -61,8 +64,15 @@ class _TLSAutomaton(Automaton): which has not yet been interpreted as a TLS record is kept in 'remain_in'. """ + def __init__(self, *args, **kwargs): + kwargs["ll"] = lambda *args, **kwargs: None + kwargs["recvsock"] = lambda *args, **kwargs: None + super(_TLSAutomaton, self).__init__(*args, **kwargs) + def parse_args(self, mycert=None, mykey=None, **kargs): + self.verbose = kargs.pop("verbose", True) + super(_TLSAutomaton, self).parse_args(**kargs) self.socket = None @@ -83,8 +93,6 @@ def parse_args(self, mycert=None, mykey=None, **kargs): else: self.mykey = None - self.verbose = kargs.get("verbose", True) - def get_next_msg(self, socket_timeout=2, retry=2): """ The purpose of the function is to make next message(s) available in @@ -105,7 +113,6 @@ def get_next_msg(self, socket_timeout=2, retry=2): # A message is already available. return - self.socket.settimeout(socket_timeout) is_sslv2_msg = False still_getting_len = True grablen = 2 @@ -133,15 +140,27 @@ def get_next_msg(self, socket_timeout=2, retry=2): if grablen == len(self.remain_in): break + final = False try: - tmp = self.socket.recv(grablen - len(self.remain_in)) + tmp, _, _ = select.select([self.socket], [], [], + socket_timeout) if not tmp: retry -= 1 else: - self.remain_in += tmp - except Exception: - self.vprint("Could not join host ! Retrying...") + data = tmp[0].recv(grablen - len(self.remain_in)) + if not data: + # Socket peer was closed + self.vprint("Peer socket closed !") + final = True + else: + self.remain_in += data + except Exception as ex: + if not isinstance(ex, socket.timeout): + self.vprint("Could not join host (%s) ! Retrying..." % ex) retry -= 1 + else: + if final: + raise self.SOCKET_CLOSED() if len(self.remain_in) < 2 or len(self.remain_in) != grablen: # Remote peer is not willing to respond @@ -180,6 +199,8 @@ def get_next_msg(self, socket_timeout=2, retry=2): self.buffer_in += p.msg else: self.buffer_in += p.inner.msg + else: + p = p.payload def raise_on_packet(self, pkt_cls, state, get_next_msg=True): """ @@ -190,21 +211,28 @@ def raise_on_packet(self, pkt_cls, state, get_next_msg=True): # Maybe we already parsed the expected packet, maybe not. if get_next_msg: self.get_next_msg() - from scapy.layers.tls.handshake import TLSClientHello if (not self.buffer_in or - (not isinstance(self.buffer_in[0], pkt_cls) and - not (isinstance(self.buffer_in[0], TLSClientHello) and - self.cur_session.advertised_tls_version == 0x0304))): + not isinstance(self.buffer_in[0], pkt_cls)): return self.cur_pkt = self.buffer_in[0] self.buffer_in = self.buffer_in[1:] raise state() - def add_record(self, is_sslv2=None, is_tls13=None): + def in_handshake(self, pkt_cls): + """ + Return True if the pkt_cls was present during the handshake. + This is used to detect whether Certificates were requested, etc. + """ + return any( + isinstance(m, pkt_cls) + for m in self.cur_session.handshake_messages_parsed + ) + + def add_record(self, is_sslv2=None, is_tls13=None, is_tls12=None): """ Add a new TLS or SSLv2 or TLS 1.3 record to the packets buffered out. """ - if is_sslv2 is None and is_tls13 is None: + if is_sslv2 is None and is_tls13 is None and is_tls12 is None: v = (self.cur_session.tls_version or self.cur_session.advertised_tls_version) if v in [0x0200, 0x0002]: @@ -215,6 +243,11 @@ def add_record(self, is_sslv2=None, is_tls13=None): self.buffer_out.append(SSLv2(tls_session=self.cur_session)) elif is_tls13: self.buffer_out.append(TLS13(tls_session=self.cur_session)) + # For TLS 1.3 middlebox compatibility, TLS record version must + # be 0x0303 + elif is_tls12: + self.buffer_out.append(TLS(version="TLS 1.2", + tls_session=self.cur_session)) else: self.buffer_out.append(TLS(tls_session=self.cur_session)) diff --git a/scapy/layers/tls/automaton_cli.py b/scapy/layers/tls/automaton_cli.py index 1fe518bb343..5e442d3f198 100644 --- a/scapy/layers/tls/automaton_cli.py +++ b/scapy/layers/tls/automaton_cli.py @@ -1,54 +1,90 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ TLS client automaton. This makes for a primitive TLS stack. Obviously you need rights for network access. -We support versions SSLv2 to TLS 1.2, along with many features. -There is no session resumption mechanism for now. +We support versions SSLv2 to TLS 1.3, along with many features. -In order to run a client to tcp/50000 with one cipher suite of your choice: -> from scapy.all import * -> ch = TLSClientHello(ciphers=) -> t = TLSClientAutomaton(dport=50000, client_hello=ch) -> t.run() +In order to run a client to tcp/50000 with one cipher suite of your choice:: + + from scapy.layers.tls import * + ch = TLSClientHello(ciphers=) + t = TLSClientAutomaton(dport=50000, client_hello=ch) + t.run() + +You can also use it as a SuperSocket using the ``tlslink`` io:: + + from scapy.layers.tls import * + a = TLSClientAutomaton.tlslink(Raw, server="scapy.net", dport=443) + a.send(HTTP()/HTTPRequest()) + while True: + a.recv() + +You can also use the io with a TCPSession, e.g. to get an HTTPS answer:: + + from scapy.all import * + from scapy.layers.http import * + from scapy.layers.tls.automaton_cli import * + a = TLSClientAutomaton.tlslink(HTTP, server="www.google.com", dport=443) + pkt = a.sr1(HTTP()/HTTPRequest(), session=TCPSession(app=True), + timeout=2) """ -from __future__ import print_function import socket +import binascii +import struct +import time from scapy.config import conf -from scapy.pton_ntop import inet_pton from scapy.utils import randstring, repr_hex -from scapy.automaton import ATMT +from scapy.automaton import ATMT, select_objects +from scapy.error import warning from scapy.layers.tls.automaton import _TLSAutomaton from scapy.layers.tls.basefields import _tls_version, _tls_version_options from scapy.layers.tls.session import tlsSession -from scapy.layers.tls.extensions import TLS_Ext_SupportedGroups, \ - TLS_Ext_SupportedVersion_CH, TLS_Ext_SignatureAlgorithms, \ - TLS_Ext_SupportedVersion_SH +from scapy.layers.tls.extensions import ( + ServerName, + TLS_Ext_PSKKeyExchangeModes, + TLS_Ext_PostHandshakeAuth, + TLS_Ext_ServerName, + TLS_Ext_SignatureAlgorithms, + TLS_Ext_SupportedGroups, + TLS_Ext_SupportedVersion_CH, + TLS_Ext_SupportedVersion_SH, +) from scapy.layers.tls.handshake import TLSCertificate, TLSCertificateRequest, \ TLSCertificateVerify, TLSClientHello, TLSClientKeyExchange, \ TLSEncryptedExtensions, TLSFinished, TLSServerHello, TLSServerHelloDone, \ TLSServerKeyExchange, TLS13Certificate, TLS13ClientHello, \ - TLS13ServerHello, TLS13HelloRetryRequest + TLS13ServerHello, TLS13HelloRetryRequest, TLS13CertificateRequest, \ + _ASN1CertAndExt, TLS13KeyUpdate, TLS13NewSessionTicket from scapy.layers.tls.handshake_sslv2 import SSLv2ClientHello, \ SSLv2ServerHello, SSLv2ClientMasterKey, SSLv2ServerVerify, \ SSLv2ClientFinished, SSLv2ServerFinished, SSLv2ClientCertificate, \ SSLv2RequestCertificate from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_CH, \ - KeyShareEntry, TLS_Ext_KeyShare_HRR + KeyShareEntry, TLS_Ext_KeyShare_HRR, PSKIdentity, PSKBinderEntry, \ + TLS_Ext_PreSharedKey_CH from scapy.layers.tls.record import TLSAlert, TLSChangeCipherSpec, \ TLSApplicationData -from scapy.layers.tls.crypto.suites import _tls_cipher_suites +from scapy.layers.tls.crypto.suites import _tls_cipher_suites, \ + _tls_cipher_suites_cls from scapy.layers.tls.crypto.groups import _tls_named_groups -from scapy.modules import six +from scapy.layers.tls.crypto.hkdf import TLS13_HKDF from scapy.packet import Raw from scapy.compat import bytes_encode +# Typing imports +from typing import ( + Optional, +) + class TLSClientAutomaton(_TLSAutomaton): """ @@ -58,51 +94,55 @@ class TLSClientAutomaton(_TLSAutomaton): Rather than with an interruption, the best way to stop this client is by typing 'quit'. This won't be a message sent to the server. - _'mycert' and 'mykey' may be provided as filenames. They will be used in - the handshake, should the server ask for client authentication. - _'server_name' does not need to be set. - _'client_hello' may hold a TLSClientHello or SSLv2ClientHello to be sent - to the server. This is particularly useful for extensions tweaking. - _'version' is a quicker way to advertise a protocol version ("sslv2", - "tls1", "tls12", etc.) It may be overridden by the previous 'client_hello'. - _'data' is a list of raw data to be sent to the server once the handshake - has been completed. Both 'stop_server' and 'quit' will work this way. + :param server: the server IP or hostname. defaults to 127.0.0.1 + :param dport: the server port. defaults to 4433 + :param server_name: the SNI to use. It does not need to be set + :param mycert: + :param mykey: may be provided as filenames. They will be used in the (or post) + handshake, should the server ask for client authentication. + :param client_hello: may hold a TLSClientHello, TLS13ClientHello or + SSLv2ClientHello to be sent to the server. This is particularly useful + for extensions tweaking. If not set, a default is populated accordingly. + :param version: is a quicker way to advertise a protocol version ("sslv2", + "tls1", "tls12", "tls13", etc.) It may be overridden by the previous + 'client_hello'. + :param session_ticket_file_in: path to a file that contains a session ticket + acquired in a previous session. + :param session_ticket_file_out: path to store any session ticket acquired during + this session. + :param data: is a list of raw data to be sent to the server once the + handshake has been completed. Both 'stop_server' and 'quit' will + work this way. """ def parse_args(self, server="127.0.0.1", dport=4433, server_name=None, mycert=None, mykey=None, client_hello=None, version=None, + resumption_master_secret=None, + session_ticket_file_in=None, + session_ticket_file_out=None, + psk=None, psk_mode=None, data=None, - ciphersuite=None, - curve=None, + ciphersuite: Optional[int] = None, + curve: Optional[str] = None, + supported_groups=None, + supported_signature_algorithms=None, **kargs): super(TLSClientAutomaton, self).parse_args(mycert=mycert, mykey=mykey, **kargs) tmp = socket.getaddrinfo(server, dport) - self.remote_name = None - try: - if ':' in server: - inet_pton(socket.AF_INET6, server) - else: - inet_pton(socket.AF_INET, server) - except Exception: - self.remote_name = socket.getfqdn(server) - if self.remote_name != server: - tmp = socket.getaddrinfo(self.remote_name, dport) - - if server_name: - self.remote_name = server_name self.remote_family = tmp[0][0] self.remote_ip = tmp[0][4][0] self.remote_port = dport + self.server_name = server_name self.local_ip = None self.local_port = None self.socket = None - if (isinstance(client_hello, TLSClientHello) or - isinstance(client_hello, TLS13ClientHello)): + if isinstance(client_hello, (SSLv2ClientHello, TLSClientHello, + TLS13ClientHello)): self.client_hello = client_hello else: self.client_hello = None @@ -117,26 +157,53 @@ def parse_args(self, server="127.0.0.1", dport=4433, server_name=None, self.linebreak = False if isinstance(data, bytes): self.data_to_send = [data] - elif isinstance(data, six.string_types): + elif isinstance(data, str): self.data_to_send = [bytes_encode(data)] elif isinstance(data, list): self.data_to_send = list(bytes_encode(d) for d in reversed(data)) else: self.data_to_send = [] + + if supported_groups is None: + supported_groups = ["secp256r1", "secp384r1", "x448"] + if conf.crypto_valid_advanced: + supported_groups.extend([ + "x25519", + "ffdhe2048", + ]) + self.supported_groups = supported_groups + + if supported_signature_algorithms is None: + supported_signature_algorithms = [ + "sha256+rsaepss", + "sha256+rsa", + "ed25519", + "ed448", + ] + self.supported_signature_algorithms = supported_signature_algorithms + self.curve = None + self.ciphersuite = None + + if ciphersuite is not None: + if ciphersuite in _tls_cipher_suites.keys(): + self.ciphersuite = ciphersuite + else: + self.vprint("Unrecognized cipher suite.") if self.advertised_tls_version == 0x0304: - self.ciphersuite = 0x1301 - if ciphersuite is not None: - cs = int(ciphersuite, 16) - if cs in _tls_cipher_suites.keys(): - self.ciphersuite = cs if conf.crypto_valid_advanced: # Default to x25519 if supported self.curve = 29 else: # Or secp256r1 otherwise self.curve = 23 + self.resumption_master_secret = resumption_master_secret + self.session_ticket_file_in = session_ticket_file_in + self.session_ticket_file_out = session_ticket_file_out + self.tls13_psk_secret = psk + self.tls13_psk_mode = psk_mode + self.tls13_doing_client_postauth = False if curve is not None: for (group_id, ng) in _tls_named_groups.items(): if ng == curve: @@ -150,16 +217,22 @@ def vprint_sessioninfo(self): if self.verbose: s = self.cur_session v = _tls_version[s.tls_version] - self.vprint("Version : %s" % v) + self.vprint("Version : %s" % v) cs = s.wcs.ciphersuite.name - self.vprint("Cipher suite : %s" % cs) + self.vprint("Cipher suite : %s" % cs) + kx_groupname = s.kx_group + self.vprint("Server temp key : %s" % kx_groupname) if s.tls_version >= 0x0304: ms = s.tls13_master_secret else: ms = s.master_secret - self.vprint("Master secret : %s" % repr_hex(ms)) + self.vprint("Master secret : %s" % repr_hex(ms)) if s.server_certs: self.vprint("Server certificate chain: %r" % s.server_certs) + if s.tls_version >= 0x0304: + res_secret = s.tls13_derived_secrets["resumption_secret"] + self.vprint("Resumption master secret : %s" % + repr_hex(res_secret)) self.vprint() @ATMT.state(initial=True) @@ -167,17 +240,69 @@ def INITIAL(self): self.vprint("Starting TLS client automaton.") raise self.INIT_TLS_SESSION() + @ATMT.ioevent(INITIAL, name="tls", as_supersocket="tlslink") + def _socket(self, fd): + pass + @ATMT.state() def INIT_TLS_SESSION(self): self.cur_session = tlsSession(connection_end="client") - self.cur_session.client_certs = self.mycert - self.cur_session.client_key = self.mykey + s = self.cur_session + s.client_certs = self.mycert + s.client_key = self.mykey v = self.advertised_tls_version if v: - self.cur_session.advertised_tls_version = v + s.advertised_tls_version = v else: - default_version = self.cur_session.advertised_tls_version + default_version = s.advertised_tls_version self.advertised_tls_version = default_version + + if s.advertised_tls_version >= 0x0304: + # For out of band PSK, the PSK is given as an argument + # to the automaton + if self.tls13_psk_secret: + s.tls13_psk_secret = binascii.unhexlify(self.tls13_psk_secret) + + # For resumed PSK, the PSK is computed from + if self.session_ticket_file_in: + with open(self.session_ticket_file_in, 'rb') as f: + + resumed_ciphersuite_len = struct.unpack("B", f.read(1))[0] + s.tls13_ticket_ciphersuite = \ + struct.unpack("!H", f.read(resumed_ciphersuite_len))[0] + + ticket_nonce_len = struct.unpack("B", f.read(1))[0] + # XXX add client_session_nonce member in tlsSession + s.client_session_nonce = f.read(ticket_nonce_len) + + client_ticket_age_len = struct.unpack("!H", f.read(2))[0] + tmp = f.read(client_ticket_age_len) + s.client_ticket_age = struct.unpack("!I", tmp)[0] + + client_ticket_age_add_len = struct.unpack( + "!H", f.read(2))[0] + tmp = f.read(client_ticket_age_add_len) + s.client_session_ticket_age_add = struct.unpack( + "!I", tmp)[0] + + ticket_len = struct.unpack("!H", f.read(2))[0] + s.client_session_ticket = f.read(ticket_len) + + if self.resumption_master_secret: + + if s.tls13_ticket_ciphersuite not in _tls_cipher_suites_cls: # noqa: E501 + warning("Unknown cipher suite %d", s.tls13_ticket_ciphersuite) # noqa: E501 + # we do not try to set a default nor stop the execution + else: + cs_cls = _tls_cipher_suites_cls[s.tls13_ticket_ciphersuite] # noqa: E501 + + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + + s.tls13_psk_secret = hkdf.expand_label(binascii.unhexlify(self.resumption_master_secret), # noqa: E501 + b"resumption", + s.client_session_nonce, # noqa: E501 + hash_len) raise self.CONNECT() @ATMT.state() @@ -208,10 +333,19 @@ def should_add_ClientHello(self): if self.client_hello: p = self.client_hello else: - p = TLSClientHello() - # Add TLS_Ext_SignatureAlgorithms for TLS 1.2 ClientHello - if self.cur_session.advertised_tls_version == 0x0303: - p.ext = TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsa"]) + p = TLSClientHello(ciphers=self.ciphersuite) + ext = [] + # Add TLS_Ext_SignatureAlgorithms for TLS 1.2 ClientHello + if self.cur_session.advertised_tls_version == 0x0303: + ext += [TLS_Ext_SignatureAlgorithms( + sig_algs=self.supported_signature_algorithms, + )] + # Add TLS_Ext_ServerName + if self.server_name: + ext += TLS_Ext_ServerName( + servernames=[ServerName(servername=self.server_name)] + ) + p.ext = ext self.add_msg(p) raise self.ADDED_CLIENTHELLO() @@ -332,7 +466,7 @@ def should_handle_ServerHelloDone(self): def should_handle_ServerHelloDone_from_ServerKeyExchange(self): return self.should_handle_ServerHelloDone() - @ATMT.condition(HANDLED_CERTIFICATEREQUEST, prio=4) + @ATMT.condition(HANDLED_CERTIFICATEREQUEST) def should_handle_ServerHelloDone_from_CertificateRequest(self): return self.should_handle_ServerHelloDone() @@ -358,12 +492,13 @@ def should_add_ClientCertificate(self): XXX We may want to add a complete chain. """ - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if TLSCertificateRequest not in hs_msg: + if not self.in_handshake(TLSCertificateRequest): return + certs = [] if self.mycert: certs = [self.mycert] + self.add_msg(TLSCertificate(certs=certs)) raise self.ADDED_CLIENTCERTIFICATE() @@ -396,10 +531,9 @@ def should_add_ClientVerify(self): We should verify that before adding the message. We should also handle the case when the Certificate message was empty. """ - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if (TLSCertificateRequest not in hs_msg or - self.mycert is None or - self.mykey is None): + if not self.in_handshake(TLSCertificateRequest): + return + if self.mycert is None or self.mykey is None: return self.add_msg(TLSCertificateVerify()) raise self.ADDED_CERTIFICATEVERIFY() @@ -490,11 +624,30 @@ def add_ClientData(self): Special characters are handled so that it becomes a valid HTTP request. """ if not self.data_to_send: - data = six.moves.input().replace('\\r', '\r').replace('\\n', '\n').encode() # noqa: E501 + if self.is_atmt_socket: + # Socket mode + fd = select_objects([self.ioin["tls"]], 0) + if fd: + self.add_record() + self.add_msg(TLSApplicationData(data=fd[0].recv())) + raise self.ADDED_CLIENTDATA() + raise self.WAITING_SERVERDATA() + else: + data = input().replace('\\r', '\r').replace('\\n', '\n').encode() else: data = self.data_to_send.pop() if data == b"quit": return + # Command to skip sending + elif data == b"wait": + raise self.WAITING_SERVERDATA() + # Command to perform a key_update (for a TLS 1.3 session) + elif data == b"key_update": + if self.cur_session.tls_version >= 0x0304: + self.add_record() + self.add_msg(TLS13KeyUpdate(request_update="update_requested")) + raise self.ADDED_CLIENTDATA() + if self.linebreak: data += b"\n" self.add_record() @@ -521,6 +674,8 @@ def SENT_CLIENTDATA(self): @ATMT.state() def WAITING_SERVERDATA(self): self.get_next_msg(0.3, 1) + if not self.buffer_in: + raise self.WAIT_CLIENTDATA() raise self.RECEIVED_SERVERDATA() @ATMT.state() @@ -528,17 +683,101 @@ def RECEIVED_SERVERDATA(self): pass @ATMT.condition(RECEIVED_SERVERDATA, prio=1) + def should_handle_CertificateRequest_postauth(self): + self.raise_on_packet(TLS13CertificateRequest, + self.TLS13_RECEIVED_POST_AUTHENTICATION_REQUEST) + + @ATMT.state() + def TLS13_RECEIVED_POST_AUTHENTICATION_REQUEST(self): + self.vprint("Server asked for a certificate...") + self.tls13_doing_client_postauth = True + if not self.mykey or not self.mycert: + self.vprint("No client certificate to send!") + self.vprint("Will try and send an empty Certificate message...") + self.add_record(is_tls13=True) + + @ATMT.condition(TLS13_RECEIVED_POST_AUTHENTICATION_REQUEST, prio=1) + def should_send_CertificateRequest_postauth(self): + if self.cur_session.post_handshake_auth: + self.tls13_should_add_ClientCertificate() + + @ATMT.condition(TLS13_RECEIVED_POST_AUTHENTICATION_REQUEST, prio=2) + def should_fail_CertificateRequest_postauth(self): + self.add_msg(TLSAlert(level=2, descr=0x0A)) + self.flush_records() + self.vprint( + "Received CertificateRequest without post_handshake_auth extension!" + ) + raise self.FINAL() + + @ATMT.condition(RECEIVED_SERVERDATA, prio=2) + def should_handle_NewSessionTicket(self): + self.raise_on_packet(TLS13NewSessionTicket, + self.TLS13_RECEIVED_NEW_SESSION_TICKET) + + @ATMT.state() + def TLS13_RECEIVED_NEW_SESSION_TICKET(self): + pass + + @ATMT.condition(TLS13_RECEIVED_NEW_SESSION_TICKET) + def should_store_session_ticket_file(self): + # If arg session_ticket_file_out is set, we save + # the ticket for resumption... + if self.session_ticket_file_out: + # Struct of ticket file : + # * ciphersuite_len (1 byte) + # * ciphersuite (ciphersuite_len bytes) : + # we need to the store the ciphersuite for resumption + # * ticket_nonce_len (1 byte) + # * ticket_nonce (ticket_nonce_len bytes) : + # we need to store the nonce to compute the PSK + # for resumption + # * ticket_age_len (2 bytes) + # * ticket_age (ticket_age_len bytes) : + # we need to store the time we received the ticket for + # computing the obfuscated_ticket_age when resuming + # * ticket_age_add_len (2 bytes) + # * ticket_age_add (ticket_age_add_len bytes) : + # we need to store the ticket_age_add value from the + # ticket to compute the obfuscated ticket age + # * ticket_len (2 bytes) + # * ticket (ticket_len bytes) + with open(self.session_ticket_file_out, 'wb') as f: + f.write(struct.pack("B", 2)) + # we choose wcs arbitrarily... + f.write(struct.pack("!H", + self.cur_session.wcs.ciphersuite.val)) + f.write(struct.pack("B", self.cur_pkt.noncelen)) + f.write(self.cur_pkt.ticket_nonce) + f.write(struct.pack("!H", 4)) + f.write(struct.pack("!I", int(time.time()))) + f.write(struct.pack("!H", 4)) + f.write(struct.pack("!I", self.cur_pkt.ticket_age_add)) + f.write(struct.pack("!H", self.cur_pkt.ticketlen)) + f.write(self.cur_session.client_session_ticket) + self.vprint( + "Received a TLS 1.3 NewSessionTicket that was stored to %s" % ( + self.session_ticket_file_out + ) + ) + else: + self.vprint("Ignored TLS 1.3 NewSessionTicket.") + raise self.WAIT_CLIENTDATA() + + @ATMT.condition(RECEIVED_SERVERDATA, prio=3) def should_handle_ServerData(self): - if not self.buffer_in: - raise self.WAIT_CLIENTDATA() p = self.buffer_in[0] if isinstance(p, TLSApplicationData): - print("> Received: %r" % p.data) + if self.is_atmt_socket: + # Socket mode + self.oi.tls.send(p.data) + else: + self.vprint("Received: %r" % p.data) elif isinstance(p, TLSAlert): - print("> Received: %r" % p) + self.vprint("Received: %r" % p) raise self.CLOSE_NOTIFY() else: - print("> Received: %r" % p) + self.vprint("Received: %r" % p) self.buffer_in = self.buffer_in[1:] raise self.HANDLED_SERVERDATA() @@ -655,8 +894,7 @@ def SSLv2_HANDLED_SERVERVERIFY(self): pass def sslv2_should_add_ClientFinished(self): - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if SSLv2ClientFinished in hs_msg: + if self.in_handshake(SSLv2ClientFinished): return self.add_record(is_sslv2=True) self.add_msg(SSLv2ClientFinished()) @@ -694,8 +932,7 @@ def sslv2_should_send_ClientFinished(self): @ATMT.state() def SSLv2_SENT_CLIENTFINISHED(self): - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if SSLv2ServerVerify in hs_msg: + if self.in_handshake(SSLv2ServerVerify): raise self.SSLv2_WAITING_SERVERFINISHED() else: self.get_next_msg() @@ -778,10 +1015,10 @@ def SSLv2_WAITING_CLIENTDATA(self): @ATMT.condition(SSLv2_WAITING_CLIENTDATA, prio=1) def sslv2_add_ClientData(self): if not self.data_to_send: - data = six.moves.input().replace('\\r', '\r').replace('\\n', '\n').encode() # noqa: E501 + data = input().replace('\\r', '\r').replace('\\n', '\n').encode() else: data = self.data_to_send.pop() - self.vprint("> Read from list: %s" % data) + self.vprint("Read from list: %s" % data) if data == "quit": return if self.linebreak: @@ -821,7 +1058,7 @@ def sslv2_should_handle_ServerData(self): if not self.buffer_in: raise self.SSLv2_WAITING_CLIENTDATA() p = self.buffer_in[0] - print("> Received: %r" % p.load) + self.vprint("Received: %r" % p.load) if p.load.startswith(b"goodbye"): raise self.SSLv2_CLOSE_NOTIFY() self.buffer_in = self.buffer_in[1:] @@ -860,21 +1097,89 @@ def TLS13_START(self): @ATMT.condition(TLS13_START) def tls13_should_add_ClientHello(self): # we have to use the legacy, plaintext TLS record here - supported_groups = ["secp256r1", "secp384r1"] - if conf.crypto_valid_advanced: - supported_groups.append("x25519") self.add_record(is_tls13=False) - ext = [TLS_Ext_SupportedVersion_CH(versions=["TLS 1.3"]), - TLS_Ext_SupportedGroups(groups=supported_groups), - TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=self.curve)]), # noqa: E501 - TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss", - "sha256+rsa"])] if self.client_hello: - if not self.client_hello.ext: - self.client_hello.ext = ext p = self.client_hello else: - p = TLS13ClientHello(ciphers=self.ciphersuite, ext=ext) + if self.ciphersuite is None: + c = 0x1301 + else: + c = self.ciphersuite + p = TLS13ClientHello(ciphers=c) + + ext = [] + ext += TLS_Ext_SupportedVersion_CH(versions=[self.advertised_tls_version]) + + s = self.cur_session + + # Add TLS_Ext_ServerName + if self.server_name: + ext += TLS_Ext_ServerName( + servernames=[ServerName(servername=self.server_name)] + ) + + # Add TLS_Ext_PostHandshakeAuth + if self.mycert is not None and self.mykey is not None: + ext += TLS_Ext_PostHandshakeAuth() + + if s.tls13_psk_secret: + # Check if DHE is need (both for out of band and resumption PSK) + if self.tls13_psk_mode == "psk_dhe_ke": + ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_dhe_ke") + ext += TLS_Ext_SupportedGroups(groups=self.supported_groups) + ext += TLS_Ext_KeyShare_CH( + client_shares=[KeyShareEntry(group=self.curve)] + ) + else: + ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_ke") + + # RFC8446, section 4.2.11. + # "The "pre_shared_key" extension MUST be the last extension + # in the ClientHello " + # Compute the pre_shared_key extension for resumption PSK + if s.client_session_ticket: + cs_cls = _tls_cipher_suites_cls[s.tls13_ticket_ciphersuite] # noqa: E501 + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + # We compute the client's view of the age of the ticket (ie + # the time since the receipt of the ticket) in ms + agems = int((time.time() - s.client_ticket_age) * 1000) + # Then we compute the obfuscated version of the ticket age + # by adding the "ticket_age_add" value included in the + # ticket (modulo 2^32) + obfuscated_age = ((agems + s.client_session_ticket_age_add) & + 0xffffffff) + + psk_id = PSKIdentity(identity=s.client_session_ticket, + obfuscated_ticket_age=obfuscated_age) + + psk_binder_entry = PSKBinderEntry(binder_len=hash_len, + binder=b"\x00" * hash_len) + + ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], + binders=[psk_binder_entry]) + else: + # Compute the pre_shared_key extension for out of band PSK + # (SHA256 is used as default hash function for HKDF for out + # of band PSK) + hkdf = TLS13_HKDF("sha256") + hash_len = hkdf.hash.digest_size + psk_id = PSKIdentity(identity='Client_identity') + # XXX see how to not pass binder as argument + psk_binder_entry = PSKBinderEntry(binder_len=hash_len, + binder=b"\x00" * hash_len) + + ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], + binders=[psk_binder_entry]) + else: + ext += TLS_Ext_SupportedGroups(groups=self.supported_groups) + ext += TLS_Ext_KeyShare_CH( + client_shares=[KeyShareEntry(group=self.curve)] + ) + ext += TLS_Ext_SignatureAlgorithms( + sig_algs=self.supported_signature_algorithms, + ) + p.ext = ext self.add_msg(p) raise self.TLS13_ADDED_CLIENTHELLO() @@ -915,15 +1220,32 @@ def tls13_should_handle_ServerHello(self): @ATMT.condition(TLS13_RECEIVED_SERVERFLIGHT1, prio=2) def tls13_should_handle_HelloRetryRequest(self): + """ + XXX We should check the ServerHello attributes for discrepancies with + our own ClientHello. + """ self.raise_on_packet(TLS13HelloRetryRequest, self.TLS13_HELLO_RETRY_REQUESTED) @ATMT.condition(TLS13_RECEIVED_SERVERFLIGHT1, prio=3) def tls13_should_handle_AlertMessage_(self): self.raise_on_packet(TLSAlert, - self.CLOSE_NOTIFY) + self.TLS13_HANDLED_ALERT_FROM_SERVERFLIGHT1) @ATMT.condition(TLS13_RECEIVED_SERVERFLIGHT1, prio=4) + def tls13_should_handle_ChangeCipherSpec_after_tls13_retry(self): + # Middlebox compatibility mode after a HelloRetryRequest. + if self.cur_session.tls13_retry: + self.raise_on_packet(TLSChangeCipherSpec, + self.TLS13_RECEIVED_SERVERFLIGHT1) + + @ATMT.state() + def TLS13_HANDLED_ALERT_FROM_SERVERFLIGHT1(self): + self.vprint("Received Alert message !") + self.vprint(self.cur_pkt.mysummary()) + raise self.CLOSE_NOTIFY() + + @ATMT.condition(TLS13_RECEIVED_SERVERFLIGHT1, prio=5) def tls13_missing_ServerHello(self): raise self.MISSING_SERVERHELLO() @@ -935,49 +1257,49 @@ def TLS13_HELLO_RETRY_REQUESTED(self): def tls13_should_add_ClientHello_Retry(self): s = self.cur_session s.tls13_retry = True - # we have to use the legacy, plaintext TLS record here - self.add_record(is_tls13=False) # We retrieve the group to be used and the selected version from the # previous message - hrr = s.handshake_messages_parsed[-1] - if isinstance(hrr, TLS13HelloRetryRequest): - pass - ciphersuite = hrr.cipher + hrr = self.cur_pkt + self.ciphersuite = hrr.cipher + # "The server's extensions MUST contain supported_versions." + self.advertised_tls_version = None if hrr.ext: for e in hrr.ext: if isinstance(e, TLS_Ext_KeyShare_HRR): - selected_group = e.selected_group + self.curve = e.selected_group if isinstance(e, TLS_Ext_SupportedVersion_SH): - selected_version = e.version - if not selected_group or not selected_version: + self.advertised_tls_version = e.version + + if _tls_named_groups[self.curve] not in self.supported_groups: + self.vprint("No common groups found in TLS 1.3 Hello Retry Request!") + raise self.CLOSE_NOTIFY() + + if not self.advertised_tls_version: + self.vprint("No supported_versions found in TLS 1.3 Hello Retry Request!") raise self.CLOSE_NOTIFY() - ext = [TLS_Ext_SupportedVersion_CH(versions=[_tls_version[selected_version]]), # noqa: E501 - TLS_Ext_SupportedGroups(groups=[_tls_named_groups[selected_group]]), # noqa: E501 - TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=selected_group)]), # noqa: E501 - TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss"])] - p = TLS13ClientHello(ciphers=ciphersuite, ext=ext) - self.add_msg(p) - raise self.TLS13_ADDED_CLIENTHELLO() + + self.tls13_should_add_ClientHello() @ATMT.state() def TLS13_HANDLED_SERVERHELLO(self): pass - @ATMT.state() - def TLS13_WAITING_ENCRYPTEDEXTENSIONS(self): - self.get_next_msg() - - @ATMT.condition(TLS13_WAITING_ENCRYPTEDEXTENSIONS) - def tls13_should_handle_EncryptedExtensions(self): - self.raise_on_packet(TLSEncryptedExtensions, - self.TLS13_WAITING_CERTIFICATE) - @ATMT.condition(TLS13_HANDLED_SERVERHELLO, prio=1) def tls13_should_handle_encrytpedExtensions(self): self.raise_on_packet(TLSEncryptedExtensions, self.TLS13_HANDLED_ENCRYPTEDEXTENSIONS) @ATMT.condition(TLS13_HANDLED_SERVERHELLO, prio=2) + def tls13_should_handle_ChangeCipherSpec(self): + self.raise_on_packet(TLSChangeCipherSpec, + self.TLS13_HANDLED_CHANGE_CIPHER_SPEC) + + @ATMT.state() + def TLS13_HANDLED_CHANGE_CIPHER_SPEC(self): + self.cur_session.middlebox_compatibility = True + raise self.TLS13_HANDLED_SERVERHELLO() + + @ATMT.condition(TLS13_HANDLED_SERVERHELLO, prio=3) def tls13_missing_encryptedExtension(self): self.vprint("Missing TLS 1.3 EncryptedExtensions message!") raise self.CLOSE_NOTIFY() @@ -986,10 +1308,33 @@ def tls13_missing_encryptedExtension(self): def TLS13_HANDLED_ENCRYPTEDEXTENSIONS(self): pass + @ATMT.condition(TLS13_HANDLED_ENCRYPTEDEXTENSIONS, prio=1) + def tls13_should_handle_certificateRequest_from_encryptedExtensions(self): + """ + XXX We should check the CertificateRequest attributes for discrepancies + with the cipher suite, etc. + """ + self.raise_on_packet(TLS13CertificateRequest, + self.TLS13_HANDLED_CERTIFICATEREQUEST) + @ATMT.condition(TLS13_HANDLED_ENCRYPTEDEXTENSIONS, prio=2) def tls13_should_handle_certificate_from_encryptedExtensions(self): self.tls13_should_handle_Certificate() + @ATMT.condition(TLS13_HANDLED_ENCRYPTEDEXTENSIONS, prio=3) + def tls13_should_handle_finished_from_encryptedExtensions(self): + if self.cur_session.tls13_psk_secret: + self.raise_on_packet(TLSFinished, + self.TLS13_HANDLED_FINISHED) + + @ATMT.state() + def TLS13_HANDLED_CERTIFICATEREQUEST(self): + pass + + @ATMT.condition(TLS13_HANDLED_CERTIFICATEREQUEST, prio=1) + def tls13_should_handle_Certificate_from_CertificateRequest(self): + return self.tls13_should_handle_Certificate() + def tls13_should_handle_Certificate(self): self.raise_on_packet(TLS13Certificate, self.TLS13_HANDLED_CERTIFICATE) @@ -1023,9 +1368,63 @@ def TLS13_HANDLED_FINISHED(self): @ATMT.state() def TLS13_PREPARE_CLIENTFLIGHT2(self): + if self.cur_session.middlebox_compatibility: + self.add_record(is_tls12=True) + self.add_msg(TLSChangeCipherSpec()) self.add_record(is_tls13=True) - @ATMT.condition(TLS13_PREPARE_CLIENTFLIGHT2) + @ATMT.condition(TLS13_PREPARE_CLIENTFLIGHT2, prio=1) + def tls13_should_add_ClientCertificate(self): + """ + If the server sent a CertificateRequest, we send a Certificate message. + If no certificate is available, an empty Certificate message is sent: + - this is a SHOULD in RFC 4346 (Section 7.4.6) + - this is a MUST in RFC 5246 (Section 7.4.6) + + XXX We may want to add a complete chain. + """ + if not (isinstance(self.cur_pkt, TLS13CertificateRequest) or + self.in_handshake(TLS13CertificateRequest)): + return + + certs = [] + if self.mycert: + certs += _ASN1CertAndExt(cert=self.mycert) + + self.add_msg( + TLS13Certificate( + certs=certs, + cert_req_ctxt=self.cur_session.tls13_cert_req_ctxt, + ) + ) + raise self.TLS13_ADDED_CLIENTCERTIFICATE() + + @ATMT.state() + def TLS13_ADDED_CLIENTCERTIFICATE(self): + pass + + @ATMT.condition(TLS13_ADDED_CLIENTCERTIFICATE, prio=0) + def tls13_should_skip_ClientCertificateVerify(self): + if not self.mycert: + return self.tls13_should_add_ClientFinished() + + @ATMT.condition(TLS13_ADDED_CLIENTCERTIFICATE, prio=1) + def tls13_should_add_ClientCertificateVerify(self): + """ + XXX Section 7.4.7.1 of RFC 5246 states that the CertificateVerify + message is only sent following a client certificate that has signing + capability (i.e. not those containing fixed DH params). + We should verify that before adding the message. We should also handle + the case when the Certificate message was empty. + """ + self.add_msg(TLSCertificateVerify()) + raise self.TLS13_ADDED_CERTIFICATEVERIFY() + + @ATMT.state() + def TLS13_ADDED_CERTIFICATEVERIFY(self): + return self.tls13_should_add_ClientFinished() + + @ATMT.condition(TLS13_PREPARE_CLIENTFLIGHT2, prio=2) def tls13_should_add_ClientFinished(self): self.add_msg(TLSFinished()) raise self.TLS13_ADDED_CLIENTFINISHED() @@ -1041,11 +1440,27 @@ def tls13_should_send_ClientFlight2(self): @ATMT.state() def TLS13_SENT_CLIENTFLIGHT2(self): + if self.tls13_doing_client_postauth: + self.tls13_doing_client_postauth = False + self.vprint("TLS 1.3 post-handshake authentication sent!") + raise self.WAIT_CLIENTDATA() self.vprint("TLS 1.3 handshake completed!") self.vprint_sessioninfo() self.vprint("You may send data or use 'quit'.") raise self.WAIT_CLIENTDATA() + @ATMT.state() + def SOCKET_CLOSED(self): + raise self.FINAL() + + @ATMT.state(stop=True) + def STOP(self): + # Called on atmt.stop() + if self.cur_session.advertised_tls_version in [0x0200, 0x0002]: + raise self.SSLv2_CLOSE_NOTIFY() + else: + raise self.CLOSE_NOTIFY() + @ATMT.state(final=True) def FINAL(self): # We might call shutdown, but it may happen that the server diff --git a/scapy/layers/tls/automaton_srv.py b/scapy/layers/tls/automaton_srv.py index 069759b9af2..d0ad402142b 100644 --- a/scapy/layers/tls/automaton_srv.py +++ b/scapy/layers/tls/automaton_srv.py @@ -1,50 +1,86 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ TLS server automaton. This makes for a primitive TLS stack. Obviously you need rights for network access. -We support versions SSLv2 to TLS 1.2, along with many features. -There is no session resumption mechanism for now. +We support versions SSLv2 to TLS 1.3, along with many features. -In order to run a server listening on tcp/4433: -> from scapy.all import * -> t = TLSServerAutomaton(mycert='', mykey='') -> t.run() +In order to run a server listening on tcp/4433:: + + from scapy.layers.tls import * + t = TLSServerAutomaton(mycert='', mykey='') + t.run() """ -from __future__ import print_function import socket +import binascii +import struct +import time +from scapy.config import conf from scapy.packet import Raw from scapy.pton_ntop import inet_pton -from scapy.utils import randstring, repr_hex +from scapy.utils import get_temp_file, randstring, repr_hex from scapy.automaton import ATMT +from scapy.error import warning from scapy.layers.tls.automaton import _TLSAutomaton -from scapy.layers.tls.cert import PrivKeyRSA, PrivKeyECDSA +from scapy.layers.tls.cert import PrivKeyRSA, PrivKeyECDSA, PrivKeyEdDSA from scapy.layers.tls.basefields import _tls_version from scapy.layers.tls.session import tlsSession from scapy.layers.tls.crypto.groups import _tls_named_groups -from scapy.layers.tls.extensions import TLS_Ext_SupportedVersion_SH, \ - TLS_Ext_SupportedGroups, TLS_Ext_Cookie -from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_SH, \ - KeyShareEntry, TLS_Ext_KeyShare_HRR +from scapy.layers.tls.extensions import ( + TLS_Ext_Cookie, + TLS_Ext_EarlyDataIndicationTicket, + TLS_Ext_PSKKeyExchangeModes, + TLS_Ext_RenegotiationInfo, + TLS_Ext_SignatureAlgorithms, + TLS_Ext_SupportedGroups, + TLS_Ext_SupportedVersion_SH, +) +from scapy.layers.tls.keyexchange import _tls_hash_sig +from scapy.layers.tls.keyexchange_tls13 import ( + TLS_Ext_KeyShare_SH, + KeyShareEntry, + TLS_Ext_KeyShare_HRR, + TLS_Ext_PreSharedKey_CH, + TLS_Ext_PreSharedKey_SH, + get_usable_tls13_sigalgs, +) from scapy.layers.tls.handshake import TLSCertificate, TLSCertificateRequest, \ TLSCertificateVerify, TLSClientHello, TLSClientKeyExchange, TLSFinished, \ TLSServerHello, TLSServerHelloDone, TLSServerKeyExchange, \ _ASN1CertAndExt, TLS13ServerHello, TLS13Certificate, TLS13ClientHello, \ - TLSEncryptedExtensions, TLS13HelloRetryRequest + TLSEncryptedExtensions, TLS13HelloRetryRequest, TLS13CertificateRequest, \ + TLS13KeyUpdate, TLS13NewSessionTicket from scapy.layers.tls.handshake_sslv2 import SSLv2ClientCertificate, \ SSLv2ClientFinished, SSLv2ClientHello, SSLv2ClientMasterKey, \ SSLv2RequestCertificate, SSLv2ServerFinished, SSLv2ServerHello, \ SSLv2ServerVerify from scapy.layers.tls.record import TLSAlert, TLSChangeCipherSpec, \ TLSApplicationData -from scapy.layers.tls.crypto.suites import _tls_cipher_suites_cls, \ - get_usable_ciphersuites +from scapy.layers.tls.record_tls13 import TLS13 +from scapy.layers.tls.crypto.hkdf import TLS13_HKDF +from scapy.layers.tls.crypto.suites import ( + _tls_cipher_suites_cls, + _tls_cipher_suites, + get_usable_ciphersuites, +) + +# Typing imports +from typing import ( + Optional, + Union, +) + +if conf.crypto_valid: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes class TLSServerAutomaton(_TLSAutomaton): @@ -73,12 +109,17 @@ class TLSServerAutomaton(_TLSAutomaton): def parse_args(self, server="127.0.0.1", sport=4433, mycert=None, mykey=None, - preferred_ciphersuite=None, + preferred_ciphersuite: Optional[int] = None, + preferred_signature_algorithm: Union[str, int, None] = None, client_auth=False, is_echo_server=True, max_client_idle_time=60, + handle_session_ticket=None, + session_ticket_file=None, curve=None, cookie=False, + psk=None, + psk_mode=None, **kargs): super(TLSServerAutomaton, self).parse_args(mycert=mycert, @@ -100,28 +141,65 @@ def parse_args(self, server="127.0.0.1", sport=4433, self.remote_ip = None self.remote_port = None - self.preferred_ciphersuite = preferred_ciphersuite self.client_auth = client_auth self.is_echo_server = is_echo_server self.max_client_idle_time = max_client_idle_time self.curve = None + self.preferred_ciphersuite = None + self.preferred_signature_algorithm = None self.cookie = cookie - for (group_id, ng) in _tls_named_groups.items(): - if ng == curve: - self.curve = group_id + self.psk_secret = psk + self.psk_mode = psk_mode + + if handle_session_ticket is None: + handle_session_ticket = session_ticket_file is not None + if handle_session_ticket: + session_ticket_file = session_ticket_file or get_temp_file() + self.handle_session_ticket = handle_session_ticket + self.session_ticket_file = session_ticket_file + + if preferred_ciphersuite is not None: + if preferred_ciphersuite in _tls_cipher_suites: + self.preferred_ciphersuite = preferred_ciphersuite + else: + self.vprint("Unrecognized cipher suite.") + + if preferred_signature_algorithm is not None: + if preferred_signature_algorithm in _tls_hash_sig: + self.preferred_signature_algorithm = preferred_signature_algorithm + else: + for (sig_id, nc) in _tls_hash_sig.items(): + if nc == preferred_signature_algorithm: + self.preferred_signature_algorithm = sig_id + break + else: + self.vprint("Unrecognized signature algorithm.") + + if curve: + for (group_id, ng) in _tls_named_groups.items(): + if ng == curve: + self.curve = group_id + break + else: + self.vprint("Unrecognized curve.") def vprint_sessioninfo(self): if self.verbose: s = self.cur_session v = _tls_version[s.tls_version] - self.vprint("Version : %s" % v) + self.vprint("Version : %s" % v) cs = s.wcs.ciphersuite.name - self.vprint("Cipher suite : %s" % cs) + self.vprint("Cipher suite : %s" % cs) + kx_groupname = s.kx_group + self.vprint("Server temp key : %s" % kx_groupname) + if s.tls_version >= 0x0304: + sigalg = _tls_hash_sig[s.selected_sig_alg] + self.vprint("Negotiated sig_alg : %s" % sigalg) if s.tls_version < 0x0304: ms = s.master_secret else: ms = s.tls13_master_secret - self.vprint("Master secret : %s" % repr_hex(ms)) + self.vprint("Master secret : %s" % repr_hex(ms)) if s.client_certs: self.vprint("Client certificate chain: %r" % s.client_certs) @@ -144,7 +222,11 @@ def http_sessioninfo(self): s += "Version : %s\n" % v cs = self.cur_session.wcs.ciphersuite.name s += "Cipher suite : %s\n" % cs - ms = self.cur_session.master_secret + if self.cur_session.tls_version < 0x0304: + ms = self.cur_session.master_secret + else: + ms = self.cur_session.tls13_master_secret + s += "Master secret : %s\n" % repr_hex(ms) body = "
%s
\r\n\r\n" % s answer = (header + body) % len(body) @@ -178,6 +260,11 @@ def BIND(self): raise self.FINAL() raise self.WAITING_CLIENT() + @ATMT.state() + def SOCKET_CLOSED(self): + self.socket.close() + raise self.WAITING_CLIENT() + @ATMT.state() def WAITING_CLIENT(self): self.buffer_out = [] @@ -222,16 +309,27 @@ def RECEIVED_CLIENTFLIGHT1(self): pass # TLS handshake # + @ATMT.condition(RECEIVED_CLIENTFLIGHT1, prio=1) def tls13_should_handle_ClientHello(self): self.raise_on_packet(TLS13ClientHello, self.tls13_HANDLED_CLIENTHELLO) + if self.cur_session.advertised_tls_version == 0x0304: + self.raise_on_packet(TLSClientHello, + self.tls13_HANDLED_CLIENTHELLO) @ATMT.condition(RECEIVED_CLIENTFLIGHT1, prio=2) def should_handle_ClientHello(self): self.raise_on_packet(TLSClientHello, self.HANDLED_CLIENTHELLO) + @ATMT.condition(RECEIVED_CLIENTFLIGHT1, prio=3) + def tls13_should_handle_ChangeCipherSpec_after_tls13_retry(self): + # Middlebox compatibility mode after a HelloRetryRequest. + if self.cur_session.tls13_retry: + self.raise_on_packet(TLSChangeCipherSpec, + self.RECEIVED_CLIENTFLIGHT1) + @ATMT.state() def HANDLED_CLIENTHELLO(self): """ @@ -241,6 +339,8 @@ def HANDLED_CLIENTHELLO(self): kx = "RSA" elif isinstance(self.mykey, PrivKeyECDSA): kx = "ECDSA" + elif isinstance(self.mykey, PrivKeyEdDSA): + kx = "" if get_usable_ciphersuites(self.cur_pkt.ciphers, kx): raise self.PREPARE_SERVERFLIGHT1() raise self.NO_USABLE_CIPHERSUITE() @@ -268,18 +368,22 @@ def should_add_ServerHello(self): """ Selecting a cipher suite should be no trouble as we already caught the None case previously. - - Also, we do not manage extensions at all. """ if isinstance(self.mykey, PrivKeyRSA): kx = "RSA" elif isinstance(self.mykey, PrivKeyECDSA): kx = "ECDSA" + elif isinstance(self.mykey, PrivKeyEdDSA): + kx = "" usable_suites = get_usable_ciphersuites(self.cur_pkt.ciphers, kx) c = usable_suites[0] if self.preferred_ciphersuite in usable_suites: c = self.preferred_ciphersuite - self.add_msg(TLSServerHello(cipher=c)) + + # Some extensions + ext = [TLS_Ext_RenegotiationInfo()] + + self.add_msg(TLSServerHello(cipher=c, ext=ext)) raise self.ADDED_SERVERHELLO() @ATMT.state() @@ -379,6 +483,7 @@ def should_handle_Alert_from_ClientCertificate(self): @ATMT.state() def HANDLED_ALERT_FROM_CLIENTCERTIFICATE(self): self.vprint("Received Alert message instead of ClientKeyExchange!") + self.vprint(self.cur_pkt.mysummary()) raise self.CLOSE_NOTIFY() @ATMT.condition(HANDLED_CLIENTCERTIFICATE, prio=3) @@ -427,6 +532,7 @@ def should_handle_Alert_from_ClientKeyExchange(self): @ATMT.state() def HANDLED_ALERT_FROM_CLIENTKEYEXCHANGE(self): self.vprint("Received Alert message instead of ChangeCipherSpec!") + self.vprint(self.cur_pkt.mysummary()) raise self.CLOSE_NOTIFY() @ATMT.condition(HANDLED_CERTIFICATEVERIFY, prio=3) @@ -507,6 +613,10 @@ def SENT_SERVERFLIGHT2(self): # TLS 1.3 handshake # @ATMT.state() def tls13_HANDLED_CLIENTHELLO(self): + """ + Check if we have to send an HelloRetryRequest + XXX check also with non ECC groups + """ s = self.cur_session m = s.handshake_messages_parsed[-1] # Check if we have to send an HelloRetryRequest @@ -521,6 +631,12 @@ def tls13_HANDLED_CLIENTHELLO(self): if self.curve in e.groups: # Here, we need to send an HelloRetryRequest raise self.tls13_PREPARE_HELLORETRYREQUEST() + + # Signature Algorithms extension is mandatory + if not s.advertised_sig_algs: + self.vprint("Missing signature_algorithms extension in ClientHello!") + raise self.CLOSE_NOTIFY() + raise self.tls13_PREPARE_SERVERFLIGHT1() @ATMT.state() @@ -534,6 +650,8 @@ def tls13_should_add_HelloRetryRequest(self): kx = "RSA" elif isinstance(self.mykey, PrivKeyECDSA): kx = "ECDSA" + elif isinstance(self.mykey, PrivKeyEdDSA): + kx = "" usable_suites = get_usable_ciphersuites(self.cur_pkt.ciphers, kx) c = usable_suites[0] ext = [TLS_Ext_SupportedVersion_SH(version="TLS 1.3"), @@ -557,17 +675,162 @@ def tls13_should_add_ServerHello_from_HRR(self): def tls13_PREPARE_SERVERFLIGHT1(self): self.add_record(is_tls13=False) + def verify_psk_binder(self, psk_identity, obfuscated_age, binder): + """ + This function verifies the binder received in the 'pre_shared_key' + extension and return the resumption PSK associated with those + values. + + The arguments psk_identity, obfuscated_age and binder are taken + from 'pre_shared_key' in the ClientHello. + """ + with open(self.session_ticket_file, "rb") as f: + for line in f: + s = line.strip().split(b';') + if len(s) < 8: + continue + ticket_label = binascii.unhexlify(s[0]) + ticket_nonce = binascii.unhexlify(s[1]) + tmp = binascii.unhexlify(s[2]) + ticket_lifetime = struct.unpack("!I", tmp)[0] + tmp = binascii.unhexlify(s[3]) + ticket_age_add = struct.unpack("!I", tmp)[0] + tmp = binascii.unhexlify(s[4]) + ticket_start_time = struct.unpack("!I", tmp)[0] + resumption_secret = binascii.unhexlify(s[5]) + tmp = binascii.unhexlify(s[6]) + res_ciphersuite = struct.unpack("!H", tmp)[0] + tmp = binascii.unhexlify(s[7]) + max_early_data_size = struct.unpack("!I", tmp)[0] + + # Here psk_identity is a Ticket type but ticket_label is bytes, + # we need to convert psk_identiy to bytes in order to compare + # both strings + if psk_identity.__bytes__() == ticket_label: + + # We compute the resumed PSK associated the resumption + # secret + self.vprint("Ticket found in database !") + if res_ciphersuite not in _tls_cipher_suites_cls: + warning("Unknown cipher suite %d", res_ciphersuite) + # we do not try to set a default nor stop the execution + else: + cs_cls = _tls_cipher_suites_cls[res_ciphersuite] + + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + + tls13_psk_secret = hkdf.expand_label(resumption_secret, + b"resumption", + ticket_nonce, + hash_len) + # We verify that ticket age is not expired + agesec = int((time.time() - ticket_start_time)) + # agems = agesec * 1000 + ticket_age = (obfuscated_age - ticket_age_add) % 0xffffffff # noqa: F841, E501 + + # We verify the PSK binder + s = self.cur_session + if s.tls13_retry: + handshake_context = struct.pack("B", 254) + handshake_context += struct.pack("B", 0) + handshake_context += struct.pack("B", 0) + handshake_context += struct.pack("B", hash_len) + digest = hashes.Hash(hkdf.hash, backend=default_backend()) # noqa: E501 + digest.update(s.handshake_messages[0]) + handshake_context += digest.finalize() + for m in s.handshake_messages[1:]: + if (isinstance(TLS13ClientHello) or + isinstance(TLSClientHello)): + handshake_context += m[:-hash_len - 3] + else: + handshake_context += m + else: + handshake_context = s.handshake_messages[0][:-hash_len - 3] # noqa: E501 + + # We compute the binder key + # XXX use the compute_tls13_early_secrets() function + tls13_early_secret = hkdf.extract(None, tls13_psk_secret) + binder_key = hkdf.derive_secret(tls13_early_secret, + b"res binder", + b"") + computed_binder = hkdf.compute_verify_data(binder_key, + handshake_context) # noqa: E501 + if (agesec < ticket_lifetime and + computed_binder == binder): + self.vprint("Ticket has been accepted ! ") + self.max_early_data_size = max_early_data_size + self.resumed_ciphersuite = res_ciphersuite + return tls13_psk_secret + self.vprint("Ticket has not been accepted ! Fallback to a complete handshake") # noqa: E501 + return None + @ATMT.condition(tls13_PREPARE_SERVERFLIGHT1) def tls13_should_add_ServerHello(self): + + psk_identity = None + psk_key_exchange_mode = None + obfuscated_age = None + # XXX check ClientHello extensions... + for m in reversed(self.cur_session.handshake_messages_parsed): + if isinstance(m, (TLS13ClientHello, TLSClientHello)): + for e in m.ext: + if isinstance(e, TLS_Ext_PreSharedKey_CH): + psk_identity = e.identities[0].identity + obfuscated_age = e.identities[0].obfuscated_ticket_age + binder = e.binders[0].binder + + # For out-of-bound PSK, obfuscated_ticket_age should be + # 0. We use this field to distinguish between out-of- + # bound PSK and resumed PSK + is_out_of_band_psk = (obfuscated_age == 0) + + if isinstance(e, TLS_Ext_PSKKeyExchangeModes): + psk_key_exchange_mode = e.kxmodes[0] + if isinstance(self.mykey, PrivKeyRSA): kx = "RSA" elif isinstance(self.mykey, PrivKeyECDSA): kx = "ECDSA" + elif isinstance(self.mykey, PrivKeyEdDSA): + kx = "" usable_suites = get_usable_ciphersuites(self.cur_pkt.ciphers, kx) c = usable_suites[0] group = next(iter(self.cur_session.tls13_client_pubshares)) - ext = [TLS_Ext_SupportedVersion_SH(version="TLS 1.3"), - TLS_Ext_KeyShare_SH(server_share=KeyShareEntry(group=group))] + ext = [TLS_Ext_SupportedVersion_SH(version="TLS 1.3")] + if (psk_identity and obfuscated_age and psk_key_exchange_mode): + s = self.cur_session + if is_out_of_band_psk: + # Handshake with external PSK authentication + # XXX test that self.psk_secret is set + s.tls13_psk_secret = binascii.unhexlify(self.psk_secret) + # 0: "psk_ke" + # 1: "psk_dhe_ke" + if psk_key_exchange_mode == 1: + server_kse = KeyShareEntry(group=group) + ext += TLS_Ext_KeyShare_SH(server_share=server_kse) + ext += TLS_Ext_PreSharedKey_SH(selected_identity=0) + else: + resumption_psk = self.verify_psk_binder(psk_identity, + obfuscated_age, + binder) + if resumption_psk is None: + # We did not find a ticket matching the one provided in the + # ClientHello. We fallback to a regular 1-RTT handshake + server_kse = KeyShareEntry(group=group) + ext += [TLS_Ext_KeyShare_SH(server_share=server_kse)] + else: + # 0: "psk_ke" + # 1: "psk_dhe_ke" + if psk_key_exchange_mode == 1: + server_kse = KeyShareEntry(group=group) + ext += [TLS_Ext_KeyShare_SH(server_share=server_kse)] + + ext += [TLS_Ext_PreSharedKey_SH(selected_identity=0)] + self.cur_session.tls13_psk_secret = resumption_psk + else: + # Standard Handshake + ext += TLS_Ext_KeyShare_SH(server_share=KeyShareEntry(group=group)) if self.cur_session.sid is not None: p = TLS13ServerHello(cipher=c, sid=self.cur_session.sid, ext=ext) @@ -578,7 +841,13 @@ def tls13_should_add_ServerHello(self): @ATMT.state() def tls13_ADDED_SERVERHELLO(self): - pass + # If the client proposed a non-empty session ID in his ClientHello + # he requested the middlebox compatibility mode (RFC8446, appendix D.4) + # In this case, the server should send a dummy ChangeCipherSpec in + # between the ServerHello and the encrypted handshake messages + if self.cur_session.sid is not None: + self.add_record(is_tls12=True) + self.add_msg(TLSChangeCipherSpec()) @ATMT.condition(tls13_ADDED_SERVERHELLO) def tls13_should_add_EncryptedExtensions(self): @@ -592,6 +861,10 @@ def tls13_ADDED_ENCRYPTEDEXTENSIONS(self): @ATMT.condition(tls13_ADDED_ENCRYPTEDEXTENSIONS) def tls13_should_add_CertificateRequest(self): + if self.client_auth: + ext = [TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss"])] + p = TLS13CertificateRequest(ext=ext) + self.add_msg(p) raise self.tls13_ADDED_CERTIFICATEREQUEST() @ATMT.state() @@ -600,11 +873,15 @@ def tls13_ADDED_CERTIFICATEREQUEST(self): @ATMT.condition(tls13_ADDED_CERTIFICATEREQUEST) def tls13_should_add_Certificate(self): - certs = [] - for c in self.cur_session.server_certs: - certs += _ASN1CertAndExt(cert=c) - - self.add_msg(TLS13Certificate(certs=certs)) + # If a PSK is set, an extension pre_shared_key + # was send in the ServerHello. No certificate should + # be send here + if not self.cur_session.tls13_psk_secret: + certs = [] + for c in self.cur_session.server_certs: + certs += _ASN1CertAndExt(cert=c) + + self.add_msg(TLS13Certificate(certs=certs)) raise self.tls13_ADDED_CERTIFICATE() @ATMT.state() @@ -613,7 +890,24 @@ def tls13_ADDED_CERTIFICATE(self): @ATMT.condition(tls13_ADDED_CERTIFICATE) def tls13_should_add_CertificateVerifiy(self): - self.add_msg(TLSCertificateVerify()) + if not self.cur_session.tls13_psk_secret: + # If we have a preferred signature algorithm, and the client supports + # it, use that. + if self.cur_session.advertised_sig_algs: + usable_sigalgs = get_usable_tls13_sigalgs( + self.cur_session.advertised_sig_algs, + self.mykey, + location="certificateverify", + ) + if not usable_sigalgs: + self.vprint("No usable signature algorithm!") + raise self.CLOSE_NOTIFY() + pref_alg = self.preferred_signature_algorithm + if pref_alg in usable_sigalgs: + self.cur_session.selected_sig_alg = pref_alg + else: + self.cur_session.selected_sig_alg = usable_sigalgs[0] + self.add_msg(TLSCertificateVerify()) raise self.tls13_ADDED_CERTIFICATEVERIFY() @ATMT.state() @@ -644,10 +938,83 @@ def tls13_RECEIVED_CLIENTFLIGHT2(self): pass @ATMT.condition(tls13_RECEIVED_CLIENTFLIGHT2, prio=1) + def tls13_should_handle_ClientFlight2(self): + self.raise_on_packet(TLS13Certificate, + self.TLS13_HANDLED_CLIENTCERTIFICATE) + + @ATMT.condition(tls13_RECEIVED_CLIENTFLIGHT2, prio=2) + def tls13_should_handle_Alert_from_ClientCertificate(self): + self.raise_on_packet(TLSAlert, + self.TLS13_HANDLED_ALERT_FROM_CLIENTCERTIFICATE) + + @ATMT.state() + def TLS13_HANDLED_ALERT_FROM_CLIENTCERTIFICATE(self): + self.vprint("Received Alert message instead of ClientKeyExchange!") + self.vprint(self.cur_pkt.mysummary()) + raise self.CLOSE_NOTIFY() + + # For Middlebox compatibility (see RFC8446, appendix D.4) + # a dummy ChangeCipherSpec record can be send. In this case, + # this function just read the ChangeCipherSpec message and + # go back in a previous state continuing with the next TLS 1.3 + # record + @ATMT.condition(tls13_RECEIVED_CLIENTFLIGHT2, prio=3) + def tls13_should_handle_ClientCCS(self): + self.raise_on_packet(TLSChangeCipherSpec, + self.tls13_RECEIVED_CLIENTFLIGHT2) + + @ATMT.condition(tls13_RECEIVED_CLIENTFLIGHT2, prio=4) + def tls13_no_ClientCertificate(self): + if self.client_auth: + raise self.TLS13_MISSING_CLIENTCERTIFICATE() + self.raise_on_packet(TLSFinished, + self.TLS13_HANDLED_CLIENTFINISHED) + + # RFC8446, section 4.4.2.4 : + # "If the client does not send any certificates (i.e., it sends an empty + # Certificate message), the server MAY at its discretion either + # continue the handshake without client authentication or abort the + # handshake with a "certificate_required" alert." + # Here, we abort the handshake. + @ATMT.state() + def TLS13_HANDLED_CLIENTCERTIFICATE(self): + if self.client_auth: + self.vprint("Received client certificate chain...") + if isinstance(self.cur_pkt, TLS13Certificate): + if self.cur_pkt.certslen == 0: + self.vprint("but it's empty !") + raise self.TLS13_MISSING_CLIENTCERTIFICATE() + + @ATMT.condition(TLS13_HANDLED_CLIENTCERTIFICATE) + def tls13_should_handle_ClientCertificateVerify(self): + self.raise_on_packet(TLSCertificateVerify, + self.TLS13_HANDLED_CLIENT_CERTIFICATEVERIFY) + + @ATMT.condition(TLS13_HANDLED_CLIENTCERTIFICATE, prio=2) + def tls13_no_Client_CertificateVerify(self): + if self.client_auth: + raise self.TLS13_MISSING_CLIENTCERTIFICATE() + raise self.TLS13_HANDLED_CLIENT_CERTIFICATEVERIFY() + + @ATMT.state() + def TLS13_HANDLED_CLIENT_CERTIFICATEVERIFY(self): + pass + + @ATMT.condition(TLS13_HANDLED_CLIENT_CERTIFICATEVERIFY) def tls13_should_handle_ClientFinished(self): self.raise_on_packet(TLSFinished, self.TLS13_HANDLED_CLIENTFINISHED) + @ATMT.state() + def TLS13_MISSING_CLIENTCERTIFICATE(self): + self.vprint("Missing ClientCertificate!") + self.add_record() + self.add_msg(TLSAlert(level=2, descr=0x74)) + self.flush_records() + self.vprint("Sending TLSAlert 116") + self.socket.close() + raise self.WAITING_CLIENT() + @ATMT.state() def TLS13_HANDLED_CLIENTFINISHED(self): self.vprint("TLS handshake completed!") @@ -667,6 +1034,45 @@ def WAITING_CLIENTDATA(self): def RECEIVED_CLIENTDATA(self): pass + def save_ticket(self, ticket): + """ + This function save a ticket and others parameters in the + file given as argument to the automaton + Warning : The file is not protected and contains sensitive + information. It should be used only for testing purpose. + """ + if (not isinstance(ticket, TLS13NewSessionTicket) or + self.session_ticket_file is None): + return + + s = self.cur_session + with open(self.session_ticket_file, "ab") as f: + # ticket;ticket_nonce;obfuscated_age;start_time;resumption_secret + line = binascii.hexlify(ticket.ticket) + line += b";" + line += binascii.hexlify(ticket.ticket_nonce) + line += b";" + line += binascii.hexlify(struct.pack("!I", ticket.ticket_lifetime)) + line += b";" + line += binascii.hexlify(struct.pack("!I", ticket.ticket_age_add)) + line += b";" + line += binascii.hexlify(struct.pack("!I", int(time.time()))) + line += b";" + line += binascii.hexlify(s.tls13_derived_secrets["resumption_secret"]) # noqa: E501 + line += b";" + line += binascii.hexlify(struct.pack("!H", s.wcs.ciphersuite.val)) + line += b";" + if (ticket.ext is None or ticket.extlen is None or + ticket.extlen == 0): + line += binascii.hexlify(struct.pack("!I", 0)) + else: + for e in ticket.ext: + if isinstance(e, TLS_Ext_EarlyDataIndicationTicket): + max_size = struct.pack("!I", e.max_early_data_size) + line += binascii.hexlify(max_size) + line += b"\n" + f.write(line) + @ATMT.condition(RECEIVED_CLIENTDATA) def should_handle_ClientData(self): if not self.buffer_in: @@ -680,12 +1086,18 @@ def should_handle_ClientData(self): print("> Received: %r" % p.data) recv_data = p.data lines = recv_data.split(b"\n") - for l in lines: - if l.startswith(b"stop_server"): + for line in lines: + if line.startswith(b"stop_server"): raise self.CLOSE_NOTIFY_FINAL() elif isinstance(p, TLSAlert): print("> Received: %r" % p) raise self.CLOSE_NOTIFY() + elif isinstance(p, TLS13KeyUpdate): + print("> Received: %r" % p) + p = TLS13KeyUpdate(request_update=0) + self.add_record() + self.add_msg(p) + raise self.ADDED_SERVERDATA() else: print("> Received: %r" % p) @@ -695,6 +1107,10 @@ def should_handle_ClientData(self): if self.is_echo_server or recv_data.startswith(b"GET / HTTP/1.1"): self.add_record() self.add_msg(p) + if self.handle_session_ticket: + self.add_record() + ticket = TLS13NewSessionTicket(ext=[]) + self.add_msg(ticket) raise self.ADDED_SERVERDATA() raise self.HANDLED_CLIENTDATA() @@ -709,7 +1125,24 @@ def ADDED_SERVERDATA(self): @ATMT.condition(ADDED_SERVERDATA) def should_send_ServerData(self): + if self.session_ticket_file: + save_ticket = False + for p in self.buffer_out: + if isinstance(p, TLS13): + # Check if there's a NewSessionTicket to send + save_ticket = all(map(lambda x: isinstance(x, TLS13NewSessionTicket), # noqa: E501 + p.inner.msg)) + if save_ticket: + break self.flush_records() + if self.session_ticket_file and save_ticket: + # Loop backward in message send to retrieve the parsed + # NewSessionTicket. This message is not completely build before the + # flush_records() call. Other way to build this message before ? + for p in reversed(self.cur_session.handshake_messages_parsed): + if isinstance(p, TLS13NewSessionTicket): + self.save_ticket(p) + break raise self.SENT_SERVERDATA() @ATMT.state() @@ -746,7 +1179,7 @@ def close_session_final(self): self.flush_records() except Exception: self.vprint("Could not send termination Alert, maybe the client left?") # noqa: E501 - # We might call shutdown, but unit tests with s_client fail with this. + # We might call shutdown, but unit tests with s_client fail with this # self.socket.shutdown(1) self.socket.close() raise self.FINAL() @@ -830,8 +1263,7 @@ def SSLv2_HANDLED_CLIENTFINISHED(self): @ATMT.condition(SSLv2_HANDLED_CLIENTFINISHED, prio=1) def sslv2_should_add_ServerVerify_from_ClientFinished(self): - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if SSLv2ServerVerify in hs_msg: + if self.in_handshake(SSLv2ServerVerify): return self.add_record(is_sslv2=True) p = SSLv2ServerVerify(challenge=self.cur_session.sslv2_challenge) @@ -840,8 +1272,7 @@ def sslv2_should_add_ServerVerify_from_ClientFinished(self): @ATMT.condition(SSLv2_RECEIVED_CLIENTFINISHED, prio=2) def sslv2_should_add_ServerVerify_from_NoClientFinished(self): - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if SSLv2ServerVerify in hs_msg: + if self.in_handshake(SSLv2ServerVerify): return self.add_record(is_sslv2=True) p = SSLv2ServerVerify(challenge=self.cur_session.sslv2_challenge) @@ -868,8 +1299,7 @@ def sslv2_should_send_ServerVerify(self): @ATMT.state() def SSLv2_SENT_SERVERVERIFY(self): - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if SSLv2ClientFinished in hs_msg: + if self.in_handshake(SSLv2ClientFinished): raise self.SSLv2_HANDLED_CLIENTFINISHED() else: raise self.SSLv2_RECEIVED_CLIENTFINISHED() @@ -878,8 +1308,7 @@ def SSLv2_SENT_SERVERVERIFY(self): @ATMT.condition(SSLv2_HANDLED_CLIENTFINISHED, prio=2) def sslv2_should_add_RequestCertificate(self): - hs_msg = [type(m) for m in self.cur_session.handshake_messages_parsed] - if not self.client_auth or SSLv2RequestCertificate in hs_msg: + if not self.client_auth or self.in_handshake(SSLv2RequestCertificate): return self.add_record(is_sslv2=True) self.add_msg(SSLv2RequestCertificate(challenge=randstring(16))) @@ -982,8 +1411,8 @@ def sslv2_should_handle_ClientData(self): print("> Received: %r" % p) lines = cli_data.split(b"\n") - for l in lines: - if l.startswith(b"stop_server"): + for line in lines: + if line.startswith(b"stop_server"): raise self.SSLv2_CLOSE_NOTIFY_FINAL() if cli_data.startswith(b"GET / HTTP/1.1"): @@ -1054,7 +1483,7 @@ def sslv2_close_session_final(self): self.socket.close() raise self.FINAL() - @ATMT.state(final=True) + @ATMT.state(stop=True, final=True) def FINAL(self): self.vprint("Closing server socket...") self.serversocket.close() diff --git a/scapy/layers/tls/basefields.py b/scapy/layers/tls/basefields.py index 752b562ec1f..2862c896e0a 100644 --- a/scapy/layers/tls/basefields.py +++ b/scapy/layers/tls/basefields.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ TLS base fields, used for record parsing/building. As several operations depend @@ -10,7 +11,6 @@ import struct from scapy.fields import ByteField, ShortEnumField, ShortField, StrField -import scapy.modules.six as six from scapy.compat import orb _tls_type = {20: "change_cipher_spec", @@ -167,8 +167,10 @@ def addfield(self, pkt, s, val): return s def getfield(self, pkt, s): - if (pkt.tls_session.rcs.cipher.type != "aead" and - False in six.itervalues(pkt.tls_session.rcs.cipher.ready)): + if ( + pkt.tls_session.rcs.cipher.type != "aead" and + False in pkt.tls_session.rcs.cipher.ready.values() + ): # XXX Find a more proper way to handle the still-encrypted case return s, b"" tmp_len = pkt.tls_session.rcs.mac_len diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index 2a156e71c90..e41ca71a784 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -1,79 +1,199 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2008 Arnaud Ebalard # # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2022-2025 Gabriel Potter -""" -High-level methods for PKI objects (X.509 certificates, CRLs, asymmetric keys). -Supports both RSA and ECDSA objects. +r""" +High-level methods for PKI objects (X.509 certificates, CRLs, CSR, Keys, CMS). +Supported keys include RSA, ECDSA and EdDSA. The classes below are wrappers for the ASN.1 objects defined in x509.py. -By collecting their attributes, we bypass the ASN.1 structure, hence -there is no direct method for exporting a new full DER-encoded version -of a Cert instance after its serial has been modified (for example). -If you need to modify an import, just use the corresponding ASN1_Packet. -For instance, here is what you could do in order to modify the serial of -'cert' and then resign it with whatever 'key':: +Example 1: Certificate & Private key +____________________________________ + +For instance, here is what you could do in order to modify the subject public +key info of a 'cert' and then resign it with whatever 'key':: + + >>> from scapy.layers.tls.cert import * + >>> cert = Cert("cert.der") + >>> k = PrivKeyRSA() # generate a private key + >>> cert.setSubjectPublicKeyFromPrivateKey(k) + >>> cert.resignWith(k) + >>> cert.export("newcert.pem") + >>> k.export("mykey.pem") + +One could also edit arguments like the serial number, as such:: + + >>> from scapy.layers.tls.cert import * + >>> c = Cert("mycert.pem") + >>> c.tbsCertificate.serialNumber = 0x4B1D + >>> k = PrivKey("mykey.pem") # import an existing private key + >>> c.resignWith(k) + >>> c.export("newcert.pem") + +To export the public key of a private key:: + + >>> k = PrivKey("mykey.pem") + >>> k.pubkey.export("mypubkey.pem") + +Example 2: CertList and CertTree +________________________________ + +Load a .pem file that contains multiple certificates:: + + >>> l = CertList("ca_chain.pem") + >>> l.show() + 0000 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test CA...] + 0001 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test Client...] - f = open('cert.der') - c = X509_Cert(f.read()) - c.tbsCertificate.serialNumber = 0x4B1D - k = PrivKey('key.pem') - new_x509_cert = k.resignCert(c) +Use 'CertTree' to organize the certificates in a tree:: + + >>> tree = CertTree("ca_chain.pem") # or tree = CertTree(l) + >>> tree.show() + /C=Ulaanbaatar/OU=Scapy Test PKI/CN=Scapy Test CA [Self Signed] + /C=FR/OU=Scapy Test PKI/CN=Scapy Test Client [Not Self Signed] + +Example 3: Certificate Signing Request (CSR) +____________________________________________ + +Scapy's :py:class:`~scapy.layers.tls.cert.CSR` class supports both PKCS#10 and CMC +formats. + +Load and display a CSR:: + + >>> csr = CSR("cert.req") + >>> csr + [CSR Format: CMC, Subject:/O=TestOrg/CN=TestCN, Verified: True] + >>> csr.certReq.show() + ###[ PKCS10_CertificationRequest ]### + \certificationRequestInfo\ + |###[ PKCS10_CertificationRequestInfo ]### + | version = 0x0 + | | | value = + [...] + +Get its public key and verify its signature:: + + >>> csr.pubkey + + >>> csr.verifySelf() + True No need for obnoxious openssl tweaking anymore. :) """ -from __future__ import absolute_import -from __future__ import print_function import base64 +import enum import os import time +import warnings from scapy.config import conf, crypto_validator -import scapy.modules.six as six -from scapy.modules.six.moves import range +from scapy.compat import Self from scapy.error import warning from scapy.utils import binrepr -from scapy.asn1.asn1 import ASN1_BIT_STRING +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_NULL, + ASN1_OID, + ASN1_STRING, +) from scapy.asn1.mib import hash_by_oid -from scapy.layers.x509 import (X509_SubjectPublicKeyInfo, - RSAPublicKey, RSAPrivateKey, - ECDSAPublicKey, ECDSAPrivateKey, - RSAPrivateKey_OpenSSL, ECDSAPrivateKey_OpenSSL, - X509_Cert, X509_CRL) -from scapy.layers.tls.crypto.pkcs1 import pkcs_os2ip, _get_hash, \ - _EncryptAndVerifyRSA, _DecryptAndSignRSA -from scapy.compat import raw, bytes_encode +from scapy.packet import Packet +from scapy.layers.x509 import ( + CMS_CertificateChoices, + CMS_ContentInfo, + CMS_EncapsulatedContentInfo, + CMS_IssuerAndSerialNumber, + CMS_RevocationInfoChoice, + CMS_SignedAttrsForSignature, + CMS_SignedData, + CMS_SignerInfo, + CMS_SubjectKeyIdentifier, + ECDSAPrivateKey_OpenSSL, + ECDSAPrivateKey, + ECDSAPublicKey, + EdDSAPrivateKey, + EdDSAPublicKey, + PKCS10_CertificationRequest, + RSAPrivateKey_OpenSSL, + RSAPrivateKey, + RSAPublicKey, + X509_AlgorithmIdentifier, + X509_Attribute, + X509_AttributeValue, + X509_Cert, + X509_CRL, + X509_SubjectPublicKeyInfo, +) +from scapy.layers.tls.crypto.hash import _get_hash +from scapy.layers.tls.crypto.pkcs1 import ( + _DecryptAndSignRSA, + _EncryptAndVerifyRSA, + pkcs_os2ip, +) +from scapy.compat import bytes_encode + +# Typing imports +from typing import ( + List, + Optional, + Union, +) + if conf.crypto_valid: + from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import rsa, ec - from cryptography.hazmat.backends.openssl.ec import InvalidSignature + from cryptography.hazmat.primitives.asymmetric import rsa, ec, x25519, x448 + + # cryptography raised the minimum RSA key length to 1024 in 43.0+ + # https://github.com/pyca/cryptography/pull/10278 + # but we need still 512 for EXPORT40 ciphers (yes EXPORT is terrible) + # https://datatracker.ietf.org/doc/html/rfc2246#autoid-66 + # The following detects the change and hacks around it using the backend + + try: + rsa.generate_private_key(public_exponent=65537, key_size=512) + _RSA_512_SUPPORTED = True + except ValueError: + # cryptography > 43.0 + _RSA_512_SUPPORTED = False + from cryptography.hazmat.primitives.asymmetric.rsa import rust_openssl # Maximum allowed size in bytes for a certificate file, to avoid # loading huge file when importing a cert _MAX_KEY_SIZE = 50 * 1024 _MAX_CERT_SIZE = 50 * 1024 -_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big +_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big +_MAX_CSR_SIZE = 50 * 1024 ##################################################################### # Some helpers ##################################################################### + @conf.commands.register def der2pem(der_string, obj="UNKNOWN"): """Convert DER octet string to PEM format (with optional header)""" - # Encode a byte string in PEM format. Header advertizes type. - pem_string = ("-----BEGIN %s-----\n" % obj).encode() - base64_string = base64.b64encode(der_string) - chunks = [base64_string[i:i + 64] for i in range(0, len(base64_string), 64)] # noqa: E501 - pem_string += b'\n'.join(chunks) - pem_string += ("\n-----END %s-----\n" % obj).encode() + # Encode a byte string in PEM format. Header advertises type. + pem_string = "-----BEGIN %s-----\n" % obj + base64_string = base64.b64encode(der_string).decode() + chunks = [base64_string[i : i + 64] for i in range(0, len(base64_string), 64)] + pem_string += "\n".join(chunks) + pem_string += "\n-----END %s-----\n" % obj return pem_string @@ -102,23 +222,21 @@ def split_pem(s): if start_idx == -1: break end_idx = s.find(b"-----END") + if end_idx == -1: + raise Exception("Invalid PEM object (missing END tag)") end_idx = s.find(b"\n", end_idx) + 1 + if end_idx == 0: + # There is no final \n + end_idx = len(s) pem_strings.append(s[start_idx:end_idx]) s = s[end_idx:] return pem_strings class _PKIObj(object): - def __init__(self, frmt, der, pem): - # Note that changing attributes of the _PKIObj does not update these - # values (e.g. modifying k.modulus does not change k.der). - # XXX use __setattr__ for this + def __init__(self, frmt, der): self.frmt = frmt - self.der = der - self.pem = pem - - def __str__(self): - return self.der + self._der = der class _PKIObjMaker(type): @@ -135,7 +253,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): raise Exception(error_msg) obj_path = bytes_encode(obj_path) - if (b'\x00' not in obj_path) and os.path.isfile(obj_path): + if (b"\x00" not in obj_path) and os.path.isfile(obj_path): _size = os.path.getsize(obj_path) if _size > obj_max_size: raise Exception(error_msg) @@ -151,20 +269,15 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): if b"-----BEGIN" in _raw: frmt = "PEM" pem = _raw - der_list = split_pem(_raw) - der = b''.join(map(pem2der, der_list)) + der_list = split_pem(pem) + der = b"".join(map(pem2der, der_list)) else: frmt = "DER" der = _raw - pem = "" - if pem_marker is not None: - pem = der2pem(_raw, pem_marker) - # type identification may be needed for pem_marker - # in such case, the pem attribute has to be updated except Exception: raise Exception(error_msg) - p = _PKIObj(frmt, der, pem) + p = _PKIObj(frmt, der) return p @@ -176,13 +289,23 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): # Public Keys # ############### + class _PubKeyFactory(_PKIObjMaker): """ Metaclass for PubKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ - def __call__(cls, key_path=None): + + def __call__(cls, key_path=None, cryptography_obj=None): + # This allows to import cryptography objects directly + if cryptography_obj is not None: + obj = type.__call__(cls) + obj.__class__ = cls + obj.frmt = "original" + obj.marker = "PUBLIC KEY" + obj.pubkey = cryptography_obj + return obj if key_path is None: obj = type.__call__(cls) @@ -204,51 +327,97 @@ def __call__(cls, key_path=None): # Now for the usual calls, key_path may be the path to either: # _an X509_SubjectPublicKeyInfo, as processed by openssl; # _an RSAPublicKey; - # _an ECDSAPublicKey. + # _an ECDSAPublicKey; + # _an EdDSAPublicKey. obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) try: - spki = X509_SubjectPublicKeyInfo(obj.der) + spki = X509_SubjectPublicKeyInfo(obj._der) pubkey = spki.subjectPublicKey if isinstance(pubkey, RSAPublicKey): obj.__class__ = PubKeyRSA obj.import_from_asn1pkt(pubkey) elif isinstance(pubkey, ECDSAPublicKey): obj.__class__ = PubKeyECDSA - try: - obj.import_from_der(obj.der) - except ImportError: - pass + obj.import_from_der(obj._der) + elif isinstance(pubkey, EdDSAPublicKey): + obj.__class__ = PubKeyEdDSA + obj.import_from_der(obj._der) else: raise - marker = b"PUBLIC KEY" + obj.marker = "PUBLIC KEY" except Exception: try: - pubkey = RSAPublicKey(obj.der) + pubkey = RSAPublicKey(obj._der) obj.__class__ = PubKeyRSA obj.import_from_asn1pkt(pubkey) - marker = b"RSA PUBLIC KEY" + obj.marker = "RSA PUBLIC KEY" except Exception: # We cannot import an ECDSA public key without curve knowledge + if conf.debug_dissector: + raise raise Exception("Unable to import public key") - - if obj.frmt == "DER": - obj.pem = der2pem(obj.der, marker) return obj -class PubKey(six.with_metaclass(_PubKeyFactory, object)): +class PubKey(metaclass=_PubKeyFactory): """ - Parent class for both PubKeyRSA and PubKeyECDSA. - Provides a common verifyCert() method. + Parent class for PubKeyRSA, PubKeyECDSA and PubKeyEdDSA. + Provides common verifyCert() and export() methods. """ def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ + """Verifies either a Cert or an X509_Cert.""" + h = _get_cert_sig_hashname(cert) tbsCert = cert.tbsCertificate - sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + sigVal = bytes(cert.signatureValue) + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") + + def verifyCsr(self, csr): + """Verifies a CSR.""" + h = _get_csr_sig_hashname(csr) + certReqInfo = csr.certReq.certificationRequestInfo + sigVal = bytes(csr.certReq.signature) + return self.verify(bytes(certReqInfo), sigVal, h=h, t="pkcs") + + @property + def pem(self): + return der2pem(self.der, self.marker) + + @property + def der(self): + return self.pubkey.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + def public_numbers(self, *args, **kwargs): + return self.pubkey.public_numbers(*args, **kwargs) + + @property + def key_size(self): + return self.pubkey.key_size + + def export(self, filename, fmt=None): + """ + Export public key in 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError class PubKeyRSA(PubKey, _EncryptAndVerifyRSA): @@ -256,14 +425,24 @@ class PubKeyRSA(PubKey, _EncryptAndVerifyRSA): Wrapper for RSA keys based on _EncryptAndVerifyRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None): pubExp = pubExp or 65537 if not modulus: real_modulusLen = modulusLen or 2048 - private_key = rsa.generate_private_key(public_exponent=pubExp, - key_size=real_modulusLen, - backend=default_backend()) + if real_modulusLen < 1024 and not _RSA_512_SUPPORTED: + # cryptography > 43.0 compatibility + private_key = rust_openssl.rsa.generate_private_key( + public_exponent=pubExp, + key_size=real_modulusLen, + ) + else: + private_key = rsa.generate_private_key( + public_exponent=pubExp, + key_size=real_modulusLen, + backend=default_backend(), + ) self.pubkey = private_key.public_key() else: real_modulusLen = len(binrepr(modulus)) @@ -271,6 +450,9 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None): warning("modulus and modulusLen do not match!") pubNum = rsa.RSAPublicNumbers(n=modulus, e=pubExp) self.pubkey = pubNum.public_key(default_backend()) + + self.marker = "PUBLIC KEY" + # Lines below are only useful for the legacy part of pkcs1.py pubNum = self.pubkey.public_numbers() self._modulusLen = real_modulusLen @@ -286,10 +468,6 @@ def import_from_tuple(self, tup): if isinstance(e, bytes): e = pkcs_os2ip(e) self.fill_and_store(modulus=m, pubExp=e) - self.pem = self.pubkey.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo) - self.der = pem2der(self.pem) def import_from_asn1pkt(self, pubkey): modulus = pubkey.modulus.val @@ -301,8 +479,7 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): return _EncryptAndVerifyRSA.encrypt(self, msg, t=t, h=h, mgf=mgf, L=L) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): - return _EncryptAndVerifyRSA.verify( - self, msg, sig, t=t, h=h, mgf=mgf, L=L) + return _EncryptAndVerifyRSA.verify(self, msg, sig, t=t, h=h, mgf=mgf, L=L) class PubKeyECDSA(PubKey): @@ -310,6 +487,7 @@ class PubKeyECDSA(PubKey): Wrapper for ECDSA keys based on the cryptography library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 @@ -319,11 +497,12 @@ def fill_and_store(self, curve=None): @crypto_validator def import_from_der(self, pubkey): # No lib support for explicit curves nor compressed points. - self.pubkey = serialization.load_der_public_key(pubkey, - backend=default_backend()) # noqa: E501 + self.pubkey = serialization.load_der_public_key( + pubkey, + backend=default_backend(), + ) def encrypt(self, msg, h="sha256", **kwargs): - # cryptography lib does not support ECDSA encryption raise Exception("No ECDSA encryption support") @crypto_validator @@ -336,17 +515,51 @@ def verify(self, msg, sig, h="sha256", **kwargs): return False +class PubKeyEdDSA(PubKey): + """ + Wrapper for EdDSA keys based on the cryptography library. + Use the 'key' attribute to access original object. + """ + + @crypto_validator + def fill_and_store(self, curve=None): + curve = curve or x25519.X25519PrivateKey + private_key = curve.generate() + self.pubkey = private_key.public_key() + + @crypto_validator + def import_from_der(self, pubkey): + self.pubkey = serialization.load_der_public_key( + pubkey, + backend=default_backend(), + ) + + def encrypt(self, msg, **kwargs): + raise Exception("No EdDSA encryption support") + + @crypto_validator + def verify(self, msg, sig, **kwargs): + # 'sig' should be a DER-encoded signature, as per RFC 3279 + try: + self.pubkey.verify(sig, msg) + return True + except InvalidSignature: + return False + + ################ # Private Keys # ################ + class _PrivKeyFactory(_PKIObjMaker): """ Metaclass for PrivKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ - def __call__(cls, key_path=None): + + def __call__(cls, key_path=None, cryptography_obj=None): """ key_path may be the path to either: _an RSAPrivateKey_OpenSSL (as generated by openssl); @@ -354,7 +567,19 @@ def __call__(cls, key_path=None): _an RSAPrivateKey; _an ECDSAPrivateKey. """ - if key_path is None: + # This allows to import cryptography objects directly + if cryptography_obj is not None: + # We (stupidly) need to go through the whole import process because RSA + # does more than just importing the cryptography objects... + obj = _PKIObj( + "DER", + cryptography_obj.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ), + ) + elif key_path is None: obj = type.__call__(cls) if cls is PrivKey: cls = PrivKeyECDSA @@ -362,58 +587,59 @@ def __call__(cls, key_path=None): obj.frmt = "original" obj.fill_and_store() return obj + else: + # Load from file + obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) - obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) - multiPEM = False try: - privkey = RSAPrivateKey_OpenSSL(obj.der) + privkey = RSAPrivateKey_OpenSSL(obj._der) privkey = privkey.privateKey obj.__class__ = PrivKeyRSA - marker = b"PRIVATE KEY" + obj.marker = "PRIVATE KEY" except Exception: try: - privkey = ECDSAPrivateKey_OpenSSL(obj.der) + privkey = ECDSAPrivateKey_OpenSSL(obj._der) privkey = privkey.privateKey obj.__class__ = PrivKeyECDSA - marker = b"EC PRIVATE KEY" - multiPEM = True + obj.marker = "EC PRIVATE KEY" except Exception: try: - privkey = RSAPrivateKey(obj.der) + privkey = RSAPrivateKey(obj._der) obj.__class__ = PrivKeyRSA - marker = b"RSA PRIVATE KEY" + obj.marker = "RSA PRIVATE KEY" except Exception: try: - privkey = ECDSAPrivateKey(obj.der) + privkey = ECDSAPrivateKey(obj._der) obj.__class__ = PrivKeyECDSA - marker = b"EC PRIVATE KEY" + obj.marker = "EC PRIVATE KEY" except Exception: - raise Exception("Unable to import private key") + try: + privkey = EdDSAPrivateKey(obj._der) + obj.__class__ = PrivKeyEdDSA + obj.marker = "PRIVATE KEY" + except Exception: + raise Exception("Unable to import private key") try: obj.import_from_asn1pkt(privkey) except ImportError: pass - - if obj.frmt == "DER": - if multiPEM: - # this does not restore the EC PARAMETERS header - obj.pem = der2pem(raw(privkey), marker) - else: - obj.pem = der2pem(obj.der, marker) return obj class _Raw_ASN1_BIT_STRING(ASN1_BIT_STRING): """A ASN1_BIT_STRING that ignores BER encoding""" + def __bytes__(self): return self.val_readable + __str__ = __bytes__ -class PrivKey(six.with_metaclass(_PrivKeyFactory, object)): +class PrivKey(metaclass=_PrivKeyFactory): """ - Parent class for both PrivKeyRSA and PrivKeyECDSA. - Provides common signTBSCert() and resignCert() methods. + Parent class for PrivKeyRSA, PrivKeyECDSA and PrivKeyEdDSA. + Provides common signTBSCert(), resignCert(), verifyCert() + and export() methods. """ def signTBSCert(self, tbsCert, h="sha256"): @@ -432,7 +658,7 @@ def signTBSCert(self, tbsCert, h="sha256"): """ sigAlg = tbsCert.signature h = h or hash_by_oid[sigAlg.algorithm.val] - sigVal = self.sign(raw(tbsCert), h=h, t='pkcs') + sigVal = self.sign(bytes(tbsCert), h=h, t="pkcs") c = X509_Cert() c.tbsCertificate = tbsCert c.signatureAlgorithm = sigAlg @@ -440,56 +666,132 @@ def signTBSCert(self, tbsCert, h="sha256"): return c def resignCert(self, cert): - """ Rewrite the signature of either a Cert or an X509_Cert. """ - return self.signTBSCert(cert.tbsCertificate) + """Rewrite the signature of either a Cert or an X509_Cert.""" + return self.signTBSCert(cert.tbsCertificate, h=None) def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ - tbsCert = cert.tbsCertificate - sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + """Verifies either a Cert or an X509_Cert.""" + return self.pubkey.verifyCert(cert) + + def verifyCsr(self, cert): + """Verifies either a CSR.""" + return self.pubkey.verifyCsr(cert) + + @property + def pem(self): + return der2pem(self.der, self.marker) + + @property + def der(self): + return self.key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + def export(self, filename, fmt=None): + """ + Export private key in 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + @crypto_validator + def sign(self, data, h="sha256", **kwargs): + """ + Sign data. + """ + raise NotImplementedError -class PrivKeyRSA(PrivKey, _EncryptAndVerifyRSA, _DecryptAndSignRSA): + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError + + +class PrivKeyRSA(PrivKey, _DecryptAndSignRSA): """ Wrapper for RSA keys based on _DecryptAndSignRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator - def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, - prime1=None, prime2=None, coefficient=None, - exponent1=None, exponent2=None, privExp=None): + def fill_and_store( + self, + modulus=None, + modulusLen=None, + pubExp=None, + prime1=None, + prime2=None, + coefficient=None, + exponent1=None, + exponent2=None, + privExp=None, + ): pubExp = pubExp or 65537 - if None in [modulus, prime1, prime2, coefficient, privExp, - exponent1, exponent2]: + if None in [ + modulus, + prime1, + prime2, + coefficient, + privExp, + exponent1, + exponent2, + ]: # note that the library requires every parameter # in order to call RSAPrivateNumbers(...) # if one of these is missing, we generate a whole new key real_modulusLen = modulusLen or 2048 - self.key = rsa.generate_private_key(public_exponent=pubExp, - key_size=real_modulusLen, - backend=default_backend()) - self.pubkey = self.key.public_key() + if real_modulusLen < 1024 and not _RSA_512_SUPPORTED: + # cryptography > 43.0 compatibility + self.key = rust_openssl.rsa.generate_private_key( + public_exponent=pubExp, + key_size=real_modulusLen, + ) + else: + self.key = rsa.generate_private_key( + public_exponent=pubExp, + key_size=real_modulusLen, + backend=default_backend(), + ) + pubkey = self.key.public_key() else: real_modulusLen = len(binrepr(modulus)) if modulusLen and real_modulusLen != modulusLen: warning("modulus and modulusLen do not match!") pubNum = rsa.RSAPublicNumbers(n=modulus, e=pubExp) - privNum = rsa.RSAPrivateNumbers(p=prime1, q=prime2, - dmp1=exponent1, dmq1=exponent2, - iqmp=coefficient, d=privExp, - public_numbers=pubNum) + privNum = rsa.RSAPrivateNumbers( + p=prime1, + q=prime2, + dmp1=exponent1, + dmq1=exponent2, + iqmp=coefficient, + d=privExp, + public_numbers=pubNum, + ) self.key = privNum.private_key(default_backend()) - self.pubkey = self.key.public_key() + pubkey = self.key.public_key() + + self.marker = "PRIVATE KEY" # Lines below are only useful for the legacy part of pkcs1.py - pubNum = self.pubkey.public_numbers() + pubNum = pubkey.public_numbers() self._modulusLen = real_modulusLen self._modulus = pubNum.n self._pubExp = pubNum.e + self.pubkey = PubKeyRSA((pubNum.e, pubNum.n, real_modulusLen)) + def import_from_asn1pkt(self, privkey): modulus = privkey.modulus.val pubExp = privkey.publicExponent.val @@ -499,15 +801,26 @@ def import_from_asn1pkt(self, privkey): exponent1 = privkey.exponent1.val exponent2 = privkey.exponent2.val coefficient = privkey.coefficient.val - self.fill_and_store(modulus=modulus, pubExp=pubExp, - privExp=privExp, prime1=prime1, prime2=prime2, - exponent1=exponent1, exponent2=exponent2, - coefficient=coefficient) + self.fill_and_store( + modulus=modulus, + pubExp=pubExp, + privExp=privExp, + prime1=prime1, + prime2=prime2, + exponent1=exponent1, + exponent2=exponent2, + coefficient=coefficient, + ) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): - # Let's copy this from PubKeyRSA instead of adding another baseclass :) - return _EncryptAndVerifyRSA.verify( - self, msg, sig, t=t, h=h, mgf=mgf, L=L) + return self.pubkey.verify( + msg=msg, + sig=sig, + t=t, + h=h, + mgf=mgf, + L=L, + ) def sign(self, data, t="pkcs", h="sha256", mgf=None, L=None): return _DecryptAndSignRSA.sign(self, data, t=t, h=h, mgf=mgf, L=L) @@ -518,54 +831,115 @@ class PrivKeyECDSA(PrivKey): Wrapper for ECDSA keys based on SigningKey from ecdsa library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 self.key = ec.generate_private_key(curve(), default_backend()) - self.pubkey = self.key.public_key() + self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) + self.marker = "EC PRIVATE KEY" @crypto_validator def import_from_asn1pkt(self, privkey): - self.key = serialization.load_der_private_key(raw(privkey), None, - backend=default_backend()) # noqa: E501 - self.pubkey = self.key.public_key() + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) + self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) + self.marker = "EC PRIVATE KEY" @crypto_validator def verify(self, msg, sig, h="sha256", **kwargs): - # 'sig' should be a DER-encoded signature, as per RFC 3279 - try: - self.pubkey.verify(sig, msg, ec.ECDSA(_get_hash(h))) - return True - except InvalidSignature: - return False + return self.pubkey.verify(msg=msg, sig=sig, h=h, **kwargs) @crypto_validator def sign(self, data, h="sha256", **kwargs): return self.key.sign(data, ec.ECDSA(_get_hash(h))) +class PrivKeyEdDSA(PrivKey): + """ + Wrapper for EdDSA keys + Use the 'key' attribute to access original object. + """ + + @crypto_validator + def fill_and_store(self, curve=None): + curve = curve or x25519.X25519PrivateKey + self.key = curve.generate() + self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) + self.marker = "PRIVATE KEY" + + @crypto_validator + def import_from_asn1pkt(self, privkey): + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) + self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) + self.marker = "PRIVATE KEY" + + @crypto_validator + def verify(self, msg, sig, **kwargs): + return self.pubkey.verify(msg=msg, sig=sig, **kwargs) + + @crypto_validator + def sign(self, data, **kwargs): + return self.key.sign(data) + + ################ # Certificates # ################ + class _CertMaker(_PKIObjMaker): """ Metaclass for Cert creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ - def __call__(cls, cert_path): - obj = _PKIObjMaker.__call__(cls, cert_path, - _MAX_CERT_SIZE, "CERTIFICATE") + + def __call__(cls, cert_path=None, cryptography_obj=None): + # This allows to import cryptography objects directly + if cryptography_obj is not None: + obj = _PKIObj( + "DER", + cryptography_obj.public_bytes( + encoding=serialization.Encoding.DER, + ), + ) + else: + # Load from file + obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CERT_SIZE, "CERTIFICATE") obj.__class__ = Cert + obj.marker = "CERTIFICATE" try: - cert = X509_Cert(obj.der) + cert = X509_Cert(obj._der) except Exception: + if conf.debug_dissector: + raise raise Exception("Unable to import certificate") obj.import_from_asn1pkt(cert) return obj -class Cert(six.with_metaclass(_CertMaker, object)): +def _get_cert_sig_hashname(cert): + """ + Return the hash associated with the signature algorithm of a certificate. + """ + tbsCert = cert.tbsCertificate + sigAlg = tbsCert.signature + return hash_by_oid[sigAlg.algorithm.val] + + +def _get_csr_sig_hashname(csr): + """ + Return the hash associated with the signature algorithm of a CSR. + """ + certReq = csr.certReq + sigAlg = certReq.signatureAlgorithm + return hash_by_oid[sigAlg.algorithm.val] + + +class Cert(metaclass=_CertMaker): """ Wrapper for the X509_Cert from layers/x509.py. Use the 'x509Cert' attribute to access original object. @@ -577,7 +951,6 @@ def import_from_asn1pkt(self, cert): self.x509Cert = cert tbsCert = cert.tbsCertificate - self.tbsCertificate = tbsCert if tbsCert.version: self.version = tbsCert.version.val + 1 @@ -594,28 +967,20 @@ def import_from_asn1pkt(self, cert): self.authorityKeyID = None self.notBefore_str = tbsCert.validity.not_before.pretty_time - notBefore = tbsCert.validity.not_before.val - if notBefore[-1] == "Z": - notBefore = notBefore[:-1] try: - _format = tbsCert.validity.not_before._format - self.notBefore = time.strptime(notBefore, _format) - except Exception: + self.notBefore = tbsCert.validity.not_before.datetime.timetuple() + except ValueError: raise Exception(error_msg) self.notBefore_str_simple = time.strftime("%x", self.notBefore) self.notAfter_str = tbsCert.validity.not_after.pretty_time - notAfter = tbsCert.validity.not_after.val - if notAfter[-1] == "Z": - notAfter = notAfter[:-1] try: - _format = tbsCert.validity.not_after._format - self.notAfter = time.strptime(notAfter, _format) - except Exception: + self.notAfter = tbsCert.validity.not_after.datetime.timetuple() + except ValueError: raise Exception(error_msg) self.notAfter_str_simple = time.strftime("%x", self.notAfter) - self.pubKey = PubKey(raw(tbsCert.subjectPublicKeyInfo)) + self.pubkey = PubKey(bytes(tbsCert.subjectPublicKeyInfo)) if tbsCert.extensions: for extn in tbsCert.extensions: @@ -630,10 +995,10 @@ def import_from_asn1pkt(self, cert): elif extn.extnID.oidname == "authorityKeyIdentifier": self.authorityKeyID = extn.extnValue.keyIdentifier.val - self.signatureValue = raw(cert.signatureValue) + self.signatureValue = bytes(cert.signatureValue) self.signatureLen = len(self.signatureValue) - def isIssuerCert(self, other): + def isIssuer(self, other): """ True if 'other' issued 'self', i.e.: - self.issuer == other.subject @@ -641,7 +1006,10 @@ def isIssuerCert(self, other): """ if self.issuer_hash != other.subject_hash: return False - return other.pubKey.verifyCert(self) + return other.pubkey.verifyCert(self) + + def isIssuerCert(self, other): + return self.isIssuer(other) def isSelfSigned(self): """ @@ -650,15 +1018,43 @@ def isSelfSigned(self): - the signature of the certificate is valid. """ if self.issuer_hash == self.subject_hash: - return self.isIssuerCert(self) + return self.isIssuer(self) return False def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): # no ECDSA *encryption* support, hence only RSA specific keywords here - return self.pubKey.encrypt(msg, t=t, h=h, mgf=mgf, L=L) + return self.pubkey.encrypt(msg, t=t, h=h, mgf=mgf, L=L) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): - return self.pubKey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) + return self.pubkey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) + + def getCertSignatureHash(self): + """ + Return the hash cryptography object used by the 'signatureAlgorithm' + """ + return _get_hash(_get_cert_sig_hashname(self)) + + def setSubjectPublicKeyFromPrivateKey(self, key): + """ + Replace the subjectPublicKeyInfo of this certificate with the one from + the provided key. + """ + if isinstance(key, (PubKey, PrivKey)): + if isinstance(key, PrivKey): + pubkey = key.pubkey + else: + pubkey = key + self.tbsCertificate.subjectPublicKeyInfo = X509_SubjectPublicKeyInfo( + pubkey.der + ) + else: + raise ValueError("Unknown type 'key', should be PubKey or PrivKey") + + def resignWith(self, key): + """ + Resign a certificate with a specific key + """ + self.import_from_asn1pkt(key.resignCert(self)) def remainingDays(self, now=None): """ @@ -684,17 +1080,17 @@ def remainingDays(self, now=None): now = time.localtime() elif isinstance(now, str): try: - if '/' in now: - now = time.strptime(now, '%m/%d/%y') + if "/" in now: + now = time.strptime(now, "%m/%d/%y") else: - now = time.strptime(now, '%b %d %H:%M:%S %Y %Z') + now = time.strptime(now, "%b %d %H:%M:%S %Y %Z") except Exception: - warning("Bad time string provided, will use localtime() instead.") # noqa: E501 + warning("Bad time string provided, will use localtime() instead.") now = time.localtime() now = time.mktime(now) nft = time.mktime(self.notAfter) - diff = (nft - now) / (24. * 3600) + diff = (nft - now) / (24.0 * 3600) return diff def isRevoked(self, crl_list): @@ -714,23 +1110,61 @@ def isRevoked(self, crl_list): Cert. Otherwise, the issuers are simply compared. """ for c in crl_list: - if (self.authorityKeyID is not None and - c.authorityKeyID is not None and - self.authorityKeyID == c.authorityKeyID): + if ( + self.authorityKeyID is not None + and c.authorityKeyID is not None + and self.authorityKeyID == c.authorityKeyID + ): return self.serial in (x[0] for x in c.revoked_cert_serials) elif self.issuer == c.issuer: return self.serial in (x[0] for x in c.revoked_cert_serials) return False - def export(self, filename, fmt="DER"): + @property + def tbsCertificate(self): + return self.x509Cert.tbsCertificate + + @property + def pem(self): + return der2pem(self.der, self.marker) + + @property + def der(self): + return bytes(self.x509Cert) + + @property + def pubKey(self): + warnings.warn( + "Cert.pubKey is deprecated and will be removed in a future version. " + "Use Cert.pubkey", + DeprecationWarning, + ) + return self.pubkey + + @property + def extensions(self): + return self.tbsCertificate.extensions + + def __eq__(self, other): + return self.der == other.der + + def __hash__(self): + return hash(self.der) + + def export(self, filename, fmt=None): """ Export certificate in 'fmt' format (DER or PEM) to file 'filename' """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" with open(filename, "wb") as f: if fmt == "DER": - f.write(self.der) + return f.write(self.der) elif fmt == "PEM": - f.write(self.pem) + return f.write(self.pem.encode()) def show(self): print("Serial: %s" % self.serial) @@ -739,30 +1173,35 @@ def show(self): print("Validity: %s to %s" % (self.notBefore_str, self.notAfter_str)) def __repr__(self): - return "[X.509 Cert. Subject:%s, Issuer:%s]" % (self.subject_str, self.issuer_str) # noqa: E501 + return "[X.509 Cert. Subject:%s, Issuer:%s]" % ( + self.subject_str, + self.issuer_str, + ) ################################ # Certificate Revocation Lists # ################################ + class _CRLMaker(_PKIObjMaker): """ Metaclass for CRL creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ + def __call__(cls, cert_path): obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CRL_SIZE, "X509 CRL") obj.__class__ = CRL try: - crl = X509_CRL(obj.der) + crl = X509_CRL(obj._der) except Exception: raise Exception("Unable to import CRL") obj.import_from_asn1pkt(crl) return obj -class CRL(six.with_metaclass(_CRLMaker, object)): +class CRL(metaclass=_CRLMaker): """ Wrapper for the X509_CRL from layers/x509.py. Use the 'x509CRL' attribute to access original object. @@ -774,7 +1213,7 @@ def import_from_asn1pkt(self, crl): self.x509CRL = crl tbsCertList = crl.tbsCertList - self.tbsCertList = raw(tbsCertList) + self.tbsCertList = bytes(tbsCertList) if tbsCertList.version: self.version = tbsCertList.version.val + 1 @@ -827,18 +1266,18 @@ def import_from_asn1pkt(self, crl): revoked.append((serial, date)) self.revoked_cert_serials = revoked - self.signatureValue = raw(crl.signatureValue) + self.signatureValue = bytes(crl.signatureValue) self.signatureLen = len(self.signatureValue) - def isIssuerCert(self, other): + def isIssuer(self, other): # This is exactly the same thing as in Cert method. if self.issuer_hash != other.subject_hash: return False - return other.pubKey.verifyCert(self) + return other.pubkey.verifyCert(self) def verify(self, anchors): # Return True iff the CRL is signed by one of the provided anchors. - return any(self.isIssuerCert(a) for a in anchors) + return any(self.isIssuer(a) for a in anchors) def show(self): print("Version: %d" % self.version) @@ -848,140 +1287,713 @@ def show(self): print("nextUpdate: %s" % self.nextUpdate_str) +############################### +# Certificate Signing Request # +############################### + + +class _CSRMaker(_PKIObjMaker): + """ + Metaclass for CSR creation. It is not necessary as it was for the keys, + but we reuse the model instead of creating redundant constructors. + """ + + def __call__(cls, cert_path): + obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CSR_SIZE) + obj.__class__ = CSR + try: + # PKCS#10 format + csr = PKCS10_CertificationRequest(obj._der) + obj.marker = "NEW CERTIFICATE REQUEST" + obj.fmt = CSR.FORMAT.PKCS10 + except Exception: + try: + # CMC format + csr = CMS_ContentInfo(obj._der) + obj.marker = "NEW CERTIFICATE REQUEST" + obj.fmt = CSR.FORMAT.CMC + except Exception: + raise Exception("Unable to import CSR") + + obj.import_from_asn1pkt(csr) + return obj + + +class CSR(metaclass=_CSRMaker): + """ + Wrapper for the CSR formats. + This can handle both PKCS#10 and CMC formats. + """ + + class FORMAT(enum.Enum): + """ + The format used by the CSR. + """ + + PKCS10 = "PKCS#10" + CMC = "CMC" + + def import_from_asn1pkt(self, csr): + self.csr = csr + certReqInfo = self.certReq.certificationRequestInfo + + # Subject + self.subject = certReqInfo.get_subject() + self.subject_str = certReqInfo.get_subject_str() + self.subject_hash = hash(self.subject_str) + + # pubkey + self.pubkey = PubKey(bytes(certReqInfo.subjectPublicKeyInfo)) + + # Get the "subjectKeyIdentifier" from the "extensionRequest" attribute + try: + extReq = next( + x.values[0].value + for x in certReqInfo.attributes + if x.type.val == "1.2.840.113549.1.9.14" # extKeyUsage + ) + self.sid = next( + x.extnValue.keyIdentifier + for x in extReq.extensions + if x.extnID.val == "2.5.29.14" # subjectKeyIdentifier + ) + except StopIteration: + self.sid = None + + @property + def certReq(self): + csr = self.csr + + if self.fmt == CSR.FORMAT.PKCS10: + return csr + elif self.fmt == CSR.FORMAT.CMC: + if ( + csr.contentType.oidname != "id-signedData" + or csr.content.encapContentInfo.eContentType.oidname != "id-cct-PKIData" + ): + raise ValueError("Invalid CMC wrapping !") + req = csr.content.encapContentInfo.eContent.reqSequence[0] + return req.request.certificationRequest + else: + raise ValueError("Invalid CSR format !") + + @property + def pem(self): + return der2pem(self.der, self.marker) + + @property + def der(self): + return bytes(self.csr) + + def __eq__(self, other): + return self.der == other.der + + def __hash__(self): + return hash(self.der) + + def isIssuer(self, other): + return other.sid == self.sid + + def isSelfSigned(self): + return True + + def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): + return self.pubkey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) + + def export(self, filename, fmt=None): + """ + Export certificate in 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + + def show(self): + certReqInfo = self.certReq.certificationRequestInfo + + print("Subject: " + self.subject_str) + print("Attributes:") + for attr in certReqInfo.attributes: + print(" - %s" % attr.type.oidname) + + def verifySelf(self) -> bool: + """ + Verify the signatures of the CSR + """ + if self.fmt == self.FORMAT.CMC: + try: + cms_engine = CMS_Engine([self]) + cms_engine.verify(self.csr) + return self.pubkey.verifyCsr(self) + except ValueError: + return False + elif self.fmt == self.FORMAT.PKCS10: + return self.pubkey.verifyCsr(self) + else: + return False + + def __repr__(self): + return "[CSR Format: %s, Subject:%s, Verified: %s]" % ( + self.fmt.value, + self.subject_str, + self.verifySelf(), + ) + + +#################### +# Certificate list # +#################### + + +class CertList(list): + """ + An object that can store a list of Cert objects, load them and export them + into DER/PEM format. + """ + + def __init__( + self, + certList: Union[Self, List[Cert], List[CSR], Cert, str], + ): + """ + Construct a list of certificates/CRLs to be used as list of ROOT certificates. + """ + # Parse the certificate list / CA + if isinstance(certList, str): + # It's a path. First get the _PKIObj + obj = _PKIObjMaker.__call__( + CertList, certList, _MAX_CERT_SIZE, "CERTIFICATE" + ) + + # Then parse the der until there's nothing left + certList = [] + payload = obj._der + while payload: + cert = X509_Cert(payload) + if conf.raw_layer in cert.payload: + payload = cert.payload.load + else: + payload = None + cert.remove_payload() + certList.append(Cert(cert)) + + self.frmt = obj.frmt + elif isinstance(certList, Cert): + certList = [certList] + self.frmt = "PEM" + else: + self.frmt = "PEM" + + super(CertList, self).__init__(certList) + + def findCertBySid(self, sid): + """ + Find a certificate in the list by SubjectIDentifier. + """ + for cert in self: + if isinstance(cert, Cert) and isinstance(sid, CMS_IssuerAndSerialNumber): + if cert.issuer == sid.get_issuer(): + return cert + elif isinstance(cert, CSR) and isinstance(sid, CMS_SubjectKeyIdentifier): + if cert.sid == sid.sid: + return cert + raise KeyError("Certificate not found !") + + def export(self, filename, fmt=None): + """ + Export a list of certificates 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + + @property + def der(self): + return b"".join(x.der for x in self) + + @property + def pem(self): + return "".join(x.pem for x in self) + + def __repr__(self): + return "" % (len(self),) + + def show(self): + for i, c in enumerate(self): + print(conf.color_theme.id(i, fmt="%04i"), end=" ") + print(repr(c)) + + ###################### # Certificate chains # ###################### -class Chain(list): + +class CertTree(CertList): """ - Basically, an enhanced array of Cert. + An extension to CertList that additionally has a list of ROOT CAs + that are trusted. + + Example:: + + >>> tree = CertTree("ca_chain.pem") + >>> tree.show() + /CN=DOMAIN-DC1-CA/dc=DOMAIN [Self Signed] + /CN=Administrator/dc=DOMAIN [Not Self Signed] """ - def __init__(self, certList, cert0=None): + __slots__ = ["frmt", "rootCAs"] + + def __init__( + self, + certList: Union[List[Cert], CertList, str], + rootCAs: Union[List[Cert], CertList, Cert, str, None] = None, + ): """ - Construct a chain of certificates starting with a self-signed - certificate (or any certificate submitted by the user) - and following issuer/subject matching and signature validity. - If there is exactly one chain to be constructed, it will be, - but if there are multiple potential chains, there is no guarantee - that the retained one will be the longest one. - As Cert and CRL classes both share an isIssuerCert() method, - the trailing element of a Chain may alternatively be a CRL. + Construct a chain of certificates that follows issuer/subject matching and + respects signature validity. Note that we do not check AKID/{SKID/issuer/serial} matching, nor the presence of keyCertSign in keyUsage extension (if present). + + :param certList: a list of Cert/CRL objects (or path to PEM/DER file containing + multiple certs/CRL) to try to chain. + :param rootCAs: (optional) a list of certificates to trust. If not provided, + trusts any self-signed certificates from the certList. """ - list.__init__(self, ()) - if cert0: - self.append(cert0) + # Parse the certificate list + certList = CertList(certList) + + # Find the ROOT CAs if store isn't specified + if not rootCAs: + # Build cert store. + self.rootCAs = CertList([x for x in certList if x.isSelfSigned()]) + # And remove those certs from the list + for cert in self.rootCAs: + certList.remove(cert) else: - for root_candidate in certList: - if root_candidate.isSelfSigned(): - self.append(root_candidate) - certList.remove(root_candidate) - break - - if len(self) > 0: - while certList: - tmp_len = len(self) - for c in certList: - if c.isIssuerCert(self[-1]): - self.append(c) - certList.remove(c) - break - if len(self) == tmp_len: - # no new certificate appended to self - break - - def verifyChain(self, anchors, untrusted=None): - """ - Perform verification of certificate chains for that certificate. - A list of anchors is required. The certificates in the optional - untrusted list may be used as additional elements to the final chain. - On par with chain instantiation, only one chain constructed with the - untrusted candidates will be retained. Eventually, dates are checked. - """ - untrusted = untrusted or [] - for a in anchors: - chain = Chain(self + untrusted, a) - if len(chain) == 1: # anchor only - continue - # check that the chain does not exclusively rely on untrusted - if any(c in chain[1:] for c in self): - for c in chain: - if c.remainingDays() < 0: - break - if c is chain[-1]: # we got to the end of the chain - return chain - return None - - def verifyChainFromCAFile(self, cafile, untrusted_file=None): - """ - Does the same job as .verifyChain() but using the list of anchors - from the cafile. As for .verifyChain(), a list of untrusted - certificates can be passed (as a file, this time). - """ - try: - with open(cafile, "rb") as f: - ca_certs = f.read() - except Exception: - raise Exception("Could not read from cafile") + # Store cert store. + self.rootCAs = CertList(rootCAs) + # And remove those certs from the list if present (remove dups) + for cert in self.rootCAs: + if cert in certList: + certList.remove(cert) - anchors = [Cert(c) for c in split_pem(ca_certs)] + # Append our root CAs to the certList + certList.extend(self.rootCAs) - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] + # Super instantiate + super(CertTree, self).__init__(certList) - return self.verifyChain(anchors, untrusted) + @property + def tree(self): + """ + Get a tree-like object of the certificate list + """ + # We store the tree object as a dictionary that contains children. + tree = [(x, []) for x in self.rootCAs] + + # We'll empty this list eventually + certList = list(self) + + # We make a list of certificates we have to search children for, and iterate + # through it until it's empty. + todo = list(tree) + + # Iterate + while todo: + cert, children = todo.pop() + for c in certList: + # Check if this certificate matches the one we're looking at + if c.isIssuer(cert) and c != cert: + item = (c, []) + children.append(item) + certList.remove(c) + todo.append(item) + + return tree + + def getchain(self, cert): + """ + Return a chain of certificate that points from a ROOT CA to a certificate. + """ - def verifyChainFromCAPath(self, capath, untrusted_file=None): + def _rec_getchain(chain, curtree): + # See if an element of the current tree signs the cert, if so add it to + # the chain, else recurse. + for c, subtree in curtree: + curchain = chain + [c] + # If 'cert' is issued by c + if cert.isIssuer(c) or c == cert: + # Final node of the chain ! + # (add the final cert if not self signed) + if c != cert: + curchain += [cert] + return curchain + else: + # Not the final node of the chain ! Recurse. + curchain = _rec_getchain(curchain, subtree) + if curchain: + return curchain + return None + + chain = _rec_getchain([], self.tree) + if chain is not None: + # We add the first certificate to the ROOT in all cases + return CertTree(chain, [chain[0]]) + else: + return None + + def verify(self, cert): """ - Does the same job as .verifyChainFromCAFile() but using the list - of anchors in capath directory. The directory should (only) contain - certificates files in PEM format. As for .verifyChainFromCAFile(), - a list of untrusted certificates can be passed as a file - (concatenation of the certificates in PEM format). + Verify that a certificate is properly signed. """ - try: - anchors = [] - for cafile in os.listdir(capath): - with open(os.path.join(capath, cafile), "rb") as fd: - anchors.append(Cert(fd.read())) - except Exception: - raise Exception("capath provided is not a valid cert path") + # Check that we can find a chain to this certificate + if not self.getchain(cert): + raise ValueError("Certificate verification failed !") - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] + def show(self, ret: bool = False): + """ + Return the CertTree as a string certificate tree + """ - return self.verifyChain(anchors, untrusted) + def _rec_show(c, children, lvl=0): + s = "" + # Process the current CA + if c: + if not c.isSelfSigned(): + s += "%s [Not Self Signed]\n" % c.subject_str + else: + s += "%s [Self Signed]\n" % c.subject_str + s = lvl * " " + s + lvl += 1 + # Process all sub-CAs at a lower level + for child, subchildren in children: + s += _rec_show(child, subchildren, lvl=lvl) + return s + + showed = _rec_show(None, self.tree) + if ret: + return showed + else: + print(showed) def __repr__(self): - llen = len(self) - 1 - if llen < 0: - return "" - c = self[0] - s = "__ " - if not c.isSelfSigned(): - s += "%s [Not Self Signed]\n" % c.subject_str + return "" % ( + len(self), + len(self.rootCAs), + ) + + +####### +# CMS # +####### + +# RFC3852 + + +class CMS_Engine: + """ + A utility class to perform CMS/PKCS7 operations, as specified by RFC3852. + + :param store: a ROOT CA certificate list to trust. + :param crls: a list of CRLs to include. This is currently not checked. + """ + + def __init__( + self, + store: CertList, + crls: List[X509_CRL] = [], + ): + self.store = store + self.crls = crls + + def _get_algorithms(self, key: PrivKey, h="sha256") -> ASN1_OID: + """ + Get the algorithms matching a private key + """ + if isinstance(key, PrivKeyRSA): + # RFC3370 sect 3.2 + return ( + ASN1_OID("rsaEncryption"), + _get_hash(h), + h, + ) + elif isinstance(key, PrivKeyECDSA): + # RFC5753 sect 2.1.1 + if h == "sha1": + return ( + ASN1_OID("ecdsa-with-SHA1"), + hashes.SHA1(), + "sha1", + ) + elif h == "sha224": + return ( + ASN1_OID("ecdsa-with-SHA224"), + hashes.SHA224(), + "sha224", + ) + elif h == "sha256": + return ( + ASN1_OID("ecdsa-with-SHA256"), + hashes.SHA256(), + "sha256", + ) + elif h == "sha384": + return ( + ASN1_OID("ecdsa-with-SHA384"), + hashes.SHA384(), + "sha384", + ) + elif h == "sha512": + return ( + ASN1_OID("ecdsa-with-SHA512"), + hashes.SHA512(), + "sha512", + ) + else: + raise ValueError("Unknown hash for private key !") + elif isinstance(key, PrivKeyEdDSA): + # RFC8419 sect 2.3 + if isinstance(key.key, x25519.X25519PrivateKey): + return ( + ASN1_OID("Ed25519"), + hashes.SHA512(), + "sha512", + ) + elif isinstance(key.key, x448.X448PrivateKey): + return ( + ASN1_OID("Ed448"), + hashes.SHAKE256(64), + "shake256", + ) else: - s += "%s [Self Signed]\n" % c.subject_str - idx = 1 - while idx <= llen: - c = self[idx] - s += "%s_ %s" % (" " * idx * 2, c.subject_str) - if idx != llen: - s += "\n" - idx += 1 - return s + raise ValueError("Unknown private key type !") + + def sign( + self, + message: Union[bytes, Packet], + eContentType: ASN1_OID, + cert: Cert, + key: PrivKey, + dhash: Optional[str] = "sha256", + ): + """ + Sign a message using CMS. + + :param message: the inner content to sign. + :param eContentType: the OID of the inner content. + :param cert: the certificate whose key to use use for signing. + :param key: the private key to use for signing. + :param dhash: the hash to use for message digest (ECDSA only). + + We currently only support X.509 certificates ! + """ + sigalg, cdhash, dhash = self._get_algorithms(key, h=dhash) + + # RFC3852 5.4. Message Digest Calculation Process + hash = hashes.Hash(cdhash) + hash.update(bytes(message)) + hashed_message = hash.finalize() + + # RFC3852 5.5. Signature Generation Process + signerInfo = CMS_SignerInfo( + version=1, + sid=CMS_IssuerAndSerialNumber( + issuer=cert.tbsCertificate.issuer, + serialNumber=cert.tbsCertificate.serialNumber, + ), + digestAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(dhash), + parameters=ASN1_NULL(0), + ), + signedAttrs=[ + X509_Attribute( + type=ASN1_OID("contentType"), + values=[ + X509_AttributeValue(value=eContentType), + ], + ), + X509_Attribute( + type=ASN1_OID("messageDigest"), + # "A message-digest attribute MUST have a single attribute value" + values=[ + X509_AttributeValue(value=ASN1_STRING(hashed_message)), + ], + ), + ], + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=sigalg, + parameters=ASN1_NULL(0), + ), + ) + signerInfo.signature = ASN1_STRING( + key.sign( + bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + h=dhash, + ) + ) + + # Build a chain of X509_Cert to ship (but skip the ROOT certificate) + certTree = CertTree(cert, self.store) + certificates = [x.x509Cert for x in certTree if not x.isSelfSigned()] + + # Build final structure + return CMS_ContentInfo( + contentType=ASN1_OID("id-signedData"), + content=CMS_SignedData( + version=3 if certificates else 1, + digestAlgorithms=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(dhash), + parameters=ASN1_NULL(0), + ), + encapContentInfo=CMS_EncapsulatedContentInfo( + eContentType=eContentType, + eContent=message, + ), + certificates=( + [CMS_CertificateChoices(certificate=cert) for cert in certificates] + if certificates + else None + ), + crls=( + [CMS_RevocationInfoChoice(crl=crl) for crl in self.crls] + if self.crls + else None + ), + signerInfos=[ + signerInfo, + ], + ), + ) + + def verify( + self, + contentInfo: CMS_ContentInfo, + eContentType: Optional[ASN1_OID] = None, + eContent: Optional[bytes] = None, + no_verify_cert: bool = False, + ): + """ + Verify a CMS message against the list of trusted certificates, + and return the unpacked message if the verification succeeds. + + :param contentInfo: the ContentInfo whose signature to verify + :param eContentType: if provided, verifies that the content type is valid + :param eContent: in PKCS 7.1, provide the content to verify + :param no_verify_cert: do not check the remote certificate (unsafe) + """ + if contentInfo.contentType.oidname != "id-signedData": + raise ValueError("ContentInfo isn't signed !") + + signeddata = contentInfo.content + + # Build the certificate chain + certificates = [] + if signeddata.certificates: + certificates = [Cert(x.certificate) for x in signeddata.certificates] + certTree = CertTree(certificates, self.store) + + # Check there's at least one signature + if not signeddata.signerInfos: + raise ValueError("ContentInfo contained no signature !") + + # Check all signatures + for signerInfo in signeddata.signerInfos: + sigh = hash_by_oid[signerInfo.signatureAlgorithm.algorithm.val] + + # Find certificate in the chain that did this + cert: Cert = certTree.findCertBySid(signerInfo.sid) + + # Verify certificate signature + if not no_verify_cert: + certTree.verify(cert) + + # Verify the message hash + if signerInfo.signedAttrs: + # Verify the contentType + try: + contentType = next( + x.values[0].value + for x in signerInfo.signedAttrs + if x.type.oidname == "contentType" + ) + + if contentType != signeddata.encapContentInfo.eContentType: + raise ValueError( + "Inconsistent 'contentType' was detected in packet !" + ) + + if eContentType is not None and eContentType != contentType: + raise ValueError( + "Expected '%s' but got '%s' contentType !" + % ( + eContentType, + contentType, + ) + ) + except StopIteration: + raise ValueError("Missing contentType in signedAttrs !") + + # Verify the messageDigest value + try: + # "A message-digest attribute MUST have a single attribute value" + messageDigest = next( + x.values[0].value + for x in signerInfo.signedAttrs + if x.type.oidname == "messageDigest" + ) + + if signeddata.encapContentInfo.eContent is not None: + eContent = bytes(signeddata.encapContentInfo.eContent) + elif eContent is None: + raise ValueError("No eContent was provided !") + + # Re-calculate hash + h = signerInfo.digestAlgorithm.algorithm.oidname + hash = hashes.Hash(_get_hash(h)) + hash.update(eContent) + hashed_message = hash.finalize() + + if hashed_message != messageDigest: + raise ValueError("Invalid messageDigest value !") + except StopIteration: + raise ValueError("Missing messageDigest in signedAttrs !") + + # Verify the signature + cert.verify( + msg=bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + sig=signerInfo.signature.val, + h=sigh, + ) + else: + cert.verify( + msg=bytes(signeddata.encapContentInfo), + sig=signerInfo.signature.val, + h=sigh, + ) + + # Return the content + return signeddata.encapContentInfo.eContent diff --git a/scapy/layers/tls/crypto/__init__.py b/scapy/layers/tls/crypto/__init__.py index 063697b25dd..1644bbeee7e 100644 --- a/scapy/layers/tls/crypto/__init__.py +++ b/scapy/layers/tls/crypto/__init__.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016 Maxence Tury -# This program is published under a GPLv2 license """ Cryptographic capabilities for TLS. diff --git a/scapy/layers/tls/crypto/all.py b/scapy/layers/tls/crypto/all.py index 9c31eff8e78..6288f049b41 100644 --- a/scapy/layers/tls/crypto/all.py +++ b/scapy/layers/tls/crypto/all.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ Aggregate some TLS crypto objects. diff --git a/scapy/layers/tls/crypto/cipher_aead.py b/scapy/layers/tls/crypto/cipher_aead.py index a9bfc8b57f4..b83ddccbe66 100644 --- a/scapy/layers/tls/crypto/cipher_aead.py +++ b/scapy/layers/tls/crypto/cipher_aead.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ Authenticated Encryption with Associated Data ciphers. @@ -12,14 +13,12 @@ introduced cipher suites based on a ChaCha20-Poly1305 construction. """ -from __future__ import absolute_import import struct from scapy.config import conf from scapy.layers.tls.crypto.pkcs1 import pkcs_i2osp, pkcs_os2ip from scapy.layers.tls.crypto.common import CipherError from scapy.utils import strxor -import scapy.modules.six as six if conf.crypto_valid: from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes # noqa: E501 @@ -57,7 +56,7 @@ class AEADTagError(Exception): pass -class _AEADCipher(six.with_metaclass(_AEADCipherMetaclass, object)): +class _AEADCipher(metaclass=_AEADCipherMetaclass): """ The hasattr(self, "pc_cls") tests correspond to the legacy API of the crypto library. With cryptography v2.0, both CCM and GCM should follow @@ -144,7 +143,7 @@ def auth_encrypt(self, P, A, seq_num=None): because one cipher (ChaCha20Poly1305) using TLS 1.2 logic in record.py actually is a _AEADCipher_TLS13 (even though others are not). """ - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(P, A) if hasattr(self, "pc_cls"): @@ -184,7 +183,7 @@ def auth_decrypt(self, A, C, seq_num=None, add_length=True): C[self.nonce_explicit_len:-self.tag_len], C[-self.tag_len:]) - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(nonce_explicit_str, C, mac) self.nonce_explicit = pkcs_os2ip(nonce_explicit_str) @@ -247,7 +246,7 @@ class Cipher_AES_256_CCM_8(Cipher_AES_128_CCM_8): key_len = 32 -class _AEADCipher_TLS13(six.with_metaclass(_AEADCipherMetaclass, object)): +class _AEADCipher_TLS13(metaclass=_AEADCipherMetaclass): """ The hasattr(self, "pc_cls") enable support for the legacy implementation of GCM in the cryptography library. They should not be used, and might @@ -316,7 +315,7 @@ def auth_encrypt(self, P, A, seq_num): Note that the cipher's authentication tag must be None when encrypting. """ - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(P, A) if hasattr(self, "pc_cls"): @@ -342,7 +341,7 @@ def auth_decrypt(self, A, C, seq_num): raise a CipherError which contains the encrypted input. """ C, mac = C[:-self.tag_len], C[-self.tag_len:] - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(C, mac) if hasattr(self, "pc_cls"): diff --git a/scapy/layers/tls/crypto/cipher_block.py b/scapy/layers/tls/crypto/cipher_block.py index b85b29d8bcc..6a96dec81ec 100644 --- a/scapy/layers/tls/crypto/cipher_block.py +++ b/scapy/layers/tls/crypto/cipher_block.py @@ -1,24 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ Block ciphers. """ -from __future__ import absolute_import +import warnings + from scapy.config import conf from scapy.layers.tls.crypto.common import CipherError -import scapy.modules.six as six if conf.crypto_valid: - from cryptography.utils import register_interface - from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes, # noqa: E501 - BlockCipherAlgorithm, - CipherAlgorithm) - from cryptography.hazmat.backends.openssl.backend import (backend, - GetCipherByName) + from cryptography.utils import ( + CryptographyDeprecationWarning, + ) + from cryptography.hazmat.primitives.ciphers import ( + BlockCipherAlgorithm, + Cipher, + CipherAlgorithm, + algorithms, + modes, + ) + from cryptography.hazmat.backends.openssl.backend import backend + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms, + ) + except ImportError: + decrepit_algorithms = algorithms + + # cryptography's TripleDES can be used to simulate DES behavior + DES = lambda key: decrepit_algorithms.TripleDES(key * 3) + + try: + # cryptography > 47.0 + Camellia = decrepit_algorithms.Camellia + except AttributeError: + Camellia = algorithms.Camellia _tls_block_cipher_algs = {} @@ -39,7 +61,7 @@ def __new__(cls, ciph_name, bases, dct): return the_class -class _BlockCipher(six.with_metaclass(_BlockCipherMetaclass, object)): +class _BlockCipher(metaclass=_BlockCipherMetaclass): type = "block" def __init__(self, key=None, iv=None): @@ -51,17 +73,22 @@ def __init__(self, key=None, iv=None): else: key_len = self.key_len key = b"\0" * key_len - if not iv: - self.ready["iv"] = False - iv = b"\0" * self.block_size # we use super() in order to avoid any deadlock with __setattr__ super(_BlockCipher, self).__setattr__("key", key) - super(_BlockCipher, self).__setattr__("iv", iv) - - self._cipher = Cipher(self.pc_cls(key), - self.pc_cls_mode(iv), - backend=backend) + if self.pc_cls_mode == modes.ECB: + self._cipher = Cipher(self.pc_cls(key), + self.pc_cls_mode(), + backend=backend) + else: + if not iv: + self.ready["iv"] = False + iv = b"\0" * self.block_size + super(_BlockCipher, self).__setattr__("iv", iv) + + self._cipher = Cipher(self.pc_cls(key), + self.pc_cls_mode(iv), + backend=backend) def __setattr__(self, name, val): if name == "key": @@ -79,7 +106,7 @@ def encrypt(self, data): Encrypt the data. Also, update the cipher iv. This is needed for SSLv3 and TLS 1.0. For TLS 1.1/1.2, it is overwritten in TLS.post_build(). """ - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(data) encryptor = self._cipher.encryptor() tmp = encryptor.update(data) + encryptor.finalize() @@ -92,7 +119,7 @@ def decrypt(self, data): and TLS 1.0. For TLS 1.1/1.2, it is overwritten in TLS.pre_dissect(). If we lack the key, we raise a CipherError which contains the input. """ - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(data) decryptor = self._cipher.decryptor() tmp = decryptor.update(data) + decryptor.finalize() @@ -116,7 +143,7 @@ class Cipher_AES_256_CBC(Cipher_AES_128_CBC): key_len = 32 class Cipher_CAMELLIA_128_CBC(_BlockCipher): - pc_cls = algorithms.Camellia + pc_cls = Camellia pc_cls_mode = modes.CBC block_size = 16 key_len = 16 @@ -127,9 +154,17 @@ class Cipher_CAMELLIA_256_CBC(Cipher_CAMELLIA_128_CBC): # Mostly deprecated ciphers +_sslv2_block_cipher_algs = {} + if conf.crypto_valid: + class Cipher_DES_ECB(_BlockCipher): + pc_cls = staticmethod(DES) + pc_cls_mode = modes.ECB + block_size = 8 + key_len = 8 + class Cipher_DES_CBC(_BlockCipher): - pc_cls = algorithms.TripleDES + pc_cls = staticmethod(DES) pc_cls_mode = modes.CBC block_size = 8 key_len = 8 @@ -147,32 +182,37 @@ class Cipher_DES40_CBC(Cipher_DES_CBC): key_len = 5 class Cipher_3DES_EDE_CBC(_BlockCipher): - pc_cls = algorithms.TripleDES + pc_cls = decrepit_algorithms.TripleDES pc_cls_mode = modes.CBC block_size = 8 key_len = 24 - class Cipher_IDEA_CBC(_BlockCipher): - pc_cls = algorithms.IDEA - pc_cls_mode = modes.CBC - block_size = 8 - key_len = 16 + _sslv2_block_cipher_algs["DES_192_EDE3_CBC"] = Cipher_3DES_EDE_CBC - class Cipher_SEED_CBC(_BlockCipher): - pc_cls = algorithms.SEED - pc_cls_mode = modes.CBC - block_size = 16 - key_len = 16 + try: + with warnings.catch_warnings(): + # Hide deprecation warnings + warnings.filterwarnings("ignore", + category=CryptographyDeprecationWarning) + class Cipher_IDEA_CBC(_BlockCipher): + pc_cls = decrepit_algorithms.IDEA + pc_cls_mode = modes.CBC + block_size = 8 + key_len = 16 -_sslv2_block_cipher_algs = {} + class Cipher_SEED_CBC(_BlockCipher): + pc_cls = decrepit_algorithms.SEED + pc_cls_mode = modes.CBC + block_size = 16 + key_len = 16 -if conf.crypto_valid: - _sslv2_block_cipher_algs.update({ - "IDEA_128_CBC": Cipher_IDEA_CBC, - "DES_64_CBC": Cipher_DES_CBC, - "DES_192_EDE3_CBC": Cipher_3DES_EDE_CBC - }) + _sslv2_block_cipher_algs.update({ + "IDEA_128_CBC": Cipher_IDEA_CBC, + "DES_64_CBC": Cipher_DES_CBC, + }) + except AttributeError: + pass # We need some black magic for RC2, which is not registered by default @@ -181,26 +221,41 @@ class Cipher_SEED_CBC(_BlockCipher): # silently not declared, and the corresponding suites will have 'usable' False. if conf.crypto_valid: - @register_interface(BlockCipherAlgorithm) - @register_interface(CipherAlgorithm) - class _ARC2(object): - name = "RC2" - block_size = 64 - key_sizes = frozenset([128]) - - def __init__(self, key): - self.key = algorithms._verify_key_size(self, key) - - @property - def key_size(self): - return len(self.key) * 8 - - _gcbn_format = "{cipher.name}-{mode.name}" - if GetCipherByName(_gcbn_format)(backend, _ARC2, modes.CBC) != \ - backend._ffi.NULL: - + try: + from cryptography.hazmat.decrepit.ciphers.algorithms import RC2 + rc2_available = backend.cipher_supported( + RC2(b"0" * 16), modes.CBC(b"0" * 8) + ) + except ImportError: + # Legacy path for cryptography < 43.0.0 + from cryptography.hazmat.backends.openssl.backend import ( + GetCipherByName + ) + _gcbn_format = "{cipher.name}-{mode.name}" + + class RC2(BlockCipherAlgorithm, CipherAlgorithm): + name = "RC2" + block_size = 64 + key_sizes = frozenset([128]) + + def __init__(self, key): + self.key = algorithms._verify_key_size(self, key) + + @property + def key_size(self): + return len(self.key) * 8 + if GetCipherByName(_gcbn_format)(backend, RC2, modes.CBC) != \ + backend._ffi.NULL: + rc2_available = True + backend.register_cipher_adapter(RC2, + modes.CBC, + GetCipherByName(_gcbn_format)) + else: + rc2_available = False + + if rc2_available: class Cipher_RC2_CBC(_BlockCipher): - pc_cls = _ARC2 + pc_cls = RC2 pc_cls_mode = modes.CBC block_size = 8 key_len = 16 @@ -209,10 +264,6 @@ class Cipher_RC2_CBC_40(Cipher_RC2_CBC): expanded_key_len = 16 key_len = 5 - backend.register_cipher_adapter(Cipher_RC2_CBC.pc_cls, - Cipher_RC2_CBC.pc_cls_mode, - GetCipherByName(_gcbn_format)) - _sslv2_block_cipher_algs["RC2_128_CBC"] = Cipher_RC2_CBC diff --git a/scapy/layers/tls/crypto/cipher_stream.py b/scapy/layers/tls/crypto/cipher_stream.py index 8a207a1dca9..5c95fadd13a 100644 --- a/scapy/layers/tls/crypto/cipher_stream.py +++ b/scapy/layers/tls/crypto/cipher_stream.py @@ -1,20 +1,26 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ Stream ciphers. """ -from __future__ import absolute_import from scapy.config import conf from scapy.layers.tls.crypto.common import CipherError -import scapy.modules.six as six if conf.crypto_valid: from cryptography.hazmat.primitives.ciphers import Cipher, algorithms from cryptography.hazmat.backends import default_backend + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms, + ) + except ImportError: + decrepit_algorithms = algorithms _tls_stream_cipher_algs = {} @@ -35,7 +41,7 @@ def __new__(cls, ciph_name, bases, dct): return the_class -class _StreamCipher(six.with_metaclass(_StreamCipherMetaclass, object)): +class _StreamCipher(metaclass=_StreamCipherMetaclass): type = "stream" def __init__(self, key=None): @@ -81,13 +87,13 @@ def __setattr__(self, name, val): super(_StreamCipher, self).__setattr__(name, val) def encrypt(self, data): - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(data) self._enc_updated_with += data return self.encryptor.update(data) def decrypt(self, data): - if False in six.itervalues(self.ready): + if False in self.ready.values(): raise CipherError(data) self._dec_updated_with += data return self.decryptor.update(data) @@ -104,7 +110,7 @@ def snapshot(self): if conf.crypto_valid: class Cipher_RC4_128(_StreamCipher): - pc_cls = algorithms.ARC4 + pc_cls = decrepit_algorithms.ARC4 key_len = 16 class Cipher_RC4_40(Cipher_RC4_128): diff --git a/scapy/layers/tls/crypto/ciphers.py b/scapy/layers/tls/crypto/ciphers.py index f2755b2c55f..ef5feb65c83 100644 --- a/scapy/layers/tls/crypto/ciphers.py +++ b/scapy/layers/tls/crypto/ciphers.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016 Maxence Tury -# This program is published under a GPLv2 license """ TLS ciphers. diff --git a/scapy/layers/tls/crypto/common.py b/scapy/layers/tls/crypto/common.py index 9653824ebc0..d2ac9758cb1 100644 --- a/scapy/layers/tls/crypto/common.py +++ b/scapy/layers/tls/crypto/common.py @@ -1,6 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information """ TLS ciphers. diff --git a/scapy/layers/tls/crypto/compression.py b/scapy/layers/tls/crypto/compression.py index 049f4db092f..0c92233d851 100644 --- a/scapy/layers/tls/crypto/compression.py +++ b/scapy/layers/tls/crypto/compression.py @@ -1,17 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016 Maxence Tury -# This program is published under a GPLv2 license """ TLS compression. """ -from __future__ import absolute_import import zlib from scapy.error import warning -import scapy.modules.six as six _tls_compression_algs = {} @@ -33,7 +32,7 @@ def __new__(cls, name, bases, dct): return the_class -class _GenericComp(six.with_metaclass(_GenericCompMetaclass, object)): +class _GenericComp(metaclass=_GenericCompMetaclass): pass diff --git a/scapy/layers/tls/crypto/groups.py b/scapy/layers/tls/crypto/groups.py index 955e06ccdc9..c659eae5e22 100644 --- a/scapy/layers/tls/crypto/groups.py +++ b/scapy/layers/tls/crypto/groups.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ This is a register for DH groups from RFC 3526 and RFC 4306. @@ -12,42 +13,19 @@ (Note that the equivalent of _ffdh_groups for ECDH is ec._CURVE_TYPES.) """ -from __future__ import absolute_import from scapy.config import conf +from scapy.compat import bytes_int, int_bytes +from scapy.error import warning from scapy.utils import long_converter -import scapy.modules.six as six if conf.crypto_valid: from cryptography.hazmat.backends import default_backend - -# We have to start by a dirty hack in order to allow long generators, -# which some versions of openssl love to use... - -if conf.crypto_valid: + from cryptography.hazmat.primitives.asymmetric import dh, ec + from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.dh import DHParameterNumbers - - try: - # We test with dummy values whether the size limitation has been removed. # noqa: E501 - pn_test = DHParameterNumbers(2, 7) - except ValueError: - # We get rid of the limitation through the cryptography v1.9 __init__. - - def DHParameterNumbers__init__hack(self, p, g, q=None): - if ( - not isinstance(p, six.integer_types) or - not isinstance(g, six.integer_types) - ): - raise TypeError("p and g must be integers") - if q is not None and not isinstance(q, six.integer_types): - raise TypeError("q must be integer or None") - - self._p = p - self._g = g - self._q = q - - DHParameterNumbers.__init__ = DHParameterNumbers__init__hack - - # End of hack. +if conf.crypto_valid_advanced: + from cryptography.hazmat.primitives.asymmetric import x25519 + from cryptography.hazmat.primitives.asymmetric import x448 _ffdh_groups = {} @@ -64,7 +42,7 @@ def __new__(cls, ffdh_name, bases, dct): return the_class -class _FFDHParams(six.with_metaclass(_FFDHParamsMetaclass)): +class _FFDHParams(metaclass=_FFDHParamsMetaclass): pass @@ -430,10 +408,108 @@ class ffdhe8192(_FFDHParams): # From RFC 7919 0xff01: "arbitrary_explicit_prime_curves", 0xff02: "arbitrary_explicit_char2_curves"} +_tls_post_quantum_hybrid = { + # https://www.ietf.org/archive/id/draft-kwiatkowski-tls-ecdhe-mlkem-02.html#name-secp256r1mlkem768 + 0x11EB: "SecP256r1MLKEM768", + # https://www.ietf.org/archive/id/draft-kwiatkowski-tls-ecdhe-mlkem-02.html#name-x25519mlkem768 + 0x11EC: "X25519MLKEM768", + # https://www.ietf.org/archive/id/draft-tls-westerbaan-xyber768d00-03.html#name-iana-considerations + 0x6399: "X25519Kyber768Draft00", +} + _tls_named_groups = {} _tls_named_groups.update(_tls_named_ffdh_groups) _tls_named_groups.update(_tls_named_curves) - +_tls_named_groups.update(_tls_post_quantum_hybrid) + + +def _tls_named_groups_import(group, pubbytes): + if group in _tls_named_ffdh_groups: + # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.8.1 + params = _ffdh_groups[_tls_named_ffdh_groups[group]][0] + pn = params.parameter_numbers() + y = bytes_int(pubbytes) + public_numbers = dh.DHPublicNumbers(y, pn) + return public_numbers.public_key(default_backend()) + elif group in _tls_named_curves: + # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.8.2 + if _tls_named_curves[group] in ["x25519", "x448"]: + if conf.crypto_valid_advanced: + if _tls_named_curves[group] == "x25519": + import_point = x25519.X25519PublicKey.from_public_bytes + else: + import_point = x448.X448PublicKey.from_public_bytes + return import_point(pubbytes) + else: + curve = ec._CURVE_TYPES[_tls_named_curves[group]] + try: + # cryptography < 42 + curve = curve() + except TypeError: + pass + try: # cryptography >= 2.5 + return ec.EllipticCurvePublicKey.from_encoded_point( + curve, + pubbytes + ) + except AttributeError: + pub_num = ec.EllipticCurvePublicNumbers.from_encoded_point( + curve, + pubbytes + ).public_numbers() + return pub_num.public_key(default_backend()) + + +def _tls_named_groups_pubbytes(privkey): + if isinstance(privkey, dh.DHPrivateKey): + # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.8.1 + pubkey = privkey.public_key() + return int_bytes(pubkey.public_numbers().y, privkey.key_size // 8) + elif isinstance(privkey, (x25519.X25519PrivateKey, + x448.X448PrivateKey)): + # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.8.2 + pubkey = privkey.public_key() + return pubkey.public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw + ) + else: + pubkey = privkey.public_key() + try: + # cryptography >= 2.5 + return pubkey.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint + ) + except TypeError: + # older versions + return pubkey.public_numbers().encode_point() + + +def _tls_named_groups_generate(group): + if group in _tls_named_ffdh_groups: + params = _ffdh_groups[_tls_named_ffdh_groups[group]][0] + return params.generate_private_key() + elif group in _tls_named_curves: + group_name = _tls_named_curves[group] + if group_name in ["x25519", "x448"]: + if conf.crypto_valid_advanced: + if group_name == "x25519": + return x25519.X25519PrivateKey.generate() + else: + return x448.X448PrivateKey.generate() + else: + warning( + "Your cryptography version doesn't support " + group_name + ) + else: + curve = ec._CURVE_TYPES[_tls_named_curves[group]] + try: + # cryptography < 42 + curve = curve() + except TypeError: + pass + return ec.generate_private_key(curve, default_backend()) # Below lies ghost code since the shift from 'ecdsa' to 'cryptography' lib. # Part of the code has been kept, but commented out, in case anyone would like diff --git a/scapy/layers/tls/crypto/h_mac.py b/scapy/layers/tls/crypto/h_mac.py index 5235e4c84b3..db984dc22a2 100644 --- a/scapy/layers/tls/crypto/h_mac.py +++ b/scapy/layers/tls/crypto/h_mac.py @@ -1,18 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016 Maxence Tury -# This program is published under a GPLv2 license """ HMAC classes. """ -from __future__ import absolute_import -import hmac - +from scapy.config import conf from scapy.layers.tls.crypto.hash import _tls_hash_algs -import scapy.modules.six as six -from scapy.compat import bytes_encode + +if conf.crypto_valid: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.hmac import HMAC _SSLv3_PAD1_MD5 = b"\x36" * 48 _SSLv3_PAD1_SHA1 = b"\x36" * 40 @@ -31,15 +32,18 @@ class _GenericHMACMetaclass(type): the associated hash function (see RFC 5246, appendix C). Also, we do not need to instantiate the associated hash function. """ + def __new__(cls, hmac_name, bases, dct): - hash_name = hmac_name[5:] # remove leading "Hmac_" + hash_name = hmac_name[5:] # remove leading "Hmac_" if hmac_name != "_GenericHMAC": + hash_alg = _tls_hash_algs[hash_name.lower()] dct["name"] = "HMAC-%s" % hash_name - dct["hash_alg"] = _tls_hash_algs[hash_name] - dct["hmac_len"] = _tls_hash_algs[hash_name].hash_len + dct["hash_alg"] = hash_alg + dct["hmac_len"] = hash_alg.hash_len dct["key_len"] = dct["hmac_len"] - the_class = super(_GenericHMACMetaclass, cls).__new__(cls, hmac_name, - bases, dct) + the_class = super(_GenericHMACMetaclass, cls).__new__( + cls, hmac_name, bases, dct + ) if hmac_name != "_GenericHMAC": _tls_hmac_algs[dct["name"]] = the_class return the_class @@ -49,38 +53,36 @@ class HMACError(Exception): """ Raised when HMAC verification fails. """ + pass -class _GenericHMAC(six.with_metaclass(_GenericHMACMetaclass, object)): +class _GenericHMAC(metaclass=_GenericHMACMetaclass): def __init__(self, key=None): - if key is None: - self.key = b"" - else: - self.key = bytes_encode(key) + self.key = key or b"" def digest(self, tbd): if self.key is None: raise HMACError - tbd = bytes_encode(tbd) - return hmac.new(self.key, tbd, self.hash_alg.hash_cls).digest() + hm = HMAC(self.key, self.hash_alg.hash_cls(), backend=default_backend()) + hm.update(tbd) + return hm.finalize() def digest_sslv3(self, tbd): if self.key is None: raise HMACError h = self.hash_alg() - if h.name == "SHA": + if h.name == "sha": pad1 = _SSLv3_PAD1_SHA1 pad2 = _SSLv3_PAD2_SHA1 - elif h.name == "MD5": + elif h.name == "md5": pad1 = _SSLv3_PAD1_MD5 pad2 = _SSLv3_PAD2_MD5 else: raise HMACError("Provided hash does not work with SSLv3.") - return h.digest(self.key + pad2 + - h.digest(self.key + pad1 + tbd)) + return h.digest(self.key + pad2 + h.digest(self.key + pad1 + tbd)) class Hmac_NULL(_GenericHMAC): @@ -94,6 +96,10 @@ def digest_sslv3(self, tbd): return b"" +class Hmac_MD4(_GenericHMAC): + pass + + class Hmac_MD5(_GenericHMAC): pass @@ -116,3 +122,10 @@ class Hmac_SHA384(_GenericHMAC): class Hmac_SHA512(_GenericHMAC): pass + + +def Hmac(key, hashtype): + """ + Return Hmac object from Hash object and key + """ + return _tls_hmac_algs[f"HMAC-{hashtype.name.upper()}"](key=key) diff --git a/scapy/layers/tls/crypto/hash.py b/scapy/layers/tls/crypto/hash.py index 056201a11ae..05c653538ab 100644 --- a/scapy/layers/tls/crypto/hash.py +++ b/scapy/layers/tls/crypto/hash.py @@ -1,16 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016 Maxence Tury -# This program is published under a GPLv2 license """ Hash classes. """ -from __future__ import absolute_import -from hashlib import md5, sha1, sha224, sha256, sha384, sha512 -import scapy.modules.six as six - +from scapy.config import conf, crypto_validator +from scapy.layers.tls.crypto.md4 import MD4 as md4 + +if conf.crypto_valid: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.hashes import ( + MD5, + SHA1, + SHA224, + SHA256, + SHA384, + SHA512, + SHAKE256, + ) + from cryptography.hazmat.primitives.hashes import HashAlgorithm +else: + MD5 = SHA1 = SHA224 = SHA256 = SHA384 = SHA512 = SHAKE256 = None + HashAlgorithm = object _tls_hash_algs = {} @@ -20,19 +36,23 @@ class _GenericHashMetaclass(type): Hash classes are automatically registered through this metaclass. Furthermore, their name attribute is extracted from their class name. """ + def __new__(cls, hash_name, bases, dct): if hash_name != "_GenericHash": - dct["name"] = hash_name[5:] # remove leading "Hash_" - the_class = super(_GenericHashMetaclass, cls).__new__(cls, hash_name, - bases, dct) + dct["name"] = hash_name[5:].lower() # remove leading "Hash_" + the_class = super(_GenericHashMetaclass, cls).__new__( + cls, hash_name, bases, dct + ) if hash_name != "_GenericHash": - _tls_hash_algs[hash_name[5:]] = the_class + _tls_hash_algs[dct["name"]] = the_class return the_class -class _GenericHash(six.with_metaclass(_GenericHashMetaclass, object)): +class _GenericHash(metaclass=_GenericHashMetaclass): def digest(self, tbd): - return self.hash_cls(tbd).digest() + digest = hashes.Hash(self.hash_cls(), backend=default_backend()) + digest.update(tbd) + return digest.finalize() class Hash_NULL(_GenericHash): @@ -42,31 +62,80 @@ def digest(self, tbd): return b"" +class Hash_MD4(_GenericHash): + hash_cls = md4 + hash_len = 16 + + def digest(self, tbd): + return self.hash_cls(tbd).digest() + + class Hash_MD5(_GenericHash): - hash_cls = md5 + hash_cls = MD5 hash_len = 16 class Hash_SHA(_GenericHash): - hash_cls = sha1 + hash_cls = SHA1 hash_len = 20 +_tls_hash_algs["sha1"] = Hash_SHA + + class Hash_SHA224(_GenericHash): - hash_cls = sha224 + hash_cls = SHA224 hash_len = 28 class Hash_SHA256(_GenericHash): - hash_cls = sha256 + hash_cls = SHA256 hash_len = 32 class Hash_SHA384(_GenericHash): - hash_cls = sha384 + hash_cls = SHA384 hash_len = 48 class Hash_SHA512(_GenericHash): - hash_cls = sha512 + hash_cls = SHA512 hash_len = 64 + + +# first, we add the "md5-sha1" hash from openssl to python-cryptography +class MD5_SHA1(HashAlgorithm): + name = "md5-sha1" + digest_size = 36 + block_size = 64 + + +class Hash_MD5SHA1(_GenericHash): + hash_cls = MD5_SHA1 + hash_len = 36 + + +_tls_hash_algs["md5-sha1"] = Hash_MD5SHA1 + + +class Hash_SHAKE256(_GenericHash): + hash_cls = SHAKE256 + + def __init__(self, digest_size: int): + self.hash_len = digest_size + + def digest(self, tbd): + digest = hashes.Hash(self.hash_cls(self.hash_len), backend=default_backend()) + digest.update(tbd) + return digest.finalize() + + +@crypto_validator +def _get_hash(hashStr): + """ + Return a cryptography-hash by its name + """ + try: + return _tls_hash_algs[hashStr].hash_cls() + except KeyError: + raise KeyError("Unknown hash function %s" % hashStr) diff --git a/scapy/layers/tls/crypto/hkdf.py b/scapy/layers/tls/crypto/hkdf.py index f9c69c49312..2d2af9d272b 100644 --- a/scapy/layers/tls/crypto/hkdf.py +++ b/scapy/layers/tls/crypto/hkdf.py @@ -1,6 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2017 Maxence Tury -# This program is published under a GPLv2 license """ Stateless HKDF for TLS 1.3. @@ -8,8 +9,8 @@ import struct -from scapy.config import conf -from scapy.layers.tls.crypto.pkcs1 import _get_hash +from scapy.config import conf, crypto_validator +from scapy.layers.tls.crypto.hash import _get_hash if conf.crypto_valid: from cryptography.hazmat.backends import default_backend @@ -19,21 +20,29 @@ class TLS13_HKDF(object): + @crypto_validator def __init__(self, hash_name="sha256"): self.hash = _get_hash(hash_name) + @crypto_validator def extract(self, salt, ikm): h = self.hash - hkdf = HKDF(h, h.digest_size, salt, None, default_backend()) if ikm is None: ikm = b"\x00" * h.digest_size - return hkdf._extract(ikm) + # cryptography 47.0.0 added this as a public API + if getattr(HKDF, "extract", None) is not None: + return HKDF.extract(h, salt, ikm) + else: + hkdf = HKDF(h, h.digest_size, salt, None, default_backend()) + return hkdf._extract(ikm) + @crypto_validator def expand(self, prk, info, L): h = self.hash hkdf = HKDFExpand(h, L, info, default_backend()) return hkdf.derive(prk) + @crypto_validator def expand_label(self, secret, label, hash_value, length): hkdf_label = struct.pack("!H", length) hkdf_label += struct.pack("B", 6 + len(label)) @@ -43,6 +52,7 @@ def expand_label(self, secret, label, hash_value, length): hkdf_label += hash_value return self.expand(secret, hkdf_label, length) + @crypto_validator def derive_secret(self, secret, label, messages): h = Hash(self.hash, backend=default_backend()) h.update(messages) @@ -50,6 +60,7 @@ def derive_secret(self, secret, label, messages): hash_len = self.hash.digest_size return self.expand_label(secret, label, hash_messages, hash_len) + @crypto_validator def compute_verify_data(self, basekey, handshake_context): hash_len = self.hash.digest_size finished_key = self.expand_label(basekey, b"finished", b"", hash_len) diff --git a/scapy/layers/tls/crypto/kx_algs.py b/scapy/layers/tls/crypto/kx_algs.py index 47b8c493b75..cd1dbec9c1d 100644 --- a/scapy/layers/tls/crypto/kx_algs.py +++ b/scapy/layers/tls/crypto/kx_algs.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ Key Exchange algorithms as listed in appendix C of RFC 4346. @@ -9,14 +10,12 @@ XXX No support yet for PSK (also, no static DH, DSS, SRP or KRB). """ -from __future__ import absolute_import from scapy.layers.tls.keyexchange import (ServerDHParams, ServerRSAParams, ClientDiffieHellmanPublic, ClientECDiffieHellmanPublic, _tls_server_ecdh_cls_guess, EncryptedPreMasterSecret) -import scapy.modules.six as six _tls_kx_algs = {} @@ -41,7 +40,7 @@ def __new__(cls, kx_name, bases, dct): return the_class -class _GenericKX(six.with_metaclass(_GenericKXMetaclass)): +class _GenericKX(metaclass=_GenericKXMetaclass): pass diff --git a/scapy/layers/tls/crypto/md4.py b/scapy/layers/tls/crypto/md4.py new file mode 100644 index 00000000000..642df9c9305 --- /dev/null +++ b/scapy/layers/tls/crypto/md4.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: WTFPL +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2019 James Seo (github.com/kangtastic). + +""" +MD4 implementation + +Modified from: +https://gist.github.com/kangtastic/c3349fc4f9d659ee362b12d7d8c639b6 +""" + +import struct + + +class MD4: + """ + An implementation of the MD4 hash algorithm. + + Modified to provide the same API as hashlib's. + """ + name = 'md4' + block_size = 64 + width = 32 + mask = 0xFFFFFFFF + + # Unlike, say, SHA-1, MD4 uses little-endian. Fascinating! + h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476] + + def __init__(self, msg=b""): + self.msg = msg + + def update(self, msg): + self.msg += msg + + def digest(self): + # Pre-processing: Total length is a multiple of 512 bits. + ml = len(self.msg) * 8 + self.msg += b"\x80" + self.msg += b"\x00" * (-(len(self.msg) + 8) % self.block_size) + self.msg += struct.pack("> (MD4.width - n) + return lbits | rbits diff --git a/scapy/layers/tls/crypto/pkcs1.py b/scapy/layers/tls/crypto/pkcs1.py index 8c2ca36cf4b..82082edc3de 100644 --- a/scapy/layers/tls/crypto/pkcs1.py +++ b/scapy/layers/tls/crypto/pkcs1.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2008 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ PKCS #1 methods as defined in RFC 3447. @@ -11,19 +12,16 @@ Ubuntu or OSX. This is why we reluctantly keep some legacy crypto here. """ -from __future__ import absolute_import from scapy.compat import bytes_encode, hex_bytes, bytes_hex -import scapy.modules.six as six from scapy.config import conf, crypto_validator from scapy.error import warning +from scapy.layers.tls.crypto.hash import _get_hash if conf.crypto_valid: - from cryptography import utils from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding - from cryptography.hazmat.primitives.hashes import HashAlgorithm ##################################################################### @@ -91,32 +89,7 @@ def _legacy_pkcs1_v1_5_encode_md5_sha1(M, emLen): # Hash and padding helpers ##################################################################### -_get_hash = None if conf.crypto_valid: - - # first, we add the "md5-sha1" hash from openssl to python-cryptography - @utils.register_interface(HashAlgorithm) - class MD5_SHA1(object): - name = "md5-sha1" - digest_size = 36 - block_size = 64 - - _hashes = { - "md5": hashes.MD5, - "sha1": hashes.SHA1, - "sha224": hashes.SHA224, - "sha256": hashes.SHA256, - "sha384": hashes.SHA384, - "sha512": hashes.SHA512, - "md5-sha1": MD5_SHA1 - } - - def _get_hash(hashStr): - try: - return _hashes[hashStr]() - except KeyError: - raise KeyError("Unknown hash function %s" % hashStr) - def _get_padding(padStr, mgf=padding.MGF1, h=hashes.SHA256, label=None): if padStr == "pkcs": return padding.PKCS1v15() @@ -171,9 +144,7 @@ def _legacy_verify_md5_sha1(self, M, S): return False s = pkcs_os2ip(S) n = self._modulus - if isinstance(s, int) and six.PY2: - s = long(s) # noqa: F821 - if (six.PY2 and not isinstance(s, long)) or s > n - 1: # noqa: F821 + if s > n - 1: warning("Key._rsaep() expects a long between 0 and n-1") return None m = pow(s, self._pubExp, n) @@ -216,9 +187,7 @@ def _legacy_sign_md5_sha1(self, M): return None m = pkcs_os2ip(EM) n = self._modulus - if isinstance(m, int) and six.PY2: - m = long(m) # noqa: F821 - if (six.PY2 and not isinstance(m, long)) or m > n - 1: # noqa: F821 + if m > n - 1: warning("Key._rsaep() expects a long between 0 and n-1") return None privExp = self.key.private_numbers().d diff --git a/scapy/layers/tls/crypto/prf.py b/scapy/layers/tls/crypto/prf.py index 9cd2b79f88d..d5e7e76f5b9 100644 --- a/scapy/layers/tls/crypto/prf.py +++ b/scapy/layers/tls/crypto/prf.py @@ -1,19 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard -# 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2015, 2016, 2017 Maxence Tury """ TLS Pseudorandom Function. """ -from __future__ import absolute_import from scapy.error import warning from scapy.utils import strxor from scapy.layers.tls.crypto.hash import _tls_hash_algs from scapy.layers.tls.crypto.h_mac import _tls_hmac_algs -from scapy.modules.six.moves import range from scapy.compat import bytes_encode @@ -74,7 +73,7 @@ def _tls_P_SHA512(secret, seed, req_len): # PRF functions, according to the protocol version def _sslv2_PRF(secret, seed, req_len): - hash_md5 = _tls_hash_algs["MD5"]() + hash_md5 = _tls_hash_algs["md5"]() rounds = (req_len + hash_md5.hash_len - 1) // hash_md5.hash_len res = b"" @@ -109,8 +108,8 @@ def _ssl_PRF(secret, seed, req_len): b"M", b"N", b"O", b"P", b"Q", b"R", b"S", b"T", b"U", b"V", b"W", b"X", # noqa: E501 b"Y", b"Z"] res = b"" - hash_sha1 = _tls_hash_algs["SHA"]() - hash_md5 = _tls_hash_algs["MD5"]() + hash_sha1 = _tls_hash_algs["sha"]() + hash_md5 = _tls_hash_algs["md5"]() rounds = (req_len + hash_md5.hash_len - 1) // hash_md5.hash_len for i in range(rounds): @@ -186,7 +185,7 @@ class PRF(object): context of the connection state using the tls_version and the cipher suite. """ - def __init__(self, hash_name="SHA256", tls_version=0x0303): + def __init__(self, hash_name="sha256", tls_version=0x0303): self.tls_version = tls_version self.hash_name = hash_name @@ -198,28 +197,37 @@ def __init__(self, hash_name="SHA256", tls_version=0x0303): tls_version == 0x0302): # TLS 1.1 self.prf = _tls_PRF elif tls_version == 0x0303: # TLS 1.2 - if hash_name == "SHA384": + if hash_name == "sha384": self.prf = _tls12_SHA384PRF - elif hash_name == "SHA512": + elif hash_name == "sha512": self.prf = _tls12_SHA512PRF else: + if hash_name in ["md5", "sha"]: + self.hash_name = "sha256" self.prf = _tls12_SHA256PRF else: warning("Unknown TLS version") - def compute_master_secret(self, pre_master_secret, - client_random, server_random): + def compute_master_secret(self, pre_master_secret, client_random, + server_random, extms=False, handshake_hash=None): """ Return the 48-byte master_secret, computed from pre_master_secret, client_random and server_random. See RFC 5246, section 6.3. + Supports Extended Master Secret Derivation, see RFC 7627 """ seed = client_random + server_random + label = b'master secret' + + if extms is True and handshake_hash is not None: + seed = handshake_hash + label = b'extended master secret' + if self.tls_version < 0x0300: return None elif self.tls_version == 0x0300: return self.prf(pre_master_secret, seed, 48) else: - return self.prf(pre_master_secret, b"master secret", seed, 48) + return self.prf(pre_master_secret, label, seed, 48) def derive_key_block(self, master_secret, server_random, client_random, req_len): @@ -262,8 +270,8 @@ def compute_verify_data(self, con_end, read_or_write, sslv3_sha1_pad1 = b"\x36" * 40 sslv3_sha1_pad2 = b"\x5c" * 40 - md5 = _tls_hash_algs["MD5"]() - sha1 = _tls_hash_algs["SHA"]() + md5 = _tls_hash_algs["md5"]() + sha1 = _tls_hash_algs["sha"]() md5_hash = md5.digest(master_secret + sslv3_md5_pad2 + md5.digest(handshake_msg + label + @@ -282,14 +290,11 @@ def compute_verify_data(self, con_end, read_or_write, label = ("%s finished" % d[con_end]).encode() if self.tls_version <= 0x0302: - s1 = _tls_hash_algs["MD5"]().digest(handshake_msg) - s2 = _tls_hash_algs["SHA"]().digest(handshake_msg) + s1 = _tls_hash_algs["md5"]().digest(handshake_msg) + s2 = _tls_hash_algs["sha"]().digest(handshake_msg) verify_data = self.prf(master_secret, label, s1 + s2, 12) else: - if self.hash_name in ["MD5", "SHA"]: - h = _tls_hash_algs["SHA256"]() - else: - h = _tls_hash_algs[self.hash_name]() + h = _tls_hash_algs[self.hash_name]() s = h.digest(handshake_msg) verify_data = self.prf(master_secret, label, s, 12) @@ -312,7 +317,7 @@ def postprocess_key_for_export(self, key, client_random, server_random, tbh = key + client_random + server_random else: tbh = key + server_random + client_random - export_key = _tls_hash_algs["MD5"]().digest(tbh)[:req_len] + export_key = _tls_hash_algs["md5"]().digest(tbh)[:req_len] else: if s: tag = b"client write key" @@ -341,7 +346,7 @@ def generate_iv_for_export(self, client_random, server_random, tbh = client_random + server_random else: tbh = server_random + client_random - iv = _tls_hash_algs["MD5"]().digest(tbh)[:req_len] + iv = _tls_hash_algs["md5"]().digest(tbh)[:req_len] else: iv_block = self.prf("", b"IV block", diff --git a/scapy/layers/tls/crypto/suites.py b/scapy/layers/tls/crypto/suites.py index 3644c2f8884..1626a442717 100644 --- a/scapy/layers/tls/crypto/suites.py +++ b/scapy/layers/tls/crypto/suites.py @@ -1,7 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ TLS cipher suites. @@ -10,12 +11,10 @@ https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml """ -from __future__ import absolute_import from scapy.layers.tls.crypto.kx_algs import _tls_kx_algs from scapy.layers.tls.crypto.hash import _tls_hash_algs from scapy.layers.tls.crypto.h_mac import _tls_hmac_algs from scapy.layers.tls.crypto.ciphers import _tls_cipher_algs -import scapy.modules.six as six def get_algs_from_ciphersuite_name(ciphersuite_name): @@ -30,7 +29,7 @@ class and the HMAC class, through the parsing of the ciphersuite name. if s.endswith("CCM") or s.endswith("CCM_8"): kx_name, s = s.split("_WITH_") kx_alg = _tls_kx_algs.get(kx_name) - hash_alg = _tls_hash_algs.get("SHA256") + hash_alg = _tls_hash_algs.get("sha256") cipher_alg = _tls_cipher_algs.get(s) hmac_alg = None @@ -43,7 +42,7 @@ class and the HMAC class, through the parsing of the ciphersuite name. kx_alg = _tls_kx_algs.get("TLS13") hash_name = s.split('_')[-1] - hash_alg = _tls_hash_algs.get(hash_name) + hash_alg = _tls_hash_algs.get(hash_name.lower()) cipher_name = s[:-(len(hash_name) + 1)] if tls1_3: @@ -62,7 +61,7 @@ class and the HMAC class, through the parsing of the ciphersuite name. cipher_alg = _tls_cipher_algs.get(cipher_name.rstrip("_EXPORT40")) kx_alg.export = cipher_name.endswith("_EXPORT40") hmac_alg = _tls_hmac_algs.get("HMAC-NULL") - hash_alg = _tls_hash_algs.get(hash_name) + hash_alg = _tls_hash_algs.get(hash_name.lower()) return kx_alg, cipher_alg, hmac_alg, hash_alg, tls1_3 @@ -126,7 +125,7 @@ def __new__(cls, cs_name, bases, dct): return the_class -class _GenericCipherSuite(six.with_metaclass(_GenericCipherSuiteMetaclass, object)): # noqa: E501 +class _GenericCipherSuite(metaclass=_GenericCipherSuiteMetaclass): def __init__(self, tls_version=0x0303): """ Most of the attributes are fixed and have already been set by the @@ -1297,7 +1296,7 @@ class SSL_CK_DES_192_EDE3_CBC_WITH_MD5(_GenericCipherSuite): _tls_cipher_suites[0x5600] = "TLS_FALLBACK_SCSV" -def get_usable_ciphersuites(l, kx): +def get_usable_ciphersuites(li, kx): """ From a list of proposed ciphersuites, this function returns a list of usable cipher suites, i.e. for which key exchange, cipher and hash @@ -1306,14 +1305,14 @@ def get_usable_ciphersuites(l, kx): function matches the one of the proposal. """ res = [] - for c in l: + for c in li: if c in _tls_cipher_suites_cls: - ciph = _tls_cipher_suites_cls[c] - if ciph.usable: + cipher = _tls_cipher_suites_cls[c] + if cipher.usable: # XXX select among RSA and ECDSA cipher suites # according to the key(s) the server was given - if (ciph.kx_alg.anonymous or - kx in ciph.kx_alg.name or - ciph.kx_alg.name == "TLS13"): + if (cipher.kx_alg.anonymous or + kx in cipher.kx_alg.name or + cipher.kx_alg.name == "TLS13"): res.append(c) return res diff --git a/scapy/layers/tls/extensions.py b/scapy/layers/tls/extensions.py index c252f9acc33..552e6d86a1c 100644 --- a/scapy/layers/tls/extensions.py +++ b/scapy/layers/tls/extensions.py @@ -1,19 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2017 Maxence Tury -# This program is published under a GPLv2 license """ TLS handshake extensions. """ -from __future__ import print_function import os import struct -from scapy.fields import ByteEnumField, ByteField, EnumField, FieldLenField, \ - FieldListField, IntField, PacketField, PacketListField, ShortEnumField, \ - ShortField, StrFixedLenField, StrLenField, XStrLenField +from scapy.fields import ( + ByteEnumField, + ByteField, + EnumField, + FieldLenField, + FieldListField, + IntField, + MayEnd, + PacketField, + PacketListField, + ShortEnumField, + ShortField, + StrFixedLenField, + StrLenField, + XStrLenField, +) from scapy.packet import Packet, Raw, Padding from scapy.layers.x509 import X509_Extensions from scapy.layers.tls.basefields import _tls_version @@ -22,6 +35,7 @@ from scapy.layers.tls.session import _GenericTLSSessionInheritance from scapy.layers.tls.crypto.groups import _tls_named_groups from scapy.layers.tls.crypto.suites import _tls_cipher_suites +from scapy.layers.tls.quic import _QuicTransportParametersField from scapy.themes import AnsiColorTheme from scapy.compat import raw from scapy.config import conf @@ -80,6 +94,7 @@ 0x31: "post_handshake_auth", 0x32: "signature_algorithms_cert", 0x33: "key_share", + 0x39: "quic_transport_parameters", # RFC 9000 0x3374: "next_protocol_negotiation", # RFC-draft-agl-tls-nextprotoneg-03 0xff01: "renegotiation_info", # RFC 5746 @@ -178,7 +193,7 @@ def guess_payload_class(self, p): class ServerListField(PacketListField): def i2repr(self, pkt, x): res = [p.servername for p in x] - return "[%s]" % b", ".join(res) + return "[%s]" % ", ".join(repr(x) for x in res) class ServerLenField(FieldLenField): @@ -196,8 +211,8 @@ def addfield(self, pkt, s, val): class TLS_Ext_ServerName(TLS_Ext_PrettyPacketList): # RFC 4366 name = "TLS Extension - Server Name" fields_desc = [ShortEnumField("type", 0, _tls_ext), - FieldLenField("len", None, length_of="servernames", - adjust=lambda pkt, x: x + 2), + MayEnd(FieldLenField("len", None, length_of="servernames", + adjust=lambda pkt, x: x + 2)), ServerLenField("servernameslen", None, length_of="servernames"), ServerListField("servernames", [], ServerName, @@ -207,7 +222,7 @@ class TLS_Ext_ServerName(TLS_Ext_PrettyPacketList): # RFC 4366 class TLS_Ext_EncryptedServerName(TLS_Ext_PrettyPacketList): name = "TLS Extension - Encrypted Server Name" fields_desc = [ShortEnumField("type", 0xffce, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), EnumField("cipher", None, _tls_cipher_suites), ShortEnumField("key_exchange_group", None, _tls_named_groups), @@ -228,7 +243,7 @@ class TLS_Ext_EncryptedServerName(TLS_Ext_PrettyPacketList): class TLS_Ext_MaxFragLen(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Max Fragment Length" fields_desc = [ShortEnumField("type", 1, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("maxfraglen", 4, {1: "2^9", 2: "2^10", 3: "2^11", @@ -238,7 +253,7 @@ class TLS_Ext_MaxFragLen(TLS_Ext_Unknown): # RFC 4366 class TLS_Ext_ClientCertURL(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Client Certificate URL" fields_desc = [ShortEnumField("type", 2, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] _tls_trusted_authority_types = {0: "pre_agreed", @@ -310,7 +325,7 @@ def m2i(self, pkt, m): class TLS_Ext_TrustedCAInd(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Trusted CA Indication" fields_desc = [ShortEnumField("type", 3, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("talen", None, length_of="ta"), _TAListField("ta", [], Raw, length_from=lambda pkt: pkt.talen)] @@ -319,7 +334,7 @@ class TLS_Ext_TrustedCAInd(TLS_Ext_Unknown): # RFC 4366 class TLS_Ext_TruncatedHMAC(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Truncated HMAC" fields_desc = [ShortEnumField("type", 4, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class ResponderID(Packet): @@ -363,7 +378,7 @@ def m2i(self, pkt, m): class TLS_Ext_CSR(TLS_Ext_Unknown): # RFC 4366 name = "TLS Extension - Certificate Status Request" fields_desc = [ShortEnumField("type", 5, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("stype", None, _cert_status_type), _StatusReqField("req", [], Raw, length_from=lambda pkt: pkt.len - 1)] @@ -372,7 +387,7 @@ class TLS_Ext_CSR(TLS_Ext_Unknown): # RFC 4366 class TLS_Ext_UserMapping(TLS_Ext_Unknown): # RFC 4681 name = "TLS Extension - User Mapping" fields_desc = [ShortEnumField("type", 6, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("umlen", None, fmt="B", length_of="um"), FieldListField("um", [], ByteField("umtype", 0), @@ -383,7 +398,7 @@ class TLS_Ext_ClientAuthz(TLS_Ext_Unknown): # RFC 5878 """ XXX Unsupported """ name = "TLS Extension - Client Authz" fields_desc = [ShortEnumField("type", 7, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ] @@ -391,7 +406,7 @@ class TLS_Ext_ServerAuthz(TLS_Ext_Unknown): # RFC 5878 """ XXX Unsupported """ name = "TLS Extension - Server Authz" fields_desc = [ShortEnumField("type", 8, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ] @@ -401,7 +416,7 @@ class TLS_Ext_ServerAuthz(TLS_Ext_Unknown): # RFC 5878 class TLS_Ext_ClientCertType(TLS_Ext_Unknown): # RFC 5081 name = "TLS Extension - Certificate Type (client version)" fields_desc = [ShortEnumField("type", 9, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("ctypeslen", None, length_of="ctypes"), FieldListField("ctypes", [0, 1], ByteEnumField("certtypes", None, @@ -412,14 +427,14 @@ class TLS_Ext_ClientCertType(TLS_Ext_Unknown): # RFC 5081 class TLS_Ext_ServerCertType(TLS_Ext_Unknown): # RFC 5081 name = "TLS Extension - Certificate Type (server version)" fields_desc = [ShortEnumField("type", 9, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("ctype", None, _tls_cert_types)] def _TLS_Ext_CertTypeDispatcher(m, *args, **kargs): """ We need to select the correct one on dissection. We use the length for - that, as 1 for client version would emply an empty list. + that, as 1 for client version would imply an empty list. """ tmp_len = struct.unpack("!H", m[2:4])[0] if tmp_len == 1: @@ -436,7 +451,7 @@ class TLS_Ext_SupportedGroups(TLS_Ext_Unknown): """ name = "TLS Extension - Supported Groups" fields_desc = [ShortEnumField("type", 10, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("groupslen", None, length_of="groups"), FieldListField("groups", [], ShortEnumField("ng", None, @@ -456,7 +471,7 @@ class TLS_Ext_SupportedEllipticCurves(TLS_Ext_SupportedGroups): # RFC 4492 class TLS_Ext_SupportedPointFormat(TLS_Ext_Unknown): # RFC 4492 name = "TLS Extension - Supported Point Format" fields_desc = [ShortEnumField("type", 11, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("ecpllen", None, fmt="B", length_of="ecpl"), FieldListField("ecpl", [0], ByteEnumField("nc", None, @@ -467,7 +482,7 @@ class TLS_Ext_SupportedPointFormat(TLS_Ext_Unknown): # RFC 4492 class TLS_Ext_SignatureAlgorithms(TLS_Ext_Unknown): # RFC 5246 name = "TLS Extension - Signature Algorithms" fields_desc = [ShortEnumField("type", 13, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), SigAndHashAlgsLenField("sig_algs_len", None, length_of="sig_algs"), SigAndHashAlgsField("sig_algs", [], @@ -479,7 +494,7 @@ class TLS_Ext_SignatureAlgorithms(TLS_Ext_Unknown): # RFC 5246 class TLS_Ext_Heartbeat(TLS_Ext_Unknown): # RFC 6520 name = "TLS Extension - Heartbeat" fields_desc = [ShortEnumField("type", 0x0f, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ByteEnumField("heartbeat_mode", 2, {1: "peer_allowed_to_send", 2: "peer_not_allowed_to_send"})] @@ -498,13 +513,13 @@ def guess_payload_class(self, p): class ProtocolListField(PacketListField): def i2repr(self, pkt, x): res = [p.protocol for p in x] - return "[%s]" % b", ".join(res) + return "[%s]" % ", ".join(repr(x) for x in res) class TLS_Ext_ALPN(TLS_Ext_PrettyPacketList): # RFC 7301 name = "TLS Extension - Application Layer Protocol Negotiation" fields_desc = [ShortEnumField("type", 0x10, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("protocolslen", None, length_of="protocols"), ProtocolListField("protocols", [], ProtocolName, length_from=lambda pkt:pkt.protocolslen)] @@ -521,13 +536,13 @@ class TLS_Ext_Padding(TLS_Ext_Unknown): # RFC 7685 class TLS_Ext_EncryptThenMAC(TLS_Ext_Unknown): # RFC 7366 name = "TLS Extension - Encrypt-then-MAC" fields_desc = [ShortEnumField("type", 0x16, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_ExtendedMasterSecret(TLS_Ext_Unknown): # RFC 7627 name = "TLS Extension - Extended Master Secret" fields_desc = [ShortEnumField("type", 0x17, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_SessionTicket(TLS_Ext_Unknown): # RFC 5077 @@ -545,25 +560,25 @@ class TLS_Ext_SessionTicket(TLS_Ext_Unknown): # RFC 5077 class TLS_Ext_KeyShare(TLS_Ext_Unknown): name = "TLS Extension - Key Share (dummy class)" fields_desc = [ShortEnumField("type", 0x33, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_PreSharedKey(TLS_Ext_Unknown): name = "TLS Extension - Pre Shared Key (dummy class)" fields_desc = [ShortEnumField("type", 0x29, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_EarlyDataIndication(TLS_Ext_Unknown): name = "TLS Extension - Early Data" fields_desc = [ShortEnumField("type", 0x2a, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_EarlyDataIndicationTicket(TLS_Ext_Unknown): - name = "TLS Extension - Ticket Early Data Info" + name = "TLS Extension - Early Data Indication Ticket" fields_desc = [ShortEnumField("type", 0x2a, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), IntField("max_early_data_size", 0)] @@ -575,13 +590,13 @@ class TLS_Ext_EarlyDataIndicationTicket(TLS_Ext_Unknown): class TLS_Ext_SupportedVersions(TLS_Ext_Unknown): name = "TLS Extension - Supported Versions (dummy class)" fields_desc = [ShortEnumField("type", 0x2b, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_SupportedVersion_CH(TLS_Ext_Unknown): name = "TLS Extension - Supported Versions (for ClientHello)" fields_desc = [ShortEnumField("type", 0x2b, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("versionslen", None, fmt='B', length_of="versions"), FieldListField("versions", [], @@ -593,7 +608,7 @@ class TLS_Ext_SupportedVersion_CH(TLS_Ext_Unknown): class TLS_Ext_SupportedVersion_SH(TLS_Ext_Unknown): name = "TLS Extension - Supported Versions (for ServerHello)" fields_desc = [ShortEnumField("type", 0x2b, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ShortEnumField("version", None, _tls_version)] @@ -604,7 +619,7 @@ class TLS_Ext_SupportedVersion_SH(TLS_Ext_Unknown): class TLS_Ext_Cookie(TLS_Ext_Unknown): name = "TLS Extension - Cookie" fields_desc = [ShortEnumField("type", 0x2c, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("cookielen", None, length_of="cookie"), XStrLenField("cookie", "", length_from=lambda pkt: pkt.cookielen)] @@ -622,7 +637,7 @@ def build(self): class TLS_Ext_PSKKeyExchangeModes(TLS_Ext_Unknown): name = "TLS Extension - PSK Key Exchange Modes" fields_desc = [ShortEnumField("type", 0x2d, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("kxmodeslen", None, fmt='B', length_of="kxmodes"), FieldListField("kxmodes", [], @@ -634,7 +649,7 @@ class TLS_Ext_PSKKeyExchangeModes(TLS_Ext_Unknown): class TLS_Ext_TicketEarlyDataInfo(TLS_Ext_Unknown): name = "TLS Extension - Ticket Early Data Info" fields_desc = [ShortEnumField("type", 0x2e, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), IntField("max_early_data_size", 0)] @@ -652,13 +667,13 @@ class TLS_Ext_NPN(TLS_Ext_PrettyPacketList): class TLS_Ext_PostHandshakeAuth(TLS_Ext_Unknown): # RFC 8446 name = "TLS Extension - Post Handshake Auth" fields_desc = [ShortEnumField("type", 0x31, _tls_ext), - ShortField("len", None)] + MayEnd(ShortField("len", None))] class TLS_Ext_SignatureAlgorithmsCert(TLS_Ext_Unknown): # RFC 8446 name = "TLS Extension - Signature Algorithms Cert" - fields_desc = [ShortEnumField("type", 0x31, _tls_ext), - ShortField("len", None), + fields_desc = [ShortEnumField("type", 0x32, _tls_ext), + MayEnd(ShortField("len", None)), SigAndHashAlgsLenField("sig_algs_len", None, length_of="sig_algs"), SigAndHashAlgsField("sig_algs", [], @@ -670,7 +685,7 @@ class TLS_Ext_SignatureAlgorithmsCert(TLS_Ext_Unknown): # RFC 8446 class TLS_Ext_RenegotiationInfo(TLS_Ext_Unknown): # RFC 5746 name = "TLS Extension - Renegotiation Indication" fields_desc = [ShortEnumField("type", 0xff01, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), FieldLenField("reneg_conn_len", None, fmt='B', length_of="renegotiated_connection"), StrLenField("renegotiated_connection", "", @@ -680,10 +695,19 @@ class TLS_Ext_RenegotiationInfo(TLS_Ext_Unknown): # RFC 5746 class TLS_Ext_RecordSizeLimit(TLS_Ext_Unknown): # RFC 8449 name = "TLS Extension - Record Size Limit" fields_desc = [ShortEnumField("type", 0x1c, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), ShortField("record_size_limit", None)] +class TLS_Ext_QUICTransportParameters(TLS_Ext_Unknown): # RFC9000 + name = "TLS Extension - QUIC Transport Parameters" + fields_desc = [ShortEnumField("type", 0x39, _tls_ext), + FieldLenField("len", None, length_of="params"), + _QuicTransportParametersField("params", + None, + length_from=lambda pkt: pkt.len)] + + _tls_ext_cls = {0: TLS_Ext_ServerName, 1: TLS_Ext_MaxFragLen, 2: TLS_Ext_ClientCertURL, @@ -717,6 +741,7 @@ class TLS_Ext_RecordSizeLimit(TLS_Ext_Unknown): # RFC 8449 0x33: TLS_Ext_KeyShare, # 0x2f: TLS_Ext_CertificateAuthorities, #XXX # 0x30: TLS_Ext_OIDFilters, #XXX + 0x39: TLS_Ext_QUICTransportParameters, 0x3374: TLS_Ext_NPN, 0xff01: TLS_Ext_RenegotiationInfo, 0xffce: TLS_Ext_EncryptedServerName @@ -727,13 +752,12 @@ class _ExtensionsLenField(FieldLenField): def getfield(self, pkt, s): """ We try to compute a length, usually from a msglen parsed earlier. - If this length is 0, we consider 'selection_present' (from RFC 5246) - to be False. This means that there should not be any length field. - However, with TLS 1.3, zero lengths are always explicit. + If we can not find any length, we consider 'extensions_present' + (from RFC 5246) to be False. """ ext = pkt.get_field(self.length_of) tmp_len = ext.length_from(pkt) - if tmp_len is None or tmp_len <= 0: + if tmp_len is None or tmp_len < 0: v = pkt.tls_session.tls_version if v is None or v < 0x0304: return s, None @@ -760,7 +784,12 @@ def addfield(self, pkt, s, i): i = self.adjust(pkt, f) if i == 0: # for correct build if no ext and not explicitly 0 - return s + v = pkt.tls_session.tls_version + # With TLS 1.3, zero lengths are always explicit. + if v is None or v < 0x0304: + return s + else: + return s + struct.pack(self.fmt, i) return s + struct.pack(self.fmt, i) @@ -774,8 +803,8 @@ def i2len(self, pkt, i): return len(self.i2m(pkt, i)) def getfield(self, pkt, s): - tmp_len = self.length_from(pkt) - if tmp_len is None: + tmp_len = self.length_from(pkt) or 0 + if tmp_len <= 0: return s, [] return s[tmp_len:], self.m2i(pkt, s[:tmp_len]) @@ -826,4 +855,6 @@ def m2i(self, pkt, m): cls = _tls_ext_early_data_cls.get(pkt.msgtype, TLS_Ext_Unknown) res.append(cls(m[:tmp_len + 4], tls_session=pkt.tls_session)) m = m[tmp_len + 4:] + if m: + res.append(conf.raw_layer(m)) return res diff --git a/scapy/layers/tls/handshake.py b/scapy/layers/tls/handshake.py index 49a47166352..ba9a8452535 100644 --- a/scapy/layers/tls/handshake.py +++ b/scapy/layers/tls/handshake.py @@ -1,7 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ TLS handshake fields & logic. @@ -10,18 +12,30 @@ mechanisms which are addressed with keyexchange.py. """ -from __future__ import absolute_import import math +import os import struct from scapy.error import log_runtime, warning -from scapy.fields import ByteEnumField, ByteField, EnumField, Field, \ - FieldLenField, IntField, PacketField, PacketListField, ShortField, \ - StrFixedLenField, StrLenField, ThreeBytesField, UTCTimeField +from scapy.fields import ( + ByteEnumField, + ByteField, + Field, + FieldLenField, + IntField, + PacketField, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + StrFixedLenField, + StrLenField, + ThreeBytesField, + UTCTimeField, +) from scapy.compat import hex_bytes, orb, raw -from scapy.config import conf -from scapy.modules import six +from scapy.config import conf, crypto_validator from scapy.packet import Packet, Raw, Padding from scapy.utils import randstring, repr_hex from scapy.layers.x509 import OCSP_Response @@ -30,17 +44,21 @@ _TLSClientVersionField) from scapy.layers.tls.extensions import (_ExtensionsLenField, _ExtensionsField, _cert_status_type, + TLS_Ext_PostHandshakeAuth, TLS_Ext_SupportedVersion_CH, TLS_Ext_SignatureAlgorithms, TLS_Ext_SupportedVersion_SH, TLS_Ext_EarlyDataIndication, - _tls_hello_retry_magic) + _tls_hello_retry_magic, + TLS_Ext_ExtendedMasterSecret, + TLS_Ext_EncryptThenMAC) from scapy.layers.tls.keyexchange import (_TLSSignature, _TLSServerParamsField, _TLSSignatureField, ServerRSAParams, SigAndHashAlgsField, _tls_hash_sig, SigAndHashAlgsLenField) from scapy.layers.tls.session import (_GenericTLSSessionInheritance, readConnState, writeConnState) +from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_PreSharedKey_CH from scapy.layers.tls.crypto.compression import (_tls_compression_algs, _tls_compression_algs_cls, Comp_NULL, _GenericComp, @@ -96,8 +114,15 @@ def tls_session_update(self, msg_str): """ Covers both post_build- and post_dissection- context updates. """ - self.tls_session.handshake_messages.append(msg_str) - self.tls_session.handshake_messages_parsed.append(self) + # RFC8446 sect 4.4.1 + # "Note, however, that subsequent post-handshake authentications do not + # include each other, just the messages through the end of the main + # handshake." + if self.tls_session.post_handshake: + self.tls_session.post_handshake_messages.append(msg_str) + else: + self.tls_session.handshake_messages.append(msg_str) + self.tls_session.handshake_messages_parsed.append(self) ############################################################################### @@ -132,6 +157,9 @@ def i2h(self, pkt, x): return x return 0 + def i2m(self, pkt, x): + return int(x) if x is not None else 0 + class _TLSRandomBytesField(StrFixedLenField): def i2repr(self, pkt, x): @@ -157,13 +185,12 @@ def __init__(self, name, default, dico, length_from=None, itemfmt="!H"): self.itemsize = struct.calcsize(itemfmt) i2s = self.i2s = {} s2i = self.s2i = {} - for k in six.iterkeys(dico): + for k in dico.keys(): i2s[k] = dico[k] s2i[dico[k]] = k def any2i_one(self, pkt, x): - if (isinstance(x, _GenericCipherSuite) or - isinstance(x, _GenericCipherSuiteMetaclass)): + if isinstance(x, (_GenericCipherSuite, _GenericCipherSuiteMetaclass)): x = x.val if isinstance(x, bytes): x = self.s2i[x] @@ -212,8 +239,7 @@ def i2len(self, pkt, i): class _CompressionMethodsField(_CipherSuitesField): def any2i_one(self, pkt, x): - if (isinstance(x, _GenericComp) or - isinstance(x, _GenericCompMetaclass)): + if isinstance(x, (_GenericComp, _GenericCompMetaclass)): x = x.val if isinstance(x, str): x = self.s2i[x] @@ -293,9 +319,12 @@ def tls_session_update(self, msg_str): along with the raw string representing this handshake message. """ super(TLSClientHello, self).tls_session_update(msg_str) - s = self.tls_session s.advertised_tls_version = self.version + # This ClientHello could be a 1.3 one. Let's store the sid + # in all cases + if self.sidlen and self.sidlen > 0: + s.sid = self.sid self.random_bytes = msg_str[10:38] s.client_random = (struct.pack('!I', self.gmt_unix_time) + self.random_bytes) @@ -306,10 +335,18 @@ def tls_session_update(self, msg_str): if self.ext: for e in self.ext: if isinstance(e, TLS_Ext_SupportedVersion_CH): - s.advertised_tls_version = e.versions[0] - + for ver in sorted(e.versions, reverse=True): + # RFC 8701: GREASE of TLS will send unknown versions + # here. We have to ignore them + if ver in _tls_version: + s.advertised_tls_version = ver + break + if s.sid: + s.middlebox_compatibility = True if isinstance(e, TLS_Ext_SignatureAlgorithms): s.advertised_sig_algs = e.sig_algs + if isinstance(e, TLS_Ext_PostHandshakeAuth): + s.post_handshake_auth = True class TLS13ClientHello(_TLSHandshake): @@ -352,7 +389,66 @@ class TLS13ClientHello(_TLSHandshake): def post_build(self, p, pay): if self.random_bytes is None: p = p[:6] + randstring(32) + p[6 + 32:] - return super(TLS13ClientHello, self).post_build(p, pay) + # We don't call the post_build function from class _TLSHandshake + # to compute the message length because we need that value now + # for the HMAC in binder + tmp_len = len(p) + if self.msglen is None: + sz = tmp_len - 4 + p = struct.pack("!I", (orb(p[0]) << 24) | sz) + p[4:] + s = self.tls_session + if self.ext: + for e in self.ext: + if isinstance(e, TLS_Ext_PreSharedKey_CH): + if s.client_session_ticket: + # For a resumed PSK, the hash function use + # to compute the binder must be the same + # as the one used to establish the original + # connection. For that, we assume that + # the ciphersuite associate with the ticket + # is given as argument to tlsSession + # (see layers/tls/automaton_cli.py for an + # example) + res_suite = s.tls13_ticket_ciphersuite + cs_cls = _tls_cipher_suites_cls[res_suite] + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + s.compute_tls13_early_secrets(external=False) + else: + # For out of band PSK, SHA-256 is used as default + # hash functions for HKDF + hkdf = TLS13_HKDF("sha256") + hash_len = hkdf.hash.digest_size + s.compute_tls13_early_secrets(external=True) + + # RFC8446 4.2.11.2 + # "Each entry in the binders list is computed as an HMAC + # over a transcript hash (see Section 4.4.1) containing a + # partial ClientHello up to and including the + # PreSharedKeyExtension.identities field." + # PSK Binders field is : + # - PSK Binders length (2 bytes) + # - First PSK Binder length (1 byte) + + # HMAC (hash_len bytes) + # The PSK Binder is computed in the same way as the + # Finished message with binder_key as BaseKey + + handshake_context = b"" + if s.tls13_retry: + for m in s.handshake_messages: + handshake_context += m + handshake_context += p[:-hash_len - 3] + + binder_key = s.tls13_derived_secrets["binder_key"] + psk_binder = hkdf.compute_verify_data(binder_key, + handshake_context) + + # Here, we replaced the last 32 bytes of the packet by the + # new HMAC values computed over the ClientHello (without + # the binders) + p = p[:-hash_len] + psk_binder + + return p + pay def tls_session_update(self, msg_str): """ @@ -364,14 +460,23 @@ def tls_session_update(self, msg_str): if self.sidlen and self.sidlen > 0: s.sid = self.sid - self.random_bytes = msg_str[10:38] + s.middlebox_compatibility = True + + self.random_bytes = msg_str[6:38] s.client_random = self.random_bytes if self.ext: for e in self.ext: if isinstance(e, TLS_Ext_SupportedVersion_CH): - self.tls_session.advertised_tls_version = e.versions[0] + for ver in sorted(e.versions, reverse=True): + # RFC 8701: GREASE of TLS will send unknown versions + # here. We have to ignore them + if ver in _tls_version: + s.advertised_tls_version = ver + break if isinstance(e, TLS_Ext_SignatureAlgorithms): s.advertised_sig_algs = e.sig_algs + if isinstance(e, TLS_Ext_PostHandshakeAuth): + s.post_handshake_auth = True ############################################################################### @@ -402,7 +507,7 @@ class TLSServerHello(_TLSHandshake): _SessionIDField("sid", "", length_from=lambda pkt: pkt.sidlen), - EnumField("cipher", None, _tls_cipher_suites), + ShortEnumField("cipher", None, _tls_cipher_suites), _CompressionMethodsField("comp", [0], _tls_compression_algs, itemfmt="B", @@ -410,10 +515,9 @@ class TLSServerHello(_TLSHandshake): _ExtensionsLenField("extlen", None, length_of="ext"), _ExtensionsField("ext", None, - length_from=lambda pkt: (pkt.msglen - - (pkt.sidlen or 0) - # noqa: E501 - 38))] - # 40)) ] + length_from=lambda pkt: ( + pkt.msglen - (pkt.sidlen or 0) - 40 + ))] @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): @@ -423,6 +527,11 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return TLS13ServerHello return TLSServerHello + def build(self, *args, **kargs): + if self.getfieldval("sid") == b"" and self.tls_session: + self.sid = self.tls_session.sid + return super(TLSServerHello, self).build(*args, **kargs) + def post_build(self, p, pay): if self.random_bytes is None: p = p[:10] + randstring(28) + p[10 + 28:] @@ -440,18 +549,28 @@ def tls_session_update(self, msg_str): """ super(TLSServerHello, self).tls_session_update(msg_str) - self.tls_session.tls_version = self.version - self.random_bytes = msg_str[10:38] - self.tls_session.server_random = (struct.pack('!I', - self.gmt_unix_time) + - self.random_bytes) - self.tls_session.sid = self.sid + s = self.tls_session + s.tls_version = self.version + if hasattr(self, 'gmt_unix_time'): + self.random_bytes = msg_str[10:38] + s.server_random = (struct.pack('!I', self.gmt_unix_time) + + self.random_bytes) + else: + s.server_random = self.random_bytes + s.sid = self.sid + + if self.ext: + for e in self.ext: + if isinstance(e, TLS_Ext_ExtendedMasterSecret): + self.tls_session.extms = True + if isinstance(e, TLS_Ext_EncryptThenMAC): + self.tls_session.encrypt_then_mac = True cs_cls = None if self.cipher: cs_val = self.cipher if cs_val not in _tls_cipher_suites_cls: - warning("Unknown cipher suite %d from ServerHello" % cs_val) + warning("Unknown cipher suite %d from ServerHello", cs_val) # we do not try to set a default nor stop the execution else: cs_cls = _tls_cipher_suites_cls[cs_val] @@ -460,20 +579,20 @@ def tls_session_update(self, msg_str): if self.comp: comp_val = self.comp[0] if comp_val not in _tls_compression_algs_cls: - err = "Unknown compression alg %d from ServerHello" % comp_val - warning(err) + err = "Unknown compression alg %d from ServerHello" + warning(err, comp_val) comp_val = 0 comp_cls = _tls_compression_algs_cls[comp_val] - connection_end = self.tls_session.connection_end - self.tls_session.pwcs = writeConnState(ciphersuite=cs_cls, - compression_alg=comp_cls, - connection_end=connection_end, - tls_version=self.version) - self.tls_session.prcs = readConnState(ciphersuite=cs_cls, - compression_alg=comp_cls, - connection_end=connection_end, - tls_version=self.version) + connection_end = s.connection_end + s.pwcs = writeConnState(ciphersuite=cs_cls, + compression_alg=comp_cls, + connection_end=connection_end, + tls_version=self.version) + s.prcs = readConnState(ciphersuite=cs_cls, + compression_alg=comp_cls, + connection_end=connection_end, + tls_version=self.version) _tls_13_server_hello_fields = [ @@ -484,7 +603,7 @@ def tls_session_update(self, msg_str): FieldLenField("sidlen", None, length_of="sid", fmt="B"), _SessionIDField("sid", "", length_from=lambda pkt: pkt.sidlen), - EnumField("cipher", None, _tls_cipher_suites), + ShortEnumField("cipher", None, _tls_cipher_suites), _CompressionMethodsField("comp", [0], _tls_compression_algs, itemfmt="B", @@ -496,7 +615,7 @@ def tls_session_update(self, msg_str): ] -class TLS13ServerHello(_TLSHandshake): +class TLS13ServerHello(TLSServerHello): """ TLS 1.3 ServerHello """ name = "TLS 1.3 Handshake - Server Hello" fields_desc = _tls_13_server_hello_fields @@ -525,38 +644,47 @@ def tls_session_update(self, msg_str): cipher suite (if recognized), and finally we instantiate the write and read connection states. """ - super(TLS13ServerHello, self).tls_session_update(msg_str) - s = self.tls_session + s.server_random = self.random_bytes + s.ciphersuite = self.cipher + s.tls_version = self.version + # Check extensions if self.ext: for e in self.ext: if isinstance(e, TLS_Ext_SupportedVersion_SH): s.tls_version = e.version break - s.server_random = self.random_bytes - s.ciphersuite = self.cipher + + if s.tls_version < 0x304: + # This means that the server does not support TLS 1.3 and ignored + # the initial TLS 1.3 ClientHello. tls_version has been updated + return TLSServerHello.tls_session_update(self, msg_str) + else: + _TLSHandshake.tls_session_update(self, msg_str) cs_cls = None if self.cipher: cs_val = self.cipher if cs_val not in _tls_cipher_suites_cls: - warning("Unknown cipher suite %d from ServerHello" % cs_val) + warning("Unknown cipher suite %d from ServerHello", cs_val) # we do not try to set a default nor stop the execution else: cs_cls = _tls_cipher_suites_cls[cs_val] connection_end = s.connection_end - if connection_end == "server": s.pwcs = writeConnState(ciphersuite=cs_cls, connection_end=connection_end, tls_version=s.tls_version) - s.triggered_pwcs_commit = True + + if not s.middlebox_compatibility: + s.triggered_pwcs_commit = True elif connection_end == "client": s.prcs = readConnState(ciphersuite=cs_cls, connection_end=connection_end, tls_version=s.tls_version) - s.triggered_prcs_commit = True + if not s.middlebox_compatibility: + s.triggered_prcs_commit = True if s.tls13_early_secret is None: # In case the connState was not pre-initialized, we could not @@ -584,17 +712,27 @@ def build(self): fval = self.getfieldval("random_bytes") if fval is None: self.random_bytes = _tls_hello_retry_magic + if self.getfieldval("sid") == b"" and self.tls_session: + self.sid = self.tls_session.sid return _TLSHandshake.build(self) + @crypto_validator def tls_session_update(self, msg_str): s = self.tls_session s.tls13_retry = True s.tls13_client_pubshares = {} + # RFC8446 sect 4.4.1 # If the server responds to a ClientHello with a HelloRetryRequest # The value of the first ClientHello is replaced by a message_hash - cs_cls = _tls_cipher_suites_cls[self.cipher] - hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) - hash_len = hkdf.hash.digest_size + if s.client_session_ticket: + cs_cls = _tls_cipher_suites_cls[s.tls13_ticket_ciphersuite] + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + else: + cs_cls = _tls_cipher_suites_cls[self.cipher] + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + handshake_context = struct.pack("B", 254) handshake_context += struct.pack("B", 0) handshake_context += struct.pack("B", 0) @@ -643,12 +781,14 @@ def post_build_tls_session_update(self, msg_str): connection_end=connection_end, tls_version=s.tls_version) - s.triggered_prcs_commit = True chts = s.tls13_derived_secrets["client_handshake_traffic_secret"] # noqa: E501 s.prcs.tls13_derive_keys(chts) - s.rcs = self.tls_session.prcs - s.triggered_prcs_commit = False + if not s.middlebox_compatibility: + s.rcs = self.tls_session.prcs + s.triggered_prcs_commit = False + else: + s.triggered_prcs_commit = True def post_dissection_tls_session_update(self, msg_str): self.tls_session_update(msg_str) @@ -672,13 +812,14 @@ def post_dissection_tls_session_update(self, msg_str): s.pwcs = writeConnState(ciphersuite=type(s.rcs.ciphersuite), connection_end=connection_end, tls_version=s.tls_version) - - s.triggered_pwcs_commit = True chts = s.tls13_derived_secrets["client_handshake_traffic_secret"] # noqa: E501 s.pwcs.tls13_derive_keys(chts) + if not s.middlebox_compatibility: + s.wcs = self.tls_session.pwcs + s.triggered_pwcs_commit = False + else: + s.triggered_pwcs_commit = True - s.wcs = self.tls_session.pwcs - s.triggered_pwcs_commit = False ############################################################################### # Certificate # ############################################################################### @@ -733,9 +874,9 @@ def getfield(self, pkt, s): if tmp_len is not None: m, ret = s[:tmp_len], s[tmp_len:] while m: - clen = struct.unpack("!I", b'\x00' + m[:3])[0] - lst.append((clen, Cert(m[3:3 + clen]))) - m = m[3 + clen:] + c_len = struct.unpack("!I", b'\x00' + m[:3])[0] + lst.append((c_len, Cert(m[3:3 + c_len]))) + m = m[3 + c_len:] return m + ret, lst def i2m(self, pkt, i): @@ -778,9 +919,9 @@ def getfield(self, pkt, s): m = s if tmp_len is not None: m, ret = s[:tmp_len], s[tmp_len:] - clen = struct.unpack("!I", b'\x00' + m[:3])[0] - len_cert = (clen, Cert(m[3:3 + clen])) - m = m[3 + clen:] + c_len = struct.unpack("!I", b'\x00' + m[:3])[0] + len_cert = (c_len, Cert(m[3:3 + c_len])) + m = m[3 + c_len:] return m + ret, len_cert def i2m(self, pkt, i): @@ -866,11 +1007,11 @@ def post_dissection_tls_session_update(self, msg_str): connection_end = self.tls_session.connection_end if connection_end == "client": if self.certs: - sc = [x.cert[1] for x in self.certs] + sc = [x.cert[1] for x in self.certs if hasattr(x, 'cert')] self.tls_session.server_certs = sc else: if self.certs: - cc = [x.cert[1] for x in self.certs] + cc = [x.cert[1] for x in self.certs if hasattr(x, 'cert')] self.tls_session.client_certs = cc @@ -930,7 +1071,7 @@ def build(self, *args, **kargs): fval = self.getfieldval("sig") if fval is None: s = self.tls_session - if s.pwcs: + if s.pwcs and s.client_random: if not s.pwcs.key_exchange.anonymous: p = self.params if p is None: @@ -1036,7 +1177,7 @@ class TLSCertificateRequest(_TLSHandshake): SigAndHashAlgsLenField("sig_algs_len", None, length_of="sig_algs"), SigAndHashAlgsField("sig_algs", [0x0403, 0x0401, 0x0201], - EnumField("hash_sig", None, _tls_hash_sig), # noqa: E501 + ShortEnumField("hash_sig", None, _tls_hash_sig), # noqa: E501 length_from=lambda pkt: pkt.sig_algs_len), # noqa: E501 FieldLenField("certauthlen", None, fmt="!H", length_of="certauth"), @@ -1056,6 +1197,12 @@ class TLS13CertificateRequest(_TLSHandshake): _ExtensionsField("ext", None, length_from=lambda pkt: pkt.msglen - pkt.cert_req_ctxt_len - 3)] + + def tls_session_update(self, msg_str): + super(TLS13CertificateRequest, self).tls_session_update(msg_str) + self.tls_session.tls13_cert_req_ctxt = self.cert_req_ctxt + + ############################################################################### # ServerHelloDone # ############################################################################### @@ -1078,11 +1225,16 @@ class TLSCertificateVerify(_TLSHandshake): _TLSSignatureField("sig", None, length_from=lambda pkt: pkt.msglen)] + # See https://datatracker.ietf.org/doc/html/rfc8446#section-4.4 for how to compute + # the signature. + def build(self, *args, **kargs): sig = self.getfieldval("sig") if sig is None: s = self.tls_session m = b"".join(s.handshake_messages) + if s.post_handshake: + m += b"".join(s.post_handshake_messages) tls_version = s.tls_version if tls_version is None: tls_version = s.advertised_tls_version @@ -1103,6 +1255,8 @@ def build(self, *args, **kargs): def post_dissection(self, pkt): s = self.tls_session m = b"".join(s.handshake_messages) + if s.post_handshake: + m += b"".join(s.post_handshake_messages) tls_version = s.tls_version if tls_version is None: tls_version = s.advertised_tls_version @@ -1136,9 +1290,9 @@ class _TLSCKExchKeysField(PacketField): __slots__ = ["length_from"] holds_packet = 1 - def __init__(self, name, length_from=None, remain=0): + def __init__(self, name, length_from=None): self.length_from = length_from - PacketField.__init__(self, name, None, None, remain=remain) + PacketField.__init__(self, name, None, None) def m2i(self, pkt, m): """ @@ -1184,6 +1338,37 @@ def build(self, *args, **kargs): self.exchkeys = cls return _TLSHandshake.build(self, *args, **kargs) + def tls_session_update(self, msg_str): + """ + Finalize the EXTMS messages and compute the hash + """ + super(TLSClientKeyExchange, self).tls_session_update(msg_str) + + if self.tls_session.extms: + to_hash = b''.join(self.tls_session.handshake_messages) + # https://tools.ietf.org/html/rfc7627#section-3 + if self.tls_session.tls_version >= 0x303: + # TLS 1.2 uses the same Hash as the PRF + from scapy.layers.tls.crypto.hash import _tls_hash_algs + hash_object = _tls_hash_algs.get( + self.tls_session.prcs.prf.hash_name + )() + self.tls_session.session_hash = hash_object.digest(to_hash) + else: + # Previous TLS version use concatenation of MD5 & SHA1 + from scapy.layers.tls.crypto.hash import Hash_MD5, Hash_SHA + self.tls_session.session_hash = ( + Hash_MD5().digest(to_hash) + Hash_SHA().digest(to_hash) + ) + if self.tls_session.pre_master_secret: + self.tls_session.compute_ms_and_derive_keys() + + if not self.tls_session.master_secret: + # There are still no master secret (we're just passive) + if self.tls_session.use_nss_master_secret_if_present(): + # we have a NSS file + self.tls_session.compute_ms_and_derive_keys() + ############################################################################### # Finished # @@ -1193,7 +1378,7 @@ class _VerifyDataField(StrLenField): def getfield(self, pkt, s): if pkt.tls_session.tls_version == 0x0300: sep = 36 - elif pkt.tls_session.tls_version >= 0x0304: + elif pkt.tls_session.tls_version and pkt.tls_session.tls_version >= 0x0304: sep = pkt.tls_session.rcs.hash.hash_len else: sep = 12 @@ -1211,6 +1396,8 @@ def build(self, *args, **kargs): if fval is None: s = self.tls_session handshake_msg = b"".join(s.handshake_messages) + if s.post_handshake: + handshake_msg += b"".join(s.post_handshake_messages) con_end = s.connection_end tls_version = s.tls_version if tls_version is None: @@ -1220,13 +1407,16 @@ def build(self, *args, **kargs): self.vdata = s.wcs.prf.compute_verify_data(con_end, "write", handshake_msg, ms) else: - self.vdata = s.compute_tls13_verify_data(con_end, "write") + self.vdata = s.compute_tls13_verify_data(con_end, "write", + handshake_msg) return _TLSHandshake.build(self, *args, **kargs) def post_dissection(self, pkt): s = self.tls_session if not s.frozen: handshake_msg = b"".join(s.handshake_messages) + if s.post_handshake: + handshake_msg += b"".join(s.post_handshake_messages) tls_version = s.tls_version if tls_version is None: tls_version = s.advertised_tls_version @@ -1240,7 +1430,8 @@ def post_dissection(self, pkt): log_runtime.info("TLS: invalid Finished received [%s]", pkt_info) # noqa: E501 elif tls_version >= 0x0304: con_end = s.connection_end - verify_data = s.compute_tls13_verify_data(con_end, "read") + verify_data = s.compute_tls13_verify_data(con_end, "read", + handshake_msg) if self.vdata != verify_data: pkt_info = pkt.firstlayer().summary() log_runtime.info("TLS: invalid Finished received [%s]", pkt_info) # noqa: E501 @@ -1251,7 +1442,7 @@ def post_build_tls_session_update(self, msg_str): tls_version = s.tls_version if tls_version is None: tls_version = s.advertised_tls_version - if tls_version >= 0x0304: + if tls_version >= 0x0304 and not s.post_handshake: s.pwcs = writeConnState(ciphersuite=type(s.wcs.ciphersuite), connection_end=s.connection_end, tls_version=s.tls_version) @@ -1261,6 +1452,9 @@ def post_build_tls_session_update(self, msg_str): elif s.connection_end == "client": s.compute_tls13_traffic_secrets_end() s.compute_tls13_resumption_secret() + if s.connection_end == "client": + s.post_handshake = True + s.post_handshake_messages = [] def post_dissection_tls_session_update(self, msg_str): self.tls_session_update(msg_str) @@ -1268,7 +1462,7 @@ def post_dissection_tls_session_update(self, msg_str): tls_version = s.tls_version if tls_version is None: tls_version = s.advertised_tls_version - if tls_version >= 0x0304: + if tls_version >= 0x0304 and not s.post_handshake: s.prcs = readConnState(ciphersuite=type(s.rcs.ciphersuite), connection_end=s.connection_end, tls_version=s.tls_version) @@ -1278,6 +1472,9 @@ def post_dissection_tls_session_update(self, msg_str): elif s.connection_end == "server": s.compute_tls13_traffic_secrets_end() s.compute_tls13_resumption_secret() + if s.connection_end == "server": + s.post_handshake = True + s.post_handshake_messages = [] # Additional handshake messages @@ -1359,7 +1556,7 @@ def getfield(self, pkt, s): _cert_status_cls = {1: OCSP_Response} -class _StatusField(PacketField): +class _StatusField(PacketLenField): def m2i(self, pkt, m): idtype = pkt.status_type cls = self.cls @@ -1375,7 +1572,8 @@ class TLSCertificateStatus(_TLSHandshake): ByteEnumField("status_type", 1, _cert_status_type), ThreeBytesLenField("responselen", None, length_of="response"), - _StatusField("response", None, Raw)] + _StatusField("response", None, Raw, + length_from=lambda pkt: pkt.responselen)] ############################################################################### @@ -1478,8 +1676,32 @@ class TLS13NewSessionTicket(_TLSHandshake): (pkt.ticketlen or 0) - # noqa: E501 pkt.noncelen or 0) - 13)] # noqa: E501 + def build(self): + fval = self.getfieldval("ticket") + if fval == b"": + # Here, the ticket is just a random 48-byte label + # The ticket may also be a self-encrypted and self-authenticated + # value + self.ticket = os.urandom(48) + + fval = self.getfieldval("ticket_nonce") + if fval == b"": + # Nonce is randomly chosen + self.ticket_nonce = os.urandom(32) + + fval = self.getfieldval("ticket_lifetime") + if fval == 0xffffffff: + # ticket_lifetime is set to 12 hours + self.ticket_lifetime = 43200 + + fval = self.getfieldval("ticket_age_add") + if fval == 0: + # ticket_age_add is a random 32-bit value + self.ticket_age_add = struct.unpack("!I", os.urandom(4))[0] + + return _TLSHandshake.build(self) + def post_dissection_tls_session_update(self, msg_str): - self.tls_session_update(msg_str) if self.tls_session.connection_end == "client": self.tls_session.client_session_ticket = self.ticket @@ -1507,6 +1729,25 @@ class TLS13KeyUpdate(_TLSHandshake): ThreeBytesField("msglen", None), ByteEnumField("request_update", 0, _key_update_request)] + def post_build_tls_session_update(self, msg_str): + s = self.tls_session + s.pwcs = writeConnState(ciphersuite=type(s.wcs.ciphersuite), + connection_end=s.connection_end, + tls_version=s.tls_version) + s.triggered_pwcs_commit = True + s.compute_tls13_next_traffic_secrets(s.connection_end, "write") + + def post_dissection_tls_session_update(self, msg_str): + s = self.tls_session + s.prcs = writeConnState(ciphersuite=type(s.rcs.ciphersuite), + connection_end=s.connection_end, + tls_version=s.tls_version) + s.triggered_prcs_commit = True + if s.connection_end == "server": + s.compute_tls13_next_traffic_secrets("client", "read") + elif s.connection_end == "client": + s.compute_tls13_next_traffic_secrets("server", "read") + ############################################################################### # All handshake messages defined in this module # diff --git a/scapy/layers/tls/handshake_sslv2.py b/scapy/layers/tls/handshake_sslv2.py index f83d7310f13..e4a95cabf8e 100644 --- a/scapy/layers/tls/handshake_sslv2.py +++ b/scapy/layers/tls/handshake_sslv2.py @@ -1,6 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2017 Maxence Tury -# This program is published under a GPLv2 license """ SSLv2 handshake fields & logic. @@ -8,7 +9,6 @@ import struct -from scapy.config import conf from scapy.error import log_runtime, warning from scapy.utils import randstring from scapy.fields import ByteEnumField, ByteField, EnumField, FieldLenField, \ @@ -142,8 +142,8 @@ def getfield(self, pkt, s): try: certdata = Cert(s[:tmp_len]) except Exception: - if conf.debug_dissector: - raise + # Packets are sometimes wrongly interpreted as SSLv2 + # (see record.py). We ignore failures silently certdata = s[:tmp_len] return s[tmp_len:], certdata @@ -298,7 +298,7 @@ def post_build(self, pkt, pay): cipher = pkt[1:4] cs_val = struct.unpack("!I", b"\x00" + cipher)[0] if cs_val not in _tls_cipher_suites_cls: - warning("Unknown ciphersuite %d from ClientMasterKey" % cs_val) + warning("Unknown cipher suite %d from ClientMasterKey", cs_val) cs_cls = None else: cs_cls = _tls_cipher_suites_cls[cs_val] @@ -318,7 +318,7 @@ def post_build(self, pkt, pay): else: self.decryptedkey = key - pubkey = self.tls_session.server_certs[0].pubKey + pubkey = self.tls_session.server_certs[0].pubkey self.encryptedkey = pubkey.encrypt(self.decryptedkey) if self.keyarg == b"" and cs_cls.cipher_alg.type == "block": @@ -350,7 +350,7 @@ def tls_session_update(self, msg_str): s = self.tls_session cs_val = self.cipher if cs_val not in _tls_cipher_suites_cls: - warning("Unknown cipher suite %d from ClientMasterKey" % cs_val) + warning("Unknown cipher suite %d from ClientMasterKey", cs_val) cs_cls = None else: cs_cls = _tls_cipher_suites_cls[cs_val] @@ -528,7 +528,7 @@ class SSLv2ServerFinished(_SSLv2Handshake): def build(self, *args, **kargs): fval = self.getfieldval("sid") - if fval == b"": + if fval == b"" and self.tls_session: self.sid = self.tls_session.sid return super(SSLv2ServerFinished, self).build(*args, **kargs) diff --git a/scapy/layers/tls/keyexchange.py b/scapy/layers/tls/keyexchange.py index 03e5b7ca968..20325240c65 100644 --- a/scapy/layers/tls/keyexchange.py +++ b/scapy/layers/tls/keyexchange.py @@ -1,13 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ TLS key exchange logic. """ -from __future__ import absolute_import import math import struct @@ -22,13 +23,22 @@ from scapy.layers.tls.session import _GenericTLSSessionInheritance from scapy.layers.tls.basefields import _tls_version, _TLSClientVersionField from scapy.layers.tls.crypto.pkcs1 import pkcs_i2osp, pkcs_os2ip -from scapy.layers.tls.crypto.groups import _ffdh_groups, _tls_named_curves -import scapy.modules.six as six +from scapy.layers.tls.crypto.groups import ( + _ffdh_groups, + _tls_named_curves, + _tls_named_groups_generate, + _tls_named_groups_import, + _tls_named_groups_pubbytes, +) + if conf.crypto_valid: from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import dh, ec + from cryptography.hazmat.primitives import serialization +if conf.crypto_valid_advanced: + from cryptography.hazmat.primitives.asymmetric import x25519 + from cryptography.hazmat.primitives.asymmetric import x448 ############################################################################### @@ -130,7 +140,7 @@ def getfield(self, pkt, m): s = pkt.tls_session if s.tls_version and s.tls_version < 0x0300: if len(s.client_certs) > 0: - sig_len = s.client_certs[0].pubKey.pubkey.key_size // 8 + sig_len = s.client_certs[0].pubkey.pubkey.key_size // 8 else: warning("No client certificate provided. " "We're making a wild guess about the signature size.") @@ -150,11 +160,9 @@ class _TLSSignature(_GenericTLSSessionInheritance): but if it is provided a TLS context with a tls_version < 0x0303 at initialization, it will fall back to the implicit signature. Even more, the 'sig_len' field won't be used with SSLv2. - - #XXX 'sig_alg' should be set in __init__ depending on the context. """ name = "TLS Digital Signature" - fields_desc = [SigAndHashAlgField("sig_alg", 0x0401, _tls_hash_sig), + fields_desc = [SigAndHashAlgField("sig_alg", None, _tls_hash_sig), SigLenField("sig_len", None, fmt="!H", length_of="sig_val"), SigValField("sig_val", None, @@ -162,14 +170,23 @@ class _TLSSignature(_GenericTLSSessionInheritance): def __init__(self, *args, **kargs): super(_TLSSignature, self).__init__(*args, **kargs) - if (self.tls_session and - self.tls_session.tls_version): - if self.tls_session.tls_version < 0x0303: - self.sig_alg = None - elif self.tls_session.tls_version == 0x0304: - # For TLS 1.3 signatures, set the signature - # algorithm to RSA-PSS - self.sig_alg = 0x0804 + if self.sig_alg is None and "sig_alg" not in kargs: + # Default sig_alg + self.sig_alg = 0x0804 + if self.tls_session and self.tls_session.tls_version: + s = self.tls_session + if s.selected_sig_alg: + self.sig_alg = s.selected_sig_alg + elif s.tls_version < 0x0303: + self.sig_alg = None + elif s.tls_version == 0x0304: + # For TLS 1.3 signatures, set the signature + # algorithm to RSA-PSS + self.sig_alg = 0x0804 + + def post_dissection(self, r): + # for client + self.tls_session.selected_sig_alg = self.sig_alg def _update_sig(self, m, key): """ @@ -183,11 +200,14 @@ def _update_sig(self, m, key): else: self.sig_val = key.sign(m, t='pkcs', h='md5') else: - h, sig = _tls_hash_sig[self.sig_alg].split('+') - if sig.endswith('pss'): - t = "pss" + if self.sig_alg in [0x0807, 0x0808]: # ed25519, ed448 + h, t = _tls_hash_sig[self.sig_alg], None else: - t = "pkcs" + h, sig = _tls_hash_sig[self.sig_alg].split('+') + if sig.endswith('pss'): + t = "pss" + else: + t = "pkcs" self.sig_val = key.sign(m, t=t, h=h) def _verify_sig(self, m, cert): @@ -197,11 +217,14 @@ def _verify_sig(self, m, cert): """ if self.sig_val: if self.sig_alg: - h, sig = _tls_hash_sig[self.sig_alg].split('+') - if sig.endswith('pss'): - t = "pss" + if self.sig_alg in [0x0807, 0x0808]: # ed25519, ed448 + h, t = _tls_hash_sig[self.sig_alg], None else: - t = "pkcs" + h, sig = _tls_hash_sig[self.sig_alg].split('+') + if sig.endswith('pss'): + t = "pss" + else: + t = "pkcs" return cert.verify(m, self.sig_val, t=t, h=h) else: if self.tls_session.tls_version >= 0x0300: @@ -221,9 +244,9 @@ class _TLSSignatureField(PacketField): """ __slots__ = ["length_from"] - def __init__(self, name, default, length_from=None, remain=0): + def __init__(self, name, default, length_from=None): self.length_from = length_from - PacketField.__init__(self, name, default, _TLSSignature, remain=remain) + PacketField.__init__(self, name, default, _TLSSignature) def m2i(self, pkt, m): tmp_len = self.length_from(pkt) @@ -256,9 +279,9 @@ class _TLSServerParamsField(PacketField): """ __slots__ = ["length_from"] - def __init__(self, name, default, length_from=None, remain=0): + def __init__(self, name, default, length_from=None): self.length_from = length_from - PacketField.__init__(self, name, default, None, remain=remain) + PacketField.__init__(self, name, default, None) def m2i(self, pkt, m): s = pkt.tls_session @@ -266,7 +289,7 @@ def m2i(self, pkt, m): if s.prcs: cls = s.prcs.key_exchange.server_kx_msg_cls(m) if cls is None: - return None, Raw(m[:tmp_len]) / Padding(m[tmp_len:]) + return Raw(m[:tmp_len]) / Padding(m[tmp_len:]) return cls(m, tls_session=s) else: try: @@ -278,7 +301,7 @@ def m2i(self, pkt, m): cls = _tls_server_ecdh_cls_guess(m) p = cls(m, tls_session=s) if pkcs_os2ip(p.load[:2]) not in _tls_hash_sig: - return None, Raw(m[:tmp_len]) / Padding(m[tmp_len:]) + return Raw(m[:tmp_len]) / Padding(m[tmp_len:]) return p @@ -328,6 +351,7 @@ def fill_missing(self): self.dh_p = pkcs_i2osp(default_params.p, default_mLen // 8) if self.dh_plen is None: self.dh_plen = len(self.dh_p) + s.kx_group = "ffdhe%s" % (self.dh_plen * 8) if not self.dh_g: self.dh_g = pkcs_i2osp(default_params.g, 1) @@ -364,6 +388,7 @@ def register_pubkey(self): s = self.tls_session s.server_kx_pubkey = public_numbers.public_key(default_backend()) + s.kx_group = "ffdhe%s" % (self.dh_plen * 8) if not s.client_kx_ffdh_params: s.client_kx_ffdh_params = pn.parameters(default_backend()) @@ -446,10 +471,10 @@ def i2m(self, pkt, x): class _ECBasisField(PacketField): __slots__ = ["clsdict", "basis_type_from"] - def __init__(self, name, default, basis_type_from, clsdict, remain=0): + def __init__(self, name, default, basis_type_from, clsdict): self.clsdict = clsdict self.basis_type_from = basis_type_from - PacketField.__init__(self, name, default, None, remain=remain) + PacketField.__init__(self, name, default, None) def m2i(self, pkt, m): basis = self.basis_type_from(pkt) @@ -567,43 +592,26 @@ def fill_missing(self): self.curve_type = _tls_ec_curve_types["named_curve"] if self.named_curve is None: - curve = ec.SECP256R1() - s.server_kx_privkey = ec.generate_private_key(curve, - default_backend()) - self.named_curve = next((cid for cid, name in six.iteritems(_tls_named_curves) # noqa: E501 - if name == curve.name), 0) - else: - curve_name = _tls_named_curves.get(self.named_curve) - if curve_name is None: - # this fallback is arguable - curve = ec.SECP256R1() - else: - curve_cls = ec._CURVE_TYPES.get(curve_name) - if curve_cls is None: - # this fallback is arguable - curve = ec.SECP256R1() - else: - curve = curve_cls() - s.server_kx_privkey = ec.generate_private_key(curve, - default_backend()) + self.named_curve = 23 + + curve_group = self.named_curve + if curve_group not in _tls_named_curves: + # this fallback is arguable + curve_group = 23 # default to secp256r1 + s.server_kx_privkey = _tls_named_groups_generate(curve_group) + s.kx_group = _tls_named_curves.get(curve_group, str(curve_group)) if self.point is None: - pubkey = s.server_kx_privkey.public_key() - try: - # cryptography >= 2.5 - self.point = pubkey.public_bytes( - serialization.Encoding.X962, - serialization.PublicFormat.UncompressedPoint - ) - except TypeError: - # older versions - self.key_exchange = pubkey.public_numbers().encode_point() + self.point = _tls_named_groups_pubbytes( + s.server_kx_privkey + ) + # else, we assume that the user wrote the server_kx_privkey by himself if self.pointlen is None: self.pointlen = len(self.point) if not s.client_kx_ecdh_params: - s.client_kx_ecdh_params = curve + s.client_kx_ecdh_params = curve_group @crypto_validator def register_pubkey(self): @@ -615,19 +623,15 @@ def register_pubkey(self): # if self.point[0] in [b'\x02', b'\x03']: # point_format = 1 - curve_name = _tls_named_curves[self.named_curve] - curve = ec._CURVE_TYPES[curve_name]() s = self.tls_session - try: # cryptography >= 2.5 - import_point = ec.EllipticCurvePublicKey.from_encoded_point - s.server_kx_pubkey = import_point(curve, self.point) - except AttributeError: - import_point = ec.EllipticCurvePublicNumbers.from_encoded_point - pubnum = import_point(curve, self.point) - s.server_kx_pubkey = pubnum.public_key(default_backend()) + s.server_kx_pubkey = _tls_named_groups_import( + self.named_curve, + self.point + ) + s.kx_group = _tls_named_curves.get(self.named_curve, str(self.named_curve)) if not s.client_kx_ecdh_params: - s.client_kx_ecdh_params = curve + s.client_kx_ecdh_params = self.named_curve def post_dissection(self, r): try: @@ -681,6 +685,8 @@ def fill_missing(self): if self.rsamodlen is None: self.rsamodlen = len(self.rsamod) + self.tls_session.kx_group = "rsa%s" % self.rsamodlen + rsaexplen = math.ceil(math.log(pubNum.e) / math.log(2) / 8.) if not self.rsaexp: self.rsaexp = pkcs_i2osp(pubNum.e, rsaexplen) @@ -693,6 +699,7 @@ def register_pubkey(self): m = self.rsamod e = self.rsaexp self.tls_session.server_tmp_rsa_key = PubKeyRSA((e, m, mLen)) + self.tls_session.kx_group = "rsa%s" % mLen def post_dissection(self, pkt): try: @@ -752,16 +759,19 @@ class ClientDiffieHellmanPublic(_GenericTLSSessionInheritance): @crypto_validator def fill_missing(self): s = self.tls_session - params = s.client_kx_ffdh_params - s.client_kx_privkey = params.generate_private_key() + s.client_kx_privkey = s.client_kx_ffdh_params.generate_private_key() pubkey = s.client_kx_privkey.public_key() y = pubkey.public_numbers().y self.dh_Yc = pkcs_i2osp(y, pubkey.key_size // 8) if s.client_kx_privkey and s.server_kx_pubkey: pms = s.client_kx_privkey.exchange(s.server_kx_pubkey) - s.pre_master_secret = pms - s.compute_ms_and_derive_keys() + s.pre_master_secret = pms.lstrip(b"\x00") + if not s.extms: + # If extms is set (extended master secret), the key will + # need the session hash to be computed. This is provided + # by the TLSClientKeyExchange. Same in all occurrences + s.compute_ms_and_derive_keys() def post_build(self, pkt, pay): if not self.dh_Yc: @@ -790,8 +800,9 @@ def post_dissection(self, m): if s.server_kx_privkey and s.client_kx_pubkey: ZZ = s.server_kx_privkey.exchange(s.client_kx_pubkey) - s.pre_master_secret = ZZ - s.compute_ms_and_derive_keys() + s.pre_master_secret = ZZ.lstrip(b"\x00") + if not s.extms: + s.compute_ms_and_derive_keys() def guess_payload_class(self, p): return Padding @@ -810,20 +821,35 @@ class ClientECDiffieHellmanPublic(_GenericTLSSessionInheritance): @crypto_validator def fill_missing(self): s = self.tls_session - params = s.client_kx_ecdh_params - s.client_kx_privkey = ec.generate_private_key(params, - default_backend()) + s.client_kx_privkey = _tls_named_groups_generate( + s.client_kx_ecdh_params + ) + # ecdh_Yc follows ECPoint.point format as defined in + # https://tools.ietf.org/html/rfc8422#section-5.4 pubkey = s.client_kx_privkey.public_key() - x = pubkey.public_numbers().x - y = pubkey.public_numbers().y - self.ecdh_Yc = (b"\x04" + - pkcs_i2osp(x, params.key_size // 8) + - pkcs_i2osp(y, params.key_size // 8)) + if isinstance(pubkey, (x25519.X25519PublicKey, + x448.X448PublicKey)): + self.ecdh_Yc = pubkey.public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw + ) + if s.client_kx_privkey and s.server_kx_pubkey: + pms = s.client_kx_privkey.exchange(s.server_kx_pubkey) + else: + # uncompressed format of an elliptic curve point + x = pubkey.public_numbers().x + y = pubkey.public_numbers().y + self.ecdh_Yc = (b"\x04" + + pkcs_i2osp(x, (pubkey.key_size + 7) // 8) + + pkcs_i2osp(y, (pubkey.key_size + 7) // 8)) + if s.client_kx_privkey and s.server_kx_pubkey: + pms = s.client_kx_privkey.exchange(ec.ECDH(), + s.server_kx_pubkey) if s.client_kx_privkey and s.server_kx_pubkey: - pms = s.client_kx_privkey.exchange(ec.ECDH(), s.server_kx_pubkey) s.pre_master_secret = pms - s.compute_ms_and_derive_keys() + if not s.extms: + s.compute_ms_and_derive_keys() def post_build(self, pkt, pay): if not self.ecdh_Yc: @@ -840,19 +866,16 @@ def post_dissection(self, m): # if there are kx params and keys, we assume the crypto library is ok if s.client_kx_ecdh_params: - try: # cryptography >= 2.5 - import_point = ec.EllipticCurvePublicKey.from_encoded_point - s.client_kx_pubkey = import_point(s.client_kx_ecdh_params, - self.ecdh_Yc) - except AttributeError: - import_point = ec.EllipticCurvePublicNumbers.from_encoded_point - pub_num = import_point(s.client_kx_ecdh_params, self.ecdh_Yc) - s.client_kx_pubkey = pub_num.public_key(default_backend()) + s.client_kx_pubkey = _tls_named_groups_import( + s.client_kx_ecdh_params, + self.ecdh_Yc + ) if s.server_kx_privkey and s.client_kx_pubkey: ZZ = s.server_kx_privkey.exchange(ec.ECDH(), s.client_kx_pubkey) s.pre_master_secret = ZZ - s.compute_ms_and_derive_keys() + if not s.extms: + s.compute_ms_and_derive_keys() # RSA Encryption (standard & export) @@ -880,7 +903,7 @@ class EncryptedPreMasterSecret(_GenericTLSSessionInheritance): @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): - if 'tls_session' in kargs: + if _pkt and 'tls_session' in kargs: s = kargs['tls_session'] if s.server_tmp_rsa_key is None and s.server_rsa_key is None: return _UnEncryptedPreMasterSecret @@ -915,7 +938,8 @@ def pre_dissect(self, m): warning(err) s.pre_master_secret = pms - s.compute_ms_and_derive_keys() + if not s.extms: + s.compute_ms_and_derive_keys() return pms @@ -930,7 +954,8 @@ def post_build(self, pkt, pay): s = self.tls_session s.pre_master_secret = enc - s.compute_ms_and_derive_keys() + if not s.extms: + s.compute_ms_and_derive_keys() if s.server_tmp_rsa_key is not None: enc = s.server_tmp_rsa_key.encrypt(pkt, t="pkcs") diff --git a/scapy/layers/tls/keyexchange_tls13.py b/scapy/layers/tls/keyexchange_tls13.py index 6710af4e5b9..03692ffdccb 100644 --- a/scapy/layers/tls/keyexchange_tls13.py +++ b/scapy/layers/tls/keyexchange_tls13.py @@ -1,6 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ TLS 1.3 key exchange logic. @@ -10,22 +12,35 @@ from scapy.config import conf, crypto_validator from scapy.error import log_runtime -from scapy.fields import FieldLenField, IntField, PacketField, \ - PacketListField, ShortEnumField, ShortField, StrFixedLenField, \ - StrLenField -from scapy.packet import Packet, Padding +from scapy.fields import ( + FieldLenField, + IntField, + PacketField, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + StrFixedLenField, + StrLenField, + XStrLenField, +) +from scapy.packet import Packet from scapy.layers.tls.extensions import TLS_Ext_Unknown, _tls_ext -from scapy.layers.tls.crypto.groups import _tls_named_ffdh_groups, \ - _tls_named_curves, _ffdh_groups, \ - _tls_named_groups -import scapy.modules.six as six +from scapy.layers.tls.cert import PrivKeyECDSA, PrivKeyRSA, PrivKeyEdDSA +from scapy.layers.tls.crypto.groups import ( + _tls_named_curves, + _tls_named_ffdh_groups, + _tls_named_groups, + _tls_named_groups_generate, + _tls_named_groups_import, + _tls_named_groups_pubbytes, +) if conf.crypto_valid: - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import dh, ec + from cryptography.hazmat.primitives.asymmetric import ec if conf.crypto_valid_advanced: - from cryptography.hazmat.primitives.asymmetric import x25519 + from cryptography.hazmat.primitives.asymmetric import ed25519 + from cryptography.hazmat.primitives.asymmetric import ed448 class KeyShareEntry(Packet): @@ -37,8 +52,8 @@ class KeyShareEntry(Packet): name = "Key Share Entry" fields_desc = [ShortEnumField("group", None, _tls_named_groups), FieldLenField("kxlen", None, length_of="key_exchange"), - StrLenField("key_exchange", "", - length_from=lambda pkt: pkt.kxlen)] + XStrLenField("key_exchange", "", + length_from=lambda pkt: pkt.kxlen)] def __init__(self, *args, **kargs): self.privkey = None @@ -60,36 +75,8 @@ def create_privkey(self): """ This is called by post_build() for key creation. """ - if self.group in _tls_named_ffdh_groups: - params = _ffdh_groups[_tls_named_ffdh_groups[self.group]][0] - privkey = params.generate_private_key() - self.privkey = privkey - pubkey = privkey.public_key() - self.key_exchange = pubkey.public_numbers().y - elif self.group in _tls_named_curves: - if _tls_named_curves[self.group] == "x25519": - if conf.crypto_valid_advanced: - privkey = x25519.X25519PrivateKey.generate() - self.privkey = privkey - pubkey = privkey.public_key() - self.key_exchange = pubkey.public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw - ) - elif _tls_named_curves[self.group] != "x448": - curve = ec._CURVE_TYPES[_tls_named_curves[self.group]]() - privkey = ec.generate_private_key(curve, default_backend()) - self.privkey = privkey - pubkey = privkey.public_key() - try: - # cryptography >= 2.5 - self.key_exchange = pubkey.public_bytes( - serialization.Encoding.X962, - serialization.PublicFormat.UncompressedPoint - ) - except TypeError: - # older versions - self.key_exchange = pubkey.public_numbers().encode_point() + self.privkey = _tls_named_groups_generate(self.group) + self.key_exchange = _tls_named_groups_pubbytes(self.privkey) def post_build(self, pkt, pay): if self.group is None: @@ -110,25 +97,10 @@ def post_build(self, pkt, pay): @crypto_validator def register_pubkey(self): - if self.group in _tls_named_ffdh_groups: - params = _ffdh_groups[_tls_named_ffdh_groups[self.group]][0] - pn = params.parameter_numbers() - public_numbers = dh.DHPublicNumbers(self.key_exchange, pn) - self.pubkey = public_numbers.public_key(default_backend()) - elif self.group in _tls_named_curves: - if _tls_named_curves[self.group] == "x25519": - if conf.crypto_valid_advanced: - import_point = x25519.X25519PublicKey.from_public_bytes - self.pubkey = import_point(self.key_exchange) - elif _tls_named_curves[self.group] != "x448": - curve = ec._CURVE_TYPES[_tls_named_curves[self.group]]() - try: # cryptography >= 2.5 - import_point = ec.EllipticCurvePublicKey.from_encoded_point # noqa: E501 - self.pubkey = import_point(curve, self.key_exchange) - except AttributeError: - import_point = ec.EllipticCurvePublicNumbers.from_encoded_point # noqa: E501 - pub_num = import_point(curve, self.key_exchange).public_numbers() # noqa: E501 - self.pubkey = pub_num.public_key(default_backend()) + self.pubkey = _tls_named_groups_import( + self.group, + self.key_exchange + ) def post_dissection(self, r): try: @@ -154,7 +126,7 @@ def post_build(self, pkt, pay): privshares = self.tls_session.tls13_client_privshares for kse in self.client_shares: if kse.privkey: - if _tls_named_curves[kse.group] in privshares: + if _tls_named_groups[kse.group] in privshares: pkt_info = pkt.firstlayer().summary() log_runtime.info("TLS: group %s used twice in the same ClientHello [%s]", kse.group, pkt_info) # noqa: E501 break @@ -166,11 +138,11 @@ def post_dissection(self, r): for kse in self.client_shares: if kse.pubkey: pubshares = self.tls_session.tls13_client_pubshares - if _tls_named_curves[kse.group] in pubshares: + if _tls_named_groups[kse.group] in pubshares: pkt_info = r.firstlayer().summary() log_runtime.info("TLS: group %s used twice in the same ClientHello [%s]", kse.group, pkt_info) # noqa: E501 break - pubshares[_tls_named_curves[kse.group]] = kse.pubkey + pubshares[_tls_named_groups[kse.group]] = kse.pubkey return super(TLS_Ext_KeyShare_CH, self).post_dissection(r) @@ -200,14 +172,15 @@ def post_build(self, pkt, pay): if group_name in self.tls_session.tls13_client_pubshares: privkey = self.server_share.privkey pubkey = self.tls_session.tls13_client_pubshares[group_name] - if group_name in six.itervalues(_tls_named_ffdh_groups): + if group_name in _tls_named_ffdh_groups.values(): pms = privkey.exchange(pubkey) - elif group_name in six.itervalues(_tls_named_curves): - if group_name == "x25519": + elif group_name in _tls_named_curves.values(): + if group_name in ["x25519", "x448"]: pms = privkey.exchange(pubkey) else: pms = privkey.exchange(ec.ECDH(), pubkey) self.tls_session.tls13_dhe_secret = pms + self.tls_session.kx_group = group_name return super(TLS_Ext_KeyShare_SH, self).post_build(pkt, pay) def post_dissection(self, r): @@ -223,25 +196,27 @@ def post_dissection(self, r): if group_name in self.tls_session.tls13_client_privshares: pubkey = self.server_share.pubkey privkey = self.tls_session.tls13_client_privshares[group_name] - if group_name in six.itervalues(_tls_named_ffdh_groups): + if group_name in _tls_named_ffdh_groups.values(): pms = privkey.exchange(pubkey) - elif group_name in six.itervalues(_tls_named_curves): - if group_name == "x25519": + elif group_name in _tls_named_curves.values(): + if group_name in ["x25519", "x448"]: pms = privkey.exchange(pubkey) else: pms = privkey.exchange(ec.ECDH(), pubkey) self.tls_session.tls13_dhe_secret = pms + self.tls_session.kx_group = group_name elif group_name in self.tls_session.tls13_server_privshare: pubkey = self.tls_session.tls13_client_pubshares[group_name] privkey = self.tls_session.tls13_server_privshare[group_name] - if group_name in six.itervalues(_tls_named_ffdh_groups): + if group_name in _tls_named_ffdh_groups.values(): pms = privkey.exchange(pubkey) - elif group_name in six.itervalues(_tls_named_curves): - if group_name == "x25519": + elif group_name in _tls_named_curves.values(): + if group_name in ["x25519", "x448"]: pms = privkey.exchange(pubkey) else: pms = privkey.exchange(ec.ECDH(), pubkey) self.tls_session.tls13_dhe_secret = pms + self.tls_session.kx_group = group_name return super(TLS_Ext_KeyShare_SH, self).post_dissection(r) @@ -261,27 +236,25 @@ class Ticket(Packet): StrFixedLenField("mac", None, 32)] -class TicketField(PacketField): - __slots__ = ["length_from"] - - def __init__(self, name, default, length_from=None, **kargs): - self.length_from = length_from - PacketField.__init__(self, name, default, Ticket, **kargs) - +class TicketField(PacketLenField): def m2i(self, pkt, m): - tmp_len = self.length_from(pkt) - tbd, rem = m[:tmp_len], m[tmp_len:] - return self.cls(tbd) / Padding(rem) + if len(m) < 64: + # Minimum ticket size is 64 bytes + return conf.raw_layer(m) + return self.cls(m) class PSKIdentity(Packet): name = "PSK Identity" fields_desc = [FieldLenField("identity_len", None, length_of="identity"), - TicketField("identity", "", + TicketField("identity", "", Ticket, length_from=lambda pkt: pkt.identity_len), IntField("obfuscated_ticket_age", 0)] + def default_payload_class(self, payload): + return conf.padding_layer + class PSKBinderEntry(Packet): name = "PSK Binder Entry" @@ -290,6 +263,9 @@ class PSKBinderEntry(Packet): StrLenField("binder", "", length_from=lambda pkt: pkt.binder_len)] + def default_payload_class(self, payload): + return conf.padding_layer + class TLS_Ext_PreSharedKey_CH(TLS_Ext_Unknown): # XXX define post_build and post_dissection methods @@ -315,3 +291,69 @@ class TLS_Ext_PreSharedKey_SH(TLS_Ext_Unknown): _tls_ext_presharedkey_cls = {1: TLS_Ext_PreSharedKey_CH, 2: TLS_Ext_PreSharedKey_SH} + + +# Util to find usable signature algorithms + +# TLS 1.3 SignatureScheme is a subset of _tls_hash_sig +_tls13_usable_certificate_verify_algs = [ + # ECDSA algorithms + 0x0403, 0x0503, 0x0603, + # RSASSA-PSS algorithms with public key OID rsaEncryption + 0x0804, 0x0805, 0x0806, + # EdDSA algorithms + 0x0807, 0x0808, +] + +_tls13_usable_certificate_signature_algs = [ + # RSASSA-PKCS1-v1_5 algorithms + 0x0401, 0x0501, 0x0601, + # ECDSA algorithms + 0x0403, 0x0503, 0x0603, + # EdDSA algorithms + 0x0807, 0x0808, + # RSASSA-PSS algorithms with public key OID RSASSA-PSS + 0x0809, 0x080a, 0x080b, + # Legacy algorithms + 0x0201, 0x0203, +] + + +def get_usable_tls13_sigalgs(li, key, location="certificateverify"): + """ + From a list of proposed signature algorithms, this function returns a list of + usable signature algorithms. + The order of the signature algorithms in the list returned by the + function matches the one of the proposal. + """ + from scapy.layers.tls.keyexchange import _tls_hash_sig + res = [] + if isinstance(key, PrivKeyRSA): + kx = "rsa" + elif isinstance(key, PrivKeyECDSA): + kx = "ecdsa" + elif isinstance(key, PrivKeyEdDSA): + if isinstance(key.pubkey.pubkey, ed25519.Ed25519PublicKey): + kx = "ed25519" + elif isinstance(key.pubkey.pubkey, ed448.Ed448PublicKey): + kx = "ed448" + else: + kx = "unknown" + else: + return res + if location == "certificateverify": + algs = _tls13_usable_certificate_verify_algs + elif location == "certificatesignature": + algs = _tls13_usable_certificate_signature_algs + else: + return res + for c in li: + if c in algs: + sigalg = _tls_hash_sig[c] + if "+" in sigalg: + _, sig = sigalg.split('+') + else: + sig = sigalg + if kx in sig: + res.append(c) + return res diff --git a/scapy/layers/tls/quic.py b/scapy/layers/tls/quic.py new file mode 100644 index 00000000000..6580bd887a1 --- /dev/null +++ b/scapy/layers/tls/quic.py @@ -0,0 +1,217 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +RFC9000 QUIC Transport Parameters +""" +import struct + +from scapy.config import conf +from scapy.fields import ( + PacketListField, + FieldLenField, + StrLenField, +) +from scapy.packet import Packet + +from scapy.layers.quic import ( + QuicVarIntField, + QuicVarLenField, + QuicVarEnumField, +) + + +_QUIC_TP_type = { + 0x00: "original_destination_connection_id", + 0x01: "max_idle_timeout", + 0x02: "stateless_reset_token", + 0x03: "max_udp_payload_size", + 0x04: "initial_max_data", + 0x05: "initial_max_stream_data_bidi_local", + 0x06: "initial_max_stream_data_bidi_remote", + 0x07: "initial_max_stream_data_uni", + 0x08: "initial_max_streams_bidi", + 0x09: "initial_max_streams_uni", + 0x0A: "ack_delay_exponent", + 0x0B: "max_ack_delay", + 0x0C: "disable_active_migration", + 0x0D: "preferred_address", + 0x0E: "active_connection_id_limit", + 0x0F: "initial_source_connection_id", + 0x10: "retry_source_connection_id", +} + +# Generic values + + +class QUIC_TP_Unknown(Packet): + name = "QUIC Transport Parameter - Scapy Unknown" + fields_desc = [ + QuicVarEnumField("type", None, _QUIC_TP_type), + QuicVarLenField("len", None, length_of="value"), + StrLenField("value", None, length_from=lambda pkt: pkt.len), + ] + + def default_payload_class(self, _): + return conf.padding_layer + + +class _QUIC_VarInt_Len(FieldLenField): + def i2m(self, pkt, x): + if x is None and pkt is not None: + fld, fval = pkt.getfield_and_val(self.length_of) + value = fld.i2len(pkt, fval) or 0 + if value < 0 or value > 0xFFFFFFFF: + raise struct.error("requires 0 <= number <= 0xFFFFFFFF") + if value < 0x100: + return 1 + elif value < 0x10000: + return 2 + elif value < 0x100000000: + return 3 + else: + return 4 + elif x is None: + return 1 + return x + + +class _QUIC_TP_VarIntValue(QUIC_TP_Unknown): + fields_desc = [ + QuicVarEnumField("type", None, _QUIC_TP_type), + _QUIC_VarInt_Len("len", None, length_of="value", fmt="B"), + QuicVarIntField("value", None), + ] + + +# RFC 9000 sect 18.2 + + +class QUIC_TP_OriginalDestinationConnectionId(QUIC_TP_Unknown): + name = "QUIC Transport Parameters - Original Destination Connection Id" + type = 0x00 + + +class QUIC_TP_MaxIdleTimeout(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Max Idle Timeout" + type = 0x01 + + +class QUIC_TP_StatelessResetToken(QUIC_TP_Unknown): + name = "QUIC Transport Parameters - Stateless Reset Token" + type = 0x02 + + +class QUIC_TP_MaxUdpPayloadSize(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Max Udp Payload Size" + type = 0x03 + + +class QUIC_TP_InitialMaxData(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Initial Max Data" + type = 0x04 + + +class QUIC_TP_InitialMaxStreamDataBidiLocal(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Initial Max Stream Data Bidi Local" + type = 0x05 + + +class QUIC_TP_InitialMaxStreamDataBidiRemote(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Initial Max Stream Data Bidi Remote" + type = 0x06 + + +class QUIC_TP_InitialMaxStreamDataUni(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Initial Max Stream Data Uni" + type = 0x07 + + +class QUIC_TP_InitialMaxStreamsBidi(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Initial Max Streams Bidi" + type = 0x08 + + +class QUIC_TP_InitialMaxStreamsUni(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Initial Max Streams Uni" + type = 0x09 + + +class QUIC_TP_AckDelayExponent(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Ack Delay Exponent" + type = 0x0A + + +class QUIC_TP_MaxAckDelay(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Max Ack Delay" + type = 0x0B + + +class QUIC_TP_DisableActiveMigration(QUIC_TP_Unknown): + name = "QUIC Transport Parameters - Disable Active Migration" + fields_desc = [ + QuicVarEnumField("type", 0x0C, _QUIC_TP_type), + QuicVarIntField("len", 0), + ] + + +class QUIC_TP_PreferredAddress(QUIC_TP_Unknown): + name = "QUIC Transport Parameters - Preferred Address" + type = 0x0D + + +class QUIC_TP_ActiveConnectionIdLimit(_QUIC_TP_VarIntValue): + name = "QUIC Transport Parameters - Active Connection Id Limit" + type = 0x0E + + +class QUIC_TP_InitialSourceConnectionId(QUIC_TP_Unknown): + name = "QUIC Transport Parameters - Initial Source Connection Id" + type = 0x0F + + +class QUIC_TP_RetrySourceConnectionId(QUIC_TP_Unknown): + name = "QUIC Transport Parameters - Retry Source Connection Id" + type = 0x10 + + +_QUIC_TP_cls = { + 0x00: QUIC_TP_OriginalDestinationConnectionId, + 0x01: QUIC_TP_MaxIdleTimeout, + 0x02: QUIC_TP_StatelessResetToken, + 0x03: QUIC_TP_MaxUdpPayloadSize, + 0x04: QUIC_TP_InitialMaxData, + 0x05: QUIC_TP_InitialMaxStreamDataBidiLocal, + 0x06: QUIC_TP_InitialMaxStreamDataBidiRemote, + 0x07: QUIC_TP_InitialMaxStreamDataUni, + 0x08: QUIC_TP_InitialMaxStreamsBidi, + 0x09: QUIC_TP_InitialMaxStreamsUni, + 0x0A: QUIC_TP_AckDelayExponent, + 0x0B: QUIC_TP_MaxAckDelay, + 0x0C: QUIC_TP_DisableActiveMigration, + 0x0D: QUIC_TP_PreferredAddress, + 0x0E: QUIC_TP_ActiveConnectionIdLimit, + 0x0F: QUIC_TP_InitialSourceConnectionId, + 0x10: QUIC_TP_RetrySourceConnectionId, +} + + +class _QuicTransportParametersField(PacketListField): + _varfield = QuicVarIntField("", 0) + + def __init__(self, name, default, **kwargs): + kwargs["next_cls_cb"] = self.cls_from_quictptype + super(_QuicTransportParametersField, self).__init__( + name, + default, + **kwargs, + ) + + @classmethod + def cls_from_quictptype(cls, pkt, lst, cur, remain): + _, typ = cls._varfield.getfield(None, remain) + return _QUIC_TP_cls.get( + typ, + QUIC_TP_Unknown, + ) diff --git a/scapy/layers/tls/record.py b/scapy/layers/tls/record.py index cbebc47b5d3..e6c59456913 100644 --- a/scapy/layers/tls/record.py +++ b/scapy/layers/tls/record.py @@ -1,7 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard -# 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2015, 2016, 2017 Maxence Tury +# 2019 Romain Perez +# 2019 Gabriel Potter """ Common TLS fields & bindings. @@ -34,25 +37,16 @@ from scapy.layers.tls.crypto.cipher_stream import Cipher_NULL from scapy.layers.tls.crypto.common import CipherError from scapy.layers.tls.crypto.h_mac import HMACError -import scapy.modules.six as six if conf.crypto_valid_advanced: from scapy.layers.tls.crypto.cipher_aead import Cipher_CHACHA20_POLY1305 -# Util - - -def _tls_version_check(version, min): - """Returns if version >= min, or False if version == None""" - if version is None: - return False - return version >= min ############################################################################### # TLS Record Protocol # ############################################################################### -class _TLSEncryptedContent(Raw): +class _TLSEncryptedContent(Raw, _GenericTLSSessionInheritance): """ When the content of a TLS record (more precisely, a TLSCiphertext) could not be deciphered, we use this class to represent the encrypted data. @@ -61,6 +55,7 @@ class _TLSEncryptedContent(Raw): version), the nonce_explicit, IV and/or padding will also be parsed. """ name = "Encrypted Content" + match_subclass = True class _TLSMsgListField(PacketListField): @@ -73,7 +68,7 @@ class _TLSMsgListField(PacketListField): def __init__(self, name, default, length_from=None): if not length_from: length_from = self._get_length - super(_TLSMsgListField, self).__init__(name, default, cls=None, + super(_TLSMsgListField, self).__init__(name, default, None, length_from=length_from) def _get_length(self, pkt): @@ -90,9 +85,16 @@ def m2i(self, pkt, m): if pkt.type == 22: if len(m) >= 1: msgtype = orb(m[0]) - if ((pkt.tls_session.advertised_tls_version == 0x0304) or - (pkt.tls_session.tls_version and - pkt.tls_session.tls_version == 0x0304)): + # If a version was agreed on by both client and server, + # we use it (tls_session.tls_version) + # Otherwise, if the client advertised for TLS 1.3, we try to + # dissect the following packets (most likely, server hello) + # using TLS 1.3. The serverhello is able to fallback on + # TLS 1.2 if necessary. In any case, this will set the agreed + # version so that all future packets are correct. + if ((pkt.tls_session.advertised_tls_version == 0x0304 and + pkt.tls_session.tls_version is None) or + pkt.tls_session.tls_version == 0x0304): cls = _tls13_handshake_cls.get(msgtype, Raw) else: cls = _tls_handshake_cls.get(msgtype, Raw) @@ -140,10 +142,12 @@ def getfield(self, pkt, s): if (((pkt.tls_session.tls_version or 0x0303) > 0x0200) and hasattr(pkt, "type") and pkt.type == 23): return ret, [TLSApplicationData(data=b"")] + elif hasattr(pkt, "type") and pkt.type == 20: + return ret, [TLSChangeCipherSpec()] else: return ret, [Raw(load=b"")] - if False in six.itervalues(pkt.tls_session.rcs.cipher.ready): + if False in pkt.tls_session.rcs.cipher.ready.values(): return ret, _TLSEncryptedContent(remain) else: while remain: @@ -202,10 +206,12 @@ def addfield(self, pkt, s, val): res += self.i2m(pkt, p) # Add TLS13ClientHello in case of HelloRetryRequest + # Add ChangeCipherSpec for middlebox compatibility if (isinstance(pkt, _GenericTLSSessionInheritance) and - _tls_version_check(pkt.tls_session.tls_version, 0x0304) and + pkt.tls_session.tls_version == 0x0304 and not isinstance(pkt.msg[0], TLS13ServerHello) and - not isinstance(pkt.msg[0], TLS13ClientHello)): + not isinstance(pkt.msg[0], TLS13ClientHello) and + not isinstance(pkt.msg[0], TLSChangeCipherSpec)): return s + res if not pkt.type: @@ -215,6 +221,16 @@ def addfield(self, pkt, s, val): return hdr + res +def _ssl_looks_like_sslv2(dat): + """ + This is a copycat of wireshark's `packet-tls.c` ssl_looks_like_sslv2 + """ + if len(dat) < 3: + return + from scapy.layers.tls.handshake_sslv2 import _sslv2_handshake_type + return ord(dat[:1]) >= 0x80 and ord(dat[2:3]) in _sslv2_handshake_type + + class TLS(_GenericTLSSessionInheritance): """ The generic TLS Record message, based on section 6.2 of RFC 5246. @@ -299,18 +315,38 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): plen = len(_pkt) if plen >= 2: byte0, byte1 = struct.unpack("BB", _pkt[:2]) - if (byte0 not in _tls_type) or (byte1 != 3): - from scapy.layers.tls.record_sslv2 import SSLv2 - return SSLv2 s = kargs.get("tls_session", None) - if s and _tls_version_check(s.tls_version, 0x0304): - if s.rcs and not isinstance(s.rcs.cipher, Cipher_NULL): + if byte0 not in _tls_type or byte1 != 3: # Unknown type + # Check SSLv2: either the session is already SSLv2, + # either the packet looks like one. As said above, this + # isn't 100% reliable, but Wireshark does the same + if s and (s.tls_version == 0x0002 or + s.advertised_tls_version == 0x0002) or \ + (_ssl_looks_like_sslv2(_pkt) and (not s or + s.tls_version is None)): + from scapy.layers.tls.record_sslv2 import SSLv2 + return SSLv2 + # Not SSLv2: continuation + return _TLSEncryptedContent + if plen >= 5: + # Check minimum length + msglen = struct.unpack('!H', _pkt[3:5])[0] + 5 + if plen < msglen: + # This is a fragment + return conf.padding_layer + # Check TLS 1.3 + if s and s.tls_version == 0x0304: + _has_cipher = lambda x: ( + x and not isinstance(x.cipher, Cipher_NULL) + ) + if (_has_cipher(s.rcs) or _has_cipher(s.prcs)) and \ + byte0 == 0x17: from scapy.layers.tls.record_tls13 import TLS13 return TLS13 if plen < 5: # Layer detected as TLS but too small to be a # parsed. Scapy should not try to decode them - return conf.raw_layer + return _TLSEncryptedContent return TLS # Parsing methods @@ -410,9 +446,32 @@ def pre_dissect(self, s): cipher_type = self.tls_session.rcs.cipher.type + def extract_mac(data): + """Extract MAC.""" + tmp_len = self.tls_session.rcs.mac_len + if tmp_len != 0: + frag, mac = data[:-tmp_len], data[-tmp_len:] + else: + frag, mac = data, b"" + return frag, mac + + def verify_mac(hdr, cfrag, mac): + """Verify integrity.""" + chdr = hdr[:3] + struct.pack('!H', len(cfrag)) + is_mac_ok = self._tls_hmac_verify(chdr, cfrag, mac) + if not is_mac_ok: + pkt_info = self.firstlayer().summary() + log_runtime.info( + "TLS: record integrity check failed [%s]", pkt_info, + ) + if cipher_type == 'block': version = struct.unpack("!H", s[1:3])[0] + if self.tls_session.encrypt_then_mac: + efrag, mac = extract_mac(efrag) + verify_mac(hdr, efrag, mac) + # Decrypt try: if version >= 0x0302: @@ -446,19 +505,11 @@ def pre_dissect(self, s): mfrag, pad = pfrag[:-padlen], pfrag[-padlen:] self.padlen = padlen - # Extract MAC - tmp_len = self.tls_session.rcs.mac_len - if tmp_len != 0: - cfrag, mac = mfrag[:-tmp_len], mfrag[-tmp_len:] + if self.tls_session.encrypt_then_mac: + cfrag = mfrag else: - cfrag, mac = mfrag, b"" - - # Verify integrity - chdr = hdr[:3] + struct.pack('!H', len(cfrag)) - is_mac_ok = self._tls_hmac_verify(chdr, cfrag, mac) - if not is_mac_ok: - pkt_info = self.firstlayer().summary() - log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 + cfrag, mac = extract_mac(mfrag) + verify_mac(hdr, cfrag, mac) elif cipher_type == 'stream': # Decrypt @@ -469,21 +520,8 @@ def pre_dissect(self, s): cfrag = e.args[0] else: decryption_success = True - mfrag = pfrag - - # Extract MAC - tmp_len = self.tls_session.rcs.mac_len - if tmp_len != 0: - cfrag, mac = mfrag[:-tmp_len], mfrag[-tmp_len:] - else: - cfrag, mac = mfrag, b"" - - # Verify integrity - chdr = hdr[:3] + struct.pack('!H', len(cfrag)) - is_mac_ok = self._tls_hmac_verify(chdr, cfrag, mac) - if not is_mac_ok: - pkt_info = self.firstlayer().summary() - log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 + cfrag, mac = extract_mac(pfrag) + verify_mac(hdr, cfrag, mac) elif cipher_type == 'aead': # Authenticated encryption @@ -535,12 +573,24 @@ def do_dissect_payload(self, s): as the TLS session to be used would get lost. """ if s: + # Check minimum length + if len(s) < 5: + p = conf.raw_layer(s, _internal=1, _underlayer=self) + self.add_payload(p) + return + msglen = struct.unpack('!H', s[3:5])[0] + 5 + if len(s) < msglen: + # This is a fragment + self.add_payload(conf.padding_layer(s)) + return try: p = TLS(s, _internal=1, _underlayer=self, tls_session=self.tls_session) except KeyboardInterrupt: raise except Exception: + if conf.debug_dissector: + raise p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) @@ -636,7 +686,8 @@ def post_build(self, pkt, pay): if cipher_type == 'block': # Integrity - mfrag = self._tls_hmac_add(hdr, cfrag) + if not self.tls_session.encrypt_then_mac: + cfrag = self._tls_hmac_add(hdr, cfrag) # Excerpt below better corresponds to TLS 1.1 IV definition, # but the result is the same as with TLS 1.2 anyway. @@ -646,7 +697,7 @@ def post_build(self, pkt, pay): # mfrag = iv + mfrag # Add padding - pfrag = self._tls_pad(mfrag) + pfrag = self._tls_pad(cfrag) # Encryption if self.version >= 0x0302: @@ -660,6 +711,9 @@ def post_build(self, pkt, pay): # Implicit IV for SSLv3 and TLS 1.0 efrag = self._tls_encrypt(pfrag) + if self.tls_session.encrypt_then_mac: + efrag = self._tls_hmac_add(hdr, efrag) + elif cipher_type == "stream": # Integrity mfrag = self._tls_hmac_add(hdr, cfrag) @@ -689,11 +743,18 @@ def post_build(self, pkt, pay): return hdr + efrag + pay + def mysummary(self): + s, n = super(TLS, self).mysummary() + if self.msg: + s += " / " + s += " / ".join(getattr(x, "_name", x.name) for x in self.msg) + return s, n ############################################################################### # TLS ChangeCipherSpec # ############################################################################### + _tls_changecipherspec_type = {1: "change_cipher_spec"} @@ -734,10 +795,12 @@ def post_build_tls_session_update(self, msg_str): 50: "decode_error", 51: "decrypt_error", 60: "export_restriction_RESERVED", 70: "protocol_version", 71: "insufficient_security", 80: "internal_error", - 90: "user_canceled", 100: "no_renegotiation", + 86: "inappropriate_fallback", 90: "user_canceled", + 100: "no_renegotiation", 109: "missing_extension", 110: "unsupported_extension", 111: "certificate_unobtainable", 112: "unrecognized_name", 113: "bad_certificate_status_response", - 114: "bad_certificate_hash_value", 115: "unknown_psk_identity"} + 114: "bad_certificate_hash_value", 115: "unknown_psk_identity", + 116: "certificate_required", 120: "no_application_protocol"} class TLSAlert(_GenericTLSSessionInheritance): @@ -745,6 +808,9 @@ class TLSAlert(_GenericTLSSessionInheritance): fields_desc = [ByteEnumField("level", None, _tls_alert_level), ByteEnumField("descr", None, _tls_alert_description)] + def mysummary(self): + return self.sprintf("Alert %level%: %descr%") + def post_dissection_tls_session_update(self, msg_str): pass diff --git a/scapy/layers/tls/record_sslv2.py b/scapy/layers/tls/record_sslv2.py index 501fbd4a1cc..8e99a3522b2 100644 --- a/scapy/layers/tls/record_sslv2.py +++ b/scapy/layers/tls/record_sslv2.py @@ -1,6 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2017 Maxence Tury -# This program is published under a GPLv2 license """ SSLv2 Record. @@ -29,7 +30,8 @@ def __init__(self, name, default, length_from=None): length_from = lambda pkt: ((pkt.len & 0x7fff) - (pkt.padlen or 0) - len(pkt.mac)) - super(_SSLv2MsgListField, self).__init__(name, default, length_from) + super(_SSLv2MsgListField, self).__init__(name, default, + length_from=length_from) def m2i(self, pkt, m): cls = Raw @@ -139,7 +141,7 @@ def pre_dissect(self, s): is_mac_ok = self._sslv2_mac_verify(cfrag + pad, mac) if not is_mac_ok: pkt_info = self.firstlayer().summary() - log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 + log_runtime.info("SSLv2: record integrity check failed [%s]", pkt_info) # noqa: E501 reconstructed_body = mac + cfrag + pad return hdr + reconstructed_body + r @@ -172,7 +174,7 @@ def do_dissect_payload(self, s): except KeyboardInterrupt: raise except Exception: - if conf.debug_dissect: + if conf.debug_dissector: raise p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) diff --git a/scapy/layers/tls/record_tls13.py b/scapy/layers/tls/record_tls13.py index 7e2211092e9..ff8f0acec4d 100644 --- a/scapy/layers/tls/record_tls13.py +++ b/scapy/layers/tls/record_tls13.py @@ -1,6 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ Common TLS 1.3 fields & bindings. @@ -13,7 +15,6 @@ import struct -from scapy.config import conf from scapy.error import log_runtime, warning from scapy.compat import raw, orb from scapy.fields import ByteEnumField, PacketField, XStrField @@ -123,7 +124,7 @@ def _tls_auth_decrypt(self, s): return e.args except AEADTagError as e: pkt_info = self.firstlayer().summary() - log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 + log_runtime.info("TLS 1.3: record integrity check failed [%s]", pkt_info) # noqa: E501 return e.args def pre_dissect(self, s): @@ -170,15 +171,7 @@ def do_dissect_payload(self, s): Note that overloading .guess_payload_class() would not be enough, as the TLS session to be used would get lost. """ - if s: - try: - p = TLS(s, _internal=1, _underlayer=self, - tls_session=self.tls_session) - except KeyboardInterrupt: - raise - except Exception: - p = conf.raw_layer(s, _internal=1, _underlayer=self) - self.add_payload(p) + return TLS.do_dissect_payload(self, s) # Building methods @@ -221,3 +214,10 @@ def post_build(self, pkt, pay): self.tls_session.triggered_pwcs_commit = False return hdr + frag + pay + + def mysummary(self): + s, n = super(TLS13, self).mysummary() + if self.inner and self.inner.msg: + s += " / " + s += " / ".join(getattr(x, "_name", x.name) for x in self.inner.msg) + return s, n diff --git a/scapy/layers/tls/session.py b/scapy/layers/tls/session.py index 24b2d111b8a..de9f342bb46 100644 --- a/scapy/layers/tls/session.py +++ b/scapy/layers/tls/session.py @@ -1,25 +1,82 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license +# 2019 Romain Perez """ TLS session handler. """ +import binascii +import collections import socket import struct from scapy.config import conf from scapy.compat import raw -import scapy.modules.six as six from scapy.error import log_runtime, warning from scapy.packet import Packet +from scapy.pton_ntop import inet_pton +from scapy.sessions import TCPSession from scapy.utils import repr_hex, strxor +from scapy.layers.inet import TCP from scapy.layers.tls.crypto.compression import Comp_NULL from scapy.layers.tls.crypto.hkdf import TLS13_HKDF from scapy.layers.tls.crypto.prf import PRF +# Typing imports +from typing import Dict + + +def load_nss_keys(filename): + # type: (str) -> Dict[str, bytes] + """ + Parses a NSS Keys log and returns unpacked keys in a dictionary. + """ + # http://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format + keys = collections.defaultdict(dict) + try: + fd = open(filename) + fd.close() + except FileNotFoundError: + warning("Cannot open NSS Key Log: %s", filename) + return {} + try: + with open(filename) as fd: + for line in fd: + if line.startswith("#"): + continue + data = line.strip().split(" ") + if len(data) != 3 or data[0] != data[0].upper(): + warning("Invalid NSS Key Log Entry: %s", line.strip()) + return {} + + try: + client_random = binascii.unhexlify(data[1]) + except ValueError: + warning("Invalid ClientRandom: %s", data[1]) + return {} + + try: + secret = binascii.unhexlify(data[2]) + except ValueError: + warning("Invalid Secret: %s", data[2]) + return {} + + # Warn that a duplicated entry was detected. The latest one + # will be kept in the resulting dictionary. + if client_random in keys[data[0]]: + warning("Duplicated entry for %s !", data[0]) + + keys[data[0]][client_random] = secret + return keys + except UnicodeDecodeError as ex: + warning("Cannot read NSS Key Log: %s %s", filename, str(ex)) + return {} + + # Note the following import may happen inside connState.__init__() # in order to avoid to avoid cyclical dependencies. # from scapy.layers.tls.crypto.suites import TLS_NULL_WITH_NULL_NULL @@ -87,7 +144,8 @@ def __init__(self, self.ciphersuite = ciphersuite(tls_version=tls_version) if not self.ciphersuite.usable: - warning("TLS ciphersuite not usable. Is the cryptography Python module installed ?") # noqa: E501 + warning("TLS cipher suite not usable. " + "Is the cryptography Python module installed?") return self.compression = compression_alg() @@ -314,6 +372,9 @@ def __init__(self, self.dport = dport self.sid = sid + # Identify duplicate sessions + self.firsttcp = None + # Our TCP socket. None until we send (or receive) a packet. self.sock = None @@ -361,6 +422,10 @@ def __init__(self, self.server_rsa_key = None # self.server_ecdsa_key = None + # A dictionary containing keys extracted from a NSS Keys Log using + # the load_nss_keys() function. + self.nss_keys = None + # Back in the dreadful EXPORT days, US servers were forbidden to use # RSA keys longer than 512 bits for RSAkx. When their usual RSA key # was longer than this, they had to create a new key and send it via @@ -376,6 +441,9 @@ def __init__(self, # Ephemeral key exchange parameters + # The agreed-upon ephemeral key group + self.kx_group = None + # These are the group/curve parameters, needed to hold the information # e.g. from receiving an SKE to sending a CKE. Usually, only one of # these attributes will be different from None. @@ -418,6 +486,9 @@ def __init__(self, self.pre_master_secret = None self.master_secret = None + # The advertised supported signature algorithms found in the ClientHello + # extension. (for TLS 1.2-TLS 1.3 only) + self.advertised_sig_algs = [] # The agreed-upon signature algorithm (for TLS 1.2-TLS 1.3 only) self.selected_sig_alg = None @@ -439,12 +510,27 @@ def __init__(self, self.tls13_handshake_secret = None self.tls13_master_secret = None self.tls13_derived_secrets = {} + self.tls13_cert_req_ctxt = False + self.post_handshake = False # whether handshake is done + self.post_handshake_auth = False # whether "Post-Handshake Auth" is used + self.tls13_ticket_ciphersuite = None + self.tls13_retry = False + self.middlebox_compatibility = False # Handshake messages needed for Finished computation/validation. # No record layer headers, no HelloRequests, no ChangeCipherSpecs. self.handshake_messages = [] self.handshake_messages_parsed = [] + # Post-handshake, handshake messages for post-handshake client authentication + self.post_handshake_messages = [] + + # Flag, whether we derive the secret as Extended MS or not + self.extms = False + self.session_hash = None + + self.encrypt_then_mac = False + # All exchanged TLS packets. # XXX no support for now # self.exchanged_pkts = [] @@ -461,6 +547,21 @@ def __setattr__(self, name, val): self.pwcs.connection_end = val super(tlsSession, self).__setattr__(name, val) + # Get infos from underlayer + + def set_underlayer(self, _underlayer): + if isinstance(_underlayer, TCP): + tcp = _underlayer + self.sport = tcp.sport + self.dport = tcp.dport + try: + self.ipsrc = tcp.underlayer.src + self.ipdst = tcp.underlayer.dst + except AttributeError: + pass + if self.firsttcp is None: + self.firsttcp = tcp.seq + # Mirroring def mirror(self): @@ -473,15 +574,15 @@ def mirror(self): client and the server. In such a situation, it should be used every time the message being read comes from a different side than the one read right before, as the reading state becomes the writing state, and - vice versa. For instance you could do: + vice versa. For instance you could do:: - client_hello = open('client_hello.raw').read() - + client_hello = open('client_hello.raw').read() + - m1 = TLS(client_hello) - m2 = TLS(server_hello, tls_session=m1.tls_session.mirror()) - m3 = TLS(server_cert, tls_session=m2.tls_session) - m4 = TLS(client_keyexchange, tls_session=m3.tls_session.mirror()) + m1 = TLS(client_hello) + m2 = TLS(server_hello, tls_session=m1.tls_session.mirror()) + m3 = TLS(server_cert, tls_session=m2.tls_session) + m4 = TLS(client_keyexchange, tls_session=m3.tls_session.mirror()) """ self.ipdst, self.ipsrc = self.ipsrc, self.ipdst @@ -518,16 +619,31 @@ def compute_master_secret(self): warning("Missing client_random while computing master_secret!") if self.server_random is None: warning("Missing server_random while computing master_secret!") + if self.extms and self.session_hash is None: + warning("Missing session hash while computing master secret!") ms = self.pwcs.prf.compute_master_secret(self.pre_master_secret, self.client_random, - self.server_random) + self.server_random, + self.extms, + self.session_hash) self.master_secret = ms if conf.debug_tls: log_runtime.debug("TLS: master secret: %s", repr_hex(ms)) + def use_nss_master_secret_if_present(self) -> bool: + # Load the master secret from an NSS Key dictionary + if not self.nss_keys or "CLIENT_RANDOM" not in self.nss_keys: + return False + if self.client_random in self.nss_keys["CLIENT_RANDOM"]: + self.master_secret = self.nss_keys["CLIENT_RANDOM"][self.client_random] + return True + return False + def compute_ms_and_derive_keys(self): - self.compute_master_secret() + if not self.master_secret: + self.compute_master_secret() + self.prcs.derive_keys(client_random=self.client_random, server_random=self.server_random, master_secret=self.master_secret) @@ -615,6 +731,15 @@ def compute_tls13_early_secrets(self, external=False): b"".join(self.handshake_messages)) self.tls13_derived_secrets["early_exporter_secret"] = ees + if self.nss_keys: + cets_dict = self.nss_keys.get('CLIENT_EARLY_TRAFFIC_SECRET', {}) + cets = cets_dict.get(self.client_random, cets) + self.tls13_derived_secrets["client_early_traffic_secret"] = cets + + ees_dict = self.nss_keys.get('EARLY_EXPORTER_SECRET', {}) + ees = ees_dict.get(self.client_random, ees) + self.tls13_derived_secrets["early_exporter_secret"] = ees + if self.connection_end == "server": if self.prcs: self.prcs.tls13_derive_keys(cets) @@ -652,6 +777,15 @@ def compute_tls13_handshake_secrets(self): b"".join(self.handshake_messages)) self.tls13_derived_secrets["server_handshake_traffic_secret"] = shts + if self.nss_keys: + chts_dict = self.nss_keys.get('CLIENT_HANDSHAKE_TRAFFIC_SECRET', {}) + chts = chts_dict.get(self.client_random, chts) + self.tls13_derived_secrets["client_handshake_traffic_secret"] = chts + + shts_dict = self.nss_keys.get('SERVER_HANDSHAKE_TRAFFIC_SECRET', {}) + shts = shts_dict.get(self.client_random, shts) + self.tls13_derived_secrets["server_handshake_traffic_secret"] = shts + def compute_tls13_traffic_secrets(self): """ Ciphers key and IV are updated accordingly for Application data. @@ -685,6 +819,19 @@ def compute_tls13_traffic_secrets(self): b"".join(self.handshake_messages)) self.tls13_derived_secrets["exporter_secret"] = es + if self.nss_keys: + cts0_dict = self.nss_keys.get('CLIENT_TRAFFIC_SECRET_0', {}) + cts0 = cts0_dict.get(self.client_random, cts0) + self.tls13_derived_secrets["client_traffic_secrets"] = [cts0] + + sts0_dict = self.nss_keys.get('SERVER_TRAFFIC_SECRET_0', {}) + sts0 = sts0_dict.get(self.client_random, sts0) + self.tls13_derived_secrets["server_traffic_secrets"] = [sts0] + + es_dict = self.nss_keys.get('EXPORTER_SECRET', {}) + es = es_dict.get(self.client_random, es) + self.tls13_derived_secrets["exporter_secret"] = es + if self.connection_end == "server": # self.prcs.tls13_derive_keys(cts0) self.pwcs.tls13_derive_keys(sts0) @@ -699,27 +846,50 @@ def compute_tls13_traffic_secrets_end(self): elif self.connection_end == "client": self.pwcs.tls13_derive_keys(cts0) - def compute_tls13_verify_data(self, connection_end, read_or_write): - shts = "server_handshake_traffic_secret" - chts = "client_handshake_traffic_secret" + def compute_tls13_verify_data(self, connection_end, read_or_write, + handshake_context): + # RFC8446 - 4.4 + # +-----------+-------------------------+-----------------------------+ + # | Mode | Handshake Context | Base Key | + # +-----------+-------------------------+-----------------------------+ + # | Server | ClientHello ... later | server_handshake_traffic_ | + # | | of EncryptedExtensions/ | secret | + # | | CertificateRequest | | + # | | | | + # | Client | ClientHello ... later | client_handshake_traffic_ | + # | | of server | secret | + # | | Finished/EndOfEarlyData | | + # | | | | + # | Post- | ClientHello ... client | client_application_traffic_ | + # | Handshake | Finished + | secret_N | + # | | CertificateRequest | | + # +-----------+-------------------------+-----------------------------+ + if self.post_handshake: + # RFC8446 - 4.6 + # TLS also allows other messages to be sent after the main handshake. + # These messages use a handshake content type and are encrypted under + # the appropriate application traffic key. + shts = self.tls13_derived_secrets["server_traffic_secrets"][-1] + chts = self.tls13_derived_secrets["client_traffic_secrets"][-1] + else: + shts = self.tls13_derived_secrets["server_handshake_traffic_secret"] + chts = self.tls13_derived_secrets["client_handshake_traffic_secret"] if read_or_write == "read": hkdf = self.rcs.hkdf if connection_end == "client": - basekey = self.tls13_derived_secrets[shts] + basekey = shts elif connection_end == "server": - basekey = self.tls13_derived_secrets[chts] + basekey = chts elif read_or_write == "write": hkdf = self.wcs.hkdf if connection_end == "client": - basekey = self.tls13_derived_secrets[chts] + basekey = chts elif connection_end == "server": - basekey = self.tls13_derived_secrets[shts] + basekey = shts if not hkdf or not basekey: warning("Missing arguments for verify_data computation!") return None - # XXX this join() works in standard cases, but does it in all of them? - handshake_context = b"".join(self.handshake_messages) return hkdf.compute_verify_data(basekey, handshake_context) def compute_tls13_resumption_secret(self): @@ -781,7 +951,7 @@ def compute_tls13_next_traffic_secrets(self, connection_end, read_or_write): # def consider_read_padding(self): # Return True if padding is needed. Used by TLSPadField. return (self.rcs.cipher.type == "block" and - not (False in six.itervalues(self.rcs.cipher.ready))) + not (False in self.rcs.cipher.ready.values())) def consider_write_padding(self): # Return True if padding is needed. Used by TLSPadField. @@ -802,8 +972,8 @@ def hash(self): family = socket.AF_INET if ':' in self.ipsrc: family = socket.AF_INET6 - s1 += socket.inet_pton(family, self.ipsrc) - s2 += socket.inet_pton(family, self.ipdst) + s1 += inet_pton(family, self.ipsrc) + s2 += inet_pton(family, self.ipdst) return strxor(s1, s2) def eq(self, other): @@ -824,13 +994,20 @@ def eq(self, other): return False - def __repr__(self): + def repr(self, _underlayer=None): sid = repr(self.sid) if len(sid) > 12: sid = sid[:11] + "..." + if _underlayer and _underlayer.dport != self.dport: + return "%s:%s > %s:%s" % (self.ipdst, str(self.dport), + self.ipsrc, str(self.sport)) return "%s:%s > %s:%s" % (self.ipsrc, str(self.sport), self.ipdst, str(self.dport)) + def __repr__(self): + return self.repr() + + ############################################################################### # Session singleton # ############################################################################### @@ -855,8 +1032,10 @@ def __init__(self, _pkt="", post_transform=None, _internal=0, except Exception: setme = True + newses = False if setme: if tls_session is None: + newses = True self.tls_session = tlsSession() else: self.tls_session = tls_session @@ -864,6 +1043,35 @@ def __init__(self, _pkt="", post_transform=None, _internal=0, self.rcs_snap_init = self.tls_session.rcs.snapshot() self.wcs_snap_init = self.tls_session.wcs.snapshot() + if isinstance(_underlayer, TCP): + # Get information from _underlayer + self.tls_session.set_underlayer(_underlayer) + + # Load a NSS Key Log file + if conf.tls_nss_filename is not None: + if conf.tls_nss_keys is None: + conf.tls_nss_keys = load_nss_keys(conf.tls_nss_filename) + + if conf.tls_session_enable: + if newses: + s = conf.tls_sessions.find(self.tls_session) + if s: + if conf.tls_nss_keys is not None: + s.nss_keys = conf.tls_nss_keys + if s.dport == self.tls_session.dport: + self.tls_session = s + else: + self.tls_session = s.mirror() + else: + if conf.tls_nss_keys is not None: + self.tls_session.nss_keys = conf.tls_nss_keys + conf.tls_sessions.add(self.tls_session) + if self.tls_session.connection_end == "server": + srk = conf.tls_sessions.server_rsa_key + if not self.tls_session.server_rsa_key and \ + srk: + self.tls_session.server_rsa_key = srk + Packet.__init__(self, _pkt=_pkt, post_transform=post_transform, _internal=_internal, _underlayer=_underlayer, **fields) @@ -963,9 +1171,56 @@ def show2(self): s.rcs = rcs_snap s.wcs = wcs_snap - # Uncomment this when the automata update IPs and ports properly - # def mysummary(self): - # return "TLS %s" % repr(self.tls_session) + def mysummary(self, first=True): + from scapy.layers.tls.record import TLS + from scapy.layers.tls.record_tls13 import TLS13 + if ( + self.underlayer and + isinstance(self.underlayer, _GenericTLSSessionInheritance) + ): + summary = getattr(self, "_name", self.name) + else: + _underlayer = None + if self.underlayer and isinstance(self.underlayer, TCP): + _underlayer = self.underlayer + summary = "TLS %s / %s" % ( + self.tls_session.repr(_underlayer=_underlayer), + getattr(self, "_name", self.name) + ) + return summary, [TLS, TLS13] + + @classmethod + def tcp_reassemble(cls, data, metadata, session): + # Used with TCPSession + from scapy.layers.tls.record import TLS + from scapy.layers.tls.record_tls13 import TLS13 + if cls in (TLS, TLS13): + length = struct.unpack("!H", data[3:5])[0] + 5 + if len(data) >= length: + # get the underlayer as it is used to populate tls_session + if "original" not in metadata: + return cls(data) + underlayer = metadata["original"][TCP].copy() + underlayer.remove_payload() + # eventually get the tls_session now for TLS.dispatch_hook + tls_session = None + if conf.tls_session_enable: + s = tlsSession() + s.set_underlayer(underlayer) + tls_session = conf.tls_sessions.find(s) + if tls_session: + if tls_session.dport != underlayer.dport: + tls_session = tls_session.mirror() + if tls_session.firsttcp == underlayer.seq: + log_runtime.info( + "TLS: session %s is a duplicate of a previous " + "dissection. Discard it" % repr(tls_session) + ) + conf.tls_sessions.rem(tls_session, force=True) + tls_session = None + return cls(data, _underlayer=underlayer, tls_session=tls_session) + else: + return cls(data) ############################################################################### @@ -975,6 +1230,7 @@ def show2(self): class _tls_sessions(object): def __init__(self): self.sessions = {} + self.server_rsa_key = None def add(self, session): s = self.find(session) @@ -988,41 +1244,64 @@ def add(self, session): else: self.sessions[h] = [session] - def rem(self, session): - s = self.find(session) - if s: - log_runtime.info("TLS: previous session shall not be overwritten") - return + def rem(self, session, force=False): + if not force: + s = self.find(session) + if s: + log_runtime.info("TLS: previous session shall not be overwritten") + return h = session.hash() self.sessions[h].remove(session) def find(self, session): - h = session.hash() + try: + h = session.hash() + except Exception: + return None if h in self.sessions: for k in self.sessions[h]: if k.eq(session): - if conf.tls_verbose: + if conf.debug_tls: log_runtime.info("TLS: found session matching %s", k) return k - if conf.tls_verbose: + if conf.debug_tls: log_runtime.info("TLS: did not find session matching %s", session) return None def __repr__(self): res = [("First endpoint", "Second endpoint", "Session ID")] - for l in six.itervalues(self.sessions): - for s in l: + for li in self.sessions.values(): + for s in li: src = "%s[%d]" % (s.ipsrc, s.sport) dst = "%s[%d]" % (s.ipdst, s.dport) sid = repr(s.sid) if len(sid) > 12: sid = sid[:11] + "..." res.append((src, dst, sid)) - colwidth = (max([len(y) for y in x]) for x in zip(*res)) + colwidth = (max(len(y) for y in x) for x in zip(*res)) fmt = " ".join(map(lambda x: "%%-%ds" % x, colwidth)) return "\n".join(map(lambda x: fmt % x, res)) +class TLSSession(TCPSession): + def __init__(self, *args, **kwargs): + # XXX this doesn't bring any value. + warning( + "TLSSession is deprecated and will be removed in a future version. " + "Please use TCPSession instead with conf.tls_session_enable=True" + ) + server_rsa_key = kwargs.pop("server_rsa_key", None) + super(TLSSession, self).__init__(*args, **kwargs) + self._old_conf_status = conf.tls_session_enable + conf.tls_session_enable = True + if server_rsa_key: + conf.tls_sessions.server_rsa_key = server_rsa_key + + def toPacketList(self): + conf.tls_session_enable = self._old_conf_status + return super(TLSSession, self).toPacketList() + + +# Instantiate the TLS sessions holder conf.tls_sessions = _tls_sessions() -conf.tls_verbose = False diff --git a/scapy/layers/tls/tools.py b/scapy/layers/tls/tools.py index c7f0edaed94..66318b92ec6 100644 --- a/scapy/layers/tls/tools.py +++ b/scapy/layers/tls/tools.py @@ -1,13 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) 2007, 2008, 2009 Arnaud Ebalard # 2015, 2016, 2017 Maxence Tury -# This program is published under a GPLv2 license """ TLS helpers, provided as out-of-context methods. """ -from __future__ import absolute_import import struct from scapy.compat import orb, chb diff --git a/scapy/layers/tpm.py b/scapy/layers/tpm.py new file mode 100644 index 00000000000..d51fabb03b7 --- /dev/null +++ b/scapy/layers/tpm.py @@ -0,0 +1,729 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Implementation of structures related to TPM 2.0 and Windows PCP + +(Windows Plateform Crypto Provider) +""" + +from scapy.config import conf +from scapy.packet import Packet +from scapy.fields import ( + BitField, + ByteField, + ConditionalField, + FlagsField, + IntField, + LEIntEnumField, + LEIntField, + LongField, + MultipleTypeField, + PacketField, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + StrField, + StrFixedLenField, + StrLenField, +) + + +########################## +# TPM 2 structures # +########################## + +IMPLEMENTATION_PCR = 24 +PCR_SELECT_MAX = (IMPLEMENTATION_PCR + 7) // 8 +MAX_RSA_KEY_BITS = 2048 +MAX_RSA_KEY_BYTES = (MAX_RSA_KEY_BITS + 7) // 8 + +# TPM20.h source + +TPM_ALG = { + 0x0000: "TPM_ALG_ERROR", + 0x0001: "TPM_ALG_RSA", + 0x0004: "TPM_ALG_SHA1", + 0x0005: "TPM_ALG_HMAC", + 0x0006: "TPM_ALG_AES", + 0x0007: "TPM_ALG_MGF1", + 0x0008: "TPM_ALG_KEYEDHASH", + 0x000A: "TPM_ALG_XOR", + 0x000B: "TPM_ALG_SHA256", + 0x000C: "TPM_ALG_SHA384", + 0x000D: "TPM_ALG_SHA512", + 0x0010: "TPM_ALG_NULL", + 0x0012: "TPM_ALG_SM3_256", + 0x0013: "TPM_ALG_SM4", + 0x0014: "TPM_ALG_RSASSA", + 0x0015: "TPM_ALG_RSAES", + 0x0016: "TPM_ALG_RSAPSS", + 0x0017: "TPM_ALG_OAEP", + 0x0018: "TPM_ALG_ECDSA", + 0x0019: "TPM_ALG_ECDH", + 0x001A: "TPM_ALG_ECDAA", + 0x001B: "TPM_ALG_SM2", + 0x001C: "TPM_ALG_ECSCHNORR", + 0x001D: "TPM_ALG_ECMQV", + 0x0020: "TPM_ALG_KDF1_SP800_56a", + 0x0021: "TPM_ALG_KDF2", + 0x0022: "TPM_ALG_KDF1_SP800_108", + 0x0023: "TPM_ALG_ECC", + 0x0025: "TPM_ALG_SYMCIPHER", + 0x0040: "TPM_ALG_CTR", + 0x0041: "TPM_ALG_OFB", + 0x0042: "TPM_ALG_CBC", + 0x0043: "TPM_ALG_CFB", + 0x0044: "TPM_ALG_ECB", +} + +TPM_ST = { + 0x00C4: "TPM_ST_RSP_COMMAND", + 0x8000: "TPM_ST_NULL", + 0x8001: "TPM_ST_NO_SESSIONS", + 0x8002: "TPM_ST_SESSIONS", + 0x8014: "TPM_ST_ATTEST_NV", + 0x8015: "TPM_ST_ATTEST_COMMAND_AUDIT", + 0x8016: "TPM_ST_ATTEST_SESSION_AUDIT", + 0x8017: "TPM_ST_ATTEST_CERTIFY", + 0x8018: "TPM_ST_ATTEST_QUOTE", + 0x8019: "TPM_ST_ATTEST_TIME", + 0x801A: "TPM_ST_ATTEST_CREATION", + 0x8021: "TPM_ST_CREATION", + 0x8022: "TPM_ST_VERIFIED", + 0x8023: "TPM_ST_AUTH_SECRET", + 0x8024: "TPM_ST_HASHCHECK", + 0x8025: "TPM_ST_AUTH_SIGNED", + 0x8029: "TPM_ST_FU_MANIFEST", +} + + +class _Packet(Packet): + def default_payload_class(self, payload): + return conf.padding_layer + + +class TPMS_SCHEME_SIGHASH(_Packet): + fields_desc = [ + ShortEnumField("hashAlg", 0, TPM_ALG), + ] + + +class TPMT_RSA_SCHEME(_Packet): + fields_desc = [ + ShortEnumField("scheme", 0, TPM_ALG), + # TPMU_ASYM_SCHEME + MultipleTypeField( + [ + ( + PacketField( + "parameters", TPMS_SCHEME_SIGHASH(), TPMS_SCHEME_SIGHASH + ), + lambda pkt: pkt.scheme + in [ + 0x0014, # RSASSA + 0x0016, # RSAPSS + 0x001A, # RSAPSS + 0x001B, # SM2 + 0x001C, # ECSCHNORR + ], + ) + ], + StrFixedLenField("parameters", b"", length=0), + ), + ] + + +class TPMT_SYM_DEF_OBJECT(_Packet): + fields_desc = [ + ShortEnumField("algorithm", 0, TPM_ALG), + ConditionalField( + ShortField("keyBits", 0), + lambda pkt: pkt.algorithm != 0x0010, + ), + ConditionalField( + ShortField("mode", 0), + lambda pkt: pkt.algorithm != 0x0010, + ), + ] + + +class TPMS_RSA_PARMS(_Packet): + fields_desc = [ + PacketField("symmetric", TPMT_SYM_DEF_OBJECT(), TPMT_SYM_DEF_OBJECT), + PacketField("scheme", TPMT_RSA_SCHEME(), TPMT_RSA_SCHEME), + ShortField("keyBits", 0), + IntField("exponent", 0), + ] + + +class TPM2B_DIGEST(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField("buffer", b"", length_from=lambda pkt: pkt.size), + ] + + +class TPML_DIGEST(_Packet): + fields_desc = [ + IntField("count", 0), + PacketListField("digests", [], TPM2B_DIGEST, count_from=lambda pkt: pkt.count), + ] + + +class TPMS_NULL_PARMS(_Packet): + fields_desc = [ + ShortEnumField("algorithm", 0x0010, TPM_ALG), + ] + + +class TPMT_PUBLIC(_Packet): + fields_desc = [ + ShortEnumField("type", 0x0001, TPM_ALG), + ShortEnumField("nameAlg", 0, TPM_ALG), + FlagsField( + "objectAttributes", + 0, + 32, + [ + "reserved1", + "fixedTPM", + "stClear", + "reserved4", + "fixedParent", + "sensitiveDataOrigin", + "userWithAuth", + "adminWithPolicy", + "reserved8", + "reserved9", + "noDA", + "encryptedDuplication", + "reserved12", + "reserved13", + "reserved14", + "reserved15", + "restricted", + "decrypt", + "sign", + ], + ), + PacketField("authPolicy", TPM2B_DIGEST(), TPM2B_DIGEST), + MultipleTypeField( + [ + # TPMU_PUBLIC_PARMS + ( + PacketField("parameters", TPMS_RSA_PARMS(), TPMS_RSA_PARMS), + lambda pkt: pkt.type == 0x0001, + ) + ], + StrFixedLenField("parameters", b"", length=0), + ), + # TPMU_PUBLIC_ID + PacketField("unique", TPM2B_DIGEST(), TPM2B_DIGEST), + ] + + +class TPM2B_PUBLIC(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "publicArea", + TPMT_PUBLIC(), + TPMT_PUBLIC, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPM2B_PRIVATE_KEY_RSA(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField( + "buffer", + b"", + length_from=lambda pkt: pkt.size, + ), + ] + + +TPM2B_AUTH = TPM2B_DIGEST + + +class TPMT_SENSITIVE(_Packet): + fields_desc = [ + ShortEnumField("sensitiveType", 0, TPM_ALG), + PacketField("authValue", TPM2B_AUTH(), TPM2B_AUTH), + PacketField("seedValue", TPM2B_DIGEST(), TPM2B_DIGEST), + MultipleTypeField( + [ + # TPMU_SENSITIVE_COMPOSITE + ( + PacketField( + "sensitive", TPM2B_PRIVATE_KEY_RSA(), TPM2B_PRIVATE_KEY_RSA + ), + lambda pkt: pkt.sensitiveType == 0x0001, # TPM_ALG_RSA + ), + ], + StrField("sensitive", b""), + ), + ] + + +class TPM2B_SENSITIVE(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "sensitiveArea", + TPMT_SENSITIVE(), + TPMT_SENSITIVE, + length_from=lambda pkt: pkt.size, + ), + ] + + +class _PRIVATE(_Packet): + fields_desc = [ + PacketField("integrityOuter", TPM2B_DIGEST(), TPM2B_DIGEST), + PacketField("integrityInner", TPM2B_DIGEST(), TPM2B_DIGEST), + StrField("sensitive", b""), # Encrypted + ] + + +class TPM2B_PRIVATE(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "buffer", + _PRIVATE(), + _PRIVATE, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPM2B_NAME(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField("Name", b"", length_from=lambda pkt: pkt.size), + ] + + +class TPM2B_DATA(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField("buffer", b"", length_from=lambda pkt: pkt.size), + ] + + +class TPMA_LOCALITY(_Packet): + fields_desc = [ + BitField("locZero", 0, 1), + BitField("locOne", 0, 1), + BitField("locTwo", 0, 1), + BitField("locThree", 0, 1), + BitField("locFour", 0, 1), + BitField("Extended", 0, 3), + ] + + +class TPMS_PCR_SELECTION(_Packet): + fields_desc = [ + ShortEnumField("hash", 0, TPM_ALG), + ByteField("sizeOfSelect", 0), + StrFixedLenField("pcrSelect", b"", length=PCR_SELECT_MAX), + ] + + +class TPML_PCR_SELECTION(_Packet): + fields_desc = [ + IntField("count", 0), + PacketListField( + "pcrSelections", [], TPMS_PCR_SELECTION, count_from=lambda pkt: pkt.count + ), + ] + + +class TPMS_CREATION_DATA(_Packet): + fields_desc = [ + PacketField("pcrSelect", TPML_PCR_SELECTION(), TPML_PCR_SELECTION), + PacketField("pcrDigest", TPM2B_DIGEST(), TPM2B_DIGEST), + PacketField("locality", TPMA_LOCALITY(), TPMA_LOCALITY), + ShortEnumField("parentNameAlg", 0, TPM_ALG), + PacketField("parentName", TPM2B_NAME(), TPM2B_NAME), + PacketField("parentQualifiedName", TPM2B_NAME(), TPM2B_NAME), + PacketField("outsideInfo", TPM2B_DATA(), TPM2B_DATA), + ] + + +class TPM2B_CREATION_DATA(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "creationData", + TPMS_CREATION_DATA(), + TPMS_CREATION_DATA, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPMS_CLOCK_INFO(_Packet): + fields_desc = [ + LongField("clock", 0), # obfuscated + IntField("resetCount", 0), # obfuscated + IntField("restartCount", 0), # obfuscated + ByteField("safe", 0), + ] + + +class TPMS_CREATION_INFO(_Packet): + fields_desc = [ + PacketField("objectName", TPM2B_NAME(), TPM2B_NAME), + PacketField("creationHash", TPM2B_DIGEST(), TPM2B_DIGEST), + ] + + +class TPMS_CERTIFY_INFO(_Packet): + fields_desc = [ + PacketField("Name", TPM2B_NAME(), TPM2B_NAME), + PacketField("qualifiedName", TPM2B_DIGEST(), TPM2B_DIGEST), + ] + + +class TPMS_ATTEST(_Packet): + fields_desc = [ + StrFixedLenField("magic", b"\xffTCG", length=4), + ShortEnumField("type", 0, TPM_ST), + PacketField("qualifiedSigned", TPM2B_NAME(), TPM2B_NAME), + PacketField("extraData", TPM2B_DATA(), TPM2B_DATA), + PacketField("clockInfo", TPMS_CLOCK_INFO(), TPMS_CLOCK_INFO), + LongField("firmwareVersion", 0), + MultipleTypeField( + [ + # TPMU_ATTEST + ( + PacketField("attested", TPMS_CERTIFY_INFO(), TPMS_CERTIFY_INFO), + lambda pkt: pkt.type == 0x8017, # TPM_ST_ATTEST_CERTIFY + ), + ( + PacketField("attested", TPMS_CREATION_INFO(), TPMS_CREATION_INFO), + lambda pkt: pkt.type == 0x801A, # TPM_ST_ATTEST_CREATION + ), + ], + StrField("attested", b""), + ), + ] + + +class TPM2B_ATTEST(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "attestationData", + TPMS_ATTEST(), + TPMS_ATTEST, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPM2B_PUBLIC_KEY_RSA(_Packet): + fields_desc = [ + ShortField("size", 0), + StrFixedLenField("buffer", b"", length=MAX_RSA_KEY_BYTES), + ] + + +class TPMS_SIGNATURE_RSASSA(_Packet): + fields_desc = [ + ShortEnumField("hash", 0, TPM_ALG), + PacketField("sig", TPM2B_PUBLIC_KEY_RSA(), TPM2B_PUBLIC_KEY_RSA), + ] + + +class TPMS_SIGNATURE_RSAPSS(_Packet): + fields_desc = [ + ShortEnumField("hash", 0, TPM_ALG), + PacketField("sig", TPM2B_PUBLIC_KEY_RSA(), TPM2B_PUBLIC_KEY_RSA), + ] + + +class TPMT_SIGNATURE(_Packet): + fields_desc = [ + ShortEnumField("sigAlg", 0, TPM_ALG), + MultipleTypeField( + [ + # TPMU_SIGNATURE + ( + PacketField( + "signature", TPMS_SIGNATURE_RSASSA(), TPMS_SIGNATURE_RSASSA + ), + lambda pkt: pkt.sigAlg == 0x0014, # RSASSA + ), + ( + PacketField( + "signature", TPMS_SIGNATURE_RSAPSS(), TPMS_SIGNATURE_RSAPSS + ), + lambda pkt: pkt.sigAlg == 0x0016, # RSASSA + ), + ], + StrField("signature", b""), + ), + ] + + +# From "Using the Windows 8 Platform PCP" documentation +# https://github.com/Microsoft/TSS.MSR/blob/main/PCPTool.v11/inc/TpmAtt.h + + +# NCRYPT_PCP_TPM12_IDBINDING +class PCP_IDBinding20(Packet): + fields_desc = [ + PacketField("PublicKey", TPM2B_PUBLIC(), TPM2B_PUBLIC), + PacketField("CreationData", TPM2B_CREATION_DATA(), TPM2B_CREATION_DATA), + PacketField("Attest", TPM2B_ATTEST(), TPM2B_ATTEST), + PacketField("Signature", TPMT_SIGNATURE(), TPMT_SIGNATURE), + ] + + +_PCP_TYPE = { + 1: "TPM 1.2", + 2: "TPM 2.0", +} + + +class PCP_KEY_BLOB(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"PCPM", length=4), + LEIntField("cbHeader", 0), + LEIntEnumField("pcpType", 1, _PCP_TYPE), + FlagsField( + "flags", + 0, + -32, + { + 0x00000001: "authRequired", + 0x00000002: "undocumented2", + }, + ), + LEIntField("cbTpmKey", 0), + StrLenField( + "tpmKey", + b"", + length_from=lambda pkt: pkt.cbTpmKey, + ), + ] + + +class PCP_20_KEY_BLOB(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"PCPM", length=4), + LEIntField("cbHeader", 0), + LEIntEnumField("pcpType", 2, _PCP_TYPE), + FlagsField( + "flags", + 0, + -32, + { + 0x00000001: "authRequired", + 0x00000002: "undocumented2", + }, + ), + LEIntField("cbPublic", 0), + LEIntField("cbPrivate", 0), + LEIntField("cbMigrationPublic", 0), + LEIntField("cbMigrationPrivate", 0), + LEIntField("cbPolicyDigestList", 0), + LEIntField("cbPCRBinding", 0), + LEIntField("cbPCRDigest", 0), + LEIntField("cbEncryptedSecret", 0), + LEIntField("cbTpm12HostageBlob", 0), + LEIntField("pcrAlgId", 0), + PacketLenField( + "public", + TPM2B_PUBLIC(), + TPM2B_PUBLIC, + length_from=lambda pkt: pkt.cbPublic, + ), + PacketLenField( + "private", + TPM2B_PRIVATE(), + TPM2B_PRIVATE, + length_from=lambda pkt: pkt.cbPrivate, + ), + PacketLenField( + "migrationPublic", + None, + TPM2B_PUBLIC, + length_from=lambda pkt: pkt.cbMigrationPublic, + ), + PacketLenField( + "migrationPrivate", + TPM2B_PRIVATE(), + TPM2B_PRIVATE, + length_from=lambda pkt: pkt.cbMigrationPrivate, + ), + PacketLenField( + "policyDigestList", + TPML_DIGEST(), + TPML_DIGEST, + length_from=lambda pkt: pkt.cbPolicyDigestList, + ), + StrLenField( + "pcrBinding", + b"", + length_from=lambda pkt: pkt.cbPCRBinding, + ), + StrLenField( + "pcrDigest", + b"", + length_from=lambda pkt: pkt.cbPCRDigest, + ), + StrLenField( + "encryptedSecret", + b"", + length_from=lambda pkt: pkt.cbEncryptedSecret, + ), + StrLenField( + "tpm12HostageBlob", + b"", + length_from=lambda pkt: pkt.cbTpm12HostageBlob, + ), + ] + + +########################### +# Microsoft Windows # +########################### + +# [MS-WCCE] sect 2.2.2.5 + + +class KeyAttestation(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"KADS", length=4), + LEIntEnumField("Platform", 2, _PCP_TYPE), + LEIntField("HeaderSize", 0), + LEIntField("cbKeyAttest", 0), + LEIntField("cbSignature", 0), + LEIntField("cbKeyBlob", 0), + MultipleTypeField( + [ + ( + PacketLenField( + "keyAttest", + TPMS_ATTEST(), + TPMS_ATTEST, + length_from=lambda pkt: pkt.cbKeyAttest, + ), + lambda pkt: pkt.Platform == 2, + ) + ], + StrLenField( + "keyAttest", + b"", + length_from=lambda pkt: pkt.cbKeyAttest, + ), + ), + StrLenField( + "signature", + b"", + length_from=lambda pkt: pkt.cbSignature, + ), + MultipleTypeField( + [ + ( + PacketLenField( + "keyBlob", + PCP_20_KEY_BLOB(), + PCP_20_KEY_BLOB, + length_from=lambda pkt: pkt.cbKeyBlob, + ), + lambda pkt: pkt.Platform == 2, + ), + ( + PacketLenField( + "keyBlob", + PCP_KEY_BLOB(), + PCP_KEY_BLOB, + length_from=lambda pkt: pkt.cbKeyBlob, + ), + lambda pkt: pkt.Platform == 1, + ), + ], + StrLenField( + "keyBlob", + b"", + length_from=lambda pkt: pkt.cbKeyBlob, + ), + ), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class KeyAttestationStatement(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"KAST", length=4), + LEIntField("Version", 1), + LEIntEnumField("Platform", 2, _PCP_TYPE), + LEIntField("HeaderSize", 0), + LEIntField("cbIdBinding", 0), + LEIntField("cbKeyAttestation", 0), + LEIntField("cbAIKOpaque", 0), + MultipleTypeField( + [ + ( + PacketLenField( + "idBinding", + PCP_IDBinding20(), + PCP_IDBinding20, + length_from=lambda pkt: pkt.cbIdBinding, + ), + lambda pkt: pkt.Platform == 2, + ) + ], + StrLenField( + "idBinding", + b"", + length_from=lambda pkt: pkt.cbIdBinding, + ), + ), + PacketLenField( + "keyAttestation", + KeyAttestation(), + KeyAttestation, + length_from=lambda pkt: pkt.cbKeyAttestation, + ), + MultipleTypeField( + [ + ( + PacketLenField( + "aikOpaque", + PCP_20_KEY_BLOB(), + PCP_20_KEY_BLOB, + length_from=lambda pkt: pkt.cbAIKOpaque, + ), + lambda pkt: pkt.Platform == 2, + ), + ( + PacketLenField( + "aikOpaque", + PCP_KEY_BLOB(), + PCP_KEY_BLOB, + length_from=lambda pkt: pkt.cbAIKOpaque, + ), + lambda pkt: pkt.Platform == 1, + ), + ], + StrLenField( + "aikOpaque", + b"", + length_from=lambda pkt: pkt.cbAIKOpaque, + ), + ), + ] diff --git a/scapy/layers/tuntap.py b/scapy/layers/tuntap.py new file mode 100644 index 00000000000..19a3e289f0b --- /dev/null +++ b/scapy/layers/tuntap.py @@ -0,0 +1,291 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi +# Copyright (C) Michael Farrell + +""" +Implementation of TUN/TAP interfaces. + +These allow Scapy to act as the remote side of a virtual network interface. +""" + +import socket +import time +from fcntl import ioctl + +from scapy.compat import bytes_encode, raw +from scapy.config import conf +from scapy.consts import BIG_ENDIAN, BSD, DARWIN, LINUX +from scapy.data import ETHER_TYPES, MTU +from scapy.error import log_runtime, warning +from scapy.fields import ( + BitField, + Field, + FlagsField, + IntField, + StrFixedLenField, + XShortEnumField, +) +from scapy.interfaces import network_name +from scapy.layers.inet import IP +from scapy.layers.inet6 import IPv6, IPv46 +from scapy.layers.l2 import Ether +from scapy.packet import Packet, bind_layers +from scapy.supersocket import SimpleSocket + +# Linux-specific defines (/usr/include/linux/if_tun.h) +LINUX_TUNSETIFF = 0x400454ca +LINUX_IFF_TUN = 0x0001 +LINUX_IFF_TAP = 0x0002 +LINUX_IFF_NO_PI = 0x1000 +LINUX_IFNAMSIZ = 16 + +# Darwin-specific defines (net/if_utun.h and sys/kern_control.h) +DARWIN_CTLIOCGINFO = 0xc0644e03 +DARWIN_UTUN_CONTROL_NAME = b"com.apple.net.utun_control" +DARWIN_MAX_KCTL_NAME = 96 + + +class NativeShortField(Field): + def __init__(self, name, default): + Field.__init__(self, name, default, "@H") + + +class TunPacketInfo(Packet): + aliastypes = [Ether] + + +class LinuxTunIfReq(Packet): + """ + Structure to request a specific device name for a tun/tap + Linux ``struct ifreq``. + + See linux/if.h (struct ifreq) and tuntap.txt for reference. + """ + fields_desc = [ + # union ifr_ifrn + StrFixedLenField("ifrn_name", b"", 16), + # union ifr_ifru + NativeShortField("ifru_flags", 0), + ] + + +class DarwinUtunIfReq(Packet): + """ + Structure for issuing Darwin ioctl commands (``struct ctl_info``). + + See net/if_utun.h and sys/kern_control.h for reference. + """ + fields_desc = [ + BitField("ctl_id", 0, -32), + StrFixedLenField("ctl_name", DARWIN_UTUN_CONTROL_NAME, DARWIN_MAX_KCTL_NAME) + ] + + +class LinuxTunPacketInfo(TunPacketInfo): + """ + Base for TUN packets. + + See linux/if_tun.h (struct tun_pi) for reference. + """ + fields_desc = [ + # This is native byte order + FlagsField("flags", 0, + (lambda _: 16 if BIG_ENDIAN else -16), + ["TUN_VNET_HDR"] + + ["reserved%d" % x for x in range(1, 16)]), + # This is always network byte order + XShortEnumField("type", 0x9000, ETHER_TYPES), + ] + + +class DarwinUtunPacketInfo(Packet): + fields_desc = [ + IntField("addr_family", socket.AF_INET) + ] + + +class TunTapInterface(SimpleSocket): + """ + A socket to act as the host's peer of a tun / tap interface. + + This implements kernel interfaces for tun and tap devices. + + :param iface: The name of the interface to use, eg: 'tun0' + :param mode_tun: If True, create as TUN interface (layer 3). + If False, creates a TAP interface (layer 2). + If not supplied, attempts to detect from the ``iface`` + name. + :type mode_tun: bool + :param strip_packet_info: If True (default), strips any TunPacketInfo from + the packet. If False, leaves it in tact. Some + operating systems and tunnel types don't include + this sort of data. + :type strip_packet_info: bool + + FreeBSD references: + + * tap(4): https://www.freebsd.org/cgi/man.cgi?query=tap&sektion=4 + * tun(4): https://www.freebsd.org/cgi/man.cgi?query=tun&sektion=4 + + Linux references: + + * https://www.kernel.org/doc/Documentation/networking/tuntap.txt + + """ + desc = "Act as the host's peer of a tun / tap interface" + + def __init__(self, iface=None, mode_tun=None, default_read_size=MTU, + strip_packet_info=True, *args, **kwargs): + self.iface = bytes_encode( + network_name(conf.iface) if iface is None else iface + ) + + self.mode_tun = mode_tun + if self.mode_tun is None: + if self.iface.startswith(b"tun") or self.iface.startswith(b"utun"): + self.mode_tun = True + elif self.iface.startswith(b"tap"): + self.mode_tun = False + else: + raise ValueError( + "Could not determine interface type for %r; set " + "`mode_tun` explicitly." % (self.iface,)) + + self.strip_packet_info = bool(strip_packet_info) + + # This is non-zero when there is some kernel-specific packet info. + # We add this to any MTU value passed to recv(), and use it to + # remove leading bytes when strip_packet_info=True. + self.mtu_overhead = 0 + + # The TUN packet specification sends raw IP at us, and doesn't specify + # which version. + self.kernel_packet_class = IPv46 if self.mode_tun else Ether + + if LINUX: + devname = b"/dev/net/tun" + + # Having an EtherType always helps on Linux, then we don't need + # to use auto-detection of IP version. + if self.mode_tun: + self.kernel_packet_class = LinuxTunPacketInfo + self.mtu_overhead = 4 # len(LinuxTunPacketInfo) + else: + warning("tap devices on Linux do not include packet info!") + self.strip_packet_info = True + + if len(self.iface) > LINUX_IFNAMSIZ: + warning("Linux interface names are limited to %d bytes, " + "truncating!" % (LINUX_IFNAMSIZ,)) + self.iface = self.iface[:LINUX_IFNAMSIZ] + sock = open(devname, "r+b", buffering=0) + elif BSD: # also DARWIN + if self.iface.startswith(b"utun"): # allowed for Darwin + if not DARWIN: + raise ValueError('`utun` iface prefix is only allowed for Darwin') + self.kernel_packet_class = DarwinUtunPacketInfo + self.mtu_overhead = 4 + interface_num = int(self.iface[4:]) + + utun_socket = socket.socket( + socket.PF_SYSTEM, socket.SOCK_DGRAM, socket.SYSPROTO_CONTROL) + ctl_info = ioctl(utun_socket, DARWIN_CTLIOCGINFO, + raw(DarwinUtunIfReq())) + utun_socket.connect( + (DarwinUtunIfReq(ctl_info).getfieldval("ctl_id"), interface_num + 1) + ) + + sock = utun_socket.makefile(mode="rwb", buffering=0) + elif self.iface.startswith(b"tap") or self.iface.startswith(b"tun"): + devname = b"/dev/" + self.iface + if not self.strip_packet_info: + warning("tun/tap devices on BSD and Darwin never include " + "packet info!") + self.strip_packet_info = True + sock = open(devname, "r+b", buffering=0) + else: + raise ValueError("Interface names must start with `tun` or " + "`tap` on BSD and Darwin or `utun` on Darwin") + else: + raise NotImplementedError("TunTapInterface is not supported on " + "this platform!") + + if LINUX: + if self.mode_tun: + flags = LINUX_IFF_TUN + else: + # Linux can send us LinuxTunPacketInfo for TAP interfaces, but + # the kernel sends the wrong information! + # + # Instead of type=1 (Ether), it sends that of the payload + # (eg: 0x800 for IPv4 or 0x86dd for IPv6). + # + # tap interfaces always send Ether frames, which include a + # type parameter for the IPv4/v6/etc. payload, so we set + # IFF_NO_PI. + flags = LINUX_IFF_TAP | LINUX_IFF_NO_PI + + tsetiff = raw(LinuxTunIfReq( + ifrn_name=self.iface, + ifru_flags=flags)) + + ioctl(sock, LINUX_TUNSETIFF, tsetiff) + + self.closed = False + self.default_read_size = default_read_size + super(TunTapInterface, self).__init__(sock) + + def __call__(self, *arg, **karg): + """Needed when using an instantiated TunTapInterface object for + conf.L2listen, conf.L2socket or conf.L3socket. + + """ + return self + + def recv_raw(self, x=None): + if x is None: + x = self.default_read_size + + x += self.mtu_overhead + + dat = self.ins.read(x) + r = self.kernel_packet_class, dat, time.time() + if self.mtu_overhead > 0 and self.strip_packet_info: + # Get the packed class of the payload, without triggering a full + # decode of the payload data. + cls = r[0](r[1][:self.mtu_overhead]).guess_payload_class(b'') + + # Return the payload data only + return cls, r[1][self.mtu_overhead:], r[2] + else: + return r + + def send(self, x): + # type: (Packet) -> int + if hasattr(x, "sent_time"): + x.sent_time = time.time() + + if self.kernel_packet_class == IPv46: + # IPv46 is an auto-detection wrapper; we should just push through + # packets normally if we got IP or IPv6. + if not isinstance(x, (IP, IPv6)): + x = IP() / x + elif not isinstance(x, self.kernel_packet_class): + x = self.kernel_packet_class() / x + + sx = raw(x) + + try: + r = self.outs.write(sx) + self.outs.flush() + return r + except socket.error: + log_runtime.error("%s send", + self.__class__.__name__, exc_info=True) + + +# Bindings # +bind_layers(DarwinUtunPacketInfo, IP, addr_family=socket.AF_INET) +bind_layers(DarwinUtunPacketInfo, IPv6, addr_family=socket.AF_INET6) diff --git a/scapy/layers/usb.py b/scapy/layers/usb.py index 736b2fd28b6..d5a7f39837c 100644 --- a/scapy/layers/usb.py +++ b/scapy/layers/usb.py @@ -1,8 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter """ Default USB frames & Basic implementation @@ -11,20 +10,14 @@ # TODO: support USB headers for Linux and Darwin (usbmon/netmon) # https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-usb.c # noqa: E501 -import re -import subprocess - from scapy.config import conf -from scapy.consts import WINDOWS -from scapy.compat import chb, plain_str -from scapy.data import MTU, DLT_USBPCAP -from scapy.error import warning +from scapy.compat import chb +from scapy.data import DLT_USBPCAP from scapy.fields import ByteField, XByteField, ByteEnumField, LEShortField, \ LEShortEnumField, LEIntField, LEIntEnumField, XLELongField, \ LenField from scapy.packet import Packet, bind_top_down -from scapy.supersocket import SuperSocket -from scapy.utils import PcapReader + # USBpcap @@ -111,14 +104,14 @@ def post_build(self, p, pay): def guess_payload_class(self, payload): if self.headerLen == 27: # No Transfer layer - return conf.raw_layer + return super(USBpcap, self).guess_payload_class(payload) if self.transfer == 0: return USBpcapTransferIsochronous elif self.transfer == 1: return USBpcapTransferInterrupt elif self.transfer == 2: return USBpcapTransferControl - return conf.raw_layer + return super(USBpcap, self).guess_payload_class(payload) class USBpcapTransferIsochronous(Packet): @@ -151,94 +144,3 @@ class USBpcapTransferControl(Packet): bind_top_down(USBpcap, USBpcapTransferControl, transfer=2) conf.l2types.register(DLT_USBPCAP, USBpcap) - - -def _extcap_call(prog, args, keyword, values): - """Function used to call a program using the extcap format, - then parse the results""" - p = subprocess.Popen( - [prog] + args, - stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - data, err = p.communicate() - if p.returncode != 0: - raise OSError("%s returned with error code %s: %s" % (prog, - p.returncode, - err)) - data = plain_str(data) - res = [] - for ifa in data.split("\n"): - ifa = ifa.strip() - if not ifa.startswith(keyword): - continue - res.append(tuple([re.search(r"{%s=([^}]*)}" % val, ifa).group(1) - for val in values])) - return res - - -if WINDOWS: - def _usbpcap_check(): - if not conf.prog.usbpcapcmd: - raise OSError("USBpcap is not installed ! (USBpcapCMD not found)") - - def get_usbpcap_interfaces(): - """Return a list of available USBpcap interfaces""" - _usbpcap_check() - return _extcap_call( - conf.prog.usbpcapcmd, - ["--extcap-interfaces"], - "interface", - ["value", "display"] - ) - - def get_usbpcap_devices(iface, enabled=True): - """Return a list of devices on an USBpcap interface""" - _usbpcap_check() - devices = _extcap_call( - conf.prog.usbpcapcmd, - ["--extcap-interface", - iface, - "--extcap-config"], - "value", - ["value", "display", "enabled"] - ) - devices = [(dev[0], - dev[1], - dev[2] == "true") for dev in devices] - if enabled: - return [dev for dev in devices if dev[2]] - return devices - - class USBpcapSocket(SuperSocket): - """ - Read packets at layer 2 using USBPcapCMD - """ - nonblocking_socket = True - - @staticmethod - def select(sockets, remain=None): - return sockets, None - - def __init__(self, iface=None, *args, **karg): - _usbpcap_check() - if iface is None: - warning("Available interfaces: [%s]" % - " ".join(x[0] for x in get_usbpcap_interfaces())) - raise NameError("No interface specified !" - " See get_usbpcap_interfaces()") - self.outs = None - args = ['-d', iface, '-b', '134217728', '-A', '-o', '-'] - self.usbpcap_proc = subprocess.Popen( - [conf.prog.usbpcapcmd] + args, - stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - self.ins = PcapReader(self.usbpcap_proc.stdout) - - def recv(self, x=MTU): - return self.ins.recv(x) - - def close(self): - SuperSocket.close(self) - self.usbpcap_proc.kill() - - conf.USBsocket = USBpcapSocket diff --git a/scapy/layers/vrrp.py b/scapy/layers/vrrp.py index 3d6a5923da2..be3d7d445dc 100644 --- a/scapy/layers/vrrp.py +++ b/scapy/layers/vrrp.py @@ -1,8 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi # Copyright (C) 6WIND -# This program is published under a GPLv2 license """ VRRP (Virtual Router Redundancy Protocol). @@ -82,7 +82,8 @@ def post_build(self, p, pay): elif isinstance(self.underlayer, IPv6): ck = in6_chksum(112, self.underlayer, p) else: - warning("No IP(v6) layer to compute checksum on VRRP. Leaving null") # noqa: E501 + warning("No IP(v6) layer to compute checksum on VRRP. " + "Leaving null") ck = 0 p = p[:6] + chb(ck >> 8) + chb(ck & 0xff) + p[8:] return p diff --git a/scapy/layers/vxlan.py b/scapy/layers/vxlan.py index e6c1da98cd4..d7c659cbd93 100644 --- a/scapy/layers/vxlan.py +++ b/scapy/layers/vxlan.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Virtual eXtensible Local Area Network (VXLAN) diff --git a/scapy/layers/windows/__init__.py b/scapy/layers/windows/__init__.py new file mode 100644 index 00000000000..95ea60fbbe5 --- /dev/null +++ b/scapy/layers/windows/__init__.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + + +""" +This package implements Windows-specific high level helpers. +It makes it easier to use Scapy Windows related objects. + +It currently contains helpers for the Windows Registry. + +Note that if you want to tweak specific fields of the underlying +protocols, you will have to use the lower level objects directly. +""" + +# Make sure config is loaded +from scapy.config import conf # noqa: F401 diff --git a/scapy/layers/windows/erref.py b/scapy/layers/windows/erref.py new file mode 100644 index 00000000000..6193eeb613f --- /dev/null +++ b/scapy/layers/windows/erref.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +[MS-ERREF] error codes +""" + +# SMB2 sect 3.3.5.15 + [MS-ERREF] +STATUS_ERREF = { + 0x00000000: "STATUS_SUCCESS", + 0x00000002: "ERROR_FILE_NOT_FOUND", + 0x00000003: "ERROR_PATH_NOT_FOUND", + 0x00000005: "ERROR_ACCESS_DENIED", + 0x00000006: "ERROR_INVALID_HANDLE", + 0x00000011: "ERROR_NOT_SAME_DEVICE", + 0x00000013: "ERROR_WRITE_PROTECT", + 0x00000057: "ERROR_INVALID_PARAMETER", + 0x0000007A: "ERROR_INSUFFICIENT_BUFFER", + 0x0000007B: "ERROR_INVALID_NAME", + 0x000000A1: "ERROR_BAD_PATHNAME", + 0x000000B7: "ERROR_ALREADY_EXISTS", + 0x000000EA: "ERROR_MORE_DATA", + 0x00000103: "STATUS_PENDING", + 0x0000010B: "STATUS_NOTIFY_CLEANUP", + 0x0000010C: "STATUS_NOTIFY_ENUM_DIR", + 0x000003E6: "ERROR_NOACCESS", + 0x00000532: "ERROR_PASSWORD_EXPIRED", + 0x00000533: "ERROR_ACCOUNT_DISABLED", + 0x000006F7: "ERROR_SUBKEY_NOT_FOUND", + 0x000006FE: "ERROR_TRUST_FAILURE", + 0x80000005: "STATUS_BUFFER_OVERFLOW", + 0x80000006: "STATUS_NO_MORE_FILES", + 0x8000002D: "STATUS_STOPPED_ON_SYMLINK", + 0x80070005: "E_ACCESSDENIED", + 0x8007000E: "E_OUTOFMEMORY", + 0x80090308: "SEC_E_INVALID_TOKEN", + 0x8009030C: "SEC_E_LOGON_DENIED", + 0x8009030F: "SEC_E_MESSAGE_ALTERED", + 0x80090310: "SEC_E_OUT_OF_SEQUENCE", + 0x80090346: "SEC_E_BAD_BINDINGS", + 0x80090351: "SEC_E_SMARTCARD_CERT_REVOKED", + 0xC0000003: "STATUS_INVALID_INFO_CLASS", + 0xC0000004: "STATUS_INFO_LENGTH_MISMATCH", + 0xC000000D: "STATUS_INVALID_PARAMETER", + 0xC000000F: "STATUS_NO_SUCH_FILE", + 0xC0000016: "STATUS_MORE_PROCESSING_REQUIRED", + 0xC0000022: "STATUS_ACCESS_DENIED", + 0xC0000033: "STATUS_OBJECT_NAME_INVALID", + 0xC0000034: "STATUS_OBJECT_NAME_NOT_FOUND", + 0xC0000043: "STATUS_SHARING_VIOLATION", + 0xC0000061: "STATUS_PRIVILEGE_NOT_HELD", + 0xC0000064: "STATUS_NO_SUCH_USER", + 0xC000006A: "STATUS_WRONG_PASSWORD", + 0xC000006D: "STATUS_LOGON_FAILURE", + 0xC000006E: "STATUS_ACCOUNT_RESTRICTION", + 0xC0000070: "STATUS_INVALID_WORKSTATION", + 0xC0000071: "STATUS_PASSWORD_EXPIRED", + 0xC0000072: "STATUS_ACCOUNT_DISABLED", + 0xC000009A: "STATUS_INSUFFICIENT_RESOURCES", + 0xC00000B0: "STATUS_PIPE_DISCONNECTED", + 0xC00000BA: "STATUS_FILE_IS_A_DIRECTORY", + 0xC00000BB: "STATUS_NOT_SUPPORTED", + 0xC00000C9: "STATUS_NETWORK_NAME_DELETED", + 0xC00000CC: "STATUS_BAD_NETWORK_NAME", + 0xC0000120: "STATUS_CANCELLED", + 0xC0000122: "STATUS_INVALID_COMPUTER_NAME", + 0xC0000128: "STATUS_FILE_CLOSED", # backup error for older Win versions + 0xC000015B: "STATUS_LOGON_TYPE_NOT_GRANTED", + 0xC000018B: "STATUS_NO_TRUST_SAM_ACCOUNT", + 0xC000019C: "STATUS_FS_DRIVER_REQUIRED", + 0xC0000203: "STATUS_USER_SESSION_DELETED", + 0xC000020C: "STATUS_CONNECTION_DISCONNECTED", + 0xC0000225: "STATUS_NOT_FOUND", + 0xC0000257: "STATUS_PATH_NOT_COVERED", + 0xC00002FB: "STATUS_KDC_INVALID_REQUEST", + 0xC000035C: "STATUS_NETWORK_SESSION_EXPIRED", +} diff --git a/scapy/layers/windows/registry.py b/scapy/layers/windows/registry.py new file mode 100644 index 00000000000..62c26216c0c --- /dev/null +++ b/scapy/layers/windows/registry.py @@ -0,0 +1,923 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) github.com/Ebrix + +""" +Windows Registry RPCs + +This file provides high-level wrapping over Windows Registry related RPCs. +(scapy.layers.msrpce.raw.ms_rrp) +""" + +import struct + +from enum import IntEnum, IntFlag +from typing import Optional, Union, List + +from scapy.compat import StrEnum +from scapy.packet import Packet +from scapy.error import log_runtime + +from scapy.layers.windows.security import ( + SECURITY_DESCRIPTOR, +) +from scapy.layers.msrpce.rpcclient import DCERPC_Client +from scapy.layers.dcerpc import ( + NDRConformantArray, + NDRPointer, + NDRVaryingArray, + DCERPC_Transport, + DCE_C_AUTHN_LEVEL, + find_dcerpc_interface, +) + +from scapy.layers.msrpce.raw.ms_rrp import ( + BaseRegCloseKey_Request, + BaseRegCreateKey_Request, + BaseRegDeleteKey_Request, + BaseRegDeleteValue_Request, + BaseRegEnumKey_Request, + BaseRegEnumValue_Request, + BaseRegGetKeySecurity_Request, + BaseRegGetVersion_Request, + BaseRegOpenKey_Request, + BaseRegQueryInfoKey_Request, + BaseRegQueryInfoKey_Response, + BaseRegQueryValue_Request, + BaseRegSaveKey_Request, + BaseRegSetValue_Request, + NDRContextHandle, + OpenClassesRoot_Request, + OpenCurrentConfig_Request, + OpenCurrentUser_Request, + OpenLocalMachine_Request, + OpenPerformanceData_Request, + OpenPerformanceNlsText_Request, + OpenPerformanceText_Request, + OpenUsers_Request, + PRPC_SECURITY_ATTRIBUTES, + PRPC_SECURITY_DESCRIPTOR, + RPC_UNICODE_STRING, +) + + +class RootKeys(StrEnum): + """ + Standard root keys for the Windows registry + """ + + HKEY_CLASSES_ROOT = "HKCR" + HKEY_CURRENT_USER = "HKCU" + HKEY_LOCAL_MACHINE = "HKLM" + HKEY_CURRENT_CONFIG = "HKCC" + HKEY_USERS = "HKU" + HKEY_PERFORMANCE_DATA = "HKPD" + HKEY_PERFORMANCE_TEXT = "HKPT" + HKEY_PERFORMANCE_NLSTEXT = "HKPN" + + def __new__(cls, value): + # 1. Strip and uppercase the raw input + normalized = value.strip().upper() + # 2. Create the enum member with the normalized value + obj = str.__new__(cls, normalized) + obj._value_ = normalized + return obj + + +class RegOptions(IntFlag): + """ + Registry options for registry keys + """ + + REG_OPTION_NON_VOLATILE = 0x00000000 + REG_OPTION_VOLATILE = 0x00000001 + REG_OPTION_CREATE_LINK = 0x00000002 + REG_OPTION_BACKUP_RESTORE = 0x00000004 + REG_OPTION_OPEN_LINK = 0x00000008 + REG_OPTION_DONT_VIRTUALIZE = 0x00000010 + + +class RegType(IntEnum): + """ + Registry value types + """ + + # These constants are used to specify the type of a registry value. + + REG_NONE = 0 # No defined value type + REG_SZ = 1 # Unicode string + REG_EXPAND_SZ = 2 # Unicode string with environment variable expansion + REG_BINARY = 3 # Binary data + REG_DWORD = 4 # 32-bit unsigned integer + REG_DWORD_BIG_ENDIAN = 5 # 32-bit unsigned integer in big-endian format + REG_LINK = 6 # Symbolic link + REG_MULTI_SZ = 7 # Multiple Unicode strings + REG_QWORD = 11 # 64-bit unsigned integer + UNK = 99999 # fallback default + + @classmethod + def _missing_(cls, value): + log_runtime.info(f"Unknown registry type: {value}, using UNK") + unk = cls.UNK + unk.real_value = value + return unk + + def __new__(cls, value, real_value=None): + obj = int.__new__(cls, value) + obj._value_ = value + if real_value is None: + real_value = value + obj.real_value = real_value + return obj + + @classmethod + def fromstr(cls, value: Union[str, int]) -> "RegType": + """ + Convert a string to a RegType enum member. + + :param value: The string representation of the registry type. + :return: The corresponding RegType enum member. + """ + if isinstance(value, int): + try: + return cls(value) + except ValueError: + log_runtime.info(f"Unknown registry type: {value}, using UNK") + return cls.UNK + else: + # we want to make sure that regdword, reg_dword, dword and upper + # case equivalents are all properly parsed + value = value.strip().upper() + if "_" not in value: + if value[:3] == "REG": + value = value[3:] + value = "REG_" + value.replace("REG", "", 1) + try: + return cls[value] + except (ValueError, KeyError): + log_runtime.info(f"Unknown registry type: {value}, using UNK") + return cls.UNK + + +class RegEntry: + """ + RegEntry represents a Registry Value, inside a Registry Key. + + :param reg_name: the name of the registry value + :param reg_type: the type of the registry value + :param reg_data: the data of the registry value + """ + + def __init__( + self, + reg_name: str, + reg_type: int, + reg_data: Union[list, str, bytes, int], + ): + # Name + self.reg_name = reg_name + + # Type + try: + self.reg_type = RegType(reg_type) + except ValueError: + self.reg_type = RegType.UNK + + # Check data type + if reg_type == RegType.REG_MULTI_SZ: + if not isinstance(reg_data, list): + raise ValueError("Data must be a 'list' of 'str' for this type.") + elif reg_type in [ + RegType.REG_SZ, + RegType.REG_EXPAND_SZ, + RegType.REG_LINK, + ]: + if not isinstance(reg_data, str): + raise ValueError("Data must be a 'str' for this type.") + elif reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: + if not isinstance(reg_data, bytes): + raise ValueError("Data must be a 'bytes' for this type.") + elif reg_type in [ + RegType.REG_DWORD, + RegType.REG_QWORD, + RegType.REG_DWORD_BIG_ENDIAN, + ]: + if not isinstance(reg_data, int): + raise ValueError("Data must be a 'int' for this type.") + else: + if not isinstance(reg_data, bytes): + raise ValueError("Data of this unknown type must be a 'bytes'.") + + self.reg_data = reg_data + + def encode(self) -> bytes: + """ + Encode data based on the type. + """ + if self.reg_type == RegType.REG_MULTI_SZ: + # encode to multiple null terminated strings + return ( + b"\x00\x00".join(x.strip().encode("utf-16le") for x in self.reg_data) + + b"\x00\x00" # final \x00 + + b"\x00\x00" # final empty string + ) + elif self.reg_type in [ + RegType.REG_SZ, + RegType.REG_EXPAND_SZ, + RegType.REG_LINK, + ]: + return self.reg_data.encode("utf-16le") + elif self.reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: + return self.reg_data + elif self.reg_type in [ + RegType.REG_DWORD, + RegType.REG_QWORD, + RegType.REG_DWORD_BIG_ENDIAN, + ]: + fmt = { + RegType.REG_DWORD: "I", + }[self.reg_type] + return struct.pack(fmt, self.reg_data) + else: + return self.reg_data + + @staticmethod + def frombytes(reg_name: str, reg_type: RegType, data: bytes): + """ + Create a RegEntry from bytes read on the network. + """ + if reg_type == RegType.REG_MULTI_SZ: + # encode to multiple null terminated strings + reg_data = data.decode("utf-16le")[:-2].split("\x00") + elif reg_type in [ + RegType.REG_SZ, + RegType.REG_EXPAND_SZ, + RegType.REG_LINK, + ]: + reg_data = data.decode("utf-16le") + elif reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: + reg_data = data + elif reg_type in [ + RegType.REG_DWORD, + RegType.REG_QWORD, + RegType.REG_DWORD_BIG_ENDIAN, + ]: + fmt = { + RegType.REG_DWORD: "I", + }[reg_type] + reg_data = struct.unpack(fmt, data)[0] + else: + reg_data = data + + return RegEntry( + reg_name=reg_name, + reg_type=reg_type, + reg_data=reg_data, + ) + + @staticmethod + def fromstr(reg_name: str, reg_type: RegType, data: str): + """ + Create a RegEntry from user input. + """ + if reg_type == RegType.REG_MULTI_SZ: + reg_data = data.split(";") + elif reg_type in [ + RegType.REG_SZ, + RegType.REG_EXPAND_SZ, + RegType.REG_LINK, + ]: + reg_data = data + elif reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: + reg_data = bytes.fromhex(data) + elif reg_type in [ + RegType.REG_DWORD, + RegType.REG_QWORD, + RegType.REG_DWORD_BIG_ENDIAN, + ]: + reg_data = int(data) + else: + reg_data = data + + return RegEntry( + reg_name=reg_name, + reg_type=reg_type, + reg_data=reg_data, + ) + + def __str__(self) -> str: + return ( + f"{self.reg_name} ({self.reg_type.name}: " + + f"{self.reg_type.real_value if self.reg_type == RegType.UNK else self.reg_type.value}" # noqa E501 + + f") {self.reg_data}" + ) + + def __repr__(self) -> str: + return f"RegEntry({self.reg_name}, {self.reg_type}, {self.reg_data})" + + def __eq__(self, value): + return isinstance(value, RegEntry) and all( + [ + self.reg_data == value.reg_data, + self.reg_type == value.reg_type, + self.reg_data == value.reg_data, + ] + ) + + +class RRP_Client(DCERPC_Client): + """ + High level [MS-RRP] (Windows Registry) Client + """ + + def __init__( + self, + auth_level=DCE_C_AUTHN_LEVEL.PKT_INTEGRITY, + verb=True, + **kwargs, + ): + self.interface = find_dcerpc_interface("winreg") + super(RRP_Client, self).__init__( + DCERPC_Transport.NCACN_NP, + auth_level=auth_level, + verb=verb, + **kwargs, + ) + + def connect(self, host, **kwargs): + """ + This calls DCERPC_Client's connect + """ + super(RRP_Client, self).connect( + host=host, + interface=self.interface, + endpoint="winreg", + **kwargs, + ) + + def bind(self): + """ + This calls DCERPC_Client's bind + """ + super(RRP_Client, self).bind(self.interface) + + def get_root_key_handle( + self, + root_key_name: RootKeys, + sam_desired: int = 0x2000000, # Maximum Allowed + timeout: int = 5, + ) -> Optional[NDRContextHandle]: + """ + Get a handle to a root key. + + :param root_key_name: The name of the root key to open. + Must be one of the RootKeys enum values. + :param sam_desired: The desired access rights for the key. + :param ServerName: The server name. The ServerName SHOULD be + sent as NULL, and MUST be ignored + when it is received because binding to the server + is already complete at this stage + :return: The handle to the opened root key. + """ + + cls_req = { + RootKeys.HKEY_CLASSES_ROOT: OpenClassesRoot_Request, + RootKeys.HKEY_CURRENT_USER: OpenCurrentUser_Request, + RootKeys.HKEY_LOCAL_MACHINE: OpenLocalMachine_Request, + RootKeys.HKEY_USERS: OpenUsers_Request, + RootKeys.HKEY_CURRENT_CONFIG: OpenCurrentConfig_Request, + RootKeys.HKEY_PERFORMANCE_DATA: OpenPerformanceData_Request, + RootKeys.HKEY_PERFORMANCE_TEXT: OpenPerformanceText_Request, + RootKeys.HKEY_PERFORMANCE_NLSTEXT: OpenPerformanceNlsText_Request, + } + + if root_key_name not in cls_req: + raise ValueError(f"Unknown root key: {root_key_name}") + + return self.sr1_req( + cls_req[root_key_name]( + ServerName=None, + samDesired=sam_desired, + ), + timeout=timeout, + ).phKey + + def get_subkey_handle( + self, + root_key_handle: NDRContextHandle, + subkey_path: str, + desired_access_rights: int = 0x2000000, # Maximum Allowed + options: RegOptions = RegOptions.REG_OPTION_NON_VOLATILE, + timeout: int = 5, + ) -> NDRContextHandle: + """ + Get a handle to a subkey. + + :param root_key_handle: The handle to the root key. + :param subkey_path: The name of the subkey to open. + :param desired_access_rights: The desired access rights for the subkey. + :param timeout: The timeout for the request. + :return: The handle to the opened subkey. + """ + + # Ensure it is null-terminated and handle the special case of "." + if str(subkey_path) == ".": + subkey_path = "\x00" + elif not str(subkey_path).endswith("\x00"): + subkey_path = str(subkey_path) + "\x00" + + response = self.sr1_req( + BaseRegOpenKey_Request( + hKey=root_key_handle, + lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), + samDesired=desired_access_rights, + dwOptions=options, + ), + timeout=timeout, + ) + + if response.status != 0: + raise ValueError(response.status) + + return response.phkResult + + def get_version( + self, + key_handle: NDRContextHandle, + timeout: int = 5, + ) -> Packet: + """ + Get the version of the registry server. + + :param client: The DCERPC client. + :param timeout: The timeout for the request. + :return: The response packet containing the version information. + """ + + response = self.sr1_req( + BaseRegGetVersion_Request( + hKey=key_handle, + ), + timeout=timeout, + ) + + if response.status != 0: + log_runtime.error( + "Got status %s while getting version", hex(response.status) + ) + + return response + + def get_key_info( + self, + key_handle: NDRContextHandle, + timeout: int = 5, + ) -> BaseRegQueryInfoKey_Response: + """ + Get information about a given registry key. + + :param hKey: The handle to the registry key (root key or subkey). + :param timeout: The timeout for the request. + :return: The response packet containing the key information. + """ + + response = self.sr1_req( + BaseRegQueryInfoKey_Request( + hKey=key_handle, + lpClassIn=RPC_UNICODE_STRING(), + ), + timeout=timeout, + ) + + if response.status != 0: + log_runtime.error( + "Got status %s while querying key info", hex(response.status) + ) + raise ValueError(response.status) + + if response.lpClassOut.Length > 2: + # There is a Class info stored. We need to + # get it by specifying the proper MaximumLength. + # By default the size is "2". + response = self.sr1_req( + BaseRegQueryInfoKey_Request( + hKey=key_handle, + lpClassIn=RPC_UNICODE_STRING( + MaximumLength=response.lpClassOut.Length + ), + ), + timeout=timeout, + ) + + if response.status != 0: + log_runtime.error( + "Got status %s while querying key info", hex(response.status) + ) + raise ValueError(response.status) + + return response + + def get_key_security( + self, + key_handle: NDRContextHandle, + security_information: int = None, + timeout: int = 5, + ) -> SECURITY_DESCRIPTOR: + """ + Get the security descriptor of a given registry key. + + :param hKey: The handle to the registry key (root key or subkey). + :param security_information: The security information to retrieve. + :param timeout: The timeout for the request. + :return: The response packet containing the security descriptor. + """ + + if security_information is None: + security_information = ( + 0x00000001 # OWNER_SECURITY_INFORMATION + | 0x00000002 # GROUP_SECURITY_INFORMATION + | 0x00000004 # DACL_SECURITY_INFORMATION + ) + + # Build initial request + req = BaseRegGetKeySecurity_Request( + hKey=key_handle, + SecurityInformation=security_information, + pRpcSecurityDescriptorIn=PRPC_SECURITY_DESCRIPTOR( + cbInSecurityDescriptor=512, # Initial size of the buffer + ), + ) + + # Send request + response = self.sr1_req(req, timeout=timeout) + if response.status == 0x0000007A: # ERROR_INSUFFICIENT_BUFFER + # The buffer was too small, we need to retry with a larger one + req.pRpcSecurityDescriptorIn.cbInSecurityDescriptor = ( + response.pRpcSecurityDescriptorOut.cbInSecurityDescriptor + ) + response = self.sr1_req(req, timeout=timeout) + + # Check the response status + if response.status != 0: + log_runtime.error( + "Got status %s while getting security", hex(response.status) + ) + return None + + return SECURITY_DESCRIPTOR( + response.pRpcSecurityDescriptorOut.valueof("lpSecurityDescriptor") + ) + + def enum_subkeys( + self, + key_handle: NDRContextHandle, + timeout: int = 5, + ) -> List[str]: + """ + Enumerate subkeys of a given registry key. + + :param hKey: The handle to the registry key (root key or subkey). + :param timeout: The timeout for the request. + :return: A generator yielding the responses for each enumerated subkey. + """ + index = 0 + results = [] + + while True: + response = self.sr1_req( + BaseRegEnumKey_Request( + hKey=key_handle, + dwIndex=index, + lpNameIn=RPC_UNICODE_STRING(MaximumLength=1024), + lpClassIn=RPC_UNICODE_STRING(), + lpftLastWriteTime=None, + ), + timeout=timeout, + ) + + # Send request + if response.status == 0x00000103: # ERROR_NO_MORE_ITEMS + break + # Check the response status + elif response.status != 0: + raise ValueError(response.status) + + index += 1 + results.append(response.lpNameOut.valueof("Buffer")[:-1].decode()) + return results + + def enum_values( + self, + key_handle: NDRContextHandle, + timeout: int = 5, + ) -> List[RegEntry]: + """ + Enumerate values of a given registry key. + + :param hKey: The handle to the registry key (root key or subkey). + :param timeout: The timeout for the request. + :return: A generator yielding the responses for each enumerated value. + """ + index = 0 + results = [] + + while True: + # Get the name and value at index `index` + response = self.sr1_req( + BaseRegEnumValue_Request( + hKey=key_handle, + dwIndex=index, + lpValueNameIn=RPC_UNICODE_STRING( + MaximumLength=2048, + Buffer=NDRPointer( + value=NDRConformantArray( + max_count=1024, value=NDRVaryingArray(value=b"") + ) + ), + ), + lpType=0, # pointer to type, set to 0 for query + lpData=None, # pointer to buffer + lpcbData=0, # pointer to buffer size + lpcbLen=0, # pointer to length + ), + timeout=timeout, + ) + + if response.status == 0x00000103: # ERROR_NO_MORE_ITEMS + break + elif response.status != 0: + raise ValueError(response.status) + + # Get the value name + lpValueName = response.valueof("lpValueNameOut") + + # Get value content + req = BaseRegQueryValue_Request( + hKey=key_handle, + lpValueName=lpValueName, + lpType=0, + lpcbData=1024, + lpcbLen=0, + lpData=NDRPointer( + value=NDRConformantArray( + max_count=1024, + value=NDRVaryingArray(actual_count=0, value=b""), + ) + ), + ) + + # Send request + response = self.sr1_req(req, timeout=timeout) + if response.status == 0x000000EA: # ERROR_MORE_DATA + # The buffer was too small, we need to retry with a larger one + req.lpcbData = response.lpcbData + req.lpData.value.max_count = response.lpcbData.value + response = self.sr1_req(req, timeout=timeout) + + # Check the response status + elif response.status != 0: + raise ValueError(response.status) + + index += 1 + results.append( + RegEntry.frombytes( + lpValueName.valueof("Buffer")[:-1].decode(), + response.valueof("lpType"), + response.valueof("lpData"), + ) + ) + + return results + + def get_value( + self, + key_handle: NDRContextHandle, + value_name: str, + timeout: int = 5, + ) -> RegEntry: + """ + Get the value of a given registry key. + + :param hKey: The handle to the registry key (root key or subkey). + :param value_name: The name of the value to retrieve. + :param timeout: The timeout for the request. + :return: The response packet containing the value data. + """ + + pkt = BaseRegQueryValue_Request( + hKey=key_handle, + lpValueName=value_name, + lpType=0, + lpcbData=1024, + lpcbLen=0, + lpData=NDRPointer( + value=NDRConformantArray( + max_count=1024, value=NDRVaryingArray(actual_count=0, value=b"") + ) + ), + ) + + response = self.sr1_req(pkt, timeout=timeout) + + if response.status == 0x000000EA: # ERROR_MORE_DATA + # The buffer was too small, we need to retry with a larger one + pkt.lpcbData = response.lpcbData + pkt.lpData.value.max_count = response.lpcbData.value + response = self.sr1_req(pkt, timeout=timeout) + + if response.status != 0: + raise ValueError(response.status) + + return RegEntry.frombytes( + value_name, + response.valueof("lpType"), + response.valueof("lpData"), + ) + + def save_subkey( + self, + key_handle: NDRContextHandle, + file_path: str, + security_attributes: PRPC_SECURITY_ATTRIBUTES = None, + timeout: int = 5, + ) -> None: + """ + Save a given registry key to a file. + + :param hKey: The handle to the registry key (root key or subkey). + :param file_path: The path to the file where the key will be saved. + Default path is %WINDIR%\\System32, which is readable by all users. + :param security_attributes: Security attributes for the saved key. + :param timeout: The timeout for the request. + """ + + response = self.sr1_req( + BaseRegSaveKey_Request( + hKey=key_handle, + lpFile=RPC_UNICODE_STRING(Buffer=file_path), + pSecurityAttributes=security_attributes, + ), + timeout=timeout, + ) + + if response.status != 0: + raise ValueError(response.status) + + def set_value( + self, + key_handle: NDRContextHandle, + entry: RegEntry, + timeout: int = 5, + ) -> None: + """ + Set a given value for a registry key. + + :param hKey: The handle to the registry key (root key or subkey). + :param entry: The 'RegEntry' entry to set, containing the name, type and data + of the value. + :param timeout: The timeout for the request. + """ + data = entry.encode() + + response = self.sr1_req( + BaseRegSetValue_Request( + hKey=key_handle, + lpValueName=RPC_UNICODE_STRING( + Buffer=entry.reg_name.encode("utf-8") + b"\x00" + ), + dwType=entry.reg_type.value, + cbData=len(data), + lpData=data, + ), + timeout=timeout, + ) + + if response.status != 0: + raise ValueError(response.status) + + def create_subkey( + self, + root_key_handle: NDRContextHandle, + subkey_path: str, + desired_access_rights: int = 0x2000000, # Maximum allowed + options: RegOptions = RegOptions.REG_OPTION_NON_VOLATILE, + security_attributes: PRPC_SECURITY_ATTRIBUTES = None, + timeout: int = 5, + ) -> NDRContextHandle: + """ + Create a given subkey under a registry key. + + :param client: The DCERPC client. + :param root_key_handle: The handle to the root key. + :param subkey_path: The name of the subkey to create. + :param desired_access_rights: The desired access rights for the subkey. + :param options: The options for the subkey. + :param security_attributes: Security attributes for the created key. + :param timeout: The timeout for the request. + :return: The handle to the created subkey. + """ + + if not str(subkey_path).endswith("\x00"): + subkey_path = str(subkey_path) + "\x00" + + response = self.sr1_req( + BaseRegCreateKey_Request( + hKey=root_key_handle, + lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), + samDesired=desired_access_rights, + dwOptions=options, + lpSecurityAttributes=security_attributes, + ), + timeout=timeout, + ) + + if response.status != 0: + raise ValueError(response.status) + + return response.phkResult + + def delete_subkey( + self, + root_key_handle: NDRContextHandle, + subkey_path: str, + timeout: int = 5, + ) -> None: + """ + Delete a given subkey from a registry key. + + :param client: The DCERPC client. + :param hKey: The handle to the root key. + :param subkey_path: The name of the subkey to remove. + :param timeout: The timeout for the request. + """ + + if not str(subkey_path).endswith("\x00"): + subkey_path = str(subkey_path) + "\x00" + + response = self.sr1_req( + BaseRegDeleteKey_Request( + hKey=root_key_handle, + lpSubKey=RPC_UNICODE_STRING(Buffer=subkey_path), + ), + timeout=timeout, + ) + + if response.status != 0: + raise ValueError(response.status) + + def delete_value( + self, + key_handle: NDRContextHandle, + value_name: str, + timeout: int = 5, + ) -> None: + """ + Delete a given value from a registry key. + + :param client: The DCERPC client. + :param hKey: The handle to the subkey to remove. + :param value_name: The name of the value to delete. + :param timeout: The timeout for the request. + """ + + if not str(value_name).endswith("\x00"): + value_name = str(value_name) + "\x00" + + response = self.sr1_req( + BaseRegDeleteValue_Request( + hKey=key_handle, + lpValueName=RPC_UNICODE_STRING(Buffer=value_name), + ), + timeout=timeout, + ) + + if response.status != 0: + raise ValueError(response.status) + + def close_key( + self, + key_handle: NDRContextHandle, + timeout: int = 5, + ) -> None: + """ + Close a given registry key handle. + + :param client: The DCERPC client. + :param hKey: The handle to the registry key (root key or subkey). + :param timeout: The timeout for the request. + """ + + response = self.sr1_req( + BaseRegCloseKey_Request( + hKey=key_handle, + ), + timeout=timeout, + ) + + if response.status != 0: + raise ValueError(response.status) diff --git a/scapy/layers/windows/security.py b/scapy/layers/windows/security.py new file mode 100644 index 00000000000..8606f893c89 --- /dev/null +++ b/scapy/layers/windows/security.py @@ -0,0 +1,931 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Python objects for Microsoft Windows security structures. +""" + +import re +import struct + +from scapy.config import conf +from scapy.packet import Packet, bind_layers +from scapy.fields import ( + ByteEnumField, + ByteField, + ConditionalField, + FieldLenField, + FieldListField, + FlagsField, + FlagValue, + LEIntField, + LELongField, + LenField, + LEShortEnumField, + LEShortField, + MultipleTypeField, + PacketField, + PacketListField, + ShortField, + StrFieldUtf16, + StrFixedLenField, + StrLenField, + StrLenFieldUtf16, + UUIDField, +) + +from scapy.layers.ntlm import ( + _NTLM_ENUM, + _NTLM_post_build, + _NTLMPayloadField, + _NTLMPayloadPacket, +) + +# [MS-DTYP] sect 2.4.1 + + +class WINNT_SID_IDENTIFIER_AUTHORITY(Packet): + + fields_desc = [ + StrFixedLenField("Value", b"\x00\x00\x00\x00\x00\x01", length=6), + ] + + def default_payload_class(self, payload: bytes) -> Packet: + return conf.padding_layer + + +# [MS-DTYP] sect 2.4.2 + + +class WINNT_SID(Packet): + fields_desc = [ + ByteField("Revision", 1), + FieldLenField("SubAuthorityCount", None, count_of="SubAuthority", fmt="B"), + PacketField( + "IdentifierAuthority", + WINNT_SID_IDENTIFIER_AUTHORITY(), + WINNT_SID_IDENTIFIER_AUTHORITY, + ), + FieldListField( + "SubAuthority", + [0], + LEIntField("", 0), + count_from=lambda pkt: pkt.SubAuthorityCount, + ), + ] + + def default_payload_class(self, payload: bytes) -> Packet: + return conf.padding_layer + + _SID_REG = re.compile(r"^S-(\d)-(\d+)((?:-\d+)*)$") + + @staticmethod + def fromstr(x: str): + """ + Helper to create a SID from its string representation. + + :param x: string representation of the SID like "S-1-5-18" + :type x: str + + Example: + + >>> from scapy.layers.windows.security import WINNT_SID + >>> WINNT_SID.fromstr("S-1-5-18") + SubAuthority=[18] |> + >>> _.summary() + >>> 'S-1-5-18' + """ + + m = WINNT_SID._SID_REG.match(x) + if not m: + raise ValueError("Invalid SID format !") + rev, authority, subauthority = m.groups() + return WINNT_SID( + Revision=int(rev), + IdentifierAuthority=WINNT_SID_IDENTIFIER_AUTHORITY( + Value=struct.pack(">Q", int(authority))[2:] + ), + SubAuthority=[int(x) for x in subauthority[1:].split("-")], + ) + + def summary(self) -> str: + """ + Return the string representation of the SID. + """ + return "S-%s-%s%s" % ( + self.Revision, + struct.unpack(">Q", b"\x00\x00" + self.IdentifierAuthority.Value)[0], + ( + ("-%s" % "-".join(str(x) for x in self.SubAuthority)) + if self.SubAuthority + else "" + ), + ) + + +# https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers + +WELL_KNOWN_SIDS = { + # Universal well-known SID + "S-1-0-0": "Null SID", + "S-1-1-0": "Everyone", + "S-1-2-0": "Local", + "S-1-2-1": "Console Logon", + "S-1-3-0": "Creator Owner ID", + "S-1-3-1": "Creator Group ID", + "S-1-3-2": "Owner Server", + "S-1-3-3": "Group Server", + "S-1-3-4": "Owner Rights", + "S-1-4": "Non-unique Authority", + "S-1-5": "NT Authority", + "S-1-5-80-0": "All Services", + # NT well-known SIDs + "S-1-5-1": "Dialup", + "S-1-5-113": "Local account", + "S-1-5-114": "Local account and member of Administrators group", + "S-1-5-2": "Network", + "S-1-5-3": "Batch", + "S-1-5-4": "Interactive", + "S-1-5-6": "Service", + "S-1-5-7": "Anonymous Logon", + "S-1-5-8": "Proxy", + "S-1-5-9": "Enterprise Domain Controllers", + "S-1-5-10": "Self", + "S-1-5-11": "Authenticated Users", + "S-1-5-12": "Restricted Code", + "S-1-5-13": "Terminal Server User", + "S-1-5-14": "Remote Interactive Logon", + "S-1-5-15": "This Organization", + "S-1-5-17": "IUSR", + "S-1-5-18": "System (or LocalSystem)", + "S-1-5-19": "NT Authority (LocalService)", + "S-1-5-20": "Network Service", + "S-1-5-32-544": "Administrators", + "S-1-5-32-545": "Users", + "S-1-5-32-546": "Guests", + "S-1-5-32-547": "Power Users", + "S-1-5-32-548": "Account Operators", + "S-1-5-32-549": "Server Operators", + "S-1-5-32-550": "Print Operators", + "S-1-5-32-551": "Backup Operators", + "S-1-5-32-552": "Replicators", + "S-1-5-32-554": r"Builtin\Pre-Windows 2000 Compatible Access", + "S-1-5-32-555": r"Builtin\Remote Desktop Users", + "S-1-5-32-556": r"Builtin\Network Configuration Operators", + "S-1-5-32-557": r"Builtin\Incoming Forest Trust Builders", + "S-1-5-32-558": r"Builtin\Performance Monitor Users", + "S-1-5-32-559": r"Builtin\Performance Log Users", + "S-1-5-32-560": r"Builtin\Windows Authorization Access Group", + "S-1-5-32-561": r"Builtin\Terminal Server License Servers", + "S-1-5-32-562": r"Builtin\Distributed COM Users", + "S-1-5-32-568": r"Builtin\IIS_IUSRS", + "S-1-5-32-569": r"Builtin\Cryptographic Operators", + "S-1-5-32-573": r"Builtin\Event Log Readers", + "S-1-5-32-574": r"Builtin\Certificate Service DCOM Access", + "S-1-5-32-575": r"Builtin\RDS Remote Access Servers", + "S-1-5-32-576": r"Builtin\RDS Endpoint Servers", + "S-1-5-32-577": r"Builtin\RDS Management Servers", + "S-1-5-32-578": r"Builtin\Hyper-V Administrators", + "S-1-5-32-579": r"Builtin\Access Control Assistance Operators", + "S-1-5-32-580": r"Builtin\Remote Management Users", + "S-1-5-32-581": r"Builtin\Default Account", + "S-1-5-32-582": r"Builtin\Storage Replica Admins", + "S-1-5-32-583": r"Builtin\Device Owners", + "S-1-5-64-10": "NTLM Authentication", + "S-1-5-64-14": "SChannel Authentication", + "S-1-5-64-21": "Digest Authentication", + "S-1-5-80": "NT Service", + "S-1-5-80-0": "All Services", + "S-1-5-83-0": r"NT VIRTUAL MACHINE\Virtual Machines", +} + + +# [MS-DTYP] sect 2.4.3 + +_WINNT_ACCESS_MASK = { + 0x80000000: "GENERIC_READ", + 0x40000000: "GENERIC_WRITE", + 0x20000000: "GENERIC_EXECUTE", + 0x10000000: "GENERIC_ALL", + 0x02000000: "MAXIMUM_ALLOWED", + 0x01000000: "ACCESS_SYSTEM_SECURITY", + 0x00100000: "SYNCHRONIZE", + 0x00080000: "WRITE_OWNER", + 0x00040000: "WRITE_DACL", + 0x00020000: "READ_CONTROL", + 0x00010000: "DELETE", +} + + +# [MS-DTYP] sect 2.4.4.1 + + +WINNT_ACE_FLAGS = { + 0x01: "OBJECT_INHERIT", + 0x02: "CONTAINER_INHERIT", + 0x04: "NO_PROPAGATE_INHERIT", + 0x08: "INHERIT_ONLY", + 0x10: "INHERITED_ACE", + 0x40: "SUCCESSFUL_ACCESS", + 0x80: "FAILED_ACCESS", +} + + +class WINNT_ACE_HEADER(Packet): + """ + Access Control Entry (ACE) Header + It is composed of 3 fields, followed by ACE-specific data: + + - AceType (1 byte): see below for standard values + - AceFlags (1 byte): see WINNT_ACE_FLAGS + - AceSize (2 bytes): total size of the ACE, including the header + and the ACE-specific data. + """ + + fields_desc = [ + ByteEnumField( + "AceType", + 0, + { + 0x00: "ACCESS_ALLOWED", + 0x01: "ACCESS_DENIED", + 0x02: "SYSTEM_AUDIT", + 0x03: "SYSTEM_ALARM", + 0x04: "ACCESS_ALLOWED_COMPOUND", + 0x05: "ACCESS_ALLOWED_OBJECT", + 0x06: "ACCESS_DENIED_OBJECT", + 0x07: "SYSTEM_AUDIT_OBJECT", + 0x08: "SYSTEM_ALARM_OBJECT", + 0x09: "ACCESS_ALLOWED_CALLBACK", + 0x0A: "ACCESS_DENIED_CALLBACK", + 0x0B: "ACCESS_ALLOWED_CALLBACK_OBJECT", + 0x0C: "ACCESS_DENIED_CALLBACK_OBJECT", + 0x0D: "SYSTEM_AUDIT_CALLBACK", + 0x0E: "SYSTEM_ALARM_CALLBACK", + 0x0F: "SYSTEM_AUDIT_CALLBACK_OBJECT", + 0x10: "SYSTEM_ALARM_CALLBACK_OBJECT", + 0x11: "SYSTEM_MANDATORY_LABEL", + 0x12: "SYSTEM_RESOURCE_ATTRIBUTE", + 0x13: "SYSTEM_SCOPED_POLICY_ID", + }, + ), + FlagsField( + "AceFlags", + 0, + 8, + WINNT_ACE_FLAGS, + ), + LenField("AceSize", None, fmt=" conditional expression + cond_expr = None + if hasattr(self.payload, "ApplicationData"): + # Parse tokens + res = [] + for ct in self.payload.ApplicationData.Tokens: + if ct.TokenType in [ + # binary operators + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x88, 0x8e, 0x8f, + 0xa0, 0xa1 + ]: + t1 = res.pop(-1) + t0 = res.pop(-1) + tt = ct.sprintf("%TokenType%") + if ct.TokenType in [0xa0, 0xa1]: # && and || + res.append(f"({t0}) {tt} ({t1})") + else: + res.append(f"{t0} {tt} {t1}") + elif ct.TokenType in [ + # unary operators + 0x87, 0x8d, 0xa2, 0x89, 0x8a, 0x8b, 0x8c, 0x91, 0x92, 0x93 + ]: + t0 = res.pop(-1) + tt = ct.sprintf("%TokenType%") + res.append(f"{tt}{t0}") + elif ct.TokenType in [ + # values + 0x01, 0x02, 0x03, 0x04, 0x10, 0x18, 0x50, 0x51, 0xf8, 0xf9, + 0xfa, 0xfb + ]: + def lit(ct): + if ct.TokenType in [0x10, 0x18]: # literal strings + return '"%s"' % ct.value + elif ct.TokenType == 0x50: # composite + return "({%s})" % ",".join(lit(x) for x in ct.value) + else: + return str(ct.value) + res.append(lit(ct)) + elif ct.TokenType == 0x00: # padding + pass + else: + raise ValueError("Unhandled token type %s" % ct.TokenType) + if len(res) != 1: + raise ValueError("Incomplete SDDL !") + cond_expr = "(%s)" % res[0] + return { + "ace-flags-string": ace_flag_string, + "sid-string": sid_string, + "mask": mask, + "object-guid": object_guid, + "inherited-object-guid": inherit_object_guid, + "cond-expr": cond_expr, + } + # fmt: on + + def toSDDL(self, accessMask=None): + """ + Return SDDL + """ + data = self.extractData(accessMask=accessMask) + ace_rights = "" # TODO + if self.AceType in [0x9, 0xA, 0xB, 0xD]: # Conditional ACE + conditional_ace_type = { + 0x09: "XA", + 0x0A: "XD", + 0x0B: "XU", + 0x0D: "ZA", + }[self.AceType] + return "D:(%s)" % ( + ";".join( + x + for x in [ + conditional_ace_type, + data["ace-flags-string"], + ace_rights, + str(data["object-guid"]), + str(data["inherited-object-guid"]), + data["sid-string"], + data["cond-expr"], + ] + if x is not None + ) + ) + else: + ace_type = { + 0x00: "A", + 0x01: "D", + 0x02: "AU", + 0x05: "OA", + 0x06: "OD", + 0x07: "OU", + 0x11: "ML", + 0x13: "SP", + }[self.AceType] + return "(%s)" % ( + ";".join( + x + for x in [ + ace_type, + data["ace-flags-string"], + ace_rights, + str(data["object-guid"]), + str(data["inherited-object-guid"]), + data["sid-string"], + data["cond-expr"], + ] + if x is not None + ) + ) + + +# [MS-DTYP] sect 2.4.4.2 + + +class WINNT_ACCESS_ALLOWED_ACE(Packet): + fields_desc = [ + FlagsField("Mask", 0, -32, _WINNT_ACCESS_MASK), + PacketField("Sid", WINNT_SID(), WINNT_SID), + ] + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_ALLOWED_ACE, AceType=0x00) + + +# [MS-DTYP] sect 2.4.4.3 + + +class WINNT_ACCESS_ALLOWED_OBJECT_ACE(Packet): + fields_desc = [ + FlagsField("Mask", 0, -32, _WINNT_ACCESS_MASK), + FlagsField( + "Flags", + 0, + -32, + { + 0x00000001: "OBJECT_TYPE_PRESENT", + 0x00000002: "INHERITED_OBJECT_TYPE_PRESENT", + }, + ), + ConditionalField( + UUIDField("ObjectType", None, uuid_fmt=UUIDField.FORMAT_LE), + lambda pkt: pkt.Flags.OBJECT_TYPE_PRESENT, + ), + ConditionalField( + UUIDField("InheritedObjectType", None, uuid_fmt=UUIDField.FORMAT_LE), + lambda pkt: pkt.Flags.INHERITED_OBJECT_TYPE_PRESENT, + ), + PacketField("Sid", WINNT_SID(), WINNT_SID), + ] + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_ALLOWED_OBJECT_ACE, AceType=0x05) + + +# [MS-DTYP] sect 2.4.4.4 + + +class WINNT_ACCESS_DENIED_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_DENIED_ACE, AceType=0x01) + + +# [MS-DTYP] sect 2.4.4.5 + + +class WINNT_ACCESS_DENIED_OBJECT_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_OBJECT_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_DENIED_OBJECT_ACE, AceType=0x06) + + +# [MS-DTYP] sect 2.4.4.17.4+ + + +class WINNT_APPLICATION_DATA_LITERAL_TOKEN(Packet): + def default_payload_class(self, payload): + return conf.padding_layer + + +# fmt: off +WINNT_APPLICATION_DATA_LITERAL_TOKEN.fields_desc = [ + ByteEnumField( + "TokenType", + 0, + { + # [MS-DTYP] sect 2.4.4.17.5 + 0x00: "Padding token", + 0x01: "Signed int8", + 0x02: "Signed int16", + 0x03: "Signed int32", + 0x04: "Signed int64", + 0x10: "Unicode", + 0x18: "Octet String", + 0x50: "Composite", + 0x51: "SID", + # [MS-DTYP] sect 2.4.4.17.6 + 0x80: "==", + 0x81: "!=", + 0x82: "<", + 0x83: "<=", + 0x84: ">", + 0x85: ">=", + 0x86: "Contains", + 0x88: "Any_of", + 0x8e: "Not_Contains", + 0x8f: "Not_Any_of", + 0x89: "Member_of", + 0x8a: "Device_Member_of", + 0x8b: "Member_of_Any", + 0x8c: "Device_Member_of_Any", + 0x90: "Not_Member_of", + 0x91: "Not_Device_Member_of", + 0x92: "Not_Member_of_Any", + 0x93: "Not_Device_Member_of_Any", + # [MS-DTYP] sect 2.4.4.17.7 + 0x87: "Exists", + 0x8d: "Not_Exists", + 0xa0: "&&", + 0xa1: "||", + 0xa2: "!", + # [MS-DTYP] sect 2.4.4.17.8 + 0xf8: "Local attribute", + 0xf9: "User Attribute", + 0xfa: "Resource Attribute", + 0xfb: "Device Attribute", + } + ), + ConditionalField( + # Strings + LEIntField("length", 0), + lambda pkt: pkt.TokenType in [ + 0x10, # Unicode string + 0x18, # Octet string + 0xf8, 0xf9, 0xfa, 0xfb, # Attribute tokens + 0x50, # Composite + ] + ), + ConditionalField( + MultipleTypeField( + [ + ( + LELongField("value", 0), + lambda pkt: pkt.TokenType in [ + 0x01, # signed int8 + 0x02, # signed int16 + 0x03, # signed int32 + 0x04, # signed int64 + ] + ), + ( + StrLenFieldUtf16("value", b"", length_from=lambda pkt: pkt.length), + lambda pkt: pkt.TokenType in [ + 0x10, # Unicode string + 0xf8, 0xf9, 0xfa, 0xfb, # Attribute tokens + ] + ), + ( + StrLenField("value", b"", length_from=lambda pkt: pkt.length), + lambda pkt: pkt.TokenType == 0x18, # Octet string + ), + ( + PacketListField("value", [], WINNT_APPLICATION_DATA_LITERAL_TOKEN, + length_from=lambda pkt: pkt.length), + lambda pkt: pkt.TokenType == 0x50, # Composite + ), + + ], + StrFixedLenField("value", b"", length=0), + ), + lambda pkt: pkt.TokenType in [ + 0x01, 0x02, 0x03, 0x04, 0x10, 0x18, 0xf8, 0xf9, 0xfa, 0xfb, 0x50 + ] + ), + ConditionalField( + # Literal + ByteEnumField("sign", 0, { + 0x01: "+", + 0x02: "-", + 0x03: "None", + }), + lambda pkt: pkt.TokenType in [ + 0x01, # signed int8 + 0x02, # signed int16 + 0x03, # signed int32 + 0x04, # signed int64 + ] + ), + ConditionalField( + # Literal + ByteEnumField("base", 0, { + 0x01: "Octal", + 0x02: "Decimal", + 0x03: "Hexadecimal", + }), + lambda pkt: pkt.TokenType in [ + 0x01, # signed int8 + 0x02, # signed int16 + 0x03, # signed int32 + 0x04, # signed int64 + ] + ), +] +# fmt: on + + +class WINNT_APPLICATION_DATA(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"\x61\x72\x74\x78", length=4), + PacketListField( + "Tokens", + [], + WINNT_APPLICATION_DATA_LITERAL_TOKEN, + ), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +# [MS-DTYP] sect 2.4.4.6 + + +class WINNT_ACCESS_ALLOWED_CALLBACK_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_ACE.fields_desc + [ + PacketField( + "ApplicationData", WINNT_APPLICATION_DATA(), WINNT_APPLICATION_DATA + ), + ] + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_ALLOWED_CALLBACK_ACE, AceType=0x09) + + +# [MS-DTYP] sect 2.4.4.7 + + +class WINNT_ACCESS_DENIED_CALLBACK_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_CALLBACK_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_DENIED_CALLBACK_ACE, AceType=0x0A) + + +# [MS-DTYP] sect 2.4.4.8 + + +class WINNT_ACCESS_ALLOWED_CALLBACK_OBJECT_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_OBJECT_ACE.fields_desc + [ + PacketField( + "ApplicationData", WINNT_APPLICATION_DATA(), WINNT_APPLICATION_DATA + ), + ] + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_ALLOWED_CALLBACK_OBJECT_ACE, AceType=0x0B) + + +# [MS-DTYP] sect 2.4.4.9 + + +class WINNT_ACCESS_DENIED_CALLBACK_OBJECT_ACE(Packet): + fields_desc = WINNT_ACCESS_DENIED_OBJECT_ACE.fields_desc + [ + PacketField( + "ApplicationData", WINNT_APPLICATION_DATA(), WINNT_APPLICATION_DATA + ), + ] + + +bind_layers(WINNT_ACE_HEADER, WINNT_ACCESS_DENIED_CALLBACK_OBJECT_ACE, AceType=0x0C) + + +# [MS-DTYP] sect 2.4.4.10 + + +class WINNT_SYSTEM_AUDIT_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_SYSTEM_AUDIT_ACE, AceType=0x02) + + +# [MS-DTYP] sect 2.4.4.11 + + +class WINNT_SYSTEM_AUDIT_OBJECT_ACE(Packet): + # doc is wrong. + fields_desc = WINNT_ACCESS_ALLOWED_OBJECT_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_SYSTEM_AUDIT_OBJECT_ACE, AceType=0x07) + + +# [MS-DTYP] sect 2.4.4.12 + + +class WINNT_SYSTEM_AUDIT_CALLBACK_ACE(Packet): + fields_desc = WINNT_SYSTEM_AUDIT_ACE.fields_desc + [ + PacketField( + "ApplicationData", WINNT_APPLICATION_DATA(), WINNT_APPLICATION_DATA + ), + ] + + +bind_layers(WINNT_ACE_HEADER, WINNT_SYSTEM_AUDIT_CALLBACK_ACE, AceType=0x0D) + + +# [MS-DTYP] sect 2.4.4.13 + + +class WINNT_SYSTEM_MANDATORY_LABEL_ACE(Packet): + fields_desc = WINNT_SYSTEM_AUDIT_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_SYSTEM_MANDATORY_LABEL_ACE, AceType=0x11) + + +# [MS-DTYP] sect 2.4.4.14 + + +class WINNT_SYSTEM_AUDIT_CALLBACK_OBJECT_ACE(Packet): + fields_desc = WINNT_SYSTEM_AUDIT_OBJECT_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_SYSTEM_AUDIT_CALLBACK_OBJECT_ACE, AceType=0x0F) + +# [MS-DTYP] sect 2.4.10.1 + + +class CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1(_NTLMPayloadPacket): + _NTLM_PAYLOAD_FIELD_NAME = "Data" + fields_desc = [ + LEIntField("NameOffset", 0), + LEShortEnumField( + "ValueType", + 0, + { + 0x0001: "CLAIM_SECURITY_ATTRIBUTE_TYPE_INT64", + 0x0002: "CLAIM_SECURITY_ATTRIBUTE_TYPE_UINT64", + 0x0003: "CLAIM_SECURITY_ATTRIBUTE_TYPE_STRING", + 0x0005: "CLAIM_SECURITY_ATTRIBUTE_TYPE_SID", + 0x0006: "CLAIM_SECURITY_ATTRIBUTE_TYPE_BOOLEAN", + 0x0010: "CLAIM_SECURITY_ATTRIBUTE_TYPE_OCTET_STRING", + }, + ), + LEShortField("Reserved", 0), + FlagsField( + "Flags", + 0, + -32, + { + 0x0001: "CLAIM_SECURITY_ATTRIBUTE_NON_INHERITABLE", + 0x0002: "CLAIM_SECURITY_ATTRIBUTE_VALUE_CASE_SENSITIVE", + 0x0004: "CLAIM_SECURITY_ATTRIBUTE_USE_FOR_DENY_ONLY", + 0x0008: "CLAIM_SECURITY_ATTRIBUTE_DISABLED_BY_DEFAULT", + 0x0010: "CLAIM_SECURITY_ATTRIBUTE_DISABLED", + 0x0020: "CLAIM_SECURITY_ATTRIBUTE_MANDATORY", + }, + ), + LEIntField("ValueCount", 0), + FieldListField( + "ValueOffsets", [], LEIntField("", 0), count_from=lambda pkt: pkt.ValueCount + ), + _NTLMPayloadField( + "Data", + lambda pkt: 16 + pkt.ValueCount * 4, + [ + ConditionalField( + StrFieldUtf16("Name", b""), + lambda pkt: pkt.NameOffset, + ), + # TODO: Values + ], + offset_name="Offset", + ), + ] + + +# [MS-DTYP] sect 2.4.4.15 + + +class WINNT_SYSTEM_RESOURCE_ATTRIBUTE_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_ACE.fields_desc + [ + PacketField( + "AttributeData", + CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1(), + CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1, + ) + ] + + +bind_layers(WINNT_ACE_HEADER, WINNT_SYSTEM_RESOURCE_ATTRIBUTE_ACE, AceType=0x12) + +# [MS-DTYP] sect 2.4.4.16 + + +class WINNT_SYSTEM_SCOPED_POLICY_ID_ACE(Packet): + fields_desc = WINNT_ACCESS_ALLOWED_ACE.fields_desc + + +bind_layers(WINNT_ACE_HEADER, WINNT_SYSTEM_SCOPED_POLICY_ID_ACE, AceType=0x13) + +# [MS-DTYP] sect 2.4.5 + + +class WINNT_ACL(Packet): + fields_desc = [ + ByteField("AclRevision", 2), + ByteField("Sbz1", 0x00), + # Total size including header: + # AclRevision(1) + Sbz1(1) + AclSize(2) + AceCount(2) + Sbz2(2) + FieldLenField( + "AclSize", + None, + length_of="Aces", + adjust=lambda _, x: x + 8, + fmt=" bytes + return ( + _NTLM_post_build( + self, + pkt, + self.OFFSET, + { + "OwnerSid": 4, + "GroupSid": 8, + "SACL": 12, + "DACL": 16, + }, + config=[ + ("Offset", _NTLM_ENUM.OFFSET), + ], + ) + + pay + ) + + def show_print(self): + """ + Print the SECURITY_DESCRIPTOR in a human format + """ + print("Owner:", self.OwnerSid.summary()) + print("Group:", self.GroupSid.summary()) + if getattr(self, "DACL", None): + print("DACL:") + for ace in self.DACL.Aces: + print(" - ", ace.toSDDL()) + if getattr(self, "SACL", None): + print("SACL:") + for ace in self.SACL.Aces: + print(" - ", ace.toSDDL()) diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index 46a741863e1..6611e9bc590 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -1,32 +1,67 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Enhanced by Maxence Tury -# This program is published under a GPLv2 license +# Acknowledgment: Arnaud Ebalard & Maxence Tury + +# Cool history about this file: http://natisbad.org/scapy/index.html """ -X.509 certificates. +X.509 certificates, OCSP, CRL, CMS and other crypto-related ASN.1 structures """ +from scapy.asn1.ber import BER_Decoding_Error from scapy.asn1.mib import conf # loads conf.mib -from scapy.asn1.asn1 import ASN1_Codecs, ASN1_OID, \ - ASN1_IA5_STRING, ASN1_NULL, ASN1_PRINTABLE_STRING, \ - ASN1_UTC_TIME, ASN1_UTF8_STRING -from scapy.asn1.ber import BER_tagging_dec, BER_Decoding_Error +from scapy.asn1.asn1 import ( + ASN1_Codecs, + ASN1_IA5_STRING, + ASN1_OID, + ASN1_PRINTABLE_STRING, + ASN1_UTC_TIME, + ASN1_UTF8_STRING, +) from scapy.asn1packet import ASN1_Packet -from scapy.asn1fields import ASN1F_BIT_STRING, ASN1F_BIT_STRING_ENCAPS, \ - ASN1F_BMP_STRING, ASN1F_BOOLEAN, ASN1F_CHOICE, ASN1F_ENUMERATED, \ - ASN1F_FLAGS, ASN1F_GENERALIZED_TIME, ASN1F_IA5_STRING, ASN1F_INTEGER, \ - ASN1F_ISO646_STRING, ASN1F_NULL, ASN1F_OID, ASN1F_PACKET, \ - ASN1F_PRINTABLE_STRING, ASN1F_SEQUENCE, ASN1F_SEQUENCE_OF, ASN1F_SET_OF, \ - ASN1F_STRING, ASN1F_T61_STRING, ASN1F_UNIVERSAL_STRING, ASN1F_UTC_TIME, \ - ASN1F_UTF8_STRING, ASN1F_badsequence, ASN1F_enum_INTEGER, ASN1F_field, \ - ASN1F_optional +from scapy.asn1fields import ( + ASN1F_BIT_STRING_ENCAPS, + ASN1F_BIT_STRING, + ASN1F_BMP_STRING, + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_enum_INTEGER, + ASN1F_ENUMERATED, + ASN1F_field, + ASN1F_FLAGS, + ASN1F_GENERALIZED_TIME, + ASN1F_IA5_STRING, + ASN1F_INTEGER, + ASN1F_ISO646_STRING, + ASN1F_NULL, + ASN1F_OID, + ASN1F_omit, + ASN1F_optional, + ASN1F_PACKET, + ASN1F_PRINTABLE_STRING, + ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_SET_OF, + ASN1F_STRING_ENCAPS, + ASN1F_STRING_PacketField, + ASN1F_STRING, + ASN1F_T61_STRING, + ASN1F_UNIVERSAL_STRING, + ASN1F_UTC_TIME, + ASN1F_UTF8_STRING, +) from scapy.packet import Packet -from scapy.fields import PacketField +from scapy.fields import ( + MultipleTypeField, + PacketField, +) from scapy.volatile import ZuluTime, GeneralizedTime from scapy.compat import plain_str +from scapy.layers.tpm import KeyAttestationStatement + class ASN1P_OID(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER @@ -89,6 +124,38 @@ class RSAPrivateKey(ASN1_Packet): ASN1F_SEQUENCE_OF("otherPrimeInfos", None, RSAOtherPrimeInfo))) +#################################### +# Diffie Hellman Packets # +#################################### +# From X9.42 (or RFC3279) + + +class ValidationParms(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING("seed", ""), + ASN1F_INTEGER("pgenCounter", 0), + ) + + +class DomainParameters(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("p", 0), + ASN1F_INTEGER("g", 0), + ASN1F_INTEGER("q", 0), + ASN1F_optional(ASN1F_INTEGER("j", 0)), + ASN1F_optional( + ASN1F_PACKET("validationParms", None, ValidationParms), + ), + ) + + +class DHPublicKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("y", 0) + + #################################### # ECDSA packets # #################################### @@ -158,6 +225,53 @@ class ECDSASignature(ASN1_Packet): ASN1F_INTEGER("s", 0)) +#################################### +# Diffie Hellman Exchange Packets # +#################################### +# based on PKCS#3 + +# PKCS#3 sect 9 + +class DHParameter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("p", 0), + ASN1F_INTEGER("g", 0), + ASN1F_optional( + ASN1F_INTEGER("l", 0) # aka. 'privateValueLength' + ), + ) + + +#################################### +# x25519/x448 packets # +#################################### +# based on RFC 8410 + +class EdDSAPublicKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BIT_STRING("ecPoint", "") + + +class AlgorithmIdentifier(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("algorithm", None), + ) + + +class EdDSAPrivateKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("version", 1, {1: "ecPrivkeyVer1"}), + ASN1F_PACKET("privateKeyAlgorithm", AlgorithmIdentifier(), AlgorithmIdentifier), + ASN1F_STRING("privateKey", ""), + ASN1F_optional( + ASN1F_PACKET("publicKey", None, + ECDSAPublicKey, + explicit_tag=0xa1))) + + ###################### # X509 packets # ###################### @@ -167,21 +281,47 @@ class ECDSASignature(ASN1_Packet): # Names # class ASN1F_X509_DirectoryString(ASN1F_CHOICE): - # we include ASN1 bit strings for rare instances of x500 addresses + # we include ASN1 bit strings and bmp strings for rare instances of x500 addresses def __init__(self, name, default, **kwargs): ASN1F_CHOICE.__init__(self, name, default, ASN1F_PRINTABLE_STRING, ASN1F_UTF8_STRING, ASN1F_IA5_STRING, ASN1F_T61_STRING, ASN1F_UNIVERSAL_STRING, ASN1F_BIT_STRING, + ASN1F_BMP_STRING, **kwargs) +# More details on attributes in PKCS#9 +_X509_ATTRIBUTE_TYPE = {} + + +class _AttributeValue_Field(ASN1F_field): + def m2i(self, pkt, s): + # Some types have special structures + if pkt.underlayer: + attrType = pkt.underlayer.type.val + if attrType in _X509_ATTRIBUTE_TYPE: + return self.extract_packet( + _X509_ATTRIBUTE_TYPE[attrType], + s, + _underlayer=pkt, + ) + try: + return super(_AttributeValue_Field, self).m2i(pkt, s) + except BER_Decoding_Error: + # Do not fail on special attributes + return s, b"" + + def i2m(self, pkt, x): + # The special structures should be just bytes() + if pkt.underlayer and pkt.underlayer.type.val in _X509_ATTRIBUTE_TYPE: + return bytes(x) + return super(_AttributeValue_Field, self).i2m(pkt, x) + + class X509_AttributeValue(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE("value", ASN1_PRINTABLE_STRING("FR"), - ASN1F_PRINTABLE_STRING, ASN1F_UTF8_STRING, - ASN1F_IA5_STRING, ASN1F_T61_STRING, - ASN1F_UNIVERSAL_STRING) + ASN1_root = _AttributeValue_Field("value", ASN1_PRINTABLE_STRING("FR")) class X509_Attribute(ASN1_Packet): @@ -214,9 +354,18 @@ class X509_OtherName(ASN1_Packet): ASN1F_CHOICE("value", None, ASN1F_IA5_STRING, ASN1F_ISO646_STRING, ASN1F_BMP_STRING, ASN1F_UTF8_STRING, + ASN1F_STRING, explicit_tag=0xa0)) +class ASN1F_X509_otherName(ASN1F_SEQUENCE): + # field version of X509_OtherName, for usage in [MS-WCCE] + def __init__(self, **kargs): + seq = [ASN1F_SEQUENCE(*X509_OtherName.ASN1_root.seq, + implicit_tag=0xA0)] + ASN1F_SEQUENCE.__init__(self, *seq, **kargs) + + class X509_RFC822Name(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_IA5_STRING("rfc822Name", "") @@ -238,11 +387,11 @@ class X509_X400Address(ASN1_Packet): X509_RDN(), X509_RDN( rdn=[X509_AttributeTypeAndValue( - type="2.5.4.10", + type=ASN1_OID("2.5.4.10"), value=ASN1_PRINTABLE_STRING("Scapy, Inc."))]), X509_RDN( rdn=[X509_AttributeTypeAndValue( - type="2.5.4.3", + type=ASN1_OID("2.5.4.3"), value=ASN1_PRINTABLE_STRING("Scapy Default Name"))]) ] @@ -660,9 +809,31 @@ class X509_ExtComment(ASN1_Packet): ASN1F_BMP_STRING, ASN1F_UTF8_STRING) -class X509_ExtDefault(ASN1_Packet): +class X509_ExtCertificateTemplateName(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BMP_STRING("Name", b"") + + +class X509_ExtOidNTDSCaSecurity(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_X509_otherName() + type_id = ASN1_OID("1.3.6.1.4.1.311.25.2.1") + value = ASN1_UTF8_STRING("") + + +# [MS-WCCE] sect 2.2.2.7.7.2 + +class X509_ExtCertificateTemplateOID(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_field("value", None) + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("templateID", "0"), + ASN1F_optional( + ASN1F_INTEGER("templateMajorVersion", 0), + ), + ASN1F_optional( + ASN1F_INTEGER("templateMinorVersion", 0), + ), + ) # oid-info.com shows that some extensions share multiple OIDs. @@ -692,51 +863,37 @@ class X509_ExtDefault(ASN1_Packet): "2.5.29.54": X509_ExtInhibitAnyPolicy, "2.16.840.1.113730.1.1": X509_ExtNetscapeCertType, "2.16.840.1.113730.1.13": X509_ExtComment, + "1.3.6.1.4.1.311.20.2": X509_ExtCertificateTemplateName, + "1.3.6.1.4.1.311.21.7": X509_ExtCertificateTemplateOID, + "1.3.6.1.4.1.311.21.10": X509_ExtCertificatePolicies, + "1.3.6.1.4.1.311.25.2": X509_ExtOidNTDSCaSecurity, "1.3.6.1.5.5.7.1.1": X509_ExtAuthInfoAccess, "1.3.6.1.5.5.7.1.3": X509_ExtQcStatements, "1.3.6.1.5.5.7.1.11": X509_ExtSubjInfoAccess } +class _X509_ExtField(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_X509_ExtField, self).m2i(pkt, s) + if not val[0].val: + return val + if pkt.extnID.val in _ext_mapping: + return ( + _ext_mapping[pkt.extnID.val](val[0].val, _underlayer=pkt), + val[1], + ) + return val + + class ASN1F_EXT_SEQUENCE(ASN1F_SEQUENCE): - # We use explicit_tag=0x04 with extnValue as STRING encapsulation. def __init__(self, **kargs): seq = [ASN1F_OID("extnID", "2.5.29.19"), ASN1F_optional( ASN1F_BOOLEAN("critical", False)), - ASN1F_PACKET("extnValue", - X509_ExtBasicConstraints(), - X509_ExtBasicConstraints, - explicit_tag=0x04)] + _X509_ExtField("extnValue", X509_ExtBasicConstraints())] ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - def dissect(self, pkt, s): - _, s = BER_tagging_dec(s, implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag) - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - i, s, remain = codec.check_type_check_len(s) - extnID = self.seq[0] - critical = self.seq[1] - try: - oid, s = extnID.m2i(pkt, s) - extnID.set_val(pkt, oid) - s = critical.dissect(pkt, s) - encapsed = X509_ExtDefault - if oid.val in _ext_mapping: - encapsed = _ext_mapping[oid.val] - self.seq[2].cls = encapsed - self.seq[2].cls.ASN1_root.flexible_tag = True - # there are too many private extensions not to be flexible here - self.seq[2].default = encapsed() - s = self.seq[2].dissect(pkt, s) - if not self.flexible_tag and len(s) > 0: - err_msg = "extension sequence length issue" - raise BER_Decoding_Error(err_msg, remaining=s) - except ASN1F_badsequence: - raise Exception("could not parse extensions") - return remain - class X509_Extension(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER @@ -751,36 +908,86 @@ class X509_Extensions(ASN1_Packet): None, X509_Extension)) +# Aka 'ExtensionReq' in CMS +_X509_ATTRIBUTE_TYPE["1.2.840.113549.1.9.14"] = X509_Extensions + + # Public key wrapper # class X509_AlgorithmIdentifier(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("algorithm", "1.2.840.113549.1.1.11"), - ASN1F_optional( - ASN1F_CHOICE("parameters", ASN1_NULL(0), - ASN1F_NULL, ECParameters))) - - -class ASN1F_X509_SubjectPublicKeyInfoRSA(ASN1F_SEQUENCE): - def __init__(self, **kargs): - seq = [ASN1F_PACKET("signatureAlgorithm", - X509_AlgorithmIdentifier(), - X509_AlgorithmIdentifier), - ASN1F_BIT_STRING_ENCAPS("subjectPublicKey", - RSAPublicKey(), - RSAPublicKey)] - ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - - -class ASN1F_X509_SubjectPublicKeyInfoECDSA(ASN1F_SEQUENCE): - def __init__(self, **kargs): - seq = [ASN1F_PACKET("signatureAlgorithm", - X509_AlgorithmIdentifier(), - X509_AlgorithmIdentifier), - ASN1F_PACKET("subjectPublicKey", ECDSAPublicKey(), - ECDSAPublicKey)] - ASN1F_SEQUENCE.__init__(self, *seq, **kargs) + MultipleTypeField( + [ + ( + # RFC4055: + # "The correct encoding is to omit the parameters field" + # "All implementations MUST accept both NULL and absent + # parameters as legal and equivalent encodings." + + # RFC8017: + # "should generally be omitted, but if present, it shall have a + # value of type NULL." + ASN1F_optional(ASN1F_NULL("parameters", None)), + lambda pkt: ( + pkt.algorithm.val[:19] == "1.2.840.113549.1.1." or + pkt.algorithm.val[:21] == "2.16.840.1.101.3.4.2." or + pkt.algorithm.val[:11] == "1.3.14.3.2." + ) + ), + ( + # RFC5758: + # "the encoding MUST omit the parameters field" + + # RFC8410: + # "For all of the OIDs, the parameters MUST be absent." + ASN1F_omit("parameters", None), + lambda pkt: ( + pkt.algorithm.val[:16] == "1.2.840.10045.4." or + pkt.algorithm.val in ["1.3.101.112", "1.3.101.113"] + ) + ), + # RFC5480 + ( + ASN1F_PACKET( + "parameters", + ECParameters(), + ECParameters, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.10045.2.1", + ), + # RFC3279 + ( + ASN1F_PACKET( + "parameters", + DomainParameters(), + DomainParameters, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.10046.2.1", + ), + # PKCS#3 + ( + ASN1F_PACKET( + "parameters", + DHParameter(), + DHParameter, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.113549.1.3.1", + ), + # TripleDES + ( + ASN1F_STRING( + "parameters", + "", + ), + lambda pkt: pkt.algorithm.val == "1.2.840.113549.3.7", + ), + ], + # Default: fail, probably. This is most likely unimplemented. + ASN1F_NULL("parameters", 0), + ) + ) class ASN1F_X509_SubjectPublicKeyInfo(ASN1F_SEQUENCE): @@ -788,37 +995,28 @@ def __init__(self, **kargs): seq = [ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), X509_AlgorithmIdentifier), - ASN1F_BIT_STRING("subjectPublicKey", None)] + MultipleTypeField( + [ + (ASN1F_BIT_STRING_ENCAPS("subjectPublicKey", + RSAPublicKey(), + RSAPublicKey), + lambda pkt: "rsa" in pkt.signatureAlgorithm.algorithm.oidname.lower()), # noqa: E501 + (ASN1F_PACKET("subjectPublicKey", + ECDSAPublicKey(), + ECDSAPublicKey), + lambda pkt: "ecPublicKey" == pkt.signatureAlgorithm.algorithm.oidname), # noqa: E501 + (ASN1F_BIT_STRING_ENCAPS("subjectPublicKey", + DHPublicKey(), + DHPublicKey), + lambda pkt: "dhpublicnumber" == pkt.signatureAlgorithm.algorithm.oidname), # noqa: E501 + (ASN1F_PACKET("subjectPublicKey", + EdDSAPublicKey(), + EdDSAPublicKey), + lambda pkt: pkt.signatureAlgorithm.algorithm.oidname in ["Ed25519", "Ed448"]), # noqa: E501 + ], + ASN1F_BIT_STRING("subjectPublicKey", ""))] ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - def m2i(self, pkt, x): - c, s = ASN1F_SEQUENCE.m2i(self, pkt, x) - keytype = pkt.fields["signatureAlgorithm"].algorithm.oidname - if "rsa" in keytype.lower(): - return ASN1F_X509_SubjectPublicKeyInfoRSA().m2i(pkt, x) - elif keytype == "ecPublicKey": - return ASN1F_X509_SubjectPublicKeyInfoECDSA().m2i(pkt, x) - else: - raise Exception("could not parse subjectPublicKeyInfo") - - def dissect(self, pkt, s): - c, x = self.m2i(pkt, s) - return x - - def build(self, pkt): - if "signatureAlgorithm" in pkt.fields: - ktype = pkt.fields['signatureAlgorithm'].algorithm.oidname - else: - ktype = pkt.default_fields["signatureAlgorithm"].algorithm.oidname - if "rsa" in ktype.lower(): - pkt.default_fields["subjectPublicKey"] = RSAPublicKey() - return ASN1F_X509_SubjectPublicKeyInfoRSA().build(pkt) - elif ktype == "ecPublicKey": - pkt.default_fields["subjectPublicKey"] = ECDSAPublicKey() - return ASN1F_X509_SubjectPublicKeyInfoECDSA().build(pkt) - else: - raise Exception("could not build subjectPublicKeyInfo") - class X509_SubjectPublicKeyInfo(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER @@ -861,7 +1059,7 @@ def getfield(self, pkt, s): remain = "" if conf.raw_layer in i: r = i[conf.raw_layer] - del(r.underlayer.payload) + del r.underlayer.payload remain = r.load return remain, i @@ -882,11 +1080,11 @@ class ECDSAPrivateKey_OpenSSL(Packet): X509_RDN(), X509_RDN( rdn=[X509_AttributeTypeAndValue( - type="2.5.4.10", + type=ASN1_OID("2.5.4.10"), value=ASN1_PRINTABLE_STRING("Scapy, Inc."))]), X509_RDN( rdn=[X509_AttributeTypeAndValue( - type="2.5.4.3", + type=ASN1_OID("2.5.4.3"), value=ASN1_PRINTABLE_STRING("Scapy Default Issuer"))]) ] @@ -894,15 +1092,42 @@ class ECDSAPrivateKey_OpenSSL(Packet): X509_RDN(), X509_RDN( rdn=[X509_AttributeTypeAndValue( - type="2.5.4.10", + type=ASN1_OID("2.5.4.10"), value=ASN1_PRINTABLE_STRING("Scapy, Inc."))]), X509_RDN( rdn=[X509_AttributeTypeAndValue( - type="2.5.4.3", + type=ASN1_OID("2.5.4.3"), value=ASN1_PRINTABLE_STRING("Scapy Default Subject"))]) ] +class _IssuerUtils: + def get_issuer(self): + attrs = self.issuer + attrsDict = {} + for attr in attrs: + # we assume there is only one name in each rdn ASN1_SET + attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 + return attrsDict + + def get_issuer_str(self): + """ + Returns a one-line string containing every type/value + in a rather specific order. sorted() built-in ensures unicity. + """ + name_str = "" + attrsDict = self.get_issuer() + for attrType, attrSymbol in _attrName_mapping: + if attrType in attrsDict: + name_str += "/" + attrSymbol + "=" + name_str += attrsDict[attrType] + for attrType in sorted(attrsDict): + if attrType not in _attrName_specials: + name_str += "/" + attrType + "=" + name_str += attrsDict[attrType] + return name_str + + class X509_Validity(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( @@ -925,7 +1150,7 @@ class X509_Validity(ASN1_Packet): _attrName_specials = [name for name, symbol in _attrName_mapping] -class X509_TBSCertificate(ASN1_Packet): +class X509_TBSCertificate(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( @@ -955,31 +1180,6 @@ class X509_TBSCertificate(ASN1_Packet): X509_Extension, explicit_tag=0xa3))) - def get_issuer(self): - attrs = self.issuer - attrsDict = {} - for attr in attrs: - # we assume there is only one name in each rdn ASN1_SET - attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 - return attrsDict - - def get_issuer_str(self): - """ - Returns a one-line string containing every type/value - in a rather specific order. sorted() built-in ensures unicity. - """ - name_str = "" - attrsDict = self.get_issuer() - for attrType, attrSymbol in _attrName_mapping: - if attrType in attrsDict: - name_str += "/" + attrSymbol + "=" - name_str += attrsDict[attrType] - for attrType in sorted(attrsDict): - if attrType not in _attrName_specials: - name_str += "/" + attrType + "=" - name_str += attrsDict[attrType] - return name_str - def get_subject(self): attrs = self.subject attrsDict = {} @@ -1002,20 +1202,6 @@ def get_subject_str(self): return name_str -class ASN1F_X509_CertECDSA(ASN1F_SEQUENCE): - def __init__(self, **kargs): - seq = [ASN1F_PACKET("tbsCertificate", - X509_TBSCertificate(), - X509_TBSCertificate), - ASN1F_PACKET("signatureAlgorithm", - X509_AlgorithmIdentifier(), - X509_AlgorithmIdentifier), - ASN1F_BIT_STRING_ENCAPS("signatureValue", - ECDSASignature(), - ECDSASignature)] - ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - - class ASN1F_X509_Cert(ASN1F_SEQUENCE): def __init__(self, **kargs): seq = [ASN1F_PACKET("tbsCertificate", @@ -1024,37 +1210,17 @@ def __init__(self, **kargs): ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), X509_AlgorithmIdentifier), - ASN1F_BIT_STRING("signatureValue", - "defaultsignature" * 2)] + MultipleTypeField( + [ + (ASN1F_BIT_STRING_ENCAPS("signatureValue", + ECDSASignature(), + ECDSASignature), + lambda pkt: "ecdsa" in pkt.signatureAlgorithm.algorithm.oidname.lower()), # noqa: E501 + ], + ASN1F_BIT_STRING("signatureValue", + "defaultsignature" * 2))] ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - def m2i(self, pkt, x): - c, s = ASN1F_SEQUENCE.m2i(self, pkt, x) - sigtype = pkt.fields["signatureAlgorithm"].algorithm.oidname - if "rsa" in sigtype.lower(): - return c, s - elif "ecdsa" in sigtype.lower(): - return ASN1F_X509_CertECDSA().m2i(pkt, x) - else: - raise Exception("could not parse certificate") - - def dissect(self, pkt, s): - c, x = self.m2i(pkt, s) - return x - - def build(self, pkt): - if "signatureAlgorithm" in pkt.fields: - sigtype = pkt.fields['signatureAlgorithm'].algorithm.oidname - else: - sigtype = pkt.default_fields["signatureAlgorithm"].algorithm.oidname # noqa: E501 - if "rsa" in sigtype.lower(): - return ASN1F_SEQUENCE.build(self, pkt) - elif "ecdsa" in sigtype.lower(): - pkt.default_fields["signatureValue"] = ECDSASignature() - return ASN1F_X509_CertECDSA().build(pkt) - else: - raise Exception("could not build certificate") - class X509_Cert(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER @@ -1073,7 +1239,7 @@ class X509_RevokedCertificate(ASN1_Packet): None, X509_Extension))) -class X509_TBSCertList(ASN1_Packet): +class X509_TBSCertList(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( @@ -1093,45 +1259,6 @@ class X509_TBSCertList(ASN1_Packet): X509_Extension, explicit_tag=0xa0))) - def get_issuer(self): - attrs = self.issuer - attrsDict = {} - for attr in attrs: - # we assume there is only one name in each rdn ASN1_SET - attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 - return attrsDict - - def get_issuer_str(self): - """ - Returns a one-line string containing every type/value - in a rather specific order. sorted() built-in ensures unicity. - """ - name_str = "" - attrsDict = self.get_issuer() - for attrType, attrSymbol in _attrName_mapping: - if attrType in attrsDict: - name_str += "/" + attrSymbol + "=" - name_str += attrsDict[attrType] - for attrType in sorted(attrsDict): - if attrType not in _attrName_specials: - name_str += "/" + attrType + "=" - name_str += attrsDict[attrType] - return name_str - - -class ASN1F_X509_CRLECDSA(ASN1F_SEQUENCE): - def __init__(self, **kargs): - seq = [ASN1F_PACKET("tbsCertList", - X509_TBSCertList(), - X509_TBSCertList), - ASN1F_PACKET("signatureAlgorithm", - X509_AlgorithmIdentifier(), - X509_AlgorithmIdentifier), - ASN1F_BIT_STRING_ENCAPS("signatureValue", - ECDSASignature(), - ECDSASignature)] - ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - class ASN1F_X509_CRL(ASN1F_SEQUENCE): def __init__(self, **kargs): @@ -1141,43 +1268,566 @@ def __init__(self, **kargs): ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), X509_AlgorithmIdentifier), - ASN1F_BIT_STRING("signatureValue", - "defaultsignature" * 2)] + MultipleTypeField( + [ + (ASN1F_BIT_STRING_ENCAPS("signatureValue", + ECDSASignature(), + ECDSASignature), + lambda pkt: "ecdsa" in pkt.signatureAlgorithm.algorithm.oidname.lower()), # noqa: E501 + ], + ASN1F_BIT_STRING("signatureValue", + "defaultsignature" * 2))] ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - def m2i(self, pkt, x): - c, s = ASN1F_SEQUENCE.m2i(self, pkt, x) - sigtype = pkt.fields["signatureAlgorithm"].algorithm.oidname - if "rsa" in sigtype.lower(): - return c, s - elif "ecdsa" in sigtype.lower(): - return ASN1F_X509_CRLECDSA().m2i(pkt, x) - else: - raise Exception("could not parse certificate") - - def dissect(self, pkt, s): - c, x = self.m2i(pkt, s) - return x - - def build(self, pkt): - if "signatureAlgorithm" in pkt.fields: - sigtype = pkt.fields['signatureAlgorithm'].algorithm.oidname - else: - sigtype = pkt.default_fields["signatureAlgorithm"].algorithm.oidname # noqa: E501 - if "rsa" in sigtype.lower(): - return ASN1F_SEQUENCE.build(self, pkt) - elif "ecdsa" in sigtype.lower(): - pkt.default_fields["signatureValue"] = ECDSASignature() - return ASN1F_X509_CRLECDSA().build(pkt) - else: - raise Exception("could not build certificate") - class X509_CRL(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_X509_CRL() +##################### +# CMS packets # +##################### +# based on RFC 3852 + +CMSVersion = ASN1F_INTEGER + +# RFC3852 sect 5.2 + +# Other layers should store the structures that can be encapsulated +# by CMS here, referred by their OIDs. +_CMS_ENCAPSULATED = {} + + +class _EncapsulatedContent_Field(ASN1F_STRING_PacketField): + def m2i(self, pkt, s): + val = super(_EncapsulatedContent_Field, self).m2i(pkt, s) + if not val[0].val: + return val + + # Get encapsulated value from its type + if pkt.eContentType.val in _CMS_ENCAPSULATED: + return ( + _CMS_ENCAPSULATED[pkt.eContentType.val](val[0].val, _underlayer=pkt), + val[1], + ) + + return val + + +class CMS_EncapsulatedContentInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("eContentType", "0"), + ASN1F_optional( + _EncapsulatedContent_Field("eContent", None, + explicit_tag=0xA0), + ), + ) + + +# RFC3852 sect 10.2.1 + +class CMS_RevocationInfoChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "crl", None, + ASN1F_PACKET("crl", X509_CRL(), X509_Cert), + # -- TODO: 1 + ) + + +# RFC3852 sect 10.2.2 + +class CMS_CertificateChoices(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "certificate", None, + ASN1F_PACKET("certificate", X509_Cert(), X509_Cert), + # -- TODO: 0, 1, 2 + ) + + +# RFC3852 sect 10.2.4 + +class CMS_IssuerAndSerialNumber(ASN1_Packet, _IssuerUtils): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("issuer", _default_issuer, X509_RDN), + ASN1F_INTEGER("serialNumber", 0) + ) + + +# RFC3852 sect 10.2.7 + +class CMS_OtherKeyAttribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("keyAttrId", "0"), + ASN1F_field("keyAttr", 0), + ) + + +# RFC3852 sect 5.3 + + +class CMS_SubjectKeyIdentifier(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_STRING("sid", "") + + +class CMS_SignerInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 1), + ASN1F_CHOICE( + "sid", + CMS_IssuerAndSerialNumber(), + ASN1F_PACKET("sid", CMS_IssuerAndSerialNumber(), + CMS_IssuerAndSerialNumber), + ASN1F_PACKET("sid", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier, + implicit_tag=0x80), + ), + ASN1F_PACKET("digestAlgorithm", X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_optional( + ASN1F_SET_OF( + "signedAttrs", + None, + X509_Attribute, + implicit_tag=0xA0, + ) + ), + ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_STRING("signature", ASN1_UTF8_STRING("")), + ASN1F_optional( + ASN1F_SET_OF( + "unsignedAttrs", + None, + X509_Attribute, + implicit_tag=0xA1, + ) + ) + ) + + +# RFC3852 sect 5.4 + +class CMS_SignedAttrsForSignature(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF( + "signedAttrs", + None, + X509_Attribute, + ) + + +# RFC3852 sect 5.1 + +class CMS_SignedData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 1), + ASN1F_SET_OF("digestAlgorithms", [], X509_AlgorithmIdentifier), + ASN1F_PACKET("encapContentInfo", CMS_EncapsulatedContentInfo(), + CMS_EncapsulatedContentInfo), + ASN1F_optional( + ASN1F_SET_OF( + "certificates", + None, + CMS_CertificateChoices, + implicit_tag=0xA0, + ) + ), + ASN1F_optional( + ASN1F_SET_OF( + "crls", + None, + CMS_RevocationInfoChoice, + implicit_tag=0xA1, + ) + ), + ASN1F_SET_OF( + "signerInfos", + [], + CMS_SignerInfo, + ), + ) + + +# RFC3852 sect 6.2.1 + +class CMS_KeyTransRecipientInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 0), + ASN1F_CHOICE( + "rid", + CMS_IssuerAndSerialNumber(), + ASN1F_PACKET("rid", CMS_IssuerAndSerialNumber(), + CMS_IssuerAndSerialNumber), + ASN1F_PACKET("rid", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier, + implicit_tag=0x80), + ), + ASN1F_PACKET("keyEncryptionAlgorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_STRING("encryptedKey", ""), + ) + + +# RFC3852 sect 6.2.2 + +class CMS_OriginatorPublicKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("algorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_BIT_STRING("publicKey", ""), + ) + + +class CMS_OriginatorIdentifierOrKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "originator", + CMS_IssuerAndSerialNumber(), + ASN1F_PACKET("issuerAndSerialNumber", CMS_IssuerAndSerialNumber(), + CMS_IssuerAndSerialNumber), + ASN1F_PACKET("subjectKeyIdentifier", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier, + implicit_tag=0x80), + ASN1F_PACKET("originatorKey", CMS_OriginatorPublicKey(), + CMS_OriginatorPublicKey, + implicit_tag=0xA1), + ) + + +class CMS_RecipientEncryptedKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("subjectKeyIdentifier", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier), + ASN1F_optional( + ASN1F_GENERALIZED_TIME("date", ""), + ), + ASN1F_optional( + ASN1F_PACKET("other", CMS_OtherKeyAttribute(), CMS_OtherKeyAttribute), + ), + ) + + +class CMS_KeyAgreeRecipientInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 3), + ASN1F_PACKET("originator", CMS_OriginatorIdentifierOrKey(), + CMS_OriginatorIdentifierOrKey, + explicit_tag=0xA0), + ASN1F_optional( + ASN1F_STRING("ukm", None, "", + explicit_tag=0x81), + ), + ASN1F_PACKET("keyEncryptionAlgorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_SEQUENCE_OF("recipientEncryptedKeys", [], CMS_RecipientEncryptedKey), + ) + + +# RFC3852 sect 6.2 + +class CMS_RecipientInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "recipientInfo", + CMS_KeyTransRecipientInfo(), + ASN1F_PACKET("ktri", CMS_KeyTransRecipientInfo(), CMS_KeyTransRecipientInfo), + ASN1F_PACKET("kari", CMS_KeyAgreeRecipientInfo(), CMS_KeyAgreeRecipientInfo, + implicit_tag=0xA1), + ) + + +# RFC3852 sect 6.1 + +class CMS_OriginatorInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional( + ASN1F_SET_OF( + "certs", + None, + CMS_CertificateChoices, + implicit_tag=0xA0, + ) + ), + ASN1F_optional( + ASN1F_SET_OF( + "crls", + None, + CMS_RevocationInfoChoice, + implicit_tag=0xA1, + ) + ), + ) + + +class CMS_EncryptedContentInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("contentType", "1.2.840.113549.1.7.2"), + ASN1F_PACKET("contentEncryptionAlgorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_optional( + ASN1F_STRING("encryptedContent", "", + implicit_tag=0x80), + ) + ) + + +class CMS_EnvelopedData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 1), + ASN1F_optional( + ASN1F_PACKET("originatorInfo", None, CMS_OriginatorInfo, + implicit_tag=0xA0), + ), + ASN1F_SET_OF("recipientInfos", CMS_RecipientInfo(), CMS_RecipientInfo), + ASN1F_PACKET("encryptedContentInfo", CMS_EncryptedContentInfo(), + CMS_EncryptedContentInfo), + ASN1F_optional( + ASN1F_SET_OF("unprotectedAttrs", [], X509_Attribute, + implicit_tag=0xA1), + ) + ) + + +# RFC3852 sect 3 + +class CMS_ContentInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("contentType", "1.2.840.113549.1.7.2"), + MultipleTypeField( + [ + ( + ASN1F_PACKET("content", None, CMS_SignedData, + explicit_tag=0xA0), + lambda pkt: pkt.contentType.oidname == "id-signedData" + ), + ( + ASN1F_PACKET("content", None, CMS_EnvelopedData, + explicit_tag=0xA0), + lambda pkt: pkt.contentType.oidname == "id-envelopedData" + ), + ], + ASN1F_BIT_STRING("content", "", explicit_tag=0xA0) + ) + ) + + +##################### +# CSR packets # +##################### + +# based on PKCS#10 # + + +class PKCS10_CertificationRequestInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("version", 0), + ASN1F_SEQUENCE_OF("subject", _default_subject, X509_RDN), + ASN1F_PACKET("subjectPublicKeyInfo", + X509_SubjectPublicKeyInfo(), + X509_SubjectPublicKeyInfo), + ASN1F_SET_OF("attributes", [], X509_Attribute, + implicit_tag=0xA0), + ) + + get_subject = X509_TBSCertificate.get_subject + get_subject_str = X509_TBSCertificate.get_subject_str + + +class PKCS10_CertificationRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("certificationRequestInfo", PKCS10_CertificationRequestInfo(), + PKCS10_CertificationRequestInfo), + ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_BIT_STRING("signature", ASN1F_BIT_STRING("", "")), + ) + + +# based on CMC # + +# RFC 5272 sect 3.2.1.1 + +class CMC_TaggedAttribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_OID("type", "0"), # attrType for compat + ASN1F_SET_OF("attrValues", [], X509_AttributeValue), + ) + + +# RFC 5272 sect 3.2.1.2.1 + +class CMC_TaggedCertificationRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_PACKET("certificationRequest", PKCS10_CertificationRequest(), + PKCS10_CertificationRequest) + ) + + +# RFC 5272 sect 3.2.1.2 + +class CMC_TaggedRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "request", CMC_TaggedCertificationRequest(), + ASN1F_PACKET("tcr", CMC_TaggedCertificationRequest(), + CMC_TaggedCertificationRequest, + implicit_tag=0xA0), + # XXX there are others + ) + + +# RFC 5272 sect 3.2.1.3 + +class CMC_TaggedContentInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_PACKET("contentInfo", CMS_ContentInfo(), + CMS_ContentInfo) + ) + + +# RFC 5272 sect 3.2.1.4 + +class CMC_OtherMsg(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_OID("otherMsgType", "0"), + ASN1F_field("otherMsgValue", ""), + ) + + +# RFC 5272 sect 3.2.1 + +class CMC_PKIData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("controlSequence", [], CMC_TaggedAttribute), + ASN1F_SEQUENCE_OF("reqSequence", [], CMC_TaggedRequest), + ASN1F_SEQUENCE_OF("cmsSequence", [], CMC_TaggedContentInfo), + ASN1F_SEQUENCE_OF("otherMsgSequence", [], CMC_OtherMsg), + ) + + +_CMS_ENCAPSULATED["1.3.6.1.5.5.7.12.2"] = CMC_PKIData + + +# Windows extensions # + +# https://learn.microsoft.com/en-us/windows/win32/seccertenroll/cmc-extensions + +class CMC_AddExtensions(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("pkiDataReference", 0), + ASN1F_SEQUENCE_OF("certReferences", [], ASN1F_INTEGER), + ASN1F_PACKET("extensions", X509_Extensions(), X509_Extensions), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.5.5.7.7.8"] = CMC_AddExtensions + + +# https://learn.microsoft.com/en-us/windows/win32/seccertenroll/cmc-attributes + +class CMC_AddAttributes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("pkiDataReference", 0), + ASN1F_SEQUENCE_OF("certReferences", [], ASN1F_INTEGER), + ASN1F_SET_OF("attributes", X509_Attribute(), X509_Attribute), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.10.10.1"] = CMC_AddAttributes + + +# [MS-WCCE] sect 2.2.2.7.2 + +class CMC_ENROLLMENT_CSP_PROVIDER(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("KeySpec", 0), + ASN1F_BMP_STRING("ProviderName", ""), + ASN1F_BIT_STRING("Signature", ""), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.13.2.2"] = CMC_ENROLLMENT_CSP_PROVIDER + + +# [MS-WCCE] sect 2.2.2.7.4 + +class CMC_REQUEST_CLIENT_INFO(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("clientId", 0), + ASN1F_UTF8_STRING("MachineName", ""), + ASN1F_UTF8_STRING("UserName", ""), + ASN1F_UTF8_STRING("ProcessName", ""), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.21.20"] = CMC_REQUEST_CLIENT_INFO + + +# [MS-WCCE] sect 2.2.2.7.10 + +class CMC_EnrollmentNameValuePair(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BMP_STRING("Name", ""), + ASN1F_BMP_STRING("Value", ""), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.13.2.1"] = CMC_EnrollmentNameValuePair + + +# [MS-WCCE] sect 2.2.2.7.12 + +class CMC_ENROLL_ATTESTATION_STATEMENT(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_STRING_ENCAPS("kas", KeyAttestationStatement(), + KeyAttestationStatement) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.21.24"] = CMC_ENROLL_ATTESTATION_STATEMENT + + +# [MS-WCCE] sect 2.2.2.7.13 + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.21.23"] = CMS_ContentInfo + + ############################# # OCSP Status packets # ############################# @@ -1206,7 +1856,7 @@ class OCSP_RevokedInfo(ASN1_Packet): ASN1F_optional( ASN1F_PACKET("revocationReason", None, X509_ExtReasonCode, - explicit_tag=0x80))) + explicit_tag=0xa0))) class OCSP_UnknownInfo(ASN1_Packet): @@ -1229,7 +1879,7 @@ class OCSP_SingleResponse(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_PACKET("certID", OCSP_CertID(), OCSP_CertID), - ASN1F_PACKET("certStatus", OCSP_CertStatus(), + ASN1F_PACKET("certStatus", OCSP_CertStatus(certStatus=OCSP_GoodInfo()), OCSP_CertStatus), ASN1F_GENERALIZED_TIME("thisUpdate", ""), ASN1F_optional( @@ -1266,7 +1916,7 @@ class OCSP_ResponseData(ASN1_Packet): ASN1F_optional( ASN1F_enum_INTEGER("version", 0, {0: "v1"}, explicit_tag=0x80)), - ASN1F_PACKET("responderID", OCSP_ResponderID(), + ASN1F_PACKET("responderID", OCSP_ResponderID(responderID=OCSP_ByName()), OCSP_ResponderID), ASN1F_GENERALIZED_TIME("producedAt", str(GeneralizedTime())), @@ -1277,23 +1927,6 @@ class OCSP_ResponseData(ASN1_Packet): explicit_tag=0xa1))) -class ASN1F_OCSP_BasicResponseECDSA(ASN1F_SEQUENCE): - def __init__(self, **kargs): - seq = [ASN1F_PACKET("tbsResponseData", - OCSP_ResponseData(), - OCSP_ResponseData), - ASN1F_PACKET("signatureAlgorithm", - X509_AlgorithmIdentifier(), - X509_AlgorithmIdentifier), - ASN1F_BIT_STRING_ENCAPS("signature", - ECDSASignature(), - ECDSASignature), - ASN1F_optional( - ASN1F_SEQUENCE_OF("certs", None, X509_Cert, - explicit_tag=0xa0))] - ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - - class ASN1F_OCSP_BasicResponse(ASN1F_SEQUENCE): def __init__(self, **kargs): seq = [ASN1F_PACKET("tbsResponseData", @@ -1302,40 +1935,20 @@ def __init__(self, **kargs): ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), X509_AlgorithmIdentifier), - ASN1F_BIT_STRING("signature", - "defaultsignature" * 2), + MultipleTypeField( + [ + (ASN1F_BIT_STRING_ENCAPS("signature", + ECDSASignature(), + ECDSASignature), + lambda pkt: "ecdsa" in pkt.signatureAlgorithm.algorithm.oidname.lower()), # noqa: E501 + ], + ASN1F_BIT_STRING("signature", + "defaultsignature" * 2)), ASN1F_optional( ASN1F_SEQUENCE_OF("certs", None, X509_Cert, explicit_tag=0xa0))] ASN1F_SEQUENCE.__init__(self, *seq, **kargs) - def m2i(self, pkt, x): - c, s = ASN1F_SEQUENCE.m2i(self, pkt, x) - sigtype = pkt.fields["signatureAlgorithm"].algorithm.oidname - if "rsa" in sigtype.lower(): - return c, s - elif "ecdsa" in sigtype.lower(): - return ASN1F_OCSP_BasicResponseECDSA().m2i(pkt, x) - else: - raise Exception("could not parse OCSP basic response") - - def dissect(self, pkt, s): - c, x = self.m2i(pkt, s) - return x - - def build(self, pkt): - if "signatureAlgorithm" in pkt.fields: - sigtype = pkt.fields['signatureAlgorithm'].algorithm.oidname - else: - sigtype = pkt.default_fields["signatureAlgorithm"].algorithm.oidname # noqa: E501 - if "rsa" in sigtype.lower(): - return ASN1F_SEQUENCE.build(self, pkt) - elif "ecdsa" in sigtype.lower(): - pkt.default_fields["signatureValue"] = ECDSASignature() - return ASN1F_OCSP_BasicResponseECDSA().build(pkt) - else: - raise Exception("could not build OCSP basic response") - class OCSP_ResponseBytes(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER diff --git a/scapy/layers/zigbee.py b/scapy/layers/zigbee.py index 4db2a5de6da..1610c10f47f 100644 --- a/scapy/layers/zigbee.py +++ b/scapy/layers/zigbee.py @@ -1,11 +1,10 @@ -# This program is published under a GPLv2 license +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Ryan Speers 2011-2012 # Copyright (C) Roger Meyer : 2012-03-10 Added frames -# Copyright (C) Gabriel Potter : 2018 -# Intern at INRIA Grand Nancy Est -# This program is published under a GPLv2 license +# Copyright (C) Gabriel Potter : 2018 +# Copyright (C) 2020-2021 Dimitrios-Georgios Akestoridis """ ZigBee bindings for IEEE 802.15.4. @@ -25,6 +24,18 @@ from scapy.layers.inet import UDP from scapy.layers.ntp import TimeStampField + +# APS Profile Identifiers +_aps_profile_identifiers = { + 0x0000: "Zigbee_Device_Profile", + 0x0101: "IPM_Industrial_Plant_Monitoring", + 0x0104: "HA_Home_Automation", + 0x0105: "CBA_Commercial_Building_Automation", + 0x0107: "TA_Telecom_Applications", + 0x0108: "HC_Health_Care", + 0x0109: "SE_Smart_Energy_Profile", +} + # ZigBee Cluster Library Identifiers, Table 2.2 ZCL _zcl_cluster_identifier = { # Functional Domain: General @@ -109,22 +120,11 @@ 0x0800: "key_establishment", } -# ZigBee stack profiles -_zcl_profile_identifier = { - 0x0000: "ZigBee_Stack_Profile_1", - 0x0101: "IPM_Industrial_Plant_Monitoring", - 0x0104: "HA_Home_Automation", - 0x0105: "CBA_Commercial_Building_Automation", - 0x0107: "TA_Telecom_Applications", - 0x0108: "HC_Health_Care", - 0x0109: "SE_Smart_Energy_Profile", -} - # ZigBee Cluster Library, Table 2.8 ZCL Command Frames _zcl_command_frames = { 0x00: "read_attributes", 0x01: "read_attributes_response", - 0x02: "write_attributes_response", + 0x02: "write_attributes", 0x03: "write_attributes_undivided", 0x04: "write_attributes_response", 0x05: "write_attributes_no_response", @@ -136,14 +136,25 @@ 0x0b: "default_response", 0x0c: "discover_attributes", 0x0d: "discover_attributes_response", - # 0x0e - 0xff Reserved + 0x0e: "read_attributes_structured", + 0x0f: "write_attributes_structured", + 0x10: "write_attributes_structured_response", + 0x11: "discover_commands_received", + 0x12: "discover_commands_received_response", + 0x13: "discover_commands_generated", + 0x14: "discover_commands_generated_response", + 0x15: "discover_attributes_extended", + 0x16: "discover_attributes_extended_response", + # 0x17 - 0xff Reserved } # ZigBee Cluster Library, Table 2.16 Enumerated Status Values _zcl_enumerated_status_values = { 0x00: "SUCCESS", - 0x02: "FAILURE", - # 0x02 - 0x7f Reserved + 0x01: "FAILURE", + # 0x02 - 0x7d Reserved + 0x7e: "NOT_AUTHORIZED", + 0x7f: "RESERVED_FIELD_NOT_ZERO", 0x80: "MALFORMED_COMMAND", 0x81: "UNSUP_CLUSTER_COMMAND", 0x82: "UNSUP_GENERAL_COMMAND", @@ -158,11 +169,25 @@ 0x8b: "NOT_FOUND", 0x8c: "UNREPORTABLE_ATTRIBUTE", 0x8d: "INVALID_DATA_TYPE", - # 0x8e - 0xbf Reserved + 0x8e: "INVALID_SELECTOR", + 0x8f: "WRITE_ONLY", + 0x90: "INCONSISTENT_STARTUP_STATE", + 0x91: "DEFINED_OUT_OF_BAND", + 0x92: "INCONSISTENT", + 0x93: "ACTION_DENIED", + 0x94: "TIMEOUT", + 0x95: "ABORT", + 0x96: "INVALID_IMAGE", + 0x97: "WAIT_FOR_DATA", + 0x98: "NO_IMAGE_AVAILABLE", + 0x99: "REQUIRE_MORE_IMAGE", + 0x9a: "NOTIFICATION_PENDING", + # 0x9b - 0xbf Reserved 0xc0: "HARDWARE_FAILURE", 0xc1: "SOFTWARE_FAILURE", 0xc2: "CALIBRATION_ERROR", - # 0xc3 - 0xff Reserved + 0xc3: "UNSUPPORTED_CLUSTER", + # 0xc4 - 0xff Reserved } # ZigBee Cluster Library, Table 2.15 Data Types @@ -239,6 +264,34 @@ 0xff: "unknown", } +# Zigbee Cluster Library, IAS Zone, Enroll Response Codes +_zcl_ias_zone_enroll_response_codes = { + 0x00: "Success", + 0x01: "Not supported", + 0x02: "No enroll permit", + 0x03: "Too many zones", +} + +# Zigbee Cluster Library, IAS Zone, Zone Types +_zcl_ias_zone_zone_types = { + 0x0000: "Standard CIE", + 0x000d: "Motion sensor", + 0x0015: "Contact switch", + 0x0028: "Fire sensor", + 0x002a: "Water sensor", + 0x002b: "Carbon Monoxide (CO) sensor", + 0x002c: "Personal emergency device", + 0x002d: "Vibration/Movement sensor", + 0x010f: "Remote Control", + 0x0115: "Key fob", + 0x021d: "Keypad", + 0x0225: "Standard Warning Device", + 0x0226: "Glass break sensor", + 0x0229: "Security repeater", + # 0x8000 - 0xfffe Manufacturer-specific types + 0xffff: "Invalid Zone Type", +} + # ZigBee # @@ -247,7 +300,8 @@ class ZigbeeNWK(Packet): fields_desc = [ BitField("discover_route", 0, 2), BitField("proto_version", 2, 4), - BitEnumField("frametype", 0, 2, {0: 'data', 1: 'command'}), + BitEnumField("frametype", 0, 2, + {0: 'data', 1: 'command', 3: 'Inter-PAN'}), FlagsField("flags", 0, 8, ['multicast', 'security', 'source_route', 'extended_dst', 'extended_src', 'reserved1', 'reserved2', 'reserved3']), # noqa: E501 XLEShortField("destination", 0), XLEShortField("source", 0), @@ -264,8 +318,16 @@ class ZigbeeNWK(Packet): ConditionalField(FieldListField("relays", [], XLEShortField("", 0x0000), count_from=lambda pkt:pkt.relay_count), lambda pkt:pkt.flags & 0x04), # noqa: E501 ] + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 2: + frametype = ord(_pkt[:1]) & 3 + if frametype == 3: + return ZigbeeNWKStub + return cls + def guess_payload_class(self, payload): - if self.flags & 0x02: + if self.flags.security: return ZigbeeSecurityHeader elif self.frametype == 0: return ZigbeeAppDataPayload @@ -277,6 +339,7 @@ def guess_payload_class(self, payload): class LinkStatusEntry(Packet): name = "ZigBee Link Status Entry" + fields_desc = [ # Neighbor network address (2 octets) XLEShortField("neighbor_network_address", 0x0000), @@ -287,6 +350,9 @@ class LinkStatusEntry(Packet): BitField("incoming_cost", 0, 3), ] + def extract_padding(self, p): + return b"", p + class ZigbeeNWKCommandPayload(Packet): name = "Zigbee Network Layer Command Payload" @@ -301,51 +367,37 @@ class ZigbeeNWKCommandPayload(Packet): 7: "rejoin response", 8: "link status", 9: "network report", - 10: "network update" - # 0x0b - 0xff reserved + 10: "network update", + 11: "end device timeout request", + 12: "end device timeout response" + # 0x0d - 0xff reserved }), # - Route Request Command - # # Command options (1 octet) - ConditionalField(BitField("reserved", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 - ConditionalField(BitField("multicast", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 + ConditionalField(BitField("res1", 0, 1), + lambda pkt: pkt.cmd_identifier in [1, 2]), + ConditionalField(BitField("multicast", 0, 1), + lambda pkt: pkt.cmd_identifier in [1, 2]), ConditionalField(BitField("dest_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 ConditionalField( BitEnumField("many_to_one", 0, 2, { 0: "not_m2one", 1: "m2one_support_rrt", 2: "m2one_no_support_rrt", 3: "reserved"} # noqa: E501 ), lambda pkt: pkt.cmd_identifier == 1), - ConditionalField(BitField("reserved", 0, 3), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 - # Route request identifier (1 octet) - ConditionalField(ByteField("route_request_identifier", 0), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 - # Destination address (2 octets) - ConditionalField(XLEShortField("destination_address", 0x0000), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 - # Path cost (1 octet) - ConditionalField(ByteField("path_cost", 0), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 - # Destination IEEE Address (0/8 octets), only present when dest_addr_bit has a value of 1 # noqa: E501 - ConditionalField(dot15d4AddressField("ext_dst", 0, adjust=lambda pkt, x: 8), # noqa: E501 - lambda pkt: (pkt.cmd_identifier == 1 and pkt.dest_addr_bit == 1)), # noqa: E501 + ConditionalField(BitField("res2", 0, 3), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501 # - Route Reply Command - # # Command options (1 octet) - ConditionalField(BitField("reserved", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 - ConditionalField(BitField("multicast", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 ConditionalField(BitField("responder_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 ConditionalField(BitField("originator_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 - ConditionalField(BitField("reserved", 0, 4), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + ConditionalField(BitField("res3", 0, 4), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 # Route request identifier (1 octet) - ConditionalField(ByteField("route_request_identifier", 0), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 + ConditionalField(ByteField("route_request_identifier", 0), + lambda pkt: pkt.cmd_identifier in [1, 2]), # noqa: E501 # Originator address (2 octets) ConditionalField(XLEShortField("originator_address", 0x0000), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 # Responder address (2 octets) ConditionalField(XLEShortField("responder_address", 0x0000), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 - # Path cost (1 octet) - ConditionalField(ByteField("path_cost", 0), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501 - # Originator IEEE address (0/8 octets) - ConditionalField(dot15d4AddressField("originator_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501 - lambda pkt: (pkt.cmd_identifier == 2 and pkt.originator_addr_bit == 1)), # noqa: E501 - # Responder IEEE address (0/8 octets) - ConditionalField(dot15d4AddressField("responder_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501 - lambda pkt: (pkt.cmd_identifier == 2 and pkt.responder_addr_bit == 1)), # noqa: E501 # - Network Status Command - # # Status code (1 octet) @@ -372,7 +424,20 @@ class ZigbeeNWKCommandPayload(Packet): # 0x13 - 0xff Reserved }), lambda pkt: pkt.cmd_identifier == 3), # Destination address (2 octets) - ConditionalField(XLEShortField("destination_address", 0x0000), lambda pkt: pkt.cmd_identifier == 3), # noqa: E501 + ConditionalField(XLEShortField("destination_address", 0x0000), + lambda pkt: pkt.cmd_identifier in [1, 3]), + # Path cost (1 octet) + ConditionalField(ByteField("path_cost", 0), + lambda pkt: pkt.cmd_identifier in [1, 2]), # noqa: E501 + # Destination IEEE Address (0/8 octets), only present when dest_addr_bit has a value of 1 # noqa: E501 + ConditionalField(dot15d4AddressField("ext_dst", 0, adjust=lambda pkt, x: 8), # noqa: E501 + lambda pkt: (pkt.cmd_identifier == 1 and pkt.dest_addr_bit == 1)), # noqa: E501 + # Originator IEEE address (0/8 octets) + ConditionalField(dot15d4AddressField("originator_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501 + lambda pkt: (pkt.cmd_identifier == 2 and pkt.originator_addr_bit == 1)), # noqa: E501 + # Responder IEEE address (0/8 octets) + ConditionalField(dot15d4AddressField("responder_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501 + lambda pkt: (pkt.cmd_identifier == 2 and pkt.responder_addr_bit == 1)), # noqa: E501 # - Leave Command - # # Command options (1 octet) @@ -383,7 +448,7 @@ class ZigbeeNWKCommandPayload(Packet): # Bit 5: Rejoin ConditionalField(BitField("rejoin", 0, 1), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501 # Bit 0 - 4: Reserved - ConditionalField(BitField("reserved", 0, 5), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501 + ConditionalField(BitField("res4", 0, 5), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501 # - Route Record Command - # # Relay count (1 octet) @@ -412,7 +477,7 @@ class ZigbeeNWKCommandPayload(Packet): # - Link Status Command - # # Command options (1 octet) - ConditionalField(BitField("reserved", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # Reserved # noqa: E501 + ConditionalField(BitField("res5", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # Reserved # noqa: E501 ConditionalField(BitField("last_frame", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # Last frame # noqa: E501 ConditionalField(BitField("first_frame", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # First frame # noqa: E501 ConditionalField(BitField("entry_count", 0, 5), lambda pkt:pkt.cmd_identifier == 8), # Entry count # noqa: E501 @@ -427,15 +492,6 @@ class ZigbeeNWKCommandPayload(Packet): BitEnumField("report_command_identifier", 0, 3, {0: "PAN identifier conflict"}), # 0x01 - 0x07 Reserved # noqa: E501 lambda pkt: pkt.cmd_identifier == 9), ConditionalField(BitField("report_information_count", 0, 5), lambda pkt: pkt.cmd_identifier == 9), # noqa: E501 - # EPID: Extended PAN ID (8 octets) - ConditionalField(dot15d4AddressField("epid", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.cmd_identifier == 9), # noqa: E501 - # Report information (variable length) - # Only present if we have a PAN Identifier Conflict Report - ConditionalField( - FieldListField("PAN_ID_conflict_report", [], XLEShortField("", 0x0000), # noqa: E501 - count_from=lambda pkt:pkt.report_information_count), - lambda pkt:(pkt.cmd_identifier == 9 and pkt.report_command_identifier == 0) # noqa: E501 - ), # - Network Update Command - # # Command options (1 octet) @@ -444,7 +500,17 @@ class ZigbeeNWKCommandPayload(Packet): lambda pkt: pkt.cmd_identifier == 10), ConditionalField(BitField("update_information_count", 0, 5), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501 # EPID: Extended PAN ID (8 octets) - ConditionalField(dot15d4AddressField("epid", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501 + ConditionalField( + dot15d4AddressField("epid", 0, adjust=lambda pkt, x: 8), + lambda pkt: pkt.cmd_identifier in [9, 10] + ), + # Report information (variable length) + # Only present if we have a PAN Identifier Conflict Report + ConditionalField( + FieldListField("PAN_ID_conflict_report", [], XLEShortField("", 0x0000), # noqa: E501 + count_from=lambda pkt:pkt.report_information_count), + lambda pkt:(pkt.cmd_identifier == 9 and pkt.report_command_identifier == 0) # noqa: E501 + ), # Update Id (1 octet) ConditionalField(ByteField("update_id", 0), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501 # Update Information (Variable) @@ -453,6 +519,51 @@ class ZigbeeNWKCommandPayload(Packet): ConditionalField(XLEShortField("new_PAN_ID", 0x0000), lambda pkt: (pkt.cmd_identifier == 10 and pkt.update_command_identifier == 0)), # noqa: E501 + # - End Device Timeout Request Command - # + # Requested Timeout (1 octet) + ConditionalField( + ByteEnumField("req_timeout", 3, { + 0: "10 seconds", + 1: "2 minutes", + 2: "4 minutes", + 3: "8 minutes", + 4: "16 minutes", + 5: "32 minutes", + 6: "64 minutes", + 7: "128 minutes", + 8: "256 minutes", + 9: "512 minutes", + 10: "1024 minutes", + 11: "2048 minutes", + 12: "4096 minutes", + 13: "8192 minutes", + 14: "16384 minutes" + }), + lambda pkt: pkt.cmd_identifier == 11), + # End Device Configuration (1 octet) + ConditionalField( + ByteField("ed_conf", 0), + lambda pkt: pkt.cmd_identifier == 11), + + # - End Device Timeout Response Command - # + # Status (1 octet) + ConditionalField( + ByteEnumField("status", 0, { + 0: "Success", + 1: "Incorrect Value" + }), + lambda pkt: pkt.cmd_identifier == 12), + # Parent Information (1 octet) + ConditionalField( + BitField("res6", 0, 6), + lambda pkt: pkt.cmd_identifier == 12), + ConditionalField( + BitField("ed_timeout_req_keepalive", 0, 1), + lambda pkt: pkt.cmd_identifier == 12), + ConditionalField( + BitField("mac_data_poll_keepalive", 0, 1), + lambda pkt: pkt.cmd_identifier == 12) + # StrField("data", ""), ] @@ -530,7 +641,7 @@ class ZigbeeAppDataPayload(Packet): fields_desc = [ # Frame control (1 octet) FlagsField("frame_control", 2, 4, - ['reserved1', 'security', 'ack_req', 'extended_hdr']), + ['ack_format', 'security', 'ack_req', 'extended_hdr']), BitEnumField("delivery_mode", 0, 2, {0: 'unicast', 1: 'indirect', 2: 'broadcast', 3: 'group_addressing'}), @@ -539,24 +650,37 @@ class ZigbeeAppDataPayload(Packet): # Destination endpoint (0/1 octet) ConditionalField( ByteField("dst_endpoint", 10), - lambda pkt: (pkt.frame_control.ack_req or pkt.aps_frametype == 2) + lambda pkt: ((pkt.aps_frametype == 0 and + pkt.delivery_mode in [0, 2]) or + (pkt.aps_frametype == 2 and not + pkt.frame_control.ack_format)) + ), + # Group address (0/2 octets) + ConditionalField( + XLEShortField("group_addr", 0x0000), + lambda pkt: (pkt.aps_frametype == 0 and pkt.delivery_mode == 3) ), - # Group address (0/2 octets) TODO # Cluster identifier (0/2 octets) ConditionalField( # unsigned short (little-endian) - EnumField("cluster", 0, _zcl_cluster_identifier, fmt=" 9)) + # Un-implemented: 10-13 (+?) + ConditionalField(StrField("unimplemented", ""), + lambda pkt: (pkt.cmd_identifier >= 10 and + pkt.cmd_identifier <= 13)), + # Tunnel Command + ConditionalField( + FlagsField("frame_control", 2, 4, [ + "ack_format", + "security", + "ack_req", + "extended_hdr" + ]), + lambda pkt: pkt.cmd_identifier == 14), + ConditionalField( + BitEnumField("delivery_mode", 0, 2, { + 0: "unicast", + 1: "indirect", + 2: "broadcast", + 3: "group_addressing" + }), + lambda pkt: pkt.cmd_identifier == 14), + ConditionalField( + BitEnumField("aps_frametype", 1, 2, { + 0: "data", + 1: "command", + 2: "ack" + }), + lambda pkt: pkt.cmd_identifier == 14), + ConditionalField( + ByteField("counter", 0), + lambda pkt: pkt.cmd_identifier == 14), + # Verify-Key Command + ConditionalField( + StrFixedLenField("key_hash", None, 16), + lambda pkt: pkt.cmd_identifier == 15), ] + def guess_payload_class(self, payload): + if self.cmd_identifier == 14: + # Tunneled APS Auxiliary Header + return ZigbeeSecurityHeader + else: + return Packet.guess_payload_class(self, payload) + class ZigBeeBeacon(Packet): name = "ZigBee Beacon Payload" @@ -698,10 +913,10 @@ class ZigbeeNWKStub(Packet): name = "Zigbee Network Layer for Inter-PAN Transmission" fields_desc = [ # NWK frame control - BitField("reserved", 0, 2), # remaining subfields shall have a value of 0 # noqa: E501 + BitField("res1", 0, 2), # remaining subfields shall have a value of 0 # noqa: E501 BitField("proto_version", 2, 4), BitField("frametype", 0b11, 2), # 0b11 (3) is a reserved frame type - BitField("reserved", 0, 8), # remaining subfields shall have a value of 0 # noqa: E501 + BitField("res2", 0, 8), # remaining subfields shall have a value of 0 # noqa: E501 ] def guess_payload_class(self, payload): @@ -723,9 +938,9 @@ class ZigbeeAppDataPayloadStub(Packet): lambda pkt: pkt.getfieldval("delivery_mode") == 0b11 ), # Cluster identifier - EnumField("cluster", 0, _zcl_cluster_identifier, fmt=" str: + return { + int(x): y + for x, y in json.loads(gzip.decompress( + b85decode(x.replace("\n", "")) + ).decode()).items() + } + + +DATA = _d(""" +ABzY8*hqM20{@J?TX)+y(k}d0xaMMZvfkLQj+V3UD2bLB9jJ(s9cRrAEx{IA6shLHvGRRCepjIiBp}) +8Su@?~@FcPT6zZwNQ~%$;+PAlzfBj$QU-vSXY2u8sv^+X~vbp}(7Y9$a@uU~a!b@IcB19&W7iT&h@aY +zwo_N!#w{(VCx!E5?o)==XOXS{hM|@QiuUci|cbYka^l*%llapU(*Qx%M243JEj?SGd96!$@5j)e>kk +0nL;@MH0K1Fa;CVOWn^CFW^Wr43eNV6k9r+1524n$I9=|N<|8K?0UU$}pLuP^E0C86Bx$_Vb=Maj!9g +)8PFO+?|W@YT~eeUT!ECtrV=7F&JijS@wapPZS9@)9187dXZhUA*G4{2Lz~ii6yw$+p}S@YSDw$mk%F +&lk5S;RkasT(|?zS$Tu;JeUR}-wT~ji`C<2Lkwytmg#;ELO94mZ27nvgT9b|;g^O-H9~_-L`pi<2c0f +{T8+va2K6Z|oKH#=zjtZ*S>1DSwHl(wG)|;3J#N&{{PbrtZ#i@4b7ygX6&3JQ; +4(7F`BM|OH@*Dnf!tyAxFfEhqxg6lb_z +pDyaw1MK%v|$PB<^78&2d!D<#D5=?heO8=Pr{X*~TYllU~=)RniS!VannJO*1tdd``)7kqPfLDU6@&D +rw$jBHu+v5Rc1;Nn#+U%<+d`3^{d`oQ6Uiod~`WC4V#?ccX>z6NNEMNzCapJfRSAEMG*j5imR-$)>BM +xryYfFn?4@%pZF0Y8lM^iJpz{XY?dNQ=Ie~=j)XqXOT#f*zsbqhsBB70p|u^p=3FhMYP%B?An&Nyg_o +_-=#dlvMHSK1X|^Az9hR!$o)*d?22E}32sf&SDN?rCl==SeF{sO<7dO!RYNlMgSj(W2I~b#c|PEC9W^ +ZOeBcbm{@it@{>)!_yed0taHit-DG|;(bPYju)aaL-h +(#>ojM5cHMI)pbgipL58=x7Yo+n1F0Zdv12ru{fX*~}%AHT+$!Dh;-HsZUAeK*AR8N|Y8jF$Vj6HX{8 +V}s%nN~97qNG1|7mKOe51lZ$TRq_Ai=}_>uDmljDFLsRpyif>z&_Vj0v?MfE_NGS?8bmu(rVw5L}Rf< +ar+6(6a2g)H&;cQw8zq)!RywutgS@-ElN^&=(9Pa+e)nc>DpT?gL|0S%b&GC+DeL{NOBQn&H`q4CBe3 +mpqqsA(LYx3EhW}z=y+3auv+ +?qiwl%ELvQB8=z4q^Ea^e`{us8DU}vy;W5*5on3C$iUBFD7JN>CYpYRDK)Q9p-g64tE)DKqG@ADNb+2 +~i4ZBa99lxEoQStZ=>ot9e48E($?F5Xh|MY97I+1k#BV7#&O@5*bK_&0WO}49dxzpxZs3cnDb%D1>HqMi&dP@u +9$q^htkoHaIT)hcN$dg3v`I=~iur$YDVnog?<;|)7%0`#W#Y}F0p^h|{W} +{;*yPxUb@=t56(YE&5e)=c}Xkotmki6+P$W%_Z64Y(2l^Sd(h-@dKEi4sfBA*!y4JF!Puyp6n>~3^jO +|9NnCBp%#Wsu8y2F6NgSY2#%^}Z@`elH~~r96iL$K7=(a`}=NQ@!_933Dl8neS{<*pyB0tCH;~6&a}E +h)q3fZTG$^(N4k42E}QrD_Q?o$J)&LUI^RAqosw+?X34ziTK{1KTe&QyIMIjZ|K{*aqlbbUh*kV?GIB +@9*w+#;{q^{o5(t=#5*e40LBReqcD@3EESbjvQ3ZPtrGlPEl=Z7x3|``nR|f;yWNNLrgy6({!>AaaEF +Klr|1&omMgQh){3TGWZbWYVlLj6;_%w6R9d)4xXuL&B6^AtLC7TfeDQ^3?79R04tAiKyM%@8^kMu!W5fOoI_ugV!ya1$Sq_sYhIfC?z +$Vz%bBj>k7>qv0L!@^+)i!jG@ZBIw^Lz_yi4Q6%J-`fu`S)hL93i$4z+ha6EBG^xUY?@zpm&HK;j^K@ +QE@jZ3;>LTJ)C@@65=6_Lkdq +5|!nf!cB^QdVN9YSJuRE|FhVIHWhLRB2&vFCvpeUr{zJc!IQ!O&_+##&4pe%5xoqh%&NME4D^Z+IstP +NX&gud<~L_;TK*U@o8U+d&Kjq#fYU@OGJagQONR)Xq}2QfS}Ki3XmT!+e+5Xp-HCX^N|R+pP*k1!f+U +!ut1MQcIVf+Q3cuDr)3p!+l{jHO9fY>WU+!Q9`5e$}{Z56VP}iyu}J9Kw8kL2m`vpvlW$QDSNtTccN4 +YqS3Wv(tPD^4`owXhPM)N3aR~B})Oo&iJK0r-yRKc&&ODLRP`l56$hum^uW +jzWWm}9p>H-;{EVCyDMC)aVzf!4C(r_swFwZ00+GIo_zk^jTN! +=u(X^Vubio~`nrj4>++>@HUpegLZU#!k(0~M(7Eu!AYHKjWWddbmxCuFhn<#n8QYTpRQ8MVoRzBhEF_ +$l)rNJ&s!30&iPjyC#aG&l|I45KC4DIN9Snyv!$r!&*Tepm&M7~+SLXwKmrhel?N+dJ?{%mNqDkD5Jg +1Pj&($a)<{4E+bV5Ha2Cxo+%WCH5|CT%qb&6glpV9KEMH`D=g!IP>nL>wyD~OlNYqy!3=*X@W7cV7F +TnWAdmkY)Wr`Iwi~~$WE}onUID^$);#wJ_P78nl`k3%lGxZM3FLeO$-d+zz6P2ITLVa5I^Xz(2m(Dz) +J(P52=CzLRK^jTH-vTWVb;CC+GhRLU>opQl!`C-j`A1-b=_2n^rP)`GktC%#ed6K}l8SCMO;E;bkyPmrJO4HmB>bdjogYfL}T{n8GxFNJNE+wuxN19G=9fh*Snt;&M1(+}m1`|W91c64u#7Dql +{6dp(wb;ZMmwJF1g<%9keVo5E@9f1HYUa~~eU{}BUNz!Fv~!Q$1uDECn1<$f6Wp0zudc_})_So%jPQ? +pkOvrko42e5)hU|- +j})C~ikxRBPQB%A|Sa8jI%(r73A +x-yy$xPitkZ4-IDysxO1>6XHMuZC*I2 +E`_;&e(;Rjx&^@q*Q`;eULr^OW*7Msh{Rs0w6aja0X?pSsU9XnivLv7BK-6qbGVwl)y9t#c)EAz{on^5Aw?0zhbSw|=j ++q~&{j`5A0tobFZo<3pTDv#jZK}tVKvl`P~r;i{Chjta6`}ta97AKptNwjL}VZ-wj_k1MhG#5ptg;J1 +e0Kwc?2U;z)QyYW*5PGJ^!a{G@dt^1M^J$0t28xjkZ@BO@}Azr?6+g%q8e^6=zi*fJ$D3qQbemSUmXQ +*f>Fg6xl(7eDn0V_=U?WuQrD`=3SmJ)c}l@4Qhb-Js<9vzB6`y`|h*~3$hS75EX+dz1j&tKo{$afEFN +=L-c0v6V+@Y;1$dtcu5e@n)_fTf|WQPdUN>MgYo#2m=fVmLGf3Oj62&CP`@_4N0xiNbQk}aZQvs;>|^o +Bs#T)Y*^J!G9~QJi@EEdY6h3(p%mGjC-?YW0N_9Iaf+a?glqC|>}h#(!AW1l$0RZe{NLl<^jKiptY?L +$GZYI{WCaXx{@M;(UrZ1Rv`V{Aa$FT&4ahr~*GX1j_D$QNVvEq<@EI=gOO4p!b_!DNA0HwzedI)(6p( +J;6tnJim6Et*h>ByXSmgdwp+U7%NS7G#^bb@(ls6Dh@F8M?NK7n4#|*jyy?N_0yF10}UwjMomx*$1Ht +K4Lv{@483GmKhw9fotd*NZU}Bw1EvfdSS^^Qq8F|`sR@a>TdyjP{|4bNOSzPW6R#+Q=~Ld=b53$ZvhT3tdPM(`$On@f@pw%x1c3Vvz;+LXyJRYlNdYr`15;qm~e)B5bjOs>Q +6WI=$GR%FD(>rT*6*k5dXGx2H{|JiPX-Y+O_oF&s~%%N7+B#r$%sA7MQ(aTjO0qeM5l>FLzP75rySj? +-J-+dfo&_88L0WOLPhF?S@%;wDDLyoCN+Jz=+5&-SCJq)G?@fNe~2-chg7M*6Pf!j|EIn@F7h$u-|?{ +NDQkI-5jRc?GhXe5#@wyYm$Dlw83Y4b#TBedKQ4yz&t(}FUp1r7#y@I)T^D)A^?ezHxED!k6l1-MjWLvp2cX*Nf)Ftc|ltsRi$>|S4nl2$aFpNeVt= +2|fw`+Ubre(5`A7a!lfx6nznTM50?}cafW0$bqD*c_-$$4GEbMAUI4whH$6z0MgCyRc+rhkDg?HpkRf +Jx{?uNVyiJ22nNougcyhx8Rs7t+~h(O>e5v%OAr_H~!g;KUud&YkC<%Zbq74AX=~q!gYxKRF|DF!rX? +cd~uEgb+trlx%kLc&^|+RXG=SfUip+66~g=_;7v^a)gmzgCO*NXZXgtgdqE#w}7=8@-a0Ali+)RnV#o +)GTpYBY92;Pxx;iYa^Lwn(%*0novU66y2>zVyMzb>jHwExN9_qkxX=T{$WkQuH(B@BPB?RyD_e84Ig# +t$y@Qv&(w5zUT?ktSHWwJc*_b+4Co2}1?Fk0Gt3R28m8m_jga$`muoxRw24O-M_E)q)VPkkZ^3eoqqu^hzWV{|&@AzuN|12;FuQt)iZhtdWef-l)P2`d?-C%KxuB<6Tm%X4L~Ml%s3hXjZ{BTksOe4SRG;yOHzkj +eNY7lY9Qp1tM5JF)iS5-WFixxzVO4tF36mH80IB&_iYcdA0xLK#qRSM!E@Mac&Ks>UwbJ{(_7|8=XKE +AzlEc5sV`V=RY`lY|zK>-U0My1lU(>S`|I!1K1kH!q-n7APmtf<=PG<2jO=CW&qzq%^@zZVZC^~_~wI +OPPhko)Z^C%WDmEH?}cL@xR#apBz&LwDvCEzuc@5CB{z~RbYt!IwA9{PbL5+z!UeiZ4E50PtrHQ$>&3N8H+2aAd4R}@l#kd3vDf#e7?}KUB6SG$!Plu`AEkjRozWrC=gRC?6l0Qxo?aaSdGqu& +eHPilDJUkkF>meE2GQnXnsf-%sZ?I~!X4IONVjlSCETC$!6v`VONPG3O7tBtSuqlhSVu_}22{I7yDA1%)gp|b(TW6j5r1L6Aa+WD65l3az?>kX|#K&%R+j1l6Omp{U5sz +Z=#?gf)zjL!dX<;~pCOunvTksX3s<1!(Cx{fX4+wqLhvgmQtGJ|z`s->?{)r-iK)7l7mb$V=;!d5VGA +KsZp1fTi>#P{X|un?d4I5{6(cL}E@CLO>Xb1*`3!Be5L +4`{oBQ5}%m!O(6Uf=8kEaOJI+!ElLDifRwlMs9~-(n{n->V)3;{@Nk5cvMRK3AZ_Ie2u0Z^zLXsFHQkK<)3*iF;t3x;nFvfNk{jLh%Is};5dkV-S6 +G|)9V@@FH-oN)%;n6pD;m*yuO?U}RX5G>{ed~7N6 +UGx+S=ML;~XBNH4jrIG4-skI?XV$xR2;RuFfN_y&P6nv8Dhb?pQTA-RdZ>iUTx^7LCU78&%w^9=2V}Y +fs6fX!AQq=NrsOKeVpyb`eP!0o?-x!#m~t9$hhWY$P2md63b$S7d=1I*aqiB7A5QqcEW`GVSzdy;&n% +tl=zv!QYj_czRNh=&C18o7&2H}aML3g4S{{lHqZ)tdxJy{SXZ~2zPuwA(6BbbcgGy7lQbvS!`coJ7)j +oX2+*>^a%L%OJC8enVQ1{(;tD-uDiN@#*k()hkQ&mIX~hArYadK4huo;^CCbG2e)oXu +GbI*Mg!QEq-KF9dXAB0Y_$YT_V+I{M1dkSQVPq(CLPanKUtps&Gzx*Ch0LvVC398~Q1R$)D!lR6ZiIVC|nv^5-oM9>}8hup-n8F +Y8B}e;LZdVU?8y5Jh&fUReqIK7A +2!UaO-@H?#>hP1jhp>YB-qZ30eD82_9hVCcJ;frQQi{c`ot5RjT;3Q~wZ0!nwb_N_LdD%kDw;= +P&DCR9bwMym`=7wa@7FrPM4m~MQCs$4{7nX_#IQ$PH%8>mffI`v21e6%_bidCwhO(<%e$XCpB-9}qyk +)roGPmcUg%+P)~aAQ%aJoOZCs`Cz=XEbBJm +rfKwckj`oci|Qy!MB-ICQY{L?QWQ-xumsGW=uiXRHo+=)`-)qoRXOJ#kFnk!%p2I(l*UxTWQ-Fbt!K!BC__U_w|2Mz%W|cm7#3{Vc{v+r$=cDpTO0f7 +p@eyLx2?r3I$kX6i2n +SQ6Ir6AYlCj|e%rdw5<5mSC)JXmE9^vWVV~qP7V=g~upkRn*bd9x$^#tF-MiIv49-CF}&2=grP7Y=t*Wvtp*oAOQ|a4s4^@8Om@^BZELL&mg?icQuUeR* +X3z#F5!yyC5F3ebHCj-sy1d|k`-XYIPU8He`f>dTLe1kLB_e36M?3Q=Pf)oEf^xIcH9+tQWFX6*I8~{Z`S(OqzP>TMg1 +Uj`caH(8uc@vvrMJIolQn&sQ{z>hXu6T%u%5#)s*4QhpEh^A{d2+u}A=ChK9*X=LFFi!Zm~YYiGV*`YQ9$pYSQFiHTB4UR4c4FyL5ZkVj}1>b{`)w-c)A-m^%hxo`vU*lyTy0pCacQR@o~51z*##DtRB%@+JK^`>#!a +XyuNavA6b?)m}$1XtsI4Yz)iKW2zWJLhb>fgvG+9*CN55;Q9Fq3-pncN!39Wh*xUijYM|9iCInE7U7~ +*B!9s-+?Bgr$J$#2f&gdSqpfop=Wl7&iWjiz`(XFT;vJHQ79pYgV95D|=9F!HwTYDw#6GU)*4}1Y+bz +OFOD_Z)EVP5y#unK6%9+{n*=9Sl-i+cV#~efYdR?>#2B|{79NA{#t+ugj_k4@+&_fzOJ4C&vYx`SVxF +yj;>_ip2fOWTqMlP7``Hqw%pP@}Y`pr&Pqri$)REyBicpNyN##JK6KPEW0n4Y3MLNu>Tojh_jOCoMq#A=6c#4>J8)@War#go(UEEQue{n +LHQ#5JE+uM)Q;_c!I^?^SentEka2-8pD^EvZYxFxjYaS{M*Ulkjo!tN0*5jHs#?-WwgBP)MBkGtgTn* +FzboqRp@!<4qRm1s8$f+tYrk_tXxiD^QZWKK1*r?ek?ymLeYc`VldGPah}v+N>IuAuLuX*Jj|JX{#*f +i@(R4~$ZmCX{Iv?U^YL4*qdJQ7DwI}->uAAnna_iJK1i;}j!&X~j{vNHnn)%ND-->?a=!d< +%mZJQD;f?#8Bxn8Z$aG>oJY<2A1okf~s{&e!EdB8VvWKg`GWemML6S)cuj23IX6EwR=t{SC<;X$_EYc +udwQHc6zj~5d9E5MA(N}1yiCL%XyDuJ>Y`egb(CUsRaaO#i7D{`qDmII-*nfrb1t&BNC0PP28elREMi +)q|T7=o=tCf=@+mMM1dDo!*( +px|EEZRq->ecnOcqZ?f?!F-64Xd4+5fuL +DGdc&j$}2$DVj_*}( +KP70Q#*XvBDCg?e*XO71dCN*YMYNd;w=tQi8nBcEatx!VYLSlqhsnH6C7Iv)xJw)?f`5Bco2&@&#IF@ +NN#8mhI0?fCu2@>0&X++2NG|+2me|PE_5k2yKQRi^?bg?Nu^rPHaHt714I2X$+1P?47SCgj}FVLbJ)V +^PfY@2%M^=Ja5B>dW*F&B0%iRmn4x|P?i^(@hI~>8sQ9=napd!59kv|bGs&sS!}?K*uqZb(Wv9d+B&qWC}R +Up<}a;{9o8Y-vrVhKKhEmIJW8S4&uLidl3SOkpL=y_HUXZgQd3)L;GNU_St<1A5f*YvXsw-U1E%*JZ( +Vlb{qPz)qA25MD2@r~L9vDu{J&D(7Mhz`w?+O+ru)?j%6N;ky387P)W%MDCGrQ4>h9es2yci4?0d`Mcp%MDZJ>~kZnp~+l=bI%(&x=urTS(}8dmOCL1wE5~y$5dJbs(#9(P^?Jx>>xZf +miO{^g^H{jyG*H;T1Uxc9S%G@F4iPaq+o<=O;W-`{vlQQ^6!U`nz{fg%Jy@az&Zd}fz!z75z!uUI3tS?v5x)Q|E^9MHE6g=2(qw<%Gm~^)R)17A3!svFN`mg=%&*YYP~P0AYTCSy|$K;C*rW5n|&PzK +$HPb1>?WjlB|oo9k6G*z^$IkoMO)gOCrHVwjEwOREG69{morxCjMR5cqN%BK$KBmVVzga8a|(YnOhuq +VhL~f-q0tn@)#b=#S>+DN=&jLA_X|_X+L{;waDMvqYNC6H+g+44A@V*|+^KT9k}?;7yV=m!>&GfM?~8 +6p@1sUYi7Z<_J=Lo0^heiL>-nR?GJi3?}orRF9kH2f0boEok|BK0&K=C$dAjPm?RMur3VzCPAD2v`IL|dlM*PUVBCkT$5l8*3mhSGB4AoDE+T0ikbv@CSaBZ0esGakCJnA1K29n&fkV~+-(`F!siZYv_ut_N9r`VStx?N#rnD0#(XCz-D%Ck6g-q!PrsRRL^=X#Jv1hdyOZ8f=X)VHnWC#AVp1u*zOln10L$)8If-R76KmbJgs%sFwLn8U$h7EdH>*xj|S4=EgD +@&-f5?NF0)MrW&HuOSQqE<#FP9XJy0<0w^Q#8lirz_xc7Qkvmv8MUh&+-PClhU;#$2xW3sZV*e&pVx< +hc8-y=LX@gN# +bybtf7{r9Z73hR2d%HmhV)os&hns-I5*I<8s>M51gMb3sTG7>j5n`3m(_6%f(vde(;BoiZaO+3^%`y9-I9~Q`dmQe@)Y +qU>}OFCpt&e9?L74OoRZlK!rv8G^R9kh!v0;S5PNex2oE5Oj=_c9V7@%e!SDN^F3~+4>b +xJK>;%*3_H@y?<9|1vO+h_>7&swpQ>*hdtOj!>DCUU*X^iXfBUpe-85vzDov51PoRM>!4S_ezq&TOdgXAK +F*ywb0-!8CWT_#=vxR9W)lRT~2gn&07r|aYjwhV7vfxxznhb9Qhp81f}wKd60EX%_`c^woHS7!vo +9>@6k~?m5U`-t9`0?gKdVB24RUuK(zdKMh;o}9NP>CjW!qTZ@KWw*kZUcXzVVp`pdwd8Y-ROf*WO9VK +i}Qv~`a{*z)^PE=@r(?;RRbfPLq-4kx&9@6Yd$@>SPAxbvYdw7{UqM?^r4v&Kw*@>eSVXjC@{WvHffP +9FqP>|}YM{1{`?C%BPpw@MfMM%r4donoWKaVh1Hcfo8q6=XtcIT7mcho1gB6{8|OpYy2L{@?LKS}#iT +6Xv^-UDhD5F`eR#Isv!($yU{IB)4NZjh0Rs+NVy=ESU{vJ2=IllbxK^Fad}m=mX_mH%glt1R=nBhBrs +w)<-LqUbvla}3l`&@tMtDi_K#c2rn0qOKHbEkbffSXeuR&m8>AAy4EDCeN$zz4+8iW$Y{tP?U`ObuWR1I#Qk#bsE?1J6W`p +*5I%K>4%I_ego(w~ +x`N|MYIlEuSzc{u5gP|^wr#fe%%bE?CN`J#Ia?cz2HZ(dYlHrJ^EHTx#9yrf(Bh-V;2ht=B^*aTL`)p +3Kk=5qR&)UISg{>AM?v&xvwJSh!#(ZwLalw?aKmyt!^Nbzl4pr@F)vwU{Qz5cF+5_E(h#A}=X^J~fU; +<&!2`_mkyou6IHQD!`3^sLzI=jRylRFB7m)svd5#5pmj%#p1)SscrJhx?Q-8lkb%pPqDVo*rDNcM +|UPkHTz_a_vn0G +5$PWZCJnd|KAT?n&`gxW37Q%vA=kN4}dLRii3>2CKwV6XeH2dL> +fTur*B@g-Qx|e?*+{-bq^``*Wy-eba3~9E8Kuj&^p|VewwbA(n62Q4H}&s4K19(Wm`PSj44i0jn)e31 ++5xXB!X9@RKmm0z#gZQ5mWK9Oy=0(BI2_jc%H3bVs>f$#)x+90CC~)G&Ii3x;96R>QL;a!s)*`>gc`1 +)(oNr+4jmnCs1`M$4p+WmYQQHZ(ENNp>DxME<4XB0tBr>c$O=9S7Q+MkE$i{%9IfxH?%Lcp7x0E#ceJ +!XJ6uF=P9gS|!>9F?KtwF#Cs{bba8?-#gd4sMYbuumnD1r|LACZ;y)*jJSZ+VotOO^piB~~gGR71Uu#+{eJ4~iA?a)Vi{aj +KdEAVUn;#n09XX|sj4b~IetWfLvebO`kx$?z +qZxc(JJfX2QC9dM4FVRNtxRN~nWJMe;DgNiML;T7CPD<6z&=L(~@xCcJv7ZgHpA?XBX4xw +W~%>`oSn+Up{@r*+G01G=td;)2Qvje7Ena&kRkZV3_n^!-J(G;f({dp&pV6tCN3t=(@qvkPlT&eGK29 +3>Hf{qtL1OCyj0r^q=wSX6ga!*R>CGw2Z>M{#Wan68J-`{~nlU;gI9P2!3Tt8#5KX${U(RV)ZMNwwO% +W#a7pX&n99iwlBixPXZ+f-^jK%g6>-+vO^~B`DLE+16QtcFnCjyLMf1onU8Nqa>5urP;?s(r^{0Zw&m +Ey8>-+j~oYcm7u^Ekf$N^`L8-E0ub}fkSHOSF?KABb0)(zm)NZ)07$mW-Kp3e%vUA60$3=(3a^ZM;v& +mPq%Y{P3u3zRXC7z=Equl*>Gnk!T9knhbTN=?!|?}oiAWk-ho?FCsfa)UB(-_ +%Yof(-Y|Y|8L3RctZBP4K3P^1s04cM}gGmK=cV|-``>DQu|o!?&u)WJ}Hx=p0^6aEm`(jgrxDsu5n+9v +HS0S`$l@&jihT*WDBa2zQ{z8l-&Edi0JD(i(K+tGYJF!3W8uPDPUM+ZUSJaw+<<9Z6d! +6bBS^mR)>W#|pu)p&|Iqy&a;38(;Y01&_SmWC;G+h(pQEX`Wpfr7hi&(Z`ut)Vybf{*Asn;ywfot +SHVZf1`HHCJ|80UBrRWd=&3ABSY0i9h9WAl=%F&L_0P1}J%E0_SHl6PX~^Ib3Ljm27|5!HMsP&4fa9o +H+=}&2aEt>?=0IlySW}izNQ-xj9;0Fx8*Oc@?< +3seqa|s)u}O!6GIqj3vY?y1=Z5~!3(gY#>xs)u&v7I(<@347(yd39P|4jo^jt;oW$i+QNj&)O=#o02Q +jdgBRNsb+ngTe>;>VIR2`KvI?X?g7QRxw=xQF)u^j9A0vG09l9q^ +VWQD)vj^K*?YB;4UW65I`ytrQpyiXN-r5Zpk94K6`py(I0-sWm}Qe2z+Mr~dMcvJ6g9S~#+^elY@)aH +7eTaL4!$T}t6PW`}$;evcqWnUuzda%&!U>Q{}J`9!a7Nibt=(Bn7>*nCR>qS(m^P>->$d(7^P^M9y%r +m2GAKfLEHxANPFEIn1t(#}y$kMPD2_MkY*ogtCW=EQZWpVT7$Hh5BW#?5FB2)E=(^SmarfTIl^d45G2 +2U?0fZi_0g&0uU9H$ATFNVkGGdJK{FY*&IS4}hhvt(o8J-Dvvu-_9h8w*p&d;UoTCgYzC^km(idW-!G +gQ3&&A2&kQq9N9S9)@3yyp`tdSdW0&t8fg(FMR};zL2feu!qqg*!u#6AbY6uF7ttDx$|65zRJyzp<+C +s#bB{B%+>xSboR5Y8X)FhZsm^;hI^}tb;1pl&MQ6&xNy_VR2mU2m?h)*m1bue}d!xG^dCu%^bbaRD?A +>rNXI>&h`x|+w&yz8Y{jWz@WL~~vcOpJSEV{XZj`CG*OW4#6UmWh)Ufs~qki}Ui0Wc#c@9z(MJq^Myv +5k=|SL+4>Dia}&GC5yP1XQa12OE5S;9l)@nL95_L|&&<>*w@zYoPR4NCUC=7^eh7e?8+PMC=S-B@%Oq +beT{C^#_Iv&bNZ%BSs7Jd)jt)3NZKI%H$9FgrE#UWb_7FnPqU2(xlz>)ENW3oVoLPwbr}L5%si3`VWI +M}7=-M)wjAQ;&mfcOYUqZxvM2bWSS9+>?wbiGX~$Vj`LC7l`#O@B(W+ +jDJ0icVq8A5NSh3S=HO7z;hWUZ6CDS|FBkbbEU?>;ww(*2Z$TtWpA$dA|gb90uW&HWvTRKa3K03=uh6 +`>PY;1O`3kPJc&D|I7OHsqbYQxtK!yY$4RR%7bJEmAU=;Gu5YW2cRPs8_1h2T891(6bfOIB1B9Ir~kI +6*jb#uHnG5VUcVU5UAG*a%*=tLBpp-FT!#p8NOn!dpFtH?ZQ1_+-8n;hLWU#+xc-O!s?_Adc#CjUcCb +*($VxNyr=tk@Yie%7%56e?+Om*LN6}sHWJJZNA4#&9EpY9gbG +gZhEbF_gE^srPu)~S#be~4K#%l0WyNq|mvgnn7Ez|T{n;-;eCqCyyL71>9D$r}PL?hho*wtAMqsaqzbt=Xz=OW}s_3-KOP`-OA;ebZy^nZYtp) +$V#J-1MptGIoEczL&7dxAKs_6D_={Qo68K)0QcrmH-BY|IY1yGwcJ0#i{yt8%&*uzE|6`enSvsvR+Tz +`M1YlqI}1OLbxH+u}JrrJkN)yOfWtjj-Wq+sV^M)rxx3+V%IB>NOqITk{j!_^sL3+32vgPGsAWbl~)o%zdAf9|louQ1Lz}Voa$fU=D+R%^6)OmQP&2}w!t~19DD13m|B=U2<=i|L$ +=akpgIae>dc$v&Sybzr`hf_>qPx +T4Hd9w*rkeMN@D=d$v-l9!PVRq>MoY{)t~YTnHn!hh6$LJrw1%-;%SC?D0P2^TEB4mR&<2vmA#nm!5a +b<_i9Cpx_zT@0WId`fN-QqCj=*(Iv?nPD3dGNT|RFmxQ+)`Rch-;8wcTxcIeme%8cUjMeL<{mD|Wf{5 +LI56V5j~gLeB}v+ai4$)t4Rr0U|0v($8B;r+NAS%MoQ5A*k_sffPGb{0Is1dR5eyo*0IgEGd6$+*O`s +#i(S8~Q;qDql~GXGnbD^G?n6C8|NIvG56oi~w%MBK=VK9BY9MnKC;JN5~CRBv04g${$QT;&#zH1!0hW +T0TEVh$f;Z=M%St`D}e5VUAY!wg(2U6C^4~X^5RuyhanWMVAw2Fbxbp!s7OHX)yiJM)V_DIQ~8Ft^_?EqeHZdF6Bn(;{hEun>a1EANlic8Vwz}Mz +JuWa#=nwkr(6ad0*dyPT$V#9c?R&L6Lx-;OTXg!801u^rNzRKy$#fqn#ix&%vx;vQ36 +^W0?8t3OM7Yo6qH?M&$sNO2eb)r1ewI-V~)7Z#EZP;?24c!wwKjAxm-UBV(NVrKVAgj2NFL~k&=j49a +VmM*Z*XA;wxh}YfTyz1xE)WbNxVW#pq%3JUOcHa9TrlD4Yz*$=-BZs$3SOg6JAD)?&*(JyUKi@eh3W8 +^$8yr^(qD$Cgc*;M{sqAv}1W{gG=6sBH=XKv*t}mxFVUDT)!=Ju$lAO?KOdI6+*|w&C0t_8(QnTo#+p +}Yuk8&$jdi5V-_|@oe{KUsRdo#bJOF-ow-^aYY9&Zkq3jUP4(qCd%Za&05yv?q7rL+dq-Cq9~VG<0Te5jK+zLA2Zv=wiBj97C5f3-f>LX1I +_Tldm@wkBKGuDp4S6mS0fH2rWjEz9fR+DrlLGV_%;4?$-S&Sl+Y7I77Ea5v{3$oeY@i2xR^)500MoU2 +7HVL#$k<%h{$T{}bTCyXWii!IL6hcy&k!YgjM6_}ZCO=m(b!K~tTH@m8yNtZy3;;bA+2Gr|zI3g+}mf +9O+ZX-TVE{1P2S2rbqV#$28#>EVrd +Vw=`!FnQUXHh+ovz@VHUmcNMycUM3*d=r_iJr=QC-?Ft!e*h)*qwVzPA%+eM1o7Yxb*LI36`jN1S8F> +fH|-hVYNsJf84Ej>Sb`Oqa>8qL-`r)e5^_S9?rL*CB239)8}z)NB +$LKp;kaFG(&x9QNj;-Y~XTv$1-&U*(}!YAC}2J@C7Y{DEAopan*d6vD@w +i+{2R5d(xvKEcN~&IYGEPc6joPb-5BHJ*7pV4^i2JyW}n-4s7qM8Eo^O6AYOS#2NTw_vh4w{| +aHVdi3!uKn!>3tsHj_GZvdxFA;I@L&>3!PetN_;DUggKb#BSmwi7p#6bWAOHYfjz}9E7KE;&32Sq2QQ +n^(jkD}?QR;`}%7!}9l3LYD-e(z7ch5L9`A;FWC)4X<=L5Q`UhKe9;f@Xk9oY*5to-sEFsC+A92{R3@ +IvArw_@p1B*!f7!`O(P@5)n8)yGBFH*~orQ*y0(oz*8Op9s0En`in>k+P4h?giY^N;rK4i~tu(V!R^q%qEOX~jfx%?P4re}_QG0^@d5m7;RY^)oBW`p0{-DLWd2 +LkMK}xET9>C+&WzL!p%3}2Ae2g&;0&~Puocz(%s2`8D~@AOn|x2G +y+p~DsIp8pN~K~+iNy*Ks;Q_n)wog1#QX1AV~?9hNwJ6z@BtA3Cfch|FVP(3!R(;Yjy`eV5EkwAl23B +eC!SATfh#xr`I-HOdgzvpXhY4rWXY`4`Bx!y|{?H-(F-cF+JJre^PPi(-+z-y8LLI^#k +CTbxF3ek$M7KgJ0&=6}a;5S+Rk`{Wc7g{0O|fdzaK7@px0;s2YWfW)lbv1p5QK +!+f+}yIt)$$GRv!M(%)X=iiQZ?c9N}Yrb)txm=U|F2%fJY&LMEYL%-^utR|3e${DNMtFy-%%2dCW5Pa +t48}=}dublZ7u_k{$S+UR*iv{L1i@rn*o<5~cREZ)hP2R&$V|rVFr#k82LqaVDL|F-Sz4^ad(ZpHM`+ +J%Uukk?V@?rFcp8DtYP>&TgqireRAX)J&!NU%osKU5dj&YcyA)S-_!HN0WAPR#j+F-q8W))oJd#ps-c9dwMC2g7cD5?q%<83h~Np8rIX8Q!I66QK4Fp<+?MfPChg)rp*IK +NR@*bpUI6t@@OYPJ?BDR?o~{vi>*uhl|`_a@eUaBfHZex$8S(#Nih}@_ +i)@}3w!snj#^EFmqIAcJZs4KKR!%gZz8+)5wI)^<$);FH@PsUE)QIS<0wQ|1fhErnYw0sh^#JENMROX +nuZ}&wrWWl}ZL&Y)b^kw`a^XPR9IxCYHhR%tTJcb@SQFdhBOZC0;&by +-d&aNC9{tmNh@xcC@faJeE%GZ>xziU0Sqfj@HQR8FBWY(|o(Y5LtmM)USNwSdFP1^@-Fzx-nFx$TgZ0 +Bb36KtSmryg=2c`VK$7Jh4>IH~INTZr>YN$w&)6R;zp0g8q%vd^U(DYg-(&_?VD=dSYjwR_O$=3?L)j +i)%mkJwl1r`^;{3zseiZXWN(Pe`MqtO+)HxKlj(KW0N)4>HhDUy&bo0&@ruG24+EZXoZ5%L~n-Y{s8x +GoA^zRk@~=yD7|scj+X#hrB=cf-BYXk&m*2AbJ%@R#{YT4O0-BYd3qUx#8-nH(Z}MJL)n +V?j)~^HZNPL8jo&#B#4g*SGeo^{lIt*x0zq8{MkCh{%OQ>!L&%+XC9AJ!IrTa35j5|EwVcS8O7xKN2q +XP)?Yd??Lq7n<5u%rV>wRuxjakKt-NvEe!kZ@MDqOcEl!I%D!BDQN8c9ronPmQ$}^5rLti(U@(wdF(Q +3mvZAMS&DncudM37pg@aQ$cVT|?s9PDz#dGzf89uLhKLLg)n4lFvZ_q9ZW!QBR6AAb>{kSc~Oe@F5?n +gsm-&S{C@>ke2XZh=F-hMcOzkNB2bu|e)B4*-HwI?meNwz#J}x>l~1)G4~f{o;pFw#P}GTY0z)nz?iQ +DofJ~BZiyCE3P=4Jk>g?ew7=>OUz<>K`&J`N4Z=4Y&En>-e7NZpP#{Ej&#Z}H;Er&RebC$c38Tq-R}0 +wkjLS4z!J}YOp@h>@so6m!Hj?EUfJSK@{4fcTse!VjAcE^{{2psJ~xzC3LuU3LvAa2)#-JJJQGotp+R +6)=x{UnZ)y7aYK$Luk{^_Z5^LqF9D?(FR&FQn=GYaGD)BgTPCtiY!!zF*pSIb#hw#i4XMZNQlOLjjz7 ++hemD-Pf(^-1>V@46TmmdTV*tW`I?lyVATik7a70F*`$x13dZ+;c$3#N&9qtGR{j|U47z}ANe8{8`1O +Snh34oh!^<8Gs4K_*D$HRT5KDuSsS4j{^7>xO&7FQa@Xx9MxeZOJ|1m*R*apUrz45;uge%VMl)i9H4C +bQ*3CKM8&Y&cyR4?qqGr(0FKz8x+R!g0X@7#QR{{Il&maBcBPMQIe-Wl={D33_rmnq#eEb&PQ%7!aR4 +anjbiQ*u~~v7}`9S+zNgL28lE+M}A4dNi&&Y!rEE-k8L}1S9s+$%9wK7T){V7(X?cuM{Wl1!;VyI6h6 +~pj=QSpha1AfhiJ9LIbA9$^r2qo?(p;URbaiB(Oaj*-Qn*6CvPNCr%Q&$DW=6ocuv&h72bh1e$xE1><1w?n1$$g~Sk;V +0ID>0297v9qG2c8l0;lAwaE9^vLzOBw}+3epou`j;XLgJ`V7rs07*z~S+?Rb4og(h}ZVfJBnN@ncY{A}Y=`5|MGE1zA +hw7~e!h}Da;Wz)IVmR)24=4^g6BLkqR;!p(XCI{_P6eFAcA~Ya)6)W|&@9KfYsi*RDB^u5Bxp+m +b}(CQU9GZn){XHq7;gR5y?VF{7QokJv@dP1(b{&?&Jt0OJeQ6t03VxqLD+vA6~Mpad)1WmHd$Mk~u4;BZBnslj>mz^UUae+fL5Q^7#w? +5aE!aDkNTB})n-tMKsQzgEtFwu0hTYFxsK~0As~~(T;Ch*6I7cvi$@yS6~lR%kj +wl(iP^p8ewA#9ilNBbMTd{ERn|dY?W0}Q|_rg_3lBTEe!!Rj7Gn*4&p{%aCwLf-~?g|BW45BOyn$Xnv3#xtKAvM<6%OtUj*KH(HFW}+AjYd%Kn9?8I!bU7bA6H~&)9ISdVGr^Q=Vf-ifZrWR_p!8aOKVxd`OA!=kl)JL%56j%>TKbU&c|mn>s@3 +!A^B@jxo1V#s5rsFvx%*u#VnT%W5g|?^P-qw@k+z5M@DF>!f-w-kya}eGP3<+*EGc<1SLvU!%w#{Uso}SzzSvxit@gx1e+(t5pMHxvnnY*1+Q`}LF^RA$h36*f_i2X71p2x7-|rbT+)ceNRU! +l$dWK=k9o3iO_4T7>uPyGXz5q=9jA3kiA7h*eeFR&gb3Bt4!4K~q`>QLq2(aji+%RR0dDUgg>45z5?| +lgqQ(#(LQu$KwwcmSd;*N2H`A;k7gxjrqu&U9#IMw@0H>H=E(}qyYDmr3M?nJ{iK1a?(TE@BypgC7Gkrrl&T3I +cY*V +C0HxMYcek?Xy=y}G-E8OR4X(sDe;;-l@CaLKpzw>9;agsJ1su@y{Rw85>^hjVwn9**oxUGAp74mBq+& +zdGT&;*VBlI6SJe_G+%mjUIUTigmAkJ01W{~up!TcxZ!iPM9xU4 +5*2YRWr7uKAEdr}MsH2sPUS7Zux{LAxX;oo9@VqBz(qB9{u=l>m9NWUUAY*J@1fUB$brRc`eGXc1Lt~)*O!s41GXRLO0w0)oz~H{VvtgBl +Xb9m*P*lUmUqE&rY3qdFS*;gKObQQ)rYP@?!BICZ}+n-lpE!v%HDiSlXk>7%>VLH?uNDL<d{(I+)i#Fk6XY +TAiE^*yBI(V=5d2Z!iM6jB7I@ZRFo-u8+LVgAH6o#fVJ7k|Qx9@`_thg7m0C&k+}m9dk{&SKvJM1tTX#3vf-Yg@Yw5 +Et+c*bmLhIzZ%@4PA2l*7rI59_-0LNe%%t8H{m8#XcB#Z&DpNTKIHTwZ6;W-Px^2~EX_P)OiIJJxWuz +w3D=jyt{?8iw|#zi$32sv;c3oB`md#g{-p@1z9&)_)sS094GLj07*hHKO_8`mqLC`P^cqmd)S{nZB|k +Fv|qPUEcm;VMmHsfj!kzt#IqAgqyMAaO#6m(4VMv3j_m-5dJLsoQsg>^04wBsp}Xb0_|aMgF@;!Ia73 +sK9)}JJ|5zG|$CL>8t>RL^<S_DrXc%t^P*fS1kt@pWg(_uk`;U*C1uO)@0Hlsih=OTx3r9@YfcsY;TG+8FaE_K`7AFTSLSQU&ER133^ipGTVSbqThpd2~7A6!gmIPTP2DE=u_+cX;)pj%c-vt4=mG5 +_h;W^=@k+Gm@?-aue$yDtiN=P`NkC}#3bf?mx(g^-YpKVr#}G_u}PnVFn>#lZnKhN%!F>W +-oYB|G5kUwyY-igePXrhs$I|G7s2{YI1vcD?$Hg{NT%~m_bUoV$qfCa*z+Vp5{;IOAo812$t167mp~>y +u`w`~7oUZ!vxAEbGMLaO^B8Oa1=2~iUvv(Q(q6kov+q!!zw?Fm6P&I-CE#?Kwkrz6!)4J>Fim&W2<5a +SJ%0!abAg;*`-qBI&Q@_jVuS;)4O1jI^rmE)iCy$#ySk8k+ +#Y<`<*Lc;-lt$UiXkR~1UwlDp&&0h!r~>CHN9stHo4C`28PgIt#Tms8Hzh)rHElyn*0^1N1!9{-_E^! +sx7)EH++XxjOxE$rG#OAmIrHrGKFxN69l!3SDM_-eF=u44is2tnRy@Gw$3b4tvvtP9ns`|@33As=u8` +6O%|VEuDE?^{fxmg=*&f#r8(Kd)~Z +?`X2_3#48~oaBGBT7!P(@t?cx69tS9z7BP=rrR~Gj9v{(j +8RrH`WvjLjXo0Y;jM4wSYu0?LD3H8PpFVr{3CI!@Mb-R#A01(JJB^*s0766($tGp!gYm8`h&*+BNn3x +&8hiKOBGM0;M$srp9CD2h(tBSS)b6BUM?`t*QEw4Gnw~t09N@eU-&SrB{&0WOj?$yd)}$q<=4qQF?t} +V1@Atg96u*lTCbSRyd#_$Dv*T18oTZZe5@lvRrk@8#&+gn|^ +($Agm?S~pnYrw@^lPlK03;p>#n@Y6GKvfZVAkTxV}jS)o3#?aI}nijw1tdj?xW`Jx4wO|d#<>khcjPgkZ)L7lhl8z_VD!E29e?S?$nnQ{ +ZyB7G_9HnPG8qW!V{E?ecL?n7aRMQz;bt1?H>#RrIeS`_P+aI%pu@Bv^qE~vT%bD`b3G9SeCfinBeLh +l529Mi{O*o$(d<|1RR&wLy5eHk^KuscmUH!sfg1($sqC5d6R25}SsdeRTZUSC;{i%#6%2eX~9(4-fl_ +OQ_+m^h0h!0ivTah{M%e8AfuAAe2Xw-ZjfSbV_U=h5A498DWj6j1j>lvFWfA1ko#6SeK>=%O +Zp<$4*St5|D5Ba+yjthwbtE1Q?Bz1h{gsp`j7Xn9*r|U*n!c;=P!!2+|?uX-?k&G{fBmt*}Wlh$U7a> +KH%`NAX)x%WDXyQ_{ZoMg;Cu}!0t>uQx-#rJA?jLnio$JM|cUHjGNnM*JYx%LR4+|AiUp3N%XyCbOZE +my1@tsT&(^DI_MkRdsd6R7HV>4#dWK*?3Vh~DQ1^QXwsZ~6^9>IHIa(VE(XB+kXEsr`ctc&RI_An=gI +Xa^A4;@pZO%F1im{|CT!%e^V{sVjC3DmL-T7=w?4SULYf!CJRrP(x<9Jg!MszkJXxy +j$7PcYag)75@v)#%(A2Ba7*w3AMxz*U6dG0931%h?FmcJ2l+kGVexhohY#wzUQu-PRzs9HaPjYTcKCB ++@vKh^6ntRf5q}6bMMm}69WGDvRo4*GqUZ}s%-rXX60lWGLg=j%ARp(?S}uI7MpC~^ZMKvBB +YR;qk`=N2OkZhUJ(881evW!o#4D4J^8)Td~LM*_@KWB*}*Cu`Cz?IdcmUavHp*}@Jl3~)}-|>VAVHoi +pj84ds)i15y)Okug)@*9-cK7*=GrQLZc>cEp0nl#;Drlt*l${H +L!^Fn3y8&0rvc<%>HJLmC$_HIU+sKjfEmRT|zdEY1d9a!+eb|wE)dsVuW6whC#-hchiI2bDThmel}ck +lRs!_SE($W>uDe2K#Je}<*_$4*{w;TK}PmlRu6*PCXxY{v%dJD~$kGy4`CPb%&wf6kNeg+0_WJUAZ=`EP2QO4kiV#s@-P&8@n`0d(!D=TA +M7%7-bN8HsW)waAY3lynP$d~o4^(MtBzg0;Gf4|woFrdFqQM{=Thr0ldjgyZ`kHs}f-{4}~%KG7hYj( +*O(yV-pl+}-0m0AmSdlU>gi#P=Xm)+G*nHtc1MVYkBpunDmkinH7u9|ZS_%mj^)GW{OT_6Yf4x~r#hb +o_hJ+;7Lx*%uN5!|4m+zQlv3S{YDQu_9-*nUfW(c;FCPI%q_s%hxfg}QI>1jTg{YU|KwXgaVbvjUI}iyZh$Nsl{_Edc2e@ +bn;8%<*TtTZul9j%PfD54VocLR>;A%-aYm9|mcA%JHE({9yC22Trqt9RlYv{F|34(73~lJo(6kJY&SR=3xatF6e+7$%^<$9hJ;H9I7#!sKq}CFuTj*Tm0F;UZ5Q2R?Dh!Pb3Vpi3`-Pncgo7Kwvrd9{f#+ncBwj-KLGl@N3^Z{H?4_w44_{ubll# +#NWhQPUevcmXnYEEcOu0{zd~owkvS<==)7BS%2nWf8SokM +3zT~)LSG=WTys=nGo0ED~T?K4UHW9yQ5couhL!RG7s^|4FeTFZ88Y}khx4V2$PstiPURQu9e(ooVnWQ*HRE +)HQ?bqk_XN7IPuIc^Au?~8(3U{8~8puQ*J-7mG;k6Z34fbVJ}(CxL2qMr}m`}9Z%mS*V#?VU-sYP6gj +F6i#cPg)mFoEGl{81J(lg$#>T`X12B2km{B6|a!WZtuo1K6TZgz0aFm0K7k>+q87Txxl$ki(+SOc7WU +mW%^ujDV_TyfpZVmqLsS^(tSb1^&Qp4#OB)Azj!PPWi%&ewotI#V>LpnqP3dgG)fn8y7aSa4@mBJ#a1 +@m742yRhTxMBXqQgo~&@!#8r^OG}2*D`#IBc29;BHzop|&^( +f6jM*tkWajQrfWb!}hrzr2>9Fj`CPmKz;_ZHM_TJny_5fOqy~H-;2*RYVYn(B2qKmt_9=cH3aWmA +NdxNpK)j0bFx$EX2q3;I6J>oQ9dAY-I%9SP$^VyB50P@b-4O)`B@;{chAP;W;J@p-m(~rFVcmQQyAzM +e%zG5&_P3~?FG87@k_+#Id$sH_%A&{{9Q0Ez^rj1c_iv1Du?;%|?mky@i~1lNQwTu!xqJ(#m4nny9C>LUSoc^|sT&zY+{ +}R7)3Vy6Ha>2DlJjwSEV_797=|~d$>gYv{;AqKZS@s!?h{dM;9|7KhW&_NoBB3|(wBO({<~@pAlnxrC +nn~rH#bW>I~ilK&d7c)&-jgCxKG5h+LTN~mv5Nu$uD;)VSG_wFL_ozOHRHfu$STbQVT>HpfISe +MkF>($RPabG$gM4eeUimdh_`pc2td5wlQz3Jb$0-JpM-;paEv3oYm6Rn!G=I|=2@~g(&J`WA2|15Up$ +Kz?gM8(b3@=(4SJWzmOKin0<_anx{5EXABZR+{c5l6Q +9Bvy9uimqhMi^znnkRgZ86LTU^!7$X}Z-_?*fmtkCy4{WTv5_q$U0*;|bJJX_(M;kRYQe`E`J;e+08g +FzEz{l5NN%>!`m;~!qruB3f3CmIKY`$*1R|C<10PCy0>_a-MAXw|RLA{cuEHU8?*V5ad?{BibI`ejjU +UzKlygI`e}QITwuQ9<>Ac&A2asYYI^^(DcH8|>hPH>tDCU17|m1nFH_W9qU?X$p$$vqLRcb=XkXz~JL +d{Xd-H3Yh8L&y)USA9#255U5)Lhd9$wR8>}NE@Dy_SyI(sO#*mr(*wBoAf8JH73PEXPETzq(uq7&wQi +Wi3w~|twrHbOhLyT#4M&lwNl^gvKEG9V>pb-44~gApL3Kz`0e-wg@41&KqKVi1M*HgPAW@dX@TgT4DL>El>Li``nP;LbJAom(zlbeCqBUmqwj&V0S%DZi2 +@jLpu}Wa?FZNn5l0GeUi7n~#=HXyMJ=bIJf8Ds0A#1GJ+&r@Qs5~2s*^`o>}Ob$1*Ar}{-~@WDawmS! +vD-}%~gCdg2R{Js8Y_3GrJhWqNeWj@Ws^K+I#*CmuxA@6WK>|i>4!FLvDWAWp4R^waZ08(2|pJ|BSB7 +ETe_d>Er{PeJZzwm%reU)yAuYG+%!ShWnCYY0ex&|9QF0^t3h;8QtauwX|8bBh{mIHd^))Zqw&XknT- +s>o3})(EQ#d$emkv2VkEeSp@yb%!v{J$Uc~3Ex^67n=p#K#0)OG1^rA}k*z2ofa}ViP!=eL@)9)>Tum +0X^SR)xYl48x>$h%;3pndbu@yMrLlpXMHnFcWoB5BWtPZPm=iDchcIEY6%`%piH-P&Xp!6pSzsfB6ZFFzuXUh1xzoKVy!z3)z4c;${Lw~r6(ln6n3E6?g|5d`YAPZG?>Yq28luCgVz`v2VGQi1EJ>D6%UC-BjgXLu0Zm^-lO^N-O-M=S%UINy|J^(olf2M?VD$1im +B@uKlgJ1+s->5E}B@HqYEEsxySKpu5mvO5Ae`u(`?>Fv7x7PHwt7B5=DdGhoOK?dg&tR4LJq)UZ$#<} +wL@PmOxm(N*IYyhG20X~mwor#$%OHQj$o{CT6)9`hcN18!tCl(3}x~WH;tD#&`YCSwH`!s)WX$Yy6c` +C>5NAwuK(B{NdIylV5>>KJOzb(qD)ySNgcj)?Wql!g+1!oq!Yg8;O!m*mSgZnsHP_Q-rI5++sIO*!?r +&-qClv+zDK74~F`NVD30V;j=;EiXqxnqYIUOKflC80MNjy^g9`#y;2YL~!5aSd1>sWWc1E`WwFch)zy +-lxTrmP;}(EZge|xp(atCQ#@T^?=r>r3MC_sw{&n6H8iwq(l70hrWP&T{LDoIOyD@otsv^_Rn#wMoJ= +6A$$uU`s^cN<0l`TBCzIJLA2^k7CY_+YDx&&J1_|8%Do8}(f#C)>92NsIcVv&80?2ki=%uu5CvX3m(` +fwv~xjZVA6-AN>~zHDm_y1PgDDaPkjaOfy^=Y2HBENh=jHyb&aJsXdxB*hX`@6)b1po)6?w*(o^Xnr9 +SGjRh7V})ALy$sq96lZ#%ZGf_ISgcUFA9ywMA^IdjP^y?c2`pPku__&-JS=5k{4Qt%xtA>$=?;5OtgC)fJKk@Y0WX!zWPFK9<_?8!AOsOMTwdL6CC5*lT54hfC!;eqXaVg +ye>aCJ22b$x8Za^9c|$x>d({?(NAWJxK)S&96Et-9sXbkt7^76_5;4vYU&Bj6LA@N-c?8Sfkq#e>55Q +To*rN;8)+N508F2xZ*u1WhNIQ-k>SIKz8Za|#7ky_;Ah)Cxw46X@-)Z|K6)@tE|Tz$!1DHPkO?4JHAh +d^2Mv8p@ysa($%pE4s->tX0o>hP*hv`4GWABviLRvBi*tSTAfl5k;8FacT?uZk8Dg0t-M&&nhzi4whn +9*8KzIXlPjg&vzrLeg_bpvh3qA45$5?$ve^3}nci&8fOsZ|^2`CX*|` +q_G2V9*E3kX?}$;XSeGX38Mzrm_J>pG~Hb$4$N%C3Yslyb`-5dbrP~XMjnkIDxuLvOid&;B37yi0MhG +9*x0GyDRx;w+^W~5USic_r-xaDd$Y=)cRzD`GBPd!{KZ((A_<5SCO$iEjis_L!caJnr2MK5$N|~6o$)lwN{g?9*?&LmEK=wgNA7*l&r!Q^Xrw>574v=a)jlbBv_on|rYb7*Kj9m`XfTk=pQE^yjR2 +1t@-Ogvj4_t(vrg+b1i=_SbsdvrYtU*yXMy1|JD&khY(;Wf4dQYhJl@v$SX2HkVVTliF)fF1JtRb+XOL%|#m9Egs^42{zPy`qE28$}BWypo2%A`EDKGvCMhG-8|T +dLDC~m3yJ6jmut63kJbX9}C94sek)WHlN;+a)9H|+P8(BlfJr?pWf~4J`UaY5qSjhO6Hu_aKZ7f9Iwj +7v!FLvHh;mbOy#R=6$!e_2_WjTgPKjOZPm;W_DJp)v)ie%h|hJ;0G~c>+F9DEQgGC#q|iz+$VFbbt10bE|&r +?Yqq^sgUBBNA=L&-U_Wq_7$t3RCfOSFfzF7FC~ilTnrxW^sMh1OfdeC`)f9d;eCp%1$HT}~p;wt1Zch +Uy_bt4&9lt7mp=(AMeg?rY|vbIl)`-u^H@ysEFxD3RhkYW7-!y?E>|(4*|a(OZASH)Zf+Y`oOTOQz%bEKn*1 +JzFPdgwpYL_)BwkUi5LO398{4*KhbBVl6 +U5bo31~UD>~cc|H>a1UdazvalLE-41c^(s@$aaMHVI9Um0+p_)j_dM=;S^|Nq`??6+Z)*20IO(M5S1x +)q+B8>a9GiX;IX!XH|NmHonoMh{8U1VwzZ2cA2QghR1G;j?L_Dsg%;6ayJH1nL@lkmrI*7~L^6!R*lh +1%rs^w}|)ss~@vVe=R+ftgl*UDXI$*}J{;t%`|v4_^z&^KnfyisqR&R{-7HY+0%Yaqzy~Pw`{yP~57g +bbKY-^PLVuz|L=H)6t#!k2ejbTBoG7<)a7`dxlVR9!hx|H*s}<7Dg`<(;04V2Sm;!?GQP +^@g=EJfIUWkmt=X51J&H*%nJuE(N2AjOhlW} +<^!{`cQsl=tO3k~1H18Z9%lWSRL6T6vF(U!Ei}_o)3MFW&5ZfmVJ$jV>I2z_DHdEpL);EnVd1YxMB^l +;;(B|1i_>&$jVw0OpIht3&}-`7B-p6Za^3e3vC*Pu#mVO-5Tm>drO<5B~T_`BG%`(;Df_Wq`+nH(~5{ +8y@!x^AUzssW-JymfSNA;w5`V^MFB&=Tu41`T>rP-2R-8?;9xt@WaQ&s!-2A`^!1OExLO_z_5$Elyo- +C7_&V;FEV8^R}^%`VWUn$H7PbOarvcDj#fUZJb4{~0e>utO72d(ih%u2(mlzDG*19lyE0Lq$H!b*`yc +6l&1Ct4zWRK%p$X5v?ip^Q5B7JEugjt(&jir7l@=&))cB7tj2;tbV|3tq+p0A3qvYGWiyv^`lhxRPBk +~zG-siNlsYj3;DQen;wt@TpmF5sPa>nfjdl+$pmE|*O*qJ`a-chb2qEJMt)RulOZ~(SWsud;1EEEZSm|`yStqlpoT39oyw<84#V9i>>i%DjtG%id_`gCjVZm8 +(cv+3Ma5+WyL@5giwI@zDr_f*0Q-h%Z_TLsLtFGH{s1DCzMBc?wy5`P4CfcHdDs1D;)-~w^qgBXryUW +F{8nDcbGqAwbP&ry7!d2z@|mHmetnEFo5XIv7honM=_jRyTlSzl=&;^*qhJ~&R(^WMMFy4*!in9H}@7 +bk-?P|8{Bb$+MM132$nQSv&HW^0WqI9 +-TA_8ujcVtae%{JtL1;-zf%O6RuOTbNHHBiwp&bL=-f9QV{e)chT%R|>5;G*6Fa9)sTTVwMq}fMU)b&fd6Vy(00JtY1)p0vWEGnAD +2cX!z+GJfL9N=xr_wUsBY)o#UxhD3aCRFTKu1xT<*|EXlj>GBnHjH9*2~O$pnJ3ELgK}%$$YFAYEJ5i +G2L1CEBZ%GcY!UwOE^I{1$#6Ae&b|Zne$g^%p5Tpz_ufghFS+fHV0LppUpAuEt>5R>??MkyXU7D#`vJ +H9W}FESyZiHZ_c;)|lT_V}wSvTl(djmI15rqt&rmP2l{&>%UF74{$M~*L`cE`@nS%!U +tMEIP4YJdZXYk>M{ryeYN_8-XxgbC*iaokSMhme!q|A0bxvLIxii3RL^A+;#+wUyo+>L-3*82c^BFFN +_x<|CsfL#GgfwaZB|S)6sR9fYF)2)zb-s#)lrXB*DRcwckFI_uO(R??Ghu9y$8n}LMV>438uH&XfKqJ +?W$1%;61C$+j>bGsu#WBm1P%XwpwLPJt;WdbJ|}fLDR|OoACb6S$rFHWGLIr1_SDrc~RB>0`CC2dw0} +!NYyEX2Dp2emNAv5wW|}_;{uT+3Kn)6jg3VBbocCy=#jb2r~mgImEd|uRP$|fJB+J!_b!I2f1 +5W$|75(l^fOutlLz7e`^IlLG4cDL(yb30PdD3g4Wp>^IfPLc_C4t#>g@TyKfqocS~v>Y_5)nD;x#hh| +KDIsovM^#aMGr2sMnU$j%B86V_?rQGq7agZ|6&1lM~q=!6Nt^Nyx*G@3L_GT%2%%vt(uJDY*>zN7fN+ +%;o^d;q!J;?1r<;By~Wnc(Hlt}M;dy1cegBpX_%&OFg63;?>jCok}K$jG6DB~**wwqb0S?0uJ>J$$z>_9?D>D2`dBZKtr&|tuho9L6g%Wn)j4>&=14AW>H}8gTR|t*Ppyueozj6lV=5`TPI7zLB2V=vDI&tU7>tCn;_vjRP%*|yqo2P-H@ +r;m$yzeW%J#TZZ)HXgWzu{FYoO{Xs*?}2}<|(qEt@s{ee?9&7zLZ41bTP^+*oqaMdG4R{|{hSOrEozpb&Ikx@qE)Ru-r60tB#^g<>C!xkfC_d}<*c_>MXk_TM2aVw +x_Q-!3C#GLG+U}xWo>?JLm=L1Z3kD!As02?^jgEBoEkp`&Ku8^w +OnkcLR$(mswUF-NUAcVB6JZ03JKYsIj?pk6#)*sCqrN?g{v!`BPCf8!c$q6S3=47nG~t1QxayzR)F2_ +zO1t6S+DRji@7l*it8ICR%W|QZqq&W0GMXGlNbWDiTZPNAmL8mp_nO>qO4jbXzE5L;O<^0^kKq!XujW19^EXOzHTSc>wz8w +51Au6f82>N`LUqrI-SmULj^jBDKI*Sz^s`0B%8f&-|!kZNgiKE`7pbi!p)SKQ;*sbe#Y?Ab-|EZ`EfbGg{58(fN5U>lu2pc{o)*7x`39lo%N6#m_i&c3n2k +lVT(eI`igyosZxrtx&qN#k?y}5t8LW+1q@GEpV!j6mQ+K>*gy_q*Ig4n;x2AA#l~?MdYonX=er@YoON +h$`$JCj1JrA3z2n;t^ux=7i-p#rNuSzdQ^%;iVXL7nJzJ*SRon5xQJ7#$CW+R6{a~_h)bz!wVmGqzs!~heJFf@qZ{5aoj`oQkXX|^Rwmsc-)}#+Ws-C@OomyHTs-r_!y| +?*G9+KN|;g+I;O+63B=ZoFIrY7;pYL)J7`3l(7k1}5uuamUW7O&a=*y6rb27u8ORu@uiTSZU+R8za|9 +S-QRc`E1UKQ?JU#nec;ULW;)0Ao +l^yO;ewA*+=`ZykZyZi$({Hn%ZvAh^GGxPgp;KfYKirhSZ;5Rh@J!*wRsvwA#3 +5ILQO}lpU{XK5%1nQ!lhfG2)|0Vx}Y=yyIFXOi&P@b>unC?e}udZ9eD-H@8H|fPiKJ>GxvjAVDh+dU? +K;2QcWVa;MerFE$0M)Z--E*;E)ps(kT5ZnCZe1TOScqLGr1eX(i5rAQl<;lB;8K*zZ4bOQzYbM(mQM# +l{W{rQ*3FDA%onpP}8@_8PPyt#T|M>9|P+WG`4? +&b5&Mn&a|RwwE4`LD>=d-H-C3TTHs6UhPvdO#xShsHbx{P~ggwA4mhK!Bb^3rFe?u;>14nk*s$x0~8w +P@W%;ZJo5r{Zl426#2FbWU#-;uiDR;;=53$2HH8wFCl#Il$W%UXdpe0C`a_XF~NU6!&}#jn#ZTvRZyN +|^vB>T|P2E&go8h+x)h%jUB{2Wg6v{sQ@%{uv`|010R4`>R +YuEl;S*v*_?>{0oXiTXbsfw_oN3hjgw%Sz^DPNIk=!whxT9kFm{%iGv?QyOu|L*3}8cfFAnqBWYR1YS +AbRQH>o!*)>L|K0*U}JPvrVld%VuN{K1xS@&}E2_C9@XwK=ygluc~v;-J)swTVslxV?0?v>&40A{fjT +O(i6l%M>8Lm|ifAk<0*Z&+Mm8BVPHUB4Ut1#CHdquXp +Hoq$_TZq?2oF2+Ai(GxP)n&ep1h;{d5$27IN^W#M&c+xy?Dc=KBP9Mh +@oVwdaSoPPW`}~1S+{Vp4~O2QwEbvNxH}2Cm(`Lz+pw{;?yYwb#9#P=W*~5{Q4C*J!haKCm(o*v$4$R +F4mjF_R8jRG#zs9S>u-hhD>emj$EE$BA3N$&X^bzIR*uJA*!8PR?R4UGK;NrA~)b3Pt`o-f8IE)+!NU +j+ktZ&X!KSut52Q*Tf=whp6V%+GJSfIt3GKG@C|-4$upvDc=Ji$89cU%gM%ntF=?=jEoaItF=@Iu>E& +B5Q%_vBl0m>7&-$}?GKPnD1)}KA-IPmw6(Z@8Q)(zR7NM58UF}qNm)y2_Io6HT_+XNu|eaMvrT)_Y1& +@Nf+zI`5LyMCo^5}>Ux_^>jU7#%9`jE*};uvfl2&J?pJBa2iQAZ*p3dHm}Qv`u!v)2a%g&h6I1|?IFp +A%CVG;$6_AG~LcO1wER81uiFHB-8` +HUXcyGF=~9gSbnM0Th`1!IN%PAb^wEd>%}_p)wQWmP7?$z--s*;NVFmA9%oWAt_Y%2Y00Z`Hrj$*uZl +s_Dz->EMS_yhf=XVq?sKr3}4RJK)`X@g78SJsj0HwRy7$E$P#ewrQP4NlPz +K%oQ5-&3Y4e1(nH)IeXPxj<-p +bm|~%916_d{;U@@^Oz0e;M@Gt;dI$QV4amHP9hwD3P?temy>sV+(z9I#K171#JtWfk_*be^TDsqHoM{ +~KeY;y-~pf4!|?ejQ_D$pzViS8UY)Q|vndnZsyq;QXNN=a-CiY|!%_>4t2K#_v1vH*q*KpnM~5xrX*8 +eH@;*sEIt}08?}qWu@%;}z@Tp@z7*WS`sn4@@9H!;7Ll^>bcU0E-`l+x9S1@V0#z^>Mr0W&u|iYM7zC~&yiEq3r`fZ=Lh6L0aYn+6Z})=mweZXa1*w@GOV85d{ryMWz>^|Q_q +43$5wc51|;fxHc>Jyk-Snk_)z_HXao722&RI{>)B$YaKy&^*gdT|I!fTvOpeO{{tG`6EnRilUJhM4g} +oh_?gfR83RIS_JWSP_INOPU`xNRR{#`JPf~kOGfRLcnQmkhr~ze45`XDav(fwUR!#TbvP2ay(w3YwusvJa@$H!L4oDrAnc0EAWpHFsxKy}-C7iBR#6H?!UlD7U(&^+9{-71tlxYrOt +yiZOS^Nm+xX-L9zwg@v@t3}aiGut)9^XQ8$mwZS%znT_aCF?Sbk;A=HdgLs7f)aj%GsJ54-tF3yjww# +5}v8y!+7Rg0Zjc7XmL8$F3v+cKfNatyEfYjb5{jTd_Ftxh$(hXqoN$3ErTaG-Rx-oLqhpKk>7;tFy>i +%4&yLXZVfX@ay9p1^gf^toEczEuXCQ|A(2*e|ATPT +~&&mkc-=#K;=eCYHsPKn?E=6nN94e@^jZ@F%^7}Inr{)ooWm*WB(g)Sz;-WJf5=4izj-sHu9PKUO<&q +=3;4lch|<|kMIkc?kcK6hzFfW77(+;j2!%h*btReLh976NoM8&I4d60Y_)msS)nylKFd>)5bux0pZp> +22bI|iso3j7Y;3Cw5zmd=f78bF^VAmp +*A?K*#nUZz)3694-2uh?pMYT+lrm<^SUtXX%6Lx3^+SqFqbmDRe*{BN$i6E7)yV~`SBQYA5$y*GP?@hoq?+UcdvtGg^Ubj4l38)aGjrnfBFTI5X< +$v|QY-I3ZWDm0PtIa6c4K9;8uP7q^_42S01JF>uF!A9gH@V~KAm+6&rl@Mfk(GABUDM=N>iQ0kr8YzE +R?fq0>2Ti5sA^|iJ%0hL!ADRzv9bUbs<)zve*+;MPOwtYH*I+zLlHiwm1VJyh8!51_{}$!}{#sWR=*K{16d;gSK~X6_0V^G(Ck9$6<<=>Nvm%aLwp5 +yL^0bAp%66HvVN88!b@4-)_dM9H++`+)k&s5Am62V3TzJF-Up9i^e<#SU{iyNBB}Igze +vOtL&g$+R_+MP}OokI!J1>QR_XGKc17c1Q3UOu9R1C=JAiN72 +^2~=$Nhh2rpic=~oE-{20y0fWAf#@SoS~YMvK2nAN94yOvi=^2xdAbyx$wFa*?HrHP26~L!aD=KbTEj +Wmx#?=X7IeY$JriJ9z2OYFXa09w1)|}(m3otEE?rV@JVUaOc!r_gr(k;uS?9G)31RHbiF-M4+fBQEZ( +&2*?CDvU`mxC5~h{-ulnif(^n&mr~mc0ssCDO8xnuSUM2O>TTwK`Fm$cOFH-8|PhOt-xSf)|ii&(vS^ +b~el-|~GMX3Tllqz4Jzs84V{mv~9KYz(8XvUKBZXPFX-@Fb+#5}LJayS}p49YiA6ku%;EX1(YE{h(QMD +Phpo~&lr56U1(@BqtXF*IVWh^3v3dE>e&!+xw9kMNKWgB3lYRt6lfCq8)#ni-*ysNSgq*LP5{3&Xm(> +HZ`0DXEs!@UK?QvRc>BFv&;XWbR*#B)kwXd603=oY}yajhr8Sfo$QFE@V8xNz2+y41}fEbRm4;l +erGv7GwS#L1$U5ij!zOP@+&85vy(IS+JQsP|j)V0kTznD+_J`?(xBEW7=I2NKAdnI^`35`$=Rk9WbGK +TTtav__kMjhfXiC%LaOAbmwVbj&jba85f%6kW&Xs^pTYL&Gd#ahc29^&BHBr20S!D!-^Z1)MmW>m>v_ +k6(jcTu1%>1F$sxZX{I1Hak7p#kBaT>Z|QSQz3NnDw9x +ye`(S_^N>{mC-wMiTl)vZe3q^18TE_WiQ3E-lr&S(c?VMm)M%3KSK +7#Rwp+Hd@5FAuH^Kv;iZ0}Hs=Pw`?d)$XNHhe69{`9_PrwtEJsC^ggm;xAAz#%jpvXA;MgWWs)s9~&n +@+T+fwRJ~?XBTT-aGt0AV=?t{E%)BoXe!b7j#CnBCf!XKBk+ZFs*UTz_D#U)VU&Mk?2w?dyXT?#c%1D +f}bF*zsAPfoPdnyQ8~MRScSMhPZO=(9S<{+4f2t$VeHH;w!jD9W?&WaL4-Yu%zG3|6a=;YN{{1x(pPt +Fdg;ZgCT=dB2ZWSTm~GoWDYnOgKfwuBy71=tfHV4wAatjBH0L1!G5#0FGLqhxT+{fq6Pnx^U(7cN&Dt=H;qHi0YGcH{BeKxr@c(Jw6X6s&4A|Ym3gby8X;<{tN1MX!e~*JS(cV&O#^*&Y +{c8#>*uPXLVdjX6?E*b#hcmo0UYhg%IX>!&kCT?34wi-o$ZFxbh&dgNdXzXqZ-9+MmaB!t%1r)SpYOD +$)C{L@b*U#NX?gw9CKNSz>AW^Bm6Pfb7t!Un9(?;C}|geab=Xj*C$RnD5LQ|b-FIywa<^lcF9WOQ@*Gs=yXPy5@H$Aj`Yn +1pAWn1J`})!677uxDNgsun0Hfy-7B(4N$H7w9G+8#V*sb58{mqLDTN(~~4*x8woMa(xN!Nh^Z(N$w#< +N)7GP3^$57j@9qNi9_-!)4i=``PgEVuS~@TbToUq#97^FK`PzNf@MB^3FqXQwu9p;b6^gDeBK7rfQOM +E9`x`2_u#XQrUTna*-+}E?K)`u5uPEFvmL+ycalfaZg>H)Gbz=|RCMeFz&fYLmQFhGoYSMzm<1xIayF +<)r#ktbOG|F#`#h}+7-tL +6{Wb7WyF=APK@{@%?+M-@_j&0nf$o^Nl-&^|#J_yWQ7{1jxa?*1pg8<)?uz&9^bq80Wghb}6W&sGzo;&HBL8g +!JN4!bV&x&Nw#x0WKMqW#pmL*JwtQJoD#yV*iciBHrVa(j%6Gg2O&-;=C$t4x*$MP2D?<57h1j1yb|^cZ%}hWl) +@HJ+YB)qz4L;i52iztPjJm{ktt){YG8s*sl_K(Y2JL8<6b}G`E9re_z;UBFLj3?AHkBl7rJq*Uu5dPH +q>G|p7RIvs0gj%L~jKjWJ`9hZfn^9uK=D5NRFaX8*TgE7}3N-&8F=nif^MkR|^~r%8ATg(|RjJWA^B3 +H~z?ttD^f;a5yc>HKe%1ooQPfz*NrF=5b;qfJ!+D1~G>h2>5FMP7m8rZn3UGBjdIB!T;o>vZP>iKiwO +83@DLm1)^zo-opnaGE`dvwW7Kwz9OqYrRND1*JJMj7gG1dty`G!=7QM(Rx>{}fp;Jwm6!4qAH=?O3Fr +@{DeUa3AVLQDrj{PZ|Np-R8L}9wRjo?{jsE~%nN!^z7nsQ2hsTcp4mxB)MWSRXJ0r%X@9ll`OL>Om5C%WJ_`wVAlNq|6+ +w0cnOQTtNF>Mk=U_nCr%dT8(@Fw!5$fSb+(&wP>tX8grivlpv$K6t@r_*(ZL4KUj+p8Xy`Z?!Tp!Y0G +`);!#JJoAl)@r2%_;F07bWWWdz>huFtI$>LeE1S#wfocP0I-iku@2wbHUql*xJblx@c!si;^NPi!v+v +!??ca{^u|+4szaRPwq6pLkki5`vIqas+}vZh;5XSv1()u!?S7*>$x(f*?5SrnkDz}+d4&AXG%YlL;z) +$P6VV0ZahilP=Tad=ulzWvevDj{->LJppA6D9mF}hs(Pnjr`Ykathz(!0Vi=KVCyZ{s@{`3Ss4>uNn$ +`x#HZQG|cB^4iw|~_6uG5lh03Ip1uu-Ou=tk&3den_JtXu8e2QMeF@@j(Kc=nSI`Y8K4o(v%yyUN-R` +z;v-lUxSsQCEvb-}%T_b*kMLFa22IYpzkLny;n(G%LiXa$WPkd1V!^Gs##^XF<>0Sg=}W1!LY2wbK2O0;4}u*Nz55*KpNFvu%fp=e6M%UKpVx5 +;@XJT1J`I)pBLf%OjjG7O>Dn~&|$XD8bpgbaox_+1GJ-tkv3^Kn-Xl+0n{3+x +dclv90r$-ExT?(c$N(kupO@hguMCsIox6$P@P2#{l}*2wL2D0BV|=*O*uGN(ZYZ=N+<#1HZG9V;6c$B +@Il2>{(qo-^C*Nw8<+Mf1jmC_as|-YN-@7(*Ggoa&rR3hPM(RaLC0sh8h}&**wp;_;J}`&p5(z%;x{#UATLhp-_q +7VfV+5G5O(0C{{*;2HOp2sr9XJ1lq?c`3dIw7tt`bq)Y4<~sDLXT-VP0^3`E6;;wY#JPnie@W!UsY%9 +$KM743H0G_E#RQypgb$mCyVJOX)ri-1F>w)|Hyaogy=L3bDa_m%gUjC*fo4j;HcXt=G%6y +=Loxb-(!QqxH@}VO)q@RLWgO9CJ$M7uA2R9n+C*Y7L1tJCDcM1?MwpK7Lbo@7xb4jEny5EJo@T8m@||Ktj;m#pSy&P&sPH*SzKY%PDU>-|1 +eDTTi!|tnYCoInGH~wq)@LBFtQP50#rm&i3o|kyW{laCjxV@~`#oOVSHm_fDV +#(_R4sGFfDG8mHeDbl@aaf%v&n7W&&Cx4cLxx`Kfc#d|4;r4Zu2jR2xWE6znYjv(Qd8dlSVSX%&d9MC +%83z5WY{nV%uB$g59Fywi*+#i9JE%VWiX9_Kr2h-Q-?7hvCTx_T8Q;p*V +qlRtpxwb0!E$+611C%%_g_^I)rYuN{juvY?Feq>2l$#fIdBNZET&g!tc)!w(11m%%I#34pVi?)t2>=o +&!6F=wBQLyP%;uqsREK8sV%`+aI1}pzt^Mj}PrDq!N1UsLP`&?J<=f6AHqK|s*w&+3bi<8-mn#3rJQ} +vhb(-~2^JeUWyfI~ThnCuA`zm=#EwpCbZz`%WU8Q{b6RfgIJ@EtNvud|au+gvv8!@iyT)B66kZgDNit +41)1w_M;Zw!cZW^;ET&6nn1ma-faxx-CE)2HE}<-Yx)!@}oApO+temelkE0r +Cipg5***5hx4~63;*NcrI|jaN09e#o(j%iabNLZavth|`es6_Te&s+x3JD-S!~kt^aM81QDc{8@34-q +Zj5T|cBWax?z!t$N!mP^PG-Gm49?*>Fb;r+H8p>`Xm|I*Kbw-=!@q(>uP!Y`60kQutVi@$MQImT-~>R +!k=!kv9wlH5)r26I+tHY~m2!#4f5^pzkye1s?A)UN#IwSwsLsvx-!KEhc=6)VL^j%8 +#m-nXEQWSl7i@e=+5YYNV&yIP1h +MvtkrLKi}r`#79Lz;ag`(hpkTliDCZ4ob7R3ep{YvS+=8Zt4c;vC{=7or(-u&cJxS&f{Ju@tK^6C7Ca +H2@UKWRKghDZdd}x+Pd+O>h~em%RTIdhAwwA3Ef0xsXsgth7yHe;$!v!a+rVCUOM4-l@%k&A|6myFoJ +Sl`<_8v29F(Pp~A&*?+3Dv&!cnlbw119FqpeaGXSwXgWw1Vd#d?05J?c4BWaN_sXu^YA+JXDh+O_{oV +tvLkItE7Aix`V~26xBC#OYwDx{P-POQCL)hD(aib455Di|%mb$vNmTCsG<}}M{41)7|=G+FZCZ)O;s@ +mi(7S|#&|@<{oOJJ!h +`{R0Av+0}krtF{Uzri6g!<-h>53kH0sNhno7*j|rb*5f-n9vjkhD9m8KqXO8{lGUhyw +Rv@{d0P?=!_OH!4jfMa8eIpKsaDA6K9T>DWrI#fJ<@%&U%iIchI3&9{xRO;*(_-2QFmd`Aqgo2y3OwF +1Ep%)H)Z`1%+@7^z)_HnV=wzfZ1kOYgxEOGPFG2{krK%RMw;_#g}z={=E+V8Wj#j{8Q%bDda7gv>mw^ +O+-yl`KgkxsE@#-S((k`Rp=F^o%8=W3%)*Q9LZz6rxYPj4>X)H1PiWhOpZy}6z!%VWD68mntwpOoA|k +~+=PN~gWvW2zY`y=EMs8rtA?VeGPB5Fc|O;ZJ%RO`lw*IjfzsS8g=wUt$&P7lbS76A<1Xx8J?6QpQpG +r>zvZZgf*MU5v{U-+9`9^n--`ed8vm{j_OkMvQEIF;Smv&Bdli<8Rz{u~#;^+e^W%Am{7GQG5HEi1T% +|Mtk?QDAw+~%I?6ZpPZ(fO~2!chrhkLy4HuW{`2BRqJ)-jVYlpH>Yckm=IlU?lufOt9v*MlE@S4hLX+ +m>3wcVphX>s9lXVVN+dt+uqzDa>Y?Xqs^Vzmt&$GJvmr5Mtv7MB>pdYjnPJ0U*_+%)=dm&oI68iTaCP +WETTPCO38{Hy-#IqY#;w&pxUGe7jmNe;0=&n;2?K@Fob_bI=;Wb(rCoZ7dtf~AbUTu`$_50KTMD;Z#B +6ZoWn+!4CCX@)_Xu6Ic^v_0G*<46|*d!e^2o6=fB+tb4E<5o=Uc+}HI}HbN1DSYntO8S(vR-MMyNR^m +_qzKMJML`L7AgJJ$R>ef)oEW7UJEAgPxvy<7KL;)rKMp`O<=Z5C@5Sn5rhBbT;X+KFF)AET +x!%&z%{OjSC^R-&=Wl-2Zq{~yy9_<+LQ0RR +""") diff --git a/scapy/libs/ethertypes.py b/scapy/libs/ethertypes.py index baa405cc03a..6ce6850294b 100644 --- a/scapy/libs/ethertypes.py +++ b/scapy/libs/ethertypes.py @@ -1,5 +1,6 @@ +# SPDX-License-Identifier: BSD-3-Clause # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information """ /* @@ -34,105 +35,62 @@ */ """ -# This file contains data automatically generated using -# scapy/tools/generate_ethertypes.py -# based on OpenBSD public source. +# To quote Python's get-pip: -DATA = b""" -# -# Ethernet frame types -# This file describes some of the various Ethernet -# protocol types that are used on Ethernet networks. -# -# This list could be found on: -# http://www.iana.org/assignments/ethernet-numbers -# http://www.iana.org/assignments/ieee-802-numbers +# Hi There! # -# ... #Comment -# -8023 0004 # IEEE 802.3 packet -PUP 0200 # Xerox PUP protocol - see 0A00 -PUPAT 0200 # PUP Address Translation - see 0A01 -NS 0600 # XNS -NSAT 0601 # XNS Address Translation (3Mb only) -DLOG1 0660 # DLOG (?) -DLOG2 0661 # DLOG (?) -IPv4 0800 # IP protocol -X75 0801 # X.75 Internet -NBS 0802 # NBS Internet -ECMA 0803 # ECMA Internet -CHAOS 0804 # CHAOSnet -X25 0805 # X.25 Level 3 -ARP 0806 # Address resolution protocol -FRARP 0808 # Frame Relay ARP (RFC1701) -VINES 0BAD # Banyan VINES -TRAIL 1000 # Trailer packet -DCA 1234 # DCA - Multicast -VALID 1600 # VALID system protocol -RCL 1995 # Datapoint Corporation (RCL lan protocol) -NBPCC 3C04 # 3Com NBP Connect complete not registered -NBPDG 3C07 # 3Com NBP Datagram (like XNS IDP) not registered -PCS 4242 # PCS Basic Block Protocol -IMLBL 4C42 # Information Modes Little Big LAN -MOPDL 6001 # DEC MOP dump/load -MOPRC 6002 # DEC MOP remote console -LAT 6004 # DEC LAT -SCA 6007 # DEC LAVC, SCA -AMBER 6008 # DEC AMBER -RAWFR 6559 # Raw Frame Relay (RFC1701) -UBDL 7000 # Ungermann-Bass download -UBNIU 7001 # Ungermann-Bass NIUs -UBNMC 7003 # Ungermann-Bass ??? (NMC to/from UB Bridge) -UBBST 7005 # Ungermann-Bass Bridge Spanning Tree -OS9 7007 # OS/9 Microware -RACAL 7030 # Racal-Interlan -HP 8005 # HP Probe -TIGAN 802F # Tigan, Inc. -DECAM 8048 # DEC Availability Manager for Distributed Systems DECamds (but someone at DEC says not) -VEXP 805B # Stanford V Kernel exp. -VPROD 805C # Stanford V Kernel prod. -ES 805D # Evans & Sutherland -VEECO 8067 # Veeco Integrated Auto. -ATT 8069 # AT&T -MATRA 807A # Matra -DDE 807B # Dansk Data Elektronik -MERIT 807C # Merit Internodal (or Univ of Michigan?) -ATALK 809B # AppleTalk -PACER 80C6 # Pacer Software -SNA 80D5 # IBM SNA Services over Ethernet -RETIX 80F2 # Retix -AARP 80F3 # AppleTalk AARP -VLAN 8100 # IEEE 802.1Q VLAN tagging (XXX conflicts) -BOFL 8102 # Wellfleet; BOFL (Breath OF Life) pkts [every 5-10 secs.] -HAYES 8130 # Hayes Microcomputers (XXX which?) -VGLAB 8131 # VG Laboratory Systems -IPX 8137 # Novell (old) NetWare IPX (ECONFIG E option) -MUMPS 813F # M/MUMPS data sharing -FLIP 8146 # Vrije Universiteit (NL) FLIP (Fast Local Internet Protocol) -NCD 8149 # Network Computing Devices -ALPHA 814A # Alpha Micro -SNMP 814C # SNMP over Ethernet (see RFC1089) -XTP 817D # Protocol Engines XTP -SGITW 817E # SGI/Time Warner prop. -STP 8181 # Scheduled Transfer STP, HIPPI-ST -IPv6 86DD # IP protocol version 6 -RDP 8739 # Control Technology Inc. RDP Without IP -MICP 873A # Control Technology Inc. Mcast Industrial Ctrl Proto. -IPAS 876C # IP Autonomous Systems (RFC1701) -SLOW 8809 # 803.3ad slow protocols (LACP/Marker) -PPP 880B # PPP (obsolete by PPPOE) -MPLS 8847 # MPLS Unicast -AXIS 8856 # Axis Communications AB proprietary bootstrap/config -PPPOE 8864 # PPP Over Ethernet Session Stage -PAE 888E # 802.1X Port Access Entity -AOE 88A2 # ATA over Ethernet -QINQ 88A8 # 802.1ad VLAN stacking -LLDP 88CC # Link Layer Discovery Protocol -PBB 88E7 # 802.1Q Provider Backbone Bridging -XNSSM 9001 # 3Com (Formerly Bridge Communications), XNS Systems Management -TCPSM 9002 # 3Com (Formerly Bridge Communications), TCP/IP Systems Management -DEBNI AAAA # DECNET? Used by VAX 6220 DEBNI -SONIX FAF5 # Sonix Arpeggio -VITAL FF00 # BBN VITAL-LanBridge cache wakeups -MAX FFFF # Maximum valid ethernet type, reserved -""" +# You may be wondering what this giant blob of binary data here is, you might +# even be worried that we're up to something nefarious (good for you for being +# paranoid!). This is a base85 encoding of a zip file, this zip file contains +# a version of '/etc/ethertypes', generated from OpenBSD's own copy, so that +# we are able to use it when not available on your OS. + +# This file is automatically generated using +# scapy/tools/generate_ethertypes.py + +import gzip +from base64 import b85decode + +def _d(x: str) -> str: + return gzip.decompress( + b85decode(x.replace("\n", "")) + ).decode() + + +DATA = _d(""" +ABzY8N|hyM0{^91ZExbZ7XI#Eaioz}AWb2>6zJa3jGPeKXcNeglyY@-KbXWoE+IxqXv@Ffm=n6^CHTV6)&I=xJ;}8aq!6UL>!9?$pv +`GMJXbYp80SsD}m)4js=fFWN&Z9pC^&;iWd2T;Oc#8Qj`#hV;aMX!&)3O3HkNH4X`cC!>{f3)6-KcVH +s4Z)J<;u$39a{5P2SVhR?&?j@145>yMs!3G@=R9R6kif=#Vs(Z_r%4vh)K<>Hq+ +<<{$+8p6phA&wP968%zdMFDXfV{VP|&lZX{1Sy0z`Z)r!Lrsw6wsVMpW? +HK2ltJ-jLqx0sNmFysrtOQHs2a&&|tz=2rn|GRIdZ)S?i&94$))<;o{#?SHIG~#@{`Oxho^)8Z*Xts+ +>08!2bkEWTZVwAL^809Umhnh-p#34`C5KFu7+M?bN<8PW&e(6(>3vI +0^nSOmOLV#1WJMBznTlw4ISAr-uKC_)eM`&ZWNVS{&wla*c6)G>vc$%3CL3_+lz20L{GM;1_te<703i +?`_lI^WSS$(VmP*k51VPUC0-X?v44uu1t2PkH(*J-3Atb2f5W%>Y0p*g=mT&CA#?gLQG +nOiHyYraJt+m~t@zxV%GtwEUqJ4&4M%5Y*%gLH0kL?>Di_?FQ|Df#>3p6Bv4y1YER~}7d5RxDen3MKl +%l=PF){8TwVCUY`Z+8}O1S7f+~F(R&tk6?D(gdM{$> +Rn<4K#*sUiR>aI8mom)CpaNUWnS0o#jeZ};RS_A`+dJ44vVVpi_s9>i;mNIOV_R?23er;;32udbPPYetAO)8EQ`17Gf7XE +xTR#~jS#DYC0ZV@}Ed*NEwwe3d~neYn)M>#>D8)Mv#ZOs&hfi8v?oJXQkPgv{a;n8C$T7-sS&5nVt64 +3CMka!ezgMt}S4aQ?-&d7Ld*IqOCeMV{6fJ^!pV>J`AX&IdMSxL9KXbeelAWJWK~Z;XWKC==mrL15*J%=!MU$A +biCg29HoDTYBRLpzO|C_&2yd7U9S8d +x8u6XzCe5C^HBn#8;J{Mv?fHQZ~T0kKTOV#{)L7MZw?8Zw=}F6I|`@;_cB9iCQFa!km^)NMjV)0p5O0 +It9Wbs6j~N)eT^HLjgRUss%_=PMf&%F;P9u*ST~6hdA9j;chuibd1ImYp4qN$S!Ng6a)JBa`D>F0hdWe=uaEi`5|7^7xoyzQiLf)I5b|i7iBxP(mEZtL^N^HVfqK +C4iRV~;jg|flR!@$t_-A~S5(GomD)aR0p%!kR2I@NomVW!P0cT<_uPI-J%$u+d+}VRdhdoI{H!^yy9* +dz!#na_nk$k`1Y>R9-RYf3VA$lCJ?Ts%S*%w&BF4{>)YAMz+=w +*xr_asB;yN6Dpn6q|b=dIZJd|iQBr*L^VHN1f55%`jsx>}L6 +}0SC;Q_b9$A{i@cIQy_4UqIdGU$?!ejC~XT@GkQ5paM +""") diff --git a/scapy/libs/extcap.py b/scapy/libs/extcap.py new file mode 100644 index 00000000000..f424b630163 --- /dev/null +++ b/scapy/libs/extcap.py @@ -0,0 +1,258 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Wireshark extcap API utils +https://www.wireshark.org/docs/wsdg_html_chunked/ChCaptureExtcap.html +""" + +import collections +import functools +import pathlib +import re +import subprocess + +from scapy.config import conf +from scapy.consts import WINDOWS +from scapy.data import MTU +from scapy.error import warning +from scapy.interfaces import ( + network_name, + resolve_iface, + InterfaceProvider, + NetworkInterface, +) +from scapy.packet import Packet +from scapy.supersocket import SuperSocket +from scapy.utils import PcapReader, _create_fifo, _open_fifo + +# Typing +from typing import ( + cast, + Any, + Dict, + List, + NoReturn, + Optional, + Tuple, + Type, + Union, +) + + +def _extcap_call(prog: str, + args: List[str], + format: Dict[str, List[str]], + ) -> Dict[str, List[Tuple[str, ...]]]: + """ + Function used to call a program using the extcap format, + then parse the results + """ + p = subprocess.Popen( + [prog] + args, + # On Windows, we must be in the Wireshark/ folder. + cwd=pathlib.Path(prog).parent.parent, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True + ) + data, err = p.communicate() + if p.returncode != 0: + raise OSError("%s returned with error code %s: %s" % (prog, p.returncode, err)) + res = collections.defaultdict(list) + for ifa in data.split("\n"): + ifa = ifa.strip() + for keyword, values in format.items(): + if not ifa.startswith(keyword): + continue + + def _match(val: str, ifa: str) -> str: + m = re.search(r"{%s=([^}]*)}" % val, ifa) + if m: + return m.group(1) + return "" + res[keyword].append( + tuple( + [_match(val, ifa) for val in values] + ) + ) + break + return cast(Dict[str, List[Tuple[str, ...]]], res) + + +class _ExtcapNetworkInterface(NetworkInterface): + """ + Extcap NetworkInterface + """ + + def get_extcap_config(self) -> Dict[str, Tuple[str, ...]]: + """ + Return a list of available configuration options on an extcap interface + """ + return _extcap_call( + self.provider.cmdprog, # type: ignore + ["--extcap-interface", self.network_name, "--extcap-config"], + { + "arg": ["number", "call", "display", "default", "required"], + "value": ["arg", "value", "display", "default"], + }, + ) + + def get_extcap_cmd(self, **kwarg: Dict[str, str]) -> List[str]: + """ + Return the extcap command line options + """ + cmds = [] + for x in self.get_extcap_config()["arg"]: + key = x[1].strip("-").replace("-", "_") + if key in kwarg: + # Apply argument + cmds += [x[1], str(kwarg[key])] + else: + # Apply default + if x[4] == "true": # required + raise ValueError( + "Missing required argument: '%s' on iface %s." % ( + key, + self.network_name, + ) + ) + elif not x[3] or x[3] == "false": # no default (or false) + continue + if x[3] == "true": + cmds += [x[1]] + else: + cmds += [x[1], x[3]] + return cmds + + +class _ExtcapSocket(SuperSocket): + """ + Read packets at layer 2 using an extcap command + """ + + nonblocking_socket = True + + @staticmethod + def select(sockets: List[SuperSocket], + remain: Optional[float] = None) -> List[SuperSocket]: + return sockets + + def __init__(self, *_: Any, **kwarg: Any) -> None: + cmdprog = kwarg.pop("cmdprog") + iface = kwarg.pop("iface", None) + if iface is None: + raise NameError("Must select an interface for a extcap socket !") + iface = resolve_iface(iface) + if not isinstance(iface, _ExtcapNetworkInterface): + raise ValueError("Interface should be an _ExtcapNetworkInterface") + args = iface.get_extcap_cmd(**kwarg) + iface = network_name(iface) + self.outs = None # extcap sockets can't write + # open fifo + fifo, fd = _create_fifo() + args = ["--extcap-interface", iface, "--capture", "--fifo", fifo] + args + self.proc = subprocess.Popen( + [cmdprog] + args, + ) + self.fd = _open_fifo(fd) + self.reader = PcapReader(self.fd) # type: ignore + self.ins = self.reader # type: ignore + + def recv(self, x: int = MTU, **kwargs: Any) -> Packet: + return self.reader.recv(x, **kwargs) + + def close(self) -> None: + self.proc.kill() + self.proc.wait(timeout=2) + SuperSocket.close(self) + self.fd.close() + + +class _ExtcapInterfaceProvider(InterfaceProvider): + """ + Interface provider made to hook on a extcap binary + """ + + headers = ("Index", "Name", "Address") + header_sort = 1 + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.cmdprog = kwargs.pop("cmdprog") + super(_ExtcapInterfaceProvider, self).__init__(*args, **kwargs) + + def load(self) -> Dict[str, NetworkInterface]: + data: Dict[str, NetworkInterface] = {} + try: + interfaces = _extcap_call( + self.cmdprog, + ["--extcap-interfaces"], + {"interface": ["value", "display"]}, + )["interface"] + except OSError as ex: + warning( + "extcap %s failed to load: %s", + self.name, + str(ex).strip().split("\n")[-1] + ) + return {} + for netw_name, name in interfaces: + _index = re.search(r".*(\d+)", name) + if _index: + index = int(_index.group(1)) + 100 + else: + index = 100 + if_data = { + "name": name, + "network_name": netw_name, + "description": name, + "index": index, + } + data[netw_name] = _ExtcapNetworkInterface(self, if_data) + return data + + def _l2listen(self, _: Any) -> Type[SuperSocket]: + return functools.partial(_ExtcapSocket, cmdprog=self.cmdprog) # type: ignore + + def _l3socket(self, *_: Any) -> NoReturn: + raise ValueError("Only sniffing is available for an extcap provider !") + + _l2socket = _l3socket # type: ignore + + def _is_valid(self, dev: NetworkInterface) -> bool: + return True + + def _format(self, + dev: NetworkInterface, + **kwargs: Any + ) -> Tuple[Union[str, List[str]], ...]: + """Returns a tuple of the elements used by show()""" + return (str(dev.index), dev.name, dev.network_name) + + +def load_extcap() -> None: + """ + Load extcap folder from wireshark and populate Scapy's providers. + + Additional interfaces should appear in conf.ifaces. + """ + if WINDOWS: + pattern = re.compile(r"^[^.]+(?:\.bat|\.exe)?$") + else: + pattern = re.compile(r"^[^.]+(?:\.sh)?$") + for fld in conf.prog.extcap_folders: + root = pathlib.Path(fld) + for _cmdprog in root.glob("*"): + if not _cmdprog.is_file() or not pattern.match(_cmdprog.name): + continue + cmdprog = str((root / _cmdprog).absolute()) + # success + provname = pathlib.Path(cmdprog).name.rsplit(".", 1)[0] + + class _prov(_ExtcapInterfaceProvider): + name = provname + + conf.ifaces.register_provider( + functools.partial(_prov, cmdprog=cmdprog) # type: ignore + ) diff --git a/scapy/libs/manuf.py b/scapy/libs/manuf.py new file mode 100644 index 00000000000..b3a4b1bed82 --- /dev/null +++ b/scapy/libs/manuf.py @@ -0,0 +1,11418 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +# manuf - Ethernet vendor codes, and well-known MAC addresses +# +# Laurent Deniel +# +# Wireshark - Network traffic analyzer +# By Gerald Combs +# Copyright 1998 Gerald Combs +# +# The data below has been assembled from the following sources: +# +# The IEEE public OUI listings available from: +# +# +# +# +# +# +# Michael Patton's "Ethernet Codes Master Page" available from: +# +# Many people contributed to Michael's list. See the "Acknowledgements" +# section on that page for a complete list. +# +# This is Wireshark 'manuf' file, which started out as a subset of Michael +# Patton's list and grew from there. The Wireshark list and Michael's list +# were merged in 2016. +# +# In the event of data set collisions the Wireshark entries have been given +# precedence, followed by Michael Patton's, followed by the IEEE. +# +# This file was generated. Its canonical location is +# https://www.wireshark.org/download/automated/data/manuf +""" + +# To quote Python's get-pip: + +# Hi There! +# +# You may be wondering what this giant blob of binary data here is, you might +# even be worried that we're up to something nefarious (good for you for being +# paranoid!). This is a base85 encoding of a zip file, this zip file contains +# a copy of Wireshark's 'manuf' file, so that we are able to use it when not +# available on your OS. + +# This file is automatically generated using +# scapy/tools/generate_manuf.py + +import gzip +from base64 import b85decode + + +def _d(x: str) -> str: + return gzip.decompress( + b85decode(x.replace("\n", "")) + ).decode() + + +DATA = _d(""" +ABzY8x|Sto0{^7F+hXH5lJ|LC`xLmUuX?uZE*~Uk?$uHfWvgu2E>n4`tA!<4LYpGhJUFdOWY!Lt +gj36m@dcNuDnfkc^@q|MFh=0T%fBZjx!2b&TpI`nUfBEI^=}9lOOw+yis*3#eckx(P!u)IekI<#q=7k +c=e7nuF|I61tX@1Yv<1o&PU0%paHx*9bTjA`z70%yVVesAx!}nIWcyEOd@2&9hy%nOS!rivWvxTgbF0 +}*UNyMpIR^|_SS(+a?#>%6n@?2Jhe&x_}xp7lttjlH2A=#Ie+LphIS+>L)lcvU0Kl*!Ma8s?q9mS+{{ +V!dWHHRC$)s|NjwpO6ld>?`%mftDm);WjU}s!%CBR{;N%s>BMUE6lPdQ_8-v1qHEC +%T1Q2`Fh`agcH&)#&WA(8DODv`keMdzB0gaqzg=BC3m^bDh}OmdP2Ivq`uFy>R!K^$21^-ghT&r!)qd +a;jWO&R58F(qyv0WkLDMOalYNv2*Q&4ht7SGz7XwpsaC7WK9gGnVF?=0EHl55*}5{t+N5JdbJ7T-)r- +uQrutkji(E|9i(WS0azsD=i%L@lyh)M1Ff#uVKe2`8gX`wUC-hT4ruN%$pdBb&hxCbsN=scLQeA_XC{nE$cSNQ+iIy<%>JY14=q +0hRkrGJ?oXz!snO1e(;_!wRTK=V>U%O&?py3;t%GV{`+l7NWtlbwvseb9 +J#aot(oi#m7D){UTn=Y%Z4@ovFftK-#yO))}8;pgBCKGOrPYrNWGrJ&o&(^pj)M*nkjZQH-o2ng${(Hvek0VNY5FUrTIu@0Qh9s)wLKS~ZgHx$uExp#v)fu0(q +uV^#qm@zxG0}wo~BQ_+Tv2IfC&`LnSc1Ej^p7JTv<-TNqrDzY)ra3Gt23TxLttWGX{ss`(&7S|!nzxt +kg#_TGNr#v%6^HJdJEz@1Rc0QV%S^772jOU|jM+juU$_|Q8Fu>j6N>{f&0u*tQAqQ1sWWpL=($7E6{e +;=M#ge4&AYT9kd&VZ`8;?|`odkRUsP3TV|G>n1kgrA#t?1h_;b=BZp{lew!|zmb031RRKrW*1N=GZ6N +%E#dAuv(TR9MpdY+IOX68on${BSYf>Gy2Fswxlvej~&uatS$=>k9?(V3pz>4-Lr6fBVd!qQ#%W~8s&# +x3Z17}8HBOU&F?PrZ3D-K_#M&(3w-FMxg!QxkaV8Jd{w1asxbG{fWJG96W3tCetN6R(o6whLJ$y +` +|y4RzeNaOdJSLSdw&@**Lnrokh3NMC}}ymSxZ^!gjK#V1U9}bi%34Jf3fJXem`8R_eRlTYC(lWu0Ly(t}2InqFmY*>6EyWrFSk_$djilAbgIE6&gT4$u-14|#58 +G3dQmk94eW`E$OO-{nNFbpQcWnJ@On>Uucnn0uEBj6^i|EZN`aR3#m4Ayd;UOAdXj$|3D=`BU{@589g +g7Q}xEKwg1%wY*f@yw1!#lQj@*C9Lt0E$d8&T)7YEq4fdDLSY!S#x%Q6k(IxY7nqf1E@l0BIHJarHEk +}P(d$55daD=9(@s)3wQ?dY0v9GjHXI2 +Vu!SK32J4GpZ_f8(At2Jx@X!yIG7>a?7FhH9ofcZnfbsg9dM}(Nv1)woWFoXNp)S*V0&hl!%Q!wcUI09FK7>GR=lif3@PS%kwfPL0su*B7YJ15H*RTkrS$r +7MCD>YSR8jrl(ES1*NVm8$bCYQ|4A^-0f%Jw^kuT*+xq;k6`wN7X-pv?)fldW-5BrY{dPb}d5#RRQ&pvQdtErLcZQ!W1??yEhqa6YivWWU3`$pG}(*hX0TnAhgZn +~OCNF3slP4^wLH?yHURkA>jNef*=fu6EX716YnuBU#ZL?;vy=&c{W>}=s#D>iK5(Nj9o9X^u2Gcn^-JRdh83}HF +C#Z}~G2}isfdfqSTDf8-;0c#5%N&k3QhL#G@wA*GZ{5a}86`QeBF}A*-`@$c`)c8xcbDhFY;@E;PbOh +LJG+?y}U2HGw(iPASd~uzv``$y$K2EARB*#&B5mP$LDP!V)Q);j(9=_oquEC=j%U04pd&AawtCf24M$jtFs7}fxq&(yMGq5+-Js!IFWKS1(ug +GGn`(Q7kl)xN!Z^VOO%bq%gcY{?YbZV}jRpB-lJ+Q4t`W9n4xt66gKWf+W2+i1M+A~&~GLz&{6@`S=egL- +iCs!NMO|)$FAXnbXH*jPK8-MHYcSL8W)R^jSSJ;C)FUF#$eDmbRE(93#ErLgM?@Vva=UH;-v5-fkZ^b +LUm9Ktsa=>>?kLjo}YIy@Znq_IVPjUhRR^RrMo7+8Rw>oT1>2uf;dQc9IED2l)U>!P9&Cr% +mCy76swS~^ziL2iDwM>TZ-%uZ{zTYDa6tvv+xPKX&h{)4(}fP+S6w~`RZ +#!~wej!%Ae+hN@AFLTc-?{_$##vh#lP(j71(b)@i@u;cI{KAE&og|WLsONP&AP$B!OuxLNxZGou*vPA=Q`1QpLx +{UIZHNj`5dPXjx4ngA+YM0nzdOO)@UBuU4A@aYbUK%FxZybgK*>ybICes9P{Ps%6H)g=qx;*(&n{SrC +-4~5V)SYoE0oI&I&C{bwWDLe +O)LV_$;_Z7kz+1YqD5px%m_`3cX%hlOerKw%pw4Oa%~@uJUx%7$6fm$H1XMURRrOo;F~-8Gp<<*!<<3cueOfOg54p34+^#et7_@hM +%1s3Q2}0q|uRyTlpav|Za*G1mmVyI|OuX$F}1oK`-yW27<_0o;hGT-&eU^2EN4=^lb!Z<{kn2c(0=bR +&9ax~shpGcj8bp3EzDdt=@(#>({Q#Vk+ZJj?FI0|=nLMNCYmoaM5DzTh@REF3zikExqkXoS-#>B{Pkk)r(D+M##kZaoPc+xFwcG5HiM}Tfc=O&(@j)aGQhGE?%I$tz(R3nc{edL&-#~USz#zc +|A%lNq_k3FYn^sqEr$@Ix +i3J89RV9WjK`A<#YtcgtN8b-@)o~;sTiXjL~?F$V%p2PNA2hU+kWzQmfdO?p?&17<|)GjuC*@6HPu|3 +E!wkGs7G7d~X&|9^3a{TXF?+}C~>zdT_5kArpG3*~V(6eJbjam)6lxw@I8=zwtk}b9=JKX`7jNddit)a+jwXrnm3YnD%|H~2mq)_6fOz4-HrE;s<_b(gs1G8-{YA1-k7$wkSXk&IuMR%g$dscTJ1A6iEfer0w`itVrB`#J-B +T}14lq@W97KjZj}S!X{9BrbV +mIi_h!s0IkX4N6DEndhB;?pErHB|@MH$C^}gGZs7@w+g^gVEvN`lzk#9kl;Mr`=C>MNFn3%&K48@mb1ME} +RzI%QdU)dvj9JYWV;srUbfgAsxAMEsomWrKI?ccZJ2myz7=R7*nJFhsJ9A&Cr8W)EelvtO?wFo^j>F* +zK +xq&Zf@ObH3E#&{~N8`qA-$3KGf5NmhclYrvejy8uyoDa2pD;B|oc +$}`e^HMN_zNdJZ8Dv4pyi?-$4Z%K0wU39j#Ua=Kh6Kc+H}J?zKL(+1&7WXqAmr6e97^Az`+|0XhK~ix +B1F@!+U6-@|`d_&Heb^wC2oyb^`*5-WV0seAn4V$rU_rfR=md7+r@?n7jstWVKcH6CyMp^ve8>?O1pS +e8MC)qr6;h61A)gvsShsJnehT$P=crnHb|wRX;}aSp(uo7{ywwUzS4 +mJ7h4EteCfv$+RlD%u +GasGUG}cuttuCfTUem^;q&@$lBbM`bsoOmb*lhI&Wq?;Ki1={b%rmx({MU_kGJu5-XG14P=(+=K8Gjj +%Y=IGfeeV_tm8i!^`$T6M++D#J<4j1c^T5W1g|YH_S4^}ewBVF$tz7Cn8R5d4H`Z+^1I`Oo;hoG``B7 +oFj{o&xLS=WJCaKY?a{)!g>?hA$4}GGrL1#K^P(%CY^;0mD7Bg-G-6?v>v}x>VUH +{uQlV)v>>Xcxzwqw&_=TG)=)u3HE~v|t?2+zJV3v_N%b^dl4`ph0rDVJN)r{!jrrH^IKTrQ2lRYoDz3 +Ran{((XK49K(Tk}qx;qKwka-C{wrn*qy&3{+szYLbu@tBE(PPn{gUw)S_ibJa%P+g6mH=V}a1M&3u=c +TOI?uM~4=|)NO-7NIHim0n#-`ssd#U`+enarDoJ>CvbAB +Qz4I%w{ZCGOIYym-bFy=o2Qc8L6@&AqdK<lT09_>Cb>+4V8t8r_rj}U-&kX=~Iux8 +HPnbyNO6SkoA2m1-@o{|IcZCl$dO{`9vO(ynLEWQa*S*rOc8WltD<(x74pquTsm7CP1S~~e(~Ol;jW> +G7fPcF&mCU{_^DJY4#aR4|0)kN3!!8E&XJh8(_MGaBL(hwu(z84>?Qx(6ELG3MNgQt<)oP_y34lQIt; +kGeYt!6S@xO2^VS*Za{Ndf2{aHxATk{Fi)wq9sBh6}`LAxtbVNJAME*W5%^VWjlZf6yM47%^@V!-99G +ILWbLVF{Uy+A5^sW7Zd&~h2O_Eeu +GnaZ-qgU^Eqq6m?7WR)_#bVEH9+li&!B$La-u~LW6AEGiLe{M;?HHOh%R{EWxunaC%cGc%>@>pP4M;Yxn9B2iU%orTJt_)qS2RUC8fLVT;Xc0Z&l)uC9)u$-N{{~Ql}a@3qgm)1jH#>!oUVi47D9Kxq>7qrWh@%3;(@&$ +DyvDh&vOb#b1XyyFMWH6C*D=dftyJ9hQ&uVhrG(tTfPp$Nkgj=^SjI*_jT&lDjvvMg@1yNEQZFeITrW +u#8${VjX|ok`8m^$YHWV8#s}tzrnkOrIpA&UBUAqCL}`CI?SP(6V-3<(qUWq{>K?}$;EZs7>2Z%(DEnm9?CBU=N91_i3Gc~Ei%!W9O-92L{dvL +~JC6wXZaWQhMd>1fkJ{kw)Sr(}R7=^k`px?#zoJHk*A&AozCK<{&pfZzJaE;C%~u^znq7Gt_m +yq10pCeZmDJg}yE$_pF4j6k~zRoB>Y??yf{;H6L4u8wK&Dp6ZAm3R=A@(doPCpyLn)7eB1*CA*FKu~V +66*<+`L`&GPUNc~?Y|P>X=3l}VuLoXuz~juNs&EIo9@^t9mD2oG%c9tOW86XC7&U&k1xH0Si;a1}(wH +)ZTzL?V_#688?u$fp_{(8O**&=j(b9zS*G7+WUJ3?SWZViOy2#BKIZ-}{s3q@^%qPQc^P~%)2Uyipz( +RCStuCv4Yt58<4=pcw{`=FGyb_is_pk&qqDD#H?9X>n9etmStrlu40z#l?qFFL3gU|f5J0>J-5-!@m1k=Z%8e0hR07aiSqrw_9=%IHl1z!Fmmpe!XylBLZ1)Eoj&>ih2e3F5ss5em@@XZUde4=g?(XxU +#i#9Ye1uRN?$cxk|PW2YrW7vdQXfj*p9QryuRn*Lq$!!Qc1MrqHOBFR!7*-hcU5d(Tu64G{gZBymU6S +%@bXLh$Iqqs&piv0gA1LPGoy_X8G%gwN+Zp_a@HV{kkgmhOr>d&%$>aq!d>(vgKHj|B?)?}b+(Ew>>4fQN7KL8<7ZiYf2&$l&Iadh{ +8g>F;5-ioOWjs!KXy)fnrqe*!3kxED64gct|3_Ytbcw(269&8>jR62lJL%$rHh={O?3-;l5Z6;Ckue{ +0sqLqqQ-E7XtC{L2{P_hQ*;38S%vg!A54}s}-huGcI|fbJ6!NM=JYEZL?=4Wp%)-2c%QSzKsrg(Dymk +6CWnR!lUK(SJ0hSxlmj}m$Xu(vJ?CK1^E{KTUqY_Qve#1oYmEQL%A*seoBCNtpU0!U!zR?l<97wtO&7cp$DQG_Go}C{CGV#~%^Gx9)uyNw}l}E_vTKS +UdQb?yOz&DG=Xlg{hu=Vjv1StFg%ch10S4PaPcI>YXINN?1%ZUFn<*X(w=mh0$Uh8HC|jElAXko^w`I +a!+65y2Ic;HWhX!8Bf%N=E5eZ_EIMiJz-GH8Sd0OVF@o7C)9u9LS%`cK5RO3TQ_@9`tO^Fc4AtUHRpv +wKHh{%W*lRG01-CQkq1h+wpAApBG)F@K9Vbz101Hx6Z4%!!a6CDVk8i>cQU}fIrN|jzAr{oRk|A`3g= +Pbo+;sHM5mtWZLCz_c+WcG%DrED?VPIpt$rRIUlQ&N4r%ZWc*R5V5jnLhsIAz+Kzw50GG6ev>fH-Apo +9jGHw|TKvS)vFWC8~JINe-Y>rnK=tXojh+3hHD_S;e~xRoKA5?>A@1ZWd^QYtan^(#%hI9)SuSXAqL( +2hwb4pIFwZ+XZXj-mBD3nWVkY7%MFlit^MlW +e&kV2_J^38x1+}4%69>TC3+5f$^Vdz;kIXR_eZvsiw#O-x%0y*eXDfd$>VVuJR-k&$=xfA)KR8|1k4B?>>o&9ige6{_F74MLaxFsZ8_ +<<~9F1Xa4&a7dGhd4d7N1hxOdW!?{BnN;#9raqAFc4g5Z)OMF9LK9i%*&2WtOd>T$(<}tbrp#D=bY7s +u%ANL4ds%s&Sditg_Pt1P5?%coXfE30!6n09d=UP67Ij=ToL~fw6R*yE7Gp#yPPYQs$INUuL(n1qafy +sn%tjm)qR$i96`qAwOke7c=ylX5`nQ4h}xiM{cHt8QudHqq9SDomASnc>#z(&gqLj$f&zeXMp9XD{`T +%@#I879@y>EMmc%!oXYabs1!UKt@h1X%LUu4K!O} +}VsrZ_kIJT54RZj2)PCIPy +^WO$W`42;a|0OdE5(5IeX5T!gJz~mvx=OkZ3AZ3wVeS1S3T}Qqk7C9DCVZwQWh(_4~IbZ;ZB)2W_CZf +W$XgQfIwpA7!%2;ed&+p3J?9uE2@u4a$?8=9H?^Iao^_9wGXI3ra#OkzXCnCht-gcZA1YJk{&c0%{dS +ge=uwvS$Wq3e`Mt;FZf+t{D)xJY(}BVmub4!*%~&LRpmiAD*NO{r%d^>C{261u2nYPXa@pF7|f*jE!o +Iyy^;DKEPRdco&sh@(uk-=W(=W1(9Oj`d!a&wF=M%XEc9xv5}h_6FeQeK?19y2FB;D{pk@2pb)A*w-n +)Yd%RwVkr(l2`3=5rmjCnws;KgJE($Ms*6&gxZB=a9Vi%*$ACR#$m5p4~Z2KxI2Rmv=My4D$o2p7Xt8 +wK0nsbcX{fkGaiP>Y>i80^Nde#9!@)cCgUP{KZ&|7%bG0-x%%bUqcqHW+ic58=(DySst0C6%fF +dkcR_4vZqWhGaAoBylemxJ-btlbfEi<>0!HaJY6&(3}I200axv+;KB~TayFeZ;mobNRJW!D!|gi|j`) +k7azEYNTjmZrLVx}k7cAR`YQ%yFoeC#EBgQ>tVwv&y*Kz3c0EfJXPnllEG(rubr<^*%u%3lCZjc7WC?E>7x04<82fI3Dx|y*oH<)6?LX&VL>~CYW4?h=_PRz~y$`G24U +72TQf>S;hT^@ah$;J%d!>8Js+1;#hAU5#U`Oqno(nMSH^H0u^7(sA1wNs{^S-=5mwb4rky!g8pMJ#j8 +&($E^N|w)6czOdz6=@!1G^Wy-c}+a?=5^bOU`m$y7QMZ69^zV;VZ>MmXf1gDPo0Pm&uBXnWm#!{V(Fnkr59kb%*|k) +u6#NyNHz{gc1w%8NKW93JJUy5>Rd^lE@n`~k$E-hc+m)s2RYhC7MJfgrWkRAh4G +~86TKn~jY`1i4`Bk(Q-K2-xc7_@iLsOHABeZ30O9)Pa2=ah+U7Wwl&Px5C7gyuka%DOj512~&0H`B#z +CqF$S2t(r2Gck3j#O5XhuOVQT<;><|3Y+oW14b&d{wEK|n2G9?32UqhAGciybX)XCmXwx;U*0J`I#Fp +&V%n+ck!JcS1Hfby-X}eRfsGH9I(>nMPA)LC)v2zgKN93A)6~pW69wJ_wG^n1hU~e-EM`7E|Fq6sHyK=5#qd#plbo6-1gUb^#J2fvH*xHt|waa3n>f3pnj*+b{<>4v~!{3+A! +Y!pCP>JM)j>eJqHA3cPa{QzQxX*W)pwC3vWTg0I|N>J5IvV;w|S8vNYIAl6UEpPvEQU9o=mmz&E|Gi5DoE-o;~fmkZh;ElnO)}*^qraPOhg92E6vv<)bFh_tKJKj`z)luyd6q%s%kGQi=b1Qp@L*~&^qN(Q_*Px=Hg{1gZJI_cVI+-zh2GCv-h|ID +yhD@OFh7?4{A{1##s66ep*Cmryzq+|WPhhq*u$2fTFjDEr;X)BEaXN2zPKViDGmpsH3^uT}rl$n9gCB +{ai;2r-@nUcnIu~A>CZ6BNmmHe5O +SLkihj=_o{3mkI=cwFz>nzAds-=xbmsJ6HGW1g>p^0DhT~Lw$yxAm1E$!%%0hXd6o9cgo{o(R4dI&CK +e5Yo@(g)^S`HJts!du?F6BahpJnG8%DpBAM!UAVjW8%4FxBGkNLaL*Q7g552tU%%uZZ)|qF}I4QjG=z +F{Bu$j&Fy4tR&laWt2hG?p6Eo1)kp`qnJwZwFA%bDba(sKccrSDKgU?Y6*udxvhleYkS| +2lHJ5pvu6)>A2P6n?|E1{u09!y92(AI*8V3uiwuvXdHl}}kJURX~oMcgX%{akL?ujzcYOgq1-jrBlL_HjqdFXZk{fA5}KMnZz^4iavLKLnXQ1*NyRCy$x`Y0PG-XKzw+yfdJjyO5EgUr6sn%vbo8KiPRcz$SgK&gpX?)GEkr@p$8<5a|K35FlQcIgi$Ny^~;Adrl=Xo!kFUq3S2K5=-c!@W3rm~^( +kk7Wtfk%EK*fEU5f{nOgDU+Gp3^XGn!+N^UOjgAr-4h`-ohE|Ga%>1os9WWhYw!)~B1&$@sHb)tI|`E +CEEIrVX4i70tAUmD9}aJ2^0q*yrqa#$+^hB2Zskrfdh|OZSb_4YxVjQH|z@C*d=upjoKbWzaD?=v#N4 +F{MmYtm`sl+5zYu4xKT@%vBAC@BT6l`Zuf2m}bT%rs`CUeU1-pp(wyjHkoNA61(ADayfHy?|Qg&u-QK{f!FV@f#DB~SVY|v{pu<7- +Xryadr?{WEFmr03xZ9O_9Bb=Nj?_(Sp|2nGbY(tNd4V~zA{6Q1L3I20mrW!({s$qX18F#OEbspY^7c} +kmgM#J70H7tr)OhrBWR@Z9q|wuK-RsAwM% +f|L8}RJhShzl+W6p<{>cW>i%=0Gp6sknkV7-0DvAQu=Aobrs|o?rNXe(7CLF3&zQ1@*vkOCT*T%9*im +%nr5;-69eq85N_;M(nax!9RAUhRi+z& +_htW^!#8i2SQf+m0eh4TcIjiWoX{aV8J~fxQd5Lg4pKBcb}BpD}dLm`3O)Yn-*FV|0~7=-d?D{@$v!# +HEM@x)foL&oWIHQZ@6gf%Ylq8B+uOttu64CkuVH6O{#hmU*Ta@NYqS(Vcv5e0B`<+zQRcUKhFZa%Iqs +dUVDU-Z(F68*2+ew1RD&F+I>Cn$Miv!$ZFTOr=2+saGoF&?!$l&P{TqGZ)Lu17zmHlJv01^xnPPN_fl +wC_N0B$h#&J)dh|88UhA7L6`@!C?d7anZrU3#*dBUjUYl?@4na0L_)85=B>(!g%Bgx?aW}6(|7obpE0 +q}To#qi?u-0Ml^%qn()rBdZp<0e3@x3q9m@Rj6n8_B=Fpao5bH+9B^+aSc91!Oy~y)Z +vVkHAJc*`iYa}Go}wRFRfh4%Y69t;G0D)wjBNT^=()|&`#$uT4 +~=+^_zNygMDy3TRt{82WNc4a!T?7=A&r`^Kt)8O +N-rTa2LlY}aK@xV6SIQP)77nhUF8VEQt^bv=~R@bwK0Ry8{0wqbyLyM9GWQu)EDXcr+<^< +$+rKwK6+{qlW~AiSCAah7ecl@vAFMch-_y6cQ1Jp$U`7RjqY>;|^XFo^hiq?}nH{kZ-1y}ug2o)8aw0 +#%B{ooJ16Y#!18qpUY6N=hbH)ThBP$4kNz1(F1`fPtx;&CPs}hKKhafCbo0@8ZMmcz=Uuih*@gT1B(Q +U`0;f#rcYIOh3e`*J(8V$H@YX_pc-FGUT-XEx?Ke$@Mak0GtFO*y*aP<<=GUUR56#b` +nQeg?-M=0_Mm%HEqbQmCtw>0`lFS8eOm=KHsyzBrmRp_qgp?lon+260+3?EVnrxu+dZ^$??{<0Sw#+> +o*g!vH!XpS@9sEu_@WP|_8u!ETyKB&-h;@chEfDx?q6bpeq-7A4fcMq@AFY99$o=w4wdc@mj8T!%?N= +YL1A*NROU+|?%a0J{5rA7up$_RopHd8E*yT55cO&@^mWtG>^V=Xg6f46uO5%Y(BAZfVYMtDi&Huz0beZb +|vFLIO<%HW_6py_~V=+K%%2t8ps&X}Mmx(lmp14nl~1LFsFNDC97w|yP +-kH1t<6sbxwz%tAu2(mGkZe;gx;FsyTC?_JTtGp$$I%DFYQ7V5Z98WBS{dkO-jI}=SfUZx{#Tp6l)=G +hjiS8kZR2kTA$=jG5!8OlM~0v8+4@M_85yb{ +{7X>P2NWn2ZL5CviCS$QhFlMOD6*9D1veZuCXz3}go>hwex~B}8xJlYFBGQcV%Y9D~YD05bfkkd%^rZ +K^K1+Sl1iYB;E<5bUu<)d_@h#`Hy?z*G$QJ@OeTGpl{uX%S2VQCM`qb@|3H%e0KP1ccLIYE@tz>hJLP +I0P^@AS`umokpuRGmcqEcr{7@ZYfYz(HFDSW`Jer`o}CS{#84((gQUz%Z`=Vy+yWhhwq637UUjxkRH* +AH#{7gimAp39BJk@xnjO;;LTP?I@n*jTuBZ+4L#;dUcYbW0C({^FJby#%aQ7gezFGXKUcH+_yPChExf +ffU@s(Ml8H+0>eNPz5756vrBbAM^m+D`0ecw{i-_g!hyk5esnW>U6H0REejlneS`_kC<4Ko;L#>FX_t +CGFbj|_@2kU*HcGKMg&$ui!K?`^tD$@Hpj&B!unr*?KW`|Q*syg~2t2F=4084c?7ksaE(07c6HK`LT% +!+lHEH~d3g0Q5^GKAg2iZmP6)+HUW@-q)Xe&&}h;mnrmk6fltH^yVy&oJV%v^0=}l{chmD?*y`$7~+@ +TPoEeeT6hI7`)6uf7sA7raPLAKlwz24w|%tohggM$`bl>i+&KzxeS95$| +EwLqSU=Crd=)?dIX_#h!XuUxGD@NbnsF<^?Gq|Cn&*#sh4CK{!k|s%MP`JEIrN#`R9CdgcTX5q+d^-Q +?K7q<8qaTF+%=2M#U9>s +!5c(Z>sGew?LL4J^5=elTr-7>%X0pXtH~?=n^(l=e@*0%C9#~l_Cc3_hZm;86a>t?l@Kj8+kk4{2Idq +33swf(RI`q-&s2b>NW0RK16;5Vgk`BvL6r%CiWN;7Re0u>{3`>HgLH0PT6j? +(sHFrcjn{at!*qSH(F>ouXkStq4Q;EulN=di#7uL3!@)tTMbL_8yrSmN^KfiS;8| +z+T&Hk;;XpWAhp-BrJEb<;__p!nG|+VtJ?BCrX&Rb*%wHWG3q(I)oilmSBHx)2<~}cVr9B8o#+K>LXn +fAZMy5YKhg5wQ-q2v3Gr`dq+$&4;8MDB9$2pT4C33AeP!r{xiHsnoluO%&@vc1KlJ=a5i>_ss<;HlMihOXa|mP9InTvY*8WXAFD!&}xC6t~Ig<({JDp|n*@nt)pl>ieXHubA_KYW& +&khdMIpr2dVyp`=&>J{Vk93?e%eNRK(t@a(-7WH3sATBVR{b#3`n)bxh9E3K@~LOtX(=r41SXxk$&jg5~=h~iY%9DIKGbEX;!X-Pj(uu1Vgg}eILatK`Rmnl +S&Y~a9)r*TR%a5FC}V}Q&p2p|FW$XrRt@@hr!!SU-F$BC29PPX$jVL;y@=~8+((GWP90l&owSKFQ6%{pg_qQ!j70fMci;R)zuRBfA|cF^p@U#ANnS?G|Q&O=u +*aL!~w=9Y8R>3TS9?NBukHNt*rA7ijxqnKpxJy_O@x~NhFVM& +RRl57om&V)gB!{;#v|8WMWZa8PEAoz?aj0X(*3?!-w0&yoUHv{0w$vG1Q#W&GyDcvzC!r}R!3WM(R?> +fU{W(x;Wq3~b$@DAO;ITH*$4|RSiwq~d0FAbB{k^ +{Dgq-R)h%q1`>1rYKv{5fvcJ=}b`sZNj@ZH-H66zlxKEQd>8qcxd!L^L*7B#;A5^3GMWL_H`CBV2?`G +LRT_@{O#}fb`g)SIJCb#RSH4SN6Z1xz#%)5r4-7|UFCYkq379XI1_1JfEn<>E3bieSEL +FdZGKrVUA|O={yB>2gkH3F$PIv{NH_UYWpJSI%mB+x-a8qe$M3E|Ew<&RHjTZqYCQzpisYwq8(*EOfF +|=vlgGjU$zzc`++c+h=Hyby6$AD*sSwD;iv)enD+6AE^qg;bRZ07Q81V9>zx-9pRiW4N(=`F{=^Bet6 +YX{pJw(o&@DQR|pEJD=q{Gp*3pI~GqEe78_A)h-U;}9892ogoVj&y;`lw-ixUWv`P}Z}s-$Ug<(ThxLbhL4B=!;R+&vmBL5X9NT!Fn|M&;8xkFh2v(*aQ4EQr* +uay1q-iTHk@&)@=F9b${cG>8+L@fYViM^prKU#F$=&OZ*0EHz^i75U-{GxidIT!eViGyxY77n|bpEIG)&&@Hzch5@0X(zUT3tfe= +1z@)*1iOG9?S>AVVX5{=R_v|g{(ii6+5KWF-%*?pLjJYb+&vSa_dE+mH_ML%asp7 +Exb%Y6~xSXk@|M$eguXH?`%x$>v~9y&rE3=19B#Z<);FY^pEuMlCjfuOeN-FcvT9_wV`!&^{j{4~y)p +l9MXW8Gxu;m~BqG(B6$a>#%T{8ZTk(Km1K)IIQ6oe2b=cA>}4O{{n={dKTtZq>q~Cpf85rIhXaoMnlG-+mETN=T`2 +saKK^Vz@4#h*m-l{X5iqSE0y%zZMIPCMn0AnTwD`CejO?hod^cWf%Hh{Ow$8milsHZRf*tDZ!CS%j&k +k-elcKo#S}ZHb)gfQg%CM_4SrOsvnX}8-qg~x%rXG9)6y(ovsGSqjXkR08JPv{4;Ltw058tM8~jV+S( +yWxfEyOIeC5KlJg*g;cDNOfho<(GlB<8$vZyf2%sJEbSa)vsktP%bS5}45`6CNzwb7x20JM!Hs`Y_DZ +aIgZHW9toZ235Pca1R%yJ6aYR@uF9g#OM%r988p%+%JowIlSMU{f`ZH3_}{eFL2)G^J_@rApHglo{X} +=*jq^<#~yVhimtgJ +ln@(^TRuAoRTDZj{n#UT0NMhgy=F&kt0o6W_$i?DCdFy9rZ`j?-n}DW(%?95AM+QYS7;wbmSxBE^ztE +Wnb&v=0IJ?=oM{5P`=5mKpF6*Hz)lHPC&VREcBSnXHsc`i#&&JW@eUG#}}TfY8U)vAD|96-HG*XNsKb +dTS;RXRtTWdHu9Q%#yr*%xecQ>Nw`NL^{FsWMXaPFFH*DybA7%3!B?{1uRr{vBH;&&{JbfBQ}_&90sl +Sz;A5wF2Cb*roA!N(bq9*8a?pBQ#Kcr^2RR`2+&2s_>cF_pyQKw*Psg*xz6;?Nm~N2C!kuJag;3Xyx7 +!3vo-kdT49xTE^V{o*RPia0gRf@>?kU^X+l$iPg)YUhSyKt?f#spZ@x*>T%}!_A_zy`#}VsX@W>%BA~ +C>nA81U}Q8f3eNDBffbCGK4l7eOKfjJfZ*Ick<-0SUhE2_T%BV%52U`>VYD6hZ=(#zBwwB6t7RcID%r +Jfgse1afA76cMmqf_aPv1wIitP#5lfWUVFuvX0MnL^7*r;rNAfv|*SSvqaGHe;JJ=sdIo6rD5eO=6xG +EI3H$7l7vTJ7>C^Nuj<6H4wnzxtoe_qSewS!!j0qH-}oqnF03wwEPO)`&J!TeA02Wv%UZkbMh}P6_MJ0%&K>mcrMG__beyPYRVuN*569IM_~)VWHV9 +dU{mcS=YrGl_f2dMt2Es0$g+sQ(gdO3eY3&Yv5d4rvyC{|B{Dk|%3#=x*(xy%~$OST;vh? +CWf8nsWotxVd8o73N%~Bw?V3zO7;^&>7h%g#P0@AYpLyi_Ngc=0Yaffvr2M)m38(eJTO0PCHWkz<}Rn +J7l~rzw62@iYWBhSX93AAJf8~^DIH~6$Eb|Q?i!&23b&BlQ3#Dv-tI(H*K==X(lv(bbZnXv}tngN!gTV}G5dvv0-@E>e5h|y5!QsHk6+G_jKCf`F4n2OIverdvJ(3>7IDd8% +|eQpcQPPIQA9xy4-)O>n)4;livOsz!GlOp5XfXR1e=1VT+mI1#bDjofNH@?;7MsMrFfJ9E7%{r2FpfkR7-0Tc6_h0rkwyeKu^WM(XJ7i +qTu&Ap`u!^hOyF}{gHDh^r#RiAn@10edAq3f_riv_=}AH&s+xvOy;wYkT1zc^>X0%GuthNDSNiEuy)cb19qRMC-04kJ7D +6Tn>hNf2?z4J`K9PW>hsNwE~@~-Qm_ICOxH77++b8Z3+OHT+toFXTs&YRpZmH{Hii}WGyA +P7skt3wnue +E9{KnDpM;i(wtdN%PHogH9)XF4KMO%Uc`d1k;~Hp?LtB?}JS5#qcjlrM)vb{3Ta-I=DW8StB;E3Loiw +(wt#rinZ=)d78$X{z>qU(mr}Tl{>`4VncOe-kt-*#B5#$j&uqzDXn&!kG1|9ktSypX7k4fWUp!d%xre +?Lm@iedTQiU?QNX&^dU>TkjkI=P%#0Z8Kmppjevs1+rYoTwy_Y(oH_ +f0n-FMOu;Ao#g;H28iF3WCq0xZgkUTP$<8di@tx^T#YU%kS2Dn2G;5uNHIZuv9(uyO511n8GZ^uubKC +KNPCTu-lFEXnNu$vCWeRPFijR7_IMoS#-7oV>EnO~J4~-&%R-*2xpT`_Rh9vUNs95O1*gJN6)AYUucm +S{g(+s&SC0TCfa*10?h7QCh888*m8P$mZ@K#ENLslZ_b6q%qoxE(%rqE&Qv{Mx}fX)yX4SKb2VVfph**f*1!>sK+7LJey`;-n7+5hkj3gT +#!)f0i(wDe2sv&U&r}7J$)!yPX&1x>gr)wIya5*iVFY_)M34yfhIkL9!k<5VBo_xqUpfOI)%&2k>PSO +UV)R`Fl3l}yN`K5^GTmo*sBfVoPO@K^{lJg~EUa}%z5#ITBILGv1C;^A)qV>T93=V+;M4x`XQVn*omT +miE^Wk?06n{jR0Z^T-t2aH;1^&=O!ZUc+D+xax1x^zX~47n+&4|&m+|ra@mp3Y)dXGD*?J7iIt1aUQ) +j|L-P&YI40_%+4V&O6k2u@N00Id1R1Y=ZDVI{&4X}khhVj7nbM9ZNV|ve29W;{rAUsE)MX8&e(k>XD4 +a5Y{c1yrYw0kLXTI!+w4lmB$J6v3xzqE70=aI+%kNy8EM4L^RcpMSc_Aav;x3^+HJo3EwGTA-YSDDp` +!{M;jF|K+j#Au_Os=HAd3{fzL!t5cF6=Q&47MQ!`M#Q#n-#{b^qKPWctBhjt60LZO>_iIEs`4}H7ZA6 +Tr>WnD6`cjHII=7B!2P?Kd|_(Wa-Xfvm(ARIAme8Mn%`pl=+5ib7RU;dC!qeZtB#TGc}na0Vf1f@}?VuAhg*$7_o-fF@p>4+` +F-aTmE+uskG3i+GMd6uo!yeXlNPz;r>^sWM9wvuNH_GYYn8LnYIyNB{f3iyZ4t*nXzA={?P0;K8qk`$ +JC-m`vzzoqflFK0wb`pSHqo;6}F#ztP*1sYqykH=5ndZsTjy=>iBtq3{VkJSEzB4_J&&KJm@n*RbIZ& +~DvS1oT%tW593qoJ|6H_1X7+3*vLT`%}@+wRr;!uuMxb=N+{Q57e;48lpU-%A59#Pb$e@6rav)iMIxBGSl&Jo!ITH{9x$noJqIv9Ym=uTpjwd +Eny`qh4T!Ba={Q9=#D8=EaZ~l<=aNA<{Wi9wii_k&2 +E!#i9I1!sRQAuS?iERGm{Jzk9i1eVByfYI~5K!oob>8_^ksGm1Vxv#_Hp3iwcP%UA$nH00dr`?MEv&& +Z_6~mnKoQYIRVm9-8k%r2huO0)?uE9#omv#Zpk^_eBuX}5 +7EV^y6@`v5r51Zx#XsCx2Ew5|f^j!VqqFpJiwk%Bqf{$FDktN}fUzKI?G2g$ +^40z$Q&en;Gwu)OP{X*FR&*fv=GXq|BKrLpumRC~(;%bWJSS~sJ1BXKkefrD+QxVOkO_Y&eX8)WX0XD +-M6%u_`#Xi`D2w0BvhMP+mF`X%FKzPEE5fgnB!S&t2!Dd0a!^7Pyc%NE0d|onU4g}jKW4eR4CJ(9|n* +2KV!o$*n2t+HyOltsA9_RMrfw{f(jGRVe}W(^9e!3WEJ{Pz)BxNDpRj;#m3&bk1-E%D5 +c-Dd35!M5>{0^smM5J>`FJGenG*^JTt2C|Xj;QYkaak^rOKJlTdp_?-}QLq`Z +4IG^bL@ay34yG#!+a0DcI1rxr{d{2Ke;Mpx54`Zlc905p`s+bB;#KJUvQw{kP-%f2Tb6#;8LLroLo&N +nUU3Q_Jdvq(GJfxB=)J4@d*G522S7avy8L2o#Ftb5iH(|~Lr6!-z&8o{Vu=s~S+OaZ$b2M{-I7uLnCVYO(Sc$ +MjLBfoRF4}Lx@m(N;d%EwDW;J3q_ZVLIVWERyc1hb46R%koUeI}8hR~ax}(4sb`-4_>kv;lrCz9qf%k +*ae9eCfOrQ`fCB-xv_E6qR+SwR(Vn@37(m5SID}B_;>TOR1L}8ejOP%A_?E=;wO-XD%BMhO$hVe;^%| +ih@2%m^&VITA3cvKx~2r%Rz|h(%BKFI-x|Od>sJ`k$&{KaV_$|UK!T}eK!* +#1@XWOpV1(?kNb=P7CLV$5k|690feQtMM+gamnygRw_Vd9^z~gT0s59Vr*aOOQ#p1ax|qoz7MF{g@1Z +A({(y;s9+XTMdaEGlkp%(dV<7QbbdtG?ICO0js{NVWqw@^~{_9l0u#ttj1fd`WUVO|#z^QEce6QBBL= +cwhk`2dGet-#aWnoafrNABCup*60UC1Zxovs_WMFshQX?>F0Bp50Hy=KdhseKlCxzUf(*!Mbsu+)CXL +!P^*lBRn*{WS&NGKWm{GuFj3=#)2R-fN)usB_3PKqJVAa#gQ4kgdUj@I;@Abe#X!ys720x1=2$o`;4^ +{bMF#)5*Ozl7oZkjD}qFBX@fB#h-H_@X}09m+6VI@xZ>cAdoV09N8BvXvkDRQI$OJ6X&>X;K-0j{33V +Q%Gr~brQK6vK#lGp69B<{aq1WL1h6Dc6la#XM_rs +%_OPDpM)E9&m2!$;lZmDM9&bY?^qJ4@G3mDj%<8~f8r{2=uQKD +%dLtcQX|SI2%!6>mqyG=Nu1CQ^qWe`3D2X9YL6tpM=9ym^{N8C$RMlgBGp6;}%;?{|qsFurfa06l5X~ +r=<%dk*Grx>K-6zumT#NvOptOwIF?zdUMrm6ob_fh_O@+RK9>lk<%9iw+uI3;djK$TU2Qyk2g6*y0A-iF%H2j +>q9`#moh~tn$)EtwIp(7*<23yPyutB!cjM^}+24FAkpI5VqaJhu=7iia6#E~wZ?6DqzK%)0KXq(}tkUfAz=vxgBnRw?23pKQ$mAq?GklJ#dISWD*{GV~~|$BC7`6=}}+H=k +eq=dC7qmrCW4nXi%?y@!+669Fv+<*7Id!Ry2@8#HLpS5J2q~LX|zUYMC*>GN){UP?!}iK+p41F%RTL3 +dtJ)4kd3GGEEQUc{f)W)uql5geBOM;>$^_KA_`(nI3&gR}akkW0xiU>ua5jB*wJB&LG@hyW5X&QW}m3 +hfL}dn;|XL9D;mc$W%Uw0-MqKNjJcnJ=OS}%_pj^%H<|C2ISdXG_<1Q8x{6knUQGk-CPKS_b-0CzI%w +He_+)SbBcNm(Xu^sz8mXaFM8?SV-Wg>H7e{$Ajg$=6i2{faqE=))(OTPSmV5ZZAz6r_YZfs@xmR20UQ +CnB~l+Uk1y+(a +$%%Q;Gq9A1ub)GNhD5@cXDkrtq1|!pw(b1qPA_;fU{fP=(KxO1E`2H7{VJJP1cVxd7%j5l{d7zYD6is +%;9>k{$RAnHVUJZaDlF?NqwT^TN!NA%^0RX@ZifXiOFdd?7*wL6gSS*aLmaF^lI5Vk#Rsv4O*eFx3KG +DPuFI;DPW(5fES0WBb;abS{TAX~}?JgU(pvS+ia~smUj-NtK)jh +!E0t=1<|E?XZ=lz`f5;?2SM?T?d#4T#`z8&U9%!1E75e`mbVnE}1TvOXw!hTRW{mJ4EU`CDg+CAZi|M +U?qH@rl-KpefQRv<7M!T(L3jzpBvLrvtJi|>e3*5!6xf|+iY7Cz;H$CY_J~jZm1n|gtK?B7Mnd-;Isc +`{z3G{7?QPGe2_uKG>J@mX`>hF_38cWabUkB)^GvP6CYO~UY(6JPfiF%A_tFtu+FQ^zYC68%9>cnDnnSnUQEAVg^Lq}hhA?6aJMfrs>oE_0J=VFx_`PNW74$$**b+j#F(3or1xvn=1!pOQzAk;iBgw +0r0V_&{JY!j`Fgvn^?z!ENiFqz_WoB%@9B@E&PsFxi>YhA%Q9i+C04&EMf5;chV*iwA(Q_^x_B^iuFVkFz+tnBih=rbj}4gyD2eZ*#oa9f +{)43Nz&;a$o=-gG-jN{SwE@uyu-~WZp18K7r~5)4T3?9t2P*WrU(CQaXla(h00Ie%p7LFm8Z?Qp0TG~ +o%|z8cAabfpFXnL2=NZoHpHpvY)9Zm{#Z2J0?0nH$^N-_oY=@Y +QIK6INWlmcE|1+R<%=s1mMN#3f)}Lm4!3|FTl}VAv}`rN#!|<_$HonAfqgmS#CEop-uIsZf2A}M)a@c3U)|o_++BWS(6KJ5 +AZH}aC{Wz(T%v3X!VzBtj98F?rAkwud(K09%ZXo+>T*88#J%!PTn<7i#I7au-b6n`uEqf`f1B5VL$@P +PSxy4MnO3PPz!xq63_35cbHg5tI^$^kS_J5$4ES91*|!M~tr)^2_`#6rbifDcyLRiN2EyhA)#p4+Z{t +PC@&VAhb`ZZMc+N3q=bGjg@~w4L3E&-7+@WOGqjbo`JkHU0Dbu+u8W3*(8sJdzP)TpIIXw +2VVgEw*Gq~d-{{1%NTZ6a#PMrWrEz&yA8#Xgpa!*~X#5G +?gB(6w~-f^X;P-4fo?fII>=2N7K~0u!jDD;WdX$=@X}GS>WcJh1PR5qaaoMEaWCvAyp_q|1yTkeHL}L +Dx}LC>4E2w$3NRA!G?V^>1rm|v2FFGVU6&&&4vJ=zyW +82ALw6pbqM&)S;Lv^>aVMj&SrnQDA&RvemB+Zl#?(McZ83$DhqumRs2J$4T4t4oJX9}=K^U5Po{Pnvs +#J=v&O#^teYh+YqSpV?kPhfOoYFfGj#kZxZg-{{o0e*XCV960CbKnk{s7@Pt@FSaY^^v-b!FwP0K(E6 +jw4hs2603g$N>CuQzNZ7lKIr9O(E#RYUR3-|^zZA9MW-gRui)X-Okl{z@{!51F9Ixnj9P6AyO +ffknqGux}9pyLjmN=rrMQCW{c_!$Nx@M1@1(C$g7t8ds|Trb2xLDlZ-tjJoPp`SO{E3m-B)(OkX;Z&r +l9mFD!1pWa2MiX!;DE@u)2EJ*c4sjU2#ZswtX6)@$=x|EZdKq@_i_G_GS|L!eD@o>QA=U)X#qs%>4Ec +4KiX^zZ{X;%C@7mAa>IRE{)dCAjvPh?DP^mEssi?m$f0i3YTvHlNKxHn`%qq(liTK}DwDtquCEQN^~t +x5JKnD!|7>r?a)dk~(?P11pb%8z_5K?sLZg~WoEY#^P+<%-rP-E11703x)9sVP5O{gUbmOr7oo246(z;?564iaiaJ^uHBWIhY_FR +oXXvY^!R%CZO*NOuz`;l*0sG@}J(G>&nsqh(G{kHQ3(-(o%HQzeVClAyQs?$c8RN@6}Ih9=ACjkV|@0 +hgif{Bch&yhV30kr|ZQ6L|=^_X4iowD!?CO7)Y8dGMr$gE=UJAKuT7fx?rLrJ(d|}LJ81rwp2+3M`OVx^3W+yGZmXVUisYz2K2FMl&i_ +-ncu-EEeIshj6hT;6ji@27+{$R*GZ?POtfjj2*T3V{O&gq?NW1vrD-k3E}tm0maUoM{`!04gL}brLyJ +Ps@$3TNXwHr@{wZ$jowO3DV=^OMFgcOSl_YHd`2eCPZ=0`6MMaCEu6}?QuPQi@*pA)|zF^v-DRhvfb7 +%BGof}Dep$em0)2_gn2nGrMfm$RNOi~ozjPGtSJhO#E;h8U(sK_OoedW+@YE(%yg50#`H|=Xg5bgAt% +_pF8qRZS05?tp2OZ*48Z~MP?cCl`7@}x?mzqMdM<}#o1h}us8yiqm$AtHgF19FmA=MyYY;#Y9(iWlYsF&+hIQ4G2T=O?cRY+XWX)YIL{T=puoTCd=Hxk*6$dD_$_kQ53 +~@w;_}oK(9mk1ydYNqFd9077uqf5S=ps9dxi3=}V>q!rmbT{!(}(tU~*cQ3!|5rY~9vf+}f``N12;F0 +gwJ{X6gQf`tXsd&!|?*HB53jq-pm(1m<>>toS$C(HkzvbSk&+*tZO*Zvl@>TAbi={a`0TT^;}_wGlHG(m%Zz#@MyGqe3V7S;5e9?RB)8#s;; +Fox-Fp59ZCw}b+nL>UgQ3f4>qZW2W&_;O>2nAsBEBsJLm^je +NrNMvL2<_=*AlHmZtHdPBKnfUQn>c5&T>%-OVA+0o1CPOGh5f)`S4c{vAAn_58j2s89cR_Z4I~fM6xS(e9`X +W&o84-K_WRdUJ2|sSbpt?zp5fq2J$NB5@9MSHMizKYnYnrMjU%_AQK2EIVW%Bzk4tmI{Qf}w +=|Kr;Ea^;EP0zdB!msUGMef6xZZ#V&{1yB35coUy1VD3wjSk>ypjlFtajP{*97YM@vZ+UnFF*>IqD#J +p`#ztg0u7&{O)aRmvk!s}>?g*Y?DgE@@Q;7kb~Zt6n<@tskL&|)?jXIN0%K+mI&)@*ngR5bMK*(Yx-T +EIqB*$P$z)FMy_!V(QlIAQW2Q0e5Lqm1_t0fbHI3}foJH=L+O$QU|$Wi)K{r|DK7G@F^VbhA5UXxi=vjGieS*0$f!zbqNly82A@i{cR-b%?0aa)xiU#&?M +~LgVS(8R(a2lO&_uTkKk)s73t~U`h^g2q`}^M{%F@xGzVL*J +oie@nU`&GnAK{>X^X{Rrar*=I6jeNB`DR-vm;BViK&a{o7d*jNJb{%PR|JTUh@6g-mrS)!*V&juKQiT +nsh@Pe*_s^#gYIBFV~J4;B{^g_*VvGT-san_aiT%wf&*dbuuV)QQFqE}yeW{hHh2Y%ao^Qd?UO7T=q_ +SZ3w2QyD^+^^DF8E@zp(|U@~FwKk+#rDrEwl0^jrzY{MKl|(VyfhOgQ!Jl<|7F&D}**J~e}?Z+hZxWnMTCmiQM@^;G(=oorp<9)x~Y+Y= +_FGLb^ks^uAwqwvwk_u6NpciVteM-`R(S*ffk2LJ&ZKIbeQMWfHXgBAPZ2afgVWhbor?P{5i%hkL_0C +&f+u`3$riMAtF=9@7vg^H~7bn{|d+#LF|5EWWo=)UT`1s(>jp@B#XNY?0LQv#5ONMkH)CJYX={uhmioOBp)8zsVY4bxe2;Lc{rK&f2`zLb{TN60ENK+}59)*9}Y?%ybYkH3R&FZ-POLzv9 +X5YBYv4xHwnrf#O>uTdmiaEe$WPE%{qbY~ZT0zB97fX^);DV<)IbqtUyCl1-n&x@8)`Lic@Kn>alxn2 +(rVdgDdoPg#v~{?ZN##)^J!nc7cY?rgtXGPq$kdA}5rmwH&>D{G +WLOISr4TOhG$d&*E5`B(TqPyEvYYbI3z{@Dp{U4P(#q +vSVHa19}K2Zull^Vh%s@4x>~leR_TE1+5@Ov#hwPi4r@1 +EeiLJzc`O!SYj|FxitH`_)!H+f!o)u5+nDzZ6wIW%COYY}|10`PELC+Ud`=0!_u!SN*Fz2%wM%tCx3# +_B^V9`l!MTq@gQVZT}~?&6o~Ke??w!px;6TP*6uPchyxK1TTg^{GbY`Zyg`P@5IMX +wSUK-OACU2U{V#9$pBtz(aoQ1;Y8U~z#^fyzL(Jr>ogwn?N10kmQU3RNw^1Py1g#vh6>^v0=LDb=6tsv8Bsc3ha`bDlUD_yyEYC +!5BXsdT`f=;*!XZyLY*3aSAtS~T@M4EzK#8d_V-!k(M9u9$^CH^p_6E^t(ObX_f<*GjD>vTi*<_-3iE`?kiZJm_QjGz2%tM>V!&FCc6DP8$~+AEcd06AOl$Fs2Ws)0Fg=nz=`oJWDTS`vhANU^Yv8j +CI_M6Rb5_L_G?@0)2GHw{pWLbP==Xo=4W{!;SO{6DJ5Tn6I#&zaH&eitG!$sz{D2HtH>Q#!IPaF*wM} +6j09!KA?1Dq@nXp)fCz+Xj^zFoeuq19Li|jxmp%bPnidJgdau}GbskZ1knwk>l4CwuladM+?>3PRtpe +LPE<>9XKd3_D=K6J`NM3Y_7<}N=zfxfr&lxc}B^QW~b!IR#GHLV1ZG4Am=aP +Ph71v%upd00D&UsI;7#=>YWWKXncNOZ`4egdt5B*3gO6-@7bqJJT%JdXE~ZjFfm&Yng-@+G-FPgl;|# +rtbfme^Y($*UHTuxN<-MHS`@GujrF;?8E5W;E%b_boHEgn-Wa^;Ctj83`9lDq$>vuvi6-y|PzBMI+J{ +Ur8iwtcQ>G=ln~neA(C=GRRCF<6aP;d?dzMV7d47;Kvxx_OH&op)z7D@YL*HUKWtyTmT4Wsf+hvxa4DxA?tfdAezmTu_Tmwj@8%_6P}lqT&>Q8*Bm@ ++TWK(N;#t*D;QN{PiFI_{UG|ag112(N+FbaiA;ZDU%faAKNOFj|Di!JD#({jtBkGlKM#NrAre3YMi&kJi{8cUv$o1Y1(;Nxex9A>8hi>3ya +sWNZ^T$D#L4N_PI +*sUJwS*O>O6YJ9T_j&-)79LI%8B$gNFcDxYliGrmSqp^ph|f~;tN>7g9Jdo?iIGG01jVf)W`*5cuSPV +toKeJ(5gYB=~c?pKx>VYO=(>R*k%c?-f3NX1{EcxC68rCnP{FYl0R$^$zKn)_+$*Yo#w?T_ysJ|Evbd +_MmA-YaO#c>*a3O$}TI?5)ti^WS%}XrO!(gZ=_WL(ii+|EnscRhKy+QMzp4equ$I^!_}TCCus`1dxCd +a2Sff7AqQqNRgzhrz}kULTanbbkwvUAXxcqpDUj}Cz=4W0x_0z13DOTo}IGLbvTl3G0=qq)tH +^K@O4NxXUCcoLz-GP?yuiS>2=Ct*&&aMt4T|MekDyIAerR6aScWKJlK+bt<1~b2>jeh)Ss4i=Srdb)M()PG +>y|@x5-OcwgAG@p;NU}7R8=I1*~SP$DQIp+tbn(ve|PWxwTF^f$B}r@Hk+xai=UWJ<0ySobCo(PWQo$ +APq^s-NG4)+Xz~~jUcwB2Ja5?Ke`D60o$E%zdjtP8h@mNBh?Mmp)ZA+zjlSTyUYMyjCjgI&?os8O=<* +e6|*Efjoy1+PqF|)6Eko!u-hlV}lf=#B^)ah~aI57hQBk`sg>-QOvTF;C<+?Bpoy!x7Xe3n~9b)t*xpHXgxV0? +k4L2NDmSvXJpC!SGcA*zSa8%2j%!-CpK()gT-hYG7T%E9hyi-h-_-vVPXvumFBjhM||_K{27=ghPLdr +D5au`L@F1e;$ZGghq9S$25{SD>>1p!Y`I(z0k+yD&}cX4}{kP;X{80yYkG-y1_pEi<1|Mk+C@$c|Bp0XsBEAlu +Q@X-Lb8Ofn%0a7C&wx{P3vPLfDCR-SDTp>;?26QwJE~$P#T&Q$*zJ%E_sfg+sL>Pg(SMv@)qMR?y2m_ +`uap;-FI&KOVj0g}p%v8BUrsSp0e~OAh2`&~R|wO???84Iy(Hw*5#WLqiwQz&Z?%UZ7w08_x=IH_pbh +>10PhnBBDvoFPw*27xEzn>chf4Yae78Ol#kH2T{FXSkgg0XDX1@VD+b&uHCS81j_T;BPo8(%T2)K1M) +ia(_xXW7THchaB_SI^Z`J_4H{b#WO;$GCf|$^1seQiewFJMW^4zr5MWwbIu#U?PA}?r(xf>mx&i{02~ +Dw2ewX`MXzd?0@q8u%^yuF++}rqy-4sns=+v)__+X}d#FXDzO|*${sj@W8`W44j+$ONpi^El!9p`#&DI|x1X^}>Qo1qD(yyJ}wpa&axM^&#U&`9tcL +oEr=Z#nIpTrD~DKKhKg?4L4e6R~(WbRa|H!LMX3Hw>`Rh;~XWw=*CkP`aQ}P9B7APhg$pU ++Nm5299{Qi4yP>8+j^GI^#E$86#Tm8<@(+xq0!#X`@*3=CDB;#%iB?UliglU)0>fn|FR@TS1{p_-$ui +|^?m%lvth>s_=(4e6)f9qWn;5U|3LD*n7`72ruWCHsJ6}<5u)!;StPjL-nXw4&%ab_R~zz&^!WT(IL5X#cb +BY6JajeS4qdzbjYf5+b@S{}00!_ex8WC$xZ>_@=_)pRz=kxiyHGC`2j6LFQ1?P@pKcohN4LHQk)iQNI +-N&XlMD`)9SBYKM`C+wdTrZXDAQqK{cjL>Xd! +9E9yB4eP$l^(H6h(Aiqiz;4ilxHu33-F`y@yyt6ek8T@%6U`1m7~1sopLqGnyS8Sm0!C#L4!D?61UAU +}){FyL9kD@QCNuf)SU1N115+$XL3sy4y@;DGgBFbX65m~H?N>J%KWu#2cuwNYrrfF(Dz>Br7Ryq`Lxr$*@!$4!p+i@) +&5wK25(V96Kdrwg(WN?9I~$NsTJ7SSAwkbvO`?=ml)s{?_V!J_BqPvFs=)wX7WA9rgBwcsX@3&nEYI$) +U4o(@1Ss0^`rB+<6d&1|BbUeJwsL@XX!7p%Zu-vTgQrZtr0McOuka#|v5MWSb5#z|UDYlRN)M=OLR^) +@^P;APp8>KV^a1cU93!EXIX^?daI}kcDBVq3?pDf!X(v-+%(P`Hcs{wa$a&;jn7{ltpEKO*3B~j=|UN +o2Sv)SOd(2()fa4J`p!978q<58mR3mDA;5{SWZDV{kc;K>Vr=WtyUt=_1c;1o#zF{AJB;I+jbc*n*bv +Az9|~X9o4OB_N7VGgdlv{rUAbj4eq{5M?UpE08NDNU4b;fTbqhi=4#b?;C(3OZoRFkn8<-PPxy;B48kmV#sa=4vuQGpu8j*6ft+9A6=aa*dQ^G89>4)h0Fy~**6g?!(^jiBq<0G~K;ELW>BRXC +4$o0%EE0UOxb_PK0IOMxF>z-s`gRGUQ1q3M{1$eghl@N1!WbcecgMa50AG->GD-PYo|BMBN+e0fz{lJ0#3Kk +QdaiH(u7iP1W+@6=!MlBEo(4kPDGhSIDorGmsL+1tJ6{o$-9nkL8XYMc*tR}pS?q!k;g2Z0szA+$>uq +{*C>1oRz>!cR&kmtiUBFD~HT)6W&7)!<AMtE5zg?s^1_YA+huKH^tA1gTMde1hw38lYdu&Q`iDZjWUYYcL +O90`CsJG5oIKML*#W(<0tio|J8b@$CSF}hTrmwC-lU$fFmgC +tcvksy&VgALXDo=^#)vqB83EhzvzL8^2ZIJ^9O;j@S3HT?84DMmeATOM#-V#pWU%DFH`?lSORj~X=+i +SE9iF$_%B9E(KpT@@dbx->^oIrw3LkxdVcV^tiIYWGm6`y5W)92|ke!=Ae7RY;k2L7;>2jMZ|8F3!^#hoSdRYdYw6mn(=>Q%& +wbSZc)8@fH3iTFwEpKADx#r+^yv{+{vr4&P^V$vZKh=(=*RuQSIy)OFnfHU(UfDCI)B%s#vZ)?h2G}s7A@oc2T2(7ESt4}Zt7 +lA-qkrv_vI;T}*evdyHBYoLvGixnhC-eXQ)s-_*2dD*v6xgKC8|kJ6FGpwIohvvJtBiUE5|RIT%M_NPzH=76@Oe!SPbU9REb$&!GuC(r1saasEP^zxbM63pXr +?jZ}E{N-qjP=QWTZSqoo>a}c?2LV*G@bfb!)49y;kvv!-8jb?-XYq+!JzsXNnvsP=;kRc@xU;D9jkYX +IWg!GtXGn}*@B+G#(tGW_Pn&0fPN||Ltm85^739s@Y&91748L8Uz=R-m*YlpYS%J33=3p9^h*x2Qrfz#J^}<(|_#YgPI5FI +)4&N0a(L!Y4eT*34DL{F!f6sRHOOf3VZ`jqa5mLQU2FQb)Bw3%%8u$WpI4dIJO?kV?K2V`?B!Unx_Rm +%`7Xx1F$XY&}zBzBLQq^5C9y0| +@!W7R7>(*`=3!;)yX)s%3!WHw?A0&X{^=q97}TQ?h%YPnV{(&zp5_!?{um!UcEvvtroQf>{muKD!$js +vw%~KyvTSW-NsK5#7;6bwhXgOTKcU;S}gIiFg+20-F;EJ^OUT6~dqtGI~mcZ~075JMj}$=@LC#2zj2f +SEYA4cvV94@fX!HZ}+e!df+Q0UPZ=JzFoTGO%JpO(sUB~oi3F$NxVFCdpMH`#aG!^m)FrjcaHub8n)f +))|s%WZ`wHpF)w5b$Gx@ibidk-G>malVKCNR5HycIcpjVOhjER +W!5Nt!5P6M4FE4+w?bFXp+*o2NMJ>{svjdTmW_jAfptLEin#bKy+#u-xz#oG&2)laIqQKbgqNE;-j+! +eS1qjI6N*i;-sS`7X*tb7Z&eo9mP8bR1n*exHWR4p`>FRu7}y|fSvCB4$YTdZoFzNlR2QqNagaCO3fM +Hn`HscvYY8VpbNz{WKd3qcvbHx8sFl?s`lN1LpIL%%CVzwmwBYu=bh2M^zxb)xQ|d4!A}47yWj^pT&7 +KNcn(S%qMFGBc~$)pWZR-7GL{3}N8+ed;MxP4aoKQ1z+k>G4ljt3d`8RTYh&%Diz$rxx5nDt}5RESJ~ +Qtk^w*rqzI+!;nLS1Jx9n;*!-$&07^f0Fis5c~_rhSv79jv@qNcsh$WdSZJi@#nBJG43E$#M!N>;4t0 +JKcXlkD8H4#%Kn=)>!}+f<3@b&Z)G>mUglQr+-jKj$ +A`lJfeKwSLnkvK&M^eY9Fx(@Diq+W(RvY4?H~->EKjZbp7F+1NqrhGBk-N9J(@>GbSCn${Sf?`e_3P( +odf;Ez!p>3+0Q>eEc$4ZS#yNh`!IDry`>7yyBD@N-#$;;4WAyB(mjo&~a;P?Sxv*R55(U#6*+m>K0?h +46t;OFt<|iyFr5?v>%vr8blc_y*j*IZZ%=6C!Tt$%GZs2~U;18{hi{YbJgpch;Gr|!Q#H +}mczTgeQ|oSU@a6vQiok*p5aG<)wU*uX54s8V>>pN}OG(ys38Dn6F>LbW +4Rr5aoHVZCVb!7fgmkK9O%;uR`HR47mKs8E5`#dfID_iWp!k8f{lqPaMb?qY|Ra-Kwd|%sBHMn6%E_K +24=WCi|Gp7oAb~hEtF#i}Gm2f!rFE59w(|`T{x*4j%onvJk4)3z$Ubh5>dUj@SH3M|Kb^1Bl+1lnRKP +RczLNZz4NOpi>*`jH!oAK_i&;O!9Dq?p}e4hHNzpXOy_o7R)H&-$h2v)ij=!DuAh4M4vSoEJ=)M8t1h +JpR|z|GdyFOq4U50ucGy#Nrt++OsA+kW7?sqs+y&=wLSw3tX5P$63>*kArK+J!ZvF{{tWCxh3!QN4Zx&=d<250m(-qeF>43UIGi{ncL4 +tgFEWD8IjH!+OfWg{uAP0U_Wptr7t$KFbW((i)Gp8&=Px%mPO&a)Spd^(bMdgz!#!nEBAuR|;{7$Kbw +lGKHk5#j8ez>xEYaMnXw5S;AvT9px$f$Z$|NZYk0#JYH2XXaJNvjl{F>R75p>EIEatp!{&3t~wR7z1| +!p|!k6PDQU^NO8{QgzbzS$9;P^qp5kRChArKyRHcK)Xpa#T;h=h`%vqX%e49{{H}M(n?v%E6C?(w}K3 +?Ew#o}?i1KLIJ9e_@}oO#E!8OevoO5Pqe`S~t(WA#c9^Tcz=1JOHAr9OMn30QfZhPT#jgFrWJ%R}ha< +&+eP%zi;gjU{KmxC)YgBrD?Pq6zUVWjkJX7&fR4n26)_80z1piI8h;u{X!>37Zw`KUYS) +yaXH`yRfkUp({pQjWoNQnPf8*$gM<7Y!sDcQ*q$$Az}ifv|Oo1TEpIfGxPIG|K9BQ+FwOR$H$n|yEA| +tI${E&`+OzeIIv&YDHMtBNe`$1kC>w9UOn8)y|wBHkXvA3s@q2YXJTm$2us+M%7>(x5KT}N0@eAU709 +1G6%*Y~eVqdby=PBFcuJdmFIE;EpfBi*n1m=>!WhXP^QFnajBxlsK4My;XrosXml)Rrwo6%5aQ;%+4| +Nc}g5KIW&1+KcZ3aKpAlOjSJy~!gCLlVGvV{qzfHKek!iHAlAvDd`S&NvED1Dpzv{V)jM4(1YO%(mLF +O*Y7Ip`i^;#E`;QI%@{s*2(+Uj-20Md#%VB3^1Djb|L>af6TU7q-DEVAE&|^g1wmDRx6tS0tN8mG1pm +@b3qkvqIiJDNE^K$T5#svZ&SN8goz}koQkDM0dIi*UGMP4(yu8Ix|%gjZM)D4q+Tqy^t+HWFm19OzZ$ +QrCF%VKHP0F+@_r8=;LFO>V;g;kqwv~jz9{Trs$7ig(g-LYy)sK3762dC*q=^o5g)}lNwhlfB-_zDzj +Ml-BT@5b{B>`lOzV~(>m2t#9u*G#o{c&5^43L6Ih3|~@6fcn$43U(GUmSR7qzg{sRgk(=$ed}dMMVWvDbna=xMQ8zi0I&`1p@^9;!!7Ky(@3nVe09-U9;&PfysIjD|m&d{uE6_;_k{ZFWVdQDHYtEWVl~gRFi%mfQ5q$g51ayEb+99@Gt$NdY%M*Qjpjf%r1GM;u +3pP%x5}$aX+angM@QmP>UU9YcyvF0#ybVz5tAEz=M_|DwAQBQ`qiv!9u6E+MoexLy{LMhsIttP%7SnN +<$!i=vs@PtKTfuGzp>CY{WNUg_R@k2<6cU(^`V-hsIKmn>3yccdLdP+3p|}L26#nGZDfwBM%5b>__QD_@dc8KjcoqAXAb?FqXMIgeO=j865 +XQ$9QNl#Txrzg^5&caZTgYfkF`IP^zif0dPF5f*Yass%FDbc8U$g8yGO!2H#vOzckzlon$KCwItB=MkyCX}d*m{jQ8oD=A~xYq{02cfB0z3L6J$X2?Sul*BDzu`# +2He|@i0<4aTLmqkw)doSZFzjvZq~?Hjdm8r?F|kk#0iMoA@-SdsQ+ZHa=cTI(RXZ4-6qz{aGT}g$5-J +BuYZ+>VTNs+tsT}CSXt&qKrO4oTsk=oHx&@U2P4c$cJwOn2S`Y{xdM6e5fam|+K1O9iv+@Z>B?D}B%1 +^cHNBy7P7~D7&3bzdqS>?)sXAUXX#ijB0}> +^Xu%qs&^X%VF+8YtiW~d4;Ks^bk;E?kWYv1fQ_n?k3(+JsHJYvs4bxU1zthJcVua@<(eHh!a&M^hzWm +UX=L~X*cKGnFQDaMRlbs6FU%_Hs!GxU%LuWNqwDWx%*VECvub`njkU!a +-4-y#IXHm5dNPWQ@xZ%KeA*cL2NczIe2w@EUMQT$$g`(MNH*jz+NehH-Y;b?@UJX)OT=_J|G~?A#? +)@Ftt#AlZDBADBVq!bY&IRo4~2PD0r;0tSN8eEYi{JF0RMF^5dD4zFFIop2y(M^;xP+BJT~C=XLWt@t +=`+Zv`rTJT?enDw%G`)I~WMb2C!|+!XjT(?Vg;4L*50d&$-gu8k5HYOiln{vs&ke!(rgd=uvFd==ES%wsURic@S1BpdXl?Q}*VJ7 +q4X%gts9GZe-u^s7&(B86!g&lNmSKyu9q!sHJdpVfyi_D9kRke7vr1A +!?6IwcGT^#7{_?(Y@mMz?LYogFQ2C^T#cb>F8%??V16%gi3!SP+>ztN587!jITe24yZ+Pl^MXFsRz+X1Cqe`d-?(w#q +!{Wqc0VCtw(x!tZz`mBr1S%hBl8=6#prj21A7nDPfo+UosDLqKHMf2iQ+e7CRGCY)wmT5!QNq1s;OYv +NwQM_Xbd05+tfCx1{iBtnA9c`&iqz@t0 +%TM?jWRU$SHxCMnf*^$PNX0^5XIHbY(Im<)XICBsP&Bq*OrACwR)6@qSi7x(PF<{s$%XE(KOGW^e{K8G=f{R?UD +_?;`(QCemp*29QS%K)fG)%8wS{f46-lje2j5#2>nHkN{PmBfEHhJ=nRL1W$Uih$d&RAFyO7HA|i9#rm +eSPl@HU&g +dA}uu^EA((}iTjYc3Qf2NeZuNXvKao}Sdo9_4o2uyq-E(V!$_d=ps#vAG*MQ@Yd~l +^n3X7EI-&c#F?$jQ0}q78sV=Lwjh(kCz`#XR*8nc(Czy5hb~Hf9R7dNmk#|S6Li)05%YiuRm`q5vYq` +Iv-geE{fiQHavbUKxy+?-XhoZc`lZ_2BMmXSSp(3JaB{z6ZV}Kq8o0OIR`+v1LUPwpS77}ziyK}KMmA +Ainyh}w!R~7hwFJyC7Jvk7XgvNbe1VH6Rko*GF$i{Yez~LX&IQ5ge7XH45i69L?6l$~kfK0pB2%9>s+O1*b_ihTu|6toSG{@{ROLBtvcI(C9|BGB0T5J^GO+9XFP90(-!AG_**6o1Tw +Ln>fya-A2ko6E+SYg2Ky3^d29u4k!XQ)y`vXZs +2Z^%loy4M@*TN$Xr{yx!dzw_=c`^#Dq$TEQ*cFm2nh%=oTy*F}2b{s%O15eu6!r2k*o)k@{q@8!^q&T +~$2$Fe-%Zx;0|*rMX1&11`+`&IoPtAV +OnqUGqYY69(1^*K#*fvmT-EzbZ_PY}vp)S-xy4c!Bc^LwjW#Yxkp=3+95G1~6sB~wuLBG`WEyw=l#^2 +r13f!_#3f95{p&3Q{#a8sNo>_$HoP36_gHCoUezquH|h<}!z>6#t}&G^{qhUMjJVa>f}4NJ-bwz5NtZ +75j_sd$^sv;ojSh~yAYv$rc2=w0}RWCtGy# +01(>j5Bi&EFBzFMT6x{KOvH*88DvrMXS6GE#r*Dx>wcusKyHX=7T?Fm8xh7+OY0T%qJAmqZcJHpC<3e +6RV1BYrpn^Vk~$-kdt~#v?BkbMWwj?Px79xrfA{bv3VE1Mqsp{7cAixQgcUsM@v_%((^x(9xi$dc@>O +dJjLv#o-5Vx=dN3X?Fz`BVFdTnVu*tdoPrVkuEZ?QOke0ILZbphm#*h^+;*?lI#07>AV>PrvS{@E`SY +t2URI$IXJ2s@B_{eY@MzTV)poG!`?QVV+<(9kNAzvOCfs-0=A>poGNy8)iA(jpQ+51RF<&N9}iTelr3 +`{;|K#6kVWM8oebFn5$F@9>WeINehoifU;IQDUVO?+Hs?|0(&g-akzRA?PFYmBbXhem7BKcO@O!AzB7 +e^Do(yzM1NK!k2vb6GjD`b!4ktS2Sk^>ooRLFEul}~$Rg4>opu +ebf9uzyvd9A$q6`p8l;=)sZyxz6=J90*HKf<({3plQSBlBsQVF$Qc(`p7b#zR1=^20G~b +p+-y#zV`YDra=pkvG-z?jnEIYoh~UsB2tMozy(dA`bMIkiof9FBL}HA^4 +pJ6+gv$5FXgTEC>HVQa@sHD80@sqU+fe0ilWBKaI1<+x8w)B_RAvEFCI{(r=TP;d&12^A2!b10QTn +JBnsX{HwfL;aMMT&h65u^O+G^dJlcC&-A&p1#MWG2(@mzVC{YemZ)P(1mpLsj07y&B1 +I&F(L?Vt}pE?=iVy(Xtj~=*9Q}EZtU}KUS_P9fE5|6-%=>Jn6>O2Tf00Y2DI-Q-y=I}HVFdeTc2VUiK~L= +iMK5ngl3!Wr~~kvo%4ih_ubBeIOM@eyi%#g(7&shGZbzT_W65fGpbbj&MRn)gMPa!I9|T~``>LK +0r;015SfjbRw;kjRO?ce4vrj!wjXKI;X>srr5B5>Vt{Q~I$-r6m(Ozrqu2pVC&1yR$oJ6Gqes10SypW4YMhzCdmlbY^Imof^ekAV=t0;#Dq!LdN1Is%>oR^_>P!H=^D}%K +IW=e#T9{XxFD!h=~}*49K1nHbxK#&O8NBa7KVHA$!|RR7=4W%4$Iv}OtCbsT$MTkv?)C^T6|de#|CgP +zInuyNpbarUSAJGMkW;{EoAQC{qMPruTI6dDpBfcE5CgQ{+#jU(o +>AiWU}fDpV+Qh+!mvq2l}d2F@haPv!|3?VH>PHr9$ +GB}Z1nDJ3l$}OOCe4)P};EL{?W@VTnzBq?b{l( +qz0yf9F7NXhaxFll%$z>Fn-mirbmUNRlNz +b^!miyGgd=?Nw8rmo)jgz6fc+JbnEOS>{IFO=##AH!lWn+@w7~ijj;c&+h6GlzdPNmn@Q)xgLBA!sHg<5>L_HkbTEDDU= +So90uJ3jAf3BEuN#4|`9A=5+cv~{kRcoWix(C=@qv<7}a{}76UHzS!SY7IwRnS`=r4umDWKjX?s!W&t~dV?<~4ul~n)w$B$=Vvir;B_o-#9o{HuVzy9Knmy?^PD~@Ha2Sz+}pEQ2i^IeilZ +)N3HwSHp=RNmktS3s6&KZRrJs?u6@VXeQJIvLk}DGt9)Nc1ALGmMH2SH3%klRl*c +~xZ)I6S7S%4!T`a4o;N$4pXX9Wubj!r6ox}VLy+-ggEayuPc5RPJ#H7zsZlyGBI52ZiZRJ?_HX%0g5G +)eaOR1IZnbZ0hwiv<&v^sS8!RYqkSSriZXlbw4V^rH|)Y#m(q<1K)2*kvSvX6ec$$B^lm>P)kVeYyp- +ensc}IW5RQ6dfeNBVa&OOj^rr&|Pu)ilvuyj?m~)$4go6q5M@$W6bSPV!)iMD8Cb~Tv&lWec>G+BRSC +)^M1S*@vB)RP*9^Nog8Pqru*=1jNGkj}eu1K2^07{#u|iVAz@Ir&(?BN-|2m?3xgFV1Vu8Sbe1bloxHpluhTgQon90d)gJi&jwW0 +6g@r4>8cGNfXHj2GNwOrnE#x?Xn|F!vAb$!Z&K+}q%a^W0P7M|!~~i4wzZ8+Kp2euA2B7F0m&AswA!l3~uH;-={fB+o8w`AiS|*U=yb`PNotv;-Nn17V5GsjN2A>}C={0e9WXf-v +RW{|hf8<~P$c`At@~r15hk$b?QQRMvC>l@w)hxnI>4f-v-?aPLKBP3d06wTt5k0J9{02sGKGQ;-Fq4Q +Y((^K#1o+t6Lm>~eN>dozw_mk7d=ou^T_I%0aJMS7iJ%>V@YB+sdq9fIu=SI`TqgSfv2hd5uZsF*2J^ +%7hx9LNGfwM^6MIhWIP0ULV@0))SApD$+GIAdNjbGGGu79FmUr +^c>Jjs<$Z%W-V6*yTBV&BR+AB)f!~l)^^vQl?&_d-im8Xw9yTFS4N`hN;XvEbg5l*7-H!&2jsLNiZgj +--Msc-L9Qu!n%8n-0BK^a4bqgVbf44(Ywb6Anzm|=vA$bpgFeCt-0}r1vt8ui>*H^N1okYXYl`Oxt>7 +nwYkyISG0N|X}n0Tn1BWXbrsp!9Y|I)$OL8>g8=IxUzIdrUXsv4T=VfoCV^Vw{qag1=d;Pi+Ih|)K;!3_&S=J%ykE0pM&&rcf35zK@CY1Uxsq3<&K&y#mwLAN4NrdJOe1L=T7MDjI( +3W!Fke%9}PAIpvMAb|8c=$4U>nTTkVj=V<;Ao#bA`ud}o35Oz=ZJI%7Nj=+iGYH^Zy)n}TT_%(9b-q+ +(1L^uT2&AVFu@W;mknZVa^&%NyGcsmw<`IOa)K;WFj|37QlQC2OO!K#wohi8Ofw;Ltv_Q-xK;y?&H7$ +qmOsF&Lc7nM&3k!#mlf+B_beXrHtQe;Rgr>nAb<7k%diHzKldTmiUqH&rRb{>Y=FikQK&B8i-xvnnnUd7_K;~FGu_bp;@`x7gG1FErO@(nG1C|QVNP}Zwom}!jcj +k@wx7EB)I&f}*_qxp2qp*!KkOkQM{-$ZS;5`HoM`z5kDXdLizPkttjS-qq`Rhc( +C8*=SHU;sA#o--C%kXIzKAg{U}eEQI7HJF`p>hyeRfu!Ub2xN|#LTPpr`KSs6c0d#}g_3z%ayn-b5J7ZEv{2krj%5>he9PzFl#_0>>MMEe=1J8mgRE2vnHZ<7~mhxJ +skI51XCuJCcg&-Z1t&IOTyG^%dwV(rN)%n;tz@`m7rSNvnkGnV6Hf3SI{U{x!LSF^rti`Rl0%rAZIdo +z;;yRH^EYg9{6pO(KCw|+DyVKQwF}7OeE%&m9EE8=F86k49A$oT%uII=8erb8-TWD5v)aVk5OL;hxY| +B(<$iZp$B4`#wPEJ09s^`ZB3$FJ+@SJ)Nsyg5J#F+v5d9q@rTKXPiHfG|a@4SMVi7z>fADiaEflK +p1=T0g-GU~;p-k+sq8=#ZNm_Ce;OeOoKN)Fv;N3}**@~%p(9MOVs)DPN?c}1p$e3p9~xAH@FWT}%R1| +?aYH*i9?N$3xz!1^ea8R;>UzqG~7o}-BDD) +4cV&i4uq=#k1JiT%o2n?LJ@ESM~>*0h^mXMEAgR{^=e7s7_)rDOk8v$i;%3w0-FKpHiT7Jvd0r@H3nq +_g56O+%BAA3h!s~VbK7;z**e03P?MO~SPE;~o~>>Bl3haAs4JhC55TAC`|;*OA~46D{;S;lgjZ8-ZIy +-GxDNVD9F-y^dRHa&<6hW=7#6%|Mo$ZYIA5F{rO2D1JZg{qV0Y6Zny3tL9g0Y4eV +t3Gv1d8eiba4^t4W@4p@f@fe<&^v^X3*g6J&Q3r7#0Ec=FHNHOYMfffk^`=@aSDAgQpFPlACK-v5q=K +MP5vR{K;51(Q!?ola=%f|Gi`zPF*e&!P1AUkZ8quVL9JHGf^f9zgdVZdkTKIcUCI)2)Y$Ih;E*Nwa7;>qRV`~pwr43#O5RS+XWrtWWb@>&?Q~>2(9%hMAom2W|msaBnG +|-b9{`WOr#{z~H(areQI3zr9lY@2~dZ=Z&NoefUgT`$U6)Rn*i}VVY4+G!W)KT5 +i<#>@^Uyr9(9)uxoGg7(IC6p1wMb1LcV)oxe{pM*nn9|TmJBt_4@QHO*KCLU`yhdQI>6|e!c(T1zZ2E%~;DmrwoQV#v;f{KzL`ahN~c`d@B^8>0(%ElKHJhcnJBMf6EO2PW$ +6?^PpRg0j5I0~z{Fv?jT35TD`gykLyg|iC7qZ^1q}!BP2vga>_fzxkf#l(IKp!P_>FpkJ$PZdJ+scA) +pkwmVcfjJ6+Waq7-pf{ZP6xtJ|FO^|YH{m79)N!tUO+cdc~SW_5W&ayP$kl +$&KI^;p#@>6vs^Oyk+$zqm1Cfz(doY>rbCiYS8Qb#Oa!)k{snWx^W2F^^#JL5KM218IO +y4;nk4rTmpl2_+JBy*UG)u7U(6&*;GFU!$w7o3I4VoJR+0fWAw%tp+^9DO{27u_HEy13sb?^-bO~Tfx +f6H6v7}2!w+qo(CO|7FlJcs@P|y0EONa1BlF4peWmZ +FacF0UHI7U0Dja`4N@xGkdM?tf825Tk#W!IxpZC=5I<^DOz)iZG6MM_RcHH5RY)UsA5A$l%-={gNDHV +iS#a==iYkw$>aE3+o&kE#=rcX=y{XVMq2cJJ8m`H~s$~c8vpJi8un7KT +JVJ!)n)Zpf80EHMLwU-C+cPI1<3vGJWq>Ha>sv6$w+wn!JH;46q$tjLv5h@7D(ymVL%dyp%y!iAOy&) +5%=`p(*rms#?;kLE9GY0MG(0B5)&D>Ot=ZX~(64rg_!m&~(0gC_D%tZM9_bQHA8FA`cxeVY&93MmmqJ +2hvHoc&TnnyO4P>PX*hu#YQjRc&r~F^d~A-np}O^K?#L?18^*^j)ngNmnOzcsWfhvA?nCM$A3kIN+5~ +51e=1vP#j~z17(-`Jy>z3gYNFmSAF&e{8Yv?O5-f(&NOsQdc20_Wtn9`A+3Qcq(f&)+4)Phm@WRinoS +qx{Ke1_V05)$eTa;pFVWbi#03x`L2fu&qjIK2t)4N2)BsBfLLMTOH9;g477I3|-{BAJtS0Ro$2K?B9> +P>C92vr=Uu^v4FZ++5lC4NiTRkYZqk_D#BG2mA}^BD08GHCv}Rd~1zaD;?SY4xOOIxb) +IstR=vGzd?vOFhgDB&KW%hsqlCd^V-y@%)#LF~Xs5y-^v{MKnpXDE<5Y)swz-BphP%kL<{~uxA7g+=) +Tv8`E9SGvx1iHl=1f@B@R+o{0*d;8-Gk+tv__Mgg1BfIDzxRU5OvLlAbi7dCrL-J}PRDx5RGLO1@H>Y +Ki&IZb=FFVseFy;Yr67KEXmm!Arou&Xf$B99tpIx2Cxs%j-M{gwf|t?c|O_{iT7S;*EGA%_Vs=TclSo5ZgV#5qL%|vcZxtg;S=W +Kcwb-Lid|3fbc{s-Tllir$<3)4E0mMx!C{+Ph_u=F!9qw+Vs;D_~-vd7l5~is;*Ly?js08tCk7P{j88 +MJ=D@AC*J7A*@M{7r8-!62~$OxO-x}6WKifl{}VPv)M{x1EFIVYOaGP=CX9+6suvYYh^nW+9|jp+Uw3 +M~b4lSG{P^d3x0d-&tY6K-$#TMEQ6s&ns^)d|PdLUjARJNu-1H7FrPkc;@lm=I`UuBpf+bAsG)cyOUk +?y=`E+O|OzSjWWVgcn2gOD1m0ZGIV`;~-)Br6ip$v +ibGb3stT>=-nEydczB7#Lpk;^)-j?T9q)h(tTd6Rk69zW3ogLh91 +5xjJ5!>k@;2vcI^?@aP2d|g!AbXVnDxom8e?8Hb@1dI?i9Eq3*T&#q{IdaRGhMW;N +_PutaI#x-zch`Z=mQSOAvnr>#JG+S4%1q6Nq2p|FIqvaB&XPSUrh1vKm^m0@sOv7}wui6S%1Ox2$*dC +(eu}PS^Nxvs;H=2;_#(@pccF8;wrfj;@zd_r=mr_rE29CfoHmGfk0T)`rWKPbvg=ql{IOW#AcAjbZ;@M5gfjoVxXM#gh_oNww*^-r5+UTviGh8fqG +bZdl6DC~BZ1KboX#fG#eYjM{lxefT%hMh7XM^)5Kd2#6@dwlp8VTtaykqrSAztD=@L0n@cCLs>Hq*pLj7X}n0(u5Fg +JY9d7pH!j7@?p>mSl!D|Z>09G;ZAtEf(Cyj17=TA70)1b%FU7yZ +JG8dVhfVd>Ep;813_gsGO2yxGD8lU2K#!IIl7AGxP)m18B50KpNMFriYWSGK}A6D)MR5_&6CV0h2~#w=gs9_qGMSU?s6ziiOy=S +?7B`6)FT~aL`7d}mi?VwKv548^Ah0bDpX;gADY?lO+KMpobpXOsPo9xyw+6}8Qp42*8B +k-)(I0iEvgpIakFUpb%r1e9TL7JOzAY2^|Pw8EJ^gbWI<>W8ehC=@f*ugR1|pp=>S{Ny;XWYp8AXn0N +)CjNSLx|p*Jg#kD$;yXb1kmRgc2{kDAYURNa)dN2S{5go&46EX3|i4B^n}WWtn7-^lltFqM)k7c@4 +iDy$teKI(YHaKiAPKqF5#us>%o_^4??b3KeO=oBH_M%Z%HF{IPYHm}v#8Pb-BC%9|6Qfbjm^`Pq3lsC +R}AZ)Q?u*-fHcZ;KFF`40&)UPe|=#{0_emU@`)6^cN;v(>Aney|pm|LHgBcD_YFFI$zU8{ye_;e~T8j +mI%yxTEh>Y|JMNsjhb$!FjQ=&+nVVVa`zUHSZ~>gPm0C=UXtTvhcWHE5}<2%Hzb@D0F$OeYDG6X^~eU +BH>3DjEyIQP@;`{`B50!spNLzJd&i6x@o9OTd7D?O4Hza+z}&>iASow3sYDBpmwF9u*JWs1;0(n3~f7 +UyXu=|Ct$Q5ZCYwC)yT{9HFQ-VFIFwlXhD;3exU`>4vV1$!4np|8Ql+9uC=DNA*KjbJ?0fYTyXTd&8( +~2>fL=hyLTCk|7A#z%r2@1}-6kK?n$Px!CI8Mw?8tk|j(Y1 +nO4(5cW{ADuAE!ndM6bLUFaqF}*MX`34$c@VKlvaJM#9{zPxKa;90B5rz(N6DIzdjFTwCO!fwNCVR0+ +Il +d-30%<1#I<^4IKys%;NFo4&g|XxZIUbvfk4^Om#sMs3gUKJV`1GTFC9I57TkL9y=?Rw9}JSwp&AZjs@ +{w!m(e-s30hX>J?9LgdJA|Nc`JS?)ro&hLUR+sb}EGNA2^;kE(=5dC`Qh7!93$I$jd7ECl*TG{LyblDRU%Ndl;8be_DP0uTTxlu((2y{QyFFVQJgK;8z`{-Lw0;`>vp=o9No(ThAPQI?=mFNxs7U%X7vN~VVh^o0VY +{UFk|4SRa}#(7>kbtnUFv6~c5Vziz^-;Qg)%w^;ghPPc=9#r88Gudn#0#s ++HVwAkTp+lP2SvC><}d00OAv@Sw^h{d%DAkDZtt9Cn;jby5nc{#@xO51kzV+x=3FQUW{OXk+3pE$CNd +8GBcRu4O7sI+qW6+jGKUVAn(?N-i6q?%c0=0HLYXL(LpXcBlS`XW~+B;#*MoRt4LH3693)lWJ{N^#It +8y4a$H!cY7Gsz3x>^4>%>M%KM}38|1C(bs4xw|UoaJZ<#-N^AaOHdPl@g`Y|bQ$Sjt*lz28a5;M1zP| +HA6Ta!$*42Yzz^E4$+Q1PlVvXNIrA +X%I6rCcDn-h+kdG_M2!vzqm?`=ml}lY4Ap908Av(|NrbV0303QeZW@vJS&9d?$00; +e!Om#)+a&7XGz!>m=Ra-}4nWf4iF!*mOryNH3mKQT-M?K9ZU%Ipa7P@Hx{AIl4(L_Gk{bvB$8K{J4aT +#5wnbVS3pjr~!l@#sdR6(?md5iYGfnGkiglUH63UYKmn}}5gz2`uSRiv|Q65T8~P+OnshK#ML7H=@+I +1q-gZOWZAFVZIBLZEMlJ6gDcXoade1bRbMI<(Ms0NDEAOZLEyVadZ~TB5ezKV$Y;gnl1oN}_yI%azRp +?m%E(aBV1Mcs@bZL|HVspUrGmD+ie4s*_xoydSD3%Jv&mH<81@jne(nP2OTvr5|Sh1L*Ln5LFY6+nfV +wc0yMjld+Hjwjx7i^iVRuCiG~iY^!oEW$i&2qVFvFzO2kw$5;OGjo5g_YVjq)f%Jn36A0bOB?D~2y$O +$b0d+)bi69KIL9Tb*`&_`+=AedZk41F+zyBA09vLuSs(lL~mkhdNm+6GS2r}W~Q{^loD;)UXEhF;ls9 +&-b-KX4%QPTm$nh&J0o@4ocE*kh-8ysIS?1RUjsECFnNz+++lu*@ZPPpTUF9ZCkFhxGSOp&- +)nn10BY+VenL($3#(|CF-X8xTl6bBPLsrb)(u_s#QeB%7PaTq#(b)_g!im~y!|2 +#*szc=tWk1C1Bwvl|Zd$x_GH?nyP5xsA{?ATIMG%$McL=O#@1b18S*t<+87Aj(5H8o2MB08s%@yjE}; +@-(T6l?MStGpOUsOqmQQ0%rgNY($fnX5+aZA^-;#+?1C$!UECJWI?J$YhqV0*VisYYdPo|Mk$m0#FNO +p?Nu&eNs9j4(;jRHx5lJQ=7YK0K~CgB7;(`Xe@<+;$c?rTE0Y@oKsdrlJZ0*htSa{$27brs`x2U`Oy8 +q-IO}|C4`M^bMEh5NKFd9nDbx34@_DzBHWPsZtST^je9V`=BaB!wKZ%KWEgasRrA+7Z9g548iF__qvs +`D(b^Z7M2f3Yq1R}8~Q>OGuvq+&W0w6psrO1xwSOGR=TA#GuY!n06h{}B8r(LK39UlSxO#3m__>7Xp= +}A$mE5-Zi>>a1Ab6!~dJA;jSuS^3Rkt3F4D}Kfq+I@EyqB`x +XK>Jh&`D$FFJwojtHRftbAYC2$^<@HwQJY+5FKz$ARJ<)T&A;+dk~1{0 +(JK_%s@HTQ<_uRL3Cnq^k=M+d_xOj4$=iEC9W^n4o_it|XBy2ja9H~RIKK19aQQ +%IbfGI@<&79c&1&8p#GAZH?8Ne0FW5)eMi2YH)C0rbPc)L(#oVWYyxdQ>Dhsq +qAe|o0@Kt(l|>3EuEUo!^$!JY6-kj*`4Xc6?fYI$Ih{rFDRTFS&V-+2YicbM0DM`b5z2Sz60+p?+(ug +`g?s$k&I$#Kf`H713kw268>4Ch)-nal<@k1v?+7Qr8dr%YOt?Mm+=1t1Pn^5JYx4ypZf2J9zL5zT*0A +(1lG%rsv^PhYO3Da^b!J>3xNz5bhMNyy3+YFt*j3tAA4oYt9K_K_B=P|x0le_@odbhu=z@>q=4;nLrF +sVhFQ)D@rjqhN{IUiHS!xsXtBVE=faBxx9@zp;X?`IBsAru60+K^W>CuBA*nGmXbipKJlu%H2z7ie7C +-KtdVa^&$s0v1jD61~Au0%7irMRo%SGJ!m>t2*OZa%9mV*%^WKZ1TkaF^{%|!$pDaXalaGy>1ZA$%nz +C}H4W_fb{q2@2x4iZf;qOPDW;TO2XhYefi|T@f8>kW7OqAt!nd|nzp><@GE@wAK-LASr& ++A4&HvBY+iW+ED`~sqx(kl#%UWG_rzN>6b4dN!N?Wo@l+#r;gC*GJHbr`qlGDnY`G#km!5Mw)%+74z* +g*gY@C{PVvU~N~-TmYSWQoL2AP{dvRk-RL450#(uSmn)V2UkAgIO$rg&QNGrlBw{pAs@adxb|yP5ivn +Y$Y3TDa8+T!FaRm=|NFeP;+jtp)SV;j|pWp@$Na@mMRy(?O?9-&LRO=RYS%)jfAFVViu^(2X=elWy_X +nhF6b;ev-nN5CUXVH03C_uz6Dy5)c5YU?=`+cqC;vqs!5FGP)c}fYaY;E8@i+`vgQs#@;E-lGCB!6Ye +>9SFWy+(Aq4^^eNNxOzoH217(A0=Jx#W@S{ek(@87st?mG_Ocv;%M?!f+(#B`b5oX~i_fw|%eo=?h{q +19NJ@a9z4ubJnzk~MBMU9RO`dm+kkF7@Uy7^xOsGg48GGct21_R?pV +E{F7!{1twm)7@K*WzYR5j20>JXM!oDOaI)9A=h)T%xq4uq)Mu@$8_ +y2e5sUE}T|UMc@Q5?Y?uRevL)-T9*Obo*V|U&U}Zr|L+EcwA_s6~C4qoD{aKn&S#97w^m%RW5sohact +=83}dIG+s)efV$|zG%Z)O2CwSuF!f{pg+B*o;>0WxW)FR^(ryBrK9T!xd^gnpqsNu754+f#PzdYR|WUMxBDXfW%13gPzptQ9`|#2|-Yi(KqxJvj})kKCZKVj)W4(*noeTUm +T}pza)r=B7-F5c+|q_fEplr0>#Rv^GQ9p2H%0Di8arr*w9Ao;YM&+f#&4AXZZ>EfhIAz%sXfOi?*Y^Q@+VmYDc4c=F +BwVp~5cNI(7)L!4qMcyo(V(x*yq4x;Bt$ztNP4B7bb=6W62wdYBmnr8p`zw@-{Qp`=DsOw{J6stS_^MB}Dl0~&v;cE&toT@lJz(ONhz~=!S +3Aqks3D~Ql&L7jnLu85k2o*`{9*U-00QEbz!iP^d=F5F}ib!5`@ZF)5=Q=azwbi4|id{p?EtiBmV%}(+}Pja^*3qJtc`{K`tqFTDXsWO1y?#v7Q(3Og6op^JbZhc8(2eQC%`(W +`|xFzz{1JzHaRCWPcp`|0C&RLqJRAuS)1wdp7z95Qoil;jp{Vt$!FM?8>R#PoXjPlWOyVbIBzV~ITCe(M)_K;__oVI`&t#1I<9<}@8D~R)jLbI +twJ=sICG#>=g(c9Jfz-q3p!8puTf$p=)#DruOprEGh7J8G7((nt_1z=UX-nJCto>aEI))b7LqUU%T5aOX_H?WKcu*5S|*BY>!)N+!U!5`}~KyuDEC8hmlvu<)Zo$DA%!MJO +=90IL}4aqN=i&&9qJX*eZQ236V!gN2LQoHtmwsK&AR*Qu=4nJQ{9c31J(I!Q=3D!j42kSdrbmibIc9) +6$)9tpvYt@wBArbmFo<-|rp!E@o#s=8!%0c3NmyPDoB2`jOmpmygxPR=LOtK}8#2m!=Tg1|7bZKI~=o +J96MNuYyPrK!eq!BTkkNw`aHhUQ@%r95+Euih8=*qFl^Lu8506R>fhny0@j*Xio0h(>Jd|=HICPhe<%KSbZl!6} +#O_05ZR9Rob`Z$aU)C6(W0IDb-Zon*U3-jk-F-BETT%|AJ%8lTaQj8 +-Z2bvaUgcg~ieD47woBf?z!|eLIC{-?@Z9kw8=&!4btKb+ScrEM$^*=YuB#{8WxG(YyX|t8N)0l_$7L +B5xCFKHFI{_Fuh?1Y=v3QnHyQWa?lT4P&Nr2pK5Ois(j{)tMR0Noy>OI?*SqrT^8lN% +FS89diQjFwT#jqTLR1MuL8svM7__IId~`^_v;|yz+V?N8#-aCt2FcXrUITaehnz~89k*diq$Ix$S>3r +K|NKv-8#3ohgxvU9ne#8HLnYLIXDp2H~j6r-Vjvwys^h8JoLH}NAYT<>v~5-RV7mL_)KIYDFC9FRRLm +$K=IFjMzl94e;T*|X^%L8Q2g^}I`j2GYY=UvA1~t^r~$wvM&VELu&dKPIcSyK9tmmBVl)lf2nTXp;Iz +2>vpyLbXwPI6@=R9`s=U_%xEW>zdD)hFqs{45uLa<@5aJy(42uoRBQ2o#g?n_xCCGlqn~;UqG-o5B;d +$>#7!^I{=zF8G%&9R0h)0W(o_L*Z_y(0B#QTQ}KveR?YE^Ii243AuP|fqV89G!r#TtknM8^*tfc*;&c +2xD;sJ&06X3QH8cmW}QBA#;|)Vj#;FL$}l86r#f(U)}fK>ruF`Np&D%iLp>Pch9$8Ni3{RCYM3%%SX; +*CW4B2y|86)|;gTZiEpK#bc_5l%KnNbP#!h?FOYk)7`4z)zU(}cGiaz6z+_*8N?Fev*s->s&+=-|80w +~2!O{vZn?GH(pf{>CKxw!z-Q{V5+L3zYJDzLwXVz35QqXS+yiDuI*$jz=pgt}mt%l?HLpTk*7509f;w;5E=>J%wm_}BcinaQZRkG{SePa(5k*M7K)#LJhF|2;>R2s@5{nvkE`L}L&Z~lzP1Nl0p3Y67IGcywH~cW8N78 +65w*TjUnE`LrW2o){(Anp!2fGQ3a)6e#FqV3qU0&xlAUKrzK0Ja({9E#5PWj|(LwL<+HWu +O?Ds(j&axr(RMmHJ0zATP~re`snN@z~bv5@tUK)Ou*D-IAQ|5#{u<|;R{kRIZ$tO4_eaHp=w@E>xOdR +Vo;Qcq?QW}jw&)=$PTY;xgoU3muHkjFx)^F{5?i*NqP8i))vpDcG1g?_88@@u#QUeW92d@Mvhyw9s_P +yjR)oZJ($8y05Q&uu!jtp}G}ZA+OyxdVm}DAGo{EUg|mXRCOT=*YS+B(%7M!aXB1f&aiFpi{9F}yRTIJ{cdKXp68ay +GxQ{LWa2fd&th-mId7jCOEDR1f=~qbUU)=DOVb`k8#N8_SwJ##DNuc)p2QKLRwa#6@SAEjO2G{=( +-Anq5`5mxgp>k+=9bcvrV3>B_J=MCW@9y!N6ugqN##)>V_MsfMAy7Q@r`p`AOi5@tfX1>IkKWpNLIiG +xISS2@BGGwOl$C?X6Su|tmed@=%O?TctS^rh?qN)o1vX^;m~U?xC=wb`u6ABqkOsXa=T2Zup!`{a$9>b!&vBr{!X7BINv-&J-AUG^ry=9g# +@{9p(yA^Rq8gCaKK7HG0@VOju{U*25fsOGF$1b7fLXQvD_0yesfhyh}m98bEnXxfPl_7~e{i;c; +ys1||M+KlW_mE^o|5$A0l;f42O0`_F7~_XVfVN%9#rn($dSG1%<8w)M^BVmajIg5_UJ?nkJ%z#LYB$^ +lNs;HDB@X~1Fp+vDu7$!m?!tLpC=653_Zbv#0?AcM+u@p55n6q7Q!8R<7#6=93;qxiK3nO`!17k=pc& +foxiRY7Ylt?ruSxA5kyQrG2m2{6w?}__jw+mpvT59-JKC|mtU0p#Ot*#Ly#tcQ1Hn&is?A17W +|_GA7nWelApObpx>rjx1f4xuBRyaF=qZ&d1s)5L#{zNZt%3D_E#Z+yMgEm61UrW@Xa5E$p>|=U(!l~_ +w|m2=I5&}1aOlvo}E|Pg&~{cs)Kf#n&`u9nf&5=qr&Ghz0*D<7XW?WW1;4G!wCqrJU*JZZsu{e)57<2-Z%(C}>)*!hQKHDjUUxzW` +&lnT0WZg*LTb1Vz(Q2Z>r#w{EW?}#yp>YZeCLv9x9o*{?=9T0krg<|IoPCpdxoSFA{qx|B-&|L0dSx5 +0sJRDAj@iGo@WC>eiR9U)KmG{6oIN*;%old_Wws!~I28&!T<2wC`fyT0VV5FfG=O(2*HqKw;0rE@#TB +{7lLX|_MJ9O%G%?@78I4~As9J;f*-IewRrv`2a(`J#>3HlAlbp65ugHoJ7)TYkm;Q-g>;44Kfj?JN)q +?rwoatU=kHMGuSA;s|vy%b7V@o43ghEMS_k~9;O6|m#T;#|?D0zhKMb3 +lLoKlrtdl_IQ76T7uJN;6bFSc`vTZUJ1z>S4`C$2~u>DN;ShmxR-g@VT{hF326sfQzYvaC(Gqm%E@mY7D2T9dgQP+LQ$zREb{xkq9i7n +4iJ(JG^fM(y?!6m=)AE99*%TbY*}`L(D+`To3YIg#D%G$Yf*Gq+1Cg{B0~`HB{(4Q+w+s6Ces_jD@&nx+_UJZD +Sq;NS6T0Yu=D=VJs9j1CCp|GNTqoC+5|7L~*qa7tRotKb?1A$w18(BO;W-Nk(M|70XBu}MTq +jX88TmUre;7JKJ%?%_FG$4Y}pbL7?I${4MsCVP`fJa%)LYV(^ym{_GW;i-92r$p5*80=Oj=eUw5E(i?IpLEXb)b@qfkQbP$3h)5OqKb$65#8jM8+m*%>1u+AlD#{9~}#w%%Hf>Lp4SxW)@BrbsC#uv?9HZAb` +=CZvJcp?+TnynWV)$hKsQ00m1HtQ&E+}@HF22?S-$2vP@PWU=FK{|R(To1JFG4B@ee~_W3Gt8NoH<)2 +PXJN!bR2z;isOP?qr7D}x-=%plK^f8)8r=pI+D;{Qc=`$ou}V8Bq6|oE?wNLvkm>n4nv05oJJ^VnI%g +JD@>g{7CIJlQlILzgnDluG-ica)|@850~X<`BX3ZiWoU50Qc&@CGpKz$X4ED#|(z=^3S1!L@yN83u3MYw(o&~VBP8+kpIIptRA>dZrA32H!$g??pR +kn;#ZJ`GGcXttI(p}jVtdW9U{tnX!b)r*2smTSFM)&)YsU4qCtM4WQ +B=}Qp8mZDszxrF$Z8;;(SuzoB=Ey>m8)zoLLW;iT?8w+{M%&D`;Hz=TS_C~48WX&E@3*mOD{RL6)a^4 +-(^W&UF!scI_ifVdz;_QbSn4#Ki=stIssnSBkRHwcUOE`*P=Dym20O_nn-OE&$=8)mtfF6Ey@IVpFv= +3<%JsdjEqYUP9XZ7vQ&UMiCabjOg@>$&0Sqm-5rZBN|-E05*zv%mMjJOoEXujY9k%c?f}j +Iu;IL92!4o%Kj;K^NDF1pG0Wraf1iT8C=jphlcK!rUUaxPV^|1s7o*8&|_fTTQKpNFQ-Z-{$EEMKQ@P +4>=AP8g3Vq#N_(rjQoBKM|I9iDp6Gbp0S7@+pUAApWxL+4An7TcKQ8uPi2<|g@wpb!;Jm8br=~7?LWC +_DY??H1TgmYu-xS<;V?nv%-QVcM8XQv@sEXAW~j=$BEO^}XsL(Dk*{gXeSUwh7jpdR-90|zrD(P)wW} +OggP3~_k&H3YG6{#)h4G;>oK-P_GMrT*69$OkG!V;Jy1h&<(|nXWFi(yyhe=-(I$#7qR+MIDKavd>pk +3>1EJQMAbJF}ek?&7uM|05pa_g}S?l5z|CxF_Wd=!!x8`kQQUsnzyPc|L2x~17#l)}2Ak3cs-3OUX#a +HEbqy(|JGcY +=rO7luS|}%80O`N2X%hmETJAERhrIoB_TPS6^VLc4h$0U`$G-`HxrjFS(d6zWpD5qS~vJ?YEtL^yMGV +5$F_w8F`KCI@ZBFjYFtxL?m<-!d>Jf9=I=D0)%c(Q<4FiGo^}}Zc=A!GXC`^N4Nfl)?RRo=^4=dLR5a +$qY|chSDS?N;hl%x+@xDu)yRAwHHg{jBgy1vziOF=*d-r$5;+$8TBA0-ljgu$&w~d>3aO{ZN{VrrJs| +T;RcM$aVkdT@?erh-u4^@w*b{HWuSzOKH;b^vuFFiz#qU5h+IhrQzNE!MFj5A9qK?*IH5`y7R?M1CY3 +efe@6%G!%WvClE%N{nh8E>FzYHTwzeX{UchT;PDw#e7+?C;_5*`GS%BvRXIS3RVpVgf)t8}cwZMw~HbsbskT@i3(x!0g`Ez@8zz@@VjE)UB8QpDvHOt@LE7;++JwIBb +I0GfSx4XCFv&z3F^@_b`E(_xD|$9MFJB6)67N{!;F6UI>!Np77&y8%RctJv+^X{pKf}9Z*-4Xx~L=@<`{giX$NdFSfv?AQg5Qf?)e5F9$Z-Jv#TXzW6>fw)! +HgNRAxK3K`eoS#yjJQkoRM6FSwUT7k&MtP+-bp9ZSnB;rLs?`t5l|Vp{PfN}*V^7$0TLRk4KFV~KMV6 +U&xVFPSz>h{^?_*Urj>d=9~ubSU-zaNu$M=;DamG!nG0_qo2*gEBxwRP4pKK|lDetYeiOh*)L(#X{FpWL#!3 +0Jj^zONV57pr~h&hBJeQ<`Rc`p25a0YXS~@en(^>-5QI>OjW&RII>hkAL8t`+N$;T7qaqsqe|>F8UEciYr(Y!^%Xcno4I%i`O-NONN!0~`oBp^l +%$c5$2eP02&ODcN}$&Mc?#KM(&?S%cRcKgb{HAYyX&I@Eg5NM59+p7V`wy4MR;zC|0S-vt5IWixf!Kb4dZmY`somshu=FtZT*H?{QKxlaKr+Wc-#zoQ2 +LfN%XKzm*IO=L`S)mg}A7oeGGLq0IvYbYwM@+v6?ZYR$|Ma5N3EMRJTYW!KIc_^yNRVnrGBg^8qxSdh +dt{y>lMnqxFhn_`W+Wk^t)jBP$)n5ZW5wV +^BCd$BR8t}g8hl4};(@nSEh0D97d~(!b%@A3_KZN2PJ7UgLX7F8Y}5%Odaf6vsRS4SDA!5U-7usJGT=%x)pHEL6($zgn +Q_A9aV-33GW`B-R|0BwsMY{)ISlgS-|g*2fzBMav-wf$!*ROo%7TBkht|)=2ZlQUpBH3)-w^+J3i`NfVA4Aa>s|c3lQH>6xb_OXI(cJ3#RyTHQ_U&T?(k-BY~b1; +=8wYCioQ;l0Q{zSiDZtK+K0sg95UTd!E=++e3+81{mDb*sK|`f)T*R*@bf-N>y|Hc7+&AuQR+jU6L-~ +sAe##b!`4QdB5*TI)3(`VL80WKZT_LIXNtvM4Kz&!)b$K9oBm=e;ovhuDQURvV~|ZTHHdmH1c;XLNBQ +`kU7UT+@cM&CQO|{@TFS13g9lmALH2~1w9G33+yqkAC`luB%)59no3vU{2ZvCjqc<=u2|(4eE`@9kr5h +Xd?DM%D5!+%yM6o$H8mx^@TcHvEUfOxM{?2B;5a?n_;YR`r!cT+ +v2_v$gVGmD{l8wJAyH9%AKQkx+j#P`FAWpkiE6hmb!EQTYZ!&@M#fygTrvT9ow)z2b#PU~(DRY0*aL) +@GB9vVIp)c)WBzgeYPw_#-^X&qo`tO4-q6;pjkuS?bbjth;AfajUXR2a%)5X1r-F2B;F6 +m&LYN6`7MFd3b%NM%mDHZi_gp?96US9{5i~233(QxA9eemm`%6^th2K%JukH&mE;BFATAtnsE@Q3gu9 +B43$I4(UguZx2c)P6jT#He8{D(CTD&XXXLG?_aJ_se4V;r%0#vDV&(uB#}}a`Ov}OLoa^UZ64cf7D7R +O#oLofn%XoG@J43euN|fxJxKm^l^CUpNNX*!i-s&<_xxI%CaQMJJ6uEk~wY +B9y{0&qoEsLj1wbxe4Ob}T*>>5Qy(#$M5#nMJgz;!53rRRRXvb=1C~;i)!!heH>t^`1-j?d23p2l>lyr$EMqj +RsE0~fWT>hevODL_050c87HOqJH2K0+cl5&u{w!*k-geuI5~gGV5*;d~Ww! +zGyGEkwe5v*WSFN}P+5CMDKa?iTs#L;(?|v$hI=acY)GsxdAMm1hJcyD!PS!|oqt-ZD%F_FTH9F}7F! +;69`n!*3sX>e$R3n)Upjg>l4FGL{4fVpJBPv~Qy(%XF{snZBug$t$pVkbtEjv+XM`so1Ee~+;SL40X= +2oxQdObJmd4Py0gsqQ6JVU;wv!=Bz7+!Ovq84eeO+!2>L5MDuJx~gJ3!(-G2OchJkQS5WGQPYSEiM+< +9*&5j#r5#&a%^_ea|kcy4?xA=6?kSQM^^_%DC&{od>aH-4!DsBTz#=ZY+58d{KFcBx}&eUw3&vj$DiT +wwu`c(cwff}UXbfrQm^>yn(^JOdwFa!&Up^k7x`H{b7b|KJw=Uj(CA9!l-3{ +5=|x}?PXYMriiN!dOEgu@Z{z-Xb-vc7mV&y5&A@El^%`pvDuZ%s;C(v& +4)Km14+Gl@&wq5>E-05Q^3Tb@g{WDGCIcU_=6;0y_|L*prE*4<0Qn2z_ +v9X-4t;bqm-lLzK9fGgK?oT){~EQGBK#Q`5s~Sl^!u8J!{TY`hpGy@n7&BBJ@7=UUp80=N +UROUx6#1>U-*kA;NUS8gVQQK#noV4oL&mr$GtEz#ufrG(%>I}r+^biT_fEkSNm9TB3P)}Ac~+zJyWCf +CVQKU!xVL+#q*tZt?gih^8y9kNZTl#A^+qR?3?ntEaW*Dv@8Q7Uw8w(*bx#KW=hWuFLfP_p+MkAT)$> +qJO|yj>HM7dRL>@mUl*w=nl;y0iyAejtDt;$11a%h!6ZCBQF$VSZLnn)THKmADDu3*IzdA!>hO(~4dH +4y5Z}JYYP%C~_g^5?UQ_gIHdT`lCfWTP`LHk)=^TvFD}9g*%!47v^uHPl@H-B_#vc2;heOhK*s-fG%>o+pRoAo`h8EM5utiTt))g#tW){e8Ga@1Rxqtd`ndN%v8?FD<#0sM3o +N>lBq8y9H9MWP~)SMX}nmvGT=Yyqd+i;QR4G@O*?8RQdRrtbjpobhpHdC9z78fpRX$0X{!sEX#nXoJr +Ob=dwXXr_Ia7!1~?)rN|;+H^GP)4k)w%Da1>Ha--p;=WuM2;4JQI5D{7=yjW*<@90_fPYps^Q3xIYZG!TRqrO{cMg~rY#;>w*I7kyXGd|6IiNm&z8H0|{4-oJ6 +JTyM`M!P=p$E$$`;r-aG8|FGfx4Xtqt=9prNe~kHNZ=Nxi*?6iSl>c1d$_diISmWQ%I;cTvP}7<(ke(gW%>J9teGkE9)FZvmO;g#tQnT@_X +w@Wk9zoJ$dXpYzMMarsg`3ohTx=Sd!k?PD0cV&Gi!<^IdTZ4~@$Xd}m`Z^zVw&r3nV$$--SwM08fIBq +Sw&CsYm%M}L~5mKS(9cwI4YB7{VPB8c?^w6{YP5m}?qM+G>DI7UG3CMYGE7XK>3^dLAqJy1dPCO6>kj +T8x4rZ%^0C82$Daw5b;gF?NeD)T9R9lY}WL}K8y4Do&rgu*F|I+PJ5X?{-`E}a+GL&GtO;|B#r)I8-| +HV3a&)1{s;-TMFe|IES9rdiu+PmxErHH6I%B}V%DzIG|(HY&tG#8i +)oPhA;6X2tLEa8N^JNPa1pPUpK#!CCVx)H(F_xF@2n=nc0UC?zrnrj3?xXdZnM8RyMDWWE9IJ^|{?b! +fYwq-c~|XY#%hzzs39TSJ250Pvx)NN)FSTl%PEhF8i#QrCX#M1Hwj>73!n(l)YR@2N~H%^ihbp{fdFI +T0wOON(Z2t#o=p?F>gBkw?6LQfk?VV +$w_eyLR}GW+*a})HtzZYL9WgyF&ZqBfw6GZQg5CJjx0rllssz3rnE*g6%|KsGm1ZV-`%4p*7T`P{YGj +aGSn#CcW4rz+~^HOovDY&rnTLfL%9vJ0XX=Fu*#OP5-DdURe}O|{VM-NXpGF1u3V0?dFjCCC<`GhjlM +7uJKx|s-$5&R-iZ(z#o43T5|o507&9m~vR>f5U1Jvr2V(tBgyv{8|9R=xW(ScYZaS31*lZ#0deXy>qB +Bl}^e6;s^-Bd&TZX=;;TMe?GK8VZX`Ayh1WzFf;zY=hzS5lJf|M38xfd6puXhs^BDsq7WWA`;L*z&|3 +~3Z0O;x_TQ~>yr0(2TR^nP{wYFt90=(*wK%LSTclvX$!NcnvPla^E*a0rt9Rmio;yNkH4^e^b +7w=?n&~!4y;*@olW~*XoPFI>Cva~7kt@v`EFfE9#LQI>5w#w&3XqIN?Nk13qN=p!Ny(dDmbZvfRrYak +}5kN!(G|u2CVp^8FYU?ucu4;HK12}aapcT4t)p4x4$ijgGHENg^yYydaNCiOK*@H?W_pi-pzZ0QkdXu +q+;w8I+Wi}`p;3)mI9=Rx88e2D$Tdx7&!%?q9MLS0?X16jg7vMGT0BV;yA3vUiKmdY+2_#O0cSUP)-YntrcQ1EJj5gI0icabH^-rO*9^^8A}-;h=d@I{$9|6NP`3s|SjhuAU%K1OZgCYKPpQgz0KU{ +^%D8eP6Jd+G1|}RNp@6l47+U14KtO`kE!(sHarL>T+QQo@R&~&9{4D>Y#dw3fLE!1k)ZSWWWDjTciJO +w;&3Y`o-qf80)DnmnFbqn-UdC@!6c~Lt4OUk@$P?T51#`02q?vCl22~kt(FM+SgeJ3>@AwP>$4_Pm9| +$bNA-GdF05sfP5-qPGfn}EL&u3606R*@(@{yC=L5c8f)BNINQy$TIuxJe7pcQv43a<3`CHflMm`@tpT +ES5RlpLv2thB(&oU>B!RdL&k$AWqgfqSXrR3?qJ}BaDx_CoIM72>(A#gUvikZ0ATm_He-tN)wLmNZIA +4ScX}Oee;PJ&+=peuMQ__K-l84M*lpB3ftKzmQZ0sRGSkO>#bhh)gNxm4XCO~A!tWozqG^eqk4n&@zY +L^FBFu|dn9Q8)mx8`V=y9kRKq$NEN#V9Cwy8GWAC>g8!e`@0p?&TZ^kJ9QY`%!!kjJDf-j@noZn +{dk{Ph&=I-hgB0bM|1Pe^%K3mVG0oL+UkKbl(FF3Gi{z)7AFAFd6BK@Z^C!%3E$`_w-F5pxDXCyJ)&IDqf!YFgAXN0`bN|Bk^uOMpZ*6yF(^Y?re6yI+(tMyo|W=tCq2AkObk +V)lzbg(?GPM=T08A&iV~!=Y?pukLZdkMV8*9oZjEIp7gy73iuS01V&#up3kNEVM#kXfOprBjLyYgH`m +Ooap6VFn84s02*VEB(68p3K^^v6?dBssvL^aa0^ie{5%+pcSLfWY>w(iR2iCl3i`r_-ZJQcamVJ?3ji3zE(XO~R}$Lq9A!qe1~%RTMDaFL`Ih6JP*-$wwYVH6@sfl~0gP#8 +g-o;~+7JYGMhS^g`ch}fA~+s|n37YtuXBdTQq^{&jfGlut?zc31i3+>T<8x<#*+RapDnv +fS)R&~Mft;R)jv)L2V(W0}YN(cDfXLQ6g|J7I4t*<%I(3YNqxy0GUv%}it?*adCG8_^8eb7iQrGbJ!GS~?T +}(!X#wZ66dn}GOm?!7)VmXqTqIkEq;Zb=17k*|(h=`~V(sL*Y4p4Dpm{Pl +wkF`q2z@dxKj?fX&;fAUg*icM{*LT{E5E7-%K_=kC?C(&|otk6GO0Cujjhy>QZXt5iyn%=HtRpluk9Nlj)M3x%)S;XJ!i(81CuE1P|Xb-RZ^*cgZG#Jt4sO +c~_E8Spp)WVR|{WsFF;rXJ +3=@#cQ=B$I;YSKhRD!uwLMUDrz6WDU0x>=nrmW5=!k5Bw}*{{ID9f@fh=^M-uKnc3^fnti#V=3z>G?h +Q0@NF!QokmqM^xP?&g5r4hSNq#{JdYoc$r(ts_N5v^3f5EBng9k*7L9t0M$O@#6aLk%R+}4a-()Gvi$ +7&x(Ns6C3;^aWii%9bJ>X2V3^UFQT~!g6;@mk*%;h)HbQ3gUFNkMFSz}FhfT|!rK7qhA4zb-Ing$Qp1 +6tKYC9Jh>p+)os*A`Ry$g+JhZa3b)-UwDpN_oz?_Z{2le};fSc;9GN+{ev8Bi%C9(*kMASEWRH5>1fD +hLfxM-tDXl~5=uaqbQ+2N;prgAZltzhJzcfd^I|vz2qZxbd +J3@7oP^FfwgaZ#9uUKvP9p4#QKre?ee&aQ +l2>TeR-udFLnt9fEpq;gNkzW~j(4Yf(uJK)zc2M1O*6eT5ktNO(YyS**Et}5OuGws$6}?=4HPj`kPD-&4n(_w`M1dlTu?oWxg-KHyta)yuGy&)}a_jDU +k6XRoyL({3w|O$(sBA}^s~TK>;;qcJ+>!dEt0~;{9A6zzoBrF-OC-GC(3b3u(H?&Eyg$hoI+_mPg&N{ +^pG7+=%V-v7s`o$;d7{M#um3tiqcpT-OU!A+7*;jZW)?n&{yS$sij(^No2i8Mg47eq;?}!WsPpTg|CL +)}5pfj?HWdU=unDg8J3@h!e6|^7q7)0q2UFqE2nf?-50^E+!AYI47k>0AJ}!RUgU$!sVAJ`H6 +?lucn<)TLShrTarB_`$C@xwnGv#^DB#7 +!tT+uzJW9$fN5#4Z6^&#sHVtCyfgA$|UTUwdDY;#^zPXQt#y1}(krd;sAU(0rc%xJE1V-CB*4FK(NiP +|DFW7b7cL$p4I`GUeCE9m9ylonc{WF4U|x|%PqxcGNYN#{%n%W=k) +<h-cohbt@Xs$kB&_7e*Co#ZKg4EaPzibvVTFV{0l3} +PLL`Xl#(%H9n`)l5Ae*gRqDM&{OkIcTl3-WjZd3}ZN1w9LsIFw{1zyhtWu3o4=N{mSzv}qpV@WzIC0H +^4&*anybaLaanxy$rUS3$pEX}hQ3t+og0_wjpA37_75RfKw@A7u|0CO9gN{wSVPC4d`s-*HFS5gMaQb +sPK*9$0jJ8ZI3=K2HTQK&K%#FpCI2;z`UjP5&55Ye +2Vy}xLT%Kcbm|h|MSN~uizMdLQ-;V=#<`tXD#5+ucX}s3iOi;y$z|^x!yej#_Kpx7-4w-|Zj;Z7XB{9 +SD)WMm(I_rTmVFl^V0L|iC=eMi_{3n&{4qARVw}Vdj)+1OS4Sv|rUfmQX(7P3Llx1*UspGAsG>nod+B +~A+0sL5%$&Vg^-xQ6ag)>qg$&dMg_^Ms9U&n)E7QBX)R*$}&(L0M}*!~E_W+unQB-clom?g1asw(9bR0xW8e3PFpP@=;>C;n@&&{4?AaZ1E0P~_a#u(lL%75Z#ssrbOFl&H0gSsDY +YS^kT!S3K1Fo(rP9Vr7c76X^*wE#tXVK|}k=WD7mo~a(h+efiaLNTD`aB-_h{YVgbBL7@zHmSAji~fY +=jWHk6+g*8YA!6dT7^%~#9tu952B_CE6avL&P?*KR>NFW5soIp=vJeARZsr2C&$dDeRIb!|@1^t%Kh +nu`ggWTF*j}n{_PAjXPnU+`!^eb7y)W#b{KP +h6<}`I=TboJ}3bAA_?ic__Z|+Vu5^2p``E8{+j#;|2%iKFhN~onXU&NU*`}@v>+%xL(A6dAGfTc9GHg +3R4JwbV;sxYpyZat3z)4`JIQHfhe`NdQk2q^bw3{l$uu@KZRF`;c!VJ0Y4n?hs<2(p&fwl?YTY9qGU9 +ckUa6J1~omQto!~J;^V9v+}ZI;!e0#UB-W!|M*T0UauI~5dxz4UTqV1Z|K0qMb>5m3W)w +`5UgGw1Slw?9BI5ASvZjIsUrnNbaSj%zMz-`)L+PVjKZRMS^SHdKT=Rp0Q4lk<|RjwQJv*~GR2htlPM +qcP-+z4-s-&scI4Mf2TF~WWi6+0z>Q!A&WGpW5JP}I2zXtXPN;ZRD>EZX3Ae}3;Wy%L_Vp|TauY-l$= +$M)Mk1R@zL#)d$c^#zr_EMIXAuFxAvpf{rW|hkj6`jsyCNKbJ#edz2$o#=tGpeW|M`cY*iE_Z6FGSV^b6;D)+lYrk< +sfPo(BgjaJ8tc|LOV3oFI$yK=)+(_W!~7&4hNWHuJOAu1l1VfnJ(gI^ItPWmdwcK_9Ik5u69*v7P@%1 +uEDF<&?S${VONNSMrWhxXx2}&`?T^9IZ}H{2)uiU67w{lb+n`uw##9ZEGPbRPCmX*+|m_-qBopxyFx= +0+qAR(WjQ!d<*F-mL^dzQ-w(x(Qnd$&C{;V9g7BGI>KbU-mLJkdPpVZn>R--Sr~&`xsoeAJMy6<$r9G +dCgwHm~;Z~)&{d@Qk7@L>$?f9iapg&z6B}e@z@GGHEU7I#|B@GM!z%gNPDg5 +1=)LTL1sLsD1BjNaG-6RT>P?(Qnj177TbLE04>qf0Z?m&MM7mIsi-3J*^!l|L(ePuCZ2s}uYyW31PBXPIOcFyJQF>T81lus=^5B*&&}~b6#w0~MRM +}n9YGYRyvM;T3Lf3K62vyD%)+5UVqGCVnk%foNrJ1+CY7m(x(_79^NQBqSs@7+3zPEsvPGaGSslg!`@_NCiDw4_=OlDQBuAW +8^3q%!AJ8^n*8wK@c9T&gRaad-c?^yRl^Zosc~!Eq6S}wdbh1s4-_>x_!>L7B2N%E!Th4Q37c4^M(;cIBgrn*9;Wbf&wdSlKd0|$|#Xs|1(PDgZ8ruPpta=G_NsHz +pk*w}({HE>Hb3!k2JzAnnUNnX_ePHc^jPzaZXqf3BeL)CG4obj(gu@bo=*F}DBPVW5x_8f0!)L!NHuj +eAKIZWpYVbeI0wBq4hwjims3}P1UyARSzjCbO2=1)8PF4;G*w-5l;XQWU9O`IPltnIAh~?z=N3D7 +P*A>)JLh)TXG?Hh0JN8*O#oNB#16SOi6b7oI9Zg-3+Ttaor&cKX83?h3M(;{fo|cf3VPwZ)tsDO!i>Y +@FU1qi1)_DS&^CT$}AyEgoEk3x8<1U^DK@(e8I*o13khBY)fFnJuL`_k4%!zyT_J*6sHfB +{&@NWNLmjJd=;VXys3gtIxjA{F^Ph*v(E*=%SIDAf=GRk8NuB>Sj0f +YCDxGc}M4q^u&a}^S3pG=J8>>gX_)9+ruZ25zg+^+5WlpyCTG5y{7ThJ{(e17>$7J%cid6dHGhoJzFD +DDS3bBeQf`}={Cy95(#h!)MnS&qM0;rFo3%QMiL*wOC7C#FWx&$;7S@l^4RZ`ynzx;VrLqsHZFw{uRD +NP!!JA*W@Au^;l3XlD#BEBghABBK}zYLQ>ESK{EuOk*-&w$evda04RU3^O~0{pk3|GwactUsyv;gs_P(9@etlry%;Qvfj)>OMTWAnyK?5Un}0aEz}Z2_jg9a@OQAdxmr +D3a=ul9$Wfw7-ZfA#HCl#9L<4kr&W#4RqivSKGwXm`4KdOn->TJ)s>=)TSF>HAbfSk$bu9}C=y*VFlM +6|85ds3l7!s1Ef0p}og`(+APQc$gZxYaVg>b2VNilgW`mu1(ZsD$YsM +dIH)BpB*HaC}f#D4mv6Xokn11@WC>l>h!em9KY&#OaSpKAn~6bLzQ{E9APjZ5YQb+D +i#6c6;BDN&k2XdWK23|~+D>-RHcL?GbfMse!^5ii2e5p_-k5qED(FEzijiW-ANgV|u+lx;miFT!tkbRa9}mzbv*YyUU7iR-9rU*?)(B;SMkhJHJEf2?Gaf0(6C3>L +#^WIBhHoL*yb`f|m+aQ|8P@3DL>0yK}$^(eH}JRh4*UFrYbh%`REa}G|O +}|gVt4JzwUsc3IBlYDmqz!k3xp=h$)xHH0bkG$#s>N>g=udsxL-UFiMO1SeBrf34EJqC$t<~g +gksvn-luE6NoO0l=B!Krr7F@d3*}Sk-vK&O7gd2(3Q2ftAI#5M)l!(3@$$g +>Y9Tdelkv)s`xCYVr{Wa315-rnL2Wq`B)SQ3)aSX;O2Brq-h?WK;hf>(&+Wsrb? +-v-dz3@`iCZG=w3W!d{^Z@S$a0Sx0G?Z}#3K4sgKy4|i^;sY=M%EdicxBSw~XYP*#%ygrRlQ8l}s#=- +6;K)W0Ipr-1Jy3M$tYz^G7QB~6z5%)Gt1VA%yUi5+Xzh6Z5+OE)64a_o;W&VyLfZQQbUNtg9F=#DEuB +eId_x`}jK$Vp-AI+*3CjhJ)>Zr^G-`>&qh>8k!ydGN`OIuMKt&3oX1rK +7?SXxoX0Gwj3EdA>@u;r7`rbl +B)P7Q^e!7VVS5pbn?ih66dvF`{-reIA177YQXuMIWrU0&R(~&FO%u``-u2;#4ymg8!6l}K +nnbza1&H)XsYRu&8S`gLu6@RuQ +MorTAGz_s-CQsY9Vrjn((d=Kuwnu2`dqUi5jRk1o)uvspo>ioqNi|VQT}0PurI(7GVt0S9*G}MF +12+_*52<&2EH%NwIvZmDK*3Wop9}{+7bSzVw|z)|S16u>jEk<_YfV>EJJuBvr>QbF;EVGtdSE{zO?;g +dHnvfIS4=UPR*#$uqlU(D7WGca`PFC=@&FUajLpysrqJXTq_Gb03k`)vq}XnUsk$d+pM}T~Mb?E!Flw +Zv>j}X7CzK~G_Rlts69BgqR3}Y$W(F!J{q@ig0yl>z%9SQhWpS$|1djwtl+JYF0~G|o!$DEf!2I{Yaw +K?tK%zY9FO}c_OKAygornh@icjQ)D;-{g{0qM|^T@l?ivVtml_pfS3fJZixYeh3t&1ov`f3&&G64n1u +Z!n +2alyo?O$HR&u~D*-tF@=ov +7?SAX>4-y;^y_)8wD^y74>4tTQ;lq5p{{{aTx|AQ&&21_{S^=mw8l<^Ta$N&A!|h@^sO!QQIDCaL7I} +wX_);k$sIQosF`=j|8aPjrfX0`{Dw64)PSWr0xiTEO2;;smWfqUCuxk-uN?!a)qWCez_PR1;0mSDI-+ +9xB;-WJOXZ6LE0~{`E-IaBWj5TBIH=E#a%QVz}x8wDbbWtY&|O%7VN=?1&e*PCj>lOC}$hN(Qk)_t(9KIuJqbHz24GD{;vP?ztul{3z(Q+cAKZXSZfKO&E>a_+9VoQoQ~ZA{_wOOTqmMrWnrF@_ +K67`z$L#Jlqf=4B%=@(K!8C4qU2V6m(&x&q(P6aHw17i^gE~PN(tabB9DH$+DmAfZ#|(tdW##TCv-<| +?18tOqkbGzssxzs0SBZuPc7ez#{_e89+L^iU{)k+lF8RH7S8dDZ>n~VTX;P-rYFQmXV&kSyLI5qY5bz +R&KR(And-SGCQ+fgbAOTP;6UOnvWe$geT=JC=WXFXJ)uzglFUZei!1A?`{~P(^-)FqPu#wTp2xD>^O@ +E{IMY^*`ZPTuPC8Q=Df?{xg#e9&%%LBK!Clttm4W8fN>AvMzNqitr#ni}E`VFYl=)h$iY)WB0BWempO +7LAY)KHGc{_5{E{r5z28L%-sFTp?96DpDHuesVSLs+y?tMvQtbu@tih%GSI+}$>OxTulz{G9G80lpb(mAJ>zv`p(!UfIj_3(tQL;0qE9Znv5Fk%zeN;q!QU5CbTLQe}-lt|d +Fufw3TmV$f)zbU}4^o(ZtB0M+2b9u^A+q#P1e=EP7SuBx^+ijS8rx_2kggub9wJLe<22ilF?K8#BNQB ++r+2vkF``jl2Anr1b}d03K1dZ{a_Z|w!3x27i)Mu}OJlmKoteqX3nPw0rwb-IzjyE<5-s2-xwPc1 ++^8BtkDWvgB0Jw6P-V&i7&R+sg9Rm1CL9DB0Z(th*J`-TH>Ga)r!>kxo#p_K!x@u)B|XML|Zz}te%Vc +!$dpqYM7Lyk)a+zR)igpUEM*g!ZGtU}GZp3neI&9~b~Xj*V6`mu`ja#PIAm<$jR*^?n}9;aff(T`lO2 +moK&K#XJm=mdnMDLtX{x%pCW5(a*>WuVAs(w|BQxgUG7Fw#1$1!^8_W}Gl$P~UU0FIMJ3IQgB2`ow}0 +7d1ZS--}#HXpb7y_>c?!o0;D>iikmk;ith^KzYw(y{aV4>ZgjroU&qZcz2TE2?nH>T&i|261-!a&vEWRaAHuoO@ykAuD40J}}AI>cD@B>@_x6kiAlm8aKy9bs(%GG +`8(-<Z~ta8Pk!M~M +M+RmJU;m4F+lxJQ37<)`&gg3475sT^@Qr@hH3|D?;Uk;=nAJN#6H&EF-sizd;$pZjC?-7i(n_8{}O>4 +Vie~S+1}ooUOqXM_@ON`d_kBzv(bQx!R;v2gdlR%T$ZsndIxs5yGjDA1}N#7P~DeGfP83!x8&yU2^CM +QIbV4;u@J60+mh~{ko1^CfG*ijg%Do+7<26v8(aU2d}$tn?b_GbeY)HDZYbwDH{WFSkY1h*%|_?p=<> +{#YNyFtQUr~?FTzDfR5GIR;;UBzKw~_gm_tPwGG|$6I`2`*qyJ1Kz`H5;n3`&u{M|i{AVHz`bCqoMpvr$Sq+3uP_fS_@CM*5fZg+!b!vu +8D-P7a>qPz#6VTfV0W9YhWd`G8aaqOMM=8ZW&Y?a`;X*#)TMjz;FG`LFd2(&$Nw+^AI4_?q&?*qG;2| +)6DM;C?Q!y1RG-LGomEah>7k{&jLhZ}xfI1A9da|`t|x>%lzOblIi012Upb$pk8Sk_>L}@7^9OWA*oi +q1s~2h(pmD$76Uv@ZwUZFDTLASuKd92x6I!16gMaz}X!xRt=kN2$U)S-BiW-*;w#2c2h9tSn;B}$TI; +E=v7rx?ww!x>A<l-8U +8)Q>)5aX!>3=M3#;snod*wpZ^_F%GA~;s(5bF?_{kw7ex)N?>Ls}0;~OIPI2%I5fMBYRPM}_PD36f3p +_=(yey!8Cw|dcxGA+zpLyh&0JSmD*DzG_*!U@W7k6r9A!5?xo7p~ac>wM^5X&>gMy<%u3Q*^~eKCV#TSfE +dt+;JRrB9l#W~1fJ)#4%%fJ;VSOr`uW6mKGkMD-?H>86g*VKGEcNPymwNVq4IKgphgceSz!umj+*aQl +8e0#O2Voh*zM?bqt-EUhb0ef^F~%d+EP;d4VjDq*nn*J#2D2qebyoE0Y +NjuVvkSbH<6U^#kmy#py}`LiU>m?5NQwfhMs4+7f`2wfMrI=f&K?233@^ibfNdoX7}JN_(S5np3n)|p +jTtqNH{Pq2Byz_-Elsk5(>WOC&mo`=In7ZeNiQM^QOz$g<1 +k`|-c&{y;fAbvKAg)a8HwdTrA4*dA6K#(AR(@;#MC%EY%;6~`dgZcl{#gDI$Q6NOyd4BI!Ne^Molo5u +Jc}0WauU28FvQ|n|{%kCxWR8IM8*}a_ZxO?xOg-VLHF6YJW>=w!UBkhWS3Myrx=ro8Uojjx3VvJE5>4 +h~bzAa%%{=lXUnM5KGWJ=f)BA^Wt!ju2W!s09D%q)n@}W_-QOnYp3*SbWg~(CUlfhjv#15YEPx68LGm +bwu{@f7id>ACdDf@&`Yc!yG&i9&x+XC?S;w3BT+rSvVqsH-&nmG`~yp)af?@;0`_#`xQ;0 +YAb--VQ!Jh)K$SOwi|*dP_f=+B8(K(MsypJ<}U(%Jq8;FMMrOP6rk+LZhfloG) +@=<}lwiMrEKNWVw_#={mBNiki$xE$324h#|0c&}VmNUQ23;FXQr4n+))dK4g^z^HkP?3G0x+RCD4q%d +lemVaydX3k)-@OnQ~B=vprPMdA5A2NBe6z1x(4hlm>(%TNhC`Ec>58zt=7`u?Lyht0$yMadoC7B()M_+;6B7X@5>=WHB5ndE66Xq|wXPrlS@h$03H4KA2}- +@im);!^MPqLX#BR#iGuwOe;tGqH!!-;^e;4HVlLTBE{hgp32%r|0Jb66-uN_{nF=j#U8j7)(RUfV^+6 +Ws7sli3cb;MaW$UMQLw5?0Gk-R2wxXH3u>e1*{%u@S(0Y)sgNDT_TIz^ylQAwv*4a0A!#vxD7Ll@K@^ +A^|1PCdX;o3CmrI1`(wt#zf(rN}pX9}j%9hHQ7h4aJBNW0wU(C0d@z}}=3x|AZ+%Fahc?u<=wP>9RA( +At1Y;Cv)Hg6n>$BtXd*QtcC2cHVL(RrbA<#P@=;0y$E%#Nh*zrhL9wJ8-t<}X{WOF9!bz~PzUbjz$ifA-WD%Qjj +M25H-dcJp5P$<&K{3r7NicI|E?x%T~QDIFZb3kGP!~?g&oFkoygr+%oDs)4m<@sna8!hP)Hh{a5wZQ~FxF-GXRY=w-{xF&+XTR{LjT +;dOF5mbBArj%MP=+P?a89%H!u+paV4k7c$ogqkrMlZzF#;!PIhB=;z12nOF9IF>Qx$_#AuXEiY{X_IK&*PA*wKU9+?p3)mbw@r;!>dE`u9|5iWZa<%{%@)wBj+io5&KE +-4@CnWIeP}j+_dWQD2+mV(u1j45+hQ_jiv*T${FHznChnp>5)BfqfSGyuyHpp0UD +L)+_$NJEGmm;YF)=ATR6s8?NeFQsIRka`Jewo6KwEP904A=RrSHVbLRxMv+zIv*MI(BTTXSY_~Y<^v> +Zv`6lA+n_C)su6t&OCN1^h5u6r*SIl7i}pgkj=O@KzKcH{Z#0NY{0_@l23OwS8KC(xQ7bfAp4}8>w*H +o`H9-_8O4fPQf8dFOI-xlITjzcsb8zT9ekvqGbe~dxj2GFyl^9H27jhID^sOjG?f2a%;8oZ3(>l&Br)1}i7e_Rw+*oC>+n%!XM}%31;KM9`-~G +Bl~nP*~t5Fqw*6hgfui2!qktTtdCjEZx?JRf5L-Ja;*1Sb6hZMsY_7@U2lbbdz$fLJLQhuq{S6?6>=s +gto14Dr!HdY`5{*w;j~M7s1DLDnvuW@l*nwkWe2)2kMR4Un!J500g!t!Ky`}&=uX^ZnvobZim(JiWg& +JF-U!TxVQC({E^p +M{oU=Scmbh`VKAw}hARPD$BO#i)S2>+_%sY1wISK=Oj3P|4=+3nw4{Z&HmZRT3z9U-TRLF?Fd|~%j4e +cNCTj#wd>u=q6_voR+?|d +{Q5)Vw)<*8$R)oOPup+nAxdNAT8qI&15D4#yrZ8hG~{=AJW~1(lP~T+6AaOnwyPtG69Gt6EUSwa5OyY +gj5g~8nGH8KIc>@kLJ3(ODnTE(KiclC}Gs85FsUIzH@nU3w3#Nu`Qt@>6#MYr+yg<0OJJx+Bcx%i;tt +53vX;x$^mmnQ^s7<0OisopYGaOmshIr=D-$* +3o{8b))6o+ss6iDYqnSL*F8&Jj|!^?!@4%JAcCiUl%M+XP01)d5m(q-JItdcfK0Yh};k`~lvQ}#;j@l +zpAq5>v*O*e~G>LGF@_Z8GB*~n6R%{9p#ME~uty`bbbcZ8{Sxyz|mrcF*1pcYZlqI5`(cq*hyRvMq?5 ++J$PsgNsO>fLwk6FxbB^iS@<^k4G8gaW3ade*^FKyXMXb}Faz_v$10~oI2G!qYqLAoa75G|>q5*FUtA{=;8CEG$r^?ud&cq*Il>sOQ=w* +>d|gUdwZDt3R<85aqp{dmnjx}Oi4iZ#3>CTaby{gULzpa2d$AIuz{zcYS9A-Yc`tONDvH(5b)^p-JHl +t7C{R7>Ts^8pr&WN6=>O;JUAo)Ik$BIwzXeV&Jg(E0y2}rVyt7J@Nr@I;R?<@0JCKCLERmV4d`P1A_k ++x8&Rx&U?(X`F4FW&_5lPA=_BnmJ{Zk`JW+oniK>Q=*LtU=&#!A{^b@JB6bc50ALAbFS_GWxbM^#Y0Q +S?Vx#7;E5yo6H~RPyaQ4tbI-96mYy%*znT;vq_l00L-#S)uBuVJhd*HVNQ0^q-l^>5IyO>cs*1#F!t< +Ve(Lf)4>=WU{uRAp3Sgs)eJ!RDptQ-5!%dzTm(7P<$kEtXC`X89^P^wBZ{h&`Vjc@?TUcVr0-X}8>#g +}xsn_7_ET-rw@+~>U;*%g@b5FzCw0>29FIOc@ZLd%N?&UeOfrL7iU`6`cF3k@Wr$wmE~WaVNLA9Q9V| +GBAvXl5Z0Rb8g+*@{^yhl8LS$^+X8WGk7wP+ataj;hL%a01ZY(NT>Rf&QHomYWdERtR3<*-9hs-xry) +-NI=5J_=xmplF*phCrA^P@G0kkz(zRs3ZaX@z`s8OOV2T$qftg +w_OK9lK|wSB4Q;`p4~=MTRQ;`6cE6RJ4{+?^>z+)JD2h)^hByk>dLM16BmN_K2t>!9MeH1AaksY&>Ve +KfAj#giPALzk)oTQijJb6b)g{ZwlOjg4ys>1GxZUK2Z2luc2NxYvt`()nJbZGp?nYX;2$zo9zjL9jojMYEDnf9${#^dsZn=;&IJSB)j&-QRx?OjUn9ADlKLJ3u)L>as8 +G4lEH78-Tr$9BtiskRxGF7ABQGJvMHD&)N*kChCDZ-0Sp%7D +KDdi?n;eZ07b1A0q;ZG7ay+C4UCfC_(Jcfs$+UO)!JACgo=K-gbedW{mNy4BlmU8D?b#Mp~RtyB&aZ_ +*0uz99Ieh3bFC=|VkL46qfgn3W9E@l~=VAd>B=_$l%b(b{8iqgBe`z&$r=pO`2t2xs+h;24mqccxcek +}3A8%NB|WPB>J*GcXU)+Aj`7bn3l2RJPOAgR?|06aszPG%8^Bw#%V8D^U&3V2Xtat>~y(rjTA1A8GDW +9ZuIAo^fDaKB~WoLHAT*0JsBeN3-+l6=$(Zw;aM8Mm0CRVvyZms$#C!3=OB6PK9G}mC0IfZ^?F^N^g2 +j;A$M(JuYyyfw0ZqMAbMy(#^)?HH6H$7SJsU3*gVxU&JEO6vV(ha3F7-N^?e&&R`hN;_l^m;6PZqir6 +*hbbD{c@(d!i5riRQ$>SBRqc*lwn$sKF1W6W7TpHTF2gNw9EzjFTcPTkS5|ua%mg@7wGK1Bg9vRys=SFI|7_|CDIj1w8XTkTer@T=`C7#kv=OKZ +ZOihf+COXPVHPUNmT`t4G_iQ$P)V7*P%6T?HRXfsY|emAkW_{PsTbVrKcK*OW$bjZ-#*r&>qkdlK}Ju +hH+j!cPomh=7KEeqPH3t?>%PEz^7oh&>oe2p3{xmt$sR+Oseo^^tNuQ}cH(N*q4l%191T8q~Ut` +RU1|ZX3c7L7E1B845!?nM~K1-?z5omU=>VSllgnG~ZwVyHRP8SZbIFFM?o +O_A2B-Ks&X}U73&l2lk#K+)0|?KU!lyf#46l-_?;My!?2IXVlJtJ-E5KR6R%FP1*Ax4re#|P#>j1%1< +9`YbNn0wpc0Jqz*|TGNx^aP!^b(J}3^dGf`bG56MCqMr=IU@dI3%qa6&c(G$?1<1PtcVe`XS@uMH0gyF9Nb3mjHz^tk +%c)eJnZ*tD$`MmS_SwYny^uCzvRH9x=jfju7<;w1uEXT&GSFg%>KRr*pOy|`!>FfZoo9>(0ssDcVqlq +4EFE)1Iv27hB_+wjs6K9#7+1?RD9E!uQ-r|@+)lrv9T3?garD_)Pr5DvGpe=#85?EGGGspifsm)ZJPb +bfVVyFMDdKtYSd<~mP@rXIh+w(@{L}c3@*kZ8u!F#avg_V-x<@{jHGUC@{oJ~xmd&Xonv;1CeFyEVnP;p^&cYnIz_5u~y{2{Y>o-@E!G(X~OjQe$jyx}tU|%oo;g5q*Ac}qkHOU6Ec*r5yM%a)Z +p_q6P2F*82g*7H@5fgwT>|H=c%6DxH`X~p0m%~fXn4sq6L^UwNFzao@DrGf#)Eou)2tNPA8Pn6uO1%k +L)e`Cqc`#Ln?%*JGsO}ZSmZy*&)h?2!`#+tGqCcQ~P+cq(1Ku5|Q|(%LeU$?nI*`ySSdpG`W411)tSr +JyRlm-d;Km82<@y4mP7s9C)00dSe22^K^%E;wpWQ<`7zWsmW}K=s*zM&U_SZxzycz0#I~hCM6W~N!H3 +vhR&X@+rbdFvMzkY5(II0^d9?qB)=QW$}Gp56Nwll9h`0g^`)WjtoLCVtSprv^h%jBPHDaH0UOgq1)$BzlC4^#(t +)tFu3Tn)eDVR-EfdgK|JnI**b9NvsVdJI&mnxG1!NbeTH7oO@M_E$O%}K?Z$P`LO+yuA%8B^QDX}Pi +)67?vzuy^Wt#?&^W>l^=T02;sT9lp%p$RMWzoY}TyfK598wN1{oHpg8`hRXL^znFVxI$H>)v&-B@!xV +z~^A2U;zcoLeWuKa-@FO_TSJ^)zSIZgG;UrhEjxyM{md;r&`OS8fKbs2C2((_b7l)rQG0qQ_J;|NXK5 +rce74j*q{f)CaKy|U>urG@^V`3a|vSP#t!n>)q0?X6i*vv&`IInUTP!Y~+o-s|^k*?Oasaisse*@y50 +y(4R4i(yTWG=G{nLns_9g+o@zY6o`mkU9kG2xAxWMR~4IPCeRDx2hC_XH`cLEqpZs2!-onM{UMd2=as +yQ>BgmRZwKy^}>hUiK#!{=!R_VrvjW_I?v0J!6_epRBF>%*J_> +?OOSj$E&I7jk2=G#C(${@JkGsJP}vfmqDmgM)BK%(+2@Hj_~@$}3x;5I|VMmSi;D#+| +{$9hg`+5Q2M39vM~J^z!V_G~>`XW}-rycui_tTA1wq#D=sooh;Un)9L46BMq3UN%>Lb#U<+_CX#0#QWfGN +TE2Y;kZFdo99=?#LL06ueuKxxA`hD-%;QXzzrKS>^uf0L|6bEvc_bxtXfg~LEdmq)(%-%-Uu$` +`Q06g7>qC?(4M&&d!7%{z@&IoqY281R7s4AEe!!V*!8g0KW3(UpkfwZMLcGsoeHs}paR&E^#OM2nL_T +6LMDO5r8$}4CEtsC!50T4js*Z0|ogK5Ruoeq9^J{dJ610z{Jz|?Hu#1b|o!|3Bs$vYGq8))vHSkWIna +0+QfLlpKim7Fm(%^B^naX$ +$({;kq9;|!i?`DNLK8NmscoJr4tsBg^!pES$^e_NY6oes%rrFiLx<-cDvs&tJ`R50fSn +IyB?D~2VnEYJ)pb4DLm2H;5rc_n=S8|?aIZ}aTkQ0=z24F}b_>#JI+6dT`pOv-yCf>FO>LtJ(iH&+M| +hS?H7~C@Sf=uqsorWqNRClcAzsNjInW!YqJH_*UC9*oG+TfZLv*`MIyIS6s +3zuBUS6t>iDmzL#CYn?e; +crW%tq2h%sMG?S&p{z+f==u?<(>*yStaq)P2VEGcGUz3~vUkk5Lf~7IN;Va}}yO1A#t +;Uq|exVQ)!K@!uYH$rcI6@3^^eq|JoLP#~Y{vy@rTK4@_}_7t!5#I>+Sx7JN47(2=Oj<`tf9sgrx1`fK%@j|*aPnW4*8xZ!siB)1->ox5nUqj+w +3&N7PtmV4iFEP}#&-!OPDVk5ZZvk78aWni#GS>}HKJG1i(PL`w6ePvP5|f3TG2xAF^cTiTc=%Q57?a( +^v+G$joy?Yb?m%dI?@WgG;;N7t`n;+7ChiWfxRU{2wvK)g4REd&G~0nQdf~5Zhtubq{RfqCXbd7Mv-u +*c9je>lLE~iL{A04MAZ@t?;b;RPs9|P5XPTN`pEWf3u4sh>v$EdF)&Gb(b#EFEYL#~)~(OEj%Jl+iyVtC8(`+dg`4@<^<N%Gx6i_TJJt6 +WBmMzLROSdP)lhHhTR8)c(7^vYuGRJeKu6fP=@SN#tUUvmo)|8UNUImk +LrmX4is`OF1H0~A}S=`T=yk?Me@l>%gaG<8%ITO{)(tA_V44$P0VF}yPL}b@kTx_f~_)=r~iY3(B$d+ +RMh}Y4KWF?%LQou&^;3Uh*!AX`Mbh(~0k<3u({&=B`iDf}Jf=B9hIOj|>qYcv1k9-S<^#12eIb&kD7J +7`A-x31iTN1H19WmLdxMrGq3W6bV0ImIAZZECnRw<>X7VTWQOFD7 +;b7_1bEcw6>W!X$P2sBCfsKznI2}?&4J36|h2A{esnUV4RN4c68zxA?Qnjr_tk3WKv;4`AB>!3?Js2>gEi>o?Mh+bg-62$1$z8&&>afn%+8tYW(x +AT!c_owuyPNbAZ~xGiV +-o!XucyQ9u3&&oXu`i<*9Y+d?1%LEyZPMM3p=Tx+Y5W)Af_}6F}>$Zi<73a!ta3`kk|8(S!B*--AiQv +P!p|1-zcl-4E3>@xyCKiZ+Ik-uh2S@3zORxFK5->A)oK1lFRMNfv^OLow=usEzs*p#>$R)3fL@WsiqgXImZ91W{_?nRWJnhL;Vu}8fbW#T0g +*uH(Lq|FUi`^nLMYnuIKs&Ok^1X#+e<=rGCyNIz4rlF4ZFgY(~@0)_8Zs72NY$WGt +o^1$>MVkWT~J!o56TInf>5^AxzkujGca=)H2_+InZZ#&Qhap=I%rU0h_g0FL=m{yzCUR^dNxJbtp`8& +h#{wyE@|Io3tM#9fuFMU +oKoe*j@d|`Vjj`9aS2ErocK~x^bM0CkalI|4i`L-kEfEEi)3IXP2=^{fbIZeh{i0s@|-DXCfPdm@+Jr +S3nbOi^rDM|0i9r|Y$ogH1=c*W@WMud)vtkL@%%EKO{NIK68Tk)fku@w7qRzm1MtF#=S&=PlV*z)(hY +bp$p^y5gX&_g)90`!((sb1>zrw1W~-vUvFB|JLJ$AkK6VcO936cHJ?ZQweqINl+0mcU>ZjX0k{(H=GR +aC7TMh?0OOM6J-j{A8h#>5?(0_hR!{L`7DV%)7Mzp*}Jf2`GCJ+0mn5c*HoGWHzTID@(E~g_4!jfIrm +r{)S7pJa30A5dRE6g_v9lM!sl+6HC2@{tkL|sY_%nYG&m}q9x`7j`R6sm{$+tcoIUUz1(fZMgUp@SFi +fQ*4y7I%5EP*Fkv<7>~E3g$uRb#1ZCMJYLUGt12~@4ptD$QqDRg}Y!cvN@X~;|T2cGuK01gH65RnY&=v2iL6;+D1uK^mMbFh +N)ITN)+(_VFY-h9UNuob`uGSWWBXMP~f3Vlvc|g%#O=T`a`Jk#gQ!Y!2g}w6G^hbPR0X +>-P(F6S+8c8{eeSTX2%x}xu(^P?~%oDwdGFuA(VTfib2!~eZO!6|Bmnr1azE-93Ab@0XAV%q&NndQZ> +3XC$Aqxay$cu~4nZ)I5oh}|`F4_-38>q0wRX*>edZ9x +QhU}gB%m?R8*>X`R-9T>(d$I~ZRo`s(q4JiYaSC#vO{rU1J_8kcIPaE6U086|3P>!+&{!yZNGqwV2g_o)wh7aE!~!;q$z?h1baazpgLy?mmew%nn +DlzXw4ICCBl2U5{l(T|8{_6f)#fI +OOQ1DwvBp%hWvY%5oJU2p|&e8BRl3`4|(K^gUfdk`EWAWT{ylh%7~{c+B^2<6boA4K4;V1Ys!dUcvz@ +UBjqMW|(FnL<@n;?7WuDTF()#c&3HmoWf$wzJ43d1Z{qXlXBSaJ5(m~Is=|+V_ta$t*V|@)kf{CP$+; +ul%cX0-3?1P@~&Vg8xW4**15Gx^)Jy+3@C$AhCdGkl!X)p=e(|ro?2(AwWR_aIE4acp;USsrANZt8>5?Dk_Re(jpuLSs-tcrb +FKrsh$ur2Cf~Izd%OM8L4$*q4XXnrhjqOMhT?hp)>7^+!Wd-e&#@i9TmKc27hp1=N44*@`?j-s(ZOMg +*IJJv(P+*z^l$^BF+*;`MHH(Xo=354rYYOe&7Jehx)n$pb(P=N3f;ddZjg<*HVp4+#7M&@2wF_I-@s- +t_m9kY)XcLTFbe5-l`&1nSm1|y*Hz8vU6NokaMPznT%Dsb|rS+O#TlbU-q1a1CsH|i^?dU2@T=E6QD~ +?G?=-UW8h@Q&PS+N2GSRA+@~YBW}SZ@=@?{1WV(Z7=HdV?_^7}`3ndlFBv+1#^ARJ*!8rFGNf%5 +YXY()#gQDefO&$}Ycp6%Y*!3#TSyqLwD)r%EuUCU8T! +0+7l9$t^drK-y18=-(rW0u7Ra?zV3I0rs}j_g0EuBMx76Izd2vl~SSyHVc%sK91gTgNVkLxX~9Y`P<_ +KA34BS_HbC!;+lmTy)dx_&VbNr0Tdm7sqR8>X}(T>>4$^xc8&-8=K+Jh6`i5zec+HaNtTMI0?M}+h{m +sfDNfcO@CH70%!hASrCR^I-H_fn^ANf!8%i~tJDA)xc0AlN%9?@YHqH+PrC7S)Jx0l8bKI3P~G&LiEo +Yy`9EheoUduQlpGFx7a3_^w28enQBY4gLerH=)i}RMWrnTkz9;UYIe!fhjQi`iJv#WiENeQ{NVnPT9b +m3|p*OpD1wFSJg=b*kgzPt{#>Qlh&Nns#w}ZWi-7t;!em~0POVu{BRSIW2&m5>w<($cG;%WzG&B9C#2 +!3i{L!3%)MqmTw*~s=!3&N18Q$=?uOm=fz52FekNIU`+OG-UR1K1(E${~w>V^c}xID^UfLgm}l4m@g* +wAqtfv-2i;yF9&ZR&Q)kYX`iDv7`H0Rt`<(T?fJtHsxh*9=wCxz>D)rp +)LumsUK>+=Zt?57lSBhLmCY7B7VTjDdH00P6)T*b6$xUeG=Ef*#9k{w2=`GY+&L#mDv|8rInvaU +eThs<=shbSEQcC0QUg6T04CP@bGKUCy=2YOjI-;B{9YD>8&x)tg_5YR{P{=Ta6$T5{N*5vdvnW~K}Ze +9LTMK{&$DjXn$FydUeS-4ufY5RR}Z85Gl9AvfuAq)g5N3&N0M>i|{VbmLJtRM$Wb)fF$41uT^!KR}X6EKv=@ +YbRQ$vd70pr2LM%F^waw+0WK6{iaD@Vzs1Be-?H?+kgj&F2Yg8_rkjZtmCb7k;mRK900@gqiwS9>^>! +sOg@=Iyx`!51(L}`p(x)XWncX7@Lo1ouvrgwiZ7~f^JW~%?AoNXal1CkomeK88i)m;E*`Hq1O<&tJD3tUNPET6AdJUZ8fD!b};5~b|G|Zq-1}p^m!e=>OhME8k#L~DnmPdVm(O>VD)o*dfUwEhe72t%Cb+;)pbN~S +x|bYiG+2x2XkOtXYcUzkU^*-0QgKM|jG)C7G-Flk$;WwaE%1Oa65MIFJ^n*JuuKMIk{u^7y{0mQ$AW^7H*@8d5V_yk2&FTSKm)ESWaBARA&k`9C={SR&sQ+3M?q+)rtF +$NCA!5D)UQ@eoCy@n$c3Upzl`j!v3sShA^5E@cF%g5;|S2jFWgK)%#HPx?RrYVyM4gjsgX0DhP)3dzF +@?(k?`J^5(w4Ffcn4j1ozg(8zwyyXFfcUn3*&1T?Wa@PZ;nv3DvvA@wLj)s{m69oT+bZWkj8dv}v4_z +ZxMEMH52+MdO!#u67J5Z<6Rw4WyE`g;$!>ISNTAl~-JJ!!1X>L7NG&FLF_I6Q{vGh=#4-SuH`=-{LFx +n`3_0Z!zlzm!zE_wC-NIqZv(I9y@iZ1TCRX~&|{mw2c8DFZr9eda65TIpp34#+&faT&Iy-d=e>l4WHB%>sD4jcQ%A4I!WAswj5D +lmJY#^>fo4rIME#i-1$$Z=u7wDm?P;3SJ?4JqTYDIRC7gy +}{gWTQqy=5Er~+oJp7k@^aG*CwjV+ePHtN!LRPT}uMq%5cf&MP*diGAHB;8=fo%i@jR4tZoBm?%~nMO +uQs2$|=#}=BUYgVzNLA&TTD_jR3?p{k#`}8n-H_rVnP~-@HTZcqh2?F(1eYfyYyswGrI_TZXj&hrhBgJ{?By70GmZk5 +k5?2y%a_GW@L~a8JP1_Y1 +OeAzTWH_Dp?~GrQ&9B9DH;JlN5^HoB2!TE(64nL6>RXH4mgOjER$Wy0Nb!UGKJdY>m>thN5;jcaq>Ok!18Ifm^Q|EJr^blh=LwGYyXhhrhStaYa9Gb0C +IMOM@v)tU38#S8tg?42qbJv`u>j`)JU#iG=ghK)h;9XoNnN!-WSq>aHKmpMup-Nq>t~cOFy#1vrS$~<2xfjmN2S=>E~HkHv2qGjrgvp5@wWZn=oHXQ(O&!xT5Gc +QFCnVB?x#Iv=+868GD9fsk6!{*{Cgyb +wOnViK3}G)QF~@CWpnWrr&349B*C7yUw1;&M?aS>5XH1jn=n1Q0b4f)PXYE??p#0r~j!deSp+V#MTt- +@4wp6fSLXW2>z63xMzR(0Ch78*`J +L?2v-c_KF@W-&sbZM)QKw_f_S*TI21q~lONLZw#@^wY@@oQ%eUtN1$y=$^hYP2{Yy%mi~#*#9jN3l9T +TuW^o-4umCD7r0tcVazQ3E3Gi$vH|Zzw*YuUNwT{l&!WEhe!Ur%ScKLkI)(5aQ4ipg}i}6$d_@Q?<=evAV5>0_SgHmRn4J^KztHi-~Qb +rP7lcKU7I$(z#%^JqK*N;LK?Dg1i4daz +dwKnrIE?_`+8Dbl{>Y{_}rT;G!@p#sB=TV&RHjx0s*?=C>-fUKkJbtx+XSHxUf>9|o1sT+6xi6>I<+V +G@49=zc!ZWT!Hk$@I2gtMy1`@*Y3{34>}wRw%rb=5m7NCEX|xts4+P&5A`Wrk@!klW~-Aps$9mM3Zgl +?;qghC_Ji_xk+Gp;G^10(>mOwtbDV^IcjgT}E3Z1;&Bz(72U6>ed03ktW)tbS+GSq +^YYr^1?RmELDqgA@VV(S_(KE!NogJcNNqMKfQbo2Yj&P7)6Sh|N?~BokeCFGn$?Ds><%?N1^-COU)ck +l>5e4;8_$Vm3rDOjcAYbNiqFT~u(2U@8|`60t8`BvhK@<$>(UEheLhr^8^#YiQ)|#VfkZiqt#6fV(N(}a8s4b?e`JUd_;CZBg&FGG4xylzg2X8 +h}6%B3*FJ-axAb^a=*+p0-1N6>OK}}a~RE}pT2KFmejeUp%Rn}bULGLBf9uB-wS}UsFNS_nS!GSAi^y +YdUd`1q)ibQoapuPK{vKU6F0ij96zLtF{rbYS)FNiNqal^x)J3Yn)8E9^Y8$XsM8GveRi`BQaUOEs2k +Ss~T96B6hR^oYS^VmYkZ4X#+J9t8Q$6`j{LT;6-J7(eKx??RS*7-8|KAX%Y9GX9tYI3?O_|#R}Gu{jb +CX3-JwL7q|x2Bq$S*n*ocWUQ%toDzM!WX={D4B<-Zs(P$saw2|&IEGyj0atlMsOlL?cMNa7B8SoB#VA +C!LIjzb+MGJuk06NZ84>fb9b3oBJcb1l6kehPyI0o75iN2VY*iKY{0{z@PK~n*cdq~^trj2jC%}t?;) +e~d#&Gk<|_SJAmaKL%EY#q;0K;o;bfx@_D}2kOn7ZYF(xY50Cl$diIU2IK;XP_jva)iclTg^=N1zPz2 +dx-$%CM1`PW*Sj7AiE?uj;22btA$4UvlN0h5EmfoShD^8Be5Ri{R>wZvd3=O +K{iVaBDMxP$#{=%X3>z1%bt#;WcEm-FCuz(ZEdjhH$LSR6$Ai@;d5XCHh`71YrnF!wbCP>^OHon<;)? +c}0t+(Ui}sse%S^9RL2Y{b_+93}MTdMWNX;UM5}YugiF~nH0!~w9C5W5ZuIVrUSatzxM%cZUe +#OE*D^NsR$2;_%{GZKk +$2MN;g|L`JQ^|ESpy8mwjR)4WM +WyGwTMFV9B0j_Cw^_c|c*uc%I6W!G(1ifliVi7lLFUXui? +&E!8FwbMJar}QU>y-WHH^)1{dlvDE{xIU0RE~)^!`ZkJ{^L-c)DtJQ3Qr>22p#QFVWSi@RM!L1s&!Zf +skg5j>!o+kxQ20DQ@H68~JzwcLOd<$Nc!=u1i42ipXFSArYhW*usm;pqm=!r)7_m5k{Uv}(iaKdkUSM +@^gyw6bs-nTBn`20jhcIec*&XXMniYD2^bVxx;=M3`(LbuPlNIlXR7CV)^q~WB1ag@d1_Y9Xjqph7nF +jnw9l6rB1y)zU0JJuf8AX}?vaO2BGG8DFL#f_+`ERHBd>3n3$m>1eZ?!^ybk>Ee!}q5spDBNJ{i90`m&k@E(N8U(~ed>F%q|q)M>cRK-GF)Usf(#Dnr +SlPG-~bwVz34ZlwK-)0IWs6M=bv_KTF9T`0nRr+`*3fQ#6GD_sPF2lVCY}e(^;VC57)sH2>-(>l1Gd0 +r1sB4~!-ldNK1d_Fh;6$Ch*!bAAX>BG6$-t$#uV +4ie?u%=`$)KLnPOr>t@gejfS6BFOI*$PJ)VdLUgiiEwvD6-!WTNoIQ5Sub)2!qP^hWOwkE&3uy(l+b2 +kroSmDA(CY#IxBxA8C=G*h%OKVY;%1gX5B=*n}Pjo^NCUSni~(l;pZNggrUuZPnQzv71%Uu4oK29ynt +q6h-cl&IDs%k4=fC^&2&&vcXBa6?<7KFBlDT<3LOWf5GQt*YD3qyD +F#EdOu$3)xzTIMu=}DisS0iM6{&I{E +X_+ZhwE(=u{fwq>NWYa+e{(_g;6U^0qWtvH9eNQghK!$~hde$*5SWvzGO?&K|0GGY=qQD0Hxd3%fcTTgt&y&~(tZnR +e=0?&fLbw&NCjk28Oz6Wv+N^#uzo?v3znTJ3&g@3zlfFFBY-9LW7sjnoe+#rkogiH#sCjk*A*+s{>Af +6LMUkm5uCrtM4N=WV8!ny%GzU2=H$M+)U9ZI4}to1&7bAF94bu_FREVyRr}_p^Wd!Ds;+(dhO%EmfBB +JX`jU!smLaSfnd{${C$Sw=3n!k2z?_s)MOXrBklNr#(EtdY^v!15N&4dS3z2&3JK(DHiJuheOlfdHYB ++h6=1k(GNA_K)XYQK9;i7%C05s|S{gGJ&O9wNS5*2vJ(|lb(-WIJSkoXhk&`uTsGl~yXjF +Xl-&{^luGTWcg^&V!;#8P*QKU;()rA7@wN#LGgXJ6ogg!?#S5TLuH@o4mKWR~|Rku3>&A11fVwp|yRB +RO|vq6``fzPLoz}YP%L|EqcC4lfm=QS#_>S|--UcZHVy<@N-9QltwXBkms36ERvu>*)u|Dox7ATN>Xu +Wr*1wrG$AA-vo+-x7g%tA-kz9!$l}#meN;fQpm8ABlG$PJ|89UQ7IJGIYM=#h)pPrYyW5nrbt#)t6e&C-; +{|O6fpcrYFv-t6{1AP5+{`UskE&YE~EXT#uqV@N0Q^`FqcKQY#xxnJf*s*viIMn`*5RS>+t|#|4#Exq +{3#)Bvk9ABe|hGe6Z)#mhW?Oaq)~!e-Q#GZ~glOy-P&2$j|kEt6DL)vXF=g?qqOG$T>^GtC*$X@u&i# +_3(PV}Omw_~|`V9KtZ8nyGQR$}`AMpl`wgDNdL1d!o{*;e9x!)xZvI-`aRuyo&nuiscR!w-)xJYT8UZ +1^GqP+8&BI5MFfiSFMDsVA+I!;?-0{RYQm->WmDqj2$rcgc$`5^OBb%TFRoInryNJ_PZw)RlRESO663 +8s-96enPDTg)O@YA9b~HsI5?Edr_FRzuk(aN(a~3Q*j@y86do``1U4r_;v$)H0CiM6O4!rs(Hutevf_ +~7L>V!AY`ga1g%0?PH9vjyUqD^r>3zP&o@wBvL`H2Uc)FC?cgS90svkRW&k9>pNk>uSPv;!=`w>+>{q +bFI8!)$X0Nz_W>|_Z7Uy7wE>wpj{zHm#CilAH|RL+5X$!D6zR~v}^THV)aW2PQA?}P*BQ!nLitCBf|CQSLO)^odhFgY0z4u>!+hpHYQ +$LKlzMTQ)@Hn)6YtASnSOb-SY$ChI+$E!VN=G?sw*q|JK~bbeSf17c6Lm{mF@y4SL)4!X1FaGM+ZmK$ +){2ZDAPqH^QMHYP^<=rXRa!+~ubh4xKkNelK+sbT2r(yS)01j9jXWQV`~+S25TZ*uCVk7WhYY3RN-Lh +<~&((~aC0sx0;Ezo6hU_wiFryq*bi*Xg3D8N8E*w+KY?k0xV*Br=pg=wDj3I)5*a`H9~AT%}A<7~%E1yAM;Xp^AOug}5>P~%X&xo4 +&VsCXPhwj(EkwQ%rVk%82Y2S0XqHpL~ti6Q=)VxASo=Y4R1&HAs+YSCsgr`dKG>WR*__U!9WJ|TbcF; +%$QO!V~k)y>*W;`GZ3Ds84}`b9d~Hq$i?ciD4N)yp)WDFedN;rumiUN9)%<)#ECf-Q)-_k@m)@QzzYU +qM$IvcG(joE!)dhI?vuU0y|~kRy4IrMNBpsu)z8X`iM$*l<2`2&11$oDz92H#wF&MmXdoVA7^%y(YLR +HUMo*21z%%9!@Tzo9M@nfd%16Z@R{_>xrJ>XQMVI%9ZRK +GOqT@AIZY05tWoLIt^EX%Q8Ypr4;wt)Hx*1aY?h?uIEl84>52LeJ9->p#T(scIiE|(9bDwA5nsX!D-Z +T@Fi%VM6&(*M|_qsFOTDN&{zjLU@Tm238|FBa8lGZ9l?54fMZjS;>CFbyN7UK+^|JBr|6P{hPaiGDt- +(pEHffc1^BHOuF!x3sD&3pFhqKw1;!^ZkXv5%Ho%*}YnOp%}m~DsM+jx@0#-JF`_oAgJq@lg6*JlTvE~iL4($7QalMO}e(565@ +6m2^z;2G}kz$A|b=v_1Z1$0Ml5CwWrTzU5T<&I;qAp_kBO6*}*LeT|fjgK#n<{G9)DtmP(!JWqb(O2B ++-(qqIswEYyAjhRy}~yaF@4fZ$p_yV0bX*hMog(>G-(U9)T68R012$?wQS~zOvy*I_oe&rD1vmKrC%V +cNW`>E7tnd1op#RxT~7byy2?qk$ZN+KrgzY1&;uq7$h8s~i +YR?3-Sxwk7I*ypmNHmwG;3$Jt|AAP7Ub8Smk2%w0^SPu-CbiF07t&mtyz>J)kYu&UEi?~(!t*faU#IJ +B$$xgz{+H&5x}VO?uD`6xW6F!;BO +Fg@DaAlZ)u;p2!B_!2OpsXzdSvdU+O(jp?=4nV@;sBe`QJ37#Z26*2VIZixgez;*(NpDOB`(pdj+ol% +OTNnVc6?G-%N#)%$_vP~4r!0rJiGkY{9jOmU2i#Zmqjh&nx|r+4SBb{K=@VKh=>cIcJK=D7;yLN3UU! +sK_$`5@9i8=T|~qcQ1bCH$3YHBSj->TB1H91KhpeBPu=E+TM&-OZ5*k3ijG!2MNIXSEM(Tr)69b~6wR +?Mjg0_@wsb-17Meln5mP-y$sh=PG`yTUBVy90t2}#<_*@OJg)zK2s(9)|*Z$QZkU>t>P7!!%HZIeOhk +Xrm#N|`xfLlFoO;HU~-&7HCPAN0JCB?H%x +s+FRCUKcL=_M7Mg2n#0h-a{ozgYl$0xrsRNJNWd0Kchuj?J!xJ0eTOM=68`{Vd}iwY7JckEJn3=v>!1 +A)0bPQ$7!QW2r%Dt#N^Ak3eF@3E!h0llYf&Qjr&T=CrV(LPY|9Spox&%Vg1&i19xRSB6vvpMroB+M(MT +nSSX*?U~jx)+D2O_!<{}T33*qrW}Zblr)1WDyb)0Ofyv=9!fP9vs5iq?09T8?Cv^>YW{7tBQ8oX?XtN +ySJbRj#zb`V1~^4+3ZnCF{uRp^~H@yS3a&2H1*LDNDD>3cZ~ggdtvoCOleJi>Vvo2rzJhwBIeM4Cy9S +x(lp1z)?2)p7W-E$GYCozvD8DxgA5852+NX+r8=8L!`9?5T3H7UCT~HH{>f8%GSaG*RcN?ZJNko0Zs! +7*eaqcbD^9-F0~NOfK4VI8%>5T+;>;K@o&wkqEXBVTuwIEoCBY0t~p<`dQP?Oj95nv0q**rZ2hKMB{* +ElTvY=CtS{39%DSVV6^ZbYPdZ|{q^q>Lh2x*Ql)$MbaxYYg1Uh4%l+!4X4>%?Kg;ZxN7fHxZ=z!i26( +7Nixz6Xg2@N)H3F-a5TsdMwq<9TWv@0WFfks>C{`nrS`t+J)&k--&q}KIwS~0+8XGiX+smRFGtCU$`l-^(z{ShDcw8pjhHU!bsGpON_yoLG}C3sOSZPKf215SHPUE6($S0tr$8i-EMj7 +$*W_i3m?o*0=A{=tdT2f-sz4g$r7S92HqZetBFIJ|)gWD{O{MT`+QP54rc{tLE%Jp?$U{0t2YYAvc-` +k?WP}k@BPG3=4|f4z!7t4T5KnI+UeoC{W0i`L>Uouy46qRsubX{~-A+L2H1#-`mQIPs8gqqA=H9!bWhTFH-8mPRs^+C +>(be*7G4l9L`OO*3ov{E;SmKR^X976 +Qg6|Bzw*smp-nlM~SF^ysY?ZHu|@KmTu*+KPqvP^i+WU&A)Ix67bxFogx35t!=fbxB_%CV+~sy +OW{Prgx<>0|JRYQYve@$=zOr0c}jBwIHG`m67Kmydc(dqdeW;D^q<_yjZJ(gAb{Un4IY@tz7(!elr0Y +LSBC*^`)12<%Ovh8qqCocyiA=eo~w3Gpn5qd~zV(UHc6`T))tjxp92ZB^1l{}=|ttyNw0Kq#3_*K-v>`un+<^~4*t^R7Jq++ +UwCeMg$0AY9sd(y@SA1kY21JGouo+E*R9AJO>~6thJpOhmFc8LNiysxd+$ma)%O|7AdT1XM|-ZlTZ|2e!GPIw~l&x0aR3en7w<|4|LqtBi0erb^^v5bHYd +!76^|L{(Isbq({fwYeNLb67-7Kb2@RN8%!BA*`#@$Lf?6s*bpps!ZinF2FiU3ZhgeWRgqq7ij%t +ii@+Bfs;`=9d$^Ds_D3pJRl#mrZ!1jj5`qt9qCJ7GJFwtZsaz*&6M*}?sP##eRkL)d)-IUI0*}X<;x# +eNPPNt3ft5f7IB;JyTw(^*++AAy?&<7Q1je%rR$xjSC1nstYTzTt4?L=(>dWFj=g=H!R7Dl)%a2n6Vw +{@dF1`*<#KcrLa0k7+;D{q1voY`t!p`1vvR|GGeAc +@7PE{D!O`=<22qi=Ig;&T?y~iG)QEJ}$Q51i8 +$Ov$SbSQRP>b}k>Ug^-=sh$&g#mF8BC)v9`h&LUzOE>ll^rV5ChvLGD!dB +XNW-`A;xD_TRKC^*r|)Pb;627yq-q+D03-prLfj79j>VJsDN4aVIG2B3K$vl7*F89xgaQ4B!tg9^6te +2a1u4~MmV5ff=ePzpn>O}IP4i3@%oOwor@ukU3p|U~Jys?ZwAr(IQt8jIj8Y|5 +A~(CdMiH6=`lRuiPh4;SwV@znhXOYJOlL$rQgWvXLpa^*YgB}Fn?GU#X@o;d5tU(qlD%B4gDibmzzwc5GxeG8#h?*0@))mN~}-3)A`HG@jLi*DdIqBYs)GUCNbcm7?5APn)F>d>9Ve8+({?9w +w(Z7VoG8G!>gLFI7jD~ttto>IKk~5J9Fu)nU`dOTAB_;24wF5S( ++=R0dUu$pDuR+6dY^thjJ|mg*QY}BXfhte*LV>PYl7K*k!HDme*yg3fmg7El%|4Huh8ZNf-n@SI(3-N +%0^4+b+7~*!-h^-I#`*o!-Q5Y<;}ewKs^djiB*5nyspd|>&7N|_EaUjg1j45$-xhp4pUpb#_H%WiPbB +wU{ypRvSL!)%Pe(Z*Jb;s(%K5PcA|d=G(n}_p-5LB(t~TqibFwy;5rQoNR#&&{tDSoo4b&;e|S_=^_m +RT9j2_(@YRetOVDAJ5;s64^`P7t;1wgP=Rdlc9iHrt86(kO~N3=I +d|K5O++)O6iSs}@c5M8{-rp)ubQiXE<7zAubH$(PB!bQor!-~ThFYs!rjf$>k +UYickaQZjmSUXG})t_87pq`ll)6H!|+7a%Vv}vX=8Zqv!dJb~vh +pO45xsPxS52@|?jMI~$ckeXU`0V!fXZf+n53{STjyI56)p5!Cf$jN&Q-CrCe{2{va75L6@UCW8P%6Sg +D+Wty*d_c*OMcrU(`IPp=fQv|0X&VoTW<>Tl>bnFaHwM(zOf);L?>4%|EjtwA4*snK4J50Ky_pq_gQK +{i1CN5|CzNlHIXXst-CKLDsSV`waA3~%tbtFvG +Zim=9paH(fc=Gls+V5jhv_icQa@kRrAsbk0aFdpJE27`R(W>+RWB@G2IBxi)8yOXpsb#HRO{RoDV)$= +NA*p5vY+eih%LA0;Gj5Iyn`g+Vxq0ag>y!CnkAiJ2roEHaCZ9G;!UBpmkC&~z+x5NUDa8+l3BB;K!+) +zrbYhjYyNq_hBT&OFiJRt1J(}HKYdTXhIFjo(-T81S7nFkpU^}#n+osiU>1b+nbEh}Hjx1_gfms5A)C${Y|O-v9S0u8C#JTGu--(i}l3kVT2uT*y<2f +_tXHzI91OdWNbmdex%N6?)Z7@N|Ng$p-})9Tf|3laY69bBqe^%y_y6Vu#j^ZkLt{xGKksi@P55|=LrK +t#!VpFf8_C#Lr9Fx?c~T<+F#gqO4e9j2A)*Oe^(a*xVGm|v(!YN%Hm{h2Pn&S&nifBi)Yjtq(8d>_?0@i!!=X#aU-* +_ditTH0|9}U8X+VFujsQyeA5>uV$}3pSjAD_yab4ih?4 +pUP7;vl`llvB~qx=^^Jd)T`dnyFlWH&*!#KppQLrkv_bha4J@n2xM9MlqN(SQf>AMh2Bi>EW7sQysxO +t_Z68PCohiR!^9_6H$Q6<$VUuHh18A9_nwpmJFX_A&O*dH +@gDP;tQAWerOMh}fze@_YuLwfs$il>sN-7_9v8K4O@{8Q+%lbDA;Twk)_kF9MQW|xPg!xU1Zbcwx(0` +DC0e;Mi0VL~aenyNxRa0pMF>ZAtOgKv;+^DhU&()$qZ-C-&z{c-ggw{i${;h_SlMDHiI97^y(`?k~so +DI!UJQ(@_!V~@F^e{FpB>D~m?DrK`sA&gT9?VN(K=ajfm<;MV-7a!h<`;oI`1A9lTr*Ve^taOT4ii50 +WvaJ*YG?9^A%tqwctv#FS!;*un{=amqik3=Kird1b<=Qi-Q%#|v#7cW@@9X6&^1i^J-JC%1#qJ+!cbM$y +vR>D2(X_C)gtq#}TL0~TAAJQ4O1OK-GY;P3qavoYMC%UV#1l3p!{h%u6*|boKnMdoI9F1*8tDEi}zPVQ&ee3ZieF!Q0WKGhp!kG)zrYE~G70s1T7t*errby$FS +5H|{Wn&>t!rbW0A|BR1r!^TkSSb_@>W>*#Qmkntp!w}sw0AP7TMh-mQPUA7B{nTBeJQVf)5aOmsfSH; +^OnCheB>ZEs=oT#&s`d7FkDGNNe5A!@wIngaddF0>*RC5Gj=zzV~X%@zSuYjrw9vVl?W84T*36XvXOE +|b^Kt>T25JhS6m@hdrZie{OG{v>yH> +2bin^#2S~A(a%xa1MA6bB(cO5cX`DrcW{it +d3ImUwM7jeJc*>9mr9Dr_jWF9oub3R0JQagFsgDwVT2=OM5(Oh|EuAE&DqCYMN*GczsWZ}e7G#=aR87I~wMrI-V0q5oe>|Qc5$ +E*%S#LroG?6Q`*tS8sYS;M*|x_0w92z +&AX<#iFv7Ax-bW%1vIz7K9~#6MH +xQyBh;PtJuHHSKLT?$wz#*;2)SD%Pr6T^JDm6pqEk{F+_idb+IYB6Iry3&h>e@A`x5 +?V}uaNc9G?<#{0WayKQrD9rv_<$Ul6?;BI_2iNJlf0m1+T0l98(tyw;46@d4u8%Gz%M`Lq$dLiDOoc% +)(w$b^f)t@(7uuOV$}+XsIWyTp=mQXNkVyW0c4SE7p;g%$_7MnTjnS};x&4D161~AFp1L$P7JO10*4h +q(ZZwLca;tYFpojo?F91(PDT=TSx0l#eAb}C~ggL7noZ9$$-rkO!4}D0oqD&(4Hy?y0mzSJ1ccVoc@H +>;SZ;%lnUfmbF=La`ju(Q>=JBH2%v_G|ez+s?QzgSxsyQh^hAdp}g_NSHh5<3cdJ&5)jT=n#=co0BfG +xYK&z4-g%0ehicDLSgsZ!kq4yM^GIvQ6!{>8JBTrWtygEi~fnp#4WTW+sa&ztHpNV3sHkVldmUpxWp@ +AKmzgmg%#?FGS#5AKToHph-E;i%db*a*1Gam_vA=%ZGlM)XTuPhW?=7G*^x5LH$J7f1O5^|xyWIeF&R@Bse>5TW2Sw*>WV8D%qN?zlD82n!Ushk7Ke}=8M@mwBIS +3oSu;sMZ*nv@``LlPxTG;Dtv9l<5WxJ#9(Zr~gacwS0a1@BdunVTB-YmUe +VdLTvRh>bzwl?+OWjJck{kcZ5SqW}qg!y(Y>_TaFg=02+3b_+|34zV8|e+cCDCIAApT)dXf|Ox5Aq-E +GCK_P1pBDgJ{J$bD-to`XV?*P@t^+{Qqovpt?cla&Z3RBFtTMnw&lMN#kelzRxWFHksTU;O!Kl#=W7! +^;DPU(_IC%LaD;rjp19I`f&n`euvge|(pBJxb545;JdX)|B8R@%EiLI9&Irv`8NoaCSbIBsu2*U1opA +=3sEPklJ9<=F$cNfShd6MtBHGAXG=|(KH;_hYRn~eBzDnaY0j4;CoIN-C^@O-v`w4bTO^UhAwrvb22Y +#h({MOHIbm?thnmnb|VwL0s_FTO78av9qel?Lpwz%ZiuOd)zqucf$oQ>~y0Npn%jM`MY67e{`zL+HO_ +06Cgv+JGNrmAA6_ok)e*;U-4DGu@%-FygZ+M#{Kwp9DA=I +05cES1$Ol0+bY$znpx)pG$z^4ZmzPL1p`;fs;YS({8b=KDk?a$bEJj2b1PoI&*_%x|V4%axp#zTv%3PT|09_WhAruPA;@{E=vYtC +rq_vBVJhMC7%|~aLjDxxNQ{5dk6a|Z%yiN`1IatPzS(|uCk#=>N3Bxkqp1q5n^)Q +uv)6PLMOnBX{peVN-UtqBGhm3B~GP6wBI(=dgM +HUI)h7_3=#acg1h-J~6zw&q2emwF5YAR{6#iEj-`-l<})4eAo}MB>NULHxBlxsF}wR|~8VEH<`-cLf? +Se8B*$jUB5Og_5C+5JJQsh;uHa`6$=F8v;ZDgeRO5_7~c7HyAIC@^3BYHUlsMf6*;B9*5sXgYg8n&lV +bwx8;V}ZJtGVvKOEkVP~&!?&yo;Lgm}lu1psI!jn-DoApq<`*!u)feEhVMouq%3Q?g3SR?0~vq>ceP> +sZAm@wolL+?lB^Hs%tUyt9b*%S=f-nC1 +AOCTUBTfVnm^qRx^Gd(&jSF$5^L$sa3E7COn+Sn)kZiNR=|I5Hja +obf*T$@|v6u*(G+oe7rA|%A##zGDI2>npjIaPNE{q>rz>p#DTX-^jejgGq0wjrtL;Zw1Lc(m1&0%u4= +r^wi_|}*Q|G@{-6>RSW{;bUdK04*T#l9*pCgf7o0iIEzH-}eNSNj_dCynw&}6PW`MolZM%C{Z;2NnMh +D=fj@x$q{_jUf+ja4aT4wjdRAxU`X%GjRzY6%X(0>=RyioubKg0I_?}IBMo7J!%31geP&6M8mCO+Qab +a2`|jkV$0u!7reFpSOFViBY}?n$wxb~s#Jjr8j24%M#yc_jkwZGzh9ksE6Za*Jb4!(kU>FHVQ2%$= +D~O^ChF4i!*zU^Q?9p*A>c?VNFTi@Z^w%b}n*RjfJMTG>LSHeq6b=Hs+OB7V%XWnTJ(RR-7wH*%vPy2 +)zDG6#oC1_Tmo?6y9sDhlaxrC6}3QVzaZw@4UcDAW6kR3HdLBwaXO()Xv+Pid))cN>8pd(z7~Zg70*g +#^KGzMS+XqnH=oNagx~@2MY!XJL;Rf1mG?DRU>pCA8wocm)g#v{a}6}+?~WS)Q4Q&`N}qTapt%w4;O<^ +!59yVjOmZWL&>$>1YxDiVo3veeT!qvw8d#zi*1YpN3#Q{b^+WxH;Zg$O{r=6`PTeumpC06c%^e6!Wp! +wS*>N5FyPmk^&N{#y>>pFH>um3yjk`H&UNs0qjaWcv-*w&EStY~#{$6=WsG(y2$8}vNf3;_{iyH?8zG +xSgMxksxaSh7XswR#{3O0+~t7MPOlU +Wg2dI06A~fRw~+Rkzyp0M)3!8h^X$4fdPVlT@sTSb@1fDpTC2#@LtfDI-V_K-|}ys|}Ex=|&WCZ}LfA +6uj=VAah)ao0;%xU_*(ZOPg05K@@c_^=xa2+JVcJ*ISslJ +@$Bkma?*YU5f_CvH9Q|(OV}2Zqlq_pK-17eiGW&YGX(Fb4Xy0Ugxb5?y8?j1eY9RLbY-)HF+@aMG7;I +-rkqcTq1$K%`=w*)3$N~LGX+>Tr#dgcG8|ZvHPEcPd90O-waOj%$g4b>9fpfSm+py~a$Q_b?8J+~Ps1 +)RMhT8W=+J$WtMe*kl5@bt(0*iIp4+Xw7SJ>aExBEC;!|Cl$)pDg=nDpi;y0r+_oRAepMe1F{0=?1Km +FEI>@y;u8(xGK+&6V3ASe&fSKFRdKKk%F2j8^g5&qgX2;H6U3BX$~B}Zz;8?g*rZf^xR`ehHsSM6fWf +cObu3V?>$=IP`im#c-{SxC6*K>U!YnrV9s&d@y+?`9wB3m5G-sfALJuBd97YSGxt0}MOzpmisaXsT`c +gthkiT?eh2xAPvMv-X85X>_+D5r)V~MkuvZh_~-PAhg;(GYz4bd5=X0s6P~U8qK!N5OtAJ$j)V^0Z3p +vGb?&$21^GDq$3TpC$!!^)8(^E7SXIcz-h!!r;ox#5}mjwWUE^~`Xq-A+8l>7ndESq|LgxQX-6`A5_+ +XxUfhl{=da1|is_TM7}Nr;%h~A4tw}9JqpLRaNm%O*KXq(mYPVQw0s5lh=PgTN(z)IuM7K)XD4;|4Nq +#}*D;^H>!;8bHZTdrDXuoV!v9!}^1j;TCpPlTT*Rd}=x(B2m;YczD{Ke1&|2C7qN&=R|9L{DLqRTQ@V +?ArZAJ7NwW+Du`Q!}C8@V2;x$Wpt4PNZ3PzwiB`9Z;x%`vVsi_NZmWo{$zIOI-38O(<5Y&E&?ryAec_ +8+`SAn@OZEcV0h)z*!G4iY3B``@;0HC7;(>;PX0BSSpo>7i$w +TxPvVQwB$m)!UL?Yo`vflJhm(Vyinm1ga=&H%od_rHx79e>Gx2#M9Jt>wB~7FO_nRs})45!(%=i1CM+ +dL(nkT}C`(|thLE-%VG0Jj(&zQ@%}a~cH;ZBm7wzMMrr<0yb;-!9?zpI7#w`t09-UTu#dKB7Dkp5GJ? +&SyUi+ao)AerIb4OLM@bFcZ;Qn|qY2iYy=qB1^BN83;e?{a#DgL|A&~7wK(2k>E`x66x5jiel-VBN)W +EiJsm4yfQC^J69$+Fw`D1OQ*p^_;hDxSmHziKV%UQR)~c$6^%&d!3nP_n$t{Mdy6E(on7P0sX2!j5-o_@>l@S5;x=1h08ICc?h^W^fYb0t5$v?w+_fG? +->;#zQ@|5&;Gl4A62PZy~hRcVcB!oA*W+jR1Lp$N#3aR3hxUtwkI=F*(1uEtz1S-|~0T@T9z}t8wm(P +%T83wwfLKbpJt-pa@*%BaU4~u`w?*c@noFAN*IeD((yjXw`k25A-k>AnzYErN66UrP$5O!7J}MI&#N1 +V+r^$`yFesPXgqXBr^L?wK6j#GlOhlE4y?et+ +@n)22%Lwx7Up(XbuIrlenlM?W)-H2E8r_*&#&-v%A*9Tp=De*0dsr(d}V3#{tB0RWVu5hg-@D_CWZU5RCVY%`L>?I&9+`@J+TO$0p-? +pSh2ktl4_>A-8r>Y#S0HV=~%VG2J1{>G9P_5er(XMVCjkc5d`tFWBNGu55Lbv;B`v)}PZcT5-8Xa(W( +`VG4i7S8(fJ5_1N7}aKLJb5^Ap*&{j^cG)SvlNaHG6lw>cm!aG`f13&SjLPBwK5|7nOzzQC3cD`s_iDn`p;%iu!6`ynKuom|F$60f45yNdUGe2H}Rhmyy*ZJhSqfa+o!OZ>VOMDlR4W|^O|(2q_7BpZP%Y@Y; +Q}UJNJctzFXHDck4a8a_fhfSoI#YVg&6^L09gJ>Pb;B1(_2EXswD9Vac5?%m87AWt&BnVJnMjBAmI$Y +NP8U*Da7#Dc7w9JUhTA!lgT=Jl)D~fH~-HfT2gX>A;@Dq{hLk%VG5C&h$fjoa+Ie1R8YTnzq+GlL5j> +Z5^xIKX@er+~SWSk2;fpUgWFKx4uv=c2q8|1aOb9EbWQ$brg&za&kzxnR{SM>h{Kj7+I3&)`ku^ +DY5WGTH`tvDI=u<}AZ#nDS(&+r6=byV${g~enD5p9lz{s}atK5#@APG=&AKu#S7QYbwHitr`w5XiiE# +CvRLwlUQv$fyP)zctkM_VPs~kjL7O^WSlmCEEBJ3^#fhQ#eti;|$6 +i)u3VlP0GKR=fox>Ic+I-WD`}#`gza}8 +J-o-iw|XW6Qr!f1Ho=2bRfMGPgIsQIV;zx~uTkvMYk@s`xoo;$e(yxsb@N2m4j5t93qzpWD-pL2&AU_ +64cQT2!1j&%EV78*x05OdZ729y?ADP*h!ViXj!P;~M +Y9c~x#lTmd_NAT$g*WTUuxm!I8(JY2wz5RJUk^6O@$AKl5ahi>3lQ+A +a5!k`jH6h?rZh6s;&NOAplQP(Yf0Pl9-p|S_6=NvszRCks)fpVx6{I_6DIWdjtBi<>Pi3> +ucMVJa&AKC={>P}69g{wZl4TRiSY9Nd-G69*irt2SY*DtNGAfg*vB{RqW-Z~_3CTq4M#8UIIkz>*10n +<5A7LP=aoWV?|41ST^zoW&~DqHop)asML0F^AWsz0(|fGVq&uVh7U#-AWa;&dcl7rDqs;#q1{H$c7R- +sT@y2Y5Kisb8b~4^cuo%Ly<+2lvy>IhmUe)VHfV@&X9x9YeS8Z;x1F;{)Rr +C^M}$ORbUyUjbY;k&C61THW25W`}_NGFo+~U%zU%fi%$;BEOwvbJNI6O7{!DO@v|hcT-3u!m0Z$#rF; +MN#r5WYBX}*0~VsuC|D85TF3i-&AUA^VVUC4YK~iBqKEbcM3s;ke(4-ff;{;~bW=z$raZGN8v{{GTzH6+NlJP4D8LsRh=Hn}7BOT`9LAo`wra +RO3+xg$9LkBX3`*DBib)SHlfZePlO)Y`#YXxC5A4OfAiD#T|r&?eLg^kRSEy{Hc=A ++7bK3;y?(HF7$G>F1;C+2a%&`AS6i1#w_ +hbflw^{o}0SO`mem4Ylg_ut{BB|NuS>oaz7BlCF_I`G6563k_mVq^h!2!dA2euPFiUbM4o01?Fn*~t> +KGieYq0AWe0K}m*;A39~WnC!ZlW0As#e6sO4Po-n0emKc`~KI1n%sdRS|U9138Tw=NN3`;wrp>AIRPBqW{RbadP~2A7&f#d4mPKERgSg#T+-RENJ1<%0LM050;eBXw0p_PA*wBI-^f214p&-&lVd>R;dXY_y%H4us? +>uBU3YaRDL$UQzRMb!cgqrfr1`@BzZyejs#C)Ol#dx6gspw!@2*h@<;96vacsc4Y%0ZL%$DjBqgC^%G +JIM_M=Y^~4^HdWeW*PliIKELZCyUr*G!t{5UqcEotfzgBgw@?XNLVjzV~)nY}?k`lHr5*1B!s65Ng0i +x`j%k^+KsA)J;j)e&F=J5J{AcRbm-FV#y;8G}}otn9dvTxP7hsY9jBzij$-$1CC_KHj&2nCZ_7Cl-At +EO5DC}+DuV+TUNbfKp9C7?l^;DCwCe%j}lZ6LHtgQ~71_`}PAP$c!&MaX2?_8I1{2bj|`T)Y^L%2haE +a%V;KC-JK<{n~)5Cvu<5zFM?}53ThVT)QJa?LqGF41{LMXXdj +Jivr3W_Z6Wb}h +Og3s#_MLvhnWmJ-gew*f68YYwrykBQ3W?T)T5swW>X9PPK6T&+1VZoZwOG`dB7c*fe=L5!><4LfBoN2 +Qp%F%&%O*tCuwB!rtLDG9te@tshL}Ec&2V)OXM;TlBn-)FAanw>V#}g{1ghzcn_@m$ty}{oNb~;>a4E +jp)+*ArS>%V8VD`a8*89`qk5`VTEbS;@IdIGXkfRhSaR}p;PZ_ml37dj +A>M$t>)LUm3xK;&$t#7H8PQf7!J(?c?Bu@Es~h2%}~cU&5q)J!dD$wiq-1Bg7Or)GMh>W#Sl7&V)W{n +4`nK8S`_eE7!rg@KSrW#l5W%3ma8yqH7?8a7dJDDtjNsI{=?Q0e^w9I)%RbkU +=8V^e|Qrv5oe1(UV@d#kCz>Rhbq*j$<@zMHY*RK{ya1!)JSEN=5NX&=@zgJ{z!>w>Fi=gr>wynXCM?) +f5#V-_-_H?=b?dKtrps+uEFhrKyhOhOi4^YRE~QE#+i8VEI1fAqVA?cw)>$f_$nH-Gq5I)6Q!(Yaw#Z1UsaZ1EG;R-I(PU1xeDB( +t|Me83>hBywFntT;`MXPig2VgFezj*nsWp;vrv$kH)x+va0KTZGNmCIXG|>jV1?t2#e#|2eBEyoGT8D +v#=vDZXo1PZ`hU}2uW08mdg3n+^!x1ydEJx5K<}Ac<%F=SsRhRL4d9_Hw-%;(mm&2UUk$?ji%ZUgAPKXGc1pDzJ3~fdI*GT59IzF2*K1z{-F9Bx01AQK=kU7= +|HHZ-s0;u5W*=lyqGKUF%}M9F;pK2z0~Pi&DVCP)J$2}kJ|sy{?MJmo36FM1wW&a>AG0?AW487cP~Ggb^Oq|p71(aKkBQxno<%r3y~*beGdb&6jJG0a~JPHz7H6mVW +!BmuSr98fbM315KWDf%VAMH2Z$_TpwK`lrtAhm{#6UW;R#h!c61<5YZ|L99AqxD&gwtTyWI-vsK(Vy` +8F1Si=o$eK@ljNw%Ecq24~b#4GY!G1&Dk4Sx9+T)4Ay`>e)>W5Z##0yyEg7JEmD{=PSstTW32Ex~VtK +PEaF7i4d#|j3Kfle+Y)!Rx|zLyIKdYm_l(~^y8}@*PF`u%XkQGGt;d>9|co%KJjdNBWqd108TYr1$QI +r)qho-bHl+mtXBp?Misy09Q;!_*tO6=Db+ZhOs=mcwVHn2Sct9`zukk61P4_ol81tnLveF(6)?Qbdn2S +RXVSG3+C*2hqJYPxsqqu2vq&}JLG90B~RqIsVSMOPE`sOD;=?7lz4H%{G9cjd(EzFm8W&|X!Ty$ys2Y +f!7rgd?{ZaQ%)Ntn4{&nm^X6I5+nqfVmA*U7GPG=H)LcuV~)3;8B91>rg%rx~nWNE1Hi`fNg+IxNX_2 +$?}N#z&RUT$C9@8)eoFV1|AYHC^8~I2;IJ*V%e8ugqt+K;P8;Vd(f`7S~Jb=>A+WZ2hbc7!2 +L4XiaYC5)8ZCL{_mM$eX_XqI$4Z*zd|!756&-g1tOnk~OEXWI`0_k +P_&zi^4kegdn3P;%W+VBtc~B;XxL_uO#l59Pv}f|c#emEkpqzk!f)rR9=#>+H&t!LLKP^pT7(HG{N1M +{vhwTpAkApVYLc5-!8XLfg0=on2mYP?Lpda)Z4GsY(RL`-~MW=m%X)dIfYsM@-UZ`vad1M=`djMpg4N?-oWZ!m$f~+@HKxx+7Dje-wp|@{^<9#c<+qc5|eJgy}w}M!5Jyb=$nhW4^pE~PNC# +*o&9t==-wYOjNK&Y?A`J>$-B{`sgGJjN2v#g54sEO*sC`4-s0aiafZ{dJw$JL^*i?;JE3dxz+nfu=~b +BDj$ZvRJ`7Io$d1Cc~+b$SL6ogSVYb{aN{ul}Cv|Dy0^+C>S_$|rtwF@R0}4JpPzD6z6NS@=wC1Tce( +2R4I!(~Z|i_t72?I)be47w>hpu*=F?%?H?hIE0EU^TF$!Hw_uM+`DZ*&RWk#2-avvceZ8Hy+bV0y+bT +enl)Bto~1MD1YtfTc6VI7plTQV3nZcf23hKJ9RIw0%-K7w9xixQMrl>hXWXVc#G0(NFoeF&y#ml +a$HFZ@vgYCHT)!IDy>OQjh_!@5xD&qrFA^f^F%ZNt#jVljAp-9oEodr!Qv|w4aeyG-G+RRuUJw%So{~ +O#G7K3>z)L`d%QgOG#u=RE5yP?X4MZg|F0Ni=xsnCP{*=RlE?OzLW>-|SBM`zQzREWZE^Se5v#qz+#J +_>iJO&f)7;Pr6HROr2IW}2zr*mAI~^Gk)m>+f_JPtr39-gt+L<4(usPH}9#g~*-DK|@W2!0WpnC#ev3 +WpiCWSJjN>j{(Fc8`-gxGHG=xG+#9DR;2{IUzn9wz2rB!ekQ|q3lUN4-`@FILm`i>Ry=@P*fD^oLeO= +(R?nJE&K&F%gK^a(5fxsn{n3htSI)FkA?-@1RS|NKI^a^r`_0r+q3(KPjrX|OXMQPpvy;%(LPWH+Yo7 +|?)|uYee|!VlD>+VddmI{GZcAJr0zoxTff+*0|3?mhCEgD&q%KVR6c3FrlMU-9IEE?n&rec|@H*~gkp +5M1%zUbb}($&3QZMf1tJMF2EX^mKrDFLns1zLaP%}hzy?z~0bsq34|OK&OafDK05M3_F~Mk@4L?OU48 +0HV~?;TGr}#UK0+L%K)G4rU_UBP)9MXkR|H^y3gXU9=#Y +UV9u6dPcNA@}wq?O0f*FU(!$L@E|KA>UYWKmXZuU#(Osv{L7r^5L#}vFGCqJ{a5!EmNtKdP7p)R0yTc +#{lnRy9z?Ae3R=PEPYybMKc^68hLXft3J`l0p)gjW9-wcStq%jo|s);U +EZbKG<3*v6MSw@!(pObI7*tz73t^FyXE-l7<4?=AdPsH1MJ6PWiXbin&g5ayZ+LD +V2VyG#c+qw+zQ79v9j7IVF>4r_kg@o*{Y?6+% +U-0@c!a)_q=5}4&->0R6$Wq661Rf2Fq2lx8avakEPE&jM1fpdp7IW!$dmLM(LfZ6};%G3DW|!xwKeMzDrF>}FOy{HwebB|4Iy3R3KkoO9pE}kD${R;IXpJM`# +Y3GFE20Eo*;q~uUwmL_2=DNJd@U|4l}VpyT*|I)CMWG6MZ`rD^j}o}Tx$t;Xxk|6gpP}DRYk-3QNcM=a +i15$PG`xcJNO@+4UYT5|kBG6{OwrdOr)2+T#2?C@1}1{g_>zXE9q*OH|G +C6M2=vg!}UIjGyy#`$_n6O(0(qTbI#RKdFUo|`Gu?PJy-j<_goECOxGij0AC-D4#}YIxa^ +3=@h0fbG_%{#OQXyS>!`_!j_|zZAHXvC!px4*`!Iwt0(r8+dkC}vaQHQdmYbQ%!TXVEXBNoF)7YqcrI%XBQ2#UMF}Z2}mk5O6TgKYF(>6?6_Er +25*C29CRk#Ta276GRtqtYeyBl8R9Tf0;6sJ@h*D5I*pZ-;?x0; +Mc}Sg69S7Fytf7-FE8YfhNMa8-mW`Y1IP>jkYjvW) +V>UBw)7y;^6exRs6$~KD^2|KUgc+U;b_}#u0P-1@Bznon9a1jQ1JspX|XmVTC#moWa+j97mb@2K*x_V +k2;tPZFquA!qqAzx9!fO9x2Vt%7IN&(*sL#Fo$JbEI +d>See3t6TDau1MU*7P^+L_GPskKNu7gmIbZg#W5*N`x6o8YZia}OLjVOmR?d_@}M%a~z0p9}jOIgnI` +f}-l*)2q#rnrDY$}*4}B@~d*z)JJ?zZ?mx2m_I$u)85NME&3o$?(h8`;79 +TxTrRb0Bm`Ev7c5x41F#;3(*jh*>z9mQAI>Xi343W`D!z;5P1sO$-?>(#YD8rP|%(dnTP~S2a0E%6~f7 +1R`c!woAH%Asqu%x8Ygalj*xcR+1#?A!dtQ=t%g!|EE9LDyG_6_r(Lz53xFhN6E2w(r!V`UhSTh6&7` +asgZh^Q-?w!Kc2zbI{Ev(GIO5Khr3S!;+VvV;1{Lx}#Lcgs$d!kuMv!N#Q|kQha%Y-!>2dHxJrMzXWi +>BN2(UbeOZ#6=}|#6sO}2w4;vlqIC? +|{ky+AKrSDxF90c~hJNP7i#X*N*f9sSp(TGXpv*(cu8W(o +9s5UU$v<&MZXos~P}~BVBZ7d*{$iNH^A%l6K<*@ZP*rW}C?ZK63Ps%pY|*Au~O_a1eRgep<e0%B>|Sz1Cjh}CSjLHLMn7YW4hkjTh=&$C1n29kBn|N{VZ=*yg7#wp)d73HoB#gY{REWgC0U3sCxB@4VXey0gKQ~?zT@PS3I!48=(b%*IEx6GyIog8p}_} +m=G(qwC@o5t5UFordvgWVMB@$bC^B#%oa#|V18a?*769UQhR#5wd9q%x*>k?4ts4uGBhe9{U}&O>$M| +Y)Eki9F#0L(OaHxrDA^WBm-iy+Kh@$@KjI<@nKV@;-gl#K@dgcF=fQ6y8rIT|B!5*T0hvYV%yGhn(kK +r3WEUAzWkq5btzl7q-o`=+2^XjoT^o&YZN?(IVCLEbD2wdpKgQ +5SSdCoVifS>V~Q90~SK;FJnY&>L%fLIW~$-)V^=059k>N?Xh)F3w!!n}f)anSQxD(b%tZ%A@cRC=9>8 +oTMWO@IIolXJSTU_vj|t0zXK(5apWCcz$oDrt5FinTO~f77xz;!G$|=12eB(Q5%aA({gfK4Y_%E3A#~QC|?uJ6E6aPI)|KMeg1; +5PUvbZF|=H&qH3pAA9Y2q;zz@_$C0u6-{XQ*csH+rj3R$9gTw$Kc)o92!w!XTNA{9io#cTPpwj!biOke=#h;MkeKXU$vyGd9T8KPJ$>vaKc{1xS>Q)FI +!g@i^M5jCyik?YT=vg(W`ev5vAsSQ>VGbt`q@C?6vpAosrZs^c3Wd+*QkRR$&1(a|Y+xvKJ>R$58486 +@7QqpDcKBlyA7+QxhEUvdpq4?#`oWq*{Nj%-XK14 +byqmfTS2dT9Hn8JpH@-Z%%5CptK4Y{VIke16J36e6FPx|-l_XFH+SWY0q(^hsynl;~h<1Ejrf)cjCFG +iwt@nVlGT_s#F#4u$TAQoGjUsw5i@2azX5$?c-Jd{biYU(Wfnr4qHCYBRGo(|He(BN?(Z6#5{aRL*8D +5isS}h*BWiA3o*!nKmf{ye=OY3Ly~X#1#O&X>ZTj-%v<^-dN+)KAvJjDFFJO7<4EUK=E|C5Wr=ikzLl +5)XZkZV{ke0xGe58m_hE&uF)mk+1U%57l~M22?SB!@3~F9 +O#-@l=$7EwYC~Dnuf?=iDV`U=WL8?wx+&%Frk%+PKM^(T_BB3s5Z}q`YsD!>7Njnr8p|=!=2J{E5h6F%=h037oE1w%jpAm#i +Ir{J{Ts2x_-Hau`$G#K!U9>U{XIiWKl>ltCsQLMBPPL&>_M{{_XjY0s3K5p!RYZ9YO|Tmde74XAK0k7 +KT>7MKx-J8|lXr#7dN04oOhfIVWeZ&J!(NLB)cK5)%=NV%Twg2hC2pft|CPT;z>>IYt;)GttyDR=0f- +E>ed9ZkM&gb(qm%^Rae{_|sve3XQV-gOA~U$WJzQA$3ftXpyAWUyUYl@X7%-|ID20%XEe2?(Tkm9W!C +24Cl~DRn2a%(&;6t&_WS*CgR%$4qGr1DkjmX_~=2W^Z#5{7KTIX~CtFP0+c9P+m2M1K^e7Tu?adx=#1 +wj-@m(3leLZW{CeqBd5G=RG4`VNJnC#cvn +YplwWcJ}>?*rC2V488xbbwV>VUpWmYG?r9Y6rieyHf{4qm(a@tri?}BjiHeCq~lY2VwsyIFhLY38-fDa5R`cKIf~sHJoK=4QF}Bc__3!-#ii@3PsPGwwc}!_N|aggVnGPMDV +ID+)$`{`US;G%_P8G{qS37i@j+ipm7j?0g8GiicSl?_}4*4PP&~9XDHM@zh_Cd$_pFWLlFIgkvC;f^< +xU0RMpJPc+#~k?diZSuy`jzBsyA~AwYGMEdGzLv?){q*??mIsyJw0oQbZ@YI#d!JrvrYEAu_7me9`YP +y%#n+S+uU3&019`k%3?%?h|_s?sV_7-00}?b@Nx09~11Z|<188y>#3QbSD;Eo}5^SgcLQ1n^oI=TOLj +{#{kbP-a4U(~|DPVDB2x)^6D3n@-zrc9?v3H+oPhN<>ZojrXw^9hJU1N*;n4Y-cA*7$yQ}E#iOU~}Js4g1bW#pvaw(h`qJ$_>1&xVrac|&&NIbe9 +x2Kp}JENbDWoYn{ofR2`&m5mGV1l5bOn#I<6q-4npEr6`fes{^*mI5@)y*hKeF +-P`!*W}~oxuo2NBacr4`u43zFAw(Xh+e^7Ysz6IunNWy%VXBD)ahQg`%^{01%f0KO2=tf96%2K#XB~$ +VTDOxoW10bGtg|=62;;@Aucn4u$y0w6}15EttCxUjnVFpI%Smi&z3XyA6fPXrS`k%ohjbX~}S4X^kBa +3XHP!`toArUf~v|)nUk9n!CCZz@^Y>#7r0(3HW69p2&Sp_pUMAQpkTlf#^^Oh2o>%!YYT +Lds|R1bg~L&jtuR!pjv34=moUmn#6;b+Ps?hb11|^-wKQSiNnu+?zDUq6P;A$gIO*4ki3xi-2HV&Y#x +8adpSwEotvSXEK>*$EJ4;huJl2ek`#L*cnMCoUY@C(P0)@1qJaTyfB#3$Sma+xtAIR=mxC~g@(xVS +I?#2u5kdHH=>!KhNxfU(?>IMtmg5(gUC`8-&?)642U1fTt#MPJh~JhZUL%>Ciy~-t}+3#@R2ZJCO;nA +w*p{kaV}b}<~msfsHwg!ZLOW#NDl-C8X#~01jk=-HrG1a5l(GJY(XW?Li&OC?qh2V +Pliw8Q_M7Ll@%pWWSYH_Q($TuF>!52X)^7ixjwqPWUF!un(it)%TjcY0%SR^S=L+sY5V$1n;8mMyUAm +l^4E74SEHs=yH6_vUV{8Dq5s9*)^yVa%usvTut#hL7B);JWBqc?g8429AtQB7X?Z9NAO@A&POvTj`g +HoWDenyHO_aSR+-dfoSNC`3tbn;0eW-Zjvxadg{;)En3eIz&m*UcvuEp;X$pDm2MI!b%}PyD^J;q;WQ +C)jJ08t%cEH&BT4j7h?6dtEBzv&Te>Iq?H6?M_Ny51o5pIYD$FBrd4J!`t6JmxF%ap)ZepIoTG+ +XXj|!!CBioN5h~|dkr12=ZzBH{eX(+)D14a!GJL05b^Xyf0Ae(T1oG-B(JAy +8;N4zfC)MyAv`LU^Sn)o?O?|@;L<7w$(IE#1nuYS%k`J+%Sn72rv-qx3BeuH^aj6#JNQ{B@y-0hXCY@ +0od-&d=pZ)NGw0~*!8`hHO;|g>t|O)J{_qaiEnavF421&8%sJGu(f|72+(_dna!9}RpKIRkN5zr3sCK +#UV8;;z%8p3hxN#@om4|4x(`jNT^hcyeZFKHt1Rh|W-5Cc_dPI3AE1!nKK}bq5&$-E=_Q?FJnpH2Sln +aO}+>*!f=XhwY!*vIQ2k1({(< +b=IbV_=4F8Yv;=KqDE8lH`9tOHV;u0D`_*MGik#jO7mPxuYuzk!-vJ`DZ({E^p=>(WFEl2)m&t>Cmt< +AkWp9@4MB6|+(Uw(uLObX})Lk0z+J~siY=Pkoq{_z~iIi(#9$d(oC=U(uFSRgz>!YT4qyPtPbkf#$2~ +)3yY4d(x!U0TdxdJrG6IGkn&BdcIyt-dXb#chMR{~@~%u1y0te+@N6CH#!N#3tnUf*XKLZRvz9mAvyE +@=jhnTPT#<8Bzg%L?V;z^8@sZ!A^7F!Je2J@AmvuocWhmC%7`rYPr%gq@XeUtZP4-#WK}Llz=SToNab +S#};=>|2mc&>fplO;ls_ujA20Cc&>eO;OY^{jRIRU9$NSf?aNHZPpN~eI&j3mg`1B_YFM~$HEIg(-~_ +3J0-Dc%i1oMYN92C-7L%R%>-tGurqFh>f+A4y>T|T6NAh2v~{WPqrP0H%} +wV^%f|qG67;KIiE%yLOMIEO!Q>X?SKu}x1>-5_ZQ~(0-EeV{*>YuOn9WK;XPQfOhSILh_RZEwiORd4` +js~fRZL_3V2_;W4GDk_(_vnDwc3bR4wX%mTG)&(^Mx6!0ooKJ!Yz&}qfTR-wa?DL4#z~QfhS%`+m-w< +IZO^^*J7>|+dWE{Ojk^P!fxE~(23XL1O{CD{nmD>+uG#G)oV8y*JX>xVk2S85s<22ygJ!&nfGyB>XC) +WQ@hYYRU6e7i7uN}`tZ;~M6~leKX(FEoIg%30P;-9%DCfkGx+NBe;PQvIgBiG$^Q17)3&)V=k#q3w^q +jHE3JM6Uz|jwpC~xZ8bY^oL|xQG<(o>vcH2TN)IgcLrfM5U!GIa8s5msmzqNI1w?D(eR!;;Mh61SA%> +AkdAHCwh^t&N^Lkus+A~1JF5oi*!>vJxO=3j-m2wj+8-Drl$68{nuL;01jH-z)c;N7b^s`(BT2xbWIu +c?E!A5pCp8g%8Tj~eyI5_ms|%Aq=HF%8`dLFjC_EKd2fdfqU}mE{IFE&WD}0d_VA7OXoza@$6Uhpokq#j_YZ`q!jW_v4LPR?Xpff5$q)^-@XtX2&XwN@A>q +0X=l>pdcsNn5cS5q4hnZ9W*r}rO;`sQY4M#B$c9`W47TQ{~?(+vc46?AuO{w$pt~S>|spbLX&z8aJQ_ +};x(bdE5(fpmvE7r>FJ;;laD)#mO!UJ?9sG1_5nW>-12-q4RP(C%zc{;RSd1$9YUh^92iLHk!L^v_vg +{9lO{Uq0Ecl%kqvul)=nUB_FlB{XZDnPq~<*i6eCDc<{K?AP|*gL(GMTDwAd;>Vgb+?D+fmk%r^m3U>HuLsp}LRgAM%;pXc00gvjDfAg%U1#U=~@ARI@7db!By{4k9A?;X +~XiS}fE|*=bn|?Tv#*iYlr2JTZ^(d^CyAM+}jr7}8==rK4TjQUb)ai3~`Tm&G(C8WsaMX{vZHQ5kh>y +6uWo;N}f>5P6!5D(^(6Y5v(YD-W+XG89Ewdw$c!x<=-K8yPJNu-*qLKCe|VR_2E=M3&@NL1P#-vrVZ5 +aH-FtR4L(=jQ3Oz-v7%+J%;|zf;|5XSJW4$24>VUYp<$Ud59dTkZD`~oJ3keKhO?0Ey)AybakSd13B5 +acFv}LQbHuZ2-?G+YPu32OJZD6Q9UoKyj)4xzHul~I^C4*`Je^yVxlf-bXQA&mW5)ZTl4PFSV94a0Wc +hq>xhhbO&%sx9$tU)BDc}He0Z+Bxj_IqD53O-L(pm3(#E69ErL-~tFUH5Ez-4K&ngK!uI?jl4%M@~Y9 +#F1iA6=ywHYd_r2z5JKZ+6Kv7L!}z~>^wM_C${t_0uOMEm$QBL?`@iIi$iCEzs;w)YC`_Egv_2u;@>4 +9l72N0Ah)v=Q9`x??_SlFl}j&NEd$Y$^|tr*18jCYi;tjU*QE)0fAeMFm{5o5fwp+As^!-YQn=BsM{oS;D5c|8RP)d^#ODik{p#_u|LZ +nyQJcX@FBTVtzP?PlgQSbeQ-=MMZOT^eNwc-a|PWiW@D9QZ1reSHY1tAS)A8D$EF+!@elL8x{$MD*ak +IJNEeb?HJIM{AIC`!7{pA&g{s*;We73z0%M1SymDqI{IQd&1^#a*lgOn(Kv7>blAKZvR~HpLhNCma*c4$U9)rI{A5Trvs +|knn*mF1Tje(PU(oOEMdA@E=*^u21G6%^sEy`02PYpz4KxnOgV-4uVm=G+r00+)iaLG(dMNAQsZtJTzy5uFF0Z{~ +(r6FZm+mkRcGxN>m)3QdC|)C@0Yf@On2cR7z$&8|*&_Uh6$jon)0(r&ZnPQRyJE1X1E#3)Ql;f$24H0 +c_X&+E-CctFLcf?bO$!u|XL}=dPv&+Ri#AsNQ-Ykoqh{%nmZ$^2+_zw3 +{yH;wE3e|(LzzxRfG>k;r4w_-&uU|H+c0pZ8}Vd?YN@i}+SWsl0Uh<)O4xeN1}COSQ8W}>el~~#kq_8 +#T}v10=u0_s_fvK{sAl@kaS`g7$n`|A=a&WHI>I*}1xkizv +H=`sdm$>={De2WLSjWwJ@2$O(~*(k(=e>3|Y%@pf-J +>hr;0&D8pX~3vix>^^F=8@aN?(EA{4Sn+CKVt#1WDno#7uC?J!HjVZB2T*pOB5(2)tVls4ZLFjsXB~i +iAYJVR|0;)j~9!m2M&TO9Bfaz60t))ZQZsVa4DSAoy8+FXx-aWeS(O|PKL*aqeJoVPLx6p8Fnl +pQFL^ZPut@hhW-#wV+WBZClbv5$|flVHC@WtH0qPSrxv2(BDl0v4ZZcWw;W){ +4D;7A&ZmLN!I~oSC<Fi9G{tY(}dD`wUP`h;6s{p%B7Es0XHZ`kg11VabfX?=0mm?2VfF}2)nM&9$j{2qG +J79i3m=NsH#`Dz;MLSznjZI$65IK?yT@*82)@sVSB!(TAh>BX(o|iG~sPRttJWp5N2d#x2b>52sV5a< +Q*Fre>ABs7m>8u~)a(1R{tVu))_L%&qR9y)5E>-;4>3nCd#RBZOFrk>JYhC;Gn1`?)ldrkVuTaeuJHT2fzcH@aUUmRUAf5R3AoFx +Cm(qQB?Iz02jm1yVYDfr|kfd%chP$?(^I1N6b=vI#=6?KjLDAWlv)|Ln<;6*MIwFXEPb0#}@CAA-Q9W!gRc+ +Q`|6J$G1!S1De~tW~eXc0+ldzpEDT%GWy_2O)Y1`FE%V+axzj^VIkQTimi7wTMJ7xt>zWWTY<6H1xDv +RT_R}2zFL|hIn9`wRa{i$UH;QsT9oN_778v7CyM|(E)AN_tk%s;m)fSp^l+d>3Kgm${ +E;ht=R_zB5g)dN#)b-_Go4Q>I%S~1Pz#Z#xu|l427jCKQA@z0=+kO*N>2+4yhRXgAN521b#7xM`v_4V +yCD<}k%!rA^KAN_qypF_A2^cvlwO~dap@0xc3~(T>Z=WlTPy^r3%tuiw@K6Z0M7HujM$<_K_ODM+7xiVQw_42W{oAYF+ata{zdNdF7RAk@-URCq0x6T>DxLl7oDaP{@S%5qZTj^@m??9hyH%Wp%q-3>4yL=gDaS-6+;H-_O_@p8m>!wrWsF0lK_#S=%>?lPNUP$yBr+0K2m +N{hhRGoqJu&e(m)r51T1(Yj4#MO`XXr|BI;hqN8yrHt<4>Hyir9FAR^KmE{-{qtI{mAYZ^@~L`=K3Cs +BzMKdJNh!iSMMkYS`pLWtzuN!$R(RweBKC#T8y)1Z0$HKyFwF52G0cI^F;&?DWRpO`tAbA9{a)^tIK5 +&eNI@`Mu{RTIMR)tZ(tk7M1m5RG@F_@kOA=({{g<7)x(2BNG&bC!a-au8+}vJS3)RMYNxdI)=4s7ZR= +=A8;c+qv4jw3#d%WQW&>=i%E^u^XAM_`u0RCl0kaTpn@*MIq9Nj+UmCGfnV-gE1suHZ?!}w&!vJ#YkD ++Yn?6(NT&-qVM0xkSrp9d9*%_23g8VxWm0@GxE@XZKK*NOeaR46!dK)|*YVM&4HS^MR1sA8eJ8AmQtg +$*he9e2CJ`Y;bXrZQ!1pIv>L1rRhdnXAsN%3;oZX+USTQAx$#9>9pd!0}gga{u~Kq(xh7QI#|HTxF2o?3jVIHD=SnlX +Q_e6k+bJbU8oo7aL*?LKDUs$lOA)v>kL~DX(+nna`j-Qo;scaMDesDW0PrguGXt&>1;0`NS&`7WlJYj +VYXwfeHTOI)n4#$UtOM}Sn0-0IIH`r9Nz#$W8Q8-c@m| +M48qVW|CV%gb4Nm-baFdzUcDZ?bFb`7P@rV`#zt=>fXe5YT8O19kiSJs8mYSg5pIV^iAHh5E)vlcJ>Y3H|mzYInxmdm8>mKVHJD(AHOh(qy_ZJHbe86aEotO1orr=|ldv_BPgfTzOZD?W96-{#)M7Ge8& +qwdHW>)JyklJI&66HM9j#POb?2XTMj3SuMRzE-uK(HuE|oiRVysP%5LB&sg5HrKO!IvM~(hT4}oeOpM +#kkYc~=pednzh)@*mh);@)6r(>Ao4Wbbm9a=vZM{o3*A!7!j6R(S3BB{gSW#|n-y?-&k6-d!~9OycLh +ak2xxCRYKyL`rs2~O3(^+sNT`b_DISjz7W^0?Vn~mKy2y-lPl{!M0idjiD9Fv6#_xA}*wyNwcST{*UO +vr7LSHm0m-Af0b~{3OQJ8EF^b^cHy`oHfP25qj_}vt*Vu4I%T%nJe;YVGb>6|>(97K+~T?0yt{#Jzwh +C>80?Chu`DtgO7K);*0FT1*#oU|P~5f>feZkbJ?V{@g24BA8oK8`}3wQS#V)EGHWa4T?DoI1l%DXdnG +gv2QB_vsy7FK+1A$=ut^P4sKz{uJGiFU*6M)~-iFcyw~!&YKK$gU;HKOlXcSuP@SbSCip&V~!)CJTf< +>F}<=4SON&^Ulbo553(x>u;GWQqa^#?FNz%aC6c^|kLXs?G}YSWrVqg7P-}E~`el^)tx5;oaTR4ox9W +*Hy|~HM6+=W6@m`6XZKCifzMlAD(ZOqjB7jurkP-`%)r?1GR3Qxu{oSWb-a+0h3&s8!jy5oUv=b5(8SQxnH +t7BPRWb5_q)guA!9lQm_zB;TpF6Jzs*Fr?E*84J)%Bxhh%Djy4+TblVz>tZmP2X7pCf9DRlUmB1W_P9 +U`Kh;+oryQPkYzE*f#yhYWo(pJ5Zq>3I+#PH9+e^=iV>q5>P^vQ36fVf75g}Kx8MkxPobumu|I8)D}( +BR=+_is^8G*;%BmdU)%o0W&i5xyCz$kw-5kno +wTNd&a7ISPl~aJGZ0p{Kl250x7yJ%pe9W0!l_uyw3HxX62BF7 +cWT-0M>z1MQ2UeK9=9qA1D&;Vc>7TCGg+_JjTfCca5B{}|3R%!8fSkUC-M2YHSA*$VMXG-Le)=}x53j +6vL$a%qSj}0ajnV+En!FJj9Q;t8|GD}rCGW%M3!is;A1z$i-hu@w?%5A?&rHHaHu@)UYCuo1jviQDck +5eO)kgoP{YHnm|oQQ#P71Jpf)9Vb<+feKL2L6Hu{ug&wU4!`kbq2UF9#6THW6>M3(62?l3<5!j;l}zX +d07oBO(Y-dvg9S}{bHR#E0&yPlBsydJsGlNuh1#|;h~1R@_K*XW5f82PGlt{@)JBP5Tq$AA4_Z8UjLi +gyZK+Qk4#W&=c>ojR`gPM8@hA0QBbi;0#)Tk38g@vt*zQcoy$oYc)(kpf`pkMeB3ton7OW_KPUM|89T +okx2IM5t)J3dSM<@abSOl!dt|r&<-O`P4z=iKJm-+oUIyJD24-hcGhyg3q(R9X~{whuZidmxsuFg*|U +@mL^7MNkPvvm(cCndqUN7P3qy?xqo|rkjtJ-BJ`%r$5crq63XwCnH +JsXauy&WLh<&Fi^LE%9emqL3tN$cJ*nh*R11~+V@U_E?C|x3pywM0Ej=OIaaj{x(mu|b$SM4|IBF2g4 +Si4&I?o(hz_8Y4DpfkcHG`OR__L^<5aQUaQoljyAZ!&&_aI>UyBpEkxeu-sUS0Q`DYJ3S`>ynby`PK)`lfPEV+HPO55ln->L +bpIV5BU^jL9zPuL)iDm)l#6TW`GzdCmkq+odsYe0w+MqS8pV}~b0|#1o7>3*MscT>2+>||_x3c&dpF0 +Ne)EUwl5CZ%JnWxT-+ja#F!1I-!P~}{#r#7D*fc(8UhG_7Sb%3uHhOmv3wzo8}ow}MTK$|fgLm9Gb1i +-sELeNhioLcOc$4AoiL2o8rcGy>Edk8p=`i{`x;(rL-|?Q-y#=mL(q_{^ +Ba$UULK`R2Y!PwH?AT6C2Xuq-x*ucmF%!2vD!1MJi}_k{k277{i?jCpQk4qQIW){*;T_ejSL%H7F1Ds +;}ZnG%`GWM$zX`Z01GmJF!NNuR2zjR7w52MZAs4OH8aKj8;_M8)s4wLdi=yp6lPkI3(1BLjUTUJKh3I +u!4mRts&X+Gb{BAz~t%^sL}2v2al-C`v+rw>Rm@96vdELe1Tlf```(HhMyy^OnQPo>1l3Nsm9T@iU<9 +JYb=!kQray3o&{0W%a`Rn3aJOR4n6&q0$M3TWiRFbm*+&OHoDZYH``X6 +Bbm*zstSjt8?{N+%V7%MsVH-32?#p3vh^P|B42{KzCL0Kfa*tVw!?^n^m^%X40ay+VQ=g7&bSLdni~z +S1GI)j@Z0BY(R{6g0d})4wD@pM%Psp&5=$&sS%8kq3x~D4MW+c~HUAPp`(RcK!?mks~g7jN4+BOC3OY +R0$#`G`z%1d$)%!t0zP{Z`ic#b%Z)^sqk@+)2*J+>YP^Psmk5LoFFpPuG3xoaHo}g`263i9(RK=-Ht* +FT(G~N4?<7KdF&qMl2W%%G{EbF9O`^dMrjf@Q-Ye6{|wN)L2&vL*}3XsrMBf1oTSTm +jA`-B*=X*v{aj5eGx^A_-Hwb`(dIkh9#|2;gGS?1pNQ7n>_<^&=p3%_s@#>*A3Nj;vhXL1zjA@4-hI4 +QHm!>0fCJ5m88F(Gx15i!_Nz<3Ni-f+&!1-|Ptu(4;CK?#(bo-9LJgxW7g)D!e{RMP<-MzFg8YyDZi( +9wJ9u@9W1{o>ZE_(diL+Vd0j@TpH((%j!Yvb9L7QAg@r_qEIGuTbXH#S*N!U5z!SD?QMZ3_?KqeOAGD +<56aRQa3wH;Rp`hN6%h_uJiJnjrT~v#8le +nEL2lfGUHgi!z6gR6j*Omh=h0Z&a@5w&~pZtj*3iNBAL{^9gGYb8dv +7GU_6FajiZy^fyG&aL1%U>SYHn}I{L1$#@TRyC)$pY+1P>Fjrs1!0CpQ+$x(jItT4Z98~7CPx?AzukW +o4o{s0(wFsGljw=uiF9bXBJ<>s8) +JH*7F_Thn|r3oGTwT)`Dn>sN{*CbZxhO?fw$O>+?1g_fWdsX5lsg70+m1Ua+N>%rHF$z^6CqOD)of==X9-}b)Ib&i +j1c%M2__I8B2YVZ?{_ahR;+~KQy&;spCp1Cy +T8F57|Bm}!*1B=8%usCi1^mR7KjNcQk%X0jRgs9%etd;}E{cUN*XGVkxa46Q)j|Jm2u@FGh2FGFhhm} +0y1uI#>WL<3jTip9``BScTjDSJVhZ<^hq +<={E`_a$@*3Y4)iZU}0c6jDa~9MIon+>M))a@5#RQS3D2pcte7m3}^<~zFX|-Aj;9?jyYSwjRCqe^X0 +6PqpVI0J%1B!%Bzf4X!9?JqoXJuVIb5H1n?)1jSKAA=7RKSi9;hqo;{XWRPIPIIcDFm#9%VOT?>F*r{ +v?Uw*ehq9exsDf0`xFxoK198`_nP;%6Gx@c&^#AS!bS>G3*AZhD{h5Y>2P?(g>YzUhB}>@2c;f7M2hY}tEg*>pD*duXldfBipJXfmTfGu7lptA+0g;n3e&o7U}8buhegx`mn{&90~c;Qc_s(CA`#d7W +KeNRW{oiD+i4bt9pDRwx+C(vy?)+NGrN5cvc(Hb%X0N!zzxK@=1D+?9414T&saP{o}OR27};vZ`l&MJ +}Qqeu*5?w~2d!F+$~2{@RKH05@9l|#Ll7Tq1Dmd>+A>x)rVY81Wqh&?VhE8?mZuLQz-j{Gmt|0FeITNHxk= +rxm-)SlYQ(Ux>^_c*RMH$wg_rnwh>1T!UTQrao_0{s*O&v2_GjZ*kR5l?I{xlnD{x$E79~lAx9e0X8( +)Z-tvH^D|jKI35J+p23$~}{K!T!II~p&+w&om8QtU$dEp{%GY3BY7M=mV=>yT3o3+Zy=0yzfN}w}Jjx +MjRE?KH=f%6%O7LTH%Nj(1Y_t<2Jn`Trk`}E@dtK#IO5+@$P3kOWK%}NR=dVN=(%+ZY6Sslt! +Ec}j^Ru3iA>GFPNt)QZo_lbHmS%oA$e%E*5@knrCG+4zix{|^kd><@B+$0-tv(NBNW;9D>-xHC^rG@` +;Akx~u!YJWIv#B<2xEX?+bWkwpRKFxO1T`k79u9OyO?%|G`sUFVCyGITROKYp#9_kyDXJ^LVk2@+L|t +Wf~XeoxA&!i{IzbD)Br3*Ozp`YN{>b}(`@ei#Kgn5k6VQjDH}N%zXvbs-%&}pG?4PmJJ=9(_R_=NWZM +o=qLkkb&cirLuxsZ4)k?=Fwbms?g{svL$C0DRVmGyJPpFWxb=m&901yHxWJjm*^_7I}+kpBbv#gKLLg +OIV?cmfC3MBS3zBW&)H8&=r108C-Q~T<|+z2H|*~S!DH=4GT0^kPI))jvg3ihd9`dtJBE{lm>N8>nbX +%Os4Ab~AKWFV@@a+b48TIl2p4?k@mBgdXlCB3Eie)6N9kRQFRLc0BAM@6x2pDYpCJ>w(6P(SLVCGaqN +EM!NC+Gx$Eu@>5Gs_+<&g#zi;uAP&PS8<@?tazaA4Liqsi7 +Ac`U?7Z&`aD3mwuMYkb(xa`{;3kX)p(kNS21QQy%7l5KEp99rtBnr(b!7Q?r?s`dyW+9lM3y>da +#yONy`iU#?Fign2wqcI^tgUTWw2L +i%Qa+J6M7(wkcB0gKgz?_8w!u#JZa=wow2ItRgAL>OH=(*0+9cAd*NN3WfStF~3usK69w11C<#~mggK +AuZNjjiPZM#G&T9hEeJ!2J_lYwx{5T@_9u3wnMhK7D#IEn2#wvb!%GA~!5%$i%=rbO_59LdGNqyV@k4 +)06ZzqwH4X%gs18#rnV`qDD<JyzxnzRuIu|qa9l!KNyVC ++fe)YSwkH$0etJOcu$RhHUe>Zb2|ZIT2b0pXmf9WG|lrmVjjx;W9qGd6XYcxiG2Nr)L-rlwE}Hty3s= +C7#8OAdTX{>X$HX}NcIOb+-YGzF1eJZ6V!n1a}38Z|H1!q1aScbJ@MG+L}Pr+TfRnt4>qr1VS6C(8i@ +K3LFJz==WdErBNagorFEW3ad +M^a-4|lrNFlnv^erQrs;o1C=|Cq94)DB}qqMW0-24UgKTrFuBuDwjru+dYzTkIogt=`X;zFyTigy9=K +ga#ZA-ka(tb;*a8D0Kg~{4|NYPEX_g?DNtj}qC6l?h@-BVxFON!`Zhj*dtvA21OqbOV?7Xs;T_d$php +C*7?Mg#mr;uv};lxrs5o4!Tg$SRv2W-W%E<_o+xh=HqES*;-ZNxgB9j0%Zm*8zqA>xxkHBP!rEBgnQu +wOC2FGJ-`Hc0C>ii=4?pth&H4Zn@wq0_d*L{G!??oKHy25`Wn0aZfh4ii8v)s{nmt3Fjb-Q51A%#9T! +)b?P%-JM*u72WOd8bm)0$muYJ(^r*)w1xt2#Ab#{fq>!Q +CR+)%N#oJ997e9u2#CWhJfR)gD1#HL=1Eq(x$%$mV-d5>@{~YrKV2FV}=UQmJn(Hvh(p9nI;0mC3m`v +&DQ1%WJEB%x1=S)%*C25(Pbhij?i8r~Bqr>D(i_uWOjO#r-ApkqJg#Mebf5bi?UKMTAVe+L>unfZSEW +BB8AYlg`rdvAK3pdCiOkC`_7>lLwypkfXKBaqf{)7(GE?vQuzRRX&K~%Zb-i8}(0&isWjy<+Ms~GX13 +T-B+BzGVz857;_9#qOSH%^w+cw;^AH)Th1r4XE-E>Fk&-O>34%R$T`#G?@vI1N;?+JJKkZq_~UI3WR< +xsB-motT%mvALu%>sih4q>3lgBDU$h90;H6OAq0uQg7-wgfBpMPxYie|u+6h`o7dc}A<7n8Eq)zWhYSYLaeg +-P~%iQglKQ_J6_4Sm>p!Bn>%1=^|r@3$ciAH_gW)h7d1C4Ab*z3shtR1F!3iV_yocqoL`Nzpj`a^AR) +q1Vg4A>Ou4_|HbQ+d-u%OQv6B0+^nlN^% +Rx@lzLfZK^GkuGN14#c!t7c=3gIt-oh?AvKFD098~!gyQnbg*T`kMhZ`@o{A?$;9c6$50&e)~rxx0 +vLWJY^~xx{fxBG`TnFZvX_4AlYzC+_T<$bJf_@shDVzOy#}eK(6+&2X&}jTvo{vG%!;ruTso*n0P2ii +fEUv6$5NX;|8Xa%l+E?gfW0XVjmN+qoB+2*Ws!nJy0LcW5`}afxkI2N4_rop;W&4sx3NEu`z|E??sP6 +9diHRL!XG>g` +1HB7e-+gZK^#K-@q +jdUkX<$2iWY`?SV>!f@H0#=pBHUZy~8v$mB+iW0zgj!huY19i|xiT0-QLb<;Tzny}?Rcbrk>&{*bsh> +f`f7b}7QGB2H&kyJxumur{8(yiU(uAnl_sCek-g2@gO4o&3_Y+?JW9JI>Au`ih}zv=OJpO@Ean-&Pdk +UH?@YU{%1b8cowMOoba_kXjwCjltt*?<4{&WW~NSE?-1Ympqz!U_bC{o>0JzICc9TIzH08olNQ4p(3Dppdr)ia%dweqWbBd~j7HSQF2ccdyC4w* +%bE6<&#UL|%D6=^OJT$U6c9>ddq(I^PcL}$){=8KE5qod-0Fd^|j3+;#?~Cdm&KC^$cOD#j$_|D{_aO +7^598lp&|J}|An1h@l^rGrdetbSYM|lG^pDBS%&wIV4mcpGBVT76JA +TQ^}CA7@y?cTumUZ +Ba+UZF3P&wNUWYEfl*SsRxnoPQ?$QAA +!GX9kPOVa=V~#vcOiK8Y=$D{PsYp=ztCwH7U}B2#5jl{$s?8IP!Y|5aMbcrY3hY? +yN!|@l5yt+PdeF=jK1P~e7BW3{JGr%TnpVHRG8r@F@=p~1MU3PzH+MdnapnV`jW|=egFW%6Tcj!(w?DwREeBy^+96pKp3h76MlE18lP|+*h4~;O +90`C%=dA<^s2L682A`kR#j-8z{bd6rPunnT7#&tXEK+6MCsO^haJ?nW3J`Fxao6AK%C2L%O5W-o1&+< +;!qiwRK_!cfCCP6C7|-1^PW%n0nkay>TJTaIq#Xc5U_CvKno4MCd7rRe3oBrE*J{fj1@ +>pF=!isJp?N4ncAErvMd;2OS)a?jWJXNw9SFdSdNjq;>r2bJiHWdPNg{C_s=SJM`%P3vD(hd~fSJwbkoO8KcdJnz#qre|)X;190>oUF0PZbYcrqy{( +Mv*QP>D=s=5ItrUj5oUzB>&61OrP_LvwN3Gb6(ehRi4w&!xeb8ARM`Et;=LNCb9C?rmmrYP0x51m~AN +Y3iL_HKgCZhW(P`9>KB`@5QHI{b{@NYrqTnFp#jbe<_|Q^ugiosU&4WURFG_9rZfm7c)8eoKy}VpQGy ++%EhmJQ+m2PD5WI%~VNvOiFGK&=KUFFXh!SzCv?Ol>pKM~!Hirm +?mjEeIfeRO2bn>!X%xZf0M`%kl5-{9}QtCdmV#iktaHTMNCmqb3om6Z$}sJ*nz3bWH%S +ly&UWdTO0`B1n6M;)GpKByyl?aWqO;@aA~wX2q%`vi)FGIReYIqAkU8~W{f>=!&wRoCoY_&`1f_=8!^ +R9B6mBmj`AY1ARIlAz)1EDk0|f}(~1OD(r-^=Vpb}6c7j&`VTfL0b(y3F9W#2dSy2$ie{}yt{}~dy9` +e?7ndD}cYSVg<<}yMMmgobcr}I4WD>Xp1ZhK8qmnm&*gz{v#WPlCHrZau8Mx_Jch(0uW?-CV-em*t?( +SZ#?d}_=2*kwYRqa%>JOknd*x{rA_18^SfIM5$@UK`s91J{bGWESP^9<#<6KtV2^`?^dg1BFp2oIJwu +*5?=QGKtJgrI3cwUgbDw3@+z_?DBd*uTU9xnPTSJtX#Qq^e}i&pHGJ!Hr9$m^W9U$%!>|R~vUy9}=e^wRaNw)0TwOc+%6pl`s@6ri)I%fvI|t^W3DV)BU~2ty`gn|ZD|b>I6hMs1a29rB^o6#dwmsbUPPOEq$3w-_Vc8)p)Q +F+Ykw%IOAr=ib{35Uw)qDq+Qbl}ru08~jc=tCk?;!6Igav(XN31zxm3A5AlF&oBdx&B6hl#Loect +zU%*iN*M+OkN~uSs?;BRD1N-lD*k*;_cAG5W_t@<0EKyKFX;-H@7jDHW7n^1A6){lgzx%r0;z=y2roYsl7c}sYJYq|F34b&6t2IDfylT_y(i +K0Qg747zk;9*LF;cBrTT$GNfq1znTjQiqxGM*aG?cZsJ?%*5WEn_`T#fqGfBE>puyrCF~ +qyw@&QwSA?wrBHp$%}y^4qbT3Sa&JL6;${rh#GHTmb27Xf&n{p;IsYR5oCt%5uaCOCDv!Ar&>scTP!E +mJ3+y>O@(SQ=bZmz)Kb)JYr58|S&X+L7j191%6vm0qq7Z7o_57&w236c>dsU&&l>uE3sP^WkYN0L@+W +fR;P?zax{%Mx~F4Ncy^e(EcotwD4%MF+Vr$GfK`Z|KWUHO$9gBx(Twv6d6QlJ +n^$ywph6;hH|^}Ab?6f1E{>lCI&c9Gi_No04X*zG;Z!hEipuwm-0#1@=kADH`^rgAb`vmsZ1%Vt+|r- +()$+xUMTd|Wg?r){cV+>+d`xH@MwX1mx*%VZrqp#66|}pC`W>v61;W=eX{H-=q`ox#`i^0fzad|QpFa7HwO1t`hTX;dTl@e3BXwKOxX3qH`V&+^(^vf +Fb%Xz@4HO!6GjhliJ>wE_=bsHXc@@z?hf3h6i&&%PEE@3wSKX|Vl=s!co0C#;F`WAs`^1+_Sa~ws&rs +L>A}?gT_yoKB0Eo)>41X!YbDo4V6xF5EHw>WD)JfaS80aN-M~SQ8I}A5XVqy+1J9F2e;0L>%zd@-w9> +!YzqUzW$tTl*pmiGGEjN(~=`kQIfh?PsG3YWu&}6N)c=mLFt>~r~0alb( +sWcn3i$6y>-eV3vDarfL&1)_sW$`L%?P%XMwy6_xe$$4o)03t2=a=$Y&f5gXQ98HW_jt!<$zH`l-dub +w1jPvX}4)LUTZ}qCi=+wnf+}ut7ydJ$IQ>GPV~k1_YAG*NM2@;9^6?JD1V@M#+3{{LA0SRg{?dIh5{Q~3NmEVIjGK0|w +%H`Igr2ZAt!!FF9H^qCcK+n$=s9{}N~T8f9qZc9|-6XfMK?CNu)%(fh=@iF{V6I+3?tZn5tKeL>vy63o#bc*ON6nXy(0G(w%aF +8l@;3^E7POUPN~VKYUWf&bKIz)29mwL4FQ|CEkT533*m=fe{KG$ +SB}4?Umfwvc?FmCQV~{QGHH$b8&Msa#PtKJ?vjeHDfE8!Y|1bJ1#Da0r^3!;!pmdutuVV)>s&-+WGxU +a6NHv+5`RPY29wUOy8!@Cc+beM0hfJPLyaHa&2=lIAHZ`*u)w2k4SpmWzwA2G$%7>5{LSz>f&T9RADm +Mo=%vFKHW25qlv3=Ks1qY@X;0g_YQd4-%T2uXaEEf8&JE-9vzkPpv%NK#v|h@A|dc!LsPq7-wXm*|HH +w7)6wX>5iX*~#keHUsA$|}DxFd2t4jfx&v{P*QkXg?O&)QKJK$p+zZR9|T*_SC%O_ik2*H<_s&6ne +n6UM5peYsj{0Z4HS02o7KgA$2-+|1hZ~J5`pE=NPcz&D(M@2o_JWo={8bblw(Q7D~Y-AL|O*^StX8$V +epIhD{J0rWs(^kk)4 +3OYI%vG85legkUUoD4cc}_Uvmd1ngr!{>Z&N*=_o!X$F$ECR`Wjm&#{~s +U3+cjrdrSs|^%%#`JLN$DWd(B#_m~{WSV!rd1T6&vKX7|Y2^1RtdIyOb~RFEaZ0Qt~YOz6CfV%h#yBKK=WvoE>#}Osk}FEVn +}!)jMY-3_p8U0K;u}C&U;gNI3+-&>CP}_du)%%gMJi>!>TdGnIhyaXWiTFT^ReS1ZG!cBFr(MpVvW0t02xJ04=GR|BQ|Zt|g8quHJxt&U>>LP7e3nps(9Lkpf&SZ6^#Ey^z>+gruMiFfHusnuXt*3N0=L&&AY +#XAUQyA{g-S{j;0O+92QOVN^q2%_0AYGgbm5^o4fdG)X9OXlHaUX>m#U%p5uaIq)5xmW_%^kI+{i_YfZNIXF3Os +1a>_p8+tLDRF7$f+_$E{z6n(h1*)jlCJQZ#4HXZW5Po}?m0NH(Tm0uz36a|!7W%l7d1Z^CwaW0vQst0 +7)a&z!{u9@=#Qc!rBO>+Mq?L=l&MspbZ?j6YMdS(5n +vfrt^C-RoYD1^P0syrWl&sEdvf@^`oMo;rMI7fd4Qu57tpKid3Jr_czQcJmn*%!6nkwAL1 +`248_>@jzA6g7#}q=Nh2B(?9D-vdfbax6Ia3AoSd|?pHc;jeqn1Sugr%AHa*)g5O}8-n!=uJN67zQ@!-b3J}#p57Gi1H@SM?&^(iMk1P?|W7?os +_BcB#(y_<%L9gpT>wv0M&roys?7;@d*KOM_OQ4?I56fGtaz=RJjSpUZlgKB}k{t4(KM2odLjQ{kJrLc@BlgZLm*WLO{~(! +B?)t~8*i!q((pM|A4dg3N5nn#m{iDQ_`h|P0zEJ(QjZCRuFGc#oaIn0{8T3N!c|6($%4K{KPp2Ffwuj +pDFk~=EOczwPc(EyKB)-fuM8X(3H6vr=(Tm`b?(<#;yv!(M$j5Mw +BvMao9>ccz~B7K(!Wp^4(m{m=jqC&R{br+sY#VX{uiC=RkQkenp_5z9+L}wmB}DeYl$E%d7 +)m9m&DP_$wp;cqrO3~>KoUZYJ|o%4ViV`I%wtrYib&MOfvL+qs(;-WM1`P^04Z?A5Saw3rzfXSQOee{ +oU!(44nL4lc{<0kgA8mB~dtpORjK$+p-h%rE)obkBN$|zRV`m$#guL1S1E+kQj}>PKM)#5kOT%=iq6Y +$LiV0X%K2AQngT1NzqK7&HWMz(B|}^>Y~x$=8r0P@I+H8n&dzqhLC`F_Yk-upk`45IQjxdcmm5B6aKTP)*H%fy!5qdtR?EuGK(WT;*y5b$x +R^ek7L2?lJjMSp4PRG5~w$^T&d|V;0vYA+?Kfvk~}NO=|p?Mzus^3$Wv~xrpF3uJbHwKx$)w_UfjJfU?4~`YM|y^hKXDGR=M!TFB;+p4_n|@w` +H)MjOJ|{$#%KPHJjzdX!L@Tu!NJz}wPNjHQcY4>wT-LQ_`K`~LcpMTJNs35q%wOJbplC3 +(8fkJ7e4T_)?1Dk=Pt%`SH +kJ4xLS=@5nlid&W29;%qX~jz%P=Y|2p}5V=KofKAGxIBkxNR{Y +XL_nanuB4o{L_w8R5rOkvhn)D-N)y8iIksImFzJ|(UJB&l@_^rU&bC}P%8pOemhkey(a#q$7DuhJ>I1 +LEex<7y_$+41^!*6pL}FMAVD#t$!s{*yT$3X|A-D-IP?+Bxs5EeUseaT8D;NR)s}$pRXB@7kAnuS$~Y ++tP~OU!1Ea&0c$mdL{d!QP6;>c%bHU6A_DIQ$tI|y%U?VcEu2P>OCqWQz^>BsU&F>E7)$1{tk(zR`8kEZ&fY2o7D={d=uVT5wdwVKInpB~p8cqx~Z +#)B5=_@HAcmuMsFt~@*QYJ%w-ja5y$3#eWA#nck3Yr@{)g%pXrstFEu`f6TP#4kkm>wxK^`MI%9IADF +z&!tZIn0##a04euEnkZ&lcs}FfE57@w3oZ-F`d#+74JNzYs`4H?exk~}C|0-|8mJxR*@8r+O2hpw(+iea0T4(?ANfTAiv@>N+}GtwB?u>mv|TYr!+< +K0CbQXiF_|qn)R&vV@YUA`RE7jj>a{H7B)L}}1dv{4^`+GzdvWUVBvBj00kq$QZ8a>t?NHkV40aJ +3)G+xSg&W1vZ~K2(h7WUew;G#R`Xv0vn=FiFNfy&gi#Q>FLG5bl|~5T@|N +5m{HLB1zjjyVBV#RZz0|tM_x&tSzcbx|UJ49;QiRLTCVlqv$azaEYa|q=ky~;0q9>C|lB`z +AKGrv_X+7L%sbX&Y;gBkm!iT+HS=G%Bu4o+yL;mfuR1fg=OcgQ;*z)|ytIE-Tj_vTF;mZv2NG`oE6X> +vOg;x^Vse6p2qELIGqa*Sc@96?S#Yy^%ue9wo+9z_0APj-=KaKLdN(q3^ohp_V>8;Iq#Nh25T%6>ql4 +*36rT4zZG#rZEABH}9lM0oFYMnB`Hq6S?!wE{QX+zwCa0J3w?C_cA?LBuAW)Cf^>!=6ezU7^1t}K+RiDo+(u@4VAfn4%f+GgCI1C!3KZTnAN^h`!sbK(!C{a|4NU?zr1kfr23{Yhosi6;(n_cALnj(RSc+clManX`#(?{RI|sEi(jR$p +7@W^W~p(of&4UcF+f*?5Gw4hH0FPzCL>`ciOmGZ*N5STa>pNpd;?y +byJAew^aG$AnC;>%fyGufhGchN5B=^btyl@BjUOHnGYw#UkXGhAADU&PYiOpvjVE)ipg;F^#^4&PcUj +&5rl=6@iMF5>+gN>nl@93E`BNQtC0;Qjp}@zE8tt_$5RTnjniJHYa2LVtVBdPyjT!k#lAegO6&De*0w +_=#_L2V$!GeS^6kVB1i@toF2qXy^YpW^MXU +e8w>7yq+;6b#{R80`nzFev;q;R$%Ea4i^XTqj$k%lFo&oocp(kxyNwTa1r+G~9#b0U_y0 +bB{)eRiLTn-;;B>G}A2#KDiyKGQOd6MfQ*T*6xf`ZL@k#;%`UdARN~In#Wzhti%L_=4DHQl{59NcvpG +^tv8HZck=%%K)2=SV|0SHtsnz8eQX7+{+oOF=EMTAl +~GH};t_X_4k?o0c|5PX*+KQ7O{Rcxoo%6J*5FAUt8?Pe*4)>@yM4dA4GJO=wnCwS{s^+pQid6<}&Nwz +ibrqY1`(kH9c1{4B~Nk=6~GgT!%Mxc+2rdLRJ7!(r%2roDEdFaSDBopIzy}aDIi +b)>KM)EfN@vgpTQn6APj8To319)woHV7|IuTJ05pG8GW+Ti&Tav+N#)hL;i8j+DyEGe{}$oov5WTKp6 +cROlFrz}e%>|S02M6#wix*n0zl2rRjp%Gz4UU@ls*$HnH!#PR~CB9XMk;)N-5oK_PG&jQvh8 +#WTI$W13$8~s-?=&e~5tX$ZT5K05NUo%aVYQCcXZD|KI=q->CpSq>&*gaNJ4%aWvYf+-=e{v6-12;Ar +$E4Cq%4*xC76CANy02f9#GrP8-FxzEuMGEk3gWUrUr=Ugo@+1?VMy{3!_9!!1z5jQ=2x^{%R;nioVrQ +f0wuKH~#Cd#th9}lDvBcqJgos?Vs|BAd^rm_N|siB+dbAc0R7UPhDF)>4I%MzY!w}rHoe=DfggAQ}JV +b(HPFlgNAtxx^ipc<#?WML8y!?XnuC&F>f4}wKm@ZadNT(x>xbrsb-v=|;#E;WctWeX-eb0EtSE1C53 +LtCGTqh5-?)xUA2*P&*{i$2py{SnEjN`6cC`hX9hyCuS+scTj1NsCubi5p~jIz2s5P`%FtQpWv_1q%? +kK}=OrH_2_HP^seq8`8X~5-t`Kn@$FyE?4EL0{ToGHJt<;n)8iHqLxr``-h7%cfd6beO6fMdUE5_vKn +ZW()O8tY9LE^E++IBfy6o2qDHH?)ctVenCgB!w!@Ld$NEfF_39Fl{hG8Y!wl&R{5iJIbX9O<+e5UmUb +#I8pgZ_K$f1-OT+JZnJ`-PkyHizqT@8oKHLAZF!$0P3_&u=3!K;s3R0;+2=c})_;bj&6uZOer_nAB@$ +X0T_m#$cmgZdBSbAf82vX46sbUdHayM_P$@4d=?xk!tRLJ}5fA1Z`;LEP9sx(HIA37=lsgN4TJL-lv5 +GJTd}(0o2r{$wguP{205BSI1SOym^G#K@xT9fSjBo}i1X&(uvfX*%2O#t(2Z1Q418Ahx9`eCDw%z_JB +@$?~M4w{Z1e=I?5=gesn16NJ@gVyD*>q3$!G(`DqNhf)V@O+2M<@&8X*$v%@n9VL3{^Xf?d;FHh)@h4 +MJ6eMDJk}kvz0o&1&kv6XIX52&lwNbg#F$F&QO!stILg1tg5~P>p?ypX6sz3HU)XcB@Yq8i6xhL!ar&1 +oKB%6{FD9(OGV(&4t~ap@+F|7v>_P=s{0N6epKodd|BT3_+$b#zXw${1;KP4Y*hv~^aezFC)^*fd51> +rtkMk!XS~pXMwLsKCA`8>THeRffw1I%0WA>mqrh52nFFq#AquHJQz{K_rqgI$1R(*eUe@OlB^BLqs0J +_$Oj;_Q36O>0(%Qa#CPcajCz +mSTZqm5;0U$j6aQ8Dt=M=6sPx^(Yi2|2WzS@+uJXoKJl%}fq@l9n93gN_3BOgbfDUyPzeD(*C;6xK0| +5Le=wlsjsRuI8{b9blbR;CVE##F8}mziEuIfQ=b($rm$7ea6ZOnjAx%IlE86uFF9PDUSZ2m$E51*Gab^)!AuaHR77=Oz$cc`-sEu)hXjNsG6) +7OQkZeRB2pMlDp6I+wgWL=eWpw@!HG)VaiH&(Wh4fWD#iF8@#)y3=jYEy_XwCB8YR*; +azKg)v89Phhr*kyMX=;Rn^J49D05?LBUsCush;|*HqrFs)aR5$XndJ*Z6%W~F}8(j>XK(m^*`J6HuiZ +`Enq7uG*I_doTCM7b+qq!Ut6@O@aOz@2rW+{2rbtjDk9dDq&_UF0Gg$bWp1vAi`0T}WbPtt@6>FXxX} +i^n!qE2g_Xf6KPR#(Lm6aei5XxsradsWF9*zI(`O=|kyIPGyWH!|Vd+2^GBOwJK+`+Gue!&$3j>Yv#b +kI<>CTl(wrXV^-wwPRT=JT$^-)I^JfVsn(y`S+TM(9-YZ_JYTz?6d;{{w;-^$7xXa6efpY?L8EzyEWF +#R`R4mr7&9NKQ-sF(+1>!(WQmA(L|%xwBF^2}%=h*HJ4vLIAe_~1XUc1#YK2H`Qvp*+SkEi9NK+9t{S +qiX#vRLm3VksUpAAXYGy@GLh{|4KwInKlQDg)figAP4XHtZ7Bv1l@5J{^AIX6qEffdbUsU^_ngQTCkMCIcW`Ujzhag0Uy +Q=9oTf}qzlr*a_kLqb!?;SB~K7{bP%ncopS +VP-`m(6$N=kB*HS9vvGu93JUQq`IGZneD88hyni*V;UFyqufTOhB-p5hIyaqe@5y-E`AwRAT+sltOSw +TMtZfsV}R}G=_dTUgc2NZV15${N96p>Y;-F0nXe#wA%nIzMs+^lRgz*bkO3AN#7_8Brn)wn$eWn8Xr(&BvlLL*4hygZXdlrNMG58;BM&07$3zH< +xWLDB(Q>Il;8|>0&=`8hs*_c~@2923P1wJ>k>tN=q$>M3k{Iw-{$!YW7qO+{jgIwA)0S*LD8qnxX4`g +$}fLO0o`7_+;qt$8??ZEi-a3J>Vt@%ZlGHS+2QjrkG&4oJ%ea0XT3$^#kTc7;k>CWrcCda!D-PNpXmdR`RyC?4TEWpgx6x}3YO!qa(Z1(D%WK(ZilT$1U +;9i)6i$cLO{s>uLzrrYX8MF0qtMdZH6>hEY +fGajvpZ8h0ql+|Ns|4z{K1m0{(!LTbw&jfT{}`QcV=|7y(=o)><=(kMbw>j|k{6LH_UM5=9F~|Q8%3# +84$YNOW#8J#h?(L^Z@-tT(GL`K0{Z<2o01GK)a;c-W=Gp09sWsmU@XVfz$G#V#XtqCU`225jc +#lHt`0?4rBr?Fr`!gmg@^<<~oD!p5(=A%VMReClwZ>jMRUKVqGD@~3Ow%=k9Q3A +eW$$~@L}c?_)I24b+Yy*oWTRPUedn6qFcft +w`c}QfbpnJ?VYJhLHlCXc8Dmdd9R(1DVR?T(5+3LUuUwS7H9#HfiwRp`|cyqrc0@+Cmjnll$|3j$O*e +onw2P^YY*;xwe^W5bBBC-KJ%xm;b$hK0`GRD*2aN?P;;l)pC|M|`^jY +J*7p9!{Y%3mLz4rxaKElcKRC7ry(+p67J4cMMy}=H#FE)j{PY~Kc*yJF`OxP)0C;<;&Xqp+(Xmff2ne +F`S(^qd3h$@@*a16449dGZ8K(?R@n;U1Z}&A1X2c@imXHSGl7oL5BX(Z}L4H@>vH+k}J-m%OX3puOl4 ++i^VCUETm~-Z0Gik{;`g9D{96huY!EJs6dw09R6f9pKD|rnK;nlIub}7)2O@*N6F|2y~*^-OIlq5GIWDW`!UOx%BvO`RBh_zm^<$2aY66 +4s5{p!7gXqx5(f<`&RT)@G(gZJ#M((se%kQ^_k!`6a2(1%`N#A)#ZbGaGtDco#s4`@#_!c3Rm{SIKtOjMTu)2f`;()F +gxxHXH7+Id2unadM8lG$=O4gJ0jKzI4tl=#pr7-u3Qd~hL+lORu$TIfG38w1U0;gaYvN*S={O|99M3t5{(M@$Ben)3Gx;16sn&bk;i=A +xF@me4c?Qqt~b;sUWO9}wxMrH=SJ=lpNx~jg7cZzqIIipM-Aa~y{=rz$jm1LLes&MCv>KEZhEgw(Q61 +`v(P9P%zt6`*dfGfly21OAuY`ZdT9AT@=IyuEU+Btk_E0=D9bn<0IFV$KCyn(o}?jRlg=*01kW+aE%z%feMf@IZ@l@n +w+e`b`95xgEf?Mi2vAp{jX$gw9Mnc2|5;c@8HM1L7CqxcZ`GR$YwsW^ewraIZlik$dRQf0%;*QfAvUv +4K|=G=EZurJ1?k6p)VwV$D4qOehS!Kh~~)7dAJK2i}^N`OPBZy9Gnkfj_p_Ct-($3>SV&1E3omHt#WA +8I(2SP)~;neP|Mz?al`Y0g%!a#Cx&9o@Z1wVKknQb9aUyOa|T=ctKS57_AhJ2d3O#?HKUKMz0g}Sh-E +phj|A3iUW`x=Ki0h@#IqaL$3pDMGFjAW7Pz3d#Gg;gf8?pJV}a_1KriB7Ju?6Ikb#HS`b^VtL4D~9SZ +d7t;}xIp{oGx!F+N0_`3f&b`|cjhILH;n4>sZ;q3kvK9vhs2jxDTaHfUm5Cg(dJ@OMf(+T%IP1Ha?;SO`h3jc-UysTUUyZFiW^k=-;TDwn +p>7Xt0(RG{o-vXZB-Km5^T0Ke;1W#;4<2>dfx7QhIix9tUv24M+Xer}ZYFebk*>){Rn#DHX#PF1W;V- +77Ng{IItjdYdHXnN|8=#6GQX5<7pr+J=4lLOc*l)M7T +r+Sjses??v3HRTKKH=B3DxmW)7$-We9ZuxQGLtY*!8Yf>fBlRGg8D2J9Sv@iK<{JLffOEV*OwpSC>i_ +4tZawWdEh;acD|Su)8e-Y(h`}kc1~#xUxtV>^4wX0|0bkbVN{{{&0V*GV6_5QfQ06g08fD{B|c-D;IU +(;E)THy6ESiHL{_K4t(HZ?ZapgFhpB3Xni+BmPn(@c5LA?jn@&;9zzc@J8bz5_cmQEkNO2UbQ!-s!3i +{Hmo`f~`{ar?pQ_ +NLBe>|NlYoBBY0b9{ass{^o_iL(@U#Rt6te)*%rh{6PTJ2|02Xhw7gaa86pN>AcQ006O +7im8HWmtjGgbhEku;GnNoE>hVvcpZ5rusCc!uiP~sSaj=|6Zz<4`R4Sg#%E&cOWb!=oP0<@lU;@9{|% +o$|R3+Z5)gg0SMiLV>o-Gtd6I;mlt4AY~-y0fuxPaEA%o@_54^rsoa-Zb%5>Y>D>go${n1?7?9XKci# +;?1A10krMht#L843t!q7Gn;pzM|z)k~t+KHZVavP{mb*hU;7Z!u&VnWsN^C(H=DpnlYb||WnAL@lN-^ +yaOfy^x)*q-z21fn#_*=(nh`d5AwRn-p`>OM*kgduE4 +qi)UH9;V5o${Yww_fbtXjJ@f>K2=F{|0P*p$=HF=BsA)Sn?8YW#LWh$E>+eCkdSq062CbROP>bLKY>B +*W(SK^c)*5aLM@}M{^BNdWaU6uYB|nRVf`%H-p0yW6b=s5rG<`TmZJXQHYzxfDeU`@xfE<~UD}WA!9s +nJpnCk}ewBvh?G`c+7!XK`T}a?3QI-C;@!%p@jyaHDLbdwF`JAW>oVy-cT)jc(*r6G)l>X`ZDN3x)6r +nv1348arK=)Bu$%q3hAi=gwHX^H;ZQ(%5Pw05Ygse|0Dt^GDAkg+G$XvRK$5l$|oA}@3mpn;SudiRd3 +D8swcyl^=p359Zrh&ublxp%vDlR0BIRouCgu$>b(p1;7R4CF^!Vt*@%O4|;5%fR565zBsm8c9~Pg<4F +T-FT(V&=vJ|DNY8Euh*zSp_x$a`vJ!e7C?EKNE$v<&J9dmsy!Sxbu+*Y)9u&M{gtPR*r7~PCCM(7~f! +H?hE(|%jMpIaMZS;54!afO;OB&uAfwSzm)4p+hVTlwgo$E%wX)UH7h7me;R2oLK}T<0%Bbon@bAuLiWu`xz*`$sWj|2yLj(@zU79!$mQ ++&sA4v_jsMyckDrVqhriw$3A1b3a>3L050t0f1Mn&@1lGtkihdzP|=3n8F81l^jX`&clE1K^mF4gKGy +4QcFW>TZiqic^Tshx?(=%W&$ecc1G_A_0%&Z +!5Gl#O9Lt|Uz#*wIJnG_saUj>;d-{svMraDHzX7mE{DXl`-G#oydP_?{X$6|S#Gr(pnabD_gUL7 +<~b5tY$ElNl->?o;yoBATfYGOj+VN_x{c;UxThXCDg0oood)yjfby8 +4V*?g;DRe^l4k1c-HedVMsn@Y|iXbR_r*9SBRPtAqArZjCaj^loU%Zy@+$9BtE>D&m +L9ofZ7U;M5?T{J(v4QvLfl$)ha?_7rH6%B`%9K^kgqSyavc8?B=;Pf#h((`;pQo*rJl{T>f$!72J3o3 +katOUE!8ggZ#Hdu_NB46xmZ#mwvV%+^$LKq@S;$3o`aD7>f^dJTu{D=KrJsWho7Zz`6`iglvO_Af~U9 +wi%X>3|1YR$q`+$Xu-opA6nY{lR0ekH;PlsJ=ZYz)9i)I)ehrhdnf6`>Dts(uJ;5R2*AqJE~E!%C{(9 +U!yP{0Pd_=^bQrho041#{;_8c&Cy1+?hCn%o;lEWLR0wd6S;FTPY-Rgm5JC>ZDlA9ht{rh9tz+JR(s% +|K~;m_!)jD&U@;RoL(hkNhPq(It%sLpSY6b9Zvp^Eia<+77wE_)Pr!j}QdG+Rs>0Wu4=iTa=4_(i8qt +`mpJwXS?WrqzIj@Q_XlN8;ps8{@AGwJ26*KB13pmWpJXj%|7&7fTEC6Lut{%$Vr7y7HuRvV$K6C!)aT +?lUUli(-qRSkPi@R&Ny@>lj^Fr^mzwu +X#^B7OU%i*c8$+BAgg%R)y7FY48eEWlUWBu>~KE>{_3*^eE3V$87HgNVa$Ts(HfspC^Ut@Kk@ZG%Gex +;CPunRUnq&iNz0Kd9G9puXJ*=tUv%&yAe+Z0!L~6UbLCGjVuU5HVYofuH{z(o(IDOC_0He1=IA5mrpn +@w~y{UI6FG|bLSJYre8C7KC=jzAj-ZeeJnsYvDApF4%o%|Yd$A9HnB4F_qC86-)?5#Zf@*Fg@e|ty{7Ak1zlfCeF +luBxtXb;(!RKXJYt75yJp(UeOy2-)n?6zohSb3nm=ZtLDMKNI5cglG5fkNM#G)6_jMto1T!gDX#lFf9 +?W&qdaFm(V<4Ou@XkP%NaSgCF5>svbDA9Y^$?71ok4jN~}E> +{SLcvVwD+b^Qp;2>FpbKLm%5rp&h=PmF11NZQ{TbK&?2T@gU;hx)eYoK~=r%WN|`?6|Y+Z;q~4A9})< +r?8YkASTnteJ$6FaY5wceg8DE)xz$Lmw*wAWlRTE&|ExqeoNI+DrdHqnwi`a>n*G_3Y;u%-1L~MA7ZK01$drR((E7%PEDXl`(g9k|D_t%lo|q(5x8T9MtH8U +mc|V%?YPDj3O;~NOIL-8de7E6H&C-wm;dlGt%IK3RRBOpcb~e+`A-jNoE^;6&t#E=;z3^46%w#hjA`5 +L7OHe56y}BkM>BpG8^ra3Vi7u?9qQ+wv;Z{{)XGtROY$f2})3{gL=!6Dv +H|g{lwhkrsh2ZzOPdSF#JG_QFnl?0)B~lFSV`k5G4+TrM6{`>VTJ7q%8yY(k!$n!@67wJjtVc1l75t+ +tPqQB5onxd&JbXS(HP1Fk`R+2up3>mDbt9f{`$bs_K!gvxo|U|A>>q`lxaX0*Q2>df05bKVd{pyp4wE3KZnrH>%&n& +drCR7z1?zN%ymny(H>YWD-b{}y{HN$m&yHJAD7@z>7 +kKO!S$r7;RVET+A@|FgeCT)==l{k>xa_HswyDwhH8apSfRhtcrM41P1{= +>GG_ocTS8p7BQ;ptVf+5TZKak{7`SWWH-GT%NL1IB=`RC_)2;(fcW +vh-+RDf992vde|Qi*&V7qe$K-X>LI{YG&wRPlQaWZz?YWxXY4pWI!P41!1IiGArPGiaqJoytJhjOg$L +`2Zte*c0r{jEXeA?RZ#L@aKyvkFwNJSmVz5*1%M-^|Fkg=eo6`ke7w`Cr56-6laDg~CN1a@XjmW0vpe +a_`Qisro$$mtTPVD5TMemL_(DQ87n!W}|KUJ@i^%?;*x@6;y7L#90EbVjR4*KCcc3zNm45*E1!1?Q8g +?g;G?_{QP>K`E@8&EAM3)PPr?v#xGY7H~P?hj>wZ&>}23)l^G7zXZ_&QxZY?NAu`Vo21=O$qCL&%)^@ +^H@pn|!8egH4L2E4@dj2*QxK(rb_Z9s$p+4Y^uPRu=@8g~=+tecu6HQxnkC7999GLbbx@Wl>_ad4x7I +j+Fy9gXE!t6HUBz{pR5^isO8eTM&lCB+sOc0PS*#aB!#m=mDt45`A-hez;Nk9j!Q2<`>lo!vVdpnURz +Sk{2m&tnXAlJl0bOysY}Ptl@+`s$boIgQK+X+hO7D)!nUz!*h$uh_7Pl?}7m6%}~q6LZ!qPg)++w_;U +aR61JuOg85<`P67_z`&d26Gah1SgrE#sz7mZ~d4`nLYR!Q!d-N9N+Fs8vXd9n>UK3ASxXv|bp`|KK{= +Pchv2x)1yb|VHz*Z~|pWax^uD}3Wa_3s*$uq#e_?V^Pfv5qK*tY`S{G8dI-f%F`OxM`uVqvpkqbjF@` +haI;y^F2ibOEX@n5?<1sJG1mYm4_{O4td@!tFnYr<}-xJWp4VEDQ)FzYUzR;Nja?1<4w&!!1B5A9g$5 +xq9 +WA5y#mH9#&zRaHcfzGId~Y!#_OjmFQr7(tBw4VXr*^yjD)!z99$gNlr+BSl$p;2+Ex%MI6l9gEI9BLib{@yVxycrQbF_gP|fiaTe?;a!+&lBX(K>tVN%99lk&26pXo +lf}wh&1n#x^4!TB*ddmxk{5FCMehhLpGKY2&ryzUBm?zZI9m+9%hkyjX}aF)rDh2W=~5r?@DBq1qTj` +$yuG!DUhF-7^`c7U$ltbLY)vD$aMN&NE=nx=p+N=7L6R$)kfzsK=Hvr70Gk?4E`7!Z04YHHIPM@RrTJaZm#DH_@~g6N%N)IvhTCBe6u*6g9pU_VP{U`|G +s9QcAP4#%N5D`{itVmDP+N45s8~S>_B5CA-$eU8&v@eZ|i8mE=)-IRM}b%+U{UzEDA=MKkxK_^-<5k;RF{)myaCwv|yW +vOhmEs9StAAaI=ByR&?h7!XK!zNH#uTeR1D4Lr0Z0PAucvbmYoleyTB69WEy)TFyzu1SW7I-g-5fCi* +sd#V_VBPbgVOG8lsOqtk!xjxGdxM#p0FBQTRvV@dznL{=`NsGr&UK>wR9oXaM1frVQA>;|6&`WW7~j7zFG9;R#WrmKK{)W^D5Tc1mFbAGF!t +p07b%VbN;cI1Kn)2z7%eC@RI4wnUi0=h^_xUv?{P!(~=Fnj=n1zyYu`$gN4*7Pe +=6JGJ(jO85nukQHH=fGX-+q+;7IE_Q8tA>3>$7{C*DF+%#0=9Kr^@H9nFH2i9Zq +?~4Pr%dmDE>l1na97yQV<$7m5Im5IBeVm08OT0bx?-iMRx`on?%Zz|nW{P*By636h=2rM94ZLt0Jz}@ +959L~p;9MIy5SAJ%7poZ#ihaeQIU=YG`YPJ);1pr5%n@GtlC-RyyjM#ZKiK#T$ieK0bznY;3_`EVdxib1XT}kJBV=y@V>OYO`YvnphFRZ)Cf&ilLiYB00DVf~d-RY$$uplf^ryqUpG* +He)-6d0XFFgn#qk3lR#)yl}@*PHNB2-)ZXz;4kgI2vtD?oR-I(nHwy=RQ!FhKKN6ElzMqc2liwZdFKU +ZyPxt-4$sJ&tpE57sc0pEn>JiF}9K`-mHbQvYil+Rjj{^tXN)PueizKwiw4IrsERoVh~97Wm-iA45w6 +s9Y{0S2Pts7&`2rq5|mO%P8T{yu6{>=kc^!=iZD@rmafVF4sPX``a?-(46X2{5%ZCvkLTaXnZ>J|1iy +dmL;jzKN(=I!(ZxNK&nj8=13N++=M?MxL}A8De*#Xd#Lz1Sgq1*&7pNT(5UWMMhPs!9?X(VT?yEWMKf +gD^w9r1(iWQqVW~BehYFpi(SyvTvo0*4bUt|!e@unMwa;~ +A|Onv8GQ5SJVf=@=juhkc0ui(--16ZXTYBhOXrkVl`kE%dw;3ad2*ZKb6EmZ$F^maDg(D2Ds#SmRt&H +Sl_%~^6=(a6931#Y++U}BszHSMOZdRl6vH?z*O-3C03Pb7VH)cBE_aX=g%!Eb{}%NEd@G-$?;E|1K1Y +cGfplLl(%Dic7yrf4aoGd9A6YQx&kBm_<}?6las5XXBcY*b0DNSzGOznW{ZGgCpqHy5cqX+mV@|&g4i +wVrcG&yBGWQJ}!qdg+wSBfdrW~6_>#fWl7R&32a0gOg*Rmwh-Xp&eh`r$3L}otq+AZOOuWaz-Zl2Pn9zj>+q&l%E8Hyq+9Jm6af# +tFdbFMaP&fbxBn%zaPe;66ZddQHH(6-l-XPlLMvIO>l<72mMUM*&CwhSRgA>4kLqqRqm0^#o4E5RM;o +9X@d!r!m4+PG&PoREEqkJ|tTk61CaA`xJ)#I|l;l||Hi*oRAdT8-*_PE0Eb-#(~3uo!0oQ;>)5O9Ma4 +C#%MeGZig4sT-)C*Hj1agAW8q&}4^vza3ZyS3Mj4858_@O3|>$8wq9Zwl}Sj}r&Ngz?tS2|X?r1clV| +P$qIM9SBF&;z0$0i#+2{ZEC%m1aLZ8GT_gXWqpS)x6~)=nQ|b2+TS=`1t-5piOU>80b5d!#%=6=mj(A +L`M*4*XNaPb!B}V<4trdhD`|@Sa1nq--`f>+^v-@g6bI+b7&-s?r~9{DUIT8cL~&|6_x +glqrAF()Nsf*(c==o@2Sts99w9081%TrZ+>s}Rxsl=V2;jltvI~zN^h{_W2FwX(Dvv*va2ZPfn~s(pE +1F-ud(T&W}Nr9gfDoMaTQ|o*s9y=aT(u-&gZU^mwq!KP;3g9rKaEvgl}Na;MmzuC4KrwWOB`c4g=}{P +xq;pw*a(g|9V`#2Wp;}0dG2H=i=K+yJ(^1$_Q=18yv +5`xyQ#@z?=ovSX#9$R1up0_r}G3`VBd{?%%HZkx%99Enxlv+>Xr~zr)_+_i7O8RD6R*H^tf +2=b(4(><$`3jNwW(H`OX~(L#{2A#?AFMlfy^rFHr$0ALG(g@~7Tqfd!euoYfSX&x4BQ!d2wsfB|Z0l9 +(5Pe>5AknSOq;Ndbz$6N0kwPDOMcM8yabVB#HgzM$_Vu`Q%~|z0>FKLF4iVD}3y2Y)^?qTEg;J4wxcVR`7N{GM5y +aNVmn?3j#{?GrxzO}ca8Mf6^9SBgewl!GJUD!Y&p7e2_!9GI?3kKX5uqg7N^o& +F*~pdZHc$e1<71(R=I7uE@MCZs4NYvIZU+SfsG6%Nb^5uk`VAwdM?6*Qkx +xs>(ZWfPX`(uh~88M{@7s1PI&FbjT|D3u-W!b^-{4ruXvHR)RPBsnx%vtk$7W0Z4f{)n@q_ky!-Gos=$(x)6BQE +?fa_#iC*&3g0}K@UXbN>(`F!52NQUz-f3955GjkBimxh=@U|WlP#SICQS3LNya+k|vjFnJWZgh(BI3; +Q!buwPwKob!Pf!aayY$ +r=v|ACKz)WPeC4EWjr-v`E1V~In|1tr=W>&kHkZBOjDKEMHJtH<;j4(DJl&+SPjuP?z79#2QTGy5K8{ +-&v=j>Z12W*gmBofqbjdpE0dH%%de)sM|r1;y-k+rfTm95s~}Zr{j8u)k4v-`v;;js?O&$emfNWC4w} +H;8%l^Q-vz9;M&X;s&^M^jw;7L6<;moePs=5pfP=XEVE*ePf@Yj +m>Mn&yutaInK6a*s&)V$)QpXJK^3slY$z6}7pp%Es&g}yUtO!b3NX;X2RmdG3s!m~E20PXidgNsEc)R +Bau8U6T8}HRoOZ<~M?vih*AuYeK($yysdv8%`6z$aws@Th0I*o*n%VXe{ndf#{z@>+AWg6E67^%#+h(PrV=}u48TIZuH4m2BHk +E^t-g4G!*2<*oUSXD1H!h+%r%|U8&4Yd3qs?NHI*54bgzT=xAxjKhx&*NT2p8AxNPe}A2 +JdK>?u>7)`i-_^_ew}91vG384<&e|JQ#Y4(ZoU{|&^sRTKO4`ZPS{U&gfkVA}pR9fTwGsN!_DGa!(ZF +OlK9d^BCEay3CoYu6BIMg9HQ?j6(JDtc^s<_cKDu4xz!C)I$_&=ynYyYx(#i_uV9gm-C{L{fvO=zX(G +-*nNZ^S|_jHir@fdPE`lt6KhzGxKi@wx&~$QBdvKpkdkDI +Jh-`5!2F(u$>^RuS4ANNJabSTp8`$@GBgkrUQ@g3KEr+J%*IRu+#B`?)>N0=dWn{YC?8@9CMKDRHT(~2W~alo%F?}?mKJJib>H63g;6+nf +h4B5)EvV~8sFXy`^ysMh7XcV3YEPuM?bh>KMY1+t}7kO=;X_F^V!-FKi=viEpCbi4z91JmmT)u!#{MA=&jTz@TLiOIRe4-lFFRp^TN65ojw +J(a@w&FpncF_G0HHn%;E}XaXV*jrB*z9z=fZ?r@Td3mA0FTScR +l4*2?1cKNElKH;%2R0mLOZ7*(i3ki46yr)#%r>(Y9_NReS*8wr6SM_7Us`pP)i#Yd9-TqQ$ +MXaW&Uaq7AVJS+eJKy%PGb*$jt#sG*4r+}%ZVF+3Q&6H+$xfXvZ7ElUk$7Ti0H2jb-phpjYj%2#<5Zt +B5-V-4pgOnM#vk2=2M@^Z=N?!)JAX#0wlpvmhk=$bR@=eNq2zv0>iy%}mEIXOoC<^Dn?6>^B+Bc{Nyp +79m*xSMgxu$RFw}DXw;kQY${>`cgb?K?4=f^t_p(!KJPD`OMd0Z~8@^NC&Q7Iqu~Ct$@{Z7U9I<`ulV*N>J%F@WCT9-#ZDWe@f)PiM(#$SK0w?&2 +}d)1;!sa=zce5mHLG3vo_5+y!!KX1{P8FJA~e-M!%mi=K0V-q>!J3Lx@e(A;rynO=>XJ^y0*F7YveXo +*>IEX^Vor~r2mhFjQoZBTl;R_RX4KspRvto0BU_okw4Nisrmdzjm+rimZ`&<2=FruJrn{a~W`1FL$W0>v5nT@BWRCrvS<$>Svs@z5~~H1!2@9wM|C|0;sBcN^0f!&TfNHPoaKRYn +iRb00N2aF0-$!>$tY%j$^vz5+6JM8*MMpj_d1kTU~k^X!<*#eeeIH?QNDD$(23Nb-e{v*;RH^x+Q*P= +InxGNMcD8Sq!GKs&)_wk#Q7^;E5oWQf~V}X3S>H#_pmQV?E1WO}*H_0e1i%5!A`NJ!95=&-vv6L^1*& +4u}8Uacsj0ZC~0tJsui=zE217WbrxEqYeTX0fZy=ru($i%h%#kZ(D1KHU|(ywJ+1dV;KAo>IEvpF=(2 +ReH^ff%tx})+vCzCc+i`pq0{am+u5$)Lp{DbK?YhSI;@o)-?#dsFWn;r=h~&-Nru;~sU6u?d0>rEgqE +Bvt`n}=`b9(dOZlV@2*MH!m3Nd4UHtqxiGC3oUv&C(oY1e}k&t7CUu-s^kHn@xR2w0DnVL-(JvP2V +a7qP0S@OR1tl+)jW^)rwo|!j4Y0NS?b5nTSNb*WOmPhc^>t}L>#M1bgyk14gvp3pYdi8x8UuaafT}be +!rDr;Eogw_Fj4Zs2GCpmkpev$F-8)1?uadus_p9ySUN~?PEm1hicBiwXydB7x +CC;Fxg^g8;UquEv64RE8@GQzt2JlD|Wc%16q*5Q*lh!+RnEiESZuTwd)i12R`7Yu__WhY7crFHz0_@4 +CStqol~%mXQW^yU{>e{oK%;YdLh-jgXDbA4(A_mGMNQUyCwF^hi8aKjbA-asw4xhmUlKmztclWH7|^$4O)hZd*E`B +;tDdH+@}PIKsyg_*57(@xohDb86A+f>#WLLi2unhvV$JPzkd!C}z6(GlS*spyTG8$7s%Jf|2rb!Kn%} +AKJ$?Ft9Bk@Qg63HMh?C)7X6gDhHwR@0jZn;U*7F0N!%g(Qy^E(%es>q|im}wj)kmBT%lLM3M{^e8)V +suKq4KXXn;f9@4}!3yC&%0eRNFo{J5o^b*d+x)#C2g9>t|=n^`G;2F}j}2%^jVC+SLAtb7V6AY?9Af$ +IM(9V(sj_xOv6R7$Orb)YJ7fAGVz$!|I<$iq`w6Fy+4`a^`%;vd1JE*M>-vqq?PjOZO;IYD*?UH+R#d~pKubNKR^Q_%oJe=W8D5$06o7C9`AwBAf2y92)lo0WTTpjd(N +ul`l*(_ZMLC&uFld{unacMGfnlGtAc*@&eqMdVpscc_VuDUh=Yei@0$J8WdfouT|+VwoPA +08XjL4u`uuhz%#Z(oNv-mekf2AWN +uG0J4-~Vg4zwy7-#q(_f**@ufYoWIBL^#`q;9Y)oaUKT9-_c@=ML5Sy?RUr&FAvgz&Ais=K~B?S=0$- +9k`sQ`2&b5-Sw1SB;iTrF%_50#lHKUnSf+Tev(VoBlQY6(ey|1)LU6jxR;*8i9`wy559-&RP4bo>U}p +=2YrvK39aLf>IM-A^`bIcaT%C=pyID|yLlbLkr^s+R0&B;3s{w=~n^Bv;?ieqpO6ekFES$O$ipaGDw$ +&}OHnNY~p+>9y9L$q7!+;~~zP#ihBCGdsdh3vr2+&zncC0;L!2G3EIejtArPGEf`^(Hy>aaH8Fs%{1j10+@-uL5>`efdvMDzaxS9X6y{!DDE$vL1Mt>>iG_X( +`!9z0Ij8Hd!f|y+gt|l-)nGJEKwACwN&0;i!^v^;)Jpywky%S5h+3-uK5m{k7Z3(@&T>l1YE8z%-y57 +g7E1*We+Tjhfl|@nJQ+aiP(6MTfz;FhvBT8}osjQ_qESJhj8 +)2HE0XWR(4iTSEpdEJDDpv{M%S*Tp5RRx3NE(Chm^(0NUFK!F^aA6K4#(}D +&Kb}w~__-_(`jk~|(>?3o#Vzkixx8ApCPF7r$JM6w7$?$PX1gHuL$+)AdVR8O<%{sPt$bmP1~@NQ1>| +r7d)9y;sx6FT@@^e>JAD>=PT{Rt5SCir+L-R-AaibUIH>B2^6+0Oy`(?1Sx#e|0=I?ws%Cqe4$gou)c +9^uaQAO7;t>3`T;1>(Q}h$0E2O&(1lDYXK?sy<(*z*^B<&G8V;7L2S2=B%8;u!3Fvw-&JT}Ieaarmk0 +z?+Z`NqSE_=GXe3||FtlK;d?B36g=a6B}x-C~?C@zjU(8)#{HfuB+BQ)`nfLkO3_`M=+?o*g=_9A!32 +%hfSO5S9+jR};GllB<^>5kCYR@Fx~iFd$U3d??IugaE$Od8=nQ_V*>S{0bKToeH3BM8-H@7KMJ2m#7J +H(DJ}ADpns?kLBx?D)w6fVMve3Rm=TBj58*J^zJtFFtR{_h`uOE%ct6DWBkoKT(kq=r-2w}O{$l!HJ! +~6+KtZ`r%bwt|8ff#K+RWN`xrZcZd7{M-V|zYRmU12V={1UHpe)BY-TeXiHP9i5>I`>`enD73+}R8|5 +x>b#qnCtgfX4m_3ZiPaw!3v-hD4|8r@$Hg9^OY!?*2;r+zr?wA +Sya{CWPwr05XDN=`g}f8I7Xi4hwEi*1wa4-2{!D0_x6uCNaCW7;ox5+eP`htz?>%<&#ndb#*`tB>Id+M2EY^n2=5;b!nq9+y(8OBe&NPqyNL+ +Bef!05LLE;TaL6w0EAQ}n}B03+Qv(^`Q+Wb6r*~tLHm=;4n*htSN<+Zk6~5skl=n)AeeObEjh%`YjHXk7c +PEK?&OQAs%5(JAZ&q@g)MI_}ExSq3?} +o@L9r*m0^Qy^|MMl^5akgCZTqQZLDAg1SEgX1h4?q`WR;_0@=pQKW3)YB$`n#`b|qas!FK!2e+242kWPq`ml@(2Ik833RW-?W3&g&i2D&MS=(ep~Io^6G(4i3 +Sm=dq=ohX?F&*~jJE0ii^(2di4a;oqDS&7*u-rZA;_y7@js6@Bbi(W3D`6wWyabKa8~72nps7Qhn6q# +fwudvRCB=rd4M1~wLYjwXraf^ew*6ZDuPo|g9}nxB&E}w>El<)6lknDZ746k0`<}skHH)u;W6gIi7aG +VNxUa@Fnj$6Yl@M%!z*`y1_5XV2SUviN^P>ek9Epzpj+KpEOYSvR;r9<2oLF9!5442O$W|VI4+JK=5EaHNrz}`1Ww*!}>t!Aqd_# +8^?6u)eG&5F((OxH7;4}OLxDq-`dMU2KC*LWEjh$to+pz0Bb}yA?xD{l^Ziqk3Oq?d6Hp7X?ZUzo4=p +nv<6yt>$3YfxJCE(b9kpNl6}Pb*rEPg0iq--nP7p9pL|L2M&;) +@Vs_`T2XwOH^5ET~QH=BNRpFj`uCw^FHPeE-v<9>iTAL!@6yr1)yw~p81CN}tq$_iR&TE8UR7#}e%La +&ns+Ez`{Yea<}{+NOc;Y8@g1xYnVdYCF(Gfoh&W-&?UT!kKjQ)@oSGv>#vlH}4rsI4XgnhMJaMz&9K0 +C5&1V_@2HKG;dq@!Pwhvx)$C)U8?m6Q*m>Jv%sSE47v`)sTU9i;VOiw26OBE5)GkAO_@GS{?ILx+)3= +?)c73T9L_qRil{t)FrkMwE2~k^3QP~B_NicdPfwY|rnjJooSZG~QHZ6k +sEFd`vO!=|4QcOumGyg5IrsQP;am^|#R!<^?mTykN)ASz@K)id<-yA_0(zby4&$am#2U|azd;;gq2Z( +tR3fQ#GaYG=m!$-59oip=YP=`tbYeKS;)l*fF6>8zsj3rUYQX7WbVqGLEeR3dxiq>NkDN0PPP8;Yd0% +$x&5|kuSyqu7)8$f_+SLJmc+Ax~-g3&%CrlcodAJd#9fdkkytdS28uq6|NKUrVgFN#g>%e+`%`0Yv1fy5(!9kYYWTVvtW;36@}QdUot;nslAl!PLg3sR4KEi%2>J{lw0z?r +5qjjSzYlX`iOm$F-SEs8cim__t)T@5|jsp^}-m$y(~*A)?whZp7r9R +Q^6jpV>*4@xErQa0P#AI(1S!HR|SYl58p5=AOd^YBoFzab&o_Mv~U^uzFWmezTenzKDoG(N|i&C0(s} +)UubPEKDkg6edvEIToy}!mQSJpc5rbVcNoBKf$iz6(|{D2)oXrm`Hc)@6u +e)M4kUs&0lY4+yNUbfHVt121P-zLdGqTvuSC4bWsQUVA^rltt*LSpo_%E&!zfV+op&*$f?TaVns;D<8lbP0sd +wA*3~llDU!_Wpj*0BeNdSeK(>O{c}hr2t~hKKLkBNcRk**MKm|pWlkmxgfPi1h<%2)y9G^o64GF!i5+ +3k#saRP!nQvOO+HKdI=g9WbYun$;@G(@YCrIEIZ}dnc_&1>Dw;7{1XqU>WEKr +ZEK~&lvFZctBQBI`C7-yp8T|X46O;3^{=?l&k!NgOju#(TmT@k~@iN}m>V81vn@3q0N(v=Axhl-Ujz^c*#E*eM +Ur`pLyz{j#iKoR65R@}Up~XTAP}F!6iXMR=Iu-=rza0F@PD2L&w*uCK&BvpqwHh)uc +<8n6}U@madb;SBapH0D?p5_(=5^s&ye3QF)gx($t-1oz}U=E1M%v)Ioyq>9f5OzurxUq0nmbt!YzZx6 +57+(K1e&YV9B}7!bV@9*f3K7-`$G=E|5xs9_puRTMD+8!K^pU#+G~!01ZDY`qa_oW>BwtjpF9uMzpqy +;P92oIFZ@CmI*bw)j8t!5g1iia#RVD)7duD5SEiOnmatWIm20A$ig$Ng<6w%Ausz9x{1*aTmMwgRTw1WVA+0v6#h52d=d1_*_y&{~L>!cn@wkp?W+Kow +YO2D0ket~5DF#_c^FZOgpNWj|B^&qIb@6EztK}_!7$VPcV`jLfN?+d;48O1+=6J4`&lcwO)mV)c +BK+eju~Q1qn>d&{b>u0uX%8%Qr$SNP05SqiHSg)Yre1L=cetO`|c`o0F9!D0`JYK^|8qIf5W8H5{g-J +z1zMo#kXvTWEYQ$_AGy`8kKoQI!RO1nDk{$7WS4=pDNt-|zMBk^01j8^HgnXG{oBd73^w>E&AO_k~EL +EK|PHlL}=#xtc5&v%ASCItCB~GE_3po>kEQ#jCA5So0cx3;H<0xmsRw#GU#;t_@O+@yGfirK7a7 +PtsOXiS%V-7y{zyG7OBnZ=0QGwIdj;Fu>&%gioqHYg3g-OTH4h>8})$oav#3cS>;@qtO-uc>|g8ss(O +m6;kdEEfQEN_i2os(+j+Kx>k6O&jfFG}N2BRH8g%lx@g#y+RbLc*3>z85^4AjDH>!v0N +N`_JFmFOBo@@KM)r%Bs3MmmycF&dSB|A+nBK>x)fYi_59$x*4-fYMAwg7&qKWF(iRp4JDvR=q6G1tJ@ +8q#;S>?kDI8JLh70*53Kevf`^g+vjNVBb*!}C~zDH1w;Un+Pfa(f_6M?zv%5MxfLGjm);~2G3D;`Srt +MM9k_tRli%o)d`DohnUgdD2|C7Vf6xYVQf$ATxJK`gn&iti)q_`+Q++ISAb`vT)Cse&L-LYY_Nu2!bv +Is`n$rZVWyfpw7U>#_&Y)%wDcp=Yq#(I3R@w?aycz?L;4K@a;}j&>Mt^B#(kT+~yVfP9`_{QI +87G-Yc`qa+AyJ7wj@eanlCM3Ol0SN)p2JDCI*(tB@@Wm$+`16MQdX(?iIP8eJFoIP +zdRtcyd%^J-vpO9w>OQ)7Ga98vec%5S>Or8iRHfYtIKElC{ZRa5Lm0xzY6Z51nxV%AjsV2m~y@Yb7Ry +r;5xrL4P=?R@+AcIUx)kBpNnWZ8QaQn2&W?HJETmRO(!F50@+7`z|P~feitqqm>v!Ke~10Yf!~eT!qg +$X%5|~YR;#Tx6d(Wo9{~xCxL~GF@8Pc;^#33q4+28GZ;sjFSc+Sf?!ToQ@$=>NFYIsIxz{r#1MoE=G# +{(0Ir3UXRNmY}Vv=Bo>sW`>EwgH?|CoJm2$XgY7o^(AbSf99)`gMo9@t1DmCG$S5pZ$0a5~v!=fb5A& +U0GA46xE2tiwmFF_2d(Pa+6GMbJN7MCalxNU>$j7#qW*jTrWK(9NyN!jxj7Lf8fx!v%%;E>$a-kX0a^ +Z+S&SEMqiHD-%BiSBGOk)b=PwxT(q&)J(ul6@ghS9n!hP$(0NB1PB5@?den;&jlo?GqoBUzbXz7b{P1 +G2Rn95^;&J#{qhGhBnF*H0s||mDzUPigZ7P(q%c=TT%;yTy+9L)$ujVzMf-&HvvVum748Zh;Ot}#l@7 +^cAlc~(2QPz0dvsDncXzYVY-%qA2)IwPLwcC}UXRdZr4QYYj~28cV6pl4Nw!~Arf##9?d#`jsX6|9i4 +y0qn1>)pPw4W`u+AUrou{m`jbVT5+Nnbd8TC~7IRij$leFGG!rY#!TB&>uq3<4APGB7p$t+T}$;|aPr +0NE+I&1wcWpylBdE(}Sr94T8GR@6_Uj)Ef+9{|1vadmd(D>;wru`__+BQ+SHq;CVqExZwsE={!eIY^| +p!r%!9Rnw`)iOHujuIMz3HXjfF~r7K9GVFMO-yp#oVYoU}Z&FT +^kRCsc{rB(yWEAKclf){(F&%BdfBND(tmUk)-GLb-%Bg0N&ROleZ2idIJNwdS|&N}Rf&np9k6yJoID&dRW9 +|RaZMl$6|8kki-as6AHS%ydLR&nR2j}9e__vORL{k-t@OCH>$8Wd7INGn1x+*>dwWqsxN6qM8|i7rS4 ++By69BC@aUBxTKrF2jL}@r{8Ofxa87EVx?*ceGt@i4Wa%Q=enW`|kw1pOV59^3dgQd~L+Y@34IPIChu ++#Ut0ZOm8{HZn9UZZwlhcq=HYzH}lxq~KXTdYZ0lkDY6Rgi`iMw*BAWtb#2vmN}De5CDQK_Co;p`RU6 +)BFWf>cwSKGjQqy$PWj<8}EG(+H-hyNLB-4@NJ4^90=Y@#NQ!x&0>A|map4IzCkWYs+Hc_T$*x_35L?O-lK=f$PvUn|3o`;E}?MOD*gMv=lc8>kkE +J+-A*DefqxhHqS;?!nUU?XOPP_`J82f??vSWwnVOW)=CcU=C$~LkJnk$42lVpa7bmSFU+)FtS;b=0hT%sZ{!iKashmlZlfYt?WMSSFdIDw*Kz|nl|o1)+jTy=49gEB +a}xnD=k7DeHNW)`nF=xXP)SeD$cc79HgsM@UIRoy1z|RMy+KEh#cZzx +hV2yC->j@fv!Hv=a+bCJFn6eQp*&*r9Vm|d#2q4TD-yG^dakxjY`^1NjdSPW_HCg^jQMvo?@O)-L|eYxxf8b@Z`0%d~lRoo==c1dzBdGG +l^;FN!Ct>X3#fe#q64RuTdh7}+5W&uA-;&uJc&d2u8VhF(}}ye^$i7Lz~d7h7n)2h!~Piz+%DlIO(p# +3ykwP&*NkG)J#0MO9?3!U8~JWgtn;Y_E+4a%Um1W*;yMqpiPT({m!Qb|1-`@GRFhSPCHsonjq6eP_Y= +4k>Ns5Px8^F{B81xAxCtbH^vK%?})KNodo#TgJEX&20MlZlXbWx`{@ykmnWups~xJrtqTBg1a_kt?{_ +8|K=YQ_+jAfQ!7bthVr;h^^wH5ED6HAkhS0Bls5-c1#tsOZX8g`n^$8mAG1TVcMv_T4t7KEP@7mJ!in +{atYmU)N2&pBDBZG_T@0``(Jyz$5|mIH&fpT!pcqM?ho~`pNrm%Ay5FkOD$)_WKHXGM5W<1Q4%(#EF&mlvL6HTzX^pfE28DMpp($VVNUbxz*SqVaD&f-Jf?DG`7A#gmO0|+89FH>#O +s+Kd16Y&fuKJ|o#w_-;-q~nQ(!{xg%EwaO*0OG4R$#_iC#)ZoAP}+CPIx5QnHQO)BG(T1Qy{(0b@A +|E{A~KCBlrhFMbe_xN-uW>&l%hy{SNqjQW7o@)LySE#}Zw7U5Wwk^JOL-33;ORE-j(Fs5b8j1XhCap- +{D{LOLEOC$ZCKI+KZy05kF7VY$&W@?t*9n{1VRK_0PFvF;tx_bekHnxG*FRA{~jJ6S$M_6{=J4AgGs` +NJ9e5_RVA0|GDvQwxPP#!E`b_*y-^nyrGsC$T&`y@3vWT3BD8!9dW61Uyr1re&EG!aHcp$i|{Ea_PmT +DVQ4PCIhIn#SH~ux_Ziv`KH=Hwb2YCZcHf7zAPS<3AfPy14Of~(}Ml&j!!eMUNMS>pxw+R?GKf=o!cC +nSl)JCdyBqmjIA>W>7{+qK}U7DayB?8iF6c5EEfW&^8sHzp&O +D?HqV;rH7?mF`u0WMpnOT>yi|vL8LnHAfaQ+t7A^yQQj*;`VRqF5mOCP5aZL<@BCkj|Uz)C;>6y$zP> +#^D$QU-ki6%Z=2Ef__1W78!)&@|QKgoO17Esb6JG;Jp0&5VV%R0OOirXW}4gDQ=C`+#7b-G>ySYfnbD +j)R&^a7(7v$!h>)Zjqm?;eQ~3&^^(tO~P@LHzSB{%nlF7 +V&ez=PAlmQLNyYO+@sdu<^Mm0dB;omzLbcU-rqX#12x8 +J4FbN&&bm{DFmEAol@+PV5OjFR3lsiNDq92hBR7R+UE%PVt^gG7vv6-F_#3W%2JCt-jlO+b$A+Xv=`t +QGI{V)-ZOI50ut*K8{s!j*2kxnO-y9XEFV}Xk?J0#LEB^g+-k^PLU%Y) +qSeI+R=;9n6)IwNv8L9VCx_CQ*u5%zdsl5A5Q*CjF?$W+=~J@rF-s>Nay#@!%1cHRSOUG%v_z=yOB4W +-(>!zX4e+<@HQ-F*H^3qo@t8WiL8L1Mq8C{)<*&VDCl2Xb}7>C^dS2KE|dUoy9P+VlUxVAG_)jcZ?_U +-FH3kd_X1dM&;n0@+ablyZ9JO!$aH{q`{>feXcjvK>4Yj9h=b4=4W!bEk08gwt%iVKl9Ect)cbxYz8X +uCpgyhmXP`?B#2T=zjJDE)fH11u1Szk%@4%^c#*k3}k4*@y8K&OM(nq)(hrj2*{PmSS4&TmM7YwpV^_ +sA|G`ZRk&Mr-|wut`#oHFwH`4yhKHhVmrQ?!EpGU*_hS73>5@(D9E=^U$=y37-x+4o)R8vJw}-c#=p^bvfpZSaz5`q3v?AA1ZD;ceANk-D +<^eFTj??l;IO^FHO$a{iT+5AIm()8+0rk&b%!!X6IRD8K^6s(J)m>8V*ks$<yDT#}f3Fv5Hj%m{;<-$p)u^C*5Cg0g2}dC#O=Tsi0i1qGlp-XDQV1rU1VNC_wb +lnypa0=kcnxn|hj)KD +Cp$mVM@eE1VE`lO>$BBWnURo{0{3+X;cC_;)y=4=(%dB}ws2~&`Z_b&PHiL|3N7$iL)w~2{wxn9?r?C +Tf_9fgHh<}TSUzF)SPLf-hjlLkZ8eqbAYjc#u9UD}#{k-uo81dnO6dnP7IaBdGwP<9&kE2z1FDrV?~; +H#ahG&6pI`QV8P~v?;ECn;rQ5^+0!_`&5p3y@@1(^`)g +{5qLai6Y>IrMa>-`0&CFrqBikaCueM#xnA+Tnc1qX_U$}DmBOdal?>U+SMdsb +I|grK2QdO-1)8vYV*yBWJZZU=TFW@kn@)v^;;g+CmLQazGlNN^qY1`4)bGG5xN~-VrhEJ^a+X1Acl4g +=?=d~=?0P*a;v1wtpZM_Altk6c`4JcGH(=H?{-`YT#)Bge<}SO2!x^XQC1ZcbG(`k`Om;WPraQ~@=8^ +QOpQp2SH0x&?IS4{qxWud)eWXuB#0*9R86)TG!-jYVLF`nIxX8?M9i>Yjsy-mlQMKp)}}{W+qw +%x?K&di5=n(Y4N09t!2f4wZ+@Y;%lOyW~%kxe-)PaXd*$(O;KcefwheZ8WWS`FG(^1HHdmty9|2Hw)EBO6T_YMMhex5I<_rI{ed`YLs!*;HZ98jLzW=I2iD;}XsL +K+B)(&J-Z7XMHL!jO@yH@19I(_Gr7)|d~V=^pHoq~`XxQtnF40BaOD#Z17@W8pvqij~TKP(7lf+jrGm +trKH19~C|Y^<;iB{Y{sYHIwZws64gZu_hQqBTd4+(tdX5uH~Jv!(wymTPTTUek&f*{Lg2*1sM=P@9SRzlRi&&?zR4Oc|L&(J>bG&Ru?H@ybwU8p%6e>8=)oSzB>qHm~ +I)jz*^zu;$P!=DYqt*FMu%AQ2BHRE|7nJP!OzB7#D_v{Q2L*tBWoPUjB77YL{d#_xp_>?iyH=0cmbFN +4)rP@P=fpOA41!bT^IAv4c>%CF8wt>{okQ(ggx{Aa_aC5@lcIYcaM51`)Cpz=69mrNHc@w +%q%MCKA$fHZNE3c5)&m{8xA#HjkK20|k9fs8_$o{iyq$ +W__5eG0w$@7#&&JRtJ$E$vI_t +V(E4FI8uHN+@Nnd*tJl+|D&xRVaV9I;NH0%dr18{OfO1PMQYYO{(oDwA;(Y=GnFgb79(l}r5dI(yQ +93{Sqy@N!<*&BQAM_0v9nsb+_xI=KV)JpfGs|}v%2fr?!P4W-~0mK@2TO2o|q&-NpOEMK2?fToS9Y8( +4b(r`7uDHzC3-gb6NxhPMzPtKua(5TcmN9`aG`be{_JKc*0k24=KP9M0MgcBX7}~5~(*5X?z-Swyda~ +OmL305k3Cko7&J+NAy(Y;@`~r?NI!jonjVzL@m@q1QvRi=M|5SMvR<((*_N)NOqM%=AS|(ckd!OS<%q6vpgQ!ZSz@Y7@|5Ms? +_U~D?`9-YiNT9jlvblb&^WJTX=k+Y{z1a#THh$ +FLlvdvSh_k?p!hp+-9aLkKR^E&O{3}M75p*_0;stFkj7=1z7}hVC66qGC6D-*8;2&zSV0xR09u{|9@H +0C_`t^6xIAB+UkS$)Q_Z?ztpFIh=<_59nt4Tw-YE~4df82i05EF%Rz5VYNl49-$Q_0L2m;oOw89%%MZ +04Kj#L8zDd)|*6p(Dfs;QD`Y3qjZAe{O0q2a%ZSlT1_A#ol+Jr0)jM8%hU_|0TXG7bTRcAFz|F~KHxm +rX$$Lf%wDbVKZi;~dflDs>N4$_a#~Oua&mrfpF&iD8$tF($m|)&}!ffYsr=G316m*Ttdy`~ +S{V1O?F@3BcC_M84P0Mx>Fsi$}}Z&BVsJK=l<%?W2uUF_E_3^k>)%4hWdRfwd+PS-!5IhAaf)J2-WkB +yr4*N)OwcLhX&q89>ih!?l4-f|#o`d(g&v0g=ECOeU;@9$VbD8UE)kCP@M_nMPML@`)LEtEyC&bTG@; +Z~is{pyhp{!#UkN$b56V9Gk2x2E^_8@_fu&Q!TV(_L>~jQI$sv!qS;Z+PY91m4&JFThdTkWTTrJ?bMhm=_Y)~1!T=@kfSbB6>iV +3>SWkc}x*4s7xY+*h@o()iV5Ke{lFriHU0t0sTEF&5S-*SD(3^?$?$1kxK7M!TePvG3v8q%;oU)D_h@ ++@)~8$FJ#j|K=|0>fMyaBVmR`%7#{7U%4~SG6lY_t+Aj|f>homyozAHzmO@qEYX0;6uOqo&b6lEjx(D ++_RYJK|DDBl93s2mxB0)@iJDrT2QEZ^`t^fbJ041r+FrAIb(}Pg^btILUy+U5njW)V+{U~6MB +;xOtBr(p943ShWE8x4_%VV+{++T$ih6V@75)m%&1)ZS}*{t4F(UWXAGQ**8zxleQZla&?UVLq+zp{xzCE +h$;FY>GTI1^o@IWxR@%TN5I~3RO0TZ03vNVNSpnmD<)G*P~TVEOi>+8K34!oziTysf8vyLv8>u6~%Iy|(s|KQzkAO~N(8+#C%SmTdAm1|Bb +3<95Rq?}2#E#O572=%6@n>p?(Qf4EtR>L1m_TD8a%ommBkPcRrV-n%uwyt@6_@31alQ;V_{XZvZhV+v3`KP6z`6`)P3~$|wnd?UFr`zUcGQ4ty +%4QQKivj+T9-N0OJj;|_`ln54A?tx0Qpq;FXi+qHTJsk&v!;G0+$d#I!xv0go@KmjQ_&5Z;DCF(ko9% +)^!j-||~t|h|B8WO!+&?~7(vay(<6aPG>PNGajhy_?jE5ut#W$?ZD%8zNx(T&{wLf4dEUMTt-kBxxAkx^NBzZmVQe{tocy4f$q2$sxxEW#TdgV=Pl-@#7`cLrU!|>;7C +>v%Nsn|bkTG(a*j3wtbwVYykb{pZ$brKC1o@~ROIgWBg}J;eyl=i=N^|M_@4`V#>lg!x*C1DUGC3&-d +`1c=z%JtR`Nm{ODaH09 +#i_4C{WN?6zzm1!m9!Xsm#fvPRI>7+D)$Nh8MX$Sut@>JlX2^j+1(B>$$@)a!m#Mhd`GL4f{w7A0@?f +vNDKmQr{9JPN>se2n7UhOkC1Jp1=3ou&jF>6N%#+*FBnYQ-5cWv6@+HlCckvRdQCkR_xvDRa@CKb77R +dF%8e_q~)xoRp2pZ3Z6f4@$`6^#g#%&>VK-V@9lB-PC#wlWg+z14QyzywM9tl<^JM>PdJH?=WYt|#NO +0?EXWrh+C3lvCU?#)(^;HheU9?lABZaWVk9`WFd&W9(d9*I}Xr2Z1!`5CUEzABN71;5K2Zbu1(p;a># +L(|G5iOXE~JQTU9%S7O+G_@Giq;Q$L3wCQGBh*G3iCd!XWba+f8dw|5ihNv|JIPx)lnw+?xlwP0F!xC +A66JYRJ`^SF-~hsqFdKTX>4o?8JturA_Ipb6WPz)2^J%01dH`ZID=PCVA!MU9o?`i5uGKh167te?h8yBYC%}4Y5ICS=lR;Xq7V++L +jroPjLdC3xsAusGQL|P2ut9ynB3H-2gzxI>Z8d|tQ4sDEXLGxrDSCOU_A>qO}z!y5E +C*l{_^F@G>G<>_31xxgWn@L%;gbo5X+~%o*fPZ&_TZ8(SvKr>7A0(xOKynn@ +A{RBLGbVpL~F$)6?^+?-Si8jW%Lh^F58VWH7-?1Eh2kXM%a*#HP4lV&1491r5>c{2Hh^~KxAXO*Xzff +nJdTr)iq%-lhKeXLFFA!zs^NbIu0qqheha@8Z9%shocC{|lTP%}syJ?&h$nazWn=2>!O +y^m#OM$7q7aGsTRQ6TdndsQ<|rEgDtof%SR!Rb>Ri5Q(!+Y?q{y87}+>S*Sf+lF20T?agfJotXf&zxm +N;HZn&PP<}Wd9Y{UTYLCP*^D>pYV`aWI0cl8hy1)(VO*5kkE*IHCN(vcJx4VD|2YgI?bQgm^WPE&dr} +;;7nF^Q64r*CD*)S&wOsq{NnnMJwk(#}aPCG{Sq%pdjb;+`&^qd%G5j&NWfm22eNn)nOgWkEp{z+dDg +r)b*5iozJptZ7P)D6eZbm}s8DiR$yEWCj) +ylb9TLin%G4-#AhObdRfF)Q&k1G9T~QjzvKdtbjZ0%v$XtyQzw&CR?|xCut#vSgV)Tkxg2#7EUh?z)@ +cK*55F*vg_s1*rNI{d#WA{xF8ZEozvV?#NG~+D1?ZPM3%tR?QzZm@b>B~kK5QfB;4Qt$IE7cSYS-A)-0L?*ks0e1X?GMZks3fVezg}lUBA +lb3gyZKEra3v@8C)FGAkeaQf`6W(~{mLt{^0349VnD1YzmH#_7$WxDETQBy+h-E@z|9iCIhu#L1$;&7 +tG8FL81k`KSyH^~l5?iCv<*B=X+;yX1uSyhmaeYhV~{!WhAp#lXw9iuIxBN6_3@xer1h{$U+3TNuRfK +{k-8I1&g$rL|5Q8B=K1Lqs{^GUgq+)hT(U1bk#7DUx{Xd~^)@@w%`zK){heG(%GAd$q +2`^hL4le-b`hnFU|^Tlk0A7tUw2id7{t;%hYtq-tFIq-`XznUT0N)itvy2CagPKlFyW2b1|s6182^5O +6QQF1E4P%R$VO$y0W#wt6=r96W4!-CMn1ow@}OJWrhH(z4uPJ-H0B$>)pOl6jCqy}Gf=|=AMWx;yjz7 +mfovxW6u1yD0*TmhHtQLnw)ls(7|g0Q4ccu+{F60MZadZOV?2R5lymZd&s{?jHsLGamsex5J?{H6II; +vY+V*&|iTa;x;2hzju*C +wD6mIh!2zIw0v2GYFw$*2~YOEghYd{c|l-1(Z%BVHp)R?k0xK#ZB;*Yv^_LdL7*u{KjSUvowp-TT@^X +Zq)*%RiOXgf`iuw|qVju2o4H834eMNE@@NB_5+XSwK+jOF{KLp_qP*iyZd3JhDS+iR&l%t*Gfl(|p(; +DPO!BwdM)_|h)Z(t|LmKqB{6$}lrPkJKx(&f@3{DFdt#M!yt!S>OtZ@a76h!WGC-HFj1SKugmPsE~bD +lR4cy7+|eF(urXWwI)q>&$mKfCvQ?>%wGd*YY^wZ8p5V$ +Yq-PKY>3T*zLUD7=J{IpDZ;;SF7-+7GRm|8Q{i|BK|j;;W~Ugl7C-1GA)&6jV9@Gq>XYteUSxXc$P$7 +K9I&Fd;au&L3?@15^k^QdXT8@U2upDKVjV6hYe4Rj|5{*Ex=(tSTm24X8LbKka<(8Wp~!*tq3V?SU_l +ND1Frrt`Lx3GT5WiEJq&oH$fz~I9JOwaEyt|4+mz&HYb&NYcq^2t?-Vlnq~~&X1B6)^nxlloGI|+K*U +3SjdT$F0!jLe7lXXC!^;XZWgT1;!I34!hC-uyDI33Lvb6jFAwEUEavIxn_NrMPln&Y;lEu33f>ElECW +Izzr{I`9Q$}DF=9#4SA8Kr;W(rRW$w@Mmdjbh44UF0NFFmS?4*e5Z}vXuWkN=OH%Z8d^752t;S#!Ocu +ZN}Mh3R-xLnz58leW#!)y!sHKxdU|Eg=*q!s3Ndtc#As1Y`$in8&NLR_l-d3K51qq3*%D59UGxGut_ +NMuk%UwJO8OD%s#1QW;?yb(7ql*+mji7SH+V)>0}`KaD_TT3qk1^)_&|Iy?UP=9T*y)QC(mZ_{cEl2Q8z`4c2D%_N89GSGRF2k_p-#Hsq0;FL9Bd(p%;z>jq7M +As*U&E0)isp0-arG`F9Ykojp+$U|#^{f8zGX2dYB}5Rvotx7lYmVE>G%@k3RRxi_)5J!zR?R6uTAER3 +oXSmJz<#Mgc&b=y^tDw7%AO3b*m(g(^uStUmcYXna-2Mk(sJcM03Fi<|MR$cC=}ShiN!%J%{c!R5tUu +qDEuxK+&804l?`*6P*U+>mpaLCb< +(te-p5VSScpUsGzAJ;DulNJ?8;{WKZ__AkfoNW?bomy<+_?+#$EZ;4%vhHDaIiFwwm#O{PA0lMdbt2< +Ve82A))+s@#=06^6o4r#`7+{(?YGQuSJ3t@`e!0B&ss-4qyTvzAB#^Ernonkrq(^*RL5JCC-o^y|${F +QE2c;K!}foPYEORINmJH?wJ^SFW5u7|Q)>R@)FrB{Nxm0V`(@mnT0>&X?z3&V~C2*8US6rK{K6SB@}H +FNoGBUCd+-sf<@!v$6n~w_{h9b8g1>T=M3;*rW2+0c$sMsW#v`xuR4X7Wmdg(wD@PuuU+!)z-6Z8CT> +(%by`-My?MN0W`N7(z@JBFMkdo^bht)>T>(rXL3AL0L~48`_QQkoRluK<5(S}17TvN;e*+l+ILr*kLf +@C)h0k~VvyV=T3%D3837>b<&65Ia)Hz$-}ILTrGy6-Db)B4{V-F0lpQeJgIV1(bW%ZTBFZQ5zWFr=ln +o!$6&qC!vjD^}OIJ^SLD`NY|IQz1^n&Z&n1un1ikj_1!kD327cR^W;EmHs-^JFX8HAX^k1!yBf*c8ap +C^5c9ycxv6v8RvJ(R?xFC+6CC@jO0HkQ+8bMR_xNu-YXjwqNuNn)bW*jJ6v@a9u+sb;^)3Qu+xf?o2y +NSIAX@|S5*Y|Pd6LP-Pmq8sw%GkcumFOVs-paZgn_EeF562c@As_0n*_B2|(7`&L1eBwa46U9>UbRE`@?gR3y*h{hs|DJBc7D09J +6bggQ8173b&Kf&V0o6riL!){LpD=HvQEwMj3F73OausNGrgNgcENn&$fdS-%v5-NpCLFIC$@Ng?ySqR +{>zDEWFQ^3R87u>LFvLr^e$Q*$kmW@c$}7b{zthv2RInEt@kums1ZUf(W{%7f69ozSB8NkVfgAL0y0C +cs&og)_+e7hnGYo~!fU_#eSj&`t(HpgTS9Zo?FGeOG)Y%hb!%CVaFYkXlZLB&(S{n2brbx}pVAw$=7w +lGBW~+OkAXo(1ODtA7mIYSA(>f?{Kn^KfM>2SV3rngTLoc* +CP)fmmgAzOl8+JaIYSa02rta=o{WGs`xusbwrXE0_1ta>koe}h +DANZSJ-j5cLJ*p?y~bN(qM7u?o{{a5(Lh~J*Z&xtVytC8ehtlZij{0WOH8^9PQR5hRh%l5^ZipCpgEGeYTf!eMZ>?9npc6fY|K^ayAe)U6Qo8d8Et4i(;;Oz8deV+PeSR<10YkTfs-y;IPRi3)1?@G`li7V9xa5D0@ +PtuxOwNWN0$4JHzSi8&bJ~NT5W^r5>dWW`FEyCNrDbmo~tLpGrWb2_mFFFuFPfsQ>LzwfjPcoX66s+* +w44kkpNlkOBjjqfWh}|{nKlVvIgR7~z%5zJrz1RbZpe$d*tMd3|ub&?nbh_20gf;6`JVO4S75m$Eo|x +#^qFSpZ2xTpRyNhRP`uo3|bpC};6H!#}n_qW8S@^(aGk1;zjT=Z3-2C7@iYrcYko%xaxrV!E)(p!${T +Wp9(m+3}HXlf0V=jmEf-D6^VjEy*5)#(LaCE_2Sqr!`&`qs=`gWR_i$F|;>AetVVt&lhZ{Je(w@0o_F +@;};&@yB(wam^YUc$;<08Pt`^f#k@g}J_9Y1~(o`K#tiCJD~CDD`PJkw?@5X#kyO(2_(rle-a=#KWsL +gxU)v)y<-)4$|kO1K{su=<$A9=wyVyx(1455SnWZv0mJBwk`BKmmAk(ihHX@&?J(yUAEN>pX-zE=1#A06*$Aqh7Mp{<1 +Lei6gVSUu5HDrL>UlRKgwmMYi_s0piD-(+q-}l11>9jEdy$w1UPej3^)vnB3E%{;mptpkgI35RBcaNk5^SDw!KNqwcytGcP$lr`lRkzO!YU~YCA%2wi<+|%6li@y>U-Mil0P3=Mu~11i-HoNaQn>8-1$ +K=TLowupp3{j|AP>`4Lh@qua6|A8*klUTMp5jv*t!GVtk~XeWShMj(_ylxVbTR%$#M@jZzipJ6YgO`ecD +L3|D*|XbB1!Hujj#OYK`z)J>1S<8xO5fTpCv@ifqCG-7z?a1?n3gL$}*E5=0HH_(;(rW?>YF@hb!pC7 +G!5*gwxb#~tOD(VJK`?HnD=QNMXyf_jFLoci~Zbk~rEtxgK08gzjAgRyw=(pijRy;_E +&jb*bSX10VPi^15)=~AEiD?*AF7}ueJD0*8fA8PW^I9A(r5Tx}36>A6+om3+|Q{!o4$)h4Ia +2Kzg5LvG<9k2~OO<4t{l!3UYmvnK4TVLL;?imkvnd^SeHD{+8xg+6&aJTbT;{*KlC_Gy~1R(g8_;CTr +aw#TP;l#?=mfbyu!$M|}@S0%mXlDSBmXAEe9Ns=;y}n`%loBL0+AKA(2Ar(XC1kRfRQeSWNfs{N +X$tRU`_!953}T?f58Hha-S+Wc4pBYGZKWDts2j(^fvK{7wcS?np!FV=q{5bYolxIYTv^)kZu?g#L9(- +ob@;T2$JoaZU>W)(lG?yinO(y}^R@dTSQMSWUmyvhq_*=f6EW9n=rA$aZc^l?+G +OFSTL4Xpcg5Bt|e6L8!9LXfbj2@>=IanMI!5qcE?>+m2g$Lqzo +(oXG_-5&WX;QFYKzJUgnkOsTztesowPx_2rPt`FZ!ej(l(1zX;^S;J&l;qO*H6u2`a{^f@_PQF>y#C^xe^&1JVaA +$J0r^`TIX0+$#kV9u;eE=g#k8dfMN^gX+Ll&(R~Jl$Rzp06|!CsdHH`N-Zjn`L=jeUXgw|uNZ +vCqzWOYI7EX1r^I$zsR|l#=W8l=i!hnQ6lX1LopJAc?24kuq;GT?zTFwu?3XrlV*(*KZsJOiWc0p(_j +~geh0m*$PGRUW*fi=N2hw;oUngAz@-_&0v$$b(yA=7;(Lam(jIv2w&|9+T{DwjTR2BGcO^!^jB!+}!( +p`#Q|<)+kdtwvbe2M%1Gq~d5_fP*(~BL}4XS-cy28%6{3g5mM~Tk8j7&|JD!pq_;d(}BQZIgO!68lbt +%lV!A67&)(oGfS)`8ff!>j(ym?2G+(U+SwF1;T*7bSh!#k!eQH||^{Vuw5QL?&el{TikRJKU71Oi8u7(Dr`T3$>=c +-7)Dt*jD5SIAfOv;~e@sKIp`YnWp0PE2Ek$Ag7-{)?x(kC15jSxhCD{ZSxelZW~on8h)XWoERKeyw^M +6*In001T-wIKr9K6q4BE#uRGlt0rVfAyYh1nvPGkm|=|kiEGQKAr5%Rr3vgqUe-L9Wbjk!do+!UBwWf@wwl8K!|9!ENu(X@{2L!deP~ +0*gCC`mY58InUnX=dh^fNPDn-HYt`Iid)0f~2_;n2jm?eype5RS-y#4;!cz9sML`uUGk>i_aftK~qVp +U1570U`vf9YvZ}ukK9)ur?T6I=PxG;~Q*)P}jc>oQB6Hi*eozfcD_7fvbR%X3|;e7WkYZ$}QO;z$3ryebjy3s&s~>rn?_~ +huUapSEmSC>|DQ`xjuoBm({cKH|8*K%Y6cS2s;1KkSo02ewOfa2Kow*gGP9&NRN#E5;IBSqB0m4 +|WM5StmO~7IBo!?l6fdI+LmZ@tX~ya!~)JDF6IpaVzm+4M13GKe>=TCs8DKGMOZp+-X2coS`axSh5FxeLxbNtLt +0cWNYG^0Ng@i|CRN)@IL%vZyj|G8YbH(m*DB3OlYtk1O|i+6!yVbEg@V365>#X2p5-X?9Y}DBXMWy5A +z6B?RVpe<5+^r_{mm7M_{*6Y5gPUroNI7uc!!g(D**?XaVbsIm!?O2OHJ{^QX&aSkD%6tv4Gxh4n?V44m1bN1(Ad~%PLjQ2(j?<=bK*|-)@B84$2E-2!sZ6tZsRI +XhGS8NoYY?IA7I6&qWss*7*04nis;`(TAo3z!IYjeH6;73mMY~(z83tuS$VQ7Ci9YZ@HvCVa%N@`^w4Bv6x&w^WR<_ +3to4tgaiO4Pzz<;4e1jZ^N@wWOky2KZ6EeO)GX&g8Lpao-+5ocX{3VGhlzT%|=VY-A9*V$aqcsK>%r2 +I=Z4J2Dqb#Ii}-gs+rM`tuEIIL7?>>23%~?4L{yFHLZ3?*7GHrn9awMSy21CERhq<`_wF?jYh>x+4bx +YfEArlhEx(*(0X`p{ai6Y|J9k=V)v3$=!$m1j{ySjsarpjl!~_!b9zg*@@O$1Qm&Ti?=Hh4TkOlWv8l +@qze=StQpphD%0cckrqw^tkuBTi^W(b&R#T7n{z`KnGAdNZ8EWu$<}YFX*4^pO*2y7#9mb?EX?>bv(r +KU=Do8sEHu7CL2m@*A5iKOUiJ^Sq#^oYX=Rl3Mm-IGsS;|+c^Xj1fjxg@aWH +Y3Mj}~gb9IJCCBol+LV4#^~WI(bR5FYOJIb2r89cV#>!QKPX%3NgyETg9CJAthMk0LkL=YoU@lg|nIX +)Uz+X-Oq>T~w-A+M>@4V81;-XFcqQPnU~`N~{sIFLfh#&KwJ*^n@1_%4BZ7?HI~-X+3POC71f$R{_Grf?^whtj}WMJ^%1E?wzwtM>r +}VJP3UI0{JMl9L0GDak`Ja4Iq>y{0yV4_3c*ygf;5!{WT~*frDkS1y~`%>O91d&0+O}F)5$ +Wry-kkgvmy|NQYbP7V#M?4J?nVsgQ%e%jGb&2f_9{4iN}e{G6KMJj<62stuit2Pw*d-;LH&GXL~#f$y +;uZyVU3hP}7`}m;gsR+iJ!v7hta6!s{UfUIVfDVoCcFA7q}APQyU+(8-4=$z5!`4TbdE;G3{(<5%K&e +S90l(~fWJF+%*K@Q@-+5pF9@MZj0l7(%3T8Q%ICsUeu3EnU#W`iJsoR|_*fDxx3&{4zZp(i}lp+BBtz +1Jbu#sfVpB*9XW!<3IqV8ARi$y!ps5uSPex6u&*|);E1yN%su$__W+ikhLQat#ZC%AWP0BuE48<#Mt}mdnLwDB~7F%lIR)%bof<%M&#H95f +nt{xhCAQn^givZ$hjHf0Sgf59MjH3r|l1}6Q=uz-1z!9k+(5T=o8ygpL1+{zqM{-HPC0&7NIEoEXS?H +JZ8prNLzN=lX}f8EI!8uJEND?GwO2^n;TTL@j_t9oIU+pPAIBOiE05!D2VWG#mj-&dwpk;A2c1A9eKK +NXWUMelfe!AxI_yM*9-PDrt@mbP{2>Mgug1!X`26>}(%D~LFZLkQ!Wn_hAftITtC+!f&Ph7h&@O8wR( +BtiXS$sM00T!AQ(Oud@8RDz&Few1Cp^!mWfaApn>9&SksPQ2xybA3&H8g&i|LUw^?o+N%lS0`4(Z +ls8eN~)T#WaB)P;FC8k7)MKGnT+J(rFh!O~}08lB`8_WyLZdP;6X6|AZV|S)kW3#CjoA3zt_y9m+$jMXHyM&W~OogXJYrd-;jhWX^>2p##h2IXG$?Tw+)wt%BWJgC?Yf==wm@Mk0Yx=t3TWsq=kot!|X2B&b1KHBy!)g +vQ;5)t6mj5gc?lO*R`P@t~8prPTJ(5# +P@!6aR6a#l**AP+lA_f%oIr!S6b?y@@-F_o;0F`(`gs7xAFijF{sfP5cCA`M%320tf;NJu$(wHW +nr)>h!s)PJe0YjqGZhz2`Y9-0HHq +V-d&KIBy?bL}lkkHP&F*H?b((|cth67mty_Y6H)F(=t<$uNmKO<^vcC0jFMBZp2!Ac~T^@^`cRn3UN( +<-!n6NyydL>iV5r8FmKZvZF1C*GxamIIl@v(-|*m`4JpNO)@CAt_c=j3}9T3cLZG)H`1Awm5u9B9^l> +T8MiAhXFOfz)=4tYe;IAuLfNWNz4MrL#q2{(K!I-ch6XK9(v6&7)In@85#l0i?*dhQ`JWNxmN-!5UDxPmpx@j@|6WxNY(ljRz?D*s8$ +pHL_kSQ*!J{&acrYt<%op>Vg^h(u_HUIA?aJvVs)2f+c`hfu>vNE0m@g-F3QaHoeoLmGOw}{wlwN&k> +Ht?hNLeWek-bnQf?&fg$B83YG&@f;C_3%@h+!x|p1UEbVju)GD`&|pRmc1WS`!IrV}hc&;FLcrfGXRP+WnHmF_(~|+s5%d@XkF9O`JMZ +_Q3O7g#KPYvY1&`fk0gc9V2|DlHc&1$+7me8dF1(!CdkNVkS7MJ`p;fs|VCo5o8rbU|khVQjh*__|BB +?*cyc~f!9ormQ{6-3tXjFSp!}}bBYNiK(zi3!RmZkW07Ppvm}-qR$A>8v|5^aHY@p=hiH=(oFOnpURz +A<3`Nvz1yFbMH6$_1MBd>3ufH-m!QUHTK$({UduS9adQDxHyLUem3?#94KmYu5`<;-GC3q|gP7W`EBc +6aE$ykC_R-4R-3cNqg{d{QL63W!}|NT>^GZUF{J?dI6gdB*KdUc%tno&1pWE1_5kvDLFBLTm2q +d4s3`(-~6f5L$uPj4Gso=oiBwG#rW48j@ZmtU%kM#E-QH>RdmEX9sx==5W+mb*5dCu!tXOW3~Z!M%)j +MY|0;tBy71TZ><7GK|2-f&{VUzy|w171g{B>ky}wZO+7qM2d(sA~*4QF+p5@q@LCUO63J2sHkxQ;qRlwf^k%;A#XLT+rUF +;sguDhPQhQuuKY-zGJ0vP>8D};VXN|to__y4>&*EN?@q=813Hh=%CdN7(He=ko0mr1_Q)N +L`QgkhCr7!y$d(v^+f=20*WmoFooO<`OQ%Q&l#b436@{6Nbo|jbNF|wjsyFN>7pPH$v){pmJnxLG>SL +?`lpWm?Xc=nqVs+->(Marzr0j{)2ZN$zp1;z0D8m)SduO?IA3;yYnYG%x8!^1UIMBo)CBqabQ()my6p +i4FlSId98o>9y6YlFsG1UO-@{@nxBE=%DW*cVv=m_2mvzxmyX;rRVtg9DN{xDP2|=ytazQ}t!S?y>0q +Fwx%mQs&W!}-^m$!nk0RKwEugZk?=^%ksaWm%6iMikOirWU>z!PC;KX`z%A(&H;0ddlU&C=ox)*gon$ +=sO^>YYb!-Vo378zB`L+GDTN%4Y@mKWG7!^51!xr5ax~tCg0wQ}zBrY;tn2JCs|lE*hI`F1b*B909FNp +A^yg0>l3pg2qh&LL3XRJ94W|u?+IuvT%uJi?Hlu)04ac7tp5ru5Y@LGkPmRe*yDIZSImHN^=s52R%F? +&Zuzn;Xc-`$8lNVNG8N^qQw$GqVqD><&9Iu=M`3|N88(H;HhyCMgMB$m0nTFe5ogH}-YhW +`vnDRUvdWHqHFQDBYi)`KLO%oK`kXIpJ8O;xdNpw4OYki;_Ie4=GUGV+zLN~n4*8U&^Z&0Snke*BaMK +S44X|29Vy^v6v&EWk54wT4DJQ`U7Z4QaJT5O{Kd?)pN4nhVG}Lt{@t_i%ei>X|E5k?T?m{}4O}QRmU1 +{w%xarW!TC;7FvvaAZ{dInS_EhXt=;LXnuasw^&4r!?U0H2i@}!kXk_7Az=ELxJ;4oc2arlh9;&E^Xk +%Or3*szTH)&cLnD1OCfuwSi>y+YQ~Uso%z03LL!?~-pf)|L4(;1p&Q*Z9Gj#Uat|h+LVgex0aMftlNy +d~7NuIh4m}6e{cnTxHPXJ!Gcy?~@S+Tdq^J364Tm9#X_lF|69srvqtby+dZEq_AgA+M+@bkF;59>+0e +@WESzLnGMvs7xvk2{H*+v3c)9uZWDP{wcnVfTqb;CS3HocB*-E?jWv1sTGDjSYXoO8vOm6od+@Y-Q8f +GFw>|DxRO1mt}8-0J?jolqgE%iYQrl46paW`Qa6{H>9^Lz2@>GLf(K0Y$2T3TWe0=v{*elK3Iz&9tt# +$Pt((zW~yZAtppc?Btlvyas1t;;z6f>$c{ibkeT+;1R< +<1*6rEcEXh4Ygr{YBuWeJt|d_3&(~7scj(oZ7m+PhY`VzCDGpwwt{>lx&H}NeGsNdZQpBf%TN3B7zu5 +XY+crlL!_)*Yk6ps^2wP}=-WeZZ?9b_WJszTbsR3Y4o{dRO+(LPzy+ujG%+#&nkJp5B)iet8m@L*23# +$Y>k;tFObsI|^4#)D$<^)&7|nueT~NNi*5Xg1oH$+B>}C>q1G>gJfMkrX(#7b!mW>BM!K+pVS>h}9yNVxNS?v5eJbdqgmlT{P+e=;d{ln`AX8!-(GskJ# +J#m)=&_axQ{LGK~0U8N?q;H{{n(K*ZRHxx +WHb$)|&pwXiA%Gb8evcgLO=P#n9%efR^Y)MH3FW=8FHs)DTrYl~Yr)q1Xft|P7jxpa;zE|04)t16Q-* +durWL6N#H1xH_gnfHB>8q5-iJ(80kJL9j``lD}UE$P)&O=4tnRT{4NN{~y-eeM+L}c}fuXWB;g6B@kN +HG(+*%Ko-dtz5aJ|xY|@6xu-{tQIjYy<++)O(;r>KPULgaM5f+V}O2R@i&Uh^I=lIM971Mm!@LuC+_B +8bDYVQh&0kA&F@&$j(%bjKa8hMp%y!7VBPSl6O +JaLkwaV{_7ZZsTvtSwBqtlmAfm!or;VD}ricd1lskIisBQWCFVBnNh9rNv$!cMuZ~>fz6s;nj%2kTi_i9SQH5vNFp&rb>8Nh3aCebof)9rl2YXD|Qc&*VWf+ +7cWQmnw+7rJ)m=;!Z>E*_D}1(L>U9|~rW)gG0eH;XI79*SmT7FmS0@Ur8A9FfK)SnY4Emq!7u`|bGs5 +h+~4YG0ISuxjY01NE<)P;xTyfJ#9Fdf&3beM{|sBdJ{i=GnFxDyW(xaE^Yedo)B@&Oc%Lc)CyB8PU1$IY~Q?)A#t&T& +&m#cMFz$_2XMFAt~93PC{m_LO>jtN^8Eqg>nuynV{(jpM#Ly|@a43-tpCyq2 +g1lbg7L(vm4)nEmafrTQA#`YXAqG>+i4!d&@HDu2xr6>(&m`i2n6Sx~oDs;6$D(|YpqY9=ERuq=}_3N +CAvKmg&j8la`$8czwQX>_#^cMy-IfgwRDQ`f#6y)_xXQkL{$3U6+wf29Ie_sDlgZ%xKmjE03mOfF;(+-t6f%yk@*4Zr}}7J+w3_@Xd`(OY}UD4_f +XoiYlg8S@&(-{{sKks3uB=2?Wp(Lm?>(S|C)G}>>~0TF?BKIB~V&S<{mdY*2|T`DRIw!o)n2#$>Ir1` +jCqw`n-10wHn=eHQy$(zDP?JGE9`i@9=h7OI-S$Lo9#^{l97~(b{}>z`vvl$Iz +l!Ew6mm}}iCOI1*^Nk#k{)1k!&*fW29jE_<0i1iyBv+OklXa$S0@k(;qAUgq)pj~EvJ|d1#YH}&b{eT +b1Ns3ORA7Y&}plO&G|gR;7b_0^W;eE<>=jar#jG8*pqG|7%Rz@O5^7FsD!e{_S-PLlU^?l`U1xJm`Mc2a3a=OM^$ba{& +g1yp)_HlDb^*s-Bgw6rGm}m?q}eH9Y$Is1jHjEUv<ff{g7s=kVUP45vlWaM +M1J6UE!Il_M%1bVjegD(?<4dW6JR#Uy*ZpUxzK5j2lY*}uh9*t!AXkV`ZbvU4|=GyKe8qY9-3Jx7(M${ciAI#CblamvneNHeUHOpzm3mXsWK*vQmA|XqXW +9nLMt*X?ur(QceU&m-=EfS>A<(_g`{*OS@ZFx$r%^JXX;pB69V9E?z+oNxfL&BG}l*r6 +hIvytukda0(~rccBf9LQK9l9D*923zZoSze_4MBCX53%H;h0r#i3pfXNl)*2zVF)qWI|<{y3r+E!?i7 +~9HMaE?guLTMeK?-+0vUw1Rt?N+3NnPs`4S&qPK7I=A1W*icyQl1l_djZ>R+4&;xGCjOL1=l7#)PthE +ni>sE1+HElWo^Mdnjeu2CRqNkJS`tZRcwUCb$K3ErkOSYq>rP|T#0YY;r=z^}2{$xlBnX9tAY#yfDQl-BxOZE?m>>u!le|XJ5(9db=Ea%tdfg4~z_r +jkzNFXyUz;~YMm@5KzX;venvz;Sp%rsaFldnKS|FlE0m>^2^@P$l?kP90JGp)w0X=h}Ljm!IjisC#^T +xccx^>Vq6NFj4p<~h}7M`&HAMZvxqN;;X}gWb-on7UEbexi5Lgtm@_TvpQQ=~YU=6tyR0QqF`ya`yLsxxdf%#kvMEdw%<8e^wy{O|af(5F(=Tb|Pr +uja%2O^&Uw^BM0ZhN|+S=3R)g}qoA4oeq~fw0RD{7`Cae7wS@AB6f`r)8ZI!)gN9B$f=G7{!I)3At5e(a +v{LOioerhGTJH4ad%{Nq3`46cwlyLhzc!PW=UKTzK%%?ErMPipTAc?#`aQ_a=!Lk=_P8;igs*L94&(MAyeJE9M7DV +H%@|j3Dy?%>@O19gu%tt>De+kRlQvSTS}Y%^vHBilABrSM1{@zDvypxX7gO-tr00~=Gl`t(ENem+ifJ +M335slQk5Ex^nt)THp(F0FLlAmN!*$CNkkJY9f`VxYvT!iy777l;|(d_y#C?6CG%CCqPkiY#SZOv!M4z6TOuqz>O8j`9eJfC6`M@ +US1&MbAh3#n@sa`}v;v8IMB!Lz~QBhuW!j-O}68haW-%Kf(Qu$J$y5Iv&btORDL^JY!88eTvQH|)hZf +({0QdC1MoJPJ)M&2KzUB)Ltx;cK~E)eqTW>4N&6yRwkR%`6y +sAu!AY-aqjfy^Sb}`j9{JBI7E%vHJ&>HlW-2RAh2HR(l%i5h-wj#WY=BomPCe$q<+(O2;maIuJ;jle$ +%!Q&pw88|v=&%1J*F9w=fq1kjm7#CER2R!Q@ID*Qq)Jd{lzB=Lc~=8#zlg5?b`44sD#+PXQ?_$-&19sU6R3nF>XbP~+0C>NmfdG8 +&)jcA}yv;o+gG}5q>cEZKq|Lq?9J?VvaF?ry{gWT{zvT~+?5w+ZRr0)5YHx=qCQWrY*SZ5I;dC$De5B +FuE!kh?f&{Ovmoy0ytRq=y$jY{yh0>1SWcM45Mq0?4PEMOV&}^I-;{>dLlkgQJSscYc(B5rgV>}nhhRa80b9E55|HOdGMIQgrk9J>Tz3;&S-YNw6( +@09IG`pN{kFO6KfTyvENu=l8T;Jtp}MjnbzQ4(8pIAyk@~yrS~I}5=C;Tq+IRunpe>P1`eS(76C(QqL +Alz<-Q7qT)PRFq61riIbk-F$Jq +@Lt@(@1bMx7282hD@VllOXSzfoB~~6Pswd5OOO8N=6Izysq^KNM97~t0t@C5(1VGn4uhVAB1eHgbCYn$?qNVVO5io +bl5fey^wUgt9|XA*SozZXxS01E;lA6QnD&-Ej0pqEIlDkW +0E5+-loQ8CukYYV-g_2Afnq*(0BCaU58tzY;BLy6?lz?G;Zq#3@V6cL@>2)VQvw^yNYs$uEi4-KTyD^ +`hho$#w1Qk@~p17iDLoqS_K$D#H+Q=#>O--O`Sz|Oj4xe>M}`Fxygqd{>ce=Xj6vlb^l8W_@Q)3!~hc{j^m6tU^+wuV`?`BsN-s0SE)ya*E!a%f9iG_c12r=k+u- +@;U{rQA=v17kc)_Buo07<=j+L(9kyKNS6fp2?ffUHE3D0*YpF$HQaO9yYKMSY;feWm7^qS+IJ{Me?WV~3i}CpaxSnHJvcIY-kChpa@@;DM(Me0;wP`r=C8R@Il&Yiw?gA8et=_)q>~c(kBakKizA5+1_;X@_N$M1C@7|crA0kb-tWrUH2 +D}KmF$s>6r)-j!UuJ^WdjJN+6jtg)pw(eXcQi{*Z-Pallrsb-zfAYHVKm(=`Fu7#J-Z2hTe-kikGI@w +Kcd1y`WJJx@gVS8>haU?+SZ*!=-dSE^{c%KsDa;Tp#6*e=Za+wGRP(+^f{OYCLEKca~XvuY_lgDnjpb +zgvT>KoHb6mr!$-1^3cj`HYU-~?}s9L(iL>M2CSzG@n-Ad1age}*WqSmX_GWc7+7jOFp>|!;s6B@K!~ +A07eD}X)AiL&*6dx)$e2_`$wd^@^h+eqZTJex<1IJ5{x!b^zm4mcHUrYToCh1VRTm;J`(%X52MujYC!xX`osXxwn0hQOK +?e@SQ_CI--zcW#|fB)}WQO_%0SYm))b3Kp}_f!Pnb@ylgLaA%z4q&Y#hY|F4QXSC9rJ>&E4KJG7yV#y +m)>xrlgHx}bvh+bWnm@LUJdzc?bl*nOqKm83=`y&8r}DoDm?HV#vm~Adtrd{eL=#@6<-XZ~(+q$qvb8 +dMj_JoFF*;9|He;rQzM`YvWudJ~|EJCc-~mgy*E}*}oR!w%6a+2zlb6GJOlqUF6q_O!^QycP1Wb{Vsq +*|t1kadMM~jJ_3nlc=j3hXUewaFXWeM%f*JDx_q3eIn4_iUN6cx>pDD^Rki!RH@5GU|qGm +kS6+T?z!YXeV*&31K#vCo1C8Xbx~3g&7op!j&Thdj%A!5laOd$Lv%J)OR;e50buUY#PN3nbgc{$4J|X +sFt20z5ZuT<8%D%cDthk|(1(3G!>kFaocnA*ZE*3-vG81$Ln@uy2r=;vGmLz3Tq`{-Tqiw$=p&A0-yS2@E{hv$F)Yhx2IXSN!qH^Gr)2>d({q +{{PU=77rhT7zn%dn`aQqFC&z^6&o%4FpAuu*m-03-6lI_iO>jCgI%n7NoLDnBYMBu?AEQr`%$_TML2z +^C=KinuJA05l9;RWs=lE5ntBt%~u_n_62xN(UuElRA_~{(0ZMu^dXnkmkf>@9q73iG~aAd#WNQm+wg +ke)Cl&jrR-ViIa0izhW-Ifx*xdlsQ^;5H5U9M@2uzU*8SL(ONDZX3O4n%5X>cva)|Q8~KuN8_! +S&vepr^~A{&bO~>1DD!ul_n{7(fOB!1}MIykiU5>sAl0^+3{}qEHBa`4KEQ_-eqci}RqE2Hv +4XybFzpb%T)g0vh-TNn%&pC%_TyOXhobA7N0gjRVgEWm%Qmlb29=JwM$g*R3Q(Neiu)gQpEk_|2LfvY +NWp!4@zrYZ@R=A64X4z`PMV^9NtvO+cOfqjoYgQvsk6wjyC`l(?;UcWaoC}DPbN^vspmQ1+c^m*6Fkd +cLsAW7T4qrC%v)lE-$D!497iD#m6?d6H7--7ZUi0s&b2ui^&v{li@_oXy*b}HHv%ghZ$#v7cGB$okC> +2noVrxPEKu7f8U6!Dwv9{+s5;~gV#{YTWk=Ar<6h6)V(A1QbPl4wiI^(hzime|qK#HHQS6~`P#$U~Kp +#r(MDquuj<99Es$~Y!<&)4!}j!CKmWj9n^#MzWtV3JzzFfxfpN^~jdat^)OamJ*}Ip0g0jK@#}x<+7{ +e50MjIYMQ|*8z_ncy0Btx%f1jUM8Q_F(CmnhlXTX8ilv_2?3_U3&LO-q}*L_0|Mx*kq-*e1Gk!XyEX8Q7$#xlBH(_kS+VnT7TeN0OQwo=YoPzU$_JBQQdUMyD+SS<>ne~U$`nJ5$6t*;V}MB3LAa1Jcy9D +JEnuTKm`HVd>;b?f@{!w}Rv0DWs=)S!f)oY3A8VYaetr2n8+gG-+pyg)AP$D{CK1QX9uPuNoC1(0-$O +7~PcT+{B;(431CEB&oTU#=}ZlJu2pypc`{cTAFE`1g5@-hPG6DJb7u%4asJ-auS^$D5HGVCr!ecNnP1 +_Rzgohn>JfK;WubF=2ju`%oGf3<+XudtPJa-7Tqtv0m2`}$FdM=Sq5N=y3vrNq51DA{XQmv&A+>n@z@ +A-WCtn4QxIp58379zp!zDghfcFx#a2M=#V7A0sC;6c-}3$WU89U>1x%9vbr>qTYMeat3Odg{HmAB2tE +0e)+!+MjpOZN0@VJWkCa}47yKWTH<(%!eyGap=q<3} +j!aO4ZX>z7`%Ne_NDt~a^)K2)lrhYMa)bhlleP9v1z8|b;8iIYVAi|+Ie_sTJSHRfqqoLh)S=u9wlc{Vm5aGo>|+bc)5)N^VTxh>xF1Y8a5-Cn;-{q)hi14PG-GUT~I=ZU6=9w5ex$B!y4B&h3gX!D|xYG{|#uEm-J} +CJv0E2g)z$0kiUedyQi{<2a*UqrmS&7#8GRh-O}3V^0u{>GX}1Oe*KlW?b4_9MT&t;A!d@^0~({YSB_|Klg`<52 +-5R}E>BLQ+Xa04XLrPl_}^-x`D(kmTou9+BX!aoWM!DtDAuS#E(zYU#xS68mh0?AoFcP~(L_ZtmH+e_ +JCopNloOL12(2!mYA!EcHb|;-8ECHY?E6VW5ANBk502<9LqtwFM?g8jOx`N}3ZpZ)GJFP$12QJmU+wi +zo<~qWew?>VO16=QpYOVE}{UGt*n+lX%2J<*I-fL8nLP;r837-{csZO;lPy4G`?njaLgu1Qb{NUT&%L +L%PZ==wP%agOED^1(OOWUIxyzG?3Jt!rqy~)G{}*@(9Of0TTP1@k-idDDTsNE2LUeE*IB5m%hW4?0;m +FtlGj7tbh@Djo(wbWPW?lWe6;EJ{hgN-A;ZAT7v}r%E{oJO$}Gv|B>lq5|a!C|LVpQw*#gE5(3fjE>v +GPU}r=5SMMS$Pb3E@7S5^_P#G=1aXVE`%-!WyO}i%Zbo=fC<1JlLy&8hf^Fvc_zM^@CxzOtSBm)Yozy +Avy{Y|6vyfnl>Y;;~0%G&>nOg?YkB0bR67r7%mP>~}AUMozqxRFbTZ!DRK$@RfS>Hlr34T?Lh0r +kOt`T+;QIm=+5Xj#U0VrA~Nw#07u*k0SSI&kA*C975F_33=;o`elR`VmT0ihxs@R$v4=Vx(%dWVEwsY +1+s|j@L@#_g*S%IpFMunkv&N9#=Y}5&5lhobK12-+srwaZxJ|*QO*5p1jjuR!Xm? +pyiA7XC+brB|B$hBEf4iHfw{lhgBX4g1&T20qK9H4X>>w4&Vii1*HFZp#y-FKys%HJIE4p)dR497#sx +Yr+;{tm@rVgH&D>yH4I4l6BKec4^~>;Y&9?>r8t5#nZJgDj50z +_RNKE%DeY!fZe#>-MiYgc0XTAC6e6Im%eoi5^k0@^WY9l#!8mo}hBgVzd6Y^5osTVntw`C89{L1LfJb5S*OF28%0z4|-=V +hZI;eNwxGj=fCEAF9ZtQTazX(X*^V0q8&=iN1ySG~`98F-#Ym?~7&)E`?;%5% +AG$sYQ27fXq)z4Q7>796&U?ALYSIHFokC#cJfgvrXm!~}^@&So{j-;vR+?pOZAD2$RIKUC_5ovuuin` +k9lXwKV9OB8m8Pms4aHo8i^W +|^s$BSkBlXo!&!c}rFC*&SWE}{qw=$K(l%AR>4v<6Uh90>zW{gw#HdcGE#6%0-Gcnpx!$p9%@?iqjB@ +VgR&1SIT%1YD*M0o2@Uh2cE&Xq8o~eFn9RBrrq%&5(xYtA!AQk%`-ZUzSQs*&IDFMG#ul>c=CSMyH%{ +X=#>Cqks{W?4MD|>Ye`^aI};M9p$Y?3xU`01AhFL-A@O +H=^*vC_9V-R0+L3;;|q*Xvi-2pEe2_tC@xHax0~ubL@J%|2zCk8mj6no2=u6`k7)8$I<<)f%LD`ek18ul!=hol<4 +Ysj0OE1`uLBS{GYG424eV86A6R~qb8ziKyA2qb+7N~sop!OP@Oa)DKc=zGwRCXZ^Op=L!^)A5_o}5f& +70zSq=DvKU0jX|++ZrNj)EIE!q}8)Yc{7iHHRlEZty6@~)5g4>a(7O>Ux9ZnbtH^3AXU!CQfzX$u--} +YUxQv~fivGMB54k2z)B1gMewJCjv+=0o!>+*yBc_KU{7wLZ|`IZ_Q((0%v9Y~rhzHy^ghz)BzYyabZn +r&og9LD?-Go3yO;+xrwp7Dv*g>IsDvs-H5398_9Vqx&@PB;3uYdRxI&m=Bsd9e$zf_$D@50DLX+=2I8tA+; +jKSWmx!DCE^yjP&NYZolX?Yd)&xvZ>9xkhxrclHgrQU#Y+#i;Tb?G#iRWD)xz +O@UR^0JC2ThoTW4i~E3k7F0H&y72W@GkabdjA0^T6AykX9(&lwahdQz8=!Uc=zKw$h{ux-N6sJ$0^P3 +vSqP5SpCrmE2ol!~}xZ3_WnVu12`8vx0yrg1edV)dv1odsQLvPcSFh89b)BinI5cVIH0+GP%S6Y-X|U +ej_r$+4IGfGLkPE2}}~xHZ!Ep^e({k?7=iWn_kArWqL+1P&DmPN(ZF%x!Q^1Gvz%*@S|!2Qu@StjkwF +8=$!-f(mI2V1V2}miAT*f_ Yjv@6=qKk#95&bF|VChgjz4c&ZC&l@hN +EVT2bbLJckfH<;fv9c7i}kptKQLaVxTV39~D~K!uh{^XM^ +XAamT$5-hok#Y#H=UcAqor2~NG)^Ci +;90Td2u#xh1emqnB?*LP5b|`m*x!oufWUyT8$*r@;0g~1Uw1#}1J&lxI$^BMf#9F0kX9(jDx6zu2EYu +ppEnj7E>?#vrD)gSwbCPts!oHF+JlyzL#iN%hTLW79w10tP4(b7y|SqgC3uMuJ1?6GBNWX>)Uro_e$) +Z^C7|+|%YnY>UL3znf*z1E=u$vlJPb=B=)5aB`e|$k6GD5KkSHivzW+jl6@U?)q$h#b9`{Ld8Cc;=V! +&(n*5+-%WOy1VHJ(1RRJEvmlb&Qi^V)I!N${Gy_YO6bH^&@mIJ**z?&)-AQUC>EdMe+SG>ujp0A`5Gn +f30(1pC*SsP@JEYZhktTK-&VD2uECMVl(CMgcVbYbLGGHKZl}RO1o4%2E8u^J9`g2%J}Cxwf7xfNp0X +m5?--FUS=LFaQFtvhU+D3ZY!^3MQANXe_t^>)qZF*c|T}ryO_+yk@wz%$!_w2KuM7#H8iTn!o0JFDI| +52Bt~g3Z(lHun#+JKUY0`-YrVLr5>WdYm{JokfVZ7(9;_x=yTM2Xvhn0fC0@~znJtt-*mo7>Yr0Mb~- +IfmBT~`x9%AEtGiG}eU?uCjH>pXhTQ +zs^J&NKi4g3sPQ?QK>n@I#%;610UXjsM{E$hlhYYZEjD;2*=dV!D8YK4;}tq$^QyX^nf((?Jr9`74d! +m!RvTBWcg)GCUZM1%hYhhd+N`oy3roJ!k3SOm@3`eX$S#OcT2}VGFVMtPq0M(e>PzR>%GdJaGD7VOcGkuy63hASx +Azeua&_HNywwpAKp@>Km|wq9wCW%K61GoC$Fyp&kgQuKD~<*JWdb7i@jV<)oHT?rpfhBgrw@Z3>E=(a +0Rxzx8C6!Q=dvsUP3L>4T4^>8!-BXUyH`O9xb)c$%PH60%+9~4@uJ#&Fm^8;rN;plAZ_hjUVW_Dfn7` +_mJd0-+D$Ll6*()J|W(#&T0?X#oTNDfehtlqXMI}p{(I`_bH=;I$xEI_)~|XAkdy1qMu|xclWR%DRdT +PUFULO!5cB)X#l$ef@C{Tt+b&dP6NH>Qb@`jd24%<+)9DhC^Sj!bX1R0I++k3D?#wiHp4gHC4mb`$fF +V*i9&}48t@vT-|Y6bY#MFLaAA%s}m2?GoW_hbm#`;+nR`@{+(WxXvys4y+=EO_}q+iO3`aZuhPRAB-KS^)_mX>s6#Yu>1wwFIV +;oj1>W7blcD%<_P{aA2 +njN%__V>2tzEC2BlI*I<+V`D-u9axVAVRY{>A1b#OlQO-TLmk*K;^C0dWdXzAu2kiL-3lRNm +i|e4J4A_wZVEJ!CIJrbq)PkoReE^T*=L*VP(f#jBT>y|_FzLiH1r=Ph9s>?WXpor5Gzt`oz$6uH=U4xCgt~nQiCh#PZS=KYG$@nJy9>+qjQmLAhe+v(HUYpE(D{rJw)U@}B7)h|4nf!tPZnnV{785PiQtn- +>hr&nZ#I$`J9C@{*xCyp9Jmgewz(B_t9-10EcaDz60s5jJ=Rb9>#KRe-Gdbw~OyIRcPe7FFjDOQ80$@ +b;;H>}9B_svTY#BSLpCxqKVKDSAr#vRP2Df{39@%%DUWO#Yi8fhN);LEIbi|V(>22_xt0Qg)yq2-qn& +-^fn&)2hT6cmW*-el=MfT-e=sc|n1w6^_fx0FQnBvBLOr*L=MNV5zron5Z$4(&U(>wGlSZIC!FQ!J@n +aznW;n;4DbT#1gGP_<4UMm~{^V5jeo4Dd%1ONkK3aj>F{fp5$OXH08I|^K9b2qD*_YqV_rzjuEl}g~| +rwvI$^FnK!Bs3t*i%nT+fX<3QQkwK)8U>g2PTz|HR_{*OU##>EPLk4`E|TO@Z-;6Pv^nb7sK1yL>kyK +jgAbKE;76x<*J0g{c&bX@sT!FQbRoRsuO5>{Ww6xdEqH%NE({an~|-}4&b%H(~`(p@fMLY`JJ@i@`rXUw +$;3P)f2roR~ak%PC!Ev(kwTkA(M-s?J*BYI`hIY{NcN#Xd&roVsH-S`n1D<*DkhMfoGV}RA&W7YVQI_ +1oPE^`H*BUmswqJGVM$w@a!M7xlg=n-?&vgL{RYD00Sz){=1rKS)Cl0H*iScyuWL?O_II#wk`V(NhU* +wgVG>;uqTB;4QxqzJ!qtt2^Qyz;J5Qb*_0Oux_2=3H#=}GO7OlSH0TgP!ey#40WCf%4DF{I^}~U@Y?A?Gbv3H8iwGt!s4G$2k +gMr!yxcyl-7VEoeU(FEz3%+o&s{LdYP}+D+oy>lUyXrBs!xwUkz!iD +W1~vN{%N1)ryz{Udss{l5tbt1;J@WDwqry7v-*na+;O67GNc9;7L86I=%2^n9wP +ID*#eBZUloek$Lx9a%uQX;axwUUx5l@1_P5aD;Ba9;n;%vrd7hkd!lNmOqM0%a<&84L?wV7VFLe@Y)Q +&t*%u_Qkm22UMH4&IRE=!4ne_?ydBdUl6EG!l>LVChAME2Y$smoj@Pi*`(GVcH`=_+)Jd`hucaOnliO +QI0C9WkHHlv4i6;$BB&W*EECv8xBRvFe>|7;8j}-NL+g%6FQY~Vnx!5bk-&wdno+?)JYS +Y@5Q@MQbq~NvLGw-Wm)yv5>DxmPAc`)lGf0Z|* +M6IeDr3P(>fb_Sl*y~|y4W?SOkR~t(Z_n_HIFH&+&YsceHEn40t-^FZGsEKt;(8Iz)nR0L+Xjw2}w0G +V)7)yQC4khPc}=ceeU3!4b7lXml>>-ywnk3{nH#16dNdNIN|g)rPBs6ENCodDVaQuF&IRD)-;3Mm +O`o|FxL!Z<*>OYT#0qz>vJAs2?l9JGaaJ`ioz>z%I*bEf*#)s3rU^ZGRY)8$jpAQTcUxV>Q|Y?I$}XiDt8vR*Le>5*%}Sl58b_sT=bfc+lfvC8>3$wxj_)!#c|JVsKAo(k10y7_=Vb+_^=gW^W=MT@KZ#h9UG` +9-XG6nlMve~U3BtM!>ywrvo>VPK9DT{|&}|`s0eS84oJHCaTXWC_(0M4WUqvF8xu~Gjf-Y8Qp!Iu98M*sCt*-R%T)pQ%u6ZP^({)UL`zr9G4P~P2D6}Nps%VR0|TkHhQew@ +>@GxZ%_Ei?ZAy+y!Y~~ha_7GW~njK0et0>e=mXS4!rjL>;s7$TWEPbE$5 +tMcYT(wroZatvzj*TlZPugY_MRG%ssAhERXXo&6Zvi6OI-?hCV8A +-fIFL<>Vxk^m5kUd|~v1N%AB+GKY+APE)co{V?q@FMrl8L0H>@{m?-rhpuas&nhfnl+ZRB0;tSTa?;# +F);tE|bW9Oy%t6THPck39_vd7n5zhCOD(3U=ZydM{p0GThrj=`^uJuBr3V#g+0vI(0V8&B$<^380*UY +Lcp9-4_{e!NMaJmVQflRDEMkbG3iY%%bagXGq2#8VkF_@`%c+ObrMxg2Ad%$&TW8Q_(vloGx=J2t&oH +yFRG9fl7u9Q92*b7_vw&cB*+`u@-Pei!;17GNmU3af~^!$1WFMFFSPq`5+kc=1mDhiAq+5}TpcV2C!h +Qmh@pyZ1k?0H4V89s+lcfd->#5~NILTCUYaqenIUKeLHhGu^s`AEUKDa>)M3F0^o4^PQ9X!bXDSiNO0 +MOkqDQOFVSs=Ood2coahf?zW(h6Q3cOao;QFxJTq$AM>syFM^%c`hW_J +v!D9XGLm4i3<~X<+ygKo?YCTrdy)6 +gnGxwrt{$>j6c1TgZfXFghyn7fXCoMq%p`_f>8toq@NI^`G_6|6d?S*V=yFtgx^tm-x{FMSn9D0VbGQ +UhRgbY`qtSz|{{yrNasYTA@Zo#v7e^#N37)b#-9S;~d8uHaNp2=1&wCk>@I)>(`=`3$+g&Uiu!?7Y?3 ++?C(n^urXC3loz+F}%lA5G}lMDu+Z?KT$6Ebn3op$|BBIa(j)yG}W#55?dAdEw7;tr +3Lcm^oP1eq&k_49Yk+uP=?pQKo#ZnT2D;J$#^cOkR*4&*Mm{^R?misNPUtnOx~9oz=W@M-U{hQ;7CRk +xX&PHe>he7TbopU2HKzc-sBgTy8s=r>p=e)AkD{rPf5Lq#2~*qE-VlH(MqzAp#{2X +|+E182PQ$acry`PpKq%zWEoiiC6*Tg$(Y^ZjU}y3NzWRLtbi{!Wk^Tb`R&UXMR4~{&)}-$nh>Qs2e|X +?iL=q9X7;g6<`$*MCBgi!;_Zm;A_FXQXB2i=QI|#~++>KL2vXC2oU#Q}_`yE3-0ch(!NC)yhwr+e0u2 +a9iKfbf+zE&lQGfl$4J|}TRN{{oyeuKd>48dzfhkdo2*G;k!fR@5DGUZm1r85&s==?_A>%$~iP{7Ko@ +FbB-@8ZM>Vfhi0&hR?pKf$?TD`p`g1qe+TV7wS`O&R~}&ie#B?h`A%wo%vsZjf4oSq*we@PmWT%^-YkBAq!y8bc)pnGA`uo^9qIk|ZPz2xfJA(|75C`HC+PUlA`* +f8zA20Q4XlUvf5~My`vWFW_m7)QJ^GU$pue5;SZE{;Ipz5ymwodS6n-O+{Q;O4#}gAoq!IZQa~A5@Jq +gD;6p2M{z}$SmgF+4d*K`v!M0YIIbj$756g1uY4$(e&X +K9z{{C5fEjN-G-$Xm!_#HS20!9@qunNFZT}v=fEc>fi%}PAEd_>5<%IT1?7nCDcqxVCz2iT=v#x61Y= +#ICG$4S$EnGDE`f?9c=ISpP~gr>&RpSlq-ey@*ucwB{wK9BE?DiMa%@%p+jihIA*&Oc@A~elwnA~nr8}hM7aFqy`4iX2V>oH$}rM6E`C+j7Ye0o`z7kg6FxMrOs1 +k>HdgMIr?BqkMR{~%X28@X24`^@J&Thw1f~_Q_G(@=PL_LlH9SWIjhsHNXfDgw>Az^LGRNETFM!I?gB +fPGosfXwZqvdhlr7MrwmICauVsB07gc=QdV+7OZi@VG6BFf=C#D_lHi5R} +uUMaPB68KYg=MCX8W{{9OwgE8C*&{O!=~Ti!oLy869AOw>_CUkCM30U03?d|J^;8M`EN9L|BU20Mp?s +u3S%AT#8z~!+c;#w+fI8kf_b&ozzG35#de^+g-F{hbI39><=q&&ikznPN9O~Lt>4memel&?x;&N-VEG +Y0AeV5cRB2`LQ?N<+ls6O(lG$6M=z_mI=#k_R)Zf?ry2v +H!EWo%bX%HDM~D4tinXG=Pah+7%5WTN*~~Cd689zbw#Z485$Kd5N?wS1Vozq{IYZf&X624oEJ859*f@ +9Lu^Dk;Wz2svwcoVJ07p0d+ErvL=1GZ-`~>ot?)HJiNb5N2$~(bu%y(b&{?UE@+BS0Md +B+f|zU%ieyQ{3$YpV3Nx;m@ddErlAunACY)vT4%MX_732BZ9B(QrWnjTO;Ve@6QJWYBE3poYB +cl)P(`FvNw;PUkQfobwLJ|tRsQBXrfl9i_?ld_Q^+@3tTzk{?Y= +NZ(S(JLK--d1%FN=db%YVDqj3N{*8co*o1U%4e&u_a#V4;?jhM2eNK>ZT)7t6Y~)HpzvABaurHX;pqJ +A4;kUMbNcbNT~t`MKxCTGSGQALlU@G+j5fC8<0FHV48YMhef1J!NG7Nr?Dvk17etV>RyG~wWgE6$@8V +#G_ecaSkQ>%Dc>Xqib#)=2FY}3k24cCT@Gqz+3#~Uk_u)1X>mQJY6%Em*$f*+3SYtVib}4E2Pj~Q`g7 +ApBu|O>c6TeE0z7jA&KqEnw3pYst#XSjghmAGMtB5-7z5_H!w( +wbyNX$2t!osY&;EF5~jDFl!;JuSyyaa6UoL!l`ynKyjN_mDA81Czv5n!000l%dIPw`)uwn1RlnVLrQ^ +Drltk`VyEW|3E1;q4aR$=)gJ4blo}k@Vxz4sS@~Fg!x|JJ*@>()Nj3-0xRO4o3n5NZw(@;$xT`1YjB# +Supb9N7n<3)bX`)UIZ;*2v6Q+7_U^a?HL^j+@;CG890F1exS51ov1X(^dCclnxyPF525M95Yvo_`{$& +cN$Y@UBMjrQDy_gYrlLM(u&eu+sK?zs@p9)2#^6W-2g4CzVa78;JoO}Zupt=?#=pGfmPD| +osQ#a{$Wk$=cb5|W7TU>_1dDsB&s#Ee944qEkpGmI3$=6fhv>yA%BHN>EX_I +;~VNo>CO928#N7J_$&cyndkVnF706i|J&8uXwv^P(=EX*({muA#SglWdICGcAST*v^h7tOW(XS@RGD7 +>2&sXw$B?m;?@cnks1bm?wlw#HM<_OwK5shltAFmha@LSqq9XYyTH{Qp>sPJV_dDu+KOVpxBJ}Ih@>P +z_0Vt%D?sRuFC!#*p>=I)pe&~;Y5FA$rfFB=@82E9#l+FHxU1x@WSsyoB7dLN>hB~qX=E45=oAB?^Yr +i}a^Kiw>InY2Pb!mtd!#0nQ8|yIbW=WXB}X_viRAw={6lwICsj-E1Zy`QAZvKmULbvo9GD`xSSIQ^4N +MbW_v7@ZZh(7~n4=jwOI5j<&B3bi@@K_Ecj+e83l=s|*?1i2wEniDu2JGUhL4{1LfT4gTgy%?JfD1Ga +uL%cpw^S_{6V^xnM#LK78e(DW&i@?=3e6ul&DdzvGkG@9NQR?3`V|+GN&R!2;E^yLKt;&Uqj-bnjy%I +fMTz8@G^%D$yt`=p)4?;g$A45qJM@bK}%9?c(raWgq$V`{;8N& +n^gbc6bzY{;)xD|nGMF~(>fURrhrz{uC%d0EW;~Eny5l-*tv?ep3Z!LIn&2x1e)+kG1TRaGpK^5b5p> +1}>0EAvoLv^>Z7KV#28MKelQ%I@#*p$)cXUktN0t505Z#QvZL_(HZ@lc;@WA;G-BZ6X>xH3v$51uP+Vj^O0vnJRo> +5~RyT9tPU|G~R1c#&K#B9Y6b_(E!)C4klT^p&lRNb~|}aCaFuhuO2z5*11x)4jPL3ADi%GH~XWc?njcQ5o{ +B@J>F_>dgvV21Y4*!W7Mav74K*ywES(Xon@lPFaYvBn=#2)AgANG4$@@?I?pk}DMK +36C_~ag3*h+~qth*>O5M2rHbedni;+6A0|DuW8FMh1?@(!`{FNuG99s +j@PsLIcw|np5FF06m1RgNa$I0Q{Bf+PgxJqQz>X<8jnB7<|Bfq4b!fFl|44j~hNF9nAlD9Y{>NnLt&x +H``rR=DvR?6~8rECUd#=7t&&qR;2}R_JYzat$B~cGbVla1;2e|vv)rm?;Mki2J*lPsJ#X!bOvlFAeM# +xo)(x{j!ugv&*4W^-<$_%u!hR3AIISsBZDSNI0rfB6KUd +1HN`Q)Z;`E=^V=k)C1;&-OzWG9_&*98=lUgzTDGN|iPK3i!zNRXdwxbG>CDB$ReS19&^gVT+BkbObbbK(aUrz<^f5Yi`PW<@~n3+Q}^h0aMhM +C@;1m9C-XIo!txJ_{KPXXWVP{V8sI)eryhHoXbg@vliQ}$akvjDFWMJ5mc`^Bpw`bJMNXW~-~V>?2_wf4E|zPr>HF_8fZyAboK3;+_1TYw&JBG)hnF0v^`(%rX^y +}&fkW0?>PTwyq5zJVL^e0_g;x-3mDG&|CJ7?Ib+D%{UufdIe4~j(H>n)Ho0;17G7U^oE9xdDy-lz@tB +ssM0-l|DEvZi4y5gwG(_qWOd(AOGIk>nwjWd2mr%aK6`tr5w3EulrlyNJT){+S8cZTGuQ_Q!FOttVFKPH{p}*LQYW@I +U0*AZ@1~3akD~uy1Ma|{(9MzVWQ-Fb?^Psgflb$BrL!utc4XMFMYTU!K_!qal_ge0~;7HH-)!Z!6))e +sXk;c)TeEVvSW-fHPPDD8fmqCy%#S!wsP|5I%fsd}jQ?xU(BxiF*~d> +_BVdOUjw$^&3YZA))jAxY&GlWZcWo&-evN5cBDgKT#C`1*Rr|*BDDuTud)dCyQjd3{Ex3nGU&oOCuar +TBrtNi9mTedmhc$q+8vr$y9RFt!~y{b6iksrEXoEm_#$vt*X +4F6KQF9CXrT5N||U;-kyO{xsql=bOOgUCyf3evf`-8PpX-(j|kGpq;joQnSRx(2%yRY_OyTZT?QHHWl +*rMGWA>VkMX_Be!N-)H9fh-kgd(dIdt`VaxeIxrVx_hGJ69%-{cN)_s8fVAz;+GjTdyO +pGKVoia(s_NPNriIo@iZPqAF7(s7Tb4=oys~b7=>1#A}I#Ogq=vMpN+LV@6V5Eob6Zx3LGuLIVUzQvh +1V;`@NH=rw+vR5xwJM-vvUZ2iL*r~VwXwYtj&0>eFHFu9kG?q>W;}9I&6IMw*~pb$14C*$Q~1bu;hrl +%6pxJE_3w@Mh)HRq^1xH4P~bH}SAaAWwn@b?4nP8v)FP>y3~>x;?v+gl#e(Om7C5&}nHemCIE_9`SyRI99gjvbGTT= +V%*k(ksp*W4AC>^jkZWRed@H1-Ne`m7Ia&bx`zHxayx;Cjf +=LC=MCY}~yd4|9f6R)YD#|?pQ}pDueoNH>{;~x~C3tPHVA4}=AGHQ=iXWT&e_Feo+}#NS3~1H3=UeSg +LE4$G=?-HfoKdbhI@8h6nrKKllPprBD*gWTI&<(XRb +?mBXV{I`@z6-b$?>g(1clYBYTKez9IBfg^Wo_Gq1!-Dg766<7j|MDjVTn@a9{JVZ@}I`Tux(o9)JpY~ +OQSZxLMspY%GG?36Mk(ld|*Bw8e;BAFldEJQ|^J3ZNiC@xdZL!-i=X6D7Q%p=s!yihDKLhZ~>ZD2Rjy7W1CrY56^M*l#) +ApV-I#w%5W7_(hdRM9}NF*b7ttW*!P!Pc3A3>=$7p-yHQD@e*^Otw*80&5u-@>Wcks28@) +di#*C-I)h#{=1J4DCj!+@{yCzjZjT=%mZ`UK&2a~1aeJ=6iJb{FN +%VsZ5&yVxDrtn#YvnE;rTR_mdl9A~Mg0xEaj8V;W6(@>1_M>zT;R-451HX4K@dXVJ=ip{P7r8zI|;f6 +lR`0J)JDAS3ODqh2OgP!VH5ROuFv`{AOA{RM&wRBb}kd@+UDLi#OOSj@Vo1WEsg#fKz(nqfdSRI)5OM +w*){02Yl*%@mR02{05elfdX@G@oX2(>)*vO^^dI)9>VL+jY)4mYbD3a#6B8XmPO{JFD(TYfW6vNl|V; +ODTFx|7l%JW23UrAsr^>WoK2KTiapv&PnNk1CykUV1-B=kx2KardatsVj4^r&+O06xI=MwGqm6UUxd^ +^*V>5mQgxj#ooL9E4>C>FZL#Lr3GP0FzrNpuK-m$pJZMZaUwrv2kAioZ4`e+sN7j*dS1QGrrEkm5QHI +_cT7T2xHD0Z^kAKRxB=@82SQWx1v$kh%!0o5GhDUn~-K?c4vF-7BxkQU9!bo)!c?4@X_jAXoB1SB!&g# +R#4D{v%Z4gjb7tw^2h&+{UhX?~Wu?=?!iSutFCvR+NuWhBK&ETWesulMd)J8|caFUKJ&~vPX8r?3p<3 +h`AH;Mh(GqG$Sy0H4$;eJqs+IBbm8%WZWG^UVHYsc?oKAZdXdC3pbV;5R2wK88t +ZL*&;-z1rLpY(1|>=trDV2=Vnp63IKG5oQEjL2~0 +>9ZnOiiLN7Bw4^e=#P}T*&Jy8p++|U{GRNKt;Ll){@NzYlE2N)qLZa$>((+;%(H7pu8Skd66Lab)*9ir|AW3DFhzNiDRfLP( +bWLy@a`UbW(V7naLPCQUYmZQ4m5al?N>>u^6!z4TThu9~)hP!|=J-Xs~qyth4WjCYvQ6=}W&5%_?=Ug +;I70xWnzs53HxGoxiWe5sUapMw%&ttV#2*OZetBVygWV%miJK2VHzM11^?IEgdM)5LSEF-zizbOa8Qn +gvT5ZU$@ynm&iNQl}S$Uj$F|03a`8Q>YB00&Mq=eLvkcoV?xb2d`tg{Z?>s(q3@X6x)>n=_F7jr3oUg22;HsI%VHpq#%?pm4oIN)5cVDOj&TZQGS^oVpV$n*D+*az+i-4o#)Zq>orPmJck! +1xPSnYK-`c44s@yQ8XYKh=SL<{uEeJ;&#c!}r!n|!XC45>aBPjG~y&e6wAP-U77jL*}&p-&Tj~%G2tqY{Y=+Fdm4zsnCp0>F!L)y(|I*SQOqYeF +!*96#TLXDrzzHtXTq*HuQe;W!Wwxw!ZRxI@ZIZ<`oniVx?c}`u*JBj(i(&&n3AvyE~piblG8Y?L1=>O +I@cShlL^5oVE7HiXBtk2-&i@cc=|L~2S~0p%K(HUr%-K~W`wAO8J1t_<&OrwW*TK;kjo-Ynv)k4#=I= +-IrJ%KUNusoNCsux5P(_BxQ(MBo#Y48P~1PV>s{>fs;C`L09cvOpXiufswm5A5Spk&6Hf^EtBSD3q8K +?dK$rbfJ+|Fvc~))c`VJK{-;cEnQ6n?W$lS?7d+F^}GE&bwolL_5iZ+@KX(6g&X5r^0*rH|E`V9dHL* +6V<|9O34Rs%kKYy|#=nOy)hnv9pve9s<@`pASRqggIvA|TMv{FP2N{Y9BZZUZ9_x&~ns&4ihr?kL?6m +Mi5zSfV~uA?jrUJtx}6cA;+I#1Y#D41MmIR}P6NnTaa$YNM*1YCS}8%rF?=-P#4%Hw$h#_6@RwlVdE^ +p2DJHW;(pR8>3s0gN|VrRWr*TIUCi`EM;5KZLaF%h5)RL+h{h)R$aD53#^xB&j!^n*LvFX4Q)9cobsI +jl)j80NjR&Ip-(WA$GT~mWnOGSIAVp}QObIw+!>vyfktbFD2y4`DMKDKJhO^HVa# +`h3n8jvoZ&bo8@FJb;u^L3qLwJ@QblFv9tia|RQD|L!e`hlck;n4XAoGq;Prc_by}DQ={sHkdQGD?4K +WSv;=}C9DHJVq3a;Nw!jaIcXmgPHBl{$GbO{cZrF8p&4x05ctPfajQ#CGSV&UW#8fyvC(5A>eLGVE6CksY2DDEgsfAA2W$!SHP2y{) +fref`MD0pc?CKr|AmG3D;y)v^3onmYX>=~MQP1)hEmA`ixJ>mkGx?jx!=Z`PK=SE)F*8ZCfur2T94NV +{5Cm4t1zRp}r&r^faAx(&pw%_h4t5Vw>r(7jI64UpV&zl##H>1_#3g``Iz0u0P7*+PGRMfR{R7mwETF +$LQ&W@N459UVUG-mJeS+V+@U@GbN`@$MdF{xHT9;sLZG26Z9E7$(&?&prjvJr%NtUAriiKv*NUDDcQU +CHO)@uNt0Lemo5BR@I1{3zYV650>llY4>$kiYWb={haU$@{1}K4um0-4 +7w?l4pA3#r+Z=_=HQ?q@eg~o1$3$YS*X;-iCSO<1N2JPy}r%2S3_&wgsPVLT|h#T?*h8~_8})7)n)hs +^TiNlF18m4){in>F;EbI*LoA->w?@vFK}5db;DT48<4Qf3X46I$%!|T)LsY?hH#RTqd(*%iq`LxF}ceDXqeR +sD$l+VQH^>&tq#^zS*3EB4a;o}TX?SuxDkF~41m(S>Rm5;G<(5{Lonz@FEQeA-(U8DwFuD0HVo5$S+^ +MCkByMdzP8*H0`?I2uFS~%${?3|qKhDmBtPa&`&uGNL2LI#3N7e139LiZa9yy4)zo;eO6H`lcTVMtsn +nUvdBW}s8{A?RxRP?2-0IoB6Wn@e3|A)wr||7Jx87<{_#!wdtm=o+vVXx&+KLsZfPdXZ7Ydt9h!5HWwzqrb32hYFe?IY0|BeFB2-Az +1n71y;o&DrN4f1f*ux7{!3-wr}dl+#aHC=5AeuPkY^>K0F8jVM%D@Lk%iuZcF(PW3+{V?=5ptOcTuKk +?AA8%0~@i9{r4{i2S>KIj)V0EhZpGc*fB%9QaAw_p=&=sGphY=}D&NURE?{;hBkx*U(6LU#rRh*p>wf +X4b_%%`gtq0xRXBXp;R_PP4*QMhz#LSV6QcD*_Y5W@J&J?R{65QM_!1T|6&?Eq%`FHIJp<-r~XL{~aPL(Vw1Ysz*7FFGP{SZ|&)|kcy5;&k?1(W +3{;J?>doyCv1^c4h1NtDcd*i3Kzt_i@gJMuD_*|K)0)ZiYRnJBa3wiS(^vNXeR1qXL5umJ@zuiErM35 +?5=X18!KX_}jg?}$hJp=f`C%PYJ1aCdh*U)pVBgs#QW(4XUcar;BgbKAn==$aQ5F_T!Y!q(O`-hua!Z +$x{BsE8TgLlp6oIpRjZ1MfVB;6r{ShXR>k2l>(0a<>4tM+E9tdCZ42MB&T}HNGM0W};w1c4{;kid=V_9%@!ffq!?78^ +7H#zLG*qoXk*zV-jWo%BkJ0+AMYHOBqCpXf1RC@T1M`e +hLo7bp#LloHzCCLy4(0CSZ`)&X}N~Qg14Z@S2fE0VlE^((haHwCpWpWM2MiLa)+$FNuNf-A*p%8oh_> +#tBvn}a2@L*q*+3I2OXh9gl<*WGn;_)EALA%xqX!Aa%e`lYAvC~QXu(+E~Y#fUQ-Td#Gr}8Mm2@fjC( +5w#OSREeu;M*#9!Hfut#`6p7JXVlgThH$-2%xU%9?ET8bsJO#OsXviL(urAa~*{a3B^qOEIM1K)0|q^kN!Q6I}LZw*d9opPz}g2~bP^#4*2fZv=D+|I=-tCSRqU2_Pp1Gzj{R#w*>w;RYhbXxzs0B*VvOoz@U~{9pgw-}P4p< +1nQ0It>Dw}_N&=Uz<2RLYjgrLUeE-Q=&Ae?+K(5!P}6JaS1L%+v*;@dzS+*t(MtB=uwuTM_uL|BDiy5 +G6c|y#fR3@-T(m!U&>XFEu6+DCaRfjyG#C-A~J`;b^CTI-mLN$8hdU!!?|K40T`1T*O-kawzh-u68nY +eVqmD`AlqaYy3dTk1hmz2xk3?&{)kLnSlF@JTRF{z*848@_3*<5}VGU9O!yDAy#3-$?@;$`?L0Y8Qec +DWZ^()5*k_Q01aN&@ygiUC<0<(3<up&s}9Zm#D@~gX{w98o$cc7Fjy{ +HHCTR2eCAS|_Zk|Nae+{EcaJkRtEZj=S#C~YdCS<{bD*%L0tA9Z=%-xUHtAT`cpB9!=;#_jy;Et~++S +{wN+J1d{%Y-@m|^Y#*=m@nxFU=Jkp#%uFYOTCGcqAZ#$GCG^AE6fLQw+J +SXYtcI%N+q{sPvA{_`zhoW_JZYvzT%CDkg7z~ +z6*yd!$+h1YsE>N2xB-AA3Lm6h+4QSxD+*Q5o4Fcj}xutlP0e0nTOPX(OO$1A(>3!8HM23$nZKP!pG@ +CJxYJS#r;ytsx14 +2cMEa_tW$uR1tlEF|%;vnFY3O#5xgF5iP=T?fK`xh^1K_KJ!@*OnQJtyvtGqVTkLWB2*2zVHm=U9uZC +q>FIV1c~#g&?_PFn9G&@Qxce#(Zk=d>9qrS#TXg1~Kpxfj4x=~>uu^}~w;)2%5O__grvTFPqz;57PLg7XXf1EfVo+4{s$k{_B}UUYkG&m+1=fmCPc+x#^f&x;1JB8rBh(e?zVaQM; +cf~WVd;RCJpa!`(?{oC1`b2OBJ^G(@m?(cJ#63D`&WrvB2*L^&58cSAV>7TmITqc&m!__d1zGYi1G}h +$tbj{3JQtFX_VzFWe(L5+!1?68tmx&uepdus3uwl)5VuKH+#qi1d?uE{EN^TWVgx|`)^qW&kev&s6^u +eZ|VN0e%}Q|sUV2LdZy`@BTsqlan7M-UTfPewCB30B^re@Jr3yE@$|#Yfv_|l2Fvm7tohJTR}>FEWqD +$w2R!JX{ZqoH87M7^WSk$IX&Qs}7o1@ZouRkVw7^Q8(>)5CchZ5M)G>EDE5hUbg-^l*p7ZxHBy-If2( +VQ?(v73o==#HA`TKNH9#-F!1u;Ffn2O&{uPbwwtzYzS{AXTZ7Oo;o-6^64dWy&+iy{;qt>jvIA%cOQ; +^t#?UZdM8_jU*t#xSKfqYP_ys5iROE1uMKvKIKnf~_`CW%P&cru-=W#|hA}jvbXoi)yc@v@WU-ft4Gg +z^W+Yl^0kUX!jZ@CAvwouiNY^>?y1e%npNH@i%V8jWPO-f77)@p(BtR_MAs|`xiTTE9` +<@Rpk!+P7Mf0#MG|weBZy&&N_Pkmx3lD)EVg(x7zx#9N@X!c@YYTuFmUH8jkGOMW`EE=$4Zo&M$shKz +d~ZLQ_W|ZS?*xE^|;c^rD!}2sJ}yclO@JMq4=kTWnkTeItj_$qG)c{ab8)i)$oVeo&SB77L>7? +@lg6P0>iL9(K~3z!_*%l#AN>urivW2RIQo(3~Cr&~W3AUYuShLLJdEkL8osSQ+5`A!>+*TRpoe?1l|O +V-CtjBA;nTcRBZmQelP<3$44cb^hurIe-P2uw|JS5$gQwLp0OFMdCaM5L#UqyY3|()zPI-Y49{s=-uvt^*AUq8mR;%HAT666g +8YKA6Z!!q_E9$AJPQCZ@E2BMXAwUwcDGdm>?oT;1!KOr3;^i;I~-W7$lR1K+~J3`eU0_KX$}Mu#a$Ib +;j=i&R}O_1~<(LfEuJLRTk5Qv$F#bK#iFk3XZ-nERIlFG(4v}CFhOSj>4kpa9W3QXy`slk5E(;lv$cN +!vzLEwQHV>o4bx5l+UGc0_4AARXpFZqr_;L>*3&^pJF}VQVxVAJ;1%u)!@fgJ#gz4>x^LN`$^Ygrho0Ph|D5VRq_b2f|WPsw3jQ2*p7k2E=8R!l6T%9idhz) +cv7-pwJ;yGTp49D}q0}2Mrgq3tfomJ|hGCK4V29nszMz8W)N|>jCts6T8vFS(8A7W9v0k1I@#;`ep$5 +0OMsqx3h5RI%W&UXZd}<*m#v|O4RobR3^jSK<2uhV7fa)Dlb_1V|E40k$AJztK#JTAeLL1u4T?vySl) +S?1P-++hmyOwUaSJ^l+r7MK#d(JO?8b|G;7u_8h<%#KSS0kO^bSyY8wOT8wqfvnJ56+J?ipg+MoCK%pj2zrq`bZymy(vib~uB9)khJqbLr`i}22aSl5R$}@H# +X_@dT^$P-E-*VWWW#3QZ;jmZ5lV(o!*0he)UdOHw#|Lk-JD;_SfF{yBh>%AE>}_%c>mnwA=|A@n{*)7 +u7x(LAqs-RCzZtBKF>nq6`=;`cUf!~@jp!0Wk6VJ&Va#+s6%-Yp$6zW{u6w|Wc}aH2`KTvB*Y+vq5`(VOf(_8@?YE%=z7@{^f?55>_>!ta?`V8 +zZb&SR(NJs+S? +mJNq6y%$#fURvf={0cTXva!ByWOlx;Rqxxs>Fe?R5CM)Xy9EBp;qFUXrUb`9-%O32un>@Su9}HX>?;K +5DJs~xOBso1wT#BzI2Rds0#{)<58Xe)7D<=J5Y3SVQOu0a@LMcqs+Sl-T}>j+E@EeK5lP>Fv +R*A<8{jDAqBDB9m=TT)>ngWOh=g=Y?8QTH=FK(FMsu^o_NL$5P^&2H`R6`oVk&?o*V>*WmqD`OkN^

nvaHFLAtd&FgBaV*6qNwX3#X<%}!4}3f$bY(0oe++<@B4jh!SgI +4Wj}yox8Q)^^@(blnJS;e^Va-{SP3dx?$dmk><9#Eh;ZRP5Yj2lTmK8Su(WR@kl9$FpU4v(WSE&DMdi +Bx5?g=T87tJBt|13GCv*L08I<2(>z4dU&eqH2`ZXBcBV`>~h2_V~c~}x7AM@6B<m{2> +Gh-)5Hve=!PYKsAo;i|b5A2**~?sLMI3l4 +E6~*M|C_6esvrbY{qtkcBCE(X2PfV-g_T7V4*uGmI?E%%-7`vtb-B$ePWG-TR)@sy)f`oR=ofJhdV)p +Qkp?t>6N^$#;}cI!Cr3vl=5XHe<*GOrJqBBzDE|0o$aBrCW`Z-w2ygd0qs3HHoEw)ce!kCuf~hp$qjg +GAP7yotk3DGhgFQoH^8=DSmzyj3FuLOuQm1eBA23O=H&sPD4h8V2ImoqXRIJxPm3XAq6fB-$&aj1J@a +zo@(8sugDQXUrz!xQnvMwdF_B(9J%APhwyqtBI&YTz2BDriX@Yw9@eYD`!%WY)cH%nzb~;Y?r_*?MI< +2;+w`Fz;ajJj1a93GJ?X-QP)2q{^Pkh1#;I0g2{p +jO*zplVyzZ=i%`R_%>6)SihtsCWBU{0HNh^d(1ca|f)Ho2%_(NY7zKM-Gm+?Z^*56~reM#X$EC9`qY? +r#E$6`xU!7#BhW{;@P2tV_gFZZ>EcRosUq%N%yAiyL$ON-mMv6k@xjqgAv(I*U;8j;6)cS0YkQTOU|7 +ET!4~YHFf~Q`|2L~`t{n>J!dMnWB61oDioE+17!u#BIBS`jXKf3l8+72Shyd{4A(`qr> +`|g@VyBHW1_4=u{{OQ9U&dEBJAwot=XL4FxP +i+L&42f)kv~4-6W)XGf%s>DdN!G+(mxUIGYDl^*j^^JJhQzZtMrR?N-X2)S>VfBS4f&lFrSYX&Z@?O-R9Aq@}VkZ%Ap-!8ItOKCv&-u3%yX$Gi3w;WD_?Q;!RM*5)T4HQsZa3TV7*Q8VBrGlZyFSC +BD}KXbkiG7m>L?$kq4A%k=+ag)nHbIU6XX;1KAx;CS4TtbRIM;&)V +>f+)qv{hm&D*)k%O&_9PUR$f8w{%Q?f>oBx8L-l4bO>4bJoN-A7cSdnnsXBFiGy4)NLpDHAkihjRVv-3rN(f-=6xB-94%1q3# +sb#?2TV9G@sfecIiD$Q;J6RY-wrw*4kzK +ApyD;tH$CWq#tI3jK4Zn*rYS60Cys6hmNc^gHn^LoG}c@|m@KJ~Y}WzBeU1s#^HVH$)ZQ}XQBx8!Ahs +-EH-b)T(em>3Y2{8yQSIJawfrdI*=rF#`Ws9dget&1H9OC(0)j7_qqa;+96OoK+sfUraYlkLxW_iLYy +UXSu1Ibb_}_iGUGp&XR7F!iEf=gd}o*jlN*`*lEv-+HQVcx{&M@-YfUAN9m(iHZ-r{99dXjc`>tYKL{LeCF&#>&!Zat~BoB=Ap +VZ{_qy~YmY!6rxNv&{3{3C!KmX(Zz<2pS|KtCpVT{v!KQDhBzNOt +TNxnDXIV;|{9dJJH`_X;M=#^9I-mhnLFGIiJdKL$7+nYiAJMH~?9?@Y5#P)e?{S$@)$rSDX`U{yvOq8 +AqXW?|H7qT^Ae)d9qSOm2cxBu%U8UNi~7r1lf9+lT=k9#S#=fhp*sCNNjjzT_1Ytb9O}M{4 +{{d#%+x~dZX`U|OC*VB=+cC{cJxtsveuNQQi6_n|}s*b=F0ln*djoWVq_+}hgufv68WYz8XQ +!o!X-~)z5`=3KuGnJm;!|27B0P?kC;YuY~Fl7?ggbzR8D-%t{O0UYgDMF+DP(0nQnivJ!-81%h4V)dx&?KE(U=d^s>+jwSh}WtGI*cscdK4qJwAJ;u&ciSFr!URU*!EDC@yri4$|YkZs4T^2}z-w4yf?bOgRFqI?xzUPym8 +XThKE%m*VcizhK^?$y>=Ww|AjVy1R}1Mc6-x;SlVE$FfG5vtB0gYv8O*LE$upg;6M&Zp(tr(<6(907b +fEqAojD*=>g8Ff&|=H(JPaH+g5ab4QW^N+d(*A4sI92YXo75jQr=MHH~y9Bn#VU9q=a1zlxr5>LrQY7 +*EFl0;xV!hTwCun?Y}ZG_%>~JO7OU{QLb=WCudI9-3?)=zyK4KykV~E}?bcHlTsdd?IieX<7fM^ecBq +l@@AS2-1ws=_%6GTfZSV_jM>pmpuA4>eZywHD%}l>W#_l(VQrpkLV=!i2)V~UtRaTY-~ae@926UH#x3@E>vc+PC5Px7W&m6pgn)R0NNlP25WR*EDF$Dfq16I30<&*0txTJ7waC<*|ZSV4)9^kE{epxbkR2b>w3R +52Je9r-$4b)2*yqL5xn?FX^jw1Vl$z&{bt%@&xp%a=?VpNF%7Gu&9c*E7C4Sw3jMVUBzdI!(FyNffn- +c-dQT`^!U;DRomU_mgFrkk`NEa^3jFaM4;274k-f$ybVkk&T%VeSJL0B9Hk33Mb=vI`pi84TyQ1q8N_e*2}-PA-3WZTE3W1Bc?ZEGBlR!~!Ul*zH}??TDwOcBIwNaHM7Ro}NC;)f%?QO(3j?=lbqT(# +HNlw@d&gem~)o&iz1^^T2?AB;C_qZIfzu52<=`27Hfnu1avGxq}&DqUyjy{6rc^m&XOhl38f3eJ*>AO +;49_&ux0r^m}9=ykwErF+6T1cOyx_EcN+MrLotK1qU(_xFiiI%5ue_YoqCM$hW716-4(kVB`2D3;)#f +cKtu2GDT>g)xuSzFqrAyJ`SCpB$XO +|9(q9u@j!xW&geo}kI`{zgexANxy|#}w(#D&Rv-2;UaLAb+YlChPq#w#1KWUxex(qEp}pTy>oGxs9wX +*z58ihcgd?9u@{~O}(qe98T1c15)B%^3vJ2^Xe+pxhi)66iA{iRbjWnIlk_f*6une8$=|{6RfvWoo!L +LEEJddRQqKv28I3>{i`iI{?@am0NIQibBlx08%oE~W-*I_sbhc-}LgQ#ioQ++%=4k3Ykr;QtYUc^FOb +BS9s7LLl`E+AV89a+oxo9hT2uqKX|kS1i*c6V|GYkDWWbJV?R>^DPM-Jz|8pPQ$){d20vFk4|1uoRW= +45l0~+Y^*yd)})G(zbdyG1S#>_DQ2zE^Pl|#9hnN#i^*7UUK>UD$^&;;<~KIQw<$TD!71dzzsqM{Dw) +xnEma?UjM0>28}Il`<<~B|y8U{*awk=*32ufDF~aiKV7jGh9eo$lWfEvTSYVW*P(#nVivGkbX +#SZ}s`y1poqw&*$RgSVZ8WG`?7C*nM4rZdcbB-x1Q7&AY8EN)GLrbMOldyo}4$R;}$|=)foIIyx567@ +3cEcr=N?i_#G%-r9AWZYdUoBR6Lw?=m=VMwnI=^aVBWzM$@s0(z7beya6t8u-NO&E4|%AL{^ufKB6a^ +D6Y1v!vj?R@gVPfN|a1tP%wiDGzR1MM5(0O1eEU)=Q~HI9qVQov~Z8GZv5rw~W`YVB=s#L{O5nv@5;n +gj6&4+Zx2wPs4(M?@c913>xzh1wlRI#hdXu{t>UH!3#34*EI+o4T(mZ%~cU6vCYs;p`-rN4L-Gz!)?4 +TICNY@B@}u&gDuTKXa2ML5wg8-Ga}f;5I8*8#1O6N?|`(kYxNbZav@2QL=c8@d>8{31Efd$+p5(6@8D +-k{0rz-pB1>-e8dj$M1s}T0clr%(Ej`nI9=eMPa9IAQhCS~SjTw~K=cCAfNsJ;z=4eN13G!reVY%F0i +YYMe1sp+9dMi1%Sag*@Si|8@NVbfWW3-&%hI69aCn!<5^6;OAUx5CqXF9n-^v_k92UCg4FfuNllO^o* +&+yZ4c!A8nW?Jucd&^8Yh({B()EXqKzQ~jM^sy~(H*KaFsY$?U;&Z)goBUQ8S)q(cS_HoPBuV2n|fudeoX55RUwB_`zmq$2U&p*{(i}UeVEbCHl2+UJkF0v46qE1p +i9;gml_rtJs$mR+Q6p4D433a4~Exc3&K$|o|I~~_1YmoEu07X5=e^+S6QOqkXTQzEC@&2H2!C$M5NXb +wF4QD#YBQUHdW$Hxc`#iWQt0<^)e)0ap>wdsh-D#08y+iIp(Gd7!2BhrTjL{^Ch3B>`sQ&vlUp>UGMmuPJHNEnTo(-#_biOtgn9I!%Flkpww#R&q7Y!|Vejj^la| +0c*mSHpS80Di*2Ttg)xU`#sn%UpTm;b?Y1Fcv0iKFu)A$IbodFV* +VnvpryV@3B<#~Z3rta5+yub!)iaf(FU;P#-Ls^CMcLMPm6uy^D}CU=my}e4ey#MXCO?=YLI@^_P}5Dr +-x_IabNzesb+**?DL@cQlOMN!ee})IF>jwGNyY$6k>(cJUeTTcJ9w_SP<8rm*)}b*)cgZMS9SciDJ0n +@=ExUZsUAM<{YwV$ubjOos#c$XBz2R^{T5_p1o*p+&O7%I0=9<$yTu1Ep7il6KM8am+WS)_DqaspAA{ +L291U-$cOJlm2XO#aVR46L{BQCUyhO-Uu59|By?5+Ww`ovt +|mszEYL@L^o&D+goTj+#J&mP{I2smU{NM)=Nn>L=ZsT^BF355A_dnU$vyru*cDDuer-lweU6-va*N39 +(eH~n<_*9X}*eq(1TT0ZMGyxfwhJaTc3=F1q1$lH1^_9H?EE7Q1h|Q&}o1aeaj-2 +UZe5Tm(t_l=M;JuXG|w!T9BI&)Ig7b&sdN6l9Q`*Hj6MOPG4p6U^X8Sj$~Q6c*jB^U-E75*!JO6>KS4 +=)O~H7IS`gU>Pb1omg7Yct?|Xyk39g*2kp)c-ZOPVumfFG>0+)ew1fRFn3m(E@?Cuyjrn_3UASa{GsZ +MW5B&85?>d_wiAk^i&3BgRy0q--AdQM^o0!X|_U0#2nHz8P5-?mvbm+M%j4uqvrf97UuB +va3Q(!1IpVW$Vgb&x!mp3i_VR5uTEh4&`-9LsDFhv~2aTWOT9!e7N8*0Wv#{{b}XzVkk&n71E%L2hM* +XD0|J7`JJPdMATBnQo+#HqoI2FBReU?R^V^-93aEn8uHA;=0{}`Fh(6y@SCga7DUZ+D3Yp;ttStWd6gbzQ7w7NE7 +hs+?*WL#k^C&GB!y;Ci)*=5rm=Y`22CeLY!XJB@Ng)cckK{Ds?M0Z$V=J`i%dG{uFuw_%Z$#XAJ5}KV +XM+E)G^E;N$Bnz5lCl=QFU#C&^6oheWS);JX;IUFMfvL1!V0*hu1GH-3X43>hOmA6DU?$cTk2KRfJ?S +SY5+;RH?L&854VeV{Gw8QCSck}qh{b3w9La#E +FXc{IWl`;HkP1S#_xUA1cA5sdI@%N(@!zID2PZ?PJ9l|C+#r9+kA+9&&`<>cIOqE?jW5*V*p)1-fy_T`fAOs*Zv4leFQ~p5Cm}dP&FTwmy5zLq48}|_x{3syysqG +CMtzh~}8gBQ326cJP;%}DY+3ePOmMrU*Z~lsA-d%^_m*;Fx>NPd*@*Rtdm{;+iv2wf67V3>eGReAp&y +{Rxyyh@?bj9eFSFqcc47`WQu)?_vLVBLS9M>CyvD6Af|^e*%RtYE-%@8d +C7NOvOTj|_7F=<%4UEVH^H>WKh>5J=Ca2GYxZIau2AolRjtQ{qZbNn^i}jQWV}gZ_zgxIhCq@H*zRS( +W#r*(Iscu5Z8pv~<#Nd9)UT<&BNp%f+=!x!(xv~$EO{utf~)>jPQV86G!o@phD?ew+{URK<#MAtK?}k +XZJcD+ZTQ7N65w{vL2K1##fl~vsc)}}t+=gTdS%^wZnBt*zpwQ!?`JYGjLSe+~ZaxLy-OycJt3cz57ca#zkZMTdvm}I- +UgG3e$C9wz7}efS9e^{9j>lDuOuZ<)VHuMz@Vn-x%3I>v4yY->Q9FS|7a{I^Qm-`oF;>9yM3e>d$QuNR#Mslah~FQGC_QQ&0C} +ZAqY3W#DlGGjR{?P}?5&C2{f2y4b`6O;!7ElT-R`-k$M5tc^M2m?{D+PM=o?YE81-W;^hjPII|-aAK+ +B=`eqiVxPw)hsM{?cx@FwWE{Hc9Q7ypq#w9Sf^>bQXHgb}CZVx}bBfg86;(HUsW|W&L|QNOWsY +nO2t%wRpc`8wsaqKyCmT4b3+ZvUl7U`(xjMHpAds-|fZovvHi7m4tGGiEk1MowCy +&cPoLj#YMcU5;!L|2aR>05&j*XOFgwY8+7)RlJ%4_e^AcG2`_61%bgLru!*ZcY-0c79AkRitK_py9aw +N6Hx;t5nle%I47?&72%y?r3NNDiRyUFu|D#!LHC<=sp`-FSYfev9f%7K=O&g>hcPAOh;!rU7iGTPRi( +@h(D++A6+v64`;dBu^#x8#z}lM{ejX +b8ah7)OmrLOuB}iY$ei;h_%i?{O0nyM^KcvD_Al3&i?Cz(Rx`LiV^LiR;!DlB|p9ED$>}#0@z;lBgwI*3U7PZZ{hUWAfFJVU_xt{SnsFXvi=7dZ|Yf>SZj74j_#nJJ*w* +7tCAM!*)(qIIp;jfP>e?kkh0r~Zr&c94EB98Zya-*Jn96bQ@`JxSio*c{gA-4INO9v!hGrLP_UGhgP>*R|F?U~2>w~ce$UG>BO%bj&tWF&`> +f_|ZK$yJ~|?pETCL9A&)4eWtYkCG?{C6QH2dT5`V`%7s=8yK27XFm^Ejs~#ZU5{smd3xM;#Ld!eSO8Ah4e3!wb$k~+>$ +0{}9J-#?MPu8*el~-8WTPh<*KuB~wy@?7Dgrdj&&qPFnush*==s*CL!o>S3IN;v^fPJdLoz(v*D-Yze +8qJ+wEbOQe9!fEak_U2oO;}cWC7;tYD7Vd8a*TRA^BbIOt%I`Umiql`^9dn-*iQWUYh}*SW@c|J(uM0 +Ce-(|Vv+l&KjMW0<{q(&*ESHwK|3r4uVbm|R#mR7@w)@M&?1KLJ?=v?G|6iY9J;y~>Ox}udF)mUn)O) +fsP&|U-KHw>8fgdQkzfAOWkf9t26o`YPp1)P3mMnCPz~)(n9HWTd!LwD2>~AoPHu}Hqkg>)^deye!y| +z3gb$b6i$rm;1qWhiV-9@qJr-vcbGg-TG9bGw)MsS6N>>c9%q0t?E4L6Zt_Obv5!0!FJDuq@JVbImZ- +ycIPUSUFb5hnxF(VOplJ`|%4v!GfSdcwc(p-Yr&pbSIy@&yygeP7Cm6z(c<5rJ*icJZi&>uu+Z3pVCaX5L`H9_t*e6MzDZO#n|v|Awc6uD99TF<;pUx^@G +&*?iS`s8}-Y=7^(aEBRycgC%!38b=8Z;@cPmfM5@mU`ES%B;7!G1n$}%G6Ng8e|^|;lvW{xf)NQdD^4 +z_e!P8g8<4g(`o&cq&?j}#5j=}1F#hpG^#^$enro3ZT1y=7UKZND)3%1|A9>fd)#HtEuI;?(Szxa>>8 +>=sCK)!vc}~*98o|W` +d88*c?SU-f)zla{Ut18C#I-r?DK@G&ot_R(ul;S(9FmZ1 ++KvRuWuA7l?wtElmlBwn=y7Kccj&?2Z?W^kFSRXMt{*sbJQcRVt$&GY$QH=5na+@zfl2mQ6==?7ppN< +=?|49ubBoe8Kn@)@M_st9r#kbb1^%!`{6w=@6(fpimn<|zj!1V +syl*9%PC!X%+-sQ*Zb^k%lqe!M@JZD1M9!gr9^#XKD!Jx^825Eo~@z6!ptnfTNF`I$~9ut#VD?fxp#8 +g=5n1q}t(D4nlL&kcN2%>qpR)L7KtL12r|lYu@tQ6txYr11H(-ac>cK$JXNrZ@;!U>e$7m(WHSMEk;g +>X#n#yHfzs-Stsnb80Ch*9~ku-`H;eN!#!pTZMk!g+m5)DSO4$-{(miVz2d>JcNtQscUzBJl8mNT)7$yjgoFSHAmV>sr}64>f097AAipDR0AEAQB5r#PlajsC3( +zEfy3?~14=%Z~Pxgct4w@BtVix3Bk%1cTWY`kc8X>Y5)>2t=WA;l8Nuny(&HAfJ=wc@K_=lr=lhl?Zf +HBy0GZvbi4$I;tA0X|O?X(@#Uo-u(Ic3>sd)%SqPNjLASaU`PXw|sG;&0hdf=3$&?RG#zq{m?x_I^2g +JKW0HfY5aM7J}+ytWeaaWDu`b-r5MD)7t0gM3M1xJs&m_!nupZ@)jmT-x*P_6?^IlLTC?y%A(1aj(%yc!0(J1v6to_*hY4J#I}BPH$?%5DnBYq!szz;|?X)SqTv{Dj($k`j46fpi1w_ +o*YPfwI;5RJfE8Ui2kWO~7V&j3qN8%Jeq>#g+|K!^EKk2{V`l&%x`I{R=#KxnF`Db#snSna9=V$bzYM +W}x``Q^-dd%aN{=)(=TVT6fu)Q{MJa70oouxkgr!(F+DD7bReP~=XgvVi&xW1@%lNdd3WRAPC%_j@A__-IHaeREkSghgm +?gHktOm@LF?!6_D@|xrtvp-tdGFz3H1WGR#hH9xIktP9Uo7Pu)dZY$PO3Piw+)l9Qd|3wfNA>UW`QW&~ +X#=Ea|CODYb?|IfA)_UqP2W->Q0!VBmODqLv|F%xbl@dFalk?;=U>R +Fu?1I(Hd8%Iiy8dnZ(zuDK!ZfC9tQTP*Mb15d*LCAGz&%pFQfz@vdVGk1A2$77aFIJ3PT5LXm%PSp1k +bOVl|87MOiu!nuJDImQJQ&F>EsVBDhSx^aYLdSW?@QEL2*U+@4@X;h?>SjPGpdZ4E+>(l#Fc`CxE5}Zf^r3awBT&eoAyn{)QZ1j>E&H~C3K^QXaQ +Hn*L)R%d?lF*E8OUQMCAPkY9BXfnfD_Q6trsB2AL8$4$Z!gtXT)fZ&#+LS0B-in-koAt6hPsgV9tm@~ +%rK#R2$@K-+Ht)u?=45>Dv^i!v_S)Dh^DA_775lWS>X8;09r{E^Z|PF5O35lQ5)jgph>0c$5?&+z8#7 +EL~k&HCpi$tR3Gb@pr)8-6yv|Z8;*?EP{z(uHJB@i1n2It6zrSB7d@qr1p>XKy9(LtP>&@Wg`tZMpje +-)d7ZhL(eZkxGeai61Fv|?BBi(SuCgPMgN7HNZ%?c*=yPRnkR%%)ljy)61NXV2H`ph&%ZULj^p5A1fC +#?5!Xy)p+@tSv(e7Ox_*X+6&C$4Pda5jU5Xe_lciXH6p{caL9!;714fSretB=|ZbS}I5T*P}V(||)W7 +O2m~yJMe07Jyi<((*idUEjBxMO&Z^h$MAuJrrivpQfgRf={u~9r4iTlHM28>ONQX-ae{4(Ssgj^zk4t +zYqPIkUhn<&r*bNWMpB9HUYJEYXyi_->j)M2!hUzW$t8Hn)NnM4uFt +i;f5;UBEJ7EAhkd;qwm@qURrZd{tPZRimsw|E75^oD?ue?KhRH#<{|jCluw%vowl|d?A8Oz#+&wa?YPXk1}-wh+1kmik=cJBYJAj`0Yc^_I4bk8VJK_54f^0RjgInv7TvFpqKR_7?h +F`uhpwutRIwNr2zxJ2s;OzZ91i!2vmPN*^KjDjY;R6S6)R02|r1TH9b92ma8>>f!M!(t^U_aKV5*5xP +h9oSwGNwZ1_C<}Y|F7VuX1=QF8qu4j`r7Yss&C3c^yd`IeOeZYMp2dulGI$ubP8tIo1A_yQ6p0W#i^d +LYc9NJ?ORr-dy=Z>w8fk4MKY`jTVRqni(EZ{apbN8yxb-!0?S1NRCv2fJuug`VA;oeS>DeyNl;$c}F7 +WF)gZt#uZ1c!q)*6VXwZ*YHa()lhSTbBl5lluZX6#=^!D(yA7$xFqd{eY>o_XBJ=$kN8yBOF^HQ?2j9 +3@CQv%>mzrJDs0S{DsV+Qhm+bGQh#Jvv|50i)QMuX+(gpw%B~)J=a}5s9di@)A)-6VJYSrvQ41M4whI +xz!X33qWQFQyQNqIe>-%T3S3YT1zx(xUQCV?7z>T3WF43584>S0-}m{A>aQ{rGz8q$C%Yz8BRtXrj$S +HT#|#8Jci?wCV8^1ci4}l-KZ5avnn=}*Na=HdaJb&Z46w|o8J-2sw91EPS=y?&ENqIN2fEgJ&-5e}>{ +tx4eBkGAS|)AL{-90GG@kkpP7)Dj_}aS=It`=@bK>A?R*)29^Os?ge9MC! +shM`y~?4_)x^ULR>i6D7x8c`iB89zylb8Eb09T`9%4nYUXbIKDMH7`)5e!)@dM7qEObP9^tq1slT03h +`>cZBS3@mp;}_Ck#K)2Ydy~w=FV(9}8-0S%oFiT^EqWc54trl +!aV>W>=ss|FU+I=WE5+#5O(0~t(kH*eY=Pg?s~gxQ6Xa(;Kpdd^yUQbsY7uGogVljY6Qre-J0})0 +jo#U8D{B^l{K6!S&(I)i;3O*+(gV$;1|#s1^J-x2^l!@yr`+X@=_%Nw5Opu;!m<%ZLcbsua}Cz@s%vq +6wk7fgWK5P7clR>Y%6`01?aA_R7MbR^d2uG#D;^(X@yw6bSZGb;A{ynrg8LJ07&{}Yn8q1P +qbd@D*m4*;2vUeaX<#Ew`U>C3LrKpB@T*d_hEYqVp%O?R44qM)h!hXhk6d2uHEe3!qF3{9?Q%LA-Z;RAAwDH`Sg}bg&~SmxnNWWwSuUQ)?llAsv +8cfb(}#=Zh;^ykvBl)!s7u66&#;v!a{A^*u_fj6$ADJs3!PjRgCI^!|@-t9NKdxDhIwT_PIK^C9#F$- +9`U>M-q$RF|GkT0lhY|&o#h7@t_XqF=T+-nc*c=e|1>NU6w1G&fYy42uu1JvJj%WUp?S_@;+`b6j|z;pi%L!F|OQ=ck)k#aAF9{(w)2ce7tZrmKHh|T2%Kt)AJU1Z;&e +7gTTb$bXfI!VN?e^)ZGgl;f!wyi195eAMvW}MSuV&T_V6fR|NMy_c)N>LZ!e_a?q=qyLfXQZyX3s_<( +=bf8OdIdfj#Y&NRZ{K(q{IW*-BgD~6=c1;YAQD740`ffif+toc(-@Gyz%C^HWouZ5itrD~~rZ`eq!fsEy(&!dW9ux?O!YBwz%;HD)OX8~~Qc$1kOVCBH>2P^TBX +^k2uD#5GIi>O2xqU15tHv!g;s4O@r;GMX!%EHmHiYkN0%V4q$C-dmT-59~1zy3UrPCne7jPWAUihi5w +#a1b&Up4@z!Xkp33P27i?6ddtuN7JBtrE7b(+etQ*j=xAL`2fWE8xMecnBQjM0e~ObN`+A>e*~5} +T*Pboa0?#nqyM}gkrb1w_^?BmZxU0+7kZjVN`SW~3AiQI5o9cY$*> +7M17(t8+Bj$f%nO*bwgEGQP2x4qwheU8wyDZ@8H{J)a(FwPewaBBmd3-tWL2_zKfJOH8*&4%j$_c=ji +plG+g%!a-*p4eO(?0%cYMF{F3$izcNup3Z5jN4SMM57weQVu6ZsVrE#I8#7O2_ptnphNvh*3NY&<3)# +$#TXw5;E5c77(VLF|sr%c<^nneF41+rhTL_p{j>qha~K>1wMNxE#7>CRFTeJ_v9Qj_Z;6?pZFiyr_dAtcfqjEqN$;)tD$0Fy&A~!HDt5MmJWo +Wj(qp50Z`R1`o7;ndPZ*o57gMEpt|3?1hnHWVt0x +;oR1hp5b$iQ=o!E+j{-CLTdXhSw;`LE;-NnF_q6%QQ^=od+?*ro@#6TsRxWZA5%6}&cQqZ}Dx&^XOZ* +@zfhHrCX-=zchX8F)S6~I^Ha1jn~!cq8f?g5goYaWNQC4B@`4m^lcU5C_H$iC}=rfg?zm#Tup%B10WV +!-MQRRj-ned{tY%L0UsMFN!t>j7hsq3^#3c005cZo!RD_f-Xl#v|*$wg;xF1;~YyuNd&VA22igasrPL +dIbPssCyG_#6~gQU89M@(sb8t69v4Es_Mtr!Tf_8J}vO!b3uO<%My^UVR_4-t9PW5V3P;%9zwW>TLNO +Zg^nMV);3`k`+%@<$dtRtQ4jRxQb{nhwp4OhoZi*F0#yQ!Vu;_k*8}hRjsmVPx#$BwfO*EKCm(nP(3! +8k-;f2XmHWvSx*q^bBJhyv843elnC?bQVh-(DWys8yz(&`K+Z|A;yN`vUir{JXB>eyc&|yZ+dMqjp{! +OOey!|&oy;)L!S=@UnV%Sb3UB8OJ9thP9U(0+4^%BMh4}7$sH%kS>3ne#t$TZfK2+qi|p#|=9 +_3+C&PO2WZOH<5|X4k2m9bv=_&Gf=1)vZ;pH))R4!GjgU;Qww`^i)9IVJ^o$+sMS5XYN +M|f!foYH5ETyhoa0A0%>QuJ+L+H-nQ@7U1tQn_*eJ&UrKEJnu6eWhdyARHO1`y)By`Rw|89m~~R_fR>4Ftp|yGhy2?;5!c{WLpiNW0~z@H;!6p%b>iVBjj_FtcXkfaKMt;1H41~3Z +-9U2VCZ91XoECL%;`tj;0ZCwdZKQu)+-xENMU)2U9EsT-iC%J$hkGb@IxBUc)-x6EO?Xtpr^1=_0E^s +PfH+SsQYSKX`}wp2a{c#r4NK{0KIcFZU*5l>+~n?`bq>8mq*q+AMUd+5#>Ih2dPG;hF(&xQ43~H}&*s +z5*@`g)}=}7;2!?6*S<&P%G`&Gr(f~#(KKU6-+PR-snfMllF% +q9Q~mqW~sA=hx@1TR^0005&j_DU+{wPmGXvCvxG2aU|FDzSkN4Cv=m%a0nvsRORtoT>XN0VP*aB@TooaC>N67qHzmnY)Fz1-1;o$ +4jVY^N$4w`q6X}E{m0}EjCWmK^-*G@gPQY3c8-?|6+Ba8pOnokEr;bV6R9=tFS0JbbLp|WI$$1nn72+ +qms=b9OdheU6yeTKWoCWZ>c;2k{~3o4YxxOhF}uyU32SAfq?5Zt&3)q<$Aj2KtvEPPxd}v_BVNH06jg~&6o$~QNh~pI4llqhPCYbU?0}bt*^O?Wa|#`4Ljo?>G-=B|1s_A$v-Qxi;10M_^X ++;69VDhzK~WeGhJ4B$^1GOwsU9hOG+Sw?1YD~5B1fNqD>a95U)EXAEX*E%Uih+a>88>*-RH-tn +ig+sps(pCF)hT~led?6HT4rv#X>g|smm$=;R!sRPw9`M%FTGUnyP_4Y6K9TlwZ#TT)r7Rse}QRVOhQs +Z^Mhwu5afTt;TG7R&30;&A~U011{s7#_2kjhtcB0B0vzMh0w^F5LI)AI~gaak}=RU?GLzybDkB2-=_s +Q*1u3C=S_9*7V88$9s_MgS+VtLEiD{LYZ-7IXSn&{YF4cv*m-O~IBFWG1zg8@tLr3o&<0M<&afDDnk# +p@o33^U!VngW+RnWT-3DvtULaLEkHEd1YwM=)L-P-ik8rJ*a(XOvpH0P_&)J0qT(}8-Q!A8_IKW~wRD +WOP=(TKsC)M_!%yJc(69y{;MqqjG^yt9vi>`9Ya$FM7T}SJ+QUT}P^=&vCe;(e>mH~n=G+egQO5*j@s +c{vTepk|g`DN@2FPj_T5K$4uG^KVFmoB=3JeD4Hk5q*>A4cFmtKW#J(FI^0?SUDz(C5d*(S>gy2*(=; +6?QJaBoptMqQQ9C({CAe=>?ZuFX+wGyU$GmuJH`l(lI70L$t3Y0fWWvCFQ +CIs_C-FKJ&n8Eg0MRr)J;C%;>)Sb!4ZQ2f1tyrKcT*7J?X5$$>{5l$)E93dI+(orGy&1uuwFWg#p)JP +7ir3*ET(?1DC9o-Xq-_SYFkZxf)mvvyt9n@X=jJ^_Rg4=hTo(d3DTgK4?fVL3AP0fzq>Z1YxMe%?_%w +3{(Lx!$d^`2uE0yRQccXghSV|fGQ_%9`C_t2Tu76{Okye(g@?jPWljF5A9|_m6K!dI_RL;*4y#O +JQGZy4X-3)B8kI{9wtEgF$~WMGNqy7S4pJ}2DDbC4V0xGVAV+yeL(sRTbxIat4rIbw>o0(=p-iq6%?4h +hva%f;1q&AmE&`8l*#Je4Y8y=Pw0kN#|2Zb$!$}8a(T>fMN{0NCkC4=3)${~=~gD})J?DZ}iEwNC>2! +cSngQN<|uqdF5GciLobhi+RL!xY=(*{_0M>KfN}<1ubwI@m+?+5O10&LugK2k~>Vf!g*QWxJ99Nr@jJj%5!Q~(f!ujpuE*ORyeh!64{?p#E@vHF +L$PPMw7cW_Mo5pYfA0u1rNOI~;UH5g05mGA3<0S{FuTzj@aTX)MaDd1wtt2mM60wy6zY(Y3;L7ue +EjaoBT4*I6CD2*GaYN?QeuJM9uDL?HBzr_eZHXL1Ps-hg<)*DEF8=u??vW4^$`35SgjN-f~KgM}k$;5 +(i)bV}?JO|xQE>!#wBM3uSl-k)I9%{c21T6QikxRu@c$Z71+wURZYRdELdQzgHy$+&E%7skxxcp`^Z6 +nY;;mzqc6;|pevWBz`6!=}eXy_QQ9NnA5C&V#uY=%X3lXrT8U7M;KXj;qRRWv`UuG}dH(k7@x@?$LP! +Q8@6t;~=C7fFt(%gorg%&@+F5O(d^580mmFwR$1+?Yo-yA8S-L#2|aOEbATy-3Zp0c$!i1*5wx!L~X4 +r9(0cmO8a$)@``pPoQ$i+q{e)tHSh32-ru}AsbXPIS)uLJwFXjyVC&#Tr~M1jaRs;WI$dWOJS79MHwq +QL2|(A2K}D*w2?kxcO6^ka!%*{0hLbvqk?3h)||EmQh%vvaw*qwE>SCIp+k@O|Izj?>5U^vx8OK;fxd +(MGrvx$LT@Q2{eb`|G4w2vp?J((B!WT-1XKX2kYdz}=soo8&U)6Pdw0?+X*YK_haC`<$f)i? +$+IUDh`n!8Q*@qJ`1m|lHpmR!FIqfI7mlq*Wlk<2oSzY=jHVL~@|<+%yiv=9^0<0~-a6z%t#9b5W+a-aq{kQ6X^ac|55l(} +iWY^jiOHA+rz!%-4bWYPl{Mj5UgE3)QrRzIZGje31da|eRo1B->kKlhYE8CT89_uKXM)wXm*v{zlpk_ +>;c!P0<^=M*36tD30?g`lH{oMUsJpfM$|+AovfbSqwK>&f*1bL`P&B?*!wXhLFmG%h@75}r5_sr~a}4 +xXo3T|O?R*<)G&=y+I3mi+RVBaq^n{)hIFivMql$^+nrQcL4u?3Yaf-99nNl8iHDGid>I-2c%M=g*x+ +9p|r09wA-@v;|4@+Xt|L&`PC8vw6d>I9O +3mpX@x1>FuA$!sy1bF<8tpa8_olH^I_O02EH?SL@9@n@h$LH$tPE1$p#O4LpVCYkliuDKbP+OjzvIn +R8gvgdOu`WY|Lu2S=~?g6a;o{NfnOI&ln~UYcwH17NurV#N7oXcLfz~Acsw-+SO)@&LQ{S|+=DpL@bU +H2qX2-5-%{FZg8kKq9li?ZowHAUdR{Xt7C|83@{ZoZ+UQg{d_!*zNbL7L`zJ! +JH8F#1xnA}WD^k!c$O*KUoYZ3rB@^*Zcs*g7oJq`$91m9ZuzI{e7NB-3?Yx>; +#3&+7nn2kM;EQL=HHb|>-)u}>gB#fz?FK!2(0w?r7pu5m>n@Wo18iKTWrb5%hp{C30JZ4HtCtI +O_c^XpJ{NZagg7f5`m1KCAi;iMwboyKn+@BRP>OHcv`8tzEgRgzJ?YA7X1~ +3B*8>+Hp8HBeRqz~MmyEVAhYo>=&TRYL($lixdixU%u3xz+1Z0b}?k^}#|6)Plk +bG>Jg43kj4W4BF-Qa1F*RQoNVc5eomWy&sIS&Yg$AlwFLp8#uXxdPzDQ!gD~NPp#fWrL_Hv`Puaux->M8y*HlK#zSs& +o<~0c5+yy<1E3EfL!2Q0idEZ0d@f`_bXEm=G26Qc9(mfoz;obmo@wO70=kGCqTS4a;mFm$a*x|fvfUn +al_M{%Vz}9>mMpH9+>L0}NYh}_=QL$64p#*Lt@0o+I?=KU)Z>>vY{0>VA?R75I&^jsP`ZEFV0`XS9r- +@{z>H`rFaeRwr-Sxo6*Hq2g24+28-?jl|08V9gK$LcwoaGIL|9X&_!w(BY<1yw*^6jK|db6}}+^Q$sC +K=QNq*&Km?%0oCO++7bY5#Z}P#N6mLzWE`f^juIi4gjXn5u{SDbJ-)v=DSkGzqD~kN$44az##vY^l<8 +k*}H4Wyp^*wD}L&JX_e*z1dU^_F_4ZyeBo<0e#oBe^6tR0PwkhpQXWuboHE?K~jy@h(eH&9^<0ASi(1(GvD2>XQsjlZP6M>=*>8I5;F6)3l#EpX7J>CiU3Wp25bK +GV|NU%|EGJg%|&npLZ2qE}hR@xmg9FH5|s#(B2_uy=MJ)W2I2ffdy2%4?G2Zs2%XCK{7Qe(rJ`sqDs7 +A!d{x!>W3^_>&CxiqiNE?b~|PcjD~Qbi*pZh^Q9xo>=;K~L6TPt^_B>M2nJpclc7uQLc^PzOj}RE41V +1~FEQEgv(5pyghm4q$ow%t`?ACS;=c*XvJbpF-h+G^5e}wM*X1Tq40^$_64GftrY +x}+p^Vkg#N%cN#69|BNta5HAGq>j=b?{uW8-hQER5d*f +#0v>qB#y{bnR6g8h@196K78y-mjVd4hyK>1R{xX0n`9KF{Kdl5%f##w2( +TuyG%9yo&i^W+)O28p1@I;1;2n)mc`lNA*LUr9b8#1cV?{FhVIS;{#QuqDmw|w~%Orl2#2aRrUcyV7#2NM%@K6mla!}S)ss*CBG4IIj8N=KU((rj^`!(pc~YF+BNV+FmgC2i^?@x|8 +AgxL{c45|F=%Po^k9`BZ-hcvr{pvKxF{&i$b!J3E`7rYeYh8@Phvz$uaU6^fk$*84nC#}Kft7WBe{)| +3kKWC_U(40|9indAl0x!y`nTaJT?5ou>E&H<@0Omz6N{xt&@ry1B$gihjdl)!mAbGI>SI +NQK#HJitCHeaAC<6iEawtw-l#~c|CR@u;{@kSJHVl*b2g{A$OJmf``Wim)mw=g&IkOa#^Q(#+ +2W`pXv=IXAl^a>mR0lu)nCdU(`yRTFk6#-GR3zt&CAn67J1|KF#*C+y$@Y!>q#Ng)@3S4Z#%f-Tasc~*864mV+R^n##K`UoR5v?JYTcA;d@ +Uh2Xpo^Mhr%ed#2S=p{6kb<5ODrfz3kri{UY0W?!*CUk8x>c{V;X7Sx3ot9QF$AP&7D@KaollhkHK#oHM#T)tRT(!&B!Mt-;H3 +bK{D&3Z@xZK9g2@)4c-CjVMw@z9+H7b5$pby4S(7&vp_W!(ou>EISxV}c8f*syw*&hJ=mZ}6Qtu=#(a +!?**`<@t5igaqlCiJcW&yWZEr$I@xvFf52? +xZo*$z!csGBv^M~|GHSPpPAsn_r->}vuYHmg+3qSZv3(J7s!B#^6%l+CgfBLK_eM9OCA{YX7M=)p>78 +4tSXTK?yHOE25Vr+Z+<(|@G0(0hbhT1j3OO}P*YTtGTP>8y+2uNq>52A$tQI(Jo-5d+GDEkbxBed!1- +(i@dGVC%*zVUWeqvWmbHKN*g-eVi5QIny9$kkacIcaVwNF{uDh*-kC +TMSd_My%n|Bpon0moNr95wv5kTd+9Jpi)d(|d!k3q^p1+mqk>&^kp?J%zEAA`i(>L{?-rSjH!@q$3! +piHn~|62o=KaAAeqFr{PoHc8Kk3f%ui_)8eGRX6VWa5sB +>_0BBtVO#kIj+bR`>FoMjoBb$HwA@!rw!Z!ZuQdWGy%M3^)=iZ?D+HX27NMe6Nc!{>UjU52n_pgrbtz +bF1X{nB9!MPs?jr^6vwF-r?gmIzckl*EJuRb)%q|oQ#CM8h%*RvhzWW*@n({Sn&%co +8z&;4+=FV50=$VPv@Uvec%7<4{^41%-W&J#Z0>)5~(9&-s}JfkTDwiZ|EN*t%d*Lo>V^l0DqwL6_&d? +n_`~_-(bbDgK~z<-M}1w<*z@2IddTvrr0jeOeSLua6e2_Hlwn*W>p_qf7&-Q6>(_$KY_tGujBL{-g(i +uSO~EFO=>{nydGUj{On{|ALg`(gXY7Y#SR8FzPz^Ni8mIf}821PYNRm-gq4{yWZ%0*dgJ-s?uW-=9oI +G)J$G~9*}}fO9qux=DJaZs%eB!K8gp8m-DkLeSz*nDbsaRsiq9H1>jMU*{vVdeU>}j3JX}p0$z;N=DH +-c)xw559Pq&p*+rhJ@_Dn#6X>W?5Mdte-RuQ{ytEX4xcd6VrxX(C4rxdcu92FN-x$F1l#=ROVMX~*!T)2>b13Dh7x0Fb0qToJ}wuy=33 +xv(8e9T-bs0Bf?oosjgnoKxsiqsCEVy&P9|JQ{)DV#v{vGGIdHhK%1!t;2e?TD{(#D5Dw+XHuRSpS`b*I&Bw@xYt}jb4%db8s^b&dNoda&I8lKGM$lfcezbGErIck7+mEtXiU>iNtGCWNIa7!t +hQdZ9@w|;R4e6cCKe1xGxUV<1jOVB@}y5_wFw7>Kb-8&QrthNxPxQ!G49n#w6m4LVOQqQScJ(xB;N}} +>Nh{qQsfQ?RKnOHm|?YtEA{lC?EnM*!3evc&_hOUE>kI*K~ru`-k>z@#*3N{g% +0pSruX);D5vzhyvIcQ%ZGU4?lgyZ&*V}39wU$>ns`&1Kx0^s*&LI)UR<7>4AiDpd6Ti^ALs}a9IHnP^ +b;}FgE!v#aVxQ|_iFqmF0sxa-u!$Y5KO7nw5Kw?$J|&DSj0hJ?gORezbgQEM^K8=WYAcbE~nG9h@rUN +MXd={JBqat +IZq>XTyTYRua2ZnZC?Fg_A+R*%w`joIZ&6BrS@m&ODxv}K;-tjab^fX+Lv14e3|t;S*E^4)S{RkEb>| +@h-RB{hO8r?Mz*BwgV!o&V1RwwB|6NT@)>3+(R(#f#e4dXx0f5fi1L?f%hbpLWTvj*`STqeNy++S}ZG +UjbW3XIl|1zXE3L+H$x-z9~BFI62&b0<}N%JZ~ZLhc%QdSx)Ri+l*AWo&5H~|_9uy}HW +5?+zfW_$tw&mf6_*ES{CI^!L9%Oxu%yj)r3-XhB=|u +<4?;-YuA4l4;am;|_@H8vT&qX_!U;65AB+;I%oSFZ^5yOgXk~BsPBQ;Bec(-gPQV{`+7ibRvQcBT&~} +Tpx*06{(4h)ekBV)_oOPJ--14y@s|nzLI2xZ&QC2h&cQX1!b)pzrT@q$=y>LZm9Y?RDw)4#4%VcxroqZ +$XxJNps~-Vg_{SEJY~tr7a1)x4C1p1^8U}b5gnWzFf>X@?n68e8@gBs;ftK{aPfm`*gw}Fldray3z`y +`bb16?62-tEqH6?l5$_19%D-;a;{9PWFq&290^s=Nw!aKhztUQ=B?2vwFtCPyqE#tR`RU$H20xl3pzA +Q(`PX*NgKuY3H4NROw;3}y+6CACTMDo~?P(XzFYoa$qdn +mDe92Tf%iN%`V0qqpC)^&S2_~b^x&C|AZ^_nZEA)caJ$>$eB(mqErg^&CTZY +n@Sa4B?9?n+_jTF?~>`d^mB-9TJ~51k3FD>8Z4LGD@wY+oaT=i2lrXxUBtbSNOlg6*nV*Mm0bKyLFEKT?$%^Uix`D@_*Z1+i`mCI4t1BXzgX7^P +#nWmJED2(LQJuODxO%@a1ri +_`B$k15@hLhVFbD|AK$bTE`oMCME7fm!c(_z!ZRn)cvN6^NZ&{>UDLbT3E#Lue({Tk=o^K*X@@$_}t_ +!1UId{OST+T@bqFUEX+CA*n^I@CY7s*I6+@TzbgyO_t_Tj +&ICZ*au6e1me_=b(?HyH4;pzucgBm6ID7l>*o+7bIs*kUvS)`y_OD55i|8<~S{@s)x&c*< +w9w7IVmJhi +Ja067&TM^;~QmcE1}lJ?|4V38W9lKK-PYxcG*=K%1A4qA)(i}@1HSM_2);=DtTFGiy5a6I#lW&$hR&M +|UijZe{Lk5vpHep}`NwRRi=IS*1cpotFp6o&8kJ()lPKHy|K1+ +`egMkR;4rW;6BZyuuEEFL>#p|*kGpQA|@HJt`dbUx5Q;By7b;-WW}DTYGyo{639 +JJGGagLv(@*sD*9zP@gN}7^$w(QT)w2m96{Sd9U~9+W9n7vJiw=jzzlx6*A2-0@nDrPB$=>&!=O33T~?Vx+(h)%TPu@H9X=wz(6CII!V*QT`^a*e-v2jMUe!iq{mqHxpRBr +br5GOyTMxRxkR$4g?lSDr=1F*K56^ju9-m>|ie9EhVg(TaCkSw29Dr} +vNX%z*a@-G{#JoZ96LM0h`9qav#pkqo^k9on8l3a1Z4p_yS|fwyO;u7_KjnJceguFIe7vbkLK0d-O2y +=aD+AyR2HP6&T3>ZrH$()0*(8~)p}uBbmQPe9!~?4GXpJ6|sKedF+&B+MXSd0~s?0KQlW=70Z6;F0$Z +IvxgFK`C?F9h7yQD1~aG$D~Hs~9$%Al7l*E;9&oz@NEC0(LIU0_|m^+Jz@tga2$jX~=m+zd&{U?rP9D}4-+ +Ku2nEjO^9Hk1Wp-EKkS7lMXvje>4ENsz}Z%(WVwp2&Apr`Z1YAP_f(10Plp8V@tXL9#ob7P|qm<9Evf +_$*gu;CPRVtR^m6=KIOSn%S7;&I|zYvlywU7iBU1tY<0>4+8>1E6?(hh?< +Na(wv`#0W_8h2^89JW#t6mmXPjQa7Z~K5cmLd=g0yUs`~drAsBWzwj>ADD(E}1k<-?1DXn2&DKnyqM` +zbs5R~6#q52qEs22;pkvb>716}r(WT7VcJbfAdNJ-ht0{RK}hp@*zPBvXqj!?PR)DEQMm7YvTq$|adE}Z$$+dj0_Q9XZotxD!^wFB37;g%$?J^)D@KJ(y-8oGGBW}(b&mS29Vwlf)FTf_|3{vW +npkU$MAYO(-M{kLg~9R@NjmE7DCUHX5rOXX4H$xwpE@Yr+9QB$3f{A4%7l6WfENbMk(Wk$l|k2pkiqk +7Z3XL(`Anw{0|G`Zx3{FA_RHs#4;2pJ4;-~35#LQ2b^x;Z@rI57$u}k8tg(LQ6##7BBE{)(`-ut$GYC +G_Jo#l9K8)`|D$}Xgl`8`Rhxiak+pHLrl4p9F7NwUwHDHy6B>AQKe$pa)F%Qh3>+us1a$LVaP>i(Fu$ +nZhn+2Dhl1h5%FikTeEzrj6K%+HjQww4wnmU4q*%%mXxox5`Jh`KoKzsUboGojCuxz>c(Z=ulxXt~f=`hP2 +2rM$1u7#Q?c87SkGY3i-h&d%GrswKOO)_OO5FO~ +=N+iXUGHGk)p|zkT)Dk19beEnLfq2DI_n>^%Tj9vm^K~FF2u)ncBfZR?>NqX*@PD)*aERN=A+iVLP;JqJlkRU_?Vrij&y5KaGn;AuZw?`%e{}6QeDky%BQ;8vlXsYl2=Mex +2h=5iQCH{IJZpSr5}?yomAeOrHgE>^X?bf16j4XdejH+Tpz+4;1530Gk0ob($q#8NdmcmNHXatZ)S-G +%X3DuN3Sc&S9CU>1#dZ@By}`e%=5H5Q?nWIy$sm02)gv3tj4_lk=Z&e0*Lo3Q!=k-=~S`tY5*9oL*Dr +%fAmXMHWK0tmKQQfAdPQ_O>9|%R^0)+;x?qK$h<$v8FF#$ +sxeRvC2Yu5Jf^T-=0z@&yv9yt(kwj11%(koq9mb4jwHW~p?b>zo>GY{Ni-2B6!mAXBnO59LhNy>zav +O9%7`)M9xWmz2^QHGlY_UIE|_97CApS=9)f6hw^7QR8~yB!~vIR})Db9gzF5s9&j2VgsbcWy?q4F9NC +6&b)8H(r-px4njjB6>H_iYXt{l$sP^op5IX#up+)MNu-(mc&6ki1J8Ddku!Q(R;f0XNfFkAKqJ>w(nY +^Ll*RPLq!8u}Skfqqkuy4c9jlqXu33Q@KoW3_G|}WK(+`JWc^JnVd#EKp^o}-B54H%wK$OJL(`r#nvM +GX)jZjGg4b%BUrJRDB1^jl5UGS4)A>2IBj`0))fZKo+>FSEYV-}0yej$)jNe;u9#16nxw +=}pV&ERtU?}d7Gjy4OHOCQI`_H+tXmsAtZ3%_343UOV_kK}v~7X=+OZILqv_+4WsnWOfQ<@yL>PSFhP +nR;fZ@R(ErEYQWz#Vqr3aRj=v(&H8xv@Em2mlq_^W`uAjnJZxG>X2zrR}Ba>QYkD(k`Q`X&3*8cKu7R +YQbP4zL=WlA8vGh;5qdMNFxlU9!bzsYu*|c`VuqkAFGuo22lpjwnlO-$BWBRun3#l?yJ0{_NiF#-m`B +|t6>8AQ(TkBQYBw3;(2*B}fuU|Ms@IO+h9?9ekQcq)rc5z3;H`6nB$8ejeZ)$cg_S|KX)Af8SK)d1^% +{ZrW-*)zUI1{L1O$@iBwbvj2$mOYNfsSU=Kx%jek*a>WEPSAsyI0+6M_mY +LuF~tRA#?DM=31V^~#LoP8n+zXd^@1q8Lx`^0A~Nf3*4n_~Wu(S|O7Rr~h|1>8}3tSePat@;Urj_ECF +q06=TxX}>~J`$qzATVh8(@6#>Rq#-S5jsvCV4>q`mNg4KFhIxpx%j|uz)BizjP%dZ198(*BRlIrkZu` +hN@9wS2|84dP4>73%h}_cp3R3-$t0rMKzhNo>;ngu1nFa>f0CPgW_JFlbik|v+wBx`;1d}^9$UEDfxs +Yc+uv-(-GS8aaGd%xtOKUiE=1z)^iogW_K*5^JqSLUdm7Hm@gu}UcEQguy18@Qa6J?>@Jw-8fbC1BW@ ++&!a*g&N^L}`9+C+jiz)0{O#0$>!DLLo!kpc48%pkS`+J5-%gRk>FCTV;CPUT+t#9YRa1r1XWOAv~1+ +act>o+gRDpXsXufkm#NJWl*`VSK^H@ytNDp;?Up-{c{TsC7ok5rq2ReIG{ef0B*#q)JnBS)pmTtD7;YD4 +z4l-p@FJ&`-5@rA&wg(9iZ&43*Q)#Nc`$Z#g{3S2*DxFzz7zPco^DI>pZOd2w$PfcdaGi>@)MaP$F1w +p|+1tF!{^nf{Ht%w{d6%QjyBu%cn|Fye?-Fm`CE2`7f8#EauJfQbjnvZ4M-4Gs{B>1vV| +^Xz)!wY^Z=8U2w9fL@2U6G?J(3oESR01E&Hw|kMQyDtumJ%fHxLqJ5K{qk{eq3P#lYKM{|h|MNh|qiVQGa7mOSqXQitR0UID(rM`6qfZIu47KPCrW6T2)!cLGNtY2fUzlYJNcYqb^yGNSLmsg7k`Oplz#4A+LU#)Actqs-~07< +LxZZaGWd*P?i^<`*4FzjtvI7+VcKlps-1bM`t3Q~7N&^qBINF=_|hNh>g7=TMlw{JW_dU0Z^_F2~#gO +vi{2~v#LTW%!P_$2L2H2FJ1WCpHDov- +3`_%xsCP^xBsOLyIb*C5$xYjk^3|9Ws?-BsFl0~D`43=l=E^Ig27*N&3)CXj+f{KzLF?ggK-|NFRoy0sa=Q8I^4E=lR{p +x`zL9TFE&wWlZ0Z>a%kRCiJiqc5%UX)V+l2V)?S9tg|_KPTix3ZEGBnn@Si~6iIvW>L?0V6#Mwr}{?B +v1G<%`+}##wvmkItMx|wEx$Pu3aRas!{ao?3T%h+h +-e)9Hcrz)x1c|~o`jw@QC+WfRHkv-_>aw5a>dWCr{QUvg6L6Pr1PfZRnfUXr +*a)$M-%rciM*?^84i&|lpp)FG70k@N)6{WV3Im%{$G&MNNkv`?F0fKMSN(MKhyr4}qdf+P +yZY5h2~NfjdrI&-5F`2j7w2n+qd*!0p8BxTr17Q-F>~1vb}W{J06%UjT)~8Z<#id7<@U1Zm&NNl8Yo7F$fI0y}lYma +!HZg;BQ|9Q~BGM?&^;T@`68py1W@&#o9i+9T*TWN-i!EZ!wON=6|!aE3fZ<7`ThQ;OYZ@&R?_i;!lQ%Ydjmk-r)vq4^x~1qUl1& +hodaZpjn%{jXB}vtMg3&+W)bj=(>&KdFsIxb4@@Qw-lR;8EGH^Evn7fx2rO#9PU*3x`j$nOR+-*#1rV +#CIclbr*+d6LnoGs51c}JnEGT_CRRcQS)sfur3zx&FZ7mG~kMu7sA3zQuabMNxb&*nHxCMbmoF^`pZA +njdZi+meB!vJ$Qaoq+fsH^L{$dE?5~*?~odr!15*J$JUOjnwsppByM=&5DH0&@!B~kh2tOYvWBe*V`> +eLLDUS1+MBt135=gf%57!WvwnIdvm4~E!#~=u;4&G9>Q-b_uZS!2J7X+QRC}vfv#?Hq +xxF!&IbhQ;-UkyK9j0RVya!ZgD=F7CIwDmVeAjE?GPoBsDMbtw8+$scOO6?UJ9SHrmk(1{4Q{uSjW#^ +_M5IE%ZVz^J#5;kCS&Vbf2XEq}T@$a6hguc7OxRu~)GCBb)&q2v(?uWMshF^!H_>)3M?h=6*wEZeFgCZPV-8Nu*w0*UG>n|M2bM2!Z +^4UZ^&Jox=@C1p=J@x8+4g{9&AOgjBB9V+ry#X@VGN{DIjDhgO8u$&s)``?+%ja=c7wPL@W!(* +RM!X~hSG@2ki+5CF0_wd`_;|2s8abuZgoR{}1zf@G6#eu-0)_GLYl9QP!=H#4J7I<66lsx-O+XJN`y~ +LSyd+}u?=|7FC(hWEVf-TAUb*r^8dE3_!ljmaeC8~(_KTZwU@+i8#y}7C9JY`9!XeY&yG;VA*;HIx`7KEu2Q12VL8|WHf&=#h{&qn<71QL +E%(y#quwp`O@|8x^a$HhTTp*dzAedMzBp~RT@5;HBgtG2Co?0XB|$sWE@>6S9Y220T%9Hd+UuNTD__? +tLolq^(gKdMlj0y{IuEw)Ac52c$(uz%a+QY<+ESYQTp(B;FF;sU+P+FFYZ^MhreQ}y-jDeBqJxo@OqRW0OK@7l9zssj+W;DdWPUjhoEgqCaD`97IY5}$wVGJjhotpRQ~HdAB`j;55o}x +^QM27UkXcaD#yymELa_5kPKwEk{N|90MJ2Fy)q%rW`XdYgfKZK{%bmb4=ZL#lFD*tY27JsnIQ4_i~9F +GL$EwaB;DAi!{$Z`fk7jT(D7~DHF_U59icKeczvCS1c}Icl+DzO3rAbK3IL^+$yg7&s2xyVsj>wlU*m{=MEI1xALJ(7}aG~m_i?u +QQ_JO~JJTUjFdm8q5tT7yAMo?dc$dUoA6t_TLgF){3LR7)oE)%^>C<>4!7$ivrJSul6NfVU2ry|!EXy +E4TK1?U_P3CtHdTbm9HR?)JPR(0F5svOt6WFW2RR)$_PJfPAorLXXsW*I*Eo~5x|BbIm$!zs2r;_wtzWPcq|vsw=cbWm0p7!mCgeHfbeD|QMsq>VIJ`Yw49q +wr$dz2+a1g=E}{ldtovfyXPQWMGU?IyNT3Iei7Cm+cUd)Gup`g`XYJuDmW~2zaHkWB_e;=u7Ljb_URs ++v-E;~wz$TBiTxgO@Kquw`0KVEt=1x^5g03KY1je1ZAEpIKURV$~)bSKD(pnV&xJiE#6_*4l$wXLsO4 +A7dcvQ&^&=aBdr9K?M>{7`?zN6wpMQRp#58hgQBqcefv^=*G0+7q1WF#jqBtmcZlm@i(M{0FCh8Jd@=Jx-Zk=jD+|#}2GcQ-uW|&`+nTBnC60P)KsOsULH6=Rc-6~Xf{X+H0kMB=Tc*G!Tu1tXy`?(!>^K*l@;Kg~F1&}MC+gJd%(+dQfu&%M)Buk;5u4=W|+zij+ZM5QbzW +8~Jm3KYSuWG3_26y!G{pTreTdtPjZcOwDi56aA2&bR^r$Hr$#FI +=lnWJD-~)p`w9-G>_!nd%X_YtUbPZOW(38A0cY<9op#T@BMou$Y +d8w{A^w?G1&vR#U{mHlYQVy4YTpXLKP9QLl^`Gk;G1O`18NK4}77G#?3yYvi}NKHaTdOhd1{-%2e^{Z +{8qNa`!=7lJiESkM3j?yUWNmvbXsDUF9W!Z5{}RK*!lIKl>1pS8o^N$3^XwJ}vOnFV|e=Y=pOi%g_rK +5LoTxIRyLvDtnof)puhMFzAX#9>QL)ALFMh@m@NB^Vshje{K8Wa0J%V&v}Yq+}do8!LfOnPN&&?KB^R +H@K_KS)J<vfdeh6vivmjGolDJapYs!12X1EUf!E=eSV_P~0PQ)L|$kZUHZpC}lVJ(sn+T(}u2RQ +Hgd@N5(zXx(oglP}Ld_Yklx-^Zo6R~u;&YhyKxxvhGR>UnZ4)|c3@m~?*+GS!IuSn|BHpv`%+!&vu|0 +jRfV`MRk*UVgs59#Zya?Lc6WX}J&V)a&X#s}Z0Ev2HJuk2cwQbXOU!zR2dOe{Ks_tT(Q+^clbT@P|OJ +IxWj7w{-y8Zdh)le}vgrZ{5fdL|iu6Ec?emcH6M)?q*V&x(5`48QtiSKQ*6KgYAH!h;L={O5N-2R=NH +%Wh_o-%GX46z?t#=6G(F2=&?dHA2u0yA+SNByew-@@iRa&HvG~MLg3_FTlMPE%A7H5trE^{lpMs +wOmzV1IP=@FwQ +BsAv1n5f*?tUU#`Sva12Ojpu~u)?_)#$k45}tAcO3NukJ_{1OlStCSptsucW-I;?+-_?ao#E?{5>|*U +k$b#qQ*NBq}JV})xQyR*5}#PlNa)`j933Qeah81wIFb4#w7>1NTTf;G +%YTGhk*gHsw4DrH&GyfP(Xd$=CY%lU);6e$8{VDJ7Nt?_Q7KC+ru*JHg@V{&%4NqD=zWc|}A5VF0A-I} +MbJv~ew83YCezw0CD_>sZ@y4Mb5tMs)Q;fZw&_;s7sycaiB%Z@;7||QG#eeneS9fE6$o%*Zg(WI|%rhj(|CQ +N6mQ0HK3hBU~l3ClEl|uzfDiM0*2LCO8yVXK#bxL@^Rk?^0?MA!trI6c~-UoK{#z +@W0QHIJ4P6>TxnS9rZJ@YI?KS6Gxe%1vbd?~lfcqbmrfuwf2Tk8kiL?hs14w0Ylo+pygNw1*x* +>I`|kX`bE21}e;CUCZ~g1r(QSEOQq52U0!IE`ZI2!95sn+ivIkAOUEfX*q=!^-H4OvWw*^bg&GUTQ4^ +n%vJ-*m*L94@UcCX5j1A#%SmIvE^pZoVp6GSi}4+en?fV4D6|LCRJ$E=wC$-XRcy#u~plx9}(#niVT( +2B4w-&S*#L%zzln|SV>&Kd;s?s;yz9dWla;`xjp=_f82ScOq50L~qbxx?4Nx3lEx)am)~I^+KO=B=Li +uNj$C^Ejh|Ght*C&jSOqd!t$y{39Tx6un~1=Hiuw{*}F67pddkS>Ty?Cbq>Ldj2sN_i5HsrT)qT3j&J +*SJQI}rJY_I-y>ft@|RMnK4cehSxj>Vfk9cZ!i&z=i1`{<3raFROG(t#fxx0^t4qRNJ}&ZX!XZ%yR_eUN>vjrz?;_0#~HHf5TJpaMpY&cO`;)2at?jyF>1gFko$hh^_ica@WUBt)9VdOBD_B +qLK^(X^K*yNzCU3_4SBm8#iiN0>7p_^f<&0@GaPOug-G*3A>3T)sIJ6QE1Q!<)}G!9W1lPR8bXgW)i_ +xHv0kRA2Lt4dYe +pF-BlWDRCGYw`lH`JCpm*aBV?Y~ZpRzmqKxBrsblL#halV6E9>z?Z;_Ekjs<~3Q^}pN>0G9_WD37E(s +z$Q63G$-%I|XoEmK){lAfuJ-tq}*2yL)B?1qrW^RBl=_vqn$s^Ta8fGt(Z)&>7{g?XW`AdsSA@jRWRXJ}c522b{5IAHFy?=Q_kM?lA=1xov#_P+_)R!DTm}~YyJRuxiM?(aE +@MHfOI0IyF2i9CYSf3V^jWj#p;?7`l?CpZi7*Wi7KCmulRR$pKJ5uG4wzDPYEnux00y{jeGK{@#79`vPKe8bmJk=5k96<2o!#)MpkvLob7xo-To-xP5> +e@%k$Wc~4`(hCS87VCM)ryp$?kF}h9JN>yfoSgwF7}eLq`hM?IT#=hl?l97%!VeMEfj+<5kP +K{9e3flA$J4|0uuE#X67W*@wkwncHO^l6(xU{I$1veYWw=Kyp;!mIc;96tRlO9sL4Y1_nmf|5JPAGsR +b4n|+Hf>sv`wgLUvDyK*3~XsivdR=FJ0Tc)*Z^K4Pm@jZII|IKYIf&XKm|C( +0Lu_?#y{t?I--l)6`^SMb=H6S1a{;U0CFk9ao3n!pEv+lCLAA%~=t(vJ9SCD}L_)D6-FSpA!Qy#<;d4{A+%yg*iRq}EvfX2a-5R} +*wd+QD%F?X6H752B!ltdl-vuJTuz5*hfK>psPmqEx(vS{z|q@>Bh^MDrVq!{jWY(#6(HO@QBU*KCdGR +o-a#K2t8XW+?dwSiEQo(*j|*0EGa&G~ZeHnXoay6H_FbQ2#VnO09!A2ZEya<0n)LmjNI@DM4=lO-8 +*Pmheq+!IpQs7tq;k+7a4Qsnqi|alN_vX@g5w3>g!OSX42eoO)P*<^y%Dx?IX}y>^3%ZBdBrj#!dZ@e +UX56*`KaJWNZE4UpvMeCTmE1eo(Kew<-*(f=pPanUgV-mc>B`>JQ6@`=zlJ!P50@R!+R$xdAJ4piE!6 +=X}&(Pg|ij8Px&Uk#&0+B%hZ0*gHABz5aB=Ah1Z7n+GxIL%x&wXxhso=vd9n*QHm*fL|ObZ)H5H6VMd +UcbzdE-GY_I(Lo;w|6enzU*VR~CW2u8DzAa;(B^UbkXE*akKqvQVxyaEXZVE$Jl~1*S>wGQ?m~cbonP +Ap`dgUS^Ky3fI;qOO1A#$7pPT|_5+3dWOL1M}N_R`ZO*KmqTpP3L@BnPnQCd@x1PM&5BRHVfyN+zcJOM?VcPF53-#=dSHhUWQhfHoC5x+|HX|MF>cLK}V?%c~q#TT}Ss})=GpnYSekB^XlRfk`GAMQ306C=Y5z|9of*ax;v4I({C>ogn)5QKxju +!j$1U*GFD{7l*SHGscbW5Wls>6CV_7j5HAVz6@OwVi$F`aGM@U(eK!M+BWCD{j&q%$i%HOP>F_%8_O- +;-lB6*{t??WCr*=GXL78c|A0T54I8PNyIM!OS%~H=66xs>y7pM2whAeB>_~*Sk(-yjZKF7ysX^`IWyT +$CIobJ0GarkoDX$Z$&QTbdJcfwoNONMIywY-(lCFjRsNKjlDh&3&xr1^k$jKtaSTcLmtUf@!Hx9`H!K +YM*SFlo%|JbYv*Si@@&t%~XJt`V&RVjd({|Fmq-f6jF+ts-58G&3C5IG6r34sj_~YBdLNCXE!Yl28i9dX-pQ^<;wG-2UR)HGbDT3{8r0 +3C17+Yt)1KrMi;jB2urDUV;5<#?L-No=YhJrU6_$^LQF8C%bYQ<^c9<*pWy4Mawp{(}yL-s_RR}Z>Q0 +Vlf>)VEBPq2LQn~xoXO2wJBacsFa0H$Rh%csulxCelb(8t=5i<$B{uNJsga;+e3v2nSWBM +`L|T%RcXwY4zZ0k*XMg+{X80n=<3s|&8<@dz=L($^=<>Y<$Jy&-RaXI#A%uCyzDxCbl#`Gw!dQA>wOsOFbxPn%D&rfLvvD^Fs)c>ammyu$AT5K7!QPv(gu^DjjGsrFd}&69^_=s){5(CUXG +Pu!j;{n|%D<4si1}qS5q_p|?rGWp58t?Nc>VPYC|_bpEIRrF|@xxydi>gVafHc!L1eOx;*6$Kl;&xR_ +)#6c+>nk4UzV%+=~KHLgO6A$V1^mTBEPgi>wa(xON!0IpRJ^opk5A3-4SNWA}J7>Ms2oq8uw86(W@2E +)O8(g6D^2K*{cP_1s0R~~k(MeiHDy4xPenhsNYD%i|%2gFD=u$bo7s{Q!t=eOnO9Q +Lh3e%0GYbsn+>deKe4KVy0@h0|+hL?|c5Uc1>)d7HueB89%Y2s}^ZWeiMwPsCz+k~P*WK5aAcgp?qRe +UUMm<^j`YYkfQB-`2t&4>=eRA~n~9JZ#*=6sHLqC}^K$XkRLFL~__6B +;+F}PY6Dj;XNM6kU8CB-!3?{9;q7Su&v#hLH#DoE_Kc;CDP}37(J|ca9`N)F6q7`n~Xde`;HcU{C4j} +hujeRCM{`Mx~=4{##!x)>KU*cRy4gr$hF;Q$I!>2Sg +L}mM}^?YEn+>`|A{dEvM5Z7jj&YgUz^OEH?{&e;zbc;Fl_V$F}Oa~a=b^$-ycr?zdBLsi|qh0F)zo&x +B=kT)&z>sU46N+8CLpGNWkS=;Tf?OSy)jY(sT55cpd9os%ijY_2@(1-FtaBy15K1d>Mq-mjkzLSO8ku +!e<`ufQsTqb4m9-1Zqt2lrL)M542#V^N#u^%|<-98r=?VE|NfkzXG*gi`ITCzEF!|Y-X4TR#%|oC;ZM +^PeiGxdV8{+ZR-u?Xwy=3a2*-Iu|9`=uc`Rh9mwl6O9ec^V+Cg^7V3Y#S=D*SKa4JXYW9oJ +8KHMZ)y4BCwj-pS~~Ntn-50)a<1p-#U`igfbkX?q_v`6|}mLpHAm!UDFY?|1xo-^UHKrlSL-@E%JMdO<`HC{6Z0*hoC(NcfEdtauama75n>;~Jx@7Y|5^j2@KO +<7VxrQUj%*GS31-U_a6M^ST8zq9MZ){Ra15uj!H+AeQVpXp7N +Ju3$O=xhEuhtN#C2YQJEz1X<;jRhUi9Nk4G#&lB~J*DyDBGNNExJb20T1tMpzD)1K`imYlKiK>fqPs= +?^^W=R{@;7T(D+t718_}bu=6x6GPeYl;*enfyn4;phq5&5Md#uJ2qe9uy%nQ!@tW1jOuukVIft#wl&H +3PF8wijP+6{u$)D~LlKgubUuKh3g;^EpR}=>Z1sZNedgsh&4$xxT%|M8cKz!-z?`O&HQ3C=(QVvJAml +UbZ+~DyEXiQ#@$BX*O6>;*wHIzbtXJv7wvYUzo<_QEE1s2)^m>>=`FZu+Mw$>a2A<*{?i}c@_VO}*LA +RZQZCEp3ExeLadk?i9hCceHkoqCYxp-lwZcLuCKVNSP5CH5}|1sU&BZJxc_0!adxmI+AR-j?G>Iw2XN +B7;U8gP$RRst1XF&)$6Zb|w9__1gSf>jMcOQhI3HIPf!pz$04zNDP^#=fZ>xTh;BpUVl3K6fW@>%wJc +1vsO{7QKc{a^bk&RsyuALmO-W{!anKK%h5Wx98zk`|NKAy=l_+~DZ!w@B-d;7l{`Uf2d3|g_T>;8}jpWNs(w{_AJ2s{dEpR-d_?W=u~y5E82XqwLSCQOXPDJh!VRI@&-#tR04LD{_BueSjuBRK(%-p#+NF$;|vupOBiHP$L@(dfeZ>-^FIx_=U9KDJZ|RS`-^y}Eu_!%k3J>UHSS( +6^kXtcu&6WId_}fKz#b?PTLz@1&hMb1I)^4NOS40PqS)+z?yqk?fj=bQrYisJ5>`BD+8s8hyS%Q`f;D3-@blcX^(WsbmW9c@04izWu1>JF*YL;T% +bVo-${&#oSb7M5)-~s{@vEtU$RHrpwDsrOkEN-Q(XY~UBz$_!U*CLhLdH<9KwlLtpOFQDMQbTW+Kc&9 +?nGr&i_bo+15Go3vK^IAKeS2O`rPiXE00U|BL09&RVcYYqb*6v=q*+NT_lltct#T2tX~YclqmG;e}F> +)9beNI$M~_JIb1i)AKh#37n1G!SX}Omoc6BzsBk9Pzpfy+iN&+O1E+oxAhBvvvAX6$Cl8~;=v!!yIDR6RPqCI({m{P8hUwNc^1pO%k2dg6Q!eb=L> +EzI+1i>gq?iM}MV&b2{pOLR8Czg*Op-uQIYPyXKf#f&fARmzaAVc+Q<$G;fD(p^D2KGVj#Uc71Oc@Us +gI2OJ8kX*R-n$ySJ*78}ONG6#Y6h#B9gsgSekgC(S;ClO7cPHZwbniHujM@SyTbZx6C-9y#Jx|=J%7X +T(%8m!yQ)Yj=5IOx^6E7En{|r8-MK@9CAVNFPH30*6f8;41wwQw8)rSufd%o6Dry)N-#S5Gi6yFA7<3oIBw`#{h_APVHmn4SgD;4R}@XK;2(j3+_s +gfmt007RZk;sCkFiaWO80UE^4J$X#!gg=}~`Q)FgAB7C)32_cFhM3f%JYXuunbjQOtpk9%8EOT?PPr49vC^)8y(tLm+Nil2E-6G!@;Is?aS*o!<)W6~t^Y- +CTgi_S&rpjh1R~|Q@bAs)C074NhTDNJPDh7c;l}X`fzXNH_Ug)RJQ(O +DU0j`7uFMV$pl6F;rpkq6ie!`ww)Z>SXa(3@Q@Owa*8-sLnp1Qbe13I4Kphc+pPDeQa9i3Xv*-Gz=PY +(z>dWkvI|MYRtA6#70rshGj#ff4GPAtw|L0VvoJReFBkS401uzPT@$|jyR;eaoW%*Qd?E0pUe70sboo +&@q^$_(tXY9zHSSn&&y)GU6;7qff)#wgM#K=7G1tRWfgL{2{clJvm$)2bq)$xK@YFm1P6uVueox$QE`r{$nyguQ6SNKZMM5xD2D +Kfuk5Q04si9Zcmv_)3>Wc=F;?+RtZVLiq&&AK_HBA-vf7|@uI3ysyr{iQn54J@o;TbJKJ801f>67m|v +liY7Q)|m2j8RS+c0g`9oUGPSv6Tq-Qi0WsbSy2`X*aPa**~l$wy*l;8!ALDx-5S$l07led^wVGnRk_T +6N@=j)_W{8@a;g!JJz9P2aFb`Yhs261OG50Yn9)WZc0;M@YeOFFCy{wA^ngL|bkIn&>o_p8y9H_aq%H5^{Pj$Ng +I-@<kzW)Scm5-6@ZVDH1Eal$-MrLsw&AiMj(z8hr3J1&-k@aGk^Rv$mCku_H +u_HPk8zYfSW;WyLrP>VL&nzw&elbK&2)K(6F^8B1pErcYUzx-QyqpK)i%HyF>n1JVKUeL%NV>AU8UWq>quw&k +pp0%oyZqs;+4(Yu5J_5kkV0gv2COplwfJxQ7&bTbYvRMC>}v0bFpRGoY00fDw7Lf+nSYp~`BmZ##8WZ +G0^HgUzdej~{k^9LAZQYu}qcXq#S0JoC*R@!DNIXTtToNyZ&gGnI7eH2Lg+f*KUXNj?6I +p^7@qX5zql_dkn>s#%f~Sn+)2c#vgGn*tIH)7M)Y7H`lOX!0lw!jO1xwqj)MqA@CKe6xVEKMzQoTM5mNSJw8htxhSn`)dmyfAX^@ +m?dDl}ce(Xn>0*L0O{vs>opnCe%{WB-NWRe7IN?%3ZZKfPaG<%Vdm=KH)Lc_YP)dW=)slM?ar+_rlQyK;7$&Obl%e|X8AwVSy=1olsb>k>)FepVaJBC~=yAn)0gdYRl4KY2Q{rU2H`K24%)0Ah +G0pd@jZ=8ezy4qPWEKjw&%jO+tB=E?TlaDd> +4gI^kO!B$!@c{Trda>Dkh!61nwG86>O${mRzF2g6<>9&OZ1M_0YSol+7t@fUVDUOx1&TH`N|7FC_?XM +}u>B3D7Oww5*Q40+#K(D<|rES)>DP3VIL_%Jep}{lvF^2yUfes)oz5dit-yutQhRtpxfSvhd7lN##`@ +5%~9d1i^@RvwTbsnasA**RpdAVdS?p8clQQO$>*l2fe{sGbY^DZz!;TXyzQ%=|23mopL9H$gp8FxX63 ++md#&VN?)EVZLZPZxKJ61zQIv<14hNMDRM0LM{*XIeSLT(a*qLlMN(^-YG$;fr20bro(%ZClm&--8&z +%k>l*pK`f`1UV0_lEOF3;O0sMFCgGsPS_MGXQ^yCUxM2q_rfNR#@+h|zt?m*eGO?KYhy}x^6(|*pLo# +bg$8W409)W%VvFA@FXSl&HA=|#!{tJ4$!w>$X7fwzl#t8b-j?H+oEui+UH-&A(xy)qUwts21qmAx9`rM|ciMZdP&@>vWCl{IUVBBusC;&XuvSO9A9T2+OW;>0wHMJLS-88)$l^zPykF20 +k16GtbsK)amuQLMzMtX4GKQ}!*(Sh8e{IU#ySLX?T$5fzOrhUqzG6B`=kdB5obDFMqokUuqC8Kt$BaQ>5%xNUl-P)l%TxUmx6^yUvEjdN=|8VrjYFt$H +N1^|UIPJ<<RC%pBh>fs~K6-+zLF^zY=o?&=No +839i#NXmN>l5;-Mu|5iCRCVuk3m^05c3smjlR=)F-7p(*xN^#|hO +X``0&!J3OKF^`!k2g~K20DEGi>0+_baG$C#QaWc#&Vj#&G;#Usg_`J#^w08t4GMku9x+@4BmQU&@0^? +sd;tj`CI~;gLWidV1{|og}Y(PLL2)WvZP&Y6eI%mEFBbEKo`(e+Cx|<8j6KQ`@JW|~SJ%i^S1cc^l9E +e4dZE4*<`Twhw`!Q80-)+~`C9#{23)O(&!e|HJYy_IhHlB_w)O*V%Uj;@4$md$KX~vHK=h^^8GakXrf`5P*2+S5Q-RqY01z3*f!_!=}SQyrJNM +=_P%;?+cP_!-;FJ5BY4Lhi3@bd1m3BVWEt^0fbarK8 +|Q>9+UC+pYB%ZNKL3|+$$SP^JR^7+ALeUkMg8^gAro=W~|Lb +!?k9QY5|dg{74IGQUaD5)(I#tHau!XG_Bx#Wf3t;$(xjRXgpz%wt9X+!YY?*o5n6H|yvdY^1Ig8Cqt@$DaRa5V`8Qw^4T)nc#k1kp_6CrbSs>5I8ia$nLcXjvwqoc5QNh=jGK1 +2(h($wXj8L@6w7sik?fnDAGlw!K;T7RUe-AlyD6AzXm;W9IT9_9P9(f>MVP-G0`}i{&vQufd6Z!x^+z(z4?(5*bM_5@X>#=UhY!E%BGjFwb93zw!vPwCchxiJC`8 +^}r2p6mqZ-eZa?%3MzO+{g;X@gndgp3joqF&*xIVJR@~vK7T$PPffYoE8hmvk@3BZ}w@dkk(YAy?C!faI5@51jn6C7NUdr?>Lrj>1w>FUJ3qwvJMo>2$oo4tux*IzJSgq(fG?n3o;|gl0{bT +}&!m@3!{--rH?lpNX0aUZVl+hc_L(gEKXiQoUZ7+a>K^m*07A-?_bpcW`FTJMx^?PXYw>lQt0b5%s4& +Le;7M5_E|1id@Zwybtu#OckZ8x@3W`!NjA`_VA9*^WTh8 +{sF}K6mE(Edq;S~_cz@{i-AM1V)J4%VTdTF*fM-Jd#+H~?>(XV=6_(CyX+I+nFi2v)V%? +ICHqj3Hs`_6n}39PTwHuk2?5Lgm;ogcZ<}NfT2T3w51Uh3$zpCPg@*jMMz6uNTxe)vWguZ; +5p$9&!BGF7mNPK%Wv8Wn7X9u%fKOcafiT9UBl<5dz`F$J4axxoMm-d)agAPC>aFz{&+cCY1{qtdg)&f3@l7UXFXt`9K-CecQ-_mC4>dE#9xb5_8whnv*M<@PYfKfH^)v4UX#ng<@M+AF2O1|{MS{JE&-3=x`%|rK9H(@DVl$T^s% +>|^avBRH(45vb|9nhy+L!d4?&SPZ)zX7HUIHhU0<(%DD85xS07YP@~jdBr;4Imh2AoF4L&-6EcomDG+ +(?RuqUjeLs%&9vP7_PvIOsKI4QP+-h=((x+fH0*IO(fi49okLDU`leVIW~&T5*6bj)_ZCuR21d}@SQk +EsgzhhKE^9i2cz>_(&IBHkuH(E}llH9d;D*G3YmrUxiTKfkJyH7S}o+93{X?x+B`{v-&g)8xRn3o}Qy +V?8m>7Epm)ZYxBy7H+Q4c=KT{gO?MywbfMo8J&9SC8ED&+GTQ!JWvtOPqUF%bK%1)`1ca6^(9n(bMP% +C8yJ@q>SO$>vnDi;OzIo8rJOg6h=`(?gS07V#)?XMZ#|Ja?cF(s2Y%(@hq);~3PVZ(qwFzNmr$D?27Y&mH#px +HQyeO<$bT1|e%W_oidFn +nQeU7VM-PKQy|MPKu@^>Pxa-LjU?1qv|Og)4yYp~-!4RLM0POgzpk=UbbrZ%`wycd;Q{Py!v(oQOJAPCRI6!~ +r)ai%-8ufnT;lK$Y^}8yYu&GZw06=$sI7w!>o1q#|I6CDEVqp{>w@F|Ef9O)aK$NkOCl+fn&B$R)~!T +ZF4qhqVG$(~P=TZ@`U%d7I4{wo9(44myGOl8XHWZG_A7K|u3IL>3rIVn9OVyQxkv&FYvuLJj@T~i+U0 +&nka~uT!MM~vSpZ!?WT+qPMv86AxmV`Lyvn|$FBnR9KvtsbpXUoYjQv^=(KLKTZQGdqP{fadteD^D!> +lyGBjnyK7|*l$ldHZfs8xVQ#UfO*(^^VIL3O{J&8KGXBjENv_pJg+B~r%~1|mMM_pavIL$)YkHx+=tt +iiE3!M0ihFrMo$;i4!Ox^_kZS4mTSxKux+0XN|1?5sdq6t&d+0NbqB++BmF>^}97gPx;J0tMd22|*@0 +u75xT(qFt~UkKVFHS7|8)J%m#17B1u^uDKx{fvPn^$sL +>POG6oMwhgsLDmlH6)(dFJx9Qu7Fm@(mW5h!8o1Nn3I(F1mgSE3MIt$o^1Qz&hQDD*$*%I?G~}E4r|X +-$D30#$<2wKch&+HE;Qrl39Q4lO-Um;sML{TR%)4@tj&*>~oo95_TZ-_>b1{fS<=nbN&Oo-)^ilU7># +mUs@YGagaf3`-x3_#6M-t<$(B +*ZAu0a4^M*_(Fd;iU|X>TQF=3*3L}^91P{X)Uz*C4a8CVkouqbO@HqP9F~v{9xI5IIZB|HNQJL5;=8SPU`4bWaVyC17 +LX97ab_H0sqfj05!NFENpyEleEy=f&yaMz7Nr!osQ;9E`X>h@|(AQ$EGjwljp25c?b$D&1{n(`*E*d&IhA- +@{l3$2zdyHL+nKHrdCr;V3Qy91HLRDtIR%aLaB8zzunh$F@l=QrbQ-s$@4nO)x)SfW+W2CY8m?ar(hdO?qN^SfDOJOHI2<+JcCz%c$Zv6ur?I%5Xk_hlCg=5u- +`tWlbk-!0Q2eQB_Ro$&j^;t)bLMQAa-fzhyu}_omU_eboVb61983${WZ$|rWDb1G`GM*l&;NVk7H5My +VHh}h=cel>|uh#zyMDn`z9o(zWAWUBs1M}33$wPg-x2xdyB$}>@2cA76b!%m3TbNzvb8{2Ezy%ymj#< +N5TrG&G0I#Qmmj?JEKBoEcd;lCmmKrmQF$rXpy)*1EhGS(d84fvDoY3vUg}A_2#_h}#%D=*4^KVxlO)m0BJZ=A*+(pS{dbev9}{R>7~WUqSug;cEE9F3YZB5JfdiO()mn +}MI4VV$IV;55{p!e+O-oDXuh^&whk&Z!C9$(#+-aD>^a>6n0LkhDVFvL|_aN`Jk$ys%A@&i(7Nzs_$y +dw6x}P;(7u#22yW_04lue8qi0038@qW4o^TYLSg~rIrP&0(45wcaL)Llr2=wrUsVM=Xd!7?YVtl7BfT +TL4Xr`xbgOY{%i7(|H$DrbpZXD#-XT2N9_5L+7{o#8U^4H^5Xjg3evqdZAm#A720#EfbpDCP$3B<*iMlMvvmC`@sSIMyO?ldtc@1{$2D3adEYhN>t_t8F +GPFLz0v^b8~Ml;!i$}I6WAj1TmT%_Y0Dl)aU36Y508+an}COi=(=-b6q8sinh8YNcxF?02rQS-`=|b~ +PzCQnzkQX!3=Bke_EkRoo=s)eWTN=GcO!va|1>xX`1- +2Vt2ZR0yj?d>g@y-vkuj|YSkuaD0IJdDNhe +$rlnKx>b8;3~!%GpW%r#V>G%EHmWd;e#i!e{)piXVt3IN2pk6C(91e~-5r8qY$d}ru^eHVRPKn;Kji1pML!@FAP2scRaXK5*2Frj7*oH`*x?;h4AQahf^Uq3w&L>q#{t45{@$t1hi{f5Yf7 +x;Vr!V?iNyXy6_p+27hc!X%2q&z*R2&?&sTv9N=M|>~~59GqVeo{v?tlBHNiDuzdq#=g3i0w=O`JsAe^b9WcgFA|R9U3BfHEG)l*2WMJsIIyC{6-Y+nCEEDH$09f +EDt#TRZmI&9^^wSOm!5yuXW#&kX(B^|yTN|zyd`&`P2V4eioSH4hjNo59HsM@yzr}{oEzW~dZp3Fr-! +^I^>Lz!PZiL%HdAI>4Z$%N_s{>DgR(i&z*9(%4B9onb5m5*Zcb37;k>}-HhWBMriBGdJ1(;9b+@hjR! +b^kuQ_br5*TZzbt3q8&li-BNbXnzjC1zArA1z<>^FyOt1UCm=dg_%HV9`~q%FZ=Veo`9P6 +>-un#XTX>nZ|SOlY?2FU(_#V-9W}O7nav8iMep3Uczl3(ZPV_KzD1ZjXOiCkWLCx@Vm{hSNwM0Ty+Es-fi#z#)z={v^glE4x*ksoRy +9<1;O__b!)02`cGz*n^O@_=NxlgIz$c&YVO{uivf52-{e5|^2g%4&4DJ +AE>>qPz!XhU%J(IoX6baH4O#7m(HB?Od!X?qOB2!}o#O2QOUMCLE3OJST&sA+oLGMI{7HrtN;F+t8XN +HbXEVczNx*xr;_|;OYe;etwnf?8E(+F3M<_Qn+JnP$(ec30cL9x{X);Pnh&C`*RKE@;9}b6e$;?EdDmFM<4>iBK$hqx8FVuOV3HUPE#>Zq1a%f +=-4MT~KAQ@C?G`Mfq;CG%qMAhEK7lUD-ZbV(qW*00U7Tp6-YJiQ4)U@H86h(Z_7_4pICs +~{e(Am&DI>BFj*WpW$`SV7^6?SR$oHQ={l+yLC4b~PWWk_ppteH|mQ70e^Ra5c=E- +4h!*e`K(^~`4*c>886uZq7QFwRml@#iXYi$S@n{Ng_H7?Vs@%>7?62hp5JpaR?m!JIgc}yovH!LRbBPdIxgQV&%c|TB +6Cd9#dYVLn!u`?m+yL)U|^J!+p!Bbz(r48wXIoIUHGc+;@Ed}p1L7?O5GTjqmHD!gKRnl{m%hmLn#SI +sGCj(BiL9_zv6{a>Z(?|)_H5xTvmyUY?TE9wT=AOfBbioa`|8X@jpy&9qS@dGq|*MYPoPbA5XI@4Lm} +i$-ULx+D#G3+;sZ1TY|pu)4UkZi$1*d{nY(xb=6as^L1(nOm*Dg0j1GA4b#~R-f|aE!qj!VYKu7d8_^ +PV-+qx+S)r1LG;paz;wJ>rPRasE@Pvh*Dz}B%+9K|~@oxJ&xl7uC$Pn6)#vUSF<8kK~&@_d}<=AFV;Z +ydCnVXc7V8HrZOtuIU!JD|PHxCKKk?dPJ9nHaZiwo95L|;am&QyA=+i_?>Fl+wmV~B;eaVa5eoy>oQwQOr&;XL@fmb*0U0uHc(;R+H504rhuo|I1dFs!f+=r*e+GFCXtTL=_Axb63(rm77`GWR@@Gtx9Raw^B)nI +!VWW`GkIv8r#sQ_csigH;k>>Prry02c_ITA7CtvAmIqer?O-J!_x +e8;^Xc*uP*>L)4KN31Vc79EAB%E$uumS5>Mtwix$dE>FL`vW$gq-QMg4~7z6@_DewDrwqduum1h}9*g +2W=|G(@RQ-ApzR>-u|%3F4jc^C&etA_1`@Dv_pO#oNWe>&U)5{!I^dkI@%Q%>9@D;pT%!*rN-!dP55~ ++rQ9mhxs^L-(Se>kt;%01B9i?etQ(|?=9=d&eAnhn9hmF@Z0`Uo?woWJzTLj?kJmS6WWyi=&Kvzk>+{ +PZ1!mV15EunnsvAd8dZ-jDr0ZmCa9)4c72*7zxTD*kFPZFG#c<C>F|K7;vF5Xy(n%H$U}u5WwbCa;WGBzavERqDuQe;?C_e5{*>D_~lPi;=4 +OI%=W7o8oH{Nuv7Z71DM$e~RIP=8*ZEpjJ#LX*JVrJiqi)Iw@_C1i`w67K+2BHwO +cvl;DQ)Fgu{Uv$OxskMeKh^1A?fTvyOsKF9gvXa=u80gn*w1~xGpY0LLaNd(a-sA<%8Vo6d~4btHvEh +Jd-0d^A6pkfy_q$|DLcUd}~WFb;!|D%DY5P7?{x3Q#k+&zK=;HM)^FiZEi{?hKsu8DV`-(99p*_SMTB +;X4ML*%yH_CYtFBCp^@0pNpYAS{RS1^x=(_etGLE#3K*Z9r*8UiBVa_pBcSfsBZBTcRc> +9HmG-KZXtp?zHqyL{8=Q#(gBh14l$*X;gH@XSsq +Wo)S?b8I@q><( +jlk1DPsm*zcF3AiiYpbV_8n_!mzBUSL-U^pfb*VkImqSAxs@`VX_H3B0vCeMzW+?>Ia_N(^;CyaP)yC +x59))EJ(6Holw>vciA^LU0D&Mp}Y5OSlR2l0mcLCb0dFHs^KHL?@@+rXWejeloEkNzWfFpdZz-btKNkr%$u$C3Ixs57SAgt8yUu8Vwwd@tpn^448nese;M?xtJD__bRrDx^!)3*JuLNnqO +dE6MU7;C0&rIj<}vPBh=h4(@#cNVQ~MB&J`q8zS~bf-TWs;$V=^Gn>F&rz2~r&q=GtQD1s&1nkn9n8`dfy|Frk>B?%I3Q$vCNQ?ap3&!rEVJ1x2t+oEY{Nyuy!CP2TN2i;aS1?4 +II5kp0;AilU#f8jv8a8n>oE=z@zAk!&@`E~!2U$oJQLp8q!>^bTLe+;)iq8Un1tusT$R-{)A`g2xP0p +HaH)#m5x!3jn@Kh{z#}xyy-a-Q4YsevBGInzJpNZLm2m)NeTu6pWnzKY +L~rfLcec0)r18p}<7>T623AuJWp0Yj5-BqiDI6MqixEntugll%631kI=fHIY{#W%i +FB}W$hFMIPrayK28PDW5PX)`#_Ytj&?F-!nRZe=2evzGXdCb?ufGg-h47a4d5FL(!f(lh;X`^5Ny~gA +3kRAK0CnQZVb#1Sov7k6_rbHo`25^l~=EUXDc1}Ru}rh_U_-Rx(Li`w!maL2OsW9BoK_%dGBlQuHSRO +LuB@07~J{kE;DKx-nOt8-dxYsJs{v2v+5lvccs0ebGoXC{0sA@1MvI{w +iBo#N;CM=e3IJ+-w33id-7Zs#z0V#HnMVd6W#oIJA#)q{zn0i5M#rWrv-_-h)Vg2q#^-26AkbPu~)&P +0)?y^_1Jsbj%f(*mUVh89N`p#&x~-rlcR5IdhK{>G%NW^)5PLZGIO^5?ZnA`xo +zUXccvL-aAfy}P(RqgLPo@Q;lS1!GJN8x^}8?BHgG;Q?>hmBaP}O9o$H3s$((U}O7WzhgsIMZ@Md8ZS +QOGpveMz$28A8W6RFujWfe5m@Da&|tOy0R*GnN(!PAK{4P@@`;UeYG8iDiEtq7qIX1XQK_@ZE`XsTtM +fIVPb{q;f;GkCQAhaW5_n5&l*ukWyt@8Nc4cPgO?Y|pIZ{4P4Xj)4{h5FBDYo!8Ezj>{oqG=&%MlMM! +awVqZ^4p+82Q43Z>bWn`=LwfTuqtaRGw&wJ8{$V2&KZj*~*PV;gPLfyB>u?r@g=<&pdSkLX0FL5!T%L +4oAYnv0W3765(Rm#Z?p^if5EZ)jS_&0_b5;xc4W-?91Jb)5476z&*wINO+rajIhr#MZjv`4s&(w!4fy +c58JeCV5on%yLlbAME}0tD6O7AH~BjiK#vSs`|U=t+$5uO*Xjp=X#8VT>9{Srp21ljR*PqfMXB~5ozr8{bfx%~gS}ji^Lq}wd-e6)ck2qe%=q%+>ZT{bv>NM?JWI}>ji>%tne_xbLNwBs92 +8q!XfdW=?&69?0nXxZaeaN`0#GsYWH3RdLR4`Ze+vNd|Nv6kOMEw(5&;~ +>-g^MrWfDcUxcxfZ#uSX*xD8i$~17m4Q#p0zifiKg2zEGbt6w)+l*?06sO3gYJp}})dfXLqRpyoW>s2 +DJpokxvl`lQb=80Rpxb3+qyz9Qa)Yf2byu<@jDQD)LTS5Xhb!@tnMu) +L8HYxIM4O^U!E~MQ0M7djLF#=oBQ0rU-~)wRbvJMG`jS&|X}7@WL$Ii~rX+2USRKl?p5|MLzem7*8v| +n-8AmQ`L1t1KWdxhPQ;~RznX@I;Y*nE~j16mA%}a7N4IO@BlGwJ3$83{ZtjCA#Vs5UO7t5;X>fevG_a +5MI!do``n8!^J4yLfTxgAA(B`$q1>nN8K!8lx~9u)Sn6`B$-hFqPpON-yYz1TYef^<^C-!RVOCwur?C +Dt@Dv&|)xrt7@>n!d(xD1;Yw#wn3OAfRFPgv}T3^xyB^pinVz(E!eE+5+l)4$)>bF3^h=@# +W0^f>l_#Pv5vi^{|Q~bu!iC>0#74h;tby2Ku6sIFE~=8<1T2yQiiZPua%%ZGF4LX$2?oiLz7`l$TjTS +{0sw@r#&xqm8$|O0{v#~gf2DvIUTzS;^+1Cl}gYak5}mvoErK~S3qtOx1M-Jv8#Xk{)mOqq#zjJ*=`i +-ryU)_n=0su*V=MwC{O9Q2(BccbXhfyhq$lx>(vA8%K{G(oWEGxQdU}u$D}UPvbfDk`Zf&E$Rp84y7o +S*M0qB!KLs^x5zJ>)dPBfdh&n5fgmn6C^e^D>rf}tajA(bD1qMKO-d5|4^j+~NS~3}RD_@wB3F_xKC; +@&4ww;Pfg7;}&K}m*#WJ`xl?jbc=oHI4jqqJ1V9W5K1tGmoqs`GSh*6lp@MESHZf*iPk?gUxrQ4Y9Kj ++SdYo=@_k1Z_kBB>%Fb?Qsx}l!i+!Qq7@aF1qZ$l^c_ttyL#bWJjO8As)N=>#u*gkSIdoDP*5yPk=A^ +cv>2#z5=4I$VzmUTO6Sh;94A!N-#>7$pyUSpV-~$CpxwSBz@0RrymVi-yim&mgC3n#db2g?Ndp^0LrnWTd*zIl7bd;<4+=V)*0{A3MGc- +n@_Oal}v?)X%+5(hu`*#d81REvR5s&z+SpY<^v9G^P5~^-bUfCIwKcuy;-;{z`0jQmh^H!$M +MtOtOKrHEklg}`w4eeWPXM_+k%Zj>F5nce%5QiUln6YFNF%Vd4OX)2wi{*Fs=@B{!L +1LlZCQksx)tZT+{^%`H-h<>)c7{>=`sJ(aTxl&3-ahA@CdCWWSnYo9%bBI=#AqO;ZjNF$P%Kpk^~FQ# +2SD9XQ9Wpoz`+=X3M1LxQ +c|tIhlV)G^&jk3L59DO|u3=jwy0_`L94{rg0# +eC`(v&OW)1M(>vR9Nwe|saZ0hm^k{q=X4CBlU@Cbo!wdO2mHnRec}%BCYzc}2k4UdF4Vq{74fj?|<0G~&*a0(O2_QvD3nHv>FGIV^~&*Gp-CJ +Ca%7o2nIY`0=r5zWG;Hl~Fc*nw8H2xZ7~t1T|s5n|<6CpVdo;?j%*9_u(1Ce`+izUF^T)&jRQ%NsFi7 +IvP0u%E7j(bvcT5#JhV1`=ZTV(!l;;7|ORQ|MD-Ykio972t0#KU|q@Zhwu^JxYxN-Hh6M7U0wSoBE?3 +x-r7XIiD17~cI=B*;Psw~Zj-5VMsw$g%u)5zVgBpM4lPnd$3}SZXS#*^Tv((Qk*nZ9;kB|rO0QR +3@j=u>FA(@wHpFt?JRBSbBdIBrJeFIvhI}Sy{P<-{--U9?&b@$ImAQFpFQw>isL)onsbJZp;8**avsg +ZmZ>JarW;4QExs()FvGm^Qt +IOMzzME{1NJK`_XQdUO?a1;>^wk5Zxs&(Rtn@@AncnBhAM_-_drgNe-AUL#t5t6cfT0ah1W~0D7!Ct9 +w%OkAy=@!|C(bV0e?yW7U(MmUrB`V&|CVatDTH;MZ(Q9V-Hvxm3q!GnC`V5y>~(W3fh{Id0-U2@V?$U +R-V}{+d(Y`Wv6*l{15X6Kb5NqsBw7^dB-g-GNV!5AZNd0;T~-hI>|!=`z`IZ{N@J;DW}g?o(LFudcR_ +SzhtGV9ynYYpi8sJ|uzvZ;e2nQP$fG!0X~M#Ey!Fjia}L{hb@%+{p%C3>JT#)yL;vS^O4KYzVf}3rhV +vd=yS9#1Gw_7n^Mt4UCZRCatZ{(CI15c6n}P47Ab>1q7vD{-Zyuf|je473^GLL}=@WxE*oqg$&;eFB5 +xc^P*w)j~FBqhyp0J(N{A}4Bv{%CMGD#KDv;q9gXD3WkwuIhgGX+r|S?!I|w0O +@g-Ltf!?x(~`Ss@W`DgzPml|DNnbn3ugIaDuL29z6$@5@iJV^x>2GslFg?*tU +0IOEl5jayFem=$g$;($xSV8IU(kA +;bPh7_=33url>|!3EyGjdNsk8P87q_mV6@s-IjuNz?zmFI-ea&ya<>QF~9-((1 +Uy2MPs%3vg?u(k{*foWkT&FMkrYNwKpvk0c-0w1<&BmEup=qLlM`&vg1v^)hb=$)GKR{oa1#e@J6{xM +VjBh+@9v2)$wjrghao|SU$lqmJq@_Ob6RLq#G>zvDK5*rG<{QPI{+`o%2!oC9sh* +m6)h?PNq&(iU1q`LeRo3<%TFtI>wG#X{k)tJXA}Yt(Tb*jDBBVxM!ly6rADDQof+Z!>Pjd3j +XPjlCzAPiYqk;y@NA{q?0Ia6Ukq4srQ4g=5%iuvec-jEQPrmp<3JDMA+@{mX)b`xp1>A{e75-_RSqZ( +JmZeix4{?9GW{sHfV)g-SO`3YXk>8e3ue8?^owrKPQW9?-(+l_%coQ3`wDiv +{Qid@1FrsN|fQkPgZRL#(gBO24!cbMzC+?Q%OG&2#I6A>{ouoUoP~xjM8u4 +H1HJ4DmUC}3fFf%W*Y(!Z@wD*6P-To99?#&bK?urz>TsRz(wB=dZY6B857zOc!-eoT#@fv3CBF4zYZ8 +(Qv#mK1_T>yic8X)!0tx*6hF+B?{jZa&OOZW^QfD`Gl~Bz5F7yV +G&Ge0ZJG0bh3y53lKAH>SjF>zqDL#dZQFGV5aR*TzZS3A*qA>A-nu`M?M$V#NXp +P`}d&cV99WWC1(yl?W-4J;YPuU`0QtkBBwau2*JRuJs>J5^)A?(-VxNF@i@m6H82w?0m%U8K +Xm-e3Y+ +RK8>uph8r>$Vd@((~GxN?Z6;8MTX8wX;11zpZxL8+@UOlr13rZ~&WcQr-S{-q5UaMH$7?&YvFxdI +0Cs<9r)-y@ZFk-fm06u=gG=##5+BBFlVeKwuW=^^(Jq}65)$3@vtXu37(bvp5B;Pdl#oITA|7 +}NqiF5WZ65qpuu`^6Z#heAEtEwtP{65*CD0SXw+ndP=Qe!9nC0PJ@^x?qO3cX@L_Ag-As*lmirv0V|m +0H~(S07rAGC4!*l8FD9l`Cf{^WsyoH2Fv`-}Z +RPHN5;Hug@H>iVZamph)P`9t|C!OcRCjG~%P7(3K6qq>?;)4L2FRW(q+(`ZB{yUHEu$_7hyO)hNd5!P +oJL5cXZ;R_{-ibeaAbnGfe5slYI2)ju=iZVU4{i`AzI9am1k?HmTW+QVv{8V0{Qoi4OeO{)tgOZLdMF +QDY?FrV{-TdLf0uNDE*-rx*UXER4uRqG1+T&{ADfIj@GqsbxY`Jt+j)x6&2;&#DV5O5zOGh=6fqcm3zCVS+ojxgqW8BhVuhrTcE@5h(f# +?`jIa~chs*(dv{!2J~i<%ZXQ{$Zhu!S8lcra$w1C;@wghSZES)gKyC&nA`jN@e~#ahoZ{7WnN9&HLCG +(2`@vjEXTQZA9E+Q-X|-utTaInkwd9?$ud-DN*9ScxPcwf=rYg3^BNu>$huy~!vKq`$1j8U +u-_wX?*IpV5*{b2eP)o#b>bXKDEhHdjK;JH}#rgu-nX3ekH6+!Ro{ +Hg3^)k`@fp7N*iz#ua?=D~}nRD!88f&*W?W@l-UUc3t(@DPbNB@lV59)9R_8Dc}*JcB%Q3{iOD3tMLnud+hYWx4A*)sv-PAwo=y(eNpdEACU^6 +M+G|-xP_r`4qjr7csY1NH;e@?Qx7AV@M35|-XPe2%_sB8E#@8M8h8rLSdXTE=9&7`NL0ur`E)TAKo9c +yQhG@jsRa7efPtLbAf8U)*X8LLKHmZLZ3Z(IiVb7gQL^?9EZHWn@~T8mI0^Iy8Vh^4#`B14yVAl$_^U +S!jShteq-LEfNb#+2TWL~sB(ml%u6up|F3RS!=|BTdq0tz_9447MYAm&!tna9dTB6^ +1KhA?&0Hlly5+dg#z@pXn)TD%0T0l3Wge*UCyo}pggfw;TBZtkfIhgPZI|D+NJ%$`-H3o^(5m)nJWdx;+Bixm3Qme*a!0{q!`n|X5D2}J#O7 +Qv21oVUEbg08Nw*x +`B^#oZlcc$m~ELxHwBS_|6B?1yUDwRap9gPWDM^<`-o59F<)5;J0OHVl~tCCEd=J^Dx7MRX0&|8c`vi +%v{@IoWq-^*1zWW5ngR8>vXDYi8Nm$P$+azGJnjg`10990U`4~pp@FB+!(8PiM43`3XK;Q**>YN%Jw# +%pOl1+F@_S}eH@fXm_@S^kUg&{0n|ncr&x_*@FQdp=BKf1Ucgq?Bdgm2m=J5kzHW})m +1pKWs7K4_E@D8dVoC*bgNTziXkT*3Hib^zo{Go8>sA=P!`8Bg%pmo$MiO4&Ca^8Gk +zjsoP5yrzek6)<`-V-(0QM7LXmb41X}3v3R5#VwtU!Vre`i%bG$ICY5n2gJQ#9}CHymdT0LEZLgHOaA +{xvB_IH|wGV>=N`Yab72(iJ4-b?$6eIQRE`qTQw3R;>gl^UMJc(Bm9Eal_2J%f{v3S@oa)ry=$H&;JX +j^rjzD^Go8mcKsvx%d_)5ad#6o_&1HcV(-IL=cHpuF +KPaFX2lFv+X=R3~x@!1Ssl7No`fXY$IcQ?dWNS+KRP$ApO9Kz-@Y5pa=L7 +;IylCS|AGCSEcoC098V}OQppT-p^&SMj_Bft|ErR33RYcKVMM0xhc=ZA%rc*)2VmVpY!bya`^fw9acmpBBH*h{sn*DMcY4U9DbKg#`P2fB5R4s}I=e?L#8->|fNic7-+=?D +J~xq>mM3IxeW=|PHIj9F#31U6Aa_5{i?NR0Sg^bhU^i*)?5b-b7cEa3zrD08jRQTBYwQ?8d?;Ug>C^# +5ZrDUhS)MDyDEu!3pZ}8gxrwZ=++Vdym!m=c3Ew!2Y?@Bhn%6)c1Gia`q6y9_44S8ui&Mw8fuTpIN1{ +Y@Q0RkB19Q+>o0nu-(YoBD^(jYhJqpi8$cS0+jEet@bYcP}kQS!CLC6 +S8-Uf6P^fi +CZRqurBjPW+vtD8C#e$eyzhc$n1AhL(=OE`2SU?Y=de$26=z7xPzZR^J-6Sa0xyg)SYT}a?6M`F|E$g +aA&PEA!)xK^Iy@{f5swCxEIxM8jjh6*la7%{Lw#SilAC99@o^*A>TzO1C5XYu-46y +OzHORXVl6LzL70uiov{%iN+?z92J`_2u7T)yf=<$uYVC6z~X9o~(T$3213xu26W$Ov2jPObfk(_c>Ty +bii(|t5x^=7om=SFc&ReU03h*wnoUz>bPyjpfJO$H=>?%2#{`jv=jj{V!r!1#S|A56=52%u4LXw2ds4 +%EUKw0?9*V`I=@lWb4=WN`Fr2<>C_s12)G))f(gmZVV(c}$BUlzgdlhy`Ii^|^=|{V)su(~1l->A)T2 +%n@C+hfXcN}A%&6F~FB~5U4y!O7=dTjjk;g(G`sw2f=i@3|2}VpN!0-ICx$`KqZ=C_T8Q56jx~#AHfG +96ni|6zdUnLN02_(2HjpDM32Y>;l036F+pv~wmdmg6?0j$1(Ox!0mpJ7K-?O)}?96nj?yc#UO5au_-f +z8QUvq@?Dwh)kJ!}-&1hUtT(ePIRk;L#T^dG;N<65!HTMWE*DhOPuOSs;?XHkP;0XkOswR}-t57+{}L +Ao5D#-c1eV94kk+m-$x;#fb)s@=LD0t_oNm?AsHXJ@x4HxV>+`nC4r0>}`#GwMmw{^#H7NJR7 +CV?dqJy&N=n%W|Lv6Jf4Dk5pwB2Q)Ain_CSA!6s2z^M;Ev+a?BqYZ{}!O9Pu5?zf9HmeOWkuwy8iA5# +JJP#$mER|Rbxiz2^W^sYujG_UB#SK4e>V1*HntkkW&tEijEI?c1GFJBDpL2n4ak|0ROM586v)D>!<4^ +=*V%x*bg=~{5Jj3lSPNanM-YJ2`zXrSAkGvgU$Y8nv@8FJX>;U)q8$koGCciCiNu8a`8p=N9n&o+B;Z +~M1Z`h+Zm_c;L1AnzXb74JTU`l-y3xh@_E6@eL9*!ntcfM;BE)LnO*F=Gxonj+s;z%&d0i!62T +Gxy`BCNW}4ST@D7>IF;aLp+|BrA+occ!{(0uX+_nFNVd9urxJ+Gi)Hu6MBLXNECc^u036jk5`HkaP1) +kRay2mUo-(_CV+_wE$r+Ze$|4<<%8#xA^QyB~w|OiQKenY#Hjx6GzeH5KmA_=~kb<6r)fEp-B(rXc;s +db7%3hW~l?HeUnH32FJVPReZ3`PIP)k$;)`m8kCz7Rihvf{SJTJ$8K}&Ni01a!yIs6N$dz#e6mOaX^@ +y+Gk#g%?O=doulaFqvJsd6s9vV3`4@`YE|sFF;b%n~ORaVz0Q#m0OM=h*6M>7(^tH0pitf8TSG#K0_Z=Y= +}oSZBC~Yzm8a)4y;MHiJ}G3VbPt>CBc55GvQV<2184s^x6xf{XyxZDu6Za^!r|1OscZ{}b>GQm +2U;ZCz7&FTS?w&uO&9FT&`S{!meScstuXT8;iL4{)`nIfQCM-%d{*Am72s+EM=*Gwx3OAv +=IplypqMyP62b +<8lFY(v_50LEcPGX!hU~^(920}&W#MR{(YFOi~?uk4y7I?%eW6$4~FYL`trYQ{N4d;+2C*t5BLZ7J~$ +(80^xKNwO!OfQ~s76=WOaMJ9+JOt51Y#yoEQs}20((w+Sgl^ +Cx@a?Xq&+Y!^5E7A~-pc)oSDCDf~f75CuGi^ib}8T~|3r135z<7;gO=82J?K +TfTt19AG5+O!5+5Ly*-i6oZ$;b#)jzhu|T_k_$s$uiD8EBsek;YOWBSMe_uB@@T6KQv%q>z+WG0L=#zL}t3JU@JfC*!N()3j+#Yl=Fh?ld6WB`r5pLNfJSPi0M3j=bI%Bel#H +&29pg(^AOYG@$_LOD@c!X$hQ{*P!Sz=@bXh=)scx|&1U^qXrcFDeMB7bRf4~3`Q5ew!%t+LmPN%{yUq +ynBsqC!&H9qq0la@e`~T9bG0bb|j7??R>u-<=6sVd*TT_<1S;pMBQ~!vE^k8WfImX0h7N|nN|NOsIC16kIGPPxC?RMW&Tar0ko?$w>Zm)TH=4qryhBd> +vonx`E&$Dk?ewPmOGAtGF2yM=yHd|k(DbiK0u)I=H&*lnfBHA|=o7hDi3 +O73p81DJS{m(VSRfNA&xVS65*UTCBMX{$`gVzE*H|m<6lB7hPo;Y(?Nm4)`z7q1Se~qbmz!FQ@%yQ1# +qgW06lfynr{LqSoAwNHJc%I4#!vRADX>&DX9Z=t>+SW$l-e?A;Aym^TXdU3-~8;@95mqXK({H#a)R+Z +Os6vyrv%_>WVYfisvR}&c&ql-+C34z@!fVM5RO~>E$gmK*4+3NJEF*9GZPLBmoNBEK4_2?O)xfnE6mG +VCS%^*5SxVjJwtb0nMp!T@_O^Xu=dLUkIa=F>lYZDUD>RCyS~wl#KE}J9gPGJ*2ae&i!A?-z3?Kq%ZMTkf%k70fv51l>EWE*d3`2wH#u#Dd% +i%4pvNGc<0`Atr`#wY3hA-DH0vxY^0{fFIY-mm$IP0=p{PDsl>%4}o2x=f#| +cNHW%-BSzkPeecZ-c^+SNVRvY_u=Y;r?5E={r2fG)5Zq#s)9YCe8wa{4(QHc%)@+4TZCZ^8*!A5=8l> +eQM&d`@H>K}685wd_B85T;4%dDRnySm-5P7e?+Pz$(qeU4Ny|FK-BttwF;DTI&Lmhmar{6y%XWGo!D) +-_8eitGa5xnCFpF{mo@vYJkQv=p7aFRVu +vgRAu;9C6Em|$?)i9NQB4wD$`X@U&b6XSVZg)4H|-Va=rQ(G= +y&etKO&CjP_##R^_L!?qyBWaW~;dxQWkMrE&^1@OY(g&=ggiwUW34bcF4v(*$kN_s72r&tv~u?Pwajc +~g+*xKrPD3_7O`_KUvWd_Mo|FPChotLxm0L04?*yBxSdAjlwhx;K&R%s3ujhD0)8)*#1ls4N**x@s@VQ-QPeiqepXdzL!5v(&;R%@T9X~m7 +N>G<*a2VI%SqGs +26l2b%lg+3chyd=_9zhx!iZsPP^K_CG;V8?h9~yWH5hv(Pe_fe5%)^&^|J=Wfd;WbdsZtF*K)L{%1R~ +*8z{cZ`@ZZhzthB&GbLYk&&BgGc4rr(Dg52 +*UtvSR=#~X4*7Yen@W?Il7xJRBas0b7>g;BbUJ%m^@&&4zjcYAOsQccb&&LsyR|~lcZ%8P=ZOOcfN12A;7 +TFS3pOH%%7FtOpc!nDY3{|txqf@zt*m5$8IQ#}3$th2!cv1JFKq{z{f=wXsCG_?3y8m}55!JsfMTblV +XYmUfh52)tak5OOH=sq<_zNwlJ0T3{rYMNiSi^2?PbS!#VrSK``9l9=Q_yS$KS{(&AykVssXb=k2~fq +55cM|4L1gXHyf6DM~J35P2RQ+@^(;a0T!0JpI8J+BT;{rhhUArGINJfGyVzU|r!{a5 +wxLP|8Nb%lf{`@|x^jlzn9u^$#cLR9FG+4qI7~N?^Sq>4PlC5DS=!mzUWQ8C5+iYDlJ~{(TUtw0 +r2LqIu0eh&{jne1iD9}ns5NeAQ(V!w>9txmCq={R&XeixUS*Grj_!Yh*}dbNIBcmh~vrVi0au@(NMEdR!oYWf5P_$L_8g7}g+1<&`%F`HVgBLm(L+>+dp;G6by&}8ngL_Hr +U;Au3~qh+e#cCTOud8c@IWoxf{lHjFumnm!5jbN>=&}i(&*4sgzsn$`w-MAt9;(UTHVyeRl==4VUg<3 +lmw04mAsV(S)((*OOMg;T10uND$1%;0*a|US^UaE@^9@Zu4v<56C=`ZPRjgC-JKqY?sJD;Cp2Qlrr(3 +d>CcA#s%jk|*6d1xR^pZgSG#V-JWa*5u21AgB+Lg|BdditCflmcS_WAlLVvP|VOmR#i$*bE*TEwTdU@ +OIdKNCZ|#M%nM{3J-hl32t9rQV8FIH``ppmp+pnju#N+g$?&5_T9Or3Tg*vpyWFYzD8DWMZmOJR|l6l +S}ybRLb2tYXT{?pUryRkSKq#idI^T2pthdFs)KY|JkGw$u?C|4aeY0Z5sSR|jc-|%vOk8EpN%i)gRC&XBQ(>aBT? +TyO$(DbrnU&Wv?(|tjNB%eiku5BPhV1#MoQpK<}S%_uX=Flj>&%o|Lu~Wnpasmg{4j^g!Q2`C9nK8E4 +kSMYjGDn1q8SQxE_xLi3JUpnmW!hOuQzN4GqZkNlwqPzI!BMb5n1>rA3hy)P-CC`^#zkvFkvYXVm}`f +zC4pJVNl2r(@2oyzt)ZbXCq(Z02h?ex!jTTQXq4+?IhzTJ?suyL6J}d6#~oEY!V|?vn+0#vO-L|^cdiK~Ii9w^Q`tR9^QaoSZXeRY6LB3HAk52VSEQiEu +NWB3G*g!Xj`s+5u;D**O#82turHboZak5F(J*b*fq0+}M9&dtVaP@7jDg5w+T5X!hKpjVG<_*0Fq>Q` +LiyO`7I`xL`K+|S^KC0tEwZFl&vG~z@EzlThQ=cNbAA=+>_!DluA_1y!r-Gl{U#vF>2R*#E|j{qUV}9 +&E*6#JJAGs#9B{kwY?HVsT)Yb{^)doS#B9E5>rjTw%M^CkR{?0)@#$gvXZd4j8oqz1oTZQX_c=CuQ3* +_l-WZPR-hecJId1dUvKmUTWGR9~=htPZz5r(uy|TS +QvV;XN(?Xd7QY0XQ<^165aK2br{G4NPlI>$D*}O13vI42M5C8))gLz{N0_FY9vmr>xOPMVSygR8J>dj +;qNm|KIedr{I+7jFtp7|!r@8%m3eX~a$`4>=AoBe^s*jjprJox=}ZD+=A0bM3fOv~Bajj~fLranlcw< +6)vmVeAY~a37K5sPpJQNnfvPOnc^XN!T+ng9FEw+fnYuMS4KOrZ!L{}E;_Ak6Z6R8f1J*&VG%kLbKUC +QpyN9l>&f%gNIMLGjcMAscz4Sh-%BciPy?{xC*ROt@qrF#2zg9jz8U8#b?^xpnwY#kKsX}E<@=``IB3m?Y%>fCmlidv+b2U=`3-!e20~I_bzh4LnPNzM{@JzPcNhqV +e*orG}ck}<)Y +u~$Kkp4(9pB8SC@VuNLrUz*k|!S04)BeMp_S-|M*B*gC@gf>-wMxd +|?tcUWEJI-R@k15QF<=CjVSj}_=z`k~oo7y9B_mzSEO?Vd|9%?2j6Kwym@PziQi!G2L1S~&uYWbVx^u +%0r%tW2&A%R0uZOPP%EKl7Y|$Kv2pvspjdWnRo*wT-O+v$*X(uY|0N>|bmn%%@Dd~bg#3s +thh^}Y-)gamuP;{-GAAn}GKI7$mp+@G`qL?h4tchaUik_>q#Uk}K3c|WOsD8ce`=!i +_Zk9nau|Drr~N{zbc{h1#`IkmW0*}Yb`J?J1iX=_jSG>Gz&^<-X5NANK>#W*)bL;8}3*H+O5xtT)2{5 +L3c4TlDA`u33w4?o;Yid^qv9MDK#^3C4gR;7vxoGV~r1n1-7qgpnB*gH0M?xfL2yn`U#|GRnq5qYg`X +p~=Fre)OX1kBUi&GQEmJ7$q}&RVN|ro{s>`xrh*JkFG!5fA9{xyeP^SwS=Z{`@y1|KR#x9I$q*- +#$8wl^pQokse35StaTl4X<)Q_7IjxT(C+hUHc!W7F&VJz3aH?{=SFw<+j&Ls)K$EOSOWtv`Krt+CT>iSnMKtJ0OA63~N0FR}Q#cO${_4Qq?KCH(Lt&1W +Of)*M*bbC$j7UFi|m3VMhqztz?Wja&;U)Q!zA^93t;*o*OmKy9ADfkpfy17tHibWPQ9%BgYtDPZTPvD +SP?W}(WMnt0bAqq(Oqisvomt=;bDplX+@H;4mnpf%HPK`S+(V-DQVrc3xJ;2bK3(_Z<_0Y`VGHwPtUj +qu(qt-isdp@vNc@R|~>WGXRhYhU4SO>ypQpn<0lQPsc+O1A%u-DqoL{|UdM(wyA(NHZ7LuU(pi<)##m +{WSlY=fyXckA{l_E_+{d*lE@riuOcOQ#xb(*1z`cdrZK-t~Amb+6l~=y}mBF$n@1Ec5(6bt<|kMbDN_ +_f!?j}Mq2AS9KkxAX)lQaZ&UECaj%3Um#g)d!6JBC;A67DQ$2~z=$K`$d{2w(bdueUHSiF5w`17sxKZ +4d?J>3|5`#c%w?nKOi>THqmagYB-E +Q4>FvC!~Sp?y_WyE53WjwM(4lL(E(Z%fbPh-epz|3E4d_o6hD%s$GTzywtf2Sw^RlvpZm1J*1cEU%+bn>id%>W!AayhYHj3!i84e}CRY}Kr!RJ}@K_ojhm4nHJy)4L44@jQzcnM! +&$fIW9764h#(X-3=KF}MmbY(^C|2t146H8cA!$Rx>*Hot!&Dsy(>cM;!m0ye!6g57F_l0(Wy1!dX=(LJI?V$>DYOP^A)>FYb%9fV#&Eh +#hgi=+gpT(Afyx3@lcrudZ~p-LM_p7q(#H>WUr_EH7SPWHoA~cqaVWGv&{ILOnV2mMTU#DTN>GNnS*v +TvPu%2Hnr_<`NKsMpIXBR46#gE?`A!n_qzy9soZQ3}Ux%J2W%j$bt=CEk72dJG~rJ?x(OPzGMb?gsMl +iTVe;pvQm+^ckruK*v(IBvTxGRPEva)ZwVP)oLwkdGk?wi4~eA37;Iw~%Yhj4Lm{~QDa#R5f +iLS4g2cL}?^1s);uT!NOU=e+36un&X=2ZU(Y$!QpJ5(`fNP9$Y#mRtRG_Wib;v +Suu|j>__?TUG@w1Fe%68s6u`8NU{A`juWbX-z^ntv-HS|0&b!??8j1+n0 +`f|GJ)il!DU=1wyR< +waVSeBs%XR?fGaWbx4iK#io*@bH3Gc&KrbC_YXaS8o5np9@^>{fvO8&^j{}#Q +6(B+x-{5_kYoQKMdqC-pe$vhH!os_Li5xo*46gc7`Y9im7I=tWa#&4k+S!|FtciM-!Ka~t3dfBB9w8Q? +Rx-_+7K$3}e0d8Ppn66yV9!f6`ouLMsB+LUGfx0h&i|g`;lH_kK}lP4^=mdz>Z-^*qN`>rVy^|sc3hr +HiY~$eLuNchQ(fKIzpUc*lNP=1{c)3DUQL$tYHq*?nkm$hua&I{}e`; +HPF;&*|9V8$&o!?E#pOOn%R#fr$1P20~ogypBUR`cm$zEfc%G$SK#8|JL|H^ykMcY=bwR*3z$4QpW^b +T$fN_Ew0_o)oahmyviKgWv8c=C`9hZ=Ya!H&cif9v#+2bD{#=`WT(T3=mZo#-h#d$lL(AiH=h4;!$S; +h4+#I&fFlttKCA>iQWvVR(AfsUK`Y5^i1)gI#Jh$D0NlY#|Wlu7Y!Hg*wVVmZM^7fJcZNP2@G2dVWp? +h@mWu+QnWB6PZ!J%Ogod`TZ_E-aE+H&2!nKbm|_@;l40C2k8!yY-@bmXeJn$B}Qfsc!eM=PcmW@0CL2Zz;6< +_)Wk9u|U7HfbI6q5oG@s41W6uJkDo4 +R-a$Lt-jr9I(TmCvnG8L6bsDb5tU2#s(ZJ6PUx7_f_+|^k0_1RXc0>h&{IozT|5SaxfDg+e6nrYxEln +vzOorp9-KbwtAO_c>WEcZ=eHF?b&OLTB}++2nAEW8w1Ocspa^*}BGDLHMo2D5-%<@v_O@Og<4wx;kLxUx0FFqgVdL8>g*YD9PzbW4F}_`wO +pWRBZ=8rq5|%dk_*@HXf2gQFFUf!f$dUIRmm84%N3x0Y>=lywV`oS=ztiJHx9?U@526ytN?6b164H#z +ZDi?xt#ka96Sc7KT6D0%p#mv&r9|*#gC76W**SD{?Oypq!1bD6Zl>$%*Khz41<*h{N$}KAzky)e~U9s$pwsrE~v|3u3L?V3+I5CpU9)v+W +L0i{36>ZLS9>;#tXE$bCb!d=mJoMQ0mjMiYi(=CZk+fB}%gDT%c32N_vgAY1<0K>z;S&Vn{92U+vCwe +n$F~U`kF0U=W_`p9O+s;`>@=gC7ZvI8sk3$VSKpsGkH6P$wlbXT#%+m+@#8(MaEvy>bU@54BajI+LHD +FJV23uS447JID&b-xt|0))e>PKt>*GG6T$i|ja7J=ZDRimwK*!$$)RF7CLoz4(=7MX*B#BC|1e~E)WsB=lGxZo?taCNi-=W#AkqKP?;(*aX9p`F!A#yYG0 +>Pc4_qKjn$-5fRS(T-e%MuRJQh>@0wpQUM(DWa%HPBT#Q^2D}K>+C}uo_SjpEjDcD?5^8CxQ? +d&I`riE&G~dclrMEyN8#(C|zx4_*kBraK8Hatel#cKm#1sJv===;vV0{6fJvX7_>m~4Jwi+*ec5BtV% +WOlRw6=1@?6FG3w1f@$-3P{*?e5i`kv0P2LE^zq_!U#|YNwXd!m4;rX`-7}A!7kbuZ)3bEx3FR=S*cN +)yt5+{RYse>iWDC=;6i;&Fn?>G^r`9<_q0--M=_n9zcFV8RmUn};PNWMXcM#>f|@pEnoaSWi^!w_4Lv +}?7T46zAGcwBoZCr{t6EIP!dC7gcxaz4n1>gB&wz-$GVQCuN5H#976X9)-kU#Y0b#4wqlpNy0ZN0Z=%4Z +lY^r0W9cUo51D&NqW+O>=PImQKkyzsIA=VV^&f9W`NI-OV&8zRZ=HM}4iG3PmT6;E04Y6TKw2K*tA-2 +<|&nv01KcBLET=A5QhO4%_h*aj#geLMU{|kydK3s!l}YYc7IZx`U0t0^LNkPi_1qFiRo`Coc1ESrGeI^dt>fa=Tl>MzU>9Y<&L3#u-#s!6{wwO==d52udtHOJt@pYZfB6s5qh`bR$TmSffT89(1Dt+wiCX& +l?5|!@z~%9^Bb+{}IEt4UhUSc&+_hD?~~pvw+H~`Xcw*jK_Ygy&m6sk+>nl<9qNA*98o^PnG+3Cn0zZ +`QPN?MwT+MKqypTUYl+2d2wc8=6Kr9XClnV8+7L%JvTLJjRA(jrb=F6MI#^31gil$&0tJ=;6~`%_vg+ +u&vnIYrEl%IexOilAG^*AMau$~$7LZeWZ{7T1UiElJu9WL-l$LvZtE&gqO|x*99<0KC7vb>5C+-i&Et +@&r{$wunWb0*zcAayxZY$ny86pvzWSc$QwCJS7E|R2QOl4MwP4NbH#`!UoOpcH+%^hFHILYCvw#iyB! +5U}4+aR0N;&!2I!k{UzMEjGzhIYX$y*cfgjnN<)>d2Rg;I@z1`pnblc&<7Nyw7E>VlIu&g_7xw%W>V9 +40gA82?hv_erfgNB5GuZ2_q2@$LFHLo%B74E2t)!Qe&9#-flE;I#jT +u4Fxq0y=8OEIe&K;YWLFeu-)a8<@to>`(z1Q +d(id5j37U{RcvId~i`JPP@lPQZ`n0%A~wbg>gfNDsXE>x7_B%e0qBxTw=VX0s@f^xHJj@^$s;dW+DDKj~m)YteEZAhI5eX9BpqmY#s6Z{AGq +GYD3hc!Nw2OsxcEKHcM6s2Sy8O)me!do9cRs%8biu;a#?m7OzV8Emh~53ipw@x?zE8wOH`iZ!nNx&T?R~20~Rzgxu=y@U$=ywdQ94uRq||82 +2UHtjM5p5a)BX<2v9gk{^o|hk|xQK*1ejf +)V-Rdco-33ayOTJ;4Art&vsBL#kX?)GN$ +L&)EprFu}EDN!w;y_Y-i5G`Xw#WX?hJ$nk@_v8oe32&fYqhr-teYCbl&&nTwl4mMX8qMWUR6^p(pr0B +~@{?wyN8J}DR0t*2@&S$&oNGXK~ywHnJY+Pr0!SXs*69sRze(8k>km85?pK>4Oc6IW#h +qV=9JeChsJY)a7m-*^3`cW#Qj-0m_|np0tmcrZ$#;OG--`p@1)DXhsi6MD;(>#)SRR?OVcuuvqCO+JH +?9bM^hUYb-Hke63!o1^NZk9ht0`?fP-QyWezbH&{l}$7ezi2*~^?O*|JUfC~oh74?j2-)BjPGBX~-k^ +xa~bd2Y%&FR4v8-*g_oRjosdzDB0n1^bziZbv47BI-B98^?PkQp5PxFUwdq@w-ufJ3an)QNjRHjJa(WGVm@JnVzDa>NMD*mvvZU#RL@ahKd!#amZy9fHw{%u`lPY`k?crKZhN+k97ua#sr9)vD$�kginw^Lx#V5gXmcApoUO&2gElBbjdwso{ +)I6;%)44<=v(*$x9J75{^#ZN93)>f*u-#lh9<8gL1kx3R$XVL+okbAb6y@uaFF?Ho~45-)&j1=AHE*~ +5pF$JpS04D0b0Y#mo<9dD1n!QRo?7bl^3Sdf4-JwFEWPP#75DQ7sWc~NXNT<$nvo>3sI}M&qr2TZfLzTYZ=Cr5JQzpq~9-0Y(oyv=E13sk4Y9G`o7q&jUjVX*c_sVkgk7OXb~f +#9IN-%8x!#>+p!df4e6H`o@RVW|?eXc6^KU#+fXmG^<=gSU!x$Xzupq;|BMV<%uaPrGMesxHF84|dUK +aq6QyTX9ewOMS*@cgh4EjX~&?M}p2zrp8UhlAnyMSoeQ4hTdU<_r?YyR({gVn(^sxI^Vqo3j +l#G%kH;8Mk&ka&!i3$hfy#bh>ps0eikKSP*csoi8)#fItNQ@hS&rJ@cfgwm)aRYP_CboeV?s)CSnErr +SDORg@-PWp$u3Ftv=YllN0_kra;-EBhc24#D8K49U+pn-rY*U@fxQ=^AL?l5Xg1PlIH(EKdp~Y(CN8K +qCSFSs0p6<~Y_JI7?GFONR^N>p>ufi;A;H3wV-><H1{^6xZ8b|Y!2Dcop_q*(OA_;5FV`kT+XO;CRewzT&NSO1)Sv23pv|S{F1{kG?gDL(9V6`(lbWuY()4 +(6KR1^=$Wn-S}JD2qGnfOfT?zNE$H(4Bd79Sru}q*))ozfLGPUH?I&rlkS?6tayq;wKmc+-+=TNQr6V +q$@-oqR%Le$oomZ5H&Tdb0F<*XupqcxW|4Wu?J5Br`f22Fc=9?q}^6ivmSEI8o9vcuNDVDuTr;$ +wJqasAl>q0l(K@ykMo+3JlQ?OGOP?u=>*VJ_*ZOy-LRngD1!@L8GZ?B3-6S>RjMJr~~O0hH~>B?G>pP +rFR#Fpo^xT@A48u5alUv|D*63tabb4OBoF#1j3yWQ#5Wuh0(jU_HQ+(TYS7RKv}NUi$Kxd42Mbe7`g0 +LoBF-w`;$O7VLOB{#k!1i~RRg&JYNN1j4y2yntH)M@u9u(b|Cl#2n~XlMXYi-+~IEe5jakhCpbKDz3C +roStFUK6$Rdm(jetOW_IBb6M~vZ$=Bb_3kw2$KO7W0^xu_XMZ$$cH2XL1h;i4{t +&q;Kww+DnK8VRxh)2)04)u?ZXmb)Wci%^EhpYn0NSdin|UUy>GRFfg}6I(OAoRV-?nyff*D;yeyUq1S2fT6oUWhaI +!j;(dc0@$K(w~>s_(%~m7zM4mY@(!QUioWXwMa}mrRQEd6CbG^ed@(4 +J%mIODVkb=8*r_%pa;Y94g?~;d5Y-u-`LH)XmxRCkx%5PQwy~eym(fiydj13JQfuJ$L18w+E}PjwT9n +BAhVRV*YMWv=)!@op)k&+bZXP>bHp^9kko)=al>y(7#AWFI?+Ymzn4^KBV#Ht=t9Mfp6v};!U!eJbmTBeThEfW85QKsT&`+_O)rnEJQ8p`_nLh%GvlQJ`9}pKVXR&*AGc4FLIO!H4qxP0$wJ +;uXl9Zr)6ugwoa)-N*^PbRG>i>mWc$thSC=?KK%o+Rf+S1Wu_ioc_>2Sfr;S)?}s0Sm(*BeT2UZ4*;#9DO6IFZR(4Ziw9U7RT +_>3OfUe=3l8lV6=+V!y*8^zaw}DtppzVxWCMgk@^RA?>UEAA5exePmE2^|YG5*GO=C*1{S6D0CLCNZR +>F5p$!fwppZC+nR}S2_e*Brw7_k7s(r-b$$qp*iR!RZXB8&l=$$C*g$2rbVFzf$5+yzeE9k| +?%`Kh$&@`mEwJQ6p^C(9Dptqtz^Sk +2-^Rqr0U%}L$my@~O)z$)%r*G=^z4^S0f1WCsjl%Wl5^phazKr+gKBy&$i)M~Tw-k9=jn3pfIyUHq_& +@k|8bc*&*(s8HRVpO +O-WiS=e%pcNY3(#^_q*dCGvJn{%b+%4dg8svuE^t@D&`?G!uugL^O{7q)~# +10%PwC2UTISM1X6&S*M1AHrQ_DSb+7htM^Z{;hEFXl-ZAh|hwL8huL3!>*&v_?qoZ36YnMp-1dv$?Pkn +YOHNQK{IXR)?h_@kSbk5Zd7_*-28hIzBAz|n=|PX)H+lqc1tuLJn^x<%QU8(ZMIlxK?$-7@k_H_XJr* +L5-wXt9W_Pr7xw(1(mp3W(L6;E}qAesz^J^7LYJjgG9I8UPot+{k1D>C+40-u{!GLK;X*_o+@fp?NrF1&&b(MQ+$#3ODVu^?a)1Y#8P#2Z3Jt!$TycqRI)MV$$RfvlK+<4-vl6jOS26bUs}@bA +;asNE9?6<;FG;S)w1sAa`yNgjyHx%OoMeVoxDWVL&LANGuU&G2zSMSTWeH%w_RMHepa?KSnY$qTpyku +em)fTg1|C1@ZF}`TdO7gh9MU(Y3@na5!k_?TDG?;jbmGPe7~=Ys7Mv50|8-9UTxQVtFqZ|smVvvz$Ky +4vMOw-sxG`fhC8vYNxU!tfCg4*ys%ROvVZZtWxeSu_hqe4E +7eK+2dP9>-CS0o6X&rxj=;0cRL-298@%nevProZxasLud^`K@pQt0Nwn`bSS0s*YcMp8dz6ttl}J>DP +W_f=uG;#io3szr-#$S;SeLoI-+=0OnZ>7XD{5jcr~*~VPi{d6QCj-?Yns61JC|4$!2qGrrZZfhGxUGN +#?ud&>FDExrao+?Wc$7PbrgQSPoK)ibftj!tA*nCHS2 ++}Oy;V5s1?FC6l1>oap?6!}-Q|mgE^TIjD|BYKqxsYL(`tSXCq~sq_*>CewP^J%#)P92qg;yAg^w%1f +2ifNkf)`cHwyTGNuywC0jG*wU^<#$FY0*>1>h-=#0X`;`={2f$bf +{G8BIPBpnIpA`q2pkR$j4XJp$8$z*fl#O@eRChsmM#|gNT!du1wtX;G$Jl +_3_sCXqy~teFMt1K&7Sb~Sz=Ni7>f=J;Iobp=n@17;eqZYygF{jag(FQq6I!O??dw%b=;z_g +?VzP@3^=d(T`8}RnL{Ii_{jFunlKMmF!*`tqCb>&Y;L#5+=QojmKVmAq +QP1grZ8Z^;Xy7aFfC$Ypue6azAPmGvU2;@K131=4?QJzSZZKbF4fepspuQgw@AvAYLD_;mk`PB*8O+} +k7ZUQE3>L=;8t}|_!4BfuO{(7c3YZ7Kd~4D?P$Nl6}jusJsCO8db-elJcu6tT~BiH3Q%qx%D){k}A3qL#ctO%fR +z#wUk>{Pm+hJG$#}X)X32(M7p_KTIER1al?bJ!POWmf~)RjVUc`bN#qoN=3+^p?sx{v^!oDgngGJldUIQOyf8c^#y)!9lu@u;ESD#Nm2i3SVeRY$f9JN8;CmN8H +(*!dy&eNF(pa#HDe&eY}*_aN967LUG3VCONP^dZyqJXik?UQC1!C(yj)NK=s#CHD7;Se(;>NdlH7hu{ +>f|{s5K8~WwP3G_geBOs-&Ozm4q261k3998Z;h-u|KR(*ha)I?+cREhTEyicQiozq1BOzf@7n=Swo@H$yui$Qf_ +P{Ygw=gdFAUWvNwZwIG!nX6#vu#b3FdRm8qGOi0uig~7<{=<5d|-4xkMlwl5;pA_?%j?)b-Joak5GQ8 +pq5u7|-)XzMMRuW{*H9q(bCf1|5h{zTe>-dGWL!V*t@=&}rXTLl^&LbVYMs4n|k!pH;@m7P)INH}2fY +J9iS(fcRhCgAR^Z;MJ{6iibp3iSWQqA81U=WR1e$dc=Y2w$73z-{p%OG}-*!c4iG)jQV-JEEaQVc6JR +m#32p3pib_$)L;eC?!M%}11u-C)5IQxj2s@-u=`ZiObIBk>~DhzYr`G+1J+0;3myDm04)=1d?8xMCw| +7aS48EsM=iLziA_a!Q1{ln)0Frs0oZq^+W9QECz-z*b^#3t*8qR%O3)wTg8uOA>i)ib!034d!XQ6=wM +dHuHOyYq>?TRGJGT~@i$Yz^zi&ZAsC)IUy`TC%k_te@#IIYs;~fku7|F2kz>u^+q@E38?#)12k_2C!r +_ay1r+ZT+#LU&VtASRHbRXW)m)|`kPosJIe3Kd=5W%rDo0VR`o%s4m_LSJ6t%3Qe>c*%AeJX%my&jgS +2LjMPE;EsmaGC`#FwJh<9S^_di-qC__<#P>zxyx$$N%{+|NDRR2O;-&<^&~vc8&h!fBSF$-~aeO{+EC +Eum8*c{BQri|L{Nj%YXO3{>y*=pZ@DV|6BhHk*`psT&8Do^^#g36e5=6H_z)6?q&IKm%k3M9+3q?A#3 +3z0l$Not18>Zc@MavJB;A{tyB@9oB%VsNu1LFl-J%57GSI&VWCZ0h~XT={7elb4O~SeHQBX9k0=BE6j +uD?UdR6$;I$3)lqghZ6>N41YSz+}h&O^kawk*$lgE-^eS4$zJC&`Z@x{6>nwKPLZO19qRB>DhPDpykAh`ARWdSbz){xOKb +M#II8ee$WCWUxq|j$;o5qS4K{Z|? +S{JNPye8-r-T02qwAN2@a;p& +$R!suo8@kSY8EjH98dxZP`XQGYW9;EuL`{Es-5GZ;hp_Rzs`6&8;5S%qt#F4KVyPY)~hy*| +ZSl|UEFks=FgZ$N#0UChKYDbK&a_h1bTC^K%zopb_EL#Ob@@_`jrs2Wp@CI`Xuz8(X6)*B~alV{9u8< +Q2fl$b@PuXt@QV|WYQuQ$5{x9G-;sEFy7!C+~5i^OPc!OD@w?0IoXlpypA>DMZ%2X)YX8CeIm{FxWq- +EN-DEzC1`n@e((YiUUYOn+OE(&+p4W6I)H-mN2$$PS`$r^B-!0;&YCsiYy0P-!mEZ}Ff%+TRoA>lSxH +|dqoN2^b{L*WRz%qo4DtD9#NY%lE}qsG*|^tRMoYyIPH_s5cAbuWxJ_1%;}Y#*{h9v?31wTPM2;(4-g +UQq+A4q1!Y1ARCu2gi*XZ=U6fzWUApO=f*pDa9M~t#p4IQq0T1u4&1nQs#Z)qL$Ar1fOu<PHuM=fN9BH$p_U!c!;z4uS6)_sMwJ>O8s$nMWzb^ErIdc#E^zAbsK`%i4Gbq433z +oyy4GX2HhQ^efsyX@p3O6hCEd_G5;3oe}=ytSr)*nqCuIxI*X^DR&z4~Ym9WT!&qdG=cukkF!3vABSB?j9pF8Q|?B9_=x~&D-SB8T+ce`hI01d-ENxm3(~Q1Bsxup!9KGO6l_ow4+5`+^EeqD#iNV=StB6{rY~ZJBrdsoD=jo;RdFFFbG>VkC@r@9(6nh?CXYxZK9r-lB`6|at22!nPoC76%ZCJmJf3r*$<9{^3 +cfBV3#cx=Vr_bjs>r;NtQ71b6OUOod2RiIJHElw!={s+Ydgr4??p+dmm$J>xj9d)JX$YMfq%j(5Qd-# +kQxR5!)?Ft;TV;vERB1qO{D$O7y<;9;&@7=$=KK4ui>EE4}v}KuEmb;yC{CGYMwowbo8V!pfW|) +ltaXBI=o;7vdh`;p};z-#zQ07V~4#g$p)Edj;z1CaW +D*|084Xn{g(GpeH*Z5Y3QzV!bW@)7*a1h!w{9VG{!M*X;b5zSoMG#bm1hEG^dSyc-p$8x8h6_qGPif* +nj3MUrt~R`_gt9QPrCuPNRxo1G!=LqP|ccb4SDYkzRJU)lr*1Jm%3T)XcAL5B28EP&`RgbtJrICQ(?P__S(O+#)aBsvMYZiPw#6NE +5s;)bE!UvB<{YGEVS<*_-J^84k$l3WL_b*6Rbeh;QJ=h^i)g#MZ)HIg?rq0miA@9MZ8K>CXr`LDoB@U +KZhuxu>()QOf0BC$w&NYqa+C^GaSoZx3PV6d@+>}|(e0|;XHL0&7BlFAg@tY(qN#-kd#r@*kiGHvby3 +JCsr5wzynXIeCQ~fbj8Vr*IAZDvrgwf%=JSLD`)*?vSGCJ>+8(ep_daO6Psv2-Z5D`7GX_HIPL0Qu5B +$&suwg~))Q^MS{zSRhLxUCf{n#3S6+oIlHe#@91?4xT{%<$8?NxfOZKkXQDi&8)nfBmKoqHqM9c(w5v +7kYNCJ(7|83R6y#>^xw*kPf6D)T&<>oIA7(^PaNvMRHcjep`VtIP5T;EcRT2FY?F7X}D}Y)MruLsMv! +ZeZ*LMr-g4ZFqQgQ-#;0(mjRImM^Z$%E7+dw0B(F!5!G?Gbd0m8h&*wx(GJXrJ-ziTqDEvw|tR5F<=_ +e;qe>n@QXz2F$U-~>(U%_-E%}dSm&Kb?jEuFuVE^)goDbBg8)_buD)KaDcA*9>B=zo3wyWD%w&Ip1_M +1(%i~U_m2QK%My#nw(dj$8k@b-%qs}BL7h)7BVK|9WZsFynHO>x;Yow_YXb`q3j!hmVhBf +P1npkFxqg#lP1$F;1HC`X(}ur!P$0k+($exY8j?rVr))v-mI$qZ|jtEyufS%Pp97NKQH@tSnLgnPne+ +5ut9?E0^#q+EF98G882=X}9|SuS;B7bJ;Rl3`{(HG+d-h3d}>d4hfzQ$;D2)5rm0?fi$47jnUnCxuKF +sG7klpn*VC;bw7izMNfBmU1yt#k36&BU4)2H+Qj-7cjVTm;lIM?SN&ocFjFSW5XAQ0#GdzMZSuQi5y; +?xH8=cT-8at-Rt%)7V<{U=J_+KY77J?zHQ{o;AKxM(0B1Ryzj_fr3EkTcJqd{!uJ|e#@waN`?5f_>o8 +BwC(>erX@{C3*ymJZgS}n2!}G$Iu7?c*L1s#x^c4+x!Yr@YCnAThyG~v`JRzL*h_nRdkQuW{* ++oI*jfL$(b?45<~}ORsqc&u9$tREc4sjJm6O@eW6PDNs%4;j$V@3?G(zV^fdiERyDaf2(p^SO?}!bAe +JlE6Pqhm7WCh23(n*_Ns-ZWFr%fZ+x~EoDorYlH>BE~Nni +(ZQwGHY@7Aeba6BxSJ~;6P}uPGQs?N@33eqn6nGTTaG~)4tP!Wb8@Gox-7o&n<_s1m|)dP!Y1IZZVL7 +4@tlhQey)#n8w|MD9)M!Fv*!w=DB<0ge4Z$K0Rz-X+UqlK{;$K}>+tgANb1F`M#;zFa^zXKfpj<9K~Q6ks;yHWg8Fg1BCl&~WvQ6N}wJ +fb;ppv~2b(4q~+W!D(581A{(UGcVVF(}-!l?ogJ@0}9qv8g;i +nU_=h1g_LtR4Z@uU6sUx9Xx=|!?AhwcK!cj+MMMD{Rpz+HRl1wc$23JE%$GbqD%!BaMpnpMyWmyXw@f03dZyBlSqUyC=%0MKR9lX*Sox%= +dMWF@<1VdDC}3Nsa>6)46nWfmzTq91i~V59bBDXsRCnKipKTDf4roB7?X+5*;Q9LS%}rb`kWeli(n9x +Aor=t$b_t>j6vdbnaz@7_lCh&?gIO&@mKR_qMk%WT%=f=2IB4L){i%Jvwg3e=5%$b{b=*! +O=FP`>wS!lj4A$Ebr8K^25nqRpaTWw=Y%oQD?QO*rMVa`IPbHWI10N4ax$$%)*miPopl8IHej@tlf)Jf)Sw#qaA?G6fQ^cb4wGlS{TcA)kwAzlBHIKVs|scNIn|>U9 +0_Q?P$3f&TcLrCv|x(|Nc9JM{$QKsCcF*0svex84vRIv2Cdb-4+QJpN9VY+lT9(=PT>zNdr^<&iVX8b +!hoNxNMpHYuz%ENpJuC3Qsog?PqL4*AZ5w(L#mBYyqaeJflJ6S +gM!QI#)SL)~L*_gj!Ky&RnRP+5Q4n6|t~weZah-oxp~vZzvJ~^%8yt7`3@QduYJPY(Z#F(+XY2KEA+E +DJ5%1|iag%53JtxH@2Uu!SH>8IAeZo)T7OeFj2am@*t@Y!`n7GX>^PA5rF{q}sH+G&=92CyZqtL?y&t +FlR%(7e&o4W-by6Cs}vwz0jt%DYzDJ4$$$Wavsj8NnzMM4x=DUxQcu3ZUKCp^6N!=){*#9-(Hwp4+Y1 +_l^Zn1Sl|GF0%o^+8UW%|N^v-S>nxbQwp_Mpos9WIl*Wtiy59R&m;GJLu$13`t!nf1oR%vHEa25 +@BBGBXyMQi{+pdAd`n-0`dQKmeL7l|`7o`GDD}U?4vv`PnjimYD%UBj$(vS)~L=zMO}?jR>Zp@2deXF +J2b;vot^$WJgNOLnpWOTMf7kPM>leU<8`_4Y2+S2H~AEiZt4%;m8@`ocERgZ)EkCJva6#;Ivjl1X`w2>#piVf +ey#Rh7t0(8@3ti|wR=BcG#(16nl+Vw?AHufoEs6{D<*-o=5IIh*U^hK)H?Z)=1fEL(;5KjqG>r7le7g +!hho&(pq4+7LgJ +}oU)&TTOb1$;1~N%F9zG*CKXg%Q5M0I+*3iL-~R@AFOgl`7(d9#5JD)B)2btZzRW25h7L176p{*j4=< +{&Ao$Z(Mf=%;9pHKE5bw9ngKePs?3h8z4vamD&*-Z}Fr|vt03Vl9#5h3Vm6-X6)V6YRqqby0p8P1MHn +5^us{PdLpqlKx)t;T+fL#@XA8O~b}Ka`{pA|Q7Hs_=4ERA2wD>x^iDY{VygBi1Vf**# +qk+-{&tlZ~*lRcY&#X|za+N>a3zM-AHhF;tbe0_GD3X5ZC~J#+g-(oe_seQIE*o^I;tZ}E`~|A_*2V` +e=!NOi^4(q>+2;9G6yv9PUMzU&h^4VIBans5VzLepf^%+g)7eJLyWuJF0fK*|AuC^e=jZk4LM<&Likh +G&eRNG6X%14M9u@FPxm*H_m&Ah3qi)3c!96MwHgUnT5$me21;-{n+VAQZ|JcbpXnNN_#Qhvh5AqdP!? +VTX=heInnz;lAhleazB{v-BeHo1Jd!JRe^dc|JF}QGW69SKofC$R{n#Wi~GIl>;Kb7kI%pj{UG#PqC2 +aY36~brn$Q_vg)8!+g;bKwb|i(3tbl^+R=WB35fQh-F~dQbxoWDmpT0N7>@dLm<+#|E@*&6d@COcqZ| +JvJ`@JJWunW>N;|R*G?7!1X0wbiR8fM0inYN#U=)cXBNw;#_V +;OY9xpRXEk$Bb^wkYkLe&T5#@&2iZz8QFEwA*ebZi8q-UeQMZ^!aCAF(}m-?KuE(+mIpAThjp79x9$1 +p*PM09f7qdA>rzgA}6{GK@i@H)cAF`&+({0V{W~W9lC;InR1evY*d=28l+68lf#{tN#ZPaW+*8mnW-f@GzG%!G`*3hh7bbI71D;1~7q9w&9WZvGpH +;$x;7!P=(yv{}sL{4@ippZ*Nz>F?(>P+usoQRbjT%dY-{mzSRcCYCnW_ZpECS>QM|Y#eDik_G3OK!af +M>Z(F}`5xrCccW@d1k$@xI!OaBYjB+~^!P!-~Y{`pfvlOCT~$DGy_O5qj$+0IFdaVpX;VWBM?1@G&Rm +uvnA~s5T)Z!^QN+r+g|^POSyrnA6}vJXfzP(EklRwFYEsDph=v66&;m(Cwmn34-0nYV(MVpUdGebUF} +%jf)0SA#PjQ9Gll!cyTa?H6>@@6aLFcsJYN=uzIgiFbXa$53vF+i1^+AUYHms_tQ7>s!maWU0j4tfa+$@!_hQc+XX**9CS9h|gmAXcZ1-@O6SkNwlv9=2uK&^!D|C+PI1-I +i;o0bOFCZP*?n(nB$Mh` +!^m834Ddv3_d))3GnLws^Qr4UMA#jE&!xrX+^B=75>7(wn6}YHDEo0j)|#Jg9b$lSKJm91xW#e2jTvv +@-VS5hV{xe-1&kc_6hKEEX3X)tN~xv_LiMvb2TwW6xA`8f5ap64>gpG_x=1A2Me^HR`b-+uH{iQ^*7C +0Fo}KE~!Zlq~g|}bf|3wOjZ7TmqN2e%7NJ=p)HNw-X#`R3HSH(9Jl%`(e_4WvP=w9yaBul38)$gu9eK +G54{AP?JOkFl=4g&B0ma9+mMa5%SkC2Fm1xt?&=_lQG?spp+ug!+g4ALbi +SNCR)RRq)lHfHemT;;`BK75)Ew0Y_=f>PBiLw3G#;;1>+A%&x0Zi>*&>-Nu8aINW#9=_Xge>IjN3aMT +fupf5$Xa!vz(374f=xa&TXaVixnA)sJPF`uln6GFQ3v$hNnmhd`se@DrWi1Y8B;AV+TZ}st}pETlo>U +Nsg06V%8@O)cQo1Ce_~Ug;&|!TRsu^Hg5MD$t-;(*>>q`)ij|6yx$4E7NFzu +$QI64ct_~Ie)hK30_;*)}__$BDk>4(ZYYr-vOPk1o+uI|I3&d#j_t`3+$RgpO^6J`0>~K;fe}Ed5sk*hC9UZ**fV;q +9k5Y791lm9Y!1M|a8%`_poc@;e=7!TZRJ-089cY> +pxTHVVQQptg-gl4k1M^wWC|*iMhdYQA4iK{cm)$w?t)_6(bqT=Xdb<-2%Drk@NokddS{=l_h6(PQ4Jv +(ML~tIP5^pO=LJ0@0x;MWN~!HIdGiR$3j*<*V5(#uV^1@j||)_{m@@i<>$AJAAR7F|zmKDal}z%w=JK +&`33o7=zpv<1#+3)puGHL4rmeeK=DF%(Js)AOT1Xs?h(thxCPaBqS>Yb3QQnQx5g4%ws?M!pdO& +J(W%>@5LC*euvyTpttt5Z@jBkER|yQi>-cLdE}{W{8&0svihROBFP~~!<)z~@&=<8^7^XnZ84YwdZN? +EF`hZ#uyz1WV@GMf31i?T>|LJyF%G_LLE=u|@rBz4t)y+}T=CIr4W$Z3sk4v`pZ`y(x0%eO}#q&# +a`h~;8 +<@T598^FcBIHp19rv0n#T@py8)&8j1q`r?gLOHQ!!t@##~a4M!k>P=yjMV|YNZJ8F-G0N201^qi*buDP3z!G=M|f;iXN=fWCUZD;=;$&7e%1l +tzpSqhB)<8U4j_l;Hv?7gd?cYI@Q8;e9xdly@_%wb0?rI10BcrPxH?O +L~y-^k7S>?yq54q$M+b7&e{TL60#_UVpxcM+pgR`ePB%K9} +{iS@cjo)Q!4p#cf}7-krCbzP({$wJak+6Rq*77BeyRtX1Y+CoEG7wd!dsm~d$-}^*;m@B)b)q+I7ORH +t2@`(dQ!L1t7SkujhjedviEXqC`>C$}0SQ}Mp^RW%H&x@J(B>xC8~ikfHl+sfoN5xr +4^uzyUSKGW0*{EW!L(fJsX;fPJbgW0K{o>L0A+Me9K9 +8^OFKXBXRRn#6DBlU^LC9Mo3adY2g9ro&|hulM)s-nVNn5kj~TR@;tqVS!jSzD9xm4PuKd7HtDd<=E^ +Q-ClICP-kn6d%!Br{LV~+%P`qKLXdpClUXYN-V_@}7xzc*60Ypp1pE5!bdAN(oEi}}dh5>d9d0AM^KJ +2lI)F0XEkZhCO*NM#F<*pb|%>s4+`{A`T+?`Rc(6NlmUwUG^^>!T7#$-m^1cFMjLFqiH^1`S^-UKj0^2TzS=#?0y2yv +P&2xm4%VF9G7Hwf9}o<~3e#a!;9|o-)$HP>f5giQR}!?4bor0l6D7MlLO+;SuWp%Oz3e +6-Nd_jouUp4kA`!JF~TsQ*G6dD3wP~3-rkEGbs5>-MOxf~uKz1C@>}ppp##w4Cyz8x8YvH(Z!Bu6Xt1 +-WDTMFxt%<-+cQ+HEKRTl}YqSyP!??Va#P^PEP}(5%aJ=gUU57lE)hg#7#J^>Wm;7NBix8QOLM&t{+wyt +Yz%l8f?<`Ks0P@KZ+xk_57PEHH=~H3>a$5qN$@L>p^};FWc8I8m1}tcEr!O;Lae2y7V!Zu65x`yD +5YrRas+$Pt2+GlzT9O8hFd%lAkNL!l>#XF8dwV6Eo&DP@6_4_p#n*o_=%yF#ZpnxntbXKsI~O#K;;rg +U43_uHEs7pBk?oRhhPP7DHk>W9CuxCDD1D#8h4e>Fus^H?VShCDF>%EmG$66j{+T%C)VpSDXiJ@4M%} +(DlKH9Ay%7<*Od`y)TYnvezb7yGG<`bD5eS7u`gADH@;}l=`KXq}xXXA_&QCByMIV*`2!l*Z`tWvOCa +Em^z(llZT69^Y@Q>NU;~p=Ml|R8h+KOmE#irQ*V#Mf4ZHo;YB*Hy1&|;)or>oce=^>y0!9k_`EJ*bkg +V896$HNf;Vt6<@gpH^;oFAI@@t$lEambEsA1vw6ntGvw$d_wIz<*^l%`ni}$WeQ0P(I3)DL1EqFFt1@ +Qb2?zScjt%!?FXaJb()8Xl2r$%6plrULt0*GBk0@wCyzt0OFVhx?rKSzuEfIp-NO-3Vo4=0>fT +r=t@7(}c%jGDi9g)Uv|fN4kr0CjSPc^s3rQK>>e6nhlzS5D|A|C|Ey{`nfCa?-}ZrE;!W@RldAi)^5@ +0M90Ceiw;4Yg@rEr3aAPL`VA1$_J@ +|Fo<=5J@^i2kjnt0$6n2Dq5+Z9=gZIfrd#Y)MiSNL2?u(+>>`;bsPq*3+#oFvaFP#Fu@zPqiGr<(6q|GSI4ku;JEmYuv +XhYk(a96`Fs6-f;V=b*@>B?l89-`qw1IJ)Z8(d)p0IufadRM*dX&F$OEs_B;o9{5Y%B0pkkh@x3VQ$Qm4#jkZ(oa8~efq^6p;RA({(djNUT0g`j-pLw*v<0v)eK3us +Hux~9-aafRhw}BC}CRaUh88pEH6-PB49?nT8?GIku7&ssk_cDwa2 +iuD|4D}XTMTnv^dh&RM+3*l|k**4f;+!MD^Q~4F_kzIEdoQv!Mk-BmT=u$|r)CW-JBq$O8e$H+H$+^6 +3tlj~AR!8?GKGLS*hUbkkn-;WEG@2~T9JiYTz2?*~Ry<(^&IcYF_OQa-*O{jr)FAjaZaApXz)CjX8F0 +MEqlN%4REhda>wpa1?(|M}lMCgRuRdzSd_T;4Y*-)c)($Oi?)UoHHrZ|yR^&-488DbWo27T7uYJWmw8 +v!PHg1^ZGRPj#aV;z>5WNG(tt*XnoGx}c6az9Zj>%K^WX`S)a1WmnW!H=PD_7cMzm9qlGM(%JwPcims +6^J!WsAHt%QdB;9=JV(QevmGO#Q${uR48{*>F~t&Qu)#9$$tpxzz$KB#&-vuRYMTmFY&_loRr-U8?j- +V=;7=IMQQ?L+S6KkvoZN#;BoC8kUB;aP_GA6}ekrY8WLBir^@5A*;LNUt3Xt3Ti +a<0C?P-{(xh1v4bVV)_VwE))E90|X*L8SA*8RQ0l7S +X~!s78w=D=ZoMg2mF2;ES!7Wt)$UpRh!}r3F>u`Hv?$NH#!j;O=dj(&|9M<23wA@MvGaNLX41@!86>< +?)-dnR}bLwCe5aSA#G%zGfeTLVKDNv@rXWOx+PN;bT{XT8wt8gp;ifa1q%$?WFJ^{?>5BK^x3mQo~8% +Spy({Mn#u2v-=5nePk;NOQh+b7&aa4m2v>!@eDcjZJ*~XkA7O2PyVY(R1B??##W32ptAzZHIA0+nhTf +D6*g%+VW!?5SR&ioqKqbp95DNLWZu5&r!!NKiEDIRu2!utS>={Q3xUG(;1i~l4t>f0RqFrA!SMjRIG9 +6|H2t=DwgWi5qx6v8*X^?`}=zOUP!MeR}*pc_-YZC`&aeS%M{tPf>k$hvfsr{B0FK#X=@Tv0c+;;YqP +W#S6q(LRCkG)#Ag9R_EiDN3#z_)T6ReYkV7$ozu$UXQZ%0r#rs{|TdKfTVf{QF!Q6QKdTLXf+gM%Pap +mskNy2FbE8)?=SPry2cx2X5Y+)kJ!caRgxoCo{UL>ICWIHTAQ1y_6{~~`ef>plJYehI0?G@84roDv%MZ29`(B{}&26p^^4h<3nx +sNs{jiLO?alKHeNW5MOb!Qv6W`L$qU-C}s#gXuPCPw+wbOw-rmuAo2B#~SrkT7h5D0@@I)MspRAPgpJ +vR3a3+HZJ=2LT88v9R#S2vNS(w-WfEw}D2GxyURe+o>~aKOT)>neep0@bc}TyO8;O#``Byq`e?L@7ba +Z>jb9&vOOXBrlCel^<)M3mTPuUC4>da+|8I0PO{N#GPoss0+e*zMNVfVg=Tvk^T&ilqbKiIojumAlq2 +l8{8%l<~ioPD4GBTghf^Z2^r1$M+u`!U_aJi5ak8EcKV>vaok2TyoW{6zDcSJ_^ +}KzL!NIr#JbX2Le#OAcc>WT!+2orzf;0;zu=rc*wg*wcYM*T_*xJsN)*-KX-BK9;rNg0Ky}3OU)WHFe +Bj#w!u6Y5A0nzy#l1d_I_Wpxxdq;*<{&SK`}|D(wLMrs2VKo)M|EE=*HW!9B*D_mszi|;CYAy)i5*|a +Idk;rv0{Sbu(n4elRVUnKT1J12!O<(Q%K4Gc2D{n8bO(fNB^scm7TUpbk+@ +`fu)AGTo6bPy}uR};cs{dzI+vp;$cWRpjG$?If=>Wc=#AT_=_+##UUFB5x6f!#QuIkg!C3 +z+o@1FBWyc+lQCr?;!9DO=Tdr0sO}9XYEVDrv5`UL~!fi)@oL2z0PS-u}@dDK4b}m>XOiAR**ie~tOb +oi=l03}+b}&@L5|zXzrvtHH9(!U8gM@DG+*`2fFvg%v9-5Qs9larAaNEOz^&cwJ~tYi*B<^m)E4XT|b +4K_Czb1;z9*H?~+18q~I1nmRqU!J*Ied7i9Px{n2Gbcu>CX`hXUiGD=X)K!&RdON&~km~}NMl#MKo~TwZenwDgE3=1-mBbI7kL<|wd$kgynDoWL*J*xP|aykb{MWRP3!D|Z5HgefPyG>*j=JMSk4uFfd +#%`i<`avt99$FszpC9Cl982iUzKh3he2YKFhkl%zfxAXOpOzWRnNgaf9uXyI6HVH0*6RNy1nBXb+9Jo!^skc;^;+sAVx?EK9j}@-3?gI!3Sv_$nV3ns*k35H)h-JZ8FK9IG~y +Yw2rroD(?>X9`{buUX7XM;2;S5ubvAA(3xROU_D-v9(Jo4YPfN>_^ts?NsKG`o=zU;Fq+GmEYYblc}V +UT;{NadT;P1U$E-e3r(p={GC<1FnLY}0e(Z6g>5mqaUV{;t?Z@J&x}oL6wospXFwtrsuVmP4uxrNwk8 +gbVZjzb}un(YGci8BAT1j-brW}}Mq}OE;>AS2zy&khv1Vvgd5@Wa1fXxYLPHAspicZU9rIOYyAPm6v! +Gq%_y}gB$b2u-;`W{dqg6ENGPHDq~=u4p3vy=Iy0>U7BV=4>4Oe>V7OG-F?GBJz_2!qVV+6x&E&uKb) +7|d5GzB->~hF1^t<@bu3xNi}&qscBi9Eeh8mng~Z^RgT-i@V$cp%AP*%9xLdwAKc0I$#+b+YmIP*OJx +Ges6t&rtSXrIs{T+()08Dxdh!z)kL5m%s<(*rV44M%1Gn!=MXFMy5%}&BpU +QP{KReVMR7#Gj%A@FH7zX1nJRBUa3U!&zi=fTo=Sn$AIG8=%VJ$Zdo_(AdiG6KoLC9>C7s=#78TxOMX +9cv`|C0cWlL(He38woQXW}2qpq;8Pk@=zNueaMS5=4w9Zcm*ZdZATF##fgo{j1w}$m%lURnfl^aD8p% +s4fy<9#PFNEz)KA^#U`H4G>>11j5|iKPSEVNcllh&OJSwAUcS4dbj-}7BBx*W~*eH=N^zlWxvLFfkvm +p%lN|h-YxifbQ)j&4u5{pgz315k;&4pGvnt+i(bFMTn6uRuBqmf>-a=wPf78pjv^M2B9BU<$@u1WnHT +*gOQ0En+J+7Y{Wc7!2ymhcQ5RsvPOzYcB ++(F57^^*sk#pYTEeb>gLLGH0YW35io3~Cpu2OSSZC>CA+w*Zm(R~DXKJJuH@P}=KQ2-$$g<>l35(M_x +${8GRi!!G+J$AE++2-MV>_fYuznofwCyXM!9AI9QLprjo$2}mVe +`F$6yb^SRQ*}zre%!I2no|UYLTE6FB3>j5ww~9;W%9y+J6!hnt%lh}LfIC*v&(qw^fm-*%ReYF%3EWr2w)L`zp#=5KbvyB6~}TvWP +Gsw^Up4t4S|nsvuj+)-o_|j78B_83T6QVrA$g{*ataUMhG^dkOA^U5#zLfhvliO*lZhHAAq2Bv+-l9B +y>U9^S)s6?@RJ|6nG#I+3^r_W{YHg!%kgcp9ECiE{N1h_!D0YM_6xekR9sOd>Q!}AabjP`f=XgxNpB +-w&2E4+NlPx-khR9571cxU`y)1*Q?Due-zmvTT}jK)>LC?GNux{{f3ae6MMA#CG!S*JKoAgreC><#Vz +TudnhPXG46edSLaKrZCzv4$PthE#qMO?dnvdIpuC|BmLs}_6L_i2{CD)r9-tqdZmF-b&=8tfZw8? ++TQSuB|9h+A^aFoi+Qd<;dz4HO?!6@cvJSFi$v1QKBhFjf&`~eP9 +2beGu5Mb{p$BT(|F(n$Ff#*71FtXBSHo-JoGg>veN=bg#;YHg7ffHlu7PDy%+66!{?um!43{nXdburvtxlMb&1}k`31U=maiPRPtQ4^K3aF;BUY8_y=6D9u}$*brgb)YFs*B +RVPeqSFwj>w2BE2b8cc!Ti6IGOo1Ohw1hw#<5!Z2(u~uKo@28E>hyoCRHYcISO};e1!y-+w0S5}HcC9 +txrd!@$n{D*t{P98B*aZVjGg=dB7rhNc=~g}(jz-DCaCHI@fQobSPO+KGvDu&uk3?Q*Wh +lz6Zi1X#Ee7eh*#mw{(urklDV+sz&)}#@K$8~ZBmC{wYHXyZk6|(wx?><1t2sl4BoX62|i`It?PJT@u +^E(TKMmm}s+P1gUK%ps3m3W_y!%M|%0af%E7+yG*r3WBcE=2r~%Ty`-My|Cta|j4C;$f^b6;;(QbyY; +0w%2&T4rW`2lMaq}X_FLY^s9>`}p;6T%`ATmcJ+<4ur!PxoRXR!$?0xt~R~l_?M +-x60R=lQyesz-S1pVS4-Fc)vCoZmx3sYvl)DR*q{*{IE3|HzgUTm_W*rK<}x?Q2pR~RnjlHC%4J@BPg +Y*uIe|Vu<$rUg5CwcA=kxiDdu3Ya918>976IT|o$kW$Pp5W~dwNF& +d?V+$Ih6&R-50mXeEMX8P{@)3usCZi?Yuarc_vrikuxE=_W-uhdkNyiF&9W5NU +&e=yT~$lA$=sMJZh7W4w^zLt2fqh7laa36nh=cQ(aFu#=WAq8K_C>O6e?Q6&Hb-f3m5kHWSC8<0m)o_ +rb&aDDS)dT%KVM4eAx|l0T;83MTt*o2>jcW{}c~H#|=wiW)^Gi#FJB_-?bGqb +H)U!()tn<6(bTL=Ww5UYt=srBt+SNsT;Y^qcjGhD4FgUJJH!qB^*JV~QvYM-YNqc86l^`!hVh=D#8u6F$gQO=KMI(Wsa* +DlhP2EAWl84Mz@iooRw}R;dY<^_IT^7=F__2qIFbTpfm>8%xf!$e9;M=*y@+6t3s5c1$$m*{elkk`HR +efrFQxf-ZeQciNCa%kWckkH%7wUk@qU~Yx<<1{IzsQM@%43b_sR3d)uON+Z@8OeK@O$- +nkX%+)mW8wd<$bvUj4(CwSg=$68<3Qr71`eb;ZgB5iE^MG3{BY2HSi84x8xdQ^xB7b9^ah +q?Hq1%g0~SB-UMjJLvY;VM%>^_whR=-IOd=O +5EfzMpvH=uUnAGuOyvYoU^3NJ^CssufHnSZiJnvjq$=#RfU%(TkpKgODecK^KEE%YSA{$W4Z#7l$d}k +U4p`RGIK4|3mZn;Piscq%2HaSK9BT_{j;aQ{k-`W{RxN!zyu7^98!n;FQdjLhT;(6Oy@a%$MgIL^#ei +u=jUTbu8A}GtcBop@c-N1PCrW^(gM7_P22_(8GbdgKqcFbI!D1FLI~ClDI1o4T=?tfclBYpxABIn8CS +fwE8UY?4CnW=_U5gE@>xX0^l|hLH%6f=@hJ`MaMvOoh16djLuGUtw5#Wo}HM=fg20ILI+(IeQ@>iz<_B&AABSWR9FX?ZE1l}2o7Qi9!!-;H{6zLRyFT8 +_jvdoBQ?88o_KiHb#aXd3~}(4ojs)I-` +TSD9qIjYP{{q9T>u8u}I7Ewn%3W791$F5W1E(jk~wvb+0Nu`9Y+BZ$v0cmAA*fYyg;~Yn!=q!UzWZ!k +hFFT#?^wkz)%UN|nFBJ-oRn)|Jz(Ji9wBD0;&0$y`tNp4(&NgPjy~od-dZYS++xOS54BDag`BG!CX~7Grnyfah@SBwNm&k +*r+h`*WwNw;pwl==H|XAT%?w`H8eOYh>y!u9nb$J+-*tgL*2F*S +mk7EZ!OxUl7fQJz%B*Ouw&JC|FVSbNOv(k09j0{yxI~xn$ZjMx5m*1#5qlT7o0ZXb`>oiz5ys_pOGFSdc6=uv3Ya6iuM(Z*_7I88}eeQ_P(D*ui#3;POWkx%6d1FG48X=m?^( +E1Tm%0Fn@J=$F(tJm1q{UFj_weQq-YflLzhhD8TfCVUE6acG!7cL48Mp|91H(1L3`$PyK)^4Kb_de(? +^JMX5M1c4*QZ3uC9ub9lvYao=FE<2;FE>sxh1RWsS08lo_%C?@8;`9duhF9z=8H{BrtZbt8d04p?-F& +u@xZsNdv!$x1^yb`496})5YZd6EUBYqdujOX8c*UTbJ#G~s(rq73$2b`WzUTpf&5NBDZ2A4c6hZ +MoThDCEwl~&2=U633?T8kvCH%6YB)P7@s+>}@MH`?+6$t{>EVQW8EQX6^X~(kyZgf`$fm0{?#ozx~&d +>gq$l^J?kCK14*xQW~@Ot#2U~HEVbG+wMTL7yAk7sE2Ojz6S!(mAI9loTGwd +mdT&iY(~1fny;E+2KhfOB?AKa>J^W<`x;8p&D^tPwn|MaRx01og@-ORiVzJ*s8W;|=K;jE+uqw4Yx|2g%`Kcb~$NB(J}wxT<86F7il(rP}>6_>CDYipk?Q +M##rxchcl&|&}XF`h292?l|1h%q4Rlv%P%c3En`%~BnwbSXJ^HZfN#mE`=+$D_};iJ1(s)E*^W-dOz> +OAj+vfxF4VS8N18r}&91%_h*`Jnd#zf_pDQ3gFzPazNo5h3j?F`J8e4zI@RP9hB+-S?5j4Vt%WTR%%@Z(8XP3u4hko4A#cbps3&yUnE4KhS0|1Xya$zHJBSi)tSa>ig}rDB9iat9IDo;~NIJ%#1tMXZxtD@3 +UH~u6n#aNX|i}G1WgzK_~c9vT)U%6k;B|Qw+_*udh6082BE8z~2SgByS^l14%u4S$aI+!c6_CGNDAnO5Etd#{Lr8H~ii@Opl&UTn1`4_~NHt8P +Qb!8VH40?J2+RfHZXi@8U +XW*4U$cq>VuHptf}xd1zl}1L1tNcx6DfX|O$f_q0XT1ts|(EwNgc(#8r4D%u!*d(AzQp_M4(ZFt63CK +}KJVXD4Wev;0u58rx=C1XVCt4#`+Yfz~Tt!j<^!hW#BQt&R~lK@tq=RBMGlvKPZ>{a97+@J`%_pT~U9 +{Cvbe1>b<|4-YyG&hcIYhvT$Ux7G-a#q}wS`y!yI6wfDlzNuPOjYI#B9S6WB)|elWim&eIHGTdBWw>H +(e|h#?14RQ4{r42{+4?k9^Jp-wbp*E1(D(-t0T^dst>i-76@$Yy`JB?kJE_-9wKYJ;1^QjlfeUBQ8c; +;f&Plz%r4({gq`RUatd4Et@NPsJF_O^{WyVP&)0B2a|1j=nj_SHh$QC{%93yMBK~f6Jp!*PFr1uo=_I +OAvnPPKH1GF%ktTmes`rPGv@Oq1DC1^QDw4pd-DjdUgF>0DrJ)U>WEU7T5On +E}<^iX=i;7ZI0vq1kH{U`f5=_gYKvqR5GNmRvaotWkGBl-K3%o}mg;_!VPdlkQUwL*nXE?vrn(&77u}CbIz$f2Rqb~d)+dR{{(Al^1SAY;#B( +KigZKJ<}ueW!rKF^ZksEvlmhWZ2+?9`OYD&UAC%%TAq&QY_E8U$1Dr1PELd-F65cKuQtEm@~NnS7%jC +S+R}u=Zx9{raH&Q5bQtK*wL+Vd=;v{Quwn=^y#pX-U7v7E40X6usq|XtYIWb8{@T(H8XUs|llMShS}< +z99)D_FbDD$$GFs_iV_R_a+t+C9s<*1`_R1<}lpf-dtQp$U$rX{=8@UdC%VBP|s1--e4xb#z{PowNy9 +e)}RuUGmlm#3E$3U`n&-9i@IA<1_@qv>8h1V7Upu*GxEeL)#Y%O%#lQ#0TvN6Pa_nK=>Lja2~Rt2YB_l?AqpN+qu~k3_Sx-{3yyv2LTTK(*SRWO&B=O<_x05b#15w`nwwPmkEc=_T0{dX +ywRtGT+?Jpr?9*U$ilN*0?Qx-PV`u7Oq7ZF#G>;S+JN-zQJ#kSxFy^hc^Qc{HMs<06|SRED!QbIv^!n%El2m$1D(_Y@+&GsX`KXJw9574QtOOFMu_VYcX3m +u|elJJ5Q0gp6M%wUYz$Wyo;nwth>j<;!ghX6^WL#tkyfkbHY=LbM>;$XbWam_28Zp*8y +y~4_sN&zX?o>6GOMjRlk6&gsNCNLGFUa+DFNa8HKlB8UgRQxY$zjNahkA<07{iNi8NN+PytRuO1){v` +&wbC;ivaLm6^y`m&h}0m5FxTF%u`JWp^@}pHGOP^cjS@`78z_#+O2538v5!R#9QX3X*QQUadmkcVNJ9 +FUc_pm&2an0-^3XuY+PZ6g8?3*4{7q|U#y9|g|=LXXoYCk +Re1ZZdV0IqgJ+Lb`l*!`iKBs1rd2EZ07jda=4)3BU5kGqG62fEboHmi1hjI(F*b{O#5_u&LPoabS9aq +G`Y1H6k{_@>z^u^YfcGMSk0ss+_w&~=`}ah9Yb%0F|!X-=1Uk;G2Uxx$i+DoQ;LUM=0wmGe*jNMLb#9 +Jq1zV~+g6?M?5z*IQ)I3V47#go*bL-uZ&EN_t5Ff6#?KnqA>{*~BzN0=->aSWQXbxhZ_zj85B)9ftQR +q-2jt`kC~!sRG_-rKq`*`&aSTreyFuhv!@3*-1T$4OkV#WyUbR-_HJ_bZooX+diwTb$r_3Mr8VuXd(~ +}^hnMZyCtJhK;1fPg4H}5fjWc{P623a4ZeS-9D?Z@8*Pt({1Zc4xCDYCzQ5U4Z8d~-i`BiTI$p)!*sg +A_9^hp&|XW@4F0j;hu;b7~YC4~fLIdnxW!R_tsT{17wtaw7;AtEc|Db4hFzI--q71%+6( +@LZ5worOL*jHm?&Mk!Qzlv|2rsA0V?avRX2hew+QsCD2a`IS%+9;NK4}Zq9D7@WTL_6NwAx +s^?FWLMK$Tk3~+=uG?6x^dcGH5h~4twr>h-xfXW;6!Ua36+jQ`?r>0cB+YSTSeqrNJOL0A)z^PXj!Wl6_@O +V;a=bW8pOfq|SHL3#`q0xdU+_@zdwTEKqp`bN$#17FLOi?JA0XNk*@0jI{757aAA6?(HpnhtFbK4EIL +tfE)28TQ@RFFKM+=Y}k4Cmn1nXuI_f=Ve11q=rPD^}+7r|!+4uiwbhhF#alNs9Gvp@?(dFz#ao-Hq?G +3?e)cLY3*96ib}ETU^4Ut0!M(bzFUPLR>G-EU`&$v*m;^-K6kyj$ +zW4sz?+Z_l_q4-7l%F5_gukggq4K`XeyZL#+fRurdLE~lh}B}+{!Vco%KPRyp)ZqE0zQ)W`;Fn}1Zgo +`q;my4jX<%;VpXG3>7W_JS)NH?XYYwk>j7&E^6V!0mOL8Z5vp#%*lv?Mgu%ej%p@EbkKo0&% +fIuPN%tE`UDuT? +h≦{2BATe-{j0HTplLzBD7qEzoygEtzd+BMr2z0vQ}sQ`Kif3QK3#Pc5#q~_;zPJ2gxX>88-1&{T^ +@&{7)lok+mk6i*?vEA+TG`phR&RLG}hRk>)jZ<7*U2(qXDR`~K7E7jkT5oP2cZA`7wQ8b&_i1qEv~NU +uXw=OA4 +#_+EBo^-FZ5qlVCC`Dt;+`FZ`g-jP=EK#s{&CGgO3&29S3F@=lF?2y^jYe0=uh&osdjpEx~C5?&Gw?4 +M!7AD)72foC-{K+f{Wr9X`ZF^6dZ(^hL;Ir$r4WFQ*U7DWs)DH87&_Fkh7OwXID9>{!^|(v*jx-R{wy2}?DbQnEGU%QsioWT2 +}_Y+UR^zRbTabZ-s~+^`du6g`1iKVgqZ1H4aGYYc(fw|QWsT}dd +*I%reUT1WAos11;hvDXHG+ZcRK_7hP1w4-As;*`I;x9UGq0?KP)9;BV#xH%Mio1lZ$zbz*EQrI9a;!H +Z=CI_?~`FpRtem1p!Z^w@wiG20Jc^p`0Z}`Mg91Lkp;1>UBCAO_GBMGunG!4zwDtd|HSJ-)TXT(}OFz3V1Npj6JSpdl2uZ4EsX;~72vD2XMg?lv-Cb(4Y^$ZE{1#T3m)mHcJEst9q=7F9 +a;Ir%}Bcy8Z{p^l}Wb +XdvRLnpsz5BRFU*;?5{(vv+T-{~fjHVH61+BNMRBzU-Mmlj(j+-&a20vR4-d7wSd)8OL%{`TWd +D1o_48tn2}ek2J`?sFMwO!HAPqN@U&Emt3}1#lzn!R_M-`P~Ga3B2yz?d0rPe*X|%MZXNliRbp+`v=c +lX6zXT9uN<16_VJ7unutkyuKUjwh!IIODu`TIk&Q>h79VoR +3g<vhk5)C1HB9*qOEXYBDXy!;cS?Pst8bCDo9XOHO)WbxSc?ju1skLpdr!G2b#PRHN=mXCZx7?Ui< +|zre{*&P77qfCkcZIZYO+7$8m0WXFm{>+@JFN-x#jKv9iWJ|{~Z{8zwLlNb((C=e{|{qQ=l@d@WmfU8w!nAumOP*iH{ +qTWT}o{c|AR5TTs%*M&0RNV2#^+l +l1qKKFDt_B)->dmdRKJ5g@tRn6O6qZW-Twh&NS7m2$WjV>b{FZ)Iwwp1i&Be9vndY#*joaH>Il9 +H7FZs9@yWf}kYyAyI9Ul-Z)e4zbZ1iXQXES$OV)|>GunUt7`9Zb?k3l0#)W#Y!O?ysXFiKnv(nCOmQ$ +X(oA-Du}@-C^UUd8Pu$;jXJeSz^r)_HHZ;@Jd~(>&83g;XH#?@JO|>4!D8VZ85-^vNhw}W +Nb~v^x$+%V#CwxGE+)33mk^_e(3U2X4_>vP6~GM)-7T9Z{55BQs4&kL(>QyEuh+>%uBina)K@7s4(%C +0ZA7ucnrGY$Bx9)ky*?F&+t7~x<>OrQMhhUWPZr8&hd6ez*ETf>{ +$O@&_s@FxWnRme#eb)oWj@@DV-*;o~IUgh!Q*~$#Is(-t1*gye@Wv7H7eNW4JyaOK`*7fMHv1eh666cOq|4$^(P@Yl;0{J+PkA$hPL&Qf3bJJ?Aos +L6m!b8$e6Zi;9&C{j;s1NTIMVr>#e^Cva%J*i1?gok+944ur?$TkJ7f%HSt2r>AE)16>R9m@4QLu4(+ +~(Cd>7UrAYEgnI;J|B30C!1sqfJ3v0~^GZNf6OMkK#F9*GVy4luOEFwP0P#IuvgSYlGsp0#tuhTa<5w +(QSXIO4=3h3@W)h90fZf_P2GCmlAE)_i%s_eYYUvy^f!A+cE-y`y`z}?Px~DB2Ur7Mr$Q7~b)nDt0j`U3w9L>OQ6ftQr==2ZTH7D9; +`6MsO0Tvq%j;am}+X`nNZV`BVeTgZjo&r%7rqFbOY#F}7*Iw@W2qm8zTI8f@j+1ZPIz8N@a*l4x))OW +PR5rfcboD_kt^-5gfG`kbOBtl9K6sFLXGvqVW+#cEJEg+v2v3tjJT(;V|wR54v@ij!~#>G|H|jDq1jD7uY%T}k +7Rp`bGr;dSW$aShXOJU(nB@mx&ttM{1o9E?xDFBTE?V2s_Bq?$UQu8kAq8i?RiBqCk4H66zDKky;V6pvgSO+=hVUtQfxPR-*>-WNQjGw +R1`fFxPhnkjCiydVc_2)}3+Jhp&4l4v6A+HDvC+mtamJS9(8%@+pQ8!OBght#H1q9zr2n|IWWbC!ez= +xULz^ug04T|u+^<~j{URmd4E`1yRGQgsyY5cLmzGrYm_AA5$MFrW4bSaL?&$jg4}Bx!Z+hlx$gVr$`H +vbKw`o#MoG(S_HYfu@orLk&EI@Z7dHz-s}N6Y%b3eps2*(E$4FfbGM6Wlt41*s9_o?(MOPOB=Ad1uuK +t4VFQP=0beFc%+S-030-rb^}-XBej*$RuP-T^*x+ECI#lT;n7V%Wx1{CMk;4%Io-JgWpAlG9U2%fhj8 +C1xwcD~OegSh_z#1N8hQ~uynqo=xai?kuU3q_Xo(T&sgt)lyPsw8YyryMBz|Wr;30j@}E-5gqq +T2atKsnqvG}^uqydEx#-Qt)0J1Bth$N-N}Y>$tgNRzpn!xq_BE)f78AdL8Fr$}yTD@ULlmU<`bowm9^ +!Rf80;K?y>Q8)24j2ea-M0(>U4*}*Bb4TC=EZD0!E8!bez|+XPn$Y8^pNNv;?xJ{l_~-vL9AncJ1P2C +rmrr_p{b+~#IT1M->0~iY%VimM{ecCFUkEOFgZ$}fqLxq{Ddl-x*)*|bE(-KxH_q<5e=J6ee5NXg+{> +|7*QmE@5*KHh<1m#t#6irEe)s|zpZ-IYH$@25}jx9=426mqOQvh%2#qAi+ceofN +*c$DTru7F4AO{P8fV(VgOxxtke(??r4LtS58lc_+{Gz3l120jEO&}r^xj?|l?1Jp`t036l<{Z{-kRI$ +FmQ+Y7J`*MImYLm(<{s?A#xQ)}Q)K_WD^lL!b!894I)1A~Tu5;6Tl0M|cWMqJ+5Z?qz;>59&7Ov*s0H +!Xmzv5iQdGQ2?GrX~{Up4R)N(wi;UQGwUK%q+qps*n|9%hZBiPP53VE0JsPN1_)|0cT=*bkT_9C7ucc +yz`q0H-dgk#@^V2WJIU3VVG2g{z*r{Q7CzE$?b%C7GzXDmMg@wAxJ9l4!y2|$#ld5lLup8c0 +GE)990s8k>Zmiw+qL+bbpz1mHn&`!yE@DkOCh9E4q64j>XkhkV8XCG&te7mb>l92=13X04$L@md`CuA +<|E?8C4cN=vi^v8=ec>4xsN^io*J1OA)K%%1vTnY*tTEX)3$Bjjj-riiY}gnW+qg}je4rQTtN=@mt?y +DU#7Gp`ZtK(mF;4S}1ekxM6ue#-K!0r!uQh#%MBp!t?_n)G(`>^peF@A0!jG1d>f8`}p#h4^&;)dF6- +WDI&MH}+@Ev6tC{aS^)2XasnHa% +@y`ob)7pj1Y(TuaiEl?u##IPVPSPy>PEENCP%XM}ADFl!wgHCDmRTM!lrvJT_uj!k9viqQCh?;L_6@e +-oMB;ZI49?6QXnga1;mjtRGbtf(-9#wP^6N0n&salP}!<5w!5)2ISuqB*U#oWe%oF){VR=Y +`BmXC2kIdu-`k=)VkhoAhb&)5hly4Ju`$h|Yzuy}E0^6k8)%byTzD2yqZoS`Ruvd2FH7btWa5~ +fzxwkGLAfn!mUcISVR!0a11t}UtYM)_0%J7-N)v%a;av6T+5n9-kIIZl7)!23Ogf6cLuxoSL^AKnm@j +wZcGfOi2-fzcugwl$lelk9#lV#Ax`okOC{_Gz0J@g=cpMTwqd06P^P`kJs11Y5_a-4j7kfswA;**s6 +2o~o~UblLe4D#w>j+HTe0DCS%CEV0cnH0b*@ur5dIO>wJM6CUo*HKD^6(j?? +GF6!R0X)jLifVU|a$EtUlA4Dp&8M_@!onA7RAfOVF~e3;KH=$s +j7)_`+X!hW~#YPI3!8M2pd6O)#6OPi`XY;317a4H-6nA$TGSY;}&zdMnXF|Whl%(V%(Y1BDA`4V +s^D)msqd6pQ+L5h)29Bir?vqtc&jBa|9n5{bvN8MRB!kn&+~2WPRC|ge52#ogTA4e>< +k1#(^HyJlXRjmt7eAVw6u8rBXXJ;6-KP+4$0na-R>M@8bE`0*_Gt@cxiOD8Bsie_B(3`Fj3-zuG@JJ> +jj#?$3>Z+$MzgZLx(*-BHFL>-LNkr5pQ(avbll9x8axYAVDcmEnqn8fo +8fZN}?C#(c@H@hY*!r!2q7!jn0xke-TTH;yh(`LS-B9?oCX*%nH+bBPn9*E)i(bII2VayuZhgny)8>{ +$=<66Q{7B@4g~^|3_7pBaG}6E$WVb^{*{-BTkDs^AH5kp$WXh3HN1(5OIS{d`LuvJXPs9i}XElI8@RN +ip-6KKRvzx68!V!00Ya&Xll5rc1y{gM)lKFP2BHd$=p-d{iDVtOJzHC{&suTz2;2Vfv4CB#KbyIer@< +DZ_0k7M;DK=`;>rIy_tHU--N^2n)dK9nN?F5l-K<(Mi^AN#iS>7dD)#fcRyH#uV(fMMK-w1x9MUc7(fPlRDY +$2&O&w;EEU&4eW((f$EGdo|J4cyiY43oFx*v_BAq)KBgsnmJE5<>0+vO7Y$f(z +O#KnuGil4fr4Rpyu->0MajS}e<_sB@78da;=p4OVA?GO1%z9BrkRB`aU6`27u)~Q0JY`9Kps1~%AfKw +p9tVi3>e&QZvTM8y3mg?cMP^3RWjuOOm1AXBP|u}X@7@}(^|Bh=PZ<-ot<8O^R&?s6dq5uTaO0v4pl@ +K#CXJPr@tt}|8M@Tw#Y(zb7)7n?C;~{2tG=3ei~49g;E1oCCVScRh~T!$4OGCY6)$D*{f^qcjVlzcpU +5Kx40U`4#055nFXW8srJ@S3k1LT#)TS*oQJ{Pl`d^1pp>>XB}yR5KZzh2#Y25wE8tG={$X)AQMcEVU8 +pyo&mh6cWp?h^SHsj+q3*r)_@X0`qIH&J3AGfGz`VuiSme`e^5MhqUbV6GV-c4E(7=<^c5N5qGpY#}| +E6FN4DU2|xB5XWmj$K7RVm+h*9wPfN>mr5jQ$C8n=2?O@a29wUFZIx{yHl7YY6;KyUl2Qu+QJv| +TVA~6+LTqGV%Um9KkA6zy35`j$;9aXa)`xu0w@ny)IB}!iI~SEe!TuA&?!WiY6xhJW*o`Rm-thjb$mn +t6D`q$Me~#xjO6lcSvcVRg9n(#2C|ZPlIy*lQ@7I9wZ+I$4^DhOMLEx>eH{6ufv3&LDA-$3#(;K?XaU|wcG +DKoYn>j2JAIHK(K^|m@eRfK;#MCmZ9{+Sn@?EnN2;fF0HF%gBduTM~@N$Q}|*oGSQb!UtQ~mzGbh!D|}5meT{+mJ-a!ckW#(-(|LToP}IO(V-L2us_bF +l-N!O^b^bk>2z{34Pqr(i0`K|Q=DvGXbXq%pi@}2i^@M}`a*-pwl~U{tA%>@~R=*L=m5Nh@``lt3wm; +0yIC8~JPl;)&t-z|}KO<%hwB;Qreh6WxW3P +_m2uIf0SF8-FxSGt8{k8-)>f^~ULZYz=G$D(S#R# +1_~s9+TqCzYjr?ae%8eMxyOr%_x43(@N2nzPgst&P?N)TPDR~se^Y080i$H9V^C4Spv!{VhO>n{eT +oi<*gjGex+W)>iO6e4p5TixH=MU(3i&Gx)VVzupsJ2fnRX&~Tp6c{fSE)BT_lYeD6Mzi@A^CPJl~ETc +L$7U)E*m^Tl`>CEo31k@&LhT8qa^2A)+GT>d@y0La6iF)BhrpezBcowu$C!rXU`t*pHEcQYitUr;(~Ys#Cv +;UWNCtPlqG}?6zAX_a{Ow^)k;S&_`YyZiRV|owo_VD{qsQ=x)VRMMIOq6kbOn{ob(-U^of +FXr0?#09ZqV0wL5@=Y;`!t@H#B^F#93PZaSE0nZ@)EgYk&as9Nx?*tB&OlN)=z-wI0 +27+R}eH{KI%Ckf^f5gaMUfWTivu(F+Jv`d-mA8Z~xh^(VN1pI{xKk=jo%p^Re_7fi%~$6ooJsn)c;&X +Et7}+2>&XAjcf7L#w2a4uB0>zT_Ngf8T|7)6f$jA6;tl}WNQ5JI`W$<*HNb`{-#Vy;f~~lhP?r`mcsQ +Z5xw=N9Iyn*<)SK4n$uXQ~EUyCh?yNsVXRQXFLRkzprH5X`Ix-xKEbpCscJ(Sj^6qJXbz(@4wsqRV@P +^=`yTYoNd|I(b_0^S*aGIjK>p_7sd=r?)Kdh~-z^XD@kZYbE@faB>`0zMKDcF}Bu%**F?VNCmfFya$z +xOdlWB_Pu>Ym!-@O^7zf}#Aw@dr0nnLx1KP(JBYa&EW(YCyEzbB0%U5)YrF(Nhv&{)7P@AQf{k0>_NE +CNE7B0J4oo3r}5KYZs|0s@;z6(Lc+)73!)-mrZM(HG7nr&{}Sv^5VPMVJ%BOUR)hpdUa>kV!Pl>8c*PrXzzvsm6z5%{KWo{>#7dN&NR;Y%jwtL0I+WeXKuWsnai^$%re9Vz7|kBU&? +ahCqab(&rH>>n7MuU(i3au5(29PgS;$5tg>ocGbC74<78oo1r#t^h8k-PvQKF-U&NC^vB;hc@CczxOW +vg8;-})CXZ%NOzyTrrr|1k~WE~091pQ24D2(P)4LpV7Ec348ayW*cnSG5P3r&ah3;fbc_J%{}5L*(NI +uRZkz%?t&vgd@YHRs>6Y%Kt9EM}e|d_5dFFA)Ioa{IGG?~8YPcxx6_w8C77Q~0i|xqxWhmg|ea_hudK +q&fs;6^LlqcMzFl#o-C17_{I~HM+U7jm4-Qc)dfTO!tp;{OJK +H!+R16uTyZ;~GO^pyWDY4THxtQ>7DYZw+&r?7?af(KvuzyIi-(Wpl$2q9t4y<$OfHZ&m-No0hu}&s5; +85EpHCd2ypPKqQ1GJl>ekAPMi^r(|G~`(5xSPIH7|vY)(Y?)IMsQ$j(CSDf+&tNCan&Wg$$2`6u60C2 +0UjUG0-ONQ2)iykAFlMxMoUWjJRIpfNqsNc!TI>Y|iodU1Q8s-)H#E+EW_eC~g_mcW0~qnbSD=Q+*tS* +eUe4UBP!rPk(1|4Y1=;#ZX8Sc{LzzKvg=T4k(7=C4ZPGI3`06(8(pEPMlb0(d^;9+g#1 +T6R4@r$Q)NE!qXF<7qJ2k8@ZJVLQLG4a1>zM3aq%=i&nF|a=LvWk*&t#g6e(m6OI3O7fF9I|cK=>Np_ +**r;%!8t*UWwXybyp7mJAO4zDS)>=e%I)o)*D-v8r6~dk%xD-Pn@qKxg+oL*3A>EE +K+wAeSsM)&6CsiZqE@UxRdif$&xZI)#j~%JpAsGEXI7B{wO>2x?}Z7nl+(Ib#di=r(Tjv4OC}II8WlV +qDk~83!0+RobJsO`8u*hw)TvgfhgAuJ|)F`VM3?`Dju_SZ>KGIm9T+~q4Q=y>{=kw5h18ymSbITE`bh +!;^@ZyK6en`EkM@n2p|6@jj8FcV3yeO4Rl4-^OlhHL%#}Cbm!IDQL7{QBzD*?TE5V#H%Eb +6fhf3n%Nl8gBEq}}r6|qO+aV3J6^W{otv{hiynRX_594ea7tcDiTqDKzPXgSs?dMru3c#L1bQ*}}F5l +ZB@DEW1%t>(98-)Wzy?}0aw{J(j1w73{lOqkzfsx;0#76JsM +{yvK~N8aVGa7>T1l7jXevsJic_pg}kXwg1Nz=)%)DQ^3qeuc$iE1k9 +76}9G9@k?xs3j)&Acz1qyDNeJtBx!!@XlRFmC~d#43w271Xl(IEmPocOipe)UYH5$IZpl_hzD)j;Kiyr}01|>~I0=SxGX(=S^BB|4< +0lO~g`BwpB4KMqjb?9>vl>l6-#lIA#dMF3FLa78-7rkc*#z5FBJeEI+eSwkt=I9Ni)1c=IrclDFr#Q= +r#)Q&v%%jU>Kz^{M)-n%Ar?ir>t)12MISV`CmXH?ai+U-SYUp*e=Mj5{D-(m1hD(Le{;cnS2Z# +nYMgNgLCIa0R`PXzFD&Q$33USy0;mcy}yD{vyS+x_Xx=IiI(3BziEs+H}C_smwNT7?^U?g{f_uvd4Cj +~tz1FFwWr$bdXPQNX()h|Ea2GPAwAB+L!^I0Vt?W9zIM6_jogzt;O*Pqi0VY2p~8ip_XCao&Kr9n}|! +za&0{*spQJTC=MWUA=B{iu{>zbnj?Hy=n>e1#kKbw3bFbpWe2RuR%O1x!F*8Ntk0G>4)eo;LXms9nsl +5<~#|_oNMsf5K4AyP9&ksu||pa%5VC4|-0pO$HK|;0apypgBEdsRU;6^@KUObrx{)rpaDdmRNv^V)RV#PxH&U>#in!Jk1Ei1k +g8&7W_JQ*(=rtJ@JYMmxD8y1rlvxQi#(`B*uK5O{>d_M9$9zsR@B%{Bc6MFR8)gZ1Gp#{_2Ol58OPg8f_DZY^)X&`#Bt*BwuC;nkvfhfqRv(fD|MtK<*)y>mR8kWK_G0p`j#*F*)11Ty6)6Vb9G%xzM +90=y!<{TuULiLPG7B9dhf*)Co8&@XQEqq_B=~h@cFnYY?aG1Rz8)P>er-zmucSA**rO%m={7~wx%Uak +aP+T2Dn+9X=yKt-r0QjQ?oQ27e@*PG3k_{21dg~_LKCHe3Odzx?D`CDmFa4J-qb}ynCb`PKIt)Mvn3X +tM;2Jp=Ds@JGI3yUw#j+d|!Wt3W!!R3zy?2o}?{-xtL5AQ&g2XaN}&5%T@e+o@eT3YM_5K|I$gumb-k +&^Efjblp{{Yt8cG1Tl?}ERtNq9N{ux@q{OafS~C*`jK%~-FT(+4trGA6iMfmk_|_7%^)Lz1FX+#YfE# +4%UV@M%b2^H9q;FVHbP{xgD|}O!42Fx4d^&<#SM7WZsOoJiIkRXcUW0da?T$GBuNaO<1dPt(YG#CbrZ +zL;f+ecKBn7UpN#!Sla^>?xhkQ6k7w+_NVgNid!U@-pkW{!W;&HP=XDT^BKXDLZv#&bx3O7ShW-|o?T +jjuN@QRNEWV+FQmPjCVlix4=+v^J*HdR1Sve|Z4B-yOmsTvHL|UzR}q+L-J;eaPP{^c;d=W7phyf1b6eLyaj +s*4&riXyF6&{zVgfL(HH%82V0x|G8I^Jhwm;ISqVx3Zm3qXQM1`2FiFZ~s#^dNI7nSXAftFG8GFFpET +*7P_wq#{fvP7}gYCI?pFV@)`;-n9X|YKvF;`7xCo8H@6WDX@6Fj|VI-p>~C!mk3N+GfMefG42T>zB<% +04%M<)X7APYL?2BsfM@qQp+@26%wnPkp*%*)8Rh4@j*q)=3t)^P}?J2(l)EwKk9*(c*5lC_;>P7d=7|sa-YepxJ$vlCr`3X$9@+m3d122c;`0L!GW8smV3!!8~{XLCg%l +^C=rE#W#rw}tk*XeCA7mrj7NDGdOr3Bnbpq>ZpDb2si0gk-q>>eJ{E}ba?*gq^OOgBYCVR*njRDR^jn +E}Y9$O0zsOnrJqI}N)cLvzcJK1lU;OAZVJ>Db>XaWxDD6V`Q_E#?v^|2~Z4oe(}w6Vor;@O*)CSE>pV&cmUv9$kxC0NZd6gZ$DG(b1h(NknuJX;_qYW(Pc2T1*j6S42U<<*U +9q9PIdF?`o#4;C066zo;N<6^MjCjRprzN#k$JdM~XuH|}r2705q*wD(x;|3&qNv%(0k6NckoF~h_v9>P)-o%w{Mxc4I+y8v~@%*D-OR!R^RWw@Ka-Cjp@Yw!orCFoShmR%C?46-^)&le@lH +mG% +o7K1U;Y9n2o3UNlE%ugV}L%Ja+m{efZAAfV +Ya-qXjbhWB`*mvU|`F~?*Ym-96-nl{q?^HQXKQeZSg0=B0dY8mb?{G1NQiE&RVz$25Z6eO6VA)a#&!( +7jIW9-DCfu|5_)G)`$FVK`4S>_0DqbiPz^Pyh#rDu*;4+tF)H +(M1?(*19agrtaRjuW7(`IddP}OxS7Z`=U8)FE6HOg9kL0l^BhA!oB)rA^=IM}%T +!+!g6G{;szV1>`d~w-^b+>5a!UN$?7SPj`K!?bYD$?y!I84OB{|F(BH=*k0f2^q`^sUfqI7lqU*@$e@ +cIR6%ccz&82?{RPvQO7E6d-0_j9VDvv-`-@SE+0_fG)wBps2l%rX2TCZ%WTBMM*#e9ARA|F~c}P9K4? +SHQN#aGFMv;K7a7C+91CNkcZ^w0EFZGk*qDZdbk08q}0Z$?Ow2$i|lE2RSr-3{n|0h%SU_Mz?)nCjsd +psu@=U21UIz7!49`Ew+os7+YalXC5lTv +#o0W4iB)G!4XCpwX~bBGDY)CQjodlB7@Sk}ltZNc#2hpr+<1QJQv}o~*XA=n?g(bHO=)j!q>UH#h)Md +KM>DGARlrlID%KUcoF)kdQG6PD8WAx`kKQm +u6~5G-E5ymqLWPmX4C(TSvht;{$C33xVruCjN?Wkd7WXhnaS#Z%bgV*%Vi8>Z;y<#0?s*L<#kr&0QnR +O34^ogsK(kHLj6Quw{)T!*VIAbtZgxZ%=36ZBg=!5Q|$0t`C5%B|xB74Hu{@!(tp8wcmxq8Nq8D~rPdY8!-7(PjD&0K_L66J)TR(A;Q@FC@fYDzIIjEAOa7drGK|0@q;+E +qTp9_VErHF7zmn;?jM9aAO_-Qr0lvOx`qyW70+V3iR_-Y2ro{-gSpps+4`JmfB{>(>_!eW60+r%s!OA +tcU3h~=^SY10Vj1^MizWn~LS_RvIT9!JE_xA&@^JqaTOvpZdN!D}8060h>|Y!>t?lGkP)QLno#8-HEO +iQrXADAoXWHRZJUJ1`mwwo_QF{a|hJP`SCu$=A8Q!DXM&vP>z@_o8j?BB=6lRHdt?Qj9fPEe(g3)8UM +3pvnli~%(;Z83CqDvZ$=Zj$n@=nX$$(I&y0n?1U=HT|`B_GBU3CzdfgECYpC5B0{F5Lpr6w>THNI);>YxBFw;;AH +M41h<7t!!a3ok;Cso8ta9a?0I15vjmlU%rn=D9s;iR$) +W(|l{|EFSf%g$w(H*y%)KjMz8N*t>UHORGA^|{$=HSGeUC1$XZE^vdxx6QHS5K6(r@u((vQEo;G@0j! +R2Iba^}Qarg`esekho-;u3{e0*lznJTZw?3U6<3Z|?3s7V!kO1OOf&PeHTPma&s_RV%0(U`$Zt>C)8$ +3Kh!4P__bS9;*&sV$&RL?%2v4bMbZJwvl(r;RK&O1>NQvs1V#d{hAJ)0MGE9#Kp5B7}CI5N!o~2GQR7 +WTqGq{dh}fyaOwW>vxKv_$ef%D^?i=Hph@W9VAHPV#)n)FJ>%t5pN7^4AY(ik;AFMyGK&f1T3ofcxF7 +h}kw5D@pvZCX_ll^`P7gK^CFPq)INR7Vq0YM^!d^8?3dLiffl#?%yy%p-C9iJ#v~eOFlB-x=Xi&YiyF +%Bf3DcxjkHWC)2Uu8305=N{u6T6mDgCa6I$JKRMqMPpZ2B-7O|hE?{!RG#{Ptr%GQcCm=5LCcOK0K=5 +WM;;yrS(6tggo*9X%!I2?NNfIGNi`tkFI_3S|C_>bfjIbz5n4PJ3+`@w<*kNq@!wbwpEcHzU&7m&Ch` +9SFGR<;yrnoh-xXjisJ27d?vUV0?9b19zIUD{|PjQu`Z$_-gM!IWW5i2kcb_((2mkkH|*O>)9q~D373 +gUVKR4A<+8*;1SZV?>%!jH;+V8$zAdk8{N=>%KB=8?kSGO7|zhAT!J8{p*7KK?5EWsn2+~+c$*WShVy +F8+D$&X#>)38XtW4CgVNa9W^B`JVvF{_F!s`Hh|(2U?s=iNEd^F?Vj$H$*=|2-Hbp0f+uQy{U{m1{yb +9ImAKH|g_gjCWB}&YYo#u8nCs!B&&mj5@R!S26Z^{aS;r^b<3dr&w{VeS#O%sYN@fSQHh7wdi3}1XqJ +Kc3xCp$%!;4|5!F3`OQF_W*0BZZkkUiF#v&Q(S$}vV9+sZAjS<_XZ$E&m>A6c +M4}hRro$43bJoOtyhhEQa2NjtZqe3}I3_ng)7G)b;(F`LYMp@bEi9IKkz^wYDod_a!-RJ0eY&c-r}4L +|gIYDKTK&oc)iRg;<(f*%H(n3e+8(ef8^f0xi0~y}WlGjMq@tyAKj0s>;GOX!{kmr9E>}!hbHpb3a6F +sD$a<%Mr;&WyO3XT-$d>9(771)f+OX=?!nV8y#*LGgPCklj%eHJvGrD#`Hp0*WVu)yCQ)#M;C-@fr`iR4ax=c +WMzsDb~epMu>}2@PeWTGpmdo(FUnX|!x+HZL(9#gEvw!!-$j7xc-UE@?GJ4j&a?S`TG?xLPme@)c${C +Gew6M^)K@n)q@?+{4e4J-SH1x3Q<2)eB|$zDjL16w*Ew`o)x6St?W5DM_p`ivZR~>(?!a-0REt6%+@- +jIgn2Q@9t~C6*pYyDcX^}xUK^SDc~6DjB>A4hUj1DHV|v19&iDBSMOhnJ{KFk`(vh)0+Rx3>ZVS>*zk +E)XI{8rnn+mD>a*_zwb>Gqu+8x2sx?w2KsHQ_N&xmf>08f!G+fK@=0JM$7{-TxO4r*-M7dqibzYCu`r +iz1o2EhBfg|rV)U1#}sEh$=T4+2*`2|&1-aBTFKs&(zIAV_?BF$JQ6Ys2}~jszt}KS(&@MpU#oULEL3!gDAK9w@8vfiQe}5e&<-MfNlQ3v23ccU&ac|!W}ce!>;>X0^Rq +~>wco~C#NS|qc4h>3^f60U|p~{@6}0=X6fwtHFl7_fr&0v+=7ZG4X_`U%i=?^a8q9bY{e&9mJPrrz)uet{uiVqb-Iq8c+rf)Nx=2ph_HFy}BiVtKx +{VDHxS|}~UPnQ64?lC97~l~qFyvfn5nlg%ojgsGWL}{=)mM3T8e3$3Q=D;Iru~3a5Q!A*kIxKvfY_(z +w?yrjinA)ufeE^DKLY1;ptf=t*7+-=^)O! +$I&X|0q-lRZHsFm0+9I?|n*HP;bRHN?IvB?4!_k0e+;|iHN=eH4UhXh(kg05O8w}AG*BSL{idr>mr?) +IJm$ic^nR7-^Cu;pI59b$y=p|1_5*U45jkbQ4`{X5urGP5;kbq~hzhQkYu%zq5xJak1IXkwR0r&hlg!|R6{QTKN^hRp~u><;o-fPoA>r0=>TS^0L@0eBpdc}1`b?nV7zn{+ +?@DN>1hI(lIco+YHABgz#O)?vs>>vZo6AVPWd$c?o}PJbTTVo3sG7q`l#QZ1FNJ$MG=VxhZu6dDMZgc{y*e5=#PN&F)b9Wl(s)tRqq +hPJE23S8$=0!H0d13T`-PG-+ZAGU_cTmw&`r=7*8f~877l?Y=Eaw#qLpZF6$J&(DZl38$tTO7xL +1Zt9*|aZIIoM;mr|u&0Xo%gv)YUQ=?lKo!Yzo^YeI7!rP4#1fE6wT{lG8`dbzy4DE8F_On}ECU%zmEj +m0qygf8^V+UtRp(TQjNa!r$DWxOzHBf9%Ipe$9slkP$(3a +PKJB$Jh|^)wjeW=%s(;wjZ7ad^2qePIUdJ;Alu5?cXKz>JMROHMgO57gPA$gI}&3@DRy&N-}mwJ@$h< +%V6yhIP^UO?fazhc29kbi~RegvPl&1GXTvP%sw_KmGO2F=~AE)>Y4oi;%H;yq){*hl +9P6z}Cgv@tCgPTsHhVT5lA`%y%V$9hUO5!colA=g6?eJB=xp_IyqY<=wYLFy1$#>gSjez*sc2m7M@Ap +&O$l`K@z%$4jVRd*?JgKxqCB{lMhw+H5@K`mvHsf3de)Q$DiUXt>0neZ(@AB}H{b;9@{}F$8pEClZ$% +|iLcgWAMkLKSV3$T_$4yZGI_0ih>@nPVag8(>StwOXvpoP44(BpV!;HHoZHm2e}q#r6p0 +Z9NYFqDAC$nQ$_5dS4!#uAuE6No_KE?)1htd_ryF1~y;2~`Bv>U$u`y}zh$Jm+7(N+2ad5h~cB+_M`A +f)=E_>?+272HsKc!C&30?+F5fsGOM=0|wagb{NVN|8Jro@cS3H6ut!D5n7p#L!V)ABwW}V`wx+XD2T+ +={!FnCTHqFOa3UIV2E+NwO`^Qp1U!vsY+aP`-SqqY83UDM)u1WP#+|%;K}!(-Jfyv}pX&-~1N1m?_kD +9}60m2e&?KzJsL%b3*Z%zg`84mbq6q=9YkC0FSO>hVzn9J8pSEg$Ve +lV2!qhj{CBrbz@`ZG>`JxChjmZFykd4NHM7Sk&5Bdg0ly8_As$y#-=!>e1 +FoLPP!e`M=(l`Yy(Q^4{Yu{-X=laFV) +awWPz7jJb-s8iO^FWyD)FI_ZLtY&auLeRs6Y{@3dQa2Gv8OJ1|KQP8SXR%85#{ZzZ>ZzJ)8~BgyfDBcl)LX_joT*b$Y^OY%q8d_&JKOG +1pQNgOXd+8-d$0hwVi?;fo{I-M=bkl-(Vqm@~l|4HF(XkZFN|Xj`l1mK$`7c+RWlBll&`LGRh^eAA3( +QcB*<D5FgPycx_nZUL+S_+^?*7)y>;c&`w2`b+_h; +Wo5~TqLE}5@pdhx_jGwQsx$Th!({#@vh-(dBTEWX1T%)LWS9`ofye=MHW# +0$HHYi59fIaclo4zj(hQAv3%CRQz-Tx@)4Y$N%3Rp!6}+1v;WS(-o=&gCbO=~n=cpEc}c+fF7; +lh##`6r{e|gZ#sFVkEs>D$3Dk%MdOSFAafrGQ~^&T?G-K496mDd_MiXBNn7{jpDE}ai` +-}-x|Ts-U0Z*lCb4c!WOEMk`J8gGh}u~p+l`KUo0Y8fY5K(?i%XkQNWivOG^1T`2+Lm{lrx(FwmNtY! +hP>C(f;{w8mM!UJm73G^JE0<8H}w4vHZmV+kM52w=PXemwAfmLt{YRlH(Tf!;7ojp&$wn(7;bOK5 ++lK7^>Q~^(;Sv<@^w6B%DUR&53w+|OL64;m678#EIXkNsb1m>IXh+xlKS{)4SaeE*}RY!Hh)Ij#eBpF +}U-iZEu>)Y~GnlhWm))ge9J3aA0G^W{lNd!WB+hiv9Lh*$5%p=BGc2ius;XEupx&J6A9#DOY&7>=X`U0o=43KNRRW#y7_zrpx$l|_7ZFQGcyZ_Zj4vqV1#xhxde{6buXd_N-qakf|E$%0`UwNZx&2CgBWag +l@C}UPa_o-^u$MLP2*nc&94LY*B^0n%2;0Xf$u#gZTCrg5LDKu!^$m#*fpy%s^1<4}B&Efv%ufwGXN< +0)x2kmCh$fSCR!U$hb9%kg;~Lkzv4iY9A3onF3V4K!m&W@$D4$7O5LHn*Pct~myuV+^WjUV5kAK%Q)| +%UL7x~y9rv%!1HiD-PpIcex!&JXtpD6O#PrgiETp54{l(FU`S0T?>yN}Bms20WYE +@i+2^lnhZBc){F9^xZW$VrQvy4h2?EGvE{eRYj14km&9uf{jcqF9s|L4|EW0g9(q0vITX#oT*U+W1%v ++7L;1--5iKOi|har&O-gk$f!n6lWxom=EF!CR}iE^N9v`gzNI^cl;AWD&_#-88l>%#;?f$ZxluCqm$z +R6lVe+pu=Miae?EzzUUN;PtSG<*8qnFJ@GYAx;K5~0${gHAB{_1ZH8X_NkYedsew`gVCiB6ShV@sQ2Z +L;Qg4bw`YZh5A{M}CF*Le+EZPy!M2a_1gG%$wJCc(%LYw#mNTACbf86_@F8UJmKlcaGbzkQ#9lj^AhH +znR@BJdt_FmPHB+7jtnypQ>FW11fFNfU%J4ldNQxv$Z*B)tTlWAaAN&Hl#BNr0zV=BKHP&P$`L=~WR>nvXAN8hSTblyF!mfPA}URV%|UjbwS%`35pD +8sFQ6Q9HoaI?b?hcjJ6fCgG9+FGT8kiX;TsmWGG=c3RW0dDQ-rC}BW7X?jwdFYu%X-NpeI#@7!tIIXu +ubEg-un3n>ur4tA)!$F!YuM<#+`WkqMNPDBPJV)^(c2B$345kFo5w&iKgrdIqD*bbjj#MWX4Gt7C8c= +{o`AKcJH{BCW%P+gE(iebON5@B^-_J<3QpjlCY2amQydKWOl|Na}qvjJ$>>lHBIv>-sG@zpTl2F8%!O +s0J_Y#=bdVDOrc)cQCU_3P`dKV0My^8^ju8Eqw9im<&+wJbRhl6Mk?DT?<;jEFWPc~jutMb&XB{lQmt!n>DnR`+KxP@E5w^5Szjav! +*rVo{`&N1v!5B8fe2`8NcnWy{6Fr=}s&?5>L6q-M5gbNksf&RvKs$|w23-*=%5?d@2F3-)e+bwNJ-%O +qRnvc7qV8=0Y2tb?k52?(fu#9tF`4F>0CBO@Bn%|el5RI_=Bo4v4OVu!AxYk!=M(yz24grXN}mJ0J0E +w2)_k5mztC$0J(X27o2zd(lMS%L4zFqX5Rc+#3CN5Ju*VJ07Yk9T(O$K`g-XS#IN3;s8=L(_hUtd-k( +gW6S63S$GzX`meCtC#IT8$UXCbx4@Y5p50eGrO29~o0yqryB>D%W5bM04(9S|9Ju&r+{pgtDeEq@?&6 +Fy>$P-QyOeTM9|sIRVj;c5=+VDlfbdR?>88X(77C)!nveqX)QDe#(&)ksae4Mr%`1qRdOzI{99U{Q_Gx6-LN9O)m-;piQB24F{?ayTYJag%7{CbC|$Zc@&_#mmYP(O1`fa%zsQBV@-^u+r9JBXGl5&F6 +fKBu1Edz`!fzMu~;4cr1P~-QEqbY#HRVG9rynXo)ew_KlOP#Ss3W1 +_y-ZyoHL+z@Q0yk>v)O3sMe{K4E}$@ltL5AXI-i5a#(C&pZ$&}Z%9{-A9pF-f4ZU4Z8#FG*A7u0gy9jLrOAQDlaE%<|#OlQl}35NEz9NIgral5;Bd;i5B+)A +MHu5hUc#>_DJW~(q25Kq>|yh*Ux+fP%k9iO)9TYg43mhB+F(mOY(6DQgX?*j}3j)01`A)A#Zmyf1p0$>+DF2Gyv +ot9$X;@8j6r2)6o2UrDFZs6xoWcUU_a^ELqfD$F{Cu^JVDA3#z$hy|Jgj4yP;%| +aS^8F%mNS5Bz7i|60*8xH+t{~Q9xye2FRjA$`Ns4M>B&f1c$BfA^kzgJRBHBK@GR6XLQx~Kg2Udf`-6 +TNd72SsiU2=bo}A|w)A&gPZ=QPylgfi*3?v1tT)*!*pv_4&u;VCMG1{(jbO_jt9c_E7@m9f1(`&bY5b +$4XRmdSWL48_)0X9z<5Ii8xXQqS$mcLbE+#i>*1w6&a2C_^uPxf*n_5@~mskK-Gs407ZjfHrn(7H0f0 +NVlag5W{E%=<}N#?PSdAn*(t=iX%u3nqRqcF3+!G`8AQdxFU5B!DF;01m1j$yV-|d1($v$7V>E*C`?O +V?orqg|=EhIc>4rjUaSn&r4k9yEy4ks}X6lvJ5f&3jfeoN$VUNuJN>J`C_)8-z; +CB?(3YzptI2?j)wG-W!b5UUfpdnGBNWET@)K0^Ba$*oHG`a@aSsOVBf813W@*aMLBx81_d4h&02Dv)j +bkhd$F^KJ-`pe7iqoE0(EQABSzE~>vl?K9@rM2g}EN6R@el5U0P4z}4W({W`8n!O0)m(L@|7)Y0 +FrRjq(<1zz-~QNY{O +l3-fl3On?oTnw{702JglK!|~QXygJk}oHHm$iuegBDR9kdAi`yJ$#L|SN?4CX$xftyoB-6Qn}!M#1Y2 +l4!TY#>dSskx=IQ}lUUyD4c0Q*XC#UTl9x@E)3wChl3@iYz3@rZbG-|^26eOdhlmOd&1=#Xwzgi~{m1 +fybMV(E`p=mHMjKLlBFlocKcFRAwC>`g2T@*?e+k>qB +q;(=TM`32LUy$`BmsC2tOG-@qydd;i^Quf1AL9P2v%;27&JN}tz(D&(w=QFEgZfLnOL>cgY-|k+xkfw +fiP2hX;D&l4S3sb!O>OKW&^IzD(xxFH1(Y;UPz8--zQ0tOOTp@;YzBu?Q7Np%=!y<%V}uzWaZK_OA54 +cj6>1lbI*ZCJ +^)1dZWUI$0VWmVni0^RvP^m{5nMPY7sL>mu33~c%^0-j8a6*c%#8w +1^({6@KG`skZ>U8U3f3Ql_!2?F3Ds^k-5KDrUKCfV&IeJo6Io(5v%i6fjVhhu$*3h?{2jaJ|F7jg?ab +aPpI>}&e8DD)cAz-=UIY~3m1e)c(;QBn?o|JKhufHvWNb%KbRqfwzkP0?JoRH%g3e&cEe*UTsG#zm3- +^Z!{S{sNF^p?V7Nj{jUFPO!zDTE=hafP9VCP#fH_lFY%KN63w{#@WL~aM`!SF$7lJyvwdn=8j8}fE(m +Er^ZWckE@oD1{#N~$OL{|G;WJ9dP_4fpJhWA}FWdCn_%j4`hx0SQF1?*S;07w|tz8+_3p3i=e;_43)v8bPQzg<-Z}T1CX?ic0l2Q*1(CL*vg|7c&{ +EF{x@{IN9>S=4hZbKbQYw|+-x0HuwmoLr9Ak62A(l~U*ic3R*y617DU +8p`Tai?blUqpm4FevkO|Ty19`0N5rsDPU?y>2D~%qKx$Wla0X@{$ZFX@3w<5+KunUn9Cj2szlh-yb3= +M1h#FAS*7Fwmd_CZx0IpF4Re`0L<7pqKWU-C&X$yW*pKmuX8tk4*+GZNC!nz%_uIyT)k1z +E|zN4YM}yr>LXxNG@-IqMBFwP`l0yv*@zRLFHhD;jPpCS~HUn%ZHmjp%O^+h6V^tc39EQ-9v1!x~4Zy +AQy-B75#A*W$Y7RuR_-UtaKlX?voQ +-eeVb>6v7~q|pUB)2ys)ME7V{O;2(z_+m>@d4qeY(XeI{U=5L(;l$&lzxk?(7q54zt7{gXyE$ct(RqZjTJYQ?$h*so=>1>r!Zh(X5Se7)6#85@3%~>D@uW_3^NgS0b~93 +Cd+)0>%ub)DsRaiP-k_PAhOq!Z!kaRo2wPZ_qzTiRW3PL(}~YcJD<)Dekd*G-}k7yBSzhgZ^ich@oyZ +MpyhWQy&1VZ4PcA*c80x^_jEU%wV5JMDLmR2qkv7F@SY8?Q5(ntNk^%Bq5-CtVtKqcmPUxVpYqjD-Po +tAfN}k3T2UMN13}-QxdH`=x{j;-Z3pOSsPv9A-hliqjpqzb@aDes6(r_&W&4u;N^=J66>%1^vW>T_+p +{hUH0`x9;!f1s`AWu)Wq!OP826(vRXwX+L<>I^@Nu=QYRlomU*32#)Q4O3W1NB&e&;6p+ +q(hOr`tTQmELOmZQr2AxcK(Dj6t+{VDvUPVGr&Ql`p6>BCfX$AQ12iqe6^eYi&sZraQgcL{(xm7@W#y +&q-%L(rijq2?p@m8ngRA(Dzmy_`6#NmSzX1YzrLp*PF~ce&7^7{q5hA*a?&Nn!8!fL5pcJgWE6m%Oa@ +0_xI**{b1}Bm>`IE_ihF4&dH7x?|Qh;OIM-}H^av6%R~V|$jetDsWbLoc0JZX%vVd^I;^pCmG|n>%jA +UtZ{m=r%DnbIVwNNkj{?nRQ%(>Fi?F|q%w}NwXX3m_=X2>^e~)F>?^P!oeqtLcD3BnWRpd`9Q_V=_o5+JS^<6M3sWufisKtA`vmA>ZsbSDy#O3&DVSye-xh^es&ahO=BC +)qQUupJCmYR(ynJ>g#+I*9-F_yQwPnqplfM?%Q_Rp`-fIDvyQfN4wE~VT66YnxL4->5F +2;b0Av#(!M+M>fY}l&Yz4~j&UdUsgKjgJmaAHCwhzt;k7e2`otdLK1u|L`c>1{Suz9-R~?sjwZNxG_% +S~?T|OOI`EcmqGTc(r2!F>nRHpClBq|1Y+8_`X`PBzejpYag@rZ-U6NBMB)?pX+#eKqoiTts^Q*^>2D +sNZLfPnupVK+Ujq_Q?Ouvr1IS1jm1cV4<;Vk+>q3Kw8O^ok3l>gj{|hzf(m& +_T%sapHsZ67=ZO@^bIA!)Axg?6!T9RU|$MOvdGV~8qal@hJes$rbl-f58A8yrOSL;>U2>*uzKVeF46q +QlJdvd_vG0$fTommdxMREQd>&SNR(5GPmgtM_3dyM=E}k1nK(M-PY#=1_p~ej?pC!O*5+P1 +C?|s9_cj5}|KNEAM_{?&a6kVYkLO+t7Ww!2AT{bFyHIK1A{-EP$|2L(~Y{y7-EU$pZfx-~PdXeWKkKY +m+m&94wQ^IVpSq1R^o7=zs4^8!+qaE!_i#HyJAvvc;R& +8iC65b9JCyRCEqgkh(mfW0cQxU2$I$F!lGPct?MphyS}h9^`y`D}aRG>z?JP3pbo=)+n&Zm=o{5T* +`_qNORJWGG19GI^KLz|5~=WgvtLWJ#QYK(IWs}JSue3m;P0Nug=cu|c?QItIe;yMes@852irifdADM3 +4KFzU^nGj&1E4`lA}pih~pa$q12X7P{U%XBKsBvWh#8o1{zVmuh(!bq{nun0giMaFgb=~T28H#;XjtK +hk*jU`|zWocxajofRopRr)c8-e_oM+4sS&cp%B&p*L4nFGDOVW3YD@Z0JxeV*i?6)|8&dW&*cCLE~ph +!_q4?^j{dYMEgr-!RbtX<>__VY1wCH=#nBZ)NFTOH(6+0U{ZbMgrs8AKZpc~pIhhS5zN^@FSaXgp%TDxlc +a!~4K +U{MO~qR$4_H;O5KsF#BIL*`R#cqvq!Sr5D1O7SZqq#6-%#XKd{?(kuF~d2!pmc +7t{nz*XQMVkW8sEih$6_4YjHHSC}zicOQcFl>y^zc0ToW&XrHWfsHaYQ~%3CG3Zt*&lhVq|irn_0Ztv{>@mFSg^afq6GXLtX&=q?>UK0`80)-1B}86Wx9-P +_6@Te1!9)@F)osMKI6dtIKO{e^ZC_qS_3s|tc$R>=vDH +pUu3C(evzg7^-98Cq{`B`TswowLJG* +@=YFK&(KyqdRc^svfRdmh`MR)1Q#y}UrT@?v>k*^!p4w6 +%U4Tsa^B^}8&gMrXX~d&N%Fc9be}3g=L_Z}Uvehp#pJt~^Y8O$XhJb8H+3>)w +jBMZ-ux=kam%W{b>D`@8Syn)gV%cuHnj=co&zv3Q8I?WwfQFrL22P}rDa-hJjL?8^Jv8_(bjY63kLzqA{&~&V&{DAEE@xho1BLvoP_=s#NpOr|P6+*?&OSt$Fl^(5uqXNB1iY}+veGx@6VYu-^c ++E2LBbSJRW{!9!IgcMD*ekdn(X6M^;ySKXc4sk%MbaRI&oTI@zv8EFw!U_7Ydkhdkzp|MsXaC9R?hGP +0ya{V>hrk35Z86d*r7^3%-yX2$HcGTvH^kds-{v47@G}x?Q+(b2rX0;-T+&1U7WilkRyak1~BMdJ^NoFH +ZJk7iW+i?btHscgb{`Oar*#83;l*y5Vdboy}nDy1o`#u-X5iHa~^71LY;p-^^oA(JdioltX4L +%a%xo`wkM{Q9k>;e83KaT+K6`6~C=T@aM#Xjtt6XBDqwPVffH6DO$nM(v`aPB2`}#foclp*6u^Qynh!UN~{*__{i#vLhL-B7RjGs{xV4h8i;p-uo(QfKgN_~iu|GIDPvLCN7qK;YhN>Y7 +aFb_0>&jtLtD&x7|2OM#J?CPBcPvJrHQTjabc$w?r(XZS25c0@pU_Kv}0##9L|HK`1HqdRFF~_HQg)Suv +^m*SY5UdfM}Mp7n)$$-xv7pM;7<`OMo!4;i^Q_o_?ikIxN&XAMO7rT-&Rjamd+Fq8YOPD)%mb*=5>~k +dO`qm-i@|mmn6wp&F>qT?aDTFrm^~TTiY*T{H|)Q(6OVVFH|bxWDSTo*`4)D!V!8+@}*qZoJayh!*7Q +=XX5oAUeW^q6RA)Jj~X5GR_#P<-!-k;70JDE;0JdkcE&1$_hKlI;q=>@ENZZHMi;!*un4FqpXUuvmt!}s@04dJ!{NU)0oBUZNkw!zzoSa0VyoFk1cD;0Z9C4 +iv^_4-B8m}Sskj3W-Jk2LPYA$J@k#aHc +Frje!4A^dGS8INse!XW)$7#ZtLpoo<^U3b$`C!e?=uRu`d=M)^ia>d%V5zD@mJbv`=!rMyjWVL?`=L^vzGK80 +SL%d`wFSIp!_q8u)%{;;0O^uw|~F#e=W-Slt=hj<$007bJpNUp@1RyW!+zi-4zFDCGd>vEB+UD)6PsMet_K$sGK_>`b-^9&`g%(~@Tbez_iMN +k7fTDq4aV;m0VNw(?w_^{8+cCYZ)+fe}^tPtf`o!oxDFdWGO)zOw0fA@}8GsV@>8-BPABnx@5wHe5s; +?tk*C5|J-R%jsYAinZInPl2UWY*rXEJPbJR&WyjbZVNeiUHPC8Sj7Hs5+0pMtVK2;(d55mx|C +4p;IrO~gQAP#%teFhPg1nC5UG$ugbMY-g;=-rJWZlC*x>IUybe!cNVPMkdSUOhxAl4P12IF3gD2GH+B +Ykt?-#*#2+zSZw|6h2LX~5sdue1@C#>&;+QwMg2Lvrrw*xst^9oZhvB0S8sULA6}}Z58w!qzZ1-&YoA +n_LBeXz(Au8`KJba#jR_2*zB_p|c4Qk2=-c%+UzH_wMy7KQxly~jog^^SF97ho|$bbrhw!xP0+oezEN-05&4S-QmlRoAO$vhEB}wjOSSnRi +bxDA(+rb!oaaVKQ9RjFFpE(@RHeC6WU2Rq^BU +r$kqp7l6P|1%A9Kw?a>IQhQ4wJLBBYbVq8Brnw`TgOg+;7XN+qUGlc=(W-cov(+;LW?bvQt^g<=Gq1w +i2T6~y5$v(VdW;O`w{+?ZJp|0q!&WHb68?*6l*k?GO%Vq1Wpwd**nbx}AP}(=85uv@Z)qmjOH+dWYBi +LKF1?v>4QD@LJ?eI25I{MkKX8H8UYgl%rU+bbQ+>^=@iwM9{yI+N{3~^D1#o1L8*0O;+7*Pl6 +~OiBD}WEh>T|g$$#iC9blPr!Jdc?GX&4THu1JQNGG1>5M|EG +Luv3rf{)R6^5YvWDlrgT8*_K(i`+ZT7LzB%AuOEF*rKmjLyeDfJLSuMzGw+IZ4+j~X1f;KBxf0cU(ho3Qu)SL7v2kRUU%%29(@rm4E-q +$+oM7={!&7jhzb-&9lz-|TJl`WTY0O!d)ylMi%pls=V*NCyGU!;$U%d(sL)Gg-IZFvoL{l#3Xp$xN|r +HTVB*1Na5hNvJMeqz?~Aca*LrHKPbS&8iqr&-u!ewDw$vjH=+RWFU9ygJRb!E4x;Oi$L?>$t^IuuA# +Y8whnQdI{C2G}>im1uC=9|X3e3xZ9VTI#EX?D`ugg&h7dRylbZ7-h(%i`d^4?M9cYhqnDtb5=gU +H@VJc=|9L&66Pa;pJmDR<2K^7l&~1`#Y~pV(Uk{OGk7(;?P-UwU}mQ8h5^QcKvv|vLOCGdlfS}3h4-& +1X%#rqIUnxyh*`Y;+bWxq6Z1#z5sK(fUvrF@htccQTD*dC&N?Pmx?iOQedu@-yP(Sy6$5alGiu{nUd`iQ&2FJe#LsOWrbtdX%{gG}07=1i^nxQ7mv2p%Mi-ss1h%dSov-y?fF>fV0INVkpYwV#ml=|AetfBb=}fgwIu;MgooGqOe_zaV3Zc +4%3^#+}f(2u_>lROUQ$%Y3@Fuic|F)6{92V77RWc+lO;B?I^s6pg4a)KGu>b|CMwL^*e(TVav6W{t!t +NeuD~9iODZ7~ttMOrl8=p`0ue-TBS}f5(~OEgG)96x4gjvIh{-dWRj~rtz0DFH)0tL%=7Roh52HNX0J +ei=U|{FiT`Hg71RF-4;|&thI*`knl(rGLxf}{AUUXg}};qEOI6ilE`^}pBwFifC%}oiqzYLbK}`0ZV= +cMxv)ZA61rFoN8TMV9I-L6dK(4yEW)h7Zt4Nb{Km6drP>rd__B{lfGH)p2121FCfFFu!%C^G +FM(&F|{j#6Tdyl$%qY6djOAxMIRPE94iV4jYbRqU^& +sNKgQHb!-GMJh5AJo)q^K?zEtCXfQdoC-PF~4WOxKHT +(R?d@OuV$-C_eikVgI8S_i!>^$*+I8X9vQtIHL>o4L@pmx%2Uc!yJ%9cSU9T0$KFGP#UWYm@#y(ng!l +49xx*xMi}nM_L`39CgOV;g8WQa~tV4Sy@3_LOt2M!D~2bwwH4BVy&$4(le1A~$3_1l$_<&sD-C)J_{? +h_V1X^d8d+JWyG~Cwg}cvGQ5JC?Bw)k^T`+u;DP~HxUkq@_53(dRM>9dpS3^oC5lm%lYJLFGXoFA=L_ +Bg9XZ)MMQST>RLXoUdbnVr(rAhCGu&)-DH3ug!ZQAQqXdBz2hkL0u?1$-uOV=z&)}mnS|P2UAX1#v8O +@h>DKlB3X>)L`PB6DQeec6_PX9uaWPLy{E7Ig2?&iy5h;(0)Xi3zJp4@HuE!)O3q!%d;Iejc$A7CtKX-q +|H302q>rjwu?7`JRx6>l(U-J*kS|d(!&Rh!P>6>tHoRlCjXs0g^5s@JT{Unn*ewo0es(qV#yY>a3X|e +N(lrdgK6~{klTaIL{QCed#|fn#|6_do`|;pd&}|gd3TVOrLr{v2!qy@+J@du)KP%74$&7T9O@+v3 +e#8_;0ma+BxkQBt}#gEa%kxNhrWlT&{nktl{kx)QJZfNL>K*Gd=uaHt{J!;1j8scYz08ZUBBbK?G$gnTS> +oW#;HI05ETnKE2-GeAu8_pBn$dTxQFc(#BB=oqCgVALydUYyPCw#!_rqn8IdA>2b9K8X +Yf=1O9FFHFbS;J?sZp!Dtj)AP^10t8dh1u^W&2=R+o|HrHo3-KEhH39#4Hqjtj-)LP%zr+SZJvPf^1| +Fmf&f{^b-k>l@>N7I@LYa^uQS+v_TUb2&_NJpL3A8dbld+E{6p%X^a@twH5>=`VAc^FcO(AG^?gQn*W%W3LI(lklOYTR>n9NW5ZK($-?HzUW9iCtVa=Pz0*?Y>45t;hUlZ?;M;5QwIZ!Km`Z)OPv0dQL~_Fv +;$R2?3!H6^ZE^c+=6*(i>=;lrGIrgU#AuOK+R64{#ura6aR~?t#rxZM}t@La2gW3b29Ng_llTQnLcwB +iz=gVkm!?uFlgI@QCp^4 +E_8Z0e!+5~lf~J{mUgMYLYS%KSPc{}85^s1#c#<>YpIxx00TXXySfzGdRJfse+erndrV@T9OQi@;wwg +O#CKy`?~|)!N^IHaKUTo^CU5v@q4CW;kRt>xUI*+o%$3cy-ZT_^D~Xp__+Znw(|1drJj@e3B#Kl6q0r +=EiRuFNYr0Mmu?#vo8pr7<|P +z48x0)mk3jpd6pjpDhaS`?&yRe)*wYrjZum{qaV83U;XUI~Zk9aiz5o#`Fa#p)-QU;b3Ru+Q|aNv9Lb +YGlA9Kb}=yTcCdk-nQ8iY4;ew)MCsmW-{8@mOs9LmYY4M=cxjMkT_5OTrJC$>-Ay<)&97IGZqdnRraW +XSN2%Ziq4Gpdf?hZ5%{=1GS)Y5mfx&q)LGMk4a0jnu3b)6u$yo5yE_Z8I_fSF*^?&_=*{u_N@ljFfdg +xQAkcdv=za;VzKy<(#_>P{q0qX7+9H1>b4PNg!0-zjS2zC7FF6F*Iha;Z-1kpiZMN +#7%D545e{@oJjtUvGi`>(_qrnKnn^?aGNs0#bnQ%fN(r(D^23NJX3)K*Yq9Tqmb;Emj`rjRe}zY?|jG +8ao68AVE-06DAOuDDoe|aD)OXb(#&0_w+vo=0?fBz;R`Er2>_A5bAA +1e-tf%iOC5fz(&^021X`@aX%Q23=o7qd(6PrV0}e}2=#`FpORt>XBt{11O%aN8IFi{u)*Cc>kf`TH${VB+K7?{LTWTg*D`tEGGmHt^BI +zd{o)7h|3Zkx6h7i0GXb<&<>LtZ2%|Mf!rkMspp?eX2VFGF=eSHQKT^r~GoVh-=B@q-EnB>XY{txvgy +NNDbBMksd0p^BCsCU=bhjg)&=}etk8nAH(Wa@-^Z>7=o`LO>zR+p>+YtJk;;gHo2u*E@Z$r!kIr&o!t +!Lni?hbDC)9>UEW2eo*DACDl{Xmajgn@V2dg +O69atOO*0uw3F%{!{mP`#0gmO0e%qbPxMw3nec{j2>&$YK6R5sNz4 +({O+$FmrV1u-On4Batb(d?P?()iVQp-Iajcu9-g3U=EkzU);OcsxWKW+{GQ1N&KXicZ;>gC*beMf}>% +oyt$-!MMOsWgNe3h;-}HK_rs30&Lig3-XgZo<_R4DfQvB-OIMilb|b}bSWZSaDf}G +*l$Ih!$M}g9;c>Tk`BO4UwG+Or9!=k&2?74Y`0>`&n*wu^B|1u#J4FNL$n9FYWuY52Uw=km+BS>p&up +~X8N)B0-weP;-YqMmO%-&*xfZzy)-{T*$q!iEd&RAbdp1PvjX&UCbyW_MC{p~t8|zJROYVZg@IMk@!? +Y)B{Kdr(0lcCI$9ijACi&zszf{g|1uzCe*FAiC%tY$?_?xEptJ09vDZpP|Y-0CVQSG|vB3M7(W8?EB) +&*^ID^MjLW>RX|U%uo8?lUp~4Dc5g&zOkyx~FUV{Wbif)JP~tC1l +Ru~I#gijlyZS6QDLyrPWu%(_peKl6NQ?l}e4S5Qx{|MP-k(RUI~cm_=VPoisTt(;bNQuq}&sL$iG(_p +Ps!N}+wvgmr~ycP(y1b=$wY6vw;?F^N~&~VlY3K!$=7ocrtpkggo*?p;kvk9B$ROc3qb%ib7dvN;_Vg +p;ohlO*iXRd+&%=JHNALRNgXxp^9q_~-(b*X_M#KxYPti`lkNtG9p#ED%fo +p5R`$o(U4Z;2>T8rGRg9AZNo>MNsXNZ=b&fgJ94*r-bzClFCO=kEbZe#(X3xk)%@o7*=U~0*pQk`2G@L +6R6Wq+Qn8@jxyQyZ1WX36Si6z7mWPsxgT08w}rc~y-xO0pK1~BuqX3Atnu#_LD%`0Vw15iz0D?!qlDU ++d!AbtEQU_4;!?t_{sjyWJJq-JVky2$?7O^Ul0~j +{D`p0x|B3Qg7XyOLeuzqZk^g5)8o+Pehw-(uHfLhUVgO1m?@Hq=dzR;K-s)XI;%y(M0k|&XBJbevScx +(0wI~q*gm1t)*78_tb7V?KM+Eb0OhszN2`;$OqyGyKo=@cehC4Q*<~epA(zJ_>{}#U7kOhz^ej$otu) +Ho&AM1Q&}^JN3PTl!FJ`dW=d>6XK6W+nWZx!zS^TF@}@0CI +v2k#e_ul;Y?JNuVbAEMpU>ZFYTqO}8wqMgTe^E&t&!$jx?B&D7<*6=trPBAiTS`S|vF*pFbK2i6{<+F +#xH^Emh#-(JSJjS(Cvv^KuGr%$kEif^KLFzVYB5dS+@38l9Pu|hFYC+}un4+5?!$zi>5TGu}Lk2K!fA!Bbj +UNcKbW?o}!MCB@T-XEoYuRj-4H-n+^G3ujOTW&$6w!FGmObN|p^5S +L00N9ekU2E6`Wu!n=yweVB7+&|%Fbeq-9-B$SP^fwaY~jjRWwa{ +QPgnVVUX-s@y51FG5s<8eXF(ht)M+W}xhbbLRb|9DZw;X(mk9A0bK_#;?grSZsWKm`zNu-ZD~?#N^rju5z< +Lzk(uZUCE7n9)NDOC`q^gku8)BC^kzK+UAt-y=ml-fI@9Th8P|;SMMl3AHMq_2ZvN>(Aa)%wRt}6U8C +m$8^*c&!p6@3bCx~CRNIS1Jz%vHdKnaVwO=%7E3nfl%?w2`>8tjTvafYF!6lx@KUN89ShbD08xvyD4+Q2pgYo_Yu)hfOutyb~XO +yLW>-*gJ*FfLOc@SDg~zFe&`Im8NTQs)624S79v%@n;{4)mj;JIes(#H^Wumtk*gHdKkgZm$yb7tcps +eTPlEF(wapxk{S$09Wr!kxP(0tkU1$7GEYq1%yIxvNGF=^c7Q<$^35if}SP|R7R1UEGvPknyrDk2}Y) +6tN~kYLsJ5C^N=s|#busNl1u}k(2|V~_P_a>w(1$HS6*xr0|abXch76f6tQ4X$P`vN0FSl$bb1~o%~c +|Q+~gimXOTDFRIl`-k#)x+aL(D1jovN)zGf;_zR90*smt#)aH?L#Ggw(2ri3M;W;ml|vPudKghIHWje +GE&f0I;Rzo}`7C;9nGR|Gh~PFGXR5?q8P{3H)26N^DDGFKI?-1O9BDLXqeFeNO%kio +5@3V0hq*{8`2-2DEl`y1+?i-fST56_>3xx^cW1%c4~&f({-CSlgavF|Z-O==u^WuJ(tmZPy6c8F!f0LQqK@`Ir`pr_lj^OqhZcBrux7E{MQzvp^ua$KHC6jJm +33%26&yV{3IH@N7^vS_d_|@gMH$MNoOnsd|uwLm +1!g9)Y~4<7y`nejlKj^k}_K0@uuitG!Tv)#&?udf_Z0FSz0~HRNo5<*u#e0+sh|@}}2JVq2b)6!(s;3&1w +Eu9~SynI=!9wUK7&K;>95l_@v3@o02?1y|NQH9!zTs;fdK%Vb?N*uQkQc4irsiY4gM8TOR4!0Rx%uKS +x(l^aOwzk_X*Q7tegaR;6tHxYd +440OCY(Zm4O4sPHpI@MhAlo+eMHwQ?`x(gWp9Q!Hiu3Y#vK0qu8^P;F~GdhrYz-Zb(g|@fE67=0}2e~ +{J{oRm6$xB3uBykV6BRIZFigcltZ!yOsUG=Ikh!Yw(>Q_!K!Re1K#<>FNmy}3KpC@gY^f>W9|Xx38}h +iO(Dw{oZ8SQSWwx7p>uD6wr#Ew07r?R2&TEPlplM|$S_qdSNVNjeyZ8F7kMcFtzFipge5qSgY$HTX*i +Dv1nefno@F8p>>g?EWp7lSEQ36n=+;YV9h1DBPJO>m^~ageFRX3zzd1 +t>P@Q-V=b3tW<0fPClqDsxrz>%LS|z(SYyJh_+Mm&^f6+sdyzQ_XUDr&*fm2-o1_+B?*IvvAtp$>8pJ +Bufnhpo$yWl(B40%VJ~Iu9_o(c#1~lkK9F+%%?t49qIF^0rH1n%2$Rrp*=Pb_=gCyrcE)+7%s+nG9e? +_0BsWU%OUcJH<24REf+J#jf+UWY*?9*brlrZ51s98fmP6%U1tv3~dtdvWsJ25D*O-VAPE3lYfuB-_K;OMIAb~Cjrx9NQ{(@q8je3t_g(2 +M`v^PvJ*J0491%kXkc-Zk_dArKZ-av)4K%eOy}(@A&JBvA3{R>jQfhcOqcyQH|33J5~tQYJIXF`VG#k +lo3Z7{zP?wI#5};o~s*iJ3_XY@)>Q1Wm~c2H|9>-Fyc4DX=wD^U}kUB`L^lV1UWO9NSz5ePz#WZ9`C{G2d_Fc@D(4m~;hntH&oPq@$~3nX4e{D%jO0!; +@xH3gbg0aZX&)dHFcKOJ}_AjignG%{%UN4qmk|nd%vjRdRareZ=8DMHUwC3zN5tBiU&ud`i~&6;INiS|P`AtbfhV9 +QraxA*NEQyj(?WM=Wqrh~Z_||oH6pvm{DsmzZT%^wHI!Q;8N3h6UL8ha)&~5|UU}VK=abhT3(Vv=?0+ +ZL5CQ^_K!}6m8eD;1I21ifm=J&|HKqB5%Fl!!SCaiUGj740t(xE52=vQxb+Jm9xdDO@vLjI{uK%P!I& +M_@#2W4`)il;AR^GK)uG<^&3G2AId&lay&R%=`yhH+J^~5!BfA%7gQ*0Hm)DChG_B%!G(H<^tb{gSs+ +a+$XpVp1PnM8L2Htl^asR;zn{0>R!Q))PGywY{`>9*H<);SAzTgpcRm_g!#em@2a1GyQ=^&AJCC}wSb +4^WI~A~-hAcJH4H!+UN2*JJK^$$_#$VpQ+EWZWAHNExu+t<^IPK=iEb8hnq@hYokxILAaV4FsYeQhOq +d^GQkCR^Qa7^7})+>ZPdz0?;)(E4I8Qn#?u>5BmADSGI`At_d1xTYq0>-beyohJ+ex8-HIW*%U4J%N4 +920|cVSWK$SciQDeq5v53&vq!3wA2r6d_oj95QzD1-+;vbepKj@W<>s-izrV<|l)Cx11=lZsx}k0M;I +U0muqqsY-=jj+uG99GR=;XG+ZOzF3Q#rJwglhaXqFPR*>AHg!C~!!3cJ*e&J7cOO7I;P0j4L@VWa`?Eo?(z +Ry}|zLhmpd{&6!(<`^EDt`G=|kgij_AE(58^TGBM0r_tCu;y`buhZ~u-(LCv=1YletaA$l>q$%De=Xh +PO`)&g0J3&~0m0Ex4tchdP(+q-WeNcjp*_xp$gvyf +n1?B67Ja|UQgz4OWYg@aY0+K7YstU<+?W?Cpnw@=-*Fk0Q+NK=`vfE#76fZy_mtjx24TrM9B5QLH!H{ +2fb-{Dq4!%6XJB`Gm9nGe@4WgaE2w5oKo*ZrgE?TT|HWN!##h4p{;faw43hT7ZUQZ94jQy{=R4E9Blk +i8zRk_@gWod&4DdKw^Wg`+00_reV<=U>0j1%H)zKwMGNS>1d{$+!ywJ;!4?gJ(5XKq$0@zEz3a3!x{= +rRhUV!0Sr9wI7|D!`>6dL*F@-PnFzcU5PsUH0dTLudV%lIbP^F6$+aX9S)73gAJsMKA1~vM(K%3(VcFeb8!jugSXj?%W@~2l|G0wMDSHGY;&?vrePAo2~#uyQluT!2%9tT +xh`T=w8ridEWuH=tCq6Vx=K~9s^5KV-zb8>Uqy=kb};U&m=ZNwLrB+#ZrT-tLUSUaMWb^Q|_zTy{OYZ ++ov;oughDx@8qB9#hp*yG8)L|G-p5$I`f{|F!^~=kBql5`6&Rc>o@ikLH-OUd_K#;R3`Tv1OG##p&EP +c@*-(y<81F$rQ{CM)Ed-e&p(df{h5{@&Tdwpvebh7_`m-7znD4dos(!+W0i)tT25gYtV&rvq@x7RSOW +whq|EWU8oTdI6Ifj$Sjr-GWMTlkCrU%D9uYs6`CNP874ZD=mt3I1p-j%MmM}$8& +*8ha3*67h?FyN>a~B9(y44FvHHRgZanHOgvF=|RQyw)FDtTEZNPW-9sYx_oq+gGz0ColRnCsi|5>mHY +BdMURFd%E|MmYOGYKIv`j0@Ek5Y-nfy8uyR}tKG!F}7jBR$w2Te)gk~_|vTsh@T4c@e0*oWjY-Dx2u`US)cu +L&?u-`%m9Hf$Wy0b_meU-?tX+f1sW|ztwDj1nLa&eN~>;Q0FQcOgJ$A@i|TWI1(dh+=Z5J#i*@E|Hwk +PGNRVLA+T$*7P^&D@-+8@Q-QQ1`xGCrD}e-9g`BOq>)l*6=75j8gTAR?C%`J$X?S$vt0lAK9yCSxBbPo9m$I0Na}AWf +(I8HO#sS{u2Gyl|m+05WM@Og!VAJ_a1;kF|*D~Q?;=33cRN3ko?9`D~$1)kP%LPlKPAc8XOPy@*Kvel +FO;EZHiL5lJ45GfHD%_x~`j)>$GMSCwOa07Z1%yJJy_dJF(rRPeQ2CPrvsZC~*wzociU%{hM-IHbpc!6_P7wJtA_+92{yQ +hzzi)3kmK$Ol!FcR_gNK5(4Vjs1&K{;ye9iMpwd(dNwl-UyQmd~TW0j%;dS`%jf^!}=SK+^=?7re-`3 +z;e>0s;ZZ^C?ggK6|9yPU!E<1$<2%T~WYwJNS%w5Hq@|YG4WYQnz3F-9Y()G+@s0*FzM?X2!N}W=|eh +rOEIm;3fFd@SwKaVTZ#{K6RSj_Qul&Q-EN-20l=6ikvb5*@`e{?8!-|6&+?afT(XgQTr8ji}w{UcX~g +!6iDdsHSZbZd%n-JXIzE|jsaOU;-t>LvClUeye`1>HK0B#i>ztkJzW3`V@EpAF_at8=2l|J=z~;&BIt>i>m9_Kh;EDsbOmK92_sgMP +XE&~LCnTGl3<*aZA^HC7^*(s_Vi$r{jrzyS;Jkh+&CU3jfo1IhN&yy||m=HnkQTrUtr`s&KdW%PUZofpj6T% +XKum}|vE#NEECnNck=!Cw1oCriFo7DXU|3QAsjou12D*>-5g<_J_QgQR}tH0jv)V*&Rt>&xQs#Lix8d +xvrN0I7H(q^{dTb`@>Stn{R_cU_$$=UfVcfd5ucGN>#_#t7w5Oz}K&m-|412iyVdRq^vnB+hf`J}IbA +XM!#Op<5b*1JiwWvPga95DJoz1unQF6H6sDK~U58t74{jRTyN^_T@Kv=)UKmOjuU!xKLV=o+~OIWXxZ +G<5@c{&qj>8a&{-^Dh87KQ4{6vH5}ne?>Ns+sJc(ESLH34A3Bc=v-&Xy-gvoTbe%I;^k^IJ8kc=-X?N +3Pa^ar{ig;(Av{)$17@S2#cbb3s}n_ko3+(wGv)fCJLq45)`Jze1cX8NAI)_EBH@{DE-wp-UP#PuvtU +C5dXkK42YC%Z(pN5i$C445!?-LqbNfJwvA`z@Z1Rh{(uSnJiyyD%O{veiEqcQSo`VLWdXB>@&9^CRX@Y1`ac*y6t<{en~G;UDOl;Y6AOz849M+-%;pG$Mes7I5~6ras19sJS@$~N!RS6<(q8A(dvZ528!d63QUPHQdyBnJ+mrd4erEu +OoYvTjzLhV_ha}I!a5%O=0IF2|fFN>`d8aMBxJ8*~NE>N^KvXV>_q~Bx-@f&~j}{K1{>{FtwRQ@JulF +KuhLQ6%0@&<`F_NKoY>yW5XNGkz7GRBoGfQB2eDYT#@-R&sjJ1kAG`jgRK6myIA{u%ICb0JxF%>xBz$ +|+)b^XouOSfmSz&y>Exf#Z&F6yFo2TaEm#bydZy-v$Nu^v*a?6ysT5U@&g +FUiT?&@e~45-Tny$qY>~3v}|lO}c4xnl+{P5Et1;P7^q?e>0wz&r}zfn<|!BnW1@8yo +Q0xctC_?&jWysah;`WtKj3VE<^d)AUm?7gyNFzyQoKPxP=}^tT2Go2^PDWA`%xL{Crr%d5oa(I57%BL +f7X>%l<0w~<Y_^$x@m1O`HgcgfEQ#7dLo!IZzWuCfWZi>Wxpa-6|4ofE{;bwH$c@Cof +hS`13#+~@|!1vX|>Nl5*t2BdhbtiMHCJ+*tu08RnmGMBF4q2N&0>>p!rI|`7ELB=XC26OF-%W__1O@kMRSG19&nQxk`*8AP}*t2Ac!+f&Hx-yD3!wO{e`;qH0@ +CH<%&xG)U~viLa(7i`W7INW`)vzhUcyg}XP0mdqL0;>Ct`LLbn$20xzj?1h8Mao%b&=jA?UYYPsX_^_ +Lx6|z|3x9iUrpMz>LoBDK{*9E5V4b<*R&WIHVy)G7b!tY1lKltB(hv$)0eum^QC~m|H +=4I6drdRpTFS)HY1th-YGip=v&UEdiE2G`&ocM-({vu5QEP>60cO>UKlEyD{c-`4cFagWa0D-7n@`L@ +o&U5>~uU?q1$I@?7EDi&0Hp)F<-DNh%l06AflA3!Nav%|fz7;i};T7ZM6Or(q?33|B&bI9A +|@JmLWV{fZ|Wwj;Jic8LzWYGs*8eOu{r+#hkEzJgxO5i?nO@MMt=?n|1uyWM;!zEH?aW*eN(N@r_#JE +3=u-NLDt>&>VD+nOX1v)$Ak+kScoY{S*>(k!`X(ACPP0wzj&{h|l^dm9eO|HQmx(phb3 +wEEO4ubWl3vj*GMa8%wWvS5IQop$QJSNckK^-JZA27=If1sKWt8F)H61cu5ox1l0J@JKcX$IN$ac24| +vkxrdtkYha&@PW{u=;6_Uj`Xvr6S521CPo`zkHW +LK)AL-VPwA8P|G)u^z-*#wA1;59_4ppx(QOZ6zBbf_r3@!rf{9}m_-SP99tu9OY^PoSW>DjC&^?}MP- +lDgt&1LpN3Gr6@j;_=;<*;b(Ri+&y8=QZ?Ya!mZ|4Q$@PJFMrn89!0uc>8WtG^!=fzWQ2+#;r>RCa}J +9dxjl=%?1?c<=y^40p;eIe)Se$+sd5SzSDEz~&q+XCD$n_tofs?JCSAvl$2G^oN0dLf9zznUkVo0ewAYqku?n$5?4_Mwxb1KgtC>apy-twEZ#A4Aw= +pO6S&ds(~AI!u%`bynRl!e?_D06|aDMcr=@JJ6)}wVR&iU04QLBCe>4eM6I(4may&ZNWk&#$m7!cW7g +Gl1&`#1d|q)Moc;7|r-qu(x|)XYCGYY-bM!)JAQTc=@>3)#qf5FJ;^ix|T8C7OPFGaouqxXESCZ{fr` +V8?n!-A)ZpTVzruNpi<0FS0(T5Q_bN-&xF_tS8jH^$M(T2*}e@a;D_!RGdBuG(y#EFa*nt>&Yp2Q1Ce7H5E(#W}lHcg{OvfE#E1 +Si{BQ60F>1@}Ph)=(lp>hQ~X){(_)$;$1VVsan1a@ZA%ze5g10zG%FkoHvxj3$Aoz4XWLIJ(?_%@15= +wya>81(eaOzRtE?F_0RuyaeVn-|NQTsLKyI>gB|PAK&pGTm+ctDEF{01MB{+81`zXYw!a}V7xX$Odn7 +r9yu3h%E`t75n%Ou;&{u3#+Yaln&h;9MLTWz~fQoFjya$BazKM%Jdh(EB@JfLr?0SYUUBa1wJ(n%3uF +v@cGc~w3CtZ~s1owJa-Sdhp(`mxMv2Kr}$NH(=y;<_C54y1rRAJV8uwy=mo2R8YJi-RcuG~2+6`r!l` +WSAArQWUzxGQXf(G)fr8PN`FH*B$@JW}J}p)XVxpa$zKcOZBMR-mWi#qT=L6|c!~ks}bA@&uqB9Ul6J@WS*D$z?hra)J5E0B* +Y4{MP7{WwY${M=Lc7=fhZ*FC)49ks`og3%26~NL+)Cb)XWWHHSgOD+*py*rSupD}6N)dOx?s_dn?M|!lm)vgAStszTT#6*gKoy+A8R>u{m{WTo?6ke#eRYoUl4bI!fY69Wb +~~Tqjpy=!oy*KTd;k3b9$xx^ea*O24>k~u%E`(j8U?hI)dI7%ymvhMQ80i*x;2)1hNKG1TsLO6{x|jM +wm^?wP%xA49Q(G)X6D-ULS5?9ZR%~8*M3uT-I&rnO{ZOjDkc_)HGQ9ImKS%XX}AV@SmakwWlyObfAIh +^3oFN4x6T-9$Nk~(S}Q{C$Iz!v4_Z%3hgtkTxWm=g#cCL?EoFwIN&!!u^!}tGG63n*`rhg&N;M9V^0U +!j=NK#vAB9<&-9{xUi9I|p9#YIL5Qt!J&q~!Ff~4Y4d&GRZr~KYI@LGpy{19j`onB{#N``<973tBw3p +E~)%0k6$0E9!dca21JfL;c2$RKD*Is0K?f*J&Dhy#ox|6+b$J8L&T2>0udQP5=$A-hYFsH}biHgP8?L +F-|;NML!~R|$mFr`s+eHHCGnd|4H_i?|@rb=qp2^`XH1l$#@!f%Unq7iF2=OL**9kDJ9h0)a5-_)7Q^E?o!7X-czd2y8cUl&tk +zjJ}P=ab(Iezl|_k$1lg^k0^Bfv$I6`b<vzYVn@oKw@Z)W6Ts2SZ2rAQTIa=nE$J<1KBSI#|?{*cQhD_llzmWL1`07g>kN +%D3Zg%SaJtAkaNDV<70p|FC;j!;e$X36kahwxabDY08$9cEK3Sc~X8Numg?CWeD9GJiyyp#Qj$VsrpV +kqd8U7gFkd_W+G51Jr$OIR;`(`Qzjad5n*ymqm5pm!WUCB+LhhxLIsIQFjJ&v)s6uF_@7fw`&!&c*)P +cL-TLbx4}|eJIn1!~j7Em+bv4U2WBmLQrBn9V?NjO({p32JipC4n(lqw)CmCt0m%WxyxjxH5mnrUgKH +v*wMgSFW6WmdDdiQ3RKTX`v4-A`+&Lu#aJ1zR94Po(d{uM<4_8S_!BEudad{+t}zAdZP#JU`7DOX&s# +Z7DPUs3It8lJ(NUfexKkg`D|rVeglXzYNP-@v4U@RhIrH>NWCOF;p%1((DsJrSa|8*$Dtn)n0ZKStOR +9(Gk=)s&dO}r|Ek0%G5pTH*vB7}%BvM>|TJv;w0DW2^z4mO> +B?tCh1aJ-PTC^b$Byw5m)LB^6Va#T#+$A6a?xQ#4ImwFz$0;kBGQ +aYwYz)WA*~V)ijx5`)v#$T2O6lNP<02bGw6 +Nl)V<$!Df3Jpw+eXeRP6HrnX?@?ELt`@CO!3#Wn=f`i5?av9=6Cq)K5!&b6FvqGD~B9NJ=~)7|^G7uf +=NB4!tpZZN^7S49qujf}9qbj#(pdE7KVs03>zB+T;w9VzMe;lw;H@&Y(}X(Gpcnm0rNoPN;E@2|lPUJ +{uIB5&TTKz+4`dTG9(y3dF0cA!M0XZ?1+eqhlh)vZTyv2265hFJ^&)pqR+%i(+$hpmHVtsO4;fftW{A +s^HrE1%E_2Qj47HVgiu8oJ(q_)B1y2`IPl?~blT0oH=~y(4OrP8s2+M}Dp1*{UH1^Tn_%&@>?2U6P~#6pZjTi4ocI+fXd9*jI7P} +wmft@|vLf4bIq2)0!P`)h)xu#U9xgPbKLd4K{91R?7K^3tr0blrVb^01Sx>>A_a_ZCV8JcDq>wg>NNKP^(<~qd{qw&)lP&#U|NQUxf0%ftk+!Zka_KIL9Fqqr~)uKzHpWA?4!ht-5KH$lX-j$v +>ae7okroZ>tWf1eZ&5aw#?>%@Cl49f_9hT}0t{+?8W=me5CLOB-2_eU?Gmf;aikq +*x{%aLbO|C~cxEW|jY$l%?X|bFokLDLHKzHJ@FyFpJLw^Odp+@6U6j3)MgnN*=d9x;AI_%_pYAtN}EN +6A#~@YdR_5lFlav2tvAlpPoBp{VLv4$*`|{V=slO#(k1~^xS8P0#5^<5YLcds7Dtuy06i%?w_fpK>H; +CZH$(*Rlnq29C|Rc$CAN*OOw$ZgU#j#SZiNvEtpENVbuOx9%z4U3k9vy>iNFP;t>+z?}p;Zx +oR9DGTx6w2j2RzNg?Sd3s7rqZI|J?URjIbb8n+4 +x+xU{3;8)8XX*V=AX0~FXCG^>B$)@AKb~ENK$@9_+QRRr5XK(ynzim6QB_ROS&_pFrdzPq%er!ue`rp +WyA=>Han(;SN$+riPrv?wLYKb$fnHg1K)ha-lZv94p*gD!wfH8;3pX})KfmBpfCm9GACV5XDFX!vDEg +l2HRNXK{)QTPWHMzPT*J#4{}q0WVdzLQ>xdPn_{O%9J&t&3Ac7?z2^Ry&!qFlW~{H(L+*5j)$7br|2@ +gFG!G~(Sp%Vv_=ZnKJrwy-jE=9Ae1pZQ`|%A2M$@XF^1{Ve&dNIPv-UNgO&*Qo&R#wAsl9b{vABQj!Y +n?++~2VIk-@ofz!PqEPJ2vQ1a(rKhzvM^HgksV{CCRSt1}|mn58$4k^+8!;ol%2 +-j7y_BcSs4+09jl!zS2$6ZaSu;66XL0jEp&H}r~!nd;1gz1NrEoflqvuMK|i?3laO?hp;bZW=sAH4f8 +H8oV>)>(OKy?mGs+B!V52{(8!%B?4j55|p+KDfcf)<%YNNZ{tE{rLHyoH=2wygnmyJ6FKKV(JRtWKV` +6&dESg(wpQ%oDb*n9XCHjV_M;3aHxLdB4_pf^7Lncc7(;I3lY_*fL*F%zI-e&Mj8rxv{$u-iR1k>t3%bVnKqPpL>`2eYO<`w~>IDRrAA^i(omPFn4p?oZ4sMq`927xYK(udwU;@dYl +s-}v6;V@r|=}PG|>1u)27RO#(*Bg$~utn>p8t$rOzt^1ff?O)NRm_VdC)Mh9 +e~I8`*z-A%(1gNih068Yhp@_2A~S0fNvtz7_ppq`pL#xzqj*CU`Ze_h7yxMfyuK(-@$KvDQsHX8)34% +vfy8WiXwRcb7;{75eFEkwvQ;R83+#_N%F$XF;lpA1liML%Vw<)3x5sVQ7I{wF +f+PV}qOz+_+t=T5?gS&@4jdCyNnwDitKh%d2I)t0*x +=o?T+HMP14*$}Yf@9#NIh|$kCy2YYa%>Qc}g}g-_KxEQa6*xBiy1-g=%u5PSOh5j+w{C=i@B=N}aOOB +qSg-5}}+X+0oa3{j*rYqldfWo;`gVx$kCA=k|sKAkMwTWP|fx(=2m(6@V@AGbQE-Y_pZO^|KWXX|qHn +or*rl(Gg+t(g|hWA<|~q*l#J*&5#9N4vZMXEPho&Z9-A)0Mpn=!MDZs6!{GvGbgnAz`L`IIizW*sY_x9;}8)ngv-%NIDD;klrc1;=U*@2~ +clmWtFgsvwX9y>(ij7$MhNS;{2PJTOh6Rb#fw=>E9NXGW(8#d+w ++07VZDw%p?9ObRbsF8nbfH$j`bGp-v90k#P;Kl-0=_TWu1#z`* +;WWcy5+mg&ZI(7sMFScXa;ts5G>`oPfqe<$;!zK89_XBGbyzr%iQ*I2P#{BB?iArQE-i0JyG$-4s?-)9Jh?w@I*OGg!O&VSF^X>$sNtMo_GOP3hx?Gg|giT@DY^yw +J+i^5`+l|R$Tqn7XS9C>py<_fIWjkJ-t+CR$Qta&Giu<^HU-FCg=I^HR{pxx-4b@#co+B^khH-9EFU; +D=c9K}e?-<5wTKNivSi>}1d4C3+;zps)lx?7v4lkAmQ0J6=-Um;$lDMH_xj;@d$M4fa1h%3ZDJn$k(G +4i}I3B28AZJGx%b7z?koGFCF+x}KsKICA&EQ!_G1KN`gqkZ%JVCr+4DGJ_Vhks9WI3FApb>;Bc{#1G4 +@mrd6J7Ag_E410GzJ{jq*YiP3BJ6w$_B#(FmSnQ45#Q$SsU6B@+Q`_=Xixj+!PWIS2lg0?S?u6@a#z? +Jfxza=A4$OvRAk#dmbr>SY)hJBZEXGNw)3FJ;=H#xI`-~zxWFbpI3`|H?C-)-whr_xoz)oA%-1B%u%_ +@lDgO1pTO915J?TY`&3sfTo_~~BD2PAsD5@zyHsxmgO5h;EuN>kmo+|V`;W~z6(5v +z0iDPPcoJjpc>3e8|IO`FB2L2b- +`t=;P3oJhQQzh21MqV7OZyJNe) +%}5^5E!wJ(b?2JUkP7=LwZ0Rifbul}ElNpWWnKP)h=Z*1D-#UaoG`$Of~{NZmAeU3$-FMiNhH6y1>Ev +Ft5v4mArj*$au))4F^C(xr(M%mL~Wn&gO3T|2;k9;jHH*AVP&bRatXwI`XMrpPz{#ZP4cKUE3#IDh6HHE`b4;fi1P1AR2Dmdz!Y1E!N0j(9tM^=-TFv#T)g$JZ +;MtE@(Z;Uo33bK|$G47+!rt-Wjt4%_v|B@MfadX0eL{~eaX-h>iKv_gj)0%tnvI_1VsUC$N2vnQUJW; +N|3IuFmtW7~vrvMSXTrQjVYT4dYWjj!wE%0x;{6_}-#9+x{e{m#0Npsk2;zfDq%>bd$N?7U6j}SLbIExQ1PEI=O9JLsQPHi^aWuX1Oj9X~8BNuiq1)-Pen0rL +J-b%xmt68VR_uP+*w=?v_F^iQmdM>_@N^`R4!gzy0sw@XO_iuAu0ksi>&cvAGpY_S7}{lECzLT(MHkM +Q?y(rgu*ji;(epUUCp_?irj4qI|sVRQ-sgIF-v=5qSMXI&79;&ty7VivemW6BxD*&4CEb?x%2<)2SYs +7Hm|Dq63jfN{%d}&uHMZ8=Wu(?fI7cJu6GgKW>1(vFK0~XhbeA$0x;r5J-~R-yo6^$+~o2R50+)qzS) +YmE){S*AJ-+bG5+dW=BmCC-)cFW>HH7^6r+B8nuOo<79Ldj>otsXb>1=Pu$J0i#o@>_I@)s>Iy!FVKy +tiVQe4#3W2~P7hD&~Oar);kqHQJz&%YUvM}C=4TRQFdxbp4IP(m7%tgBCWIF1LGpPvXMB%D?Adnu4FQ +4DrB_#q&tFhmb5+4ZCF*6{*@Gnyf0zw!8{tx(hBP7U<^6VKb+Z3JU9`L*M(|3p`))Kz6Pim67!a4}VnQq(`u8J+x +{9WY5JlN3!g{J&GtbZO}e!jX0(GIa7Ae19FC@*Bwr4rPf)Gf{OMe(R4vA?~#wkDwIg&!ZnaD7JWj|0< +;e-p|brj!NH(sUxRL3hQbs7Wdgy;qh3NDlasxEUt6h76qaC?MbnAxX>bOYnb2YbI9U6iCa!GN>;-!(ai`*JMJ*W~5_@OEdz`5>eEe_43i`tO2CzBEz$Ml$Z7sy3s*C8W +PNp$X0AJNaa@d3|4b0p6)n%&L*cth0$Ibd@zXWJ1`L! +wdE>he9v-LHUkhn?1m#6u_5Xn8O!n5mx|QDOtj$L>EnC(nG#=g?5T2bVF=`+`l@ORb-*=|WHMP&$Q_n +WMi3C9+Q}><&Aqw4DEL%sRgYGEdUZs(!}J6ev8lgh=*dl&C(Ye;7vfN5lKV1Nj@c)SJ$Jn&sYJ<0bdWwYWH@>P +g)`hZ@9qI2+g2w!O9B81_IL5HvDc#HF49N2D1f$-4-@1-nxrJzn0`Vw7$%$X=Xt{2q^*HTAV)q=DT!z +>8DH^Sh$|MP0C)RRla)2HZAhHFSpZPq>rk+OKI2lz&G@p>I~-0P>mO5HrF69Xh|;G;Q}_->6BcyYnD3 +Qo7-nki{%prG-HIW13F$;EX-Q0&A!KNT3o81&{7;ez*Y2&ZtTJDrD+MPn;!lSYz*W3?W6pvZiBs6*%) +Vz++gk1jfZfI$Mm{3=Kw^fT0Y1MJ%;$X_3z!RRO^D?_FUT_LKuqnB3OdbpSqdk#>SY`LKR +04DI4NW6Xt$cO?(^Omb1~@yPZOh_ibwqmD8>T$8=COvmXZj>PaiBde6~-_q3EaBa6Isk#HWa$K{PQ9|M`{NGfkrF$eZ@XrBo6I=fgK= +;#lhkvRp?w&Q%vqKARuHK^TWy?%NI4Z{t5!GBhG|=gqb%TkxTFl+IKpg;W;gV0Fiud;}Szzrf|gPhTI +ZMV#M0$d=(*!StHy5H5$&-Qs-AP@Q&=>_hLyJ2Q4lp(KeM~cS=w5QF&Ej7ffT2Dc$1GDBo +0@qCT&01qS88|n#G@EJa7|$xtIql41Q@gSzIA+sJhf3&}ePSo71Yf!huZJu?$Jk$Kq!>1Fmv1`;dHt9 +(2T__m=M-nv04Iuhe&H)O}4HjDx9KRvLUT=r`AGcMUVoZidOAD-LEh-Vt{teV((xEIWuk&-rQTY4(Ai +K2s+>Q7HBTX0mC_foaJ(K7KiDV-N{%@_G7fE7!~k$XWXC^y6odABo~!&K86$8zq)q5@2Ov8N|YoTI-u +g#_n2Sw1Y$>0D}>nN}!27LRj?#b=g^xB;A9f!{{N+2n2vW9tMa@Y=77iWQ|o?P1H0!O+k4@5KL4sO#{ +L~x_Ai&#R7w=K(Hrx?tf(WeWk)FM&23_Xf#&X95bs2a>LEk1XLDKc?}2%W$Cms0=(qqNf8wjbvBZKbQ +H|lK0e@q>k5ANc1lzn4tOdK@kK*SA>)TFqv#xT=T#RK#u;Do{hgaR;c@3kB%nm$*-avV-O!<_Jd!)R9 +64H918mDs9w5Rar+HZ3bC_t5u9E#N+j~eDD?TI$Z?LlRmNTn##>B4v-yQ+)bjEoQQ5Bi184hep*d!9J~Q8kl%8P8ZjOxcU>0o*YC-qES{7BfktK-jBpu@co +MzgGPoKH&Kw93!3^`Z3dr?DlF0VxCu+_!W^6<_8Nx0+eADR|iE|d-0lcVrrb={lI$+$MQc*SEEEMq&; +UrHjp}hlVb`17Knb%P?ni)4Bfxw`t>A5c|Xs=H>p@y+Imj9GOFHu&G7uj5cz#w!bXny1OfqU5laq?Qv +k4EA={btu}stD<|`N>?J94-((6nwPkKK%*yugS{Lz&KT_;N2xwT`S%@$A|5qw;IZ63gCXV&*~XP{=Tv +}m@dL(N0XXb^BOzWXpPf{g|}c4r5n5y5fXfJL-MT?SrSM@ysDb#>Fm0w=Ai`vq9DvCYji&6bzwJz{+& +P+Mx-BVh&xMWPg7TvQUmcVNu((((MwoD?`_zv1*?la$Z+Ki^a0cBGNB|Si)65X%|BXsnQc)tzRGU1`J +z}!!7x|XYaG-EH#y!~sIYmob3$@N{N~NtL4730=~i|+?~jm;TvV6))d-phwbNEy|2{&oNM7HZUD*1z8 +hFMC9Jc28j)I`wk%j-ta&`VI9x4=GyUXG^ee~m213A$8!Yi>u_ELmI=lmG%Ek)pPASwy(ZVyGE6A1%s +vkxvpQgR-Qs=ZKpKe1jY%bwpy?l|!lK)B0e&C)!raDHMTb_a09$x`R6ca`u+Jl?$obVLN`YJn@TT@-b0`@MBgwJHBk9yRA!Lwi53<-12)nBNG1${M-do@bvkpMS>_DE +BTkfohhfPL$9H6qgW4$n24-lt}PbwK_?Bz6?!hf9}c7TEIueR1<_hy>q(-IqV7mxbS?5pCwocXzWwLA +!HIABpyxJI={OhS(Id9J*X7^kS~A%@m4|P+J^MM;DG(7l9+$HEmy$Z@=XzjgA`!!Wz=WjVSQw$yPejs +3_-J^^ixX5%Oan#lJWL8!~MO7y3y$PfblI4aEKn0-e@w-vOHPI;$5qi}`%1{hNQg2nbnsXT{4mM@Zq# +4`zlx!Duyvu@;zSBA6V{YSDb;fZBmt)!lvPXtYJQZY73~&X#3BG=Svm>NNsyy8c}=%Dq1Nr9Qa-4KE?Nq-n7u9}+T~fqAjAo0_YeJ_vQu@DqA08aYsuC_fH1`L4pw?wZ-Apd3f_J9E~J!ts8>8{f{&s!1I1Zict#!LPh?L7#@D7xB)8MQP7gOvdY-UXdDmXJ-lG +8rijjf#Y{bw#qf!NVEFVg_%qOKfu>yP9Hn@k`DMsJ0EnU?B4i|Q^!8IWK{N_?yrsT~(lETEuy+-uBO +xV}!fI{&$*o8{#@jL05z$^pwbmJsh`2j(Imq%ns~#}<>uMPoLD9X)!KujmW*)Po{3M*x#s-^e +rUNc1%3=z9i9J+iSmq)hPrdGyWZE544~n3jJ*Emi?PZr_&|t`pO9{ve^?mneWguU5A3IH%VVE3D|HP^ +FuOcJb~?%>PG?VGA(YBWCZfi{y-mrqfSMhOk`}uz;_V}mPX(&w47B>;I2$f4_yEWfuEr|3P%jw{k&od +=gpoP)M2>6je8+>ssQ4M4VbGfaL3k#=)hNw=ra8YEKFrny4pF06#u_}+xmBETK1h3YZIuLOieN{>7S +rw=RfiCnZeM)lz0ib{&Ulg^Jvm!>xxqImdKCINN^diofxLiDEf4bpH;A-jHIcwnpH0|FP8I^}K(J}7Q +8x=`yUtQ1GUCq;c-e1nu(1HMwt#xN5oN;lURcWTm5j~hw%FWea^m_l&^3LAS<-?NMt(r^>2sA?T!D}+ +xAHW$QKQ7KLU8)v>R|lqAz!@NeMWvW-S1I2n_DOg7J=S=oPzVdQhi|fba~1~r7r`*e@@&2&5Lkp%Z!> +cS+-?is%Qn^E!-Ya-CV0W`Mh|H=g{l1BbP?|9%4$nq+tZ5}?9bbDYCvF+8HjyxF1}3_X-ATQr5NFIG% +i#%Lg0$;+N-|E&N!tBqSxwJoGrb1^4m^iIy`sluk+?#IWwX?IPOJ%sTt8Z5Q=Aix-8~%2`;}IV0m?!*`DSf)9pjBw!FHGD0Qje)PWt=nh6EnY +Hm)}Q4$0nLNVI0#7>xJPAs;v%zRRb}_NP)BsXZYK`!iCdN;Xf1H@w0^{HX2!ivkfr +0v7T)eOUYBrzHch~W5uTo`CHdB)Tf75HuT6729owDw2>z>?Xzh~&0w41C(+3V~hEA6)Eo3zN&^3kY88 +u(^z>z$YB6B4b$*AM_^)q`*DHr~sIjqkI-H*!1gWHN>cUXj}Ffp6&c*3Zjgj8U)w2dhg-Q-qFf+pan- +kqGccOj@TcT5l@<7a`+5-vw_}uBucs$6Fv{uC6h!3Dk<=B>#vc^0pVgBwB3=GuIsOd2H5I4N~0wC`gY +zUq&7FW8G)Xji)1sGT*YGJ6%~3JAM9gcXyIuG$?|P0l<%ezEHp-fF)ouG$xv?02C?Eg$tAThw=6#*lX+M*5OC9}r2gZoBOsia7UOg +Thga4T2w6-;zXA&7Esti096)PX2gzn*8x4ynN60)$WmC766?R)=<6g8DMrn<41({VeR^A8^W2OFV4sm +XVrtRtcC8lR~4Q~sv+D-n=27VM7_^ei+;f@F9NHh26=Vs`$M`iIW?r9#5JCkd9~r5^* +bJ2b(FM!-(U{vt`51IM3ve^HNY9H|N6hvfHqLtKk*^a*a*M)ZoJxT3A(@JnFpAEU1gZK{xILPAc{W$p@yUU2=@jIvC(vJ(%9_grd^4x`4$q=w=!WFSvyaio +H-MZ@w0nVy)eqH{lW=jJCjjI3ee@%Y|Cne~=k6y6nkBamA`NB>q=f$O~+ux}_2rogIjt9e +GI5Z#tWTUHfW2gIu&#Ih%xATyIZ{<|paGKqww<%ek7BJtDw@5Xe)yhY4O7U%)KTg%;9!H=b$o{H)KxY +T(UXmRxh+YF3oezA;L9lh`AVmPb+UWLfhJ;8}YZF)q@VgrPIrcJcI>ecJn3^z24P4MGC8!!tH6*UEKxo%1 +d+=U6Hy_)3paCGUyB!4vf6+(Z^PGnR3%l33OHapER*rHB}lzvn0P(#4MR4+ex}+Xnp?2JRiGu^Oc@|8 +LT-WtHCWZ5v_#~2L9(CLvu6FerHufD{N+;=is|9FjU)>Q5IOnCZfZD-uZ`SZ +kL3{6C)026H-%@5LYamvwj@ae5DFnT)&{%$`LI7&gJMJC*N;#VdYe;a4k@FLD*kHsNfxs +YY86O|<<59me7{EViA|mc6+eN*Y5I+rG=}caf$FqO_Y1MmGio8l6QePm&xboEi(JHVpI26x +L=70z#{wBoR~uHTgawED6C~i(fORju~JY@oY_P9OzOb)>2rh6zFpn#Z!idg0$l%&GLSR%e(;rq19V#h +=^Cxeaz6%U`?0+mMmxqevC(=d)4`}2!!v0YDJJ#JKpas1K0X`-U;+S_O~C{2mZhw%LVXo{KYcWx|aba ++tp4#XnrI@$}0s*o&eM=6miIJaR%X7XP?&nSYK8w9gbYIBLWutXj(USbQ3^RUausq9@WxhVQb*3qdOB +3MWWr~nQ~=zZ~Iw=fUrD2AFR!Ub2y_Cir^P^H=B_mL%6Ah1fob8W;Lhw}L3{xr=TQ7r +=L_48qM{+Gjj$_tPrk3DlFn$v8?7&QSYwHgm=OrQRNs3dXn6SqKUUvcHrU)`VuKrhW29xrI#?d00!mkOPXIrp7`VS-pS4%m$w(}7IjMew#Me_SeU;2bEi|5SJ2@-JRf~GOAV)d6e +rTbkuO~VTxc>zF7HGAR*GQ5^Hn3}g`2DTLJVjl35Ui2OE>WOwddCq@qAV~TM*yK=KyyBZGCFLNU`4&?hl2 +R(M4{`!yiE-@9+QG7Un=`qRpgcdpMZEM_d-LFkheu??e|3pLiB(t)>7*u2CO9;*FO`98guB6plD`vO| +pP?o1m~Mp$EI^)>3TxkvK=WE@7#DF>8zla*ZKl;lTT;((+z~Gh(yU-~%CTu;|u4OQafKX3l?Djf2ZG!mRDdfN&YA9w${fY|6jX!HAc6e3ATHY{d$O97}+Qxy0)8y9mI4`4y~??cnH#cREw@dP^fXgNtVA +hI5SANA{(L(JV(OA?ILweq93D1&~JPBK@PM+3j#Lt9>}LkAM;7w2?Z6J2axr}HIq4FkIAa-!I*O6RZ2 +In&jRnQD=wv&)eN9}ooq&z+8_1!V +!R7&epirvzq~jt_*l^7P9^_+0`qez5FMe?OBdUd<1yA$llANiESMQO$q~FwJ;1rrE}%;`tqiaZhYvTY3gdMyx_qiRL231ZM*RLep%0hFulBb8eLPzD7iZRWL+ME==aBEjUNO6`W%V +L>x<3QH{k1M7hayetPZ9R0PauwXq{u#X|6Sq%<hk&d5Q`wRXt1bc}8L}03A|%J;5dyVxUy?(Nx;^j +#i?Hk7Ijg&9`6v4M6%QOA1@H~qKG-39EFR&v^2f}AfRI=*w3M%*P* +(r?f00H;05(8%`z|YifnR0>Pw?n+mwb{Gd~0Y_ +$JPxNNfQm)bAbFR!Usc>`|X@?(B)r^D!r;9>4qi0cPWS-!iG0TZB^e61!2NT~~q! +U+r|l12TP1qfA=gX*MXv3j+d;s?Fp5NThFnnPhn(fr%7JT7oqai~EFmH0L0i0^{^IT +qlg&8zXkU!1A+gO(m&i(Z!%V>xs3_jh0$0};Sru@D(b_H#MTNIxsD-J|o;cV+^DZ*S{0{tPmw0H*PcZ +8gnT@bkMDx%u$zfLBO_w;qIq(gAMnU@XFdP;pew=jSE^8M@_Ufk7iln@9sV$r8T_+@l;5suETF3n;2N8eFz$0#A)I`mE~^b%ok?OYiUXf%FU +m(-xe`#u+?q%~Amn|7`Sb9Lj~rB2-9{7~E%j?&8l-!5Juvl$KJ#FN0y)Zx$i`QG@1ug}Ydk=fG0oZ +CBi%8{NPOd?O8-L;O%W$L~iF4b6z%g*2T}JwM5>an?cp8Cf@aIoeKF$#Y4vB)M_#DTrwkU5lDGl)=^X +uxXtE;w7p5!xYAa|)<**>rq3orDY-KC&4&M^IlKwy#ar46K-K%-uTbF6x8K%kK}%mzoo=~c@_09l^C< +BsK!`qZD%+5J>i@96j6i{UtsbY+-T9!-m9RmO@yppl6yrzLLC5+Zd>;{leP93P+Y5}!4MCNAwXR5uBNE~zqz@Zh`EYqBe<0evt{8xfM~wi2Qw +HP9}04#KNWd~=4nPCuqgM|8`4X)4yedP^Obf>!IVzB)uENPbtL%l??00nHdO?I&)WQYwduV({XFuuZm +=g%q4t@OTD0Ha!@r_MT^H|&VIqPy*ODA$wK3$~TU(#;c|=UHa6YFP>Jo4un@oH3m)keJ4O2x6&O{SfZW_K%O+ +dw#?Iaf8q1;>A~yARx1St>sBanm_PJ(-NuazIx33JG(DRuWSPN&A&xz>FmGZb?h8I2v*!bMpbb)+&kQ +@mbh!}%)R7imb(l!1ncUjJ27AHG|P)8H8-(21bhIP-F%&xGxYwUO$1^%upl5r=j;#J{lf=ciAJXlu~* +~M08Ho;=9@a8vrj8iUl6mQyky7MP0#PrS7&u}b@i-n%3rOoYie1DVL@k<#Ek2o7rfMY46c_R55l-xIB +z+E)o~CqU!_YCBX5J*XU$xq#y)xcVKKY?bMf@$GXnvjSKl%6=^`n5-bQ75cV|IB2%gJ2P=1qNGxk|f< +HzMtukwtx2CoLg*NZ<1JA045`ac#_=gkY#)!-kCfUXA=c$!XaAUgJvh{!l|t5I6b&p22q$cTaiu}lp| +batmBr#V4C?<@3Qs|#M(PjxnD{MtkW`Gc*bp+=3sS0KZl31hz8ZGyrg(B2Fx2eY=|;f_GHu +4qcO*vWFOFpYRN=39|%T7t(3-oV}5WpheLw%{%DQqx +UzxZfUu)l-^(!8`7VB!?YF)L~ZyF;U-au|vbnUhNxEG8WWKwuEsqPlU^db7|IHk8}sPP +yGE774?Cmq(7I$-09?EE3*peOP&BKcte6UvjMeA;6q?c~=~pC_gxk+B-P5q3O|qs8jU&0$F>f6hUag) +o$hUex*7u`F&e}We2`);;T7M!~>#ft*-v+x2zMKB4`xko*1wS +XnLn^BuQr?mBYN4ItAk^*4(jAY*jOsvH;HyI61mvpwaFQbh4fWF(-(qj&0Q%t-bQ0tmc+mO_0Ig2H(u +R&Au}+<0-iRO*e@|~!GzKUS9gIvhopXFP0cr0Ote6FRAa!#!r5>VUt|-NhFKXE00Ke*zz-29t +{MIaj>T6@1|LmdeAB*uSi-xxfYo)b3+Bb4{50+kh3K@Zt>-Gv;tP&NFoXvg;o0CJcMw1UJd(w9h-$tN(`rUf$7#uNt45*=~u5#wy;M@# +h+B_y4aaH({TV2w2m4xrqceO8VM>>eT>d(^Xg}!b8g>;a|FsHDVE(S!*Q6Vk6l7FiFcMt$Hg;EDTv&# +%wf8r@p2cqUlDPvXO|Q@#iYTVL@x495F{DrB9l&PI1Cf5dCMa7s;tZtZFtcVL0b+N@hKC~eFTA)8KRXr@1nk@VW*1-#usGV!=ECR +&o^^1A0fXk9o_)B|@q?y5kNY`U(|T*@H#TpGWUtW!)~~rpVH!0+kmab9ST!p#>3f}1a7Ks8 +&Uek*1kIG?rwrrAJLfml2!GgLKT19z;}EjP`!Q1@%S<+eC}+~F8r3-O2xyUq%*qRuoi3)H*3&ta3tlz +-%Ma^MxS)47*C+|iG%TRX3Jzkhw@IFU49ORjG0oZe@&pv{+bh-`tccPS`a@ip4`T-TEiNBsk=+{Au?K +RzJhAI>U~TCBzC+3zx1wMq+Ce2!+t)Pm0W{q5{?=u&?W_?=20hT;!uFZkyZFYjF7SJUiSm1n;P9t3B< +dp$}w@2=*Jg|kWF^TrTOrztOQcTPlGmcNym!b-T@boZW8wa^S;I*7EGbJ``Nq-hD~5B~&P*=ga~V(x> +}!Z-6}l+HoFAto;ZfkD1yhuA!5L~$TKJDBT}8b^`(_zVV)>8Sb_@rpUU`aDH)9Nm1V+sEvoU_Wov1hV;}>QktR2C% +Q=<>T&KBn;H`g*YKsRLPkKUXCyHYyU}^^0*kESh|%D6`aXz}YlB&P_h~sXYJ>%*8FyQbKsS(JTr@vZ% +yf*s*2>Bb;eHh-t^LqcPKR(oL!AxMbM#qTku%z@V48+N%4vH83F +9rXp2*UTEnU%vhB9H6`(1vV1NX{MeWY>7bdjg|NvYEL&s2lJAd0&&;h!uj%XichCkFUP`CK1I7$Ea6NhTEQ&0ADXOaEpF6&3Yf??j~`f``%YCent4}xLj8?r2u`|M +|0>J(@TR?Zsjf~!Rt?JiECi^4H*{h;Y!uD%)n?>clg+3E5L`Ete&;pEu^mo@29-khYf!M+dAotx7D`C +?kKgu~4OnXGIH2aW!bsW*xw`|jE-2nv?iVX>G$j97FzT@C2+_GkAQDPQqyPty$;;K&D7zjD_X+M1-9a +i$XpGM@%DSw~Oe`d|ha+$3x(=sR|RJjq~(h)^Rf=y^$^(N}Ba5#T0Y=>__p$mChMI5U%q72w6KuiCTz +CUKnMv)zKi3?OjGtT4L0rz^c0jqDgCuo{P;+b5m?2J9Xk=D>hh%i9e}dROFNFkP2woK*;j??OXEi2qT +(`G$x=t+Uv+o8pxF`&oE(ab^^H4aBlwDYvyGuY-jrd-+r;0jM2}t6F**2n=>4)1&5X;VIr#@o0BMF8= +itmojab8sN!cb@MQ(w?KEqu}nSw5tQ!=VwPGTwYz5X-Qkslg4cADVcYHt63z<+s&ift*Q*2K*4Q%B7d +rptVorqf98k09xGxHOSvb{4>i|9svcN)Y-9aC;;6eHF$Up#y^$o3ionH#B +fF8sQRTxhHtth9H0RRfF4+sW>G%Gjl@BEgIgogO`k^%xe$fTc~^vi`BKcfBvAkc^h>RQdI(-Qg4;myt +2&_>-8(2wnhc&9Da;*olMQmFu}J8blNl7`)FIIza{32?cc*HgviF-N&Y0x{bs}M?GS?RJSFH!q=Hw-HuqB{(>WW=#d{_?x3rH1W!x4Ts$wA +g#@-`S1_CI#dL<<^n~Z-FA2SXKI6msi&Qrh>~&jN!cB8Fg9cgUZ4YSTdV^?Zu%FHWqMYCBB7K>q6Sb1WY_4w9l)8O!g8k+zi-d|4pWR#}XUWZQ{E#{jAj(C~b?| +px2U_>gKdiIicOwzcxrfryjm1hCorb4ZR}wIdq?-tK!&;#(-N732z#U~V>xCOv*A!x6fetuf%PqgStcap4_ML)Ts^$`yjR+e@Z&JwVhTT0=ZhMKSqETy#48$2 +(tzR98Q^Bju#X=zPkr7M;E0XLxzCf0j{rDrp;_1jTe#uyY{nhOi>veyP1Me_?lSIdfIM?y_A@%_oeDa1=L$vfy0ZtM +%obT|d6~@ohR@R5~%h0G}oho_L9lXIm!jK9i#8n9Y?Y?^{2@8VBA%e3D$K38qhiCJ7)g$eqm57R%4Zw +Y+l<$JZg9F+U;I9)Q3hmdBlp +*s(=o%|Bm(1JG0PlTLx$anMQ^F{|vAtGO*MKmdl-LQ_z_D$>a;A>9uP=*b_8;xWP#EDHicQSqC2wXLS +Cr$Zk2Zitq6YKyHr45}2AJg8)jYuPP-N^~z?yS`X*?8!>eNbe^$o%|)8Al^WTLPs3Ln +&2ut79t%hsq&UyvXpV8A!?frOW%uQ&GO*4jUGag*+@J2>)VC0Euuv%rpjYT0--Fi&yHxq%6&5JeMSy^ +)x$P&er_1~k)Ijs`qw!hKB=a?8}{Ci#AKVO0kWh9Aq1Q`Xq6Mb|}^&p`>i6~H%iOVBly3cV*&!4t5Ut +Hy$ekO|k0wKdPxW$E|lAQ$HSw6Ngc2>s`O{BP#F{O5oCclk~J^FRI%>+#Xux#j6f%}jzi($FYZdRm{p +@Rt;_8Vpz#B1OUi2k^kIi0nz5m-4F=o>pO)5qcsCP|%SWqYxo<`ipcN1wguV@^kFs^_@ +Q+^QA6sHwAL5imL5+@ETt_sY?@Ik38*Nq(fa6@DzJIM~fYgB1k3J2&N4Qk*_#lW2-8Sq20evGnP%Beb +IM7LP460a*VXafnt&*#l8Q7rCvt5H<%xg89mZ+KQ#6@NYwF(;9LtF>rco(v+d +`;ENMBIs|DN4uFE0`p?V>Msjnkr1g^hs!oBT1$b^H?p-0len!j<=yzZ;r&L@693Vw-5L0k>(It8bAG&^YWL3ZrA& +l;}&3X)=8(iy0D_JkBi$5=S`*3)xffBe)8#NVXv&@5s)DW|K6WIS@=>5D~Kv(kK0UXykZNjKQK@WK*; +DdDoe}y1J#ghinV-2(38cKGV+_fcI8#(ij(~)|XU7byS3YS6Griv!h#O+#DKkr-yy!uFOd#yWK1e8m! +i&i+&?HtTVdfr;J5@V!fmzEdRlYhf^41dHOKS?sEqMM3rgH&$L4VE7{h+lrccGDceYC +%jx6Z}4~yL5CURt_TQF`EzvW~+^44(7=vD~Nt_X+ltpt_5v*QMzbaEGB0E)L@<23!D3W3KLpUg*7(|v +9Ea7-<5m>`2c#e>Lh_pZmp;(iY;s;M8>8v&~|$rTx{Py?k0R$`kCeC%V@CrolZZ$=K;EAfrf5)&Mw&v +55)`~Z|cG8vy3%ERjVseNBB72evFRTlWI3(G!E=Na13{=CClYl%x7`s +~IKak@ojrAsQ+ +!E3CBP6;{Zxt~HjELzI5odRo_uLls21n^*`jF03jKOojN92)2M-&%|t}c`FNL$SwxWN;OkbLJ_M_zT$ +8@t}c?p0vuCEho`ItJCAAK@TjT9%vT#8r*eSJ$j`)CDH2F+cTfg<1R*ZRNp+K(H9e_xBkSktkK1j0;z +M-2iG8%W}z%w?8TvpgpU|@x;TK=W!y`p>1jxG(GGwhmo3QW1gflHnx#&=R8KyRHfx$TKv3Sh8hHjg4< +;fVu2i0Difxq1-+-}Da-ugfnfNQ`gV=CJXg9|lO@mI7x#re`88nk6ed}gVw(MvaWn(yfBecCI%m+;Sf +%-RJb#wJjzMbd970CjyM&LEx)KqDFlY5trPJ3z&!Q*i4LU=Q`?FQK3GgorO+9~pa&k7&m5Ck^1PLK +fFzyc=UV0Y@&q=IDpVT4W==Q=RasP6FGq?xCr~^}F1;Oyz|D?!VkMQcaMCKz|KdyGz +UAo;HrrOAdJE(NIO@V)ii1(9U%mdiXjI&LPrTm6ZwZU~CWQ>AqFj|H!)$NPeoSS4L|(&_#pgCg|7Ydh +~+jSMAQf-w14XwP+TJaz&VyZ%x~!Vvg?XrplZxWO0fuNP!r!7ncm1P-Yw4(N-td{lTuA1o*vTA$^tPI +Bn9-4@I%5BT+5J_=1Odv&w=nskJ6f1`3$rIp@e4Uox_F!|u^t;8EZFa6Fh6zIe*q)GBAyDbFpn;Nu?2 +fr4Jmq@K-+ZLMij}MtQvtJhDbfR<=m;tLxfISQ_sUui5F+7R|!wRsET%cFR_^2 +bee#3>JLmfu^<3+UnyU|h3$vX0Z-FT?vWW|IAtIhQeev9K!p5XJ*j*+hdFOSK*-7WDCR{zOta}o84w_P9c=rA1I%aJ^F_5lv)@ +BE9<;@5`nRi +xG3x)|EMi2eZ61R0tU!C!wIXEqR_MR^wtC9rmIZr-kxdg1 +9^Hf{B&jv=Oux_A$Ec`NJKi#?#b04IN;@wECI8fp?!~8Z^Mi{n)Vk7 +_-5XCE0(a<>N#M5nGwG6ktcD<@1N(ZF*f?@#urUp_=0KL0KdoMaR+=g3U)%W-Vx)wbavm_b>yqu0x;Z +(zNNGBPSO$-KE3ofu(tEIFVTrXOW(3Q2fM~ZFlqGq-TPQ1u)&u&A2fY>$iSq>Bqs +v`LQnJR_0RddRFx*_{Zw5&%`rMB4G0AY86)d@1I!&sj0^e2oIyBK^Ae|VAb?dYbz@H@q&4-p_&H6T+D +_vg(!wq?t9Ts7goBmY5C&K +r@Tl_V%eCgrW_xU}L44NBDc0qNo7@qTp;}z$*2^{j5p<();58tR}B~B$=i-1&y+MH!l(RS!$hbB*G8% +(75@TJ}U{xnN4lOnyPA(YQ(g8b+G63zK{Te<)~pxgn5loDLh|{Rc=8*Xl`2b=;3cyS2t(LIq`aH5D?; +@r6o!Y@1L^N#3D1C5A_m8^=%QWY4%we5FiRZt6M%!G`*>Lm!0Ewv#v5gVw_%deLgIfAvkXI87mzu5fk +`}h6}4)VXkgv60)~T9qvX}XImfnlE2T{d{Ldx83@kjzNOn-;)kUU9l}FS0-I^-jJ{|4aJ$I}Osk~{{? +x#Qg1(~cvBGYHIStRQM&^+IZ +0|I`pfxhv+-)yko{&z^CFc;3QIA%{?JQs5vucmRNHMV9bz!ALzZp!`%h^*}X5m7EfRHOY`{%#@$Mj$S +qsoFG<(zfkc9YLVsnY(UqJ(7v0U({RJWeSw!ArSq15<9VG456Z?a;NY-OkZJ9l`=wF3^TV5I8gmF4Af +Yd$7tskaH!>rzt1^x9P%cf3~xPaa@e2@Qb8iSg>N8kq|#v`5hi(k`}O8fr?>3KxnORxQRuf4tJ>steY +P7UZq-8*TqleKyXcAz_4)Xbt?@f1-#?50=p9CZ)p1A`J>xCg4CQJ&`(rWVX`YJ<_5ebiwCh7teiboHC +cwDzCVe&;KtQ2rz|$9f=;Iv$95CGswl5lM{77lO7WF&xIzHas=1fJM54Ux1^Fsqay+5&gGI0xxBqq<3>yiw+2BYmcv;XZ_q%auyA|s^U +U~nsgr4{l&rG7rE^Ektm7Bf)9kKZ7kf4e^utXoXRu-PQk$`E$S +rY23)<=@KbC=0{bR-tZIx7!|@^iOvGRHV70gSQ@EqTn>Ti@=eCCLA4-BWwVaMLE15jsrj`}IhWYYpoQ +o4xzC0K56UFL1V6NPE-7h2Gl{KjWfoy3=BDuUu*!p|!?5NDE@3@7EV;j+{SW4`XENJBlBBAGoV?|HC0 +H8oa`JUQ`0GR~L$)r1Rk)vAR=vr8Ec(`VD<3RzQ@i!kQM=Vk6*}LH8Sdp-EjLdG^)M>G0Zu08lWzHf{ +M+Z~f|hkw8!@)uAXBGS~A^0BSi9Rcp^-3miWvr8!cpL&aU)aI{U`9PO!6fGFRnlErwMO|Gb#EPv@TK><)E$VdaDU@4J5`Och1zn7Dt(Ghj7cB`h?#5z1}Ui)iuuYMv2Kdd+ +&i1o)v#ru9688}d911Gl@v!EC0eUVF`JNIi}m_<)|+bv$KFEC<8=j=vZJ-^4 +O2;hri^^GMi*urVzqh@>uoMM{o#UKyIQ80%Z5+DM?W=K>9)VPnsiAmf(a3PEUMjgrI8?g|(*7d10~jL +xE0HKFP177{YM2sAr_U-SEz%sK%Kt&nLQ)g><^DIU%-s?vE_F+jNo)Qo4x441TLKTz7K1#Tty=oX%4S +6JtpC_1Pi*NdV_vABh)Me$Y^tbSAeD@prT!*iNgLV_<`*J_IKBD#u-0eaC*$Mfu2IfppN@rdLvw@TS& +1h~*j1a}ilNgULvSgR$NefATE!L%qVERJbGKnVF8?q|0TM(D6zYutlk7}YQ`n~~6c)>rm@wFV@jO6s; +9zjY{d>^jQ@h;mYy@Zv-Q7f@z~6SR+$TA!qKB#$6>G-_K75jpebicD^pm8{EjdNY7#76dl~lrj{PMkp +Ex&HGkObYlU;8*2#{z|PMTN;1psJ4P!$j=7pWK{xkU0BV*T_qknm&a-^aU!4`S2Esx1DZ5RxWw9VXl? +9uQV^CI9?|?~batj<_Q@1TCcTS4p-VR>_{P2ag?RdZ8+m^M@eiV}|9qX`c16YvuFs+OXH@AMIPt)Q>0 +*p#;B2jg6wA&19H;?<#U+VIQ9^+o^9Sfz{^n|U!J{o`zgtVsDZi&do`jM>E1^=zL*bACSMa_K>e_VYz +{SsOp7)PZu#iHiI}%>|%V9g(K;SA30*@VL_9?Nq;g1K78)OFw(^j@X1R=419{9skRG%z`1 +>z2hUc%v{5c1mk&ZKwywqV%j~iW7L{Qp#S#;fuI&U%kLG2wFqF^@uE~C%yzBH!h@i)_g{T1O~4HZag`z~Cd*t2U|J +>h^zp8&&r@SSb`M2p#C7=HW(bWCc^?OU;+jZW%}czQIi +T5r?1Zr7K-N(SGMu96Ef+_(Ek4>VpQ9@_(QW>k56(mV4s*0Itl_=sqNYs-5`#oK1@df>lL2c$kQ+u17 +HY=GAH`PI?TNgIA8LBHl)(OOPi!uAG?Z?5+^@G9cERdC{EO1?s5hRiR;Qj=6WNkm;qHo0?-0q4T*%}V +sFyb*^IHf$+>F-cxL83K8ri%!8@Zy@2KZ(^bvn&wnXQKGiz(q%!9~cvhVH>v^#y&vQY$;)+qrniIKB? +g?509sgVv(uTqxvOJj9KHf@=I7($($EPkQs#z4_u%bIods`wRgVxyW}PrsL@zcufAiF&Pp9u8dvs&qb +zdKMiq{8LQX5xEM+*;P?9ii)iMinx8?hT(p6oY`PPlp=p!wo>cJpZtg`~45bcQ{#3vQ_hKWMHJWdkVY-t#LjAQ{Ba(r*D8Q|GT4aARvG3hy_)b=~JvKB;nE^&`8W +R67p=u#S#qmqR5j;szJ~vV$exCTbcI#uU_7*TxygqN*e=dfJx=hE!|p`Ov&L-C)pNw7E$F$yf#$dj6;IZVj`8o~hn4W>1~V36k9$DCVfCW~Tjiv@d)KwsS$JE8mVB*Y^Mh}9%(u +AH~O$S1ixX87{wqI$@*ac#jXZfX0IoXh>Rn5l7E8U_I8&a#)cJbZV+)9usgOhpeM`cDG_jqnL}aEs|q +`^Sezd#-tXapZcLMAy@Fj(d59AaIDi`@Zn@Ch(%F%1}8wpnbQ!NN_zuXVom#RCNXf8u29~7UnYs7KUS +8%ASKvtCzija59p$YxHAd`nrgmzM4)~M$tgfV{Kh)LCaLR?>N6z<+fO +*2-xf%`cnZ6a$UVh*+0G;+c%4d~fF+g$)V&z;b?6*W*L;fCCPg$AzInnaO3u&{+x^~Ft#uI|C}`WH!BElsFW49h+ +8`)xEjw6NPOb7XTV2eWO^*n6teZ&o-yddZ0Fmi4p1g|JmF!Cr%-I!?$CW3XTpdWN9=!W&)biz6QzN +Yy+drrR$Lk|K(cHFf^dg!hv3Smc_>D6%N~%__CPEdA8HFMNV%KhIO`h%~^vCGAETN|wCyvmck?)6jze5VcEoYw=p~rv8kK2?&v@`UC#15SfpNNXg^sH%xt#C)X&N2NNB}p) +gyX;r^txofFs0dA`^B)`1n`KfH%(ILbuidB!z1d7s+je5=17KK4ss?u$X}5d@;4ES2-E8{#|O^FVBLG +LHtTQY4q_2EHlQLjTG2Nk&#_u<2YyT$KBFpS)^%tmE8LEkxnK%;wk>&b +EVX!oTelIehT#w^yYX|5Jy92ARk;EFLK`Y=ed`NLM83)UkKfegQ&U05*!E1!%+4uuc*B7N}(Kma}<`p +p>3(6+N@xO{vl3SNK7GjuMe)AO|QAOPfRVL3Y;dA>P&`r+?BByvyxSWJuj-YC)<@bmrElnm0TkQaC?nb`b@kqYt<9EYc~scC +9_^=tM@S{0ci0bk%SFBFAkLDU`WI9m8b2M?T|Xxo{vrv{D*vgBvVa$*_Jia5rg)<^I#xb9a*;=ir);7LgchKnBN8c*SewR9s!#-}ZGGCl$ll!AG!ld@Z)Frh^S;<7N?fo8Wtxv +I2nb;`zuqvCSR}k%rq7e50PJRDt(9+Cg@Y9XNFoa;oCNi%cZL)8y?DU`QI4)}>vIVT%l^9hTv$xe%)Y +^Kv$3CwiK0CHdojyE+k(+LQUn5xHqQHjNY_lJZfG%@L%O;tkOq3-abttUbV_Ak@r=cRwKXl8$c@a4)Y +nj!29hpk4>tEEavvWY?x*a6Xm5`MCKMa2`la;ZWBpQFSD!&kY#llhAf3W1${fHKMoQO4V|dUOCE|C!c +}el!SLOVnnCg|p0y#`^xg}ma=;!l^=_6{rvKr?p=m*Yf1oQzEgf+|+_T{%l4o;ofGUy&3g}da!K*J8- +r*3a02=7fRUY%{ED$lZ_fiZ9fp>WmI8fu~78PR9T+7hy7%FZt0nI`(}|D7Vm%**c!Hj!^Dvk^Yj*$X~R9GfqYjj!N-KWfQA_GgoEH@KsA%k^>1_pvCrPqpo{E?{5+p*p +{u8PHl8aD0zx1&e`Or$P=qe>4|31`=j_>6GSWbAap;N{hZk=KEsz91?b@tKbyrFG-s0xCj6Y)j3T@mZ +%rlnh5D!fMN&wT4m($qMo!zAXy`y?T9$loi02#p=GNLYW%W)6EiJHxoRgpE=papc1IPIw +Yc8Nb~$P1i>)2Aw(K4qAI!2v~%%=HWC2tM~W@|C+t#>U6^TSgcNMDkGT4nBsuUkYH_4Z6F=nsXf{?8qMfo}B^=CB&VDrVJZ^vF`0|x>`VsXBUei>?d^G!iu +6Qabdd9OytSd-;~mJoo_U>29h@FVW|0_Y?K32c!Yq2-YFm7_$WLY|4H!5<&ab;s-{dI#N19DcGjjOSY +XJ-1iv!%$C`0ahlOU)gaKww{=lSU#>4at380#5M(2=s|`!d4+Y=+=Zk*jV#oo!&iW&fL}UUyWdcx}hS +JxDzqJ-Q0;63@E~8l{4&-b7N?!y#yp^L#0z8)LI(?ANiUxL(tB=Q($K}E?Sj*dH=txRe2k17j +T@G5U#&xwX4*gBaO&uKN+p_K-#TO=DTagP6H%`KAP8fUB&C=cd`3|LiijaaKTrvQm@hArqgH_R6XyrO +C)nYdTWXlb-X_{}!usL>TcJ~!F8c#y!&@;tV-`nJbeHX~2p%(R}r0LzM6OWm1(cQq}Qn;9A;nv +n6t3Hm8Dgk#>$JzI-n7aatOuy$0eF;i~6yO2?JyAvxUVm30Jv=r?s+W|MoJxZ@n|_UhUo5b7fedHIz2 +V%E>5s7-4S5CZ8(6#fUi_%g%e>W{%!ab9h^a*F*aY?t@MnNt=P1cbB(tMgYYH_WxT)9SDC^yfon7@#b +8ca{Ep@5d=gIE`LAeNSO!E-LE*)@Xg?6ziku`Qk%?(1rvohiY2r2*tsV$S(9++}BiDWVmgA%G6 +9*5D;2Xi}|*$`)L0{x`XY*>HZfgyHAVNlOuhVE6uN>E`49lHt1*Ny(*LZKFgH>fkwpC^V`bj=rJk6>!vo?xG8x5;h?;-N}GL=<&ejt#^b +a>AJ9>bD{)rcMr9jFEfE-gFW{`z9;HN4C(CQp77~ +it~#P>8jf0#xV8djrz}FcyoI73FF=!&~fj!t()lIt0$!eg@PGmp~3SA0d_+w^^=e~LF=1p*j-O1 +F)C%2s+moR74T~T^Bu-55rG}#PDGUs=Q)nPV3yfF18Ld=IHL1KOi^78d&_f~D2)=Q? +DoRHE^2l9t9(9$Y+Gh`^0IkN@u?}KkuEr(k8I=HjhiV;&lUPt>6{u>s2c`z-9TB5x=SAhqOu@g0q3MB +ZsiX6u1S^XLRWs-3+||6lSb7j3S|bzxWhZz@k1Duf=hfmCR&w~{3~V&#TMaQ}NfEzUz{|<+-3~z)19S +DQPe1mnKuglIZL>(AX|$uiyww(Q5j)tMAQ2ty=Pm-#q}C}*t0O{(;}rI}su~v4w6q`~6kx@(a!%YCAl +4PsVaW@uXER-r#{z#!d8*RMKNqPN?uqBCDcl!v@uTOt1bicE4MafR_So7k^0KxD`@MG%jcdL4whUaS) +V;+_zXJpOJ9sxdd9PV#>?~?G#4+>EL4G9J;||5AsUCjtJO~1ds4XV7>^92VEwQjZk+Ak*Sb4%DmbOcpkKLvHE`uk;ZizMF+n6@20|6nZJ{7emOerDw#rn^%0Y4%i@^mh#cmkjU8he(V +^`+m(*>hfIuTnPwoa=C{F?Q{APVYSiN;HMMC +3v>qxg9i&M`Xns-+hWSlV7veMv#LC%Wn9Ec&b&G`aNqZRu3W#)8I<2cq=HyT@=#&LYO-#Ub%-Q8Ytrz +MhPqff)=W;7(0b^?J$)U+*5Ns{RF%JE>>O&2e2zOD9g7>O{~{cIg%G;Qq1u1~@|J`eSjvmpb)*}o0#R +POXd$atN@=S~PJAQJ^8L|O8C4^|&6JZ>M=Ip#ru@GtdFc20O)H*ewF|w{h7o87P*>JS`YU>bb?cx;pd+;Bk&jr?nhBiU>Y}8O?nS{UY%)R +w+^hpzQ$Do^8Bvf(z}pqGUjomuc3@=m*-_Q%HN`R +Yn=h&`j)+b_G|Z8;|*dbO%SwJ5SN&JWGqOTI~KC%nwakE^ +bia(JnJs#}+TNN807=*gnOJ177>Nnx~$L|5=k(rbs!d0Lb)2e492F~) +4A^l%?0QieXducoiI;2M24`5JM?KoDou(=?@%(Vk423TNq`E~iF=b&Tg-qy#-TeFJl?ioyu;~Nmk%-BZ7d?ZpV#1$xMbt&ili7fMkdk1cdz0C&IsSlHI43n +$wS2usXBtW*HtFA2oI#>(E@J4wdQn@lqFGa=;CyP^N6Ytv!TC$NknX$525pJW%IC;Z{(*=)iLWHjIX_ +37RT9sLNi$a(*^T@3Y*1K%;vv(iTc~9j8dx5fdcyQ5xKM;%Qwui;0NsaJdXwOr|e!$I;7fAr7X4oGYh*D +kYEV&+f5FoM(L;p}jxz-ui{f6)**I~&bVZQp^+_6I(rwrZPtxfc{W1!y@HERACj+7&|C9pvyLC!+%mQ +rI-@aAb1XQc;*hdoHDr}SU{Px@OhOevBGNedglT4R;E;p_r$S!n&9BvZLMPcB?JT?GCk{d1wL#IB>VC +sxv^gaMUK?}~i>rvO1XQzgs>CK-zKyh}agO#8vKB4WMEaJTW%%LRrlxZaZqd@40o9auW!JY>G_FNz +;^Ozu(~s9kuEDL0n2nkH6Lvddh&JS(WI8{`H<@T{*Q&ywLcv14_*!6vj4oVp3&LmC#s*=ur>lb$PE<# +V4$-6rz#&QZ3*D%e!ZAhFiOkCc)rjeFsMwYunWObR*(ln+=esBuYVsU=a#^jfSt^%v2&~M!Wrc +kI7l9h7hVJFZ@#m0M&7(L(|^Z-vRve6dF~HX?Zp7EK5vUWjikHnmz6o_ugeW3P`J%DuECn58W4&0U4+ +_#jN>mj$&5PUw7H&A7D;lH%}()m)-nHndr2-kT6#i5hOQR*MM~)(8o-*c8l=#KuUbsZ +;F_JHB-xkTv655S061qbL!?GH;n<(LnIepD*qnF^d?N0t0+`pvnmFq^UQy?pSi_)zJ}RnNr^WMn@uJO +nw6pIAq##)#xlqlEdCX^YB|y5(p4|JPZIoQqZAIZ;WBX@BAv~A9T|Y=HpwjYtX#e+fCJlrknwxekp9| +q9w9YFJO_Kr`l6&z&5{eEDxA>Ao1=Z#VJ?6M$z@u(!pC`n)Nz=>0$ysa`iq=vvL_dsDC;T5HeE~nN)j +w4$iJ19qwo41esy+q>_NhKHRN9>CnQmg?56ykWUjp +k)P{+o2izIcFW*+y&;q6|-t1q~`cS`F78O~%pNL>Q00dfdN$88b6I#y45u$^(h_Fg}Kl7r);XeIW-IE +*z23<|=3r*qciP}YHm2?1k)HVpz)#%seK;!BWvUb-$aT6PD!&Gw`r5_0K-SEi_n0qZ?piLB8_GZKc<6 +ij&!ykY-$vA^4~nKVT?h22j8kR7vGBXlEa%57U=NN2A7!Un8r^5UsYdLEmP36_oB!&8XVnB0Q0?;T?Zd0~&slYVk`nT)<&{p}RMkJGC +!fw>yc<(tpJD2Yx#@>fT}b_cQwCl-ZxNNp8PjmoDDDy{C}(4XFw>Fsi^s54a3!Gh#Si-G)(_}&vXTpHLJ(N9fNP1+K +iz!>dS5P6&+OHeJVPzp-;wY~w|2JQXHZ3p*)Y3@_67tRRq_kR%xDJFJxa&vY-+jgHBy`Ww7Lgp@%7W9 +Tv%gKgVj0!{*#2lO`M#cpA{nyJ|MF9DPyc*@XghNxQON!xcr<`l}_ihV<(aDeZqPSE5clY)ny_QIYBr +r?kT1{Rt(rv3%;LSz(-kDSt=->yGT_VC4$%wROXp-ARy!lA$5_Ol{>KeYWk?TiZDl=!0X9Mzce@$)o1 +$WU*OFk-A_dzaL5T9iTA(j>CB*U-0g^X+RUHX6ELlM^@8gLO%c~LEEeThY1fbe+Q&p~+u~#lhU$gNzv +S>jA_Tz&M!^tVsgrA$9O!`4U4CAIK4~~tGzbg=)sotGMd2?{HsN(%}{ +tKlZ7+2zdF`dPG(&h|$;^o&B<#eV=^d-I;N?_HS0Vti#-7RBiUMD1N5DD6ga5{%Ka}JA`0|BAo$MBQRk3PxR`NcqTe~gMh3kmGFi$owZ7!YbYJc?52*n)tNX3K)+XbJdw$DqbuBVBS +b)gU0mwMH-IHvUMJ^Q^KZ2non2^c6Evi@W{4uPU`vo>>y})(;XK1UPW43?2v)a4w(y!1Fv8MBj{CkK(q>xBb2=Ntj?qsUa4$}Ea6BpQlgV#OPu1a;O;FafA<*Q%=&Kuv +AB^^S9UXILBhzxzRTi+Ms- +CQO!>Y}FBtPcoI#O +z!72tcWEl{n7u*WS$EJGK8U<(+`bBrgMTo&cX0uK?|3IyIe&Q(!1L!1Kjx29bZw`4%>?PDFnkjqwE(&iKd$@P$W^6BAkD|0 +U9sU1X}%ksztKHHJ+`CWCKiZ>eEOCdOaz_8t^l*}hDbPGJJ|LZPGbCnoZe;W+hy^lz#bx)d2f{JZQ66 +^cRYH8=Py$X`?A0Ufm!N*<>HdVtt~cV7$Mh_xwgM)uw>`wRpjgkYm3eK{4o`Pnsh`Y@gSLKyEjZ90xM ++`LB^N01(RpR$#Fnke&Ebhf +)Y8sNv95YfKoW1jFoY~h&GN6d#l;j37pnlwl$))Ckm418HQJVST6P2@XOU6BH_e#@3SE`-XNLGT|5PbSG!} +s_2d80g>5_wv8@tl6ieqfrU0ID8Oh~&@pxO(yk3Gl*Z9~zBAq>c`SN4-pjI#SRAGiB>_5k}m9Nq%I-= +?vL!H1HJirs&lo0dDl|zh%?u3ycBODjcxugPQIj#7)!h1T#@vZj$vUFXO}nGZApgr4?z2I;omSLVbP1 +Dj~=R^{1=L!rwndb`~PnG-t!32{lpYeGS;@-#i0*?xs-J{82VrP~5;Q)xc8-7kl@Ud`lUO(=wHS2cBO +ov<&xf_(S}E&?_>mTObCzy~EZ~z_X^q^RvuMG63G0V495v{K=zcO>!3{3R`IkiW +4X($)7We^QtPbmkw`qv|IFXx?b9p%m=R9~q?}pn0}#=BpPHh%mq7bexsB1oqv@aQ$r-sCs;K(AsVRh% +46i43U_d+X#M7A0FVOR^%@Bk{@u=Tu?C@2dH5~)~I`$PbNc{bPEBfWm9~W4+2j@Fmb@GJt|0OspO0w-1SV9crNp{k9oN_w~xEOr| +0HmE_A*DgDr7jMX9n~jhw+2||ks;4e5De~k)NQL>I`t^nCyIVjeb;ARx;vANN*+R$lD6m@DAVfY_@9F5yHyOT~jqD#?$SJ4v7$|4bl1brE*nD+#lFZld~mY*Whg +Ye8R%1slAHB+ae4A96B>$k5v8R2<U1S-q4GY3id{DZb3KcJ$>h#mqmor>S +D*`KQb8&#{?gIi)(`h_OJ8pYnNwZANur-P`>qs~P-iWA^k#QVf3OmFUb(gzfFN4qqft_KI>O0JV|2@I +}T#vI(r{jZIk#=j-X21k2MR7Ub6->T1p27DS;TlKKj++G|e$bJHz2jsHEVHqcj1`t_2$$R)79S`#jp! +S%Vxhd`jRDAuMJhe!d;^@?rE>5y=^%*_Bh@fe;*fT???#!`CyV`}{s2-byBlN3+**k%K2NotEN>E}qE +lOt4^4Jg63+UepA-eJ1H?aKn%%+6Ga#NYPK_xL|ayd@F@{sy-Uk{)wAUDVfP9vGh)^)0mrj~|jDFdrN`f6dI>(|}s};+Sv-zyAa)e*Bom7I=VoHmIoagM-WK&$4~pTj$_sKDd +tot*&0^5S94=lhUUecnW!@s-2T^B}Sg%!#v;L>-ZC6s?b+ASSVBMr*O6?!B@2Cyc5jcaew6U(db6AI)g<}uRz34& +@#+eL9hYYz?N8}Q869IMPggx{i(EQSiGD&^fe04Rzp_)~Rw48rjoH$Bja&_s@=1lmU~vo!HZ15Q8@p1 +a$$dXfFd|a{Pa|{Rf#6N}*eDJH{DgV}4^v2B?<5d$#k~US2;H4tU7W@mc!U^Ts4-ivucU&>WYRhPL9y +~27B8`>_QbvS{XX)vO%&MoVWlIioAUYxDrZ%h@*QBCef!|(uwsx5(wsMU+j2paGlJ|yX9c|+3Wm@M8* +jTQOtxQ=#SHI^1NMAP;g)%aCyLsJ>(juGWg56TE3(J~^Gz}RrCHs#i}@wZ_sfJrqqZQshEb*FJ}8(DS +ap+(vs#R61A}nz?2>W9fv3?#f6I<2s9rVDq8&&tP;?QlE_ir#&4Q;oh=d_@tKk<3b>hQ!9SL>qCkYd9 +qY>2SADt^Yha%Hge=ZfBYrdU&!&B}A?bMfwKD?JbK+QX~j|NnyzFG_6{TT-myz&d?2ThV0ZiiDMSFng +0H;(F|tp8?BP*neoo1mzW)GWzz(nifhgf6(pIVe#Bn^K;O04!R?JDrvZyma$*kRHQnceT!OQ6gum0-i +>$rPJt?aX35)i`-g@!~GLeFl6I4srs>69ZfG7G~Zz-aKN4ioP^6fNbV=;MKVJJy8@m@?!AzNOFFo&W1 +8!{gct0l8(j7D<&FEhQ#n+ZR*(CdZ`@F2}a2*IZGD;xR#E?=36gXD +K3E*Hz`_5o)OyQ#xyoY`QKIOZ@$(|XG!*8sR!&>rmLGA?VZ-qA!nQ)7pY-4<%J{DJe_TP^DRM-+E0gNI@8%!3W$IzXfC +$9Ej%0`8Z!HW;ZQ*DpatYBeBRhYyRx$anhN5|#XT~2PcXz!fX9@k@5KH9PfPr8M{5h_ZtdCVkC6|=xH +$;r6}WOtcPLMQWzxD_5#Z&=Lq2Ijeez{*T8N22pd#ugz(ZGDUwxH>T;gROk(`QU`b)!vP_0(mmk +qirgN`&QKf%9{NzVMJa#tZ;bVOZmQP(3)iGKTmL(-FR5=F`>mc&RJf>)Lm#u3l})@D1upHr4UN3b;Gl`0njW8#zM-;W?1(tiDnYQF?owH~ZEVUQ9ElR`d2XV9>>aC%Yh$)LzQ`9kN# +7>vY<8UtpArK+Ldq@C2}Cu9io_lu7m7s}KK>A9(csrF@x%ZRkW5nxv7QKLX#E23?nN7x+&t7*s~DD`z +eZef?U!j8@DcFGOfsfk>Gai&TTKsExUIN&NQMV-ee6^19tg6ltG5YlO%Z4h2s}a_!knib+5jS~WYXU4 +=m?_iX0_J|O?&K1{+^_7MLx`j0#L)6ILUs?DELXecMYUT#0CGGDbcNq8X#Cv$PQ8kmV-^0Jsazd)VRB +t?0ie#U0+?AQA&I7Hbq^LcV9|N5SCIgaExfC0>lQ$oNX)EYW>{V5W8TD&dp2{;aH#>b)$;U(iV1scbj +%aCge8iU)L8oq*d)JAjS*SAYsvLI+uXWLH|Hn3o2C8de^H&_uwB2L(;CHRM-uV8pQ+dpm;%I0t*yPf% +j>2pLj55rca->BwtzL=lgW@MVCJ?U}Zz}>tY1#lEDXE#5@V`>?-o3)8o^Df3AS15z|QhhD?omXIm0Qv +`Mn#Z)KGM2_8!G(-tl0k0K%*-p$n>=FTVgY;@?EtH0R6m0v*(at7NOMRTm|`PhfiG0{pgeDh*^T_*TEx5Nr`2z7CnvcA +8J&1gnS#G;lNoYKWOh7c`G*4XGSSu%z)pd!FnRUdDea)0GBfx}t`yqmXkW7q1iyM;oXC>yJM{M^HQ?S +H;w(RU%lEXBu>m4i2lI%mQC9Yi_Y)f+WxKU0)A%<)z7Nw4L%jg5W6LA_+Z+#4)+5?uTNrSi&El;Lk-? +CKh;zhOivQ$)li_G0D4pA&8F-c!BNdOF9$)8-)j;MD8|E!b9O<-)6T8Yr>M-EBk1OE;ZQqG*n4sVg2Y +}$8#Tua+=O;Hp`v`phiuRb$K$+lf0LcX+r_8m#>s(Vec8_qek`hB5a9>tNzpC4GSlu%fNy)4uqxLar1 +yJQBF4=We-_e(xJ+LW&W%Xsnb_^(>@-bAQo;{^OR)x81aALerL=u1U|?`4UwZ(mO`7Q!;|92{Mi7HP= +=Q6iMiFTh}PUrDFos0fFG*zbec@nxYj^wDAy?rl1IUHNWJi+{_A8iNk-ZHInGlDOxa+YIi7e?;wk%7M +&VTCvJ8C@zHZk#P>tBarxhiY?FS+vvSys#r?~`rcFD>%;W&Glriq~tB_Lmw7>GJJEDXf^-fAh3v?+?B +7b}rmrWzo@O}TfuT=PwI5{gyqxXeax*$)ZK76)y6Jy{YEmd;s9_HGMoe8u(a%npo5q>X+!(Ltx?6w>k +Ow_9V>C9uiQ6~>@TvriIzy^JMT+T)O0zgLl4KO}Dbc$r2?bJA)0q?JfY^9rIv5$*javA!sMPXn#}S?0 +VDx$=26!>O>yN;M( +*kcV|&z+%nYF1WQ73s+C2kf+erB#J6w3$_bhzWD%;Z`29m9SLZaBemP7=i$pQ`8t{(wGKxgic +Dp=d?OvCHy?&W?yF(NU0`ZHq9L~#yA_RAlwd(37e#K=!Mbta$bgM_;Ky#iPO+>5D>2pyoPTeiAtHS!P +W(j|ySP~e0H2SeQUp+xFf)x|%2B!vight*c?PU3Eesz;$7W(9;t;^H!zPy}+(piiU?BADrp2FTJHIFk +)3J*tQQI23YL^LX3DadDq7u-;<(yCbD03Fu^oko;SUdDeBy+HHk(Lwn0#Cvf|Bz@k+;Z;ALm&Hq}fv1rP)? +T8)-$wHXQLYiJ4~&CGfh_EZ89VbyQf8zkS~Oc?p*6)H;BRZlS*h!eV3^wK);mW5FK<#N*>j;q71(ePi +91Uqk4SG03qPFT8K_dUm->o>-88zmE0S{GvjalY`r&SN-`E#Lt+zFJ#({9K-t-6l$@$5}DFHZuBJ>b1 +C(M8#rz8)@XhU0;2V*TcLl)t?Oz%KFMT>KQ!FW-uMl$Hm{2rNzmZwB-imE3YC3JV~vMK~%pRw)oq=umBoc28jywkpG*m1Ju|#p7{>{SEjOw?%dp8` +(wVMzrT~vkUJ;C3H4uzu?msOAurUs*b?kC)ki|#`$tb{+hUPR4$@qqpsVNL`v`NRXz +BMJH~?c+JdB^zaY+RodBqS_vz&6^M-vL#JP9$-J9$PZ|KJfRUlAjL7impJf0U%>2$7*pM<YEAC3Vy7|*nA +9gwZmgX1~Fe6?GyK9)M>dO|T`?CCz?HV1WQ`p~pH7GQY!PV@ml4#2nsL3%YEl9G5)Bul=u1K^YZO;rb +>di%QqUlac{W91?^g$==oj6j1S&^sT&8SsC)4{2hUH7eF2h#hq0d9Y$wvM_0)TzSo30fe>f?b$lb;D( +fltKq&<8UdL;~TnlY^Vw$jJAGMiM=_MmBKnX!}VvRr4eG6?k~w~Hg!p82&nd(X&A4RSdQk8ti&>)bVG +(Pbikv9wlu}`x*|04-SwfnbHD?LMo|?SOlZh!z52L$Ph$!K*1(HhNU(SPk8>jxZ6A2q^F>AQie7e*;t +ULM&P!j=YgwU#WfUqvt%9Sv|C4@3=!08~F{|bKJg2iX59y<;?uh-)v0p9`p@?~?V +1Yr2_~#?Rdb0G?4XleuD8~Uci-qCyOZK+NbNKYI327kCg6jWJ&c0q|dcrnsDh~>(~-b4*pN)`)_2S!r<7GNYU +FVwhLAC%69c{;aNUe1~;m`w24Gx(Z!mMgtMms#ZfK=r84I=7px?RDi7q+t>NuuqH3h08ysw +L;Q{#kYs?6D(Q(Y6}a9TDh}y}oAsVFr3j_CUZhC>wsdOh`j}{H)m!P+Zo^o!?K2?-uM4f^#u83`F75U +7QfuOHZa6y!SnNM{Q9ydh(i<#1hYeT7RMW;lUbLP_v{%XQLI+)&LyfRj;I!E9kQsn^83&F28RV-n;i{N7#C2ZLcJKwwUJF0n5U&^K%_~cZyU?tc +c<(lH~b(4;ga;OjQ3JC2*O9a(P+muAN#*Azj@-=;yPG9!DA6|h0%2vQ0%5DZx-`WBYtce4&Qua +g^N4}dG0M@CoR>~gBuBMrj0IwE=bRbv~?T`^G)c3r$VupCCbSP48*J&7;^#p? +J+mK5I&@Cao~*15G-A>H9&W!EeUNLI!7DQK~MaMY~vXCEaGMV=zrz6FMQUza{ZsYuZ4U1TG4>i3IDQX +1e9(v5j$=S4b#^R}A&2=-zmTTpAk)Vn9V67C-UB%819CR#ae`lsiaW9INr_tu5kxbGK>! +oTrkmVWmq$t;-*pquiD8UK6wnoKpFfCjt*HAvJBMWplJpd@3Dyy8dWF3VE^tTG5qd065$HX@X+iFm-< +`|PhdEKm}dU#g2dqn*A(ts#O%^{%8Wb7qSo^nISM-|@u$fHhdE6NM=Oe~auYB@y?w_sybY`{w0WaS72tV4rwAK}>-xj&lN|^IVMgHB@`RV1E1ZIV*`65E@cQ`Ov(gX!`t0u4MM?q-=iqv +C(A~RHL9>h#F<>2L$9J4+}H&}*g$QL|$mED;r8j5t7U9$JEmq?Uac$dyiH}CGaD8wS +C?%nYp%YsG81DW7B5RF5BO@TcO&~OFzE;OUaO>tk$48amg_e7*;Xl+y)@a?Zr69L{KZo!Z6$TyGo&Yo +(o?!o0v->xAUm%ku!Yc^tFFWVpO+g%Ep^O{_7GF_O|6aqF;#SAN~tP)qBRM4E0D4b_-+Mi4*u~-9kii +XBP^ilqoZYZ+Y`nP@9yuz!C>stdnLWB6mJG~55?=7Jf78&!Oxw%}vvr&zVQkc=$); +wD2#Ll$%EUEQ39eQiKe-~&}j_0ka&>oR>)BM~FiELb-C%=WoFr-k~Oy}YEUz6%9b2bbF!8(m>*i+B|S +J5;0h%NeL{xc=zBCW}M@t+rv|7ao>z`}Oovf~Bpu5eUujB6-Y)=V|g{frrS=@>YI~y)-m0aP8_x^iOLi?*H%r +t#i_&;GxK9p-h@oCub?Jrcn02mWetS-5iLMg+%eOs5tZ1y#sakbG8bLWjS|c?%Lo#_($?!QAX5(U +#Oa*WQ0vq$7jxx!o!!#ekBRH^R(=~e~-x#sFM01|vsdM_XVUH$}Y3d$X +a{65PB#EZfEPxWE7cM(&PRJ$+euv(ZMzM}#bi{n1j;>Ig#*XdEf%Y1EL}sPyHJ`QTNLjd-_{LaPfJWW +~4DJ?gT(GMlC^h9|`Uqm;G+v8}#SQ=D}l2%p|Z25)R0?fA@rh$;EdAlwRN`gw+Znyr$6*edsp;?t7hJ +RgpWNcd{c=ijH}q)ahI!UCBvfuY5#IoYZ!Zl}GP&5EEHKB4!GfQLvsoIYohaU$EKkQy?WobJ_8vQRp@DAm9znESuly?k8{o%JUtn0KXAF4h{+VgY +3kNGR=k-h-a9>uF1YNB)6mCc>e}TjskIw?IyK0DSKbSu}B)Y(i0sH2SwnR}9|1~WaU()PxJSY|L2<;8 +Kj)SBZ6SaY1AU(h0L&byvYF8=-BFo8$e|NI +w0&*A9={`KV6S_A|5H*VuIEY6iI0Kqo&KmYX%e=x87|NXz;=zc7OXpybp +mN%e{b}JVhWwWNy6z&1eRP6olx+;f6zBSM+Ir}JS&rI(nqg`0egT*uX!o6bXJs8cqT0zSLc;Ofk>KH; +FFj0>2dLjUMK}TLb)j47U?oDZh}i|x@BMxV965JiA6x~R%=*qH)4Dx%J&3H#@41Bou=&!I~byCqYhAc +^nRsS-!t+ob`L}qx~SieB+%~$DSg?p^qReDw|!U;@YXpgaW_^N)u>w=N9N@Almkhr4g +-ed&0@5v-5q>q^7<2fq-;Jc&1bPJESjr-0}4EIcb=0}QW=yeP`}J7`Xs2A)Es+${Z&jWGP9A72jQ@I* +19>>0$hekEa}3(242*vk?0^(v`aHe8h63{(yE$QCR=vgGIga@vn2SeiRZsO1Whpo5?fn@KV?-U5Y|vc +cuUv|TNj(Q|?0eh9OGfTvLo-?8t9PNw6S`iqzGqVM+|p!^ow(gg@ke#|Ch+ZoL*u$tc|6KnQ)^=;e0z +zbw%*VSKt`E;9qRu^2$zzvz4SoEiuUv3>A>>|?a@6A|sTGxroq5P|*pvOM)R+D^Bk)_lJWF`7P{g_fR7Ir-R@D +u6o|X|kLEGovzKC`#*_v!F*=?8Ah+iJ6O(V +d$|}XHO);&D_0ZgT{6!+0_Yh@8_kw`w;_ctF$;dTIW+U{*MGb)LWb1ze)@Fv)e*P77Q!yI}@zX=+ +MxS+FTsc`24${BT$?)Lpf}zjM@!|CZyI--R(O$@kF53 +u7P`;BgJz2#QN$=F0aU933`z@EMjTSB#Pve5R3Z*=p}-#Ru7l4T_+crV=b22mDYd7iaT)kecvA1ME)L +6Ljm_jMd=qsKd{egA>g-q=4ohqLy{R@e3+65^xNs6t-c29H~a>08yR_8c*)iQi6U-uHc|?)Z@9b6Hqf +1>4gTKM*52fEy4PKJuXTch^&Ba#gE!Q$91RrYqYmi96%=C%3dF|MW+0_UY|f#j)>AZerdxQLSX6p>tq +)YPy*N~r80zIa~rdSK!eT_}nD%n0WI4SZsU4+OC>GOf +7u5Q?(v5y^BdJyG?H0OuqhEpw +8qPPoVRcUXvD(o!XiX&@4pp?z1lO3RE=-8j(gg7|MOX@#A_%E{~7m5XP)^C+NO@yO6MoT1n#kZdqn37 +Y*&DuEqD5ok%Q@%^MIo)-e>W?ZLK6(_?4)`nOEj}RX$Z1l@$h;1}dG$-;=MigJq5-?3?U(0aLk(=}Zj +#VQvCQ_z_zVL1YNeC(sdoRy$EK4=LmjKu{IuOhIyL`;5dA*{?I64qtXPfC69g3lHmrcjXL=8g?)(uL- +C667{WZ3dn*+(GU=xpO+00ADGKKLZ(VV^G&9Zs4i?-f6U<>==o{^e;o}`1mkZ=%Z!>LW$xWFeApb0VmqD*K@{1b?aBQ$napz+=MKQNawe +)t^1}3U0GK2(*>_DD8R~118MOw1msz#OrlKerYkKOR^;O+hlmXybRCvY02{=;|Xa&=kG0fZJO9s5-%Y +NTAZ@TRM*J5JqNd)BU$<$`8q^t?ae@-WvtLKVf`KhhB=z5$?(<{BZ5bzKoU7Kyph)eU)#1t`Lz-q#(y +)_V>!)Ssjs*2AW@VsK! +ay(kw5X`ommcQ=;{I+z1u`@ab(P?Ps0RhdeORYOchuz)d{vw%~a&i%A{$d3@jZ}nA)D;ZiN~hMO-Sybtb?w?ifUuz%gD0n%&-ryif9}0J;fR-NIy&eV4#KeIRT)w|V +xk&^&btIMzC1THpph6N_rt35s)^e+WqJ;x%}d8CL#{J2zf9zzL`KAJ7_<{Vu&yEnFd~HD2~mvj$;w +&)H4(zg5|s?xI&hHwI}$pD;Xd`hWk{*arT_7I0NbG^3U>(p@-RvV!JI$ZBtiZt%bdJD5}u@?g!^r;nf +9Z@|d%v^mWT8gllh3g!QlP@`fB6Ex*SrrYXky{6FD!kb&1o|w5xV9y|G+Y+(EgA|sYL +V~3w5+&MOeE*Ip81n98ci@u?=ZU4Hvw+(9B0OWO!Rs#t@;)ohVF9id|I}^6s&^j1SyHg3g5s}px7{I} +uJTlJxUv85zyG(nwcw~xtN3%0HR{XN)y--DYkv@5^aUVo!zc|PK1-p)=$tR&iN? +f7Q8!eK|*Quk?!6U+sE>WkJ!yVg}geQ^u@7S4K-^{X-Y&ad>s^-&`Qy4}m>IME_$ +Jh*)SM*Avhs@8?s+l&gU}f$SSGe#V@Rvr(cLc{H&6JWO9}AW?Iz^YIRXVS^$E;Uxa^^!7qkhtuGlT5< +O04i80~?QUM;1~MY^S;UIz)cni}_@x06MljgqaHW{`6%&o_}V9?$ +ifA`ki9o4m|)ZAj>W_ZB@=XrRILH+p>As5`ov3>2yYhTLZWmms<1f=en3IY+#@Ux*7MWR{4I?zOdC3w +2zwICG~lt(uxGgEm!Dob{MpULb}bKap;Rx4$~D2tVKV=BKHgs9X~Y>_+niy5^F*rAmh_WBsi+=xYZwq +B6aN}n+OTCmObeu%8cd~xGG&X!A|fr#* +VZzHQ6h!FWfHhIi)3Dn1}9SDBBLEUY)05+V)8%Tu8m)y(j<)z4@G=Iv@H1G&{XUSvodPKo&jndz18GqCk*=3uxyD(N3H~m7Raoxu*7Sp$wiegm-#_V9y}$CL4}t5A*yf$ +tTGp{DPUywYDe)v7H5i#GCaUJ{M@V^(A5)3s#G1D~VWe%v_(HU1aDfF9CS}0npR +e|{ds{ecR=0N^27Um6gFLqR0HeJ&(*;qQ3Zs6`^eyPv``mFcVi4P=-ei1Z}C_7={q264Q0op(KcPO)g +C%K33T4|ugg@{j4xy5Y|;Tj#MLq7wr&?TKq`J@G`~LF}von5nB;TOa&cfy4^M8{U=$bd!%d`uzgQDD! +mP9X4F?POO>5HSiR2jhoJk|7*IYfG1pC@7*vRv_$d1yJF}M3sXE0kcV(O+f2G{`f8J6fim&f^pe|P8MpJR=x+<3Iilc}pR4Mp6M%Lys+bnyn`!_N~3JV29$9iPT$&9bzst;FWVe&Fg9t&$vAYfKU)z)QAp2U~m;7{*1JR#sA@(}u$#~xu(ol4{KMN@T!8CQ1l7m3!M?Pdk(`=C`U_?Kz0*yY!=l$!?t{1yjo%`XHU +4;=WWx5%GZlz=|)7p0{*)#HoQgE*UF4T8v_asrf66vKkMBl6ccIBe>b~QRqpQg!BCx;o}+Svu?G+Wq0 +QwA(gK$3`ABIj#bPP5r!kTQTZgVixDfu$Aq2O}H>+nnk!XOx|cgVVuV@|qIki~){Js)6j}Jd`sHuP#q +7qf?*2UWifi+6$sCU*8Rk5x(9QcAr7L&KLGBwm=5tO-&+6SV$ny2eM!QC +ByPwpqlk5m9`*Ep~buqj6A%4SK)qQbOSeQ)6nlFGI%#BdkJwLq9P}n`gB7Z3{ +MTi4xCj-$&{cGP?;|(yXTO$@ugp+Ge0Q*AYqc~WuH>GR&=z3GOl+PNfy9RPl^N;s?JiwG&0gQ8YU$<} +b{L1+9wl9IuEucBKL!B(t^%dC~9%8_rK=ZXjT*x379XTc_@~JGA2e%%LT%SI8oJ^9(cw6GzE8^u9N&&9(}G=Kc6XAjA2r!D?3KwJYp<96G< +QI=(UbWHNfk*f-qD)eYzVt9=6L1B_v>WwoEqQ}(tB*PDOgWw&XFTk05c1qHMgH%_Ip?1ZJ*X<1H>x5> +7WVy1c%%BFPbe8hPO?CBUVxo?ie`0ej69_;{Bd>n45TgK=Zr009u}CH+@z8i#-9`RM +IwJdw;w6;geX4^ev>JYTHCRp30r~Dxud`aI?CLqsT%|HZxLm5jKp^BF9W=y`g3`B8b5Mg7yR`c}zF3v +t+i0L^993)@is;(wq{N-?NdntA5*bk+Z4A(yCqwN#(<8ANoM#UymR_VvB331_SFOu(^NWncy0wjWpc( +I2wwFYCrq=0Avalx^1e>R2EJ7^4CgY;`KYsVG{YwKpLe#dmM6Wz7U>~?l=Vf7lM+h5J5gyL@Q^Lykoa +DJbXF%t-(8`ezAGzY(G+TA+5Kf}o?*c99r?^We^X~#sqngMU2+_EL2Y3Nww>4xIKX3=s>@Le9q{zxN@ +Dy4yxH+9s^>A460I6Enm`w|Pu9S-7fNN+^{=H;XUc_kWKknw`J` +rC_U;_8fnmOsD;H@m$Qy5drU4Ph;xc8~x+{ALvtmgau*bI&cha5sSxgA-K>@w0Rz?KHS+pg^{K8a|hU +xtJ>S6eK>}RI(|#=HCSMhwz(~M&5t@38eMaSSEX4ezzEzPVJw^!bC?Pmc!XTraH;gHpG~uot?Nf9S&A +<-i-hXHk)BZR+hacq!H@J$-#2#K>^@7+5z&$AGt1TR@ft~pc5i6tuk# +r7`*?V<#lHN;gehWW9!9P3-<%3-51{N9MWDS>dcY_L8MGt4DSm+@lqHh%MV^}G20&YweszZO^*BR!j!su5^ny`#Q~Q$IJ2FEzu=W9Wd&3?+D=~>q +fF)xU$h2GA1!Xs=QbN3qkqQKOX3EI|I!tuHoC532tJ*)(@pp_ojw37*!5{Cbh0v70@vTNhB*Kig5=GL`>JduVoRhi3Mq!DL6_85RiZI +WdPexgRA@?v(6!0`MaXlc=ZSTA;#NzPyk>-rec~sZ*3IESzR1^+)qkUDz9+}`|0obi>t$iREcitNnSi +WJa5TJHVEQiNM@x@m9A+U0+bK8BpE`rGVurjP>`Owv+tW+(`dCUCrKARa;$bc37sojv#zMxw}y9l{!4 +DbjsdKmSXEm0WM?9s1jKV`rXf-2@GBO3S>-@YtLRk0Qd$?-7TZB~qSSL~owm3t8LIrr5#n-(U~LV8ki#@X!qC1 +JPsJ=z3Y!K}eX|ks?ViY`8pLOV`1~})-OhsLz0VA=4${oCGug2|>rDxmlOLNfodaQm`{;XNZHK0a{~s +s#Z7!110S^#HZUlEEBE1N`ODx{3T=f9YUS+g(P=9zI2;Hk9g<9Q0ulB&zq7L8^~QrUK|DEnc~NlIA)K&;s3z6HWNXu2K;IR<}rpy>@+Bb^$Y +F^gCp`KD(N4a!)`{+>?1-Gqg9uF#tz)!_*?SWP>y}!QYsA*%oN65ZVQCwf!48y}8MY-S;nd8_j=R$@T +EZJee38DhKROgo41~<4M|$#0uVeB0WKkCnssB5tg!QRk!%hX=6AOzh&0c%9n68Num8k${(-23Q= +w=7P*8VN)o%Qn3|HPJ8w+H4jf$(QD_C}wI|qShkZ!x0{axwycrNBuo?cJpQ)NfcV2O`M&8pAhJNb3No +A#y1N9at$h1CHT&b4XyLO&uI@tygBw8-4KTEIrAE7w@C;Xub{uHO`H^LKx-wT30Q+EEomJG`JM?%Gq> +zHidR0T0mVzF2J9Q~kC$z@7|336_qmzO1RR*)ZXEL|izkFlR-XCI)y4iSG@|d6S=|^Cw&9(*pTrV`=@ +VFyCUKW?^oR-7*X?#zkn$HI+XybOIJ9*)+`!@CZ%Qji!>|pkgjHHH~*z3WAzQo4+1ovXTli`6VRB<=j +&jX&|i#bJEnGyAk0* +y`z|ffZBST|og1=3Wl71cA6yacapjDIcB1pt&e68~yHBzW{8=?Fjm8#ElJ@SN%dDgdqGp-gk#e1w69QT)@ +dHmn~t8&x(vH|!+PKZQvG +5+8~YC>QhF=kX{5{%!vsTqAD4BCuqBc-g9LdNf4wa3QLSe{xAnRI;LrHx#y6#<3Alt)^JZJ3R6sw?o{ +IPd6*&z&g_iG1W~RNjyo6m5gmij&N7d0JSngg{-ahwl&RA6W&Dpw|3=JeV=7-|`sgHYeAL%|2c>kf{f +9S>{&odq7I+siX^L?4-3Nk*A#VKG5)AS4+=%$<)V`HHQXc>ywvkJ8jl}xSmc$DSz%!`GMQDN +kc;2VW$LzE2DB6)QBsvre-A#ef%)~Z69|)%gb{xP2Y#3Qcq!Zw +0tLtPk_@74+t8xbOMa|+_L5?em-^M8&9((g}Y=MVpo~ysV6U}T^j7aQ{TkT*`EK-)vKx=|iT{gVNWMcz(7D3b#2k+ +mZ&G|efMp*+SzwD>}$!GW0dg4}nct5EC*a%r@>pvYW9Ullvm)kr;=bN&y5kNA^4R16)JZf=bjQCmt>% +@&^3hE!(xF}ZA%IRu_eA&1rBBn0~mzFRH0izFc1PfwrLkFDg?vHFbN+ehoKnr4Je@@&K_Vc)RVKra`; +?xkgg!}VL@qKPIAp-8mzzuUC)v1_Y6t~5)XKBuFuR}(24(RG~qrDdT*foGH?ubB=53Ci?y3rHiYM|1+ +CCJ9*z-IPw&4HwQ;tTA1i7>2OrIzFY>K8?s6^b221I?8p-%v*+(>;=E^IT-p*<1rpp)&QJlN{p=aeSN +VV-4a8KGBoH%42G6j%*a?GZvbl?z$RImT%TYS@pJrK_Yv3vLm`-7>10^VVm}x;y>KOlc&3 ++g*c)QEfRq05xKim|&JImYC`-SSF|!zuy(s +`?EApMyU?BQeacTRIMABUU*SN|MfMQW)j#hBE(lvQQ*RryOGX0t#pld@DXd;%z&u4?P3Y|_^>H$_7UFcV +;Z|J;e#Y>g22-VkNHUn^E8L~Ge!&J{f!g1IU +#?ZUR;^Aoxr<$v0aQ+FKzcShI67ylc4Xrwl)Fe^7`1B`T+>~EUMIKL~IVoIQ!!@OW)MXFkopTQzACwB +Y@HMG%nx;oMa480mVJqNR_!niLty73eh@AKwGu(T7*1*h>pv5-;SNB)M@DMB^y1aNb-G}Gr+A=mR3-S +Mw|v{0mB!5a%O>t2!45gSnN;u_i#0d(-80c5i@?3hccRb=AwsJ|IA>#4zs=Ggh}X#>q^(xp^JF~z+>L +bzzv1s@vX>{QUVr2+lxfJli7WvWIT6UF`{JiRvaC5TU)1lAd5EJFnW_Dqg)Bt{~TD}4X3}^hwKUBhpI +I!I>^$@i>I7nzEiG^=?XYwy81Da2|B{f^pZ@}6KTMnZ<+74gKWy}oudCpG^}h=>mYA3e=CD)RR{p>kr +r6%vnqQNAhy(u&PrU|sh*BzqHe=s;{VGRY#8h52$mrmJ-i)rh#JJCW2{WZXsh^2RU80kLM;a{$l5MjaTQCY4qj+L~2Z*aY{G0c_g;&%~dnFL`y?Jri5w4GuY%=kCLFixK!~{G*-kwJn)bWd?x<~ci3hl +R_m2Ziq{U)@S>gJr}kSJO3yz*z3IDpZB`2zmCYUOz>CJRlhp$Ch9)DC|(Q=Gctp$7e@1+0LxF+Kqc9@ +SUZ8cwg=97lHDT2&)5TK3meY_H;e?hw;0FB)1*)3;1j9B{zSk4W&!r+O$|42~V}5ZO5ohw6qXrimO)n +)FBTxr>-9ZXJ9)xzD6>s@UN(Nos5Qz(2w2p!%XR62C$YFx7ngRAiZRr!Msyx;uHFY+dWzPa +oBVX1{!a;ihV;yukJ%Sa|ZFw>2zJKxi>7@i>v#ZNE#ITs>vp3X=1<+#;8n-)JO{ID$Qa~?~$82bPYXq +3GCDVfV_=-igaQ`&Eh%fu34H@A1$G=(qsF$G05%$W9LeEOY0MpkV4eGJh+F}64BA4J +lIs9#ovIv0!_TK}$~rphi6zHrngO;oy2)%!*tCPaz!8ip7b9B#qx=nLN|x)Ym|n3ye#WcH_nYe7wIc* +t>(2jvWzS27<6PP6cppqp`&8KTBUG#qh}=!okk~4-u$TXie-O2P0=e2*&fd1Z+e>Tagd>n9*b4tFwT; +NLJa_((d!#q?w}XAr-{Ju-e#wraZY|P)x_!eG(#{kO5w(*o9>Dru(tB)QUcGo{&VNyN&+V=!op3Z8Vx +jS5{8m;w%E9+|2jx;s|Z|2`Nm{;3V0eBU9#Z|@{diiKGe{UcYFX}Y#m#@wm{^k948aq2sC +B~Xp%msaC&g#DHQb8Cn+*NQ3Somx#H<6-T3~}Xo`-4)=tqX9pu224zE1PdpQw6Xqx$6zQh0_c7sMw91 +k~@y6YfAB@cf+NhXC2nnkdbBhW#9NbZ+ac_dWXR)8fMh7K}2@^G4qu&;`MYTyyl4SBHT?Q!yKIQkV>8 +&rZ>5+Y@{f1URwkZnbgj$;1s01tbf+&h4#KJU|IlCLnElf1}z`nB(l&j1A5JGJH>)(Xo?vmxt0eX#X5 +8zSjBgkLTP#S@;1Iba{7A*lo>L*yPgpATWF)L@U$Ot{Ba&8FaPs20%BA;L- +abA$OJQQ?Rh=-@>AD;<&2@zWbN^v7d3zV!w-Unpb)_Q-EN50Ix}_NqQrSEP#em2Za*iw1DBD<896K?u +6;n|bV;`QoU$&4Sqsgb(ArEf<@%mKFnaF`c6O$bb!nSvpAB*r;zY%4G!K$9pXZ@Z1@?p&MO+*aC4!t@ +=`3hRFo^IkE)F6A^fX*f;gAUuR~~MrnS&8NDW;uaC%3X@J$gZTbeeO-Yys*j3e!hy}XZ$7(gj%KdGep +^N?zy@VEc%4ayql8vULmZW%nHz2HooLxNW;M-zY6cVtJ2aVf&iiep5ODj<;lI&zZ6X4rHt|*?F_$6Bc_Wj1)U&zvz}!VxVB+^x5Ws#rnrSD9$ejg9o#Xn4`?N-1o`NMEAbmv70A#Y^<% +7or%?6|DYhB&(3uEpL`q))mc~L`9wfG*?jm8H7x9thvfU9Q;7dB4;Xaa0B@n9y%7i>1yfikfQUhVxZg +)j>h$^E4QyN@=&UHA5&+QmR;FcxTv>G+5X>G%DLaYg6Zv_D#JJi<3cqu@O*Ybng&>UE;3v(v|h=E8lHV`LB?(bn0$w$31(}3uF0jBu~eye}# +J24xRhZO&_FDxgSeD=3Z(g!LRdSI~^0wMrB0<$qWm%JzBB&8A=F`Pgfjy0MfEP({^ +1H#ctE&iLrLH0$gB0Plo4KS@6Uq(|Jmblr%)Sd?-N~K$(V@?|?D7%Z5lu=1T#nSzFzHk!gMp?E5!0IX +`xHaS0Dp*v$Y1Zx|C!CEa|!tCY6|D_>GA1>Er||5uQ2RhnxY=}73N~E$2n8qw@lfqr`P-faJGYm#Tt2buqHmDWM^*dCBA^$ +tG_Gc!ZLL+&7)DRZFQ}yFW?F=WnF~9wF~rIoo&bYffVzrV`pFowZnB-RRE~`i|!l@?Ie4CNxw^A_8!& +RDyP$8@|r!*Rb6=tWMiv%{sC+5Ed6d-^bojaOy``Y;ru@JIB@M)_dMdZ>p*r1DQkHBJX>Wr6tA28{p|k`6bn|8y+=zfY7j?UE8Gy!HV% +tOSLP;PC(PRw%tpOj}C%uB@Vaphl2nQg#K`f#bTaRwUT#xTj^x}fuJkiW+H=x#RV ++pLjycQD(rG?`QY +nk5`h6ebUMX86W0&54Yk71)bJ}|l?s;vrsM||_+wD{s*N^m{1Vc5_*<^;0I&@l@u!1qtzms~@jPz_V# +&mo}pf0JAR?K!Vl1iSrkDgo6$rM7{vNh=x?4s@f#xZ-;VM7r-y?=(u2sR14#-Hy9?c4JKL5YKAkZ(zi +xo@y0$0ADh4_2w}3)nPN!z?uSdb53p_CeQaJCPAI%4=&5rUae4#vV)AQU13s8O-OU0fH-oP;T+V()_9 +qM-lEHf@UsXm>Nim8CG|FgdI_``lc8@y`_^0QMnkvs9Z>-D|G9RHRaG31oc< +LS@EI=t_8;Oz*er%ugLdDP2=tSAz>(JA-7RcK(xyZ({M~gfaxI7eAY|n_kK=DRhTi4L&X~Eyp<1&E4y(YnB1i1*gDEG8=5Fz +*5}~tusU|;kTp;W7-mf=_B8bSBbhJ#0ka$41ATwtAR-BKJ5=#rmek-gc4-H#(d{Z3ZLrI#bAIgtjC1_<`qzDYsdC +6?czz-E3m--N8h!z#_K<`d`vlyNglhKMcxaFYs2eniq@*FR&~|lF#?GRhtVXbHl^!Ze7P5&%85beTE;!|5&`Y{{~((?CWoKFi4_7uvE+d@fLYkHJUW=VfuKBfkE3d!*qEwQ&8_xe~3SEr)_d +gmk}9@`qx`^Y?%j_3gC05uE*+3Gcxb5~`099Vj@?tKIz3Gp(?XRthmn2BP5N9bi!HD&69v!7gDe>pur +*UO0lOUsGNfdtXeueh89BEY-yaU)u;fH!L%`&S +HSY_6N-kCOzpiPKSEG=D07(^RBJaTo$TPdw^TX=Oo*1SO=tnGI2iRHlnho +R0P$_?23KD#0Z=%)u-9EkL*tsWx~X%2q=2rnpo#^1KU+zGW01?A({tLVyR6(Lx0W!MD|B)J*x^ep!KZ +z&8K4bb7FFQ|-05D2$m7>Z@iK`?=3DoD$6aWi1RyBYL#Ye7AdCa=Y6wOYKoX>`WI4F^kGnX302aX?jx +H+R8vjRvQemsdLGh#ed7>Pq!H<|l$)y364>{p-8Iv707{sA*5UqW3lqXc}zir|TU^%k%_xL9I|Iu&Q1 +-bq9ob7*0hyI!f_kFmQP2Wy|L-b#>Fxt%v@5K|vFkVmbz_-ywUlI4XW-69h`YhJ-qa`gW`%Ncg#9R%o +wvR1bF8_IqvNm^cr6p$!{B@aZJ&rJxOMpr;(-i=F6O=m^@Yn{;IAlR3Z#K=-K0)uH=;rnBs|m@fLM1K +vWP$lxqHO1(SS-Ad2@_y3cOvJo9+dw~cnd~bjwKVFl2N3dYT1Z{)^4%Ts(^g>~a#ex&(cxVJgMpd66< +n)@Aj7jXE^|!_vLXTV}+-3*p#Ud%;r#B<$uUC=Ob*`q+-l6a$k|LcLOsSSn(*F0{xeT6XrUx0k2=FHwl+orJ(1Adlc9OXCcW4i0vVR}YaWk{8wcJJefy7b(4EM#FU5sSgJH-=($eYP?6==xo??udd!5Cx1ve|a*> +FNTo+A1jkqgi8$YGH!AJy9Ajj_`R*X;b+yV~~js_Q+DARgmk1=#wIX98o$AXq(k&O(kn5k~6E^bVW)F +qstubvDL+qT{v^}u5SPZprxz7~_21n%c}2+;3QIM8J_39M9O4UY~Fgv;SeirRuUt|+i#oWi1yLUa2v| +HJMm0K7ls!tk3JLx**-DpXj$4jZD_RB-!ye03S0$Kl{c15cr=%hRyWUpz?e=6EAfu6e~J979K-nr6?b +0so6osckeN5~jI>MSL@up`I=~yS$`eYl$VS$>7tB;pF=J-4-4vU%)Qj~joXvAgv-j`I@y!nnN +1x_6#&;NsN}^mtwWbz3tI62Jrf4(5pqqPpJ4GkFr^$H{~I;@d$Rvw4^{J17ui|7OK~FTtFm;N|eIVIagt72FQX+AyNx- +Ub6_NxV|MAJhTk2-eI>QD_lrK!_DpWtHVI>BVS}?lJ>Jl_*A9ePr^3!zI~3%~{>8O_W`pGsu($8VNv$TbT>Z(c2-4(P0@j#EQ~6mwC5Gu^Sh_9nmI$dxur65n2L +9mo^4Qob2rQe6e$!$>cMCnPCev=eB(vFUA;GdlLlhq0-EK!+@fbS0?!owAi=ZAYR|_rTw|;JH)tcfJ1 +@}+!Xs#L}@GO#fqRt4WdT9yLqHUJGTKlNo-SV-!VlIBkzG=fyIzr%C$IFkI&48tAr7{Gr +f6v8p0;0_nd$p8&r+ReOi*Tnx>9aAoIKd%vgW(o7Ye7%Yt({m_D +q&Y4(E{cVfFa#I(=xa6qceGDwGOo`Q$BkoZ@4XbZfrlx|2RE~@Bos!ywpo(@jgkOII595uD-=1oz|?p +TGU*_~@-wal*>6{;i(DSIH+7ZtJ8U)cxu)@3D~V2cOkG@h3ZnCc>m{J{uWJ@fbb2X;EVe{f9#R+PzC^ +V6RbFte3?4z>F#L91`x<>_pyU$O$5Ub%dthk(ptriG8Kzi{gC_^=SO7<=E-0nFeL63s +xmHs%RH%gS@AC9Y>CD%*lho&=KS1Z}~uLHr%-I^l!_;8`T5QGC>CZdte_MEY{hF}{ +tBxQ6p_i;bcu-0j=^QvE>G2KXq9B1ES3gLtb=sa_K$gfR@xWSJQtzt{EaY~OzFdi}`nF41cL6HZO%*9 +>^)*VMKl5-zsU3F;kDpYI;#0y(NP1 +k;LV`NXpTWgMEroq2_V3YD!LsB<)SkVTTUAljQA50-RjBH7|V=Cb`LUHo&a6by-hs^TJ$@E9Uf3QyyD +?7N6y&Dm?&CBVLx?So#_V;)w9xk3dsS@N$_w-_NHM(_jEawJ?FL=RZhrdv6s9d1&j;@2A5OXV7KJz)G +==5XsK#B3^!&>*I+AD-<^%(-5`mza=j(WWnP=x2x^wX=*eDUAT|@7A9YgfUK=a!9FC#9TnueeVEX#F~ +Ai?i4A>)dpL$U`pG}A33F-%Sng-%fi3!(NmjdWkBJKG0!v9kO`935+z(=Y>BN7Cl +VOplsmDo7Cx^drwF52~qZwwSEZd4g(<>(@tOwv48z|#l~;WiD*{7x*A@++((%>$8iRz>1axKyj!AVDl +{>>$<<1R3WEGU{k2o&wU_>K3i0toyQ!#s{LEXt#9?M0uq6U6OL!O!(- +$u5lO&tLN%DJ!HDGz_hP^7c3O<;8S%j=IN1+mBYu+H#$qAE1jKJLcLWb19sTWPxOUJWjD7>LR9q&dx) ++1-5I+;Jh@Me*pbdXe6H@;=+!rG^PpTn$wS`HpnSJwxRnl7w8>Mn&-;;RZ^(13?Cu!s&NXiZXQZ}9v` +077aU0-^)4)+`Vqk#ec&gNP%{Zd-+}FBJ)|ez)2f{UVm8aJj%1j0o +4VDaP+%#uys3oJ}tLiKu#{Hh(#@W}oq2mVNAzBkS<(9G5p-39L$#P7}=>0kSlB0RQux)wU%;u8*z{7=uQ=>aqMG)_~yW_K#`=hFJYU(DtVJ<2*yE%$KQK*Du&&e79%nalLoillwA8BDbp_Rw#IE$7cuyl} +Ck1--<-o>=`b%=Fm=HLq$a-Q#d-{WWXuY#6q+^0OAuO7UK(^I5kC;2&ke;ZnX4-y#fPq(qfi;&!z&n; +nhA6C*(`mnMw)FffQPHd(}>I&@|jJ0J?oBLLDo73XtFlO7RbE@rV8maCvLgJlZiT21WT%;>Wy9za+0n +XyL$Wo=AulE_jAQrPvsd;Q{Pd`P0JgWd!&So$a5QAL8fx1c%X+0H#&D;+JfS&w&Y4{j1Ak0jOO|RPn9 +4Uw|05O2Nz-rez|*(jMRL9tE9kd=l-rVxia&ncP?Sc-Gg@3qk!9X2|Rz$x8|J`0U8)vlmEeexFFN^o* +Ulu&8e{e^9g|w2^UIrUK|@t(Hh!N2CA$&)T~*H;ydng6s1y!fH{s%DSXZKJ>~RNP-emq^Kh>m5*JB43 +Rhj0Um%{30oV@K3do1Ak+~hg!GYOt9oFDx?hIEEi34})af9~a +8`C@Y;Kj84144ktM+#```S$WU_{7GjGcKUs(M@%ZM1%;rrr)o~xS(AkT^-{Cikmjxheh9`tFROZ=hj= +@`gqy8wV&xV~;8_D_EntkeXh$cPn`PlT11MX$fQAvT(+@K1W@!lwd25rbC8&jCc-wY#VT|t=;io}xb^ +cZV%K-sM^a2s4kFoxlu+G+NT+wK?cWjkO;Pb^A!ZRF@iD6s2AVvsBSTZ+7X9(D$GiBql7*DT!@m_FR{ +3?3;r?|a`3u}DVp6h+ClsSzg;Hh-~R*OYp5GSY@u(%yj<_F4hr#UupLWVm3ohUdCJ +%Hk9+L&4SxI{p}519AqKmsa5^)gP#7?7w*)1W%9XgU8aYpvsspt7C(Y=p7@#^-(;pYOnhq=JnLd(6i( +m8uh`oOFG~dl0DSp(bEM67?w2+~-Ms(G)84bAk4;|#V=9wv!GLLX%$BTYZ+OBz?{~@incFax2)UOE~HcXMUHNvkU?(wb=czQznucJj$))00&_J^??7GpWuucqtN +j_R>_P5-1v24rWXE;hLvgDVtm~K2zMsdz`$>xFMs#`z;WL{w3KOKMZN+b2)pD30pn0UUuL9h)V$xuoz +y*AuXH|iQ~xj@Q&6wvcBu#mgZ!*pwCRH`3qstgRJnRH1SsiCw|$>ZloYw!bDTfO))U2aDa|3jzPu;VyVKy +w%Fm2%-PB)q-Xf5qJ2#nNDKL$UbcD9YE$zq(N&Jcix}SML{e!n<_30i=2lvGwU8ltehr?K=`6jMN&*I +g66HMR3UjsmU>)|R+|0c&P4~X$f!9ar~ZkvhH;T6Gybg_YRriJ!WED(rPZhS$*l0uSHr_E?>&q_|4)_ +x(tp`K`RqjCoR@LTtLZ;z>Vj#!TDODSQUV^-1VX+5IW30O4xmCiyZWTtYEXe&V_!_8Es1P%;Zi@~+A6FNL5c9oHR?dTxOitq;C@*y +>HF#HJOtBOABwW^)d7-x9Df!pkf{N^w9Msp|T7zY%CoI!H%#g^m=E0fbQy3sGj-S;YXqL{Y{U2!m70W!;UK4um4ZY-0YoxJU-izI1|Lt$Ldlc^e@gV +&z@i`|w^%Pd?y7~?-SKF|0WFZdkkR0t-I02IP`oS?W_#V+NiC<}T&-vx*p7`-Zw5Wij?6swQ== +pn-?DY3ZjS#J&5hhCQ>5}zaGiiR;}6R|Wue?SRm;rPm9&gCyjZeNY^S!TK&0fIwF62(AIh|Kr%C6aVs +RJevs;yyKtL{U9;>@D9B=!%6Qn>o5|{;L8?Sgku~_FSopE3@v25GZvTZ(Fs@V7lB`6!ZQ=o791HcO}A +d}VsMuX?CB`DKp<0MYMFM!02Kv-0-_m!nE8n6Umc`tYR(+!71TzM_1ti#^7bO$_>Y%`TqBNTzh?kIco +${GkJL7}tJ&Y;UyFz7+;Q?}qU-`vj~_4M`FEKgo=G{MEaHwZs4vu>2lrgF&vNLQPN{$6Yx>^1|YRg>9bw{F)xCYe*sHL6oyawem;!)s$a`@|?C| +0_yIeQ*Rc7A!Qcgb}bzk{+P8mb#ZJ`zD{LURZJZ0p{*A^b@8qxTZw?xH5UJdPB7DmvF0d_JOc9@4lqJ +@!4d!>S51gFdz3?KLHU~rpvs<4E>J7F^5FIROQd!MDt*wH|UbTT+)Zh*eJr!ca)L)dwXR~T}xoObTI< +EzQdc*ueO8@WER-lk_NwTcUWp+p0LSN&4qQ??TAkZ0ew_3O6&h@1S#e#Ca8@GI8YtNVw1{{ssHoyT+8 +*lwcP5%hX_`YP&mzr`l^TkJ3B`aVCr&zW$eO#7a*%#gl4;2!a +qt)efTEcUe_UOmReg&AjmxlBXI$w~2o#Ug$+gZ%Ow?{wOJC$YDG#tvikCJd*+PaL>k$fb&|v&U?fRpd +zR#aD!(`T>iC??0;34c~YouY366*;gu#fhFPBF1rdgvp6FlEHVYHblV*-rY#8bNLwG8E`;3^^TWY`-e>{;R(UQre`ufORL;o6gCgkeq+5 +hl*YNcPw8JyH?wl{*FgN%-@r!m`_7f$-M~4UovbLP)AgfZz^zHHgL5?d8*P;odIrO>r~?%ln@XiOcb7 +7uQKnL+-CXGxia=QO=;tVcK~+}=f93Nu=!$@R0L%+=bA=e29t6aXz={k@0PIP=<$jOTiOlC}ZOk?T +l~aFOI!{?aZxEkwiE0*9%Mtl2^%)aWeYF7wT{|o_>b2WS=VQD7wylBZx%Jc!`ADn10sQCLyhP~Nf0J~ +L?a}oaZ4YspWZ8-fPSe$$CbYeYe*$xz#gJLf0mVaV(Tq;^|QHTPs?v8iCO-^}P-`BX~-jLPZg +Oj(7zB9(Xd3ERjx(1c6@s{PP!O!!bHJEMlUZq0NO0E|sen!B>&%BQrFqa{iJ<5Uu{`NcC$>0w)H^;?2 + +m&`7U4ZbrF1xed3n4A9c+$(8D?tL +~nOZ~Zprm1}q?*Z?#qWzTvbMyf%j~G{fN0Odxn!_1*`jQIAcZxUGK`=&yilqk(WJEwH(tW3PWY_{CNN3PJTu*_9N5?jZ~Cykz@;cC2rD4#JBe+7}k +^5_2Vo%=Yc=JU?H={`vVL-tHFNpfA>EHq~2pTHTdZK3yx7OhEvM0XH%Gj3+#jITQw)tvPUuOP|Yq$w# +a@guvr}f`q{bCVQ|aC}*R;{fzbVnL)$s2Yg90`2g4&RBSG`PFV)iGvhB{zR?@JD&4c`jojPv +l+w%FxUC#lZu#n@@8O?Oy*}b^`?8aiO*+fvwi3C)tJM{w{$&E@_`Kn4*LL>RMb5KqPBRFBH-iFpQWV_ +7nR~u9xp1Z2FkaoIo@Z_Xbqjr2saYJPYYx0+vDM`%hGFn!aCLqO+EoOEM%eWD?3HUlkC7PM`+^dfF3P +v(y?HM_WXtMc?d1^zs27&Dor3P`p0fSnWB;uZnb58AvEw_bvmbf_6yTc|1dOL%KzYC^q!U?7zs(aTZZ +`4XVq1zbM^K)Bc}p;;`ugI@GQO9!xn!>H3&OOIS+a6>u}$ZYMG4tA2H2d+opOw}yRZ5CmQAT92|xf+ +SRm=EcCnOPLKy}Jgh9VuueZO`!l8Tl%C9%Ebvrqrl5Wtx`!VoE;!UvuwPwJ3*}Qmkv{|W~G^o54xxAY +09nGAKI~m=aGvNKr9xK!wM4zLHdgzWv*PjUpgC^mP7+iI!lOJw)S$G@zge(?!`9m*pKd?3*fod^G+s$ +Xk_hqqm>zjKgaCQIu3ORk|EmO14;v^RvTNlMuGhL{>fRi)_@LUZR3nvp<`=#!9@2}dQT +5p!KEXyZ8sGpvAzwxM4V@Z-u_S9X-d~SfyNEoP}4fZYaP@yT(y1gd9Ps +YFO;~5=QdX8tLSPwLM53k7xg_$mG#B!-178tDFE+<-R<8lPX-q|fkP-*mTe2ZgEnahJ +!lIrt@I{+7GS+3-S@RlCQ|F0prXhl$}Db%~S(f)M}G?iBIz{w{WIm<7LGW_p(0;>lM7?Or<19Kepn{D|jM1j#+cW0e73JT@~q%XTJbCQ((|pW6P|JbKe&*8X_heoLy2?c+Osw +^UK%VVxuJ3u;8#-3N+LJUtY_VMKqGr8=p(q+H7qR16Z`r~l3f%+V3!V}lg`U2e}3NDY}1wtd!EZ_>K&k- +tZ^JA}k-(<1Pr5^R`TRAO(&}g$+`X?eTtYmNCcIVFMrpO)XVsvArxDLyfZfBls^K6-!U&#Q!ajV=C^- +3=9tYjww7vU*u0{(XX{rA#xFt>1f@~w>ZpLn2yTMO7|H0|9k986V?p;Zeg&|0>2TW=`yd#5RH0Xt;70 +09ke;F;cT`~4Dkf+$2Scng>oyq8xq^?sS#m4}~$D>~FMpgiJ{Mi_mm_kYiyE^l%6TC>Wu4&KzdWPHHr +HqRqx4v4i?e*Z%ht1LMbh2R6ltMHs9H?)i!fR$rutuoEshIl>b%GsmThRBSAC@5QpC3hxCobeBUD}UG +yXKzT;)6Ii-9{WKNg=2OYx{`_Y01z;L;5RHB{(Hq-?tE534yT4+okm2ZM`$92|DMYugGv~k5L(*W$ +U=XLZEuIOS301;5Y8PDMYS&_}RB#V#CbP`>eVnAPlM+5V~W?we^2wGl}EUf@&m+etwg=h%XBhL!UM~Y +#8Tvabdi8P%Q-@5Y1y*8sgfo3C!0zK4KbD(FP3t3z_O6h(K60-)&mZP5e)O3ECN10->ztY +AqO;C&n4YroIFXG1mi1Kn5D!udgh7=tBmE3yoIjNLvPnYwpaG&P7GHL1c&Vcg#kR0U0|I +9>l<^1Bx^y>amK0C9T1>4*fCp%1n8waTxlng1?&$9;zs42_gG}jSxi@}e@6s-*t}q>-@wxJVI9FM;(( +hI#E{DbTboR>NZtIScEhrlum$$cxMxWET%$RoUJ-GV(`K15Lo&93Y51~BVA{ux=M1!DgeOek1*9*DEj +V(w8pqC6C*`Tca~;Dtifa!)VA5Sbjrh)(`g8oxRh~)h3;x18RZifJ)Ij93R2}AFK{A53FY +5*gHC#s3VKPYPdohUKR%fz4Mww*JCa#`BLV>afhu?ubK4x0PA%<{7ly`@qLUXU@SN=h+ZA9o%xE}LoR +Lab&jw}4t<}CZo=!H8@$O^aQI@Z6ALQViT`}3owxoXW<0%0Qq=bAcl{I9WcI5Oiq@Mz34FK3^LL>BLd +(~%*6Sh%6>|kTbl(^wIcc(Q_3i>W_EvuK-kZK9m+KB^a|Jaxz#?{l*~cfZ&$~NmX>07AkJVxa;?Mq-H +ske-Hu@DyTT%>xD=@ansdiWS>bUUA%RxBZq33-)toA=Eog&9rf#YSjN` +h$+ACV%EUzeAPphCscHWfN6HZB4cmOaz5cng6}zM1(Ucz;_nM9g=c_3^bAiDjUBU_zH>O8u?hW +}b*IA!uW$NeU7oqs3nqa(jRmmJVmPU!PV9|WpLCh*&YK>t_wkHvS!>Wt{h>X*iFd?hvxn?u_N?V+?Y( +ugJPALxTW^|aA#-G}WPzS(3Fv7~8*dU~wLSc@w{MFB>(1dTSEth^JDQh;DvuOpA(UtLk^iE*__<=^L~ +WMFre>hBo17l8I-gu>Zok4@MaZ3g-w~694WgTD`d~%f5O@-ACN_BNnA-u1Z7D;%# +DTiCqJzDP`Zy7;msWbalIM;DuXy6F9YzzG64y*JoCh8tF<<7pWKzDv}|kTD7i1foihGe7RTP%`JK5p? +xcsS);$&cnO8_&V97$ZJBd$HBEd%i~{IQs#-0GynM4yXX$Te|_O$oU=CyzP8WZFb3egyGmi;%~nQBnX +d^5i@fH3G`72lQ%rVlIGS)d4aOWoMbr0Za6K%jS97(H2qJ6=u>r& +wiNNDgCW-6o*S>yTn$c2c+xMHV+|ziaz!%hdphKTYw!X)VJcEo% +|K}8I4*U(CdUQ_S9&kL>z;kjzyZk@yG7Vm#pH}iR?2$ +p1HBNgO$G?=q+S*Yrd-h7{4H{iinwj)pS4FTIaQ-9eNbBj!XocMPuOP&@~NC{t<+fwDqmms@dc$X5t!jEmO+MN6=U6f1bW&hs7~E`F2ghKm^>k>3(d#GwI^2%Le_GKVl59=Lct9#w2=)lyMe*Y +@YpN#|`=xej347KOP3a^N7#-j7{)Tv`;tp1twpW!5K94B=@s6R5~jZm<1yrh>@DXG58(3Noj*9PiikeZ-!Y*4t5%*sxA*nB^n}WCTv;v +Hxz6fdz7_D&SPJ;UgH@AZTx@}7_vyC1)*Bw<^xd9!z$NQm;iEz|62F>OBBu`>mFL +@=i3h~Z#D0b@OVxc8u2m0*1Ka23!N9k2E;WZl$YxCsF4E;~;*|ekfI&i010P7 +ux}w%&+~R(rklKb_*)@Qq5&GcWHcV_1>4@p}YFkFJE@eJ55%{^CC`4QAPrgV3pX`EmkM!xe)-RG~On* +pnLR=v=a=DZR_|i(fB6p4c6I4TS^FoMJTxBf-kOP({wz~<{9aJ8z4FXYt}N&f^?f_D3?lrHEWw@Llw` +m!bN}quvSN=Rd*tMzKNeU=vp3|me+AE(}fI2k&*N=KmhVyoNa$%UxP{uZSG{KOKbsacWT;&(YGiJmv? +G5Qx;gMJ-jq?`IlbdDR!GPbDN7SiDz=#g))OaU`+$lwEy|jKzlzp64thJp7m(j%ewE#RXm$kOjA<`S> +y0}w%eHr?|mdP|(bd?d>z!}oN2`+pcsfuUtPPx6rgT|s1LkHnTE*X8@aN4V-D0o8^WAyaupSL)X~OwJ$23V-|X5fu +Du`t1+PED$o9=uPN5wPC<1&9$ZcAa)h8mHnnrCs|T=QVO+;gB@h~^gf+Jl>%3>( +8KdKK{x3c9ZYY}W;cB@LG;*shz +MA;F|n&(z?+)6(=+qWCCo;)EH=JYHrt(ttpyT@~g0u6ELNf-u_T_j0PD1pv3!<5du(SQR@4TN7sxNbI +fjW5PkJ1G@xnAn&?6E)UtRkEP6j5cw8U4>sKbXevs&wm!6QlHvK9EzU`YZO_|EEu3r@mtWip2;jh54=Y2ug|qEeE#<9#d3yc|0|X%}o#UF@Sr^9l +_j=n5Qw4LjEHf`czjp%!q6KShC)yakhCB8 +&pz=#_K1D;9gbP3%`0ThqFfT2$jD9Kc8IJI#e-NcJc? +2^lAalxw<=lGAq+TXRaYn)|c$mF~sg3~Vs9^CxG8skp(IFc*99B*u9hX$}XZ&*t^fr9I6w{8INSCK#J +ui4I{b2WX)Zn8V=RZJ}qh>F_Rw==2-Z2y5>sVoo!u53{G(RS7>tpNGS;xFdkd`q!qZaC`l^r|!);rU8PGJS_b8a)h(N7=}eWPl>N$0WHv8rmO2{C&#C?=aGL}vld +PCre>Mm(3kOM85$rE?OFDZa7KKSOZgzz#m3aQAyA>@?UKK9_d)XxUc3G)c2PmFZ&y9Sxzdqdh#Y{&-* +Zim(ellu!ZU0OUUj1F?t+HjbYag-jCl*(XN#hhWUWuhnvBzjNWVr9NislKl)|}NmW%G#D8Z~_HTy$MW +c)*>>^^o5UFR7UPtE}xxa!H^VC!$QPtp60Hu!PmvvpjQ?;uzpjI!7f{!rGINh` +LIv7~$uB_@L(g0T1fuH9J;Hf)IUYsj5gC9Tmh2JEt=rgTPzeiIqdxu8BFje`y$KcwghgHjqq5sEPOcl +1phVd}kWJ4%>M>3wJk9JWJ@pt!cg(+Ia*IaV*;+_F1ZGMDQKG`_(b{X=Z!#U@bi2;7guXrlypn|9(ax +{nt~A#*@QnB0;5O>_+LNOX^(5Va0s_{`&Mu?>d97~~f)1NW)dKYFVtPlrG$!e122?$R@)vG(jzl +WVxK;ykb!uOv1G@+0DbAwkXV +;f|e>l_(!Jxr|f{rK~Iv7VX%yPjRBX@a;}CF5Tip2Q}OBW0_3^YU=V%cKj#FkI>SD`!*}2zEv~QkMRVo8st|jT;#?Hy^` +0CNvIuJagjmuqpe(OG^%`f~s3BH6kmm%b90ywa!vb>z(d^GFBG?(}5F!2C;{OiYcEmV4jllA&$+#gIP +XmBIok9J0}!4w7%oH+c95eSP?bI4V@@Ah`E(PnjWfeQ-TFPliMmRrCGkO +n13>I#@{DFCoEQ``4gT|g3I{}R_@DoizoQe3LdbOOh$#T}IgDo%w@9FLAJ*YP@L~-hcaJj+hA;Hjyv$ +Y)QXu#ji)wG#PQu`!Q03ALfCmoFaFTtJQ#}zSrOI1SeWP7GJIQAtu?xPH1LSG3_PB{{Kn2#(9c^iIa} +-%~7|vW+{Vs-z%b5+UBVY~tIJ-uAm%Ff9st7 +*HV;7Z@M{Tq!p~!37xFoYhzGP41-2xsx2yo84?pQy-X#+t-;DbVXqywHb2J6zpGm5zk?g}{EQwA +ImVvR#`g9~8V8)g`PNA2@6eA86xDjiwR&$6tIG^;k+lgtcBVem=2W +L<^yDML+2IWiTt7-=)(T#SU;wO8WiNLE9$U0y!;!))uOOOrz4kZ!E)m-TXBEEu?egfcvER^=Q(iwjeJ +7b3WdR&}ubE_2tKxo8T+F5cD3~t7q@nq1A1OVaCE=!_=^J2J}%HrzJ|41Oo)_?;w??uVb7t4pcsF7pEiSD{E^r%I-^0Mxmm$DUOMWzpir ++|I94jP*H6QSSz%*v=i_0iwF!WgBi^#mAa7vVE4G`Qu>0PcuqYEKvk!uR4{fu8yIAI!n3X{v7oZ;*Nm +4g#S^mZ0Y04JM*|S%Gwv;z;ksjD-DdtpTSp72{~~}x;o!cIT^DD`B!(2vQgDAMS)(KToN5YjQ0-3S4g +k$!XBe@I^Mq1Of_z;#AOP(g{#~3j*GfImj$(j7#M++Xd0gtwYyj*#!Rg{m8EWH2&XqBh30NKY#Y! +MX)6M})gLJFa5=VltOXYrt-Ix=a2_Zxm00sN0>~P6!A<7^NpFG3$zRWHcO4l}s~mOEnMIXDoRQV{WoJ&2Eixi6I>2{~9{D19b^!;;CKrqVnE`t6hmeh*#H?q-**Y227%$WafiMVoH8G@ +NgDv7RUn;|}XRICJh_Jkr3=qRIDe<_Gt5wg~|s@2@yjJ1R@(htjzTuPO +Od|?u)yPf*->G-yeFk`}}<)tz+DaJ)rk3c&B$(0%}+pXLg!(^X+P#nU&{>fy!>u!$~%gdgshlF;ijTf +S4%}7E?X6)NR^+xZe31~N#MKvNh){M(0g0$CIg!k8`;13B7Ps`|q0MC^oKvVG?; +JPZy+;Xipu({R|0BX@=dfHi91IVI?iSxX;*XZQb6d6OZ|uuowWmgllC`UPZh*fsR300Tjbt&Ce=ijgG+?c8*V$-$D9-zkfNubh-GzbGX&K^FdF! +_fajx86O2*PsR#&zSb09zi>$dP;KL2&T_FenWyk+Xe +F|9Hlop9TfCa2wgUd@9E=o89_idOK +p!}Wvj0kpp)ILs_NP*rmMmH^ZCO@c;bLKMgC@q&!EdbfaS<_L)Dx;)71gsf)`YVueOH4e0fQv2=KZei +VJ{_ly$2Y1_GM_Fa070mF4rR}o#4jk7&eJ-6VRsJDA}ZpUGOt~fV3%dur>$o@PbsyOK=rBZRC)yO%cN +}?A~yoo(9WPwQTKB$lfL$lG63QifWqLs_TZSGkZE5^k4xh$5O_A(zK>JKwoz-SOYMNEQk_lpn-W9)vO +uxZo9m4aIF-&ft72Q$oCClIMT2NW!7NMeuVPpZLkomOlzKpv1o1C5zobEwqVbd7T4&Y(M6lN!WZhdt` +q*j%bbhsY5)KGJwdi|;ct_XCW?8Ig>(oFLn68qs)|5gwl^HpEv}BlzFPs=Oh?f+s5{wcknlgaF +7~6IbM{B$NIF%NBo=KdM=1D4M1Gf<01`WD1Ff`qs!1IP&vSC5F2Tff#I-O0zu0Gq-#}E_8P=L(*H%z3 +UtOS@SL$?_t{p1=tv_u%}vv(ehDS;y+b-*-tiUftowF-?Qx4W*cmiv5IJci7ctb@GnMQJl;>%?${M;m +bdYdZD7|teu=XAqrrqEkqJbHE0orEjgj`c(HoaEd2gZwsIcnV25(6@X* +gDqc7;p+w*|X5EFul{lsC;Sh<4>jH;6P8CWX_jBESt+A_+xnnS)rxpZ^c&Xc#1-e+>j(iX{a42FsO=D +P)BR=J}uhr}lMZ!ER^gOJC+oENCT4Q1MI~#2SKVphT$<3z%?7ZLK5@(a0Ar7#&Uy>k9)>h9{F9-%L`R5J}ki6En=h^Gr}_^JHQf72uQTtA`yT&vmvWP|8LFVR*SL?UW +IJcSsF2JBf!i7<3C9*w`mHmk${VGtk7L>MwwGu_>q(0-^RELJj=RmNF^NJL-=D4XIf5F9+QcDQLg!Ql +i|1u}qX5>Opb{x>j!(Dc-K0PLO##2o78v-0}`U~TMdX^!s-z24YhQ}X~3YgnED6(&$_;vEEPpS15Xa} +tF@aL9tTs1Tpiq-ILxx|g5GE)5js+=IJZh%0gttQb|{xr&*1YaS#YRn`6Ce~VuQ#Xi5^gP3*K}?} +A1-Z~17kLD=pMZG>`O*}@nQND}1l;MbEw@A#`jTa{LYvuYtOv3IV00^fVCux__!dU=pg#Z4({`-R&o(`3C9SyvGL?{ZialX>OB-*A^2Lzx+ZVtwb_^q8AeK|G0A-hfEpOM4cAP +y3&v2jnCxU2HpL_cbO0u$UiLLi=ahp$#`1A)`t*zqgvg!`nHavJ?m@D?o)h6OkDw` +(phXS>ZopFp{w{h%f6PG2I=Q7TI!usWaU&My>vC{&cXNKpR7I>$eeCiP&=v)H-^Ha7+iwuFVC_SjY2 +_%cdk#1@WTq6*lA6P>oh^Nzxz9xd|Lv-j&&ejWsCEo(KpYGXs)#yJ{Tt{NnLK;^8y4)b +orxtho^FyDb^k0t6+qUu|4&)}i6Jz+@bVM@kb&#WV%DjFDH_ECofRA4PL5e))YyvQ7k>^qCNgM%_{=6 +cQG&w9X{pa@0C>#ukTtcl?#GihaL*yGLAIR@b6#pTZaBYgNBOeVbu$O$a` +d}b2yB}U54hy*3N-UnfUbeHPU5gR8mASU*5p^d>=Gvx$+==y?fMMv{-z+O1g=joMu__h80qpn#ID2wA +$tc+Pf{Is4WFAYgHV+abu#aW4t!sPpnNy*=+mxp(8tfPam}h +Fwcrby6z@DVy2GM#x+582Q^ZS=i2Nspmb9-V_#Pzw%;-A_QGPG&}P7|?8=<^?%2tc3jW%MEvCW6B<-TF)@9@=_0#{ZAO*>!l3%Y;9g{i=ysYSs0f)9;#0(X;z8-{b(y56foHy +&v1MGhO8k*XK=7V_WO*_JI+O!G9jZEnkqkb~&Py{#1J*X+r_uxk31Enj2bYK|^n8U^*VxXrqg1~I%IdO!wK{S~4IL{UDL +hbHIEf_G3%cXc7iG>dED#1&%rIgKeI7>2={j4$mmwktjl;+*dKE_XzmH+Ka5#u2q0uWP0P3ZO%fq|F3 +*IAmzv5bhxIy7P9I#BTqo3Ou0x=QS0`PzSx9mru&8186UFQGq|8WOWMt}bMfBv@zDL%;`DgT1#7-<6# +t+#;>Dw#f)_iNUfRJ2rH*Yh`5E9^gJlz6pJMw~ItmI$DxhP`06{x#c~*zY9DQ8&Db2GM6za0S61!a~@g^!?5gSp#o)h95Z))W)Pv ++P_c+8+%ljatf!=06N$7{l;$G@!3$LG5o~%3Y$T0mu#CZ37Fe`0(9U1RzAC$9y{FKbi!DlBuIC5#Xnh +L|GbK-A-s~m+J&vEoXe)vmn|MxU*}NP;wxu{2{a*fdJ`_tPI#a6NsP`#5tH>jzw$0_|N}fP20%4iPHy +QI|Z9r2}EFqT;=5w18Fq49hVeG4g5LEOaY=GjpS7Cqg$K7jbH~qyg@vqP7rD*K8cmKW|$fCgbay@pyvz!yfQluj+3jd +eLk1j)_>*4MyQj@J%lWUBA4ly(-M*oAK44yF<>X5xRtLJ)TG_-2h+c4t}JFCIkMoZL+)VR{2B)jx4C9 +KoJM&R;9BmJa%QEGAxOJ6v=cMr*cr_QV|da@t1LeNl5cAu=--nJ_OttbPS|t>uxlNa5T-9>v)O$K?0S +fA#{1%X`%XA4kybN2#vghX4a>J%bSr!q2&z#YtP}c7NQL6j`26Gpv%bwY#&rA`)b4R?6ctl^atcbT8DBiFHhJ@v|x~n^?3|3}4F^op~Z@WMzf|}h%#3DFq3vQhr9 +)K4DX+$*|W%*n?DAR?!zegY}@|7JLL^;y0UYoK|4zQ;@fZvdHID1E=i%3La=eyq7Cg%485Qu7HgvhH> +d)$d_ZomPc!<|bpk%`{tm=VLMa}&{NN(i82j!UwE=iGXa+LXTfNg +k9V*bc{<`uivz^C#yT^*vAWXYRE^yw8VT$|u`7b{w+*i34E@I%s98OXv453rUP63CL7N+EyS>@NUJHg +`R=-2Ctst(+#Yd9+Wy)O*Z@lF6EYFlb@eb3IE!;pSHJgF6zP%Q@vmSgE|HVh1u#3FxRF@ +%pMFdn+i%k_yek%vi5k|GZ|m?)Mi~w7l{FaP^s3IIf9Xz(WYf>dg0y~EW7Vt{9Ph|`kfPY@pBqmOL(|3Ls0C6Dvy1DwozCeAGh&dOrUI +p$z!*8NIz~XAP^_kc@-9?@m>A+YDA9zh2=jN+j!!GoQ(|}eqfQmJ5y23Tt{k=0n4Dj5QjzEtAKLJYxe`yHjK;m7E*`ejTeO(QUfpEA5jF@w^s6^evh|T|K_(5!ohMKt;)!rD_qNrKMO +yY--1=?Yu}8Y7*z>0?M+W4~W3?0S}%piq<4D&QRGDVdF>|;5(94^Rd}<)as&qa~4z`n!tQ^$-mER*j6tM00bfN+r`80wdW08Stu8K$7~7#7umQRn@Y>9J5{#2i`SjyEJ$ZV3tVVjU +)B5DObm_5{=?M!ii^CGwAv)5j^0EzOJYkI@VxeI^T?%Zu7K=p2_YI7>hmA9MDeDp`V*@>msjdh&Lje1 +(VjJ(7&~7~eKDN)7Wgm8v#R##DL09~1EMoPeeP&u)d2y>rzSUvqZ3dS7n>%BVBgcavX0X%(B^cSN(%& +{9f@X5qUQ|Hy&9~;^W7=EP2%Eof^V1Ot1zAY`5(m^kwBQoTRp!nDNn^jc@hO&bD16r7+>-9uqt!iB-+ +k*u;a<>F~B!_g2RY+dJ}oHNkpFWL9fi$)WFNvWUXfH*nX_pB8i$$z)4;i`3%oSDzu%obQlPEej_l +ww>lLoLx=&WQwk_kIn8=8kcsHE~YiEs0+EQ2PoZ2sw@NKNA3yihT^Nj#hQUOrG{# +{e(RBmPAcyMrjixw(<6I3x4U0@mu-o3pYP7psJTFi2LVUYkU$`K}gFDkq;PJwFHtK(-7`0rMW@#aQ1{ +rgt8}mf-Zx0Vy%MhpmKlvh(IVPVV%Z&u$eEX%PrODnGc%^soH?{_FqF`US6)CFB+cv2G$L4dVFM7Wj3 +Dn#8vW1DwBdv(`YA$oR1j`$F4I;^OF=$0d)=0JcFSl@EaN0idQ|SWi%c10&RKA<3B@vTl7>bWSEzguw +v+kJa{IqUQul`P0?cvEVr}OY_8Oj?R=hR<;`P`n?;pdxbx7qVLvJm!Li5EOkJvF>YS!HY01v0KqFUHH +n!M>3OMdWHo6KkN}X|;VKh}I63bS^lB18XA%yFB>!qaDF%D7N#_#S^IT!SNj#m~Onag~P~NNr_3f8fI +#4M>|Ev}IMHW=oMk46EvN6u1SvbvBi@1m%^scMz9*0=t&TFU(O`_$D#*@MLYB(?)6AOe!f7Aw3&y9;T +%gt#Y0eklM1e1mxg`Bw4%Jo{$`AL@ObpwGgC}XXW9XcuNwQAs1Z>@V|r+ytrSe1b_>i|!nW8F~TV?8h +90U8`uiNVdXPS5f#yRf1(7Kn>V(c0#z%Y%MeU&ZO)q^*}gfECDY5*x=RdFZbWh29)sW_@%Wu#Q-&5w^ +Q&FOQQXMqrmYdr9~ERgB~0J_bU}t~ZAz5!6gR^<=t~`GW-l5o?TzVq5JEk{N9PTP4}r>_|6ytuOQkhc +ciHtXN~rNjg^)NpuS_{<5<6%7sB+^xn>1Pi^&vqVo%3)qIoIBxTC24Ig3wOQnkLb7 +BK7x;!rTZ4Sj3;0Ih_p7gly$|1j3>zf1t9aNPaVzn?qp(ghu<0zlfysLa@)Vaa59IG^Pz;WBRwxmsRi +f>^*F@CXsaBsHEE@md=g3Ps|#tcZm|1x;5;(lmBJx +2c&9g}?oQZnK$ciwajQ-{P;qB!33Lm!_4FZ#9V}Q_zwY0OL)0`&RW|rs$LJ$zijDPCBGlVX*t!u0RV+ +axApNhCOL7YSJ%XxvBi59Si7`3xyX^kIXL8*(xc0?lBSS?#?Rd}Fg`UR!v&DEv=lH>9kfFJ}E$p5(CG +cf~~_4PUtxF(ToCNf?_if0AU%qH<=t`k`-r4{aDpc>XCx(qH1*D5bo1cX5* +-AL7c#M%25a+*Y==_mSoD@;}=z|AtE&|C#kfHdsL03v0>iZfN{n6i#t3WZv|dA+g$K=t{(D;u$A#;dt +Zup)&8RG(t%QD|64%m@I_%Mnija0h~&A`qV@OcS{(+<7YiuNCoUzAyA9y^X(Z)I>TGAzl>vDwKJ6KKr +Rjl$tMtuidfBRIy-Zb1Jl*QY>-;z>7?NU#L&91d$q_Uwg%D0OHYH1YQOnfT{kI3E#RqPJmDkzctp@CU +7v>JqtTdajj5#JUr+x6gvRlOZy(I*kF@*H5bE)qwE2&yeii4g!0+*_z5b6f*Pm+!lGo4n@1FzFY!#7^ +1}KDmw*ohqSOgP^#Gdx=;GPNh}%@)X$2Z9^VN8 +&||@U#zg-?l_iC_k|`(5GR=x=c5#!)HOsl)oC_4hSk4a+)qXKWyMdjOuE7y?z__JPwS}EHH2tht$n-W +_z`kgKFvx35{29nlwFBGOR|MDwL$sOT5q2Rfw@?Dsut_IuMn@KmI|9O>b(R;bLyJs>wFzsZsbBzWhaB +ll@2qY<8L(p#cO9*BT5`1w;C2DgT!PWLO0_kb7~TGnKx8Qyk~2)3#Bccm2*?DHF|YeTObhai +~bW|<#M+66!T~N75!5*Hkw3RfqlgNDFb#a%RR4>{9NU$#O5U!aJZ_S!x2+ukjGFm#%!G>vkR}f9kqBN +V~uDm7qB)Yvo%ai2TW}rJ&&qO1`%3b&W<1&%S3Ia+4<(@&p3@O5Qtb~ENSp4f4XED1bo#b?#bs=|JwO +t4bH9u&h@3!YSt0YP*$>VPi{c%En_Oo!xospR-F_Wxl~xOzyeplfG0 +NhT>W2t1TW>S-{!_l(r?e82GY)!eBcVUZSH69!5~|%jDo{AWGyPB3FNZ7fV)Z<6tB6*(8WcASVAB=;hVXf +mz3%Uku2Ue{jWsuoy()ZAYvtZY^>lm*}ck;5jawm(6LNTObfYI*nhxpydcFFO&URL{{lmc{D?}cB)ZIuzi( +Us4TM8>iTtDXDlm()6JhRVCk7xWld}_Nw(hjcqN@F}0A06q9;xgBT{i@^k$|pyu_&W|8te&PwMrVy^Z +MB>V!ZTYZPDDx!YY&#U=5upOp>Lxif##NZ>RrK;zJ=uGTq!usLf{6>Ae08chZYO*div&` +JexhLWSiBc2kFhf%-Si_f>~#IeN=0Z6RW&k>GN~%*yiAA`Z)WE;p3Uju+MfS{*I7Po{o9%bOJ+&4QR|eYfTE(`wA%Q(_5rvtbK$KnPu+BR`t9~VN+#(XopvWfEW;2?$8Unz +-PNt&S{MTqIS`F45w-tzFtd+_8bilVss#|ZX;R*#Lru|4J(gCQQDz^pvuvvO2I*ZDiK=GHsRdCfEgg0 +R?^nmDvV)$Riqs~njir-Y0$W;B#x}#>#CZ7@<7pzKQ~}sYoof-}<$4os(iv4N+eCKG*qXbQSwnP}m#v!?aakq{n +EY3e#grfr7J)hIA8NOVzyb+0Fz5|)oB&fOuXMxmoNB2yMPxi@1P3g~u0v1Q7WQw3VW5Q@!@5P^d%y|BtLc9+TwS!A&+(mB>2=L|jx}SMZODNVi;tnTaQz3ZVL@Fe}nzqmbi +ez)nxCMTC|Mnf_3TRS6{Ug7Q|2O3ehnmcbYU_Wda;8t@p;;tkRt2r%MZi%2l>I4yH=0I)X5cr;bCmw4 +c0LCxGDz6*>rJt`)G>Kg!H?OJc8Ahd}6(#sw;B-l-$p3T-GrpqTi+9m7*x!zC>5QxCCuI2)_i1iX}Q) +$%ZuJktIz`n9It(KFxyn~7(jw=X&HACgF)jGzYN@2hOp&hUE>|L4LE#ko#7C7$ECEz=ft0N*U=omQE?d +0N-105l`kuFK>k`%v}lW&JAx7P3AR43R*;q(RPJ-2TwMQ6aj14@`Z-2rZEDPG}e1Rd+S7k8C(n?WB@1 +8835rBaIMQ6+)`PhI**7K6HLl;FAWa7Q!qb*$&E6i%6v}N5*rI2)WWJQV#$ny2!-PzDJY0`$;h{e3)A +`Yf4k|(g{_rikZ>@EVxxY>N{c8l&m5(+hzoNqAG3)pq~3S_{2vAhgyPTtk!zdsfBpBRnCO2`tJ(Z5BE +<}qyuT+>i~&4*j_oW&i8&um!Y6yyApi(M)gjcQ!^Nz?c!vePK4jGkZxR3HCN3WIjX?}p7$6YoU&x=Tj +~PW*FeSq+?wSWk)?@KI~R|y%kh~x6sl*JZtT<}<7yjkDJ1cX6r)*7eQ +=(?bE~GVwTR0C&MF-kA*g*E;;_6`ETnbh2*!`GDY12)XC*+9&dU2eMZ52@lutvzn&AO^ws9$ +2C14E$pQw4GH>cbYH4Un5Z$wAA+UQ}UF7%Mqj)DckqH48}mgA&!qml-z0k7T81~4&G`nf6=gPD?jIki +9l!Wz$8ZWR+lB^>m|kt6gQP*lY0TSQcOOVGGQgq7<_xCG|t6x3#~Cx-$dgnKf*-T0*ZmSe3*IKH=eLk +Mi9I%`Wxz%W9Vz>{ruo9lth)MZA*k_D(PNN-m?7C;{@vMaFuBWP?y*itIC +_Cs8tm{1tH3KeMiOZge)Y+4kH-riBkGBZBB8gLJiULX8&eHgt(c(g}Ov&G;4%ln~TzP8t!5i8=B6gFc(~X{@A@mjFRzqe06=T-R~ky4LwXFh({F%CeE6%E@)}ZR6-{u22eF;{tZu@93%*aSg +JdLomBVMNvS~bI!1qsI3q<_!z%KDh{nTkFl0T>NSLp1U(bLYkAu6#^?6qo3#bjH00=;)RFulAjt8+vu +tYaTTnwmv0OXHo3$QRPdRq&S&_fB`^^RC7`D?Vus095)uVTHg!d$7FG$(Fy#OcEv%pMbsBEZh(Tf`$7 +^dj(#!IKdH1fj~1J$30-Mn~C*#1LSTF@E(sN*DRvshP1mHeM=e*dk)dm3)+mH64Tn){tOL?PM8V2jSB ++Hq0>q!k|5#67fq+0?HybJnRze3(^z0&;slT#xXXtjGRgUMGmODt?bNbUJeIcdI$?*J~4nD7>XGT6mB +AZ4*zlu|wxRO0N!CFxGii`CC8!$h!i5SB3-Ga}>7k%+Z{lya;(05Xa5r&!pF?DOKd+#&waB3?-}xV{- +XTgHH@8f?oIc)1RZ$dk5s1OP#(x{VO81VUDNBU2E(_d!t^1$acCXVWsd13+-cnxXIB8^r~YRnB{YP<^ +Q(Hi;g6CRnduk3?X(fzswB?>c!Re#y|X;mj!-fl>jyHn#e&%j;_q$7H$y^KrW}j94GAwhauFO&^p>FM +8J_ML=j|&cUP`GFFyI__{=`z*Sj}?>7${Vue~@rH1*;BIZdKc2P|408l1-fMD22LUj!zr4p^-ylh@8U +DzTPN>EIdD`G-m_k<%J$}1>LEn=Z8x5^%@n_O)I!%AD^^lHEN!DPI`uq415+R5=rE-yBB_#F{cXn-x^ +o7~2F^G+WqR%-xj79cH6u8R!Wcm!Cx4k<`wKYzw13`oFJX45`Izz9I?SDV}4#!jVNo3)-z +dKjz98}GuLq&#v}Z6YX%@FcPG985>w<`wkbBKGC_bfJHkUa1sOUv83loFRLt(A{vpX0PchgXj)_Qq^H +e6#YfwonY7trFT5ggE0=KzdaR3NHZf=X;uA{;4tgoF9saeDA+aj>zYn)NJy;(1t+wBs;&h3&YBT=lqM +EwkqwQGT}h_%JElaV)igM-nI)t{Il`lU^BDhBQVYlt3+%>Ejaz=8%sqk7qB{!@|RzU`@d_%a|c?Lb9# +ifNFwhyeoib;yJ_fW;s72v@X-6VkgZr<4WbQLKrV2zL?BIofBnVUQi2fWOyUA_h}My}g`EyFe%a0v~bwAym?k!}ypbxeW_0O3&8_9yNL=tN +9`P-bc&m7ux{5Pu}-bls8wfSIqXJqoRERQ`~yR>4erS_BRN<*YTPs7wZ)G_wYLe;tge!vWF4W?uwpYHa8+eu0kLzn)@mYwbz4F=Pq6sB+}x8+8WS +vf|YML4In~_cHmb|w-UhaU|#PnBHt|{pnRnhZyTbJm4r~Dnp~#we6Ag#EWf|>f%q43`RD&V2YnPO1z! +LBXp{}dqo9iluogU%y+`yDll9!&ngLq^uOU%POu3Cs0Ttp3EVqG31dnmmXd_-pN9K@foNL!R@`07x;R +mHpNwM(v<{+Mxv{o=Na#T9FtbhqoTKZwYAAHN>) +mi85RJT#!1W7KZ*4QhP*feFQvux9c?|i+oDXk1*K^CE(5Ud61+gliX_zx@DqnBpMaPqU3H(xXo^lL0P +UTycBd{|?jp-mcX$971YFtiKxepi?1)f;B`2(pN%bdOfY9lo)!tqo +rJrW*FIcc6vAj)GlGn_nHW5RD%R%IlfdR0Rj;sSZ;;@u#yPG?($t@5D?HI>xVu;)XUXHQ>6#_+@m?5K +c&$ZN`8V+p}10ibwN3It4zp-nW8Z`zuYLYAmA$dc-sx@iae?C*4`Hs +EHn@fxqKK1ps@H!yWz6mvzc5=DEX8S5C(zi-N|JUC(LMR6IS+zc_xox7+Ms#C$Cr32m(ChO9}thRjX0(u7U(9Lsy6ieEx1VlZoZHi#0DpQ+y +DtB>Km>M_m4jni|oguFS@l|?pG0k)nIR0`#n8PsEx>2S;KF90j@*#+yGF +~Yx>=0!JP$=X~T#TpAD!NA8EJU>?sOMU)C<61*}$GWi*E_&l@Xw3IZN^m;W7%ld0rUP +F?P)0lFzJgY0(@86{Z3U_eKT0N=ogjB>4V@Y75T%z=Ymtu>y%nEf@C*LtWpAOOX-s$6A@BQDBYBplmB +NjbN{TVt;fKsEoKHM-xkAzCm +&y_$@E1S|@0zb2+O@tN|VYNUrX#wnfDSgr^+R +WNtY~B(9uN{$C&NuptrjvZD|Ajyp^lXiO*7)`LW~l}f+J2b1OOz`h_1u~0s3(!cBF!kkc+0)Tkc9bDEz +MShNK+to&~3jCP*Y(eB|DIKyNlpFZ~2SIi=IIVlU`ci<3$SXmd-(@-K{19O07YGR|+*G@I+mYJfnr_A)x_z3tQi?G?EOmH$;{fj|Tl%bglGZDPM%n)LC{(ww +tdAT(kv8x*AJjw3^LU05Ixsd!D&mGwX4ZhNBI-glW2( +XjqHj!V#A9B5>)eZq`hDC^PCQ2@C{R|Bb0P%7fJSs-iml-%XP?HO8??AHS7ot2^wgIa8T5n!m2mTe1% +z67W4D6;#&Xp}7gMd=}Y}k~AgNOoio@Ci=oKrDB3o3gJQC~noF`JWp167F +Nww7z3@QXU0MWjS@cHGNm@h{E2!sqC;kixh7j3X_v(#o|ATXzK6@%X~VB+;=*@=^RJW&=1gY-)D|aC>MB)epSolzixf+{I+t}=TJ!aizV`IH)vzgx9Uft^1 +^{<#;GT{;K0XP6DrK~%Ps#6b7fFi*_e0cc#x*u0Sv?P8to;8xq%=c<5b+>^1ss(`re^l?f)}hMfKZ%c +Vc+le{jAe?w4SD73Q44o!7HHo#O5U6{ek%;msqm{L-#P=TX{)%Fmm+FoT_UM(edMtYggTfLD-?_&eJO5z!5Wfn&lq +!bACJA(&F@=;MHjRDH5VQq&$63-ntC4Z^s)PNIaEf4C#TIIKx;XVtYu|`uGv`sUai@zGSgW@*$nU_08{$X~%BWISdus|`fAT&~)&6i +$u?2zDD5QQyDw$Pb4KW}ugy89#qGu$Dd1<8FtCqmqhsFx60=0pV6fu|Ic1Krz7oa^3N}?F4%6`N^m}* +^(yVZYr4g^VLUb0&xoy-p$a*}jrl@=@Y`%RdFb*cqnDa*uP7WuJO(do={Doy*u-ze9BLZjor)y>z+FIB|(j+XT$w{@>Tcu(18FdXGoUwe>QY_OueZ>Q!fX#y +GJ6LsS5`hi!BX$C&3$%I3Uf7Y_2C!nIi~GHmJ#NF^On~a&7LZeZ(!WX1F{HE7i9zZRMEic930y4*5wE +9l$3MPE9M&wzeYG%qJB_#iNU48fe^>-m7zs&qyy*-7)>dw@=F?icUsghTI;9Ar%b_z+dz~tNeatKmZA +id;oq^zbmAviA{MZR5u~;&>3lJ7Lok6mn)pI44kZ3JIp@_U0GIPfi)VFnN74&skc8seL@Su&|TPeK1@ +=Yi{tsDn_n-EC+Cl~36_L2{$rNx@if;$nVV4if}`>=cvZ>0Gt$hwDC0@6(@>Jo?DPX+GFbFp!GIuYiy +M%9=0_{qo{dUhjDOp*$!D%qO9Iv|Avj^I5QHJV6|MgQU1yv*OdicS7=b-HYuQIR4bFV}$zM)_3_KDDD +soqL2_B{Dk^5eLl`x1qunx*GEPpm_Hd1FK%JHKt%yQ>?YJjgpNRYD_4Hu#MX3RPOgd=9ha7N0UQLzRi +vD}nmGa!)KJ(7yaW+|1q5An+BtB=P)_LZbX(^A*zu +KoG`YepX45v(l+F(9*j{X}tU+2l8$QA|!(sNNH66!3yvFDVL0Icsmy&ojy&PuP^;ieR+`gaT>y;foE? +IZT#-u5Fa_P)nn^~Cf%c4w|HJ_{w~&OA#>$8-(PAAyl_=UNYvYwrQRzGp#Ls*1wlvsSM4ot9dUdkmc%=+tGIhcLLRw^l?$>W)H)pLd2YO+ivJPEIX`3lvy?`MhGN#5QoawN9xu(?61YFYr%|W)*?@gE8cAEIt&&LQ^C{8KbC@3?i_n +1x*-{khSd5C>s@32b$$ub11Z#9tAc~+;8`)&&{!*q$K7FV`!XQw=??U_dmY!esRc=^Pf3Qg}w2-lWxS +%Fz^0kDzJKq`Ug0AP4r~*Lyd*gBJtRhC!(0Cq6|1%8(6>ss7avH*M;kK4-!HyzG4m78Ue+yz)n| +mH9?sHa6d2nz9L5UeR*-kQUmTjKcl|t&6q}$&erkOJ(|VZNhCi1-Vzn2B)`D>J4Ql8^8DqeRMjcN-`_ +26&PT7g#}@Vui#p`mVFTQQXr6e>1K*fXb^^6)rp#`pZLqo%mkhEO}k>5#PP&)+wUAnrirz +5`*Ds6uU|AWhS!Fi({<#&DMi)+SC|Ci|ySvN~^F#gEVKFcF|UTI5@s4hfp{`^9>gxLo-*vJD`BSS5}Q +DVolUcw@^zk7QaQVUEl7bCj)(7I6dnz9`w>R?n3>q;vWtBbAIcwg$2RF{-YqIwW#BSa%P*PA{RlUY`cn~r*qdBB765?CygKTfVNPIw#Uc#vYDcC5m#!KGpG7+$Ndka4m$AfQ^kL1S +TAogiB#e$q89BB%nfHO{IIp7EsVkj&{kO1@r`R*Zn{j5;K3(#y~_rEMdyut7tH+#ylZYXbZoQZr5B2b +e>DM-ycPz69>PbCwIayfYS8dJZP2WmQ!j(mPJmV=|TS!DQu;54?eAT;V&UZJI)*4VmV6)`!5vY8?_X4fSd0j9u0~fDL!;B6oEi^v3_lgiehqO$y)ENC>PG5Qo#F~vLo7Nbj*{})$Ylt%1^_q650- +-iPkshg88lYteA}Nx0dz<9>IFwtx!m}VOWjU)vZrt4}2fZEACcVHhvqQ?HYZ-%Sz(W7CB@l){HCZ+5c +)kacc4@g92QJ?LpfR+cva9TK^i@A{x1>KosQu`qVG6=^@jXZ-<ibEb@4PQ|0;y^6BQ$`+3{ASNr;a)cYpEI-bI{J8$6EQr~u#aMhey(~<@o8WNzf!} +KTAVEUpMkbds%tHcUNN-T}cz9HNb7V4i%0#@DzKA&&=hM<1P{VUUvkvK+yew1u+vsT0g$V2)m90k=9g +;WAps+*y%P6_J5D3WT; +hC#ko*xe?o5{F@T0K;=Vq+I~oJHbP-d+b+XpyMlL=$U@M^-zT?LxaqMW|IcYWZ#$z)%&>Km>2g8um+z +GrfF+VcCK3Y<)St)U9;!aI`0gyp-|!3_zrjrr^3CFZ}OuE-t$85tPKeB7 +c{~ddsQjdA=T8&t5S8kE?qW#fJ*FG;?uyBD%TF_qyoQktP`C2r<2pR!KUj>P$N+I(Nfex{hi13AmjA9 +Fc0zxO}3`Yd!AH|UXrYK_R=98)DPupbVvsUI@fy{Z4zUtVL@z8d1_3Dlutu>)cZ{wgDo^tpxU|`>7o4 +fF4^ke2#s4Tuy*ghSN)_z0;dbLmF|cip#0ju7yN>=QZ0$WI}4Q(ExPkGpL-HMT_tf!=8*)o*^tCfaLB +Pq^dp~S2L}Qu$;G5K0fgUZe0Z`@H=*Is0BhBE2C@p19W9h+4E1j^Al+V++4?jw0bkcz2blWRm{qYm=D +R)q{1&*Zf}hxTNEYQ!y_t9At>)%uchIMA6WUQ3sP3;Ca{%d?tncl80#eZuj6xI=ds>%PBysxWT}=HYD(NiLg>;)3l9t%DSTM1$ +hflT#5i~v|Rr*QuB-Yc#Lzoe~5mIc>F%#-Fm{PdlH@mBsy6lk1=~~_8N#xFZ5?~q36`{~OaQY +KAQ^;g^NnVg-S71A^)aoTpaC=J6i`rLq2KUjD)WPklNM#TeTdt-}NYP8^LJFszR0;WxPs8R)U)H`pd +a-Q$OFqbJrtLIL_}z4;Dlk-p^YG?FfHp-9C_;i@t5bo8?x@u<(>f;)zmb~GxG7@MAP7lmlsY6PobE&88TPs8@q2|NgA +*s@^i0@^vE1)Q;1%dR$kAkK_yJDN%7uGBcp;CjL#Z%yY_;}@&W#YY1KkJ*R?t&VGCX=^6g?~L8!yqZs +bXErYG@O)_@vWFw?qkr|3)PbbSVN!EJ60|bgV26U?DO=&zrL6xs44*vKrAvqGNoTlb?nU8_z*Y;z4}u +`dL@4`E<=49+B51pq*_{lyXj?dtMqJdK^W4LJ)EWOWo9~De5>c*z-Mppp2Y3ftNC*BF}OWeK-?aMH=e +Ky&Z;FW(C7mvswdV+BxahbonEA2c*sK8ja;f-RnXlbO_QJRtYM;Fcv2oL8+fYu*$WK{IwXF&lyV#7se;0h22PMzN(ZT(MrYMzVhybd#t!M6ehp%ke+sh#1RV +y1rJAy;cCmf#e*5^F-jmPHFHEH|0|KZSM=QIukYosgE(;P6v6l8RS}f=ObQMf&& +KH0HY8-oyt7RcpGP)n7VF|)H*H)i^=DOy6H*l89+ITe&<1fO^)#j$PBEAmI8QR*_@9ZgwEXEWSp1rRi_ipUKwGfFP=UPn?24858HG&>n# +>l!tfW`c3mzdXy4&f1|7;paDTtO9mP{n;~035N(Dm@UtqR#;$-24qaG^4Oed+&egVvOZOZ2*7>sx;bixWJe>Y+ZR5Vau*KRDGTq+BsTg{C3eSmS>Jk|gukKxqXc38YmITwH>9Yt>qsw_`Zst +YuobX>Y3c;<|^xeI5R7NWOHgJ5{>WOYG|;u^@ni#*{UGNBNOq7T(%TcL{ +DC)biQvW4Fw)My{D}a#ka^Lz1QQaj??wVCt)uz#4X{0!R}EuL5h@C84!Ck8C!36`GSZ>6Jd5d*udHLz +_(XC}`)d(zWh(lchhMu=m0sMUt-d9nt%yy&@2XN*PJ>jO+a0DCOJfo?ceIy>srAwvjqSGB@SOtH5s;< +}Va!naE**wHi~&s*Ow+JMiyD5SIA1o&-&=9p2AfDtDOKB3>qy+rs?#kFsf(bV~u8Goog_0{rppl2&Pz +!i;WR7Z6Gh20pZQ|6aO9#(A-y!#0#a0M(*YU6L#L+c0zP9uArlN0;PDAg9v#_HbM?=LwYOAt(CmWO6M +qzGq{42ohRl6h+`jD;X`L~C{j1d~d&lZ1XZSm;z5y~=6V22SR2upV)reMQ-m&8K)Nr7Y`K{dhv>MQy# +X@+J=Ww~adF@bbRGxS9!UlhsmHHa@&mw+xwf|mNWseEe9b1i_dvbMYLk|gN5xHaNS(DFEVrruST)Ih$ +TsdhJ}k+2|ccoETVsRN3jqYU+ktO$gmNFDjHV3))|iz~lULi);Q6L)pC=#uaU)T8la~-V2sW8v3Vox)AH$a<&fgjXHh+*Xk20+nk)VDxMPYE>bIizeGl_434dnr8@eY1p#E;CAODt8a +;LDs8hMm4SC&TuU~PtF9yUav;W1bk%7eEPFSVfbg(+&&Mw+uQqSJm?WvJO2mt +id|Cf487%OPIV~^H0x4yNwzcHs!Hy`R2$)4{N2^jv(U16`=tB+Wfg?GB)*x-Z+emjnT8P=eSB$PdM<%A#8 +E#vglm6)t2P^DK_G3O98pP`vjk(_`N0ey76el3X+f7nI%97+4BXc?P#a-Mj8pbZ|MvYS{9{1W9!CO}b4 +n0!Q$CYH9bZtj%>7rWm3bq8wG!K)^3< +(B=q5A%t1UJ*-(bWO4Alm8xbV+C9&%9B&E7j1_68GHm6{?(I7c^gy=Be=oyY7_KWFG24y@c8jPg0u6* +u)kf#ad4wV^LC7>Dnbhjd4y((pyMKArOW#)*25`-d$Ae>NT*Fye>&)UKLXy(aaJ|qFhiAfHfNZ$UBc; +m5AOYkxZau=JY!jo(?8=Ng?As#wzmys>&PJWSm`+$QZ)}^whFI4qJT4v*p37ZXsO~#@GV~8<`~FpRf_ +)Ac@RG&l%2q9l)9Gf%{asK4F)>r{hok|779%LO;_Eo(!sxCPoi8_s3*n4lNOcrA8UPu9Ljzr}JZ0U^N +DU+VeremsfQPQonrn6thd(mp}ePsxtk+W%sGPCSdN8>g77w%S}jys4cK&L_3mH|5htPxc02?!F-pbEt%YZO%h0eO+*z~Q?y}AWfAX^JC#o23Ybj7FfVd3nFQ= +IQaHzoJqj#`U-K=W9>OND)gUT|JkR{&`5_oRAxVReFT-> +shX%2z)VV1MDDYKGA=0XU_LTI-G;lXTPoYj*aU9QQ7XS*ByplxH+ca}B}~Yut6F4jIS~@??!5EG1FPW +|ibDpD^AGUEBfAcptCW1-RR|s7xyw>bXSSzB2#_v%OrI2NXeAg0hKOZqpAUcBRK^w?gZ(b}L?6O&+eC +NZF@aJta@*lI$gro6`HjLOtiYOM;i{&CF!j+Gz{w9OP_zBgu5}?oe64%D80x4YuIsTw|Ze|lqm#Or&4>&2c_*a^!2YOVp68Be`O7+%8vXdOKBLua|tG0(!YEO)h_aG)p +i>w3nzy3pvzhhT=j4qBqvpzDuz<52Ns)yHIdA6(9I5!jp^DZwueW_zjS8Aa9OGcb>wmmHXnS)jxZp#Ehq~RC7MM +PTm5#9)(BQ7RrI6*U)nKW9>M43-K>TI2)BAwGvUQ6d1veyisSo6ydt^-O$1K>yB{X%hUDB$I64=_%#> +@a~<&(wMzM%f+av9C5wezjh#s@nuPbE^l0N>K7goWD3Nb;2zK9wX~S*=DZf9hY&XCJQtgrmuoH}YE!R +EFP|Qg(+^gj8(!3(Vsym2vNtrUaUV7unJU|m! +gvQP6{YXMjm-HvYd{C(&mJe7H)OP9anZ)C8eILdM!ctbdEncN6p;DH7In|JQ%fBQUgGu +Kdwo*}HcCoV{F^6e=%$az>>=`}50Z(xNQYCW9o;polXE0toukU+_ox>+`20q*Zz4nKlVluDuVFYkcx7 +IJ>plm%~7Fs%N`LH|hbfR%2HOW_JtG_-j>xYR$}JV~ayN6+cy*@FFZSbvRfMNJ{UeQs|AfL*igxzY?c+bmPORDpbKOyTR6UP=T87jm8H%?{^*lKY(^%ho3wi{w?2bEYLq5q-UK(&1S6s`|U!K{`?x3d3|~> +jpzDh7OsX;ZNwXORB2wku|M&kAMgRT3_zwmJvG5i(u)E*jyrW1p#@i|4Ml0} +54-2h_28umz~@-YTa;S5r8(+Y_B9mHv4lbFmd!xD@0*}_}ejS)h*P2z)RkK`&gB61mScV8ufuvF{*J< +_WLdT!WHHb!$+fyVn|3>$VjnST@5#9ROY)M(zRS{~2fd3Bb@hFbhUd!$nd_F=xmQwj_Al%oFd^hkbkm +8_FlVyw*r1HzK{81MyR&lS%Z&fJC(pxiz-zRBP_(@KwID2oCNe{!2VhQJE>kOh+nvh~})zkT=q@9%=% +d-hILQ(hTyuT6~rczR9LBeBX4t(bcxTbb`;J)*@KxQ|z%RCsZDMUT`fpJ9qA9>K87|LLaCiyl#VN%&K +@`e>|kdZc6VBK>0-MK>hHMMk<}NpDoz0@OW{uv`>SBt*u>ADW;nCts6YvOiAZmd{g}xjHYOPb~49MEV +wQaWRtFZk)=m3P1n}gXx>bHE8anKe{RdqXz26IXx1#=;d~if)%h{trNJlJ8O?VPGM5!$pf%9SWqCTtQ +rhdjedHhV6n&NHl+!{oywQR1lfiCmmahANX+tXR{okAp89RQe~XP-kF+i-at?pO0&6rPFVb)7kXr|2g +zADlYp)L7ds~fvZ<0#tmZ(TKCdCYd9T{StVG$a+v2Nfn%q}_nI!K|U}^IsB0`J)1W +vPoG}+vh(*T{8Nl0IbdXmlsg%y(`sQu~SI{G*_qfbBP|wQ%nI96c}3&NbD%6N7@uG-)0rdoh|=7w&zb +5<>0E}xdYK22~>uovB_Jo3*OP#W-+jaFHE25kxXS)DZOH#U47Cc$qM{r2htpr(0F$|_Ai#&B5wk8~@r&4Ksd=%thaLB#JS_H4P6SV~<~fi=Zp|3Q}70veu>{q;z+Vw`34S}A-qHU|W(B_93nbm +t>80s^|9)m>Q6)tpK0sP?sRP&KpfkzytADhjE+^NyUHW@fXGxY7owz6ikQ6@hK>EzjrSX73KP^ +GPExY0Qe#uIJfDmOfiTn@=HHja1R-}rPaY4-9KD!K4udH;uC=TKNY?V|w4~md3t4@2zO4W!K5O*ubw* +Y_lB)dZAzF|0D~rqDzFf|)FO`gP0%53TcHbib%W9v6?wc6k3szT3hN>%>#3sz-wnM|uwKW_GR{RI)@* +V(MvmkmTUAfv+j$SpeCIgz$V?9VLVJk==3_;(|8sE@+dHLBM2~u8j+|VO=%GFmH=eT^fP%fs~ve8LY4 +oVN&m{DYbg&D1rKyyqafy$!Db|X7f2nUU4p7bdn;YMu&2=ZnK!qT&|Hpx`3Rgs3MS3-C?=e$Qsm5cbU +{5>_WChuq)5(m5FtMO<85J2U7tJcw-BrBJr%d0>BTf7Okqlpisp9#zGgiQQXcC6kb<;p0@_6g=(5S-R +CNqVGS0iROHnl&G>3ZmhE*%lq{bzW%)+aqbqg&xey+oyrG>HkP(L9g@uUAZd&lS^XkDLj}CCKWX +;e2O#S&DDPdmXzR)Ah3%Tq;8khqt$;Iv>DP3F@EJ!)4f;HyHnc+~xp8`m4bt#bvnz?zZqP;p57gWl@Pz=E(;6QTFIE_)^{hYk(x!tvt(1Ct`~tdL;o#n!)pR#KamTs4U@6fXU<!hqd2@ge%ixFW0H@ao;(p9n$gZP%xhk^^$w5yx$9{;nb1<-%LN~ks2iigr(Z_ +*Yb(aOS3kphX?+Z4wyMU+VM{&RArAPF84_Ba;>*Q>#4q3#~`sF44nkYzN$m$k>mxX=q3Q^bm^qL3{H8 +jeUF4M`7SAbAx{zqtRaSHFDEJ`(UJu{(OMBF?_4B!xs}I4W!3<;bZ0iNii~_=8@1_sq<#rrfJhbuoYB{ek&q{f%DPm#L2) +16PEfeS&SF(xV}=i#NBEDTgyk(kB%RI#H5Xod#{PB0H8Bkw&UQ9%%f!1b41fSTd`K8YkwD+sI^7W|UN +qRzfALixU}ALxwEZ}m#kj&=xc>|@Pwe?IX?mp(=yEw~6I%w%Ey=)H=1gIb;n{|l749IG@t7(d?0CZvQG;h^Q2Bc03W=3PaBee>?rSVFgjat7}BLa7*+9v0X_#QR%yO+*5hrI +KqpB5}Y!5s6y-$F+*?5|eCGf%ZqAlr3I#*hxPKP-dh-0JUwiJZC$6D56Z+>MjWFLoJD1e(Xq=R4;mgn +>+ofg{SqWJ+Dlkf)=I-RRvf}3<4kbx1qBdC#b)xeJ(a3Xh^??>-$JCNX3&5!aY*K=#gO~sm2xpTTzkP +$E(@=3v_F~}iijvy>GKFOGJ;6kTmwou>0lcvRg49}zF>rBb401!aJU}`+ +R8n^Tu&7QN&rHDDG-NK=(X1ocZhK8wIMevj^v`2E5g#_E2)cQ6Ouy!c(l3_&ac?0Cz*nR>*^9zJ(ZX){`NX7DLKJuoN2w|X+@O;WH!&AS#+{jsYgvgWyfkgiM^&yrdF4OFGDg +zlsY8U~C6A;Lo7sODnmDW4OuIiH$AiC;VCqM)~vXRu~*NJ!GU*pEi0v`|gjqpf}t^d!++B=re_+co2B +zbhUcm`M_w6LEngVoZ?&n<}M6Z-QWNn2LZY$41!JcH&Z!SL?x=mfG38g0ovTkkhlyA)E9EC@rDrle9? +CFwSKI3CBze&s-n&1nbrj$ss*2VBbQzEtnFcM#J#$*k8$MoM413GVfK3GCb7xKJXP@q!`~LFYvsRC#`0g==6}+gd`~~pBcaJN*{Z +#fTS@7idAQI6AGl2#5SG-N=C{DDMiE1>UB%mDVL>3pHd#s04EuaJn;wZwuGI0SJg)(0e+WoqQm!m7Rd +t!EH#`r=s;bkJB`qzwT0Q_u?GrtP7J4qdNYWC7rXtRZ$V9(S1Ae9dY7d97`lE&VPlBf(tNH9 +=a^+v!lAH6Hu+(yTL-LRphek;kB7ZO78-;rFc#9w`WzEk&VwG|r8@8;J?!jQd6{ +A+)C@tdhos;k +fsqc!L=+R>%lhO6^azY?9iOb2#8(kXDAyAIkP}MKYsH$lL*(XWKE3-NJBraLZDibq6>sa3>aS14+Y~b +lCt$1GY#RC`IT^`)Ci6x5P3?~e!czAC{4esWxJYFr@7aa@*X +Z^8Bs>Aj<1o%IjrR?L01B2<*4%RfApJv{EHVOs +v-Y5$8M*sxBQv$STH+nqNjdWAGfK=A#@q%2Bx)sc8&+j`o4WN_IBG6AP6shfvirxTI`c%M9+QjOt^_K +te&6uNft8tN47S-BU8Bn@vr}x_+S5pU4XNaALzBuFpZNeMG!zu)0jSKLvG}Q0$XeINeI|(y8qURa!tl ++k=_=H0VFohXRGD>dUD0C>)@YQCl|&^-TaBQ-gOvi>)UT%slV!zcH}kN(Ei)+#|QeP7l9id!PvY{Hb2 +?>Z%}B~8{>cdm&Q+QpMnpG+C^81x>IRnLH7vckoQ(niQFc)-`vMBz-NL!DMfyrR7NR)_FFlAjrh4wx{ +zzgIIxR4Q32Kx7i!*;1Tg~OWZfEx>ytJ_4>k8Hy#`Oe5J6a4?{XElSGMbuVB~W8Zi_XU1c3DeU=8tHC +En^M8<(gSS`wH0q#OwrrNmuBi*mnDvXLN(w_lUQDs&Lo0`8aRYhm_DL-PIF!u@ydFy%JPF=TAPt$LC> +iY2q_dX%Y~nqwgt`l`@4tLu}ZqQ+?@G; +Xyp0R1dvUO?yD;~i?8KkC(l-_wpJzsrS!1k)8 +jh7y?HOoHKcrAuC3j&=u{8^<)xcHAg0$LE5=9OKkY0K<%FFjjp5pH}>R^>4D+>a3B(4|*=#AB5xaNPg +tiyPh7|D{TK)Z=WpJXe6{t5~VC~c4(fi)zP{J5+^Wr4Nwf7k%J?_?=@`x2`uRNM>^ge6GQao%LrC`+G +oDUbV#X8&=|TD*1X*-jpd&u7mT*6sy-+cp{3^KXc +`U)7vD?!i`#~3bcx0`=naA#ytvT+&cj6BS@s(7?B?M*T>~qXmU8FPy>)fa$$jrpPnrl%lfy(u}O40*!Hj#4OxK_E!HnYiEbRb0*kD1G3f`!3VZ88rT#7v4$SzRTSASw6mWB)(+>Ethk +l;;XcWeQ5E2g!YWt<|B-A_CxlJQ;z#6`%E*RRJbdbDJY}vm0oh=AyPlog5%)7q!$DfLrK +xi_h#=jJ|YL`?~3i@3aPi{|3I%RFKAici`mXL!BGX{VlVvTzw<6DQi!+%qoQ59+l;*BYW#3&y>Os;1t +TPG%Fa{@+<7Tfk9q(jlOak^E*iRldvSVPiqnOtJ?SqPp~ZuLom@6 +3tElIXQ2B{UGQc0H<+d^EN`uq;AotJ_f7HQB=Q`j?RAM{ImpDh0W|*At>~tsxMGYN>{O(zSerJ++QHG0+e5iFuk9`w$-en30TYSzl8x$pY$nEZ)UCcX)+C_7KE +YZJ-})c=20~9GEoKI@%k=COfILRumWNfrt2ci+qYGuTUn@dt$UOGQzg`w#Uxnqz2LX$-NDnUjlL7F?2dU7TQ^k%%vrLp; +W-Gbb;fX465H)p2|vCeHOtF3W29nvE)+oel~u9MRBZhT5h`wlZI2(RS@sIFB5RZNLGW}ANS|~pH_4Tx +`)1(exx47SbgAGu$^3V$!ulj<`G5cC|IG_NUSHS=$01HYM!m{MK$5lCn&x)ILa6WXs_JSsXOQ0IT&8M +G(RK^070yVv;hNsrK>hEG_v`AKgM=;0(9*C)Bl_2PV%Y?R +F$)*uYk=SWhyoQJ8pi;iI46cAV|rfz!0_~H$W +hQr#q5?_=s>?IIW{(PXmG7({lQ2_7>PHn|dCQA5d;ob`Tc@OHMjjNEW^<*H?5nk?Qhit>RScy2sOu^2 +U@Wj^C?pQm1Kdax=O?I2v6?F=Qox*VboUK0h;e>`v~Hb$NCtCrRJY_(iqPm4B!KZ;u_jm_E*fbPB!Ia +zQxF{vTHu|NG%xcw$&&l~(5eIo+P)zkJL=oE-l+)c!8kZ^wPG5dr|&Eh6S5m9fsgYfdhyb)VeGPH0N9 +}0=piGwT7yK-vGvQgAgq9?5ppJJ%Z)#E$to5`PyANsAsnVM-bsHJz;?)j0BZbR<3DBd)+YhWXufm+M#d!wOcfZsQ(Zs +u0X*~wV2$C--&>e9_B}6LVZ%me7dd_vQNL-1JZZbBoaepl-m{Cne1loN#8Io_&r +vg0N&Qv1%7#R88+w64D(Qs7+*5Q@%clX+G%{zM?%6Ld%wF?Dj_+NXAI_7ojRaXv(s@d`#+tAL(hvNvg +Mufh>$R)qviaR(p<<)Fj15KAJ7ELa-ZVlckceE8f0($xX_dFT3#QW^n`GeH2Js^OCP}_WG1Z-xcNCCq +?Q`jxdm5U;U0+u}`WS{b1}|X(ICCR~NK{!BOLG%;jSLcJd;_c*#;0!r+-bwRLa +hu-&EDy%sVG_2uhl?P3y=Mha^}y@Zmzyc2>z3{>;yr3@SLPHnVx}im6^4@1J;fXZ=yYfSiHdOODwP=7 +JU-VOlCp3U^T!8`2i_q#-mD{0D$LK4oDy4<(YL*&P*CZc7pVVxdRMH5kngKbf+YP)ID4xm*H0#i(4>e ++_Cq-2=;Mb=Hwx;A9~;fFPB4zw%W4*4Z@MpZ^=`34JP!f$1vH1nJHOZLUjTdkksWhlhlBeE!TQ1xJ^c +Bx?Y6{0>~trWp&wAlE6^^BfG9;C7Afy7?`qt@fu?*r1&tad6p>WKl=_zpy=&393X +BK$@GeZU9|)lO)1HL%BD2>%?a-FN-vAg;}0IPaKf)W~5iXwr*aQJ7BpF+z53*LYxo5$bRuj`~V<+cBr +RAlVn{<+Zm7^Cx~QTwPDOz8~afG3c>-(sj$FW;i2kgokU764)g$rAPlj#?|(#?I(X-tX~3IBS|?V5hG +`wUkSc)5z0{8xWQ;qgOhAEiXAqACSIgta~U%aD;(MJcl*6Pt3f?1F9N?z*57)tEfiNW3d133#@+p^8HYSsLy({8#@1_KyN&F_`I|xgDg1V0hdgCVM{J%0_G +**J7#DPRVJ^q-vmB7xljqgoDw!O9fo5Y(EO6$?acgH=Fjp5&fIoJ;2**Z{V9orD{4@je9oinW58$pox +yJc5DaCKv3DmUSFZ-#Y(76XB-mI*f?`OYd0souIk;46?bmt9?~)bCaZhu~y!ZHrKn?%49sb_k+c(KH~ +A_ezR`iZHcm3h^6>c3zy5~~Q;YTY4$xHYfJub~34QEFEKqj%B`w?g*& +=%|%kltswm=?FNPtXIh22m0mQoL#QLaH~O<`@eM3^ox6fNot_Gd*ku=q& +=#ex)J%!K1Bn?`s-wWk}dj?pm_f8`KqrMp|2yj-=m(-IC2c!uaCwV2DmEr0zK2|@SCdZYB23baj1yad6`?E|#bTC&j;lwItnaa0mjfP=BgxQ>zM +poOGEr82bCLKxCcV(5UZ8%1jB7YYK%h~LHK$DO?A}&{mYD%TWJ>iK8K~}lMh-98^{(FX??srZgTlT(JQygO3@HdiiWF=(>Lrg`^0R#74&c`5$$lo9V!@lDji0i+7@Vm;6&nEz*?%>QHU(e4KPgFnosVAg2i#1Lz +WkAb??JL*6F_cSs=R;ZL=h&9DHUNCfzAwcz^1l`JoO8@vzxw@4_{TU@?(CnCGxy;RpHvyf0Q;I4dw@E +25s^m +*1U-k9WsOruRJr!cf%wEB3ij#xxNBifTFt@@*_BgXX1Qnm}leX_F@CW^(0uda{!7Mi~%9sb1o#Eo1-( +e%mDJv_X2ge{aSlaGEGiG~p2C<_UO{w(Q6OsfM0cLJWGYp5Sd1u0JHYj|T|C5JYX)_f7>oOVaw^!9&? +euyPtJ3#V_Ii&at${gxE^S>GYD%t8G#ehI!zI%LW$6Yf5NZ!8iI4&r@55M9|22U9m$kM}=deicUHk%|ee +vD@k%KIf9eIyorS|h<_=NA+fMiErnwH +}Da0Xh|S|bXUqU5SiC|;fU&D!l9;KA>4!&fGC>Ygsl^Z1c;g5<7H+SH&zF|6EVF|x3~Wy<~{?`EPaL=VtRWEyG#d_J!e?|Zaqq=(JMi_D?xb57Jop>q^mHOS7EvjzlAFU0!ggEkU +$K>Xd8-);z7mwhYQ76#!#SG!@Yng+*S{3`F^cn9 +Fi|V|M?nb%G^u>zJceT-99q&gMR8)n1N!zs1#UBS1uY(SHX-(5g2PCr56rkn8jjt)j1tz-l2P-Ai!jv +dc2NwNlobhYh)jY$;x>yNC5Ywf#Xb(-rBf41_o;r2MLzux)VG7$Nc2l*!onDp8rB&qQ5rB4U#Th>Up# +X>-lo_)$ioX4ZKoMzJblM2~82y_T7!|&~ZlQZq;mowd$hwGA_Vi6OOM9*b>f9pJtPO$;GT}>1_nou#X +4va$5?6G}I$4Bv4v-!|B|cEUm`?fB>RXC3Zzw_wbl)l3@z_Ujt&32*5C^{_eHaGHI4fI6>+0JIq!H!q +AECD0l3FQRQGNkO%1%bT9=}jmtk9B_t`+c>)PdatlR2-&++tUaB6)LX1;4^5QPYCMep#~&h4t8EKw81v%2vCd(l_wZkLHlee18NC#@bsY1(6!!9d}B+4De$_$+B!Z*#I8Z% +oZoXlH)GX&>$)^gF9f{S_>wA+H88k+=nP6LH76gMpykO*q>E!iuVxCg)*ouhI=c+~?X8mgY+41Ez3`^_kug%Pw|TL#dS50L5KczK9 +v~7w>79~OUNf*}tK3p)F#56hpVLJ>n8JhBVX%b2tGY@LNEP*B-=+a6qR^J!q_eqt81z_GlH6MEA+J#M1ifD6{~PDG(0tcktS +*$oW1*aL6IX3LepP%^3G%y-zq&p=UZPkyt=~6Z;sRt9V^0Xv^LJ)Ba}jL!M9{o{5s$x +&*19KFp`y^>4m!#x{cQtvbruL7$zY5&*9q&o02^j7Z+Ak5_bLqd|PUV(l;$!wcx35`3uxcWhZz1heZf +y-6miNxuUdE4RABS7{|$M$lS)LI)&(x-9f6-#;oC_e*%LcHm&l5;5Ms4GyL%ra6Xq`?mcg!YQEUL*Je +i>A?;_5LJ_SNd@JXyQ@e;b(LMOd3?$)w@=|PiNX!VmMySC+}72#HmZ^~YJL|+))@pIVGUeiHy|O@%W~ +^T40ZL3=NbSo6;9)mpcX22sl1Ve_7EWHo?dWdNV=!bq1>x@CLi_YA3<13_RZon&HLPUONR`?O_R~81g +HY`WL6BxqNc`b9F06xB)yU8p`-7{$6sY!x#POIBWRW}QKK}Yh +YPa$~oXSPr(!qats2PAj$H(TY($vR+bb<#PVC;BaOYc}BEWYxZsCbg579rt?$d7~>;fv3Or0E=K2;RZ +&-Bvuv#5@gQkjU2xP9_St1ghz@fFMq7e==ozgqq7dytgia}|EVWLW5pGuRncfi)dqr4{`ReDHSPC<_8<)A~ze?FI?4WQzM5V6Cu +nx{NyTawk>CIbGGkuHh4@3gp=Kh^uPf +LF6`0!oo*VR6r{ACQw_6k09ZtO{^?nM(acHBzWO(k5LOO7AY`ORSr!%uy$(3iNBN`S8c9Zg`|cnyIxD +=^ALnIXzM!y=DsZNRBj1Vmf0K1g*!{9_f(mQ@ApE+$!+TPx2$1@_L@6)9?;KoP$?wc-Lal?vWyCv6xg +!v}xe8tVfz8rQd?i#|%_X%UZ&S-n-ZAJ3LY)4U^=X7ey+*lzRihkgH_FE90eHNI2;y=i`DJkJ?VH`3e7Z-4w>dK;ukY@{HTrZ +MH$Lfh{?q(h1am16q71qutZ*0{!s9x4SoHM9-ZBvr!t+X1j6Ilb$Igh2=LURE@@}yN#Y^rw@H%N49p9SBpDNTx?|2cde%PB5iEKGoM`q(ut#e +Ylnn5Zez82%_dzp!oo8t|^l50!7H4joe^4peR~q2%mF(vv#nE~42=*RlxN|ok&UjLZM-rne +87FQuu)r6bXZ)-t0L^}RRwIp$CRgp21Rm*$^sf6rzkCU7R3(*=2U%6%R8ut$GcO8wl@F_LYT1z<$%AI +=D|sBI3B>0u2&7uUPw(spbmJcBgr*Y8YpAuk)dbI5UU;Mx(%(rQONT!V4XHH$W+-(~QE6{rp%JdEKYy +eanyPhppdZ^p`*N}4LS4_PtUQtic?PNqtLp-}wAN^`xmB-ozgB~jF9)&gz>d7J=setDD?4ESS2Ds| +j1>)h0#S)72$~z+`I1Nt4A~_@v>`mWc(%?g=Z?JkSGEoKA^nGPBX3tOvc*wW0RwS9wwfdTYfjU<8vcT +F=Z7pR@&#j@h1=g-h=8^~X&71>#B`A$Tta^;kA|w#H$a0z5cTOw_q_}xf=aE)OudBgCWcy_&69)n)N= +P|Mj#kg*g?S_I>fSuVYRGbNZH-2>8R?JZ;~!65Y#*3n +5x8quV=0jaF|rq&C?J(8Af_PBU8ralS_i455dS)?K6%fl^T9!eS&jSoaV@`w+~u>R2*FR-9V)gNs2D? +`rsi{Sbi4aiPHd&^ndek0<}^Jk!fP^s3SzoyFSA05|u;R6@ +}9IOST1N56<3kMPntr9&HZIT((mnRU0WZL@U^~<{0V8c92vJ!+QF#o6|Ir7XU_D)#V`!;jAP5`ysvo} +2F!6VI)9_Bwyf|VHu&6p2lheg&JgAaO^&CMQNk6i{ZSE=&x^Z>HQ)SdQDPvWD|acd{G*!Uf81yS9+%p +)xk>@`fDgPj!hHpI|Vf5yi}n#y*gFhA-qWkCS7d`I*|xhhNiXb_cxKh589P#UY2GdqAh%}!Z6lt_$yZ +#uaie+*Vj3jzshR_Jdn^1n$jlAzg=?0Py+O|neIw^C2(45*VS9=0V>kskZX-Z4|P-qH5>k<7?nd1YM( +4eU&_ai&6gqp|;S<=yzpD<7U9fD=o#!GdH*fBc6Gd`P!2Gl!1ML|sXNYhK;M3GAEhg!fqZ6rm<|@CL4 +YUZwOkLhYW}Cu#^%Rd7RvynTXtxtA9Cs^`ii?GHPpZxs}>bx@YIMR_H=aERxZ`{WyH3JF-V5uS-wsxW +GJY86GgqE8Ttm=(6NrvZVaX8_io!V%jfMqgD6qFM&i8#u2LNCbfeXDSecrP{iR)I>qJKSb)rrW{xht% +aaGk`~=4wU?_;STDP*ovL4jfZel+*&b<&UTI?Ixk^Z{N|p9pA*3RTOaVyv%m|fF`lhyBCH2ss^yT$PU +!>QnkKw+^D5JsvYlm`IGip$HyYXgyS3X$}8P`RcDFY{(gh4%8MbT4rTRf5&!TGyhxX~sYp_)VJk+$fw +{s_+M!a}uNqemJdy}k-u4G0Uh@xh6Q;7LRMV+oDMn60MotQ#K?8cor;yLbq{(I>FLQb|YwYFA^>o23j}$(`G?5z!9@s+G1bmA}3Lmo{7?aq~z%vGu9tnMda2 +qMPp#v%l^`?F24Rb5K+s8m=2tpGtqvw&t2aJcTSB3u02+ps7wa2WSX}OWr@RV%UBVEr}K7pnX2z33w_5_75D8F?=XXVXFXIR_0?prM+>#^H@^B2D=kO~XtUQwYco$1|69AsPB_#D( +mIrjg0&9c{IDZy^3Q8{(6@YN$e0NUSW$)Z^>}~Q$ZtVt)u>=3?B;mPE9-xwy&3SQ9yG5kjnX1Q-L{bL +mY`igyl4{3W&1V;rE4`6|!CHf`)H3UlVkelb%JOgq+SkjZ*?9;bI!0*HkMOkTi|2|H$uuv^sA#C1!m#G=s +kw5ZdxAH7P$i)a6mZTk9nk@6hjT%gZlGsDRK$TWF%s4qORlQD9wRmi%$gj4uv*kVh3)L~b)FoGY?7>C +rENg~@ni*(&iKWB6lS-_i{N5{Aa3$3C4FY+|>NCXok8~M9?#sOF(%)Pvv4pq7{a~^= +(RoPeB6)4WkWI5Iw2x^}atu_?IN5cL50Mk4IsFB(-&ppz!c=2(BC*y+Pv}GV80n0etC!+*2)g81CW`3 +muzbx&S6fAU>aHDtN<-r0_R>YOT!R&9E#|b2Dxh`On2jik%#aj@TwxaRDnsveIPDQPnbBe$xAQwM(-cKsahYy&y?T<*;p81+h%_ddLVL*)=2QV@pd12aTWSMTYPmsaD3}$`4lO +a+iKn2*Oga-B|CMmYa(CX0mWKKpfQCp1Z1zuQDX{NUP%S@1>J(0z4HqI4>=Dl{1K# +Q2Euy9s;|Njn#H!nnw-|<`;#}RUGzur;>{w3)c}CdBrMjR7ZSPzqe_j);B*9ada~Mk_uN23VVKmgU|b<(n!JsIZi;Rdaq$=AI7LrUk1cghBjOG%`Cl?&(TJcCC8`F +rBV!K>UofMzU|lr9j#wk7=_N1T8)B)9ksN1U~&0&8j?jEy}8$qtZ5ig046Y&`sX#mn=%`?VqISuTRHJ +)Snx;%RU?E|05ES^Ia+7$#if-L+7?sV+Y>VAqlrF7(iB2X!GCG>TUYN$YZ{QYi2k>xH#)0CENuW9+9k +KnzLp;xEv^0J4evlSPYz0e=pMBz-Ya!H2}uQm6tuLKu?p#bjbVNK*l~g!UBdA&Fg}FNg76f*~0MOjKg ++2UJZt%Z-7;WN*RRDKryChJSZO7AW`AkE)5FZVrFak8wy^7&4$<=&8{GSX_V=S(FH4EX5b|Lz2dP(!D +f-3Dg>~BCru>NU|6&x(iFoISs50Rl`aj5>hx+;7LLJkmN9XJ%JYKx!&7nM+9N1d15dmEzFl<4+jf2w* +;YDa-w$8Tb$}|%TWR#I9;*`($E$3u?Fi{YLXeO4@mdeLScPcHQ#z+Z#bJIAe7?S+O& +*e~IfjR(Th|M{}x2}Mx7enkBa+5Xulc%3{M0aPNO*R>lG6uR5SbGQ(-2kw5eR8oc<7@`r)J~R$B!C&F +^0-m4^({rHj!RsgKP2&sC*kMB?@v(sZHJZeMDWF?;ZLmaaD3ms3Ln08@z?Z0>9+=5jsx*GK{_3NvoQl +%-oYk`QYtJ|C*UDTUd~lqNu5>9=#n)*cL9aut6p3P3@8|=G$g6ZWV9N73?|op5L^HVB5|>te_R*~M6d +`*VI0b+wh81CZ^BI8MrtDtDmhe>--hp;fAsfOIzy7gyx2o!NMe|vIDiZS{3BOY5JuMn16UX(x!wV>Yt +LMqK9jjgJ0NgL2tItnT!t~M8Q8`DA8T*3967S=`K|jc!p@?t+os4Vet&Gn@JVKoNwO1JtdCtV6J(-^1 +o8k#R%NY5GCjuFn9MXAnO4_KlF6iz7IS9ySFfNaQFw%VARNeKk?dplw)OdCM}V0CBErMN|L#~`JG(C@ +OOzlZbTs{X#@!@^^hrT8fLb?{@m#~Rnb&-h(p*Wk^x$f4pnJ^glTzmVV>13SOq$h1ndutDaNA%ZK5U= +EZEHlm*#2{y^*%wb;B6LQ;uZ^Q>?hw={8?2cr8(9>^L59s!+R^syO&_r_S73J3<#&z}Q3{tbaHm1@iNy}GRekG?ffKbGm?f>QCWj?80&dcIlW;azj$zU +r763t8))H~R#a67oxg?U+LRK0jfn_fl%TZ(=&RS17yQ`pxhB}|wM##W6A;GR}={v=k5`}BvcW(d&MHh +hFFxGdzPhR85wlisF3QuWG`TTA`ti+Ic~i5ZvFlYni8&PilJ&ri6XcAu|D(^Gqlu7Nf3-K-ITng>doH +9`|4;&mm9y^~bm%4AF(%;IJyD+cpTt|gzuGTu`*y(=4I2U}QgOp@Bgdm74xTHThWj{;ch+)+>3mJH_n +@vouDqzoVwv4%UZa?bcx-4dv8z@7yzFnE`MajYMEjca_eO>pNilW8BP5I>r#IGZkzNm@v>QxSc`$)u*f +@jpGqzJ99#wbkDK9ghGpw?Fx45Aty}-*lRySe_lLS3Iu;x<(0!Hx#pC=?CKQGP8F2py>k``N(5bqsP$ +|`duA7>t@?Ln=WCq3o{MhIBour{r-;n~n1p;do)7j>Hnm(nFe2PJ*s-RD*nV+l(B$xTL{H$i&N9{z{A +c)rZqHLuc`M9iXHXIF3Hk{6zM8cS#B?9tE26J62)8!INk!uh@YcGw)T&V^u6&2CYCYj@F0!d>MwYMvp +vplQo0fNxfS#7(&|A++lpw`v7p$SZ;=x%#-tc}$E4EGklBwIkX8VpOl;UC)2te@(md{Vo-d^1X#mq5< +%DsnIote@dY`*LfhN_sPKSOfmaB(vG;Wj-V&wkw>5O~WZH%z=35EB`Vm!>Xrp}$th+O~&A*ODL+|R!0c_4}ok~Eh^3&zSxL= +KT@NXu;XD2u8-`JH8$g5d4ZJM=$>WX)67-pQw@>T-`z}ldzz#G6+i4!qG=jFB~ZALe(OBG=KY(?6W!B +n=*2X0~-pL8Zy;A(>3e0wDf=sU12Zm(EtN>5qdL*8cC!&hpQI(;JhK8Xg!NB@G$zWf +OB=hMBlI>Xq$s)aT3h{>E8+FxNSQH9dXl%8>ZB+MRuB3+X$2?&H@DYBulOhSSaX19emY;upnm{JqLS; +$GS4zPKhnTj;kZznoH_aaG`Kt9J|krTgV^g(>GH7TOgqa9yu|VuJwQkZa5*8A% +(f=T#1KmR8MiZY!&>#jAhK{(-iFUZa0lhh>2zR9!fQ6Ge95Qci!G(+fq@gQ@xg@DwH7PeWXQ=eoc +XZf-$(CuZwaf0=(PXRRA#QCC3zJ@?(>fOR)SM8psQ^jfY7RAf(!SXM{} +==srnHyzeEZaDhC%8lF?g`Xn^*rZ5-UR8au$IwiAw(wSi1TphASAbzC~uHGMrUK(NRlj`Jk;tQWdC!f +>}%5%ylqhW18nNOON7v=!p7t@mCV6Ycvpw%DY-)s6-SSkt#{RY-LbgY-G|{= +Sq|i69K=NxFA$k8IE+R-qFP^aejEPrUpBZent2-fyAF+U6a{90^b^u9IPOLHV0BaIu0<(vw8at86;W; +Hb`mb8uI$4UQop%FBHLR(%3yoUC+;BMZIk`mCeP_4kxbGktRDKqscbw+WPsDyvg-42{s%5OL;nSUP)m +9wl3aO_eJUxChCWnE^T3?l5h&WeQk3l%L#Xcvh%GIkL`KA<0d`Ptm9A>#MWqG72mRpz&z1{&0_^C+Vl +Gf_&8aCRtz&_uc7P9}wB;8IOyUtqs+Cz#+xEn&I;C*AX4hE&+@?hCbTTm5a@tp6YpwT>|$iQjHKT9Dy +Sp0;M$F~F +)lxp`S^nyqMdB7^Pkl3)>$LuafVDK^3UO}G4(L#ohjYT`?ZNdyqAZo5y?m|*H;Uj*p +BYZ29`F*g^amjHNk%X_p@`$_tI(#V|eUVm9Wkc(#bdJBjL)RHhJC}H|2YkcSidN(Mt`S^o2P{=^X`iN +}g@+7k-!Wz0`k3=vDNFmB6TtN-cZC^r4X~(m5lS3L1H@#VsRu84PuBJa`u;0w4Ka>@G{UPCh2}{5%i+ +Uyt$j*`z2u=E&1dKPRro~ePtz8C5Mq^SsnB6DkcX0E|vvEdZnjjbq+7|$T7K(MQ2oln$HWR3Nt1~DYU +`c{pO`T7|8Lybe%Yya@2G&fSq@L0BAtjzrS?IkB9>yH2ky;wd#ewd({|kxCeUi?M%g0rTz9IviL*F)I +7|n%w_ZNm#a#1~;hZU7vG|;;p(Tnm5`;pb8L3WJ>r=%8~9Qy9{Nmb+J_psDyah?VdIZ{3Tr$|u~=V~= +8Osr}Pe8#n6nr~S_wWu_>wcS~JJgIqTtvUr;Ys@-Q&p>^fr_WZB3cy;Sf_Ne)nZ!bm2CUPXH8%r8z0w +Vd014O#vc+MvY^(}k1tAcDSx;D&6Q6`LFG?uwlYr*>vCQpEj*xU72f3Q-)+wN&+s=31kp11(a<9bxE^ +A1U{AyNOU6prg?}Z=?v4cX=zg+6kzceX-30S-6^+AUugi+fW`|AvJqT>Qm!T3>fT`x$nqX2?gd$q^{l +EG+G^lS-*&4)PIU(O#K%_SfaEWqz1z&1U;y1Twbg=m+o#>1T4PLF3xrNui-0sQ7s=R~ +>i}zw_keURpTVqCR>Uw01p~f-2CdR1bzJcPtkE%scIS{PxSpm_Z9z;^WvWa{WhnoUY+VRQ;W9F3Js2e +lGnG3I0#da=Kv`YtNFd?Dnk6wH8B1I@I;R{amo^ZTd|e1gxB|~)e*}reO@O=s))1|xT*7&pK9(V{hcr +FOMPP+2Uy?AorB~Ph8=(OSS8Q?XMBY1PuhD0YHb@N${q7*xu|;5I@g&m+{pz$}MOtn^L_jJQ4>H}Bh0 +YI8V6w%RYTvlnmfNgTn9Bd6l=Oh6dKi$8_I!S@D#b5U>`Qu^W&MCX9yd5D&Mb?csK3l#}}9qLRfA3THFmK6C58?(PZ#62)A +o9|l8l3fWEo1X0aeqwYAJRqftb!86fs0qJ9g*`jRPBtQU3cXaH&q5OD2%9rGERYBm3j>RVEo=^7rk_K +j;L1B2P`e30~1EM!$k__gR5fK3?V01O)+Pu6BI^$;7rcKV&%}`sYRZt?phHAC3a&SOWm$Pt~X-AztfD +8puPnR2z;$^6P;Km(}uqL@-|Ax{Q|6XT!fskVh>jIF2-8OAMxB>EXQKdGUy{4Milni!X>roC!_!2@w- +U3x(1l`oT0jXc2qHbdhO{vuMA{V(q59{ZKxl%*7;X} +?Uu~e_^IP?R6g1}4OIZtQo8AJH`BDqhnyLlCo-KlLXZ&AtSu<$d(CB~^H5WzsEl;NpsoLnY0Nb8&Sxt +J!c>$?wF49$7og1NRq>{8oZMf=1WlC?g&>2itS(DZ?o$yd?=UNbUtLJ7W2}nkBE{g!dtc?drJ#kH}p- +*0b5*A)m9wWGWm4GBPFQ^w4kccK2z`pp&qZ=5f~6i-S>~w +FG-W7YMU9s{UQ*Ww2yB_$wwbp-@~^gy9`M4;@vqCZUwB#@muSS1f*v1$7gn<2*4|j`K*Wc(*THobSz% ++sBB5&CNr=F#9HEJejLD&mmb#u0*N&>FZ{?0ZGCDDT`enBlIs0d+~AKcM&6f;mD*TdA{cv3=+A_eC9G +IyTvDQ2NI83(XKABAD1uF-X5ruZ?iTXEI*#XeCca#c9t75K$DTtsveq#{1e;5H1SET@wUgwV1Xo=PtR +Y%oa0Y|UhhGvCuD;g$Kth*TS%ftyty~b^O~Tr@RV&di9JxTXP58=7f)_A9)GvLszEl9#&WwT7|JsfLf +ZqK;57uySDQm1@q(B(rI~G!?jDqD|Iz_+qNN935Z|{2jJbI{eFe1TE=0fMCBTb4Ig!VxKJo{L&h^t(s +m5$icdzcmc1WnxTCsj&Zf|EX(Wy%6X5QZAHIc>8%ZjZOuuKpxOiL3Gz!uWFR44%nRL;tW()TD@Pk~_LhXg7gCg;n73S57f2tyl0Y{8-3`TwBRRuHge==_2R;K|G^p9pRsQtv +k(zA1@nKth$5EbSywfkhS--ojLpN8)b)X;Okx4Ywi-Gc^Se2u)3%X0<*n?0+rVg%p|9R07z0ux{BPjf +(f9H7ONrxqIPhA^aJ?aX!h?XJtNZbi6Hr4I1Fi{D8zK!^t1**azs|0yKtJPuUL-vt(*ukqZF{Os+xmK +^tEUo%+9g(39AtShZQoEp+ZEiA;tPLTF8$N(-zVCjO$i=KERo3)Hu5Kj>yj2uN6xOlGp0lxCqocxFbi +`jYUn{XsB`J?sI3I=??MFJBzb4(lKUP5s76xK7?F~3P?66Mmi23YT?BE89%fBf%-`UBq +MfBly{|F8c-Ea(H_gYW4n71t0qRj*(ZZsShb^~?ykYV+?E8r&P|@t^xo5#k4=S@|oH?0_^a(U4rdu!$ +ZDqMimPAQ6kFtj}*US4LTrw)Ok$hmMb4^u*l3QCs3jlIg{OXNd?%(*mhN!}7t?_Ai3a1cywlL;S*&EC +H!mKp7lJW%sAm(=R6f4aml@_-xI3U`&3!z`#!MQz*+Q?1V=Jq+$8l0cUV*HC*a(*H@`A_AGQ>c@nCqZ +#l6ZJ^;Q)Z)4Y|cTyKi-_nh`^8%?;)SR776DX-3W(I^N2;+L}95@H0Qu$PtQ=10^VV!|+Y*TcDU!kU3 +VSu@6Z5frpRTSErrlEV&a2=_p&hDn--_i_{Ph%(39YB9&KDH?*GkGH?P#h@HAds9Ym_fO8PsMwWCqPw +co`YFpEJ=iQe>i-_G1sxJoL50jZ#BFjYz06dt^E@*`M##gNYN)u1j3v*tvlQywMv*(X+!z65LmOFw}j +mWq*nP@K)$FLtc(eQFf@atGpq-mqYRzO;{e_7ZN|r~tZF+x0MnKJ8P88!aL4pA(R +mSCXN};40tj@9jik;Xzm1$FARd)mHYP<(0ZAm;>_0lPL@hpHBZ=nq6l=TQ-8+I6wFeP3-E~he)#lxyZ +5Qf&yRVVFmdEQm**<3Hx1C?zt(7CmApmJh>SsZI%TEoiu%2-Bds-6z_XhSe=~#H_v%M%9HFiOTST6Qnm +%u2}|lsz@$KV@$o20Pp+VdvffCn>K3q~{~u0FMYS_3zv+5#tP(ZrAn0@e&`4$G;mY-j`kYqU!lMcV)&1FY5FD^^Ytl +IX1kWEdN$jjjdPXURGpkcwT_I+$Bo>ly^ogWZ|1Zg@zsOz)H-J{uGdsRaR4WNGz~LP;>S%t3x|$Mt*0 +u4D$m`9)w;1p~B+gF9~u$xLpQ^_XEHT?0;8UDn>b`yc9wGH|kboS*Cii2w4xXx +t@(06BMvE$5z3f7MLB$z~(c3(w>auLRqpVlVc10qgfK4;GW2<*#?QX#5xkG%)_=yh=O;`3z#Q@sIU=mAgl#J|CL`W@@O=gM9yD9PafYq`(jt6)Fg##aGRF_e +zsNJeG59vdTc+qQx%vzz!V7<6nG5}N3Y59E0==^xryFCMHxnvxqxt#hs>sp +>)+5Sqk=gd@kfREwb+xN6(D6iLJ`;I-urZ3b0d6)GTk$zMpl7?8*$l;C29+&iYW-4rBT#zW0B-UXJ20|D`kN+Eo{mt8Iam!FVwXV)GtXE3Ivg|&eKVz=Hi;!Af!AQCKSN|u4JfE +-^(fbO +s_0>NQDJ6$Bz*i>Hhd{2hxo{Gv&3TLlDMCIhi$40?+~WQWOXcCDll2~&*pPzGoVtvSlJ`6LIS`lw(kTWDSS&*s?(NG9S1qrr7zq@M^IrJn&wMq +rhADD?`~Aei+G<`MR|W2=>Kbe;*+C{kHc{B+H-N0Je3PlZ^K(!!<@L*fxNf{T)5g9cbDTkCg*H34*4j +Tu!4;IB5xM$Tn*mlfKFHAWDcbj1*N5pFE5GFuK^Us_U|#bEhNMx^ZcC`XZ5-GHyJ(V;s+!^u5PARpQQ-GP(I~pC__@7KzPbGi1swPegb#&g(NrmE2obkDN($YRhU#Hwjm_k!VQrj1*osDs8XVryRt?UzlObtDhAG +snMz@TCjb7G}q#{XLWMyS^-3X}iW;RYpDv{Ux2LDyA=FpZ;A)7s&4qTwxM|~H4Xsf6+%T-fj(3t@y7= +p6_b^n-KB_s{Wc?pTY3+-?v5XKkVy%%Ue&F^YkIvatFNs^9?W@$+o5L)Ow5z>y_Wo4b^CL3xC+Y_TGW +*zVVB)V{xnCN2D+f7=L;Cei63lu6?U%M(Kafq&&jQ^PdyS2SvgX!z8)WAqxomI>} +T3zwaThelAN463=-zz7$v`yI^an8O43lxjz@1HIdi5)hq%J=KR>1a@bz@yILOwMXe{ABHBl1=o-YNfL +57h;1CfFPGbc?HCFUy1N%w1sak#vd&mi!ZxZJqVsp}-8I+%%P7HLO?mgrp65eNo$xv> +~@z+->bofHf6dL(+Sk-ORF_?%vrJQs1H>nTOiNK3r468i1{9wjt^LnZr#bMEbw|=ta>sA;~+G$=RT4e +qz9?=HK-%j=?6$`G6wG5%^&QCf8zJJ#vfA%XvB-Eo)&x0I`%IAxS>2viV(lX||#c@B~>QsXorDrkz^O +-E5E{i9N2;=12Bu?oA7wK}A9jW!@=clj5Zf^qR()PO!X_`J|kysj>w&aWX|@NFtACPBTN&c3dP*=ng> +VOOUeTr_op;={usY5O7N?nuc|AB1t^daJB9p1zU1c4Wy7H9=GXyQYMQZ;5;;7570%)|Afs5q;~#k3BC +;rJ)cHMa*i)$Ba=zF04-??J-No3UxfLyBQG3XUk{a?FD-!8FQrWY6bK@zqHq449CXV1Id(nNHS-whF&JnmUA +FQ|fNDLBRCi86CU_NIA)(#H_gXla(GvGx?zY_50dal(ZusE%=}X?y(|>c%;8g{8LR$$%C*!U6F}Op6*zY)4NLb>lRXNR-$&$=v3&JGZ+xX&>0I^Dand +^G(8U)e0SCI52ah??o2A^19JD)@A$J%@-6_TRFE2dIy3QW{Jf*T~>$z}ZC{v-cRdp}(;4{Zs%7o=hhN +m26r6){k&o`3;RG`d<9fjfD0LW)BYm3Wso=bk-x*06p?J+xbyyKG!#cP3AX0%52#Ig*6LB=Cr{TKyYA +7!t`OQ(rP;+L(|^o`7C>R7zdt)dkkn42X~4vLY|>uoZzd!}=yS&dyE&&FY<%ilio}=+ckgD!SImFG3Q +XNH@wEpq8wadDTBBH_>OC-(pKs=Yk|L8Q~T{pUDuoN#NAtE@Tv5Or!unT=WGHH2O*46A>^B4A%GT#`sz|ZESgLLX`E0Y{Rd +O8@2u)+w+Kdy~G^I`CY=L%p?g3wTWPM*%Q^~A>OsN(qZ))G+ZsjK5pEA9LO!HKOAhH_~XLSDdAuFgVU +U#-5Im+4f(2FjkF;tMyAczKmr_5K6_3_Aul}2~NUsGRX5KRQR61ls3C&7vLC~NsB_4?&P=e;Avi8BA1YPEOx9`gqoU=0smCkYR&kmTk}?Ziy|1%x%G5y?otf-; +!yqJhr7m}DeZ*%Z<%nwM0A0IJ!B^w`GCTaB`Cp?~5+%90P6vSsV#xtc{X0-*^6%xjEf(wSVQMfp85kp +u{CTtRR1cWeVw)Xb{34q(+j2FV?g9G&x^FddUXce${4+O7LQxLOCwM)k=h!P>XNmLa +T}E{t>glr8*tT?poO+1$}6%7g(NwN%W~>nei>szUxdzpBAv-swZvDWbW%R3F`_{rbt}N~KV?SBjW-;O +=#4YLXF5`ygz2}G>O8m5`%b92@~*CvXmks4-x>r_|IHzh$%~Ah{v9&|#~&p|$SCUwKoIGinAJc2S2bg +}5eOm4PhM`ZkO1W>%i9QV18n4z%;YbGYlS2`87(2uywDOT0xO7WvzGR%PJ|>xNz~37UIb+m2p9O)g+w +SgyJqTY69&Ds|KW+Pzx~1!B2u5A*2ZYd|3=Cid)Dk!7L>0$>l@Z-V6n-No+Hlz{hBp|#A^9;YuI39YClm)}dOTZq7#y)sXINK% +&1nKBxRn^GAl0fNvZf~=b4&QF3GuASpp61Gh0tgunluCZ$ON@Uj}#miGx8vqs{YcFCj^JhlFhd3ob_PYme7{zQqC-<{@lGb4 +2m_maOY84V0h|_kD%)S>-V +HG)a&$n9^%&$`sH0zYCX`kN(HeM>3BL^qL-{775?mQArP8+hGo!(yg(|%s*>cjH^72IY{8R67Ok +`oV7*I)lqbXNPni4~7zPYxYvvu&phQcuiotVZAi42aOU%Op?}MM^S)I*)3G`?dhzcHI^7u1r3&7p3Ed +Upi8YOAu%-S>tdiTtXmy63pE%nNdh9pP|gr#XWB}GV*lrT*fI`%o$)_%GWNPQ!VWGU&~aTllat$rGqF +tjNid#B!=tjHnhPM&o_ljKBgX$RNikNze2vkVAB<4@iw;`!~v&;~b_Fy~1OkU&`X15pJ(|E30-EM&fF +7%1!Tyv%i?-rf(!Ds#~|3dU&j7+9C!BP0z@|in>?DNJ@M|8o!nqn00T}|fSTVBX1$UO#Y? +Nw1nYWF4hWPf_MMg_L{Zk;4|~LFpz{`x3?*DHa_tD23=;>$FzNVhqE~N6NP!ZKFY)k2t?mkhB@;<}DV +P14Zy||NhVn_xN2UvcdzPc8qubO>Pn~Irv?^Xb&s#sB0@miAB5n*Lr$pU6Q?po7-nSqikeHi}@VMvR>#*nA#hxnSMcarr+EoAw*RnjAL?jR{ry!OR};>0;?f+;3$0yW=EAV=H;Kv;6CVuV(Xl4f +)NAOE>3A0g{E(3bdB*iV*f%wQsie6HRVk#1k`FNUWvOIfUzUuSBxNgMTTJ{0ED!g#9#HT4Efh%LGTHO9V~oE8qOU8W-vx_n4gt5pz;Z<7>30I_T +g@3vV_)B?zAoRX@?0BdE2-j{Tie^7G(nByAMi-FAcfe|TNhIfBD0mXNJvL^d4N+27NhUHT2H&MDN2>8 +v5NXTLy<)^f6rgsPeNCzYhf@FQLbwsKbU&?7(nH+xz!jP=8<-Gq_&4{|rK{rX|f;6yJX24wTP1@q;ms +@*c!ix*2TM(oRuzH@f;leI~OVOlUV)9Rc&|PmL62<(L{CiO=mubJ^FFd!6EezXF${oG8YtqA9V)i5hR +HaG%#*QX4A`Q*$GM~sIE-Q6$7g$p>R&Lv!n?UHfPNSVR+IeaPjRsgNo48UwP%#Vx8+>UIsbi9>3#WMG +)kW`-?ASUHkhVar2!YHfhk>CrGhIY_7+m@>=LP|D%|;}P@nOS@o^k@#4C6XySs^13TQ61y1k$=SDI!5 +k;@!-!a1k5|2}`W)F=ex<=W>Ck!~|=9CJG%r9|V0@*O%940K(Fs8(SNZ9>&jpq}83gTu#!GKxkr(F>E +}lz#nNf26Xrm*0SmU;K<{4I8 +Y;@TnGLm5Oc)TF)^yl!(=0D*wd1I3OtXx%MlKhTBIfKvIF!ZmUQU`NfUs1tFsq1^F>#vdOw(BL(!iS6 +m?ScgYO>Czx*)WL4a*McWU|t6kr?Q_n52Ul=!!pPV~ao)eYZr0|I417sy_^>Ewu)w(CQ!mQ`yOX5s_* +pxK;~9QZLH9t`Ob{z->_re&8RGcn0c3fzVaIBmi3W+P)=gqZlJn(!}MmYG!c-h7^J@)JcN9c@r3s&c^ +p5Z;|I&Ivo@MZOJY3sZ-h0wMioq+6>d`hppTU(2H`2NNEG<;_*QaT;S>zadGb&GNiZpW0}>q&bI+Kv0 +OyDnycvJ==$b78ta*gpf?goQ*)kH%Vvqv4+EVrn{}q6Ex8hX9DARl(M1INQh+T(ombH6YF>Jd5RtOxN +=S?GlB_8u+?Y|fV16!sEV@+q?*fF3pkTt_6hiI!k|&lg|-GGKOxEvmAtYO60opcm=tmUW;Y +&&;?z%w%jpKc#o9H4d?rES?}mph@CwfVK0<}WpPvvt4fW>OmGLkoQSuy5BdonLBLs!f4SHl+#7M!hrL62ogT9&l +HEMCzRVBK_Quv8Mrr1a2&RL>e7s4b9gTWf36Co|8kn;gf&MdHVG+~nU^#2;lJ%gL8^c$YCDiB27N8;NgG1mXBfi*H?Y2Xb^aX&Au0JaPX +@F~{|5IH*8`iB0PT_SY$FvjRXXbUcNrg0MO472HdR@f4E8rD5cGnz)966+HL5F0&&5h-@8BU70Pjw7> +Y-0@pSDxIsc%(L+zz=GZeSi4-JLZWn~UwZRtC0R>8*sJB*7+oinK9Gy_qk_NOASb~tus%P +PNHjc2l$5(rIdLUxZ`_-G^%qs5WLi`QLJty>yPQktB0~)nOrOgC +Y5jRQ3aG#SnU8(Oww$E&XJJMx<#*&{zO)hqh`b9+^ZWccyyrit4NCz +Dl>#c$rjjGAuWzKP4mn;uK`#iR7YEH1~}uGxkgHxn-Ye(Nk2r;I(F8ZdZfUCa%DR54wiZVp{W;>-F*t +A$?-FNmX1%sz}nR15h-+{A}tH5&7@&%n0sV(IdGDq({W8w0->p{`Toobb097yYfLmEf?Kk*v%@8UPOK +*5{f`&8tpl*Op8Ax}fF#ZtdV#$#fRo?jw~rUU_j+OM+boOKP1eq{Hf@*sRKUlbgwfB&AVs9FHSjd>sK+>MDoG_?ylslhf +(@NDrK)r@;z&K*MGc +JjSzHpBE3%=l~Fggr@_ISO;8^KuJ(XpAzG_0uVsG?}OVP$Z!+>nqUQE1p;W@?+spT +Q6AeN)yO9-OJu+nDe6v!q~96J2`qhPmP1%SrypDKK|Nm=2m#hQ2zL$Syum^`lLxZ~`U*|I>2~QN5Kw<0_fNQm!0p2dD52iY>~?%T+XBg0W@ +caJ|yYUc{CujM(=wp@Ck{uJWw4`EoI6~fchr}geGM}FS{lNiFn54f`YGw1$)@g8HFU^dGSk6`W>}(&2 +U+0To4WFYTW80(TN=@jWp?JA!bQ|AbPOx>AOy>eMB;ztB*r(?2Ud6O-w9+0P2q%((QQTvyZoSRT`g(k +GJdJY?AH_Y}`8Km7lRzIARjNt+=VcjqZX7j1ab8(s-#2s0(h5N_JaGtpmxE!HEKajutTN5uRPz+x(Dn=cPKQ$g^P +wOjf&jw$ovlud|5^;Su9mpA>!ksp*AjRBH__GE==$T0w(S%ML&?GuzukIcFΜJ~}))+JC=uaP +T|3LO@F@eyuUiuqZh0dp>)m$ssik|sf`>kXxo9HZK1WwK|J~fbF +=Yv`gZEjfvzMA;08=!i~ex%W@aRVT*mN-NnArmo`;WNORVR-d?0jA_AEz)lULX*79Q!32dnaut6+$PP +nGybMBfUbEb@3(dy_J6pCHH_{StVFtk%y(Z|Sgb#)K^k)uHQtyn?d(@-oqG< +rGKDzfsLySe6`dNd$3_Mp6HX;>Il7S|})E7js)dn50VgClltYE`Lei=C+zCeKCQjJ9Y{&I_rls)PhG^ +N^pKS5q}7uH1fkkIF|yqDjvAl4w41_Y8?*_bh4YtDcW2u|jq){+ID3?O0sNX$?*OPOo;0Kre31*HC&% +^R>!bh!cmVW~SPN$&IWBB2q9d!jnaTF0RR)`p~`rF9Ni=uDorqKqsc?NK(l%KTf%;3eavcYR-daSYUk* +9ckjJgqW0O<0;_SpFO{LIk5dVr@hjLeHGUGOMs-*KY+2e|q#`0kP;fmQQYgKKEh2tsNV=fGB5NmL15P +Va0{uOc<=?X(T3Yr?jj^@Dd@9Vx50B}e8S9FvKB^z-d{yV=x7EM}Jy2H7(pGw0J!T?PX4pKFRr!rw$q +cxgU>sfAECm3)g_{IJm)Wc=9)b#l*mk}&9}4s7+L@B1M4z&Jt~UL~L5|SzBy~@QNKbSpAt@D$MB<{CIxa#RTi4tx1KxPy5!ObAlCJ1Oc%e3*SyA5KC%=5y4%LVtw(^fPk2th>XDbxH8=zueYILb +RN(0(a)(67(Ph9Eq~+7Vnwjw=^SrENEFpI-f-tn|I@n1;G=v +#UV$_5M?oN&D$Z2LN&9NGuu@Oizbggckts-;11!bL`qvUSP12|}Z@4v|;9y{xwC +Gl<_mgU48Q4%gnsvtb?R=TiTc=^GU91ANMoqi56(Z6RRpp(sG5%cUnQRb*A=Vf*tifJ;_O4)Uz1xWNL +y!w+B5i$24f5aCtfd)6^|HKAv+r;w*Ql8q-Qmus&(A-k9~vlom@0Kx=)C2m8hYuH6WN%bNtRCKbfE5# +1px#GUZ_J%!G@ZEjhD8Cw`7fn%R{NWAiXecVi`XK$(Muz8VjJx(s|U +5L-RV6u0c&{ll1i!(DTu<0B+S&3*5nT2kRi|xm4)(~BJcXgTDWrKjNgH|LY=Xbiam(IRMz +-Hmj1QgpCR9KD7n}tB0<8|vOsfCoaotJ0wN&fhe84yU;;%$4y@86KqDt>RXy_;qECSTST+I050n=U!F +)p7hNxW;c{ppEtrR1K{2;>Pb^R8Bo6AyG0>ld1`N&J17(bjH0m@p~ISr>1K)WqXTSSP+U>+e5OuQky% +G;2m>w#3UaImrpVWHJus`1_TmojEO{k$h#76qk&fU`YTJs+YLyFNl`S+z^GLg!DN&=pkDo^?n&9>_jb +^L^g-sZk+)ZV-^NYqLEE>gaybhcLw!|6-c&nkD))-EjgbTgzhS{#uKb*KP1-*9>(;=#{o-9)NXz +CggthEkeQVU(I)T0j5;xT*73m+KTA$PMdOcwM@Qcje0u_2&6STs+dGYo_gtJG`~xy)6{@KnzqBg +yOoGZYxLU0ZZWBio=Y4Nlg#Km#P~&>ZziN@#*Gi=gf3m#>^~ZQYGcNvIZ9v)fJ4Lp4VZqlzj(*yU6^K +9vRT&VaEIWx_J5kti;mXecXp`&HHuHN%A`pdgwnojF>ZcxCQkjp$R9`ek%S`jw5nPL(iU9M`a^D@Ug3Yh0FCb>~4Cn62G8d|Z3u)=;0F5j$16#Yt_GvQ_;Xf#OS7?oE +n(1R9(>;G9Fs8VOXk`1tqO!!tc?|9J#mX9=kiWol}|d&PYXTMIVK6xr-6Tc6`H^m1UHO@vPg7aZClDG +y0X9H=mq@BlOX0LKUcolC=Zt@>XThW2s0Q$q{D|N?vHjl +~Vtdc8zE>B=j=~;U->?X3hD~s{S3oe8Utf?7N)2x63M>vfbV9k!uwrXlMD(hJmvP1E +jU%*8aj{GFpo#nLadEWz~XBwBc^PV-g&FO^d3my@ihUF`)h~Z;6#ouCM9zP6F7oJOM`{PA+v+SNzTX4 +tF5@ja?aK<%?;XModHRTqY(_5=0kNrpC7*V9m_9y70bubn>EMeQn5?bVvXCAOH3LQUCWps(=3PfBoP7 +_5b{T|2zCh{LTH4e|s~s)Z9k}$_0U&|2!rE(s@~BKPbx~!ZtVD_H9z2^vOnc8(`b!m{dkXsotVlrmJQ +;u-@F*w`OpO +c_AR5>#AB#y?H2&Uor7k`s9lK5AVR0Gm^R +#Uvj(&mOc3T1Vo!ur>}aKCoHnp3v0-nQ<(@ep+U0+&{?`_ +B120J+eV-gEpK@b7EtmI0xt_oq;S7muXX>m +l&Vl1gZJGt}0?P4$oe^aIEk4??rqu&T+q>!XiJI;6)W=EB1P+GfThtBCc(c+^qSK74=;=g%=|i@d6~A +us^zo;_)a>P(qCB}T^~ux99Llhxz0MwyraEfZr+X%+cV$ob?)@O779d-CI*u3z;|z^6Nh$A`xUtg9It +-?E}!(xX7|_%Fm{@%dNzXPOVoX<8`|nuJE1mYx3$^h}qFMOxv{FtE1rcuZQMHzh}iNi+094}VO8Ano- +y(>2_M1FWSPAAgkJ;6Gyu0t2jBeOP3N#YX~xm-{%r~%gM@G +T{xVp8}dvLS!8u)y4dB=H$Sj)B^o2ongC;Fzj`^849J<7Q=@!uxGu{o8H}?m)V^-67^4L}2ZFteH8sC +zt@vc_wR(zGH|uAm0!{cgY|@&+f2Y4Hc{nKaNSJGPlqGJ!3IHSvcQtJ0K!n; +Fd&i6+ol0hYEc>)0}$;5&6?+3|rZmJY&XKPOrfvd2EOlV+zqR5F|fdo7{QeH2&8aj(_|Lg9*YStp +HPEtZ8Or2y>SYYiAv8dEpTEJJ(r?>$63w0J-E74KI|C=;F7{U4Zy;qo|I@eRFw(Js413VY>+5Xm0>eS +p~c>l27J9v*tzLS*ctjJSy>k(X+-==!-G0Ayeyke7}XQ1}>V~--h4Dd=juVkHUz3Xh6G_py<%z)4YbnaND1^8)A7cn5IRen#2eFjE99s3#;h3zn@;p?nm|+=1Aa~1<*ycMFFJ; +}y*Yv~K>*`Z!_e=e=^%`%X_kgH~PnE{X4lpwvLH9?8*7-YK6{&YK5Eg_Y);6-5Axb*+3(r7>j=lR*?|A<1(nFaWh-hIwv-i6#iIbPihwq$28@=7?+Q$lKruGjr?8NV!`&a3iy~LMB_}(rIX5bE?Fb=N&5qP< +_+MUfcGYCk`X1cDj#J&)(M^vgeH2i+{nn7L_|^kBzN={?3>iEwIdLp)LbNsX`0t}WiB6B=lxd8utL+g +g}w*Fv)O^O(K1uNyI9FqSJL(|5;*`*cJ_nFI#Pg0@u)8bDNIHnK!M;nMttTgGl0|A?PZx%sK& +UM*E^lP5oAm1JDV#iAi7tBVOLp5U(C|!KmYqf|9TrDD-pGlascjK5h0 +Nj)(F_KqRa%DYT0MDhAh)It0hLF10e?_hB*lLtyHnGKLG_XeKuU3P)$Z9B^IwBB2_9%vR!UXlJlu|4R +1l9_ZD=gH;AvM`i5Cr8)X*FBDyX2$47Bat@sZ|3DHCjI@kO*n~t%NMoCZgQH&-`+vMEa{Himfjsi#yq +1(Od&<(cE?&V?ATqqb4hwst|rdyR~yz;f7t1E-APUqvT66j)pMPwb0)UkSb|Vgjqnjfe=`;UGm_o$@L +OSB{c}b(56Tf5+lu59~bsO!2%n3BtG(@(ZIV5ykRu*5QHINen%p@KLULjxtfq|aU28u$buwDU_NI>b3 +TrYoeLmE~6`*`?H0PVM%hMmuIVvNn{j% +cN`@#J4EO&o}@JLCX!w*(>?RIvI!2g&lk-757Q +c`W0f@E5#YZltR-3JXH3~_Yn^utXSp7l+0?Djzw`!WHvh=R*f1}725h~%XXFgGd0b +D`obywv<^+!lNJ=s@>uF;3^t@0T7lt5!fU;3UC%T`vyIW_h!<_9wDjsRXbXfi2-uEVo4P!JvJ1|2E(Wo5dd`F2+|A%a$d{EXebc`Dk}fme=onOF)VZW$U1s9S}o +KigeNl%aQOF=OmT5!i+R`647`tb%LYJL(z_*{b8QO?!)3)pn;U7$E`*7IvSLbw9SM?!$68_>9(3@77BoLbT-@!F(Wg>hr2Q~t;Sgpor2?DS)SgLA-cfz2KVLQ( +r99f<%~P +15>OnDWfgRy&-)+ln5q)o<-jYpA%&uhyW423;Bq<(~}b?*(e6|>siJ|_p_a|6Wk67x!S&=In +H&!zM6-p`>`Y8p=`jn$+eRj_;xF~zWg{ITwl3a-ZnbK4o?quoG%X6NRmT=O9Y`wH$6J_yte66_EhQ|; +!v&`z+@Ef#Flja+RO&f!))CKn23uqMxjQ|VolyQ6-A38s;UQ{3zQv)*a +syeH>-j+d>S&-a4<_hz@wKRs~13%Ep5299^aKaHHX|8pkGd4`S+1Kl&vptMUYZ1COe)M|aHQRLG?cV1 +FEac9T`Sv@H^xM|XK4`tIjZyub~q4*-vU?uNXVqsSiwSGj-->U{4Wc7L*ZOdISQhHc-g^qYO;0Gl4Qx&Oh@>7nzofN(8wK_u8Jc7BR72 +0#vhG5;FY3AkEtbViBJ)$2?N%E+0N=!mq??Q^JX6<5s)>n2rsHiy|=S6z5~y1Vy^Ck??XZS#+dv +c|i+&_KsyJmtFT73{v^kDRdY^cEvZU1HC9BdsnT4BS>E-x|2TX@?ds{~3CxJI)jEOiWDyOOgd#plBUb +U0%87svakSJG3WlT}K1Dm4ABj%Ik)w9-A`qT!)f6K+T*BUoRGTtHDD9=JFJx`r%``>AiQ}Y>3+77;?i +(58g1FU4YZRJ0D13dr9dyzJ$J+ +~LmL6#V)LQr=G>L7!df)Q18;eZJTNz5I~W)_dJ;@J^^vHQuSLz$ym1kv4HEQGJ*uiLEtp@v<$OJp`c3(y{I1S=`JF#GxI +^wf8cC*)nmsR(+*(Rik8#A?JMrFf$);szju1#d@nq~m-$&YsQ4RzAMY&yGJ>pqR+Q!je03jsoDwTxo-*!V1p57`ptj41ylIghTss +((P|FYx2ec7hfqbD15wjCmk%aGEDnFfB*)BB8!J*>I-gK6MN(A#FLmQ2@?^F*R09!zi6Cmt6(A{&3T@Z!U017l8FSn;YK>LhETUyMNQ +;hDfMV;!uL16!4+x~>wUzkMFW(SF|F*}3l>rvlvcIyR$8wk}P5W)56=6d +95H!S=Y+$))$KJ_#upG`JY1~|JNV_g=!iP`tMktU3=g|+W>$X`hHa?G*?!d6*q0gAK@Rcd=i_T^a0r} +pJgpxc}*tHED?-+8Y7^8Q}t +B?BY%S55cXbbpx^C2fBZ)^C4*`?N$0Tj%&_-v@ok*mWPP?%%R1g@0SXi>jFyCUU?0-Kn8zuCVjc4ZLc +8aC+q2@IT7xsl*A|Kxcn;)1;&g)15u%H5)H3jYSBr*?7_MY&!3m>3n#)=X6eV*c5g)(E2kw7jb3?b)6 +&TulZxC-pw@E{y~Dax~7xy)BP&PYNzUqtmU5&JOuTg0tBH+KgYD1WRy&)_XewLoiNY0`Uc+{1{eUWT6 +(8`;F@d?vj!}!Q9O2`SA0|VjCNkTaaY|S%+u+XRrO>BA`)-ufSceI&Qj@!*DSi~Dr#>Lz2T3m4aAGKB +)VznyPl4w{FeRT(5_$mr#m~R{%f|Ts>klB?`{oY*ZX%hnBwI_3qsK{TQ}Kl(>45mn%_}fZ%=I%Up{bm +_D_!;KSMGa+m{@`ageLFYLFki=|D^Ob-g{~Doa|gR|WAQZ9=uq$lL)v{L3Es&hhEtfxAnpQe)PSo!HF +w(+#Vay3UCj9B)~@bXTLyH1@(!R&z?AZlP^wP2_QfL8)z13!+zrg!)+v`aE4U@_~~15!e)i<1gD-_KkX4<5^9+b#o(Ea61hMq&C8XI#~<)M%j^*43l;fN +L?YE+O`(6GHzsr(VrIDhSoqHgwf~Ec&=)IowBy`(+O%b>6T4qgGpIHawiP}$Ygh-#@0mkiuzBRxwxms3|B>5VwK);Q +grcQh9)+B>X(@k2{C4tZc-|{?_c{6iEcIX0j$c)mb-0>Vd<(;o*hlVj2#!~$WYdvWCse`e-SUXtqa`I +SzN@b-3KLLcH+#bnMCp%L7fBX+4U(Sj;o6J&8!eRw8AZ*8|mmMW0-bB`oS~*&9Oea^3Nxkh5NMzOqZ& +~P0BD#+q4d6lhs{Z++o}C5(BmmW2(+A{ywqCnAr_0*Wa+lSOEw^Q0ds1%a?)MIzHy-?3mD*ZYEjYCay +DzOBD}!-@)dZ4D0IDq@e!JfGx0By7f=l)DnR)W;G8nM^=%9gYBzgy7fc6vaw~9_r_Wqe>NPN2Q{l$H) +zKVO&`=wN`-D6okf|t<3rj>YCjV5QKtIs5=W%fNQ25vmF{Zvmmd7eGMjK0Y(-{t1oOU>5Zz0>{uH=0h +W@3FI${L!QwCVX`4?w_7GKYKjzN7o#%M6qgmaC-dLAJQ%l(s_a1ta`-pD}GVuux1$YkR_d5z+^iz_r7 +k2g6g}+JOS!weUUn>y#*Hhf-VNW9afidt?DSxK-*a^7eVQOqXoByQ#-%xD#OPal|#9@&(eIVW0480JV +|#rIX!aY0@d5GNKH)=0-Gxr+j}uPdOVleO_ffv8bBx#1~ZecW1D|JkG&1*vN@EHZ&Eb{*g_HQr%rXb+ +9_8I&WzLdEOzQXE3&B;+&EzWs(*U8%QwwUz2xA&Y$3DEugdDa%pY}7FaeW@>LqG7Vj-eQlchyk=!07V +(zocwfwLHciVXbp2W1_K(?uB8wc*+Qcp*jn3Kn}VZbGq?{wGr^w9B=c|Vp +_3T1uZ#kSB7<%X1xrzc_e@qC}dia;PVffG7!@4V{gzFmKLh1i1}+-?EyiJY!bf +MV-d&CLLT#!bT#dJ!5(A6s!9E++%MD?TrC-)W$0NJ4t4r=Vzy0IY;3^t(V2|O586Jr?V&{6dKp@QCVWFi4xGL@)v(SiF +*$Aqy>&Ee}uDs7)@gr5ni~)hfGhFMA{*fCtRkX!P4cMn~!v4hTytI2BJMMi=Fs|Aq+v7>d&fi%qF_?4CCm-%(VjPQ(gw3KL_o<=LYgCjS%SWM*EyWw6*b{DG*4yY~T~%T|?rHGA?9*oPD&JPCBO17)+ke#5ZOP +4m}yAXV4E4b89Apf4EcR);J=<^miaI(6wdbztKgwhu`tou8F{gWPsttw;i)shVPQ*HixU^RxXErc8L< +gt70jLxP{JgZo^l5q)3eWtCRvt3_%-09D#0>)z_%&EYbtb9lPzOd=*t@m;Cz*u;fxo%B>UKMdct0IRO +Rb`*qbp}W +mhecZrVFSzuws-Be#1A@rE)vfp0Ux;N*Av}&z2cK?yXzB`w&Q$bcmaQ_htlPSw`cuITZoL=F?M05l&f +Yw^xdls)_U1sk=^@jZ{4CN{cW}4XkbB=>HRdn?U<+u|GI4Ok;+w~#kHeeTi0Kiq6q1fa{WmreRrTbij ++PhaT9RKlz)~WjWvF__&Lrf`?@!(~@>Ew><`YUar|P0CjWi&TvceR_P+dGGD16ml +^G}0=)~h}kC1zN|fyDr-A`wykE4%KQp4OADCgVj`t#~PV+D6qb#zaESC2YK^U4eOTAIiSz+8;Z++ +)p^W81Zg5w>E8?E1AnC@+yR(L(MPCEm<+7$J$=S<41dMuZ5wJhQsK>&$Z4TssU!^zTk$R>CU?|coXC` +D1v5(@0q@0R8C +U{*d~7C?0&ez-b))`a=y%*XcI+$X0j>ugl#KZZG4Rm_xR@Th1(O=Ep*=d+i}8yUXw$$90f1iPpPJ%V@ +rFGQ0TE{pqoJ<0q0rFvoB4PA)G3s4^THeV#=N9djlC2jQ1R&tu%L@%IV*iYF^SQC{^bZc6^08Tb)PKi +f%P&BXg$qdX24n=DgL1-sv6_Z9uXc2Ay|Wb#RTFQTn6I8xCuFxKAQ+Y8Bol>uVK)WqEqM>jnrunGh8V +*vNBUxTq)}ZOZOeDh3~KaBu31gnZ6>#YXlp;_{O^lege{fGAD?p-J!M)dT^Vl3WY(Fh1RJ6|ECl84pH +1cEKq*zIl+ur+cmyZNv!-?2V0C@aoUDo`+mshDs{6_et-ptV&b&u^+ux@968ttq!k!xu5H3=f&=jqg| +$-jiy;$7Ff)}fKKo4ws)flZKjT7mN{uV6s=&aZHzoomvNQ75W!t%3uAO5Y<#Rod#5|j&B*TO_DBoBIp +7ix^Sc>#*>m-_gJ5(W4$j8jB?sK#s^zb933;C>xX5voGX(Y{+tCKdZT){d+<#uWOPIK +1ST$(eTrvx7M#22G3#NttU!!qwh0D0+XVj7`RW4BI2oUFuzy9_ch0MGGs`HVs)gSurt$aA-w2s1+{-g +HAE0Z*z`B&Gx@W|6iKBE%d3r*Cwk9to(sTSHs;leh94iA*4f|`-4Zk~nf4awHRz+ +IN+N>N1Y+m^7CM_f!+$YmwTlh0`FOGLkcV0LIRnrsa$KQ<4)v{g|<)Cg9Yzsu8SZ&=Yh24X;HjcR+_3 +f4c)`NEq__auCM40&CT_$TRWd_~HcFZ32tG?qgHxEKz2mIa1VhSKE;VCWl@n89%J(gwV=Z;C$!F8;k4?_(1Hb6>G5ZLL)?rWq +A(?L2Fxgr~jJkQ|%QrC>fT&b?xcG3vgdv?Ce;N4Qjv0M=dO( +MB#kz+$Cr#_45kU^kzXNrZWy^+Y>I%nR<9hPmzb;Xb?s2&HwtMUCo<@hS`(t~|J^5Ab!Z>usk+;vLu0KY} +a9L*)eN?MJXj)sHM)&odyQpi!=BcsB7GDI3ha?v4&$bnk#0X0N$N2TbAq^8ga>@H7Zt_MUi0{3zw +DeJLR3i0v`gg>%-w<5k=_@Mlw|$0N#L?2GYyCe-l3F@CqwJq(Dul`t33%>x#T_(%4*G|H;9(IAN0RG) +1E6vs8nCM5d-bcqVDvE1j?p_9M>qhH(V7qT%3(56P`NvF@ +@HJaNns5E`6Lchp_KqC0D9oSFJpKvugrlAWwp-t9{t8XXDsV*dYfhrol_K9w3|;(@$zmu=wKr{HQ;3c +4F(JaGx+74WLkngJ%;Z*9FuDCke%RlgdDr((L+5#YPv@yU1G3KxK7B62-RIj-Zya4v)o%^92DnXXpnA +BWI&U6LF^kVTUqOYK$s+@7=T13SOH7pHKxakuX58Vd9h5EPATz(OaY%&@Xz?f~H&VvJ-?CuS +CHfJBfWmh?@@h`D@~d;E*LJ|2j)SDf^&o_SAv^60jft27*@;tg>>?Y1LKS0RNYZX;eqo5cAHi6AtdLJ +z_};N6lmF(NnO+mzB$0FDt=nSW2G +l0m!l1RhI|oRkI0ZxNiPKARpc<&(@PIT(P**i`NgjeyPOfBhGR%xXx~ADRtUX;rINScA~CzPj~3WNdb +&Nk;7zm(?U4BM43F^L*KPF6e~9yj?;RzGo5zFZq~^)5qX?c=M4!Xc|W&F}Mn-le;;5?hOlf*_rEWW<% +BW_~11Qak$U23cq+_Q~lhOLaz&KRh`|m^M5R5p(~v)mJLGd*Rp@`;Pl}5pL&#`uIOf5H=%zseK6ia@7 +L>|+yV9Sn|8=D9iG8Kt_#@4^$mRbGW4h*wg#s>Ha2%vEU!CIQk|nW*r(9Vd(}&J)Xii5_pE4N=)Y&hn +)6NU7`R7lvXC{b$;u>b!Sxw+G{yQF$E*s&&%!yvW9F`oo~o>VEIBx-OjPruJ*Fbcu-HuLyq4zUjjK<{ +ZP=T%k#|`G8PHP#p{bqSL+^C&n1zze<#(z-s9}?XLG^T_Z(gTSQ=MIhbO@~}CrN1fc5+VYKCgUTd=}* +v-PzQk2s8{s_U`L_?`0~d^(@0pFjA3j2q_M^*z9%Mlu#MO@mo7X<`Fj(sCp>>f7ae4xsheb798JNg-H +%>Mo3-8Cy1Vt6z@RdvH>muGMbqaa1ws$bmwyi7=al51^flns6;o?nHnAL)~IQ76ic!v92iecRKt878?^3a^CZ7tl#g&$QLv8MM~T*4L~xnB7Uqp9VCIzSYJ<}+F +8i?ej0TrcFw#IsBu0eC-iX<#^yK;HIV{aZQeK=PIMRsoWyhWqwUO$@OOpRxz(!cj3kifo0w7#SCYrcp9PHR!0_FCwChI=;iA}wBrp~LUppT +#m;m>Ik7mzR;x>vsE>jmLr-2EiQe7R)H1mzI+U0F8ij4rNiDiW-8%jrbvURYZLIr6UOYPg00Kf$r2 +5)u_b`p0i+p;4K}!Y*iS&IB`qS9Y?R!?l<6?eUXNK`uc+xxOsOW^p5AUjtfLxnsF?Z=tcn!&X2zr|Z= +<%}e@PwI{cOgrbLc0>vMLcd$@j2lfy-p0d6QZN*WWMIMYaX(}Lc+Y4w3;UztUh6rMb&K651>2M{chlZ +dC`TL4ba_azQCS`LzC5V*!oBGSi@xmGJa=g^GCF6Fi*r&*0$2IAW3m+pSYE%%MRUr?nV( +0G~ZA00pXx?sGg0&{x1kqx9bkC$Y<4P_HpS&!D-{ +f!SFJ1Y0RQv9{)hR+5hITBUWzO$6v>qajPfc_m7CL7v?hMgA)A|n;-el*NBI|AT6@PV!+Ccr90*c7sh +Kj<&;DplaBny~}ipNR=#%cxK=^=A5Nf^*wdW;b#e@<;W +xeb-W)=gFzBpIbMA}OC>@-43ylgt8vC~<*VS`Kb#_A-K&;)!0>2E54*Q!@4za&~@cNXf;)u7-~zr~2rt0+}#I9DmZTGb+dg~!!l1#Rjscv^zGIGH0paF*_eK9k602VK@OYSY+lAuTp_c8<&71xRbcbl(!p{R8XCRFf#1PG5Y+v9T|A{R1F}0=}jLKbM=tGg({nm-e56?yP3l~Sel_xJ7HmrWvTvZtLXbXx4c1U2Kynw2VCyvK9`K{vOo=*!eLhz{iG+lwgs8Q&}L>GspbC!rb?A&C??EFQCi +wI&+jfue9@BWuEsi-#nC9N!Tj0P$B6uq*ZZw|3c`)*snrp+?Lut|(wWgQoT~F?=uzEgqlaYJzs}Tplq +0D?BHjE*Rg#Y(8g;`y@3g2lAMG!(d$5x`hoI2df>QHtW9-gx0}w64WOveSeGR3YQ22@+6Yosdw|*A8B +&=QGviw&D%9*W;sYw@9USoH@NEStD*v|jvCq5u=WJ?JC=CfJ#k>|Djh5>aZKrDuqbwtj|By;pHqlsy3 +JKGP2-UULLxDmW9?~F74$BpB&`;FnZyqdkz$5aKtzNma#Utc$m+V!Xs&}}8`JbH5F^Q2K1UW46GJL~q@UBgdNM#rv?SN%$%c#|GhEZD-W6P|vBsPNM;iSh13XN3t0)IIH^Fs`%;ou`1i~OO +Tj%MP{qs0T3y+@3)AXF6}TfrHg`v+nXVZrY8B= +nBz8APxCt6zxGA68jN!Q%P@rO2Tow7;0#F+~aU`%Uc|}&-)Ja+u+E3@O-c{_R19)RKU0q#-GJS-Nt3p +Gs1&S^wdH*@7E}dd0tSdvVsJayo3NqCDGshY7b*?79HkY`G=m$;*GTv&!G+VOK@vqsPJ6M2)X7=9Zsj +8fYN|Xd5nDI1wx@ojdyWPVMs#=@HVMuLBzgw+G**EQ@-GqOjHY1YU>+g10^2@GvC@JW5_SZK+jAjYFn +||KGS{pQ{^x7K*(X~nrHGn*umY^gPTMXA7EFVAvSS`2A*Yt%D%>YU@o9-sX4F;m6I!EW;&5#3j9Rq=o +h10zs*DSmH-htmZWJDN$#&DQ(pcRu^4-Q!36Y+dmR14n#0%4HuOCxUgxmleYvvi(*I99h%1?11r-Dr- +?mb{c1{+?IT`nd`ZorX5bC#bZ1zG0N4xw=!EU`cfKMbDp^f~5$Y?jcQUh6KS0qRw+IN=cpizJzfmh +i#@-&}{;SvJR8c4TFNaJ8cJU+E7xKTJhwg|?DT>CvKapUh9pQ0L1d$BGN^sit85VFaz&#rqCogV*UM423f#>^gYHG>hNcWp9nJ_8&AWPkdSsaw4RL3fY0C;SP_`Mc#y{vzV;08#m;>{X>opcVw)H7$tZ2r2hQay?V+q(H%udQsBid +D(!H+FD7@4eq+jrl(!8fM`npf634T686Ke&T+6l$%*&bE42R*JE;-jo}XIwjt@xJAV=s%#=Sl;i5#14_NW)d;%i|ZA8*s4Q +J0LQdVsCm|`$K=As5fL7V1FPKTDc)8nbux^q5Vtud^JYL6>Cr8XHzEA0N2ks;qJWgjidZWU<@B*SZh8 +&Q#i&nATK`!c1g(YzVlK}Z+_TK27$Fts-6Ke5><<@={;yb8VHFVk+07fh~FGx%08n83-|CW8X0RG7-l +gX%CS;dG|&pmKmU(8tS5ZaYM1g(TftrtR8pqX*l{~wAuotiTh@^^Z-fA+_>( +Pb2jZ#4LDKDa&;7w{h+h58=`*ZsgmAbrweyMurX +LC?f}0?|5Hxq_C|@YDGDg#(+NAUo5fnRk-VWnUWS5-jgwL-ejy-C7OS8FWC-Kfr6&;7n+nwf07f*Qr0 +8vPF&cudB}`$D`im(S_HSgqjTKj_lXQ$t-@6&CWfEPFzK-)GQm{Y9y7VzWeG!;x&U*?&@q-a&)-`Mylyu!DWw0jhN2HZURA`- +ac*vmZ(wt`$%I@!P{Eg9N}uTBS-;%k*3I8MVM(WSAh`V-d}})lrgSGywU%;|aQU{XJ}-&11GNUkxq>< +0v@49$XG25C)xnHMeIFX$SlKi5iP(2OTrH<-ok2<_U|J*=M$cC_Ng`0fC+Z28>z@eQ62C^WyL2sCbxT +T|9S;V5>0A{3|x=jG&JPwb<;_&Cx9~)WW{aI;)tW!Yj1z%7ka?n2Sw6z0Pbbqw{`m%z^#kbw(b1Q~a3 +QTY?6pT(SDC4Q74c>Gs-eGV8%8&YzYG6U$40o!`wi3;R?nSDCpq5XR_*JNRdqS&yoGrIc;g54-e&D5G +SS^^W{9uF^~cp^()rn;n+2z1JV(#aQknn->SPcb6qP-gvz}2QJo31p|OU^S~e{5T&IJ_N!nf#k&ngzW +ZRR5JKe^}aVibV^cgmOIWH6t8ri=Sus5^UHwzd!&3Rf>g^A0RV54T)3|Wf7yD1qRE +C%77u=roh9h2601Bh*&Jz;bjb=4Cl*_{Iy!!L=XMNMUi3dCj~T~6z7b%f +dBZoVtrO)S<}QAcH$)#BtT_G1Fp=-{?;rplH8j3YhO|gghDuFO(6Um6+rJZ=J)j2mKI&PPz(U8ff-tY +rq}(V2Et=>z4Ed6pgXpm_z>ZA9x?^c6Rb;;n#E6HXs){&G7ZHOIdHMuKg78e-$GJyM!1p-l$Z?-hTAoK<{iXW3_xYH +p6oCSobdgP6yREt%~K3}95K6n-v)eM19!Gs7Cx%;k5|U!3engHXPX#@Z={#cUC5u$Q;G+*km=eF^;BDP6`1m)m3i);uB0v5v3K4yfrpOdUGIs6uw +>r)#pvW;K2sK7AYV4u&^K4Bix9iodtqSbI}p=4RVDkOs~0Xo^`lasKzA?69liZnOLg98E(90V+jaz#g +XV_8Mf|9S5!1#95CSrzeI1zO1$M)WM*Y+vLCg+dMHRLE*kVFffn0ePRThSLdyIGfKkg|`Nq4=p9@l*6 +!p)_19T#us#i&F?>|b0-y&B2lRv8muy}8zHL$o>icA7?lgb+T_Ik45P%0UvDS4f1-FUT8BU +$BdynLAzaAoXYIv@c3vGV9v*PEt~F?v1(1$K#@sN>dEMw}p_o*>|8baUytTFvb*=h&CP@Vu+AA=TNcZ +Rbe_3tN#)Spni~ZjA+74N2 +S09$Bww)-j#_a_n)X%g}$v7!YPwf%3g7hp&c@s~bDo+C;5llfi%&~BYqdSq(th~7k|LODapMEj7TzL +~`E$=i~-HB0=FF9CIhOnFt5$2?N|Ax)+j&;Ff(c! +@hHreF-q^c5pU$Xx7ncpCmYlM4&LkmVSJH=~!)%aZ|wOiBLNCneNn7Rumx!hr08R82ej_MmTPH5@D~u +_(w`&j*Dz+%d3#i^m8y^O4THmH-1fHbnxiufHY7q?t&gIb1B_Qk~=)nDbk94_Gc2e3<2PqVjw+a9KY# +kPCz;-`+5lu{(Cz}uWx`hG(eHM`^W3#906VaIw +59V}u)y$3onV-(%?0~@9x(T3Yb_n288x9!8-+G$9GkAwc1r+M&+$|jo)KmP9<{&4LyqJ=->w8H5jPY( +m$8Z49o-92H(@$KP^AV4+lZ9%Ku2CHJY(;g2pl-cD5&2YYQL@FQ@l6{-(da}z&-EA=rtF7Ou#jjdhuN +WZU9jpm-+ou67`ktaiGFMxQ23t5B#-t`Hw8v3-WPE>olx*e@2uT`B~^NhN6;wF +F%H52frxzf;54F^kMt>-CaY(#YTa}OcOy{35A!^Adt3=i;%bTOp5@-zR`y4^&GLK@-T3{p=mv>gEWkh +eB>${Segwhxj>bIbN?rY<6c#~V_50p%XaH#GqA&WM7!Jko=1NO>-u}vv$BRGiY#af(Cee)36z04xMWl +eLLQC{X4>Z`;p*C9(W?S!GVfP^1sWbA4bgFl517x=o79+el=f}OgI(rGb*nYlZ^~KqTI@wi=j;?iPbknG>8p%>zNSBQOoL1jmqs1g%0nIOi +*2!Vs?dwRMuTfvILrW3M(lS+TlvQ)_oy^nfA1=L9V{Hf3YtD+MnRD=p!)?o8ga1}4LN{o+BD{X?8MoC +_u1DxkDbXJ{=|Kq%z>w~_8nDG3jCNWd^*YxjzlW?#U1J5=$T +rTOx{_^u3q8muQSm$H-Go!QGb*?lfp53sa8clNjG{W+;Z$4hiw#U80)!`79X4e7^h`{^w>?14 +P=hRj~!#nn=jOHZXUcrE20{E^6AD6ab0Qz>(3e`6tY>MgXjCy)^s<(>4d7c^-_I~^I#Si9^Sza +2t>lmw-Uvgk*p8v9AE1jHc#2QdQ?iSnT+B1&<8g?mvV%FKelshExx<>HVS0lI$MkfBof02QTMPNEoiX +djPu_^OQVK>6mgb)J)hQm&Z{(AwMwYg4aTIP$&xv~Eq^4LzGf)!mcWkhdrekNW%~sW1bPQhp

L|5y +2HU=Vr<@IV(S9`)Z=K>uwLa}#jR7jy(k1tZ2UiC!UtFj`5*p3dV`(G=Ptc*yZpIz^NE&G>R;fj}htdh +Eiy-DC>_v~JaVw>jwAWLj)qFrfQ%>h44d;kQf{_lcmu^|MDu^kb4_#RTI+86bZey4&-^dzsKjp13aVz +(|S*l9?-kJ)31$do&Ewd{$&S(^-REwjVi4)9ncHe39!>G}y(bqlU3Z#&z+G)l(h&!f<>kuo_jZBwTLnQx&5#5RHNsP!r+RKmQB%m1$aK0ASY +pY4(^_@e(hgasCMo$O*>e{=fe_c|Sf0n31*V(Cgs})tMT>D?gmw_til5xQMbie@zpmMjNncmy@EQ`_% +VI>K98miN7fz6hhktYk;p5R&VEqgTnAbt}@3Gq2`&^kpw08iZXL!taT +Qt!uEA8e`U|sfSSYxq`cj?3t`xDw9f?qUGlYy5aIPo%1)_kJZ`a~gV +<#oC^s0Aa>({T*}UGJmX!M0>h5AVxXWz3%qHBEgLPWWIc2AQnerSY#Fa7Zcr~MD3~Me+@BGD#NEiAmg +S)MtR8fmrMN01*fF8?u$uhp@?+w^?QE;H7^cKnE0tS>oXtetC?a}lljb +S4QW=T^1)IcaC?LmX_`@hAWU-$813cp;YCjVBMTY+vBJY#vPmN>cnsKFfVI_%K-;~0~Do;45(r4^b8i +~;h^$MIMA9qyMu;TmcJ2P7(FZBY~LxjvIb4dz(c^Lw(N=J&3Pi`HADQ)9I?U_}E|5{`d8TO^~Gxcr9! +LL#!#n=EGII);N&Ew~E`#D&y2!WN6g^I(O}3pnNG&0(^8ep;wNOjSM_6dT&Q}E$9go_#{ +`}ws9O0Le~=j}2eDgtFmf6V0G2~w&m$>fH*f#%fBrw^(`)*KN*p7RHe+! +AejleO+y5S9`usbkG$6h8|D#b_FitY>}Hf%LcH4ouLTbATU+}lg9ypQ_0u@oRsI%1}jC@&j4Q(Qo>Q| +#M5bIlXtFdhNqo~fJptmsILT{>UZFBNNcNRDL&M6)>SQ&wRr*G6|brrTg +Scm!9wzdMq0oSYTsyiXkkE2}y;QV*^mz+oW?eFe5BVan$0US9CJe#K*`4QC+j_ +_~(Y-8UMxJY={*>m^T@;=G46#_KXDzXha>R|QpT@|h*^A=O(R<~fi#aW+&Y5=RmoW)o~BURjs2JroDj +bmL65<1InIhlg#yofVjrY8~iaUAXg6b&;KhXu`hb8#(^2~QE+- +fPQ1U;*&@}y*PVE>b(8hRuIAvhOHm@ts99-6&l#+kM2S&Jr*)?R93?}Qb2(4Kd%e~IB*+70jZS3+=Cwl^H3bC?SjL@C{2uzbpj7o3xSCm +INaRwgNN5t&mOhN3mqhRI`Wed`viXom1>DFk`Fh`-IT;$u(I2N2EP0Tx^1x3Kp+y5>r#y3hs4Fd_c)! +vyMHEPbse@c-C=b_=2V|7s$A1sN+5FHn-$^)eY|)RCn8%n*_e7gwkEbk!a{di0AUAaTn}sE&`8LyFh^3lbr_;tyVuToaQIB +``GQpga|S!zxCZPJ@|ouO?xEwUn`@qy?tJB#2G$`j$V+Y)bKAdJkLeBpeMt%ArDDiY^(BqB8>iJvzf~ +9#8VbdTEad1K10;1L{q^NmMB<2h8g7a3RtF^qZoBJs1z@@|*&3UQA{WW%7>&9QsrPB%4Pd?VAVf(;y0 +0bZ>W~fE1vKLBXP&r&>aTbv!pxuDeqfdm!t0((8YmyR$=s)w4sXU4bmX8SEKyDWzx-%^tQrDNDn(_LZ +e2EB8+VmqUb|{eN(S9kK6zj3zTN?x5^Q8Cp~7CVq&mze@wP-vn5-^Q697vsKq|)TLU7 +ZkP1FVoqO=LdCjlDTe{3C4UZ&C_(S@?vXU{x9hWXO4d#Wd@^LzEgR+9YrM&#UN*14oGL3-_63z1JTJP +P>ds5%279tSzUp;b&6_v`v|BWnQJb*2_V9?D~|#{&9}Z0-ENmngzBWt(HwzV|WK{NW9|m){8}e)jKJR +yI-bR1_v>wemWpiKUb@9f70S~5$;myDZnzEeze;Hg06vo*tZa+??H0rWmn4T-o*r<=567|K8G?oDQobY +@KbygWmlPnGJxdz41HXRCAAHU*-++JSF~yF&IlZEF$B{oN-qQ3_LvZLHL|$djwMd`b)u5=m?48$+KQf +QA9cxmgtta|U!r>LEM19!Hna#o+4px(`4&bm7598GowehdViD1V|S}{5*~)4CrpH#%}fS`a`Kvoz&Qh +-eEsRiD+MmhPUfgPIjj{&v+(_lxG;@?$jpPDk=0=0zy{OW^^@~0BZ{}a-%&a^htxIaZ1ymsh(hgI}xC +Jx4YZnanfXbH(M^@sz1@zCbPKsj=Kfs`x&745@g?yPU)QF!uRqwhBz{pPg=lC(5gb4v~4q-@%7N8E?v!Zx3Z$ks}HX=?cu+MVsF-PWE` +WRQqT&3k}u=*yI(fV$ +^@O>Y;>`pmq_WD%*Y1pLE81Z9aHeCq}*J6NWx~O9xuRfDlN9d3mAF6eI@LhmayiWZ6HOP9z9AA5gQ3~ +c^Ja;9L6~JDN8VfyISE&&?)fv-J!|@^Ek)xAD0USghsg-&I(f}U|l78b-1bVPF5??` +^9qUD$yfiBm?x2RU_DFiObtB-i<94Vd9fy>t+R8jbWoGM#Cr=jTYrY4D&_;p^>uyv+K11JAL&Q7ZKzRgRK%C$Ik8 +n16}Y~0{cPZabiTwTnv$mu&dAMsxdrx +62M9Z=ov;*|@uTITZ^2XR$rHoV4#VV3 ++@R63ACft5gt(KN?G@LHAzK5fcEksT>V)|b9Fxa#ZMjskRb{9(x^K$5dx4Y5KFEY2ARW(9hu9;J0j^X +a%u5>yT>aH_)aZfswI4_E5lZjI2jO=B98>1dIUaFa<8z^RIL&k*8iIwUVTVAN5tt75Hd^u?wMXwR(lt +lGZ@^S!xa@m3_lWt!UgP%vNzjaX@KQ`$wornd!O?E6|5au{HZKvwB8{x0_^3UoIb<7cFZD>a?AxX4mem@vaLFc(A)jbG+05VeM}8lOe5H_P+lOb6dmYPT`6$1-P{5KQE&G|g +lkaP+!@$vvs?PMNr~Ml;Z0$vHZ-uNv=-dZDAmjSxv0{K(x)_72``c|z1Cg^yOzf&DEB4`k|R)}{89gU +;190D=mxVNE!enkws?wE|9;|9qyTJ~g&w1!K7)5#d1=W2{fQwX&U{G5MXv2b8f-W|L6q8oO=Z$#8YTp +ow?ehsE6_3F#vXq}jqDSuWa_~=LB=@AmkX8Op@CJa-3#)WD{Y)U#+dUb4J`@~LM7Rq5}*%WUUcB!mgbMS +Nh`>V6bcR@AeFJMvk`t{&ay7B5MO%F-ET(-@%FgjR1sq}SPT^d9rg1~Hz;$ox=_DG=i_eZH5fr>dd(X +1VwHpA5zWP04cSh~lu-(hRTlqMMo4K1Sw4d%1s(A8}eJ-U1s|q+3^b3m9{ovGA$!964Wj7MaRB0FKTg +Qiw;89aYIW6Pu}0s9sC2eTLcs|5b=Q&LIVBLfT-$;NO>HHf|QPm4 +#4L(?^zByhbvXTUP0(=6aG*uPW7&ri>ZNgD!i0v7Tjzj}^;ga!yi_C>=5@e_U9^c>lC(6?(WrU-3^X&B4uV-g67_*&FiedZ6IFz?uAwj9}&X +1m_PuTBgnk^*lwk9PgE<5LdgNm1k;Y&*{e2!+hduQP4>-KrHtdz^_4f`z|PtBX{~iIy&=eBNv#9lC^uPsUj9sUy#WPOy#cLdwY#jQ$1q;rn5c0F?CkK +^ZFRrkBoL-f$l+``FOwgdJ5PbN$3Mt*_lQ+!`|KXO-jHq$=%57rA!2cvIu%!1s!l3uj%P{v9 +A_ygJ_!S^uT^o7U?eW3U)})ar8i2V>hC-p(Up0V46xpHmn02~=xLEN)_Bvn@q(oWu}V*GQ7tM*LJGij +vTDt+Ec`OgrS%?xkcg>98;Iw|rmuq3Aj2#Mlq^K4dw5Zd7bV~f=|j+;^{Kd6}m1p%avx=sNqnQpE|v09b)?eiL*Y-r +u`P6FmfM+GE*{G$En3W+Z%=PPCxVl|5MV~*|;!*qv0So9Q&>QgDpY8xq+yd7tA>-E*Z9Cuj4u)SiB7# +0=AD`|TKqSZ6cF;DZ~++-@j`T&Yfn^#77U$5zOSTtkr`m%q0F^+=s>j8ixL_)#K!P!^$BwZFKx2rk?V +uxDWK$Mc;Z!M)9V6{OSH7C!oOnKQfU_Q4G7mK`-US$5rK654P^~jvJAO2XHr#~dEwE@DAT#L+E@=?w^ +tjtk>DRERQK%z*bVbW}1hQeF7ZgM(KT<~OPKi(NMf-u6Ag9?lUSY+?yzO7$e;Ge^l +YZm@1wlC45FPZtx+sU(Lqmf)j4#2I|wm+Cx$eZc@B(M-l~4TGRB#hRyn?XWU-QkdHHcCeEoZ3TJCUOF +rCq?)4@#DW!@$b`J7_Gzom(>`$$!40k?fb5tc5FM~zbi1b~tXv{Kh$RitlUcPA(vVH|Ago9i?q<<~uC +(CG9nyik#*zWT0D(v=Hqf2YFx-uzS0qiFq;Z8n7$hng@*p`&r8eE=F|%7X>u!%_^9AXneF+GJMJ}4it +FZ#?XV*8$s92~Fl(TDLkWP_<-t7ZjLLM)-@!rezPS}}Ci +Qs|9A$!jog@LrMIUW-k+t69bX{HScA`>{Q1cXy}P1}%z&X{R#lNQZWtElaghw=ev>|M_44!#pK-&5V} +Hye7|91MKGG3`ihGo$-VFzmq&w^|c(39sSc28pD-T&#nLtg13~;!JA@Y?@g$HZMl9?O=hOliv*6S8*` +vuSHi-*kWn-5uzP|8y~^z%;UBX}I~d(~R~(q9fu7Wqg5lB6NdP5h2mYPuM{iL@7ME9X9zQ0f0zxCR&^)X$;0!=#WAY6FkRT$}wZ<$oa48 +zyUVpVfAd1%3h%y75I4;>qajDDqNgxcOMASyiT88>RpJG$jQUlk^MVT*huS(-Fe9Z)bKqMr@hjJ3u4u +rX$$+O9llVC$gxe{zSK~Tgp8MAAEP{>4toB1QqGeI>?4_N!~?b^mQ?k&_8PxTi|^V?&%nx4TKh~dTf#@blqs_s2#I)q7p&SpyYetu +c6>8X;7B8PP)do&(KWx!Z0$b@Rz9s(wS1pt0|uZU6o#`xg3Ts0Ead0yQQkL@?o48P_0Fynw5O|W>`X} +H`qem9{}rs^er~Ax5pAhOPX44_f%TE=2VkQ;O^MZ=(Q>{ZEP#6B!qzo)5uLmv>Mr1tl*5sQ#Z?G!UG1 +WTjWmbp5HIG%)Bu4_3cVEn{7?EUcD;UC{G +G~uE{}5wgh4A=1RE@LYB;;3FEBu5GD}A<@x*E4vK8|(unxdGW?wa!UFhfOJpG6B9w4AQx9sdn#=Ycoa +z6*)zq)Szo4ORk#S3902J`}YKbGQqLk(=L-)KWuiCtv?i-~5nyRlMjX>fKhdOfvwXk?h&yx-G`g>r6KStb-1^v$~( +s)TfHX6JTa@kKLbdl02#6N*TN~;6!-CB$s?;EFKF+p;n~rx!qVpvbmR86{|7sm`#l}483iv39FXBfL| +c`SRv(E_8w`ViQ%#IDOQHlLY*Bii@^!|#rs$mL6kCt$-pW3EP;-q(obfzQT9hEbDuIBsLPw3pxTz8uX +L=mlyDC~HNWvt*BMaY$QByVuaoW%LH*#|RS@-dB>xH;EbdU2M_8Ib*&%mnoAkS&%DtNmTF)~I!U* +~+6j76fDh%HLXK4-e+TAc4Cuz;}De@CV<2dR2d~&GHp)9>^ufRd9S*OLb~?ZWx2{jimQUIV-S$M&`iV +Tl<j|M&b`gk1{wClm7^{NU^%y=$=Fo>$eL5F3?YX9EXIL^lg2t*stGaR73!b+4@PYlR@U6vn +nRwif%m#WWiGSN@yCZ-PQ?SOxW?rGEGh2o>DfE0=cWcpE`h1Q}k;kCIWT14{N#?5Ht_xX%5!Gl9A)#a +!SSXD`t`4|MF6Q?DfCs+yqRrzJAfl!E>HqMJ|3L6K%W~;H>4K69h)+0}Vx*4j@R#&VYA0H=6RgcyIqi +?Iuj#XWHeN2eG1S|86{I&+KI((m&(<(MqhY^WNRtG2Qof5QKVu)Ty9j>E*4A&vkQ}%c2;8zlkKl;kHb +h<4{DXafo#CRran~+V?Q>~k-n3_R4F3&SV*US@!>M<}_y53H$i;G!YJi(usCGJns){VQ29EetWkxbjF +K8&sKU|L1-plZc{HZ`0rD@=WpDZ)vW2yX(H=I15$(S;u?_ZO_T7{%4n@VrPsSYI|SdO3vE4WD5VJb?l +pR~Z4K`jnC0Rx2!C<|*>SAor{VmWfupfCiY+Pv9R?1vP&L9x~W}#d{%0v8bMlxc-lR;5E +7kKxP!IKJYus4RtC(KXo99FEnzZme=gckCAn?o0V=(7;wUF^~!u7C2E=W%(T#XsdoHx~ihxFKjMMZ^Sj^jl?ME|BgrHCoWAHa{7ZW17jjmV1*h|7TJ+v?ba$gDI8bNSO8$5&#e5J?ISeO)eO=le|z +wlJeToug%eD-Zah(75L#l(8xO7w-HZg%T;x7=#4@P&3(XCC?B=t@Js%yjMR^@rc1jKOii~P30}2-5bO +}>zj2Y2mIPOh@z!velxLMra%PC}oaAAaAkB2tlEE^`}g9QQ+YR7Pxs}(Ln-*Y(^#p7dA68#C4r|v$1K +6tEqd$8L@&C`_FgraGa&l}ETw&ii4ZWiU{CG +_7ML%iiSW2v3+uYjfX(E`9P| +~LML@uc8UsR9rx=ZIZ1rUXv=;N9MU|k&K0}sD^s14_Qy8YhG$=S3bWtG%c$Gx^%)R3s%a+Qn^!)Ra#8 +2Y9((&AhkZ6O04P<@tcxX*Q;(t74Es$q!r5$X<+zYZ)(L1Q5n2Q2*MC^Z&gH% +CJ0MHRbJ(W*Jp--c^-ZoU{m~KATCZSdit| +F67cAJc%k}u0zq^u^>EO>T5D1H8;WolSy-+-dQj=3VVB%(LZ)MS^vYse9APf-79bp#7TJ@-NEQ?O>ng +Xi2*Ql3e{`;5#-KVun94L0Ju{Anc=JAV+8>T>Q2C6r`>Agz`g56ITZ@^jdJWq@nUxD>=Oo`IfJGk*Uu ++M*j`>!L9yRi361n$=93R+vCn#<*px6Q05o(g;QlxTr*!mlZOJ?UnCkztr`lhf6XX9u>nTv`?RI=jTk*xp?iOOQ`f6Imj`2NlZk@g4pp{;bxvJB;C``6> +x>Ck(j0E;c7lC$ODpHB5sIcU&v&3SnkNxx9f35v+gXZNVmiZOp*;&hqII&%P|HRW0t4=~HZgkO++w$u +ym0puuKR;aX!zW=~0ieICkWYI*P_ppjT8QD^(b&0_MjFodoOIGz#g-q=m;#jZ_uE!f#grbO#xHwn&@i +!le}kI>+On+2?3l@3yBKsOk5Rw*&McVVmxTrrettO0$)n(;&5V^O}%RNcEu+n-Tku;4v~VPzV(ncsV7 +4Ni$t9-;`@0gRnWf;Sd@6zZ}v02j%!!gxcu6bx8*RHLZJNlXS;V^coP0Niae{dC@`JnYP>DL@N|&G90 +^mK(??H%e`lytg0BQs;N<9|45pUo!!}c~5c-S15S_-?Df{MxE3GoauzVcJ%@58E~gFxLHQ--5a`=<+@ +*CPl3zq)$)*7uxVLKQF|_YOQ!`XBxtG-W9yc`-f~}}h#&PJ_9y5@HHIX|f8#C2k^IvpHRhkCGB8L +&HD2-&#l#{`W3?cFUTV5#N1StL*{WG?i}cn-&1g1Z0%PHBpm&8=6IkI7tx6+3_(Yh34%>Z3Wkh%(?>H +!k^FovHcz?2fetGnb68IK*7dSq+R=X@w6#3sX8y>)pB=4T!7j?r1c)ydC!k*IP*tam*HDl{01N`$%D9 +YtkdaTA`{Qy_TX(j9HSrTrZx{hO2;3NcZ*IwEWc~PqNz_j`vanp-_dC^(fG0`3RC;Ak*i90?z1)-MOP +k20bufW)^hx+K27bwJs|tvh5ZD-RUyHoR(|}`Z)69T7RN#-DWaW1GbM3est=4LiOZQmqE8+T_T#44Ow +)X_Zlx_4)h|d_1K{lP1EOCJEt|+r1gSiZMEzW@_D1T$bxm};K}i+*JG6-rb(|KMk51+M1QCXL`r}Xlk +Q?DS5h>s)4%h*aHuv}34}qDN_X#thkdA%lhZaW6PM)6&+%)*4k~OE*ven0AVYN4QUmrmHDG@*cHBiyH +3E6MLaw7=n8kCPoD~9Lk=s8`8jKTeo5>3$n#{Mi)%AcPy!u**)>C8X4ON&Lv-C=tHsh|`<-t14_%tdo +kB$R%)jR1jmG9luS?tbPlyC3Te=O2@%7J+Xj}<07L+%nJ-l{l|b`tFF^U5mw_+(H#Jgx1T%B8yddw%P +Xtsa@Wb6=wCW=sKRxpazaz5?!Mpg&2(+}-;e$tOIU|J$O3+qy0os{r)|>AzR!{K=OonkM +z%Q(zPMvJwu1o!w7LAUfVZcNQgUjD~Ta{iuGzNSn5uc}SAeIb8CLFB3S#Kpy>X#T@)qs>ne65Wa`t%zADz5^L=dKLZZbz( +s7ZO5&BC}H9LbtA&GjfO?%hdjy!rmB{%Jydfs{^S2GHmPQH{XM%UBznsC?MjwkWlUDjT#Hd-seYxD0R +a4_fOGup(X?ZVG+Dm{u^xqJC1U~LNdH*kv)u)X$3Dx0ihA6Z$Qa9EBpL@mQFUfzA`Xe3!(rgsR@=>7((stZv!!eAg +zKpl1Ej0ZR_-0_CTS)=VnBhRE7l^k5cu|F6qE_r621O1wA?`mR+xkwZ<^#gLHX$5>n(_OY6pt;Sak$h +kTp^otp)B?HjkOv_$@Qp>bV61)Cll7Bw4DTVf}{hXow(7H|$;RNX^D+%loyR7KZ2^c&>wJ*gb6b1ktF +z+stpa*bVL5gP=h7plf5_3sJLuTI(M820Euflg^JOq3y4zlY>*X;skv%Pu8^@|K590{w9 +i}U>TB>oQ4uQo~4&xbC!qga5f%fu{;K@@h+#aY~g=~HX)o6uQh`7M8bP5epOP}uz6RQFCBQ;pg@OyF8 +s&|`)Tx9Vm$*aTwp^#QaT21y8(UrVTOC=wGa7a!wEzvsA3JIc23w>{1HW{(zwx8T&G3|bm|HZ+|5!h< +6072Nat4Z;aXntD-R%U`?C{%x~%Pd=dl0Bt5HdKR^>yCZQE+gXEYbAA=Z|5@kp$!Wb$j)v%YQY|Syq7 +%7>!EE))fstPujTzHRKir +QALqcQ9Rng9-fz9VWeZVbRmj^=itR}{lR;K?cra1O&+h1N$lgFTi>;`# +nrl7Ekvpt(s~nnK2;W|HdA_TpX?adN_N)T^;A>Jp0#(BgYVS;c92l{~}!SOX!^(w&qbQ;!ZikFgj*lh +oXvrDajg(s|s^30PP7`jZoPv6xGIHJTyXW@4*WX5!%mUz~=e#ycV9ffM|~f&w-ReMRYQqF-i4IX(V*>*H?348&} +_OE6d2!$5uyCc}h%YGQAR$Y+b592^oJsqz-rYqRU6`8w0C(rdywxp%3$I6bsWO2^Hnm28OEeBaz%#kO +Dfx*&swUL#hz`OKJ0VD}*G9IbPtvC@r!Xk8QP>vHij)G;Ln80}jxaSDV3z=*gD;~Mf*%IxfCYBw6>=o +=zT&cRV81(c>#d*OmA|wW;br+(vK5^2@u<$6~Rb!CA-bbH+oJ +QY#(*KL1z6gUFRSS-gC-}#~%BhE2?`iGCA6Hn1%p@2|mWgqDB=>B&frOD3EJ +<2_xaO?D>Mr|p%Ar&=1_u7LCPzlh4{(ap8U7kANWph>MV2ECP0~l-WANDVEf@fIW_i?eBilkaMEyZq^ +RTdu=`I3XxZ^fN|+cw2+kL9@Q^WN)UBK?Iv{Js8KQaHCFdW?1Lm%}Dtyk7ILjnd;L@vASvd4JenDXgJ +dcXDoH^@r8ynhfSi#enX~N#M%lEGzEgOy*?jzwBB!>U~%RqDg~l9}`S$lf*9u&+={?VP1UvL?r4e3VT$hHc>xHCM09s#3LV=Al!7NKX;9ybe45xaD2d7=e8$x7s<5X{3U>onE0aFQ;fUA>M;9SuQPl0vH=O0eA#VL!2X`aUuLs_g)mQXVTU9)P4w +#y1kun2C18-)QPQrm`}4R4}5)DRt4TeptW6^%@n2!ui6GJA3T33>KwYL~qNc1p{ho@4S(zK`UA$AGc) +sNRlOx5Tn4DsVu9wN;PB?ZwI;wAV#HTX^Mjp^AVT;QEE6w%u!*Pm=y?wIF%ardeQn{3dBsWb_l*%i(S +Qk}Vz|6ZA_tus)pI-;hU|lWWcJYjK!3CAt +Nc+E2b1=TTDl;rm=CMO2Fv3q(sCxCic^Fy&h9yg2i-6c+9mbfvp=~E|zKucbhp_tHs~lXo3|_x=hV^G +W)Fs^s%_D9gCps5m*o1>GFgNKD^{?D$4{Iz*#ZP@2|59!0p9ZnNHR01|SfT$*#Y$-(>k2`&e-N7RynC +{fYtIrPO}9i(Pd8(rZT|ie3!Y*Hk7(m9oeS;-s14srUCm)T$zP0!3r;*67g8d{(!66wjaI7Y=L;y&g7T)YRMZ#{Gz;2)_G5B9@f(i#F4s#)vlFvm?mvb;h@(Qg#9vgg{9ePe%eYb{QV|G+?C{OgV{%a)OysnFV|dJhLW7GD2P>OQc+ +Bih`=~Nujd}DAx@f={t>b@XdJzmr{R90;k-x$PUv8TA4DA2AY2L73hbO#}>&{y*2vWAY(In22Ubgtbf +bK|Bu{M`_GEY=z2DKn5a|UFe2D5Tr{xm5T4w$tAUA+U<>9Do4LK1Sb>tCZAZ!s^VsSkm$C?~sS2x?t@ +7gZ5D(yj3-c~pD}43PT`k}{v{v5lvM3)Eh*SwNG+)p4$^{`&Dg-Gpa!%i)A4 +LkX7|hS3=|;)ghX(((X3-8ZY`x}aq*PMKMfaG%LGFjS4)-fw_&ZQUse1JXOz*6W&pQ69l4^rsy>z-&{ +|o8_%+VT0m(YakTb#73qDaBO5xpp%s8a2z=x5SfYDWUFpc4ds02O@R?_*-ksq|u5v<7a-G1|g9IQTD$Ug78&ld5*W=~5v`{pw=Bapx%-MZGgFd +!#F#Fzj^v&DR_|FQ+<5BH8)fskz$lU|LP+xI!-K(tq5VY{;iUMnxBu>xmj@M;P@g|gSo*vitS==B%qY(Nx!C-{!z0YwafiOr`u2lDBIqN6sKmYGs6=YDu&=}!g +v?jr5esdoaPp@@)LjSx`Heh7(=hrJ8PUq{jPd%zOhw(gqW4Q^AuzBMhrPpC5zMFVCmNb79$bN7H(|Y4 +s@>*~Zs9mh*GbX^jCaBLCMfR@kqlc)il!d{Y3G6EltR@Z1#PpBum4wWk>tEj5`T+=BH37ZHV}F02{yk +3Glc&?PV&H#~Wp7VUi2geSo!@dbTfp`^DHERT9LCW=R$yu`*&zZ;%1|Pwvocv?rA55$SU~PR_Sfqpv4 +Lcy9hwGpd*yxMfd9QZBY)X)Q)wU-DSZbQX&GmUiT9OYr9{i6lxi_@q+hhZHIRRk)=&)MvP_ebgVm1#2 +Pv!M^CNzFv3C#z{IA&{`M(=v&l6<4bUE0G76Lnr81Q{$y8Rc-EP*tp@uFay|oW^XZHl(;HjuW-QGGgU}|5wr +Kp-tC|LUjOpyJc%<4(07u}XA@Pqm8}jXPH0jmqxt0O=R8&bZh#Yk(Dk)9Mz8Ppi?Pu#6i}uj)79P-FJ +RI+Ek^-N6f_;}^)O0l#2tXOk83Z8m&x+J$fm|1s{nrndmWUjfS+Q_*Yj^m0}vVlc*0NecbdU+3Yojd5Qk1{`UNXoeZLZakp;~v*$my7ppnhhjjr8G-}3aec+V5feEg&Mzq=aSt!`=}r_|Fqtr;mMOEtCGnA6m +b%eYg)xR{oK3(pIyS%d!}SJUiEv+L=6}x&emlRvG;lr-CRePeF~2HYy*f+z51SPs91Y}*%9`y9F1etn +CI&(&A)4!Q&J&Mx2_AzlOxdk!#=C>gkP9tx8%FzDJ0qOu65&&;GswRCV?k-_A}1WUeZb8t)erY6$`kS%WA_`YtxOU1~H#hZFbY!fqF~I(Ax2PQvO +Q$fTuH?U1dlCmQ9||uoPTUrBg{ooK~~q1=}&8E0`@xP=NsbnN%=?gISqYbHvEmo|H$^cnPXdDorcM6v +`WRf +7clEumxwsNK$Ww9k`wsuHkIKBYah5d^caWiyPu-R!Z?wl>M5?I3TlA657jY^Y4?1g>+q8%q0z#0Y5f^ +LF5*(fTDh}EwW`o$QwNYQr%pwZ+C2#v(>_c4DHbxwPT5l8WJQ6{X^nQh(l_Zuu|{(WZ>bZ +;@T^4St5VU;Yhm&LsU0uWsNX1FcpM8&MPPs8S66T04Exj*ksXzbeA?XMhJyV8g8ESB!Sk8+4Ed*R-536cFJ;NEl~Ag?_&!fg{iF +j2w$;V$3UYB7MV&q+00^6Y_QB2-ePV0~4;YveNv6obPnWrPA(DvMmloQbhTEH{;(b +>Ug$p2o1sJkL>=uZh +glgC5Pg%ncMDzb?)2g=dbWxlL-SKEM3FLRa;lxXUt)juhsW-KA9yI2X>7Irwy7uUfhEoxC1U9)S#zV(*;)FUGb$eD-<4_FZF4Y8FFNe~H+a^|;k) +jD4%uoR_%t0J05bS8j&3m5<$9ojQ0Q`S_SI~%Qn1Tz&##zjxzN@z4XkyHd~BZ)IgH}`X%VMVTXcM$s_ +~5u$@V%JsJDk6&GYWjz&8t6Te;$^*OE!uWqJ%l&-A(X&;DA8He^8ro +rR*8^(m;-6O53)>DFqVrqdh!i%f^BM*~?#(0kPW#&eRnOR$*b?dmH;b +CQ*GMqPPJ~F3q8gxz0Wji9rMC^%a^3gibB?qvEXfSO95xTxJyg8bqL!{j#pzv&$k6GVWoMtzXr&z_p< +u)RqGlDww`t{2#xrO7G)9m9DEqg(yaPO{&1aDtJrNpv>L45!ann;)#R2YjDMY0GIE6g;sr-a;qGC<*C +5T9+9hED&ytw`fB*e|yQCJ-R`;}9+h!!jPW9~uyq3q4FLZIzi}YvgD +wQHi+4%-y5i4<`%>UzR@nnHO1UIrd$W0{Inw0ceUSO2GEG`w#fN*zN+sdaYPkkE3<&#%cXth;9Xrwp8 +R*zY;1}`rr%j0&4dlmVro>&$k08_X_CGb +-CoCYi|2v5i1VARA?oH>>wjjg?GU-xPH1v$eI`ya-}3nPu7^>i}8*?GDS=dT+OZu3oU0LeS=P;xA?KkV?NI1I +D_5R57HtA1|h;IAKt@i+#dY<&J*KFlRrc$9#+C0~NvxB4^u%yCYU{Y=%KPGdw6j_!mL4QojR0E+9rf`sX6|t0v9hQ2Jv2y%s +CSpUfVTOB6S4j-VH0Gpaz^#Q&jg@@;+m~SG86^&$QS~2z$rsa9 +#qhigQ~Y-KiC_d5t+m{f-v97sA=Fl&NbNP(pz{gpn<@&Z47}{572;)wu&JsW?&&?vuTLP5KE8)?uYl- +~CYCQ!m`PYZMT=aAb!*gROXuPBIHtloBOmeKR*n1xMs$Ce0xRu5B>eHI8r +iXvap1sgHzkMUqS>l``?_%F1+m%DhmjLZIWjLSj+q0pmSm{P<#S-vGNvt_B4js|aBRz%KvT!w>Se4iF +-Vj&O)t-MJafa-sB6{i(2evdy~Z|i;W%DM3(jU`w&g;@BW8o;_@zRZgvldqOKLtHD{^Q0)rT*Lwfn9n +(_G!P28&erJ?4M(vdbCN(91bf2oVp#t3|NJQ}dDVTq5TiKG9ug8tt40e5rG5Sse~$$N{3jABI5_(>*} +Vdm`aY@X>8s=-%6kr~@xuZhi`Z>RYrg*asvd$@3_XV*C+`r_E@8mt>n}0}?|BaUr$I=Tak2Q4<~Bz}0 +;U(K{&odlj{8?pKfg<6>4dnYuoeEok!(lKLR`l4IL)QXGcr8lZ(@Yh=+5;Td~sJC0P90_l#pB1c^QvYK;tlK;(bABahr +>n`cl=CFg?T*+(Qkm^b=ygSA_YFS7(=0r5c9z#O +lxw+kINu0ha>2(3GyWhyRW#pALe+EEQUlQT@TF^V*-+}{T{>koUjBbOOCf@t7-3BX^y>q@+x5eBDK|G(uOHD?pz~&n +@$cCMLY~J1WsZYyJCeME-a}I3R4mWS^VcPF{>^A&Mxx84WlwCfTZuENAF{A^Ntxy-; +R+PUX4+GzPE~yPrJ7NA2?xoL4I0Jt7w^k&iJZ;0Aw@Zuo*1`MC~b-|cufU_M03-dvhZSgp`>C!c7wZb3jV+Djn;?MXc=GIt{3=aRfG+Z`-9W5!fr~)fw+sX> +$dCVWbN5bz4Q}k$XT?8Dsu0E@=`R)?-S$5JmO>@xvIapmv0rwWXH+sPQ5a)>u=TfUd+K_&vLN{*q+Is +wuxGr9kOx7q09kr5dRF#X|33a#0tOz6aC>sMDyFZh;%(^$g=HOS)dS!&HCiz}dCEQt4_Ij)WqMyWbwG +oaI3Cvwk2NFcz+lw8 +w1P*4*GUl?!JB<=YPU{`TPwo$_NA^0dQ?yY9sEd^r`#`_})(on`|LbF3_R7TcRxLp7yF96!$Ze!l{Al)pSADm7~v+z;&{>bMJC^?zy6#2v$G16tFk+-eBCnjLvQbx5H}o^-2>VRKt4V&EC#GBP-}laSylDi2_2SH&pmW)|FUDvSN}JD(+ +5$HF0-J%6Ic2Pjr2vQUg~=gW8mpoXhHlrT83|Dx6z`6`~U(E1!*JWn#he#)SB)zbIE4F??1H(LhvS1xN&4vcI986i +4Ykx7$dP_>yzhthPV4k!j`Jetz^+7Y`PCF=cn=FKpP5APfF$~!YJdKU?z7bo3sI$6mcX66!+Op?>If;WNyS#6^-UKZcnZNzx1Z@ +^sbe0#>IOD*)pEgUX@`fX8wL-w_r)U9MmNBRg3l0=7n(P>Pd)WVi4hvE8!aHy9GRIm)*AfVg%p+0X8O +U;*+Cdohc+uFdoM8j5S~_q5R4%7{;O2!x0?KCDxjgE-(YwX`-dV;{( +$9!w#>yu@~n}*kx?j2hlUiQ3B5M_aX@kat=^Tc89~9o|kD-ma!)9P{5LR{2^{thzrP20g{;3~fUCx`+1MW)GkeW;T@3_!4QDAB#yMQ#W{&+v +Zuh!%gS9{nQFGPKr|D!-jgu!1_+LD6_3zT#`_xPhvreG(2%G5wQ-aQeKf08k2rm=}ghj@Gt^}-QV2I_ +O`&dh3nlRw{y2NMw-7y^2Jr!-Wj48iwatf&-TP`;@mQkJki_yRW(ZBfj(ct>4XNOxufi3umZ9-x;pC# +q9puqLBhJ+jMD{(`6U#TXfx>{qx`6PEa$S!|*l7CuXKE({+sN)RX3GUDqY~D4MxHT;d%E=7QC#4o?u- +QhcgspwSOP?LPvjJ}x2#ACvu%oDSDI^O;bIXFg>jhp!nrq>qpL|c%BJwStNcj&vjEb>$2LccMYDQI`PDT^LSRD&K+In2GHf +=w1icxI3qtXIJ%h+SaGR08T!3+Rre*tGm|7qV@|2=qU4Q!q@VEOWSulgIrr(TSl@;*UsPk0lnf>0&Jk +w!!Pyf8<&sIInw|TGialKO3>_;G~)S4HR!PtuijFRL{?k>0nYtE;vZRrCX=T{nCXUmd->RtAJJLZ6ix_-uku8N1C|lp>s6t33p`aO44Jp#HI9vN$R7PU7sBv}c?~bs*Fs4w +rE>rW`de_UPef^{t{I?Rq=z?N=Z(}e{PJXE-2GJQdv^4@2Gzm``6^CX-(qtqU!xBfnf_{{Y;%>z2(+- +->3c6WmO;Jf^G`!6<4?S$jaM*%uj%kt|1y3R)?^$N>nt&~NB{(&L$4ZX*km{P>@& +64UH0>=D6TTt0SJUey$?9rXSE*3$i8Z0wFq|3AzFdN$g>R|tTR*>~5|zDr34;3X(~w-x?2@ +PnvX{#Sv{bPEKz*6k_L0ZR9G{U7!Jo6GpZt#>iypr7F=pKI%b-rO_0jY113LhKiy*k`koyDvXFI=~+9 +@0ZuJ(3(s5y`f&0vt_1--M_K86&$XN-RI)eW8}R!<&3<0{W8Cpt8`dBRG}C=@m$5A3RmMU69S4kY_L+Er9uMjJuUbY!MscLoxK)f9a~3 +?6Kj*d?Z>IR7d@M7|FQ9xc^#|?8`K*jL76?Re)6mrl`vUxW5S~r1gns%BT;}KPZLK#eHY_lLt))$i;n{gsa@}D_DP3NmHypvwlDNcb~MaB9-d +^eYVfsInt*#idhRtj{8evz`UQ&D%>3)(e|UmlQgd4$QIOOAWR5e`0}(MI(pgAW8UcEnW27^<6=CjoaP +$T^gc7*X9oMhD&G!JcZVmwv2>Ib}6f1pF^~r8$_U2!vTI*-hVdrt +j4@EJ9E@~5V4)8*^2d^VAFq3*GZ#(YvR6n)zSzYG1`&*;y`;Agn$ig1%2)Igk +rSwsDSCiEGDn1c`{sXHF<7@=>=VY0wezwue-60*|uIB9xUe5f}0qSW+C=d)L=Rx#2nucQp!Xo}TdyMB +Ln9sleU#ZIV)wMqEp9gjJHiq+iP4L}I(+XvTulEVc6CzUISs +%JEZKWqu=tF*jF3Wwt=n@9t`Oj@j4K`;^(WB +RIQBA+?^JpQj+F%hVUcGpT_o#jYQ)MF>By^b@$K?%CjCE+K2@`~OH|Kw>ThVd=b?#`4mWjdFzSv3%doR~LLcm|0r)RXrf5c2T;(AAM0;-usD8|3Y +fo&Glq@NY6uAMSUle!dm{#Q9V|kF`J%R2HazkT*^kN6-sA5{ +(9HGWM!>3ZbZ{lE3N_tI=Xaua<`!tJIN{dneV9WR9MD%oelWgJ95eKpVLcu%N|cT>`IX!2pi)d!~Jq* +l@1`?862ujr#cw)#&|pvb&*7&YriSe)Std%MmpF~Vi#^RfAWVuDkbh>g*nR8fJGMALwRk8~ld$deva!EbdE8I*$~Fxbp%dGkI^gDTNYgI#)cGbIdPlE0&ec9>ncENuaT$xS41Xozs2V;G~v`x +}CLK=a+)sR{zxtXo$VWKdmX>WSUsR|)iHk8RrUVUue?V_gIYD%XM*JoCRweU@qa(@gHO6p|;4_dCG-Z +3tkn@ptb`CJY?a1tzU*+q*XOlx&n6CL86O}L8pi5g8QbaSvGEUrd$77f#a=aAtm63wcP#=F!Tt5;v%~YlU--M~uolDdy^8c{=70c{<&|79bDY +t08sACyo# +WiaEFh!Sj&xScnN|kw^bw>Os0M~!ZuLgaP>fsfNElL29KJZCNDmKx>v>Y(vVT6gM%~2$_L_!}4C%J=` +%>HCDmF>w42a7tpKD>E?`oq}7Z%~S*Kj*JSZ6?*YjtM?kNQaLT(^a_yr^1)0r2xis_`ck-(9Nh<_mdS +I3Nsi>(7jLdp41Ab$n9djQ#O3jl*1^Svhga*`lW{=wKaLpkD=Gp?|2xyY2ZYPS2r|85k0ISknE>?A)E +d+^)zr=T!twgWaf%_rTRAT^KnkKS7gH0#Jnu$ML}#c)A}o_wA)Bheh^PsVdu7gKAJv<#rhr2!c4I2sp+p7Zt&{y~$Tnojs2l-$U|L^;ISx +>T>Cj!(%EA^_y8!Sa@nWtFE^Zfe2TVv2oV)!yew +AEm|Tu%;dvc~K`9OG=Ym^t+t9av9&X&hY-4PK~}x7sWz?^QR{Vd-9fgO1q(^{rl@G?3Yq@>`M6uRy!1 +rHwM(4?~j(n>-@=T3wc^^Qxx|SQ+nI%n4|9!F>1Y1yZpsEvU3v_gQ+sy&JO91$C!&45K;_;I};ajPv| +toTyCG>^?JT!UTBHnSVg*$*u5(%cjSqfQd4nWoz}NOt1wrB8&wP+818Z*BbO*_-8rm(W?yG)_^g?ztm +&d3YjrtczLw^(SIUy0Fv35q5m^RR(`2p%6Dyr`K>|{;gtbIDZEni_*&zV*LFm-H87(7SS)2J);Lk352@O7d1wWJILcu!4mUkBH-(}N!?U1soY=IVY^MTgrxmQ?hdozxD0J8r(!ThiuT0pTGkt~~ +>IC#ic?Y>Xl99UJ-@KKa0N_uDyQtqX2eDtmw?NkciM{UY1$gI}buzu~U5qZzZIsh%T- +-Z=pP%3uqnkMS?fCd@t6op_m0HZ;weR}r1YY1-dt<=KFmd_9d9`uU$%4A|MX#;JI9j7=&>LM$P?NBrc +5Kjb)oF7|smGXx%t-%i9Y}o$DqG&pKsBMua&dwrfkmp1;B*5(I8?6}GE?at3F< +7%;JOSr_n;o{ul{Q}TW(_0hQ$YFWP|n3>E+o>&Xv)H2-Njnq7%Y|tS;wm`C&V@%Y!WXy8$cULYC_d0O +6363NT~HRQL3cMGU#mnFjwVf#%V|{Lzi5ui;l&otnzvm37Y{@K#NwqIH|>*?X0jSLnW!w9Eo8D$76?SS>3?KB;Ht7jx@{J6#TvX|q|Yu_kQa=0jxxRxc`Io303BBtz)`+CCrmf +_b}fq{nx_?D4gFZ!a2L|<{Wy0ns{#)L%&1y$`xS?IjWE48?Vam4B?$V+w>aUK(< +7T;J~8d{P5g9amcK&CyB1i=oaIbOSQC-H&IXl;8k|wW2nu+H@+w>uInAQPRq?orYUyZLgc0Wx#c!wWx +}s(p$)EFW?>Df4sM_|O$RXfv5Jl9S-tRWib8PA(#D@XWAymJ8|}4#_sD$B7tvGueP*z3;-a7FO_d=K7 +A5v38%%$G$~_Y_n9lfiqH=wyCX4?3zV^~uEO39c#Ypd7SiPucg~KXZ6hd-6wt#Q|7rz}^g?WSXiI?t) +nhX5;1W&A6lIB{+6_1gA4fr-J@ZU`};iS6HM6L@b6aiGDDi)$?b&6|Hiv&3Sf2hZ_$GM&FLPHnwc1=C +$2(YpN6Mk_i=#^}@*Kr}E^FKz}hWP7TMBMz!EFBE^*x4>jG|kJet^)hHJhg6mvegr5Wh-r~@MUEt +?Dj)QU@XGmxr3yVnJ*wHQmG|1(edO;%iwRU9}{h2P}^jgiK&|>Psao&$lF9;U_>JPk3`O4)j{vigBQ5 +4ZNs?e=07ULM0pNiZ7VUedn*cnx)u*qfMl1~AcyEOybzn%1b9ThBNWqVB%@E^@6LuO(Z=2qRj@KK6Iy +!3%sDx0hOZ4o~}zV(J1-MBo>#VW3FAmvj)%Ey51tw*EX9@7Vf+Cp=;Vx3ih@m+|sX(al8i0ts2cLlfU +#_`e^O-r*&J&?vrS&eU0Ye7qaLL0Dil_G!-Xs|os?Hsq|K0v=ZeI%tExYcGxw<>mQr5EGH_kW*5#)3r +x;cTJe`EK#Ly@K0Z%Xn34BQ##><%K|4R8vpwJh$D}#cgb;FZ787KsZ!5Ht=ei+jzr7uS0$2!Z-6lf8Q +T-8}JW|X`ab!P5S^GR4x3f(G}BII?uBY@mysb@K54)U2vZmhE~YYLEvZX_) +%Xwv)X;DwVZ7RkV6$C5Ek8*-iztAUw8=j88ye}$<5hKP!CJnUmpug2hgg}vthOmDna|wg54+!|BQW>K +76Y5ff{ctJx~R>3>z@Al`C~8iD#_>^*=LUoawL9fkI9X{M7plSu&vU^CsA=$O6W(@bh_@7KYZFKc805 +n}08hmboZPluP1`G7`9-dBD|YRmQEkGnB3R#Pu9TmokN;>ni)Iast92Ef?%5=D)|@(QijbFZbl@;l9~ +1PziMUHoB$-qq&xA#Vh^ttSx~6EerZ{!#6hng5)q(XZqAP>mU8rZoE7Y*FWAUW}i9TVSSw%mJWIwl&7dg|H^LZw1jtFKJ_>DYPu}e +jxLJnUc(wFgR6Qb-sCJ!D09OY*M^wcIZWqg^*(1e%>qN;em-(rn2pzZI53^4-EWYB|KCofzIE!IEcRo +}J?$D*^`f_b*y=tI`j=^7vq5usBx$ysQ9mEULID2{9ZmxceE6B~A>v(~=kGF@urIgFxBRohFy3jH%P5 +hu0Vu4;s{+-7uZ>x6(X=wQ@8{TW!}^vC~Y=eZl66@8m$jx*W-`VMBKGu?W*)QfR$;^r0b-*07&{fgF@ +y+oDzUfx-6ONIbvS3|4h*$kTF8g7912!v${cwb%}RTHXK%=*cbTWHzDKus~RlbpQiwJpwoeDg%x(*pnT?qj7GMP3)2j7^*XR~{xKBBEYP?yn`4fpiSafTS!NCbLLWgm_nqpx% +0mm&eXtHwkzai}yI*xQ+$D1~2u|_SyIJ`a`T~00W6M`M4o!PE{1&rILU!DDl^8z`~)R<84cOpG@(CANMgM`P$#0&wJIUrz(w0b3Qmw%c63(F<`+naO_G5&H +KQN3whadu~b`h+gMOJamZrTQ~cIh`)SSXO$?bw@O5lf;XBr}I?|u)tBkFi(*hAYvR|SbBbEqg74k-Q& +F~#dT(gN_(R6Y>RsO+Fzdrc@!`E=IhDWSxYM_-ZyI!Ih07p{%NHYz+fK`(1VR{L*Ui8K+?vUsI1^1bw +%^*;dztH-?&l*}vu +6o>A%nM|Mt(llC2Vcv9LEN{${n^xn{C{Sv0qavB*Rrqa*R6+e@Q<(KM;oKn#JX +;*4(9hC&49{+y4UD&m-UGD0@m9}FYhzhzbqoZerg2nO>(6A?{!+VQ*OPbef +!h$y#VcrJKtNnU;{m%FhfU@wd5t_7w)PZ#J)c53Y%`_qEu~QU68-ke^NxmkKOevG8M|zy{vJcC#%v(B +<$!_gRv0V0nm!dN(%x#xbc_Cm1d#^>>ovh%z1s2#cV6IRN{_PmcrAgwe$63kY_ic>yT$zK2ww$oK9&BA}te9^3Me +=h@p@L)8Jga33~7>qQLGdg$?QeLo~sLLx%6ky`aZAOIa)nwUsk_?eAsF5b3kF$W-|^w1XTGBwSBFX@2;9NFfi&`I{yBv=x#>6S)F_Mz27`xR +X?%mjaTU#pc?er?_;0+&TC)vJckYw}fcDMMfe4O +kNR%MUs|HDooh7_)z0LZt}>@D0L_!8^`6-(Wh#EBf`yKkQ-YL$3%w;`xH?4l~DZ*%PGtM$fWht&eF71 +R@HR<0Ri|oX16$TT)B_2Nu4Yw$_Z;FF5=1NNt +7iXpJqt4=c;RijL^iXk8_-I6va74dWJP3USBU2FPP@6NfkS`m0wD~)eeIlA#YE9(hV_Sc%mBKV(lF`J +uQe^W?y&lspix?s(R>QfnDUZ!`+Zk3(31-;HlI|5z?y`gZSNiG}%%mFLC-3HmLbU*+y2F^dAw;6mvl^ +gGGbp;oc>B+K~EkK6PG6ce+mAL%y&&d1m{p!PSQ>pSINVHfdAOOW_T&}7a6@{uyAIf4=d*$}mT83!tm +Agm`5e>*BC_SK`(F;Lc$QhJl4N8Bg;^O?&tf1;0(e_g-p==HD~=#}~o_xpLF<`T& +CD*>nvi%n6Q0uusWxe5QvW@<%{txw6)(r$QJRrXh`npOM5Wpt@ypRgNM8wN;avBRB|HEm$O#44G?VeN +qTx2j1Ek6I~Tkpbv{ne+46ImX|-y3G*uxGOiOkCYjjV=z~G(L_|NFMAcIPxY}%;I5A_LOgIg-PUmjI0JpNuNVnx +rRkoT{p;Ha2K_?z(V+k!Uq{(z3;D6YEcti@-1satGk&-%=ArSxT-~UfMt^U`)|EEXwDRZ4>mGiLPKsQ +WK7kzN~p&DE@D9>h@<+$?!D(`k|MQQK-{h!VsZKFf;+@23MS>l(XC)Nc;})0axatApH<~@Lf4eb?*3O617>Q_oD87lz)S9``M<5CH=s|ch4L*j~=b_1Z(BQx#w-{-B!@ +iI_F0_qrUK;^=iQrw{D8**A?B|T{V9{BM1O)Y>6_>u~&2F37Lw#yzsPQ}CD?l{dzI?5y$h%yA!3LHVu +yLWiss89X{N2QjHwbJLd1Dwnu2r#)O<%yR7j>fLZ!dL>%k^fnS?D9r62cL%>7{5N>^|KwzEOzUU&f0p +9jMd*VG#8)-uJ1GLEOXC@n{I`BrH;65Gw4(jIlmyMBCznf<5Lc>Eg*1iBJ)ggV*0XD8@#!d7Im|o +!>7+~GK>{XkSTS!}{b5H{ +e%yOw(BVNd0|L-jxuqz+H_NW0As8?a3LC3=1@wV72aBFfu&e2^g +rG+;$w7u95E`+ec1uE*{$_3+)D`dsHc@Q#ue1gXt_Se)_OApY;pI%!1XvT_z%Z)TfQX;V)Ffk>FZkjI6 +PW%6X&hhS1t!YK-J)lNlfk62#CHttFd9hl}ZZVQ2ze6AXj4)V5*Q4NI2@+^0?wTP(!DV+VZ;1xp0vzE +QbeCtG2GK(sT#iiffOuJNXdx&o-{&vr(ePUtL8)FzUpEiwWiG-4xLc<{}Z*H!1{+45hn*(iIOhWcp2N +Ny>#dzv)*N+^~sO7z{5ap9d~-WkAed +Y8w~wX*9q%5?E-5(cu6s*V;$u;2NHOjnhP-hTs5~ryYF#XP|GSTf^W(!1DR<JSiB<$Qg=cW8y{yML(s?O1+ke|^ ++*N0+cG;8H=MM|6w9b^s#E6A|6A!S<{F6<2`{3xZQN%zpA9exTY^;e*;!bn&a4W_KAyZ4D5;fDNZ)B_3{x53ds%bwJsUH4yRhiu*B^b1s=hlKkO24nI5cDUw;Y@S$pq8G_^Z5E +NL*q88)?7$V4KQSJk$1pk_3c7IaY@qvRNHGDOU1UUS{{w`pXFHo`wNR-9dZ;4lvMbE&vEZwhyB>-hP} +*EeGPwWH1gQfk0>^Fmy?S`?wA^vxRd)0@#}}?ai}aH$1kFU>E(dFlhe)7T^~eW_h8M)wdB??b{pM!>` +aQ)1{0SD_5o#t9Lsk~=^Ky=W{v#%+eVfWcI)}O$wWB_bX$C<6t${67Rz7Tu(`t +{ezB@j)+&T`=K;0VpO3%{4%detTAEgOO&ugvvJMtW@v9wT6;QfmJ%_3;%4Gbp~Z>lJ}py?d`xld&!8E)GwVbm+AgV8_0ZQlFI4k1h-XvmF;LvxW(*)dtGMS}h5D_Tygbl4s#DUNA +F!*)%HqairpF0;V0MB8&=fwxp}d5S+`@l$IM@0+eqU`$)@dbX`CRJd>05-@faga-F7;a#1I`CM!1I2kFr(`WHpl7_7e(v^fQy(7u}e7k7){0`fP;?*8r2dt*@qdDc@IRM+so`$B?5u42*W`%yp8_L)h#83R +Dcb%dO^H|R=f6qHlW^@pmsNST&MF~o@upHPL&11AWkRdIaZI};TH*_kJ`+1^;91Z0I@H8M2_2Yy^}I5 +!}dUZaPt*<;n6#MKT&^F^}f+yPok+er-u?{exYBA<=23IITv32Z0+wH(JGCl;g{CxdCvfW2wMCs7qUz +!Y3oTt;fUO}qe9Lq?>+$jI*#z&P2<(A%|0DV4{)zJo7W(_b`_^mzmc&bAmIJ%xOw!HCTjVmVV=6UGFr +X`f_?nT9qa`&EhuWY-^RU{71|Dm+(*R#dF~Xm4)nZ^ld3Dqb9CSlFMyuaMMdQSv`3o~h2WX<`{>B+gf +CfGVE0{+H~MpV55&K-#Y!KV1_(rKaEAnWA@Ad2jpcLzM6oASbL4!{@i3+ +{kpT0$9i5Oesow{Yof+U$pH(@$K1Z*24Rj7Bh5nY#x;22ymSHl>JE)WPn+e;93y$@_stMb!m)Av)H&K!UkBQ)-Nnp%Nf&3?_k%+ +*4EU1I631#BF~6O(MVoQ%zqv*2KIU?3aZL(LKlAM68ZLDjLxme^KB%HHe&1_JVpD!v&~Jp!Vl*ps!Gi +k^oOs91T4rp3w|Lf&`dO`)iW^`=l%B)E_3pS+2m{4I`wBXY(^iQH7_CK|9Y0qPWIqXG7KMXID}mijn^q-p@dBFnH=Qv{m0&9v>Gtul1IRN&Xzu597+U#ojy6(LLHJ`)FU`MT&KxAvL-MtN)+QavC +L7S->Ee9Ky|(A)a@?iO4j76^-$t~~Z1&xy$v$q+q!0Zq(9!_9+$y-&8U+NkLab2*cFo@qfII{;!m&Rk +(~O^`Ux^vYuuwqVCQa@5A16x?ac9_wx^PZDXi7skDeLa&iaH!OLN +X?NEuwi`{)baCw>W@PXxR=KzO!}JjkjaD=BLj?6nLTHK&nXpD@0#I~0bx^C>i0;V?1zUq)@sL*BF +(l@%hy3y7MpyZ7*4+sd^x!iGYUJu3ZTIEHS2BUA_=(FHJBj)JSgRS{`E!W! +Mwc!B^4%C4kEr>Si%NHm=&$YnF*%g{7R%X%_?ODJ^wa6;6{Rs+73LyyXSk_16gS(aw@PTa;8x0On*m| +b{n?>eV`c=g!b#&tZ&fLKQBKOPfrWhRS_Auu*<4G7pc3c%QoHfgk8R2^*qsa)-0> +f(2XW;3w*tZa{KFMy4|*0)AdyWhcE{7}d5D4z-R^o(A}P +v%ad2$tU;eVsbj71VIE$vpFW;*J+6kq=70b0FEoU!YKIe=RCq!tT$^oj=~46i);Xhf6?&3;(uY|aY@F +|rWJ&Wq372Yy8B_T*WmaVU&Cj8V+Bal$$C?P8e2}W=Oj6%Y|E>&o_=vi@27!qNY;-}J*SOorm3YYkig +giHqNe+bGg;3SYfLm5QwVS?Wylf&Z+nMB<#!lT5pNm0HKjEP=P(C^)MyI`gS2B%3`h6c*^DR2@2X5Cm +J1}xYVR&oM0zp1XY-5N0UBopcFeRV{pO6;I<(kV&}v$g(Ec|#W7_-p2k@|*AmR+WD}@zvYCo7d|=mVttA2c_|a9I(xq^9Yx_A!66+kT${+Qw2hikj>N>ga`iG7i +&NL6w_p!|8@?1iHwICH1igOvy;XQxd*Wl7!hdeJIAr3ds?vQK*!H$)z$J-Y?{XDIjBWl2lawfz(@6Bt +V3R7w+`MK23njsJtm0s2QOzrmk^)A&SPam}PwZUBn*g#5sDf6Dp3vhTFAOMNF|NEHSJx~v@DIfHAP~l +G>OK{tHKZ)>Smvh^@MxWv%Y7EQ@sTO-as7qAx`CK1Ic)Ej`V?iwy_n7i41E^S{?m!6Gk$y}`m7#>nv; +t4h1B=)=JY}!?n8gg^uKJk8ynkPl->G-~K_6Hn{npbzH8>Oig~i|hEib;r5PkN;hk9xoC0d>gZZ1zF% +c+tl!lFkaDS95pU-b6M@KstM5IuUY_Vu1;@wajnUqE#R0bx*F&!OXbNZSS4{^x)E&oYH@$XzK33dCUU +O0#-5mMMht2!u +s@4sl8-3_b*S25NIq1L4peU&k!{@1+jwWAelqVO5-N^YE_{!GF-mR)g5n*Cet+O!WJI|NdXh3zqCcb~ +&V=q+d@iN1ytc2arV49!N~ukn`?3%b*q02H5Zn5E_Y1#`Z!LtJqz-U1v((X`ySWUJ(e3w)GE4iFKFqj +=0Z-11z=lIKb8Rrli7S%BTB$^Ens?TnH&x)9g79^tEUAD;%usvpE?Ms^ +xCpMbO54A=CLjzNO+W8ex*P|%Pr~Fj-e4t#1c<7S^=`&09!PM9U4%c~hyx7|G=}r`UoSj +9Vc4PDf~sG4EfcABuPC{Dkyik&0W0Uy!zlef{wh-&I +!7Q3Vq?QsCYK(QXRt34ro^suR@U*@;+0{j|!{TLt+xn_m +llqS|*U`%Z%UoXnV-OSsghnDLifpDOT| +FQ0ct{b(AM=bK4{U@=&hYNxFpE#lV@8DLi_ +01rbci?(nF$UZRVCvtaMkpd|-vD{T_PRPgkUT>RO +^i@GExl-`dh46tllpLMsvGJ(dlLNscC&!6%bS +kYTLC4bkqw;_jb@0aLT0|BbHmyTR#-X^fmgVoGde}__6?XEneJxLhjkAUdQ@5*VqFYfP1=qG20U=fj38nYprO`59j5qed`kxfk>t=zo^X? +LG`!HuhG(%FR5>a&Tn|AF!eJ8(%N9L2#EP^D`wYAT+w)@}ud5@)gfYt&A0 +pGY!BC!yYH(k`D4b`$}OC0|fOnU?Klp&WZh_{;A2;{8CTf33-sO67xbZXs&^nAJ)DuW%YfO_o~2S!2< +kJY^+x%l&kqDE!GZzLmKiH<9+4PC*+7e-$oc%z@|qJ$0W`NTvl1Ku%5$Fdb< +)JO`r^Mpera%-$YKG1S0a~qJX`Qs>WrR3+z`+@6nCTrwW>cZ +Cny#12bIhdawlH|<7wC*>o;Gf2n3)5QKoZkv)!qL(lEY!|My&9wFE~JkaIucan_gaqWkm~yphJ&Oh6c +v%5sCm3v6EC=ifxOkb*#@t(}Cw^d1Uj^_2nu92 +k$M*tS|yq%j3GBCm;Fe0y=(BACbaz{O|^0R@~$wnocf5ObQ?_O8oWdq;GwZhSPg9^Dmq00eu4D-)yLT! +{aj9=q+m0dk-+LXLV0xdj`^ocsi +KPyQ~|KCEXRZJGu@-z+%r5P#D +5!2igW65_SuH12YX2sazgICj8f%;_y7=#pmv(L5XSV>CFB6`+e~n0S>LJIe`fkXGx{ +|GQINZ#%2ec!fjQh#TYdA~{67L(vBdx7MQ=|i#Gvi`bwZUU1lTzHg~IV@TJgaEsOdz+UZB>%lxqtPeo +}|)!@ENfdKCyoUHc?1gW+{>J~_|Qxi)Dm5Qu=LP^DIn4$?%Qf$6O*cWOs)Oo7wIkG3@{g0*}sx&bT{7!qd_dpJPtrM(@wQ%l{l`USaD@sFBzG7`{n@a3#W5Lgqi~E +bf-=Uzj9(eSw)p&;s%aj+Ze4UVPqrss@oB*Rip7kJ|ODe86z?ItXIPV*jc)T&4;gu031W>TB(S4St9B +<=4t2AXmbzXDhLf(BYE7h*9C&SScx%&vz?+=%jT&n!6yjv)U`T`IZiEtcxVb}%{5L{U6n?MaSr7RS=|+ymk; +!v1Kxo8^VQ5ZnoKWb?Rjh5T +lncoL4V_B&U@+0eAELVj!Jsfx{@4Z28+N*`k3q#pH7Yy**A@^rg=6Bf4V`;&@W*#)B;kbvhTic+}1JU +6%Q@a}EGz}7Hb^g0v(wh>2uxQ1FXYKH5J3cQN=SXj80g)ZfeJXyE5*H(aef?xU+bn>!!?m9tIF6H-gt)YoXfgs7}Rxx9L9Xe}Pug%JGgDy@Ebw3`%NrSrNt +d4m`~pJF;}kZ112-qeQU%am!AijIJbWXcZiY1_X*-Y5%*MT!~s4f50V621BE><6y^%PwqCf^Pu(hDZY +}c%p=|LDm0nszPm0c5wJ0XB*5Ivw-r)3O2e+>3I6x=zl{kgayJP!`1Km<{IQ8{GgUq`iTK-5Y|fcy#! +1E`%Y5w5JGdeL1gWB0^Trk3jRP2a>Ef}zLdkYLB6e*u;Mf*NA~Hw(+1VN2W!xlX2h9q0so%lO?sP-(x +)iZGkhHlDhnUzzoJFv0tJQsD+cj<8jxl52JdWx{5w{X**0Jfu-LM73GlB$rR0O@AaVu605}tv+L?xK? +J$}+hX8=RY*vkdE2)jYeA%YP6eS?6VLDqp?r3e0GwQ00N}H<=#&U@@Nu{aj;W6Sy=%_%`aw&_xEH()N +VUQAMGM^t>YpkDXfHew@z9eo$TqpYId{yKlWU=opToH +OZYaZb8Y?h0EJHDZDkXY%ACf1@WPuNe>)U{*q$zPnk|GO2&;iFd1n-owlh^)qK!k(EGS@HCwi83Z2KI +;)LAuomU{8B#*^a`jauiKQ|@P}{2P-8CsoGz +>6dTIwYg2BJb)<%Zmo_|Sak_PVShrxw)vO|V%3#-Ng9mRVH}ftN*Py;MJIlj4`1`8`Yl-o(_&&)G%xM +_*bD)V)eF+c{@9AkifM*{kRpo8$v~Sq!R49E@ARv@@xB`c9^b05~aqejl~M)}>ha#j3EIM^2ZC +mq3JLY-<355YI4d9{F3)kRV1|tje&Ih}L4OaWOTC@baU))7F{wjv+vT+S-DpO8W6t6kbM?YpWq&9U$5 +njkZy-j2)pMfGQKpqm2Y!cy=B;AONMQZw +u)OSbzPnxv%&?aI3lHv)c8kufQ=lYLY>?bV=e*x?Iatig- +EF-Z{)?9tMjl%cBCyUJ<+P)IfkGct3ne&)G7hzQhWZqrfTj84ZvG=X+|2!uv^6TzG0(L7W6B1`5Q!+1 +fU$^~bZLz5^JdVv?u0I=gVZ#G@t5BRe&b$65=81FXvf&J9oCgsX%HO*xTAz-60!ZlmO$$XjW6|-T0`B +j?4l5i$daMRlGnGd|^S(E&e18v->{6cEoJx3rcs^=m!DGf8YhXN@IEd%&!O(sG>Fpz~vd +`c}Btc&2?#UyzuhZ;aL02nG`!K&Ctd(z!# +a49<`Q_-jiZLZ{$q@C6!l@Xl1VvJiM(Y%#C+ot?ZH4L8;R*;7P#ax6a4URpS+Nud#SBei4l0E0`>EQo +RHT@u3#S;icozoe3Sf#!EzI;+bofHQRV?yQL~e0BXVRvpjWezF%_vvRqHN_8 +mpz@Y)+FEMRQ)xGZ94H406~a6^*jt#&vI$@Tp%p1!p|1)V6q;0@|K{wo`-hQgT4t2pIYF8scdxj<;4U +|B2RdZ`=v?ggyX9zHW^sw01gm|b)NmHUp<@hCbH)yQ72rVe8^npq6PdD_J1_nph3z+OS +_0X6E|>FL4^-AUpwy_~Gya}11tEjl)Q>VT+`$G2iz>-`@VIM&vJWrg2bH^0Ndx!-4smCHEFa=O^va0! +k!*l4$ez%3?!lTW6}lE$iE`DuKB#>vN>7~3w@{;I&QAgd$TwNA@%BJDn?(JyQ+50Ozm{`lks#=w&6lc +FR&*{mYWK}_9Ft5cBTTV1ph`O+Ce)pzgC=#IdT~y#p3!?YPT3@%_~m_1Ev3pgme)!Uiu}Pu+Hf{7VR4 +k00~;jYr-l#>!DuiUb$a?FXB$_vX2IU1fYv+Dg)-en|#GWDO4Nzgz4N)StR=0D#A8HCyDaHZ4iME#k8>o;!yR{V3p9`Lz1fB5yXfL^fj4^+!e_xvgj +|_&RM-+U143)j}$@Hi-lN4sIR2#f{&h{79o9*+`7=0hddy<5y=hwaC}~etJkOkzq`Lcm51Uf(UD +MUL;QTxBkD1Hi9gAIcm)eeDcyQTpjitNL3F*Ov3iRHznFD@5S6qG*)`7_H(56FI+6^LW07-YK&ztALo +*iVa$n(RvvtU*cjp%(A&4AOLw|$uWH4j4BX60?_W(rEFGE+UDC-^|kKjfB-aOqpe`LYI@ydu9|EF?(c +Bq<+2=xhoi%NE1Uelz;nGRQw5||4hTTsRBqV%m=qzOa4!d`YHef$zQsHZxxU(>z$u26a|S8L?68-dr +bV>7E16K+xfH+w?>A@8hw6pui~O^HJ@WIbT8^|~q4HIxwhEcg5ikc)t5p$CD{Wy~GDiehK97p`;je5O +HvZZiy?a;ntOLNeVav66iyyoO<{CH!)N_!x$Wg7O#VUgaqNZ5G0sdvlPdyHS9-u$D3Lykkf!enu_p}z +iX+fDa2&z~5Yt8Z2q8!>{zHv;T01g4MJpi^_l4_Ax+vK=v&9yw6oL4|-H1Q%3lNzULi=5XVl|FH&mUh_!U|C=T9qmz^# +7Mu7U{BH;ZXNOS)uds@54?A3$PE6NXp;iQhL0OrC9YG{tY;K!8lN-6M +Jd*%wvWR@(K|U*sa1ar%a#CM*V=6y#DQtJ>|vG&ZZzH2A(=GF%-%3B_7WRh7cR1oln4Jsy1&zicuWkb+yHa*wp +&25?Af!l0)E(q1$KY6Z${4Ohu=4?b5fjuq^a$27+;*~RP4Pb-%R9%!C#@Uf2@3g?nv#C{Zv?vw%Wj3k +vT;E*&umA7AO~d~5dcr_`qf#Aw)W7l6XbtwsBXUT)Kd&FayBD5Hx?c|`SM_z!GhXR0Nf`qW4$Wbi0q6A{Dnk+gZ5fT&(l?_M`O-2@mY9=^>sJS^a2Y{HdlIOnynSuaG|M02_%^38O)rTmHX-0XFso- +A75P9?+Vzz%xG9{(ZNp&HY4rEgk^)fx|XANdG9yRn;Z6^yzNel=wSIt8xbfY?R$2lY7HFZz*m(;0-+Cz?Wp33cuI!lTifdF(U3c-1=+b)4N8fyh +JonLD``={|gBVnNc3#YjCG*N +{|oOP&6FhS3yI|PLBJc=a4GdU^c{t+)~N$7N&fr)y+dBvCQo61AqxdkF$B1$ozV +!H?qFIMdEtM2BTP9tMp3Rc8m`2+y_?B)owa|_}5p1wVSRdh|Hal0>F!x__KEuc726zMXZxl%`N~imsQ +(2^rGo?0EzKNt`WfQjNW+i;r(s`K7qAne?KsY<$ts*FWy>jyNy27zbFF2prSDMUr!fqx8B%&r%iOJW_ +M9;zTJ1uOh{+0Oqk8=pu=#T$~)vLF~GK!WE#3hW?b@~J^(4&uD)q;Z)^0{n<9yzC;5nDqY^GL3mX +F{m5{EWri@vTlsZCz8H`SlFgNjonhpxu9YbJuKhM*?Z*%~7E+gY4v>U-?=9V?^w!CAY@;$p=SRTCw +H?L!o!ltcNDi>XpW5rERMk(m`pmw51Wz>kYm>+Cg71@hHmCXadi6GOXP&EVf~hOQf#~pxWx}?}SNC47RB_s +i=kr8a09|}6HhFrsnH-zhc@^P)n_PM}sVvX7=Ian(12Ld5nY$cH4aD55GOc}i=Y)HU0!54iK`}FQt;T +0-Kvf7j+u;gt6RqWYIs2LLs-_#I$S-@T!RmC?}58oj$kH@Bdxa +GV$AkHwA>-KN0%V~JM&qjBi&q@Ds(ho*f=QBWP#4Q2hXXxu +ETis@nwj2ur!XPbfy+u+!)hrN{>1IMN?Mu7ML<1L4pv(?^>exToVvYW{S3I6kaNQEmv3Z&piPuwA}r^#}dkOUb#rg@dU0omU*dWFqC?)|*K}p?5Ql?FW{JOS&$49hfH +Ru(J&G4014hjw<5^z+S1_p)}g_Ek&6@+p&P{^v=<%D9!co6Dlmbg`JJ1!U(u5>kz9$x>PGe`2_6%Awp +-7Q2R>#JJeU~b>-0=N~nENIIDB)xMCjna&;&Vbv(TZv@&K-ehma5l;WIPtwZdf!zBAky~+*Pd6U!|D@ +eC!@|gGw0&k$}h-I3Hy2K^`J1R|wT*G;p{-G$UgP>N4*CBWC0O|r2I;#oPJ|*!XoND8)0Cw%!LNP#YH +O1ct`FKBVR7&;t7Ch4*xkG;63rN|Qh6`K#baCJi8yddIE7NJX)R^W)NfU6Q^g7HbB{SI*l1ovaDrRGU#6HPRLM=jaN!ApW79rDh8RK*6UC0BB$3=o7$+nf*U=yoy +^J47Gwcu=ZCo?Nvo4PiT6>L+#Rp_$>R{8oR1KQfEy2!m=sz&r6V0=<8PP +kk7*&%=Im6A(Eo6jBc=GUi*U_0b`#Rx*C55S%ev?-j-O#{b5IRQ}ngp|+u<|4=rWo%xmui^YonGH&Sw +yT$b$~GOOsCe`5 +WRGYEOA$>=1aVhWPJEO5{AROYfL>=nKr1hW80(!3j&4dHbomfOo&@XS5%iC~ZM{H8drH@%^+Sk|&PXc +>VOGBV0EKb=pYz`)iLMtX^0ge|vn1=Qn|Fw@;%eBKl4Mdgx9OYKNS=A7hn +T<_RSX5QJ)*ik!QDY*Y0x)-raJR(yv&SR;ob0M+;m!373%6-RM +#u0aFv>{VFXGi0;+S6m0dh$hG!$9~aOm&I2qNtV3?uLHy-QE^TOyfn61sQZM0^ZWi!dMQ0sDHCcPP;_(7O6+3F&OMqmKprd7T?~&?n~88Gs$;)9r4LjSjhH$ +G7)h5rzi7dN&pu2prdRp*fB)i;Fc3Y4Y}ODvB$jt7AN2J4juD7K50 +<;7=bV54e~pr5!ZG>x|6#&035!*9$%FYidXg?x^#d_}Y>KFC&MX4gO9{rdv~`k#a7G{ust?E7nh%p}Y8q?+YavXZ4jM*4-zCFjvKu_ndFlfuCI6B_HwG@nlKmAqM!QtZY2yL0*lfu6PT8_tk2qcbELd*Xqyj|E;iE6#_82_NZCi2IW_k8 +!C4b1ZszX0otAAX02}i;n#Fu3Q$u9`mhBIkb9zO#2_UtF?ZC0K~sc^*{E9e_`Y8`_UVD +X@!lxK7pE`C;x44wxR?iL4XYlUzdU1C3e{1_kVNgW2r-8fBsmP+^bizOja^YFJf)y5)gnkyHh%TQ7nW +B3s)3t5Gs^O{an{mk(;kMT49{w_I%NAljeA^ +>p5gsja8UK>vBwtpnqPtQKW6jg007V?V<=Jyr&r{!wV=JRfzW|wO42U!l^({2O=PxN!>lIQoz&?#F{h +`!TFxP5!UAEDsqFzOTz%jChWmfZ(MF~A&-8!nR%yCPqHrAqYE$in2vndZGw$Sy#=HdOk +_af&R|fkxc8?~)H$FN|5XvgwxySTbhq(d;L<3NC4f_5oD;6_O;=0?H@!_V_ut{5*;N4|aj!Yq~vksk~ +oX8?L(GConcFs(mr4%rDRMenuB45%%Q+ELge{?0hvl!aXj2zFADyHwyFY*x+>Yw8O2cxCnAA=Pk~MYvUl7My?pbSOrb{yObbg@c +lFm0vHmDIL=tcZ=Dl+AZ(pFLOWD8SLkw9mCf^T$FgY_U{jc`NDIqGjBIU>crYhA|6nT~-2}kM$b{#Q5T +s+!5vg^ocU!nbwi}_=gd@0u3Txro%0q99vu+gt+`r9SS*|Wj5O4V0+o2U>NJ{=-1$(`^{mmIaS*yz<7 +SxOrO^&EyrvrxL^wtd#K*(LvM_3^)C9uRu5RX6KrXmwpZ|9B9^WTgTSaixs$l8r8TT%*~dOqWpxG|32 +rMb#;d3F^i8Fc9Iz^Hz=m}+n~9wW733P3otYp_wSEis;aQ}ezLcJfeo?MRn;q$y +Br$*{CwXWdhlY_#02Vxr1Gz=j?FEpN}|*S;=>&?WlX7ZbI2$-it9Ss)L0vqey4%$i>XFWc`9T@TFdS} +&)*R`50i#DOpvWS*Ce;++JqTY +8-*Eh$*8(@-{#W>0Tx#^Ob;$x`1Uc1R9O3sAcGP$%7t&Z#l84gcV?#X+W9By%8Ljm!or|Q8>UKKX~^e +jkNV_BuZ1W($#x8u$%7`xE$aYQqlzPoT^bM;-{<*Rrs|csw9WrlPOmLIo2=ARScbd2IWFJAONlW9yI} +Z)Wby1F!7==KoF|!>wvf?hBxm=XHac+esj*i-X0d<{qQ6cnv&2GKPlXr2~676aq@r{TN=V4Nx6JM-&4DBJn=cDx3kijeW9kp=Na%}nzMW2pNJN&N*Vh6jdjjH`xSnDHA8e)TD +OL6H!}Yn`=F(0X03@qqlf5xDG>mM;PF)-c?6ak5u3o&J;}~=+FMpiIu+mR+4-hN%&S_7Hu)$q!?&_-77Y_U?G-QJtcpO042&}d;K_JZ +9D-&R#N0qRC%y(U9!IzlkLAR=Y&Tc8ZY(YI*%BSImgaZ_liB<_bd$@+k@YTg(>2psW4IJyik!; +A2IdW}F>G}&W(P%5!}P)Ym+MHd1#48xjgZZsoPwP=9<)i6C|=TG#oQ*x=IuUS4f6+#4_G8^i9iJEYkC +K=jVPN7u~>zn~U?(f*cB{rqizAuZ-N~t52QqK~ZQf1B{G9*PQ7@>+Fm0}L>cl4yKkJ+_5zTG|QSy}&Ay900l9wa4_xy^Rft`!2ncmN +KEKbB8ST|xq^UCUIVeo1Shy-{G!+8j`bE@)5q5%lvLz?$v-as+um)yGhI`G(24Y5&mzf#{v$xdEAsZ? +Zg>Rc2BT4d8yafGoM!a`q@qwRr-z+Pv4^hWY9xqOD&($grlOw}3A5iHb^S%qSvpSlRp&kZpD&QDg}hR +*lRE$hiA{#mc}McGJ5wyT!E&TdD#=AzreSKNS%Nto|OzTFPrcEGz*Lkgf9F!ma^@rzS=5oVy%11guqH +j0wldd4kLf%9uc4(GA4^4l83|c>{)N0karw`K1L5QwjsJnV!Q>+@(^fI1G%l=59x()1*IeH5ZeuKB$# +BYt}YX<0+FNNP6@VC?F6u+yp_-@zIa7LMqcA0Wt)VzP~W6J0NP=H|s$MmQQw*K$M_oL(M1Bx@HuosYg +p;(zhCJ>wpZGUkgzGs#@mP<(&fpP&sMv%?8$biDKhxKQJp2XC|QE=Rb{}0DpLVG7PZMLo!z$rIY%+-# +1li00=;=-HzSJFNditz8(n$ghJx$5o>JpnXERB@T#ynzVd#VJD~h=|NM`C{%?>h$=JLLq$87f=m3#dv!M<)pqmQG +$a)2@!|;6<1XzK3_K17D#LODewBequCop_buhW!7QGm4z)>8~aTdoOyAGYahbfFLs3N;q|zO5iSp22} +!IYbCBTSG`us^`1s!%E)kcp?BoqXh@skOEeNJh##S!B*dyMqHYGlMyA{w_!xGc+GT1s|CX5S~G74bkY +<3G&zf`4ugP;X1~A+%YY5ZOd1rU*_jop0JhHN?$3}`m*umKu1ByW9U-LnRwXwIT?|rzEktuGgDjj8=o +7W7M;50Z08Okwc06WK4s9^&Xr%V>v_F;(h(5CN33R^j8(Xc0R7mF2BRF@zWT|oL8nFDaP2s5*wRRON0 +0H;}zhf$fsY%Fs#S{^e!j7+8z#eGKW1=|dr|Bp{7BJO1RbdrXy6{7j>9v3LCx$GUeCew{bpX>M!zpyx +_fNiXwmmOBfc3gTwaN2mQ=_;NNB{iS3+xWQr8^|9f&qrjr@0PS) +*zLZNEbP*Q{S$T30+Gwq#6j(-CHH)r`Fb1n*{`OASl&>HWrP(bh?7}mwu;j$D@+G^(};P&x(=}Sbf>s +7c5DP;Ainm(7wbDGz(uYu60`Ti+R^}DR5Ln13xz~h<3wHvTb2QGr~H9LEWqDKPa>_tRdk&20oU^9Nm= +4cO^Pf9G6cPSh+O2z!;z@k@UQf#xTKZNCZCy!GCzq?BC$LUxu5s75wyiO;u#%H7-Dd06DJLbP7EH|Ve +Rh6D>cuO2;cqT)lF*u(7belJTHxzp^r}oNM%BB(oF8}%8S<8JBG7*6!Lz3MOz2ERZA4XH5FA9CNO8=g +!SM-*-GbDrQQBHg;c-GH7C85#WFm!wvcn2o{3+;f=+~~yRv>55)4MGD`%3;B>Bcp1V^e=0or$B(Tu5< +A=Aw{a5T%JqIA%1cxG+MkH5xc=vRYuc#QjU*M?p5WMM%o9*+Uu8oad{KCRB`|=Go~?P$YiSG+eotNU^ +h1l_+x~mvW(!omG{w|O+X+*LD!%+_xe+g|C@zHLNaOki;Dq}+rx4B(yR?0CYElla)sUv;Ilumq{P{D{ +QTHYBJ^HpAOLwy$;fz-{&2jn25^g%4a+Pa<(I6Ws)>(M0AlToVSM`6)V%gg18YXc&DYYx%=_+Ph?Xl^ +H%XtcQiTP~X&v^gQGJ??(2ZIasRqIz?G&8ktdpL5U#mM=e3=#xd3uHUvO--D`U=jgzf!;Uc-XUPi>r` +UhGfr-Ms;TA0|M42w6j4KWm8dF1Xx2uXV%a9R$oWZa4R+WV?(l2UQPSB=FMq-s>x{;scEy^UqnX5&nrig5(> ++r)ymL_4-{QrpJJCDk8gw~^=KT0itrec&n3)tXBRrXX*q&MRt3iP}>{-#8ao^cv*86A;{^(dcWU6S%y +Z+!&VOWUzj`(ZLOBKk2K +syc)3Jct4j90t4tTawGe*ktJlsl5PBFObrDaYlbc$1pEzI^J|{n8ylY>(6kyyyVf>!Ke+*k3YvbcfFR +WHs6=F39h6leMQGjiRQOWcIHJH22-s +VTQs6`6UhQTEf#il01G8oBcZPNm0k(jiuW*o7SoH>u3@PjuzCljVKhhKT?s7CV^n2;~s0) +&=g}Zbs>0kCSB#CWqxW={A_>rTxH}q(_pdVEFvb~{!!)*-vC(AULCE*zZ{u`S9DWEd-Mg6sgB&|8n9N +zzkC&$z<8W;SCI>oAF|fFt{9csjV)-V#4oJv0z(+}F01Ovxpu>8po~?lrC}$+`cUaZHbfV$1=@wXC_^ +`*a%+Pr%q^*v3kR4E9PL^5p?l=BdIcS4q<}C;jJEmND54nINTs~P-5@g?#YfqTa|cAF{A5m~p;s4nTK94qE!0)lv$S$o5hkX)ahCH>*!k2EttXjIp-$mK&? +EUA_i5smIm5?%HU&5#B}I>arN5k>9}WQkb=+V*HbQ^qV3rya`%w#mlSp^a&il#7V$!{}Bo{%(LO+IRIf1S`qXPcrWj&`L}y$&!y>Ok*7*oC*U#EdRED~0Zg9yABinC2S7N~yk{~%pMx +TuPOh+G-ID=Z8i}}4mMmE0@EjODH}|6fg&>z0b`fU}nFS&?wS!FXCK}OQY`i#Q;}|mM^P4i8JSqx~-@ +agV5QRZwShVk1I^5Z$9GKbQ1Fxy$TiAe`I3k{f6m}js3965wfyiv__`!{OJKL;Zn;7h`h&oagx>ktKI +5JRQr+FqpwUkdMg$LeCE$2Ubu;J|*zZFK;k%pzf61P%B0l59=*+|hItDJBJ+?lSC=L4N$OHB;wGn_2T +ihxjv{{)3hKW3FNTWQcRV!4$uXFZ6BQg>}toJ{HQtQck$Il>L}9pjvpf@gR}w)mPH!}j#0d@SnHfGv% +R#%v(tdT@Pu9eg{T!CN^q01wvN>I!=POJyLRv99_nX3?><2DU8PQmCa0mGEqR5?XUfFuQnEdIp0l0F}-UMd_~0E+k?ek8v08M=3hUV!L@E83yi?9fa)_j9r2wP7`p_MJXfsB +%F^mxm`i?%h%F*Qlb~V!H-H^{_`6T+$9he3y>p&xK>O&2rr;YD+sK8y%|*x3zkE@M5L5NsiuTjB9%N|N6|T*aSl5#kjX*1ocizYuvHT +fX0{fCtOQX@8hG@X;K0KT$}?hvgJkzc*h-LS$R_m=Q>1TN?1G7E?sew^>t@52S +*)K#8wabd!qcLd2RHzX-gTX??2V^bEkM?vF~u0H-iFsi?9Cn6W!1Q)x7vBHgFnmH{a8PhYGC7_L;wk+ +(0+?~pMWR24yJIznpeihxiE4xBaXpoRSDNvJGkI6r^;4?x;*R{lq0ijqBa#l-5ky>2p6Os2?ln0gf^A +md5L0ME+GdYJIBVzir>qG|}lu1FeX)mX2^B;w5liyE{ +k_$uTs)3_+rIU`z`$i7+Z8m@@eNkm9>mA7iEpXI=KH&iLG5)J68(Cf>4Opx8T46;o#n|3vHQK09TnGb +%Lac3Q0y*)Xb@qb>3;0LkJ$HoMSV518TVS>1QM$4I%RfY4~fTJKWmKw4KRW=# +w=5C(zrnNGy$pZ}?Lp69eN&51+zyu8cube5X59t~JKbL5Y+$QfG|SZI`ssUF#t96qHJ6yoR=wSYe~u@ +Hpm44&^y*2p`efzZfC|2O)zHaf0DM~c0EMY%V{Z-YQs#3d@R)*eq`gIu{QA)6W?G#azkJ!dhm(QGg*Js^e+2D_#mEG83h@N<${<~j}f +lC|6UMREfeqT}bX!SX*!J%BXmXfgtQWeZ;LGbm|F`-018Tg!$R`GPh~-wGJpl4j$|g|Z-EZIWMD=41z +6F)TH2SfXGFs4>H=mWWxy+ +54M1{Gf^dSd^aMRHUkz%hQwr|O|ZC_S)v-S1=~#hh>4oleVycQ53p+ETI*jVFo?5volvaUMoUj09x%N +_S^71wUA*QLHGP#m)R8QD`Rq(8fVEu(K%KS5g(sPLuRyJ~sf-%Dou(9%!S?#YsTZ^Q;gmHo^_qW-#@A +C?5CFCTF^gDCk<6RMw7>uX)iHu5sj8JqU~%Qtm`twc*$4|e=_msNrU1oIZ#i?qfr5}XjQEWy#Q3y7u! +0(aC$?GZ!(R)^i>+z){ytMdG1qDB0479aBMg;+n?FWM(7C#GF%?+*0v};WHRu#rR2(HHc84BpXHRMH_ +f*2<)j(*Zw)e)55W~Y-X6o27I9y)1SQh|{z=%iS#bkoL`t$r84rfo_r_Tifp^!kBJ&F3c%%7xqD#tJ< +W|Q)Vzdn1mxFU!TX{r}$VEIG%OX*Q1v<(Uh#bk91CbeTvSAZ`ivNeX~U7FU0v4ue7hnU0^8hoB+PbKX +}3M^tbZC@&>CTZ3F>jp6Cx#uU+!$C0s_ri*5AHn`&>e{%#%TXpy|_80V9l9R*1#cB%dvT2Q0 +e!Hmj7|K>?vrU9;xq*ej>-HzV>i;7K07piC&3R;9SY<~P#E90z=DJR$qzMfsE>C9NrKq=9d}NOo)h+p +6>)iH(xYUd_n`Q-=}ERSVf>-P)E-Fs)u3n;-zzY>(XbAZ%q;r;qmz2ta&LqmgK}QP~X$I7S6a`os5-g +aT7Hu~{c%cRV{fJ01jCg$cO;wg3qOCre{D5-QIXJ*J>Fm8iQ;2q;ZfsZ?{cqRFl+s0hbz!z-f!k8V$P +-T4&9N?x<^6#;>0xt%1TP}1vwN_3Zn0&IqQ$}C@=2~oJ-oKK%7nSKcd_)hz!zfaVw!8ntnTeM3mn=Sy +M(OfiKLQ#}!l29ZRAQ{Va1iPjzv`$N)(RObQ^HD;E$9Jh_6H1|9*ZPrADXqR7|48#(Tj>pG%wBe)O4? +-J&mZK^tdh!CN`QQ&i%L2q6red6L<6LPyFCATfk0SvavDU*?{ihzlQ#L3v}+bMSCu^>Ec$r$smT+N5T +EJ*W=lHCR3Re*x~$PK9W`PJnCNU_7$AK9DOz-#qFp9B|WDRvKzq@vwr7cO$z5CBCXJX? +MfZ;RH2yzV~x&F1`Y^7m%=BoWVijl@ekawS$(T9nIsf7dzDWs8+D=pYh*%Yvow0d4|tq@a{x|!-!kiL +f5&u}91M_{~5#WB$3U{6Zu*6lEkX7_-`dUB2@eS|9f+hZhkEhcD{$6})tS~Z +;ZWOhe5CQA==@@k$ovLMkeC)CQWuFi%x3HUX2FJ%k5uT1X +QLtiIFc4 +{K4-0|!srC}9VxHpsa4VHS9O|+NN0PC}CKDbd?21#&!@`q7bG?;s@6m1#kulpqXLEiz<_YE;gWC`u^( +T}Y7l8w_R0zx5XGDxUy)XMao356c^;RBVs=fnwPz{Lw|N))^)78`ho9H!VMMU=PuG8l&!;U +V+{cjI}^A2oIY6KgHOo9b&*aU(&9M7Ly`ePBY*nj&#Y^(j{hei%hc*=%?Z_8I-3rfCRtBLn8QU&_tzV;1|Uv}tq-@N)=-<%L-}kq_z7osO +4RKmckEyhl-)V;QISFso7xgh7kl%Sq32%EFHSBdsSl057JsrRIcQ()L_e?v3?En88|&6dR!svut#R3x_WSL0eq7tw%Y^$UHZhjwwPyfj6 +oW_gc0@f;;p6#t*>Sgfzh5ET#2h_5%id@`+1@f=(`O2IP}>=McR52D@~RELiU`KlZ3t{B4>QvbfEPx; +HgIAQGnX!dtN@J>r?XPR@giBV*XN{&98j9lg5OvRWOoIn0VzLACf4SjgGgBf`mWHo=087#SuNKwqCwa +4xa5M@iQ11a~GqV8VpXb?|tzbW4xye5APll*5bxc*Eb&Kbc}Bm~13RaK6Nvve640MSq?jK9#oujcG_G +zb|+9&b9!6HQ6^Yk4B3fl%)d#P(H_r&?Cbg?mTG~<=OB_FwlHz{dKZJJ*UnQw_xs`4N+DcoJZf2+y3c +E>H|CtYWs}xPA;#W*dx=~S&9Lcqw*-N#ugy=)fhd_jZT!2eUm+u74hgO;4c`aw)e>17M%AePE;UpoUs +9GZGy$p;S=0JR8HXpVt`NxCJ+}36qtA#?O9=fpkW*9j|$AIU>y(EJwW#MY=lesqR-sQenp@$6^L(ifP +p0#GhBegQa~8Az(U!h=*fOMmXX=NeDM3+3)sA-6?8VdOxFi}3?A#tmnj0FkU&^~4i;?p{Rg9nL?g9T! +n76P3eDT`dqj=8X0i1i839+0B<@kV_OGNF_H5OqbxnfM)95)EN}j +p&&Y?83oJo5b)lC<(hs`Ni%Vky;vX+rS;r167d(zk7{PP3^j0EhKsdCvgWP+Sq^MzA06|r87mBUZx5=|9SQ+>d?7+=@(gN-0V;Nr=K$~+hF-`UU-+#~+el_D>CWClFB=w0@)E`(`KUso#Iejiy{%Y~wJp&(?5ZZE1FRvj9Aav#Vow(yU@dnye05}(v!YlsQ7lOkFV$Ub0A +vtdQUsF%E(EF}o5{5I=d%jhQg07|ut+vCe|r=(h}5#t?5<9SgVgF`14WOiuD+T5*`w&c_&N#B`?ty28 +I0lp>wMY#YLvVle2=dx`6%nE{Ek2m-!4#M$ETCC)xBbF@!?3o>x|S84j1*W}H^F$T +WC5lAXqBmrxgT+9v9BSY&^1qVhw1I{!s^Wk4-Q_v=0p7SqB*4C5TzA;XrM7!+^gXT3IGPYh87$;#0n< +MbdF08rnG6xsI3Hev5h?49PS!*;K= +H;^q{5N2Z8;SKZmai%7U?rU=6=1E*_E)7Nto?-a-WC|LZ!!0%`hBJOT{cN>G59dkKoFPymPl?Dmq(4{ +5);IER{JiV9%ZX1qlZ*w8>x+gfNxUnQ30x>X?dI?yV+?vKp>7&MRq1mQ%$%1UD0j7S<4wM@?@`EmV6nuGm{O$PiHonv*kmE$a1p!wCT#7EwuY!wjbLTz^y!< +ASDLGOkbar>p7lpn$d)vPVBSj_zP6giU^27v|xT<=|S{Y3ZM?3U3Tld$S09wp2=o5jkXzqEFUt#p=Q9 +S0Lnx+L>Yt@@3z#5te)%>UYE6tH&t0JHXx2gpM0Ia{Uf}dVG;jh4gIY*_nRyJu;GI>u?K@6tDIxP$k8 +i_0Q*%sCp>`{h!|Gcj=b=BHG@0+~cxgRrZlWlG{u6_dr8 +YrDS=~buV#UZ_0dvmVNzZ-aWh#9yk18yDd9&~qN*Hj&!kIN~>;CjHc>;(SL56c{v7zf5GuzT +2T9jtffxGFC-uky{Z~NLdi0%F6lF$X|Xh~?)cqrVYHd{%p +z3zcG3;u{t{M>Ee_8&6`1fUtt)nyr;b0(A4e)qtew^uSRgM3or;XwgpP$=1 +1qIKBai(HQ|o3yhL2PP-46?6~0Rb^}T!OeLE{JcHVtr6&&c=Ph?$h88%_SKK(6LWGz<(6!PFu}dLGid +6+mjwrQ&A{m*Sa7$Xw0n1JRCq>v%A&N~+yJHCn;Xj0Zs-pQsAf_3v$rn#PuUCAcF>@q;@LM8vD>`}PJ +?fk92hy+io2n=T8>Z3fh8#?wd{Bc$sv|mp4vOzp7$QFe?c~a&!ZAP+uD#pc-*6$dT +it#C@jy8#jTF(q$`q_THD@euT47OV3wTjy-DQWmfIPi5M<53pMg-kum%1EJv+pW$@AM= +hAP_a)sh{p=O-6dG7LC=Hss{EXWnal&efulXyir58X~-{ABo-mwU-E(Kt8=M3jn}eg?+eqeEJLpbv|x +49eQJ97dS6zf3t5!Ur2#^tY6)$e9^k1QjNdcD?MhiuCh^a$Xz +-u*UKn8TUpl+SmCo2m1JgwmO<(&f}HzjzX^?|p_FXZ^HEKMee5A4&2zK%(?5-hM^@36b?OWX2k%77CB +hivaoUME=ONmto6z`c*-RE}^$!ZEluokLIKdC6Xk4i%LOh(+6KWAei$?LE;^_8+GHb`wGJDo~InO;*+t&YD@iW3_utJavUy9b5EgDog=pKCNJ<2eij_qABwuMr_5aCvQQ)&@W= +)0}~J$hZa#b;Vdlbw|4wTwPyAiP1s~ShSaZJU;#8dRys#HmZ;X& +qzLc9dwmVQQgxD`P<9e%fY4jUE5P%UAmd;F|3R~PV=mM4Rofz20DolG@2Jj7p;3)aUZ*YDZq4e +Lu8&_r`2QnDls;ZbWeNnEvumSoL2hxpOsbby2k-~iLv|X>KV_J+Sh8pyi(ctExQ$*Twh)elC!g~(dj +n^T%5<=Zgs*W`ig_*%V~8Z?~wCnCd+ZA#jbrh`XUKqOLXNMOJSJU+b@(KOSeBVPA$`02giEnSo +^H4ZsP3)w^@zz@*@3_-VqQAEqH>SZ({inJ4!nM3#st*FSeP<=j2FbI>DG!vEXU5*lJ*>pK +NM9$~G%`pPASN)JxgRg!#om>lJVjY|w_@i@IfZF}-~BApfJ#y*6M*-NPTFLnR8lIZ0SgQVx|fwX7jh< +FohF2U-O-_Ypl4eH!~-;!Rbwl6dL_oEreOtdihxk4_B>lU( +&BYxAmsXk(@I5wor@6&wLn3IY7go;XA=b#X;wy6$?kFae*9Ud{#fvxJjuFve+FN=pCWv +PjU3}C9mhh^TFy?tgJd&3xGS|55#n>067<&zXQFK3*bfY2@SR34bw6AG+%fXk4B?qRr*)H+~RyR(}=Z +&NIWSZE*&QcXMEgB5Madxy%FW2f#EQ#_(aS^j)9KxmYiCCDE}_1s0FH#SX!SDmUgokI+pWP@9MzaDXDWb+^EXPJG;=@zLiGp!%j3ivmfQKnyD=-%j;58 +&pRFNN0ULbVd>nVS)`h8H31_bixVBd8{c)EKt&I!8(y6WbS=o0PUS3t@t}!czmzdzZz1#T2p3E!C0?+ +LXnD}tr)`~r(akRN73b;mA+@Xm&GpU*ih-^VkCEdx%IIIhQpZ2riMqxGn6M3Q~a#HjaRp{gLH|3ABH*bG^6jc*EBchFb$fgvgQg0Z +P*hHktgbZYZOP;CJpG|t71VMo=jw}vlsvfjZ91b#2ECi)wYlR1q8w(YoF%Q*{%4XyXy(O;QIQcuLXkv +T1-#A7kZjDi5+KXd@DW6?v;YONtZcPb>egv0r^YKAZfX +VaH>JFMN6A|lAT)ygC0|YtNb*jNW+gF}upeX(rSO4kXJs#jp_wEg102<@ZfZrZB3nE!bv~B^{@e9syy +|S_Z?pXF4KW<~S~@~Di;>i(XC(+Ahsors4S(u-=-hds&ai +;GW{ZjjJzAfr(^vPv@o*UF0P#DeAHDg467L9o_n7tNWQbOG +1%yU^M8jUv4dV^ORIl6iHQ*Z{_CK&s3q{x*;*-JAI{m{>{z!qHs;Q+1?SJAQwUy^}sh_LYSSt +R>*WY)!D+{sD^{+QfXn-|4o3&3vxV)t{P}WvnZKTd<`Q4WVJ=HT*eAIT$v+L(pycM^-tmr^YEqU=eMe +>DuvD`wi9#e34)Kfdc}O5f0Ei@!sD}Rz9R-4!mt@KkUZg`Z8@F!zjIFIT?D-M%pl5jY>O*Vs_wM>!ZZ +o$a|#o^g%vjg`Jyg4~Uy<)wajH7b{JyT$PI7(}f+clg|?k5b0`+xpl1_`Wop2PGNE}ZZl11;-_e|^=1#c6-Os9_h6f?D#l6$i +Kdb#Y#r2s=Kg|y;cmiLr57(LXq4=1wq&tuXR +}=ty7qkg&T9r|q;sUn&I}M5!LNVS#-mQV+umJEwTB)D?MBdT2_*_!IWx^7H>bNGd)j3$6asx1%N38oLW>WBfRl^+qP4pRMztWGWl8f+mF0RP*Wve%Q +h4LbI`;fZPL7`@(>b_$UaG)w&Koq*|0Nj;es##o@iBGW-*g$K;}bXj4%>`j*b@G?d9MS)kB-%1E8(o1 +wmUuA9B3^nk$4b{#H|MPdpv#1wYdS3(|kJDb7ePWVhz?>GU_>>wd?(TM0S9m)~_jA0uMgq;-iGK{A<+ +8ZUhtnW8Kp+wUEWaG&jp)l_TfC%-UX^GFV2}fE{Y_;3WGT!Hu!}EYLA-A+z8*K9yNH5_M-+G-ph^}xl +NmZEhpYY0;pcALdT$#z++ivr7Ras9z=xZU1unBof6ElUrFHcBD#YP#x8-lr)A}qcUQGnkwBAOvbTUF` +-w}G5R(bY>p83~_1p<+Zwiw?%et_;e%=oa?-Sb4mm+YbZAt$<$p9A*Aw7Wa&8H=FD{?6Jx`~dRw$8pO +zN)QM@%MCw=SPIzNknSr6sEPLsNij?7abZh;a1Zk=V8um!$UxyUe|g8&x%9z}DdJA8DPuR8upU-Nj?Vqs8zcFqLY3SeZ4*zd{aPJ1W#(;mg?f1_DEk1y2UdC +uA%k!oSAYC@ZL1=$~ow!*57ko&SZWoXGrj&fdC|k@1qeos5`g4!2Wgl4VKdz2>#U8H}pph-;~@F3h;S +BYeje4_tF9WnwsEk9m0RCsw(4E%P_b!GvIRWehyr$&f^p&dis=BsRlwLk0IC(Q}Ac58mZ%X)=OIuP6P +Iyt>-9|)l)f2>oCh7ED(sYTF&5w=wxfPi>s^R1U7Xz&vR{d$~Lb@vbiiH-~phVp~a?w{onlecBYGe;#)6aH8||82Vn +p+Ssv&1{W>Cie#HniOYAxks`P4snIn7%fTMPFTDy0`Bm!SH=6sxB3!J4rwVG?lMzXXVpz3$~64u73pt +A=lJOsD5&jT%TO1xHSS4#rBX^|NPg)70=OKTQ2OVnh>kB0duMPx{J%b!Ba^+?Psr9K|m-3`_@WD@AQC +HCLKX*>ZZWrK`cY{{L8g+3f5~L4-&&-0m{I`Xe!LbW#$b%gq29jq6HEM6Jso(G?}Nyd+d0zr~{cjXOr +_(s#sKwU>Ac6wa)6Y64CXyLTNC#_SPGKhEYd1w-N^vHh##LRQSNP^Fp{$sXm&5##$I4G}OY +`*eJm7*zB;-%g=ItZ?6e?*q7p$K9`!>M3YUQ%VqHxVbb08Zg3z`cs=yfF-&g*ghFM3aa?7!EcOfr3OC +)t!rq)QxSVS7>b5X_b(!c^wP8%1w6MW#u!fa4+Uf4@__^zatg%sW6FI;lkKivZBhDj#iT1i3Z^|Wk{W +P;l*oLM@sH;oPJTO4m_MhS*9mB~5ZMhccEaE#P88U?R(H=8BuJ!DD%s75p7EjXn3EUm4t8Y0h+3bq;x +d~xfRj{)>s{$wn{4NwsN8%37Ac^Va+dki${?P$5&t2p3C7tNSZNRdOJLu}iXu@|?g>sSogMa{Jn))yH +r!jn{@bWDXh#q)(XreGvV}L+mMlfUQ>P;WK$Y(&)Y-uS!7$b@|{J$m5mM9j&5w`lsd%WNgs-Vlx$zEaZ9wtKY>r9@HsCrMZs1 +YEYBLl9XJ5Q<&@NPCq`=tkb)u8z!R-5bp$nGZ>@&y#Li3Y+TFWX4>u`1o5%JS)}@=~4~U{ViYi$S4;M +`=>DbYFL_9DKV(yh2gTBGZCsaK(T +#r9n&I=ea@x4fN>w?zr=-;MD3n2z&#r{Tc4Ic;~DPt$kY{o)$nom~iKsx!TAbv0H18+5a(BJ~@xPym> +&T9pj}*ddX41f&C`EZpZs3CxeIsFC8!b%v>S2(?7H8Ow|t?C?E`4q8C8-aQD|!Jma12fxj0Wos;btf$ +6Fm?y|}Jt$Zp$+Ng4~1_+JVM2K5#lHFu=KO38HJ`GIWfVj=f7bg?sq0@Jw06l~!$6}tw35VEO`E81^O +a=&zyj2|UbHCeDS&_?K0qcM_jeQb^6+%$4j{W|I%5NMp?LFxqSy_UBOBSpbzPq|ZFphZm=o?;m)jVba +kGI)azZZ>(ayk~E-h!Lz4>4yttPcMd=?%}s%x^;V%(+1Bcufb^kVKP2-**yT5y$ +Y*1wpg4xEbdv6f{X+tiT02WG8u30dHu*7o;#7>yY8G(|BZU1k% +nUjRw4qBVj=USDFVc=>qbL3A+LrZ%{nDln|L?|a^Rpu%}!~!kxa07yK;_pI=vlNSi_(m73G1c+o)vMv +OGIl-#mVJSyg9u9^v2b=g+tj*Bm;pXdpBKGEDUZ4)voPP^jOpjp)X1}`D~tO;wQS^XY+!oqaVyG7@%T +Ngm%FO^ZYKy<$9Om9rq}82w0y5R8c(juyarW71EG;=>}T%3Cq}Twf!AE+0aI|9P73xu&$FvW0{wgf(d +%i3Oxnlry?P8t6{27&)bT_eH&W!ymh+?^5Tl_1%S1ae=&u-h4Nc<>84Y_8%R^8Bp85zn@Iq2l*rz#}F +5_!L&hot`7ejh3tEZHJP)Pav7lc>f02>C2^c=U=`5&9K)ln8+J)D&mwLnXZ-^i`oQ!_9gIMJx3EN8jxRhrggt;Seo$I9%(+x4H +*A{;FP3E27R*$df>X|c?LGo2IuWm?Q60in>9G9g8p+=qVU%IYX&sd!=;-a5tF|GI(O2Eo>~UHmu8|z@v~2l%JP9W2m`#^jl27 +v;*4u#T$)1X1QrGjn9kAaxkA_>VPWm(IAn?co|Fa%jnK@+x*hs^Dl+HU`Y%@Sc~^^tnv5RFKY0dYJpJ +RQHt-H}?&@-x*Mqn0CxDdG5yfXHV0AHXgg!i!R7AIqV;IKT+36noj3{IIv!p;G4d$w%>VCW3mN!|xU4 +A|O8Z=2B_0?rF1%~j$NN?Q%NCDr6Y=5=IOe(?T(M$JIKv6+r%RQCFZo@=Vv?xq#G%7H_Uaaoq7&Eg1S +%cE}PM#qU3h9{v*Th*le9C98qhoB!mB&Oc9RnKk*d6X{ck7uu1yx5rO_UjxfHtab!A~0Ag=gZaxBaby +?poVkv-=qbR7Jz~unxKVtdLgbYG7ZvYUwc`qiK^iSZiX^2G0dH2RlNCad~2{u@79k!&QiYZENdy3#Y} +C>_1MCk5U6+(4tRlU2bq@`*41FdC|x6n>LcdUj1+0vB+P^dX&h;78lZLUm74Im2DU|g6>Y?-+$SlJB6 +}TDB|S>H=8(rbT*Sjm#u-BRkTKnMh(shZ%Xi_h}1)IfwyIhps`KZ9CZet(O$up)_q{xXZYJwN6EWH}aaWFRVk-?q=enQO>{180cJ +xpNS~3#{o>poyXR`b#)3$)*XsW~gZO(>yfqcuxQ_F$eVCp|KwUcv;L|HFxh +&Dj*0^r4a11%YI;8(_yR47UmH=-BX%As~T+{@WZ&NpU-@-_RsFIr16__B=5^2y&M;r1p*Pi_X~40f=+ +&-=KFpR?9*YhK7k2U*QT(L1wlwnqt$|ybZg)E6Fr(E#UPUoD0Yq_Mc!>XYWV64R>w-#eV{{@^1AXqf% +Ua^SF~yC=E2)MHoB1?y|LW*hit{Ze?#Y%-emPu5)caIOLoWBinKAhsjpjpo5BgKIPE7M?P!&R*DVv66@IA__Uq7|htyx07#KS=EgQDBJ#YIClX_tb-S#196mpKsb2o%9RO7d +KLyR`SY0FF#$PmxHP_GDJ@n7}HQYVJgE~>n>3Y9gd~lB+Xx +#Z(jnWu0Z)c^w#QoqoMlP0>awS=le^g!mw?qg2W8nM(QCk=IXS39Vh6y+$@vC9VdAJV|o`eUCC<2ZV +gf)|?~GKD1g=^h%umTy}ffxLShnNC4(9UWvZ=zi`rT&BuA9F-bqW3e@^IuW=RF{@5LtFmcL1ws{Iv&} +3ttbMFIx7#dW{#oXFk2b*DvFXg={dUX@^6QxZ_Y(Ho>+I%xJ!V^di={}n_EQkXaLTVaAY1tU=WcIjwZ +k6LlSlrt8%YX)R-h3M{jvYaSBsHfJXu|*+u!9By>H!DYcpw+~N(N$5HsEEm8%O%p2hYkM6y1buo&^Y1k^LTQj45-sS0ih5+bt5DR`ua{ +wthJ_cv3=mJg_zYR4PWovE(~C4oDB5y!;&I7zRRX#U9r{xXMpkNS-%>FY7S}$S +}5=%!H<$-L+4y2Q8)yZ@PRB+Po0mwVb(BHvmhOtb@I77&Q!~<{)CMrmiqNrfD=g`o^sbWDT!&z>E +%yMlv)+GnUStQ}U@m{&O}YS-Z-3G~6f%|>a%F0;Wsomap_8wZ-O4i6b~-GqW`*4adid^;6E*G3Y2jqfk$;|A?X7dzZTVq +w>3y1^nItO)8VHS+?&uxvVZBYWd)UB_%Cwpg5DKMFOP55{prM|~{OVEGDFLC7Y3hr&H)jYp6C9s@2O& +LHQD(m#Z=VVcmp@ko$5L;he7{Xo6!V}%0}uu|mAbKgSiQUxGiUD6v72WA=9g5={5o$wV9cTdmgKnVh6 +hZ=M{Xy>DH>~|7$+6QK0UT$f4#8FO99V6Z=Y1TfxXXIWwkYXYS472dQ<+8?*!L%rX3jgwE#3780KxEk +-yy_l0r)6Gx_3x0F;fEe(1wp77`X`>0=%q4Pu!58VEoFVKGeSjlG9lqki8Jd)Q{mO#m;vd@viC22^#; +u5RdZxMA8EboIluR{Hjb`^+K`PVevGwiwu^J3HvcE7H5_*1gtd+vZ`1%c&K&|3l6@crt!YU;A?M0dz( +Uwg9P*`oXG^hrBTL4VJ9VH9@NZe$nwh>;R^6fKa6aaD+qVKu*qXlB9ouv-!yYp^>=!(pUS;VZfvFj5`f|R;^eLU)Wu;;`RX +`9LirScuAI5jesfT>-eymFG+zN37jzvPZW&6!=IK}Ud@6!VR?b`bbJ~JA~NEQW7P9LadJKdJ=xPTX0j +It5TvB;G^br;{FP56DA1ThTiEA?0OKzLlUfGsrdETjh5Wd}%Am3Cy}bTuF)$3_A}m%VVcc@l8Kl%vPA +Kr(Yh!wZwqw1#OUw>fBG^j&El|3s%h_Lr_gZYAM +tpgBlq5(YG4WoDzAE%(zZ9+}l{7~uJ(LaQNML3ZK7XtlN?lzNilhgyF&w99H3ePLoOqEvN$K(CvOW9G +hdR2&tOo433I!#GA@<5D&j<*GidT`0@6ypI9ZlZ)+BOvAd-{~86;fPIW(Ek2Owg@vA9zJ5;LQF&1nL= +PEDAd!u(m*JgQ$!k1ci9bu;UZMWDyP5!?^ +7m>A8I{yGw5|=Pk_VTdOA4a!2jaNmcF7l!))oUP|Lm7~9>vce# +5)7j_4_)^X=^w9_2$L+A`^i38OInTU|dUbWlh8BV%vtqypH1*ZhFXdxVm&N6DGAaoOh03Zn$-V6Lj{b +G`FZ7ib#Z##jo-k%f44Y`0(GFuydb@(WX@B@$as@QN4&BYRZM*WHOm8wX+>QBwkxHC2~Aeu0`R^bF +Ok&`l7X8&+rf6SJ=hlg?d$C`uepl^rjMPzX5PmJv{+^F#;_V>l-aW-0i!BJyvP-9ydbc&G&P_Z91`(z +SHW=NTN@^>N#L54RY5!;v;nD$OA~A&UfAkPqQ|xL(}B%Dt0x +ret<&zn5FrM1J79+@sd}^(`!)3n}wV7)#dgNzkoLXc$~`bJ=Y5~XQ7<$Y8F@zp|i&Lhs+Z+NNZ3U(ey +A%Ef9z*W&R?blPF;M6mQb)L?Po$Tq(XZikLaAKP%+3ijVh!wMR!0+xl0vf<)0;3{X+bG^UNVni$lA&0 +~|I1h(sS469L(ZEf>g)!fSL@$zdZeqL)1Twn*Z_P6tmDp6u#*w1W^AAiLhWd_)1i+9*z;Y#M^SXP+8H +6)?6dypa5rHP+i(g6MJt#L35vUbCaz&-e(o2UQ(qe`#Nx04wj5~~l93I;wKsvx5zQcSn@4oIsMgV?1eYnjgH5@N +B*di1>sMY~vY7AaL0cXH8@@==h%jj$W_(R_Tfr#I9z}U&| +?Xa^|-E)^b}5s)o=Coc)Jz~3%pRO+I1*Z@RUIM0gTXNVV_7pFjBT@4^X)W=#YG7^rI^97^r|3rtF&UIu&yKBb{j# ++7+MDR|RR-%cl3Y<2s7R*cF=I!Y^Gx-RNw)%l*xs#|DXBPBfop^&v$)H>@#_p-YE9mqLY5i0H02i{ +cdMNY3jo{mQO%+&qRKQHb59e&k?dJ0e6W#f_PZ(J|o`aGQ4zP`*J&ke?h(S|P4hfC`TmtWa|$dK+`Sz}E`B7dai;5b6$Z=)B|)Dc*zA?{Fa_F=xYC00zWgw7+? +M*P(3dfd+Ov}Pc+4)G9MgJYVw~MJs;@3CSf~f_qwugI#A`Ie|BFmOVULAP{NIOE=$fm93K^tsZ9mBM%5bq;Bf=RxgU=#8ix7fZt5Z +(+r-ipVWYlc31ec_)MqAu&Pd97PWjP?Lq;6eqX&zwi`e1+}}z6^wJ*j72uC$RhZl9Yu29@1?-i51%yG +YCP6)an~t~@^pJ_Hh?+)&=E7B7tSWP!A^nynZU?ZXn7N3@-L-esuYceyD#vHpeQJR~q+ZB&V`iz&PQR +(jzo#hRRuH0k>XIIdSaQC!;xy5Pcn* +XW9g^J{|=uf7jtgJN<23h%!^&NM7Sx}75hZibKL;+!tYP-K6<@kML8+7xf^Lj2`3_)SZN~7#TBCk0EA +Kia0Ok@2-LwTl=tH%uV&gc4EV$E*@1S0f3eGs>)CUsKJVqez*ONLrcq3U%wr{${gLeTHfF9R&g!We=z +FrtvZbSzE3S)3Pi4L6MO?S(w{?nTZd2Szi3hQTkGLwhkx^?6Idfu;v*Se2mpVQ~s{Lg +0h-@Gl2!kdVjWZpaJ~4j*Wn~tR1`C!F(u*@rtG`qMZq2u3foXT}d3Ecr>JV-;UD{YqqctDm|22PV3J{5vTp8SG9V3I$yfcug*eYEuJ68-*FPv3LkedV@D%DQ +^gvc0QNeC3-9-DiMkSf8=)eFxv3#x7m5%Z8{EUzQ^1UXUG)$(3|G@b!LFj=tUQeur(PAcmJP)KqzdY1!FW9-bCaWZDc0i?pBnslK2)2g9%n?pr^-X?A+M2k0S8ldHTztc&TQ4XL1!I5j( +_5$zn?60h0&7!MI4j(j%RnESn?G#miOzfc=b@rkyu`qwfR%!cwa&&dsr&E*$TEC;FRy!NQVJv$#O%2c +@lbTwsEQ2*+54sAtMlN758;>a*m;nE2t9bRxTvkZMr~(WKG#ob6%Ms^KQGq;86$`uuLZb&;eA*_@!c2 +acef%?%V^{v%jpVFu-76?FdZeR9`RwE02-Vy}XEDcRN-MyB-HY +cf74>VYswWK;n2kYu)Z7hZ3NBzHjxKqvjqH00@3`hKD)R!2rS)Qtg&r2|tuaoi_L?Et|npO&3Z4zaWj +0joS&069S1cG1UHJ#jK=?{JUGJy4MWEKlIOlM^!LN$+8^3SNIe%jJ;2JgbAgCdwOykolgUif!|-P>3u +XFuLd3s;>FJ1ag=%-*;)1YydDvkG~FoOSi{ir+P`wXwj>ez@VxNLb~^b-L~#>8VdbkUv?Ykp>O=B6`4 +`9hPr$GU)ZHfBv6(_?RmoEF#Id(`M!=5I7@>(eq0IsjJP~ZCH@iUr)Xm_3qP#~#KA*K_6h?`v(6?J587De!Y|p7IvpD4e98GT1dSZc4h-k~qul3k%1>?KyZw5>wKaKm_^v|rIkY25Dt4qyzU +jM7_erEgG`-hO-oz*Hm>ZX1kYJVbgG)rj(%z%ZApKYw2;3q48nHIS$i1Cfql)mbFXUwG8Z8P)aQI)2Y +sG)$PEWkU?kdDbnIB|Io+8n&P%#4%lti+7fm{hxaEN5^XxwX5`*}T{3dcY2pVgLA8PV0xX`scqUh=hZ +wm0r6CjI8BNo;hgEtt0E0R#fNx-s&6l?jfU}!_SUq#~)cevOpjz$8cl^`MPS9pBkEF$*uPoy!>sD!Cg +~;r2@;c{D1d;-e>CzM)Da8%#3v~FO{33t4oh2y8%lZiPgTH&NwhK2-^DZ{wqk3X*S6?pthWy{XKniH} +1yKW~X&53*k!$c+M09K~Sbz;8trS3ptYZHWNm<(94SYpZ__WRt#Xn)(#7zL@qsPZiWF7-k9co3BgUa; +MOkV-+VugO?B~Yyerz8S}hh%x`H-_J$x0@?sf#IUAwz~xR#BrjWr}b2b;|V=-S!v_|V#CDH4WfCCpSA +TSK~{(1Q+YA4=G5KPVr9@dI5n4j63o1D0*Isy5X+`~{-%HWq*It;s7|p;f=$S~kU2$ZU%1NkZ(rp3S? +hicRdVNkpf5`S}`e5=XSL +9)@Jm0szKTxjN%>SD5P*b&qLAZsB;-9xhW;abu)_3@Hq&lf+=Bg^gLSyOvo5lJg5Snq)MQuK=qyFEWa+}wp8J_p3{3|03mQL!8b2tcao?uL1|(6z-JLF;b7d+qLF%P*MnAV(zwzuT|X?n +a64N_sZ>BD{FV7=V{oiMKW*`k61^>Iny?won_n;oCbr*8=OFsX9ss1R{^21}#uf`^@^sCxawR&f?_aBK4B9;OdsXyr?EMQx{-B-%^>JCgC(wKK}s%L5M|NxN?|kh=3)#udGY$g^aFn +P-QuMz6m-VEKn{8-8y2GbKcr%_j)WH{Tj4|T53Lf0&G@)yn+p>cwj86Sf-hUjjpPgLbrHtqwRTrr@O< +_Q6|!s14OVSludQ?v&F8YsS)RZ=V002W#^9fGV0pM2JhEi+vKxdbw4Bp=Cb?&j5eOaVe`Ba +lI5_p47l_ocga=3C~MQFay4C=R+=Qdm%fge1snjf$w0P4#q)r|C41}tNwM}j5Ji}sGw8=AAI%W5b`Gg +BnVf`&@7A2%u;=eaCO<(jcziI$4o-O;aP3#uXfa-p|bai3MX2s#0lw+uaafW$ktwu9a7XP$VD&hZD#& +-V5ex~%iIKWO4Zl7MWMAMmn}e_$$d_J=CnL_p|5PNp`f>cmT-t+O-^4mK=h)kZpNKZFK$nuoixKP_J1 +(KQenEljX$D@tomt2=?OX{3M#b1AKDq*o4-4V@Iw*H6^@dY2*9lH|b5b7-4v`a&b-(!z3EKV+W6H+!% +Ezu-eE^r`uP7lK{u3nbl;@$Bov%7v^d3k0I_hhI3Q`Hrq8HW1~Y#|+y&tPd=+f`?6M#nTAV_%D&RxfFW(s7tFfKj|n5!*RnU3B>^a;q&BKv!qs+>Opn +DUp8ejxjnIh!H>aeMVVKHVWfZ>JJ} +(qy&UQ*-sPP)k)ZC*Tw|ZUkeZ5xf+Wf^(CgbL@%bNAM6rT%^gbCZ<)fXg?7HRRQR5{cz_K3Yx21aCzBB$HIlZ^;{N6g&Z*!2n-D}fe#vdJbwDL#K +jA}ef8-y>e+S(mgsHDL%P~AOId_i0cCjMxt6m8d^v0T?Xl|2Z`$Dw517{QZ#erS1@YnPPEu&@*Ev|xo +PNmp-_K6^gXCh6M90d{YL4mps=&+JW=$W$@!k_E-%bXXf!Sj{HY&Vow_fXEcNtP@9N+1D1IjPG76c1k +lIj((+CQt5VrnCKvClxT4pW!v-1D7fl`QHD9h#;AcOA{Vi4ieQ{ve;o7|zJ5Cjx$l>fSB1s1!VuFN^kB`t(8aI(cAib@>0MSoW=ZY=&;QQ9C +v@*^=(7#>nPFjBSQj+&wf*(HlgKrEu=?!8=8-?KMl+o}`w5Z%V;rIF%6^Aw1Wnl1U0d%p?u#;*{a5q@5g_Op +#dqnid>Ai3yz9JQ*?!Omuj9lA!|%DRGCotiWR3mT#&Y%zwU>*?@mT5Efp0oFRFnzxy$Yz-@HxX1SCcL +EE-eddvT&)OPx-ld{cjr^GJ6~>JA{5SLPkd7inLf`>=!-q`HzV~>CY=aJ-CuME7Is^@SW*9JP(HrGUg +T*?Mfdc|gi0Eed=l?29%Eq?o>xa`uaGK1Xn-QM+>av1hZI$2CTA$8Bb@cW2<7xf(i>y=(fCqFaZWuG` +-r(vc&g$nJEky`~LCjchvRatEI84~&SvzT7jo_0}EiT9SQAPfKa%CBtA?M4yATYXrV82esLs0k!{Px#;35mpVgTSL<-`nQ^9rvCPoBE2d^Z)6VDBH+g98mxlcUui{zRec& +`;06YC-`@LV7rDhD@WcZ#ei(m`zt#u`yM*7cxonknf0;2!CE&kd@%VH%*FkOD8_0QqY*%PLeJj&pA@g +1pGpUh}}A)JBCb2&8D4h`7zMmuZ{?Bb1aU^c1<&i-+OH4^=yK2YsiEVe3`v}E8)uOE9e^Q(J_o<}QO +2Ij?4hSwaKLGhv@a^>Ae_&eQ5zW<^~-PkxKG011M)R{YnmI@;{epeW??6eMvj?;H?-YS-GM?V#1ifGU +|pjoU7dGyMyRA+Vf;1**6NCWF3PH^+O-J$JXWX#tYpugs%*cNdphYV|NSKNL;JD)|_Oa6r(2Z;xVAN6 ++OLB=#em<)hpKBEn*(A2HKbpndAok}xejn$0e~>|dS}#fJix@WaXj)xnW|WD{9rQ+$b~1p-m=kunNDE +lwoVBL$A?B^#xF1rJ4Or>-tBJnI2$PHHO-Q5uf2AsmETv2ZB0IjJA!Uu$zxMvhzrX`tz#rN9b)dOe6u +QH@dek|Vvm4Pc|#qZ?mSHb?bq9J}+E&C!Ib+-^E_T6h$29U1iQ*$+761%-4#U`e?d8K$AjA+w-P^Bei +7{nZGBMvWqOZKM?TJyq{*%29+F@>alngUR_+YW^KZxI|Z%F90=)6*@=du#~UXY>U92vUJnk2Fv$1IZP +|d@~zNYf?&Jx%Pq%!T|8ONM#s&LmYc~!la}t^_Bd}MZ6SiSxdAk8Wr|U4Z`bx&SnSqeGPA3; +9!4PvxLOAPf=%&@mJlpWp4kz2+bA$8cK1axy?@RM#riTLoxhTqOMA7VitbCEG34cpb-Op-O3texi&T`4V=a5!#q2yA`PTLteAP6g{jqaG~hpHCcMcqk3y60|rcv3^WhdiqeW=7WxuZspg4 +g;8N=~2^Ul=+G(NNZnScp2-V&{yhejQ2i^`4e`bY-*Rg#-SKkB>%|`Ahqk#?Pgr>WP|5jfTjOf>$j!y +bfHG^}!0YW3ya<_YU$kh2qGiw;|52G>n{;l;=TI|)Y6(bP0S<2Yab9P$7?nyvsMAI(jSxhTf;Nm^Tr1 +#5aS{Sdr>D<}xe)dk-ph%8onW+`+wtRh6WZpt&=2D->1qO`$lS%s8%NS6N4wzjvmQ|`r#s^*que-YZ? +hly3a2@+3l9C^;947~@eSU#LB02R#kxq`M3J8tFG5q2`{|EAhoZe6L>^Nj4h?64CpX>jhwRc%=97);* +*Yy@*y{J=Ve@VZ1C$%|-8zrW$B{G$jwS!1VM2iH}0I3vnb(V9?&Lf$2((6&@mhfQN@)K4(CWqUDhLkuo5EILpdkpz*_i9!9-ksIQO}8-0~Zc_9+4>)_9-a-eF6!%V +F9X=j%h2!`H)3pL=p?a{qr-^~^HjnMCkT>Y_-MIgO<@=U$|xEJpp>Tg6(hOA`6FLEUAl74sth*#e2_+ +i98C+U`wR8OTxfzF+)aI&AJPqDvRDE1%(M~F5R_=da_#7++OkY|PO_5Azn`RkUXxc3LPKdD5Zv3|M0^ +Jr`tcaHT0Et)U+$$Xd+*w2)3d!wzux>%j#Np(70fcBj;AVzWeEzayZL%DXv4H_2lQ=W~q8&shUX(LgM +?UMVbm58U`o~O^s<%mq)_MBm_u1y{!&n&95OqB5EZjpWmYqwm^ehQC;ghJtkN`pP<|C;nc;Ys?xdp5ri{$G31Al|Ud8w@At}fq##*P;{WSj=GF$yT6)E66s4dKzUY$8%_gG+f +Dtb#~O5eDH_LHJm-HiwkcO$u{MG@o8{HRBE6ZyWW<>H5TBiLEzNqkly5E|)dm47VH +g?-S@-Gc(LAlYLtDn)gC73W26fRO0pb_9CSR@k$n+1%ACAs}D69!?9i@8emLs{p13LL*YB)Nio1Nd3H +6m`-mNxEIxLu{t3?Nu`Va3wFXVG=lWvAMbMD&7HLvwL}n4Qt8E9q>mB^gVM3M#fls))@D3P+k#LqoZj +47GEb4WnS(Le4MD)$F&hJh?pl)$L;Ll;Nik$cZ1P2>(xrzZO9P}_S)tE#n;3K5VhiPPkC-7c2-DqSp3 +dS2>zdOCD0OwiEABB9=PFsmx3hR5&wU7lL1o26))qz|G!k9>D^M`+dtl?$O;y@`pZ$VAlZPTUGf1$$V +iF01Nu=m5Nynfh7J>mjxaOKb#&6}SR1HHkunj|OoGoR-b=MB_@ACJVZ%cI#fs2ZObj((V6t&)3=~=;e +$S8hRK*;C_KuKR&@gMS8tk3pyo;r6tfDg7UCE0S+^sjJ((JFBO-c900IuGqG_3TQf<6=^;>CEod2<)S +*?Al7O>w;;sS2$GJ^9z8Zf$?h)0oSqy*3(&%VIv>Xu>-FPeR-oJQ_W;uvC-_gFa@F-xKX}yJYy!-5MG +h=OXyXtt_r-oh6`6s5m1g(c?L^3;E%*QSnu*O$sUs8DJe!2Y(D&}2_n)09n&x6SIOIA)1vKp)}@IfYDrn2=M0Vcm6MpLq{&lx(*UJ~AX-CW&hZuTSAGkPCO6-0aC58iQf+ +HdZiAK&k@GupPjz0cAM_fG8TBC|I&nLj=+$>ANEQ4+&erggjp>)ZB9Sy~q)H69l91b2}G0+2Vlxm$Pk +{UtabTzkKoxpvygyOJBcv*XbEX_uCBO_Thg$mcN!R*lJ|5h)dR-QF`?7lIuZ(BZYnw3X&5t9+1r +pc0=+O$#X7?Y7??;%Bw_GdfzW+Vuy-W&mIpryOUup+0QL-!GQQ!wk-+5(ted@4{|EH^n^u9>dAv7H +oM7&~v#^am}Ay-$f@*wxapVEVqr?5Uh>-8^NB7>%JB_eCucr7+~@!eiQP`bW)z%$gI^tfg9<@AXTAy +v_(-b-qTAtkI$O?htN$s2J31EbLQ*)1x?>-o&tq3wia>vva$JLi?B7z|ncKM^)zwY4r{>1xOs?0^bP2 +ntj-Q#sq`LTF0t27UDuT08`2ld{12A3z`&Gz5gORdo!Os)4ZOSJH1RyuELoyA%$;{v>18$`K^1uEZ+I +LUt%YF_k$2J%Ga)%f*nbQUy4SfL^BLTWr9CE1ZnVvntZfj~nB_Jm}Gu-NdTdb=o(|N*x8)(1#^KWsW+ +r?R6_A$QxtPZ%pbMFiQ58@j$(YnWtctnzwGWdjKbkNys`s(_iA!)d&rt^5)R?WCIl)&(Bi6Ol2_yt*? +twYfW0Xm(vkyTeY`!UuMT2s=^{ltr=X-ye`(%$SCYv`ySAW|!&|ij-fVc +O~0$|#Tv+Kxtp00r|a_&>k+skC0XNLXTC41u +4Ek<1ZgmD}0Q`;(qyF}-P36i5IMPoswx*UY+i~wp!B0MQa**W%!(shx^pXMgW(BfRx)l%Te15CZeC>v +C&&Fw-Z{^(R2GC$fH$5tEG+o$zP+%Zii@|bOaJ9K+xnUB+WVcWGxdFtoipAroH{-DF%G2=Wb^2cT7r# +bae>Z_QDepKm?WoB?*@$3FgXuc!^%rRSyCDjtsWr~00sYnzM8g1@iUH!xMbP~q!!IY&uz!{*AbN1GqS +o6@lb~8bXx#RI9^kw;-bmIOZD{Cv27=k}`*A_J?kzZGZ5;P#8g!ez+9!)N=zUtZ5>`PEx&r-tWD|@4% +d@Pfslt4cufawzv=+U07XUqI+dLE~$XRASbZc@m*q&c5?vN5BIKL>quPhLV%I}N(QC!4-FX7L}PSo{Eo#qAB?Fa_=N_LKNxP|^Kei0 +0q8|ZS<30=DrF3Y6_0#QctmWbNzvZ65Af@h|72MyR>Vbi6!tlQ(DyUXnPr97kfvv7 +>AZIbiE}3!oz;JdOU4HzLNoA7f#3$fMhc7Q>-=@2drM=ApqS-AV3Slb`_9FHWsi22lmPKS#Hw)tfX>^ +es}+aQsac>d0j+TUT|}zG_nbe>Wk~`3o{GN-VNjQc?A#mJ}$==2t>+oZZugg&Kc~J!BUNv50+0$14aof1!#UI7z4@tVU|qLL2ZFct*vYUARUE=S>x +zhq@A-y*YRtrlZ9?a#~NX_5VnU;r6z538E?%%t!?!}2WvNVLEdAJaf|KHqCv_~<8mJ>9Yh22=lZkc&5 +WWz4)qxc#ZJ6BQi{3(!Y^5}Fj5j!LaCaOqxuGY#+tQFoi+Nb(sr*G4x(+U?lDS`XE)i*^u#rn1YXOMk +7lCDTQYnx9Db4qNEKxS$UT_HP(pXZ!?UY3a1L)hz;(1hjaMlk4(Ne>Z})fIY_jINu!$CNmKQ_V6fF=#wJSC`^ZP#Q0-HV5c>@Pz1jFsX`DyX +0A_{xIO_-0h*=Ae#M_!v|hi_@i3{I&!Y+(PUy1F)Qz|XHR8$G}Jc`jLdB)y)C;OMI^|8Z9R7Mna>YM= +57Dcoru_y&56XF=!5!^t~&uu1b0z5BsE)+ljHKA0R&Kin{%=OZkL9LF;Wgh3S#rk150=u}(<4){qnHb +EAJ5`pb@i+HB?jHbsrNCf9+=Xv>p>FhoNVUX~XNvQ>B5zXBan<8tEo&wZ4Wh1UlpAj|h_56Ky*58&_E +CuMZ__%ce49AK_N{^t=k{3zlfB=*b`whFE5_8+CsDN>XULk#X8741znQ9;uvTG!&wmvLo>C<#^6&DVG +O;bE4vNHCU^J8&w{Hxu9o18>+o6>5XFcEClhC#WUT=#(__zG|yXg*4zVh12#~XiMs}nG+z3 +4v2%yFEBm_~?$-`8IaabU-`ho=GmxrfY(--7O!b*1#s&!gj7H1!R9iH8JqruHJ2X=T5)cr +m|DHNb-@yoY-ff@on2nV#~HaxyP_ROE@?6J|cJk~CFpHCM=DZ|N!%Z>LJyO>XKJ7d5O4Oq*Sp?@~?h7 +MR8UK;?dkQp1dNe+J)bQCC22e?YrVl>S1G5xO`?(G#&Y(;E226={!7-cOH1Jx9fR-d(U+4}bSj38}<1 +?iyE@aS&=W$Y!3x6gIi4lJse{lk*@qN^_)uv>w_!Hy`aFYEHM1~OP@3B{ZUke2FkezqQBn3uTW+OA_N +;-wYrWUElXz;sS_#}B>z=;X)I?!1$Se?HaeOq$o{ +q(E0cIVhK21PZ#Kl9BE@1a77132mYI~Dc!_ZuZf%AgfFnTs +xse>ZI4Nb%6b4l1Ml;$vru`VJCT?C9c{I~NAUdM9iGJ8?o^`-F&B3YvA(k||1_(#Gdt^O|M6WrvG5%F +YSfuAtUmxOPZ1W{0Sl0mK*ooSY_%?kjmxWA8CrSM3YBf`x>mfOh%X)n(l0-Y2G|=V>ar!BlhN~7y?ua +88!0^R$uXk32VEKUpQSqD8LfzU_=SJ +iO)Jq8JGL6hV*DgU7X#sCm5Q*7|%y^1eMkzuj=Jt$Vch?nGLlH}L&$)qY8Ff3+nr2Pbk@LL8qW+Z^)! +43J0VUFJ`Dc^j|LY*9dHL?WBse-52!utyn+k+EM{V9r~xuQ`BZ-_tl3ckv|6e;(b#15)nn8@DUS +c_Ozf6y+k6p|zm`LLvQWxZC9(yC3FK!8gmYV+L6kqw*O}GT#Dh(1!il6y5r46_5?B!z|nickYh3-a}JU~~8O4?keTs5a|u{#YzEF{A +-<9B)>Co4jbV7~T+ob=CL3*my9{s)MO$lfru$w=JcM-T)DWdL(VWw39cy<1rI80-apy26hf0Q>+@=+} +E@QA)13d3NEj1Er(u+VNS>-0kO+PRROyA5g5);ViV#99x>`6O;nZRYZSj#7+eRI0|Kpr +OYs}KnKs^(@A4SytaZ@=PU;lDMv-ANS71{hUI{vmIgD(5iDwvib>Lk>vRVb6z#zE`sZOCuTR!>tu< +`{w|5fQv-xVLY95xvSn^HcXlNkUcP&Jc7HW|uUGLnb{hT2cHBC`|Bws$QeZ`attLFoaI*U1OAz) +;~aS7fVHq^@(glN_LX}cwVT1_LxPKX4Mt0}7QO<6Tc)D=l453zP@6*M6TufAuhqf>bA5@d$coFvEqDi +-7A;^U>`N>|cZPKZ!uxf^3Q`aja_0(jt|#bc9(@_~B0C;aH_aDP-wc*Yr6vDyqZ +Uc<%fTdn`?Rhez9EA%8E6C&^r=+bB@YFjarYXTiB2VjPmQ(f7M!pcJGi1>SUpG}gLQ-hS2gDt)#-bA5 +u@lA6Oe0rtlQj3rH{$RORq%I3>RBx{nUi8@P4o16Vn?$zDq2JJ?SG|M+~@(WsV3$!iyfAh&6_y`3u^PbD45P!1|PC +CG0Bb#?yH^NwTDPT4^A>v)_ceB1)Pvszj|-bg_)nKc`Do)USbZmAG5|ugcr-Up0N2o5dPQpsW62Nu?F +|_)$4W18P-}cuTWeZNMu8wlo}%mFk@Fitu;YUTV(nyW=`iP`Ts!EgFa~NJswhZzBb`-|`F$u_=-ROCU +5VR&Ka8ZEoY&IMX~M8t~uzX&8_{V~rW)>%bn!!&O#VAP_CqBm+wN3Nwc_cI5HwXSDrpsVm%GwKuPFBV +Lw^HDv7U-G;>9i}kN5X6KW#9CLM}=_qHhW>FAbj$lQn*|HoU5C#dt=DU{ex0*B2J7LU*m|!8l7VLg%f +z2Osjuk;B=(TsrAL=z&-RjyhrBHg)AO0f!n?nc8LnUkED;EcB!3vm6Ie<{Z57q4Mg*|sVCS%aF7_P^caPS?aK=P(Gq^P ++!xFfX;X*ZrxP4u?It`GHT7Q{Q)Qe8rjol)2J>Icqp9kdCSzhB-9^tNR$JgA=ZG=PV875Pi?-sZ_@LH +_n-bXhpxpK%_9+wLa{*2|J&+9U(CvUdMu{$pZcG51RRr8**6qBY^S4$1}GrTQe?JSr<I5u9WuD2i2#hI{_=dskdFX_x$$r6b4XhBPLjKgyO#ug|MF8pApWM{-`rRiy}7-zXEFqotxbPMQlRbJRnNK2J*MFO9XYSk@}Z2 +gY8wT0wenDqhC-VXYLc$&eF8>TdWCVf$VO4s#UpvuYKq%8xwQFcw +qf`IA5xL|(f!tA^3_#f{Y`5>N%IM!SNb1QZjfe8E7Kr9cT;>uHiNO8Xs1zz14e=qeaWn+je;fIt{jZK +>q8H8q2?gYvob-w6taIt2%deewI<$lsK7{gYRX8C2Ljaaz8|*Os(FO-YqyGL+)o-)r +Vq0_Cv5{4DK==&XCy|#>J2_h<&$4GUIRvPyLS0T$Ob)BGAL?SmM_RUYc}9)sGxpGbSr_^!e>JllNW}r +7(T)}tHiYcaROce+Kxt}>q0k=AUqhQ~iGXBYUd7q+F&@jB#;Q_8$2x5jH%(n!+JIlQ>*-gf@_cS74_{ ++P-U#5G^kbE3SVpLUEg?}??cenn^~Xp0Bk)-*$XPto6d8G;feS6 +OGbISpHkW=5OUm(Qgv|9c$gK)r+3ZV`R-4cEBl~VaYl`)uj{)SfiIYm`zL2>M8THyQ^bz`T) +_8dOemeNug&a&AiCfjm|-sDPkE28CaMrFH7e!^=iqhLSCNP4|08k1!klP20N2RR2n{gNX{=u)6T>9X@ +Tz81qbwqt)%*=E!wI3;W>gOE8j2zVUblgW^<$F8TLME?8LX;t#&T*(J`DT{}L^Fl2}nPL}@9}4(=Rez@%j(>C?17Tc!B-2-`7SJyMs&nvBruaT-Z3Spx$&x6ib;Q-DQWv0G6{-L9x +iROChjT8pET_d<^mx!*6p>)4b9m2@N!pm}TiIZ#8c$Ua{WMuvLH0JbHC;c1KJKE=<=d8*QZHBjlm_KA +fot?$D%7FJ3HcC_2Wh_$uZ^Zxs+xa0?2n;1mc@h}wC*pUxR4@|8|7?9(dr=2z>cFz`Z#(*5vVQtknJN +RKBZ4lbe-@s~68?OPsT*uj`R3-Cls$qp`sC~MvvJZzJ$c9zved4{P>6`&Qh*k`5Jt9T{d-y?KCgsX|8 +9E>U$&n!|`4}bTEN0+0`((q@4(+a5)e;_ +StWM4vkaM&tmk~s|TRYSpk6l}+Hzf*eZwdsiQw8~ZJbx;P%f&2FEDjE=qbd)(>l`*~JDYbOmg$Tmu1WiS8iKWu)_iaO4~=05-6~fE4uxT* +a_>)4}{DjWZ2|LW`7C$)ut@E{^r*-$Orv<6~Q9i|1rjX&|7la)*Rlu5yZ+OPqX~4wLaEvR6^yhT3q;Z +T@{_Z7l^ix<~|^+sY&>69(MKo}+DxXSP3$1nasVkiTJLBUeM;@~I=B?CKb +6KTC>#{hy2EAzoG#d+3en4r73KZ(Gbh**cK +$Gv(c6^kBd@Qjs9fQc<`z)mwB^TZ3n}@jaOvsydr9H8;H3ME(g~aW}^s>5N#|pMA_hdo?aMFmo85aQM +`EyUMF!G^aEQ^gW#9*q@Sd}ZaR3mKDcr!kM;A*a^2m-0zHg6zW39qKHK5XFk9NVbk0=jG(F{>U;IiWs +Sj+QV=ahM!`MSU5EI3=Kn@I8%RzbM#gk3BDit`==r@z{fdS5@)1;S)jS(qe56Wtw)t65>+1;Mum8ynrZc@z&@E!ZbFv?_?Q&1sq4qC0spmDasIiQKcoxGMUB9XYP+9UU^Rz +1UP-X#maMLnxx4s|&=#`bgSIsq +4Ho~FrcV%@;MuDXb>^6o3B~gB>OqU~4<7rW0b7gG +(nwBu_I{{lfR+kns_D;{^l9E|}Ek9?#n%oyTvwGDHFPm+XB-rzef~5fXG4xd_RTxR!}>p3DZ0wH%Ete +Rw#Dm~-sIp?%i3=w=iJHbjlUeUO`MiaztUTqap=0vj5v4{W^4{PU3Lno?A#e#eS-(gF9kz0+3vv`Lvd +mJiM~rT{ymdKky2N9~|UCOHE*$geZj#$msYzdT(mk_3UU2nUCC=2h&c#e8IRpaxn4W3mwA?HF8qxw*R +xwd?sjy8aXX5rYdMFR!;118l=oSSVtsX8LDI3Dq|TUQcHf}_*>Hw+s?M-sL6*#hxix*=MKR +1D7x<}eyd6-UtVCu^8pT`i3#*>|)!j#PM;aR#+G4bWJH=h4RIheOdmSlCn`C}|NdK`+7bypF%MGy22B +n;_$9FtkId8)HW4t+S$yq2n`1uN+S#0cD2|nnsz$WUkyp??%3R0NnnQZmUgM<$cYrCf`kj*zJ&!ArJ< +uv_+0<3WRj()1Q^(|QCL}C9gEG`dZhY9KzT+LtL3s{|B5Fh~2ryNxXw#vnGs=h*!WUjBS?_wFqJ`S~a +MLXUAtUp_{;|(fK?-gIbkn50vRnVVq%nS0_Sgq~a+7ws8pRG{Z%i2bZvpovnaqnyfGBRDafE6oMaVo?5_Ng}Zvaq$K +q-tD9%iy%T1lb!8YOdNFlt2AEY*=Wg_m)@!}SOu~KQXHuAY-{lc#tU6{n +K$Kv;&NAmm-Z1d4IpBRFK?F>O`kqCU0XC-SwGK0RK-6a@&ulVF9}{n!FD(#=Bu^4~kjQz%>Ws(vYf`N +6Y-j=HNJbZzk@OoozAa1+V{CQU+caCmGmipVsULjDL(g6EvV6eIWpuXYX0G=MnY(@S^)5>Y*u&EjR0; +JzyxSg&etw*f7a%)6jjJfCNR^vts3I#Qk$f?GY%%Y(W_-r(zZ)BCfDHGvdJ5~-z9o{KBDX+#M(Dwnd` +%&1r*yc7O-)6+ZB3EW3V8`vOAUlV)WrHl_b&XEJ;jKzTJH4}LA}=wT~&S7w&vp?jT}x7BA+vv%Nx%|Y +3zV30vJofU~Nkjwo$;0loEfRraM@o!SX8hUAZ7mNz3oPMA748`aK1!5`nPDYSl)K6$bTl4D!N9KY{yK +Kq!>4YMjdBA0aEqI{g6CQ&OReC06ICBt;D>DT|gTLhtl2LC|1zg`b}Z5P-;s(5SPJ+hMiravEwF1n>o +B92mvaXr9*Fy9d+)$=cVTDGDz|uG#h!pxk@b-a#t69k=mDn>U|;FLe^97s(^Gx>Z0(l*`eb)06H&Mrw +m|Hx2V_B~R-ye%=6tLAdou(M6nQGI!Vh)Jpcq9{WxMd${d&+HCY#D0v@s)bLoc%y22QFe|x(`s%ypiz +@S-EK!g3B;4zFaNP_W$rG$#2IR1aC0Bm9SU}N|JBzZ{)g}D$F$Yw`G*8%8GhAikS-g^+flD^uNraJ75rsUm|m*}nC(Oj?Yf>>pl^&mlt8nSz0AgLxQ$x`H`k-Vb##sAu%G^B(cc +^^o)K}8i;>FKN1GRbQ0RSGo{Ur*X&cP=bbaYOZUEjrJ}2X76qkQWKE$k8rWy!^kosz4-2cYzE^0m-;W +)pDE~DG?o9ifmkK#fYw8n9+mU#6OiAXzX*R4qbxCLVvD+$g{_{S369*PpnHlWrQ*Ys@DGf)e6_$_^0j +1pO+?10lS*e-)+Bo3~k+hK|?C00Oaq`s{VE0ezb^w~6+FA)ff(xgCcUAkVSeDAdN<4zu!rBi7#^CX{O +m=Itb$Mhq=ib4OCXzCxnf;it&SAC$@=}i)nqQ0Q8;p3(^9R4OEFThH9edLh1M0$%0bj&yk9f~g+1mZS029m;Kl)Qk +JVNmai!VG=puz<53<1NC#1moeZSRW-Lfw8@w4SBQh*-f*>TpcxfY9d4~Dd)_IP+49gCrR`Rz5%<876_ +SklR2oBbqRz!~iiG#3mI61kt~VNOQ8RNZ&Bq}rLk!8p-&SeG4Ec)K|~)#@?|iVt-Nk^}BI)MHe82la` +Rv(Q4;E#!b|{Gpzgme1IQuCMMmsO)jA;ItdE5IR@^Z=eDH_5YRz2?a4dR7iv9U;kGzPT`E4C`Z8}FBR +wPzy5FdMY-IDVlz42wuYacdQ!E43I|x~XCwg1Jk5=A31Dv<-Jc%d&Sj +GkN|7cE6DecAZ5@hs$}alKZhUv;`YC-gbjI^tM?qFr3mA?UQ%_qLk@!>tAZZ +YNAJT&?Ffgo66-}1ECOT2x*IGq3(`;sCMh{rP5-yQuybP1M(*rsnU3u-r7c#2-YxuDJica#ef{t28)d +q(oa&;nLzeTZKvI33+86Lfajwt=`9#~dMFK}|6m1CfIV|)-1+ysc+GVNjsg +`nbJE0J3i~9_R1qik)5SQ>OwR^|)|M+9dF!%?JU0wLC2!0LSdY!s&7T9WSfr2XIF&%rQKR(;=7RnYa? +>~XTuT2OO4AJ5B6T*u;dUJ^uwZD9l(p^bvsWJTTD#o2oQOR)5m4~xtL?33a(4t&XdMlbdGdJtSC^Bhx +mBoGR54IrE_Jv#T}alZ)#It)DjGP=l-LstWVD7Fg%L}*50=0GV5^vxl}nJ5qp+8mPFqAD_B{)*ZeM4Q|C;R|5B{ +j#6sY~4!OUd|x)hO^j@V+cqdzU=HuG?llz}sn_Wa-;klD|w15E89QS9JnnUDhw*7Tfa#vwS(>K&uTH- +K8<|bRLgY3y(wr-OwX>mIbj=zzhGv6G*qm_~p1c1Fxw%Wr7$D%zLw!GM<5@lW-8^znR> +5kf2p8mr0&2bY7DI?6pdnh>;9z)7&^bI6xakEI{GS$3>!buL0|RS;+W2idd(sIzV1+@8q=Bqr76PnXS +~;Kn|<3-o78LuwH|usr``@`5OlnRSg1+7CL!S^?YxzH#s0SHodTqs|N~+9TQRvVV}i$IYl63KwH8@_H +tQ&T<=xtYIb~AJ#HMyT>|$DMbxgrdkzUH$f~ojPeYjJ1qanLd5_ymsxYm;L{>^urKZs46V@;GRPpP&b +e>*^C4lLMM<7~+6u`rnr9AH!RiB^Cp3>!9Yg3quwj8Qmz@da|}uM~fS1~LrpmI$W +C??~n$N7Inuq3+=d0smb*Niq(!U$4h`fz*T51(U|jp`W(425b!-ds)1UH2xN^9t)jlZ-5?0KeTNRgEJ +QI{569XZ5_CByc+AUG#t;8;bY=}0Hnv!MnTs|0ZoBZsa~C>6Ydk^_IR_e^#NfRM3YPH$M$I%lP~WRE} +WYe6`AC80h$O%e9BWKMO>h_guYw&1C%g$=v{oiQ~e|}4YUI`aXFM;i)pxhj1Ca3v$8t)wV>)g5AOyR2 +tZWnN^trodBxb)+MAM^(ll#{;j;yfXWhwSX|q<^D2_XUR$nE3m%M(CaZa +&uv~E&tr9P +4C{(@AK6Bb^@4O{NSiSf5sEdEX@}PC0E?D@m&>Yy*K#2Wbzs5|<2#i}xaU%g=Zeyzv{rc$%&7Qa#T5c +o+w4XRV)_@r8>9UW%14N?(6N+T4XZIEEw;7T0wvzx1r|bfEu$mY8o)zFvpmTb1aK!}(Qxe$#`;OvaRk +E5Hu+AbyVs@I#adqom*ytky`Vm+mpI6{(rl7m)Fp@w>-XMVQ3V)Tk;RnoTz1NQGhs-1hqdSiSx);GIo;z>^=;d*SL2@0V3u>%O&RKYsPLkNe9`pc&L;~Ou&XM9vl6BsW*_>!vn5%Q +(w;j|O9qb@0UzVDMS_8Vocn?g%5^R4-ho${uuPX-Z-SbXd!*lQMmV?Uo7j)V8x7CXibXjVwpI}TpOfi +{FgWrULxR=Db?UrQ1lESoy#EzWUQm!BDQ|1%&SpI+?%RCAO&hyzMMGinYnwIx$D(nb6Mk~8LjX3aGb* +A+7nYL)k@%O$j8u>zlfYmB(e>EW^(UpmppA%_VC?EiF!-6d|IY-1?kh>O8hBdZfSabX?r>EjOOlZmJx +?dkp4(q(%*q-(gh&T7h`9W%O^4M-K2@42-HK`9FPb%8S=m*huxP7UGJkbZa)n>~#qLrc_&9(1vebo +-@{+)7vNx8kE1_Fu*kin?SNS4`A+hxE8=#C-+3EE%_)Ww@d!LAIf@!Eb`!Xrh!sM4LP{}X9UAP6|oi? +J~llnvd^o0E4ugPqde^Wg(2rw!gMtcWFO+(%;v5z^b@B(hKS*oA0xg_vfmTaCW;2w(IJ>+4?+(M*^bw +~lI!sHuD(?1%q&Jb6ZI@wPFp%GO>ede7}d33NVJtu_*|EUnI%dZk0z$7%<9u)CwVt6=hfJ$Cnd+t@g1 +e@g6ws|zLk?n!bgQ&)W*4F{axYxjq?Ce(&01}i!3AUQ2q>!C8hZ?9pn@7V1{;OI96xB}~Ea!QaTi#s_ +bX-Gf>!`to%b#?W6!CLvexE{S?2Zn87}`0oxHW1szN5`F=60tBiPnxO1TZw}9Qe*Va4cYChlwQ@@vI_ +%QpMJOAObd9l0StU70N;K_Q>sL$NKw_s$)x^7hg+vwLVyZ#C5R{G;Lb^ +Y3DtF=wR!64eT{VsoAGGJ9szrmQpKl*@>WPHbO4GF_*e`)d)%t#C84%=Y*Z5A^Q;-W~C0^{pL0|X-Rv +rU@RV$>|VgI`!o^4lVQS{fiEf+yg}Wk3pcU{c1N#c=in8!UcsK+JSKqZ)0qfCr0dUJORY^9Mi-M$Fb$ +0yYbHm}07)DigI~6?M@Tk5=`In!+ag-hGXtKa3+sg7v)GM$l&b2Js7${mTqr16aQA&*CmA6WRBYB}rC +(iOmFl=q?;ruLO2swN*zIJ@Ai3o?E_PtESvlst}c^&HTOigD|=S*mxLJaxM)xevOONs!0jVw0OhMOQ +hsVF)j34WC3z*!plvHr`48uY+|sYQyH$ZZ|TBtt0?sG0weX^*op7Y=`#Ows)0}_6-e-gtoEA^x9(?kP +usMJ<4Kw&A@=+*KuDB6ENP%j9LZ$lRL?=X+L;dTlFiKRlcw+cx;7X$WJ&UN0m*8a%@f(hIs2(Jlj*BG +o&C3CUB}z+u{LtvU3BkV-x44IS^dX*Mu%~-%C!w-0dEo~Wp*uJ>iay_^%rP;pKZ8@7*Tzm(R39JZ?3% +2AmG5YE>p}8*y_5r<6Q@gUuDZs>Lc`$#S|1|sgf2oU?#wZ+*9l>+}-`u+vluOKb)-A@3W-&o6+!+1AD +)HR!RI$m9hQVJ3+$WCe4FNW&4Etv|Ppp2#MA;mXQ!(3U1r5L`%PPXoPq-O<&Bjx)wp!!s?ILnIKy?6*4c9<-%e1ZYoY3yjb0ei($Ug}??0Qqg+& +`GqajN#tAj2%G)A~c=Q;B_{`x~dC1quYwpIRAU9SZ)lctYuS_^nMn*D@R$DJYX-01?Xf2ghDcpKn#nS +ld^mAk$f&acDw+4_GB+JgU&MEjqkU)P?LPe}&0Z42DB`r8zWi=Wf-lL +=;ge^WeND07RD#d&zhLuk4WC@Imvwulohxup5`Owpo%$T`d^gnY&kyP1Au%iz8nA_CH{yXT1&p<((E! +)59_#Je#9Ixg;o{vw5-8VTzMH!o6xhPg%klJF10fL>9?urjF(wjO1=D2i!dpzHD$e69OCL253Z+@a=X +jRj8DmxHajB-H4gS{pm2p^`^~ku6{$~y~s^L`E2T2A`dL@td6zHL}z}I_?S1|`>wR$W&{R{j;yQCD@)R_W>eV&B$!BbaVCG4|z +Qv__I9bSz5;YB*KKp>I>`>Zl-n`;mxYCML!?dihsZtbuoRI7th+~sM*z>u}Vd{~aBsrnllyl6AEW7ncx0XNE8XT$FgNaw7HZWLg))cLb#}^|AXK05`} +QQt4Ivl#b251-6*X?N?Vne%H?V5xPQr1iX^Mjt`ypEOb0306q@3lRLAfWvKwq?>4_KV4mU6$iHxUg9_Aw)BA?tVhVZs~^k6_~_$NRzc!1 +^#HSeL*Ha`h7ytN$oX97oqU{YMaa7>&Y;PXxjs(_UQ597MpbL}?YPYSK%CSEb5x6q>=#&P{01IujW$*&fsGZ?U)2Be2 +$L|Cd>H+J>l3GFuU}Rr}K1(`)fb_$#`^yyei~n6?43mEX49 +HuE~kD(^@_F#}c`qH8Gx@R$cvw8IVtBbtN`$8(jBq?yg3W1i~Qr*tv{VnV-(?WWi6VfzZgT4LO>62;r +WBIGuyKQ$Q$`e9ZI0|GJCd6=3==OQk+FU^niFZ~oJB@>YwUf1j=#DK`M}cinKSJqPoooEqi}4c1jvSx +MKjH<_TfC3?+gO9O;NQt;w&;e@R6?9Y-nllkC)^53bUR$*SmGbCWwody)Z6OkKgbt0w@*>YP6+@Q$qu +sXx@b{0E;ozbzq%9G3!HU;y-4q%gsm0;-aZR6LY(>`zKu|UfL^Gh@kB-_L +sy0sEc)c4h>v8;_-te?-aM8-A^upv#GAfW{5Pu|De2T_^HqspsLZjuz)s2dv~U6)LE&$PA_F(UB4 +2PY_jdqo46uA4fL96v`;&|18=X*Hq>ov8HQ8p)g9P7l@KkB!Ck#burpx#$^dLJvR$nfp-E2*P@OtOLj +jktq0Ny-=d?|}o9HrqOtb^VxsakGy!;{4oPGFRR@Q#=`Oec;p#sK6RJx@-KlC#EJdYMmR6S0+mL~O|}qQtC`cD;N|XL+R^sL) +sKyOT23Nb48rBu4sx&z`RV(g*w|J}YBF4R<{HOX=|xUcqt#lU@O#k<8fk&YX-j<9>&eW?^~pHH(RLv9K2vnX}#_)w{=4JN2mN?(zy=Fcyo22>@XbuJ$l_#FQ8*#re +!eE2K151*8(Va#9nm2Z<(ahcPlfUrOGfYQKe+h +&u+?eG0kX}VYh9Iq*V)agYw}jU9}ea8tlJJ +QFudFPVS^c)(oklB(6^R30%E5R;FYH`MBl2h0MND{3d4i;F4+yOI;%kc=vlQmQ@-~)cacBNJ{Jn)>RP>eE(>7543TrU9wue)cs;P#5%;;%^jab4HH+i!{_E{T)v!txmO)b!Ro->M +=gy`*4WXdmg6c%TM>m;qmZT$P1Vd{xXX#dA20GRr0jXee1IGSl{lm0$z+xf>6Nq|GZ9K<4H_z(r4`)q +U(Fm8zTtEj#bC7sro_pyGCJx8a0JONalSF3KebF6SKFFN5=XS +(EC3Z?4LW2ppkgDmc~7PL^n02!V0C^wV68P|Fd +vWD#d5BI&hPofb89IWvoX%qyR17i#s>ugVL4Z7^kGYGn`cKygXuuyvl@tT#XfegS&!zyPk;j@ +cV)Hi!oCfH$88PjajG#`3)k}LrSrVeRHwD&Z8h{vObGT2zj4e6v@Abf5#5D6unZ%}FsRaDVvHA*q4(W +7R(&k5|6bz*43T*MzopD@#uI!lb(?-o-xsZRl|X=;}G3uwZnNj9 +)bmx7b^ySGb^IoVxReL$NX-a(cJcz(`Edox-{C%5p$6~y)k)uZFZR3ofJ;V(tT^_Md#l2`CtSWH4q9> +-QMl)x3Id5OKw+xS(g*+?eJGPtb#hnBW)DlHmdiXGk0p$1e(j4m{kk+jNR+(e_|p^cJ~C% +XF^!8!_$8UY7$Cx9fjH68Ul8e$rTT~LJ*YQZ1>V=&X}Nx#X2fU9Tw75D@ZWY&3tBB2?(Q=~V*B=ie<@ +;34>qvUs)3#E-fFdX!u$dE3G8z7l7ZhYXVQ%9)!2t}pg#y^@yJ`c$&%aIQa4c2fKTm#$P=P~#HD4i!^K9AE(1EG){>a+f=`#*V~*^Q$Z?jw5|{rn}G86W^va}hv{Wn$& +9Yk03IJ^}@VMj0_^44nnahwL=%!MP9~?B#NtWCk2bv=BDmQTBkQTQp3Z>N>6zHMUTeh$FqZh +Gn?lzyrg-HdFz?KWDB$i&EId*(FsoS%q%c+o8D;uOH2T^l$yb9mnRXUl>lF0D=DJUYWv+DmD+@Khi93 +DU-rzFE*e-j4D>rd$K9KgEy7H8Ny5r9xg_X`#I<0H%<rG|U#rN~L&* +!eh!qr0^7v>dBuLj>q?QN0>HhZ7SfGFkAByIf25PxfJYL>VyT_o&GBc(vnj`63xBk@%YdOgAv$acPBl +9afSJi+_bb|2`=e76?Q|nmkx%wbx~g##?`#$IPgxh(}YMjA_9Jas`hkN$vLRe6|OFNhFujS&sLlfsja +fpw=A@k*L0R*tyaRPuuka7jDRkkUw4Bxo8xC^^2>|HjKUgT%RfZZEw*(Ic>IRhzMy3uVfC320|e{m~I +H?X;F@+^Ek`AB>|MHLeU;8s#3Gg{2FtZ2Dob*J#Bc%g}l1yZTO%@_Bi&z@L-Q1IWo%c7?s7|ydCZ$FE +ufza*-=!8og?X-d?68_M|sz)IpJ&K=bFeQzQy@McW0t=Y;4}$veqY2d49HO) ++o^8Tw>%rn<0t$8B@i0P!9J#roj5|U>{)gmGC=kraswTG-MuUsP>V)`jf+N#x{B?zfMGYw1qb$HmZNP +RrKiz%DS5?Y%Hle7=FRLWv5lIb!&H)S +yriSxv8zr1@~F0pcO1xGLy0@0wFF7Kqet_)-m7h}a-O93C=bDVeCB%V2HPz1N?psz-Hu8L@u6P^YD!@ +Em2&ULDWzgEJIjTACAuwpN+Czi9bIF6fd>60ltm%tUFm*OfO$1?G04u%W%4!L=JETPWmuHMIHdJZWN!x(PRJ>+2Dgl9iwE>pv36~@!0qqhoW6ZDNwFl0s-=!MVE +1PDOnh~xKJ_-zxeX7;&Jj4FQ(XA#ey}g#ZK1hH|Sm9)ID!y^5Az5fnlg8A^3beP82)gA(|e&fI&*JUkHcK(Z9Mxpk@lx!g<9z+sT +FC{>DDg1GD<_NvhUY#X{;ql>Y78grf%_I@1`Nbm1*78A1$2Lo~SE5BByu8bx(Z*nB@Kz}n*g?}C&lf(Q+$dGmCPwOIRtC+=c49mtHCm9Rv +c8`VZ<|U{#?B!GsDg%!8&#}D&Jx~gNF-Ls$)NeZ_xlDk>a3G-g^+Dz2OqDR^yZd^N`!T;S>Dbk^w!4` +mMHSkVc)oDmY-cw^wR!)MJ%n*PtFA@_vydUs4T(LfKQfdHMRB20bl;&-W1r_74VN-tTT%L;<7F*=4bZ +yaVn-B5UzskuM|=2Boj&19p~D#618X?MB!eg*>_6m#~8*GsR({f#a}nMlnsqj{2wFChL8hCFo#;AFP0 +MH_Bi9Uw9xF531DH9cktico+bL;Zae~Mt{~^TlKj;utx`EB}`MJJ_<$;Zsiu%Z!&_D?b*8^K)oyHqkw +}o!+QsDk$~i(1Qz`k&)tp&=FpfTut&&L_b*n@uFZ}d+f_in%RKlyd;)#m8|lEhzJ8D8YI_sRe3IEc=N +jl-pL5We)IagdD3ylBuWZTmSU1>vnaVulU`@6~-(x48z21(`8ddICwE$AVj`nCjU^EVB&%xV%#Dz^kd +|ayG5$nMOH=Sk+r6DwM`ojK+h$U=z_uj|^^dz9+J!6IS`z*ESJbhO7Lzq6fieh_pZH*{>Z+&LkdoQS1 +s*YfFbrXRZOhNm>-y>7fYy5fWIUo>8KU`BbI@pIDLrO#kMQA~k=VEyNz*Jc>nHo4zNH*3YS_!IBBlGSJu^R>$iPnRQg8NHB$JCcDUeoP0-+E+mdjIl6M5BXSQ7he8QL`k*1 +CP0NTyB;@dFG8KQ3oqmrvxYG*<;)=4}@6=WocnlamLH{XW2uQ8TGP9TwOn3lp_dNX7s3yNZ|ttq-S=h +dh7g!2E%Tu?6j%qlMW6tY5$nCx14PKHhKd^>}JEb}}Sg86w`5f%XN%SLAqito``%x;VCP3MOWelwy`! +Z!QAnrcuKQsUvLSpsiGmpa!mD(2aM2>+4Z;7u;Nr?rts>5E_Yd*#ROG7ytTyrdVO7qO6RPOf7gdWu35 +~KIOVer~>jQA6IC}%#)YGj_fvmqm?#qZitJA%wR2?*)J1;Q)p^z%XMli9!!a*2Gy~wq08M;?J+h)t1q +tK2F(;U7V;I7{QjQCNJ&G9=GE0LB$aMH8!HLX!D4vPHDJ8x_KEU)veA=xJcgyF7LWmS0=XSHIl_KRn+ +yX`hKLzwqa{~){&WQkdTfADi2t0x#foP6*cnRz+@R3$5~t-ft(qh1>V`0{nZt%o7HPJ^Z3Jmd2+$)b3 +&9%xJUz)ZCF#g81!;z)H%$KtaTVC++NM$l{jS-IahSd%2d*TJrGGg^S&TdPSo`ez6E;L2$ONPh^wELy1FJ +fsBWXnw&`8Igf9ES$jLuLZ0mF6<9DFpoMqjcWTAE`V5{alLG!rfteT03LPmVB-@N|J8s{xi%%QcyDiGyWV(Luc9YSB` +|+NHaUwQ@G0emG=Az$7W?aiLhKj$6ArS#iQxN1dNU~(G829NP^6Mzp8@?5)mxl5bpwSC3~V#viyv%#t +R|$bk1t1qtK9Tz1|T$A+cpu!J1n4n8_#mEmvVfZ5(q^61$LQ#=qpUS_?}!P$Y8C3kZ85Bu~bv?8Ix82 +_2%>WXRoSs)YVOP6-+_>Jbs?Q%a`hL%}pG#%? +0Upm~iZ%UL4VPveAzzV~qtG~hr*p68$GD+78+qqW<%j~w8<1!#d4t{6?S;C*b#-9F0G$WIDuY`BPJ+N +}j^T&zvWvP^XAXd1(_@n%Vu#u^BPgv`&m2^yxXVc~9Z+9t9qQ&a7(4SN1md<9f`-pvD`l9JcBAqF`5|>R@r%lYTF$* +S2Ye3Sboq+X01Ze@bxuk>)HP8f+nj5VY>@PK4YW!QC>HSCnzB{Sm);Zzd46F|b0i~)kgB?wAqF8)!zh +}TE?kZ2a66?4YII&LfWkAwYnZYhge)jS$Elm=E0&bYgj@LPbw{f|YK*$V3gvt_kOuRItfdC{VtSl(kRp)Xzy15$SFMrZNCniL#ZH%s|=sy#niy^p2*0*@c +`*fJDwacNCSwX~LHumx=7G#H?6#b)zYXEXDrr8+)_C?7HoKkY&fC}?n +kFud2!Dra{$)PD%3;eq3!jAphRZBizND%TF#sVEZ)th)tBF2>aM(KZB?QcD-oZTs5;VXd4TltSQii#6 +|GBH{O%o&t?(X72z>at{`(_fqV3rO>#VuHqQ}dhI&#)6-ZI4f=kLR}&xBV4!IOskNCSXVyAP_CmN8>! +=gsf%`*ZgW$I<|BTjK4|^wWbE<$K-|ew<2((oPCkc%Vehhss<*N&OF_|Iz*ehI14=KyVpQyq?{RQu(c +{KK-+&^YIDc#bn2?Ldt&RidCw>5=4`uS3RPi;){_OQyNX>^Qldke7A}$)OoY`uF+fP9!cb~>*EK9K6{ +KZKfk-pmV~Z#~DQc~eASo89z8MAF0c1GI(3j$*tQ~T`m=NVcZ`yYjS3C=oGJVPhw81@IK2p~|>Zr>eW +Z*jO#gjbui}YzqIuWa@LR~gBNvNaKgiK;>k@Fb-=EJhU3N8xV(1Ldpm%ha}AIw7kVFXQ4h)Xa7|NhhL?tf9)Uo#=H{%{9G1_-J}aNsuM|?#{1lJ#Wwu~I4s0>f?)}tT`}Bm^UXMx +kg!D(!Of+EAnFnNP7~(3p8F5g(p*Bl{+eOW^nNMt6QeUgfZU(l5Y%tR`_6Uv!GhH;Q#`c~1!7ctpll2 ++kh7o-O=WArGJ;PE)1VR$psy>U(Bv4{j`S_PN7el*+AUL+hof>LYj>*nRCLCbW3^CHKlS*4{k@);aaGM^y=1MT8A@bJ}`0cP~|rv`QwxZxTMtQJkJzf +b>-Gou`|6U76J@r*iJ1w75*cdYmLz2?C)}B~`t2qE^#fl;{ChVPFkhP*^*sl!(g&OL^+%qX0Uxf8x9W%7{$44aU^ +9Z&C_m{R>Uiwg3;i9aKnMsA-J#a7F=re8HwR_@@9{}8t73pr#LK{ +O35_m8OkDA40*($Ur3y}2xMi)nXR&gxD4JWEy_*r7g)t!(ovv;)R8yH5@dih6tP +!)@Mu>2j3bMNyU%pp6j-i_8}e0{z-yVb^xZ8nx_oVlx4bw0WA4K%0#R(C+yRb!agl2)PJO +N!cy7~YXa89P6M3-Z?n^2-HxI+Nwcg@ks3IU!+8=<{;@=@T*&+Fw3rgu=go9Frw!tgJj?SZv+y-=3m@ +6}yX#8+zOjB`L89B{+rM+5$g^Q6Z) +UXJFAoB=@7R6%03X9#G4N`zW`kFvJ|kJ(`EGpH7-L<-899i$5k6x+Q4|h54%BJoH!VQ<+&?V-hS9-kj +pfV5#Zv-Sdokxgxh-)tEqTr#rtzGE7iBNO$e?%Nw;Qr{)bg07w_Inys}L32iCt%3m?L7gO?2g~asX8u +-AMFyk2Jw>8ngJ4E~fH3r2xluYb?1Y09(SicM&*_V0YdG5!wxU`zPJ4vs{5Nh7vBp?P=G2iaP}kumcsf8b9Z#^1Ok#tL0)#ip5BHO`) +^7RM)Hf7&yunxKVXiX&eKViJcuekoP*1-BD)x|9(EyrfIV6O$=!^~Hr +ac58QJl2$t3LLouV(cDqlhF?8;>aYJtUQZ^0 +HV|>QY4qxU!;BohtPYwt`%XFQ(G=IluodFl|c$&)Z)BqX>bUu|^=#dTSa-@3`uEuwB +vqIG-e4JiN494TMDc%%~ELZd{=P_6z79w#6X-KTVKS6)(Xw89y5!B+`SMhkjT-%b3{ZQ}9{&>iO!SNG +D}dykdt@0`O1T<6tjjY4{fjc5$bGyswaD!teFaBF^@`%kd9n=+1!I!$FZgFhG7>rOEEkG9_U`+PNr# +@nxy%6CF@{7YvP9PoykgEFTgE+>`R^r_HL5VR(B#I&%vPksQidHGZ)AOL|fc`xxZDdf3u~|Iut6f3{2 +>Vx8S>z_vFm=V_hRyFZ@8?{yiuRFId`^rhP9LS0?kMNvJ;9t+nTnlp@D0}e>y^@0QVv6{%ePpRhb;-_ +*F&mX +f0ux>LqXi~BX!<=87Qz82R{Y9~Vj1R^n%1jACGx8q?8h6a1&Ou=wY0`UW;sm~9b=YM+Fp>CV})A_N`u +)<^rV)gx4`=tcy0zZl06jLg)s7OE_x~Kj~gLQcNGz^B}O)xD$QOlHGKte%Sg;Joo_pbtl68)7GV1E0w +cEIQC^A=DaE|xAUTDy&|N8VXd!kM;IKxj0c#*Yi}_^*E!A~DuCTuj^%Z~`}%!3v3sha_E~{?-PG-(v( +}x3Mhd^Kbt6!vZ0Z{ZSrcAKPbJ0a0pVY|c#deMTzw;Xq2xcbU!;6EKsY8b0$wcI-aC3T?0ifex1F1R~ +@oKhQDlbFz&+MJaDlw1l^*x)uy6(CeElmOkTf*``^76IS;Frw{D~%~HoqEU$jJTOaFv7N(=st~ar0r_m-6&!Q1ScyGK$leMCJCnS?ZOdq!f)LNdazvkjN?g^#OF4h=vj_BQG3`^ugNNUU>CrFk(mRp +Epg|V5R&YedjQgzVa0Zv!sOT)nB7psn)>X}2&joP;MDfr64Q +avz21&%x_=th46b)6Ruh-=wS*n?vXOl8FCJN|aQ=X^M{0d^;G^Ir}|(Ir;My*z6m6v|(JBNebA8qu_ka(X5cb +WQsQ`E>BA&f8tT1BESCc(y&c2n2%qU*>-ZJFKnNFiD^D=tX4(OCS`gd;}GSa#JfMvkGkC)Y?A1?B2Vl +Euu=#()ok!8l-?d7{JhAP5Z4cu+K>#WdALMg|0QNa(I((rN?4vA1;mF!9Ercr9tm3`D0lQLV2cAKny~ +9#FAdC&_fOoo920zWbnn$)Zf#Cv2_58B>p4bju2xNyY`_$!No9r2WzVk +*=aE@$p77%W^-*jTD60>QamwA!q3uCh>pmmWP*mAu?V6kTL3>ySlAauK+O1*^=bqSnHIP)iO3qgS!XV +W*&SChgv^OJx!@zJUEGymV-zH^jfy|B-cUp8w6**x0! +`65&^(SBtT1JXjBV~C+XjZR#^))nmbmn +mlhgu%2zzZTCGpMxKEx7E62_+vQ+S+CJSj~_LKpA +3coTGWoo{||DM#D=2++&b~txcu7>Pk)JR+j0tyH9g>mn}ksQsMJ6%ZONF+HSQVNCUSNJOo{HVAG5LPMc~Z1SQ^EMR}nnri(R&h)OIvVJ`BD!U2JO=AN;Y +Bp=oeObPxpp0cZNeP5O@=3qnDPZ{<57~s8&}!Z;Xtgx!toT@HkvjPm!h@(#` +mMJ%R)O0Fh85cX%J0}>8^i@N5kLEr&e&wxptgR>6zEW&|4(d1Lk#ATkYk4)*wSo*Cq5}vg=Rbl#=eAd +290v;j0a6*xob#T2;8i~innhehPgd?suk$57v{wMql{??z82uiO+gYC;4k<0S|WI-x +)|EQ8W+4!*6nf(=Z3C(qkwgVQu7d|g7fsk9BHOG2Ta3#0wk$NtA2>hfVy&<>=2ImGI=!=amGEMo~{nE +Qq-t}6(qRwyvqR8L5BI>9%Nq_h$~X44p|;B1kZvSBzZE&Z|mQM4)Cqw!GCf4-%6Vuq-<|QhjGhNXC8t +WU=ThMdt*F52kf`Y!Zueayj!|T#-^T%^Yq4~qjr!8zAGxPXUL&d5u7-U!SOC0}Wj2f$h^1K}a9V*XKo%4~psh$pDWK-Ji7+QIuiZm#Wpp +uULN{Ln9tlx@Fl@YiWDiFeKVR7{#-Hb3M4hibBN#fd|M_Fr6=`LsN7(Ao!t!gzB7Wd3y!I4iczymb)1 +K0Nn5?dUP!?{@ppJsb3wZD^g%=Xem-zxHwbegC2pVIUlDOYdL4%$(Oag%EhGAmN*M++dz1P&8rgeyM1 +qVtjm`T4oq35u3>c8Wei>U&qMy0L?1#5@M^PW%hp2SBoCLr>t~Oc(&e5))Ze@Ofv1%eJ3g;2d;NEhvEDi7oW +kiH6KC+#ShB#mM`YBBruU#ozs)MxSJYn{GSv(?vKYD#F@DN?!T;802wNie)EmI__{yEs?TxTOI-Xa~t +k8(iLtZ5;(?w>U}Jai`e7tAOXgfYOBhcVfm)QM-OTp2;%0xl}67Ys!D+CJN}F3SN^7M&$sM6hlF4Pnn +D&v@1h7U{8@r%;ro*c5n64SAm9Z84wE5&@`HENRQPgxgZ%=sDNkQ5uL^Z^QD^^|F +Te^O0UT0#Mw6Y3uCp>BBgAMvvB2%7CR;T2CxxhN-`?#g3NG%oYo1sk3I+Gt{xM8m=~1ihi6&G$~j>1_ +tE*-cEnI!1l}~qG5THn}>9J=X +=E*d5{X$1NSmeK~Tu0!|n>y2Y|E3?WMk{&&vMBvW{I*~GW(i&=KiCoklszVk6wZy8b=EXD|BVw-GW`z +Sd%CWl1uJ3<(tY!)LND1vYSMcN@M*PrE%f4+$f(5XOuBxa25uAc*BD!L*wdJS!vi#ILbDO{4osLbz_8Nz1EsdHm8;%Ax_6C7c`MK$UPm4^}!Cc@m3A}|F(c%gJv8&|5GjX?aqeMH|zB)$X1GoZDUwyOb +aw`(lw(&3J6qa8eYHX)3{rX9_xX&zileZo&@oYQ`t~Ykbg+6QB*V?^DeFr#y5k$ +eE|xgC2}n*9=f?wOR&dv(F1vPT2qzHDwEW=t*_gks9KZ#ZAl-mU;<*h)7LCSG}MpW(g_-0@&q3ja@8H +l0T$V3KL>X_V%fef|Y{Ee(>8e<;uFQ{)P*@UKTH!9XU6^dx!Gl1O4u;Q)qRJ56j0SU)mr@0-~kWEU*| +Bd>~gD3xI3pc)E*aHopUL1>_Y4JdHdIicS|9Qo@>UF8DexeqJU|sSDV{JJdW~9|U6tv6_ef1B=_B4bK +7(k!xzzicx}8#JUiK0-X1SAEneWa4Ny<`4vvU0S4~v_|}yv$1>t8M~9s9$k +i)PW>~_&ByrC?LdFuKT3bEh<~Hb$?mi8le-(uR-thU@`{lvTFU*9s!nK9jLs^&|{>J1lFT6Sm`!(IMT +~RD(%F2pSIw^`b1&rwF1`QAgXjlGQf@Bfyd`qUK0h_H}`>=aXI%r8IA1-^#X_hG94VNClyy7ty3O>TE +KQIQNmNgxI8jjMZ@aAgp$pDWKuB!fZXgcw@j?>x@*0(N{t^s?szwD}w&w3)XX`E|+lLl|%Y&yuX8&ve +Av`z(4nO{CEQDGDSGe@YU@2}X?$V4pwe~VK=z`J)b;to>ya>?XF_H#K)A4pSSK-_Pwx+ntb|5z@p7F;sDiYu)xcp+|+#j^m^rYkZfZ^~qB31l@`onT*7Gr7%X0_Y~4!-~65-yqntJT&#<0ecroe +xEJkXefbPw}B8Dd%VnoJ)e6U>Mih5xcg>b6}S}%5%e~Jvlf1!=EhNAQ?LTEGW&b1#rS|UdFsaf&`JXs +P)w860Jud*Sn*puL5q4wI7<6OxaxO@z(5DC?0?D?b*lij?Wyo<$8hN-=8h&HC6uvUrR|!a@A%vwMz?* +HSfhdW?&lJ=^?RLeqkq_&kRTX{)0g!h76P!J)i^t4^Dwx$iLd)#!kg=nZ-7TA6qV@gz29z#@3<=}tu2 +FTkQf^7SVgC)!3v$G{A$g!m?tq4lY6Q1Pz(@r%YJA}Bw6jF?jt!h+fCn!y!ifI0NpD76WKN$q=)VtXt +HSsf+S(LIj|iHR+YW}Rw#t0eQYn#wO3KzZAWYTuND7FEaG+P_k)%=OUfrb85(*1e++epc#os6b0QdCu +8JR7{)h*`D0LJw?&-Gcw(+Rvp%yOueBprSBXz-r9Pr*z(7=0;QnDYgU&LbX3IkqMk)aa^+j*3|qNYf+5lr)?ryFS_)Ic(H$C +cL;VCA5MPVs7|tK=a)$9mfacz~V~58l5lem(s1=Fi9fRDUvpqgllJLW}GH;{z-((E?L7Q>tGqYJY6~E +Ikolwyi0WQ?r@;x=bk$hpthRie?_ZA%oD92xBz3(KyNByJ@$y8ACnX=9PBCor;pjTi;#=7Y&Q&Niml| +Z-Q=3SON}=m_-t4|F?*dX$+p%^bNad-NjjJ_0t_0YWkCxwNx;)+s(!U!lhla2xzRev2hCO +6t6}dhSxk~;TKWi%3_^hsB;5NsB|~RV&i|<0kA6!8bn4(?5~*z@u@SJ>^GhA;VOpK)ioGvct@bro`M$ +84RTT++8En37r}~i;FsAUM(Y&=XvS7t!5dqAQCb@GROGh!~y-Nx13>j=ak3_YKl#!}s6-|0^{Sm^!+;O+ghYsqXXW>c}GVyP$o8(< +G*|MQQ?#6xb-tLf7UYKd2_s-!48~`ClZE&6~ou{09^WBp_}dMh^s?J7T%*b38*=ZkW3*{#DEcaH}os3 +Z-oyh6CiX6nW%g%QSrn=|||7J!=c#GP^;UE6~Rfr#{7W8;g~1!nHvJ<$sJ!YhfndZrb4TXs)%YgVS +4>kz|ji%0IlxsSYWcktsZ@a~Aai)YqQ-?~W592L))i=&Q81>QKyM0gmlotO-Rwg$v6$#db4g3NZbv!hTXwd)uDoGf5OU>02H>Fp037@`DnRpC{KiU-eocnA(=*s1m51&5Amp9VWb2-~GqXr{+CDY&$A3OG=674Ct03-slfhD +Ci)w8+Wf8kNR+uePB$=0MXHb+2r0R$u?=CNE6pRY(w=`Md6lsx=3HN6s`c1$=!VTAP7~n0rU +dlmOq%AjYetHhVR&SRM-oE;JW%cxJz{@`$*?58^kR`#xb^2rFGls%>m +wx`k5$of6s68up^7Kcvd^wMpZ-qg+V6C(|4!$|!JTFGROE@z#*>UF(G8HyJ^tCLEa%yW|MEHOWddNkd +PCZrqr1KdW+A{TA3pokp(84ReN3iBF_R#}1Hs2cxDI=TQHHJ-0k9Q_*xbMpDi)JM0JH3Tym(HzLkkIZ +zNh7fbP;mk(NZrLD3%GDU4K3=#+jbk5I;-1lE0gpa;C5C9vo`k>%Q?oD95kyKnTylaeV9xDp3q26hR@ +M$}F9lz*hotiw*le^{UANC4G0!0C1-c`)M^{^}YQ}2^2VL0>E>~WgU5BIR_OiBadpLhlxe;)Y&|n3qY ++Jf&gHUPKyQQo~5M$TnEs$ed@~8Zc +fm7$f0*pI+zy(EdfYgVR%LmEwAxelLj-{32Qx6}niLf{8@U$+xE&juzOmYeIKEZ>ZtDPhNvjk#&pEg8 +L!%q+n{^h_CpueZk^b%HlQ`XwC3>fOPGHF#>5>+ZFxddbwUd2+G|wX2dA=t!;Yr4sG5g~nzw +~q0V-%xk_0|~RzT-+&2@rfajbha*{d&*2t3Dm85RUQg;uKHQa^4o-9r8VZ9B4>N+q^&=++hMcIVdUo^ +^X79`ms*ehx5K+5nFb`g@OLs|*T!a1UllfK&j1@7VH&#9E)*UyyOz~f$Pozk1oGaHcK}ht~5&9m5oZ%yg&L_lxd=X*B^NVR}LiV=Tfaz?e?38V&U6 +D?udBC4&xJ)!*p4>tfWjMtZF>hz*C62ni>kGpUu4foWgd}H1G;|j1*oiX4lQ;UHAIe@iVUXFB0^xL)Y +47T!)ioZdt{dK|fzZD!w{VHvJkIUyGb^!6N1Fxoc=MucIYWC@u`4xdOUnES%2O%_OR8d{8JJ_4T_ehs +}MN%mvU5xd=W&4WE`lu~5L%2yMrMNjA-%pRuNd+pIm|?YZ?o{?yvd;%{@aMc7kOB7^-{7^YXPpC;F}coD +j|8KY)+1JyGz`UaC8H}JMK}Ru`Q2K)8+U&_;;Z>S0A>3Z+J`r}>r=a4_%6_E?u)?H2d>B4#WZ9*b=KS$tcM0+Az!19*M&KF +!{JLAeiAZy#iIX^JSVv!(<9)>^z?b5YZp@2TIYawZwT&BRC_?{IS*iK8Q>AxnoeOZi+WS|4 +|_S|I_l*|t9BLkt?DbP{2sR(b`ACk$90Drd6;N~<{D!JNKy-Dj6kHW{4$xO<>J@I%fH?hpJJ52q=5kO +_s{Xv01uEf3R1U5y((!oH4nvg*^eaPrZwu&5teF|U!r!62J3Ul-_&Iek%E`I`2K_sEJUK+R(r3*2B~@ +dU7@FH>_7xkRZ6#@UftHgUW^clbx(q7=QK3sI$dv?zUZY9a-jD=OzS&kNqH}hkMJyD=hrocN=QJ-ztA +uecQYRTAneb_Ao>dc9kO|%dvJ@^w@LO>*YGGD%e8vC$*D$z@ap3}(pV?z>eqlpE@9%LpvXHfibqSVp# +Yn|o=8rCZT4DxT_P8g2A)Ff+-S&f%<%6cqjM6l8mRpnD@>vB7(`f;#;XBW-Ar9g)}1rRRSY=@k(ZMnC +xfIkllG*5m^3N`$S7RXa&B7_kgXen>IMB3lSG1*W2ey+{-JGay3v*gbHytOy4C7Djx|pW5wQW(tRt*I +x5+fie`ZvT!vIg$=Ge*$WBzJalpNanyZrs4?WxC}M;r9$#cxu^>4dT=pSu_F>Hlp< +84E`!8IT{=RI)?Mbm`9-tw`A=IG=%n&Z~QLFLDfqzPR{}lkr`f=_1LYaK?m-_-Y3{MM4C>nKw-Kl99v +O>r14fpvrkt=4Rd0|qI%qKMz+v+-P6N742_;BYc#wY-t~t{AwzJakrAJpCn5-R0NQ_g9-MY+_2D8R;1Tiw<_}5vF!w&|Dk)^%rV&~)M?CqF7y=psl8)xx#qr1Y +;ssi(iTx-vD(9dED}*+@vvi4Vi|`JSi&Ug!p|a-B=jrTzs(_~vx5glWDOaAt5gY>3#blh62t12O^KAN +}^4~rR2eO#PzrhhlBlJ!x*7;Gb!|FP9GZOi~gJhhV+BpPlB^}+Er&RBuu{C2cfqU(lSTLr2T4d9y_9< +CF?Rvs`wSSB-4Z(%Hn>-y%mviOm*I-AEAdb>H5$OpkTu6BcO}Nn7Jj!F;xzjytqEF+*X0c?dE~6bSm;gKKoa`4WdS{VYNWFxT49bRTIStKP0f1!mJ!GvxNelM(?nW^ +Lz1f^|K&8K*Vk}&Q7^{m|@kjxdh4z4=p(%$F4vVXx=?JaH1O`?qiRg2|c68`PV*V$q1c7#

U$hEWH +v=74&t-zx+91`#ha&4_AZGfQ~Z|t4)bbWNF8Ns^Lvi$oiIbI}q+rv88I^pu!W?2z%jjUts+ah)G(2i| +nk#3u37{GeeK0fD0h7`c=W@_&0if4-jS`t4l^SK2cq6MhY-@iYm<$wLJg>zx4?$(1g`2H}C;jkLNP8I +Mpa&5b=UOf3Ea&%k%&A228aYv)-Iw>+e-Q4mGz8)5InlsV2y +Ws0QI*eg{jBfoBAI26dYxF`nSQuY5mRGW*V%>orABHXApV;3on?>NeWp3!4cPo=+QMJGy|gMd=NyW=? +%U9;m$|+@s|SGyBK1TxgdjE5WLtuFZZ4k)@Nn)xzWf1E`~P +o;@1i5z4vYL-*vN)F0A3f7S8V3fO3DeZ+?1%-lyZYsDgQ`5b-@thp60yMDT63>u;bx)qz)d`@MR1u#F +XCwNvz{^-;7xncGv@U|WR_2O#mT>_i(8O0C*OzZZk^xy0+Yr+Uz^X*~arD2~pz^$wHO15D7f+~cjhF4 +(YmO>)VZuLC^&7(h4#0SQboKwG@S{x$TC-+9%q+PsMTv@;M$-C|#g;9djJ*EWi{03LOU(Es)!2ar{AT +-@ax@fmVsmwnyVQt~TA)bJrAPf(a?+$o?Qn;)JWr0@nISSnBjuvtpjKzgaZN~e${GMh8c!XsAg97WAP +S=(cMw%l*8M4!y)%HbvZ>I0s><5UU9K4#wf^p?}o=h>kK>`-Y)85;N2tBy<{7-zcK)82zuFhCQ_gA&& +LN}hATIH8Yy>FYc08Kf6?0!bU&WYfWrwm5{R&3uLNnG>W_CFYpGX!6xFNwBN3!oG{Xxs}9yB0gAf>*X +WGX!-NtV84R!f8|lHf8})=U%;f#%zd$r#v9i@Hr(}S~@-_L>q(@=-+5pU(lVpQ2TnzaS3{&l?o*9Ob? +)x%MeQ_22-NewEHA7I=u*k8264;1Fxv!&&f?!aWIKI-s!kTsRY_qgteqxWK&a1nRV_H +|>aoz2VaqQm+ZdVX;RINow9eDtH2x597!RiL~1@GkT&3SwqeNOZFQ>uU$ +9V=M=<(->{KG|E{P2p3TxyE1(*z-xh%gFT!HLOAt9F(6>`36g)dBfKta%n7yu}D)q| +_i|>kMLu~m-8laKIw=9Schyn9ABN3*y*B*+`f@nVm;1CIR{q7&gkED@$JBrk!z6TQgVLuPLb}Hr#bWh +K^!Tz=1s3Vt?HV46XL3^ql_&e$%fo1ny2_0Jg{?Zt9<0{5)XHMeEky?dG@H +v;26O66m@BcP@zGZmT2zu?-=m>JKdCr^&VnaLz30@83)g?~GcU}b +&#-Iavo^a(SBBrv9VXzcUICan6J5+9ZT?j-U9VOhIO=GV*nbS^SQ%)^bx84SV|*vr@aab8>{C6>Eaz* +ES??ja8>XMtS6)Z;!8z|L4Gn0AREld@UL!~wK-C8In5v5`mgBeCKWBHi$h@-tp{}o4s@~xO`#~3;D92KPZ3}^MAiItmHa*B&>^sqJ55H= +-TWEN*nYjVNoKV1D^aB{=!Xln<7OgY;AWYgbkr}{{B=ngzEbTEaqWTgfxGfEJ3avfMT$mY2YcenY03i +;KrEqGI^e4>P~gQ7Dft@l{Jwj2Y}vXicG +ma|1b|A-IR$0a-IU|Q%t|ZTDwXYs4pwvDTFfFVDb9io)LlY%KrYw2t>luZ(gFI%w6$cvbj0!2&|rc`V +)mBs66h%~`G#O@;dK7EBxg|iOeNFk;WZpu)GahhSp? +a~u5t^NG5OlD-Y8)~4FlZ}X?J9Q2eJfjm~aNiMD5`h?gBJAxiAVC+w5)K#xJVZHBRK4Xkf&qfgp_Md3 +q!;G?@2y-L(6+_x75De>&oCLMQHH=9J|};9^e-498)ZM#UucS62h_uYFBd4loq8hC&A2&deQg)AGrJ(W0Q*f_0rIw2$S$)O4+bmpZt`a|CjP5uvo)%>4Wddr`Xp +5%Tp7l7KG3M~``&m1K1F-W5hm|zih7{xWgK(x>Ifv%kKY8N9>To<7wd;O)lSg=X$Oh7n>J-V-n1zh&ow!Q8e8)UW91-r(==VXh18DRg4FS3^(P=qdZs+9q{T`V{ju%`7xApw +5{j7k$plMkh4PT)J1p056mH{mV4P?U^`uSoez6$8PQ=Mb$4>Jf=#-p^_F9rXM2A}tpNc!Xd(ZQ8sMsL +j<6G-w`I)NXsRq@jSZ^deSZ4W~~}l66&H48aM}3D@@y|4;;Z#)IR!~bDfSvTH +6NQdMLh5x(l>VQ9F_s7x(3oZh&4-9S`q=1mc)KpSIEcykL;I;ZqdNB;&d(`_!yf30cSy=);?(G72T59 +JQOhi|5(Bcrc+AsF@UjY+u$a9WvU@~1o>7eKS0&!tZ{mvaYJ_x{*=v6jpv_K=3BrHl7TmR%@a_Z=IAT +7z_T>~?Iu+{x%;xWTqO6&$LuM2T552l5H)Vd+Z{_%&7afsp`SCbl2{JEi~6=i5dA)@_*Akj#ShuM$W> +mP1|~~RUuGaBPib?XcFsC28MqgX?j%^*ZKqukb-VFQ#HNAx?0pKO0=2%k%pLFmdBX(;C5U+sL@CZ2l@ +1{Q<(jFcJhJmL9Zs@ulk7{D#{j6(2teYTb{+9<)OpY!YyBt8v1g~l6y3XNdX +Y(M=-Uf_Tnr_YNy0nZ@UxbCP?K{5b62!$#IJcV3an;-j?ZgU+p?g~DsJARu9-wxGu37^@6jbC?WEqJ= +lwQc*`B--%i>SpAdRiJ>=7;Sr-MeC=FiM{F+uvZ=18q~?=&eEHh!sZs-yt<|~bvEoru`OhUfRos}_BL +yFxJbW$*BRgr7;TyCj<%D{lD$nHK!G2_yPu~FVC~{0U8G0I`%25>X=MbPFYAxt_s+F@T>;}>=fd?G|D ++qtCeDuh{%8N%hdr9c8hC`(8>;O9HlNmi_>sWEe84Iu7I=U65Wn>|>i*&W!E0D(`c#&G+~XXt)dUCJ+ +Um{NtSr2-S@%VuP}HOm&E+1;+J@&0B)`i|p}ZRnNl#{#GsN +kJ&A#*4lz(U=K?Nl?NA2gb%LX+*E-#DFxWchjqfG@ca=i<*4&npp#iCEDdFg|NhVa$$zCUZtl%#(%3d +$Q`m9$cdDP9HJgW~A*lYMZf2THBUnu7Tmw%b_n!5-keGgChM6%e2&`vjl!R@L8cU)}MIaTwZ>ThX_pS_KG2U|;z*D#x6d^YBC3^ +;hpFDkiy)kwTNxOR|*{_tC5Q(6HXeTza`Rl~2t5vcUW60b(!5$bX8%I-`A^5b@d+JA%5j+bE{B!G;72 +goCsn2IN=7mpVGoa9N4dvHEU74S5YlzN&V4F~87c^uy8QN^k|K+Sw%QK-Bt(Gj-CqKHK3yduoKOF)l) +)xcB8>wP0qI@Ko>GcbRK^Dg_IjWYtCL9mB2n|eG>SDdz5cC5$3xIKJ&{q)3u2Z$dUF~|~Zm-WNjo2c( +gVCSMI-Pv1Kd_(dN4bwcssC)seBS0;+#;I_u_eZ1tl{wi7$N(DEgWup+$;$%7PI6^1K5X@rdpPV7(Wr +|sxa;L*9L)t_zo;Qnr}l{AP@P4wmiavK@2AD9>2e1c|q$2uT$oSH=hEP(xCyb+F&HqMoR?D{B8)H8j;8SRE=lDo +^4@xs_cMJUE^-`qodIgJR)0nS4YU7dnD9i=dLI*b#rxlbA1u}m!peN0Z$|UHeOj;z9_SMTih3AF@6%c +2j<~=ho~B)%2hEJf4z}FOR5c-_AppnB>7O +iEdUP?TG93U7Nzl;J)oSW%!`dsmhLUnIh=mW`BnPhfCmWH?UYJu!=_#?=Hm$!5VboZmX%`n``|s1M6e +-~P&MSzTm-uex7%$3yVU>>(F>Z%(^(<>-g*RH4*L2oB-Txp5M--;--Vtc&@)@f{L +xQWgaZ4ko2Mc`@S>PbO=KJa5hr(ZY`fJ&qSw9#ROsF~n7teO*9F__-bw)SeYOtUUCpMjiSDBhHzltXN +;t)f1jNgtxOVHqQ3yx5Q6Q-qB^Q=|pG~wCwXYxvVwBrZVPgm6IUV< +mXs~#nYstE+pw8w=0&;1)qvxi`XbnoILU;eGONNx&{5^?@Hn?7Z^24sIjJlE~3>be8n5BAf~ +V^$d#LUrbYbs6SR6xEjfmp@FhPrf1|A{y#=h{MZME~XKsw+yp>iPrH3@|PQBP1Z(bjSWtdV@x(A$bdz +WN_1ccBIqd7Qg*pRrR<*t7PzQ`#qjR&@;#gKAW(xX#-(@%~p!*r$nF+#0Mbbs<)?rMg#DqiU~I+N_i` +fc=`nG4&5WOunZUc!+ef#_l<4?&WxHx7JiasCHkp+k!c9_ler>2(937atAe*ENp%qe;HYDgh>41p0u; +VUie&;(}$5G{&WvFxuM!!A@_vK*$jHl6sjo3dndpL)nQ=Z|9gLjo(1E;&-z&4xk_8;rv{!v(2Lr4MRk^a4jUwi;)SRmQvq}nZXNyh +`n)J|mAmKwH4KE+Xx9;m&0~?|2k)2>*|P&|t^-lmt@0%R{RBIBa4CV=Nzjfv#VYJu=n?Jbc=^I$hR}oS3KvDDip=LJvnt?I&pcjB%YY +-vqD;HbG3Zbim7=C;A%JPbi3Eq)yg!4Z4fY%X&!9A!xmT1KZZuBqtl{si@pbFMvAX;8K|-bGp*AV#%& +jwfc5Vr$)Mb+27e5n4svMt>;8Xpl_(R;g{vs$qKH|_`z=jCIV& +mHhWrQks;ZhfmNCL={vhwtQnW{de!0=|E{XHEaGTLZdA?ZXt3t}nce=i$HHUZScvk$axpE9Ta191Cbm6$B2#?ZVeg*`2CqT(qgc2 +?O$xZPTdYIBb{4ZwG3sAm-)LVAD?^G;osRm}$>P_=S&po$5vO4FBF4eWa-;OhgYLcBsv_W)5`Z@b24$ +7<8#2+q%$_N%Kma>SjkDHCby*jh_YRbbd|4W_!qo;ndh9Izk~||F1PAPEx6f+6Ae*P6Mwm8Xfli{Bi( +d7L-2b|Ddlw&*Y01U)(cn@SjqMC(@qIy%@CV$KFk*W@sSA4WInWt}3 +z1&9Q0awFXm><0Sp5Al9pQQcLNVAb37Qg9svr1%B=Q3NAIWPXf%#3b&~@*UGNV*m0jOb5Br1K)p5d&( +$kXeT$D*32>uSG=8!R{#jdOj&_vxc+XCsn_&s2>>a|LR!vNcs068kH*6wqzIG6Zv))%>6hP?TP{so6LkkaMt1U&B+fWF0VCcppe9cQZR4wojtqH1Rt0_8b@hTVNNiiqe!@ +SP{r9y+kLodQ}<)A<3^!S!MO_K+8PHNyz&G9b`LeEqLIuiLE9P9#}JN0U6PtX&d@z2?>Y=H~@p%4I@h +9{ytR5-yPj#s5|YOsNP4LhF-#n^wCre%D28(;Vucp91S2OYR+a+g>U)i>8eg1}5!90-mSaxJZ(bb0gnW~5W2EwJm1+FE%ixi22-Q~Pgo6WQ +RNdJ&w)^eeo%t4DzM#gzYkD^^y#oIf`tRk +?Zv^d*`;l9IPGIxfv&tnxK5}5>9{Bj@CYS~qSS%rVMlONegK8~alQ;+^D;5OBlN1{^7tXw5i;B$E#ML +wP~A}tyn(JlYQ4Q-_m5VV(<5@V?sc}XBtAxVGQ?)i8h~L?R2VOfi^O_Y8vryok|>2t +nEBhKXut?Y)!&=)i4>cQ6-)1pLlauh$gIBwS7xW%46Se@4t1;+}3w3^pcj7_MD0a~+y;hJ|milhQX7> +-2QXALG<873QZcxO*<)OFbKMG6T%mZ8>(CJ*Y}+9N6kLSiA;1bwq^XAjy+CHUxp+fpy|P|LDGO$%ko~ +nB_shEtY>RQ{AE0-N*Zf?0|bD%VGKAhDd{-Jtn$rt^q5Yx}*<9v6P*+Ers4;eA23GDB@BWQU7QiKR+H +rR+G9flU#{u@re*Occjw9r^4&8&m|Vu1f%g@4khTz71Q4~o5yvr1;b>a7NiDxc?u;hZU}y-eLfX*<=X +$&0L{25j`-{HQ375wST&EM1HVU8?1pX26p0AC6`Qt2M%-1BJ%XM__qGcoW>5JGwS^i}`>fqs4wxS;#i +5I0mW*7Q#B7CD?8A_B!?+_tZH8&~RK(9nU!sAh(2992mdscty&uE;(B&Nq$-H7M +Kh=@bR&J=v0u6g7B39O%t)J8Ji!)gv`Y8;gy;D&Kv6qskN15j|@ZN!Z`+<0YH`!@n6uvT|Njghj`uSs +;XBv14rDZ|9m5J^ZRlJ&@sh}sTM$Of$iNI9OJVKTeK{C0nd`}w5nbo5Cv$-cN5WxqcpEa@}nX%477%L#N5L_2)Ym2V@ +aF~waKQCShcm^5!tLk7aZM|fkWqN}suxY!n>gMu&t(9$*fO`CO`7e|h74Q(PWFl?Qm(*C+6c=5`!<$i +j?OEU3^{0h(2j%E4xw|PPB9#VO3@qO3e)q6h+G#~{E2erYM`9C#y76oThm=H?g#x*yi +z$(J>9^o#-#aPEVP5lCu0wDbL^hPKl>F{AyT|q;2~P(8`!+m2zD=$nF|kd`0@_GF3C|8c&xZqX-vCMu +kJ?5h?zYN5qJin`7(LUv;{LMVNII)%)^XsRX1AQeV5q`l%+IzxK0@Gu9F`ikuOww@R-ex-OFS$d_ms3 +$FTt(p-Hyw3yHb5yZLrWMIl$RqxIA6!@1E~?ra!OUW+`=)AG>(kI<`YZTE9|rB_L`<6@M8BxZnzD9f>IE@=(ai(4K;d^43!C4l;M(EL@T8^Uf(HLo$#qn|Gv;KX-o3scA1X;q(W19@_Js$oB$KQ9gWxB`4< +@|#@CEqu$#%Ki+7yAadv+CRT +dL-yoI5Lx~cHq{j-02<&CDoXpC+Jc-i8uhhPLjleCXkMi)Qv9ycMKYKs3o=z8@C=%-_ytL! +z>7stb#FEZHISRR-Kf~qKlpSx4FtK{*1r-8v(T>V?KBjn3;W@%0481p8b%_V`0I`1>trF?H><>*PKc) +=Ry|Fa_=Y}u3xvns{i@oDVB^_Pd_^_n?(yGtxTCNjJ{t}4MP;S92v+@Y(=UfHc#L9HJYFpE2W#MIq?c +c`~Z(e6TUglOX@iqCo-LG$hkMHAp1$C_ufLHxY6!{!^ +QdD#~G3ctx**97?GZRWRrDhhu83H}!vl-IuWe`G(6B4GeElYyC6$zqN}PO>OtU|kr;J6oIKJ^$GCrbw +8s5OhU-S`C?Aqzb2n6Ah?vniq;F=2Z=oOav^eL#zb&6Fh;)Ed4>6uzi4NmSp+rEju-Xv{nm9@1zf-PL +cS!c{-<@61dehkcE~8X`bMu4j=MkFPPXr%}euqyjQhziPzcy6Vli~7GlnAgq7Y>%~@oCyE>t9#7Yocr +oUeEl#Ve2kRJ$-GS}J~??5A1mU*H!vj###Q0=W@j=~OwH1XM=Cha06IXBV2X>_b#dl$s*sGicZE#r-|KFUgqQvNiwo}uu#!#~-03d7vS5@xmT8iw +7w|e3AX!rOOD}j?Zt*TdyXjA5GAB(4lsqi2>QLTf4cA8c6lbi?yMsv;a9jd+_HBcs{M0M*FN8@6Ji4hCC>?V);evC~F>V{g? +q3+_qQld1I)p8Cj0Nv*=O&G4_{Q?hYP-Xyz=LcE`h0VbWZ$u&J@^{TwbIUf*Ue;?s?`Ni~9VmMt@K<} +LH$!2pj?vX=bO4A5|Cv2e`1QN=E@qw93B<0b6f9m3_WNWXO*#BcO7)YCO2v2F-^qYpqdd4?l?3|A}z` +o2sbGZa1Lg{ekpGv_=0csRv=qy~6|N@DdF&Va>QCy5%-<))N*v7DzQjT>NPbLFfxBx9zp6$1WYCj_ST +A8+SDE(qU!&g4GH)Q@IggPvAMjWy$0832u@Ukf;`X6b_i9w4MYG|jCP(1*|SWc(i*42de&G_`ko3}0o +DC=FBtu8;CxXK(j#0)KomCweFcR<45egrf~){N{(fQ6r*cvUEBHqGa>rgfiQ3x9t{saFVoZ?P|hMa{! +)2T!C*ybbwpucs40s3rmoXz_qfcN(RzON^wr}R|Pzclv@Qw9`9XHJLxuC?dIuwH-yXGe{}N?V9nQ{_V +(zAN5iYwp0x;8E{PL9b=23M8vf1_f6%4L=|odckJRH8V}>UCFe7#MBSSFV +>_PC!zq9cU2w3!kYi+mrAX!e4Kk_mgPb}~dEmo#AblThe>GjRIPfs^V_i6-t0)!xsEiO~6F3m(=_H@% +7NW;XAgCHqC4F;eXBvTDMg+RhdkKGFF3FnvD@?Wz2m)rk{5|Cy2RjxnJRQ>hliD2c7XXxWC=%}{fZAe +`MY45qi_BycaR0u3r6*aA$HS0WzJAn_E>f2F)m8%STv?J*(m0)GHHf3?yD($%`Ww)2B*$|nxo9Xi3nb +^?x6{1WV$c4{&M*~ei;{Y|1R_ze=?KIK$a0TRE7Ln;%!-7Es{}Bjg^JisMw_u}C+?4j8ZNeoqM +_PKs#BfS-L-I_MgA-%N<#2dTnNh~Hn~dsx}1Fh-9v8ZKv47G41VKZew_c2Y@r3QHJXstjWZkgoRPmR`_`nA= +U!LfG5?0wyY_H9GI6=YAS0RAxYmuQeaW{kpWcVeUxtbc3lxtwJRES#O|EMokv}JGj2|9hUzDjo1 +3}3E4-xE>IX7XgTuH4!RF2yDlA)mAcFCL1&pde(&gKl$o#tHCY(*lePZj!wix@;5Q}tINUNb8!{x`F9 +BEfvJ^G#*dxxfXHvZ&$UO%N%3!T>wqoQc0-i^-0KAzl8_zN-OW${-HJ1_?oLi?P5K4`omUDI8YhW*bYVHe<{~ +ot_I}}wn&zHZlhir9L)YA-ytmL-BvmT#Qb@Q_jy4dmhH8S@{nhc3xaBZ+z*=^`=L6M-XxtU%bQi7L6bDM;Dnc!DVu7Py3p +mBSo5^C&w12=qc-FcJz{5jLBUjZiwzqxds#M^0 +#&Fjye7sbzauhS*jgI4ETUrZx_=5a+D=8 +YLaaO`QF94D=`C2y@UqlmE0fe!mY#;s#C+OEP%KJyDgFYxNEI;J3=wor5 +!=@PHiPH`Qnuv-#Rc^BOR68g&Qc`$5a3vg31Nk8b}n*OEw2ZLF71;Waz{r{gfvgt{get@GR<8TL9fi6 +nx!B$iS|gB>Z0<2f78Xor#|M+@>JEWScG)=+=u4r?6}weL0W7-B;^{?z|ciRzS#*k8NrJ3OWBiwI?cc +;N}YURvz<$@x#&FYF)|d^Vt>DgH`ia7&FA`yT@b3BHO_~i)4=CMV``>>GOeO-kxMqJpEb;*Ex{Zg1`K`g0Xp +5e=GG$b=XvssQAD7FI^dy3>RPv)RlIyMS2?$emrv}57iXIqz){7)-1Mz5MKS2El{LP9n=G^MAkQIV1| +u7?PdA12s$sg$xmuQRNPSLT=N5Q~{#u!I=Y50WpQzF`1ONS>|HJ4r|NWo;^UZ>=Hu!%KKRBFP5||raw +GMeAXR*}DLN;GtJzX)j$vPP;Q){j9L;vzya2sD=$G0;Ml&^2)HDc}`#D(6=azsHF+uWYYjX?mL^>OjJmj=F0v;TUKeGLf~8pp-ECmFVx#fWj{60_`VCQl@mre5uc-qbBL4jw!bn` +fo9H)%8UE@f>Puk1Qd6AHf50~EOCp_NS`x{j0PKcUli3n!P{zqJnk7@`aX~cQXo~XNB5w@K&e9(nl$i +tGJ1X$;eqE%w-x2XnVUdrE7fW+tR3du1%K>4A +z)|e3f8DD)MFVm(6iE(2(2Mhp#i@I+Eb+;M`lwQxXDr-MUrA-5V4g|F +GxI0e>vBuN`AXU4292Y-oh6*d{uffU +LX}ii2g@(1pAr(xh>!CVwa4L4tFvGy$xk}sD05m0?xz9p};oMMH6b(`n)yAVFofLh=U#oxzNH-L9`p( +@~{u@?5mU%nKd%uV32hBU8{`Mm=nq}j7>ZCXKbWOo`8l@@981K^Wpt7PHT_(Xp8x%YKmq$rd~uczpI$#bG2j8>+dVvM?`5+HdS|`XVHSb#LL3xFX7eD0S}OF$kX8bmux +zJE*A982FO1{z1kx4Za>W>>H`&5DF8K#&T8$$#0KcfkA-k~kMH{opJ|J1-!h}!nfqJaC_D0XrsdqF>y;jda`4_AuX|3=}o2GVo;>%-8_-48(WJIuuiBYYOs3WE=0FfjugJLl +kK)2+?9a&)%Iyevzcm_SPA07yqHAKJI0<*IOu&rT-cGoC5QSy^1IT3BHw>%x@2StLr5%hi76;p~+69A +dsJpW#I5mlU_RhGm3pGK1ax!4lOgRr^|lqpYwK6rY}*^r|E>K|{{CYE@!5N|>^&50)DeoMWla?eDFL>yqxNO72}O +i>OxC9O8yZ)Y>ePtCAXT><4jEcmOXiLzUH=DB%5@hW|#5TcYUc_*G%_ut%KqTEEzH7q*^gw +ZwSNPsnRJF?l04?fqX_#qn=$VRiI)G;4oE~y9P}e+oI#km?@^|N +J0Jja`+1v9h{b1v>E6{<)Y;Q=LGi!P~D9iKvZ#EaCV_r-~pz0~fd9Vm0|W9LtiZ0TLsij6ZJyD`ZSEMd|SH)0slJhE^<_2|cS*99z3jO*8qW=Pes=eVeQJD04Yusg3-nc`|s +=TYyuoR#+HJQ|(BH_v`6To~QXf1u!+m*Hsv6}Mh0jmM3qHP+eS^?CwDbA?v@;Nkbi4_0Q@-en0BxXr3GvXmpz(ZsXL+a5GjFcbKG{49mCl+{wE{f>`%pRR0cFr2Ym%HstpUN^u)h +==B#%(4y6B1K6pjIszIlAMPr4Bqab)$+yEhC|MCHZ2A%msK#BJecgJK(e}@{fz?HL3&Cq6xl=~I09H!k**6Or=@lxNE@vw>jE@-XV=nXH4DlTaU$>EZ7h^w#MZ~PrxyjXT!z& +aMXe^HM6``{Bn+-2WpITlpQI`e&&46r4c|c$O723qN*+OU%B~VrrZn&fO*3ye1rZFF&HT7LE{;ui?l% +9KJ|w291_Af`$B8cWL%n+3SbX#K#@ST>@Xx)8>+2Sjp&7XL0JT`>d^;Ryry#l0478Yv3uA<+9j^_wJKA+G&)oa0Uj`gpvc$reJ`l3NCk$m$O~l +ZKb1zez)Qd@C8{@+`EmJW%2x9{|oL_M4p3_)axztr=we#1$eJzTH+O9M1)!(%57Gj{P4^_5vemEOB4c +7vq+X)taL-yWMR|rYN*cAWLzNdESh2K`iWp#z6D*A$ks5_8o?1E+Wxc?=X_Vu0HT9B2Ey%Sw9xZV?iCgl +vjFz7|hU#b~~qPcjWWgbZA<5Y-IGB=s=F~Zn$C_8xn?y+=J$d@^_ +b?u+27pLEoG#Tb0vfuK +tlNHgxcJqi<{ei;19}loXs`x6jIWH;9d1qbfy)`?V3_l?w~5qKclO`bbmVgmA +S&9KQb~ip*_+F*)}I9I9gujDFwp&${imG-pI_Wogdr)xUB=wIo?m)n6BgXpT`ap +Jexm0MslLVX1rAM6l<9w`Kc8%$^En(I!3}*}{!`?>;W`IVk-rI(WMwGWzadAJ37g76bh)@YeWl-*A0d +R__r9D-mK?256P)Ht*#qrf{i;LkZ9>urDtw_Msi-D#c!PbA +B!B@&33JLh+cSPpNwsQiSwuO~rJV~Q0dCZHs0-i>Uj0w9)I$5lko?fLW$ZFszq|N?LNN@Whg3=^N3Se +g_5U!1_KO+zxmGh!_G;K9tZ9_@Z_+?;|G7;El85g}EB#7fF^$PRmAh?zol_yl> +F0y!yT>Az4%CARoY5$8xW^ULSs` +)!o#3fXBvOXAZqrBzQrfZ~WRJbf7O5gM$2B>_Gz#P=T#bL}z?sWGP5wPYVUd&q1LUs#a5>T)JGK2!S? +DJ0*A$5h;thsXS-?U}sRz&Q`3BO=Bvs0_1>4zp+#m0qopSF&o=jg&dX@v`>W9`m|S2NZcA2Co=&6TK!O#HYv|J!uG*k~GaUK +1WW+H*d0;fJpW&7})}6Vd=wPaCWabe(--n7OED))m +o-?RoYHdsda^)xgb#wIo0N%kQ_YbOF^z3~8Uw%wUG^+k?g44*Z^;zw2BNF$TU(?Rw{o`3r}o8v@Wr$5ts +13W^jawK#N!^4h4|5SwL+$JD9!-YZjtpTgPH&4rS(3fW^Z1L0kby4@Bg24^B=2|QDA~iLJsTaAzltFz +2Q%0KKF3mJ3uwf7R4WaG$FWKCj0|Zu&Uv{`&i>-Rq-15l_-h2(+N#5`Md+(9EA)L{zGS +fP{+Xrmlz^wuN{+w&X&sR@w~rwSx^j-6ORVcD!SMK*7=$~>=GCf}nylIiNiF*}DVFC+F`ppt5P1Yqpn +n^Gq!*vGC2ZEv{OGLCeGt>7HjFwz&0-P8OF13k@;rvxTEIF4ZRquy$=hG_>M%8sb3TXY)~w_CF@71Z`2 +YYE&j5fu>KGQ_Or4=CRE+7VmcB-mNWih-&A3n{iCwjldB|hK%!@{@{FC+=H-`67Upqwu-@zg^Nyr-dw +Y+ek}$vH*uGo6LdusT^1zwuO=$SN@nzQB|IkZMJzIi?D+?*X_-Gp0RBBe78Zgw@NNTp1`Wkc2H`76yQ +u)wO1dAem)~Iy7U3{72N8Wv{n*3j+-C;8c_dYQ9pA=73HTua&G|L3*CGzjP7gG|LD%MiF4Z6shV@Yih +v%SxLsSBNAX1N5lwhbJo+Q(jh4yvfY}{wXK-WN!ALhuh4k84K6$m_yGW?65SE3DAO`i^ +Ok8#;z05`{MT;ybzHGrFC^`Zz{B1LJRH49xuJY?l)l2B%w{w8BPjmPzYKM+L^BQ`6#&clx20zD7Hn=5 +@(IzkM3T7~YK#^b_7AGVu*X$8zr;H$bTvMT<>@ApMU9|-tx#VQM^1}pYZI~%?*g>IH`!$4Skw=@hz%r +n`T^xtMT96nt&(YlDsFcO8`2B7pVv@~RZJtd(nuYGityrhOH#DF(tyw!_J*0g=1xS|uWcU`jI(~>9qJ +;C1fdc~9XmBLJLYp&xVr15y(TW_2%B+y}SQC-M-Vew*@D1sFg(oRv-5ZcKh+2T<{Btl#b(;ulSWi$KR +l}E_rjIb^|yku7fd?XE&d${1DrYOo$B@9Gw8|cYB@84cwxtOznz+0QBeCMX1R=Uc{r$hof87+|yxs~m +RTK5h#Jx6Uxn!ALzO;oO)1}m9+@-tJOxs7T+pC&Uaq$prtky_e__PYWOs45P$sg +tjJVIo_ohzwaoUE$14P4KziidQ*pbBahtPT8E9c{4sUDGuJu*FxZI@*|ZRjmNj>j$zzQt1MPij^5_xp +~MRQ(YBYgYAkr8^lJdU#FO$i0SqM$f>vS=y%zCnM_qFE)7g!E?Tp}o$F-7&S5{;FJ^&0eN|H%b^bB?n +LU=-gC~G-7^%jr!2GzlH$tig?B-LgLl>bAOBr3Ve}|iofTt0hkgEy4W`Qdz1XqJ(X3tav_Jnn9V^>Hd +rTTC^EQ(`*hiH^&`$<-gg&q@iMND4B(5*`VgfIm>!wJ68*=r<9l?vU^#!^jesLcO61vuT#m*ux<^r&$zKD9-%X15TxysiM&#&Xr=bPJ)Ljs;fD17T| +z&I47hI~TT!6p$z?{i@*Xh+e72erxi@YZbf$p>Edv#ITBLiW4I7{Y}vtIMWiVc;}y{AdeZ~AxiLyn@%Y^OotYm$N|dA?KOoy@;P}O6~%Nez+cAi)@w-<6bRNY@M +AH5i_>J&QFAj<64kjgdR|FLWcQ)@gFywiUk?uZhB)kBC0KnYX$@DJM}KfT +0x4OmA&54ltM+70{~hR9#hr>$SHRPV8K1VxHJ)jsdt+ +xJ6fq`*XGWMpK-7ezTszNJ}J~H~T9><{1Om`Wt%Xh74Hh9foWe(ZVnDX)u=J!$FfNK^e_1&o5Jes~_Ude#FtotZNUjdzvNsMvDaYv4!NkG{7)m%EokGxflid237%cAB$GEY)oSl_8>&}aT +T@pP6v?9nWOjt3+y~5NXddg7$n=aShBkJb;N=4e)ve+NlDDV88QwB+;-t3FJ`;VsPh(Ca*J-bSOcMux +J}^nSE|?UvHQ)Pzp@9*Wgn#}o6?&L_~^Oz3v2M^>H=hfGq1J3ssn+LG1~0!3IWjpD_l1y%EdC5*7?i; +Ia5s#3KeWO4;Zom+x+$j1Cufy%aX3#c~$}4s!v)~tDn$WvO*?$3xpx5*K`SLtp8Js4`^1(tzf`@N51{ ++I8^w^R!S?lMW2V3dcRuYLKOY7Le`^5f>q#TU;|z8HgAJ>8 +tP>m&Uqt8Sm-yBSA^1WeDJC=Dy+x~(IBYLjtf4aYavHfROGMrsQXYcayKO_UC@5smd6$LY^F|BQt>pE +M8(v9p*_JvClI_G>YXA7C%WczX+6^u3C>^O3-m9?EydEJz7j&i(y!sBAts@DFrYaj0EuyN1#0$9TIy) +MJ5bKL+F$;m2U>tn*k&mLK(x;+;_7*6?q3v;adSCdssBL_FEkU3e!zO29iR5v4UluSa~vy{6sAmOcLQg +F10V*uY|In?`#%DkgN%S=B)eO_#3f(qxq|GhHCF!&&JeFMvfTioa$woVWzNPQQC)R&?qsYMrNcotMMfdR9rLVH@dVBq=Vo4X%_;n&atA(78D8p +!#bgEU&b%Zd~yhWA~9PYT&203EZa<(kZzd|mX1V;PHUfGgKUqaCGa(56B4KeJ@zRaG%bH4qAc7Om(`g +^Z{)*e*`t$m0h|mCo#5E+2}aFXZE#eg8fS>by%{;2Ya)Cj>OxiGO*Yxgq;WTn@5$hJQl>fk^*JXX1iX +*gouQ1kBrxNuK7|LV@)A$<}nk|0mMrpwp1om)OBbE(P9(3+&!=>VvaJ@GD^M-}qW +37#=CeNDX&81AU~s?R8!F1AgzPNvzmn3~>x-f8wyeVOsh&Vg@4Hu_@sEliPu>=tBs_MkN6z +RgS8UQ?ICBCEDbw+`KpRaZ8=Vw}vMi%QuY6nxYwKsC3475x}K_#!T4uAKptG|hdA_4TNF@+i;XqFSN~ +FAR_hy~rhL1!(1jys!sDvNj9Um7AIE|y)+aQbxe-5l +dtb*?Rt@0@@PE#Fyi^;4yvr!s%@iZ=7t{4E>sLsL)?FWt@m*fKjinmYjI-^=X#L8?mSo9-0?WGYIqbP +dt4Z`vmGS+Mnr@5ThcE}oxq2rzGCE1pq*7ov-HJZ}hXGbxas`~T3KXyO>`s!NOTx8bO@LIEvYw(uS%1 +G56mFR=DRvd5*8}AcWt}9pi^3L!HbPe4TnEvWvxoqYDJxrArQ*l)1osX>fNN$I$ +)C4(H^CQw97*uWqWDBqq+6>kMzWxR9!LeaIg6CPM#?IU+lw?SFgQ`&S7_yJpQ;{YwhXbwR3DBUbXmgb +eWG_e4c;`jT4E5Fh4oD!*my;5#H?nxjZ3i5F*Rj+uYszfF};svt(S-NC2?{AWt~k*m73xRvDMRURMGq +!Tm5i#6}Z}i0GLz7EcF)SwYgYLZE1TAT&kKq`ZhXe!Je*DDgyn9^aFo*2O8U#?R{;1V6D)jWK@b1Qczs{;oDcRK>fmUHL?b*q?Hu6&1U +XvN$eT=xa%yUQIAErbebbQDW;n{89DS<2XYniULBREsjJGXDsvwj8(J7u6F#J!1HeuLyZQSu +jb|&j&`Qdi;|-M1XBkuJY3om`#h9$H8 +G`$3d`oN@6CXCReUO2$b&-Ik;Xy|h{Ez%O52LVvVu)gf^nJ`&JVPNsuKqsNR+F9TKS(v9?J$fq09jo3 +EW^xhWid*7GQ?6a`AD9Ni7on=o9$d>+Dr+cL0;MHyp6!n|??E+0+?qP3L-VDzgv`}SHCeb2(crZXH#F +Q||&}6c|@a&6ZDQKKou=SMDslYO+hhU +1m#*lO^rwxm7#zM>bit)1WM7g|=cf*#23t;3uRCZVW+~&OA;ku`^2Yo8Mh8z +#;rc1s^9GoZ22ti^3N10E(+xu(tvt|epiP;EMUz87C?Ml1v(!IuN(CCJAG1TSU4? +^k~-$;JN0HxbZ^w3e_N%O>Eww4GJpe?dF@4a#pDV8CCqpYghIq7L7DcJ33lMTYM(MLdi}{{kpuhvOv! +^cvDmB|>a6d_%V`iVVo4(BO7Or_fsZ$D#ik>#^K0IZc8z6p9uUOtBa`-Nc-#&HqCJ0qyAqo-M$!zEO> +Zf1S26B71;l8&&vmj$D-(V$!B(vVk^$%OPF~YW4yOTYae)1kuS&AXYMAX-3T@69_YWN6+HG}euuj8hl +~0ThNrJVbxf80n3`4w5k(9h+4TR!{-+aPR;?@IoTNaa~(sY*^d?J^;(esJ;TNL`NqJ$f+x`}cG$9J%< +ED*?=`7^%H5)I-!DnDEFBP +lX6OR4+kz6ao~p0Wwft^E1N9YML+hgY5hI7pY{Iub{gfLgF09z}VqI2>h%6f=aYcI3PlbG3CfhtZsxR +xh3j4A)yWUVFaIKtQJDo18>*8fOkDe6}64{R|=vTHLE5D3ik~bE&e7Zs9W%LED_v*_c&92Y`4$7ASt| +q0B^HVJ+cy7n|&QJrze)DFZCBqU>)A2E$7C(JF_7@rmg-RB^uw}<0oQweZh4>&BtHeVSa6f*k2gTpu@ +8it;xN^NVfrnXJ+1V{$8WB0*6%SGL6;zOgsCTfD{F(uRlfMo5fZK=s?T-`p_3aHoioEpK|DKS@rC0O* +f${PY+<;{NCkOgh-NRm+Pg9EC&NeNm+=)6plT-{9FC}8fXZk}}w5k2nVSyY_ka0gFa!KF$Ev=W7``_L+>ftVcDlUX +LN^|#ZiC60Uy2|W86JxFg(VtbdYGDahmNL0@DeYI=+jiX`#<@+$TCAOBg-q4F?+v1?=&L1GdHw ++Az8`R)T1l&th``GQj45$=1|E+V`TN&jD4}-8yrUjW9Z4o~n`1;0;%(66EY#`_%Z7Li7acqYq&QLv}a +2CpMjV`UxIy`wZO{8D3JT@hD%HF6S=&<@SvIHY7ZqYBQgoE`F?6L +#+*WuMw-}}OW?Yqlr*TA+gyDx&GSj8g^ghXKGV|WpK;NN#tzLdT8*f)I7gy?$AgLHjyZM_NzxQ{^#P} +U4|k_2%fBM~KlWnAdMMCNHSKe*3=3$Jf)!>i$Cx!*36iELAQUooYL6Fw-!swBM{0fQb9(P1 +_ZsYW_K=kbqLleT&B@Qj3ey?&1XT!E(4+8UOfk=M=!<>!UYljtpBMQIOa%P~_DHLqZg>^l1r0Qak`fQ +lw~sI|1_+6s(afpr_rO}ZqJrnoS@^b5W$xZwjSbK+d-q@!9(aubVMCa`5ZfI9UFnb~G1@ly4k)yug|dKFz+&qr}vFP=L- +ctJFP4!Law|@))b>#d03MY6p-4EP=7tXXmY7ycvSt+TDFA_MtfjtR6X}{`!gSA#t?>boPXNxb<+_3s^ +nVy9W}+U^U#`!>3KDVzdu>Tzy)>$Sxjnm|P#`>BiB!Aqs^j1P5N^-;6?}tV=Wy3OR)lvV50=OT)?@`i +J;1dIO%5w=cD%lH4;+%{ygZW{|BWg+2hejZ8kZ=OP$Y9)hml*IQ%#IlwaVYRu4!!YG}`vtkj?B=}^IY +}y)`SJWdUCveZwFb_YO?DiRqd8dq3N{%S_1uC_h{)gRpLJ+h)m?sjd2=V@G9?fSxzoQ +FutdgR)RNF!kBrFWYMw0O+hqx-7!8C%7*(VNXyU*365B%J8faVC3wa&T=u0$CoUs3?VA=r*FHmWGO@zp=&^t1=4{eF$-_CChtGY4zCHVDu_-16zy8NWg}k|AdXVDRswZbl3L9P}69h}Y%mnosbbyLgsZAP~vC_4nzv-OA~=SuEIX^H2Nd5 +pXUt6VQm|%$5tft#?r0t9G#8=_8t3-C*|Fr!%J9Q +CRv7?Y5o*u1iUTD?4QXHOEOC?qi>0e^kTdF)eVtC)~x +iKtj@wA+6_c|jf0#~KIg(#p%Za9zfYzH2#KbS|47Tx3NLScW9xbp>kJ&=V!aMd$iQLqFp*n&x2n)-BM^RWPk1*^>D +BPBger@8r=>54}R$ow(gjuNi)z0iBkvNV|va|Cwl&`((=t(K=p(F2ZoAV~xG$dPo&kE9>WHvPvgXWNP +Hm)5P{T)a>=?6KrfFS_GEUyrL!`@9Imd)8qPFpJ||c3K|^sayN8Yx`&6Da-pgAm$MyX(@@YsBH9?0kT +<*Q?R|GBf>gctB1_~u*I&nzj<)T_;z!LDTV}WNuCSaXN@D(3y9Lw+}@|gu*0Yj_gJkVUsZoD;~XdOIQ +Sk~AOLNRN`nq)P=#Sf#7tsGm59g+blKJ9KM-k(MB9ViL}<{A2I}<7ViHev=8*>L``3IpY +0#7(w7^)d$jj{?W{QFc9hvuQAwu&$%Xvo*fH?0bVE5%32!&?$R&(p3^VvD&7cS64f6r1&vZ=?)2G5`4 +OzBMxm=^tTxy-9aEN{qPJ!F9s`}qe3HOA(@%ijad2Zt>y3=pScyp~_uN5~O|f0aFKA9XR*6FcXJc~o3S4@CEHf=j(yN(fwAXFF)yb0$!6au5V +Gv;MyhGq2aRIcizN)XxsP<~4!X>jsz{)NYwt1~8%Ah}w%lccI3>~9iwg&dvHJ(ij2>G-uqX=#WE+oh_ +x)3`snBiMKWy;^tjNUeBdCE~dA|t#f_y6f_KDCt{vcdSJk~Erfz29##=)NM>A`@7*Q&z6Fb^dkgenXS +=tE^#WVU?f0Sge`VeLU{kLT%5G#`cq>r-OD2KuN=P4vdpS&`~*!|Q15-AVE{11uGssR)Z=4R$N?KFuk +%Z!z0<$Ke_Uc>U8B9BSQIyl~_set!n$RAL>4)=Mv(7@FhCo+s1h5di3@q +eT`pIFCsvA5jL#XKx6z$r~vj+XCLLVs45QV4x!o)NJ`+HKfWz~Dq?H|LLsBjRO^}x*MfQa{A~$~_*w% +Y(IyL#jEL&S2E2`8+RPh9~>a@5cwDv^e +pI}t}B868D=x-sq50@isw`g-fc<1_R;D&+AR2O(^CG!0P(xf|0{M9Hu_D5tICr`u~0y0gvpX-;}9Pd0 +p7;p>bs!Bhj;~N?}P*TwQMMyH<=_^m8!B54xq_r8wak+;!Bb*R5b?;Y;6a;Cbtry8;B}3B{vONpRx^R +J{_X9>n-Lc-dxr|wWI!)UnfgYeYX6AHb;bdmwqmd-9*|oRTmQb5ObY*Qm(|6E4Y4S-1);#(P&_ +3?r`Hwy9&2S9;6720*^S$QFp4o+E19MeoCpm?YMfY;$}J(z!LiSD_t^q@&l;&?t`z;UWiHFEFyaw>s +;aT@Q3+TPM}H~Fa2$|r`1RqhdT@1*p@t=#-bRV{lQ$fuxi$%o}uN +QG1;2813ukz`7FQ$_o^G%LoP&tYW`q2V1t^d7Dv=j}Hc=v)vHK(Zz?_CaHYmd>v9(LeC;ojRFD||^xw +__o?FD1u3|MIdVz=^JgrU$6cB8a}pa*0u~%%@`tHYUBHc)-fnM+pWkOc;>OLgt*?HA3N8*E>!VwN5$J +3Cxi^&ixe5(*_8M(gl~Ru@l0sFAmK+|4^C~_SRrT9{+!yV$V%MzZ3h%x$|HMJ499`GK|as6-I__+bou +~pJY#Iu4}6*u*S_}z9mTLuASAo$ +ErUNrs}+(7CKTfN*Ks1veP0#qUSv4w{aQIiu|s)FBS#}iE!)C-bN*3{pXkI^sF9p{go-loYU(HrwrD0G$4uhw0AS1^c*ul8LfLS6 +IzTlPpc{S438@><=1jYd+nVRSwBm5V&bFA0lH0ilt;A5!hD6Z~r`EfYzBWq={XlBAPX#QMTNdm|Ug3* +eJPvRH_1>^SruWsPIiSOM26Trb=Mhw&$8D!;sCaV*^BjZ5{&`j5TetwHZbRA;*wmU3BZJ$5Z<*8)HXC7l`JU8A0E7^Dk +HDHIMHs^RbHh87&vjcxn+}~HxB1R9e0z#vyZrx^<{VY%5kD>WM0r}zq%Zr2ze%`p9xTSU +Z71D#L`vvaf_Ubf3Tn5JL}RYJpbx4HL +DqbK(sOay!rrEp1digU&Lc6%7^HNPM=x7d+3Ia&k~FeuK29Fploip$t+&z8czUA02TarLL41RwC<_{#-PZek>ux5KtvR3yNxsTM0OVA&g%hWGhB6se+o}DIE +q{Yh`^rIpF;{;qUMaQ2>yO1Nqph}GEU11wH>cuq{# +ZOG!~DT{;O*0$ti`s+wPJ=*iqR;hn4v<}7>v0w$U6!gDN{Mk@$2RsyK)|*%nq)=g1f9aiWZ!amrn +f(G310Vf3n7%07O#oIEWnmEfg;FzH^hd7sfdA2f0L?Cxm~;{YV6qbV9H*94xlev8CV7^reWWmS_z@~k +h)|Ng)5ep8Y;%Q&*G&uTgC^T&ObOLXkXp&PKeH{Lw{+g{%*upVisKlgxf(frz>saNBj9T~?8-82~E0N +9hoVwB9`6r_QWXk}VAS(JdkEMH`bk_E0&uDsZtVcOs6-7nRHz3`NjH-EanhB2%>`~VFAt +a_<0G&S{C0sAToWgp<0t@X~|fWOrz)60TD@dayTerd_4AXbh8aVb@p*I;1-jbId46)5jzl +>o5U#O~=rc{xxn+&e}MT2qFt5G=q)Kb|u{zFU*kRJmKG|CJ`%39kSzZJT}9VP3%|u4cXlyL208Lxu2mL6j$Mi{;%+O#h3Q%gU745($9}G^K*JJ;#KsO5l@gD$%#4@&`&pk)`ey +x6sxfc!)3s{K`?NJ^qXT-e>Xqj%kz3I32V=7cl2M0tM_c;d@M#ij`Ny!|y=HW)p2kfJJ)@>Z{C%Y^Gy +PqzQ8jt}q<#_zzzDg7cAlGW{YRwl>Kmt@hMDarcM<%(-bdvQS_AQWx`#4R~=d9F!-For@K?axr(ANn% +m>@o>PP7o6&4;+m`n+t=N`X%rS%0|SVBBzq3TDEAA$3FnUefu`w3@~otXZOdi+Qkixhb*vK`a +m;K;uEKR{3(`tGcG7AO$`Zem>cIp +K$)gHEIDb&%4n(H`+Lx|-acHT1*a_VbWdY0IAvb4UDX +(EOfEf-p)zu_QQn~mnvA#0 +B7R5Z|a{Oxe}W!3Uu$a-)39!?odt;py5`#LJ*Ol);olT4Q0CQ?(FqtVUxNv$nVkG$_@WsysD +c=|RG;6%%}>2Il?l9f)yQ@|zWnFwDHVR%y_T!eA8s0hK(aQ;yocj;^Fsjh%-M`hyL|CVH3TNfoPG7y~#140%5KTpqD}a$QNj`qr`6YfC9m>#xe-gg}?vFF>JB&PTt)xj5M9T0*+kY+-`;ZI$19mxn}L^A +uITIWuzxt{WxKV7xGo6wKs%cZ0&4sxRH6vuB5Qb_0c<)Y{(clrIV$XH@VoBh`b8V@zNkTof_@(f +}3?iD#wt3}}7kR$cin9uYK*wkj=f?W|Fzvbb`uO5?Es0?|gLA!SJ#4!j|TkFDTnZv3P4Av!|%6eAOir +l3SPE^A*$-C{A +nuFVS~9rDWUv+zi=) +Z5QLB#(5QxMmS?AZVk@nCBt&?^>3NfE)3bwb8Kp3>1iFo_3D=pqX(rpc*0htVO`BdbwI)-Y%DvQ|7X3 +#(LYX|V~E0F<7h4`M7=4Qr#^-n1UYOWsc_u-XQGZJhRsIQv@$guBB0?D_J=?(=oq~5%z-R>`ve@h+(U +Z7e(74P<8H~diwgIajgmpGqjAQVcb{L`d*PD*)e23*9mWrATN6u5RS*Y@j}d=mhZO#@`N_bbfOg2Rzq +TL!S|(Z=`w2cPn?n=Tz^*pz3;-_C$*Yd8PDUn_kv#(-;Sw|{gAPgCUz)8K=o8dh;LYd$i;->pyRP1Dz +lYJvG7v>G)$74TT+ji^Jm%p-Z*m$d=Hta@4*4Q>09Pq!I +_b(mRu2Uq>Zhlt>%XVYg2yf&f5wksLdxG`z?#o;J#x8;^zYu@F+0$_c~)!fF&UbyxX9sIhq8-+-UhwE +tIKOq1B61#ZfYVgef_8z-b9-DZsSPN4lki|m?+IoJ)i^|3U~P}Q(7VK#n-fgjZgz2QMU0aRdzu6@i#H +A%SJQ_|I0YMz0nideMb58O`eYwrpe6*X-1Id0a&Uo5AwxbC#FabqE9gCHc(ud9|`-!{?Q1~9*%2=&>V +mS=TY5$kdRGj=Lb;AH?c-RuYKe+3z_xm`Iq)a0D;T4w(mLMx#meZPbzhZsDW@-irQSn0D9(T-K8OGHI +^UOUuKyOA5dT|oKK3>x##Ykp@yc%onoZu;GhaLDAVBatoxS=Lg6OoTX^qhHP0IULDLf?y!t*x?XYc|< +u>l;nwMh!k$M+IY2B{da#hVaFh8Ti$QSp1wGJyBaoD-9iDeMNqc}W3w8yIa1hBYs_#0n~MQVXSL`wIv +qmJx?24kt>S5Y!gCxxkmrBIHP>_xe5%DYkMvBmHw>h=w>w +h;A{GKS}@?IZpiGSi_tJDmLp)p;kx8raw%L`-B{q6WfXo!=v$iXO-+jAZMpA6A?oV{R39%>B>6c+_%FsZ!m%8V}vQy$>kI4 +ittQK;;}7|WmmV!LC#%`7Z0EeoU4C147^-eHY*evS^S0JZN52QgY@^(&kNz%u+2c@79f@-DB5B<%F&$ +XwXq$Fo4+$iO5Qx8}IjL|*k8k-W@c#m2cT*lmC=-<`5L-pG&nHId|)UUi~-qFXs~^`#Fh4pf{6wB0vf +4(VA~{LtP5=rfw&cmIOViW%z*ENN`o+bm;xZsZFo^#S82`6WUf%%Wo#uOmM~ZvBY?LLu{uptVn +Lm4kj-;QNCz1t2VvZR<=O58^U2w~hvEIXgBQ>|lIwF(SOMWI6VSY_W~Ckd60BV`B5xezn!GX`2y04Wv +gX&UHYiJ{y>)Tg`mvrOiqYV(Je@-b)}f(k*>vF+M!Rn&!@c6Rp~yX~1kz*kIw8$_oVw>`TB&tWGzja< +MG_{u^&<9oU$`*hK#FPK({k6cqzx#JapDbv%o0G?fAL)Ad+=`)K4l!yUj{V2^B1x2N-9lunZv6tiCd5 +E2Q1pL{SGq*p<*7OgYxs8{va&BY)mSL}oQ%?&f1?H;TA8=--czH(p(#A8tlFkCAR);8dV&+XsUy(648 +Ac}QM>Mutc<2YX@SXNKzO*9O_|NDRcuV(zQu^GY7` +zG+&~5%1E?-`O>h7wA{7uaJpD2*kFZ~F#$W1riB64&>%S&boMsuXQ%b!%vcbm$y<1B^T|d>YGho7I^pRu*oX{Mc$}?bA5Xk`ol;Ap%DBJyhvBuvTIFV*r_Uv3(lTIwPM=vz +-{LG=Rod63z`Cz%oWvU=wwg#Y-fbQ>QNptt`O^JtnOrlV8_~?82#YM_Gs*Ts(s{_!tu{<^05;y91n07 +KYjA{CAFyMCmpxbTpA>7`NFWr#rY(LGbBOwDow8HBK0?Xx4tt*ZBpfZqMKM=@vc{@g9pmqB)!FglJ=3 +$riwwyuhx{c?eqN;n7^?*S8EX>C!z``Dz+ZE{bf5x1tgVjVk-2c(L3*%<(9lKV +#yt1fMjLZFxF09N@DAb{;&I8sI_$*I+p-3crvV +Ujdw+_B5`)Nzn>a{rqqnzToi5n4AZVN^A^Sxe)U4eY#Kl#I5Y +BQoP+zX+woTH;`-kj9e4Y^VW~jIc4sU{pcK|Sf3HyWLcaSD%#Y5jWsfxNS#hA7Yz(aBD6 +z5e`|DdWkjg*JnUP6@Ii&{L88v?KlYJmFu%+p8$+kbM0Z{3beg;Z#DW9eWWX}l7q`jtvy8zIPM?KPeJ;}c0=cf|@yw2wc_{E{@|Krc +_CiCIZp#9u{j+3ecHpDA2!z&@6yVzXI*d55Q_e?*S(n(PYB}mpv`AeO7sb5FUoD*(0zzj7mgXXzD>?zxvJIB)HL$&26+YRy;D~+ZMQXHY)kni+u0O45iT8853!0ilt8MNpJgoM*^eejhL0 +OY$1C7}2+=mK=~%88#mrw$BCp9_kPU9#Z3iLO(!%ydc0i4Y7^2xZ%IbecN>fh-A|E=f`PT8JP3gObx +INup5f>?h4(n801RwUK>5mkb|pvZ%jL6gX_oPub6HXbAHOxrm0hcPsXO&`mzqLg06r3fOIAl08{V +NP?4;zM0)Syvam!5HY}e?Kx)SpDIgS6)^kBvc%b`K?%Af*EES1E9FE&ASA+Q+mEib-5(}#nZ&7ic>pJ +I>YDlP-IF}|sdpa*2(`$E%H~OGr-UIJaa{#_WdNJjAuk+q_1BLDmNZekIe^wH?`TchMc>AL&a`gp?DT +*+KNFU?kVu{fZ5xK6xr$Rm6RuzV?!vV_}43gv-Oo7Q$boIpnc~br&+an*7zu)XYS|sMq%y9!*% +h=OT*8(~|B9n{h#HTm(%hzSbfNWG})mJW)v?`vKPpO~e4hSE2+LDcFufeWwx@upo7|`vg8?<(M`T*ZZ +xiJ(Fxp8BVRh4BEElrO((Q}eaSFk$dax{R*X_T0ZiWAqua#s9@lW??NX)e77=S4QPKp?{G%s +6{6EEmW#x8+J$b!oTY?~9_y7VGIp?&(`my9r-<<5BVys~QOaghfx{3!G7~7_YvjP?B~-mh9!qNN~W~< +EpXUmgOFBu41_eShK>EP~Z$4X$^;E98L`ofJ8XO$X8iC;sdNxEO-CMo0;Y~|G^3`6GKcS0T2D43xY-< +D6W<`or0ZaxLOcx2=Lo{l@9$;-_ti#DNDt&Oo}+jr;dDC)|xHjX|i6!&3(cy^G%J?JWZB!l~tnwFBPe +U_zmL`WP!35nwmE%;yBktJub3IK4lDCTQmYsqI-9n>#}R(Ka29EFv&*}tf{=)MMCsk7YpcEWjRV_^CV +dWW%`m(MtAvC$j96KGODMV(_u0C|KtUY0p_~KHLtrryojr3IQl0XxZ&uLawq-ql`SQJfD{%GpCjK%Gz +q6)nwyEPV_>N62Lfs^}E^zSc-BwFeb>Od8x4uhW6X&)@gt-V=F@AqItS0`9a@Eos!s;+UNc3Wu1Na++j=mwUqPVKSKwn5 +840@DXDtu)$NiLO*;j!@4BO^t70Xs15>)Pq@0>=9VE%ZW0f_PqUNcdgRVzTGtHhl%nsew>Pc$I5kcZ +{qu%orDsZ-87%@S3^!aJkHpCu%LrPxa9t6@V(RKXNqD2rx#MG4qsE(@o>@zF3w%%!0}QA<^2Ei<7lV_ +2xPpn<4q_<`W=0LNs3ohl}z!xjCcyb9V_b=+s=&nuQt&i5T7D+OhP_=BqljO4t>3ES9vWW3hyV8pTq7 +E)s#%8#AT*TtvvG@pjInRTf)vJOu78n*7=gJvi$&_3|Idv)X09r~jh|C-CIi@?>weEmR-gngY9pb4Fq +nm_)eStI^w{%vDd0RRyNYLdCr=3|QNyHcWY|kq5$)e +DZ)nht}fn0Xu9g$~@m4Mp&UyOTArvDF7j$V`f&OM5^G_fiVEl};1cr7ze^Xcc90P#6~j&nXGTC5`VNY +)M0@2S8xRor6rsBg-L*wLRDD+8Z$)(9tR)!6q6eHxiGBYyk-Ng|QGTkhEP(sZ;s%3j7VZh<97EFLz5k +j4D!(9*8D-recDfKwrC1A3=XKz|3^NVM +Tcpha&LjbU;PEqFC2ZXk(fL8MB8nEG8hs2$8Orf_X7OMfF@{CzU8qiW4nP$s +o-^Ac!BA?o@y%-g>a``e)7Hpc`3`=+}QOb`$G((R79-Oq+NV=gWs!1EJ7lu`Jgp%%rsE?dM+bg0niU^ +brO_FAUB@4%TOGz-U+Q3QUQ(FXD1Kq2NS213Vtrcz$ymdYZ?<0{cE8tEzE`N4Z~P&gesCS-;Lpi|zi# +AkjJ^^041vCC}dbGSIZ`9rjKwHg@L;81`|d#!dt0{xeZy%KDe$T^6N!(D?OOHpV{h%2mQ}6ne0G#kjx +zHvFGPp^(t+w@)&1XOupb#V5k0U=A(N9la>~mg3~7609X;?=$Sf%@kT{(EIF=DnmPr|3bxi4A(3TbFZ +MKX>j3f{FF>*i8K!lFvg-R*kZ0!%$RT@{OjcB-740J@5uh8kZc*Sxe;9sIIzPKupF#k*dO|wLU`9*Y( +HcPdb`8wL4Yy}dN1)R=D-ZnfTgtWW$yaCB+g;9YO>ZL^^Q!7QYW=)@aA^W%qG9al0I*~k;q>K)(E1tP +$$?=ML<&;&}7Wmn>73BY+6K^C-gNgCj}XH`R_lP>cP@Cijxv=tLN$8YJz{bzWFMKqZ{d6LZ0zG(_pZ& +SL4u+)Mm6Jd*~35pft=^{=m1%#t1%KB!eXVgH7o%Ew-XAf@N17##{mF?%%0$G>onOAP={kG5j9OMpmk +IIEQ7i_|05xNo11&eNM!5zMREHadAn2tZz(4$zvAZ`*l!cbG9d%X^-}^qf*&C{mCEgvk3MwF*iKu+Dwp^XgIfZ +F7$vAbK2)1P=#^jsE04TMB%_cqXlnG>=DdsOz*SNF{TyycWPBBWS>q~19%5}0^~hex91iPwiw%r7cJxa#ho|yd$Z+JZpeLQPBu%SqyktFxli}UWgKC>JYwg4g_W4bUlw=^p@C4y)%%cQ{6MhVrV5%z7Oa+{-5#ax!`zMK=VJBmr2#^sgbk3r%I95Cm +V)qgl4M|nF9UkNXrHQivUz(AIK9*uo!FVavQ>cBQakyWS;2OD&oH{6vtiJ7`+Gr*mc#BBiVf&0zIOnt +g@Q>H=Qv5%JpV!WENJbWy$!ADg;@GmvA|3i3S_e$tBtyIUV1E_`ntH!l2;BCl@itwqovnh(&gN0DFl3 +88S1*Ae|CE00W%0#LGN9|!?P3j{)QpvY~Ark2Fz!G)sN{n7GXu%+4jxMFezNfhZ0?;)6oF?zQd>iMNSoBE6U@MeJgKsA5S_wZbC +(CrPx$Jrqi;2<1U_VRZ(#tS`S_7ewPB??+>|~tat+4(~z6_>X3iWX7&WYvAXzHmSJsZcf0rcbcSk=hA +e9s_5QwhC3-VeVTAa9Fbb-V7-S-PReoZ_-wI)RpDKmll-EPYAJHS4pVWHVHn*I#8{eWq9Ts`@LXnzf^ +(>PJa6j*+59(p@>pqquSa!(((iN(a2_<}?h>`UhD>VaR6f*C?_s5Cp^_qFYn$@8R(4kbqBsc;PsZ7_g +e~NQV@GuEuOn34}qaG_Vv{D@By7eyX#m1K4nI%1Cn#QHvl-{S3cMn=z=d0*%)&&?N_EX+TR_b9DQ~5; +UYt5>YWLmd2O|h^7_iO>VQZZz4 +GKMYEn3)X9%t#pf&>1$I?PVo!%H^kGJPWb>N1e8SSTPg0?j1b_u1vqL9?vS2(*1z{TMC8FZs<={&ToK +h6|y^`f4~+Qa0}HN4SoM+q;7aLcrPMnFkq}92YuQB`#}4e)F9UPo#!lw^JG?7eiI4$UtPnR^Vw9gAQ{ +s8S|I$>H(k3BV>yC~n3I5wakNg{y38(dt?9NT;}V~4yAn}7SXWs(g00U~LUDk)9h8K1n8BCE%zvHOr- +8Y;QEl756e_IxaN)*fx`26=p)F&9%=dPUt>f#jA7dJUt6f#_^oL8xWE*5~ICy%Y<*|KGeW>=2CokyYf9BCi`34ppOO)W&7Xa`&>NEV;GvF3# +Agfb;wCqL(Du8UZjA=D4a6qAw-W`n(7M+P%Q&U!RrFL_APXf?ktvV{UdqvXjg=PuS>qCD~AAW9j~ZRl +Ql?Ol35Za6UCp?lV89PrM(uE;y_-?DY@fC61zEMY{MyWeH$SNZRf<#D5@S@!1j4r0j!8PIx-U*Nl!Js +ek3ri+D==<)gjnmlL4f&;ta!m}E^hyFV9meaI&NvkCSVG-7YAc-Y1Ji+{KbvcmGPHDaPt0a0%|9VWnr ++Ekf8D#}P^v-Olf&BpkRbhpY;Aq(^C3-@}-K$kT;lO->+9~6+@Aq$SA`VE;*qDAUu#uPLO6D;ept71( +=As?xTAeX^?H|0;#d=AN{mXL6_LST%dRj?BUdfE)vVZl{jU^&(+?1~I5ZXf~SFOo-XAkj2N8l(K!kz*8B>Ac{8S&dSHvZcMNKHgB^xfNVntHAIN_)EK3&WS4I-4$uOSo>(uIr58njL#*wx;Y +>E9GE3v>$9Z8zCU&+1O;@fHX|=X6G_FIYCs}SyRuSJcn}MX8l>>7OKE6=ucp&m1oltB*8Ku<#6GB<}d3Dzwo5TItm&WcuDtglpS?l*(66cbU5=S49yZ8czRX$%iTymzX3NSEl>>0C>?!>&d49 +eCWef99W~%ap3aGkEDp~c-_25qA-!wgs1=jy2pW5_OBS{#FGB*_!@Mf-^J( +VLP>u_wtkKz|GJQA19t@;UCO*(@Da3J8hBu*woeFcd9PCuoo1U*QKo6nT?p`T$l^CK%w~+nu^q%3<{ND$EQJfZo)WaI!uNs=?z=Wq?q~ +EOXi1p0>aAhgf1nUp#6+EFjg~o~Q@qfBzr(6X=h}K}S)YEP5@WlIJoNf^7JL){|8@6m_^(ST(q=*8=N +Jz+>6c*iW*P{Ea9q_d@3Om`Lu|!HkJS>{&tcld`d2kF*5jgjOz&f)ceFqtA~w2NHERXB#z{W4)}aJjZ +egV)-EQs*%kHj#-G3W>7JT)&Oc{_({IhOr&&d+7IHfBbA(g6nD3j +9r$Z3>f^@*tJqVp{-z;IZ`gJHqy)kvrb~F{ks-YNa^3$DabQj)jRA}9*(btQ3ehvFcXv08bWvsnl*h( +>)8;AIT>scbqwJ5S@ZWmhjgw`Jz`2^-XQ!#;dYGCuMqasV9lO8YV!viMB89R6tfly<2?H{;&V*N`@oh +XwAJU00fzU|!FnS-pEnlUh)Ed?xPJON4R#{SV;K?1T*d)k~s=9wP!&PpBi#W~Yy|0~mU^47=pltfAKI1+^=QHx3_ +hS;elI@F3fve%ob)-VS#|j7?2);pAA?sI@9rm7+dU3>ORe?Z3LV!^$ +^vpMkvv>uQ^$#ozae|d0PIQNqj3_FeA5CdX1N5J6o$^x62N(h%N`2vXF_=4`ETon<;H@W5E!b9VEudx +p4I4yzN184mN=u#`QtZ8pMXuVlLsV?5!4C8N6?{XyCWF7=BB0H~ZMWt2J2jbY_t)w*$@E^Rz}uaXP4y +NM5FA_%rHl2cnrr!F|e5UGl!XVtA_}g*4bo +weFaERf^Dw>(F{SJ|}pXRJR-&*gKE)1uG?8(he-Kyt1WDCE#BqB*e@2C0=l=x&jWkhSuK^t)zFx%coI +cfc08?IIm!qRV=TQA3-%f;iwT}L8#^9!7h1Itlo3063kKv?wQR@sXmomA +g(NEBmBSK=A=FxSv`Ua*(6SjbX)nd2?fPj?Ulk`L=#A9kr;~AiDN!dX_$yp!Bw<4b=WjI~Jbk +aa5Wb>Bk7zI4%;mgST&H?{hA-e(IrOMi;oar_C5{;85|7LXi#Q-7EIJ^_XYhV72b_4In>>B=-R2Y1Bm +sCkq}_U6E|e}~fc+*VVb}C{HHV{JzVaq8?gj`%$ZyX~rHf^%xIQ-Y*C1T!lc$l6 +={CT%vkUpJq%811xOF-;ici#ksV$YJudQ5#YVwC26HT&$d7xe75^Sz=X9S6BGtKYQTd-5|L<6CaGFw# +h4*5W-JxvgX&uUBFl&8`Z+Aes9WC5tD +LDJOaN3+Z<2{EN){!I8l$Z{it$q|s23*6MTbvl=r?y-T+y=bfeci=iKF3dH)@txS*|pne;gcTBotIQ4 +q8q@j)l~bYTVm2J{@1ejQ5YZq3H&vR1DFp|pG8hyB=@mC)|Hf`6rXNlV{lzq{T7tP&uXPjCIzg?q)+` +y!m79mUHCA26G^OY{-}KV2_J7rJ|yq%GK$l@N@lABC$|N{O!aN5vd@AmZ>l7*6*LgA40jo5XkYSd{8`OV};7MGuuStDrk=bz!csY1I!2k{jCbH>rCWBw_wGS +tqsCdCMvE0cWs05R=_`H;i8d$pb+PIp%|wu4n(2dLL@i_v){ON44N!Ex$ubbk<4kEKs?CYYPr57B*=9ca +?DPwmtnKyR4)RtY=~+eT_5wMh5Wd9I%MfOHIImN`u#v(p3_u>mkJBu+6A4D=cI_^Fth~AnLKum#fjur +7GKjKpGLw +f1x0{v*nfhdz4V{@W=u54vj_HYnp6mPyu18X~Ugsa?92v!CRFf1)8Knp +k%rh1gO)#YG{u<^74EZ%YDLu6SLH1<2J2Pw1V$GiZzQb0)a?o*AzweL2lN6NFQ2U!XEIutCe0Y(#3M& +l+{OJJ(u~zYo$+5ij2YkGXoltiiD#DkU8(c)HCR7J+wUkv +UAc~|nAl%0!N;|xcC&aWlVqZe3k!H#mvo#=krg&xEAy(KZau52UhEb!><8Aaa=pM)Vs1^6ZT?c#S=(K +v!D(A19USQY;?7PEuly_u%6Jd +>UqFQvK|}%0^QWSBX>3KZKNujP$BFHC$(MC_^_|RaAQUQ{0sm5DGCCo35;NL)A|-{BE +O)5NxL;k+SscDJ#hvb~RO2enS9T1llEj#_gL_JM79LI&7Z6zEZ*g79C9r2!-CTZg;U52X%n&a>hV&dO +dKR+%L13fsdr;HHw+NO=kFr#FDs;~Qb0&#-m1e~G0~66bjkqJH6TMn!VX!NUYGei5Bg<2c +B_0|tct>Xb_v`ytFfItk5#JJeGv@;oYOmxbC~pCw1)}^g{Ub^$-LP6hOi$HE$~Cqu^2WMeY}IG!am;M +ikBUM0wNP2rImHU-#TDmqwyF|ID-h<3pXuZ&TavRX^h4RyB(KF?JWUJOofOH1L|dcEn6l~D_P&C +KreU~Nj+6n}sLt-&Fzu;IR4$4ETdTj>b1ihz>+kPF>o+)O0h$8nsIOL)0-rScXw+7}$^497^^~miZmx +ja+wm_+krp?alAl?!Slq^wr`P}?Q8^`EUoF|+Gcmf~J`0X?u7M~`5FezA`*gto`7>QcjJS=Mtt(4)H1 +}KPpCq{@F7^*&SP@vZ_z&F$4e9=KnpORWe +uSN>`mBIZNLN)eR~7w$l@~fhiDDIyBe^H~A*)od2a~3hq4{Np1;_Mx_0MBNd!d`b5ivG?jk&%4x*gd% +ln7i^N+CT0Ns0b(lLUxWJlH+WY?e&N$Tez!^s +Eu&VSIfZ=dVS?K%+agPi{QAqcwZ)hK~SS=1IU1e^deTfE<6>8d+&kbRG3NbJ2bJjP$+r%4`jux7CPeU +_-WYg@(=9o_wQOxYaH>fB}n1rij?Sr%&`6p~8C5156wUlA@T)~O%tNe)h#${eRNNja~t)Qt(KpT57E_ +3VQlyL(hNgwbvd9GL)Qn*K`h=>_9eJCXd;Dyu^%73e<&P&^qg#QdO4D9v8A4f&sdShRZ7f5k?49{7lHLlZhV +3r`tA^pncY(rhggVn;$K~T42V_Gx3mtEVXH)B1@;~Y_3C+6j-D33p#8O?O~;Z1Wmo{M;90IvVv6>CkT +W^B;F5YdYrK;h>MFjO-c!1919^KkJ-~YF~CA9$Jtdumk~C8$&v26l12C&pu%K{_t@Rd&Z*UBCe0PBD? +>tSfovDRMnb87a8J^Pm)@W@mZRjwz117?lJ7gNgrGGgRnz&VibRfrN;jr&2`e>?^92L4*?=)F@1X_<2 +i2kLVjYEWPp*rw#yo?gG<#b8j8`&M#{eA@r)LL9^csixb+Nbts})l&mK;zf2r5`-R@$z8I&82i2iq;7 +u*r^%n%zFm?vly@7gnHy3UD7KT-~yxHcJaSWXlM-&+2yU-;9jflVDBjKzkZrqr(W*MWBf(-0eR;(OnH +r58x>by|cjqSC&YU@BbQGSjY)0hSMZQUN1c2klR81)SY}XTTe@WGS(*#4<3WHvnZaGLq~%xHcrUn25+ +&Bv@Tw4z}rG-gBBV@r=*a2xhj-K)Ib>hxV}p%QAd6bzoU?g +A}tgU8kH{aXmd!8>d6Kp{CS^FY&aT%Z7W5|*}#PN>0wo48-EbK-~rgoSOu;!GA1ojO>i#5rx>^tm#(c +1MED%TRIq9mfos%pc`%c>tZrqG8lW0;Ep5QTkGT8^mXk8#G}z8esDxeoK_OW-l$N(=y-^_)`Nk%Sg&S +Rb%=Bf06fVMR?|N|aJsNv=3J8rvFgzcQy({=335$BS}7>g!q-~uyoTYP?52=*I8m!6fHa19Goxa6R{cU&(KrE1zy-W}qs7yQ;R+ +;0(Gf%D~HDAd>8AIZ1K@K1sA7@t@cMQye`OUw3um-$XwmWf^F&iRA{wsIxyx@?;WoP@R+~nQ2VF=ymc +F-lvr2hXpKHaru|Z7yIa5a&Q<|(!k^AhH_aIxyoR3U}CPSpmoTuCjT@cloHs0N`0gq@S~f2gXs3__}s +k?BH1+HPN$vcmjPI6#60h(8K|iBL`n5@Em2bCkn29ENzqTxPzt;sH={xWfoSh@BlJg^Sa#aXSZjoQ?< +-i@Hm+2GcU(dsOfz4C6=rTRKy}3cZxrmFec^8^vT@;WVP&UELUv?#@Uj3sv-qzi4D&(*p^$6r3j7wcJ +pCI^xGDWQwi>*NOr4O+ywLl-v$SmT!j_in3_|>6Ze?6uooE6aD +kJcvKqLl`!a=VFAo=ld{k2+38gY}|l>OGmf-S~$X&~gWg;%9uXfY2y=o28>ySPS5Gd7q{I^eN#WA``&w`vC +^JFIK}F0#1fEuBF}fy{jOMf?|H1yna_eXyh8ZZGJHXt)@IM0E9+S38tC{n8mIhGUL+%v^n6m_?Y`rk)$6Q$`t5QapC@rC%Osl_hYtll)#Z{md_7OH1&8Yyr}8FfVlqx8` +Xp;4yrVxve`Uq`;=i* +9#CZ*gaL0N9-VY2V1(G={3cI0aA5&N&v=-YK#LNXWQ%b$B1M445uAT#Y6`#~^$V2r>&z7t#qoWV#K|h +rKxm}LS*9aWn1n{tM+vI+==?46w_>whx-FR7N(gaK>_=VqGHd4*;0j1?yVHQwGH-{e4`CDEprOtS +399q5)v1B67&RLs{|%Qm0DeOX0_?dOX{nx`DdMJt;Q4lN_;V}Tx8Z+DlKA_aopUIm$DTY6Ld6|#KnghC71y2aRe_rDpCqMf5^+Jb1Ci +T>h=#>Xl~-m$-;!;0%2f1BXT3Q7GV2c*l6hTBh(vT1m_cT+mib~WuES1i`Y>P_@GGn~i&&eDH#V1B62 +ibssn?7xba_-Wxq1PHwd16F#YKSB~9U0<~?G>T5{POeT&bAO*hh-^Bj|7!39($TQ{*6TR$D_>k*xB_5 +Eqx9AnXD3aq#*B*hz7cXj5movFNjvh<3d;s)AQZwfqQyK;bA9@gqSW^rPSb}pDV5V&gY~OnAM5ZmYWt +q~eJqDVccPtVQjp*aW)$aePDmFq2Wy+MkM(Y7v9R0m5F_0G`~Up!|Gh{efWlz`_G2oePb+Ci`dIOXel +xFxe;w-x3kAr@O*W@X=>6LcUlz&n_6@q0&BIgTVMRTT9x3?A4(I{+Xa-Hn)LdbQ0+VJIu$8{rNk<>bE +5s8!-K4ZVsikA&{ZQAezonlXqRU&!G@LURw!WfVi^$hcH(G-#WW*~6S4beh*LBdEE_WqvO;flI5XS;k-;@S6o~_6!>& +8kDn4%o+YDQIEsZ734|*TZ_f7+~EJk1I_og|1(o+W +vfX>3(vIJC4y0K`z!7P*#I8U79XANqt*UAqcX=IJIao;gjT`uhJ^dz0nHk!5XgU2hR~7j-MmQu-y%)a +Ex#lvo-|q?E%Bq974RB%lKjnPP4KZ9hYAx;MS(H@LfNz3X@BS7`Sb-62vOvg~A?st*|+0D%}heE8h1l +4EFqKH&)iWKPKD0papX((h-Z0h;3^5Qsj0A0bKRO{5OTjCuCznhjQU61lCwL`%md1L7N!5oV?IazEzq +fge7@>sA_cqs5{NM%AlhS|zD>?rOI7H?>)c*oO;B=cEzxl(46ifzRe7+~~1F?%OaY1z0eR7JJ;J!9nv +ud&?*Hm=!`QY<~K83aYUJ2Y^IMQ`Sqv<;A|8#|rCwtI#O&Jd=#e8i?6SqUApfea|P?S-{DD;^IZ?1s4 +Q}3;5*forc^1@Ki9Mo6tP9&1HCS6j)-R2Tiad`=O8a%YhxLNAl(_o@pco--onL0}|V1EyO<4Ed@S1~wkIN2<0cy(G-uo+$)q~fMQ1FifA9|DI@{tn +-Q-rV=OyzV9=$fKbVKXn +A{z;?D;x#k9j%e^M=v2y3VxJ4ix2~PY3fL-t%Ju;Qv?gj$6=`0APi!>Uc^CtX0(o23AZolcLsD5$~Y1QLla0ZfeouSkI=$H>n3`NK-TFZ4|h58EFS{`!ppgfG#qa!K)?i&D#HMiS3TW#k3+PvJiI*h9Fi|5@lK)UX*;^4cSKS4C??6jg{ +NwQkhQ!!D|7Y +RFGpJIE8oYnF@T5x&D}(A^a5%hzg)q)E5DGoQvbdbDD0AYzc*x?y_sa){ZX(8t3@Xkfe$*6S8r0VSMq +VO&pni~#Yo7#I0j*6a_-~W!EXkfPlk~LuO;RvFRv3vQ@G9)*sgSDrx`UtU&01g3(ET$llpEgyWA{{z8 +^nG&nSQm-010^0rXyd^P_bCxWN?-Tb5+feyl@MEaucdftl?$!3g;m|&F9Hus({c)D*>v#wXvKp@!W6r +Ck6QPbr^rUVFPo(*)>#qXK@BiWm?{ia68Zd)JMwQV?M5wBauJJYz}H2nt3gdMqk(18c#x4Hh!d%Q5ntxlQK`$R=H;WZlGBT;g8uRSO3cQD-h|+|)?gWFI{i?YiaJ +x!q?+BuOP(&1Err19WNo3GP{RLU`~RchalcJ~Ibgp^Yqi%sHUHC}2m(8`Iny2_hG!V_xnh}eA(@w4W#s{YmXi9BJ(fk#ileYr`1R +$ajg9Hs^*(PukMV`fRGnz!Mo9V}K2nd3S6BVO|BULfYE +$1#YE)(8#@}kQH~m&0i`8KGT-ebv6F;e4Hi=n8WD;fv_l%#cl$uG~~7Rvq*G78w+>(1l?mssc{bvlg0 +C!yj*w&2t=|Z>3bbit3k|*_-CGCLev8Dy~ujhZLqwXdwiTqF&H>tv=8(0K&Y0VnqeBkvKpcR>px?am9 +VU~$g>TwKoG5Yg5@dBF+pLP#1e>enV~R6s^yn8%6hlOhS?yU%@efpmK@mUZ=XbsT~@MghclKXiQ%lUK +sM_#Hi%z7Gjw~*j{DI90ZqM~LYcdLMnL6X+*va>0%r6+)B@B~ar;EPE_V01eX6-%xZMEypw5?VW`f7G ++~CstYSs%0-0HyxmCrceHo1ebs!_1@c?nu~m|pwxl9o9K{At3IW_PFY7P63h7%^A}F`1{6Tmzv{hTkY +JFsoWwM?Tf=%+u;%t9VE<4wQ2O8qz3ScOtnD$ccpRneaE%&E!^;0aM4f`Njiw%-uqw$K9kqQWMM_BQhKM$cIKa--!P)T2d*2IXcR63?lcb0_sOhV{Hp_^)e0 +V3P{WlPIy6|L9fJV2*t1(S|Q1VSU-k``v&n +!Gf?>Ph<%gGo{P`cALlP+aH|ET>n(An$*Sbn8L5J--hK1SiMcT^Mzw2s>0&%UJUF8z2<2gO-Qj!$CNV +bzKEo=NOLEYwLTTlGhP%x6{V$r3ac=W9t_#Sj8zt&$B7(P3J8g +uD%1T1L4k-N=leu|H(x< +1SxQ{251p-kuw9H(8wO!JP&*HqVbE_g_254JJIxtzpP<9`_lx@x2XsjEJM2)f%PIh(^w +7WFch>K*`j4aQt8bt^SMltokU%K(UaRW&7_;U!qpyE*-1#T1wf%sZ1V2n_>({3BY>~X_HCF*zG|+y4h +%siZw*yiMdYdiF&q}#~hEH9F`W&u+u^6ipU4OvFkPMi7NEaAIY(XuG7(}`rGY?kTWh{O73c!i9=Das~ +9p>|SM<>0(_L?a)XCv_YsmzGNy$6A?$jm>lwoC*UST0kYQ?Y6#A`%57P}fN@QMD9*=m@hLTtWFGu&c7LzMYgKGKLVy5up^WmMuwK=79CdYd>7W2BK+u!9mp;XolnFtd%|hL`%cxCH^Z +eN;)*6s!^iLW)_^`cz*-rOA6?p?@89&RjvpBsel1W-3K#EPFpthzQvJJ&QVmihzAM>Fs&wp^JyQ6*%4F6b^ +)v$!qh5Y*a$Y7gRB`o +@9%TKtrUe7wjV{KkQbO9_rxbn9gW!!lOMG8)vX-?0}J95WU*sV!mQ1j*N;fN=D&#K{;m&}C=?uA+xj< +^16}$bz*-aV2d7tggOXkXa4Dya?5K<9LG&8FLwmPoX50yZNg`7X$rEW>+JLINWl|a3MF>+dnbi +h~jkvnXi$(IH)bL}9&A{mdZJ1Sv_Lx`?Y7N-IQSU&XY9qr1S>;lv*N2Q;sq)+ft+hiCWZ5<`d? +rwmr2E@|dSsVTM}sB{2jABNkF`8#JZ${0G@jPURn66PU#-2RT_E5YazT!pugVeyaDYfBjiz~NR7SZu! +-Xsgmt2*p2#a7SCs>rHcu6o$NLF +<-G+L5mb)L{pX)OGo=-T)<2llfJSc+hfKHK141dg`TQo~l0JFoQr?R4eCaB<*g0HI|Xd`gtTcB(p=c?rg)>Y#O^PbkJrNaeZ%9SJ#CT +t1j%GpJsXdY@SX7{zLk;Ecwgp%Zo9|lK(6&uLWEZKKUAsA{jQYX=N4)30S!SjC7!DqGm +gHx&U>T!XuFxd%xCX3MRj#`ab5C`>;yW8W!)A}F~GI6bLVuHWN})UFf|EWGdn-7ACg&GzUVWR26kSb^B}4|>@12F)1=5iqlDSu0Hc^u5_UcO}vL!VsMin=ON5)nmMDJED +vVrH~ooZ?`8E36(j?MM5`=(ss~OHu|;8aDdQ +c}P(L_NjutV#eFs*WjKeld3*2U1>9W#>yJ%=(@8p=O1E`W&*OCMuSWQuw()z^dYh{AFCD^96hU|tGJ> +S>b7xvgl+2+0V83)Ct6l7>l%b$*dTXW(s5!my8=@uxTnM;sS=Qf$=Xda}%&R3s#(zp2qKC3IVA+8_{x +V^cEpL19>(pL_s))BId@LS1&?dK&dH5v$otl|){jDgj`E=aIN^Krh8qbqX`kzkEOJmMyPKTaR3YuE*| +E@L0z?&!NV$IO%Zh=6QEJy`4kn6EoIlB +H9dA1rJf_3Xi!~@88`e`0ZM+@PA`Q`fde8d48IX(-~zs~cqCC&gK{sZ8D=!P`vYNzz}C}6}b=n+aDzs +se`s-{9+J^0Szmcq1royA<~ia4c>oQ`@!))RGgfrvvb+}bT_8zjPDuN^Ro7_acXcXkoU?5}vCfY1mx6 +|?e01D3WfOT(Wh%W@hQx=y@C$>tiphq}YoZf>B#5sP~x=~6?UF7=0*(4V%72_G~WcQv}`zrs=$Osm*w +%Xn3Ml@|M{OjiCreV6!RhC6gfsS4oPE)z2*0mzK!RbdmS3X#RP|CMvCh>|z&t6wihoz +I#mCB~29n3yI?xAgmJQ!b%VnuK&lo6mV;W5!4KVvsxh;Q-ghbSfiGFXGAvXp5vIUDp7<5<^jJHfCFR^ +}^3aoyYX4%FnGwnU1dj>pYbeH80L}>}g3;2o1q|JbCH{jG9+1YS(cUjFAI?Q0Z>akF-y^5yEUOIblR}FktI&zCRvq8X03EifD% +fK%f=?etHB3TVyw)c-Isf-Gkl)i)aX-veujP-CRn^7>H>wq5#ghraO5ewM_dwzfc%N*M3)rCh$R^QsD +e5h(lwVbUVPKid_C#j$StfI%i=<-qBP_~s{3)oTp?Nw+OMcXWMVe`pa$waL^zG;V*FM>`pH0em9Faxa +aPQGuKji4<)QkMl?etbm0_WhlrO_T&>Ydp)#^wIFD8Bl*`9*Y|KmcQiB@y{5O9V`%zidI(!sdvIT$aV +=GK63q+{fauVaVgt4a%J#a!Z? +)!hQXTVz=#Q3+;|4Pv`$KE?*P!;^k9nBbS(Rd8w9_~*rwD|qD~Aw?YgU>6buWSck9xVV(xu7M&Nw@!I0 +Vh!o(#r0qAJdWUc>6%#+y3ERtmDAh;*Us@BW6H-2-MSRGhOREx6e)nsmzKb_>N?h=cxKf;3+yPBje9O +~1{DcBFn&NX!##+U_6s9B$p^^dD&{6L^JVZYX$q)d6X!IJprq=Eqe7hJYCV5>0qm0|uLEV{i7N= +(t2zg{in257fa{Rb6vXu{=eERQHFKQ&VSzacKU2V-$okT1Q$uKH7U`AP+WtDswJE)p7#)2(9ze>{g-F +z9qdRb+OoaY9RSzkZs*ba_eJvwPSkK%HXTSK};*~;)9+e3qq>zC8&`!rp;O3wNs)}vddoOG+sr|;gl? +3fhQyBS5NcFVpQ{bzLQnz<0J^3l{*vB0iuvZU7AjJeg*Vax~4}BuJO)@}WgJRgna6Vo%9>s+}t46s20uafYvO>W3zs+96Tcr(E_JH6CyILd8r +QU#J-s#eOEMpNFEiepFL-u2*yR3RM$y60y3sN>f?5w{*T_|}|he-{%f_Sr7eiCdFgzlTX%8*$(2>=L*-mlwID%v6r2OD +=8_r;EfAe8p@J1T={(AcRC01EZ_p*Kykn1lLRqt8OrH|AoYh{t@8cUcN!Ky#)TAlv!OTYNp0zm@{ss@ +31~4f!X7)@~GVC>1d4(q?9DB|}NJ0iF!)9p%M!d;)GnM2tLo6sK2T$L0hjK`q7N;s(VU$Gm((K +8?+n)j+qR6|?Ui(tlT}V)JxB?`>G9ra^-AUQ<}}hGXG?03?$WZ0lpE>Cb5p!Q3@*d%{_CWggi8qmdY; +ONq4HnIo3T?ScZ{mV^*hgoMWUvCLJf;wwSBaNiYimTQV21src8D4fENXK^-(OA)CQsu;D;yi^={wmZ9 +^2eb4QZ;5?aKzfvWQ&LRQyw-!O4Kobv7bm%i!+=HSvO%PJuq*pr{4k1VkE)_C0P!CZ{)cO77xmMt5v6 +q)9q`xdyV#-WEJ>e|>>;igND6V5&mM8a`}a=JF5q9`l$z>evj*ze%%44ETJu{f_gweu^;NeOlWDIyu* +-LKQN20;!O00i@}vAMd9)8Ki@z&&E4yf(gj<@18*P@FehFW_Dwaxt)nJ|8%E~mTu3D{MeDF5&-hcSuU +6y3FQ$i?;_fC9T4={;m^F-Ad*FdEfJ +XcxjM5NIAV3cF$UoWR4#gk1w_>AU1^Al%t=lWaD5z}E+_vdPmrqRUHLsu+N{!l9eW!x+LZv!lK=%AJ|6?R)nHGBvl%db?L! +YMHV;qQ4d&vti!}nbbQ3)aWN09mXN{!_)Zb~uf(Skwx)EbA+hrzW`41k%@meOjB)tY)mW@@6V8KoX%v +JJb`FEl%#|o$|P-|Y$*z>mgU0AS#hS$C3x38rG0uhF_tEM#9YR7|0>-Zn!?!*4LqhL*n7&dHig3ei96 +fOS89O^TFNUiF&zTexjGO}t;i%EaD5DaP#-G5h>j0C}kPb~XrAF&_ijc)VF3)}!cj|M^^vIe)0`KB0oU%lc5O+ +L8*=z@g!0oOezia#n!b~4GV{T1n|8a!sxk)g{Gmu6pA*n<9U!F%{ +*YVpC#GgVAC&KLTgR4uiE>Ke~j!3JM1tPJ#U_u+J)+XZa47vQzP14#uc(f +d;>_(3hA2^4`@#7T;0T(GXxmG$9m^Kb*Y0lz +HoUI-9(%$?irGq$XaKc#=tZ~uc@3-m|z(;z8gdFsKf&4P~>6dt6w)3o8CgC7Ri>gH5g_ZP_-GY+5+*GqvWru0UxwO2ft7;6GlwK_x?smriu&}8b8~$zs*Jlwein&bPL<7rShBdHLn4C6bhYhlT1 +@MmI0;>i;=D7pn@lcZ9Ve0`_4LBl+xbb2-lU7OsMxJZy%5uYQF-~4?rul;8bm*%>Uapmmw0$&~eM~11 +kRnj(>c)I4*P(U6feHg3NxwD)>}zu#T_$S{v_E4kN~YhN2FBK=np*p;x5#HRqx(ugbl(+DBPDLFw865 +_N^Cw;fKjuudpcWeUL>Pwn#>+g3sQ6m6h3uTmn~WGx~(yGyj1V!(o;>~h~mUFA +$(7)tw?y#FfJE_(yxE=!85Xv~3Gm}G08a-B?d=-Y2B5H3R(J|;vy%XmIH_CMQ2L-s;*sqqcL7?{Tf2# +L}UhmuRNj0;{X*f*e#Iu822mfe&bLAEk2#4gkH*a{8hD-Y(`95C|xz6s(1lX{rj|YrqTMh +v?g7)nPX+N67Qj+wYvvX|RJ3#@T;MUn`u3I?f&;Sar^d1+6^?NY=xV&Luk%8PmvLLO`g3KK06e0Mp#ioRZ8l_&#*L~7+!+Nos{)I9{pd%o +X3t0sG&6pLK56)GSMccI-W?9zo5V2Hlsmk9@KUifUpd^os57L2{quL+sOY|B$chv&;~D>Yy-gIGigzN +*||45;ll+DBK?Ef#v&i@7pH!cWFvxM^wZHkj3Ii!)%i$r5uf(<}|rVp64xQi216bmQJ0Sso}xZIYnpT +u-%|3xT+v5;s>}l;-tbq)#bYbT8v9mO$Be5k5<%7;%G!Z~gAt%!YfLWV!cy+@`?I0M*{AtYMtKB$-OL)IcX+PGaZ5RzeNO{POym8(mKVqU)&$XeHqSHocqkYop#tz=xj +M_m!AaDzhwptzeYH>vlku0=2EJ4tbMK@`ob%DHzZTifU_B$LK1!zKueq%4o1EE8R-E$jiy3npd+}Cix +SfVyaYQt1+GyNs?jM;b@XNKs;ei#&fhWKj1GFZ6E`#t?$C8X6HF(S5n~G1&(q@?m(AVUhy~UNLIxb70 +sR#vg+OfqhoO2LN&z#0q8}saXil-rcN$$apyl=QlA3kT~a#mb)1)~C9Tl1n=i22WmL}lCQ@O6BQldQb +QKmpSq{?zS`>z_i9NPx{j#m1>+M(66GeT-)e0IRoLjzC87Tf52!+-h1G)jTn_eVoGQ1v3Lzn~t5T$pa +j)rc)Oq!$gc^;QKOWXn$M{K@f$Q(smFZv-cud6R+xXb{^S}6Q$tE!2IB7YL@YzOlLJHzrpTe>izY0%ywKduK`EqTtb@D6OrD+I +3OmG>%@qh387aVwRo*JUh0yS&L>k2xPd{PBJ6a=8hvuNWXrJ(l$~dZ}hJIL|*W8PF{$&GjZzVb5n?rk +fZ!Vl;2O0`L8+SZ$}h%Xzzpxwjy0%1@!*Ujk>X^ZlxN@(EygNu8gYq()GNeaW4rT`bc`LS?m4)cl9 +(gJ}f6|>c)k8~1+>&)thXUS8Nj?#Z)9|{PK)|Y+w%(`kg3*ql$H!}dZHiVP5M&VI%zDia2X^nFP`k^rcB=)6-0{=ltbBh)73Y(}H)i84TOAFY0>oy&Ab^WNN6%$4andtyHMN`arB3$aGHZ+c$0Rr905verD4RNZ_REX- +#jIiq)D^wYPRyNW=htJexXDub`dhBdfbKWLZg9h#v-vl;FiJG1Eh5%X_NCLHFY5+V619* +j$IQlr#iCBAdZ7)41;?3@!)^}6uZ`TCH#`$DOYi}Xe*^a)UJl7qi)14Iua7qjKB5`N>N8 +_o!gY=nF6##)EXy~>Z9#epyC0Bts?7GzrK84Qt{7^8(^ +64`ze;|>Jv0ysJxWvU!BAN)F-ha|A%Eb|jn{l^0|@PAhF90E@lxKoZPN^nHS4 +L%4y8~sc*brkS%I{O(fYtat+>ifmQeh$@5gQls&p+rXRxo-4TS05hVm~+ev#@g)Wb7b!_no +9>#HBem$1j!_E4f)+i7->=02nGD!>*%w?1h}H_$V)^ndXPw)IRiDxJFPNsPrmEtKH1D|Yo9 +fCdEZvBWtdN{v#-WK1H1zLbCB|xzk6Ebr?Q1-((CZ*^&eRKJf2LwMFN%QGsSO{`PT!Lh#Hq~1OxUtB38BBo4rnB +ZDLixIVr5j;Kc86WowbYDtDL$E9+#2=zKZ}Xy8ch%4|p=+Z;2i_%7B~zrlPrH+k|UU2vek6ttuk?oD= +4zj@N66~%{FSJyf_k+7`dRj2L0&uM^OXqx_R^s!>t+D4{%WFbj}vvnZIlC7IrnPhXMUR%m-R +sv#X^#pcw>P;G>K>AH@8okG>1KTEpoUi+~MB}2#u;lHxG8ojq^K9^GA}~S(U57r~&^L??ODG2fCu{ZS +3$p7SMb7H8>mI3~RLw)YZ+*zSy77k`gz?QAma#@-xmZ@W6>d))NH6A|ydyeFpG(w$7I%eS*h6&0&NXA +S6m&*3Q6?dBVbxyAyy~$x>_|vFNtoY9uYF4Dh!yV29*QKC|G6rTBcbA_qr2-(ey(f4LAFP+yiyrh5m+ +jG}+^vV1VsJOk`b0d3hnw8J77G9Vu3)Evg2paW?QQFcU2@`;vZ;5bYNBf1PYP`@&b0yNuJ2SUI%nTnrZ +jpXyU5CrrjEvxBdOByFr(kZZ@qG(7e~*Z47i-UdL|X6hRNOW9<3v+nslHmOf9V5{^J1Qe1m$8QfGKB< +^UIlDZiHusD}(*B~r*&+%NbxDjB#kZkP=8^OLkF@z!-xMudOHk_xKJq#nD1L|gBNhOt7+c`PP*R)uwe +aVEI>x*l50M$pw^`SV&ek8+;07oD)`{V5Vlj^~3Z>kd)yfiTkHVSBc#kK6!LcTBK=j5ji`IZP*wRMwT +_F>O#Zz;y0#jYxWRFw2pVm_NBYg~$|KYNCoWMySEMpx2WUUjXBu^QIGX_;nlBL`}m-^#*1wy(eqI*>9 +PmlC+Fbv~l(iEj8bTP`#or3LSAVNYw(*wf{T7RhhX!T7)v7GvI+9*BZI+i_24vE!r#zj1T>u6MiDbjU$Ia+v-#a{y<5 +uD1;56_K^zokxv^|g-n3R4Hk0jm&_mkhX7GHkJT^G1?lO3~EJS!G(LUye9})gMQ0diC$AFbZ`K+kjg6GQ#TBFq?7+_W;)s|M;&WCe +VPErD)kgLN>wmch+oTttYbHx2@7R+;Nlc0$iX0GtT+23PFnrFHtwRGAYNncZ95- +W?J;o`XmZt{Dv-33n>6&+)34Bpj4=Jog(To7+Q@b0HAtYs;mGt!$w<=Z5PXkSp<#u@`!}i?=wT6>&e) +?*ynjRk=cB>tPfqc3OHxj#b!&PA~Pxz`M3to|<6?1^&l3bfB7x8vG?a+lW4)rte=3E2YC( +oCg~tDxrOMhrG5B#+1Q9XV3u9B?C5Bxs;nq+%qlg1^x>oCNvlg)H=bh +#WSiE>kxvaQpvviG)B)RJizV{xNYWFvS&H6f&L|M*ZAtzWb(tAq`-67WW2W6(nOnF}t~U(`g$7|5QJ+ +5YnYQ+QnoO%$EoK_frETWM3<%OV%~6v7%ySOxBL*zUdYds4vT)Y#Pr;8#X@Ni_o56O_*{&R;YQ1mj9J +1V!K^DRGQkLpqYk_Qc%u2ptV(G>3>UljT^*;6~FEVH|X2J7s*FkjaaZp>*uwEBE6eP2GwYZG2ZiWUzA(a#RB`IDonqFl}4SK97(>DCNAVSlS +S0}6dA)TF{T+&KyYUI~XISFy(BK48X-h4yVrv1&8MO8f*s+liBMu)fK2Lu$ +2>VnQLLet6+zHR|-U(4KK@0dD(ujby-?$U*mS&{6KA%cD$}40W|EfVRI2@Z`E8gdYZGEx=5Kl>)|P?E +APbX@AQ!V$uUjc9|s(;by+s|`!&LWQ3B_KQUjsU!w=Q{fGG^?l_A-bYO(mYY4WJuMha-F6wntTMXD=g|s)s}COiuH~2#!$rr73W +43n{#*2j%j{J2zp560Dr`NMKEF<7Kdu85gYrwNs+`WepbK@>dl<7NBF|XtY3%%{Xs)Gh +3lX?zn;Fz?ovKxhOjGQ8YUi%_1{FV$bEbrpQ6#9w^n|7?F9&l=J=N+_ygC +W=kGjZVrhr1l6T2nL+$y4Cvui1ctU+y{Qj<-mKlVqr=~L?JtQieWKv2X465DRm3g|q4(^9nI+q`S0JK +u+G@@<7Mg%*@0zD=Z3#>)&e@b5nn%({<9VMy{XGToDTN4W+!H4+_^f9_y#rT=lJ?cA+Ge1ttX`+EpD0 +^PMFCWkE*>^GN@nUMi<_utaiW0EKL_aMa!~EHkg%mUp5-E$k5wO<7`+T+-<~mQ@jtRVqMXl-<%o)3@w +I`lR3#cn4?@am2hVmub1+pq7Q+!ipY^1`A}u +)s_Qt6D8ANtGMDZ&>{Pp)L>=y#j07&<5E^7rU;XmB3tuFz#mvuOwB_L_@1s3xj?YRqHIpTkOR)?m$hd +r5DUY9w!~!%>n8NJW+Ro&gTO7YS9$R$ISF6#xdK8Xe9&tjta1D?W*d>1+2T~L?glHTBmjyefaTWXEOpaw_WQ_Ehyn48V +xfWlmKW`M4juZOG~^3@RE@$+XCr`(MLJe-YuQ^>|v?XE);M{7=-l2$n{;^`AaoV|BPQ@b=5#9#GcFLh +g>ELq!FD-aA1&Y=@!W(i3vqk61z68NY?e}DjMJ9v+^a@Kqv$niASE-CEDfl2T-ma(c^Bxy95E5Np6UX +OdpZ!LsoOi1O>8P_oRJHXUtRnsvRU2uy58q=~2^N`aB)XkTWrwkz@@FAySu_CbVuG^w^0l&3#4QOab| +tJr;KTVa^z|=9L$|r*q8EVZbgP0i$KzeC7oK7E1fFl=N$|P&3lwN@U4giOB%#p0swbX)6h8l&GGbP%) ++8&@>GhXGeN-d+ophBCZ$=SQN4M0-H>c-i#v$1S0dT28^Qbql0+!nAKIdl^Lnpp{oGp+p)$f5bM*onr +1&^tu{}c|uv)+aG75-87B@Nc_7J+Q%q}kuzSSkgmuR>Rf9p`Y^vt?r?`U!`= +)uHM9;k(RH`>_ccn)lemxXa-BrQ&f^01qB(RN^Mj*4@Bc-;zaKdfS)V0|AdYdr$NH*^GS+$k>Orl!#< +8p+*_4J>yk({zs9)&LYe~ae6y42XP7Lu^jhCJLy3%qD(H=SEr|U9-QeHLkk2TIJ<^-qw(#~yCBRNAn% +_XwYJx@pm~oj!qfCIS=5v9<<8^~b_3*NKl!BL>jgCk`+%#jc3h^ZYXLCv*!sD-+92ARH0Zgp8Uz-WSmhJF&y86fR +@j5jV!l7Hn&-yzXR>Z)U*U32n=zUa&xr8AtlQh7Iksq#|BXR)Z`I9=x7DxLg^Kq$mtrOz_s%s3d&KVWDZ9qT`X1s`=iaV0jW{{S-U%Oo6v}fUru*xSUpU40k%h{5ZMUH5o_#pLWXpf&H01gJJFi^G*S?J=Xuu>_|Y3)`LTndE)>WO@|MV1|?- +-suWs}rVX4yqkHSDo6_eFrCvHr=o8&hEVT`xRbYXVif%y54@vcjt%|U-IMhvAWJ?aFV%FKqzFL>a;|H +{)wO1!*nnlbVo~k=a?PgcKQLwOqsta@*szC^SZc<5eSW_7p-6l5_Uk6^uDB}tF3K7?XxQpYmF{3xEnc +*7s$f0T<0PT3S7uTHqRUg +)b9hI2JmYBd!1&|fQwCp%0y-8J6We|+-hXx2lisH~oSS$5tT0K^oP9rzZr!H<|^H#k%8@5s=BvFR#&6 +ad)-npfekTq|Atui>z*g`mZq^PTF3*i{WR`q2K;OA9#lVDXHJ +Y#D>Oibbr<>CDFeyj(?0}^9e23jr*pmvPU{Aal2K3Tl|M~am31h|&g7JlbpLeeb7`YH&7LIU0Cc~NtB +bhtJ0qx}w=hThgqDP7%JlT?wn*&%TC{R+dreuLSzcUqyEtN`$RwNR0$4A`e_vVxxbF|f;S!e645lU>m +F<`t9paj63UEvPSyJlOcIOcFi8#)=RJ_7l9g@-#zdZigKNw=mj+1hpsJrpaS&@+l-B`4svdwfG^--I( +bwszB}Ov=B|R{|62rSdymu4{C0m9?O0BxU{NlPEUSUNse_BsxnMU2ZXAlpsZb5%G-AO@uK&mDtj#AWv +vvJzWOdWo70uFurdxU_IR0M2|5OBM5-CZ-fk%kQljJS5_Esa)d@~IKFZ=(@16nO%G)zgy5lourkz9s7 +KO3do(Te!7UVR~AB%J{9gc1t5P)=Bx+DjqOAgE#z7w*b@@>9I$R2ysS*;T>>*Q`x(w%(^q_<@8A+pYY +@xDf*kz`deK(Z?7AJuIziYmW|pHmLZ3xk%l+|?$(PPfV4Z|I-mBA?;Pfk0TKSEF^txNhs@c;}oICB{? +3!}$9&_2WFUN|(GS)KyA{uFvT7dvrUjki!7^=QG6&Yk?cDSp3A>;Z=e5O`cVURD55+fyc&INc6Jm_(M +KDbky(dG^Rm<9+;u~^zC$(Kfs=-fY8W<{&aiH0^$9LmoW#X=N`*M+2&dLC+*fAU%FNV78yoz^=$~K)x +T?DJ(m3nO0=*Z3th~wqU%d&14>cpArJ;hjs)49o^8*(+wt(~RNYAfb|*XGe4gW9z9dT>1*L!s>Bogfi +eQw1J48lr@l#%yLy-p8OV{$4@tUN6tNl=eT0FPf*Oi<-EW$}8gyE?yG8$}uMpvELv*Yy~-oQwuOe-KX +s(o=O=BPX4`8>VWQCJd)YjP?h_8ui@&x{u%r*{Hap@s50y0QVI&>IG0W7dKF2GNRV>^d&r>^Vu4^HBr +pid-Sub0@rIDZP>KlkC!^FK$6%X8P+prc*p>e2L*(#Tp1epbN+7k&Rb>!1tfck$NWCBUTW_JyF?Dq%b5 +0A38Bqd_G1lPH`nUq`(g|BwVTe-LNrQlDHPqyqS_a9kSLmkQ!LFJnNYMI=VeZsu1I+4=#evAirdQ{Z0 +qcE&#xxLQU0DTa*f?pjBi?gfE=_8Q1ocEa{e^l>7L%}tZMz4nWuLhSCRJhij7wywMNS}gtV +G`vh7lh{iOIP{)|LLve1Ajq@F6#J4vbEax1@2kyB$RAVfwN(9s9S_UBF3;mkf5h2BF}*lo_9SQ=_V(K +~dp%~$KYM(2!CMNjRSTMT_B>yJX#Z%>hxAy`$*!VIi1C^XFs|&oz6v0)&w5vvs!hkC +;pTBXdFjh><3Z^Vdn9X^)Bm?^HIXUG|T8VCg*m+mdo9P_M3|?|aPrhx#7z1l0x#U=b3hNtP6_w+sSpInDgsmcFHh#bl^q$Yw@frU^-t1oT*~JrF=2UGibiQyIgg&VCxy%9^yWKn&%f_%mK&3K<7l?} +MhaZu4D+t45hFmV-M2uukRR?)+}3MObFghbhVSRRkx4)=oK`{gAP!?y|f0BgTQUabQPG8IU%4JZX)ls +RII#O!!+@itn9r58YA|4PKdti&!8)gz7H%@(kESp*rLi~>dZ`HbLmH(&(FbduM88(Mp@qc|XxPQ*();R;Fvx_mZW4PfR;qN +E-KUA>?$BT@?NTD9*l_K1&$c>Em(!8d*ad*S6avc0gdgF71AlLLTGSkox17ya3 +Hp10j)Y>T@2fjlKjB2*O`1+YY@1EYZ@x4%1>pfGAuJn-{WOo_HR +ti%_iZL`c;bh$xG>`2EFQ{{oDxWOYSbKHHcDQ_j=QVH(;J(uSm;a6P$pR0#i;wQ-DLvk%L5KA2$?Arra< +oMFYRXHwF76aGqZ)6VbzPO{PfAyKm7=GMP(Ta_`KiRHoA0HKR0Cy#Cpn;q}ZJk1sdqXq0@H1tXJYW)w +M7hYd^`kbmj&7N$D;f-uW%E;z_|DCEQ(!e~C^l;hnM~j;O13^K)&)9(W3EzS)qR#FI3_oNwPullm7cB +>X`vb-?vQfj>w6d+i_Mu09#H^j`bp?OG^srH+heGLrP7|4dQ3}RPW!~g~@pRt3{&2A)5?~`NUK4Hq;L +tqiZNDW#bE=YUwx9(~=6^m-;$oI&ZyXT*fuPGQ_8!uVq!SK|G7wf3p+OUW!Q7I>3=9x!nwE(M+dFasj~();=MVWToeZxDfP<25I-ri4XlQAsNIYLObnH_^nGBCldaa$x6|}J +80!`7p!F2d&F~#C!3J8rBx-~t3Y>E|w=9I%vyN#4G_p#RqBqN$Qac-jg45)oi{NruyWeVGj4Rr(~Z~8 +MS7nfMj%mAgfBTu}YXdrj&?{`iLJ16b_-Ua$vSev1>TMQIPP~yw;IDNw;QB;H65yf1U+L64o8MGiyxg +h_EI^@u4@1)(_Vf^$?`i(u`caJ$%&f!!N2UjvvRRIA={a~t9VlnOhCMu55tJ7QWMTY_11mV_Me__B~` +kP6+KqA=EMi>s5Z{M4}rjoECc}y)3h_C=7i9Cvq*!SGJ2#b`Y*7W;$RwZX42emmKHP}h=Hh-xYup=KD +QeuHkN6x6p*f2(;GFr_&a=IXj=e>3N +xx880M!4YRM+Ue2z%#9WNSa86ba{ufmw;>2n?u|{0N93_U<@F(*kBC@sISe)Zj +oNS(aX!#+{l4!o6CJ+vM>qf5p;k4v-3Fi}h{*=*#neO^u17lOvj(KKwEq3^}N`r@1ftl3Waq7^Tv37g%C(TV0 +7@AYH`6~?I(xi?WV7unEyFc3N1}&C3zJ-5v0BT=zmz4_3J1|y|WUh!13GkkEOPb67Arlj{-S(_i+iV9 +NwqXq&x1R!|!&@D=GU&26_)UjzXx{7mgFYPSk22yR*_8~qjdABe<|~6FQy2a>k^;!7W#3YdRS4M9o{# +k?j%mdRX<}2wC5^p}tjNEs6s2S>Wi#~G3%5RxKx^-KKRtKgGd=3VfgiA8{EHu}C&`Yt(l2FG%B<>D1+ +2hH!GUeuVEg>;Df_Yu8W9n_Eee=*Dk4S$m$7)3#E<{3V4q5gQVKU&0f37*EgpkB%TWE1KxmZyfDJ)Qu +($zueG3?*9GFLIvt)eOyMCmGrqTeW_<~4>agd+oS+c}Rse`m|KmanqPT~)9z{A9Z&&Kx%J#`t2&@Rba +JuJ%5+^ev(6r~kPFT{Tv1QYUMLqtqr&R +SwfpT1CohR4z~z7zU^ZhOMCodaIn2g{BZ0VkkFj`mj9HjA!&3o0tg ++)|S99;=TRWH4x=B*vrM2w46#?4+4RRx!}}Z3UY&eZ|N~&gNr0xWJ$q+jO>t2DMRrluPxHHvYNSn#@X +MvJK?Eb0&yS=770%J#ee@Fxqx+KFsuIi|9;F%ol7KpqG|l6L6JPBi&}!@>7ly(aS;}?{6UwLv386)Ln +&7=2JB24FeQI?LRUDAfmXTY)_@H5!S+WAXiihD*11|_L#p@ha`tF8*#3zb*0D*BkYJTVBZSHSj3(xX0 +u-^S?x+2W<^t$~o+)-nz5&@LJ9J(@J%LI#@X*Snff#s)*7|KeU9;vL+QLc&QY}^p>=#mSWna{&a1HPC +S<^|m*QTz1;9=4|G4|4FRU{nnYRf(3=5G&bhG6)hB?Gb{FM9hY{G7KclNNCIkou}KjDKLl;t$?smf+J +|W@hQZYZn1^b;T%_UG$j7KfDR9FAXu)=q$SW4*!VZ%>i7t^aJz>dMtS9x?kgGnuf~kg|7z@N92i0BWEy>QgBbun6zU%d5;QiU`n!5bG6rO`7E9X;qkGB!AS-48 +5EjV_1^7B-*^B7k_-LN)q1Ue;nQs=V297>5(^zQuD`y>Swb`*1N9MC!WWt015`rMscd6{W)Dmwz`}70EBz>)tT2r=6ky8f2}}eG26EdQjk~I>Mt!GEU?ogNzXo3id)>!R8nySIukV#{746}G|G&ezeKd6@?)K*X(2TQX9;Ju)^ +yF_cZ+Dkr>&)vqNGuwNEr2x(4yM9X-416}bZo9>JBX;vzwb3oV`!sI18H?903OtC?;?A39R=dmG*E`* +*$7oy6O=t{kx{{*an#t~BZA@sS_ZD1O#h6N=cI@fI8 +ey2bG!RS%+!3EKF9uYk;v5+!2zPxv2Z;}<=*`#{9PShLiHidFaP`hD$=BS8!jl)k=9}aW6QIW^!cS4q +0RixzNNK`HQ8p@hyUMy{cm~!?Hn^BfaX-cN`58}mSA0?x-kX$pvl+1e)t#YF+cKhIZ7N5OL!r$YNYU= +j|u8N@KU_&|NUQZzIe=KHw{*2Ey{8Z%tq8{vXj#4B!xd@w*M!WtWc+{UFV;J-sv*7(+M7AdsK;CERvU +Ty{4DEx>l3vv`_l9l4HIsFPDp{0smPF3GKV+u=Oldtar6VLSJ2(y(hb%>X#2Yd1k6~N$|nhqeip5?N) +(e{hbWaogPaU*kv6>4& +Z(;)GT&(hpY<>@W%^SHIxqm(PZ1Gzq9eG!@d5y42JG8i!|D-a)AJi|J=X!bPpMK`|a(&Ter;!!#3Nry +WH&?!8|G!47eAMbiL7@uaIZ4T~!4v?>NO7pjrq>ysP1}(n#0s@g%^9B(Xtz2w2b_Xa1MM)WUQFx#h& +_MlAL?6(uZVF>tmf=qn6%-!ze0lm@W5(W!v_@l9^~iIGCXXr>a>$# +%$8?X}qC-tFMn>tGnzk)up5U#mhnfi=)>e*U|plx0{rFUr?=qA8g*aLpXGLb4cnW9v^{zz`n0X0F;!^ ++UQc1O?^*pxU}BJIU(|*7L{wWm+&0XSIiIlf?5yp`X?Q*UpvHnZy~6vlj+jLrY_DTKxBaWe>^ZNd{Ns*)rR6O{vu=EA@MVdXvS8;|`69ojKT2=HPHjGrs_v_VnupHBGw(5>5&P2Ylr6M@JxHbe +@5*Ruy*VJ+kT%@qj7t0}hgVF#Yk!u_{c8D_0OlevPC?7PPno^EdhQEq`=&ekFRhzMrvJ3*>QamiZ^Yz +KAYt*71k1)T0$$&+qi}JTAwXuuRsW;wd?0SJf4=>TN90pp}w8bTfk2tAK(&Nyme*yY +*JB%Up%45+PPA!$E&Y?B=!;<|VSg|?)9)3QWcRD0|pcPNcHLGSrrL%qD(>U8qrS~CoEL4#mdCFWxJjv +Ar$_RDxb;Dvm5JVewOMByb*;&Kth53|GoAicugw>aSKav=2eo +~XeB>cOn?6p%?uEL> +Mng0i&oG)f6p^Q^9(4FCF=aDgcn+t;xe90v6`{bzYle1^q{dFPb?caK4%%($+_bnGk@&HPywIo=k^7| +qDS8LswI&7QlX#z(N4}1pEEISyYl%>9tra+HZJIk77o9ANdH}>inARWT!(*P){=uV#4L1FMDPacsqUNa5FLZ7w6Iczdht4-hUY_IB5QpSpZ>|)dd-bBL=L-4()jC>{f^RkXHa4=YJ=U@50UG)%B5 +@ArE@cq=qtK#4;S}^bzPrlJp<&-p0vnB9E7$$6~a+SBzlbz$8sV)4`tmO&GAu%@5}+k1je#7W+7h3gq +tBKu9Fe6OO4-;whaibbiSD6LuJ~WHaBpxV+OT;1<|>hs?6G`IHj!1^dI_6)_*+P^v8q(1#wdXvv49K! +P$3j4K)1QZj|VDwvQu7fu128Rs4*I$|Y8?{h=P1E)p_T)coD>>xpt0{m(gFF3fEnTFAxG(k~=*`77=) +>)h;g=OKAXpM6hJ!JEwrqQ(pBxOq)eiftzESicHRfG2&sW?#?=k+)k&vm?w1VSSn*9ol| +Q`QFqDyr32PZlMdZ5JFczHTK$tp(HcwM>*z%>X`!T?&NLQK1t`Hu$9~rwrHyGQ#|>?d_My2uhZs^CW+ +T>CAv^=AX1WTRNAaEj@0GZ=$N3i%+crX%G1_X`0LTBz +ed=&|`0KeA4!}KD|S0>It|>UfvI@^rmFlK`j{xy0I1w1=dzLlfpFG{w6OM!5IMpk +f&Z$o6&s!vX^bZa~m-bDABpe3fN5L)draX_h?6#&O!vBZZDD&Jr=#U@xAs~4S+DtT^xx9vYF4EfZkgy +Lw+dG&6vZid`XttFrk1mQ{Yo*5ym5o##i_|B)s^|1PMrFW(5dPyW`fjtpT^$ox&`KA1A{D4X6bRnmG} +$&1K^ojaW+QFdXDr!U3D*(4NvLgqUrlXU%V+$~|I}NXiUYuzquNH4IV};OVqsc~zt&?sMU0$fO?k=^; +lPeL;g5eiR1fhw;JzX*n=!zlS_iG%x4k^zL<5l?DijWV?Vdg^rRko^zn|G7rgP(Wzpo&^SH +hdET^xe4!(fCPKoR1Y(m7F-*Q0dk*q_hdVi!ZgopP~aB#Ho_=;1H=d;F)>|)S;|op#aVI=8m|T79Jg` +VmPVp{te6GJZ%U0~YgObxvYS$Bd_B`Kqt*hKU#s4;lxpshStKRc(OZ!!OACx;Sk~4VXPVH0l-S@Zepn +>>ZVm*spWordI42WS9;AY1!TV^{0NGXpMk@R*w$Z9*rmUR0x;kM|cEO04ex(#ZfHbT}TlrmNQ@VJ0vk +6wTxu_;EximPCXu<5mSfql}`oyyr?`A4D4hast0|jNso^0O=_Ax31GHiXvVdFDu-z_TSNEsSha&bzt; +ZNC7_K%f*;ZL%cfN`IHNm2|5I?a<}VStbbX4T5mvFxH9Z29$I1EkAF5|>NZC*_E>yQ;am#s(Z9Z)(4@ +15>(`@Q!Poh%=pNuK>&5_(R)sKJoh=bN2p^e&}(Vy;eBFX0iGRrwSNXy-HyqXA^s~HjgX5zx1ldG=~X +QArKZNGf-|F--oEON4F`;ahB+^wZ?FN@uB}6F*IaM5`-2EX5zfSnKPrrNig}U`^^g&owCe!pReV&dD5Qq-elO)Pc`i$h}bs_V&u8^S705gE30O)RH{6v#{mYS>p1^ +An?WBHSNOQ0D1QutbcY@l=$f2<}4$k0h?DDQm5ho}G?UVW*p>XtO}QJJI`$?N5C%`a7L#%td85!SC#Mb1v^)@ON^j#oxL2!6)q^P +mxds4(4m?gXce^{QRIhxOM304-alVY@F;l~vYQ72$__piXMZL??@+`@ozg5ZDx{L9m&{ +x-L3D6Pc2);-jO(+xrquUGFHsS#$ohh#A(2H^be(B8atVfG2z-lVE9q8PUx)4X0K)g|4q`72zv?5(C&*#Ih(WkWjL~X3j(GWtF|_06;AGDlBHD%>!btLc@$ +uJ_3iIrJ&uOo*`>2iF`83;5#bJn`YJDRNW^WHYl&WXG<(8Nbeo5~dLT`s%+_Xt*GxAy2lnpenXD!+&# +sA0a=ngUtoDz9odrhLaqA>)dh1#8l%$aty-h%YmOwRR!mZT{!ur<8=J-RG}e|i2@d1bHM3c<*TpQJ%c~|>bY&Eg>B(lv +Ui?0~h%@9=n8kK!hl@g8wUtLzwrB0Nb(xaz(S8g~srT+Q5fqpf-G44nLv|Q8YVnh2=VMfGHT4{zbtFp +kh4q-rdP9&nwD0q3WBWqyx>FA9$L+r7~V|MB<~>Rq1{ZNOAIRQ*X5-cfF+{z~xNe)&SS+*byO=v)CyD06yDaAzRulZuGMJs +Um`rZ%P3^H9jBmXK6YD2bn7@534qDSRGw;paD!!oS#nC?s`|rn`7w(P~$cje^V*jM$YYTuwPfjQcfxZ +)D$D%Ns8D#ErLbByHwP101gltCb=wqIS|#MK-<%zT;xwr%7&4?*r}*`vs+s<$xkcPGZ{dif@x~ExGz) +KHg(w+16Ccll~~`sn#xh_8Bia!Qp6+s17;Y?b<>^K^i`UI6r{zZN*DB?th%zh*i~FD(pgo?($EITJym +Hv*YC!r-PvwXlh&VA1O8e0E491ZZ9aWWejiPy7^FnO-=O8a!q^yjPRj0N_eqgHMN7rjU_dPiObXF}8* +euInPxCjWdytd>$UQ&`6nZ1?>MHrU!00gvghrHvK#B!@PE?eS(r0uDDIhd5FVuszecKRP*}NaQWz!r2&Z>0gP6TJt^r{05 +Ko6_Rg)9PMbJ{J(p6Spou-@Yj3@?9_>V*L-C%2Td%dMAX@oQDU4~rih5Hnd>&a}2G>_rmGOl|2km!KA +T5>*Av5GUDk&xg-%2E?N*K4Kc##q_Gm<;pMZ7!tT6Un0ta$k^3%94{8B9%~kiXj8jXdPq2l3FaKwaVc +B(+!=2%%^%gd+yN7^cJpIFhMbhN3#ONjWU8g0z^*m2tz9Y6hBd0RWy=7jUamf7sc!5lc^SVcFPqH$Fk +nRzhmj1}=>@lYOhZ+t7lp(KCQ1C9tPE!~-MAA${Q3$@+`^9^mKF#^wfAe+%TY8+=kasCh@V3Q2L^$TT +`kJVbcU`j``WnKds`+^DaaCgE6|rs?J=wM(5u-x2W5-b;qtMp;=9;rd37a6Pk=1hG<5s&_8!!2WwAjZ +EV64-t(-NSnwQ$%Y`|K}7s%2YP?zH?XK1%i3s?DuaZNUx0ZUH(Fh(svs%&QTjWcPlMcvx~p)~RLXa4p +vi~QMSi)lAl^1?@3O}okBx;AY!(41ybD5s!uyBZInbHbW47rR@(vE_rcm_=5zH={qH=-LZKQ6iDc6j +pUbzSG`-9<@#Nv{;pkA<3uco68LGr$?VdAC>5FIvvAtSXwarg%!^o$R??2C)fpVuK5P*nxUD}e0d0MO +Q79fW*Pu_?pNnvK41n=`?r4%j5xXTHl!xM|(ZRT(1I!s&wq0vjsUs{h_;J3}zl#+BvZd&&{7%{&Mt+D +w*edes)>0NkimDnNGQOUB1>!&nJ-((_%0V=Hm&A>CazIS|9p%V6%+2bTDIY`ZShQ|Kx9+^(aR;rIyKW +G|+|LE%78Nm6625J9D%1HrR4+G>01}s#-kC$2_g(m>8LO(^R?lG$=vT0Budvj~MK_V^_%#5ad0f)Ma{ +thQ)XdWJI_YJTqH5Pnz%PfF*wuNrv|7Y!8mK#TwZNc&BuYm1A-7LE$9Z`JC+Uy!$lvF8-+9ETvGG`D8 +iP$0m9gtL}a@>P%vpep8=y?y%?EXpr!1;xC_jtKee1TbJ`%w!O9s+@Yhlf9wi!b=7R>d?+H#kn)x7W% +_4_uXPuvI$8eW>lWNYFdLFAWlWM>_Z(Wh%uD{C3Sh;J}bJo^~Mqz#+F33cmIu^2FM`3!nYXbF6ldK>s +Nu9X>E*(m5A3Du4R7pN6V8Pocmc?-GGPgnQJpJIfZX0S%Ib3IEKdMWOdYx5?7gZAobC{dbz;gmfsOM2 +KXq(SiP|PFo}++@<9smabx&w2fHVi|@Y;yM#JjVGmbX=AEFD%d8WM-Qk#wflesY=mD#^6A4y>qsyphr +f%(s9Tem^E04eY?p*o+*fL-*HwH0Lr?rk)N1o(5 +o#S+Jw3Z4j=>EhYh3o=Xp1M!y+l=E6`wvo7 +Jwh;6SDk8p;dic9v3yNVnP)-^t6%G}Bby25iXUJJqs0r?+}j;gDR=!LO5p;*AK)V}E=b1bFuO4jQ)*n +MZ}Ge5exgYeLO#qS_6SbyO(EN_@E?F_H524Y+*%rP>_1mtd~$Zqlva3Czg5vmEnUVG5pw-vc{;o~aZm +0^U6PpiP$F*D|NfSB6vYuD`(;t3wr47y~r4A<|WMeH?yMc%u(6C;XP^IdNb+VI^0u66gehfT^~dB!4K +31z;1B@Z#2Qz;6p4sE)Ch05i5Lls+KiEbsM_jA64K3qZqsK^QTDeI&VFFY +=|U+nc-a#Sn`0tN1l7-QU@ +MDUx+A$E%N?K>Pyw +*!?)=Q?G?T$!9-zWg9-Upc*#siKxVN2w|<~O^P+KJ?pn|mNq?0*tRU+j=Wzx}-sjGry(~;)2?DOo#D{ +D6LO@0QZvSXUcq6OT?*xHx5%yNInK=v-ut#I_h)F0C)JZm6GMr{Cb^_=Pk1*R3N#B)x2eLi=KUe6tn2 +|;>z_DexBfD5;QXB1Kq9OrajekhE6GZs0|29qk1>ero?W!y+@DNeRKFEsYyCR^VCrJCQRvA9Af5PheV +SqC +r7?UE^cjRTQOP(Q++>l>WI(X7BRI&NujNQ>X>1-MQ36jMP{!RuvPdOh?;E*S!{fYTVN3k6U^Pu)H2oO +^w0WsVH;(jghi#Flu>Y_VBGnB$?DS*g2{G9mIxW&%)tMsd`31ry9{+fm!L(|9Xz=dnLvo>TXZ?B>J4_ +fKnQ|CL7qw1>cpcJ|@&&t$s;+eZy*mMOD86Zl5sJ}Ev0y! +f<#Dixvo1Y^;0Wi&q5PBc+fB1rrMJ(>m;l1-#x?uD0ep@UYOX$d{uw+b +OpcEoOJ9(Fsqiuj)Ol=cI3i{&P2Q;z$)8k2~3)u`D;;4B=YE&NXKgf)M0y)Y`SZGwvEn3X#QiCzZ51H +i~zr5r}mJj)&JXgE?~Cak(?4J8XYNG7tBDrJMPkO|G-qHs39V#hdP*}rtBlX%Ja7?B7)PvQ;2;rQCqZ +KKKL(nBsXrREL%+0x69sK^h^rB@hXU_9CS8XY!BZf6EqkB;JIm +`n+hsm_msg4*09-+F0PASTkjh5AoC{`f7{~To3B?RKP`*JOQL|M(KvO(T~)#SA-hQ)G?fPcj}X(zO2C +#A+^6$rYlSHAF1)KmY)wJ1+zmezsw`e^x*?hoC-c}YofVz8w7l=>!-xlWio2zTlq(4q|{6$UT%D +9Y&MgX&VXsoR`_!5Wyt%+*KZ}a4a+tPzSgBz?FfPZ1?_tT?e^F@oB^_hoOq4FNConD37uXYU2K~39P6eIW5_AmJHlVmINPzuMm$qUN0jp +nh>#`$=F2j$;9wK0^BSzxP^-5P}Q61ko3g(j8oJ9R;feQSuzwtmc_4+5Q7<5Wfah0N7FqcS4y?490q7 +Rs=)qTf?|7qwu;33k1_PBEPSR9Jz(^U!&(L5;vyZU;Z>KQmS>J=6kT7C-8hl#~MKVaF!dBm4BR +9g9L_oaUW2Zqns48Kut@+#uj-^sh`0=N{49m2Fx$Q()VnwO11a%lkcFy}HmB`(4DnhVV&l>##xt4~;q +_&2;ma>_8B#iT23jWb&5e60HB?rXW}P0{8s3$QBaVDm-MYu12T;5>1hBrqwM6X55jEnO;Z4@oqqq=1Z +MfrND-&r(Q=8sO{T@dcNp!KOEf;ZZ9v>1#B7`cz{mlrw^xBYX0EjOn7%6ACcaXgH!W}f=WzrFr1fr^i +v86T&{{QsR14!?+Y@zZ0r^nBCRw(10K*!nq;_Rv>NFy2#X@Z3i<;3YRLTGgB2Bu)=_@^5R8y(xGf +E4* +Uyo>WumO`lGI$i6^)-r3Qze3)8rl!1-oX&^qTSCvE~!n2c;`9qn3i2)mw1C|#BK3huZ{9Wu_HEEdnIjl78a!|lHAQo`^J~b +Mq2IoEA<8m>B9bR$uYhalBC95Q!-j%rMdUa$CjG9Sy-#s9>p~QHh$l+;KN;Zy;3N0yNzF0Qr#e5*&$? +q;dX6*yrs=Ur?qEz8N8&A_x*-#pI8*He7-Dl6w#TjBxQZNeC1?RzVmZmSat2svX6z~+%V|z?>^@1T#e{`P`RP7Mqk~Sj$mkBaI)@2i=WK;kxp7sz@)GY-e|Bdl6F7k*(=;$GJ`=57!l^yUZb#b-?cB!C%@JQ4Cj +-1mRo&8YG`~Yc_I!E1q+$V=+oX#o!+<61~V#e)3ORhYuNt%(8a+3@bzimd?Y%-VajSUd!c?z9B7V2l7 +MY6sPHpm(d6>bZ3o1-em41;Q8Hb*WKBfL)+6$HPkbo{oCfM=(z~~#3(V$?OHaaEn(t;;#ql+fvHv-ly +`~bZ9Ubbj+<*8DR`X&>K4bu%L8i|)iPS}{+^$tzZ;0bfFU^)Vp&5ZoXx0mmi8e(_RX9yR1?5j^Ib1P- +)L1XGH&D7W%!gV0VN9KR>!RH{PAP(DEU43yr*x2~xEvMmkY+~`%Zasu#J}wWV=8JmKdc|f9fpD(;=s2 +-;-{A~BQoZZ3vsxMTZ~yXN$n8AJv-XQ)BjRoru^v#8F2p%wk-p(Ka(f4drCS%Ij=D-cKG520FPs-jZeOhI?0-i>iJ>H6R+} +x=x4skmvUETR|yN&~N_L_`sVKM>u-i(H2A~y8ih$u&d95( +pY{wQRpcRi!u@oCy=>{r>3Z~RMadG +HfS$PsuNEg2oNII*i4>}Lw*@TZ5&Mz!_X1FoLn<@bL&biAXFX=!#+1-8cql&s5_p}hzcZaSL?;7#C(^ +Jg9(x;=!F1tv>C-To^}*C^5SRr{8Nj*LZuF!3Ikx_tx8IPMQlUXwg|n&Qda0!@MQ&hG@WoS$HlEE8CrXLgiq;E>$p)9-iLi^4u_2?k4u+SP# +f)UGHg8!&&)((nFqy@Fg_-5BV{DP%+Vs$4`kL+hgJ?@5_Z!+40{vRJJ0v5I{d!0&Ori;kWlKHv|v!PI +3wQ!s`X3$1P~@B<^M%xO!UwIAL+4-&nMVqSb9UQQ8A4o*}+y~F|dse2}Ri=P`%Al@pLH +Rutzw-|YC+NI$b7WsuGwbHaJVc@<_Pu?jXX^^LpTC-`9o-0o_Zci;{1Z3D_b_o9PG9xlGWLpjD +7G7_I3JHPWq0lXe3hNt37vn3j5zv^0}+X?a`b3~;>?Hz*>Kh=pHczzNKVIhpC+?;s;DtE@ElRb0rj!! +7t5kXoq7yrHp@dMgmcP}$71c-^+eVBFMDEOKF)%miE*&B=FPI?5F!Wilu9#1t0-lxH_t9Q!LK<2nl=ihQK?oI;yAsiAmN@b{8~qMw$@b7s613hA+3wrj-mw8V1|w#l*JUDF +&(XMz*71d+i_0VjkxJp6I-s)46aHc4&EQ}jIjAS1_oHZToOE&RI5r&$|8t4<4Y@(%w;y3l`Q$N +`IOWaiF0!dr5aq~#=;Q7b6}Ok*h;?u&ebUG*tybhqbB{5^(@8tsnZNQV7s}5T +4-2m54`Ao-Q`iX>PJmiXYIV6N7xE$p#f$C>kAuhqv6QbXQ}bk6A+$zHP{{lM&(;-t8@vtLG}dD%O1)k +%dHz0fg5HG`awUqT*0}ZTqxifq<084+}iUh)8jV%K1*_w_My;(I$xh$N4Bq%?&@ME>AJ2i^JKOxqGG( +78{qweAbvQYi?QL(Kb)k`lls#+e)Z*>0G>a8NlV`3#podRh0|Pt>j{Dl%XARK+EVJO3*+s2>(0P0${g +&QAvQ5{`9%7vN0(=?7AU4gwMi*nKG5m`Wm4c +0;P2=K?+@{L5i{eG3fSwTNb1$g3^+e{+S$#w9CJG8dk25+85{(~ONZGtFW&eF#oJjQsYfrp6assCmZl +3Ae_&D9fj$u9pZ3=O)KywKC^4`N<&Yee#Cn4-*O02@E +;D<9Doms-u@-?R#e!Gp*f{y@yd9y-bOAf&=@AWMa?$k*45hX952Rt(zPK!)A5AM@u%?i-*JbHXPi?63p1!i)53kU8`G?OWaX8YDu0H0 +o;pgpNV{{t2<8sGjaP|WB?u8F9v6-i%x#GD?=D3zmV>wG?Ow_T;~`LqT=9&wts*wjb;VPO}BrUsO130F@E5_Y# +RubH8cOj>V^SpXTWzQB+tbDW?lUGIsye2tMUg8TvpPxk2 +0MzQG-y)g6Y&m{w%Qp25-xBA%J^^orcIJ!I;%$2}h$4JM?DcIZv?FJSTx&;bw8x)lF{{_;O +(R1jKsH{hs^oxj6srdSx)Q)2pIEK)`Ry#2HO2nH#V@;i20QCzE|IquUhClLz;ss{s-*P97 +2m&}a6CH~II6e^0z`*?f+{b$>|WZIRSg#oY~}y>r>(IQb!geS#fPQoG0Z)|Dxd$C?LS$>>_8NUJN^Uq +U#Y!~nqqnv2W&;j5|~kmX~v48&u#kV>Nbtojl|Ab>Gwr~`gHFai$pEs-7Lw|A9|}%U +|X&RNcNcK|B_&zayTEtxljR5qj#r7bEiGQ2xU+hVDl+-^#FU(D9sDNiyy>uNl(Hqw0`0JP>h! +wYWWAMDJ;%azc}N;$n@3iTm;hyqw6ljZHQJ>TDIhiC9n7K +P*<=>>kY4&XjHgz5Yz-(`9Y7SQQHvc@M1avm9?;l5Rr$mD@!EClnKo7>0-lzd9m!uRD5dA~TM(>G=?) +=;M)&>X(UGiT5hn93!80IB+BeG%XpnR}j6oU0yZj=Z`tD9Xru8*Vum5d}LyAFb?%kvfJE0yL;6h)3^ct1^37QS{7$P4A>9Sh#d^T&iWVfc(YJ#oY7hc7k)s +mI(WZ(|o{HfUZUWBsqS?t@5cr#d&TT|k<3jUnF7i&u130k-$n5hmc?T6P<$ug8RQ1rfFn{G8tc&llL)8A+IZq#YHlkM +^Vajr^qEM?4Dc*v8MC~Cwsd0dt-@O)iT~#c({2`1OaD*tOLKLb4Gxp=?2w=Ug^9uEd9hT$(+J+0jiY< +45?t#J6(dzSPGyphr+RWxD82tDbp@$rK*>^CgSi&QDuqf~+idl9MW?k>}Rslzug=9kGz0_|@?%Kx|`n>C7sBCjhvS8W@Joisf69GWu|A&3Mq?#}lRU!2t?|<4cUe+JbF-6wGEq+<5BCwa_m8f)0fc)u2Vi)NfusRWAP@?)|rRuedQ!$vE>>glamE#3;yyWY%-w(FHV86d&S*8N%bGMzjmebjc*K_ +9Ef5vjk?hsFf18Oqf;Dgp^Q0Qc!|1v?mVU4;m1X&pzbR(5bZhAva{2R@g$10`le)*vdMp`=nVezzNyN7y@yo(T~6JzDu-#Zz_J-e)CN~Qyll#8$nCk4R2io} +7kk8>cxqf1JxUI~|EB2RxnI}CE#Xnw)udRB<}t`&{=ixk<<-RPi)O7 +7IUz{!l-b)=nZ70hI5R!&h_Rp0%`pFr{i;VAc!V~WZt*|b5wG*8JXoBA>mu}Y7)mk%8`8e-UYDolyNv +_UAbi*H1X;(09WYy|^|N4&$MC9WF7xwK90_yfH9^hvO2Kajwt1dGm{Yy>$_!qiatpI>%3R$Q#muLv*mFHh(-v&>lSx(xK%*KLjU%i`^f@2MzJx=07BxQhPbpE;D&PT<)U<28 +7;%!8IFan-?&-~03yD_^` +UUY-r`7B8pXPY<~R=pUbzTu+1r_isBW!2M^%<1#ae_7;=V!jyz +y~Avvwvd`@>HUAX*co7Q1+@Y52L&z4ysWQSiN=!ccT|i>6moL`UB2xlH7DyyHFxeE++L^Fxk&oj3P-2 +43#9EM8OuK>`&Jgwah=wXyHx27$IHrS1gTa-yG)2K=#HJv&RWfwaFoL;Bm>k91~S`UKveV%QO~ft50D4Y4X&{=*&KD!Buq*Nki22x`Tst25;i^II(kjU#T}S|$x;JPp;W +wFl5#|3XgX?DtbkL@h7!mK*~Ejt?(TIwRRMJbbOUYj0k$zSTFC%9VwjOOzhKx4wfkQI6AZxI88g^X7- +7oAYHpK23D}sn8QyV?zAod-S+ZCd;1O~oZO%Z@PcfZQ??9eL!_)iI8}=MrY5;5(J~lZUCXWtyfYRmLy +G#co?nuLEoFW;}KvNEALO&y6lLl~4SSE{S482-l0AgT4gHQBC98K?Q#%R;J|(P=XDW?+G7OMcwUR$Tm777=A(4SGra1)&P@lO2GS98w%zJn9}T +1>jt{bI)lgoujM)P4fL|;)K|~{4B20V~<-eNc2-6S5czc`U65|`hWLy)CJBm{OIR!TV@wQB-Cn^EezR +>|1*bybEciC$JCImH?G(fkS)T8|dr#MRH2^`3>pP+%Xyyt%(Lo?0Ce=xl9q92bWuu8TkEK%~;mFn*p;K{#go_+m +HBmi6``S&Tg>%M0icnZPdOUJ7+qwO$V<_T?W^>dL1UBz0*uA1@Rdibh0F1q#OOx$`?h+Y<>!fZRvOOI +FAb|WVqc8>nbJ@Et|%G4g25Qqf)tJQ4wyV>UJZP5wjfbAP3c(m#&&C3isWu`Bw0-i=x&^}Zvq$3g^uC +g2A3XyquSD8w2=81R%w^zd)yz1|eJMD-7_YNirH8yMc^-wfi)Qo5F84sQrK*#Uk +d6s~VB5hlje(jA+l;Tmw!l7B|1Bk_%Jb;s|ft-{)9*o7{(eO?^SgH&Mw8=x*Kw?TxG4?lToyOU54aZT +mCY0|WPw!?cUkTiyh^8)$?zm&dVJo2t182vG{wEOT!anIYUl`Xm +kVlIeD7G-?4UKMqWBuW{c~H0T0kSe@#OY-Rw?zXdnk?*k059!@JL;OQ-u0(a>oH(De{U}JndU&Fu_+FU2bOpq^`%MOy^mC^hB&0~ +^9sSDlD;A@y!PT5fU#f~kCAjCx_${&8xbn(V-OnUs?R&x>GlIG`P5Tt&vhL3@C~hlH4{?LzHh2)Kt=X_S8KCrv+IfCx;PrTzU=@wA6}F$FokoNlrmf +`m$MX}Nm3>ncnU37>gTp);>VsUBNE{EKU|`vp_>FNAe#g;Nm)%@#qMj$sAnZnUs%4?Gty{^s8KCNvVl +~VRW|)*1ZT|(zE&i#tnK0sP(%1>?KOEzB+xnl2DY0=K3$nVb6FHm=2O&wz5iJJ#C^sw3=gY#pPN0=<^ +4Wcyga03`F59$9PsXtStFT{fvSE&>r61i?Yk``*^CZwV=v1T8-Ap-e +_I%n&Rz95TjAJ}xni!iSRhwPLCG)F@&JHJ_%s{7+lN3bY*gBZS`FFmV6O&gHO2Ak<8sVW|I9Cht6R?J +AkNju&g{nqvRe8)qKu5Z2Yir5ld|K+iGlQLU8N^+{ZMopoCvYz0Umzb9Zrx1HBzG!Cfcd*i96zAfct-?9HoPHvKF(+WCDECWdx)H7g_D& +rk>1A2SK`%W0nr2S}Co+OCV}%JuE%k!{Lmza6~Fy0ZM!@A3~xt|M%Fzr^x9T}rEf*!tQsgJk$K`d4OM;y*n= +TXbnV5M8n;jn4?$E_B5SU0=%tP!By6?%m-kXT^mI>_0#2iFmKXDkexf5~}V>u_9^WAk!CGQzgBpiz4| +TI{Ht?q9-%{#C7&gb#*JThVopiJBbC~cBM5Li(}N@)!!35G`l;fV{2gX8E$r%!XR$&`r*hHo;c+KSJ( +T5n6=b77bQDYem#gvQ*r*H6_)^Jr|Hx;aJ3%zm+BYYO2$s7AG!L8sQ(rK570I;KqVZ=+}2cu(w +U=3T~z_@b!i@vuH-rGG=(vJnS%A4EoqZ9V9oWTQk*)z$Wgd|#7v#yy9S;{!r<)$VmIHV<2#|?AdAo*M +MeiW!aINm`c;|g@x1)r?VMm +VOqdSxk#iZG7b;*T7VeOIrv;mlE)%Xud!L50UjdE_@tx-EycC|*L0RFF>M1$LNs7r6|bK>Q{AvVBmd$ +3Ii$R>?wO$XtMVaskuJU3GwPm+nCh|lGcZjrI&8gm&&BVH9-d6ETLQN8E!VY3H+p1r3`AqW>zhqxnpD +dI+Z4quXb|finUc`03mS#LA;8jw!(q&tYxYA&EC)c6m^lF(j~G29DpAk@ovNt@@}8QRHWL^bqPn`(%d +h5e{WUN7S<%daSGgWmLN*3L&*dgd@^7YfyoZRG1V@GZ<9uDvEH=Optj5!{R0~`KUfmh2Cdo^>SUO1?Z +uql!OAR+L>$9k@ZX-7{)7ZentycWwY4#$($D&LVCt!mOcccEz7gLjVH#&954Yt9C^jk{DGLHkDO{#|4 +xWaupxx@A#cf}$@;1Ti=;!TkY(i(e8)`l7~WYJ2?*FplZv_jY81|CbE&`mRoMqS+$v{Lde5|O2o#5@4 +U^NLqDeHQV5tTM*CsBJ_wXYAUbg4A7E{3`{$N5Io)fmBb^6f-jBs?>}s%_w_Iy`GjxC-QGbgUfsW?&> +-m3gEO^TXcy3d=qlNjs7`sb**-j_^ng|c2|~6R&2pR^4vvWbMZj8u*5vAX$Egbq;;US?uvLurQOIE33 +)sJiY#c~IW#Hnuqg_84R|9YaT@5xq90+eOw)(r$1qtq-~pncJ#qNoZ~yx1d=?n9n +BA<#X>Py?p{28g0^GZ~2KB*U=O$vg%;3XVOYzoMx9V<~q|=tgF0kWMxY__#Fr{^kkJT2H+@XF$B+Bmd +8~M$Pn3q|XMQ+l?2zYB*Vrxp-ZwUj=wr9j1x~^9v6K8LxE&R53$*k~a%R+I60`L$CH-q06WCMGDT6;r +lm&3j%zb$%jR21drSqN)sT-+t)GEX(|6e<@wsT)~-k30^7XvF;|XeG|H13&?B?*H&{l`p0VOGi>~!WZ +oZ4q0UWMcg>@Xos@Cq-nFIq6)0LNtmkeWm%`DH-@J)OAPP`W!pSGesIy?nYVs0OlQmVIZ?pVNDt-3We +0aFZXh2pZ!t3rZ{QP9jfW6;2GQ6aC@G%exfm7eNL*8BlBl#uLYx|L+p-QMzuWfs +e*pVGTh#T|S#S2{fQQJA4Fyy9XV6*j1O(8-{9{w(f=Hww?sFUll8qX6hiqXS#ss!6`dBQeN^7wv+6e6 +C#AzUJB=-hZrhK6pWQHo((6Ib4kJkc^vU=%&IrJ%97uJMk8$5s+p1s1l8Kw?+Z=m5y_=_3*^Ca~?CrC +}|Yk6M!KD--@`u?Zu+uq>baT4mQ9$oJHqwB$~2|!+7ekFIZd9~rWu$HSl@vtr&7o!(O&OLfrBj;wa>g +hE^Ci>st6bM_w59((n)sAtXht&`C%)n4?miCL6X_g9Lix)9>@x0RRyhdEPPa#q|A5UD!7OCdCwqQMEV +50nCNBC)jn@=`xAA!r_H@7BRv4m-sj&-C^0T(xXN6Onk63a90m`o<$bz@%xXp}FG6Z=#OvNDPm)URoh +m9&f6omF47dnHXrAKrapgH{b*g}9v5n2NeYBX9w3|#^<64;BVM(kv)a0dG`s%g}oAu4R*AOL#n_D9`kV +=d*80~lQf&h1*AWz|A@k0WWdeQ4#bi>0||`}YYD;EP<@a(7F$PiR|?sVQvw=?rCL +Xgf0R2}FBQg|`?8|7I1707SVhjBopUR;D^;uYg_y3-7}%sKvW)*NhC|+&pj8Xf$AtuAELkE00#A?^HTIjs!65W~oo%SVw%& +jgl%xrG|I1%wa)9~jBAe~-rP)EszO#eu5gOX6a4&QTTY +og1hX+4{)}SOD0qiem37)#0Kei=GVRt%?J4jB*!%C){40FsZu#_rhZNyl7nWn1fhny!gQPATZl8QNI8 +DI(^>q(@qZb*ggAmL%>D8C_ycHe!=zv2p@3AwA{a#di{8d{hZu*7S2C;mmyt+v!;pyf|wJX)--()?*@ +fJaE#_V6Z7#2@y|rxb|PcmFwi&{ft7*qQ@%^$*<7NG=rI_)(I+6)R-2eJm^xu`M+Z^NG_OqORyIA3r3 +|00D|*T|WtF^DjG8)kF(05tFvvb0n7G1tySyVrw{jvXX=te+pJqd=51)S^v^N3@SgT3X +;CvTNy-OV@?Zhfa9kro}2KatGj>%8|PmyPBIfGqCyS4Bn>&m0<QE268uktSoBsnL&C1fa86Gq4&lJ*6UdWYCC>n_g@Szr3e6LD!cu$N22%wyB< +#z+J9rl|)taKmi!o&Px;+-mKiz^=xV;!nBcJNQa)z2AWrw7^63V++ufCgeK;D3hoerJ%nnrYHUaM{dr2lPfzC21!PQ;H<%UHR@p +ZpUX3<-o%q{g1A-Thllxs^lE?!_&s01Vp42g;2m{ZBZ9prZ9{kz@QIF`V603i|1J~z1Ufl5LJ`(>bjr +_ue#p}WCAgmII3?)uJYz=rb7>0w&pbCZB6NhK4CQpO(Gtr;!=4%pKQ1#9mvv$Ph8YV!Nl_<6NZ>rw-| +N@;j}(XGrL-vPZN3+_Jk{l6rT!_Q@*`tuvux63Im59Y?vS2s2w894oL-Vm<3>V(}i1@}_#Im334&r)Q +%O*72=zAi?ilr@x4S3S7ARY4+AW%nrS2qmMx!4pO^1+BFds~k47t77GV2WXLc%fh>yt;WUukM}vKG*? +^nQ}BOe3*$Q@u%0G?g^L~JsN@|QNByr7$Vb)@Km6bo+7V>ahfoZV5HGd~i{-*vO9>>-h0{n^DrXWnOX0rU`_j&+$9lXQ`dC9t;;i{r)ttFtH0i-&&xjWZF*J-|)UI1` +MNcj;5IG~GWeSbI7Ru(~^k=BLnb{xJV}>)%PR#`o4iwmx2HhJ6as$26b7iV|RWe?!BY!oziNnk%+$5# +h6AYzvs8?pH~kU=BKEq&L7U>#dQ3j?)QM;@LmcD{_nJIeXjyJ)jqYsG4}$jr40e2KbVV}5*H4$RI +YJT;2g>EUTp^mm7kuR^_dj@e9Bek^U`~=ttVW!HxcSx?eMiyC))0ZU};ST6*L3wy@L>KMGaUK>B +Jub7JM7c}yue2%`kzr3gQn}&(gO5@}0(UY8OPqxI*i<4iTa8i0cY|N1}PH6p&% +~chYN2p2gywy-QcuX*KMR?OpgssLbY>P#(LU(Mca(Y=Tef!wAmmvm~GVPE*u`ziu?_`}ZRW%#wwn+!X +%BHl;KlPuBYiHs~~YASnZw_F+l~1>-d*D0*v#rlwMr#!s_1bPBOYF4X86C3y!~+yv1!3Kx-V( +oP_#AaP($u^r}^TYJ>9+*dyG|E&ixqG)WgT|37tW^_6OO$-iGZoz`4GOGY3q^2}aOJv62v&2FX0chMBSAPX_s3kcm_xJ)@z +97B3345VYCImiQrM)!!eOugu62x`7Mk^4mn0241_ovlW?l5r6Ly|myoPjB0DGvd-F*cS13+_L%+^s$U+q|MJN#sm4y;Jmo#@7sZ@%5 +b>Z396!N&aIsSAk6ztyEWCymRM^^j(LyAH$bqoO&F%aW?kTAKc##Ki$%DF~DP%+Mc3GGhgU_F&S+)S@ +Mt>;1QasV6C$4`lrLwkJ^jVlElzI=gE(GI?0j+EWw{AfII_NR!d7_Xdlk4FDuyc;Qcw^sn$m{np4%2d +wZ-sOB|F7Dqy)yS7qTYSzlv_yCPXPtu+SMy?z%IXsk8%F!_=MTnKNh$Md{e|5J25=nsPHn|P*xM~G87 +Tm2A;Q^RYKF2E{%$?|l_0qh}9@Vsz8{q)hj%;?B2I3yY$tFdF@>lhaK8<{YeXN|x!2;CIqZ(k-^P81y +W0ms6JzR!2m>d9T#)V20jPY!@#|Fp`so{?Y<(R5|JRK(tx7-PY^#2A!ppIx-ie>F*=Epz+g+2UE3ne4A8=|4XFvZTIQ$?g_`b%5ncsquF2ynkESBQFBYrzHlvmIv|ZTR6DAjl+= +wo%4)k^Q-EtAydcc;5N*D#->pm +=4w%uP=IZHQlmdb|+9!1i`iuEL}>IR!C@r9n)?SSGr_1CJ0rOt2c!4YmvjB&m;JEl44*GYW4Dbkz;(J}**+GKE&YmnB8YX +kn`pJ*Zr$rKFNR6z4M<{z(fu3|hA>?4>B^hP9c*_OQqj<=p8r4My*&FNKy=to`mfRlqGK}WkQiF8%oa +IVqYp|xlnIO+5__$;`oO;ttR^G=Y58Ym6CnHk`p +DMlL6%vX3b5V}AH;vb?Uo?)aqXAJKZBRbAyJR`N&Nc85t-YFm_wLeUJYAM)>iMflMn^8cAu>~=G<_MS +iP|L*cz|SygT959Fo-JMeM{6w9hwT^w*^b+A#K}9h*NFsGAv;zKY)Si2F-Gl!tRl4;3>4tb&k}OOf@e +?W%Br_bM6eFahMpzG653ZNZ0|f3jL#fseT|VluGX?5S(oHX*y2k>OjrHBXm`{NbK!~qEAbuzZqWmQHA +eK9s~|zaRaGXWG?S+z0yHK#<0TSF{p`5%DuzSg*#{lma-KGk6+$v +s6IpS+#`)fgr>iW_fW}W~j2q1Uy2@m}U+^5aYFYzsw`nL2}vS7)X)!M-;>yxOoS?3QhZHc*KjpbE(&#LLEY8E#jHRLARWHcA7`}G ++7ppS$;>l`j9V~q!;kjr`bp@x=7bLw1rjviWIx)!m&>x1sBLRk%i3IP@W0q$>$X98A@?4_ZZ^nLWUHA +)qP?F=KpOnw(QUgyRe1gB3ly~pLr=kBl?yBmB8=_X}mkp7yDGHQ6J;F~mi7Xa~YAMVj>K&W#hsi5U_z +OA#iu^YHo<11h5qc-s5ie@rnht%T)2~(;W@lX{#bS!UBX$UaDdjG$FAx220uh|s&!YeW{A3+YVg05`R +T;2746n;sU=Hqfze%!kqEnC+@HEO^VS$lEC_KmqH1;GJPTWB!LsQFafq4v}==JqymXxau`R%jBIw{me +PhHJ-wL7F4Bhlx*ib@P+ImPh#?JD{HoKl9a0YaG4MCr*!V2_I5K_m&3_r_8V)K!-bcW6yL%+6Ume)-F +U`8Zq7Vf?Bj#*d`^k3;JtMj$-KQMy_###52;i1OjFpLbLfjj#`?DvZvBL-W8bE~;0fqb3R7r4kM=g#? ++lnj%Tu{pn_O-S@XkB*v!3}y%shn3W3NbXF`0R-s?_<@ +%6a6qYlJ)TZ3=Y6&wtZW~E$d;3<^Z6E8X}i|;y3<@DWjoLTyFHc5-CLYcc3Y^$JHH$BItd{ +mlundKpB5nCI!~C1C#~ZOWt>#Guki~C#`D1JrM51eXequwY2XUA#n`QWET|E)=^Cbu9({G2%bv1J85i)dY +3(}%>w#PQr#s?}RV*fryW*oA4sQJWu}aWXz|&~qEl$hRLJ9d7jUx?95anGwb?x~S%?}GyJ6jqXi9R>^ +Gkj(KVv)`b@CZH3-eg!vX|p&GoM-zuZk0f=X0BtKNmU)M!KMus1O!>i&%qSI5PQiu<;%yHI3ynF^;H_@fqhi0}}yR#I%Hh(un;{v0b1;Ggy$k|!9=V@H +QP-dHpS=wX;2HW;uZCl;VxIiZdpssJeNttF@A)SsS`QxK>xUxqQ6lLf@Nu{_MM?bqUVg$)HX@Ca$%F4 +YAPzr#N~?a|TGa2}+=T|fRyCyy$i%n}^h0|kGiDcP%C1ZS1c1HE&RZqv_Zvqh1Iu)GQbJVJ98b1*Lvg +t^|oxxCJEqGcoCX=Fy$rH&XEB^mMtc*d-!-{f9GuP$`x=B$HW=dRZhg)WsIb6Yk=JDk+2Cj-nC +|7f>%WDZcTBfQ@9$h13MKg`XYGu0dJip0tL$E8bp)?lb&K`=5p_L2G7L>2BVkWRTUY5+ +^>@J&r*^DUA%@v47orj;pKrrqe7CG&OBBdd|2?u@rL|&240)eLyy}-LNd&MGa{w07`@9ksY?;xpo{Rr +|b5aZ=Oa4>jU73&SI)zz(rAk4vOqDQOpp#a@c3ZMsazjT=1B!;_%fVa-wqJ?&tPljE17c_ZA89D6_V4yQ%I!#e6VX$B-g6RY`FJk_Hvtzd +zjlr!21V4?;m*KA8xn}^Ir9D9ajT@?d1zf&<1zIqf8f^o@f1K4DGn#0D1FVkXaif|3EpEeD +3)vD<%gN?KB1W*|SYN#v3DU)$hmc>2TEf#o)ip+$At%@a48xi)uec>InO+)c-U$%dqG=U8eKQCbCHC; +&U1ax(GMXjDoP+uRUWqc&B=FJ>1CaSXKMkxdDHLb6HI&=G6U)~Ld5~vT4FHmij^fH**w;6u9d+ +j_=~(-S9sALcjxbg3$X?C>Msh(X09&-d$L4pPtv-I8S^gdS3)-`K+u?4Nefz5g^3o61+$22+qr^g#?m +eg+v4~=DxEDNpM5BSNwn@w#s8Qz!alV1WKeNDe?Gk44uP_=d#n@p;jusdXZBxy1pC?(>Z#hCbk;W!>g +XTyc**U&%Sb+gttu@j&39{Pu#zx(H&7MSH(q1!g2Fgi%CLkbii|AjwyByv10-EOm8}>z@X@&Sv$mHq@ +lb3;c$-viSicT2gV0LE|!`2l~Tk>T=;iYw+`i>xz|N^g>vJ4$#Mt0F +Lcxg`xDFlgi)zxXdmuT3b8*ya4xLy0uvWF(8Kt4vL}Zq#NGwG2q(x2?N9KB3{Z+rh*Ge2Bc|&FG!CNCKtsYLQ?Z*qygV!#Nk)A&)E){C3qVmtcyXI1W~41`IFGYobzSEA{ND9^5?pmG* +%&MYks+c}ZY8$r+%n0vH-M_r*j)q;hYz*V^a|SGP(2XJ&v$NI4p?%M9)^%%QrkF9rP#UIKGK5(=ik%b +v9rK8bTlU#ko9J{M@Wb08K22(^R57wmaM^0MyLspqYO_r!Bq-BC?tfpAZ>$QzF(5Q96ypTF~_eDSet% +PggVoV@Sr%c7X46A6@#I1fbH#lai+c_<YODbC4un>;54=r>%`7W{#ojQKJz9=%cV +{8s1*+J`=@P#DWQhsLsoDO)DvX?F7zB})TNZk~_|F!8XFcJh=O6$qmIe0r!yFTALq31900+7={&<4WN +}q=awyvXt>(rhL|*A>_rl|lluqonwU?DwGAQ{bO12(E$p-fgxiYe`pao=-@_5v-@6VCY8bT&+u0?@cn6x +N!97q)C;G&l&oA;s`8H*A{Fek=`N8NQIO2&!qP1s`p`Sokp}2;9bagH4_+S6|Kh2HCq +OkrhEolqQ$7#fY4%majyekVlgkr+tY^-PCA7sVm0VK~O3;S1|3^U^9mMvGyw0+iczyqXI=rA1zK*bg+2Z?d0D;+pa%&ivM*tLFM;`HU?e~JRbH*wFQ8Rv!)mkyf69T0_$0^a>qg6G%D^(foa +bQ}Pq^u)S^kigPrX(CYyr|yKd+``s%ooXu_LqoM>2h|sX)jNmJ?zq;ouKr$Tzwd-8!cfpX$b1#Y}TI5ge8vi{zh>(_$ +Wb<6Kqx~|tzlr*BbnW}M3V4J(|2FbQad>q*xEWl2b%(-cUHs~+A}>n4?I^Hu6KXbui{Y;f7M)|$*KtC ++x|Q1f)V#80Qv`;qyfP5w+On!Et=T%V(iueCqH~bX3w9CMrP2{pz`NV0!SJr2EIy<{J|rS{rk%d7X0V +7fSBCR^bR=;w9WpaH&>kJ&s~7~l~ar(nQr53kz^G0;^%_O#wQ6 +Ey(-Yc|MD&nN&V1Mk +c?O}C+FL4#TBuQ@2-6eT~ZHdQNSZe|A4&=IOsJw;Z(eqV)9!=L0#Sg)6Uvx&J0y^5L4b@irqT4uXA8m +C+@#$XHB`gdOfgZnvqrqE}OR)Y_L+R}-W^lgOpO*0)sEg~pCCv|nBVO}xr(P?*Xp2I^t0dPw05srv`^ +0+`BKzq`|y0z5SfK}faN|ds7)ohw+z_g>}pGj$9+G%T^B!m*Qui25W%EvwNf;YR +|Zz+lBOwmQ`M`iT#To1GS2-K1FY(tSi?;mNu^5AEL@dcmM{oD!ZexRa$sMrIYP9X8)|nBvra%%)Ou#{ +&Hs>KjYJq^y23wcnp6wG-*8tHVqk`;QPBp_uuzOeYEVH3E$8yeBHyB1lJ`8G*Z +Tsc73%lv@3W}pEBD>-2i)b#=%Ffna;VOecY#aI(6|24l}nwh*z22_Zds^ecv1LR`91w-XE6uxR!} +05gR1iYHHHq7xYf82Wm2aB#y}_?c&dL9dGL@769_wZElnQ;4LwJdZA(Ox_-|+i2u~he&_y-MTP@`f>#l$nkl*jWGru3w;__6kGj +oZhy#862OEwa&RO*K9HL-!)U04UbX(#;aem%zHO||E94r|FJVIGvUqI_3VEQoC%D6webi +hMo3_)b7b5YT`;uuYTXAq#V%6}{&$D@AfJbPy@V;l +uDcD{Vc3zssT4oAcg)mGq(AqlUZQr!Ix{J}owAC;Zy0#zGtUY=21hB7}2HOgRVVXXVrfCT`tbnJH8*8 +f$C~0T0>L`?|`X(@ljN?_YzhLb&~lcaR5QnYHN@H>3gq`wc$Nlugf^7#^FQdjz8$$`^V +GnNvh@e|>1Wtdsq*?w15Y6;LNMc95d!fae?!0J%#a}~L`(A9G@Gun-H*gpC49~<gqcyP(HV|>*hl{;SGG-Lj)e}iwNlq<}I^%s@_52UV@m)>c4N(6qUu(Rs82C3p +_;o)IOii>c`%gc@_Y;AyxrC&e9(+jT&NrRWLNxs==@<$GWZbkbq~JOb@e1ywUq&My=(B%VPZOI_SBS4 +X%i3?$B*{<=E7@7+|tA)KIGthaZQ=>qfx(;8c!-8f~>=-~Y>1bs?SZ`iJD#L{qqfPhcQ-9GfB@mqZQG +$8`Q;d_V@M-vR^uv#QivAlAHyc>T-m*T248_pUAQ2ge)wI{eQv++90c06zlq46X5-9hmua4@OeDhP4XDdoHy7 +U*$>LRxxxRmN;vb0e1$&o1QDjwuwSC+VgzGzAU>8FUXiy|lpm)B#NF19EN&xqJB3g2pEd|g+MEU_31G +Z%@fwh*o`aJ|^7twj8oJNp{MZ#>FClahZz@R3W2>tP@w8u08k^oAJ@G!+b>kfvClPH`Ne`u3R0cK#CB +S=seEi^7+&BDgGw$yb{Czm2Ei(u6=7q@-wI7j8eUl5b(jNDJC5`IWv-gj5r==}=rE!9p +;1zbdmc?APskkemgW%07qEfNf%akAPE_1h~JJs28q0{8&b=aG2n+_ff6bZ62K35t0%qW5WdJ-F5BmKQn}&_U!dArTy(RXK!(XCgoKfS-mE!E +w49L^(@-VOP+#?(q9dGLK&MKRQxfL&BUtC3PB1F7$TaX2VrSHFt@qRcPc|0 +2`7Yxqlst{fIfWto+thff5S_qU*$~3kzbZPrIE#5CwM^3cbX+{;nE>xY-_e<{tS@7~my~nmx&`1Nl26 +l|MUmCGcVtmeR1?81MQVzQBY!dsrwZ|$DgGLxmiYGnRK(Pjn#Sl))YWZOMeq@&vjSOpX~wQF&+n1sh6 +6o@2duBlr3Ax7kK*Uh(dBJC3JS`GV3i1$Cve$Q*SCFA$RB%n&?SweIlwDA9i0vwpJT968fG{Z`&7bHaO +{vo-`Aj7Lmx7W+}TEdt;h=&7Y1b0V1e$ym2gk$hn{H=rhy*96IGSzseKe{rE8nwN2huYUQ%}5AM$Me)WZu*0rY%PN4C +cV;za^<&t+^adHq0Y1r%%uPx01s9}aB_ztF+fD2ld4?iu;c(;58$13dF{xQV7jxtWjQ +P*k}=wp?1^AyOW<_t93ILw4BMwffcQUhT-=8Oap%EdM7gjeb(VZsEX28(T^9j8i`x^nIQDqJT|>VO8j +V=zKuhXN5J}PoI)UJgW)7mtrgu(!RmRmRu!^sRZ`1$o9?$tJWTN_7ohn70!7|*l!-}@zesrMnm;l8mb +@6LG1TkCm{QHYju&z2EzH$u<|xhy(8fJ;~pWb2pdaG@V=EA=1OTox{{mO~wTe-6~Kr5d7dfj +Y~D0=R8(E{o@p&@W0($vR3FfmqL4d*vRPeyPkz7MD5N=#qqKe**-&2NpQ2^fO-ZLibWpS;Ql%snAJ>R +$G6-Q>?~W1Of@iUAGR*e&x99u=P?$C*F>!BQx3mst_TV&*mMk1qN3;~K~9JlwJ`uVK_uSoZ|_h4+8)s +B@%K&Fdw|^zr88ZJt(Cl0nuF;*SLYZJ&97y^k;l>=P +I!P`(u_ZS%FTnmV27YEcF3j`Tkkx@P2mhz~-|VAHsU)9hzbFcKQ#&QVu42W#9UF2(hYEVL9kcO4MEQ$ +ZtZXK*>KtGCr>WP%oyEBxxFRX}st{IAc}O$~dz0~->4tgK7>TFsKBwO|RTr{?srlCarbm$T$2tc$foW +qoz4GFFq=oUY5qB>$m8B-f68fnD9fY9$_mBjPS*6Z6`fdlRqnx9M8Sah;&1vKeAGA6fe|0G5|FaD#2i +SL{mm3Jpa44IIg=kW2m=PiJYiSWMGwE&yizfWA}N>{qalphFgJ_j@*eo}twzzXzKiR*Z_EgcpWC)dIo +#!xun9B&sC2$=o2|xjYVJ`5}?I=oUJh7T*)>%aa-4Da0*Fkm12}zkla{G%A_^Zx(0otxg#MmdpG> +TFa1!{JZmiWOpB})nT^f~2z>Ttc<34#?Jf(A`Ff|bRGRY@fmK#lE)kiSAyZ>Z}#sPSU*l!QIPD@~M!- +Cg8EX3zUgI$01)dbG6C#PgNQbOf-jfN8XU2;sjSZ9nXP!Zw_m*(b(Yb(-8Y +zkUfEbx?gN-Gpgme!3__y)C<;v55w>21X)(J5^RX}J3Eb|reMK5z$65TPJG-k4@7z0YUP}GoT4R6^)J +a|V%r2D*jA9OM5SNy2`eipgWZ4=LZHV$(`!A}67hjAMLC;{6+^HCLbJAojeS_Euaax9ch;kB9NItDJJ +D!;X~_PkRfuCyx-0gLLtedX`V*FEf@BA`>5rua9wJSRVq^lhY4lUf6H%>3G;oba?6V*lyLpV@05mGGA +VvdEAvxUcB7Qk9R^@sZ9CdZO1w?byU5k$l8z3sLn+3!B8#mT&AcK57OI}r`n*rk3rN+BGv_=a*c}Tpg)nw+Tu)E6O{_}M(us=~NK6Q1^Pi*rVGTeK9EXu+G)BMGbx7)=i&A-Ez9OqA2Zh%J +!4kDD1uY0(gkDbKr&hah7Jj}Z^e*(RyfTz%wLMl6j?~3qVGk9F4U{?V#)b1tmO@F|;Hl6kdr{3ZvaU( +-ko{|2@3caE=uy!1cwVOqhBF`SIgtspl1IShHLOAT%+3g|xWKY2Cec{z@{v4QqmjQoCW)qulOJF@K0c +GIrav%PWWtqUd6ZH;J?2UNfDl>PW3E +duVUH+igm?)R>iSi_r;P7W<@c7NEWF8ZdoFEFgMEP=~JT9aTVanxxRXlk%-&v4=F1dzQy|ErHaFIK;v +TJ|K3S&poVk5{AKY}z>%LCaA{!Laq)YBkLuWk*b_$3{Y_LR&xYL7yYUF(z=rG!@@okzIA3io`j;#XHw +pn0;3f0its@lcIGf`p%U=@Re^mk7jdjb$qb6|Pu!0(R=fmu+u)ssKEVjm!)Eutk9Gzs=w`_HFa|+*5e +jIAPI;&euzZ=$ZJD<7gi12p(z=rEAmrv4}PP(^%Egm)AnlOJ|`?vl0dU)mEUIO@U{;dblBTWDF<@!Fp +iHAeR5PG`d*I0wbk0WbJAP|Oy*^QNq>qeL4ctm5wB579GNedV(-%ayD(X6_1j`>Xj!IAbOTc!V} +RbZf-it{$^lvCfUHu5RZgU7yZ(Ce>`QG~NUa+_1pi>~}D&lGSV}fq0jpbNy!dbFV1JDQ~ezS5N5j9s* +T6qib3>y2W*xVBQu2TD6HzAHZFR_vgS*g5N_9+;(d7&x5fV})packom|yVTTJ;$9^Pd22?o0T?;R +JZB@_Tp-|Zw{}|~3LG`C<&NefbXmfzmzUEg!QGHAC2$m?JRd0Mv6R`pu__qPI+Pny*-=4&=8?xk#4XE +wnkwosJZv!0+%3u^i|6F!d%VPAcip8FpuBXy;*O@Nz6Dr|;SfomQO4ZFZ(!}p?ynZuw-K;p&+VZv7)Z +PJ)~O3ZsfZZ|wRP(5Da(r6qlCg}%1#`m%LiTNsDOPu-3}+~%(G-Ah|DlVpnL4hnyRWSjj!olre8N;E +!2b6R2J!4KokrZ>=P9;sET+BuOCl40~~SoEn*Qm=yGC<{TpjHV(mH36D+}aNFL(qmICMY?mm!tXp(zV +WTJt5@06AI2*&9pd~Tzs*a8O-+~IV!MnEj({ln4|K$234)ISn>Emr6iJ{CrSwkNW@sx@hyhbpD%1G`V ++`67`-*$Oz8HoHb{>#>@Y@$444Ulr1MW`I5iaNdWNb1@QvZC)DP;0*3{&RV@g{UW_CiqkYE75>5ERT=xkFVw0h +(PruX)h0H`pf_h>Z2ZU*c<4#ID2w4QL0qzJfHeJussUhE2;Lvdq8lcH~?j;n*33nvjJI+x90%|oY+_i +1Pu$_rC7+CO;=!N7q|o_6m0Vd_j}AX5>Ah~OiNUNB`JQmsxnNq`N%aOn57rhJHPupx-`x!Fdn~)t}X- +^{S}7$?fSYE45x|sU-5uXG6ux3K|MwlIrOyG$N=gOS$SbS=X8I_YP)vc(U5UV{0*Bz-&^yL*3$h!iZW +d`>S}`GKFhV+Yr6FV#=|MdQ4EmeC~MnRKVs}6bzWV>YF@6B{3V~yuJUpjYv2epS2YDzksF-yJ|cB3A( +U(W>_LiVG+q{enS>1lG<{>8+eL`CSRAPCA8j#RbAJgA%5*q!bKse25s*J}2~Sm?dIR2vVqfCypv$>Fz +Z1e|qmSNyKibl+TfT)J>mI$obVE@T&Q1sH*^<%J_*l!43iwO=pY ++w<|KixB>;fg_M$Ex8W;$^G5!rSFVoqLgVIA*bs14m*Aa9xJ``^V00>3LO8_W?~I|)~@xZLjZ%2wT$? +j%yZ3&YysPTh9@jxX8gQ@|FnF{~aj;AdD=}cVdfL^<)@YYQET}%z(jJDat?oVr5{CEMjdTW(7>2d0yoM38-oXa?vr3mK2YTk{ETYEE +g?m5ZVC{TxPtLs4!(O!8Vyqsu)fczT+&Q}zHw^V!Kb=b8_87y&;Y{{8SO2U{2}nj(rv&g$r=;H$h_c3 +E;|T|B0lS=0J*t+mYB6uX`kUk&MVNo9m6~5lecGt2TY|CfknJUn7Q>LK+HSUtLA_havQ7=Q{aKee7~G +_wp?V`Gj;K-n<8Xfd))c +?FnktlW!s?AsdM$oW(xOZcA&XkqaGB=ww;w`}bV8NbX7 +CRTqDXMa@s;}_t)oLbsenxr1ilkXxskpbAQ^noz5QYKjcsa?wU^7t_ru)X$7irbPE3c@wXX(!bJF9ia +wrfq>Pf$gYd!G@d6D3f9tC&ClcshF{Y*2Z9Slj_dW9xq**M@?a^jSr>Vi@a;y7+iC=A8Q9u#CI!e$(? +rQoXW;(uuy?{52)Unc+zNK3*R-q=QO)g*>v}!*(D%O89&@N|uc(EAlj&{tsal!XK6l9O-&QdHw?hj8u&>T~=BK>6R>%2Y|M5Tda~LoMq|qZBuyE;hGMh>FF$d<640wOKCVz= +@d!7e3=`4xewD9303LJqPfJc6w%|z)sez#jzrN^oVcD_WPHwvH6h0cm5g?+wo^~hY_77_3R<{N0@+73 +?GQ{I!@7M!x^_q9&uBj9>IRU_S+$1Dt`E +y8qCs~Qrlh_WJ0m+aRbQmS$zLOe;hqV$SVPMpw9Iiz@xzmgP0-JG?DeHTn4zx +evRQjBar?R3F$W}rA~>{;x&yiC3v1020MGvnXj=cv2RpNWxOmRzdu6)&C84cTkwDcp;GN7+rON|cwPF +flYf7ZxAE^9h?YwKej!W7)6I8byfALyN;^*jY3CUOP7d$V0uoe*E~<2Hbt6QnCUl5(MT(|}j_2a<&*| +T{^%7ZL%)pkM;g=?WqtJ&>V@V88-b3HEZ%bbBfeRuE4Ytq_NYMc;lt&Vln{@glfFmeb5T-9%+Vq-PYy +m~`<8m@8@;`8MMrNVYSnrH#N~jJCb}$7bSS{f(a|v(&zEMCNfzpHA}5Z!Tvtewi;tlIhfT0ZgL1NG>%p0XfLNBxP!7Pb6?Y +e5ph_&^Dtb7%-lK8s=^=WX|&NE$3i-?8=#4ru5#=p9}>$0z!yxd}0CT(*Kq(m+`|&_?azG1L+~Lb3^o +472D3Ce*IZPrih2Zoh8UHfk3#r;}Z#)TjJY4zL^hF&VcDTaCK{?8B9P`1J$(#*Q{+}BM`cZKZqugIx~ +!!Q(>DeBDmo^yO|K++)PY5bJ$_!3QaW5u*)*Vg5p2_*JD!rNQ&p!rO0v54Hu{r3l$>lGanWFp+BlO

*xC}7dBgQJaGv^e>Qcv0B`zT(aX#@`ofTNJ%N1&g +s;Bh@XGY|7)BFdZjwoIO-0;B*@hepeU{8_hifH4uZSV7E1imyl2j($!8C&(9M1qJX?(SNjNS#gDZJw}!^YaP=mIj#cg{VN7a-xaY48RVFb341@ +W8@M5RDFGtBnSX^8G3dy}PdrkRLu^NiAOW{d+vm893Izc)$t`_q(W~Av0tz!!ZY(OdADUmdIPz6BzkC +>3EgJU(a>grU3j=vCNH#JGS=+)S`gVxMFj`^i5^gOJGxoMoSY71u{SA^<2ml;K2B{cG$+^8$*1d^(K +f?p6uhPQ5&1Nc0IAlaU2Q5&SCmP-cpT@W0M3nVvu)7zwu)1L)k9p&SH_A(jVs$?uz`37{_2`J6fsbZK +LEspq!?hhGU_Uvn`2s=dKDB!Ub%XPb+^cQco+K|iP^|&~S;oaS7BP9nuqT0G*t5fpf9)!t*_ +jpKH({J=UJ=jYLeV-)V7-;tDb9!(|JrT(gw1pxC1OkkrC8CV*ad@NG`0)j +*4y!@QcVH1!nLk5U(E*2;fX3ix%BIP`gfH)7NUqlO;qvF?emiTdTAHyCXfCI=8FhQ>4W$q--4g6?KzL +XfwPS?(4j*JU`5?&c)N&vb&$kA;=tDL>P>-4RzZfS~SW+4RekNG^2DS)IPQ(oRx@SvBAn668nVC-SP# +8QDHh0JPkW%Z~UpUh?}Oe1nA;1AlIBJOK4OUvKW`RtEW0Y@Scc~jv!EjFmdz^q4`qhk`@|E@Q50cbFa +K2&~_<}C5j0?%xj{H_4Y3)T|mNI_F-{E}e5T5>oFMr3h$vrVO^v7A?~9S2|sg94wx9yjnIA<2w5q%~k +a`7X59rWQ9R&$slir#P2&_4a4TtF+vHn^FtY4gG^s`h+ZP{`@t?SW`Z$*b_BZx8FkD)Qt|d9qF@#i)> +Np;z(OyhgWxRy5<-7%((ehVdJcHk(86fsGs7ZBd+pk{&}ot3ApKA+o99Jh9#6BWSlPY>?)q;X_!fHBo +HN~Q(|4>GrV7VMLK=vz&x{v6|vud5bSzg_kxh!Vz-Fj=c)ndVdF(@Q)xso~IExIfIv>X@uz*oD?Aw!CA^Z6?hS +N0722Qm$7OEe3gtkvu1b%msHw-^zQLs_^mdvBC1US)GHO_C>+m_lb#;T}F#h0vZd@1Y3pYgL#*6pXM; +tdd!wOPPvk|RMF1?2CxnY-7&yz%dDRkLCVY_nn`(>aSyYW}PqZyt%sW+ex@eb3lMa{UfcM|=I6v-1?) +iyR39O^U8|){CnfbDYEs{%Ld9e0#_U{`RhyA!8lf`IB76|F`dRX?x5Wx^K5{`;1$(VTF*8r(5y%I-U+ +PUk_-SgnMpG2?W%z&`fzo&uE9NMB`j~9@lI=b#=qOrl;u9a6ux&AFz%QI0iW%F0MYZ-xxAB0x!ubQx% +I~gA<^0%JxvVDAMO;&H))a44bdGPK)(N9gj`b3;^dJ&e)dGn7`Cw$>(5-ifH3C*q8<&35;;2ppO0V-J +cvphaD;%JXeTl^^z3UD`L8+*{fT!>cmX!N8AP+@H_4@b-9TJ0HPuF2^9rzng&}8IrGcs +|TJX6J(qtJ0XQzxwbv~QIA%Ol217!G^<`*9;QJV1>&QoMhga-|Jss%QRtIy4~IcRs`hO9+G-evtOVb$ +NnOH6^4z){Ff*Ye_Lpb0=KrA~vz)rg5$(>>S1sBZR5pd7n?8QN2SFH3L|G +@D5T%v%VMM?YZaPaloiUlu?Cp{$bkyh|=1oTR6^t#4G>SpClSMfHoa^QVkmw`D-c)e>%un-46dYLamM +vms17navwXLBSytaG#V; +ZO(^O`d6P-R(%F;K!bC`z`tM58+U?@$!3VtL0=ioQ`5cIF=oiHxu5q!`u%XBI}a1ChFO}#PnP8d1&{- +*%~H2l-Z5bm{Da`SDz1jXYXSylva067BwQ*Y8J;WyW3d%P;G$;Snv)cOw6@831AxCU#-T8~$$Db466e +#bUb +nI?VLx;HlSpf9-_mKfkuzJ!Xp5%&YAAjNe1mSluQ+k_S_8CD6ND4p_!(9l~i +LMDtRcg&#n;9o09YU^rDC_W2)sA=Bk=Lw^K{I9bU^6d(#$vYulvj5#vHx2dood6 +I>>y7erO)7YfW@h9DU&oOa%xahEKP$Fqg%TF2nLP;4vk-(9N`eH7wKC+)|YppYE7PC&hA+PSrzgIh%o +ULn{0nXyLDih8i(lcCM-A>;^@2qFDpTJoAG`_#YlUe`=kaNk8YNE?Xd2_n@c-qa9w=xIaTmeU*nKQS} +#wA@E)BJzT*;6(raFkCgG{!|9v)$#@)xG&|WXfU9^dULgWsd_+MeG==9*01N+z}q^CSyQOqPRvPtJ`Pk?CW7PfaiH9m`iPX{1KaR9Nh%$hQb3tFa-^-tPDa>lbNc{5rFOsT7eRvITdn;DaCh80Di~W%s$i(^TXC~2VyUc0j*&Can +bhG;fDMGfVMF%`-L29D}AtP9@70bos^6s_p2PD+zqJ64Mml6K$G7bfK7CG#YmBPgE8qy^+~*JZQf +lN<#;AFBi!23wUwyg~*kN6p2%XAAGzRQITey=J4@tZ*3G)bS!u-YxKsiI-`Y8US&;L(V;vz@cR>JTuO +wEN>i_Jy$8A!>Y)j8tnay7pOZOQ)^D;y=^3TBsoS7preQc6dPeiR0H)a$;NX!3FF3BTbQ(Vn%JcAG@LWr5cD|syBQXOj~>fXtn1Y=#TJv|1k7C$i=`gp++j)|W1)H@3jzozb&FGV>Y+M(9gfm@|tgxhib0$0R!~V;h9T$kU +}rKA5Wi(xO$MlQ7D#fV$5dHTbR#1^l%nWm$Ev;0;xz21J*-0!rq@{8c&LNW`KhTw*qBF3`3?r)$4iK! +Ojxh&S7d7Hl=7EcJ{cZYwe2)biT}%xZ(|{*RxX3*ka9o!}xKYe94RXEJ!491Y&iYa&cMf*%k3$bI{#h +O-4`oQv7`cjzN;?PYamSZUjou{8@yqa{!J+_|@fEQl_yLF$dqINWaMXFA1D4y2?rpY}-v#bwa&JG7B6 +)kLXvpPUB2;%UDmm(T_*WX3oMeE=u?@iZ@UJJI?aT!7=j$C(d&Uo1X=IBVm7{M>-o6`COK=DxeBm?O< +DdBzY)jSWs<%qsyzAzeMhR&g@?g|B4;=W^!q#Ge@Rh%~tRI(Rg*M^(<#FN9T2pp7Rv8_(iOMBa!i#2+ +@DSELPmD>DQ2>JyOAI4HgOX{w_!Qv|WU3c^a%}|I=WV#S2`RG_s^O9yy0SUt>}`Ex=fYpyW7npkf1RZ +=Oqq5P>6*iOjqY0Y0h^jk^3P76=rA2kfpO={+)Ls +G2Os>WG>bpdd!VPaCddxrr^>QrBZ{|v>|UGE9_(AXFnqxH?_fj!aT6nTBLn(afbrgGqwa_uq@|~6P+p +3?B?}Q}(7;g*c46yDsf8(mCdYb`sKMfe@g2_e59i{yB*rF}5;zhSwPL$4o!g)Y9LfA?;d%lpCPumNZW +Z8n+hf@Tmh&`TQO?tNm2NK%qj%e5p25*P&a|>Y0a^S#=JS9vu!t{UuZ7J>0!JX`n!g??N!I2LE2PKLF +t&hfcplHY#i~S|1tkE>iE^yaAUJG;^*9&SA+BSXB0QLA4#Wq{ak$@m1p>oCLjg)Yy?1eXjLPbuN8N>c&}Q*a0S?#0p4C0Q%lI|GxZTCs9`&aT!nQ9pOKwvv~nyPW>ZA4NnT9rJg;`Do0-A^+5a +<16FcSoxu~b}euaDNPIeLkteH(R@ter`CFmwWebG(15!uN!1%QjeM@XeFa@OephiJuC7-?DNlgkW910$92 +QbCzpkcW&M0ss(9-f0dFbh+9srnx=72m1cJ5@xfmVC{KCj+tuyR0@rVe}wzgh1#0QxaW7Ak*@3q3Bt% +A89Vz5?8HUn(g|U3ZntU(<{O{TX1p2DiS(;=3$w0^^OIO0O}1?NuN^PbeJokiU)N(gFvN9@=lzAh{zP +tOvTOeV^5T?EQ-UfW`4Y>y3i&3&V{%9X$Kd{pENh(Y1m3DNh$@#rq#K%YY}(mgP5Kjd+n{T2ZTba?-lWvp1;BycF +loA+7Bv{-_rpReE~k&96QwKUaW_Mz>H0flY$VKw^UodetZ3Q<~+x6ddFJ52wIDs;AtmoXg23OI<4FwD +Abd_-O%@%fScF`e9Ov`6Fzrndth +YC1MRDPP^=jJthPzddBl((0^;h@Bwg$MR7z&`$) +|%15u4ZFE|*@w^yHT{7X##JvhRP>N)lN1s-3OV=iD;;4iz974j&{@|QtOnRH!3*nCtqTFa3Dvrgpg9{sycA6 +ZM18LWroNX4*p{MXIi5Qol+E{X?E(6%ky*WTKOshQ_I7OG&WBAA&Qh+MfVU=T6zmn^wAf{9%8M9D`y4 +kU9Fp?1gm>gHK>iHP4*q@Rn}TKq-rL$jit-fhR)NpyjslvM +S=aXzrkaY4u>f$%wffcPO=ZmNUcHUFI)od_5w--V$a`2#I<`LIdm)K&{>PYQGqSn%Y6W?*Vf0Q?(jY< +5kRodW@$%mQ%Ysr~$(V%qVSrk>ywf=y_eHy6%3|+%wa}UK}7-H}!IgC5A3@KV3>7VziEr%iunpKFNww +68I%asQTggYbh}E*XLBANs{;*z7T&Dcj4Fg+E2eEvNx9S +ZC?qm|yhJQb@FH;@1P{}u{43MNFrX6Sw%75VubyUt%o;7FwP{!%N4QqZ&IDUXZTm#J3_q_6NfVE(=q@ +hOSOUr$xOs?eonf%7gPszr?|I)UGt<%T~8fgNQV(`5OxX+~lLiFkQ?f|8hpmHT4(=Z{o$OJYEerHTX0 +KYOb1w=WI&pv(b0iA2J8xosy6*~t`o=di$GGtVV(G&0*jcosx8Pl1SO*@yX%@Ka`42j|F3sY2Nju!Kk +*@)2_Gu(>)6Vzk5SeoeFmHas=|ZvwS?CD3d6PX3tIS!=!gP;D{lnB1IlbF731w77O9k_RM*lLG85-^s +)o6}xtHXoAiRoEnfEq?!c_A#>)hLH0f141%`FNFF*r5!*E1 +@t89YO>8NXWVPL69(%Pt`|eAGf;9qc%>`=RAB7%*PTq60IqCtv<5=(Nq6iF`!1(RiRn~))2Z0U5*g;h +X=keY_-BPtJG9HYC!JN+rXb%;~Vo>w$-ngCtg&A(kbPM9lP?^sIsQQ)VygR1K2P5Ko3RBBAT_W{W=_uI$|xf +WXh)4Yy+NYaJz%kb7|pi>rCpiB66pLKqV=?Rx@l9RxXn9t?|;J}Kr7yMWmIYx1G(GwDRXTn*(`})^Qz +!LAHeMv+w$u0N?`{N77DEKi`r*>-V=RxlV&qPl8wMYL|L-gD{J#zf1lOW=2;VP_D^*i0}YPG*fKy4bq +}yY+FjP1ju+MG$Pk!Zv<=)gx7=VT4ZeMg%W4+?nm^?RI0C7 +BbScwep7t6=T0(}VFxGXtcDgLf>MGBkV}05SpehR3QUxKO`r;C#%KZ@+uqE21YOl)Tu%or)dplc?dHS +2v!-^?r`PKN_?IiRiNrVD~GD7Ku9ZmrIwFR4=a!N>G@+l43qY&dBG1z0{=~K!8`Dp{j%ig$b17>g;4R +u&8mWGE;1Ntf9c*c2{<%(2Zb8*w)?V>bA^V^AbbW8bK^-2G-0K7~dTnxHE-pz3hAxu>bWN9fTm^vVdCpJ3NQ;I4_?NIEWkp +u{PFaJeak_Y!r{nm0!hjk{Gnj%TGiBT +oY-qtez)k|x_4E1mI;@IZ#Oe_^3Q50Px6K?KO+FW_-lr}p|57W>0$3GVEjO&+XN^qazl=^O!6phIAs1 +2h?7yla!A8ocff%qU+SKi`a*+MX9SBfPvd?KL40s0CzKL#+H5A(y(du&l15B29qV2c>jzlKR-W{%yfb +G`MyYATuVJ!zY^uX#!zkkAF+y1g@Vb?fU7S=qooO9i`4oHSVU5pTJ)CwUPgGb{>SyQAR%TPvJ +)>K$V+l%{gNWex+SJj2Aub^|_WF+3PHafJ0c%%9_;F92^gO=c1}3T@)c65`&V{tnmcF`@|BW)+l5&|n +mRWtbkJc9W8Lz;aV@keIDrqOuq|Jbl-%al-mFPGrAE*xH_wxUgOxq6WY;!Rg^XDG-lZjkK2@-{Qwf`c +NrbSORVt3lWbkLoY6hTqj>hutmoKLArM8+?{`R(&j;E*oSOC3Tgh!tD7sl#2hrAyRHq`3z*JWCyWXrl +&ZBZ3;Y1d%l(K|=)Q5FA4hl=hKH$3y@)jhZwr-4JX4&^Cx +dlqT?9`;o7|69GP4BvF!n+{XMSn$XVGRNUElo8&L1^{WER1Jduaoxh2~*B_pp^qV^yQOs>}wt+ya!Ql +kMhm@)+g#$^y2b%;SFK@B(kURRc*}8KX&ES1r!sC)(qy`&0h76Dyx^>d*FL|tAdE|~KU&8Cl+evt@fg +{kZJGs2Ob+2vg*@2Dt2YhR2f5`L&n5^IIUGU}~?&2kxDRNf{4ArX`9c+lU)mR|`3wZ4O=pt6sa$Y2{@ +u?_)!v-h3gYFzuv+&Oh3iup3}b=5W`x`JoDohT$p}5pEDqa +c39K+Z*l%ds*Pm{Ha5*93w6+8nn;tcchF@Cif~Wq8wdIs5Be-abJyz_44El$nZ07Sjwy~ZKx(2$WmsB +&1_;tj4?*j#G7Kcp`3Cfh9b-!N3tH5pWnyi*fr}vpljY_XOM2)pwXGe`)r(zhvqEz(qmiS-4?G +`_n2a2<$iHHX;if2zvN_lih#dGxzCLoR&|O>JYBLK8iU-11SBxd~8*l#44*y0{%o;D`mY7BF139Gaq_*!f2 +~+<22*I3_N8HnDgt=d4j#MuhT~ajzo@NyF{)6@AaBw5J* +xkaYH8ok?x3pn7ea@?zls{y9*4h)=tXn0_Bb2tR8gs*NzZk#Z3WBI7*d2!RtK@67nM$xJq6A{J?)W +Cnk{?K1#l2E?e4*|WmTyGe@pIn_3wL~87?LCwrCV%GdO_jmW|GR%_VaixGGkx +hkRN0)nY)~3P8G_##)71+#^a;&6>jr0lkl(|f~-A=kUVymH*?u_~8NR3z0b`4P`&YDL)v1rt)X$7XrJZo2_9&Ooq^M+ds+)+oqF6QtTNlIQr> +82R@#a(+ce*OzJ;FJdJ|(}h${YXHj550l_35O4NQea~)}^(m_1&Ol;ux2XgT!E0{o$s(RxqH9F+t;d5 +qWNom2|9m;Vyt|dlT!C$LKth=QeNi1Tmxq!9tO&xeo4>mSS^;~Yc7U^x&2m(wWr>#cXkJ<101|4G>oR +vt@Fh!@Ce;7|`_Ug{B{$u|GtU}E(SDZv0Rk_qni19;>$28Ef3#al;gtTNY0R7^QSCGk|M!iGy+E;x(-;~y&JKGP*+6- +F`AnH(^+BODJ!>XqW^$sx3{02N(`hK(Yq`Iuf?n?;RVNU+sjJ}96*bBi$~n+u*Cn}B&`F%pdZv<)iAt +A>Qb-M@5!hDeF;n}3yh&fewUCVjb_4Ck;{ZQ0LKnOk|6gipSP7Xj=bCChgXS4y)@(rSC8=W)^kM8(x& +yaKk{5O8_|HZ9-$9Zh+5v1eDRR}01XTMf(E>M8P+tB^2WcHEa7{&e^beX64+tJxhG!HV{(p-gfwsjGR +Lcwu)T88`HBSwz4ipGrd%{W6PYM{H-WOCj@aN}*PeojG)YWaT!#1gh= +RT_DWx|2>NyyR0=lA6){p%%$qZ2l31Gr;ZO8Ax?=h9UDBOn|AYbuKrw5?{4MlqEhO9SjU{7kk9XvYimCccg~!g~^B9w<7p3eht`T|a{bnr51ME3 +K74z})3J%KU6W$r|iL0v{Z}^TZv?0qL+2Qr@Tt=U}IoHecSXMH8xH$)@-h2GBld$H%(^jnwos;$6=Vb +{?Bf;z>14ynCE1UO1X6w4GPnZ6L1L{2WB1T`=L7Rfz)93HU9o}!htCdIM%}tOk4p`zh;pp45i_~`*>x>BlqvU!ya=W*I6Mw<~tLi$Vw!lQ6NTwCF=i;TC4A#b%!i@2F)MY$yEUs-iPrtvT{q +CmmpX(9AUUOsIw9JK63#E;pon!!ysVRv3|}0i<}&q8R&;Bc2lp5)|7llA^W^_J{kk-k0oM0lS8Q)f)i +#q)h(6>G504cpkN9wI4s^3aK46S5x}6uZhc?IanNRA2=}XdtIrSt*ohK!SeDJGS_oRk^ez08fDKtAxl +I~5Ruj>$IhrSL>N!j(5Wn*H!(}%6=!{;MdQ}H~W(x*?aL?6Hs_Wc`p +YLUzzXXm#T44@`?EAfYNwS-yd@{g6WY3AfWeJ8&lWakpX5NLY%rQZ%GFMydxci@ll6m +tD=kUE}Mhlrhp?*veotrJl0r#bW57y09fZoSH<7K8cD;%=V1No^q79Jeaj4dmdN!POp0kyJL +{0#mG0)e93`+SJ+*k^EV#bAuqq(=WGeNo8O({#3DEl+yA&rkBf4&j +vV`O9vp?=)xY$$7^!zgniZ%ig0H2aufGWogDi`ZB+Xmrpk-h8O@GA0a%(0`Vv8_In>!GAA|+SW4;sC?*Qf4?joeU6H80M*_7 +($`7|-JwD_t7!sW@pY?6>1Ibx$l35yS;0R>?B1q8Mko$Q0{3Vw3I~I_^G|F)sKiV>G4U{<>amsBtosn +foHasw;$LeaG=JaAlzq?*3bcJMhm%36&pv{d`*EPQA^vvu1{4S5Z2YQF?98rty9Q*cK7~W-BQ?jwW1V +@PUmlkzc3*meCw#6;L9SGoPG_@jKMoV>H%}rwx1I!}mvVg??MIH4IPJIE>t;7gEzCoyI +mWBZYJdQa1kyW}+8-j1wVE7*8Bi4*tp62{vHk7Onv8g*Q|%JxC_L->G`2RyZUxhLejO7a2{Cm!F3L1n +ZXwgL#BG2%@@xDO8{i;%X9h+QD=f9Ef0F#NIiu73tcWEl&{s;@x$bScDkh>yecR6stbb;`7JsYo%`QZ +oSOXn3E@feii;Ucq+3D7E$Kk}CV(qaWQvn=6$EObB0>^vY;pL9p$=%B`M@?M-Mq +cMhr!x@bxd7_%GhF4Y35&kb-8SsTa2#%x2D7x*R3g5))s=Pv+2W5wXOfW$q64NRGyd_Q(WNx`dt+;PP +I>0(nT>Uw*-cW?9J(Sl<@VqC~hV(iIR#iB}JPWvwlN142wzE=^@d~vaF3MKu!cP|4DEPW0t{#e<;0xF +Zdd`cf;u~IZ}^tu_tD#aM5`rD8B%Cw^(`=5^;Jo*PaD}*@pynjxU*NOo(h%!$^RW^&KDV`nA7Kku)3W +}_dXtA=T1_>%y(aYL<)mL(SY@tBY{D0aD4m0-F`Yjhr?6!a`Zw9RQpAb?Z&BSco2rEV8t!nW8Nb<#2- +)ou#BE+bfcYPnC){SSf0FFjd|MEq%2A96iKak~&17oj>;dGaV)s!utllvT-AuHer^kj8aAA38{CQPnB +3GZ&D=B9vdY$nE15|v4w{gvn{bNk-J<#$YMLLRoa)5Dt3`Pf3C!!-Suql+S2QTv9K4|2C`JwsawuYG{H*04EJ_nG_tV?>*bITjcxC1pD3_%HjzFrGMh +H+E5ihU4e}}hd}HDMDC*)=d-% +GO(`}$a^}-gvV(p~z_P|0{S@A$XXl~|SUmi4yny4*PnR&`FPDx$j)cy|m1F0p&0^5G`Vf~117cjra$G +J);paOJrh3zgnCZA{?K`HU)!Orln2sG*1riQ2fWulXM$L5*XICqj{Sp=@0!JX{(vgjf?cxME>hXzYSW +!=b8bo7m4VxDE$b3)Bk;1&3s7ikW){@Q5&u?{E;`^?5-s&@E5J7sYbtnNgl6(8e)gHdfM0s1_O4h(xX +wT%_9qj<%EEii)7E^B}JcjRY_NxQ6hx9HoE%)tJ%Kv<*XF}ZA|?0ljg>sU}b-sB_2Jf$$5 +p@aB2eu(`zv%moq&!lhMIdZO&RSAla5D%F1YiWw-a>vdk_3)S*R0@?rw1kGCog_lu +A%-@n(%m8_u(OHjr>d=w$B55NG*w~y~h`R2+-5Cba!o^RMiH&Om*joPOBX@c?sJABCYk70w_1#2g2Mb +_xwdhIQZZnR#J$Zp*Jf#`Ea?jl;ytKeU#D7DZc{4}(dz3!WF|?6ZJ6CoHdd-ZdX|x8;gDI3=R50^GCeG-ejB*;Ea<3Z8Rl83p?-=hEqBlKc!yLFIZktpAHiXGeOPD#tx3^PlfOR&W(sFN6|=W0 +=2L{t*-HaIV$P#qcV!(x#X(}eCqaxzng9ZsL}@iHz%UZ4ib%Cp05?e$ZhR9TWeGGGUqBJ1DMa%LPR60 +Dh2QlMuG*@MJ1pfw4Ucy)twoBbA3e|_~DBY(s)hV9q_lgc94EDRsgs7R3$kQ#<^I#Y-3W?B4F74WL!2 +MZiPr5$aGuy0)dqEx1x0H!-bd0eQYT#A^Y>)^%oWh0p&;_9AtW!V=iOmI@k7ZQ-%3$jo@sFp1sH>@mE +rTrBj#q?>d(So|Vfnd^3Vd}NvNO7l2d)WLpM5E=3|3ypE==*oF#mA?y0IJ~!)iOYYI&!ei8jfsCpc=_ +KYlR5uf&F7t9skMd0Uvu9(2SoOES=FEUUc34jVPfJSCt6O4Rz-)plrOzd@}<4{GZ4AraLTN)5Orm)&% +Z>hexl=XlCjrf`Axh)v0YM^?ICOeup~O>oW$TeGVpMnV=PsaN?m;9#+ZJ09-TEL=X&{leafyc{ls*U= +U#Z&bPsR=#Iye%ZqCb9DyA7oRF=7pv#QC8!%ntEPs=^ItpxYOZPk$V^>zT(Bm{%W9yTQ6El{r(DPYa& +FinJ`DF~0CV!{LyUluwgoE|HJz(KecXS_)FI^6d=3|ypbmsM&7&`qE);vhLUERy;G&4R}_i`8auP-?F +T^_?qOdbG&wR$)oY3^xFP!sd2-(_*k8YLn^hWe)AWSkXAyg)w!1!8FLY)BcdlLf4B<=8ZUs^19WWEtGsu;_>jv=c6RSfKqBK#rb~hjYY`}Y;O^ +QSu{CLU;;Ao@~f&jkPxAeuLp23)Y>3Z4X+_nnI|qZqP!+ +6h6zo5$Qr@F6zz*SUWHV!P#k>=ZD4_nj*J}0uG`hnN6|99Dqsaqq$Dvm+v|URs&f)ZeK*mx`C5G_9YK +wU*=1Fg2FR1DW@RMdcZAlJNEs_X9i4`E)1ttq5F@xLB!gr9rQjT?r&_MPa-?^`uQNd?8^xSn;hE16Lz +j%X0zl4n}SPG1Kypk!&6qSx6cDPxx7!Zq>N>vj0U`BV}!xJM@ +CHSBilF4WoYsmhhR}0=nw``dVYO+NU><*jR7^zZ;(GSzS4Y_eq!x1=i*#~r!FRN08E+o)_40BT+J7>q$B9;-KNkYK(H|??D2{QNp +#()^)GgI6x=DBg>BVgQAVz^t=XFm*BJaB>;0r1ba^g1=BHNA)jf}*I3IqRU_+J_MX0w7?12#Tofyv2P?puGDv(JQq;I-?;c<+y{Td#2~=_-aZXAZHL;Mg=vAjS7Myd% +EP}HW;;a8{S(?~B0;X`g;aR&;Vv8FwGvK#sUd9CndeKK~Onq1`oV+bLAoF^(8CY5*{!=o2=0JZV-U%~ +`gSbefCO#oNn)d3}M+NiH$jMK%@$-@2oKo7N6?;`>>nVAtG{fZ$y^!T3yk&=fhqjv~0Mgr14W@ZYhw&Bp$Vu%o?;~t(qPMB|I&DWHb#;sSvqo60pQ#_GnSH>BV6(?U?U(sV +FkfWoR>mG0=-Q&^mFKe>pwH3 +Q|(nE=KdYSRU?XR@OK_z=a#Q5cuYkL%mM6t(%2x +b&=P06?qua-j990FFkmISUGF*rs7?fSvj&vGN_7sEN7KU(@V@WKLtQ^abb*-zt`@x2}me +6Y{&w79lG5e#9$jFDkI5iP*bWV73lFE&i+$Q7$!6Ko(DJ(TAkC*-Iu#yqK?4^B@WI9&bmR-Q(^xElWk +CC4r&QI<~`YHyhX>;nA8x0Rf1sp2(Q};ue&-xlY>=V2hq1<;+eo!A8OA&VQMpw(_#&uwoCwZWf@8!hx +uC014@Bj!ffdC#IRpqoS?_Zith7=q*!`+395qvjzng==`*Fb38TLd<}FxiIwFGsPpC=C$`Ie^K%2Pu?oXS)JUHvr +G#!KC*~#EQM_J&#bMnT8v}ZK1g(J%E12Qyo2Lm#%@qC`FIA9-($MRkq+5~~mysPJ561c60c_K)Xjw~` +!=^$YJg})@Q!<9c|jfVA5h^x*ITGy{{H#|6-BS2N1+-q=?&Jz6Ez~DO%9D(q2J7S-jXxm`yS^c}iQ^r +Q^-CwD^Z3$Esw_OM!=f`r}HyluN0J$4m@A5Q$O|u`d$)rwQr%1wa?qus#7>C@b(j#}$O=8^4aTDZUM%173E3D-^$B3dFdO`B)=REz)OMy{p0IKC(VCgSQV1Fo0; +c#YqS`SX+t_X9K-^Stf5$Ou$hTv+XHrv6{oiX?;hCeXn^stmtK1Ka3z6DH6Tu{_b`X{w7zR0%YY0376>-`y6HwPnz}O3J +IQKeCIaAgI6W*@WzBx`NKSRYSc*k61)ZVHl`@#T`OcJ{eAL%nV7VCf$&u`F`v<5ye3(>Dlwhh0G(W3I +-jnL`SgDQc03q{!tp5p +""") diff --git a/scapy/libs/matplot.py b/scapy/libs/matplot.py new file mode 100644 index 00000000000..b6da620a040 --- /dev/null +++ b/scapy/libs/matplot.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi + +""" +External link to matplotlib +""" + +from scapy.error import log_loading + +# Notice: this file must not be called before main.py, if started +# in interactive mode, because it needs to be called after the +# logger has been setup, to be able to print the warning messages + +__all__ = [ + "Line2D", + "MATPLOTLIB", + "MATPLOTLIB_DEFAULT_PLOT_KARGS", + "MATPLOTLIB_INLINED", + "plt", +] + +# MATPLOTLIB + +try: + from matplotlib import get_backend as matplotlib_get_backend + from matplotlib import pyplot as plt + from matplotlib.lines import Line2D + MATPLOTLIB = 1 + if "inline" in matplotlib_get_backend(): + MATPLOTLIB_INLINED = 1 + else: + MATPLOTLIB_INLINED = 0 + MATPLOTLIB_DEFAULT_PLOT_KARGS = {"marker": "+"} +# RuntimeError to catch gtk "Cannot open display" error +except (ImportError, RuntimeError) as ex: + plt = None + Line2D = None + MATPLOTLIB = 0 + MATPLOTLIB_INLINED = 0 + MATPLOTLIB_DEFAULT_PLOT_KARGS = dict() + log_loading.info("Can't import matplotlib: %s. Won't be able to plot.", ex) diff --git a/scapy/libs/rfc3961.py b/scapy/libs/rfc3961.py new file mode 100644 index 00000000000..e07d00c1df9 --- /dev/null +++ b/scapy/libs/rfc3961.py @@ -0,0 +1,1479 @@ +# SPDX-License-Identifier: BSD-2-Clause +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (c) 2013, Marc Horowitz +# Copyright (C) 2013, Massachusetts Institute of Technology +# Copyright (C) 2022-2024, Gabriel Potter and the secdev/scapy community + +""" +Implementation of cryptographic functions for Kerberos 5 + +- RFC 3961: Encryption and Checksum Specifications for Kerberos 5 +- RFC 3962: Advanced Encryption Standard (AES) Encryption for Kerberos 5 +- RFC 4757: The RC4-HMAC Kerberos Encryption Types Used by Microsoft Windows +- RFC 6113: A Generalized Framework for Kerberos Pre-Authentication +- RFC 8009: AES Encryption with HMAC-SHA2 for Kerberos 5 + +.. note:: + You will find more complete documentation for Kerberos over at + `SMB `_ +""" + +# TODO: support cipher states... + +__all__ = [ + "ChecksumType", + "EncryptionType", + "InvalidChecksum", + "KRB_FX_CF2", + "Key", + "SP800108_KDFCTR", + "_rfc1964pad", +] + +# The following is a heavily modified version of +# https://github.com/SecureAuthCorp/impacket/blob/3ec59074ec35c06bbd4312d1042f0e23f4a1b41f/impacket/krb5/crypto.py +# itself heavily inspired from +# https://github.com/mhorowitz/pykrb5/blob/master/krb5/crypto.py +# Note that the following work is based only on THIS COMMIT from impacket, +# which is therefore under mhorowitz's BSD 2-clause "simplified" license. + +import abc +import enum +import math +import os +import struct +from scapy.compat import ( + orb, + chb, + int_bytes, + bytes_int, + plain_str, +) + +# Typing +from typing import ( + Any, + Callable, + List, + Optional, + Type, + Union, +) + +# We end up using our own crypto module for hashes / hmac because +# we need MD4 which was dropped everywhere. It's just a wrapper above +# the builtin python ones (except for MD4). + +from scapy.layers.tls.crypto.hash import ( + _GenericHash, + Hash_MD4, + Hash_MD5, + Hash_SHA, + Hash_SHA256, + Hash_SHA384, +) +from scapy.layers.tls.crypto.h_mac import ( + Hmac, + Hmac_MD5, + Hmac_SHA, +) + +# For everything else, use cryptography. + +try: + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms, + ) + except ImportError: + decrepit_algorithms = algorithms +except ImportError: + raise ImportError("To use kerberos cryptography, you need to install cryptography.") + + +# cryptography's TripleDES can be used to simulate DES behavior +def DES(key: bytes) -> decrepit_algorithms.TripleDES: + return decrepit_algorithms.TripleDES(key * 3) + + +# https://go.microsoft.com/fwlink/?LinkId=186039 +# https://csrc.nist.gov/CSRC/media/Publications/sp/800-108/archive/2008-11-06/documents/sp800-108-Nov2008.pdf +# [SP800-108] section 5.1 (used in [MS-SMB2] sect 3.1.4.2) + + +def SP800108_KDFCTR( + K_I: bytes, + Label: bytes, + Context: bytes, + L: int, + hashmod: _GenericHash = Hash_SHA256, +) -> bytes: + """ + KDF in Counter Mode as section 5.1 of [SP800-108] + + This assumes r=32, and defaults to SHA256 ([MS-SMB2] default). + """ + PRF = Hmac(K_I, hashmod).digest + h = hashmod.hash_len + n = math.ceil(L / h) + if n >= 0xFFFFFFFF: + # 2^r-1 = 0xffffffff with r=32 per [MS-SMB2] + raise ValueError("Invalid n value in SP800108_KDFCTR") + result = b"".join( + PRF(struct.pack(">I", i) + Label + b"\x00" + Context + struct.pack(">I", L)) + for i in range(1, n + 1) + ) + return result[: L // 8] + + +# https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml#kerberos-parameters-1 + + +class EncryptionType(enum.IntEnum): + DES_CBC_CRC = 1 + DES_CBC_MD4 = 2 + DES_CBC_MD5 = 3 + # DES3_CBC_SHA1 = 7 + DES3_CBC_SHA1_KD = 16 + AES128_CTS_HMAC_SHA1_96 = 17 + AES256_CTS_HMAC_SHA1_96 = 18 + AES128_CTS_HMAC_SHA256_128 = 19 + AES256_CTS_HMAC_SHA384_192 = 20 + RC4_HMAC = 23 + RC4_HMAC_EXP = 24 + # CAMELLIA128-CTS-CMAC = 25 + # CAMELLIA256-CTS-CMAC = 26 + + +# https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml#kerberos-parameters-2 + + +class ChecksumType(enum.IntEnum): + CRC32 = 1 + RSA_MD4 = 2 + RSA_MD4_DES = 3 + # RSA_MD5 = 7 + RSA_MD5_DES = 8 + # RSA_MD5_DES3 = 9 + # SHA1 = 10 + HMAC_SHA1_DES3_KD = 12 + # HMAC_SHA1_DES3 = 13 + # SHA1 = 14 + HMAC_SHA1_96_AES128 = 15 + HMAC_SHA1_96_AES256 = 16 + # CMAC-CAMELLIA128 = 17 + # CMAC-CAMELLIA256 = 18 + HMAC_SHA256_128_AES128 = 19 + HMAC_SHA384_192_AES256 = 20 + HMAC_MD5 = -138 + + +class InvalidChecksum(ValueError): + pass + + +######### +# Utils # +######### + + +# https://www.gnu.org/software/shishi/ides.pdf - APPENDIX B + + +def _n_fold(s, n): + # type: (bytes, int) -> bytes + """ + n-fold is an algorithm that takes m input bits and "stretches" them + to form n output bits with equal contribution from each input bit to + the output (quote from RFC 3961 sect 3.1). + """ + + def rot13(y, nb): + # type: (bytes, int) -> bytes + x = bytes_int(y) + mod = (1 << (nb * 8)) - 1 + if nb == 0: + return y + elif nb == 1: + return int_bytes(((x >> 5) | (x << (nb * 8 - 5))) & mod, nb) + else: + return int_bytes(((x >> 13) | (x << (nb * 8 - 13))) & mod, nb) + + def ocadd(x, y, nb): + # type: (bytearray, bytearray, int) -> bytearray + v = [a + b for a, b in zip(x, y)] + while any(x & ~0xFF for x in v): + v = [(v[i - nb + 1] >> 8) + (v[i] & 0xFF) for i in range(nb)] + return bytearray(x for x in v) + + m = len(s) + lcm = n // math.gcd(n, m) * m # lcm = math.lcm(n, m) on Python>=3.9 + buf = bytearray() + for _ in range(lcm // m): + buf += s + s = rot13(s, m) + out = bytearray(b"\x00" * n) + for i in range(0, lcm, n): + out = ocadd(out, buf[i : i + n], n) + return bytes(out) + + +def _zeropad(s, padsize): + # type: (bytes, int) -> bytes + """ + Return s padded with 0 bytes to a multiple of padsize. + """ + return s + b"\x00" * (-len(s) % padsize) + + +def _rfc1964pad(s): + # type: (bytes) -> bytes + """ + Return s padded as RFC1964 mandates + """ + pad = (-len(s)) % 8 + return s + pad * struct.pack("!B", pad) + + +def _xorbytes(b1, b2): + # type: (bytearray, bytearray) -> bytearray + """ + xor two strings together and return the resulting string + """ + assert len(b1) == len(b2) + return bytearray((x ^ y) for x, y in zip(b1, b2)) + + +def _mac_equal(mac1, mac2): + # type: (bytes, bytes) -> bool + # Constant-time comparison function. (We can't use HMAC.verify + # since we use truncated macs.) + return all(x == y for x, y in zip(mac1, mac2)) + + +# https://doi.org/10.6028/NBS.FIPS.74 sect 3.6 + +WEAK_DES_KEYS = set( + [ + # 1 + b"\xe0\x01\xe0\x01\xf1\x01\xf1\x01", + b"\x01\xe0\x01\xe0\x01\xf1\x01\xf1", + # 2 + b"\xfe\x1f\xfe\x1f\xfe\x0e\xfe\x0e", + b"\x1f\xfe\x1f\xfe\x0e\xfe\x0e\xfe", + # 3 + b"\xe0\x1f\xe0\x1f\xf1\x0e\xf1\x0e", + b"\x1f\xe0\x1f\xe0\x0e\xf1\x0e\xf1", + # 4 + b"\x01\xfe\x01\xfe\x01\xfe\x01\xfe", + b"\xfe\x01\xfe\x01\xfe\x01\xfe\x01", + # 5 + b"\x01\x1f\x01\x1f\x01\x0e\x01\x0e", + b"\x1f\x01\x1f\x01\x0e\x01\x0e\x01", + # 6 + b"\xe0\xfe\xe0\xfe\xf1\xfe\xf1\xfe", + b"\xfe\xe0\xfe\xe0\xfe\xf1\xfe\xf1", + # 7 + b"\x01" * 8, + # 8 + b"\xfe" * 8, + # 9 + b"\xe0" * 4 + b"\xf1" * 4, + # 10 + b"\x1f" * 4 + b"\x0e" * 4, + ] +) + +# fmt: off +CRC32_TABLE = [ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, + 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, + 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, + 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, + 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, + 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, + 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, + 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, + 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, + 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, + 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, + 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, + 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, + 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, + 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, + 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, + 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, + 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, + 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +] +# fmt: on + +############ +# RFC 3961 # +############ + + +# RFC3961 sect 3 + + +class _EncryptionAlgorithmProfile(abc.ABCMeta): + """ + Base class for etype profiles. + + Usable etype classes must define: + :attr etype: etype number + :attr keysize: protocol size of key in bytes + :attr seedsize: random_to_key input size in bytes + :attr reqcksum: 'required checksum mechanism' per RFC3961. + this is the default checksum used for this algorithm. + :attr random_to_key: (if the keyspace is not dense) + :attr string_to_key: + :attr encrypt: + :attr decrypt: + :attr prf: + """ + + etype = None # type: EncryptionType + keysize = None # type: int + seedsize = None # type: int + reqcksum = None # type: ChecksumType + + @classmethod + @abc.abstractmethod + def derive(cls, key, constant): + # type: (Key, bytes) -> bytes + pass + + @classmethod + @abc.abstractmethod + def encrypt(cls, key, keyusage, plaintext, confounder): + # type: (Key, int, bytes, Optional[bytes]) -> bytes + pass + + @classmethod + @abc.abstractmethod + def decrypt(cls, key, keyusage, ciphertext): + # type: (Key, int, bytes) -> bytes + pass + + @classmethod + @abc.abstractmethod + def prf(cls, key, string): + # type: (Key, bytes) -> bytes + pass + + @classmethod + @abc.abstractmethod + def string_to_key(cls, string, salt, params): + # type: (bytes, bytes, Optional[bytes]) -> Key + pass + + @classmethod + def random_to_key(cls, seed): + # type: (bytes) -> Key + if len(seed) != cls.seedsize: + raise ValueError("Wrong seed length") + return Key(cls.etype, key=seed) + + +# RFC3961 sect 4 + + +class _ChecksumProfile(object): + """ + Base class for checksum profiles. + + Usable checksum classes must define: + :func checksum: + :attr macsize: Size of checksum in bytes + :func verify: (if verification is not just checksum-and-compare) + """ + + macsize = None # type: int + + @classmethod + @abc.abstractmethod + def checksum(cls, key, keyusage, text): + # type: (Key, int, bytes) -> bytes + pass + + @classmethod + def verify(cls, key, keyusage, text, cksum): + # type: (Key, int, bytes, bytes) -> None + expected = cls.checksum(key, keyusage, text) + if not _mac_equal(cksum, expected): + raise InvalidChecksum("checksum verification failure") + + +# RFC3961 sect 5.3 + + +class _SimplifiedEncryptionProfile(_EncryptionAlgorithmProfile): + """ + Base class for etypes using the RFC 3961 simplified profile. + Defines the encrypt, decrypt, and prf methods. + + Subclasses must define: + + :param blocksize: Underlying cipher block size in bytes + :param padsize: Underlying cipher padding multiple (1 or blocksize) + :param macsize: Size of integrity MAC in bytes + :param hashmod: underlying hash function + :param basic_encrypt, basic_decrypt: Underlying CBC/CTS cipher + """ + + blocksize = None # type: int + padsize = None # type: int + macsize = None # type: int + hashmod = None # type: Any + + # Used in RFC 8009. This is not a simplified profile per se but + # is still pretty close. + rfc8009 = False + + @classmethod + @abc.abstractmethod + def basic_encrypt(cls, key, plaintext): + # type: (bytes, bytes) -> bytes + pass + + @classmethod + @abc.abstractmethod + def basic_decrypt(cls, key, ciphertext): + # type: (bytes, bytes) -> bytes + pass + + @classmethod + def derive(cls, key, constant): + # type: (Key, bytes) -> bytes + """ + Also known as "DK" in RFC3961. + """ + # RFC 3961 only says to n-fold the constant only if it is + # shorter than the cipher block size. But all Unix + # implementations n-fold constants if their length is larger + # than the block size as well, and n-folding when the length + # is equal to the block size is a no-op. + plaintext = _n_fold(constant, cls.blocksize) + rndseed = b"" + while len(rndseed) < cls.seedsize: + ciphertext = cls.basic_encrypt(key.key, plaintext) + rndseed += ciphertext + plaintext = ciphertext + # DK(Key, Constant) = random-to-key(DR(Key, Constant)) + return cls.random_to_key(rndseed[0 : cls.seedsize]).key + + @classmethod + def encrypt(cls, key, keyusage, plaintext, confounder, signtext=None): + # type: (Key, int, bytes, Optional[bytes], Optional[bytes]) -> bytes + """ + Encryption function. + + :param key: the key + :param keyusage: the keyusage + :param plaintext: the text to encrypt + :param confounder: (optional) the confounder. If none, will be random + :param signtext: (optional) make the checksum include different data than what + is encrypted. Useful for kerberos GSS_WrapEx. If none, same as + plaintext. + """ + if not cls.rfc8009: + ki = cls.derive(key, struct.pack(">IB", keyusage, 0x55)) + ke = cls.derive(key, struct.pack(">IB", keyusage, 0xAA)) + else: + ki = cls.derive(key, struct.pack(">IB", keyusage, 0x55), cls.macsize * 8) # type: ignore # noqa: E501 + ke = cls.derive(key, struct.pack(">IB", keyusage, 0xAA), cls.keysize * 8) # type: ignore # noqa: E501 + if confounder is None: + confounder = os.urandom(cls.blocksize) + basic_plaintext = confounder + _zeropad(plaintext, cls.padsize) + if signtext is None: + signtext = basic_plaintext + if not cls.rfc8009: + # Simplified profile + hmac = Hmac(ki, cls.hashmod).digest(signtext) + return cls.basic_encrypt(ke, basic_plaintext) + hmac[: cls.macsize] + else: + # RFC 8009 + C = cls.basic_encrypt(ke, basic_plaintext) + hmac = Hmac(ki, cls.hashmod).digest(b"\0" * 16 + C) # XXX IV + return C + hmac[: cls.macsize] + + @classmethod + def decrypt(cls, key, keyusage, ciphertext, presignfunc=None): + # type: (Key, int, bytes, Optional[Callable[[bytes, bytes], bytes]]) -> bytes + """ + decryption function + """ + if not cls.rfc8009: + ki = cls.derive(key, struct.pack(">IB", keyusage, 0x55)) + ke = cls.derive(key, struct.pack(">IB", keyusage, 0xAA)) + else: + ki = cls.derive(key, struct.pack(">IB", keyusage, 0x55), cls.macsize * 8) # type: ignore # noqa: E501 + ke = cls.derive(key, struct.pack(">IB", keyusage, 0xAA), cls.keysize * 8) # type: ignore # noqa: E501 + if len(ciphertext) < cls.blocksize + cls.macsize: + raise ValueError("Ciphertext too short") + basic_ctext, mac = ciphertext[: -cls.macsize], ciphertext[-cls.macsize :] + if len(basic_ctext) % cls.padsize != 0: + raise ValueError("ciphertext does not meet padding requirement") + if not cls.rfc8009: + # Simplified profile + basic_plaintext = cls.basic_decrypt(ke, basic_ctext) + signtext = basic_plaintext + if presignfunc: + # Allow to have additional processing of the data that is to be signed. + # This is useful for GSS_WrapEx + signtext = presignfunc( + basic_plaintext[: cls.blocksize], + basic_plaintext[cls.blocksize :], + ) + hmac = Hmac(ki, cls.hashmod).digest(signtext) + expmac = hmac[: cls.macsize] + if not _mac_equal(mac, expmac): + raise ValueError("ciphertext integrity failure") + else: + # RFC 8009 + signtext = b"\0" * 16 + basic_ctext # XXX IV + if presignfunc: + # Allow to have additional processing of the data that is to be signed. + # This is useful for GSS_WrapEx + signtext = presignfunc( + basic_ctext[16 : 16 + cls.blocksize], + basic_ctext[16 + cls.blocksize :], + ) + hmac = Hmac(ki, cls.hashmod).digest(signtext) + expmac = hmac[: cls.macsize] + if not _mac_equal(mac, expmac): + raise ValueError("ciphertext integrity failure") + basic_plaintext = cls.basic_decrypt(ke, basic_ctext) + # Discard the confounder. + return bytes(basic_plaintext[cls.blocksize :]) + + @classmethod + def prf(cls, key, string): + # type: (Key, bytes) -> bytes + """ + pseudo-random function + """ + # Hash the input. RFC 3961 says to truncate to the padding + # size, but implementations truncate to the block size. + hashval = cls.hashmod().digest(string) + if len(hashval) % cls.blocksize: + hashval = hashval[: -(len(hashval) % cls.blocksize)] + # Encrypt the hash with a derived key. + kp = cls.derive(key, b"prf") + return cls.basic_encrypt(kp, hashval) + + +# RFC3961 sect 5.4 + + +class _SimplifiedChecksum(_ChecksumProfile): + """ + Base class for checksums using the RFC 3961 simplified profile. + Defines the checksum and verify methods. + + Subclasses must define: + :attr enc: Profile of associated etype + """ + + enc = None # type: Type[_SimplifiedEncryptionProfile] + + # Used in RFC 8009. This is not a simplified profile per se but + # is still pretty close. + rfc8009 = False + + @classmethod + def checksum(cls, key, keyusage, text): + # type: (Key, int, bytes) -> bytes + if not cls.rfc8009: + # Simplified profile + kc = cls.enc.derive(key, struct.pack(">IB", keyusage, 0x99)) + else: + # RFC 8009 + kc = cls.enc.derive( # type: ignore + key, struct.pack(">IB", keyusage, 0x99), cls.macsize * 8 + ) + hmac = Hmac(kc, cls.enc.hashmod).digest(text) + return hmac[: cls.macsize] + + @classmethod + def verify(cls, key, keyusage, text, cksum): + # type: (Key, int, bytes, bytes) -> None + if key.etype != cls.enc.etype: + raise ValueError("Wrong key type for checksum") + super(_SimplifiedChecksum, cls).verify(key, keyusage, text, cksum) + + +# RFC3961 sect 6.1 + + +class _CRC32(_ChecksumProfile): + macsize = 4 + + # This isn't your usual CRC32, it's a "modified version" according to the RFC3961. + # Another RFC states it's just a buggy version of the actual CRC32. + + @classmethod + def checksum(cls, key, keyusage, text): + # type: (Optional[Key], int, bytes) -> bytes + c = 0 + for i in range(len(text)): + idx = text[i] ^ c + idx &= 0xFF + c >>= 8 + c ^= CRC32_TABLE[idx] + return c.to_bytes(4, "little") + + +# RFC3961 sect 6.2 + + +class _DESCBC(_SimplifiedEncryptionProfile): + keysize = 8 + seedsize = 8 + blocksize = 8 + padsize = 8 + macsize = 16 + hashmod = Hash_MD5 + + @classmethod + def encrypt(cls, key, keyusage, plaintext, confounder, signtext=None): + # type: (Key, int, bytes, Optional[bytes], Any) -> bytes + if confounder is None: + confounder = os.urandom(cls.blocksize) + basic_plaintext = ( + confounder + b"\x00" * cls.macsize + _zeropad(plaintext, cls.padsize) + ) + checksum = cls.hashmod().digest(basic_plaintext) + basic_plaintext = ( + basic_plaintext[: len(confounder)] + + checksum + + basic_plaintext[len(confounder) + len(checksum) :] + ) + return cls.basic_encrypt(key.key, basic_plaintext) + + @classmethod + def decrypt(cls, key, keyusage, ciphertext, presignfunc=None): + # type: (Key, int, bytes, Any) -> bytes + if len(ciphertext) < cls.blocksize + cls.macsize: + raise ValueError("ciphertext too short") + + complex_plaintext = cls.basic_decrypt(key.key, ciphertext) + cofounder = complex_plaintext[: cls.padsize] + mac = complex_plaintext[cls.padsize : cls.padsize + cls.macsize] + message = complex_plaintext[cls.padsize + cls.macsize :] + + expmac = cls.hashmod().digest(cofounder + b"\x00" * cls.macsize + message) + if not _mac_equal(mac, expmac): + raise InvalidChecksum("ciphertext integrity failure") + return bytes(message) + + @classmethod + def mit_des_string_to_key(cls, string, salt): + # type: (bytes, bytes) -> Key + def fixparity(deskey): + # type: (List[int]) -> bytes + temp = b"" + for i in range(len(deskey)): + t = (bin(orb(deskey[i]))[2:]).rjust(8, "0") + if t[:7].count("1") % 2 == 0: + temp += chb(int(t[:7] + "1", 2)) + else: + temp += chb(int(t[:7] + "0", 2)) + return temp + + def addparity(l1): + # type: (List[int]) -> List[int] + temp = list() + for byte in l1: + if (bin(byte).count("1") % 2) == 0: + byte = (byte << 1) | 0b00000001 + else: + byte = (byte << 1) & 0b11111110 + temp.append(byte) + return temp + + def XOR(l1, l2): + # type: (List[int], List[int]) -> List[int] + temp = list() + for b1, b2 in zip(l1, l2): + temp.append((b1 ^ b2) & 0b01111111) + + return temp + + odd = True + tempstring = [0, 0, 0, 0, 0, 0, 0, 0] + s = _zeropad(string + salt, cls.padsize) + + for block in [s[i : i + 8] for i in range(0, len(s), 8)]: + temp56 = list() + # removeMSBits + for byte in block: + temp56.append(orb(byte) & 0b01111111) + + # reverse + if odd is False: + bintemp = b"" + for byte in temp56: + bintemp += bin(byte)[2:].rjust(7, "0").encode() + bintemp = bintemp[::-1] + + temp56 = list() + for bits7 in [bintemp[i : i + 7] for i in range(0, len(bintemp), 7)]: + temp56.append(int(bits7, 2)) + + odd = not odd + tempstring = XOR(tempstring, temp56) + + tempkey = bytearray(b"".join(chb(byte) for byte in addparity(tempstring))) + if bytes(tempkey) in WEAK_DES_KEYS: + tempkey[7] = tempkey[7] ^ 0xF0 + + tempkeyb = bytes(tempkey) + des = Cipher(DES(tempkeyb), modes.CBC(tempkeyb)).encryptor() + chekcsumkey = des.update(s)[-8:] + chekcsumkey = bytearray(fixparity(chekcsumkey)) + if bytes(chekcsumkey) in WEAK_DES_KEYS: + chekcsumkey[7] = chekcsumkey[7] ^ 0xF0 + + return Key(cls.etype, key=bytes(chekcsumkey)) + + @classmethod + def basic_encrypt(cls, key, plaintext): + # type: (bytes, bytes) -> bytes + assert len(plaintext) % 8 == 0 + des = Cipher(DES(key), modes.CBC(b"\0" * 8)).encryptor() + return des.update(bytes(plaintext)) + + @classmethod + def basic_decrypt(cls, key, ciphertext): + # type: (bytes, bytes) -> bytes + assert len(ciphertext) % 8 == 0 + des = Cipher(DES(key), modes.CBC(b"\0" * 8)).decryptor() + return des.update(bytes(ciphertext)) + + @classmethod + def string_to_key(cls, string, salt, params): + # type: (bytes, bytes, Optional[bytes]) -> Key + if params is not None and params != b"": + raise ValueError("Invalid DES string-to-key parameters") + key = cls.mit_des_string_to_key(string, salt) + return key + + +# RFC3961 sect 6.2.1 + + +class _DESMD5(_DESCBC): + etype = EncryptionType.DES_CBC_MD5 + hashmod = Hash_MD5 + reqcksum = ChecksumType.RSA_MD5_DES + + +# RFC3961 sect 6.2.2 + + +class _DESMD4(_DESCBC): + etype = EncryptionType.DES_CBC_MD4 + hashmod = Hash_MD4 + reqcksum = ChecksumType.RSA_MD4_DES + + +# RFC3961 sect 6.3 + + +class _DES3CBC(_SimplifiedEncryptionProfile): + etype = EncryptionType.DES3_CBC_SHA1_KD + keysize = 24 + seedsize = 21 + blocksize = 8 + padsize = 8 + macsize = 20 + hashmod = Hash_SHA + reqcksum = ChecksumType.HMAC_SHA1_DES3_KD + + @classmethod + def random_to_key(cls, seed): + # type: (bytes) -> Key + # XXX Maybe reframe as _DESEncryptionType.random_to_key and use that + # way from DES3 random-to-key when DES is implemented, since + # MIT does this instead of the RFC 3961 random-to-key. + def expand(seed): + # type: (bytes) -> bytes + def parity(b): + # type: (int) -> int + # Return b with the low-order bit set to yield odd parity. + b &= ~1 + return b if bin(b & ~1).count("1") % 2 else b | 1 + + assert len(seed) == 7 + firstbytes = [parity(b & ~1) for b in seed] + lastbyte = parity(sum((seed[i] & 1) << i + 1 for i in range(7))) + keybytes = bytearray(firstbytes + [lastbyte]) + if bytes(keybytes) in WEAK_DES_KEYS: + keybytes[7] = keybytes[7] ^ 0xF0 + return bytes(keybytes) + + if len(seed) != 21: + raise ValueError("Wrong seed length") + k1, k2, k3 = expand(seed[:7]), expand(seed[7:14]), expand(seed[14:]) + return Key(cls.etype, key=k1 + k2 + k3) + + @classmethod + def string_to_key(cls, string, salt, params): + # type: (bytes, bytes, Optional[bytes]) -> Key + if params is not None and params != b"": + raise ValueError("Invalid DES3 string-to-key parameters") + k = cls.random_to_key(_n_fold(string + salt, 21)) + return Key( + cls.etype, + key=cls.derive(k, b"kerberos"), + ) + + @classmethod + def basic_encrypt(cls, key, plaintext): + # type: (bytes, bytes) -> bytes + assert len(plaintext) % 8 == 0 + des3 = Cipher( + decrepit_algorithms.TripleDES(key), modes.CBC(b"\0" * 8) + ).encryptor() + return des3.update(bytes(plaintext)) + + @classmethod + def basic_decrypt(cls, key, ciphertext): + # type: (bytes, bytes) -> bytes + assert len(ciphertext) % 8 == 0 + des3 = Cipher( + decrepit_algorithms.TripleDES(key), modes.CBC(b"\0" * 8) + ).decryptor() + return des3.update(bytes(ciphertext)) + + +class _SHA1DES3(_SimplifiedChecksum): + macsize = 20 + enc = _DES3CBC + + +############ +# RFC 3962 # +############ + + +# RFC3962 sect 6 + + +class _AESEncryptionType_SHA1_96(_SimplifiedEncryptionProfile, abc.ABCMeta): + blocksize = 16 + padsize = 1 + macsize = 12 + hashmod = Hash_SHA + + @classmethod + def string_to_key(cls, string, salt, params): + # type: (bytes, bytes, Optional[bytes]) -> Key + iterations = struct.unpack(">L", params or b"\x00\x00\x10\x00")[0] + kdf = PBKDF2HMAC( + algorithm=hashes.SHA1(), + length=cls.seedsize, + salt=salt, + iterations=iterations, + ) + tkey = cls.random_to_key(kdf.derive(string)) + return Key( + cls.etype, + key=cls.derive(tkey, b"kerberos"), + ) + + # basic_encrypt and basic_decrypt implement AES in CBC-CS3 mode + + @classmethod + def basic_encrypt(cls, key, plaintext): + # type: (bytes, bytes) -> bytes + assert len(plaintext) >= 16 + aes = Cipher(algorithms.AES(key), modes.CBC(b"\0" * 16)).encryptor() + ctext = aes.update(_zeropad(bytes(plaintext), 16)) + if len(plaintext) > 16: + # Swap the last two ciphertext blocks and truncate the + # final block to match the plaintext length. + lastlen = len(plaintext) % 16 or 16 + ctext = ctext[:-32] + ctext[-16:] + ctext[-32:-16][:lastlen] + return ctext + + @classmethod + def basic_decrypt(cls, key, ciphertext): + # type: (bytes, bytes) -> bytes + assert len(ciphertext) >= 16 + aes = Cipher(algorithms.AES(key), modes.ECB()).decryptor() + if len(ciphertext) == 16: + return aes.update(ciphertext) + # Split the ciphertext into blocks. The last block may be partial. + cblocks = [ + bytearray(ciphertext[p : p + 16]) for p in range(0, len(ciphertext), 16) + ] + lastlen = len(cblocks[-1]) + # CBC-decrypt all but the last two blocks. + prev_cblock = bytearray(16) + plaintext = b"" + for bb in cblocks[:-2]: + plaintext += _xorbytes(bytearray(aes.update(bytes(bb))), prev_cblock) + prev_cblock = bb + # Decrypt the second-to-last cipher block. The left side of + # the decrypted block will be the final block of plaintext + # xor'd with the final partial cipher block; the right side + # will be the omitted bytes of ciphertext from the final + # block. + bb = bytearray(aes.update(bytes(cblocks[-2]))) + lastplaintext = _xorbytes(bb[:lastlen], cblocks[-1]) + omitted = bb[lastlen:] + # Decrypt the final cipher block plus the omitted bytes to get + # the second-to-last plaintext block. + plaintext += _xorbytes( + bytearray(aes.update(bytes(cblocks[-1]) + bytes(omitted))), prev_cblock + ) + return plaintext + lastplaintext + + +# RFC3962 sect 7 + + +class _AES128CTS_SHA1_96(_AESEncryptionType_SHA1_96): + etype = EncryptionType.AES128_CTS_HMAC_SHA1_96 + keysize = 16 + seedsize = 16 + reqcksum = ChecksumType.HMAC_SHA1_96_AES128 + + +class _AES256CTS_SHA1_96(_AESEncryptionType_SHA1_96): + etype = EncryptionType.AES256_CTS_HMAC_SHA1_96 + keysize = 32 + seedsize = 32 + reqcksum = ChecksumType.HMAC_SHA1_96_AES256 + + +class _SHA1_96_AES128(_SimplifiedChecksum): + macsize = 12 + enc = _AES128CTS_SHA1_96 + + +class _SHA1_96_AES256(_SimplifiedChecksum): + macsize = 12 + enc = _AES256CTS_SHA1_96 + + +############ +# RFC 4757 # +############ + +# RFC4757 sect 4 + + +class _HMACMD5(_ChecksumProfile): + macsize = 16 + + @classmethod + def checksum(cls, key, keyusage, text): + # type: (Key, int, bytes) -> bytes + ksign = Hmac_MD5(key.key).digest(b"signaturekey\0") + md5hash = Hash_MD5().digest(_RC4.usage_str(keyusage) + text) + return Hmac_MD5(ksign).digest(md5hash) + + @classmethod + def verify(cls, key, keyusage, text, cksum): + # type: (Key, int, bytes, bytes) -> None + if key.etype not in [EncryptionType.RC4_HMAC, EncryptionType.RC4_HMAC_EXP]: + raise ValueError("Wrong key type for checksum") + super(_HMACMD5, cls).verify(key, keyusage, text, cksum) + + +# RFC4757 sect 5 + + +class _RC4(_EncryptionAlgorithmProfile): + etype = EncryptionType.RC4_HMAC + keysize = 16 + seedsize = 16 + reqcksum = ChecksumType.HMAC_MD5 + export = False + + @staticmethod + def usage_str(keyusage): + # type: (int) -> bytes + # Return a four-byte string for an RFC 3961 keyusage, using + # the RFC 4757 rules sect 3. Per the errata, do not map 9 to 8. + table = {3: 8, 23: 13} + msusage = table[keyusage] if keyusage in table else keyusage + return struct.pack(" Key + if params is not None and params != b"": + raise ValueError("Invalid RC4 string-to-key parameters") + utf16string = plain_str(string).encode("UTF-16LE") + return Key(cls.etype, key=Hash_MD4().digest(utf16string)) + + @classmethod + def encrypt(cls, key, keyusage, plaintext, confounder): + # type: (Key, int, bytes, Optional[bytes]) -> bytes + if confounder is None: + confounder = os.urandom(8) + if cls.export: + ki = Hmac_MD5(key.key).digest(b"fortybits\x00" + cls.usage_str(keyusage)) + else: + ki = Hmac_MD5(key.key).digest(cls.usage_str(keyusage)) + cksum = Hmac_MD5(ki).digest(confounder + plaintext) + if cls.export: + ki = ki[:7] + b"\xab" * 9 + ke = Hmac_MD5(ki).digest(cksum) + rc4 = Cipher(algorithms.ARC4(ke), mode=None).encryptor() + return cksum + rc4.update(bytes(confounder + plaintext)) + + @classmethod + def decrypt(cls, key, keyusage, ciphertext): + # type: (Key, int, bytes) -> bytes + if len(ciphertext) < 24: + raise ValueError("ciphertext too short") + cksum, basic_ctext = ciphertext[:16], ciphertext[16:] + if cls.export: + ki = Hmac_MD5(key.key).digest(b"fortybits\x00" + cls.usage_str(keyusage)) + else: + ki = Hmac_MD5(key.key).digest(cls.usage_str(keyusage)) + if cls.export: + kie = ki[:7] + b"\xab" * 9 + else: + kie = ki + ke = Hmac_MD5(kie).digest(cksum) + rc4 = Cipher(decrepit_algorithms.ARC4(ke), mode=None).decryptor() + basic_plaintext = rc4.update(bytes(basic_ctext)) + exp_cksum = Hmac_MD5(ki).digest(basic_plaintext) + ok = _mac_equal(cksum, exp_cksum) + if not ok and keyusage == 9: + # Try again with usage 8, due to RFC 4757 errata. + ki = Hmac_MD5(key.key).digest(struct.pack(" bytes + return Hmac_SHA(key.key).digest(string) + + +class _RC4_EXPORT(_RC4): + etype = EncryptionType.RC4_HMAC_EXP + export = True + + +############ +# RFC 8009 # +############ + + +class _AESEncryptionType_SHA256_SHA384(_AESEncryptionType_SHA1_96, abc.ABCMeta): + enctypename = None # type: bytes + hashmod: _GenericHash = None # Scapy + _hashmod: hashes.HashAlgorithm = None # Cryptography + + # Turn on RFC 8009 mode + rfc8009 = True + + @classmethod + def derive(cls, key, label, k, context=b""): # type: ignore + # type: (Key, bytes, int, bytes) -> bytes + """ + Also known as "KDF-HMAC-SHA2" in RFC8009. + """ + # RFC 8009 sect 3 + return SP800108_KDFCTR( + K_I=key.key, + Label=label, + Context=context, + L=k, + hashmod=cls.hashmod, + ) + + @classmethod + def string_to_key(cls, string, salt, params): + # type: (bytes, bytes, Optional[bytes]) -> Key + # RFC 8009 sect 4 + iterations = struct.unpack(">L", params or b"\x00\x00\x80\x00")[0] + saltp = cls.enctypename + b"\x00" + salt + kdf = PBKDF2HMAC( + algorithm=cls._hashmod(), + length=cls.seedsize, + salt=saltp, + iterations=iterations, + ) + tkey = cls.random_to_key(kdf.derive(string)) + return Key( + cls.etype, + key=cls.derive(tkey, b"kerberos", cls.keysize * 8), + ) + + @classmethod + def prf(cls, key, string): + # type: (Key, bytes) -> bytes + return cls.derive(key, b"prf", cls.hashmod.hash_len * 8, string) + + +class _AES128CTS_SHA256_128(_AESEncryptionType_SHA256_SHA384): + etype = EncryptionType.AES128_CTS_HMAC_SHA256_128 + keysize = 16 + seedsize = 16 + macsize = 16 + reqcksum = ChecksumType.HMAC_SHA256_128_AES128 + # _AESEncryptionType_SHA256_SHA384 parameters + enctypename = b"aes128-cts-hmac-sha256-128" + hashmod = Hash_SHA256 + _hashmod = hashes.SHA256 + + +class _AES256CTS_SHA384_192(_AESEncryptionType_SHA256_SHA384): + etype = EncryptionType.AES256_CTS_HMAC_SHA384_192 + keysize = 32 + seedsize = 32 + macsize = 24 + reqcksum = ChecksumType.HMAC_SHA384_192_AES256 + # _AESEncryptionType_SHA256_SHA384 parameters + enctypename = b"aes256-cts-hmac-sha384-192" + hashmod = Hash_SHA384 + _hashmod = hashes.SHA384 + + +class _SHA256_128_AES128(_SimplifiedChecksum): + macsize = 16 + enc = _AES128CTS_SHA256_128 + rfc8009 = True + + +class _SHA384_182_AES256(_SimplifiedChecksum): + macsize = 24 + enc = _AES256CTS_SHA384_192 + rfc8009 = True + + +############## +# Key object # +############## + +_enctypes = { + # DES_CBC_CRC - UNIMPLEMENTED + EncryptionType.DES_CBC_MD5: _DESMD5, + EncryptionType.DES_CBC_MD4: _DESMD4, + # DES3_CBC_SHA1 - UNIMPLEMENTED + EncryptionType.DES3_CBC_SHA1_KD: _DES3CBC, + EncryptionType.AES128_CTS_HMAC_SHA1_96: _AES128CTS_SHA1_96, + EncryptionType.AES256_CTS_HMAC_SHA1_96: _AES256CTS_SHA1_96, + EncryptionType.AES128_CTS_HMAC_SHA256_128: _AES128CTS_SHA256_128, + EncryptionType.AES256_CTS_HMAC_SHA384_192: _AES256CTS_SHA384_192, + # CAMELLIA128-CTS-CMAC - UNIMPLEMENTED + # CAMELLIA256-CTS-CMAC - UNIMPLEMENTED + EncryptionType.RC4_HMAC: _RC4, + EncryptionType.RC4_HMAC_EXP: _RC4_EXPORT, +} + + +_checksums = { + ChecksumType.CRC32: _CRC32, + # RSA_MD4 - UNIMPLEMENTED + # RSA_MD4_DES - UNIMPLEMENTED + # RSA_MD5 - UNIMPLEMENTED + # RSA_MD5_DES - UNIMPLEMENTED + # SHA1 - UNIMPLEMENTED + ChecksumType.HMAC_SHA1_DES3_KD: _SHA1DES3, + # HMAC_SHA1_DES3 - UNIMPLEMENTED + ChecksumType.HMAC_SHA1_96_AES128: _SHA1_96_AES128, + ChecksumType.HMAC_SHA1_96_AES256: _SHA1_96_AES256, + # CMAC-CAMELLIA128 - UNIMPLEMENTED + # CMAC-CAMELLIA256 - UNIMPLEMENTED + ChecksumType.HMAC_SHA256_128_AES128: _SHA256_128_AES128, + ChecksumType.HMAC_SHA384_192_AES256: _SHA384_182_AES256, + ChecksumType.HMAC_MD5: _HMACMD5, + 0xFFFFFF76: _HMACMD5, +} + + +class Key(object): + def __init__( + self, + etype: Union[EncryptionType, int, None] = None, + key: bytes = b"", + cksumtype: Union[ChecksumType, int, None] = None, + ) -> None: + """ + Kerberos Key object. + + :param etype: the EncryptionType + :param cksumtype: the ChecksumType + :param key: the bytes containing the key bytes for this Key. + """ + assert etype or cksumtype, "Provide an etype or a cksumtype !" + assert key, "Provide a key !" + if isinstance(etype, int): + etype = EncryptionType(etype) + if isinstance(cksumtype, int): + cksumtype = ChecksumType(cksumtype) + self.etype = etype + if etype is not None: + try: + self.ep = _enctypes[etype] + except ValueError: + raise ValueError("UNKNOWN/UNIMPLEMENTED etype '%s'" % etype) + if len(key) != self.ep.keysize: + raise ValueError( + "Wrong key length. Got %s. Expected %s" + % (len(key), self.ep.keysize) + ) + if cksumtype is None and self.ep.reqcksum in _checksums: + cksumtype = self.ep.reqcksum + self.cksumtype = cksumtype + if cksumtype is not None: + try: + self.cp = _checksums[cksumtype] + except ValueError: + raise ValueError("UNKNOWN/UNIMPLEMENTED cksumtype '%s'" % cksumtype) + if self.etype is None and issubclass(self.cp, _SimplifiedChecksum): + self.etype = self.cp.enc.etype # type: ignore + self.key = key + + def __repr__(self): + # type: () -> str + if self.etype: + name = self.etype.name + elif self.cksumtype: + name = self.cksumtype.name + else: + return "" + return "" % ( + name, + " (%s octets)" % len(self.key), + ) + + def encrypt(self, keyusage, plaintext, confounder=None, **kwargs): + # type: (int, bytes, Optional[bytes], **Any) -> bytes + """ + Encrypt data using the current Key. + + :param keyusage: the key usage + :param plaintext: the plain text to encrypt + :param confounder: (optional) choose the confounder. Otherwise random. + """ + return self.ep.encrypt(self, keyusage, bytes(plaintext), confounder, **kwargs) + + def decrypt(self, keyusage, ciphertext, **kwargs): + # type: (int, bytes, **Any) -> bytes + """ + Decrypt data using the current Key. + + :param keyusage: the key usage + :param ciphertext: the encrypted text to decrypt + """ + # Throw InvalidChecksum on checksum failure. Throw ValueError on + # invalid key enctype or malformed ciphertext. + return self.ep.decrypt(self, keyusage, ciphertext, **kwargs) + + def prf(self, string): + # type: (bytes) -> bytes + return self.ep.prf(self, string) + + def make_checksum(self, keyusage, text, cksumtype=None, **kwargs): + # type: (int, bytes, Optional[int], **Any) -> bytes + """ + Create a checksum using the current Key. + + :param keyusage: the key usage + :param text: the text to create a checksum from + :param cksumtype: (optional) override the checksum type + """ + if cksumtype is not None and cksumtype != self.cksumtype: + # Clone key and use a different cksumtype + return Key( + cksumtype=cksumtype, + key=self.key, + ).make_checksum(keyusage=keyusage, text=text, **kwargs) + if self.cksumtype is None: + raise ValueError("cksumtype not specified !") + return self.cp.checksum(self, keyusage, text, **kwargs) + + def verify_checksum(self, keyusage, text, cksum, cksumtype=None): + # type: (int, bytes, bytes, Optional[int]) -> None + """ + Verify a checksum using the current Key. + + :param keyusage: the key usage + :param text: the text to verify + :param cksum: the expected checksum + :param cksumtype: (optional) override the checksum type + """ + if cksumtype is not None and cksumtype != self.cksumtype: + # Clone key and use a different cksumtype + return Key( + cksumtype=cksumtype, + key=self.key, + ).verify_checksum(keyusage=keyusage, text=text, cksum=cksum) + # Throw InvalidChecksum exception on checksum failure. Throw + # ValueError on invalid cksumtype, invalid key enctype, or + # malformed checksum. + if self.cksumtype is None: + raise ValueError("cksumtype not specified !") + self.cp.verify(self, keyusage, text, cksum) + + @classmethod + def random_to_key(cls, etype, seed): + # type: (EncryptionType, bytes) -> Key + """ + random-to-key per RFC3961 + + This is used to create a random Key from a seed. + """ + try: + ep = _enctypes[etype] + except ValueError: + raise ValueError("Unknown etype '%s'" % etype) + if len(seed) != ep.seedsize: + raise ValueError("Wrong crypto seed length") + return ep.random_to_key(seed) + + @classmethod + def new_random_key(cls, etype): + # type: (EncryptionType) -> Key + """ + Generates a seed then calls random-to-key + """ + try: + ep = _enctypes[etype] + except ValueError: + raise ValueError("Unknown etype '%s'" % etype) + return cls.random_to_key(etype, os.urandom(ep.seedsize)) + + @classmethod + def string_to_key(cls, etype, string, salt, params=None): + # type: (EncryptionType, bytes, bytes, Optional[bytes]) -> Key + """ + string-to-key per RFC3961 + + This is typically used to create a Key object from a password + salt + """ + try: + ep = _enctypes[etype] + except ValueError: + raise ValueError("Unknown etype '%s'" % etype) + return ep.string_to_key(string, salt, params) + + +############ +# RFC 6113 # +############ + + +def KRB_FX_CF2(key1, key2, pepper1, pepper2): + # type: (Key, Key, bytes, bytes) -> Key + """ + KRB-FX-CF2 RFC6113 + """ + + def prfplus(key, pepper): + # type: (Key, bytes) -> bytes + # Produce l bytes of output using the RFC 6113 PRF+ function. + out = b"" + count = 1 + while len(out) < key.ep.seedsize: + out += key.prf(chb(count) + pepper) + count += 1 + return out[: key.ep.seedsize] + + return Key( + key1.etype, + key=bytes( + _xorbytes( + bytearray(prfplus(key1, pepper1)), bytearray(prfplus(key2, pepper2)) + ) + ), + ) + + +############ +# RFC 4556 # +############ + +def octetstring2key(etype: EncryptionType, x: bytes) -> Key: + """ + RFC4556 octetstring2key:: + + octetstring2key(x) == random-to-key(K-truncate( + SHA1(0x00 | x) | + SHA1(0x01 | x) | + SHA1(0x02 | x) | + ... + )) + """ + try: + ep = _enctypes[etype] + except ValueError: + raise ValueError("Unknown etype '%s'" % etype) + + out = b"" + count = 0 + while len(out) < ep.keysize: + out += Hash_SHA().digest(struct.pack("!B", count) + x) + count += 1 + + return Key.random_to_key( + etype=etype, + seed=out[:ep.keysize], + ) diff --git a/scapy/libs/structures.py b/scapy/libs/structures.py index af5e1cd0fc0..3f2339cd413 100644 --- a/scapy/libs/structures.py +++ b/scapy/libs/structures.py @@ -1,6 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information """ Commonly used structures shared across Scapy @@ -21,3 +21,9 @@ class bpf_program(ctypes.Structure): """"Structure for BIOCSETF""" _fields_ = [('bf_len', ctypes.c_int), ('bf_insns', ctypes.POINTER(bpf_insn))] + + +class sock_fprog(ctypes.Structure): + """"Structure for SO_ATTACH_FILTER""" + _fields_ = [('len', ctypes.c_ushort), + ('filter', ctypes.POINTER(bpf_insn))] diff --git a/scapy/extlib.py b/scapy/libs/test_pyx.py similarity index 55% rename from scapy/extlib.py rename to scapy/libs/test_pyx.py index 7d8dd6aaeee..77afe77720d 100644 --- a/scapy/extlib.py +++ b/scapy/libs/test_pyx.py @@ -1,10 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information """ -External link to programs +External link to pyx """ import os @@ -15,31 +14,15 @@ # in interactive mode, because it needs to be called after the # logger has been setup, to be able to print the warning messages -# MATPLOTLIB - -try: - from matplotlib import get_backend as matplotlib_get_backend - from matplotlib import pyplot as plt - from matplotlib.lines import Line2D - MATPLOTLIB = 1 - if "inline" in matplotlib_get_backend(): - MATPLOTLIB_INLINED = 1 - else: - MATPLOTLIB_INLINED = 0 - MATPLOTLIB_DEFAULT_PLOT_KARGS = {"marker": "+"} -# RuntimeError to catch gtk "Cannot open display" error -except (ImportError, RuntimeError): - plt = None - Line2D = None - MATPLOTLIB = 0 - MATPLOTLIB_INLINED = 0 - MATPLOTLIB_DEFAULT_PLOT_KARGS = dict() - log_loading.info("Can't import matplotlib. Won't be able to plot.") +__all__ = [ + "PYX", +] # PYX def _test_pyx(): + # type: () -> bool """Returns if PyX is correctly installed or not""" try: with open(os.devnull, 'wb') as devnull: diff --git a/scapy/libs/winpcapy.py b/scapy/libs/winpcapy.py index 8d50121588c..42bfa850921 100644 --- a/scapy/libs/winpcapy.py +++ b/scapy/libs/winpcapy.py @@ -1,18 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Massimo Ciani (2009) -# Gabriel Potter (2016-2019) -# This program is published under a GPLv2 license +# Copyright (C) Gabriel Potter # Modified for scapy's usage - To support Npcap/Monitor mode - +# +# NOTE: the "winpcap" in the name notwithstanding, this is for use +# with libpcap on non-Windows platforms, as well as for WinPcap and Npcap. from ctypes import * from ctypes.util import find_library import os from scapy.libs.structures import bpf_program -from scapy.consts import WINDOWS +from scapy.consts import WINDOWS, BSD if WINDOWS: # Try to load Npcap, or Winpcap @@ -66,23 +68,11 @@ class timeval(Structure): # sockaddr is used by pcap_addr. # For example if sa_family==socket.AF_INET then we need cast # with sockaddr_in -if WINDOWS: - class sockaddr(Structure): - _fields_ = [("sa_family", c_ushort), - ("sa_data", c_ubyte * 14)] - - class sockaddr_in(Structure): - _fields_ = [("sin_family", c_ushort), - ("sin_port", c_uint16), - ("sin_addr", 4 * c_ubyte)] - class sockaddr_in6(Structure): - _fields_ = [("sin6_family", c_ushort), - ("sin6_port", c_uint16), - ("sin6_flowinfo", c_uint32), - ("sin6_addr", 16 * c_ubyte), - ("sin6_scope", c_uint32)] -else: +# sockaddr has a different structure depending on the OS +if BSD: + # https://github.com/freebsd/freebsd/blob/master/sys/sys/socket.h + # https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/socket.h.auto.html class sockaddr(Structure): _fields_ = [("sa_len", c_ubyte), ("sa_family", c_ubyte), @@ -112,6 +102,26 @@ class sockaddr_dl(Structure): ("sdl_alen", c_ubyte), ("sdl_slen", c_ubyte), ("sdl_data", 46 * c_ubyte)] + +else: + # https://github.com/torvalds/linux/blob/master/include/linux/socket.h + # https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2 + class sockaddr(Structure): + _fields_ = [("sa_family", c_ushort), + ("sa_data", c_ubyte * 14)] + + class sockaddr_in(Structure): + _fields_ = [("sin_family", c_ushort), + ("sin_port", c_uint16), + ("sin_addr", 4 * c_ubyte)] + + class sockaddr_in6(Structure): + _fields_ = [("sin6_family", c_ushort), + ("sin6_port", c_uint16), + ("sin6_flowinfo", c_uint32), + ("sin6_addr", 16 * c_ubyte), + ("sin6_scope", c_uint32)] + ## # END misc ## @@ -215,6 +225,60 @@ class pcap_if(Structure): # Statistical mode, to be used when calling pcap_setmode(). MODE_STAT = 1 +# Error codes for the pcap API. +# These will all be negative, so you can check for the success or +# failure of a call that returns these codes by checking for a +# negative value. +# +# generic error code +# define PCAP_ERROR -1 +PCAP_ERROR = -1 +# loop terminated by pcap_breakloop +# define PCAP_ERROR_BREAK -2 +PCAP_ERROR_BREAK = -2 +# the capture needs to be activated +# define PCAP_ERROR_NOT_ACTIVATED -3 +PCAP_ERROR_NOT_ACTIVATED = -3 +# the operation can't be performed on already activated captures +# define PCAP_ERROR_ACTIVATED -4 +PCAP_ERROR_ACTIVATED = -4 +# no such device exists +# define PCAP_ERROR_NO_SUCH_DEVICE -5 +PCAP_ERROR_NO_SUCH_DEVICE = -5 +# this device doesn't support rfmon (monitor) mode */ +# define PCAP_ERROR_RFMON_NOTSUP -6 +PCAP_ERROR_RFMON_NOTSUP = -6 +# operation supported only in monitor mode +# define PCAP_ERROR_NOT_RFMON -7 +PCAP_ERROR_NOT_RFMON = -7 +# no permission to open the device +# define PCAP_ERROR_PERM_DENIED -8 +PCAP_ERROR_PERM_DENIED = -8 +# interface isn't up +# define PCAP_ERROR_IFACE_NOT_UP -9 +PCAP_ERROR_IFACE_NOT_UP = -9 +# define PCAP_ERROR_CANTSET_TSTAMP_TYPE -10 +# this device doesn't support setting the time stamp type +# you don't have permission to capture in promiscuous mode +# define PCAP_ERROR_PROMISC_PERM_DENIED -11 +PCAP_ERROR_PROMISC_PERM_DENIED = -11 +# the requested time stamp precision is not supported +# define PCAP_ERROR_TSTAMP_PRECISION_NOTSUP -12 +PCAP_ERROR_TSTAMP_PRECISION_NOTSUP = -12 + +# Warning codes for the pcap API. +# These will all be positive and non-zero, so they won't look like +# errors. +# generic warning code +# define PCAP_WARNING 1 +PCAP_WARNING = 1 +# this device doesn't support promiscuous mode +# define PCAP_WARNING_PROMISC_NOTSUP 2 +PCAP_WARNING_PROMISC_NOTSUP = 2 +# the requested time stamp type is not supported +# define PCAP_WARNING_TSTAMP_TYPE_NOTSUP 3 +PCAP_WARNING_TSTAMP_TYPE_NOTSUP = 3 + ## # END Defines ## @@ -284,7 +348,8 @@ class pcap_if(Structure): pcap_open_offline.argtypes = [STRING, STRING] try: - # NPCAP/LINUX ONLY function + # Functions not available on WINPCAP + # int pcap_set_rfmon (pcap_t *p) # sets whether monitor mode should be set on a capture handle when the # handle is activated. @@ -321,6 +386,24 @@ class pcap_if(Structure): pcap_activate = _lib.pcap_activate pcap_activate.restype = c_int pcap_activate.argtypes = [POINTER(pcap_t)] + + # int pcap_inject (pcap_t *p, u_char *buf, int size) + # Send a raw packet. + pcap_inject = _lib.pcap_inject + pcap_inject.restype = c_int + pcap_inject.argtypes = [POINTER(pcap_t), c_void_p, c_int] + + # const char * pcap_statustostr (int error) + # print the text of the status (error or warning) corresponding to error. + pcap_statustostr = _lib.pcap_statustostr + pcap_statustostr.restype = STRING + pcap_statustostr.argtypes = [c_int] + + # int pcap_set_buffer_size(pcap_t *p, int buffer_size) + # set the buffer size for a not-yet-activated capture handle + pcap_set_buffer_size = _lib.pcap_set_buffer_size + pcap_set_buffer_size.restype = c_int + pcap_set_buffer_size.argtypes = [POINTER(pcap_t), c_int] except AttributeError: pass @@ -417,10 +500,10 @@ class pcap_if(Structure): pcap_breakloop.argtypes = [POINTER(pcap_t)] # int pcap_sendpacket (pcap_t *p, u_char *buf, int size) -# Send a raw packet. +# Send a raw packet, but it returns 0 on success, +# rather than returning the number of bytes written. pcap_sendpacket = _lib.pcap_sendpacket pcap_sendpacket.restype = c_int -# pcap_sendpacket.argtypes = [POINTER(pcap_t), POINTER(u_char), c_int] pcap_sendpacket.argtypes = [POINTER(pcap_t), c_void_p, c_int] # void pcap_dump (u_char *user, const struct pcap_pkthdr *h, const u_char *sp) @@ -714,7 +797,7 @@ class pcap_send_queue(Structure): ("buffer", c_char_p)] # struct pcap_rmtauth - # This structure keeps the information needed to autheticate the user on a + # This structure keeps the information needed to authenticate the user on a # remote machine class pcap_rmtauth(Structure): _fields_ = [("type", c_int), diff --git a/scapy/main.py b/scapy/main.py index 524b1ba62c8..990f27f4ba3 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -1,44 +1,54 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Main module for interactive startup. """ -from __future__ import absolute_import -from __future__ import print_function -import sys -import os -import getopt +import builtins import code -import gzip +import getopt import glob import importlib import io import logging -import types +import os +import pathlib +import shutil +import sys import warnings + +from itertools import zip_longest from random import choice # Never add any global import, in main.py, that would trigger a # warning message before the console handlers gets added in interact() -from scapy.error import log_interactive, log_loading, log_scapy, \ - Scapy_Exception, ScapyColoredFormatter -import scapy.modules.six as six +from scapy.error import ( + log_interactive, + log_loading, + Scapy_Exception, +) from scapy.themes import DefaultTheme, BlackAndWhite, apply_ipython_style from scapy.consts import WINDOWS -from scapy.compat import cast, Any, Dict, List, Optional, Union - -IGNORED = list(six.moves.builtins.__dict__) - -GLOBKEYS = [] # type: List[str] +from typing import ( + Any, + Dict, + List, + Optional, + Union, + overload, +) +from scapy.compat import ( + Literal, +) LAYER_ALIASES = { - "tls": "tls.all" + "tls": "tls.all", + "msrpce": "msrpce.all", } QUOTES = [ @@ -49,26 +59,79 @@ ("To craft a packet, you have to be a packet, and learn how to swim in " "the wires and in the waves.", "Jean-Claude Van Damme"), ("We are in France, we say Skappee. OK? Merci.", "Sebastien Chabal"), - ("Wanna support scapy? Rate it on sectools! " - "http://sectools.org/tool/scapy/", "Satoshi Nakamoto"), - ("What is dead may never die!", "Python 2"), + ("Wanna support scapy? Star us on GitHub!", "Satoshi Nakamoto"), + ("I'll be back.", "Python 2"), ] -def _probe_config_file(cf): - # type: (str) -> Union[str, None] - cf_path = os.path.join(os.path.expanduser("~"), cf) +def _probe_xdg_folder(var, default, *cf): + # type: (str, str, *str) -> Optional[pathlib.Path] + path = pathlib.Path(os.environ.get(var, default)) try: - os.stat(cf_path) - except OSError: + if not path.exists(): + # ~ folder doesn't exist. Create according to spec + # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + # "If, when attempting to write a file, the destination directory is + # non-existent an attempt should be made to create it with permission 0700." + path.mkdir(mode=0o700, exist_ok=True) + except Exception: + # There is a gazillion ways this can fail. Most notably, a read-only fs or no + # permissions to even check for folder to exist (e.x. privileges were dropped + # before scapy was started). return None - else: - return cf_path + return path.joinpath(*cf).resolve() + + +def _probe_config_folder(*cf): + # type: (str) -> Optional[pathlib.Path] + return _probe_xdg_folder( + "XDG_CONFIG_HOME", + os.path.join(os.path.expanduser("~"), ".config"), + *cf + ) + + +def _probe_cache_folder(*cf): + # type: (str) -> Optional[pathlib.Path] + return _probe_xdg_folder( + "XDG_CACHE_HOME", + os.path.join(os.path.expanduser("~"), ".cache"), + *cf + ) + + +def _probe_share_folder(*cf): + # type: (str) -> Optional[pathlib.Path] + return _probe_xdg_folder( + "XDG_DATA_HOME", + os.path.join(os.path.expanduser("~"), ".local", "share"), + *cf + ) + + +def _check_perms(file: Union[pathlib.Path, str]) -> None: + """ + Checks that the permissions of a file are properly user-specific, if sudo is used. + """ + if ( + not WINDOWS and + "SUDO_UID" in os.environ and + "SUDO_GID" in os.environ + ): + # Was started with sudo. Still, chown to the user. + try: + os.chown( + file, + int(os.environ["SUDO_UID"]), + int(os.environ["SUDO_GID"]), + ) + except Exception: + pass def _read_config_file(cf, _globals=globals(), _locals=locals(), - interactive=True): - # type: (str, Dict[str, Any], Dict[str, Any], bool) -> None + interactive=True, default=None): + # type: (str, Dict[str, Any], Dict[str, Any], bool, Optional[str]) -> None """Read a config file: execute a python file while loading scapy, that may contain some pre-configured values. @@ -77,20 +140,42 @@ def _read_config_file(cf, _globals=globals(), _locals=locals(), function. Otherwise, vars are only available from inside the scapy console. - params: - - _globals: the globals() vars - - _locals: the locals() vars - - interactive: specified whether or not errors should be printed + Parameters: + + :param _globals: the globals() vars + :param _locals: the locals() vars + :param interactive: specified whether or not errors should be printed using the scapy console or raised. + :param default: if provided, set a default value for the config file ex, content of a config.py file: 'conf.verb = 42\n' Manual loading: >>> _read_config_file("./config.py")) >>> conf.verb - 42 + 2 """ + cf_path = pathlib.Path(cf) + if not cf_path.exists(): + log_loading.debug("Config file [%s] does not exist.", cf) + if default is None: + return + # We have a default ! set it + try: + if not cf_path.parent.exists(): + cf_path.parent.mkdir(parents=True, exist_ok=True) + _check_perms(cf_path.parent) + + with cf_path.open("w") as fd: + fd.write(default) + + _check_perms(cf_path) + log_loading.debug("Config file [%s] created with default.", cf) + except OSError: + log_loading.warning("Config file [%s] could not be created.", cf, + exc_info=True) + return log_loading.debug("Loading config file [%s]", cf) try: with open(cf) as cfgf: @@ -109,24 +194,70 @@ def _read_config_file(cf, _globals=globals(), _locals=locals(), cf) -def _validate_local(x): +def _validate_local(k): # type: (str) -> bool - """Returns whether or not a variable should be imported. - Will return False for any default modules (sys), or if - they are detected as private vars (starting with a _)""" - global IGNORED - return x[0] != "_" and x not in IGNORED + """Returns whether or not a variable should be imported.""" + return k[0] != "_" and k not in ["range", "map"] + + +# This is ~/.config/scapy +SCAPY_CONFIG_FOLDER = _probe_config_folder("scapy") +SCAPY_CACHE_FOLDER = _probe_cache_folder("scapy") + +if SCAPY_CONFIG_FOLDER: + DEFAULT_PRESTART_FILE: Optional[str] = str(SCAPY_CONFIG_FOLDER / "prestart.py") + DEFAULT_STARTUP_FILE: Optional[str] = str(SCAPY_CONFIG_FOLDER / "startup.py") +else: + DEFAULT_PRESTART_FILE = None + DEFAULT_STARTUP_FILE = None + +# https://github.com/scop/bash-completion/blob/main/README.md#faq +if "BASH_COMPLETION_USER_DIR" in os.environ: + BASH_COMPLETION_USER_DIR: Optional[pathlib.Path] = pathlib.Path( + os.environ["BASH_COMPLETION_USER_DIR"] + ) +else: + BASH_COMPLETION_USER_DIR = _probe_share_folder("bash-completion") + +if BASH_COMPLETION_USER_DIR: + BASH_COMPLETION_FOLDER: Optional[pathlib.Path] = ( + BASH_COMPLETION_USER_DIR / "completions" + ) +else: + BASH_COMPLETION_FOLDER = None + + +# Default scapy prestart.py config file + +DEFAULT_PRESTART = """ +# Scapy CLI 'pre-start' config file +# see https://scapy.readthedocs.io/en/latest/api/scapy.config.html#scapy.config.Conf +# for all available options + +# default interpreter +conf.interactive_shell = "auto" +# color theme (DefaultTheme, BrightTheme, ColorOnBlackTheme, BlackAndWhite, ...) +conf.color_theme = DefaultTheme() -DEFAULT_PRESTART_FILE = _probe_config_file(".scapy_prestart.py") -DEFAULT_STARTUP_FILE = _probe_config_file(".scapy_startup.py") -SESSION = {} # type: Dict[str, Any] +# disable INFO: tags related to dependencies missing +# log_loading.setLevel(logging.WARNING) + +# extensions to load by default +conf.load_extensions = [ + # "scapy-red", + # "scapy-rpc", +] + +# force-use libpcap +# conf.use_pcap = True +""".strip() def _usage(): # type: () -> None print( - "Usage: scapy.py [-s sessionfile] [-c new_startup_file] " + "Usage: scapy.py [-c new_startup_file] " "[-p new_prestart_file] [-C] [-P] [-H]\n" "Args:\n" "\t-H: header-less start\n" @@ -136,6 +267,31 @@ def _usage(): sys.exit(0) +def _add_bash_autocompletion(fname: str, script: pathlib.Path) -> None: + """ + Util function used most notably in setup.py to add a bash autocompletion script. + """ + try: + if BASH_COMPLETION_FOLDER is None: + raise OSError() + + # If already defined, exit. + dest = BASH_COMPLETION_FOLDER / fname + if dest.exists(): + return + + # Check that bash autocompletion folder exists + if not BASH_COMPLETION_FOLDER.exists(): + BASH_COMPLETION_FOLDER.mkdir(parents=True, exist_ok=True) + _check_perms(BASH_COMPLETION_FOLDER) + + # Copy file + shutil.copy(script, BASH_COMPLETION_FOLDER) + except OSError: + log_loading.warning("Bash autocompletion script could not be copied.", + exc_info=True) + + ###################### # Extension system # ###################### @@ -151,7 +307,7 @@ def _load(module, globals_dict=None, symb_list=None): """ if globals_dict is None: - globals_dict = six.moves.builtins.__dict__ + globals_dict = builtins.__dict__ try: mod = importlib.import_module(module) if '__all__' in mod.__dict__: @@ -162,7 +318,7 @@ def _load(module, globals_dict=None, symb_list=None): globals_dict[name] = mod.__dict__[name] else: # only import non-private symbols - for name, sym in six.iteritems(mod.__dict__): + for name, sym in mod.__dict__.items(): if _validate_local(name): if symb_list is not None: symb_list.append(name) @@ -217,7 +373,7 @@ def list_contrib(name=None, # type: Optional[str] ret=False, # type: bool _debug=False # type: bool ): - # type: (...) -> Optional[List[Dict[str, Union[str, None]]]] + # type: (...) -> Optional[List[Dict[str, str]]] """Show the list of all existing contribs. :param name: filter to search the contribs @@ -236,7 +392,7 @@ def list_contrib(name=None, # type: Optional[str] name = "*.py" elif "*" not in name and "?" not in name and not name.endswith(".py"): name += ".py" - results = [] # type: List[Dict[str, Union[str, None]]] + results = [] # type: List[Dict[str, str]] dir_path = os.path.join(os.path.dirname(__file__), "contrib") if sys.version_info >= (3, 5): name = os.path.join(dir_path, "**", name) @@ -250,17 +406,17 @@ def list_contrib(name=None, # type: Optional[str] continue if mod.endswith(".py"): mod = mod[:-3] - desc = {"description": None, "status": None, "name": mod} + desc = {"description": "", "status": "", "name": mod} with io.open(f, errors="replace") as fd: - for l in fd: - if l[0] != "#": + for line in fd: + if line[0] != "#": continue - p = l.find("scapy.contrib.") + p = line.find("scapy.contrib.") if p >= 0: p += 14 - q = l.find("=", p) - key = l[p:q].strip() - value = l[q + 1:].strip() + q = line.find("=", p) + key = line[p:q].strip() + value = line[q + 1:].strip() desc[key] = value if desc["status"] == "skip": break @@ -289,6 +445,9 @@ def list_contrib(name=None, # type: Optional[str] def update_ipython_session(session): # type: (Dict[str, Any]) -> None """Updates IPython session with a custom one""" + if "_oh" not in session: + session["_oh"] = session["Out"] = {} + session["In"] = {} try: from IPython import get_ipython get_ipython().user_ns.update(session) @@ -296,177 +455,93 @@ def update_ipython_session(session): pass -def save_session(fname="", session=None, pickleProto=-1): - # type: (str, Optional[Dict[str, Any]], int) -> None - """Save current Scapy session to the file specified in the fname arg. - - params: - - fname: file to save the scapy session in - - session: scapy session to use. If None, the console one will be used - - pickleProto: pickle proto version (default: -1 = latest)""" - from scapy import utils - from scapy.config import conf, ConfClass - if not fname: - fname = conf.session - if not fname: - conf.session = fname = utils.get_temp_file(keep=True) - log_interactive.info("Use [%s] as session file" % fname) - - if not session: - try: - from IPython import get_ipython - session = get_ipython().user_ns - except Exception: - session = six.moves.builtins.__dict__["scapy_session"] - - to_be_saved = cast(Dict[str, Any], session).copy() - if "__builtins__" in to_be_saved: - del(to_be_saved["__builtins__"]) - - for k in list(to_be_saved): - i = to_be_saved[k] - if hasattr(i, "__module__") and (k[0] == "_" or - i.__module__.startswith("IPython")): - del(to_be_saved[k]) - if isinstance(i, ConfClass): - del(to_be_saved[k]) - elif isinstance(i, (type, type, types.ModuleType)): - if k[0] != "_": - log_interactive.error("[%s] (%s) can't be saved.", k, - type(to_be_saved[k])) - del(to_be_saved[k]) - - try: - os.rename(fname, fname + ".bak") - except OSError: - pass +def _scapy_prestart_builtins(): + # type: () -> Dict[str, Any] + """Load Scapy prestart and return all builtins""" + return { + k: v + for k, v in importlib.import_module(".config", "scapy").__dict__.copy().items() + if _validate_local(k) + } - f = gzip.open(fname, "wb") - six.moves.cPickle.dump(to_be_saved, f, pickleProto) - f.close() +def _scapy_builtins(): + # type: () -> Dict[str, Any] + """Load Scapy and return all builtins""" + return { + k: v + for k, v in importlib.import_module(".all", "scapy").__dict__.copy().items() + if _validate_local(k) + } -def load_session(fname=None): - # type: (Optional[Union[str, None]]) -> None - """Load current Scapy session from the file specified in the fname arg. - This will erase any existing session. - params: - - fname: file to load the scapy session from""" +def _scapy_exts(): + # type: () -> Dict[str, Any] + """Load Scapy exts and return their builtins""" from scapy.config import conf - if fname is None: - fname = conf.session - try: - s = six.moves.cPickle.load(gzip.open(fname, "rb")) - except IOError: - try: - s = six.moves.cPickle.load(open(fname, "rb")) - except IOError: - # Raise "No such file exception" - raise - - scapy_session = six.moves.builtins.__dict__["scapy_session"] - scapy_session.clear() - scapy_session.update(s) - update_ipython_session(scapy_session) + res = {} + for modname, spec in conf.exts.all_specs.items(): + if spec.default: + mod = sys.modules[modname] + res.update({ + k: v + for k, v in mod.__dict__.copy().items() + if _validate_local(k) + }) + return res + + +@overload +def init_session(mydict, # type: Optional[Union[Dict[str, Any], None]] + ret, # type: Literal[True] + ): + # type: (...) -> Dict[str, Any] + pass - log_loading.info("Loaded session [%s]" % fname) +@overload +def init_session(mydict=None, # type: Optional[Union[Dict[str, Any], None]] + ret=False, # type: Literal[False] + ): + # type: (...) -> None + pass -def update_session(fname=None): - # type: (Optional[Union[str, None]]) -> None - """Update current Scapy session from the file specified in the fname arg. - params: - - fname: file to load the scapy session from""" +def init_session(mydict=None, # type: Optional[Union[Dict[str, Any], None]] + ret=False, # type: bool + ): + # type: (...) -> Union[Dict[str, Any], None] from scapy.config import conf - if fname is None: - fname = conf.session - try: - s = six.moves.cPickle.load(gzip.open(fname, "rb")) - except IOError: - s = six.moves.cPickle.load(open(fname, "rb")) - scapy_session = six.moves.builtins.__dict__["scapy_session"] - scapy_session.update(s) - update_ipython_session(scapy_session) + # Load Scapy + scapy_builtins = _scapy_builtins() -def init_session(session_name, # type: Optional[Union[str, None]] - mydict=None # type: Optional[Union[Dict[str, Any], None]] - ): - # type: (...) -> None - from scapy.config import conf - global SESSION - global GLOBKEYS - - scapy_builtins = {k: v - for k, v in six.iteritems( - importlib.import_module(".all", "scapy").__dict__ - ) - if _validate_local(k)} - six.moves.builtins.__dict__.update(scapy_builtins) - GLOBKEYS.extend(scapy_builtins) - GLOBKEYS.append("scapy_session") - - if session_name: - try: - os.stat(session_name) - except OSError: - log_loading.info("New session [%s]" % session_name) - else: - try: - try: - SESSION = six.moves.cPickle.load(gzip.open(session_name, - "rb")) - except IOError: - SESSION = six.moves.cPickle.load(open(session_name, "rb")) - log_loading.info("Using session [%s]" % session_name) - except EOFError: - log_loading.error("Error opening session [%s]" % session_name) - except AttributeError: - log_loading.error("Error opening session [%s]. " - "Attribute missing" % session_name) - - if SESSION: - if "conf" in SESSION: - conf.configure(SESSION["conf"]) - conf.session = session_name - SESSION["conf"] = conf - else: - conf.session = session_name - else: - conf.session = session_name - SESSION = {"conf": conf} - else: - SESSION = {"conf": conf} + # Load exts + scapy_builtins.update(_scapy_exts()) - six.moves.builtins.__dict__["scapy_session"] = SESSION + SESSION = {"conf": conf} # type: Dict[str, Any] + + SESSION.update(scapy_builtins) + SESSION["_scpybuiltins"] = scapy_builtins.keys() + builtins.__dict__["scapy_session"] = SESSION if mydict is not None: - six.moves.builtins.__dict__["scapy_session"].update(mydict) + builtins.__dict__["scapy_session"].update(mydict) update_ipython_session(mydict) - GLOBKEYS.extend(mydict) + if ret: + return SESSION + return None + ################ # Main # ################ -def scapy_delete_temp_files(): - # type: () -> None - from scapy.config import conf - for f in conf.temp_files: - try: - os.unlink(f) - except Exception: - pass - del(conf.temp_files[:]) - - def _prepare_quote(quote, author, max_len=78): # type: (str, str, int) -> List[str] """This function processes a quote and returns a string that is ready -to be used in the fancy prompt. +to be used in the fancy banner. """ _quote = quote.split(' ') @@ -490,39 +565,97 @@ def _len(line): return lines -def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): - # type: (Optional[Any], Optional[Any], Optional[Any], int) -> None - """Starts Scapy's console.""" - global SESSION - global GLOBKEYS +def get_fancy_banner(mini: Optional[bool] = None) -> str: + """ + Generates the fancy Scapy banner - try: - if WINDOWS: - # colorama is bundled within IPython. - # logging.StreamHandler will be overwritten when called, - # We can't wait for IPython to call it - import colorama - colorama.init() - # Success - console_handler = logging.StreamHandler() - console_handler.setFormatter( - ScapyColoredFormatter( - "%(levelname)s: %(message)s", - ) - ) - except ImportError: - # Failure: ignore colors in the logger - console_handler = logging.StreamHandler() - console_handler.setFormatter( - logging.Formatter( - "%(levelname)s: %(message)s", - ) + :param mini: if set, force a mini banner or not. Otherwise detect + """ + from scapy.config import conf + from scapy.utils import get_terminal_width + if mini is None: + mini_banner = (get_terminal_width() or 84) <= 75 + else: + mini_banner = mini + + the_logo = [ + " ", + " aSPY//YASa ", + " apyyyyCY//////////YCa ", + " sY//////YSpcs scpCY//Pp ", + " ayp ayyyyyyySCP//Pp syY//C ", + " AYAsAYYYYYYYY///Ps cY//S", + " pCCCCY//p cSSps y//Y", + " SPPPP///a pP///AC//Y", + " A//A cyP////C", + " p///Ac sC///a", + " P////YCpc A//A", + " scccccp///pSP///p p//Y", + " sY/////////y caa S//P", + " cayCyayP//Ya pY/Ya", + " sY/PsY////YCc aC//Yp ", + " sc sccaCY//PCypaapyCP//YSs ", + " spCPY//////YPSps ", + " ccaacs ", + " ", + ] + + # Used on mini screens + the_logo_mini = [ + " .SYPACCCSASYY ", + "P /SCS/CCS ACS", + " /A AC", + " A/PS /SPPS", + " YP (SC", + " SPS/A. SC", + " Y/PACC PP", + " PY*AYC CAA", + " YYCY//SCYP ", + ] + + the_banner = [ + "", + "", + " |", + " | Welcome to Scapy", + " | Version %s" % conf.version, + " |", + " | https://github.com/secdev/scapy", + " |", + " | Have fun!", + " |", + ] + + if mini_banner: + the_logo = the_logo_mini + the_banner = [x[2:] for x in the_banner[3:-1]] + the_banner = [""] + the_banner + [""] + else: + quote, author = choice(QUOTES) + the_banner.extend(_prepare_quote(quote, author, max_len=39)) + the_banner.append(" |") + return "\n".join( + logo + banner for logo, banner in zip_longest( + (conf.color_theme.logo(line) for line in the_logo), + (conf.color_theme.success(line) for line in the_banner), + fillvalue="" ) - log_scapy.addHandler(console_handler) + ) + +def interact(mydict=None, + argv=None, + mybanner=None, + mybanneronly=False, + loglevel=logging.INFO): + # type: (Optional[Any], Optional[Any], Optional[Any], bool, int) -> None + """ + Starts Scapy's console. + """ # We're in interactive mode, let's throw the DeprecationWarnings warnings.simplefilter("always") + # Set interactive mode, load the color scheme from scapy.config import conf conf.interactive = True conf.color_theme = DefaultTheme() @@ -532,27 +665,24 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): STARTUP_FILE = DEFAULT_STARTUP_FILE PRESTART_FILE = DEFAULT_PRESTART_FILE - session_name = None - if argv is None: argv = sys.argv try: opts = getopt.getopt(argv[1:], "hs:Cc:Pp:d:H") - for opt, parm in opts[0]: + for opt, param in opts[0]: if opt == "-h": _usage() elif opt == "-H": - conf.fancy_prompt = False - conf.verb = 30 - elif opt == "-s": - session_name = parm + conf.fancy_banner = False + conf.verb = 1 + conf.logLevel = logging.WARNING elif opt == "-c": - STARTUP_FILE = parm + STARTUP_FILE = param elif opt == "-C": STARTUP_FILE = None elif opt == "-p": - PRESTART_FILE = parm + PRESTART_FILE = param elif opt == "-P": PRESTART_FILE = None elif opt == "-d": @@ -570,110 +700,170 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): # Reset sys.argv, otherwise IPython thinks it is for him sys.argv = sys.argv[:1] - init_session(session_name, mydict) + if PRESTART_FILE: + _read_config_file( + PRESTART_FILE, + interactive=True, + _locals=_scapy_prestart_builtins(), + default=DEFAULT_PRESTART, + ) + + SESSION = init_session(mydict=mydict, ret=True) if STARTUP_FILE: - _read_config_file(STARTUP_FILE, interactive=True) - if PRESTART_FILE: - _read_config_file(PRESTART_FILE, interactive=True) + _read_config_file( + STARTUP_FILE, + interactive=True, + _locals=SESSION + ) - if not conf.interactive_shell or conf.interactive_shell.lower() in [ - "ipython", "auto" - ]: - try: - import IPython - from IPython import start_ipython - except ImportError: + # Load extensions (Python 3.8 Only) + if sys.version_info >= (3, 8): + conf.exts.loadall() + + if conf.fancy_banner: + banner_text = get_fancy_banner() + else: + banner_text = "Welcome to Scapy (%s)" % conf.version + + # Make sure the history file has proper permissions + try: + if not pathlib.Path(conf.histfile).exists(): + pathlib.Path(conf.histfile).touch() + _check_perms(conf.histfile) + except OSError: + pass + + # Configure interactive terminal + + if conf.interactive_shell not in [ + "ipython", + "python", + "ptpython", + "ptipython", + "bpython", + "auto"]: + log_loading.warning("Unknown conf.interactive_shell ! Using 'auto'") + conf.interactive_shell = "auto" + + # Auto detect available shells. + # Order: + # 1. IPython + # 2. bpython + # 3. ptpython + + _IMPORTS = { + "ipython": ["IPython"], + "bpython": ["bpython"], + "ptpython": ["ptpython"], + "ptipython": ["IPython", "ptpython"], + } + + if conf.interactive_shell == "auto": + # Auto detect + for imp in ["IPython", "bpython", "ptpython"]: + try: + importlib.import_module(imp) + conf.interactive_shell = imp.lower() + break + except ImportError: + continue + else: log_loading.warning( - "IPython not available. Using standard Python shell " - "instead.\nAutoCompletion, History are disabled." + "No alternative Python interpreters found ! " + "Using standard Python shell instead." ) - if WINDOWS: - log_loading.warning( - "On Windows, colors are also disabled" - ) - conf.color_theme = BlackAndWhite() - IPYTHON = False - else: - IPYTHON = True - else: - IPYTHON = False + conf.interactive_shell = "python" - if conf.fancy_prompt: - from scapy.utils import get_terminal_width - mini_banner = (get_terminal_width() or 84) <= 75 + if conf.interactive_shell in _IMPORTS: + # Check import + for imp in _IMPORTS[conf.interactive_shell]: + try: + importlib.import_module(imp) + except ImportError: + log_loading.warning("%s requested but not found !" % imp) + conf.interactive_shell = "python" - the_logo = [ - " ", - " aSPY//YASa ", - " apyyyyCY//////////YCa ", - " sY//////YSpcs scpCY//Pp ", - " ayp ayyyyyyySCP//Pp syY//C ", - " AYAsAYYYYYYYY///Ps cY//S", - " pCCCCY//p cSSps y//Y", - " SPPPP///a pP///AC//Y", - " A//A cyP////C", - " p///Ac sC///a", - " P////YCpc A//A", - " scccccp///pSP///p p//Y", - " sY/////////y caa S//P", - " cayCyayP//Ya pY/Ya", - " sY/PsY////YCc aC//Yp ", - " sc sccaCY//PCypaapyCP//YSs ", - " spCPY//////YPSps ", - " ccaacs ", - " ", - ] - - # Used on mini screens - the_logo_mini = [ - " .SYPACCCSASYY ", - "P /SCS/CCS ACS", - " /A AC", - " A/PS /SPPS", - " YP (SC", - " SPS/A. SC", - " Y/PACC PP", - " PY*AYC CAA", - " YYCY//SCYP ", - ] - - the_banner = [ - "", - "", - " |", - " | Welcome to Scapy", - " | Version %s" % conf.version, - " |", - " | https://github.com/secdev/scapy", - " |", - " | Have fun!", - " |", - ] - - if mini_banner: - the_logo = the_logo_mini - the_banner = [x[2:] for x in the_banner[3:-1]] - the_banner = [""] + the_banner + [""] + # Default shell + if conf.interactive_shell == "python": + disabled = ["History"] + if WINDOWS: + disabled.append("Colors") + conf.color_theme = BlackAndWhite() else: - quote, author = choice(QUOTES) - the_banner.extend(_prepare_quote(quote, author, max_len=39)) - the_banner.append(" |") - banner_text = "\n".join( - logo + banner for logo, banner in six.moves.zip_longest( - (conf.color_theme.logo(line) for line in the_logo), - (conf.color_theme.success(line) for line in the_banner), - fillvalue="" + try: + # Bad completer.. but better than nothing + import rlcompleter + import readline + readline.set_completer( + rlcompleter.Completer(namespace=SESSION).complete + ) + readline.parse_and_bind('tab: complete') + except ImportError: + disabled.insert(0, "AutoCompletion") + # Display warning when using the default REPL + log_loading.info( + "Using the default Python shell: %s %s disabled." % ( + ",".join(disabled), + "is" if len(disabled) == 1 else "are" ) ) - else: - banner_text = "Welcome to Scapy (%s)" % conf.version - if mybanner is not None: - banner_text += "\n" - banner_text += mybanner - if IPYTHON: - banner = banner_text + " using IPython %s\n" % IPython.__version__ + # ptpython configure function + def ptpython_configure(repl): + # type: (Any) -> None + # Hide status bar + repl.show_status_bar = False + # Complete while typing (versus only when pressing tab) + repl.complete_while_typing = False + # Enable auto-suggestions + repl.enable_auto_suggest = True + # Disable exit confirmation + repl.confirm_exit = False + # Show signature + repl.show_signature = True + # Apply Scapy color theme: TODO + # repl.install_ui_colorscheme("scapy", + # Style.from_dict(_custom_ui_colorscheme)) + # repl.use_ui_colorscheme("scapy") + + # Extend banner text + if conf.interactive_shell in ["ipython", "ptipython"]: + import IPython + if conf.interactive_shell == "ptipython": + banner = banner_text + " using IPython %s" % IPython.__version__ + try: + from importlib.metadata import version + ptpython_version = " " + version('ptpython') + except ImportError: + ptpython_version = "" + banner += " and ptpython%s" % ptpython_version + else: + banner = banner_text + " using IPython %s" % IPython.__version__ + elif conf.interactive_shell == "ptpython": + try: + from importlib.metadata import version + ptpython_version = " " + version('ptpython') + except ImportError: + ptpython_version = "" + banner = banner_text + " using ptpython%s" % ptpython_version + elif conf.interactive_shell == "bpython": + import bpython + banner = banner_text + " using bpython %s" % bpython.__version__ + + if mybanner is not None: + if mybanneronly: + banner = "" + banner += "\n" + banner += mybanner + + # Start IPython or ptipython + if conf.interactive_shell in ["ipython", "ptipython"]: + banner += "\n" + if conf.interactive_shell == "ptipython": + from ptpython.ipython import embed + else: + from IPython import embed try: from traitlets.config.loader import Config except ImportError: @@ -682,7 +872,7 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): "available." ) try: - start_ipython( + embed( display_banner=False, user_ns=SESSION, exec_lines=["print(\"\"\"" + banner + "\"\"\")"] @@ -699,32 +889,61 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): # Set "classic" prompt style when launched from # run_scapy(.bat) files Register and apply scapy # color+prompt style - apply_ipython_style(shell=cfg.TerminalInteractiveShell) - cfg.TerminalInteractiveShell.confirm_exit = False - cfg.TerminalInteractiveShell.separate_in = u'' + apply_ipython_style(shell=cfg.InteractiveShellEmbed) + cfg.InteractiveShellEmbed.confirm_exit = False + cfg.InteractiveShellEmbed.separate_in = u'' if int(IPython.__version__[0]) >= 6: - cfg.TerminalInteractiveShell.term_title_format = ("Scapy v%s" % - conf.version) + cfg.InteractiveShellEmbed.term_title = True + cfg.InteractiveShellEmbed.term_title_format = ("Scapy %s" % + conf.version) + # As of IPython 6-7, the jedi completion module is a dumpster + # of fire that should be scrapped never to be seen again. + # This is why the following defaults to False. Feel free to hurt + # yourself (#GH4056) :P + cfg.Completer.use_jedi = conf.ipython_use_jedi else: - cfg.TerminalInteractiveShell.term_title = False + cfg.InteractiveShellEmbed.term_title = False cfg.HistoryAccessor.hist_file = conf.histfile cfg.InteractiveShell.banner1 = banner + if conf.verb < 2: + cfg.InteractiveShellEmbed.enable_tip = False # configuration can thus be specified here. + _kwargs = {} + if conf.interactive_shell == "ptipython": + _kwargs["configure"] = ptpython_configure try: - start_ipython(config=cfg, user_ns=SESSION) + embed(config=cfg, user_ns=SESSION, **_kwargs) except (AttributeError, TypeError): code.interact(banner=banner_text, local=SESSION) - else: + # Start ptpython + elif conf.interactive_shell == "ptpython": + # ptpython has special, non-default handling of __repr__ which breaks Scapy. + # For instance: >>> IP() + log_loading.warning("ptpython support is currently partially broken") + from ptpython.repl import embed + # ptpython has no banner option + banner += "\n" + print(banner) + embed( + locals=SESSION, + history_filename=conf.histfile, + title="Scapy %s" % conf.version, + configure=ptpython_configure + ) + # Start bpython + elif conf.interactive_shell == "bpython": + from bpython.curtsies import main as embed + embed( + args=["-q", "-i"], + locals_=SESSION, + banner=banner, + welcome_message="" + ) + # Start Python + elif conf.interactive_shell == "python": code.interact(banner=banner_text, local=SESSION) - - if conf.session: - save_session(conf.session, SESSION) - - for k in GLOBKEYS: - try: - del(six.moves.builtins.__dict__[k]) - except Exception: - pass + else: + raise ValueError("Invalid conf.interactive_shell") if __name__ == "__main__": diff --git a/scapy/modules/__init__.py b/scapy/modules/__init__.py index a2226fa9189..1bf976f08af 100644 --- a/scapy/modules/__init__.py +++ b/scapy/modules/__init__.py @@ -1,8 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Package of extension modules that have to be loaded explicitly. """ + +# Make sure config is loaded +import scapy.config # noqa: F401 diff --git a/scapy/modules/krack/__init__.py b/scapy/modules/krack/__init__.py index 83462abd08f..f4178b62f47 100644 --- a/scapy/modules/krack/__init__.py +++ b/scapy/modules/krack/__init__.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + """Module implementing Krack Attack on client, as a custom WPA Access Point Requires the python cryptography package v1.7+. See https://cryptography.io/ @@ -18,7 +22,7 @@ The output logs will indicate if one of the vulnerability have been triggered. Outputs for vulnerable devices: -- IV re-use!! Client seems to be vulnerable to handshake 3/4 replay +- IV reuse!! Client seems to be vulnerable to handshake 3/4 replay (CVE-2017-13077) - Broadcast packet accepted twice!! (CVE-2017-13080) - Client has installed an all zero encryption key (TK)!! diff --git a/scapy/modules/krack/automaton.py b/scapy/modules/krack/automaton.py index 04c2ba23ed7..260257b7c8b 100644 --- a/scapy/modules/krack/automaton.py +++ b/scapy/modules/krack/automaton.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + import hmac import hashlib from itertools import count @@ -12,11 +16,24 @@ from scapy.base_classes import Net from scapy.config import conf from scapy.compat import raw, chb -from scapy.consts import WINDOWS -from scapy.error import log_runtime, Scapy_Exception -from scapy.layers.dot11 import RadioTap, Dot11, Dot11AssoReq, Dot11AssoResp, \ - Dot11Auth, Dot11Beacon, Dot11Elt, Dot11EltRates, Dot11EltRSN, \ - Dot11ProbeReq, Dot11ProbeResp, RSNCipherSuite, AKMSuite +from scapy.consts import LINUX +from scapy.error import log_runtime +from scapy.layers.dot11 import ( + AKMSuite, + Dot11, + Dot11AssoReq, + Dot11AssoResp, + Dot11Auth, + Dot11Beacon, + Dot11Elt, + Dot11EltDSSSet, + Dot11EltRSN, + Dot11EltRates, + Dot11ProbeReq, + Dot11ProbeResp, + RSNCipherSuite, + RadioTap, +) from scapy.layers.eap import EAPOL from scapy.layers.l2 import ARP, LLC, SNAP, Ether from scapy.layers.dhcp import DHCP_am @@ -66,7 +83,8 @@ class KrackAP(Automaton): def __init__(self, *args, **kargs): kargs.setdefault("ll", conf.L2socket) - kargs.setdefault("monitor", True) + if not LINUX: + kargs.setdefault("monitor", True) super(KrackAP, self).__init__(*args, **kargs) def parse_args(self, ap_mac, ssid, passphrase, @@ -82,27 +100,33 @@ def parse_args(self, ap_mac, ssid, passphrase, **kwargs): """ Mandatory arguments: - @iface: interface to use (must be in monitor mode) - @ap_mac: AP's MAC - @ssid: AP's SSID - @passphrase: AP's Passphrase (min 8 char.) + + :param iface: interface to use (must be in monitor mode) + :param ap_mac: AP's MAC + :param ssid: AP's SSID + :param passphrase: AP's Passphrase (min 8 char.) Optional arguments: - @channel: used by the interface. Default 6, autodetected on windows + + :param channel: used by the interface. Default 6 Krack attacks options: - Msg 3/4 handshake replay: - double_3handshake: double the 3/4 handshake message - encrypt_3handshake: encrypt the second 3/4 handshake message - wait_3handshake: time to wait (in sec.) before sending the second 3/4 - - double GTK rekeying: - double_gtk_refresh: double the 1/2 GTK rekeying message - wait_gtk: time to wait (in sec.) before sending the GTK rekeying - arp_target_ip: Client IP to use in ARP req. (to detect attack success) - If None, use a DHCP server - arp_source_ip: Server IP to use in ARP req. (to detect attack success) - If None, use the DHCP server gateway address + + :param double_3handshake: double the 3/4 handshake message + :param encrypt_3handshake: encrypt the second 3/4 handshake message + :param wait_3handshake: time to wait (in sec.) before sending the + second 3/4 + + - double GTK rekeying: + + :param double_gtk_refresh: double the 1/2 GTK rekeying message + :param wait_gtk: time to wait (in sec.) before sending the GTK rekeying + :param arp_target_ip: Client IP to use in ARP req. (to detect attack + success). If None, use a DHCP server + :param arp_source_ip: Server IP to use in ARP req. (to detect attack + success). If None, use the DHCP server gateway address """ super(KrackAP, self).parse_args(**kwargs) @@ -111,13 +135,7 @@ def parse_args(self, ap_mac, ssid, passphrase, self.ssid = ssid self.passphrase = passphrase if channel is None: - if WINDOWS: - try: - channel = kwargs.get("iface", conf.iface).channel() - except (Scapy_Exception, AttributeError): - channel = 6 - else: - channel = 6 + channel = 6 self.channel = channel # Internal structures @@ -220,13 +238,14 @@ def build_ap_info_pkt(self, layer_cls, dest): """Build a packet with info describing the current AP For beacon / proberesp use """ + ts = int(time.time() * 1e6) & 0xffffffffffffffff return RadioTap() \ / Dot11(addr1=dest, addr2=self.mac, addr3=self.mac) \ - / layer_cls(timestamp=0, beacon_interval=100, + / layer_cls(timestamp=ts, beacon_interval=100, cap='ESS+privacy') \ / Dot11Elt(ID="SSID", info=self.ssid) \ / Dot11EltRates(rates=[130, 132, 139, 150, 12, 18, 24, 36]) \ - / Dot11Elt(ID="DSset", info=chb(self.channel)) \ + / Dot11EltDSSSet(channel=self.channel) \ / Dot11EltRSN(group_cipher_suite=RSNCipherSuite(cipher=0x2), pairwise_cipher_suites=[RSNCipherSuite(cipher=0x2)], akm_suites=[AKMSuite(suite=0x2)]) @@ -313,7 +332,7 @@ def build_GTK_KDE(self): ]) def send_wpa_enc(self, data, iv, seqnum, dest, mic_key, - key_idx=0, additionnal_flag=["from-DS"], + key_idx=0, additionnal_flag=["from_DS"], encrypt_key=None): """Send an encrypted packet with content @data, using IV @iv, sequence number @seqnum, MIC key @mic_key @@ -486,7 +505,7 @@ def send_auth_response(self, pkt): log_runtime.warning("Client %s connected!", self.client) # Launch DHCP Server - self.dhcp_server.run() + self.dhcp_server() rep = RadioTap() rep /= Dot11(addr1=self.client, addr2=self.mac, addr3=self.mac) @@ -532,7 +551,7 @@ def send_wpa_handshake_1(self): addr1=self.client, addr2=self.mac, addr3=self.mac, - FCfield='from-DS', + FCfield='from_DS', SC=(next(self.seq_num) << 4), ) rep /= LLC(dsap=0xaa, ssap=0xaa, ctrl=3) @@ -576,7 +595,7 @@ def send_wpa_handshake_3(self, pkt): addr1=self.client, addr2=self.mac, addr3=self.mac, - FCfield='from-DS', + FCfield='from_DS', SC=(next(self.seq_num) << 4), ) @@ -633,7 +652,7 @@ def krack_proceed(self, send_3handshake=False, send_gtk=False): addr1=self.client, addr2=self.mac, addr3=self.mac, - FCfield='from-DS', + FCfield='from_DS', SC=(next(self.seq_num) << 4), subtype=0, type="Data", @@ -703,7 +722,7 @@ def extract_iv(self, pkt): self.last_iv = iv else: if iv <= self.last_iv: - log_runtime.warning("IV re-use!! Client seems to be " + log_runtime.warning("IV reuse!! Client seems to be " "vulnerable to handshake 3/4 replay " "(CVE-2017-13077)" ) diff --git a/scapy/modules/krack/crypto.py b/scapy/modules/krack/crypto.py index a4803defb67..47b7c9364f1 100644 --- a/scapy/modules/krack/crypto.py +++ b/scapy/modules/krack/crypto.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + import hashlib import hmac from struct import unpack, pack @@ -6,8 +10,6 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms from cryptography.hazmat.backends import default_backend -import scapy.modules.six as six -from scapy.modules.six.moves import range from scapy.compat import orb, chb from scapy.layers.dot11 import Dot11TKIP from scapy.utils import mac2str @@ -155,7 +157,7 @@ def gen_TKIP_RC4_key(TSC, TA, TK): assert len(TSC) == 6 assert len(TA) == 6 assert len(TK) == 16 - assert all(isinstance(x, six.integer_types) for x in TSC + TA + TK) + assert all(isinstance(x, int) for x in TSC + TA + TK) # Phase 1 # 802.11i p.54 diff --git a/scapy/modules/ldaphero.py b/scapy/modules/ldaphero.py new file mode 100644 index 00000000000..cd4bd4fb658 --- /dev/null +++ b/scapy/modules/ldaphero.py @@ -0,0 +1,2134 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +LDAP Hero: a LDAP browser based on the Scapy LDAP client +""" + +import uuid + +from scapy.layers.ldap import ( + LDAP_AttributeValue, + LDAP_BIND_MECHS, + LDAP_Client, + LDAP_CONTROL_ACCESS_RIGHTS, + LDAP_Control, + LDAP_DS_ACCESS_RIGHTS, + LDAP_Exception, + LDAP_ModifyRequestChange, + LDAP_PartialAttribute, + LDAP_PROPERTY_SET, + LDAP_serverSDFlagsControl, +) +from scapy.layers.dcerpc import ( + DCERPC_Transport, + NDRUnion, + DCE_C_AUTHN_LEVEL, + find_dcerpc_interface, +) +from scapy.layers.gssapi import SSP +from scapy.layers.msrpce.rpcclient import ( + DCERPC_Client, +) +from scapy.layers.msrpce.msdrsr import ( + DRS_EXTENSIONS_INT, + DRS_EXTENSIONS, + DRS_MSG_CRACKREQ_V1, + IDL_DRSBind_Request, + IDL_DRSCrackNames_Request, + NTDSAPI_CLIENT_GUID, +) +from scapy.layers.ntlm import NTLMSSP +from scapy.layers.kerberos import KerberosSSP +from scapy.layers.spnego import SPNEGOSSP +from scapy.layers.windows.security import ( + SECURITY_DESCRIPTOR, + WELL_KNOWN_SIDS, + WINNT_ACE_FLAGS, + WINNT_ACE_HEADER, + WINNT_SID, + WINNT_ACCESS_ALLOWED_ACE, + WINNT_ACCESS_ALLOWED_OBJECT_ACE, + WINNT_ACCESS_DENIED_OBJECT_ACE, + WINNT_ACCESS_DENIED_ACE, + WINNT_SYSTEM_AUDIT_OBJECT_ACE, + WINNT_SYSTEM_AUDIT_ACE, +) +from scapy.utils import valid_ip + +try: + import tkinter as tk + from tkinter import ttk, messagebox +except ImportError: + raise ImportError("tkinter is not installed (`apt install python3-tk` on debian)") + + +class AutoHideScrollbar(ttk.Scrollbar): + def __init__(self, *args, **kwargs): + self.shown = False + super(AutoHideScrollbar, self).__init__(*args, **kwargs) + + def set(self, first, last): + show = float(first) > 0 or float(last) < 1 + if show and not self.shown: + self.grid(row=0, column=1, sticky="nsew") + elif not show and self.shown: + self.grid_forget() + self.shown = show + super(AutoHideScrollbar, self).set(first, last) + + +class BasePopup: + """ + A tkinter wrapper used to have a popup window with basic controls + """ + + def __init__(self, parent): + # Get dialog + self.dlg = tk.Toplevel(parent) + self.parent = parent + self.cancelled = False + + # Configure some bindings + self.dlg.bind("", self.dismiss) + self.dlg.bind("", self.dismiss) + + def dismiss(self, *_) -> None: + """ + Close the popup + """ + self.dlg.grab_release() + self.dlg.destroy() + + def cancel(self) -> None: + """ + Cancel the popup + """ + self.cancelled = True + self.dismiss() + + def run(self) -> False: + """ + Show the popup. Returns True if cancelled, False otherwise. + """ + self.dlg.protocol("WM_DELETE_WINDOW", self.dismiss) + self.dlg.transient(self.parent) + self.dlg.wait_visibility() + self.dlg.grab_set() + self.dlg.wait_window() + + return self.cancelled + + +class LDAPHero: + r""" + LDAP Hero - LDAP GUI browser over Scapy's LDAP_Client + + :param ssp: if provided, use this SSP for auth. + :param mech: the LDAP_BIND_MECHS to use when binding. + :param sign: request signature by default + :param encrypt: request encryption by default + :param host: auto-connect to a specific host + :param port: the port to connect to (default: 389/636) + (This is only in use when using 'host') + :param ssl: whether to use SSL to connect or not + (This is only in use when using 'host') + + Authentication parameters: + + :param UPN: the upn to use (DOMAIN/USER, DOMAIN\USER, USER@DOMAIN or USER) + :param kerberos_required: require kerberos + :param password: if provided, used for auth + :param HashNt: if provided, used for auth (NTLM) + :param HashAes256Sha96: if provided, used for auth (Kerberos) + :param HashAes128Sha96: if provided, used for auth (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. + :param use_winssp: (bool) (only works on Windows). Use implicit authentication + through WinSSP. + """ + + def __init__( + self, + ssp: SSP = None, + mech: LDAP_BIND_MECHS = LDAP_BIND_MECHS.SASL_GSS_SPNEGO, + sign: bool = True, + encrypt: bool = False, + host: str = None, + port: int = None, + ssl: bool = False, + # Authentication + UPN: str = None, + password: str = None, + kerberos_required: bool = False, + HashNt: bytes = None, + HashAes256Sha96: bytes = None, + HashAes128Sha96: bytes = None, + use_krb5ccname: bool = False, + use_winssp: bool = False, + ): + self.client = LDAP_Client() + if ( + ssp is None + and mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO + and (UPN and host or use_winssp) + ): + # We allow the SSP to be provided through arguments. + # In that case, use SPNEGO + ssp = SPNEGOSSP.from_cli_arguments( + UPN=UPN, + target=host, + password=password, + HashNt=HashNt, + HashAes256Sha96=HashAes256Sha96, + HashAes128Sha96=HashAes128Sha96, + kerberos_required=kerberos_required, + use_krb5ccname=use_krb5ccname, + use_winssp=use_winssp, + ) + self.ssp = ssp + self.mech = mech + if mech == LDAP_BIND_MECHS.SIMPLE: + self.simple_username = UPN + self.simple_password = password + else: + self.simple_username = self.simple_password = None + self.sign = sign + self.encrypt = encrypt + # Session parameters + self.connected = False + self.bound = False + self.host = host + self.port = port + self.ssl = ssl + self.dns_domain_name = "" + self.rootDSE = {} + self.sids = dict(WELL_KNOWN_SIDS) + self.sidscombo = {} + self.guids = {} + self.guidscombo = {"None": None} + self.guidscomboobject = {"None": None} + self.loadedSchemaIDGuids = False + self.crop_output = None + self.currently_editing = None + # UI cache + self.lastSearchString = "" + # Launch + self.main() + + def connect(self): + """ + Connect command. + """ + # If host is None, we need to ask for it via a dialog. + if self.host is None: + # Get dialog + popup = BasePopup(self.root) + dlg = popup.dlg + + # Connect UI + serverv = tk.StringVar() + serverv.set(self.host or "") + ttk.Label(dlg, text="Server").grid(row=0, column=0) + serverf = tk.Entry(dlg, textvariable=serverv) + serverf.grid(row=0, column=1) + + portv = tk.StringVar() + portv.set("389") + ttk.Label(dlg, text="Port").grid(row=1, column=0) + tk.Entry(dlg, textvariable=portv).grid(row=1, column=1) + + sslv = tk.BooleanVar() + ttk.Label(dlg, text="SSL").grid(row=2, column=0) + ttk.Checkbutton(dlg, variable=sslv).grid(row=2, column=1) + + ttk.Button(dlg, text="OK", command=popup.dismiss).grid(row=3, column=0) + ttk.Button(dlg, text="Cancel", command=popup.cancel).grid(row=3, column=1) + + serverf.focus() + + # Setup + if popup.run(): + # Cancelled + return + + # Get values + self.host = serverv.get() + try: + self.port = int(portv.get()) + except ValueError: + return + self.ssl = sslv.get() + + # Connect now ! + self.tprint( + "client.connect(host='%s', port=%s, ssl=%s)" + % (self.host, self.port, self.ssl) + ) + try: + self.client.connect(self.host, port=self.port, use_ssl=self.ssl) + except Exception as ex: + self.tprint(str(ex)) + self.host = None + raise + self.tprint("Established connection to %s." % self.host) + self.connected = True + + # Alright, change the UI. + self.menu_connection.entryconfig("Connect", state=tk.DISABLED) + self.menu_connection.entryconfig("Bind", state=tk.ACTIVE) + self.menu_connection.entryconfig("Disconnect", state=tk.ACTIVE) + self.menu_browse.entryconfig("Add child", state=tk.ACTIVE) + self.menu_browse.entryconfig("Modify", state=tk.ACTIVE) + self.menu_browse.entryconfig("Modify DN", state=tk.ACTIVE) + self.menu_browse.entryconfig("Search", state=tk.ACTIVE) + self.menu_view.entryconfig("Tree", state=tk.ACTIVE) + + # Get rootDSE + self.tprint("Retrieving base DSA information...") + try: + results = self.client.search( + baseObject="", + scope=0, + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + attrs = results.get("", None) # root + if attrs is None: + return + + self.rootDSE = attrs + + # Get some infos on the server + try: + self.dns_domain_name = self.rootDSE["ldapServiceName"][0].split(":")[0] + except KeyError: + pass + + # Display + self._showsearchresult("", results) + + # If we have a SSP, auto-bind. + if self.ssp is not None: + self.bind() + + def disconnect(self): + """ + Disconnect command. + """ + if not self.connected: + return + + self.tprint("client.close()") + self.client.close() + self.connected = False + + self.menu_connection.entryconfig("Connect", state=tk.ACTIVE) + self.menu_connection.entryconfig("Bind", state=tk.DISABLED) + self.menu_connection.entryconfig("Disconnect", state=tk.DISABLED) + self.menu_browse.entryconfig("Add child", state=tk.DISABLED) + self.menu_browse.entryconfig("Modify", state=tk.DISABLED) + self.menu_browse.entryconfig("Modify DN", state=tk.DISABLED) + self.menu_browse.entryconfig("Search", state=tk.DISABLED) + self.menu_view.entryconfig("Tree", state=tk.DISABLED) + + def bind(self, *args): + """ + Bind command. + """ + if not self.connected: + return + + if self.bound: + # We are re-binding ! + self.ssp = None + self.bound = False + + if self.ssp is not None or self.simple_username is not None: + # We have an SSP. Don't prompt + self.tprint("client.bind(%s, ssl=self.ssp)" % self.mech) + try: + self.client.bind( + self.mech, + ssp=self.ssp, + simple_username=self.simple_username, + simple_password=self.simple_password, + sign=self.sign, + encrypt=self.encrypt, + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + except Exception as ex: + self.tprint(str(ex)) + raise + self.tprint("Authenticated.\n", tags=["bold"]) + self.bound = True + return + + # Get dialog + popup = BasePopup(self.root) + dlg = popup.dlg + + # Bind UI + userv = tk.StringVar() + ttk.Label(dlg, text="User").grid(row=0, column=0) + userf = tk.Entry(dlg, textvariable=userv) + userf.grid(row=0, column=1) + + passwordv = tk.StringVar() + ttk.Label(dlg, text="Password").grid(row=1, column=0) + tk.Entry(dlg, textvariable=passwordv).grid(row=1, column=1) + + domainv = tk.StringVar() + domainv.set(self.dns_domain_name) + ttk.Label(dlg, text="Domain").grid(row=2, column=0) + domentry = tk.Entry(dlg, textvariable=domainv) + domentry.grid(row=2, column=1) + + # The "Bind Type" radio list + bindtypefrm = ttk.LabelFrame( + dlg, + text="Bind type", + ) + bindtypev = tk.StringVar() + sicilybtn = ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="Sicily bind (NTLM)", + value=LDAP_BIND_MECHS.SICILY.value, + ) + sicilybtn.pack(anchor=tk.W) + gssapibtn = ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="GSSAPI bind (Kerberos)", + value=LDAP_BIND_MECHS.SASL_GSSAPI.value, + ) + gssapibtn.pack(anchor=tk.W) + spnegobtn = ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="SPNEGO bind (NTLM/Kerberos)", + value=LDAP_BIND_MECHS.SASL_GSS_SPNEGO.value, + ) + spnegobtn.pack(anchor=tk.W) + simplebtn = ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="Simple bind", + value=LDAP_BIND_MECHS.SIMPLE.value, + ) + simplebtn.pack(anchor=tk.W) + bindtypefrm.grid(row=3, column=0, columnspan=2) + + if "supportedSASLMechanisms" in self.rootDSE: + # Some algorithms might be unavailable + algs = self.rootDSE["supportedSASLMechanisms"] + if "GSSAPI" not in algs: + gssapibtn.config(state=tk.DISABLED) + if "GSS-SPNEGO" not in algs: + spnegobtn.config(state=tk.DISABLED) + + # Sign button + signv = tk.BooleanVar() + signv.set(self.sign) + ttk.Label(dlg, text="Sign traffic after bind").grid(row=4, column=0) + signbtn = ttk.Checkbutton(dlg, variable=signv) + signbtn.grid(row=4, column=1) + + # Encrypt button + encryptv = tk.BooleanVar() + encryptv.set(self.encrypt) + ttk.Label(dlg, text="Encrypt traffic after bind").grid(row=5, column=0) + encrbtn = ttk.Checkbutton(dlg, variable=encryptv) + encrbtn.grid(row=5, column=1) + + ttk.Button(dlg, text="OK", command=popup.dismiss).grid(row=6, column=0) + ttk.Button(dlg, text="Cancel", command=popup.cancel).grid(row=6, column=1) + + # Default state + if self.dns_domain_name and not valid_ip(self.host): + bindtypev.set(LDAP_BIND_MECHS.SASL_GSS_SPNEGO.value) + else: + domentry.configure(state=tk.DISABLED) + bindtypev.set(LDAP_BIND_MECHS.SICILY.value) + + # Handle dynamic UI + def bindtypechange(*args, **kwargs): + bindtype = LDAP_BIND_MECHS(bindtypev.get()) + if bindtype == LDAP_BIND_MECHS.SIMPLE: + domentry.config(state=tk.DISABLED) + signbtn.config(state=tk.DISABLED) + encrbtn.config(state=tk.DISABLED) + encryptv.set(False) + elif bindtype == LDAP_BIND_MECHS.SICILY: + domentry.config(state=tk.DISABLED) + signbtn.config(state=tk.DISABLED) + signv.set(False) + encrbtn.config(state=tk.NORMAL) + else: + domentry.config(state=tk.NORMAL, textvariable=domainv) + signbtn.config(state=tk.NORMAL) + encrbtn.config(state=tk.NORMAL) + + bindtypev.trace_add("write", bindtypechange) + + userf.focus() + + # Setup + if popup.run(): + # Cancelled + return + + # Get values + username = userv.get() + password = passwordv.get() + domain = domainv.get() + bindtype = LDAP_BIND_MECHS(bindtypev.get()) + self.sign = signv.get() + self.encrypt = encryptv.get() + + # Bind ! + self.tprint("client.bind(%s, ...)" % bindtype) + try: + simple_username = None + simple_password = None + if bindtype == LDAP_BIND_MECHS.SIMPLE: + self.ssp = None + simple_username = username + simple_password = password + self.encrypt = False + elif bindtype == LDAP_BIND_MECHS.SICILY: + self.sign = False + self.ssp = NTLMSSP( + UPN=username, + PASSWORD=password, + ) + elif bindtype == LDAP_BIND_MECHS.SASL_GSSAPI: + self.ssp = KerberosSSP( + UPN="%s@%s" % (username, domain), + SPN="ldap/%s" % self.host, + PASSWORD=password, + ) + elif bindtype == LDAP_BIND_MECHS.SASL_GSS_SPNEGO: + self.ssp = SPNEGOSSP( + [ + NTLMSSP( + UPN=username, + PASSWORD=password, + ), + KerberosSSP( + UPN="%s@%s" % (username, domain), + SPN="ldap/%s" % self.host, + PASSWORD=password, + ), + ] + ) + self.client.bind( + bindtype, + ssp=self.ssp, + simple_username=simple_username, + simple_password=simple_password, + sign=self.sign, + encrypt=self.encrypt, + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + # Reset SSP. + self.ssp = None + return + except Exception as ex: + self.tprint(str(ex)) + # Reset SSP. + self.ssp = None + raise + self.tprint("Authenticated.\n") + self.bound = True + + def tree(self, *args): + """ + Tree command. + """ + if not self.connected: + return + + # Get namingContexts from rootDSE + try: + results = self.client.search(attributes=["namingContexts"]) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + attrs = results.get("", None) # root + if attrs is None: + return + + if "namingContexts" in attrs: + self.tk_tree.delete(*self.tk_tree.get_children()) + for root in attrs["namingContexts"]: + self.tk_tree.insert("", "end", root, text=root) + + def _showsearchresult(self, baseObject, results): + """ + Display attributes search result + """ + if baseObject in results: + self.tprint("Dn: %s" % (baseObject or "(RootDSE)"), tags=["bold"]) + self.tprint( + "\n".join( + " %s%s: %s" + % ( + k, + "" if len(v) == 1 else " (%s)" % len(v), + self._format_attribute(k, v, crop=True), + ) + for k, v in sorted(results[baseObject].items(), key=lambda x: x[0]) + ) + + "\n" + ) + + def treedoubleclick(self, _): + """ + Action done on tree double-click. + """ + # Get clicked item + try: + item = self.tk_tree.selection()[0] + except IndexError: + # Nothing is selected + return + + # Unclickable + if self.tk_tree.tag_has("unclickable", item): + return + + # Does it already have children? If so delete them. + self.tk_tree.delete(*self.tk_tree.get_children(item)) + + self.tprint("-----------\nExpanding base '%s'..." % item) + + # Get children + try: + results = self.client.search( + baseObject=item, + scope=1, + attributes=["1.1"], + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + # Add to tree + if not results: + self.tk_tree.insert(item, "end", text="No children", tags=("unclickable",)) + else: + for child in results: + self.tk_tree.insert(item, "end", child, text=child) + + # Get attributes + try: + results = self.client.search( + baseObject=item, + scope=0, + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + # Display + self._showsearchresult(item, results) + + def load_guids(self): + """ + Load the various guids: + - schemaIDguid + - propset + + This cache is used to resolve the GUIDs of objects in ACEs. + """ + if self.loadedSchemaIDGuids: + return True + + # Property set + self.guids.update( + ( + k, + { + "objectClass": ["propset"], + "name": v, + }, + ) + for k, v in LDAP_PROPERTY_SET.items() + ) + + # Control access + self.guids.update( + ( + k, + { + "objectClass": ["controlset access right"], + "name": v, + }, + ) + for k, v in LDAP_CONTROL_ACCESS_RIGHTS.items() + ) + + self.tprint("Resolving schemaIDguid... ", flush=True) + try: + results = self.client.search( + baseObject=self.rootDSE["schemaNamingContext"][0], + scope=1, + attributes=["lDAPDisplayName", "schemaIDGUID", "objectClass"], + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return False + + self.guids.update( + { + uuid.UUID(bytes_le=v["schemaIDGUID"][0]): { + "objectClass": v["objectClass"], + "name": v["lDAPDisplayName"][0], + } + for v in results.values() + if "schemaIDGUID" in v + } + ) + + self.guidscombo.update({v["name"]: k for k, v in self.guids.items()}) + self.guidscomboobject.update( + { + v["name"]: k + for k, v in self.guids.items() + if "classSchema" in v["objectClass"] + } + ) + self.loadedSchemaIDGuids = True + self.tprint("OK !") + return True + + def _rslvtype(self, x): + """ + Resolve Object types GUIDs + """ + if x in self.guids: + return self.guids[x]["name"] + return str(x) + + def _rslvsid(self, x): + """ + Resolve SIDs + """ + if isinstance(x, WINNT_SID): + x = x.summary() + if x in self.sids: + return self.sids[x] + return x or "" + + def resolvesids(self, sids): + """ + Queue a list of SIDs for resolution. + They are then added to self.sids if successful. + """ + unknowns = [x for x in (y.summary() for y in sids) if x not in self.sids] + if not unknowns: + return + + # Perform a resolution using [MS-LSAT] LsarLookupSids3 + client = DCERPC_Client( + DCERPC_Transport.NCACN_IP_TCP, + ndr64=False, + auth_level=DCE_C_AUTHN_LEVEL.PKT_PRIVACY, + ssp=self.ssp, + ) + client.connect_and_bind(self.host, find_dcerpc_interface("drsuapi")) + + # 1. DRSBind + bind_resp = client.sr1_req( + IDL_DRSBind_Request( + puuidClientDsa=NTDSAPI_CLIENT_GUID, + pextClient=DRS_EXTENSIONS(rgb=bytes(DRS_EXTENSIONS_INT(Pid=1234))), + ndr64=client.ndr64, + ), + ) + if bind_resp.status != 0: + self.tprint("Bind Request failed.") + bind_resp.show() + return + + # 2. DRSCrackNames + resp = client.sr1_req( + IDL_DRSCrackNames_Request( + hDrs=bind_resp.phDrs, + dwInVersion=1, + pmsgIn=NDRUnion( + tag=1, + value=DRS_MSG_CRACKREQ_V1( + CodePage=0x4E4, # + LocaleId=0x409, # US-EN + formatOffered=11, # SID + formatDesired=0xFFFFFFF2, # DS_USER_PRINCIPAL_NAME_FOR_LOGON + rpNames=unknowns, + ), + ), + ndr64=client.ndr64, + ), + ) + if resp.status != 0: + self.tprint("DsCracknames Request failed.") + resp.show() + return + + # 3. parse results + for i, res in enumerate(resp.valueof("pmsgOut.pResult.rItems")): + if res.status != 0: + # Errored + continue + name = res.valueof("pName") + self.sids[unknowns[i]] = name.decode() + + # alias for combobox + self.sidscombo = {self._rslvsid(x): x for x in self.sids.keys()} + + def viewsec(self, *args): + """ + View security descriptor + """ + # Get clicked item + item = self.tk_tree.selection()[0] + + # Get SD + try: + results = self.client.search( + baseObject=item, + scope=0, + attributes=["nTSecurityDescriptor"], + controls=[ + LDAP_Control( + controlType="1.2.840.113556.1.4.801", + criticality=True, + controlValue=LDAP_serverSDFlagsControl( + flags="OWNER+GROUP+DACL+SACL", + ), + ) + ], + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + if item not in results: + return + + try: + nTSecurityDescriptor = SECURITY_DESCRIPTOR( + results[item]["nTSecurityDescriptor"][0] + ) + except KeyError: + self.tprint( + "Security Descriptor could NOT be read ! (Access denied?)", + tags=["error"], + ) + return + except LDAP_Exception as ex: + self.tprint( + "Error parsing the Security Descriptor: " + str(ex), + tags=["error"], + ) + return + + # Resolve the guids + if not self.load_guids(): + return + + # Pre-resolve all the SIDs. + owner = getattr(nTSecurityDescriptor, "OwnerSid", None) + group = getattr(nTSecurityDescriptor, "GroupSid", None) + _to_resolve = [ + owner, + group, + ] + if hasattr(nTSecurityDescriptor, "DACL"): + _to_resolve.extend(x.Sid for x in nTSecurityDescriptor.DACL.Aces) + if hasattr(nTSecurityDescriptor, "SACL"): + _to_resolve.extend(x.Sid for x in nTSecurityDescriptor.SACL.Aces) + self.resolvesids(_to_resolve) + + # Get dialog + popup = BasePopup(self.root) + dlg = popup.dlg + + # Security Descriptor UI + dlg.columnconfigure(0, weight=1) + dlg.rowconfigure(tuple(range(5)), weight=1) + + sidfrm = ttk.Frame(dlg) + sidfrm.grid(row=0, sticky="we") + sidfrm.grid_columnconfigure(1, weight=1) + + ownerv = tk.StringVar() + ownerv.set(self._rslvsid(owner)) + ttk.Label(sidfrm, text="Owner").grid(row=0, column=0, sticky="we") + ttk.Combobox( + sidfrm, textvariable=ownerv, values=list(self.sidscombo.keys()) + ).grid(row=0, column=1, sticky="we") + + groupv = tk.StringVar() + groupv.set(self._rslvsid(group)) + ttk.Label(sidfrm, text="Group").grid(row=1, column=0, sticky="we") + ttk.Combobox( + sidfrm, textvariable=groupv, values=list(self.sidscombo.keys()) + ).grid(row=1, column=1, sticky="we") + + sdcontrolfrm = ttk.LabelFrame( + dlg, + text="SD Control", + ) + sdflags = [ + "SELF_RELATIVE", + "DACL_PRESENT", + "SACL_PRESENT", + "OWNER_DEFAULTED", + "DACL_PROTECTED", + "SACL_PROTECTED", + "GROUP_DEFAULTED", + "DACL_AUTO_INHERITED", + "SACL_AUTO_INHERITED", + "RM_CONTROL_VALID", + "DACL_DEFAULTED", + "SACL_DEFAULTED", + "SERVER_SECURITY", + "DACL_COMPUTED", + "SACL_COMPUTED", + None, + "DACL_TRUSTED", + ] + sdvars = [None] * len(sdflags) + for i, sdflag in enumerate(sdflags): + if sdflag is None: + continue + sdvars[i] = tk.BooleanVar() + sdvars[i].set(getattr(nTSecurityDescriptor.Control, sdflag)) + ttk.Checkbutton(sdcontrolfrm, variable=sdvars[i], text=sdflag).grid( + row=(i // 3) * 4, column=(i % 3) * 4, columnspan=4, sticky="w" + ) + sdcontrolfrm.grid(row=1, sticky="we") + + def acegui(ace, parentdlg=dlg): + data = ace.extractData(accessMask=LDAP_DS_ACCESS_RIGHTS) + + # Sub-dialog + subpopup = BasePopup(parentdlg) + dlg = subpopup.dlg + + # Edit ACE UI + dlg.columnconfigure(1, weight=1) + dlg.rowconfigure(tuple(range(8)), weight=1) + + # Trustee + trusteev = tk.StringVar() + trusteev.set(self._rslvsid(data["sid-string"])) + ttk.Label(dlg, text="Trustee").grid(row=0, column=0, sticky="we") + ttk.Combobox( + dlg, textvariable=trusteev, values=list(self.sidscombo.keys()) + ).grid(row=0, column=1, sticky="we") + + # ACE type + ttk.Label(dlg, text="ACE type").grid(row=1, column=0, sticky="we") + acetypefrm = ttk.Frame( + dlg, + ) + acetypev = tk.IntVar() + acetypev.set(ace.AceType - 5 if ace.AceType >= 5 else ace.AceType) + ttk.Radiobutton( + acetypefrm, + variable=acetypev, + text="Allow", + value=0x00, + ).grid(row=0, column=0) + ttk.Radiobutton( + acetypefrm, + variable=acetypev, + text="Deny", + value=0x01, + ).grid(row=0, column=1) + ttk.Radiobutton( + acetypefrm, + variable=acetypev, + text="Audit", + value=0x02, + ).grid(row=0, column=2) + ttk.Radiobutton( + acetypefrm, + variable=acetypev, + text="Alarm", + value=0x03, + state=tk.DISABLED, + ).grid(row=0, column=3) + acetypefrm.grid(row=1, column=1, sticky="we") + + # Access Mask + accessmaskfrm = ttk.LabelFrame( + dlg, + text="Access Mask", + ) + sdvars = [None] * len(LDAP_DS_ACCESS_RIGHTS) + for i, maskval in enumerate(LDAP_DS_ACCESS_RIGHTS.values()): + sdvars[i] = tk.BooleanVar() + sdvars[i].set(getattr(data["mask"], maskval)) + ttk.Checkbutton(accessmaskfrm, variable=sdvars[i], text=maskval).grid( + row=i // 4, column=i % 4, sticky="w" + ) + accessmaskfrm.grid(row=2, column=0, columnspan=2, sticky="we") + + # ACE flags + aceflagsfrm = ttk.LabelFrame( + dlg, + text="Access Mask", + ) + aceflagsvars = [None] * len(WINNT_ACE_FLAGS) + for i, aceval in enumerate(WINNT_ACE_FLAGS.values()): + aceflagsvars[i] = tk.BooleanVar() + aceflagsvars[i].set(getattr(ace.AceFlags, aceval)) + ttk.Checkbutton( + aceflagsfrm, variable=aceflagsvars[i], text=aceval + ).grid(row=i // 4, column=i % 4, sticky="w") + aceflagsfrm.grid(row=3, column=0, columnspan=2, sticky="we") + + # Object type + objecttypev = tk.StringVar() + objecttypev.set(self._rslvtype(data["object-guid"]) or "None") + ttk.Label(dlg, text="Object type").grid(row=5, column=0, sticky="we") + ttk.Combobox( + dlg, textvariable=objecttypev, values=list(self.guidscombo.keys()) + ).grid(row=5, column=1, sticky="we") + + # Inherited object type + inheritedobjecttypev = tk.StringVar() + inheritedobjecttypev.set( + self._rslvtype(data["inherited-object-guid"]) or "None" + ) + ttk.Label(dlg, text="Inherited object type").grid( + row=6, column=0, sticky="we" + ) + ttk.Combobox( + dlg, + textvariable=inheritedobjecttypev, + values=list(self.guidscomboobject.keys()), + ).grid(row=6, column=1, sticky="we") + + # OK / Cancel + btnfrm = ttk.Frame(dlg) + ttk.Button(btnfrm, text="OK", command=subpopup.dismiss).grid( + row=0, column=0 + ) + ttk.Button(btnfrm, text="Cancel", command=subpopup.cancel).grid( + row=0, column=1 + ) + btnfrm.grid(row=7) + + # Setup + if subpopup.run(): + # Cancelled + return + + # Get values + trustee = trusteev.get() + acetype = acetypev.get() + objecttype = objecttypev.get() + inheritedobjecttype = inheritedobjecttypev.get() + mask = 0 + for i, (sdvar, v) in enumerate( + zip(sdvars, list(LDAP_DS_ACCESS_RIGHTS.keys())) + ): + if sdvar is None: + continue + if sdvar.get(): + mask |= v + aceflags = 0 + for i, (aceflagvar, v) in enumerate( + zip(aceflagsvars, list(WINNT_ACE_FLAGS.keys())) + ): + if aceflagvar is None: + continue + if aceflagvar.get(): + aceflags |= v + + # Set back into ACE + if trustee in self.sidscombo: + Sid = WINNT_SID.fromstr(self.sidscombo[trustee]) + else: + Sid = WINNT_SID.fromstr(trustee) + if objecttype in self.guidscombo: + objecttype = self.guidscombo[objecttype] + elif objecttype: + objecttype = uuid.UUID(objecttype) + if inheritedobjecttype in self.guidscomboobject: + inheritedobjecttype = self.guidscomboobject[inheritedobjecttype] + elif inheritedobjecttype: + inheritedobjecttype = uuid.UUID(inheritedobjecttype) + Flags = 0 + if objecttype: + Flags |= 1 + if inheritedobjecttype: + Flags |= 2 + if acetype == 0x00: + if Flags: + ace.AceType = 0x05 + ace.payload = WINNT_ACCESS_ALLOWED_OBJECT_ACE( + Mask=mask, + Sid=Sid, + Flags=Flags, + ObjectType=objecttype, + InheritedObjectType=inheritedobjecttype, + ) + else: + ace.AceType = 0x00 + ace.payload = WINNT_ACCESS_ALLOWED_ACE( + Mask=mask, + Sid=Sid, + ) + elif acetype == 0x01: + if Flags: + ace.AceType = 0x06 + ace.payload = WINNT_ACCESS_DENIED_OBJECT_ACE( + Mask=mask, + Sid=Sid, + Flags=Flags, + ObjectType=objecttype, + InheritedObjectType=inheritedobjecttype, + ) + else: + ace.AceType = 0x01 + ace.payload = WINNT_ACCESS_DENIED_ACE( + Mask=mask, + Sid=Sid, + ) + elif acetype == 0x02: + if Flags: + ace.AceType = 0x07 + ace.payload = WINNT_SYSTEM_AUDIT_OBJECT_ACE( + Mask=mask, + Sid=Sid, + Flags=Flags, + ObjectType=objecttype, + InheritedObjectType=inheritedobjecttype, + ) + else: + ace.AceType = 0x02 + ace.payload = WINNT_SYSTEM_AUDIT_ACE( + Mask=mask, + Sid=Sid, + ) + else: + raise NotImplementedError + ace.AceFlags = aceflags + + def addace(id, table, ace, pos="end"): + data = ace.extractData(accessMask=LDAP_DS_ACCESS_RIGHTS) + table.insert( + "", + pos, + id, + values=( + ace.sprintf("%AceType%"), + self._rslvsid(data["sid-string"]), + str(data["mask"]) + + ( + " (%s)" % self._rslvtype(data["object-guid"]) + if data["object-guid"] + else "" + ), + ace.sprintf("%AceFlags%"), + ), + ) + + def acltable(name): + aclfrm = ttk.LabelFrame(dlg, text=name, borderwidth=0) + + tvfr = ttk.Frame(aclfrm) + tvfr.grid_columnconfigure(0, weight=1) + tvfr.grid_rowconfigure(0, weight=1) + + acltree = ttk.Treeview( + tvfr, show="headings", columns=("type", "trustee", "rights", "flags") + ) + acltree.heading("type", text="Type") + acltree.heading("trustee", text="Trustee") + acltree.heading("rights", text="Rights") + acltree.heading("flags", text="Flags") + + tree_scrollbar = AutoHideScrollbar( + tvfr, orient="vertical", command=acltree.yview + ) + acltree.configure(yscrollcommand=tree_scrollbar.set) + acltree.grid(row=0, column=0, sticky="nsew") + + # Populate + aclobj = getattr(nTSecurityDescriptor, name, None) + if aclobj is not None: + for i, ace in enumerate(aclobj.Aces): + addace(i, acltree, ace) + + def add(*_): + ace = WINNT_ACE_HEADER() / WINNT_ACCESS_ALLOWED_ACE() + acegui(ace) + # Append + aclobj.Aces.append(ace) + addace(len(aclobj.Aces) - 1, acltree, ace) + + def delete(*_): + try: + selected = int(acltree.selection()[0]) + del aclobj.Aces[selected] + except IndexError: + return + # Full refresh as indexes change. + acltree.delete(*acltree.get_children()) + for i, ace in enumerate(aclobj.Aces): + addace(i, acltree, ace) + + def edit(*_): + try: + selected = int(acltree.selection()[0]) + ace = aclobj.Aces[selected] + except IndexError: + return + acegui(ace) + # Update + acltree.delete(selected) + addace(selected, acltree, ace, pos=selected) + + btnfrm = ttk.Frame(aclfrm) + btnfrm.grid_columnconfigure(0, weight=1) + ttk.Button(btnfrm, text="Add", command=add).grid(row=0) + ttk.Button(btnfrm, text="Delete", command=delete).grid(row=1) + ttk.Button(btnfrm, text="Edit", command=edit).grid(row=2) + btnfrm.pack(side="right") + + tvfr.pack(fill="both", expand=True) + return aclfrm + + acltable("DACL").grid(row=2, sticky="we") + acltable("SACL").grid(row=3, sticky="we") + + btnfrm = ttk.Frame(dlg) + ttk.Button(btnfrm, text="Update", command=popup.dismiss).grid(row=0, column=0) + ttk.Button(btnfrm, text="Cancel", command=popup.cancel).grid(row=0, column=1) + btnfrm.grid(row=4) + + # Setup + if popup.run(): + # Cancelled + return + + # From UI back into ntSecurityDescriptor + + # Owner + owner = ownerv.get() + if owner in self.sidscombo: + nTSecurityDescriptor.OwnerSid = WINNT_SID.fromstr(self.sidscombo[owner]) + else: + nTSecurityDescriptor.OwnerSid = WINNT_SID.fromstr(owner) + + # Group + group = groupv.get() + if group in self.sidscombo: + nTSecurityDescriptor.GroupSid = WINNT_SID.fromstr(self.sidscombo[group]) + else: + nTSecurityDescriptor.GroupSid = WINNT_SID.fromstr(group) + + # Control + control = SECURITY_DESCRIPTOR(Control=0).Control + for i, (sdvar, v) in enumerate(zip(sdvars, sdflags)): + if sdvar is None: + continue + if sdvar.get(): + control |= v + nTSecurityDescriptor.Control = control + + # Offsets need to be recalculated + nTSecurityDescriptor.OwnerSidOffset = None + nTSecurityDescriptor.GroupSidOffset = None + nTSecurityDescriptor.DACLOffset = None + nTSecurityDescriptor.SACLOffset = None + + # Pfew, we did it. That was some big UI. + + # Now update the SD. + try: + self.client.modify( + object=item, + changes=[ + LDAP_ModifyRequestChange( + operation="replace", + modification=LDAP_PartialAttribute( + type="ntSecurityDescriptor", + values=[ + LDAP_AttributeValue(value=bytes(nTSecurityDescriptor)) + ], + ), + ) + ], + controls=[ + LDAP_Control( + controlType="1.2.840.113556.1.4.801", + criticality=True, + controlValue=LDAP_serverSDFlagsControl( + flags="OWNER+GROUP+DACL+SACL", + ), + ) + ], + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + self.tprint("Security descriptor updated.") + + def _members_popup(self, selection, mode="memberof"): + """ + The base of the "Member Of" and "Members" popups + + :param mode: either "memberof" or "members" + """ + # Get clicked item + item = self.tk_tree.selection()[0] + + # Get the user attributes + try: + results = self.client.search( + baseObject=item, + scope=0, + attributes=["objectClass", "memberOf"], + ) + if item not in results: + raise ValueError("Bad output") + attributes = results[item] + except ValueError as ex: + self.tprint(str(ex)) + return + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + # Check that this item is indeed, a user or a group + if not any(x in ["user", "group"] for x in attributes.get("objectClass", [])): + messagebox.showerror("Error", "Object is neither a user nor a group !") + return + + # Keep track of previous members, and changed ones + og_members = set(attributes.get("memberOf", [])) + members = list(og_members) + + # Get dialog + popup = BasePopup(self.root) + dlg = popup.dlg + + # "Member Of" UI + dlg.grid_rowconfigure(0, weight=1) + dlg.grid_columnconfigure(0, weight=1) + + memberoffrm = ttk.LabelFrame( + dlg, + text="Member Of", + ) + memberoffrm.grid_rowconfigure(0, weight=1) + memberoffrm.grid_columnconfigure(0, weight=1) + + # Members list + entrylist = tk.Listbox(memberoffrm) + entrylist.grid(row=0, sticky="new") + + def add(*_, parentdlg=dlg): + # Sub-dialog + subpopup = BasePopup(parentdlg) + dlg = subpopup.dlg + + # New group field + newgroupv = tk.StringVar() + ttk.Label(dlg, text="Group CN:").grid(row=0, sticky="we") + newgroupf = tk.Entry(dlg, textvariable=newgroupv) + newgroupf.grid(row=1, sticky="we") + + # OK / Cancel + btnfrm = ttk.Frame(dlg) + ttk.Button(btnfrm, text="OK", command=subpopup.dismiss).grid( + row=0, column=0 + ) + ttk.Button(btnfrm, text="Cancel", command=subpopup.cancel).grid( + row=0, column=1 + ) + btnfrm.grid(row=2, ipadx=5) + + # Focus + newgroupf.focus() + + if subpopup.run(): + return + + # Get results + newgroup = newgroupv.get() + + if newgroup: + # Store + members.append(newgroup) + # Display + entrylist.insert("end", newgroup) + + def delete(*_): + try: + selected = int(entrylist.curselection()[0]) + except IndexError: + return + # Drop + del members[selected] + # Remove from list + entrylist.delete(selected) + + # Add / Delete + btnfrm = ttk.Frame(memberoffrm) + ttk.Button(btnfrm, text="Add", command=add).grid(row=0, column=0) + ttk.Button(btnfrm, text="Delete", command=delete).grid(row=0, column=1) + btnfrm.grid(row=1, sticky="we") + + # Populate + for group in og_members: + entrylist.insert("end", group) + og_members.add(group) + + memberoffrm.grid(row=0, columnspan=2, sticky="we") + + # OK / Cancel + btnfrm = ttk.Frame(dlg) + ttk.Button(btnfrm, text="OK", command=popup.dismiss).grid(row=0, column=0) + ttk.Button(btnfrm, text="Cancel", command=popup.cancel).grid(row=0, column=1) + btnfrm.grid(row=1, ipadx=5) + + # Setup + if popup.run(): + # Cancelled + return + + # Get results + members = set(members) + to_add = members - og_members + to_rem = og_members - members + operations = [("add", x) for x in to_add] + [("delete", x) for x in to_rem] + + for op, group in operations: + # Run the operations: on multiple groups, add/remove ourselves from "member" + try: + results = self.client.modify( + object=group, + changes=[ + LDAP_ModifyRequestChange( + operation=op, + modification=LDAP_PartialAttribute( + type="member", + values=[LDAP_AttributeValue(value=item)], + ), + ) + ], + ) + except ValueError as ex: + self.tprint(str(ex)) + return + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + self.tprint("Groups of '%s' updated !" % item) + + def editmemberof(self, *_): + """ + Edit popup for "Member Of" + """ + # Get clicked item + item = self.tk_tree.selection()[0] + + self._members_popup(item, "memberof") + + def _edit_popup(self, selection, mode="edit", editattrs={}): + """ + The base of the "Edit" and "Duplicate" popups + + :param mode: either "edit" or "new" + :param editattrs: existing attributes to edit + """ + # Get dialog + popup = BasePopup(self.root) + dlg = popup.dlg + + # Edit UI + dlg.grid_columnconfigure(1, weight=1) + + # DN + dnv = tk.StringVar() + dnv.set(selection) + if mode == "edit": + ttk.Label(dlg, text="DN:").grid(row=0, column=0, sticky="w") + else: + ttk.Label(dlg, text="New DN:").grid(row=0, column=0, sticky="w") + tk.Entry(dlg, textvariable=dnv).grid(row=0, column=1, sticky="we") + + # "Edit entry" sub-box + editentryfrm = ttk.LabelFrame( + dlg, + text="Edit Entry", + ) + attributev = tk.StringVar() + ttk.Label(editentryfrm, text="Attribute:").grid(row=0, column=0) + tk.Entry(editentryfrm, textvariable=attributev).grid( + row=0, column=1, sticky="we" + ) + + valuesv = tk.StringVar() + ttk.Label(editentryfrm, text="Values:").grid(row=1, column=0) + tk.Entry(editentryfrm, textvariable=valuesv).grid(row=1, column=1, sticky="we") + + # "Operation" subbox: the radio + the buttons + opsfrm = ttk.Frame(editentryfrm) + operationfrm = ttk.LabelFrame( + opsfrm, + text="Operation", + ) + scopev = tk.IntVar() + scopev.set(0) + ttk.Radiobutton( + operationfrm, + variable=scopev, + text="Add", + value=0, + ).grid(row=0, column=0) + ttk.Radiobutton( + operationfrm, + variable=scopev, + text="Delete", + value=1, + ).grid(row=0, column=1) + ttk.Radiobutton( + operationfrm, + variable=scopev, + text="Replace", + value=2, + ).grid(row=0, column=2) + operationfrm.grid(row=0, column=0, columnspan=2, sticky="we") + + if mode == "new": + # In 'new', the only allowed operation is 'Add' + for child in operationfrm.winfo_children(): + child.configure(state=tk.DISABLED) + + operations = [] + + def enterentrylist(): + """ + This is called to add an element to the "Entry List" + """ + op = scopev.get() + attr = attributev.get() + val = valuesv.get() + ident = "[%s]%s:%s" % ( + {0: "Add", 1: "Delete", 2: "Replace"}[op], + attr, + val, + ) + # Once we have an ident, actually parse the value entered by the user + try: + val = self._parse_attribute(attr, val) + except ValueError: + # Parsing failed, show a popup and return without clearing ! + return + # Get current selection and reset it + selected = self.currently_editing + self.currently_editing = None + # Do we have a selection + if selected is not None: + # Yes, edit + # Set in storage + operations[selected] = (op, attr, val) + # Re-add to display + entrylist.delete(selected) + entrylist.insert(selected, ident) + # Reset selection btw + entrylist.itemconfigure(selected, fg="black") + entrylist.see(selected) + else: + # No, create + # Add to storage + operations.append((op, attr, val)) + # Add to display + entrylist.insert("end", ident) + # Clear to really show we're done + scopev.set(0) + attributev.set("") + valuesv.set("") + + def editentrylist(): + """ + This is called to load an element from the "Entry List" + """ + try: + selected = int(entrylist.curselection()[0]) + except IndexError: + return + # If there's a previously edited (unfinished), clear + if self.currently_editing is not None: + entrylist.itemconfigure(self.currently_editing, fg="black") + # Set currently edited mode + self.currently_editing = selected + # Show selected item in blue + entrylist.itemconfigure(selected, fg="blue") + entrylist.selection_clear(selected) + + operation = operations[selected] + # Set textboxes + scopev.set(operation[0]) + attributev.set(operation[1]) + valuesv.set(self._format_attribute(operation[1], operation[2])) + + def removeentrylist(): + """ + This is called to remove an element from the "Entry List" + """ + try: + selected = entrylist.curselection()[0] + except IndexError: + return + # Remove from storage + del operations[selected] + # Remove from display + entrylist.delete(selected) + + ttk.Button( + opsfrm, + text="Enter", + command=enterentrylist, + ).grid(row=0, column=2) + + opsfrm.grid(row=2, column=0, columnspan=2) + editentryfrm.grid(row=1, column=0, columnspan=2) + + # Entry list + entrylistfrm = ttk.LabelFrame( + dlg, + text="Entry List", + ) + entrylistfrm.grid_columnconfigure(0, weight=1) + + entrylist = tk.Listbox(entrylistfrm) + entrylist.grid(row=0, sticky="we", padx=5) + + entrylistbtns = ttk.Frame(entrylistfrm) + ttk.Button( + entrylistbtns, + text="Edit", + command=editentrylist, + ).pack(side="left") + ttk.Button( + entrylistbtns, + text="Remove", + command=removeentrylist, + ).pack(side="right") + entrylistbtns.grid(row=1, sticky="we", padx=10) + + entrylistfrm.grid(row=3, column=0, columnspan=2, sticky="we", pady=5) + + if mode == "new": + for attr, val in editattrs.items(): + # Add to storage + operations.append((0, attr, val)) + # Add to display + ident = "[Add]%s:%s" % ( + attr, + self._format_attribute(attr, val), + ) + entrylist.insert("end", ident) + + # OK / Cancel + btnfrm = ttk.Frame(dlg) + ttk.Button(btnfrm, text="Run", command=popup.dismiss).pack(side="left") + ttk.Button(btnfrm, text="Cancel", command=popup.cancel).pack(side="right") + btnfrm.grid(row=4, column=0, columnspan=2, ipadx=10) + + # Setup + if popup.run(): + # Cancelled + return + + # Get values + dn = dnv.get() + + return dn, operations + + def edit(self, *args): + """ + Edit popup + """ + # Get selected item + try: + selection = self.tk_tree.selection()[0] + except IndexError: + selection = "" + + results = self._edit_popup(selection) + if not results: + return + dn, operations = results + + # Perform edit + try: + self.client.modify( + object=dn, + changes=[ + LDAP_ModifyRequestChange( + operation=op, + modification=LDAP_PartialAttribute( + type=attr, + values=[LDAP_AttributeValue(value=x) for x in values], + ), + ) + for (op, attr, values) in operations + ], + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + self.tprint("Modify request succeeded.") + + def search(self, *args): + """ + Search popup + """ + # Get selected item + try: + selection = self.tk_tree.selection()[0] + except IndexError: + selection = "rootDSE" + + # Get dialog + popup = BasePopup(self.root) + dlg = popup.dlg + + # Search UI + dlg.grid_columnconfigure(1, weight=1) + + basednv = tk.StringVar() + basednv.set(selection) + ttk.Label(dlg, text="Base DN").grid(row=0, column=0) + basednf = tk.Entry(dlg, textvariable=basednv) + basednf.grid(row=0, column=1, sticky="we") + + filterv = tk.StringVar() + filterv.set(self.lastSearchString) + ttk.Label(dlg, text="Filter").grid(row=1, column=0) + tk.Entry(dlg, textvariable=filterv).grid(row=1, column=1, sticky="we") + + scopefrm = ttk.LabelFrame( + dlg, + text="Scope", + ) + scopev = tk.IntVar() + scopev.set(1) + ttk.Radiobutton( + scopefrm, + variable=scopev, + text="Base", + value=0, + ).grid(row=0, column=0) + ttk.Radiobutton( + scopefrm, + variable=scopev, + text="One Level", + value=1, + ).grid(row=0, column=1) + ttk.Radiobutton( + scopefrm, + variable=scopev, + text="Subtree", + value=2, + ).grid(row=0, column=2) + scopefrm.grid(row=2, column=0, columnspan=2) + + ttk.Button(dlg, text="OK", command=popup.dismiss).grid(row=3, column=0) + ttk.Button(dlg, text="Cancel", command=popup.cancel).grid(row=3, column=1) + + basednf.focus() + + # Setup + if popup.run(): + # Cancelled + return + + # Get values + basedn = basednv.get() + flt = filterv.get() + scope = scopev.get() + + self.lastSearchString = flt + + # Perform search + self.tprint("Searching...", flush=True) + try: + results = self.client.search( + baseObject=basedn, + scope=scope, + filter=flt, + ) + except ValueError as ex: + self.tprint(str(ex)) + return + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + self.tprint("Getting %s entries..." % len(results)) + for item in results: + self._showsearchresult(item, results) + + def modifydn(self, *args): + """ + Modify the DN of an item + """ + # Get selected item + try: + selection = self.tk_tree.selection()[0] + except IndexError: + selection = "" + + # Get dialog + popup = BasePopup(self.root) + dlg = popup.dlg + + # Duplicate UI + dlg.grid_columnconfigure(1, weight=1) + + basednv = tk.StringVar() + basednv.set(selection) + ttk.Label(dlg, text="DN:").grid(row=0, column=0, sticky="w") + basednf = tk.Entry(dlg, textvariable=basednv) + basednf.grid(row=0, column=1, sticky="we") + + newdnv = tk.StringVar() + ttk.Label(dlg, text="New DN:").grid(row=1, column=0, sticky="w") + newdnf = tk.Entry(dlg, textvariable=newdnv) + newdnf.grid(row=1, column=1, sticky="we") + + ttk.Button(dlg, text="OK", command=popup.dismiss).grid(row=2, column=0) + ttk.Button(dlg, text="Cancel", command=popup.cancel).grid(row=2, column=1) + + if selection: + newdnf.focus() + else: + basednf.focus() + + # Setup + if popup.run(): + # Cancelled + return + + # Get values + basedn = basednv.get() + newdn = newdnv.get() + + self.tprint("Changing %s to %s..." % (basedn, newdn)) + try: + self.client.modifydn( + entry=basedn, + newdn=newdn, + ) + except ValueError as ex: + self.tprint(str(ex)) + return + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + self.tprint("OK !") + + def new(self, mode): + """ + New popup. Called by both 'Add child' and 'Duplicate' popups + """ + if mode == "duplicate": + # Get selected item + try: + selection = self.tk_tree.selection()[0] + except IndexError: + selection = "" + else: + selection = "" + + existing_attributes = {} + if selection: + # Perform search to retrieve the attributes + self.tprint("Getting attributes for %s..." % selection, flush=True) + try: + results = self.client.search( + baseObject=selection, + scope=0, + ) + if selection not in results: + raise ValueError("Bad result") + existing_attributes = results[selection] + except ValueError as ex: + self.tprint(str(ex)) + return + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + # Show edit popup to be able to change an attribute + results = self._edit_popup(selection, mode="new", editattrs=existing_attributes) + if not results: + return + newdn, changes = results + + # Extract all the 'add' attributes operations from changes + attributes = {attr: val for (_, attr, val) in changes} + + self.tprint("Adding %s..." % newdn) + try: + self.client.add( + newdn, + attributes=attributes, + ) + except LDAP_Exception as ex: + self.tprint( + ex.diagnosticMessage or "Error: %s" % ex.resultCode, + tags=["error"], + ) + return + + self.tprint("OK !") + + def duplicate(self, *args): + return self.new("duplicate") + + def addchild(self, *args): + return self.new("addchild") + + def _format_attribute(self, name, value, crop=False): + """ + Format a LDAP attribute + """ + if isinstance(value, list): + # It's a list. + return ";".join(self._format_attribute(name, v, crop=crop) for v in value) + elif name == "objectSid": + return WINNT_SID(value).summary() + elif isinstance(value, bytes): + # Catch-all for bytes values + value = value.hex() + else: + # Catch-all + value = str(value) + # If cropping is enabled and requested, crop + if crop and self.crop_output.get() and len(value) >= 80: + return value[:80] + "... (%so)" % len(value) + return value + + def _parse_attribute(self, name, value): + """ + Parse a formatted attribute + """ + parsed = [] + # Split across ; + for val in value.split(";"): + if name == "objectSid": + val = WINNT_SID.fromstr(val) + parsed.append(val) + return parsed + + def tprint(self, x, tags=[], flush=False): + """ + Print to text pane + """ + self.tk_textpane.configure(state=tk.NORMAL) + self.tk_textpane.insert("end", x + "\n", tuple(tags)) + self.tk_textpane.configure(state=tk.DISABLED) + self.tk_textpane.see(tk.END) + if flush: + self.root.update() + + def main(self): + """ + Main loop: start the GUI. + """ + # Note: for TK doc, use https://tkdocs.com + + # Root + self.root = tk.Tk() + self.root.title("LDAPhero (@secdev/scapy)") + self.root.option_add("*tearOff", False) + + # TTK style + + ttkstyle = ttk.Style() + ttkstyle.theme_use("alt") + ttkstyle.configure( + "BorderFrame.TFrame", + relief="groove", + borderwidth=3, + ) + + # Global configuration variables + self.crop_output = tk.BooleanVar() + self.crop_output.set(True) + + # Create main frames, pack them in scrollable elements + content = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) + + tvfr = ttk.Frame(content) + tvfr.grid_columnconfigure(0, weight=1) + tvfr.grid_rowconfigure(0, weight=1) + self.tk_tree = ttk.Treeview(tvfr, show="tree") + content.add(tvfr) + self.tk_tree.bind("", self.treedoubleclick) + + tree_scrollbar = AutoHideScrollbar( + tvfr, orient="vertical", command=self.tk_tree.yview + ) + self.tk_tree.configure(yscrollcommand=tree_scrollbar.set) + self.tk_tree.grid(row=0, column=0, sticky="nsew") + self.tk_tree.column("#0", width=200) + + self.tk_textpane = tk.Text(content, state=tk.DISABLED) + self.tk_textpane.tag_configure("bold", font="TkCaptionFont") + self.tk_textpane.tag_configure("error", foreground="red") + content.add(self.tk_textpane) + + # Menu + menubar = tk.Menu(self.root) + self.menu_connection = tk.Menu(menubar) + self.menu_browse = tk.Menu(menubar) + self.menu_view = tk.Menu(menubar) + menubar.add_cascade(menu=self.menu_connection, label="Connection") + self.menu_connection.add_command(label="Connect", command=self.connect) + self.menu_connection.add_command( + label="Bind", command=self.bind, state=tk.DISABLED, accelerator="Ctrl+B" + ) + self.menu_connection.add_command( + label="Disconnect", command=self.disconnect, state=tk.DISABLED + ) + self.menu_connection.add_command(label="Quit", command=self.root.destroy) + menubar.add_cascade(menu=self.menu_browse, label="Browse") + self.menu_browse.add_command( + label="Add child", + command=self.addchild, + state=tk.DISABLED, + accelerator="Ctrl+A", + ) + self.menu_browse.add_command( + label="Modify", command=self.edit, state=tk.DISABLED, accelerator="Ctrl+M" + ) + self.menu_browse.add_command( + label="Modify DN", + command=self.modifydn, + state=tk.DISABLED, + accelerator="Ctrl+R", + ) + self.menu_browse.add_command( + label="Search", command=self.search, state=tk.DISABLED, accelerator="Ctrl+S" + ) + menubar.add_cascade(menu=self.menu_view, label="View") + self.menu_view.add_command( + label="Tree", command=self.tree, state=tk.DISABLED, accelerator="Ctrl+T" + ) + self.menu_view.add_checkbutton( + label="Crop output", onvalue=True, offvalue=False, variable=self.crop_output + ) + self.root["menu"] = menubar + + # Right-click menu + self.popup = tk.Menu(self.root, tearoff=0) + self.popup.add_command( + label="Search", command=self.search, accelerator="Ctrl+S" + ) + self.popup.add_command(label="Modify", command=self.edit, accelerator="Ctrl+M") + self.popup.add_command( + label="Modify DN", command=self.modifydn, accelerator="Ctrl+R" + ) + self.popup.add_command(label="Duplicate", command=self.duplicate) + popup_adv = tk.Menu(self.popup) + self.popup.add_cascade(label="Advanced", menu=popup_adv) + popup_adv.add_command(label="Security descriptor", command=self.viewsec) + popup_adv.add_command(label="Member Of", command=self.editmemberof) + + def do_popup(event): + item = self.tk_tree.identify_row(event.y) + if item: + if self.tk_tree.tag_has("unclickable", item): + # Unclickable + return + self.tk_tree.selection_set(item) + self.popup.tk_popup(event.x_root, event.y_root) + + self.tk_tree.bind("", do_popup) + + # Shortcuts + self.root.bind_all("", self.bind) + self.root.bind_all("", self.tree) + + # Initial rendering + content.pack(fill="both", expand=True) + self.root.update() + + # Try connecting + if self.host is not None: + self.root.after(0, self.connect) + + # Main loop + self.root.mainloop() diff --git a/scapy/modules/nmap.py b/scapy/modules/nmap.py index 8394afd5130..38a0521fff9 100644 --- a/scapy/modules/nmap.py +++ b/scapy/modules/nmap.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """Clone of Nmap's first generation OS fingerprinting. @@ -16,7 +16,6 @@ """ -from __future__ import absolute_import import os import re @@ -25,11 +24,20 @@ from scapy.arch import WINDOWS from scapy.error import warning from scapy.layers.inet import IP, TCP, UDP, ICMP, UDPerror, IPerror -from scapy.packet import NoPayload +from scapy.packet import NoPayload, Packet from scapy.sendrecv import sr from scapy.compat import plain_str, raw -import scapy.modules.six as six - +from scapy.plist import SndRcvList, PacketList + +# Typing imports +from typing import ( + Dict, + List, + Tuple, + Optional, + cast, + Union, +) if WINDOWS: conf.nmap_base = os.environ["ProgramFiles"] + "\\nmap\\nmap-os-fingerprints" # noqa: E501 @@ -53,6 +61,7 @@ class NmapKnowledgeBase(KnowledgeBase): """ def lazy_init(self): + # type: () -> None try: fdesc = open(conf.nmap_base if self.filename is None else @@ -63,36 +72,43 @@ def lazy_init(self): return self.base = [] + self.base = cast(List[Tuple[str, Dict[str, Dict[str, str]]]], self.base) name = None - sig = {} + sig = {} # type: Dict[str,Dict[str,str]] for line in fdesc: - line = plain_str(line) - line = line.split('#', 1)[0].strip() - if not line: + str_line = plain_str(line) + str_line = str_line.split('#', 1)[0].strip() + if not str_line: continue - if line.startswith("Fingerprint "): + if str_line.startswith("Fingerprint "): if name is not None: self.base.append((name, sig)) - name = line[12:].strip() + name = str_line[12:].strip() sig = {} continue - if line.startswith("Class "): + if str_line.startswith("Class "): continue - line = _NMAP_LINE.search(line) - if line is None: + match_line = _NMAP_LINE.search(str_line) + if match_line is None: continue - test, values = line.groups() + test, values = match_line.groups() sig[test] = dict(val.split('=', 1) for val in (values.split('%') if values else [])) if name is not None: self.base.append((name, sig)) fdesc.close() + def get_base(self): + # type: () -> List[Tuple[str, Dict]] + return cast(List[Tuple[str, Dict]], super(NmapKnowledgeBase, self).get_base()) + -nmap_kdb = NmapKnowledgeBase(None) +conf.nmap_kdb = NmapKnowledgeBase(None) +conf.nmap_kdb = cast(NmapKnowledgeBase, conf.nmap_kdb) def nmap_tcppacket_sig(pkt): + # type: (Optional[Packet]) -> Dict res = {} if pkt is not None: res["DF"] = "Y" if pkt.flags.DF else "N" @@ -106,6 +122,7 @@ def nmap_tcppacket_sig(pkt): def nmap_udppacket_sig(snd, rcv): + # type: (SndRcvList, PacketList) -> Dict res = {} if rcv is None: res["Resp"] = "N" @@ -130,14 +147,15 @@ def nmap_udppacket_sig(snd, rcv): def nmap_match_one_sig(seen, ref): - cnt = sum(val in ref.get(key, "").split("|") - for key, val in six.iteritems(seen)) + # type: (Dict, Dict) -> float + cnt = sum(val in ref.get(key, "").split("|") for key, val in seen.items()) if cnt == 0 and seen.get("Resp") == "N": return 0.7 return float(cnt) / len(seen) def nmap_sig(target, oport=80, cport=81, ucport=1): + # type: (str, int, int, int) -> Dict res = {} tcpopt = [("WScale", 10), @@ -162,13 +180,14 @@ def nmap_sig(target, oport=80, cport=81, ucport=1): test = "T%i" % (snd.sport - 5000) if rcv is not None and ICMP in rcv: warning("Test %s answered by an ICMP", test) - rcv = None + rcv = None # type: ignore res[test] = rcv return nmap_probes2sig(res) def nmap_probes2sig(tests): + # type: (Dict) -> Dict tests = tests.copy() res = {} if "PU" in tests: @@ -180,10 +199,12 @@ def nmap_probes2sig(tests): def nmap_search(sigs): - guess = 0, [] - for osval, fprint in nmap_kdb.get_base(): + # type: (Dict) -> Tuple[Union[int, float], List] + guess = 0, [] # type: Tuple[Union[int, float], List] + conf.nmap_kdb = cast(NmapKnowledgeBase, conf.nmap_kdb) + for osval, fprint in conf.nmap_kdb.get_base(): score = 0.0 - for test, values in six.iteritems(fprint): + for test, values in fprint.items(): if test in sigs: score += nmap_match_one_sig(sigs[test], values) score /= len(sigs) @@ -196,6 +217,7 @@ def nmap_search(sigs): @conf.commands.register def nmap_fp(target, oport=80, cport=81): + # type: (str, int, int) -> Tuple[Union[int, float], List] """nmap fingerprinting nmap_fp(target, [oport=80,] [cport=81,]) -> list of best guesses with accuracy """ @@ -205,6 +227,7 @@ def nmap_fp(target, oport=80, cport=81): @conf.commands.register def nmap_sig2txt(sig): + # type: (Dict) -> str torder = ["TSeq", "T1", "T2", "T3", "T4", "T5", "T6", "T7", "PU"] korder = ["Class", "gcd", "SI", "IPID", "TS", "Resp", "DF", "W", "ACK", "Flags", "Ops", diff --git a/scapy/modules/p0f.py b/scapy/modules/p0f.py index ad5891af0cf..09bef7e4585 100644 --- a/scapy/modules/p0f.py +++ b/scapy/modules/p0f.py @@ -1,623 +1,960 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ -Clone of p0f passive OS fingerprinting +Clone of p0f v3 passive OS fingerprinting """ -from __future__ import absolute_import -from __future__ import print_function -import time +import re import struct -import os -import socket import random from scapy.data import KnowledgeBase, select_path from scapy.config import conf -from scapy.compat import raw +from scapy.compat import raw, orb +from scapy.packet import NoPayload from scapy.layers.inet import IP, TCP, TCPOptions -from scapy.packet import NoPayload, Packet -from scapy.error import warning, Scapy_Exception, log_runtime -from scapy.volatile import RandInt, RandByte, RandNum, RandShort, RandString -from scapy.sendrecv import sniff -from scapy.modules import six -from scapy.modules.six.moves import map, range -if conf.route is None: - # unused import, only to initialize conf.route - import scapy.route # noqa: F401 +from scapy.layers.http import HTTP, HTTPRequest, HTTPResponse +from scapy.layers.inet6 import IPv6 +from scapy.volatile import RandByte, RandShort, RandString +from scapy.error import warning _p0fpaths = ["/etc/p0f", "/usr/share/p0f", "/opt/local"] - conf.p0f_base = select_path(_p0fpaths, "p0f.fp") -conf.p0fa_base = select_path(_p0fpaths, "p0fa.fp") -conf.p0fr_base = select_path(_p0fpaths, "p0fr.fp") -conf.p0fo_base = select_path(_p0fpaths, "p0fo.fp") - - -############### -# p0f stuff # -############### - -# File format (according to p0f.fp) : -# -# wwww:ttt:D:ss:OOO...:QQ:OS:Details -# -# wwww - window size -# ttt - initial TTL -# D - don't fragment bit (0=unset, 1=set) -# ss - overall SYN packet size -# OOO - option value and order specification -# QQ - quirks list -# OS - OS genre -# details - OS description -class p0fKnowledgeBase(KnowledgeBase): - def __init__(self, filename): - KnowledgeBase.__init__(self, filename) - # self.ttl_range=[255] +MIN_TCP4 = 40 # Min size of IPv4/TCP headers +MIN_TCP6 = 60 # Min size of IPv6/TCP headers +MAX_DIST = 35 # Maximum TTL distance for non-fuzzy signature matching + +WIN_TYPE_NORMAL = 0 # Literal value +WIN_TYPE_ANY = 1 # Wildcard +WIN_TYPE_MOD = 2 # Modulo check +WIN_TYPE_MSS = 3 # Window size MSS multiplier +WIN_TYPE_MTU = 4 # Window size MTU multiplier + +# Convert TCP option num to p0f (nop is handled separately) +tcp_options_p0f = { + 2: "mss", # maximum segment size + 3: "ws", # window scaling + 4: "sok", # selective ACK permitted + 5: "sack", # selective ACK (should not be seen) + 8: "ts", # timestamp +} + + +# Signatures +class TCP_Signature(object): + __slots__ = ["olayout", "quirks", "ip_opt_len", "ip_ver", "ttl", + "mss", "win", "win_type", "wscale", "pay_class", "ts1"] + + def __init__(self, olayout, quirks, ip_opt_len, ip_ver, ttl, + mss, win, win_type, wscale, pay_class, ts1): + self.olayout = olayout + self.quirks = quirks + self.ip_opt_len = ip_opt_len + self.ip_ver = ip_ver + self.ttl = ttl + self.mss = mss + self.win = win + self.win_type = win_type # None for packet signatures + self.wscale = wscale + self.pay_class = pay_class + self.ts1 = ts1 # None for base signatures + + @classmethod + def from_packet(cls, pkt): + """ + Receives a TCP packet (assuming it's valid), and returns + a TCP_Signature object + """ + ip_ver = pkt.version + quirks = set() + + def addq(name): + quirks.add(name) + + # IPv4/IPv6 parsing + if ip_ver == 4: + ttl = pkt.ttl + ip_opt_len = (pkt.ihl * 4) - 20 + if pkt.tos & (0x01 | 0x02): + addq("ecn") + if pkt.flags.evil: + addq("0+") + if pkt.flags.DF: + addq("df") + if pkt.id: + addq("id+") + elif pkt.id == 0: + addq("id-") + else: + ttl = pkt.hlim + ip_opt_len = 0 + if pkt.fl: + addq("flow") + if pkt.tc & (0x01 | 0x02): + addq("ecn") + + # TCP parsing + tcp = pkt[TCP] + win = tcp.window + if tcp.flags & (0x40 | 0x80 | 0x01): + addq("ecn") + if tcp.seq == 0: + addq("seq-") + if tcp.flags.A: + if tcp.ack == 0: + addq("ack-") + elif tcp.ack: + addq("ack+") + if tcp.flags.U: + addq("urgf+") + elif tcp.urgptr: + addq("uptr+") + if tcp.flags.P: + addq("pushf+") + + pay_class = 1 if tcp.payload else 0 + + # Manual TCP options parsing + mss = 0 + wscale = 0 + ts1 = 0 + olayout = "" + optlen = (tcp.dataofs << 2) - 20 + x = raw(tcp)[-optlen:] # raw bytes of TCP options + while x: + onum = orb(x[0]) + if onum == 0: + x = x[1:] + olayout += "eol+%i," % len(x) + if x.strip(b"\x00"): # non-zero past EOL + addq("opt+") + break + if onum == 1: + x = x[1:] + olayout += "nop," + continue + try: + olen = orb(x[1]) + except IndexError: # no room for length field + addq("bad") + break + oval = x[2:olen] + if onum in tcp_options_p0f: + ofmt = TCPOptions[0][onum][1] + olayout += "%s," % tcp_options_p0f[onum] + optsize = 2 + struct.calcsize(ofmt) if ofmt else 2 # total len + if len(x) < optsize: # option would end past end of header + addq("bad") + break + + if onum == 5: + if olen < 10 or olen > 34: # SACK length out of range + addq("bad") + break + else: + if olen != optsize: # length field doesn't fit option type + addq("bad") + break + if ofmt: + oval = struct.unpack(ofmt, oval) + if len(oval) == 1: + oval = oval[0] + if onum == 2: + mss = oval + elif onum == 3: + wscale = oval + if wscale > 14: + addq("exws") + elif onum == 8: + ts1 = oval[0] + if not ts1: + addq("ts1-") + if oval[1] and (tcp.flags.S and not tcp.flags.A): + addq("ts2+") + else: # Unknown option, presumably with specified size + if olen < 2 or olen > 40 or olen > len(x): + addq("bad") + break + x = x[olen:] + olayout = olayout[:-1] + + return cls(olayout, quirks, ip_opt_len, ip_ver, ttl, mss, win, None, wscale, pay_class, ts1) # noqa: E501 + + @classmethod + def from_raw_sig(cls, sig_line): + """ + Parses a TCP sig line and returns a tuple consisting of a + TCP_Signature object and bad_ttl as bool + """ + ver, ttl, olen, mss, wsize, olayout, quirks, pclass = lparse(sig_line, 8) # noqa: E501 + wsize, _, scale = wsize.partition(",") + + ip_ver = -1 if ver == "*" else int(ver) + ttl, bad_ttl = (int(ttl[:-1]), True) if ttl[-1] == "-" else (int(ttl), False) # noqa: E501 + ip_opt_len = int(olen) + mss = -1 if mss == "*" else int(mss) + if wsize == "*": + win, win_type = (0, WIN_TYPE_ANY) + elif wsize[:3] == "mss": + win, win_type = (int(wsize[4:]), WIN_TYPE_MSS) + elif wsize[0] == "%": + win, win_type = (int(wsize[1:]), WIN_TYPE_MOD) + elif wsize[:3] == "mtu": + win, win_type = (int(wsize[4:]), WIN_TYPE_MTU) + else: + win, win_type = (int(wsize), WIN_TYPE_NORMAL) + wscale = -1 if scale == "*" else int(scale) + if quirks: + quirks = frozenset(q for q in quirks.split(",")) + else: + quirks = frozenset() + pay_class = -1 if pclass == "*" else int(pclass == "+") + + sig = cls(olayout, quirks, ip_opt_len, ip_ver, ttl, mss, win, win_type, wscale, pay_class, None) # noqa: E501 + return sig, bad_ttl + + def __str__(self): + quirks = ",".join(q for q in self.quirks) + fmt = "%i:%i+%i:%i:%i:%i,%i:%s:%s:%i" + s = fmt % (self.ip_ver, self.ttl, guess_dist(self.ttl), + self.ip_opt_len, self.mss, self.win, self.wscale, + self.olayout, quirks, self.pay_class) + return s + + +class HTTP_Signature(object): + __slots__ = ["http_ver", "hdr", "hdr_set", "habsent", "sw"] + + def __init__(self, http_ver, hdr, hdr_set, habsent, sw): + self.http_ver = http_ver + self.hdr = hdr + self.hdr_set = hdr_set + self.habsent = habsent # None for packet signatures + self.sw = sw + + @classmethod + def from_packet(cls, pkt): + """ + Receives an HTTP packet (assuming it's valid), and returns + a HTTP_Signature object + """ + http_payload = raw(pkt[TCP].payload) + + crlfcrlf = b"\r\n\r\n" + crlfcrlfIndex = http_payload.find(crlfcrlf) + if crlfcrlfIndex != -1: + headers = http_payload[:crlfcrlfIndex + len(crlfcrlf)] + else: + headers = http_payload + headers = headers.decode() # XXX: Check if this could fail + first_line, headers = headers.split("\r\n", 1) + + if "1.0" in first_line: + http_ver = 0 + elif "1.1" in first_line: + http_ver = 1 + else: + raise ValueError("HTTP version is not 1.0/1.1") + + sw = "" + headers_found = [] + hdr_set = set() + for header_line in headers.split("\r\n"): + name, _, value = header_line.partition(":") + if value: + value = value.strip() + headers_found.append((name, value)) + hdr_set.add(name) + if name in ("User-Agent", "Server"): + sw = value + hdr = tuple(headers_found) + return cls(http_ver, hdr, hdr_set, None, sw) + + @classmethod + def from_raw_sig(cls, sig_line): + """ + Parses an HTTP sig line and returns a HTTP_Signature object + """ + ver, horder, habsent, expsw = lparse(sig_line, 4) + http_ver = -1 if ver == "*" else int(ver) + + # horder parsing - split by commas that aren't in [] + new_horder = [] + for header in re.split(r",(?![^\[]*\])", horder): + name, _, value = header.partition("=") + if name[0] == "?": # Optional header + new_horder.append((name[1:], value[1:-1], True)) + else: + new_horder.append((name, value[1:-1], False)) + hdr = tuple(new_horder) + hdr_set = frozenset(header[0] for header in hdr if not header[2]) + habsent = frozenset(habsent.split(",")) + return cls(http_ver, hdr, hdr_set, habsent, expsw) + + def __str__(self): + # values that depend on the context are not included in the string + skipval = ("Host", "User-Agent", "Date", "Content-Type", "Server") + hdr = ",".join(n if n in skipval else "%s=[%s]" % (n, v) for n, v in self.hdr) # noqa: E501 + fmt = "%i:%s::%s" + s = fmt % (self.http_ver, hdr, self.sw) + return s + + +# Records +class MTU_Record(object): + __slots__ = ["label_id", "mtu"] + + def __init__(self, label_id, sig_line): + self.label_id = label_id + self.mtu = int(sig_line) + +class TCP_Record(object): + __slots__ = ["label_id", "bad_ttl", "sig"] + + def __init__(self, label_id, sig_line): + self.label_id = label_id + sig, bad_ttl = TCP_Signature.from_raw_sig(sig_line) + self.bad_ttl = bad_ttl + self.sig = sig + + +class HTTP_Record(object): + __slots__ = ["label_id", "sig"] + + def __init__(self, label_id, sig_line): + self.label_id = label_id + self.sig = HTTP_Signature.from_raw_sig(sig_line) + + +class p0fKnowledgeBase(KnowledgeBase): + """ + .. code:: + + self.base = { + "mtu" (str): [sig(tuple), ...] + "tcp"/"http" (str): { + direction (str): [sig(tuple), ...] + } + } + self.labels = (label(tuple), ...) + + """ def lazy_init(self): try: f = open(self.filename) - except IOError: + except Exception: warning("Can't open base %s", self.filename) return - try: - self.base = [] - for line in f: - if line[0] in ["#", "\n"]: + + self.base = {} + self.labels = [] + self._parse_file(f) + self.labels = tuple(self.labels) + f.close() + + def _parse_file(self, file): + """ + Parses p0f.fp file and stores the data with described structures. + """ + label_id = -1 + + for line in file: + if line[0] in (";", "\n"): + continue + line = line.strip() + + if line[0] == "[": + section, direction = lparse(line[1:-1], 2) + if section == "mtu": + self.base[section] = [] + curr_records = self.base[section] + else: + if section not in self.base: + self.base[section] = {direction: []} + elif direction not in self.base[section]: + self.base[section][direction] = [] + curr_records = self.base[section][direction] + else: + param, _, val = line.partition(" = ") + param = param.strip() + + if param == "sig": + if section == "mtu": + record_class = MTU_Record + elif section == "tcp": + record_class = TCP_Record + elif section == "http": + record_class = HTTP_Record + curr_records.append(record_class(label_id, val)) + + elif param == "label": + label_id += 1 + if section == "mtu": + self.labels.append(val) + continue + # label = type:class:name:flavor + t, c, name, flavor = lparse(val, 4) + self.labels.append((t, c, name, flavor)) + + elif param == "sys": + sys_names = tuple(name for name in val.split(",")) + self.labels[label_id] += (sys_names,) + + def get_sigs_by_os(self, direction, osgenre, osdetails=None): + """Get TCP signatures that match an OS genre and details (if specified). + If osdetails isn't specified, then we pick all signatures + that match osgenre. + + Examples: + >>> p0fdb.get_sigs_by_os("request", "Linux", "2.6") + >>> p0fdb.get_sigs_by_os("response", "Windows", "8") + >>> p0fdb.get_sigs_by_os("request", "FreeBSD") + """ + sigs = [] + for tcp_record in self.base["tcp"][direction]: + label = self.labels[tcp_record.label_id] + name, flavor = label[2], label[3] + if osgenre and osgenre == name: + if osdetails: + if osdetails in flavor: + sigs.append(tcp_record.sig) + else: + sigs.append(tcp_record.sig) + return sigs + + def tcp_find_match(self, ts, direction): + """ + Finds the best match for the given signature and direction. + If a match is found, returns a tuple consisting of: + - label: the matched label + - dist: guessed distance from the packet source + - fuzzy: whether the match is fuzzy + Returns None if no match was found + """ + win_multi, use_mtu = detect_win_multi(ts) + + gmatch = None # generic match + fmatch = None # fuzzy match + for tcp_record in self.base["tcp"][direction]: + rs = tcp_record.sig + + fuzzy = False + ref_quirks = rs.quirks + + if rs.olayout != ts.olayout: + continue + + if rs.ip_ver == -1: + ref_quirks -= {"flow"} if ts.ip_ver == 4 else {"df", "id+", "id-"} # noqa: E501 + + if ref_quirks != ts.quirks: + deleted = (ref_quirks ^ ts.quirks) & ref_quirks + added = (ref_quirks ^ ts.quirks) & ts.quirks + + if (fmatch or (deleted - {"df", "id+"}) or (added - {"id-", "ecn"})): # noqa: E501 continue - line = tuple(line.split(":")) - if len(line) < 8: + fuzzy = True + + if rs.ip_opt_len != ts.ip_opt_len: + continue + if tcp_record.bad_ttl: + if rs.ttl < ts.ttl: continue + else: + if rs.ttl < ts.ttl or rs.ttl - ts.ttl > MAX_DIST: + fuzzy = True - def a2i(x): - if x.isdigit(): - return int(x) - return x - li = [a2i(e) for e in line[1:4]] - # if li[0] not in self.ttl_range: - # self.ttl_range.append(li[0]) - # self.ttl_range.sort() - self.base.append((line[0], li[0], li[1], li[2], line[4], - line[5], line[6], line[7][:-1])) - except Exception: - warning("Can't parse p0f database (new p0f version ?)") - self.base = None - f.close() + if ((rs.mss != -1 and rs.mss != ts.mss) or + (rs.wscale != -1 and rs.wscale != ts.wscale) or + (rs.pay_class != -1 and rs.pay_class != ts.pay_class)): + continue + if rs.win_type == WIN_TYPE_NORMAL: + if rs.win != ts.win: + continue + elif rs.win_type == WIN_TYPE_MOD: + if ts.win % rs.win: + continue + elif rs.win_type == WIN_TYPE_MSS: + if (use_mtu or rs.win != win_multi): + continue + elif rs.win_type == WIN_TYPE_MTU: + if (not use_mtu or rs.win != win_multi): + continue -p0f_kdb, p0fa_kdb, p0fr_kdb, p0fo_kdb = None, None, None, None + # Got a match? If not fuzzy, return. If fuzzy, keep looking. + label = self.labels[tcp_record.label_id] + match = (label, rs.ttl - ts.ttl, fuzzy) + if not fuzzy: + if label[0] == "s": + return match + elif not gmatch: + gmatch = match + elif not fmatch: + fmatch = match + + if gmatch: + return gmatch + if fmatch: + return fmatch + return None + def http_find_match(self, ts, direction): + """ + Finds the best match for the given signature and direction. + If a match is found, returns a tuple consisting of: + - label: the matched label + - dishonest: whether the software was detected as dishonest + Returns None if no match was found + """ + gmatch = None # generic match + for http_record in self.base["http"][direction]: + rs = http_record.sig + + if rs.http_ver != -1 and rs.http_ver != ts.http_ver: + continue + + # Check that all non-optional headers appear in the packet + if not (ts.hdr_set & rs.hdr_set) == rs.hdr_set: + continue + + # Check that no forbidden headers appear in the packet. + if len(rs.habsent & ts.hdr_set) > 0: + continue + + def headers_correl(): + phi = 0 # Packet HTTP header index + hdr_len = len(ts.hdr) + + # Confirm the ordering and values of headers + # (this is relatively slow, hence the if statements above). + # The algorithm is derived from the original p0f/fp_http.c + for kh in rs.hdr: + orig_phi = phi + while (phi < hdr_len and + kh[0] != ts.hdr[phi][0]): + phi += 1 + + if phi == hdr_len: + if not kh[2]: + return False + + for ph in ts.hdr: + if kh[0] == ph[0]: + return False + + phi = orig_phi + continue + + if kh[1] not in ts.hdr[phi][1]: + return False + phi += 1 + return True + + if not headers_correl(): + continue + + # Got a match + label = self.labels[http_record.label_id] + dishonest = rs.sw and ts.sw and rs.sw not in ts.sw + match = (label, dishonest) + if label[0] == "s": + return match + elif not gmatch: + gmatch = match + return gmatch if gmatch else None + + def mtu_find_match(self, mtu): + """ + Finds a match for the given MTU. + If a match is found, returns the label string. + Returns None if no match was found + """ + for mtu_record in self.base["mtu"]: + if mtu == mtu_record.mtu: + return self.labels[mtu_record.label_id] + return None -def p0f_load_knowledgebases(): - global p0f_kdb, p0fa_kdb, p0fr_kdb, p0fo_kdb - p0f_kdb = p0fKnowledgeBase(conf.p0f_base) - p0fa_kdb = p0fKnowledgeBase(conf.p0fa_base) - p0fr_kdb = p0fKnowledgeBase(conf.p0fr_base) - p0fo_kdb = p0fKnowledgeBase(conf.p0fo_base) +p0fdb = p0fKnowledgeBase(conf.p0f_base) -p0f_load_knowledgebases() +def guess_dist(ttl): + for ottl in (32, 64, 128, 255): + if ttl <= ottl: + return ottl - ttl -def p0f_selectdb(flags): - # tested flags: S, R, A - if flags & 0x16 == 0x2: - # SYN - return p0f_kdb - elif flags & 0x16 == 0x12: - # SYN/ACK - return p0fa_kdb - elif flags & 0x16 in [0x4, 0x14]: - # RST RST/ACK - return p0fr_kdb - elif flags & 0x16 == 0x10: - # ACK - return p0fo_kdb - else: - return None +def lparse(line, n, delimiter=":", default=""): + """ + Parsing of 'a:b:c:d:e' lines + """ + a = line.split(delimiter)[:n] + for elt in a: + yield elt + for _ in range(n - len(a)): + yield default -def packet2p0f(pkt): + +def validate_packet(pkt): + """ + Validate that the packet is an IPv4/IPv6 and TCP packet. + If the packet is valid, a copy is returned. If not, TypeError is raised. + """ pkt = pkt.copy() + valid = pkt.haslayer(TCP) and (pkt.haslayer(IP) or pkt.haslayer(IPv6)) + if not valid: + raise TypeError("Not a TCP/IP packet") + return pkt + + +def detect_win_multi(ts): + """ + Figure out if window size is a multiplier of MSS or MTU. + Receives a TCP signature and returns the multiplier and + whether mtu should be used + """ + mss = ts.mss + win = ts.win + if not win or mss < 100: + return -1, False + + options = [ + (mss, False), + (1500 - MIN_TCP4, False), + (1500 - MIN_TCP4 - 12, False), + (mss + MIN_TCP4, True), + (1500, True) + ] + if ts.ts1: + options.append((mss - 12, False)) + if ts.ip_ver == 6: + options.append((1500 - MIN_TCP6, False)) + options.append((1500 - MIN_TCP6 - 12, False)) + options.append((mss + MIN_TCP6, True)) + + for div, use_mtu in options: + if not win % div: + return win / div, use_mtu + return -1, False + + +def packet2p0f(pkt): + """ + Returns a p0f signature of the packet, and the direction. + Raises TypeError if the packet isn't valid for p0f + """ + pkt = validate_packet(pkt) pkt = pkt.__class__(raw(pkt)) - while pkt.haslayer(IP) and pkt.haslayer(TCP): - pkt = pkt.getlayer(IP) - if isinstance(pkt.payload, TCP): - break - pkt = pkt.payload - if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP): - raise TypeError("Not a TCP/IP packet") - # if pkt.payload.flags & 0x7 != 0x02: #S,!F,!R - # raise TypeError("Not a SYN or SYN/ACK packet") - - db = p0f_selectdb(pkt.payload.flags) - - # t = p0f_kdb.ttl_range[:] - # t += [pkt.ttl] - # t.sort() - # ttl=t[t.index(pkt.ttl)+1] - ttl = pkt.ttl - - ss = len(pkt) - # from p0f/config.h : PACKET_BIG = 100 - if ss > 100: - if db == p0fr_kdb: - # p0fr.fp: "Packet size may be wildcarded. The meaning of - # wildcard is, however, hardcoded as 'size > - # PACKET_BIG'" - ss = '*' + if pkt[TCP].flags.S: + if pkt[TCP].flags.A: + direction = "response" else: - ss = 0 - if db == p0fo_kdb: - # p0fo.fp: "Packet size MUST be wildcarded." - ss = '*' - - ooo = "" - mss = -1 - qqT = False - qqP = False - # qqBroken = False - ilen = (pkt.payload.dataofs << 2) - 20 # from p0f.c - for option in pkt.payload.options: - ilen -= 1 - if option[0] == "MSS": - ooo += "M" + str(option[1]) + "," - mss = option[1] - # FIXME: qqBroken - ilen -= 3 - elif option[0] == "WScale": - ooo += "W" + str(option[1]) + "," - # FIXME: qqBroken - ilen -= 2 - elif option[0] == "Timestamp": - if option[1][0] == 0: - ooo += "T0," - else: - ooo += "T," - if option[1][1] != 0: - qqT = True - ilen -= 9 - elif option[0] == "SAckOK": - ooo += "S," - ilen -= 1 - elif option[0] == "NOP": - ooo += "N," - elif option[0] == "EOL": - ooo += "E," - if ilen > 0: - qqP = True + direction = "request" + sig = TCP_Signature.from_packet(pkt) + + elif pkt[TCP].payload: + # XXX: guess_payload_class doesn't use any class related attributes + pclass = HTTP().guess_payload_class(raw(pkt[TCP].payload)) + if pclass == HTTPRequest: + direction = "request" + elif pclass == HTTPResponse: + direction = "response" else: - if isinstance(option[0], str): - ooo += "?%i," % TCPOptions[1][option[0]] - else: - ooo += "?%i," % option[0] - # FIXME: ilen - ooo = ooo[:-1] - if ooo == "": - ooo = "." - - win = pkt.payload.window - if mss != -1: - if mss != 0 and win % mss == 0: - win = "S" + str(win / mss) - elif win % (mss + 40) == 0: - win = "T" + str(win / (mss + 40)) - win = str(win) - - qq = "" - - if db == p0fr_kdb: - if pkt.payload.flags & 0x10 == 0x10: - # p0fr.fp: "A new quirk, 'K', is introduced to denote - # RST+ACK packets" - qq += "K" - # The two next cases should also be only for p0f*r*, but although - # it's not documented (or I have not noticed), p0f seems to - # support the '0' and 'Q' quirks on any databases (or at the least - # "classical" p0f.fp). - if pkt.payload.seq == pkt.payload.ack: - # p0fr.fp: "A new quirk, 'Q', is used to denote SEQ number - # equal to ACK number." - qq += "Q" - if pkt.payload.seq == 0: - # p0fr.fp: "A new quirk, '0', is used to denote packets - # with SEQ number set to 0." - qq += "0" - if qqP: - qq += "P" - if pkt.id == 0: - qq += "Z" - if pkt.options != []: - qq += "I" - if pkt.payload.urgptr != 0: - qq += "U" - if pkt.payload.reserved != 0: - qq += "X" - if pkt.payload.ack != 0: - qq += "A" - if qqT: - qq += "T" - if db == p0fo_kdb: - if pkt.payload.flags & 0x20 != 0: - # U - # p0fo.fp: "PUSH flag is excluded from 'F' quirk checks" - qq += "F" + raise TypeError("Not an HTTP payload") + sig = HTTP_Signature.from_packet(pkt) else: - if pkt.payload.flags & 0x28 != 0: - # U or P - qq += "F" - if db != p0fo_kdb and not isinstance(pkt.payload.payload, NoPayload): - # p0fo.fp: "'D' quirk is not checked for." - qq += "D" - # FIXME : "!" - broken options segment: not handled yet - - if qq == "": - qq = "." - - return (db, (win, ttl, pkt.flags.DF, ss, ooo, qq)) - - -def p0f_correl(x, y): - d = 0 - # wwww can be "*" or "%nn". "Tnn" and "Snn" should work fine with - # the x[0] == y[0] test. - d += (x[0] == y[0] or y[0] == "*" or (y[0][0] == "%" and x[0].isdigit() and (int(x[0]) % int(y[0][1:])) == 0)) # noqa: E501 - # ttl - d += (y[1] >= x[1] and y[1] - x[1] < 32) - for i in [2, 5]: - d += (x[i] == y[i] or y[i] == '*') - # '*' has a special meaning for ss - d += x[3] == y[3] - xopt = x[4].split(",") - yopt = y[4].split(",") - if len(xopt) == len(yopt): - same = True - for i in range(len(xopt)): - if not (xopt[i] == yopt[i] or - (len(yopt[i]) == 2 and len(xopt[i]) > 1 and - yopt[i][1] == "*" and xopt[i][0] == yopt[i][0]) or - (len(yopt[i]) > 2 and len(xopt[i]) > 1 and - yopt[i][1] == "%" and xopt[i][0] == yopt[i][0] and - int(xopt[i][1:]) % int(yopt[i][2:]) == 0)): - same = False - break - if same: - d += len(xopt) - return d + raise TypeError("Not a SYN, SYN/ACK, or HTTP packet") + return sig, direction + + +def fingerprint_mtu(pkt): + """ + Fingerprints the MTU based on the maximum segment size specified + in TCP options. + If a match was found, returns the label. If not returns None + """ + pkt = validate_packet(pkt) + mss = 0 + for name, value in pkt.payload.options: + if name == "MSS": + mss = value + + if not mss: + return None + + mtu = (mss + MIN_TCP4) if pkt.version == 4 else (mss + MIN_TCP6) + + if not p0fdb.get_base(): + warning("p0f base empty.") + return None + + return p0fdb.mtu_find_match(mtu) -@conf.commands.register def p0f(pkt): - """Passive OS fingerprinting: which OS emitted this TCP packet ? -p0f(packet) -> accuracy, [list of guesses] -""" - db, sig = packet2p0f(pkt) - if db: - pb = db.get_base() - else: - pb = [] - if not pb: + sig, direction = packet2p0f(pkt) + if not p0fdb.get_base(): warning("p0f base empty.") - return [] - # s = len(pb[0][0]) - r = [] - max = len(sig[4].split(",")) + 5 - for b in pb: - d = p0f_correl(sig, b) - if d == max: - r.append((b[6], b[7], b[1] - pkt[IP].ttl)) - return r + return None + + if isinstance(sig, TCP_Signature): + return p0fdb.tcp_find_match(sig, direction) + else: + return p0fdb.http_find_match(sig, direction) def prnp0f(pkt): - """Calls p0f and returns a user-friendly output""" - # we should print which DB we use + """Calls p0f and prints a user-friendly output""" try: r = p0f(pkt) except Exception: return - if r == []: - r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt)[1])) + ":?:?]", None) # noqa: E501 + + sig, direction = packet2p0f(pkt) + is_tcp_sig = isinstance(sig, TCP_Signature) + to_server = direction == "request" + + if is_tcp_sig: + pkt_type = "SYN" if to_server else "SYN+ACK" else: - r = r[0] - uptime = None - try: - uptime = pkt2uptime(pkt) - except Exception: - pass - if uptime == 0: - uptime = None - res = pkt.sprintf("%IP.src%:%TCP.sport% - " + r[0] + " " + r[1]) - if uptime is not None: - res += pkt.sprintf(" (up: " + str(uptime / 3600) + " hrs)\n -> %IP.dst%:%TCP.dport% (%TCP.flags%)") # noqa: E501 + pkt_type = "HTTP Request" if to_server else "HTTP Response" + + res = pkt.sprintf(".-[ %IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport% (" + pkt_type + ") ]-\n|\n") # noqa: E501 + fields = [] + + def add_field(name, value): + fields.append("| %-8s = %s\n" % (name, value)) + + cli_or_svr = "Client" if to_server else "Server" + add_field(cli_or_svr, pkt.sprintf("%IP.src%:%TCP.sport%")) + + if r: + label = r[0] + app_or_os = "App" if label[1] == "!" else "OS" + add_field(app_or_os, label[2] + " " + label[3]) + if len(label) == 5: # label includes sys + add_field("Sys", ", ".join(name for name in label[4])) + if is_tcp_sig: + add_field("Distance", r[1]) else: - res += pkt.sprintf("\n -> %IP.dst%:%TCP.dport% (%TCP.flags%)") - if r[2] is not None: - res += " (distance " + str(r[2]) + ")" - print(res) + app_or_os = "OS" if is_tcp_sig else "App" + add_field(app_or_os, "UNKNOWN") + add_field("Raw sig", str(sig)) -@conf.commands.register -def pkt2uptime(pkt, HZ=100): - """Calculate the date the machine which emitted the packet booted using TCP timestamp # noqa: E501 -pkt2uptime(pkt, [HZ=100])""" - if not isinstance(pkt, Packet): - raise TypeError("Not a TCP packet") - if isinstance(pkt, NoPayload): - raise TypeError("Not a TCP packet") - if not isinstance(pkt, TCP): - return pkt2uptime(pkt.payload) - for opt in pkt.options: - if opt[0] == "Timestamp": - # t = pkt.time - opt[1][0] * 1.0/HZ - # return time.ctime(t) - t = opt[1][0] / HZ - return t - raise TypeError("No timestamp option") + res += "".join(fields) + res += "`____\n" + print(res) def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, extrahops=0, mtu=1500, uptime=None): - """Modifies pkt so that p0f will think it has been sent by a -specific OS. If osdetails is None, then we randomly pick up a -personality matching osgenre. If osgenre and signature are also None, -we use a local signature (using p0f_getlocalsigs). If signature is -specified (as a tuple), we use the signature. - -For now, only TCP Syn packets are supported. -Some specifications of the p0f.fp file are not (yet) implemented.""" - pkt = pkt.copy() - # pkt = pkt.__class__(raw(pkt)) - while pkt.haslayer(IP) and pkt.haslayer(TCP): - pkt = pkt.getlayer(IP) - if isinstance(pkt.payload, TCP): - break - pkt = pkt.payload - - if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP): - raise TypeError("Not a TCP/IP packet") + """ + Modifies pkt so that p0f will think it has been sent by a + specific OS. Either osgenre or signature is required to impersonate. + If signature is specified (as a raw string), we use the signature. + signature format:: + + "ip_ver:ttl:ip_opt_len:mss:window,wscale:opt_layout:quirks:pay_class" + + If osgenre is specified, we randomly pick a signature with a label + that matches osgenre (and osdetails, if specified). + Note: osgenre is case sensitive ("linux" -> "Linux" etc.), and osdetails + is a substring of a label flavor ("7", "8" and "7 or 8" will + all match the label "s:win:Windows:7 or 8") + + For now, only TCP SYN/SYN+ACK packets are supported. + """ + pkt = validate_packet(pkt) + + if not osgenre and not signature: + raise ValueError("osgenre or signature is required to impersonate!") - db = p0f_selectdb(pkt.payload.flags) - if osgenre: - pb = db.get_base() - if pb is None: - pb = [] - pb = [x for x in pb if x[6] == osgenre] - if osdetails: - pb = [x for x in pb if x[7] == osdetails] - elif signature: - pb = [signature] + tcp = pkt[TCP] + tcp_type = tcp.flags & (0x02 | 0x10) # SYN / SYN+ACK + + if signature: + if isinstance(signature, str): + sig, _ = TCP_Signature.from_raw_sig(signature) + else: + raise TypeError("Unsupported signature type") else: - pb = p0f_getlocalsigs()[db] - if db == p0fr_kdb: - # 'K' quirk <=> RST+ACK - if pkt.payload.flags & 0x4 == 0x4: - pb = [x for x in pb if 'K' in x[5]] + if not p0fdb.get_base(): + sigs = [] else: - pb = [x for x in pb if 'K' not in x[5]] - if not pb: - raise Scapy_Exception("No match in the p0f database") - pers = pb[random.randint(0, len(pb) - 1)] + direction = "request" if tcp_type == 0x02 else "response" + sigs = p0fdb.get_sigs_by_os(direction, osgenre, osdetails) - # options (we start with options because of MSS) - # Take the options already set as "hints" to use in the new packet if we - # can. MSS, WScale and Timestamp can all be wildcarded in a signature, so - # we'll use the already-set values if they're valid integers. - orig_opts = dict(pkt.payload.options) - int_only = lambda val: val if isinstance(val, six.integer_types) else None - mss_hint = int_only(orig_opts.get('MSS')) - wscale_hint = int_only(orig_opts.get('WScale')) - ts_hint = [int_only(o) for o in orig_opts.get('Timestamp', (None, None))] + # If IPv6 packet, remove IPv4-only signatures and vice versa + sigs = [s for s in sigs if s.ip_ver == -1 or s.ip_ver == pkt.version] + if not sigs: + raise ValueError("No match in the p0f database") + sig = random.choice(sigs) - options = [] - if pers[4] != '.': - for opt in pers[4].split(','): - if opt[0] == 'M': - # MSS might have a maximum size because of window size - # specification - if pers[0][0] == 'S': - maxmss = (2**16 - 1) // int(pers[0][1:]) - else: - maxmss = (2**16 - 1) - # disregard hint if out of range - if mss_hint and not 0 <= mss_hint <= maxmss: - mss_hint = None - # If we have to randomly pick up a value, we cannot use - # scapy RandXXX() functions, because the value has to be - # set in case we need it for the window size value. That's - # why we use random.randint() - if opt[1:] == '*': - if mss_hint is not None: - options.append(('MSS', mss_hint)) - else: - options.append(('MSS', random.randint(1, maxmss))) - elif opt[1] == '%': - coef = int(opt[2:]) - if mss_hint is not None and mss_hint % coef == 0: - options.append(('MSS', mss_hint)) - else: - options.append(( - 'MSS', coef * random.randint(1, maxmss // coef))) - else: - options.append(('MSS', int(opt[1:]))) - elif opt[0] == 'W': - if wscale_hint and not 0 <= wscale_hint < 2**8: - wscale_hint = None - if opt[1:] == '*': - if wscale_hint is not None: - options.append(('WScale', wscale_hint)) - else: - options.append(('WScale', RandByte())) - elif opt[1] == '%': - coef = int(opt[2:]) - if wscale_hint is not None and wscale_hint % coef == 0: - options.append(('WScale', wscale_hint)) - else: - options.append(( - 'WScale', coef * RandNum(min=1, max=(2**8 - 1) // coef))) # noqa: E501 - else: - options.append(('WScale', int(opt[1:]))) - elif opt == 'T0': - options.append(('Timestamp', (0, 0))) - elif opt == 'T': - # Determine first timestamp. - if uptime is not None: - ts_a = uptime - elif ts_hint[0] and 0 < ts_hint[0] < 2**32: - # Note: if first ts is 0, p0f registers it as "T0" not "T", - # hence we don't want to use the hint if it was 0. - ts_a = ts_hint[0] - else: - ts_a = random.randint(120, 100 * 60 * 60 * 24 * 365) - # Determine second timestamp. - if 'T' not in pers[5]: - ts_b = 0 - elif ts_hint[1] and 0 < ts_hint[1] < 2**32: - ts_b = ts_hint[1] - else: - # FIXME: RandInt() here does not work (bug (?) in - # TCPOptionsField.m2i often raises "OverflowError: - # long int too large to convert to int" in: - # oval = struct.pack(ofmt, *oval)" - # Actually, this is enough to often raise the error: - # struct.pack('I', RandInt()) - ts_b = random.randint(1, 2**32 - 1) - options.append(('Timestamp', (ts_a, ts_b))) - elif opt == 'S': - options.append(('SAckOK', '')) - elif opt == 'N': - options.append(('NOP', None)) - elif opt == 'E': - options.append(('EOL', None)) - elif opt[0] == '?': - if int(opt[1:]) in TCPOptions[0]: - optname = TCPOptions[0][int(opt[1:])][0] - optstruct = TCPOptions[0][int(opt[1:])][1] - options.append((optname, - struct.unpack(optstruct, - RandString(struct.calcsize(optstruct))._fix()))) # noqa: E501 - else: - options.append((int(opt[1:]), '')) - # FIXME: qqP not handled - else: - warning("unhandled TCP option " + opt) - pkt.payload.options = options - - # window size - if pers[0] == '*': - pkt.payload.window = RandShort() - elif pers[0].isdigit(): - pkt.payload.window = int(pers[0]) - elif pers[0][0] == '%': - coef = int(pers[0][1:]) - pkt.payload.window = coef * RandNum(min=1, max=(2**16 - 1) // coef) - elif pers[0][0] == 'T': - pkt.payload.window = mtu * int(pers[0][1:]) - elif pers[0][0] == 'S': - # needs MSS set - mss = [x for x in options if x[0] == 'MSS'] - if not mss: - raise Scapy_Exception("TCP window value requires MSS, and MSS option not set") # noqa: E501 - pkt.payload.window = mss[0][1] * int(pers[0][1:]) - else: - raise Scapy_Exception('Unhandled window size specification') - - # ttl - pkt.ttl = pers[1] - extrahops - # DF flag - pkt.flags |= (2 * pers[2]) - # FIXME: ss (packet size) not handled (how ? may be with D quirk - # if present) - # Quirks - if pers[5] != '.': - for qq in pers[5]: - # FIXME: not handled: P, I, X, ! - # T handled with the Timestamp option - if qq == 'Z': - pkt.id = 0 - elif qq == 'U': - pkt.payload.urgptr = RandShort() - elif qq == 'A': - pkt.payload.ack = RandInt() - elif qq == 'F': - if db == p0fo_kdb: - pkt.payload.flags |= 0x20 # U - else: - pkt.payload.flags |= random.choice([8, 32, 40]) # P/U/PU - elif qq == 'D' and db != p0fo_kdb: - pkt /= conf.raw_layer(load=RandString(random.randint(1, 10))) # XXX p0fo.fp # noqa: E501 - elif qq == 'Q': - pkt.payload.seq = pkt.payload.ack - # elif qq == '0': pkt.payload.seq = 0 - # if db == p0fr_kdb: - # '0' quirk is actually not only for p0fr.fp (see - # packet2p0f()) - if '0' in pers[5]: - pkt.payload.seq = 0 - elif pkt.payload.seq == 0: - pkt.payload.seq = RandInt() - - while pkt.underlayer: - pkt = pkt.underlayer - return pkt + if sig.ip_ver != -1 and pkt.version != sig.ip_ver: + raise ValueError("Can't convert between IPv4 and IPv6") + quirks = sig.quirks -def p0f_getlocalsigs(): - """This function returns a dictionary of signatures indexed by p0f -db (e.g., p0f_kdb, p0fa_kdb, ...) for the local TCP/IP stack. + if pkt.version == 4: + pkt.ttl = sig.ttl - extrahops + if sig.ip_opt_len != 0: + # FIXME: Non-zero IPv4 options not handled + warning("Unhandled IPv4 option field") + else: + pkt.options = [] -You need to have your firewall at least accepting the TCP packets -from/to a high port (30000 <= x <= 40000) on your loopback interface. + if "df" in quirks: + pkt.flags |= 0x02 # set DF flag + if "id+" in quirks: + if pkt.id == 0: + pkt.id = random.randint(1, 2**16 - 1) + else: + pkt.id = 0 + else: + pkt.flags &= ~(0x02) # DF flag not set + if "id-" in quirks: + pkt.id = 0 + elif pkt.id == 0: + pkt.id = random.randint(1, 2**16 - 1) + if "ecn" in quirks: + pkt.tos |= random.randint(0x01, 0x03) + pkt.flags = pkt.flags | 0x04 if "0+" in quirks else pkt.flags & ~(0x04) + else: + pkt.hlim = sig.ttl - extrahops + if "flow" in quirks: + pkt.fl = random.randint(1, 2**20 - 1) + if "ecn" in quirks: + pkt.tc |= random.randint(0x01, 0x03) -Please note that the generated signatures come from the loopback -interface and may (are likely to) be different than those generated on -"normal" interfaces.""" - pid = os.fork() - port = random.randint(30000, 40000) - if pid > 0: - # parent: sniff - result = {} + # Take the options already set as "hints" to use in the new packet if we + # can. we'll use the already-set values if they're valid integers. + def int_only(val): + return val if isinstance(val, int) else None + orig_opts = dict(tcp.options) + mss_hint = int_only(orig_opts.get("MSS")) + ws_hint = int_only(orig_opts.get("WScale")) + ts_hint = [int_only(o) for o in orig_opts.get("Timestamp", (None, None))] + + options = [] + for opt in sig.olayout.split(","): + if opt == "mss": + # MSS might have a maximum size because of WIN_TYPE_MSS + if sig.win_type == WIN_TYPE_MSS: + maxmss = (2**16 - 1) // sig.win + else: + maxmss = (2**16 - 1) - def addresult(res): - # TODO: wildcard window size in some cases? and maybe some - # other values? - if res[0] not in result: - result[res[0]] = [res[1]] + if sig.mss == -1: # wildcard mss + if mss_hint and 0 <= mss_hint <= maxmss: + options.append(("MSS", mss_hint)) + else: # invalid hint, generate new value + options.append(("MSS", random.randint(100, maxmss))) + else: + options.append(("MSS", sig.mss)) + + elif opt == "ws": + if sig.wscale == -1: # wildcard wscale + maxws = 2**8 + if "exws" in quirks: # wscale > 14 + if ws_hint and 14 < ws_hint < maxws: + options.append(("WScale", ws_hint)) + else: # invalid hint, generate new value > 14 + options.append(("WScale", random.randint(15, maxws - 1))) # noqa: E501 + else: + if ws_hint and 0 <= ws_hint < maxws: + options.append(("WScale", ws_hint)) + else: # invalid hint, generate new value + options.append(("WScale", RandByte())) + else: + options.append(("WScale", sig.wscale)) + + elif opt == "ts": + ts1, ts2 = ts_hint + + if "ts1-" in quirks: # own timestamp specified as zero + ts1 = 0 + elif uptime is not None: # if specified uptime, override + ts1 = uptime + elif ts1 is None or not (0 < ts1 < 2**32): # invalid hint + ts1 = random.randint(120, 100 * 60 * 60 * 24 * 365) + + # non-zero peer timestamp on initial SYN + if "ts2+" in quirks and tcp_type == 0x02: + if ts2 is None or not (0 < ts2 < 2**32): # invalid hint + ts2 = random.randint(1, 2**32 - 1) else: - if res[1] not in result[res[0]]: - result[res[0]].append(res[1]) - # XXX could we try with a "normal" interface using other hosts - iface = conf.route.route('127.0.0.1')[0] - # each packet is seen twice: S + RA, S + SA + A + FA + A - # XXX are the packets also seen twice on non Linux systems ? - count = 14 - pl = sniff(iface=iface, filter='tcp and port ' + str(port), count=count, timeout=3) # noqa: E501 - for pkt in pl: - for elt in packet2p0f(pkt): - addresult(elt) - os.waitpid(pid, 0) - elif pid < 0: - log_runtime.error("fork error") + ts2 = 0 + options.append(("Timestamp", (ts1, ts2))) + + elif opt == "nop": + options.append(("NOP", None)) + elif opt == "sok": + options.append(("SAckOK", "")) + elif opt[:3] == "eol": + options.append(("EOL", None)) + # FIXME: opt+ quirk not handled + if "opt+" in quirks: + warning("Unhandled opt+ quirk") + elif opt == "sack": + # Randomize SAck value in range of 10 <= val <= 34 + sack_len = random.choice([10, 18, 26, 34]) - 2 + optstruct = "!%iI" % (sack_len // 4) + rand_val = RandString(struct.calcsize(optstruct))._fix() + options.append(("SAck", struct.unpack(optstruct, rand_val))) + else: + warning("Unhandled TCP option %s", opt) + tcp.options = options + + if sig.win_type == WIN_TYPE_NORMAL: + tcp.window = sig.win + elif sig.win_type == WIN_TYPE_MSS: + mss = [x for x in options if x[0] == "MSS"] + if not mss: + raise ValueError("TCP window value requires MSS, and MSS option not set") # noqa: E501 + tcp.window = mss[0][1] * sig.win + elif sig.win_type == WIN_TYPE_MOD: + tcp.window = sig.win * random.randint(1, (2**16 - 1) // sig.win) + elif sig.win_type == WIN_TYPE_MTU: + tcp.window = mtu * sig.win + elif sig.win_type == WIN_TYPE_ANY: + tcp.window = RandShort() else: - # child: send - # XXX erk - time.sleep(1) - s1 = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) - # S & RA - try: - s1.connect(('127.0.0.1', port)) - except socket.error: - pass - # S, SA, A, FA, A - s1.bind(('127.0.0.1', port)) - s1.connect(('127.0.0.1', port)) - # howto: get an RST w/o ACK packet - s1.close() - os._exit(0) - return result + warning("Unhandled window size specification") + + if "seq-" in quirks: + tcp.seq = 0 + elif tcp.seq == 0: + tcp.seq = random.randint(1, 2**32 - 1) + + if "ack+" in quirks: + tcp.flags &= ~(0x10) # ACK flag not set + if tcp.ack == 0: + tcp.ack = random.randint(1, 2**32 - 1) + elif "ack-" in quirks: + tcp.flags |= 0x10 # ACK flag set + tcp.ack = 0 + + if "uptr+" in quirks: + tcp.flags &= ~(0x020) # URG flag not set + if tcp.urgptr == 0: + tcp.urgptr = random.randint(1, 2**16 - 1) + elif "urgf+" in quirks: + tcp.flags |= 0x020 # URG flag used + + tcp.flags = tcp.flags | 0x08 if "pushf+" in quirks else tcp.flags & ~(0x08) + + if sig.pay_class: # signature has payload + if not tcp.payload: + pkt /= conf.raw_layer(load=RandString(random.randint(1, 10))) + else: + tcp.payload = NoPayload() + + return pkt diff --git a/scapy/modules/p0fv2.py b/scapy/modules/p0fv2.py new file mode 100644 index 00000000000..353288b2bda --- /dev/null +++ b/scapy/modules/p0fv2.py @@ -0,0 +1,619 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi + +""" +Clone of p0f v2 passive OS fingerprinting +""" + +import time +import struct +import os +import socket +import random + +from scapy.data import KnowledgeBase, select_path +from scapy.config import conf +from scapy.compat import raw +from scapy.layers.inet import IP, TCP, TCPOptions +from scapy.packet import NoPayload, Packet +from scapy.error import warning, Scapy_Exception, log_runtime +from scapy.volatile import RandInt, RandByte, RandNum, RandShort, RandString +from scapy.sendrecv import sniff +if conf.route is None: + # unused import, only to initialize conf.route + import scapy.route # noqa: F401 + +_p0fpaths = ["/etc/p0f", "/usr/share/p0f", "/opt/local"] + +conf.p0f_base = select_path(_p0fpaths, "p0f.fp") +conf.p0fa_base = select_path(_p0fpaths, "p0fa.fp") +conf.p0fr_base = select_path(_p0fpaths, "p0fr.fp") +conf.p0fo_base = select_path(_p0fpaths, "p0fo.fp") + + +############### +# p0f stuff # +############### + +# File format (according to p0f.fp) : +# +# wwww:ttt:D:ss:OOO...:QQ:OS:Details +# +# wwww - window size +# ttt - initial TTL +# D - don't fragment bit (0=unset, 1=set) +# ss - overall SYN packet size +# OOO - option value and order specification +# QQ - quirks list +# OS - OS genre +# details - OS description + +class p0fKnowledgeBase(KnowledgeBase): + def __init__(self, filename): + KnowledgeBase.__init__(self, filename) + # self.ttl_range=[255] + + def lazy_init(self): + try: + f = open(self.filename) + except IOError: + warning("Can't open base %s", self.filename) + return + try: + self.base = [] + for line in f: + if line[0] in ["#", "\n"]: + continue + line = tuple(line.split(":")) + if len(line) < 8: + continue + + def a2i(x): + if x.isdigit(): + return int(x) + return x + li = [a2i(e) for e in line[1:4]] + # if li[0] not in self.ttl_range: + # self.ttl_range.append(li[0]) + # self.ttl_range.sort() + self.base.append((line[0], li[0], li[1], li[2], line[4], + line[5], line[6], line[7][:-1])) + except Exception: + warning("Can't parse p0f database (new p0f version ?)") + self.base = None + f.close() + + +p0f_kdb, p0fa_kdb, p0fr_kdb, p0fo_kdb = None, None, None, None + + +def p0f_load_knowledgebases(): + global p0f_kdb, p0fa_kdb, p0fr_kdb, p0fo_kdb + p0f_kdb = p0fKnowledgeBase(conf.p0f_base) + p0fa_kdb = p0fKnowledgeBase(conf.p0fa_base) + p0fr_kdb = p0fKnowledgeBase(conf.p0fr_base) + p0fo_kdb = p0fKnowledgeBase(conf.p0fo_base) + + +p0f_load_knowledgebases() + + +def p0f_selectdb(flags): + # tested flags: S, R, A + if flags & 0x16 == 0x2: + # SYN + return p0f_kdb + elif flags & 0x16 == 0x12: + # SYN/ACK + return p0fa_kdb + elif flags & 0x16 in [0x4, 0x14]: + # RST RST/ACK + return p0fr_kdb + elif flags & 0x16 == 0x10: + # ACK + return p0fo_kdb + else: + return None + + +def packet2p0f(pkt): + pkt = pkt.copy() + pkt = pkt.__class__(raw(pkt)) + while pkt.haslayer(IP) and pkt.haslayer(TCP): + pkt = pkt.getlayer(IP) + if isinstance(pkt.payload, TCP): + break + pkt = pkt.payload + + if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP): + raise TypeError("Not a TCP/IP packet") + # if pkt.payload.flags & 0x7 != 0x02: #S,!F,!R + # raise TypeError("Not a SYN or SYN/ACK packet") + + db = p0f_selectdb(pkt.payload.flags) + + # t = p0f_kdb.ttl_range[:] + # t += [pkt.ttl] + # t.sort() + # ttl=t[t.index(pkt.ttl)+1] + ttl = pkt.ttl + + ss = len(pkt) + # from p0f/config.h : PACKET_BIG = 100 + if ss > 100: + if db == p0fr_kdb: + # p0fr.fp: "Packet size may be wildcarded. The meaning of + # wildcard is, however, hardcoded as 'size > + # PACKET_BIG'" + ss = '*' + else: + ss = 0 + if db == p0fo_kdb: + # p0fo.fp: "Packet size MUST be wildcarded." + ss = '*' + + ooo = "" + mss = -1 + qqT = False + qqP = False + # qqBroken = False + ilen = (pkt.payload.dataofs << 2) - 20 # from p0f.c + for option in pkt.payload.options: + ilen -= 1 + if option[0] == "MSS": + ooo += "M" + str(option[1]) + "," + mss = option[1] + # FIXME: qqBroken + ilen -= 3 + elif option[0] == "WScale": + ooo += "W" + str(option[1]) + "," + # FIXME: qqBroken + ilen -= 2 + elif option[0] == "Timestamp": + if option[1][0] == 0: + ooo += "T0," + else: + ooo += "T," + if option[1][1] != 0: + qqT = True + ilen -= 9 + elif option[0] == "SAckOK": + ooo += "S," + ilen -= 1 + elif option[0] == "NOP": + ooo += "N," + elif option[0] == "EOL": + ooo += "E," + if ilen > 0: + qqP = True + else: + if isinstance(option[0], str): + ooo += "?%i," % TCPOptions[1][option[0]] + else: + ooo += "?%i," % option[0] + # FIXME: ilen + ooo = ooo[:-1] + if ooo == "": + ooo = "." + + win = pkt.payload.window + if mss != -1: + if mss != 0 and win % mss == 0: + win = "S" + str(win / mss) + elif win % (mss + 40) == 0: + win = "T" + str(win / (mss + 40)) + win = str(win) + + qq = "" + + if db == p0fr_kdb: + if pkt.payload.flags & 0x10 == 0x10: + # p0fr.fp: "A new quirk, 'K', is introduced to denote + # RST+ACK packets" + qq += "K" + # The two next cases should also be only for p0f*r*, but although + # it's not documented (or I have not noticed), p0f seems to + # support the '0' and 'Q' quirks on any databases (or at the least + # "classical" p0f.fp). + if pkt.payload.seq == pkt.payload.ack: + # p0fr.fp: "A new quirk, 'Q', is used to denote SEQ number + # equal to ACK number." + qq += "Q" + if pkt.payload.seq == 0: + # p0fr.fp: "A new quirk, '0', is used to denote packets + # with SEQ number set to 0." + qq += "0" + if qqP: + qq += "P" + if pkt.id == 0: + qq += "Z" + if pkt.options != []: + qq += "I" + if pkt.payload.urgptr != 0: + qq += "U" + if pkt.payload.reserved != 0: + qq += "X" + if pkt.payload.ack != 0: + qq += "A" + if qqT: + qq += "T" + if db == p0fo_kdb: + if pkt.payload.flags & 0x20 != 0: + # U + # p0fo.fp: "PUSH flag is excluded from 'F' quirk checks" + qq += "F" + else: + if pkt.payload.flags & 0x28 != 0: + # U or P + qq += "F" + if db != p0fo_kdb and not isinstance(pkt.payload.payload, NoPayload): + # p0fo.fp: "'D' quirk is not checked for." + qq += "D" + # FIXME : "!" - broken options segment: not handled yet + + if qq == "": + qq = "." + + return (db, (win, ttl, pkt.flags.DF, ss, ooo, qq)) + + +def p0f_correl(x, y): + d = 0 + # wwww can be "*" or "%nn". "Tnn" and "Snn" should work fine with + # the x[0] == y[0] test. + d += (x[0] == y[0] or y[0] == "*" or (y[0][0] == "%" and x[0].isdigit() and (int(x[0]) % int(y[0][1:])) == 0)) # noqa: E501 + # ttl + d += (y[1] >= x[1] and y[1] - x[1] < 32) + for i in [2, 5]: + d += (x[i] == y[i] or y[i] == '*') + # '*' has a special meaning for ss + d += x[3] == y[3] + xopt = x[4].split(",") + yopt = y[4].split(",") + if len(xopt) == len(yopt): + same = True + for i in range(len(xopt)): + if not (xopt[i] == yopt[i] or + (len(yopt[i]) == 2 and len(xopt[i]) > 1 and + yopt[i][1] == "*" and xopt[i][0] == yopt[i][0]) or + (len(yopt[i]) > 2 and len(xopt[i]) > 1 and + yopt[i][1] == "%" and xopt[i][0] == yopt[i][0] and + int(xopt[i][1:]) % int(yopt[i][2:]) == 0)): + same = False + break + if same: + d += len(xopt) + return d + + +@conf.commands.register +def p0f(pkt): + """Passive OS fingerprinting: which OS emitted this TCP packet ? +p0f(packet) -> accuracy, [list of guesses] +""" + db, sig = packet2p0f(pkt) + if db: + pb = db.get_base() + else: + pb = [] + if not pb: + warning("p0f base empty.") + return [] + # s = len(pb[0][0]) + r = [] + max = len(sig[4].split(",")) + 5 + for b in pb: + d = p0f_correl(sig, b) + if d == max: + r.append((b[6], b[7], b[1] - pkt[IP].ttl)) + return r + + +def prnp0f(pkt): + """Calls p0f and returns a user-friendly output""" + # we should print which DB we use + try: + r = p0f(pkt) + except Exception: + return + if r == []: + r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt)[1])) + ":?:?]", None) # noqa: E501 + else: + r = r[0] + uptime = None + try: + uptime = pkt2uptime(pkt) + except Exception: + pass + if uptime == 0: + uptime = None + res = pkt.sprintf("%IP.src%:%TCP.sport% - " + r[0] + " " + r[1]) + if uptime is not None: + res += pkt.sprintf(" (up: " + str(uptime / 3600) + " hrs)\n -> %IP.dst%:%TCP.dport% (%TCP.flags%)") # noqa: E501 + else: + res += pkt.sprintf("\n -> %IP.dst%:%TCP.dport% (%TCP.flags%)") + if r[2] is not None: + res += " (distance " + str(r[2]) + ")" + print(res) + + +@conf.commands.register +def pkt2uptime(pkt, HZ=100): + """Calculate the date the machine which emitted the packet booted using TCP timestamp # noqa: E501 +pkt2uptime(pkt, [HZ=100])""" + if not isinstance(pkt, Packet): + raise TypeError("Not a TCP packet") + if isinstance(pkt, NoPayload): + raise TypeError("Not a TCP packet") + if not isinstance(pkt, TCP): + return pkt2uptime(pkt.payload) + for opt in pkt.options: + if opt[0] == "Timestamp": + # t = pkt.time - opt[1][0] * 1.0/HZ + # return time.ctime(t) + t = opt[1][0] / HZ + return t + raise TypeError("No timestamp option") + + +def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, + extrahops=0, mtu=1500, uptime=None): + """Modifies pkt so that p0f will think it has been sent by a +specific OS. If osdetails is None, then we randomly pick up a +personality matching osgenre. If osgenre and signature are also None, +we use a local signature (using p0f_getlocalsigs). If signature is +specified (as a tuple), we use the signature. + +For now, only TCP Syn packets are supported. +Some specifications of the p0f.fp file are not (yet) implemented.""" + pkt = pkt.copy() + # pkt = pkt.__class__(raw(pkt)) + while pkt.haslayer(IP) and pkt.haslayer(TCP): + pkt = pkt.getlayer(IP) + if isinstance(pkt.payload, TCP): + break + pkt = pkt.payload + + if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP): + raise TypeError("Not a TCP/IP packet") + + db = p0f_selectdb(pkt.payload.flags) + if osgenre: + pb = db.get_base() + if pb is None: + pb = [] + pb = [x for x in pb if x[6] == osgenre] + if osdetails: + pb = [x for x in pb if x[7] == osdetails] + elif signature: + pb = [signature] + else: + pb = p0f_getlocalsigs()[db] + if db == p0fr_kdb: + # 'K' quirk <=> RST+ACK + if pkt.payload.flags & 0x4 == 0x4: + pb = [x for x in pb if 'K' in x[5]] + else: + pb = [x for x in pb if 'K' not in x[5]] + if not pb: + raise Scapy_Exception("No match in the p0f database") + pers = pb[random.randint(0, len(pb) - 1)] + + # options (we start with options because of MSS) + # Take the options already set as "hints" to use in the new packet if we + # can. MSS, WScale and Timestamp can all be wildcarded in a signature, so + # we'll use the already-set values if they're valid integers. + orig_opts = dict(pkt.payload.options) + int_only = lambda val: val if isinstance(val, int) else None + mss_hint = int_only(orig_opts.get('MSS')) + wscale_hint = int_only(orig_opts.get('WScale')) + ts_hint = [int_only(o) for o in orig_opts.get('Timestamp', (None, None))] + + options = [] + if pers[4] != '.': + for opt in pers[4].split(','): + if opt[0] == 'M': + # MSS might have a maximum size because of window size + # specification + if pers[0][0] == 'S': + maxmss = (2**16 - 1) // int(pers[0][1:]) + else: + maxmss = (2**16 - 1) + # disregard hint if out of range + if mss_hint and not 0 <= mss_hint <= maxmss: + mss_hint = None + # If we have to randomly pick up a value, we cannot use + # scapy RandXXX() functions, because the value has to be + # set in case we need it for the window size value. That's + # why we use random.randint() + if opt[1:] == '*': + if mss_hint is not None: + options.append(('MSS', mss_hint)) + else: + options.append(('MSS', random.randint(1, maxmss))) + elif opt[1] == '%': + coef = int(opt[2:]) + if mss_hint is not None and mss_hint % coef == 0: + options.append(('MSS', mss_hint)) + else: + options.append(( + 'MSS', coef * random.randint(1, maxmss // coef))) + else: + options.append(('MSS', int(opt[1:]))) + elif opt[0] == 'W': + if wscale_hint and not 0 <= wscale_hint < 2**8: + wscale_hint = None + if opt[1:] == '*': + if wscale_hint is not None: + options.append(('WScale', wscale_hint)) + else: + options.append(('WScale', RandByte())) + elif opt[1] == '%': + coef = int(opt[2:]) + if wscale_hint is not None and wscale_hint % coef == 0: + options.append(('WScale', wscale_hint)) + else: + options.append(( + 'WScale', coef * RandNum(min=1, max=(2**8 - 1) // coef))) # noqa: E501 + else: + options.append(('WScale', int(opt[1:]))) + elif opt == 'T0': + options.append(('Timestamp', (0, 0))) + elif opt == 'T': + # Determine first timestamp. + if uptime is not None: + ts_a = uptime + elif ts_hint[0] and 0 < ts_hint[0] < 2**32: + # Note: if first ts is 0, p0f registers it as "T0" not "T", + # hence we don't want to use the hint if it was 0. + ts_a = ts_hint[0] + else: + ts_a = random.randint(120, 100 * 60 * 60 * 24 * 365) + # Determine second timestamp. + if 'T' not in pers[5]: + ts_b = 0 + elif ts_hint[1] and 0 < ts_hint[1] < 2**32: + ts_b = ts_hint[1] + else: + # FIXME: RandInt() here does not work (bug (?) in + # TCPOptionsField.m2i often raises "OverflowError: + # long int too large to convert to int" in: + # oval = struct.pack(ofmt, *oval)" + # Actually, this is enough to often raise the error: + # struct.pack('I', RandInt()) + ts_b = random.randint(1, 2**32 - 1) + options.append(('Timestamp', (ts_a, ts_b))) + elif opt == 'S': + options.append(('SAckOK', '')) + elif opt == 'N': + options.append(('NOP', None)) + elif opt == 'E': + options.append(('EOL', None)) + elif opt[0] == '?': + if int(opt[1:]) in TCPOptions[0]: + optname = TCPOptions[0][int(opt[1:])][0] + optstruct = TCPOptions[0][int(opt[1:])][1] + options.append((optname, + struct.unpack(optstruct, + RandString(struct.calcsize(optstruct))._fix()))) # noqa: E501 + else: + options.append((int(opt[1:]), '')) + # FIXME: qqP not handled + else: + warning("unhandled TCP option %s", opt) + pkt.payload.options = options + + # window size + if pers[0] == '*': + pkt.payload.window = RandShort() + elif pers[0].isdigit(): + pkt.payload.window = int(pers[0]) + elif pers[0][0] == '%': + coef = int(pers[0][1:]) + pkt.payload.window = coef * RandNum(min=1, max=(2**16 - 1) // coef) + elif pers[0][0] == 'T': + pkt.payload.window = mtu * int(pers[0][1:]) + elif pers[0][0] == 'S': + # needs MSS set + mss = [x for x in options if x[0] == 'MSS'] + if not mss: + raise Scapy_Exception("TCP window value requires MSS, and MSS option not set") # noqa: E501 + pkt.payload.window = mss[0][1] * int(pers[0][1:]) + else: + raise Scapy_Exception('Unhandled window size specification') + + # ttl + pkt.ttl = pers[1] - extrahops + # DF flag + pkt.flags |= (2 * pers[2]) + # FIXME: ss (packet size) not handled (how ? may be with D quirk + # if present) + # Quirks + if pers[5] != '.': + for qq in pers[5]: + # FIXME: not handled: P, I, X, ! + # T handled with the Timestamp option + if qq == 'Z': + pkt.id = 0 + elif qq == 'U': + pkt.payload.urgptr = RandShort() + elif qq == 'A': + pkt.payload.ack = RandInt() + elif qq == 'F': + if db == p0fo_kdb: + pkt.payload.flags |= 0x20 # U + else: + pkt.payload.flags |= random.choice([8, 32, 40]) # P/U/PU + elif qq == 'D' and db != p0fo_kdb: + pkt /= conf.raw_layer(load=RandString(random.randint(1, 10))) # XXX p0fo.fp # noqa: E501 + elif qq == 'Q': + pkt.payload.seq = pkt.payload.ack + # elif qq == '0': pkt.payload.seq = 0 + # if db == p0fr_kdb: + # '0' quirk is actually not only for p0fr.fp (see + # packet2p0f()) + if '0' in pers[5]: + pkt.payload.seq = 0 + elif pkt.payload.seq == 0: + pkt.payload.seq = RandInt() + + while pkt.underlayer: + pkt = pkt.underlayer + return pkt + + +def p0f_getlocalsigs(): + """This function returns a dictionary of signatures indexed by p0f +db (e.g., p0f_kdb, p0fa_kdb, ...) for the local TCP/IP stack. + +You need to have your firewall at least accepting the TCP packets +from/to a high port (30000 <= x <= 40000) on your loopback interface. + +Please note that the generated signatures come from the loopback +interface and may (are likely to) be different than those generated on +"normal" interfaces.""" + pid = os.fork() + port = random.randint(30000, 40000) + if pid > 0: + # parent: sniff + result = {} + + def addresult(res): + # TODO: wildcard window size in some cases? and maybe some + # other values? + if res[0] not in result: + result[res[0]] = [res[1]] + else: + if res[1] not in result[res[0]]: + result[res[0]].append(res[1]) + # XXX could we try with a "normal" interface using other hosts + iface = conf.route.route('127.0.0.1')[0] + # each packet is seen twice: S + RA, S + SA + A + FA + A + # XXX are the packets also seen twice on non Linux systems ? + count = 14 + pl = sniff(iface=iface, filter='tcp and port ' + str(port), count=count, timeout=3) # noqa: E501 + for pkt in pl: + for elt in packet2p0f(pkt): + addresult(elt) + os.waitpid(pid, 0) + elif pid < 0: + log_runtime.error("fork error") + else: + # child: send + # XXX erk + time.sleep(1) + s1 = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) + # S & RA + try: + s1.connect(('127.0.0.1', port)) + except socket.error: + pass + # S, SA, A, FA, A + s1.bind(('127.0.0.1', port)) + s1.connect(('127.0.0.1', port)) + # howto: get an RST w/o ACK packet + s1.close() + os._exit(0) + return result diff --git a/scapy/modules/six.py b/scapy/modules/six.py deleted file mode 100644 index bae38cde0e5..00000000000 --- a/scapy/modules/six.py +++ /dev/null @@ -1,891 +0,0 @@ -# Copyright (c) 2010-2017 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all # noqa: E501 -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -## This file is part of Scapy -## See http://www.secdev.org/projects/scapy for more information -## Copyright (C) Philippe Biondi -## This program is published under a GPLv2 license - -"""Utilities for writing code that runs on Python 2 and 3""" - -from __future__ import absolute_import - -import functools -import itertools -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.10.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) # Invokes __set__. - try: - # This is a bit ugly, but it avoids running this again by - # removing this descriptor. - delattr(obj.__class__, self.name) - except AttributeError: - pass - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - def __getattr__(self, attr): - _module = self._resolve() - value = getattr(_module, attr) - setattr(self, attr, value) - return value - - -class _LazyModule(types.ModuleType): - - def __init__(self, name): - super(_LazyModule, self).__init__(name) - self.__doc__ = self.__class__.__doc__ - - def __dir__(self): - attrs = ["__doc__", "__name__"] - attrs += [attr.name for attr in self._moved_attributes] - return attrs - - # Subclasses should override this - _moved_attributes = [] - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - -class _SixMetaPathImporter(object): - - """ - A meta path importer to import scapy.modules.six.moves and its submodules. - - This class implements a PEP302 finder and loader. It should be compatible - with Python 2.5 and all existing versions of Python3 - """ - - def __init__(self, six_module_name): - self.name = six_module_name - self.known_modules = {} - - def _add_module(self, mod, *fullnames): - for fullname in fullnames: - self.known_modules[self.name + "." + fullname] = mod - - def _get_module(self, fullname): - return self.known_modules[self.name + "." + fullname] - - def find_module(self, fullname, path=None): - if fullname in self.known_modules: - return self - return None - - def __get_module(self, fullname): - try: - return self.known_modules[fullname] - except KeyError: - raise ImportError("This loader does not know module " + fullname) - - def load_module(self, fullname): - try: - # in case of a reload - return sys.modules[fullname] - except KeyError: - pass - mod = self.__get_module(fullname) - if isinstance(mod, MovedModule): - mod = mod._resolve() - else: - mod.__loader__ = self - sys.modules[fullname] = mod - return mod - - def is_package(self, fullname): - """ - Return true, if the named module is a package. - - We need this method to get correct spec objects with - Python 3.4 (see PEP451) - """ - return hasattr(self.__get_module(fullname), "__path__") - - def get_code(self, fullname): - """Return None - - Required, if is_package is implemented""" - self.__get_module(fullname) # eventually raises ImportError - return None - get_source = get_code # same as get_code - -_importer = _SixMetaPathImporter(__name__) - - -class _MovedItems(_LazyModule): - - """Lazy loading of moved objects""" - __path__ = [] # mark as package - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), # noqa: E501 - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("intern", "__builtin__", "sys"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), - MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), - MovedAttribute("getstatusoutput", "commands", "subprocess"), - MovedAttribute("getoutput", "commands", "subprocess"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), # noqa: E501 - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), - MovedAttribute("UserList", "UserList", "collections"), - MovedAttribute("UserString", "UserString", "collections"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), # noqa: E501 - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), # noqa: E501 - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), # noqa: E501 - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("_thread", "thread", "_thread"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), # noqa: E501 - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), # noqa: E501 - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), # noqa: E501 - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), # noqa: E501 - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), # noqa: E501 - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), -] -# Add windows specific modules. -if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] - -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) - if isinstance(attr, MovedModule): - _importer._add_module(attr, "moves." + attr.name) -del attr - -_MovedItems._moved_attributes = _moved_attributes - -moves = _MovedItems(__name__ + ".moves") -_importer._add_module(moves, "moves") - - -class Module_six_moves_urllib_parse(_LazyModule): - - """Lazy loading of moved objects in scapy.modules.six.urllib_parse""" - - -_urllib_parse_moved_attributes = [ - MovedAttribute("ParseResult", "urlparse", "urllib.parse"), - MovedAttribute("SplitResult", "urlparse", "urllib.parse"), - MovedAttribute("parse_qs", "urlparse", "urllib.parse"), - MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), - MovedAttribute("urldefrag", "urlparse", "urllib.parse"), - MovedAttribute("urljoin", "urlparse", "urllib.parse"), - MovedAttribute("urlparse", "urlparse", "urllib.parse"), - MovedAttribute("urlsplit", "urlparse", "urllib.parse"), - MovedAttribute("urlunparse", "urlparse", "urllib.parse"), - MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), - MovedAttribute("quote", "urllib", "urllib.parse"), - MovedAttribute("quote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote", "urllib", "urllib.parse"), - MovedAttribute("unquote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), # noqa: E501 - MovedAttribute("urlencode", "urllib", "urllib.parse"), - MovedAttribute("splitquery", "urllib", "urllib.parse"), - MovedAttribute("splittag", "urllib", "urllib.parse"), - MovedAttribute("splituser", "urllib", "urllib.parse"), - MovedAttribute("splitvalue", "urllib", "urllib.parse"), - MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), - MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), - MovedAttribute("uses_params", "urlparse", "urllib.parse"), - MovedAttribute("uses_query", "urlparse", "urllib.parse"), - MovedAttribute("uses_relative", "urlparse", "urllib.parse"), -] -for attr in _urllib_parse_moved_attributes: - setattr(Module_six_moves_urllib_parse, attr.name, attr) -del attr - -Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes # noqa: E501 - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), # noqa: E501 - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in scapy.modules.six.urllib_error""" - - -_urllib_error_moved_attributes = [ - MovedAttribute("URLError", "urllib2", "urllib.error"), - MovedAttribute("HTTPError", "urllib2", "urllib.error"), - MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), -] -for attr in _urllib_error_moved_attributes: - setattr(Module_six_moves_urllib_error, attr.name, attr) -del attr - -Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes # noqa: E501 - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), # noqa: E501 - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in scapy.modules.six.urllib_request""" - - -_urllib_request_moved_attributes = [ - MovedAttribute("urlopen", "urllib2", "urllib.request"), - MovedAttribute("install_opener", "urllib2", "urllib.request"), - MovedAttribute("build_opener", "urllib2", "urllib.request"), - MovedAttribute("pathname2url", "urllib", "urllib.request"), - MovedAttribute("url2pathname", "urllib", "urllib.request"), - MovedAttribute("getproxies", "urllib", "urllib.request"), - MovedAttribute("Request", "urllib2", "urllib.request"), - MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), - MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), - MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), - MovedAttribute("BaseHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), # noqa: E501 - MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), - MovedAttribute("FileHandler", "urllib2", "urllib.request"), - MovedAttribute("FTPHandler", "urllib2", "urllib.request"), - MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), - MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), - MovedAttribute("urlretrieve", "urllib", "urllib.request"), - MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), - MovedAttribute("proxy_bypass", "urllib", "urllib.request"), -] -for attr in _urllib_request_moved_attributes: - setattr(Module_six_moves_urllib_request, attr.name, attr) -del attr - -Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes # noqa: E501 - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), # noqa: E501 - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in scapy.modules.six.urllib_response""" - - -_urllib_response_moved_attributes = [ - MovedAttribute("addbase", "urllib", "urllib.response"), - MovedAttribute("addclosehook", "urllib", "urllib.response"), - MovedAttribute("addinfo", "urllib", "urllib.response"), - MovedAttribute("addinfourl", "urllib", "urllib.response"), -] -for attr in _urllib_response_moved_attributes: - setattr(Module_six_moves_urllib_response, attr.name, attr) -del attr - -Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes # noqa: E501 - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), # noqa: E501 - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in scapy.modules.six.urllib_robotparser""" - - -_urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), -] -for attr in _urllib_robotparser_moved_attributes: - setattr(Module_six_moves_urllib_robotparser, attr.name, attr) -del attr - -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes # noqa: E501 - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), # noqa: E501 - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a scapy.modules.six.urllib namespace that resembles the Python 3 namespace""" # noqa: E501 - __path__ = [] # mark as package - parse = _importer._get_module("moves.urllib_parse") - error = _importer._get_module("moves.urllib_error") - request = _importer._get_module("moves.urllib_request") - response = _importer._get_module("moves.urllib_response") - robotparser = _importer._get_module("moves.urllib_robotparser") - - def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] - -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") - - -def add_move(move): - """Add an item to scapy.modules.six.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from scapy.modules.six.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - def create_unbound_method(func, cls): - return func - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - def create_unbound_method(func, cls): - return types.MethodType(func, None, cls) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") -_add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") - - -if PY3: - def b(s): - return s.encode("latin-1") - - def u(s): - return s - unichr = chr - import struct - int2byte = struct.Struct(">B").pack - del struct - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" -else: - def b(s): - return s - # Workaround for standalone backslash - - def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - unichr = unichr - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) - - def indexbytes(buf, i): - return ord(buf[i]) - iterbytes = functools.partial(itertools.imap, ord) - import StringIO - StringIO = BytesIO = StringIO.StringIO - _assertCountEqual = "assertItemsEqual" - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -def assertCountEqual(self, *args, **kwargs): - return getattr(self, _assertCountEqual)(*args, **kwargs) - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -def assertRegex(self, *args, **kwargs): - return getattr(self, _assertRegex)(*args, **kwargs) - - -if PY3: - exec_ = getattr(moves.builtins, "exec") - - def reraise(tp, value, tb=None): - try: - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - finally: - value = None - tb = None - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - exec_("""def reraise(tp, value, tb=None): - try: - raise tp, value, tb - finally: - tb = None -""") - - -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - try: - if from_value is None: - raise value - raise value from from_value - finally: - value = None -""") -elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - try: - raise value from from_value - finally: - value = None -""") -else: - def raise_from(value, from_value): - raise value - - -print_ = getattr(moves.builtins, "print", None) -if print_ is None: - def print_(*args, **kwargs): - """The new-style print function for Python 2.4 and 2.5.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - - def write(data): - if not isinstance(data, basestring): - data = str(data) - # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): - errors = getattr(fp, "errors", None) - if errors is None: - errors = "strict" - data = data.encode(fp.encoding, errors) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) -if sys.version_info[:2] < (3, 3): - _print = print_ - - def print_(*args, **kwargs): - fp = kwargs.get("file", sys.stdout) - flush = kwargs.pop("flush", False) - _print(*args, **kwargs) - if flush and fp is not None: - fp.flush() - -_add_doc(reraise, """Reraise an exception.""") - -if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper -else: - wraps = functools.wraps - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -# Complete the moves implementation. -# This code is at the end of this module to speed up module loading. -# Turn this module into a package. -__path__ = [] # required for PEP 302 and PEP 451 -__package__ = __name__ # see PEP 366 @ReservedAssignment -if globals().get("__spec__") is not None: - __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable -# Remove other six meta path importers, since they cause problems. This can -# happen if six is removed from sys.modules and then reloaded. (Setuptools does -# this for some reason.) -if sys.meta_path: - for i, importer in enumerate(sys.meta_path): - # Here's some real nastiness: Another "instance" of the six module might # noqa: E501 - # be floating around. Therefore, we can't use isinstance() to check for - # the six meta path importer, since the other six instance will have - # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): - del sys.meta_path[i] - break - del i, importer -# Finally, add the importer to the meta path import hook. -sys.meta_path.append(_importer) diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py new file mode 100644 index 00000000000..4bc469a78f9 --- /dev/null +++ b/scapy/modules/ticketer.py @@ -0,0 +1,2772 @@ +# SPDX-License-Identifier: GPL-2.0-or-later OR MPL-2.0 +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +# flake8: noqa + +""" +Create/Edit Kerberos ticket using Scapy + +See https://scapy.readthedocs.io/en/latest/layers/kerberos.html +""" + +from datetime import datetime, timedelta, timezone + +import collections +import enum +import platform +import random +import re +import struct + +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_GENERAL_STRING, + ASN1_GENERALIZED_TIME, + ASN1_INTEGER, + ASN1_STRING, +) +from scapy.compat import bytes_hex, hex_bytes +from scapy.config import conf +from scapy.error import log_interactive +from scapy.fields import ( + ByteField, + ConditionalField, + FieldLenField, + FlagsField, + IntEnumField, + IntField, + MayEnd, + PacketField, + PacketListField, + ShortEnumField, + ShortField, + StrLenField, + UTCTimeField, +) +from scapy.packet import Packet +from scapy.utils import pretty_list + +from scapy.layers.dcerpc import NDRUnion +from scapy.layers.kerberos import ( + AuthorizationData, + AuthorizationDataItem, + EncTicketPart, + EncryptedData, + EncryptionKey, + KRB_Ticket, + KerberosClient, + KerberosSSP, + PrincipalName, + TransitedEncoding, + _ADDR_TYPES, + _AD_TYPES, + _KRB_E_TYPES, + _KRB_S_TYPES, + _PRINCIPAL_NAME_TYPES, + _TICKET_FLAGS, + _parse_spn, + _parse_upn, + kpasswd, + krb_as_req, + krb_get_salt, + krb_tgs_req, +) +from scapy.layers.msrpce.mspac import ( + CLAIM_ENTRY, + CLAIMS_ARRAY, + CLAIMS_SET, + CLAIMS_SET_METADATA, + CYPHER_BLOCK, + FILETIME, + GROUP_MEMBERSHIP, + KERB_SID_AND_ATTRIBUTES, + KERB_VALIDATION_INFO, + PAC_ATTRIBUTES_INFO, + PAC_CLIENT_CLAIMS_INFO, + PAC_CLIENT_INFO, + PAC_INFO_BUFFER, + PAC_INFO_BUFFER, + PAC_REQUESTOR_SID, + PAC_SIGNATURE_DATA, + PACTYPE, + RPC_SID_IDENTIFIER_AUTHORITY, + RPC_UNICODE_STRING, + SID, + UPN_DNS_INFO, + USER_SESSION_KEY, + CLAIM_ENTRY_sub2, +) +from scapy.layers.windows.security import ( + WINNT_SID, + WINNT_SID_IDENTIFIER_AUTHORITY, +) + +from scapy.libs.rfc3961 import EncryptionType, Key, _checksums + +try: + import tkinter as tk + import tkinter.simpledialog as tksd + from tkinter import ttk +except ImportError: + tk = None + +# CCache +# https://web.mit.edu/kerberos/krb5-latest/doc/formats/ccache_file_format.html (official doc but garbage) +# https://josefsson.org/shishi/ccache.txt (much better) + + +class CCCountedOctetString(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="data", fmt="I"), + StrLenField("data", b"", length_from=lambda pkt: pkt.length), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class CCPrincipal(Packet): + fields_desc = [ + IntEnumField("name_type", 0, _PRINCIPAL_NAME_TYPES), + FieldLenField("num_components", None, count_of="components", fmt="I"), + PacketField("realm", CCCountedOctetString(), CCCountedOctetString), + PacketListField( + "components", + [], + CCCountedOctetString, + count_from=lambda pkt: pkt.num_components, + ), + ] + + def toPN(self): + return "%s@%s" % ( + "/".join(x.data.decode() for x in self.components), + self.realm.data.decode(), + ) + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class CCDeltaTime(Packet): + fields_desc = [ + IntField("time_offset", 0), + IntField("usec_offset", 0), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class CCHeader(Packet): + fields_desc = [ + ShortEnumField("tag", 1, {1: "DeltaTime"}), + ShortField("taglen", 8), + PacketField("tagdata", CCDeltaTime(), CCDeltaTime), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class CCKeyBlock(Packet): + fields_desc = [ + ShortEnumField("keytype", 0, _KRB_E_TYPES), + ShortField("etype", 0), + FieldLenField("keylen", None, length_of="keyvalue"), + StrLenField("keyvalue", b"", length_from=lambda pkt: pkt.keylen), + ] + + def toKey(self): + return Key(self.keytype, key=self.keyvalue) + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class CCAddress(Packet): + fields_desc = [ + ShortEnumField("addrtype", 0, _ADDR_TYPES), + PacketField("address", CCCountedOctetString(), CCCountedOctetString), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class CCAuthData(Packet): + fields_desc = [ + ShortEnumField("authtype", 0, _AD_TYPES), + PacketField("authdata", CCCountedOctetString(), CCCountedOctetString), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class CCCredential(Packet): + fields_desc = [ + PacketField("client", CCPrincipal(), CCPrincipal), + PacketField("server", CCPrincipal(), CCPrincipal), + PacketField("keyblock", CCKeyBlock(), CCKeyBlock), + UTCTimeField("authtime", None), + UTCTimeField("starttime", None), + UTCTimeField("endtime", None), + UTCTimeField("renew_till", None), + ByteField("is_skey", 0), + FlagsField( + "ticket_flags", + 0, + 32, + # stored in reversed byte order (wtf) + (_TICKET_FLAGS + [""] * (32 - len(_TICKET_FLAGS)))[::-1], + ), + FieldLenField("num_address", None, count_of="addrs", fmt="I"), + PacketListField("addrs", [], CCAddress, count_from=lambda pkt: pkt.num_address), + FieldLenField("num_authdata", None, count_of="authdata", fmt="I"), + PacketListField( + "authdata", [], CCAuthData, count_from=lambda pkt: pkt.num_authdata + ), + PacketField("ticket", CCCountedOctetString(), CCCountedOctetString), + PacketField("second_ticket", CCCountedOctetString(), CCCountedOctetString), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + def set_from_krb(self, tkt, clientpart, sessionkey, kdcrep): + self.ticket.data = bytes(tkt) + + # Set sname + self.server.name_type = tkt.sname.nameType.val + self.server.realm = CCCountedOctetString(data=tkt.realm.val) + self.server.components = [ + CCCountedOctetString(data=x.val) for x in tkt.sname.nameString + ] + + # Set cname + self.client.name_type = clientpart.cname.nameType.val + self.client.realm = CCCountedOctetString(data=clientpart.crealm.val) + self.client.components = [ + CCCountedOctetString(data=x.val) for x in clientpart.cname.nameString + ] + + # Set the sessionkey + self.keyblock = CCKeyBlock( + keytype=sessionkey.etype, + keyvalue=sessionkey.key, + ) + + # Set timestamps + self.authtime = kdcrep.authtime.datetime.timestamp() + if kdcrep.starttime is not None: + self.starttime = kdcrep.starttime.datetime.timestamp() + self.endtime = kdcrep.endtime.datetime.timestamp() + if kdcrep.flags.val[8] == "1": # renewable + self.renew_till = kdcrep.renewTill.datetime.timestamp() + + # Set flags + self.ticket_flags = int(kdcrep.flags.val, 2) + + def is_xcacheconf(self): + return self.server.realm.data == b"X-CACHECONF:" + + +class CCache(Packet): + fields_desc = [ + ShortField("file_format_version", 0x0504), + ShortField("headerlen", 0), + PacketListField("headers", [], CCHeader, length_from=lambda pkt: pkt.headerlen), + PacketField("primary_principal", CCPrincipal(), CCPrincipal), + PacketListField("credentials", [], CCCredential), + ] + + +# Keytab +# https://web.mit.edu/kerberos/krb5-devel/doc/formats/keytab_file_format.html (official but garbage) +# https://www.gnu.org/software/shishi/manual/html_node/The-Keytab-Binary-File-Format.html (great) + + +class KTCountedOctetString(Packet): + fields_desc = [ + FieldLenField("length", None, length_of="data", fmt="H"), + StrLenField("data", b"", length_from=lambda pkt: pkt.length), + ] + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class KTKeyBlock(Packet): + fields_desc = [ + ShortEnumField("keytype", 0, _KRB_E_TYPES), + FieldLenField("keylen", None, length_of="keyvalue"), + StrLenField("keyvalue", b"", length_from=lambda pkt: pkt.keylen), + ] + + def toKey(self): + return Key(self.keytype, key=self.keyvalue) + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class KeytabEntry(Packet): + fields_desc = [ + IntField("size", None), + FieldLenField("num_components", None, count_of="components"), + PacketField("realm", KTCountedOctetString(), KTCountedOctetString), + PacketListField( + "components", + [], + KTCountedOctetString, + count_from=lambda pkt: pkt.num_components, + ), + ConditionalField( + IntField("name_type", 0), + lambda pkt: pkt.parent.file_format_version != 0x501, + ), + UTCTimeField("timestamp", None), + ByteField("vno8", 0), + MayEnd(PacketField("key", KTKeyBlock(), KTKeyBlock)), + ConditionalField( + IntField("vno", None), + lambda pkt: pkt.fields.get("vno", None) or pkt.original, + ), + ] + + def getPrincipal(self): + comp = "/".join(x.data.decode() for x in self.components) + if self.realm.data: + return "%s@%s" % ( + comp, + self.realm.data.decode(), + ) + else: + return comp + + @property + def versionNumber(self): + if self.vno is not None: + return self.vno + return self.vno8 + + def post_build(self, p, pay): + # type: (bytes, bytes) -> bytes + if self.size is None: + p = struct.pack("!I", len(p)) + p[4:] + return p + pay + + def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, bytes] + rem = self.size - len(self.original) + return s[:rem], s[rem:] + + +class Keytab(Packet): + fields_desc = [ + ShortField("file_format_version", 0x502), + PacketListField("entries", [], KeytabEntry), + ] + + +# TK scrollFrame (MPL-2.0) +# Credits to @mp035 +# https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01 + +if tk is not None: + + class ScrollFrame(tk.Frame): + def __init__(self, parent): + super().__init__(parent) + + self.canvas = tk.Canvas(self, borderwidth=0) + self.viewPort = ttk.Frame(self.canvas) + self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview) + self.canvas.configure(yscrollcommand=self.vsb.set) + + self.vsb.pack(side="right", fill="y") + self.canvas.pack(side="left", fill="both", expand=True) + self.canvas_window = self.canvas.create_window( + (4, 4), window=self.viewPort, anchor="nw", tags="self.viewPort" + ) + + self.viewPort.bind("", self.onFrameConfigure) + self.canvas.bind("", self.onCanvasConfigure) + + self.viewPort.bind("", self.onEnter) + self.viewPort.bind("", self.onLeave) + + self.onFrameConfigure(None) + + def onFrameConfigure(self, event): + """Reset the scroll region to encompass the inner frame""" + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def onCanvasConfigure(self, event): + """Reset the canvas window to encompass inner frame when required""" + canvas_width = event.width + self.canvas.itemconfig(self.canvas_window, width=canvas_width) + + def onMouseWheel(self, event): + if platform.system() == "Windows": + self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + elif platform.system() == "Darwin": + self.canvas.yview_scroll(int(-1 * event.delta), "units") + else: + if event.num == 4: + self.canvas.yview_scroll(-1, "units") + elif event.num == 5: + self.canvas.yview_scroll(1, "units") + + def onEnter(self, event): + if platform.system() == "Linux": + self.canvas.bind_all("", self.onMouseWheel) + self.canvas.bind_all("", self.onMouseWheel) + else: + self.canvas.bind_all("", self.onMouseWheel) + + def onLeave(self, event): + if platform.system() == "Linux": + self.canvas.unbind_all("") + self.canvas.unbind_all("") + else: + self.canvas.unbind_all("") + + +# Build ticketer + + +class Ticketer: + def __init__(self): + self._data = collections.defaultdict(dict) + self.ccache_fname = None + self.ccache = CCache() + self.keytab_fname = None + self.keytab = Keytab() + self.hashes_cache = collections.defaultdict(dict) + + def open_ccache(self, fname): + """ + Load from CCache file + """ + self.ccache_fname = fname + self.hashes_cache = collections.defaultdict(dict) + with open(self.ccache_fname, "rb") as fd: + self.ccache = CCache(fd.read()) + + def open_keytab(self, fname): + """ + Load from Keytab file + """ + self.keytab_fname = fname + with open(self.keytab_fname, "rb") as fd: + self.keytab = Keytab(fd.read()) + + def save_ccache(self, fname=None, i=None): + """ + Save ccache into file + + :param fname: if provided, save to a specific file. + :param i: if provided, only save the ticket n°i. + """ + if fname: + self.ccache_fname = fname + if not self.ccache_fname: + raise ValueError("No file opened. Specify the 'fname' argument !") + + # If i is specified, extract single ticket. + if i is not None: + ccache = self.ccache.copy() + ccache.credentials = [ccache.credentials[i]] + else: + ccache = self.ccache + + # Write + with open(self.ccache_fname, "wb") as fd: + return fd.write(bytes(ccache)) + + def save_keytab(self, fname=None): + """ + Save keytab into file + + :param fname: if provided, save to a specific file. + """ + if fname: + self.keytab_fname = fname + if not self.keytab_fname: + raise ValueError("No file opened. Specify the 'fname' argument !") + + # Write + with open(self.keytab_fname, "wb") as fd: + return fd.write(bytes(self.keytab)) + + def show(self, utc=False): + """ + Show the content of a CCache + """ + + def _to_str(x): + if x is None: + return "None" + else: + x = datetime.fromtimestamp(x, tz=timezone.utc if utc else None) + return x.strftime("%d/%m/%y %H:%M:%S") + + # Show Keytab + if self.keytab.entries: + print("Keytab name: %s" % (self.keytab_fname or "UNSAVED")) + print( + pretty_list( + [ + ( + entry.getPrincipal(), + _to_str(entry.timestamp), + str(entry.versionNumber), + entry.key.sprintf("%keytype%"), + ) + for entry in self.keytab.entries + ], + [("Principal", "Timestamp", "KVNO", "Keytype")], + ) + ) + print() + + # Show CCache + if not self.ccache.credentials: + print("No tickets in CCache.") + return + else: + if self.ccache.primary_principal.components: + print("Default principal: %s\n" % self.ccache.primary_principal.toPN()) + + print("CCache tickets:") + + # 1. Read configuration entries + configuration = collections.defaultdict(dict) + for cred in self.ccache.credentials: + if not cred.is_xcacheconf(): + # Skip non-configuration entries + continue + + if ( + len(cred.server.components) not in [2, 3] + or cred.server.components[0].data != b"krb5_ccache_conf_data" + ): + print("Skipping invalid X-CACHECONF !") + continue + + # Get all the values from this weird format + cname = cred.client.toPN() + key = cred.server.components[1].data.decode() + if len(cred.server.components) == 3: + sname = cred.server.components[2].data.decode() + else: + sname = None + value = cred.ticket.data.decode() + + # Store for this cname -> sname, the following 'key' setting. + configuration[(cname, sname)][key] = value + + # 2. Read credentials + for i, cred in enumerate(self.ccache.credentials): + if cred.is_xcacheconf(): + # Skip configuration entries + continue + + # Get client and server principals + cname = cred.client.toPN() + sname = cred.server.toPN() + + print( + "%s. %s -> %s" + % ( + i, + cname, + sname, + ) + ) + print(cred.sprintf(" %ticket_flags%")) + # If configuration entries match, show the settings here + print( + " " + + " ".join( + "%s=%s" % (key, value) + for _sname in [sname, None] + for key, value in configuration[(cname, _sname)].items() + if (cname, _sname) in configuration + ) + ) + print( + pretty_list( + [ + ( + _to_str(cred.starttime), + _to_str(cred.endtime), + _to_str(cred.renew_till), + _to_str(cred.authtime), + ) + ], + [("Start time", "End time", "Renew until", "Auth time")], + ) + ) + print() + + def _prompt(self, msg): + try: + from prompt_toolkit import prompt + + return prompt(msg) + except ImportError: + return input(msg) + + def _prompt_hash(self, spn, etype=None, cksumtype=None, hash=None): + if etype: + hashtype = _KRB_E_TYPES[etype] + elif cksumtype: + hashtype = _KRB_S_TYPES[cksumtype] + else: + raise ValueError("No cksumtype nor etype specified") + if not hash: + if spn in self.hashes_cache and hashtype in self.hashes_cache[spn]: + hash = self.hashes_cache[spn][hashtype] + else: + msg = "Enter the %s hash for %s (as hex): " % (hashtype, spn) + hash = hex_bytes(self._prompt(msg)) + if ( + hash + == b"\xaa\xd3\xb45\xb5\x14\x04\xee\xaa\xd3\xb45\xb5\x14\x04\xee" + ): + log_interactive.warning( + "This hash is the LM 'no password' hash. Is that what you intended?" + ) + key = Key(etype=etype, cksumtype=cksumtype, key=hash) + self.hashes_cache[spn][hashtype] = hash + if key and etype and key.cksumtype: + self.hashes_cache[spn][_KRB_S_TYPES[key.cksumtype]] = hash + return key + + def dec_ticket(self, i, key=None, hash=None): + """ + Get the decrypted ticket by credentials ID + """ + cred = self.ccache.credentials[i] + tkt = KRB_Ticket(cred.ticket.data) + if key is None: + key = self._prompt_hash( + tkt.getSPN(), + etype=tkt.encPart.etype.val, + hash=hash, + ) + try: + return tkt.encPart.decrypt(key) + except Exception: + try: + del self.hashes_cache[tkt.getSPN()] + except IndexError: + pass + raise + + def update_ticket(self, i, decTkt, resign=False, hash=None, kdc_hash=None): + """ + Update a decrypted ticket by credentials ID + """ + # Get CCCredential + cred = self.ccache.credentials[i] + tkt = KRB_Ticket(cred.ticket.data) + + # Optional: resign the new ticket + if resign: + # resign the ticket + decTkt = self._resign_ticket( + decTkt, + tkt.getSPN(), + hash=hash, + kdc_hash=kdc_hash, + ) + + # Encrypt the new ticket + key = self._prompt_hash( + tkt.getSPN(), + etype=tkt.encPart.etype.val, + hash=hash, + ) + tkt.encPart.encrypt(key, bytes(decTkt)) + + # Update the CCCredential with the new ticket + cred.set_from_krb( + tkt, + decTkt, + decTkt.key.toKey(), + decTkt, + ) + + def remove_krb(self, i): + """ + Remove a ticket from the store. + + :param i: the ticket to remove. + """ + cred = self.ccache.credentials[i] + xcacheconfs = self.get_krb_xcacheopts(i) + + # Delete from the store + del self.ccache.credentials[i] + + # Among the remaining, do we have an option that's identical in name? + if any( + not xcred.is_xcacheconf() + and xcred.client.toPN() == cred.client.toPN() + and xcred.server.toPN() == cred.server.toPN() + for xcred in self.ccache.credentials + ): + # There is another ticket with the same client and server names. Stop here + return + + # There isno ticket exactly the same, remove all the xcacheconf that match + for xcred in xcacheconfs: + self.ccache.credentials.remove(xcred) + + # If this was the primary principal, remove from there + if cred.client.toPN() == self.ccache.primary_principal.toPN(): + self.ccache.primary_principal = CCPrincipal() + + def import_krb(self, res, key=None, hash=None, _inplace=None): + """ + Import the result of krb_[tgs/as]_req or a Ticket into the CCache. + + :param obj: a KRB_Ticket object or a AS_REP/TGS_REP object + :param sessionkey: the session key that comes along the ticket + """ + # Instantiate CCCredential + if _inplace is not None: + cred = self.ccache.credentials[_inplace] + else: + cred = CCCredential() + + # Update the cred + xcacheconfs = {} + if isinstance(res, KRB_Ticket): + if key is None: + key = self._prompt_hash( + res.getSPN(), + etype=res.encPart.etype.val, + hash=hash, + ) + decTkt = res.encPart.decrypt(key) + cred.set_from_krb( + res, + decTkt, + decTkt.key.toKey(), + decTkt, + ) + else: + if isinstance(res, KerberosClient.RES_AS_MODE): + rep = res.asrep + pa_type = res.pa_type + if pa_type is not None: + xcacheconfs["pa_type"] = str(pa_type) + if pa_type in [138]: + xcacheconfs["fast_avail"] = "yes" + elif isinstance(res, KerberosClient.RES_TGS_MODE): + rep = res.tgsrep + + # There could be 171 = KERB_DMSA_KEY_PACKAGE to import + for padata in res.kdcrep.encryptedPaData: + if padata.padataType == 171: + # We have keys to import. + key_package = padata.padataValue + for key in key_package.currentKeys: + self.add_cred( + principal=rep.getUPN(), + key=key.toKey(), + ) + log_interactive.info( + "%s DMSA keys found and imported !" + % len(key_package.currentKeys) + ) + else: + raise ValueError("Unknown type of obj !") + + cred.set_from_krb( + rep.ticket, + rep, + res.sessionkey, + res.kdcrep, + ) + + # Append to ccache + if _inplace is None: + _inplace = sum( + 1 for xcred in self.ccache.credentials if not xcred.is_xcacheconf() + ) + self.ccache.credentials.insert(_inplace, cred) + + # If this is the first credential, set it to primary + if len(self.ccache.credentials) == 1: + self.set_primary(_inplace) + + # For MIT kinit to be happy, we must provide extra options for the credential + for key, value in xcacheconfs.items(): + self.set_krb_xcacheconf(_inplace, key, value) + + def set_primary(self, i): + """ + Set the primary (=default) credential to the credential n°1 + """ + self.ccache.primary_principal = self.ccache.credentials[i].client + + def get_krb_xcacheopts(self, i: int): + """ + Get the X-CACHECONF config for a credential + """ + cred = self.ccache.credentials[i] + cname = cred.client.toPN() + sname = cred.server.toPN().encode() + return [ + xcred + for xcred in self.ccache.credentials + if ( + xcred.is_xcacheconf() + and xcred.client.toPN() == cname + and ( + len(xcred.server.components) == 2 + or xcred.server.components[2].data == sname + ) + ) + ] + + def set_krb_xcacheconf(self, i: int, key: str, value: str): + """ + Set a X-CACHECONF config for a credential + """ + key = key.encode() + value = value.encode() + cred = self.ccache.credentials[i] + sname = cred.server.toPN().encode() + + # First we look for a potential credential, if present + try: + conf_cred = next( + xcred + for xcred in self.get_krb_xcacheopts(i) + if xcred.server.components[1].data == key + ) + except StopIteration: + conf_cred = CCCredential( + client=cred.client, + server=CCPrincipal( + name_type=1, + realm=CCCountedOctetString(data=b"X-CACHECONF:"), + components=[ + CCCountedOctetString(data=b"krb5_ccache_conf_data"), + CCCountedOctetString(data=key), + CCCountedOctetString(data=sname), + ], + ), + ) + self.ccache.credentials.append(conf_cred) + + # Set value + conf_cred.ticket = CCCountedOctetString(data=value) + + def export_krb(self, i): + """ + Export a full ticket, session key, UPN and SPN. + """ + cred = self.ccache.credentials[i] + return ( + KRB_Ticket(cred.ticket.data), + cred.keyblock.toKey(), + cred.client.toPN(), + cred.server.toPN(), + ) + + def add_cred( + self, + principal, + mapupn=None, + password=None, + salt=None, + key=None, + etypes=None, + kvno=None, + ): + """ + Add a credential to the Keytab. + """ + if password and key: + raise ValueError("Please provide 'password' OR 'key'.") + elif not password and not key: + try: + from prompt_toolkit import prompt + + password = prompt("Enter password: ", is_password=True) + except ImportError: + password = input("Enter password: ") + + # If we have a mapupn, use it to retrieve the salt. + if salt is None and mapupn is not None: + salt = krb_get_salt(mapupn) + + # Detect if principal is a SPN or UPN and parse realm. + realm = None + princname = None + try: + _, realm = _parse_upn(principal) + if salt is None and key is None: + salt = krb_get_salt(principal) + princname = PrincipalName.fromSPN(principal) + except ValueError: + try: + _, realm = _parse_spn(principal) + princname = PrincipalName.fromSPN(principal) + except ValueError: + raise ValueError("Invalid principal ! (must be UPN or SPN)") + + if not realm: + raise ValueError("Must provide the realm in the principal ! (with @DOMAIN)") + + if salt is None and key is None: + raise ValueError( + "Salt could not be guessed. Please provide it, or provide 'mapupn' " + "pointing towards the UPN of the user." + ) + + # If password is provided, derive the keys. + if password: + from scapy.libs.rfc3961 import Key, EncryptionType + + if etypes is None: + etypes = [EncryptionType.AES256_CTS_HMAC_SHA1_96] + elif etypes == "all": + etypes = [ + EncryptionType.AES128_CTS_HMAC_SHA1_96, + EncryptionType.AES256_CTS_HMAC_SHA1_96, + EncryptionType.RC4_HMAC, + ] + + # For each etype, recurse. + for etype in etypes: + self.add_cred( + principal, + key=Key.string_to_key( + etype, + password.encode(), + salt=salt, + ), + ) + return + + # Get available kvno + if kvno is None: + try: + kvno = max(x.versionNumber for x in self.keytab.entries) + 1 + except ValueError: + kvno = 1 + + # Just add it. + self.keytab.entries.append( + KeytabEntry( + realm=KTCountedOctetString( + data=realm, + ), + components=[ + KTCountedOctetString( + data=x.val, + ) + for x in princname.nameString + ], + timestamp=int(datetime.now().timestamp()), + name_type=princname.nameType.val, + vno8=kvno, + key=KTKeyBlock( + keytype=key.etype, + keyvalue=key.key, + ), + vno=kvno, + _parent=self.keytab, + ) + ) + + def get_cred(self, principal, etype=None): + """ + Get credential from the Keytab by principal. + """ + for entry in self.keytab.entries: + if entry.getPrincipal() == principal: + if etype is not None and etype != entry.key.keytype: + continue + return entry.key.toKey() + raise ValueError( + "Principal not found in keytab ! " + "Note principals are case sensitive, as on ktpass.exe" + ) + + def remove_cred(self, principal, etype=None): + """ + Remove a credential from the Keytab by principal. + """ + for i, entry in enumerate(self.keytab.entries): + if entry.getPrincipal() == principal: + if etype is not None and etype != entry.key.keytype: + continue + del self.keytab.entries[i] + + def ssp(self, i, **kwargs): + """ + Create a KerberosSSP from a ticket or from the keystore. + + :param i: index of the ticket to use from ccache (client) + OR SPN of the key to use from the keystore (server) + """ + if isinstance(i, int): + ticket, sessionkey, upn, spn = self.export_krb(i) + if spn.startswith("krbtgt/"): + # It's a TGT + kwargs.setdefault("SPN", None) # Use target_name only + return KerberosSSP( + TGT=ticket, + KEY=sessionkey, + UPN=upn, + **kwargs, + ) + else: + # It's a ST + return KerberosSSP( + ST=ticket, + KEY=sessionkey, + UPN=upn, + SPN=spn, + **kwargs, + ) + elif isinstance(i, str): + spn = i + key = self.get_cred(spn) + return KerberosSSP( + SPN=spn, + KEY=key, + **kwargs, + ) + else: + raise ValueError("Invalid 'i' value. Must be int or str") + + def _add_cred(self, decTkt, hash=None, kdc_hash=None): + """ + Add a decoded ticket to the CCache + """ + cred = CCCredential() + etype = ( + self._prompt( + "What key should we use (AES128-CTS-HMAC-SHA1-96/AES256-CTS-HMAC-SHA1-96/RC4-HMAC) ? [AES256-CTS-HMAC-SHA1-96]: " + ) + or "AES256-CTS-HMAC-SHA1-96" + ) + if etype not in _KRB_E_TYPES.values(): + print("Unknown keytype") + return + etype = next(k for k, v in _KRB_E_TYPES.items() if v == etype) + cred.ticket.data = bytes( + KRB_Ticket( + realm=decTkt.crealm, + sname=PrincipalName( + nameString=[ + ASN1_GENERAL_STRING(b"krbtgt"), + decTkt.crealm, + ], + nameType=ASN1_INTEGER(2), # NT-SRV-INST + ), + encPart=EncryptedData( + etype=etype, + ), + ) + ) + self.ccache.credentials.append(cred) + self.update_ticket( + len(self.ccache.credentials) - 1, + decTkt, + resign=True, + hash=hash, + kdc_hash=kdc_hash, + ) + + def create_ticket(self, **kwargs): + """ + Create a Kerberos ticket + """ + user = kwargs.get("user", self._prompt("User [User]: ") or "User") + domain = kwargs.get( + "domain", (self._prompt("Domain [DOM.LOCAL]: ") or "DOM.LOCAL").upper() + ) + domain_sid = kwargs.get( + "domain_sid", + self._prompt("Domain SID [S-1-5-21-1-2-3]: ") or "S-1-5-21-1-2-3", + ) + group_ids = kwargs.get( + "group_ids", + [ + int(x.strip()) + for x in ( + self._prompt("Group IDs [513, 512, 520, 518, 519]: ") + or "513, 512, 520, 518, 519" + ).split(",") + ], + ) + user_id = kwargs.get("user_id", int(self._prompt("User ID [500]: ") or "500")) + primary_group_id = kwargs.get( + "primary_group_id", int(self._prompt("Primary Group ID [513]: ") or "513") + ) + extra_sids = kwargs.get("extra_sids", None) + if extra_sids is None: + extra_sids = self._prompt("Extra SIDs [] :") or [] + if extra_sids: + extra_sids = [x.strip() for x in extra_sids.split(",")] + duration = kwargs.get( + "duration", int(self._prompt("Expires in (h) [10]: ") or "10") + ) + now_time = datetime.now(timezone.utc).replace(microsecond=0) + rand = random.SystemRandom() + key = Key.random_to_key( + EncryptionType.AES256_CTS_HMAC_SHA1_96, rand.randbytes(32) + ) + store = { + # KRB + "flags": ASN1_BIT_STRING("01000000111000010000000000000000"), + "key": { + "keytype": ASN1_INTEGER(key.etype), + "keyvalue": ASN1_STRING(key.key), + }, + "crealm": ASN1_GENERAL_STRING(domain), + "cname": { + "nameString": [ASN1_GENERAL_STRING(user)], + "nameType": ASN1_INTEGER(1), + }, + "authtime": ASN1_GENERALIZED_TIME(now_time), + "starttime": ASN1_GENERALIZED_TIME(now_time + timedelta(hours=duration)), + "endtime": ASN1_GENERALIZED_TIME(now_time + timedelta(hours=duration)), + "renewTill": ASN1_GENERALIZED_TIME(now_time + timedelta(hours=duration)), + # PAC + # Validation info + "VI.LogonTime": self._time_to_filetime(now_time.timestamp()), + "VI.LogoffTime": self._time_to_filetime("NEVER"), + "VI.KickOffTime": self._time_to_filetime("NEVER"), + "VI.PasswordLastSet": self._time_to_filetime( + (now_time - timedelta(hours=10)).timestamp() + ), + "VI.PasswordCanChange": self._time_to_filetime(0), + "VI.PasswordMustChange": self._time_to_filetime("NEVER"), + "VI.EffectiveName": user, + "VI.FullName": "", + "VI.LogonScript": "", + "VI.ProfilePath": "", + "VI.HomeDirectory": "", + "VI.HomeDirectoryDrive": "", + "VI.UserSessionKey": b"\x00" * 16, + "VI.LogonServer": "", + "VI.LogonDomainName": domain.rsplit(".", 1)[0], + "VI.LogonCount": 70, + "VI.BadPasswordCount": 0, + "VI.UserId": user_id, + "VI.PrimaryGroupId": primary_group_id, + "VI.GroupIds": [ + { + "RelativeId": x, + "Attributes": 7, + } + for x in group_ids + ], + "VI.UserFlags": 32, + "VI.LogonDomainId": domain_sid, + "VI.UserAccountControl": 128, + "VI.ExtraSids": [{"Sid": x, "Attributes": 7} for x in extra_sids], + "VI.ResourceGroupDomainSid": None, + "VI.ResourceGroupIds": [], + # Pac Client infos + "CI.ClientId": self._utc_to_mstime(now_time.timestamp()), + "CI.Name": user, + # UPN DNS Info + "UPNDNS.Flags": 3, + "UPNDNS.Upn": "%s@%s" % (user, domain.lower()), + "UPNDNS.DnsDomainName": domain.upper(), + "UPNDNS.SamName": user, + "UPNDNS.Sid": "%s-%s" % (domain_sid, user_id), + # Client Claims + "CC.ClaimsArrays": [ + { + "ClaimsSourceType": 1, + "ClaimEntries": [ + { + "Id": "ad://ext/AuthenticationSilo", + "Type": 3, + "StringValues": "T0-silo", + } + ], + } + ], + # Attributes Info + "AI.Flags": "PAC_WAS_REQUESTED", + # Requestor + "REQ.Sid": "%s-%s" % (domain_sid, user_id), + # Server Checksum + "SC.SignatureType": 16, + "SC.Signature": b"\x00" * 12, + "SC.RODCIdentifier": b"", + # KDC Checksum + "KC.SignatureType": 16, + "KC.Signature": b"\x00" * 12, + "KC.RODCIdentifier": b"", + # Ticket Checksum + "TKT.SignatureType": -1, + "TKT.Signature": b"\x00" * 12, + "TKT.RODCIdentifier": b"", + # Extended KDC Checksum + "EXKC.SignatureType": -1, + "EXKC.Signature": b"\x00" * 12, + "EXKC.RODCIdentifier": b"", + } + # Build & store ticket + tkt = self._build_ticket(store) + self._add_cred(tkt) + + def _build_sid(self, sidstr, msdn=False): + if not sidstr: + return None + m = re.match(r"S-(\d+)-(\d+)-?((?:\d+-?)*)", sidstr.strip()) + if not m: + raise ValueError("Invalid SID format: %s" % sidstr) + subauthors = [] + if m.group(3): + subauthors = [int(x) for x in m.group(3).split("-")] + if msdn: + return WINNT_SID( + Revision=int(m.group(1)), + IdentifierAuthority=WINNT_SID_IDENTIFIER_AUTHORITY( + Value=struct.pack(">Q", int(m.group(2)))[2:], + ), + SubAuthority=subauthors, + ) + else: + return SID( + Revision=int(m.group(1)), + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=struct.pack(">Q", int(m.group(2)))[2:] + ), + SubAuthority=subauthors, + ) + + def _build_ticket(self, store): + if store["CC.ClaimsArrays"]: + claimSet = CLAIMS_SET( + ndr64=False, + ClaimsArrays=[ + CLAIMS_ARRAY( + usClaimsSourceType=ca["ClaimsSourceType"], + ClaimEntries=[ + CLAIM_ENTRY( + Id=ce["Id"], + Type=ce["Type"], + Values=NDRUnion( + tag=ce["Type"], + value=CLAIM_ENTRY_sub2( + ValueCount=ce["StringValues"].count(";") + 1, + StringValues=ce["StringValues"].split(";"), + ), + ), + ) + for ce in ca["ClaimEntries"] + ], + ) + for ca in store["CC.ClaimsArrays"] + ], + usReservedType=0, + ulReservedFieldSize=0, + ReservedField=None, + ) + else: + claimSet = None + _signature_set = lambda x: store[x + ".SignatureType"] != -1 + return EncTicketPart( + transited=TransitedEncoding( + trType=ASN1_INTEGER(0), contents=ASN1_STRING(b"") + ), + addresses=None, + flags=store["flags"], + key=EncryptionKey( + keytype=store["key"]["keytype"], + keyvalue=store["key"]["keyvalue"], + ), + crealm=store["crealm"], + cname=PrincipalName( + nameString=store["cname"]["nameString"], + nameType=store["cname"]["nameType"], + ), + authtime=store["authtime"], + starttime=store["starttime"], + endtime=store["endtime"], + renewTill=store["renewTill"], + authorizationData=AuthorizationData( + seq=[ + AuthorizationDataItem( + adType=ASN1_INTEGER(1), + adData=AuthorizationData( + seq=[ + AuthorizationDataItem( + adType="AD-WIN2K-PAC", + adData=PACTYPE( + Buffers=[ + PAC_INFO_BUFFER( + ulType="Logon information", + ), + ] + + ( + [ + PAC_INFO_BUFFER( + ulType="Server Signature", + ), + ] + if _signature_set("SC") + else [] + ) + + ( + [ + PAC_INFO_BUFFER( + ulType="KDC Signature", + ), + ] + if _signature_set("KC") + else [] + ) + + [ + PAC_INFO_BUFFER( + ulType="Client name and ticket information", + ), + PAC_INFO_BUFFER( + ulType="UPN and DNS information", + ), + ] + + ( + [ + PAC_INFO_BUFFER( + ulType="Client claims information", + ), + ] + if claimSet + else [] + ) + + ( + [ + PAC_INFO_BUFFER( + ulType="PAC Attributes", + ), + ] + if store["AI.Flags"] + else [] + ) + + ( + [ + PAC_INFO_BUFFER( + ulType="PAC Requestor", + ), + ] + if store["REQ.Sid"] + else [] + ) + + ( + [ + PAC_INFO_BUFFER( + ulType="Ticket Signature", + ), + ] + if _signature_set("TKT") + else [] + ) + + ( + [ + PAC_INFO_BUFFER( + ulType="Extended KDC Signature", + ), + ] + if _signature_set("EXKC") + else [] + ), + Payloads=[ + KERB_VALIDATION_INFO( + ndr64=False, + ndrendian="little", + LogonTime=store["VI.LogonTime"], + LogoffTime=store["VI.LogoffTime"], + KickOffTime=store["VI.KickOffTime"], + PasswordLastSet=store[ + "VI.PasswordLastSet" + ], + PasswordCanChange=store[ + "VI.PasswordCanChange" + ], + PasswordMustChange=store[ + "VI.PasswordMustChange" + ], + EffectiveName=RPC_UNICODE_STRING( + Buffer=store["VI.EffectiveName"], + ), + FullName=RPC_UNICODE_STRING( + Buffer=store["VI.FullName"], + ), + LogonScript=RPC_UNICODE_STRING( + Buffer=store["VI.LogonScript"], + ), + ProfilePath=RPC_UNICODE_STRING( + Buffer=store["VI.ProfilePath"], + ), + HomeDirectory=RPC_UNICODE_STRING( + Buffer=store["VI.HomeDirectory"], + ), + HomeDirectoryDrive=RPC_UNICODE_STRING( + Buffer=store[ + "VI.HomeDirectoryDrive" + ], + ), + UserSessionKey=USER_SESSION_KEY( + data=[ + CYPHER_BLOCK( + data=store[ + "VI.UserSessionKey" + ][:8] + ), + CYPHER_BLOCK( + data=store[ + "VI.UserSessionKey" + ][8:] + ), + ] + ), + LogonServer=RPC_UNICODE_STRING( + Buffer=store["VI.LogonServer"], + ), + LogonDomainName=RPC_UNICODE_STRING( + Buffer=store["VI.LogonDomainName"], + ), + LogonCount=store["VI.LogonCount"], + BadPasswordCount=store[ + "VI.BadPasswordCount" + ], + UserId=store["VI.UserId"], + PrimaryGroupId=store[ + "VI.PrimaryGroupId" + ], + GroupIds=[ + GROUP_MEMBERSHIP( + RelativeId=x["RelativeId"], + Attributes=x["Attributes"], + ) + for x in store["VI.GroupIds"] + ], + UserFlags=store["VI.UserFlags"], + LogonDomainId=self._build_sid( + store["VI.LogonDomainId"] + ), + Reserved1=[0, 0], + UserAccountControl=store[ + "VI.UserAccountControl" + ], + Reserved3=[0, 0, 0, 0, 0, 0, 0], + ExtraSids=( + [ + KERB_SID_AND_ATTRIBUTES( + Sid=self._build_sid( + x["Sid"] + ), + Attributes=x["Attributes"], + ) + for x in store["VI.ExtraSids"] + ] + if store["VI.ExtraSids"] + else None + ), + ResourceGroupDomainSid=self._build_sid( + store["VI.ResourceGroupDomainSid"] + ), + ResourceGroupIds=( + [ + GROUP_MEMBERSHIP( + RelativeId=x["RelativeId"], + Attributes=x["Attributes"], + ) + for x in store[ + "VI.ResourceGroupIds" + ] + ] + if store["VI.ResourceGroupIds"] + else None + ), + ), + ] + + ( + [ + PAC_SIGNATURE_DATA( + SignatureType=store[ + "SC.SignatureType" + ], + Signature=store["SC.Signature"], + RODCIdentifier=store[ + "SC.RODCIdentifier" + ], + ), + ] + if _signature_set("SC") + else [] + ) + + ( + [ + PAC_SIGNATURE_DATA( + SignatureType=store[ + "KC.SignatureType" + ], + Signature=store["KC.Signature"], + RODCIdentifier=store[ + "KC.RODCIdentifier" + ], + ), + ] + if _signature_set("KC") + else [] + ) + + [ + PAC_CLIENT_INFO( + ClientId=store["CI.ClientId"], + Name=store["CI.Name"], + ), + UPN_DNS_INFO( + Flags=store["UPNDNS.Flags"], + Payload=[ + ( + "Upn", + store["UPNDNS.Upn"], + ), + ( + "DnsDomainName", + store["UPNDNS.DnsDomainName"], + ), + ( + "SamName", + store["UPNDNS.SamName"], + ), + ( + "Sid", + self._build_sid( + store["UPNDNS.Sid"], + msdn=True, + ), + ), + ], + ), + ] + + ( + [ + PAC_CLIENT_CLAIMS_INFO( + ndr64=False, + Claims=CLAIMS_SET_METADATA( + ClaimsSet=[ + claimSet, + ], + usCompressionFormat=0, + usReservedType=0, + ulReservedFieldSize=0, + ReservedField=None, + ), + ), + ] + if claimSet + else [] + ) + + ( + [ + PAC_ATTRIBUTES_INFO( + Flags=[store["AI.Flags"]], + FlagsLength=2, + ) + ] + if store["AI.Flags"] + else [] + ) + + ( + [ + PAC_REQUESTOR_SID( + Sid=self._build_sid( + store["REQ.Sid"], msdn=True + ), + ), + ] + if store["REQ.Sid"] + else [] + ) + + ( + [ + PAC_SIGNATURE_DATA( + SignatureType=store[ + "TKT.SignatureType" + ], + Signature=store["TKT.Signature"], + RODCIdentifier=store[ + "TKT.RODCIdentifier" + ], + ), + ] + if _signature_set("TKT") + else [] + ) + + ( + [ + PAC_SIGNATURE_DATA( + SignatureType=store[ + "EXKC.SignatureType" + ], + Signature=store["EXKC.Signature"], + RODCIdentifier=store[ + "EXKC.RODCIdentifier" + ], + ) + ] + if _signature_set("EXKC") + else [] + ), + ), + ) + ] + ), + ) + ] + ), + ) + + def _make_fields(self, element, fields, datastore=None): + frm = ttk.Frame(element) + frm.pack(fill="x") + for i, fld in enumerate(fields): + (self._data if datastore is None else datastore)[fld[0]] = v = tk.StringVar( + frm, value=fld[1] + ) + ttk.Label(frm, text=fld[0]).grid(row=i, column=0, sticky="w") + ttk.Entry(frm, textvariable=v).grid(row=i, column=1, sticky="e") + frm.grid_columnconfigure(1, weight=1) + + def _make_checkbox(self, element, keys, flags, datastore): + for flg in keys: + datastore[flg] = v = tk.BooleanVar(value=flg in flags) + tk.Checkbutton(element, text=flg, variable=v, anchor=tk.W).pack( + fill="x", padx=5, pady=1 + ) + + def _make_table(self, element, name, headers, lst, datastore=None): + wrap = ttk.LabelFrame(element, text=name) + tree = ttk.Treeview(wrap, column=headers, show="headings", height=4) + vsb = ttk.Scrollbar(wrap, orient="vertical", command=tree.yview) + vsb.pack(side="right", fill="y") + tree.configure(yscrollcommand=vsb.set) + for h in headers: + tree.column(h, anchor=tk.CENTER) + tree.heading(h, text=h) + for i, row in enumerate(lst): + tree.insert(parent="", index="end", iid=i, values=row) + tree.pack(fill="x", padx=10, pady=10) + + def _update_datastore(): + children = [tree.item(x, "values") for x in tree.get_children()] + (self._data if datastore is None else datastore)[name] = children + + _update_datastore() + + class EditDialog(tksd.Dialog): + def __init__(self, *args, **kwargs): + self.data = {} + self.initial_values = kwargs.pop("values", {}) + self.success = False + super(EditDialog, self).__init__(*args, **kwargs) + + def body(diag, frame): + self._make_fields( + frame, + [(x, diag.initial_values.get(x, "")) for x in headers], + datastore=diag.data, + ) + return frame + + def ok(self, *args, **kwargs): + self.success = True + super(EditDialog, self).ok(*args, **kwargs) + + def values(self): + return tuple(x.get() for x in self.data.values()) + + def add(): + dialog = EditDialog(title="Add", parent=tree) + if dialog.success: + i = len(tree.get_children()) + tree.insert(parent="", index="end", iid=i, values=dialog.values()) + _update_datastore() + + def edit(): + selected = tree.focus() + if not selected: + return + values = dict(zip(headers, tree.item(selected, "values"))) + dialog = EditDialog(title="Edit", parent=tree, values=values) + if dialog.success: + tree.item(selected, values=dialog.values()) + _update_datastore() + + def remove(): + selected = tree.focus() + if selected: + tree.delete(selected) + _update_datastore() + + btns = ttk.Frame(wrap) + ttk.Button(btns, text="Add", command=add).grid(row=0, column=0, padx=10) + ttk.Button(btns, text="Edit", command=edit).grid(row=0, column=1, padx=10) + ttk.Button(btns, text="Remove", command=remove).grid(row=0, column=2, padx=10) + btns.pack() + wrap.pack(fill="x") + + def _make_list(self, element, func, key, fields_list, new_values): + tbl = ttk.Frame(element) + tbl.pack() + + self._data[key] = data = collections.defaultdict(dict) + + def append(val): + i = tbl.grid_size()[1] + elt = ttk.Frame(tbl, style="BorderFrame.TFrame") + elt.grid(padx=10, pady=10, row=i, column=0) + func(elt, val, data[i]) + + for val in fields_list: + append(val) + + def add(): + append(new_values.copy()) + + def delete(): + slavescount = len(tbl.grid_slaves()) + i = tksd.askinteger( + "Delete", + "Input the index of the Claim to delete [0-%s]" % (slavescount - 1), + parent=tbl, + ) + if i is None or i > slavescount - 1: + return + tbl.grid_slaves(row=i, column=0)[0].destroy() + del data[i] + + btns = ttk.Frame(element) + ttk.Button(btns, text="Add", command=add).grid(row=0, column=0, padx=10) + ttk.Button(btns, text="Delete", command=delete).grid(row=0, column=1, padx=10) + btns.pack() + + _TIME_FIELD = UTCTimeField( + "", + None, + fmt="> 32) & 0xFFFFFFFF, + dwLowDateTime=x & 0xFFFFFFFF, + ) + + def _filetime_totime(self, x): + if x.dwHighDateTime == 0x7FFFFFFF and x.dwLowDateTime == 0xFFFFFFFF: + return "NEVER" + return self._pretty_time((x.dwHighDateTime << 32) + x.dwLowDateTime) + + def _pretty_sid(self, sid): + if not sid or not sid.IdentifierAuthority.Value: + return "" + return sid.summary() + + def _getLogonInformation(self, pac, element): + logonInfo = pac.getPayload(0x00000001) + if not logonInfo: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x00000001)) + logonInfo = KERB_VALIDATION_INFO() + else: + logonInfo = logonInfo.value + self._make_fields( + element, + [ + ("LogonTime", self._filetime_totime(logonInfo.LogonTime)), + ("LogoffTime", self._filetime_totime(logonInfo.LogoffTime)), + ("KickOffTime", self._filetime_totime(logonInfo.KickOffTime)), + ( + "PasswordLastSet", + self._filetime_totime(logonInfo.PasswordLastSet), + ), + ( + "PasswordCanChange", + self._filetime_totime(logonInfo.PasswordCanChange), + ), + ( + "PasswordMustChange", + self._filetime_totime(logonInfo.PasswordMustChange), + ), + ( + "EffectiveName", + logonInfo.EffectiveName.Buffer.value.value[0].value.decode(), + ), + ( + "FullName", + logonInfo.FullName.Buffer.value.value[0].value.decode(), + ), + ( + "LogonScript", + logonInfo.LogonScript.Buffer.value.value[0].value.decode(), + ), + ( + "ProfilePath", + logonInfo.ProfilePath.Buffer.value.value[0].value.decode(), + ), + ( + "HomeDirectory", + logonInfo.HomeDirectory.Buffer.value.value[0].value.decode(), + ), + ( + "HomeDirectoryDrive", + logonInfo.HomeDirectoryDrive.Buffer.value.value[0].value.decode(), + ), + ("LogonCount", str(logonInfo.LogonCount)), + ("BadPasswordCount", str(logonInfo.BadPasswordCount)), + ("UserId", str(logonInfo.UserId)), + ("PrimaryGroupId", str(logonInfo.PrimaryGroupId)), + ], + ) + self._make_table( + element, + "GroupIds", + ["RelativeId", "Attributes"], + [ + (str(x.RelativeId), str(x.Attributes)) + for x in logonInfo.GroupIds.value.value + ], + ) + self._make_fields( + element, + [ + ("UserFlags", str(logonInfo.UserFlags)), + ( + "UserSessionKey", + bytes_hex( + b"".join(x.data for x in logonInfo.UserSessionKey.data) + ).decode(), + ), + ( + "LogonServer", + logonInfo.LogonServer.Buffer.value.value[0].value.decode(), + ), + ( + "LogonDomainName", + logonInfo.LogonDomainName.Buffer.value.value[0].value.decode(), + ), + ( + "LogonDomainId", + self._pretty_sid(logonInfo.LogonDomainId.value), + ), + ("UserAccountControl", str(logonInfo.UserAccountControl)), + ], + ) + self._make_table( + element, + "ExtraSids", + ["Sid", "Attributes"], + [ + (self._pretty_sid(x.Sid.value), str(x.Attributes)) + for x in ( + logonInfo.ExtraSids.value.value if logonInfo.ExtraSids else [] + ) + ], + ) + self._make_fields( + element, + [ + ( + "ResourceGroupDomainSid", + self._pretty_sid( + logonInfo.ResourceGroupDomainSid.value + if logonInfo.ResourceGroupDomainSid + else None + ), + ), + ], + ) + self._make_table( + element, + "ResourceGroupIds", + ["RelativeId", "Attributes"], + [ + (str(x.RelativeId), str(x.Attributes)) + for x in ( + logonInfo.ResourceGroupIds.value.value + if logonInfo.ResourceGroupIds + else [] + ) + ], + ) + + def _getClientInfo(self, pac, element): + clientInfo = pac.getPayload(0x0000000A) + if not clientInfo: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x0000000A)) + clientInfo = PAC_CLIENT_INFO() + return self._make_fields( + element, + [ + ("ClientId", self._pretty_time(clientInfo.ClientId)), + ("Name", clientInfo.Name), + ], + ) + + def _getUPNDnsInfo(self, pac, element): + upndnsinfo = pac.getPayload(0x0000000C) + if not upndnsinfo: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x0000000C)) + upndnsinfo = UPN_DNS_INFO() + return self._make_fields( + element, + [ + ("Upn", upndnsinfo.Upn), + ("DnsDomainName", upndnsinfo.DnsDomainName), + ( + "SamName", + ( + upndnsinfo.SamName + if upndnsinfo.Flags.S and upndnsinfo.SamNameLen + else "" + ), + ), + ( + "UpnDnsSid", + ( + self._pretty_sid(upndnsinfo.Sid) + if upndnsinfo.Flags.S and upndnsinfo.SidLen + else "" + ), + ), + ], + ) + + def _getClientClaims(self, pac, element): + clientClaims = pac.getPayload(0x0000000D) + if not clientClaims or isinstance(clientClaims, conf.padding_layer): + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x0000000D)) + claimsArray = [] + else: + claimsArray = ( + clientClaims.value.valueof("Claims") + .valueof("ClaimsSet") + .value.valueof("ClaimsArrays") + ) + + def func(elt, x, datastore): + self._make_fields( + elt, + [ + ("ClaimsSourceType", str(x.usClaimsSourceType)), + ], + datastore=datastore, + ) + self._make_table( + elt, + "ClaimEntries", + ["Id", "Type", "Values"], + [ + ( + y.valueof("Id").decode(), + str(y.Type), + ";".join( + z.decode() + for z in y.valueof("Values").valueof("StringValues") + ), + ) + for y in x.valueof("ClaimEntries") + ], + datastore=datastore, + ) + + return self._make_list( + element, + func=func, + key="ClaimsArrays", + fields_list=claimsArray, + new_values=CLAIMS_ARRAY(ClaimEntries=[]), + ) + + def _getPACAttributes(self, pac, element): + pacAttributes = pac.getPayload(0x00000011) + if not pacAttributes: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x00000011)) + pacAttributes = PAC_ATTRIBUTES_INFO(Flags=0) + flags = str(pacAttributes.Flags[0]).split("+") + self._data["pacAttributes"] = {} + self._make_checkbox( + element, + [ + "PAC_WAS_REQUESTED", + "PAC_WAS_GIVEN_IMPLICITLY", + ], + flags, + self._data["pacAttributes"], + ) + + def _getPACRequestor(self, pac, element): + pacRequestor = pac.getPayload(0x00000012) + if not pacRequestor: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x00000012)) + pacRequestor = PAC_REQUESTOR_SID() + return self._make_fields( + element, [("ReqSid", self._pretty_sid(pacRequestor.Sid))] + ) + + def _getServerChecksum(self, pac, element): + serverChecksum = pac.getPayload(0x00000006) + if not serverChecksum: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x00000006)) + serverChecksum = PAC_SIGNATURE_DATA() + return self._make_fields( + element, + [ + ( + "SRVSignatureType", + ( + str(serverChecksum.SignatureType) + if serverChecksum.SignatureType is not None + else "" + ), + ), + ("SRVSignature", bytes_hex(serverChecksum.Signature).decode()), + ("SRVRODCIdentifier", serverChecksum.RODCIdentifier.decode()), + ], + ) + + def _getKDCChecksum(self, pac, element): + kdcChecksum = pac.getPayload(0x00000007) + if not kdcChecksum: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x00000007)) + kdcChecksum = PAC_SIGNATURE_DATA() + return self._make_fields( + element, + [ + ( + "KDCSignatureType", + ( + str(kdcChecksum.SignatureType) + if kdcChecksum.SignatureType is not None + else "" + ), + ), + ("KDCSignature", bytes_hex(kdcChecksum.Signature).decode()), + ("KDCRODCIdentifier", kdcChecksum.RODCIdentifier.decode()), + ], + ) + + def _getTicketChecksum(self, pac, element): + ticketChecksum = pac.getPayload(0x00000010) + if not ticketChecksum: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x00000010)) + ticketChecksum = PAC_SIGNATURE_DATA() + return self._make_fields( + element, + [ + ( + "TKTSignatureType", + ( + str(ticketChecksum.SignatureType) + if ticketChecksum.SignatureType is not None + else "" + ), + ), + ("TKTSignature", bytes_hex(ticketChecksum.Signature).decode()), + ("TKTRODCIdentifier", ticketChecksum.RODCIdentifier.decode()), + ], + ) + + def _getExtendedKDCChecksum(self, pac, element): + exkdcChecksum = pac.getPayload(0x00000013) + if not exkdcChecksum: + pac.Buffers.append(PAC_INFO_BUFFER(ulType=0x00000013)) + exkdcChecksum = PAC_SIGNATURE_DATA() + return self._make_fields( + element, + [ + ( + "EXKDCSignatureType", + ( + str(exkdcChecksum.SignatureType) + if exkdcChecksum.SignatureType is not None + else "" + ), + ), + ("EXKDCSignature", bytes_hex(exkdcChecksum.Signature).decode()), + ("EXKDCRODCIdentifier", exkdcChecksum.RODCIdentifier.decode()), + ], + ) + + def edit_ticket(self, i, key=None, hash=None): + """ + Edit a Kerberos ticket using the GUI + """ + if tk is None: + raise ImportError( + "tkinter is not installed (`apt install python3-tk` on debian)" + ) + tkt = self.dec_ticket(i, key=key, hash=hash) + pac = tkt.authorizationData.seq[0].adData[0].seq[0].adData + + # WIDTH, HEIGHT = 1120, 1000 + + # Note: for TK doc, use https://tkdocs.com + + # Root + root = tk.Tk() + root.title("Ticketer++ (@secdev/scapy)") + # root.geometry("%sx%s" % (WIDTH, HEIGHT)) + # root.resizable(0, 1) + + scrollFrame = ScrollFrame(root) + frm = scrollFrame.viewPort + + tk_ticket = ttk.Frame(frm, padding=5) + tk_pac = ttk.Frame(frm, padding=5) + + ttk.Button(frm, text="Quit", command=root.destroy).grid( + column=0, row=1, columnspan=2 + ) + + # TTK style + + ttkstyle = ttk.Style() + ttkstyle.theme_use("alt") + ttkstyle.configure( + "BorderFrame.TFrame", + relief="groove", + borderwidth=3, + ) + + # MAIN TICKET + + # Flags + tk_flags = ttk.LabelFrame( + tk_ticket, + text="Flags", + style="BorderFrame.TFrame", + ) + tk_flags.pack(fill="x", pady=5) + flags = tkt.get_field("flags").get_flags(tkt) + self._data["flags"] = {} + self._make_checkbox(tk_flags, _TICKET_FLAGS, flags, self._data["flags"]) + + # Key + tk_key = ttk.LabelFrame( + tk_ticket, + text="key", + style="BorderFrame.TFrame", + ) + tk_key.pack(fill="x", pady=5) + self._make_fields( + tk_key, + [ + ("keytype", str(tkt.key.keytype.val)), + ( + "keyvalue", + bytes_hex(tkt.key.keyvalue.val).decode(), + ), + ], + ) + + # crealm + self._make_fields(tk_ticket, [("crealm", tkt.crealm.val.decode())]) + + # cname + tk_cname = ttk.LabelFrame( + tk_ticket, + text="cname", + style="BorderFrame.TFrame", + ) + tk_cname.pack(fill="x", pady=5) + self._make_fields( + tk_cname, + [ + ( + "nameType", + str(tkt.cname.nameType.val), + ), + ], + ) + self._make_table( + tk_cname, + "nameString", + ["Value"], + [(x.val.decode(),) for x in tkt.cname.nameString], + ) + + # transited + tk_transited = ttk.LabelFrame( + tk_ticket, + text="transited", + style="BorderFrame.TFrame", + ) + tk_transited.pack(fill="x", pady=5) + self._make_fields( + tk_transited, + [ + # + ( + "trType", + str(tkt.transited.trType.val), + ), + ( + "contents", + tkt.transited.contents.val.decode(), + ), + ], + ) + + # times + self._make_fields( + tk_ticket, + [ + ("authtime", tkt.authtime.pretty_time.rstrip(" UTC")), + ("starttime", tkt.starttime.pretty_time.rstrip(" UTC")), + ("endtime", tkt.endtime.pretty_time.rstrip(" UTC")), + ("renewTill", tkt.renewTill.pretty_time.rstrip(" UTC")), + ], + ) + + # PAC + + # Logon information + tk_logoninfo = ttk.LabelFrame( + tk_pac, + text="Logon information", + style="BorderFrame.TFrame", + ) + tk_logoninfo.pack(fill="x", pady=5) + self._getLogonInformation(pac, tk_logoninfo) + + # Client name and ticket information + tk_clientinfo = ttk.LabelFrame( + tk_pac, + text="Client name and ticket information", + style="BorderFrame.TFrame", + ) + tk_clientinfo.pack(fill="x", pady=5) + self._getClientInfo(pac, tk_clientinfo) + + # UPN and DNS information + tk_upndnsinfo = ttk.LabelFrame( + tk_pac, + text="UPN and DNS information", + style="BorderFrame.TFrame", + ) + tk_upndnsinfo.pack(fill="x", pady=5) + self._getUPNDnsInfo(pac, tk_upndnsinfo) + + # Client claims information + tk_clientclaims = ttk.LabelFrame( + tk_pac, + text="Client claims information", + style="BorderFrame.TFrame", + ) + tk_clientclaims.pack(fill="x", pady=5) + self._getClientClaims(pac, tk_clientclaims) + + # PAC Attributes + tk_pacattributes = ttk.LabelFrame( + tk_pac, + text="PAC Attributes", + style="BorderFrame.TFrame", + ) + tk_pacattributes.pack(fill="x", pady=5) + self._getPACAttributes(pac, tk_pacattributes) + + # PAC Requestor + tk_pacrequestor = ttk.LabelFrame( + tk_pac, + text="PAC Requestor", + style="BorderFrame.TFrame", + ) + tk_pacrequestor.pack(fill="x", pady=5) + self._getPACRequestor(pac, tk_pacrequestor) + + # Server checksum + tk_serverchksum = ttk.LabelFrame( + tk_pac, + text="Server checksum", + style="BorderFrame.TFrame", + ) + tk_serverchksum.pack(fill="x", pady=5) + self._getServerChecksum(pac, tk_serverchksum) + + # KDC checksum + tk_serverchksum = ttk.LabelFrame( + tk_pac, + text="KDC checksum", + style="BorderFrame.TFrame", + ) + tk_serverchksum.pack(fill="x", pady=5) + self._getKDCChecksum(pac, tk_serverchksum) + + # Ticket checksum + tk_serverchksum = ttk.LabelFrame( + tk_pac, + text="Ticket checksum", + style="BorderFrame.TFrame", + ) + tk_serverchksum.pack(fill="x", pady=5) + self._getTicketChecksum(pac, tk_serverchksum) + + # Extended KDC checksum + tk_serverchksum = ttk.LabelFrame( + tk_pac, + text="Extended KDC checksum", + style="BorderFrame.TFrame", + ) + tk_serverchksum.pack(fill="x", pady=5) + self._getExtendedKDCChecksum(pac, tk_serverchksum) + + # Run + + tk_ticket.grid(column=0, row=0, sticky=tk.N) + tk_pac.grid(column=1, row=0, sticky=tk.N) + + scrollFrame.pack(side="top", fill="both", expand=True) + root.mainloop() + + # Rebuild + store = { + # KRB + "flags": ASN1_BIT_STRING( + "".join( + "1" if self._data["flags"][x].get() else "0" for x in _TICKET_FLAGS + ) + + "0" * (-len(_TICKET_FLAGS) % 32) + ), + "key": { + "keytype": ASN1_INTEGER(int(self._data["keytype"].get())), + "keyvalue": ASN1_STRING(hex_bytes(self._data["keyvalue"].get())), + }, + "crealm": ASN1_GENERAL_STRING(self._data["crealm"].get()), + "cname": { + "nameString": [ + ASN1_GENERAL_STRING(x[0]) for x in self._data["nameString"] + ], + "nameType": ASN1_INTEGER(int(self._data["nameType"].get())), + }, + "authtime": self._time_to_asn1(self._data["authtime"].get()), + "starttime": self._time_to_asn1(self._data["starttime"].get()), + "endtime": self._time_to_asn1(self._data["endtime"].get()), + "renewTill": self._time_to_asn1(self._data["renewTill"].get()), + # PAC + # Validation info + "VI.LogonTime": self._time_to_filetime(self._data["LogonTime"].get()), + "VI.LogoffTime": self._time_to_filetime(self._data["LogoffTime"].get()), + "VI.KickOffTime": self._time_to_filetime(self._data["KickOffTime"].get()), + "VI.PasswordLastSet": self._time_to_filetime( + self._data["PasswordLastSet"].get() + ), + "VI.PasswordCanChange": self._time_to_filetime( + self._data["PasswordCanChange"].get() + ), + "VI.PasswordMustChange": self._time_to_filetime( + self._data["PasswordMustChange"].get() + ), + "VI.EffectiveName": self._data["EffectiveName"].get(), + "VI.FullName": self._data["FullName"].get(), + "VI.LogonScript": self._data["LogonScript"].get(), + "VI.ProfilePath": self._data["ProfilePath"].get(), + "VI.HomeDirectory": self._data["HomeDirectory"].get(), + "VI.HomeDirectoryDrive": self._data["HomeDirectoryDrive"].get(), + "VI.UserSessionKey": hex_bytes(self._data["UserSessionKey"].get()), + "VI.LogonServer": self._data["LogonServer"].get(), + "VI.LogonDomainName": self._data["LogonDomainName"].get(), + "VI.LogonCount": int(self._data["LogonCount"].get()), + "VI.BadPasswordCount": int(self._data["BadPasswordCount"].get()), + "VI.UserId": int(self._data["UserId"].get()), + "VI.PrimaryGroupId": int(self._data["PrimaryGroupId"].get()), + "VI.GroupIds": [ + { + "RelativeId": int(x[0]), + "Attributes": int(x[1]), + } + for x in self._data["GroupIds"] + ], + "VI.UserFlags": int(self._data["UserFlags"].get()), + "VI.LogonDomainId": self._data["LogonDomainId"].get(), + "VI.UserAccountControl": int(self._data["UserAccountControl"].get()), + "VI.ExtraSids": [ + { + "Sid": x[0], + "Attributes": int(x[1]), + } + for x in self._data["ExtraSids"] + ], + "VI.ResourceGroupDomainSid": self._data["ResourceGroupDomainSid"].get(), + "VI.ResourceGroupIds": [ + { + "RelativeId": int(x[0]), + "Attributes": int(x[1]), + } + for x in self._data["ResourceGroupIds"] + ], + # Pac Client infos + "CI.ClientId": self._time_to_int(self._data["ClientId"].get()), + "CI.Name": self._data["Name"].get(), + # UPN DNS Info + "UPNDNS.Flags": 3, + "UPNDNS.Upn": self._data["Upn"].get(), + "UPNDNS.DnsDomainName": self._data["DnsDomainName"].get(), + "UPNDNS.SamName": self._data["SamName"].get(), + "UPNDNS.Sid": self._data["UpnDnsSid"].get(), + # Client Claims + "CC.ClaimsArrays": [ + { + "ClaimsSourceType": int(ca["ClaimsSourceType"].get()), + "ClaimEntries": [ + { + "Id": ce[0], + "Type": int(ce[1]), + "StringValues": ce[2], + } + for ce in ca["ClaimEntries"] + ], + } + for ca in self._data["ClaimsArrays"].values() + ], + # Attributes Info + "AI.Flags": "+".join( + x + for x in ["PAC_WAS_REQUESTED", "PAC_WAS_GIVEN_IMPLICITLY"] + if self._data["pacAttributes"][x].get() + ), + # Requestor + "REQ.Sid": self._data["ReqSid"].get(), + # Server Checksum + "SC.SignatureType": int(self._data["SRVSignatureType"].get()), + "SC.Signature": hex_bytes(self._data["SRVSignature"].get()), + "SC.RODCIdentifier": hex_bytes(self._data["SRVRODCIdentifier"].get()), + # KDC Checksum + "KC.SignatureType": int(self._data["KDCSignatureType"].get() or "-1"), + "KC.Signature": hex_bytes(self._data["KDCSignature"].get()), + "KC.RODCIdentifier": hex_bytes(self._data["KDCRODCIdentifier"].get()), + # Ticket Checksum + "TKT.SignatureType": int(self._data["TKTSignatureType"].get() or "-1"), + "TKT.Signature": hex_bytes(self._data["TKTSignature"].get()), + "TKT.RODCIdentifier": hex_bytes(self._data["TKTRODCIdentifier"].get()), + # Extended KDC Checksum + "EXKC.SignatureType": int(self._data["EXKDCSignatureType"].get() or "-1"), + "EXKC.Signature": hex_bytes(self._data["EXKDCSignature"].get()), + "EXKC.RODCIdentifier": hex_bytes(self._data["EXKDCRODCIdentifier"].get()), + } + tkt = self._build_ticket(store) + if hash is None and key is not None: # TODO: add key to update_ticket + hash = key.key + self.update_ticket(i, tkt, hash=hash) + + def _resign_ticket(self, tkt, spn, hash=None, kdc_hash=None): + """ + Resign a ticket (priv) + """ + # [MS-PAC] 2.8.1 - 2.8.5 + rpac = tkt.authorizationData.seq[0].adData.seq[0].adData # real pac + tmp_tkt = tkt.copy() # fake ticket and pac used for computation + pac = tmp_tkt.authorizationData.seq[0].adData.seq[0].adData + # Variables for Signatures, indexed by ulType + sig_i = {} + sig_type = {} + # Read PAC buffers to find all signatures, and set them to 0 + for k, buf in enumerate(pac.Buffers): + if buf.ulType in [0x00000006, 0x00000007, 0x00000010, 0x00000013]: + sig_i[buf.ulType] = k + sig_type[buf.ulType] = pac.Payloads[k].SignatureType + try: + pac.Payloads[k].Signature = ( + b"\x00" * _checksums[pac.Payloads[k].SignatureType].macsize + ) + except KeyError: + raise ValueError("Unknown/Unsupported signatureType") + rpac.Buffers[k].cbBufferSize = None + rpac.Buffers[k].Offset = None + + # There must at least be Server Signature and KDC Signature + if any(x not in sig_i for x in [0x00000006, 0x00000007]): + raise ValueError("Cannot sign PAC: missing a compulsory signature") + + # Build the 2 necessary keys + key_srv = self._prompt_hash( + spn, + cksumtype=sig_type[0x00000006], + hash=hash, + ) + key_kdc = self._prompt_hash( + "krbtgt/" + "@".join(spn.split("@")[1:] * 2), + cksumtype=sig_type[0x00000007], + hash=kdc_hash, + ) + + # Doc was updated after feedback ! it's now very clear. + + # [MS-PAC] sect 2.8.1 + # Signatures are computed in this order: + # - Ticket signature + # - Extended KDC signature + # - Server signature + # - KDC signature + + # sect 2.8.2 - Ticket Signature + + if 0x00000010 in sig_i: + # "The ad-data in the PAC’s AuthorizationData element ([RFC4120] + # section 5.2.6) is replaced with a single zero byte" + tmp_tkt.authorizationData.seq[0].adData.seq[0].adData = b"\x00" + rpac.Payloads[sig_i[0x00000010]].Signature = ticket_sig = ( + key_kdc.make_checksum( + 17, bytes(tmp_tkt) # KERB_NON_KERB_CKSUM_SALT(17) + ) + ) + # included in the PAC when signing it for Extended Server Signature & Server Signature + pac.Payloads[sig_i[0x00000010]].Signature = ticket_sig + + # sect 2.8.3 - Extended KDC Signature + + if 0x00000013 in sig_i: + rpac.Payloads[sig_i[0x00000013]].Signature = extended_kdc_sig = ( + key_kdc.make_checksum(17, bytes(pac)) # KERB_NON_KERB_CKSUM_SALT(17) + ) + # included in the PAC when signing it for Server Signature + pac.Payloads[sig_i[0x00000013]].Signature = extended_kdc_sig + + # sect 2.8.4 - Server Signature + + rpac.Payloads[sig_i[0x00000006]].Signature = server_sig = key_srv.make_checksum( + 17, bytes(pac) # KERB_NON_KERB_CKSUM_SALT(17) + ) + + # sect 2.8.5 - KDC Signature + + rpac.Payloads[sig_i[0x00000007]].Signature = key_kdc.make_checksum( + 17, server_sig # KERB_NON_KERB_CKSUM_SALT(17) + ) + return tkt + + def resign_ticket(self, i, hash=None, kdc_hash=None): + """ + Resign a ticket from CCache + + :param hash: the hash to use to compute the Server Signature + :param kdc_hash: the hash to use to compute the KDC signature + (if None, not recomputed unless its a TGT where is uses hash) + """ + tkt = self.dec_ticket(i, hash=hash) + self.update_ticket(i, tkt, resign=True, hash=hash, kdc_hash=kdc_hash) + + def request_tgt( + self, + upn=None, + ip=None, + key=None, + password=None, + realm=None, + fast=False, + armor_with=None, + spn=None, + x509=None, + x509key=None, + p12=None, + **kwargs, + ): + """ + Request a Kerberos TGT and add it to the local CCache + + See :func:`~scapy.layers.kerberos.krb_as_req` for the full documentation. + """ + if key is None and password is None: + # Do we have the credential in our Keystore ? + try: + key = self.get_cred(upn) + except ValueError: + # It's okay if we don't have the cred. krb_as_req will prompt. + pass + + # If `armor_with` is specified, get the armor ticket from our store + armor_ticket, armor_ticket_skey, armor_ticket_upn = None, None, None + if armor_with is not None: + fast = True + armor_ticket, armor_ticket_skey, armor_ticket_upn, _ = self.export_krb( + armor_with + ) + + res = krb_as_req( + upn=upn, + ip=ip, + key=key, + password=password, + realm=realm, + fast=fast, + armor_ticket=armor_ticket, + armor_ticket_upn=armor_ticket_upn, + armor_ticket_skey=armor_ticket_skey, + spn=spn, + x509=x509, + x509key=x509key, + p12=p12, + **kwargs, + ) + if not res: + return + + self.import_krb(res) + + def request_st( + self, + i, + spn, + ip=None, + renew=False, + realm=None, + additional_tickets=None, + fast=False, + armor_with=None, + for_user=None, + s4u2proxy=None, + **kwargs, + ): + """ + Request a Kerberos TS and add it to the local CCache using another ticket. + + :param i: the index of the ticket/sessionkey to use in the TGS request. + :param spn: the SPN to request a ticket for. + :param armor_with: the index of the ticket/sessionkey to armor this request. + :param s4u2proxy: if an index, the index of the additional ticket to send along + a S4U2PROXY request. If True, it will use additional_tickets + as usual. + :param for_user: if provided, requests S4U2SELF for that user. + + See :func:`~scapy.layers.kerberos.krb_tgs_req` for the the other parameters. + """ + ticket, sessionkey, upn, _ = self.export_krb(i) + + if additional_tickets is None: + additional_tickets = [] + + # If `armor_with` is specified, get the armor ticket from our store + armor_ticket, armor_ticket_skey, armor_ticket_upn = None, None, None + if armor_with is not None: + fast = True + armor_ticket, armor_ticket_skey, armor_ticket_upn, _ = self.export_krb( + armor_with + ) + + # If `s4u2proxy` is an index, get the ticket to armor with + if isinstance(s4u2proxy, int): + additional_tickets.append(self.export_krb(s4u2proxy)[0]) + s4u2proxy = True + + res = krb_tgs_req( + upn, + spn, + sessionkey=sessionkey, + ticket=ticket, + ip=ip, + renew=renew, + realm=realm, + s4u2proxy=s4u2proxy, + additional_tickets=additional_tickets, + fast=fast, + for_user=for_user, + armor_ticket=armor_ticket, + armor_ticket_upn=armor_ticket_upn, + armor_ticket_skey=armor_ticket_skey, + **kwargs, + ) + if not res: + return + + self.import_krb(res) + + def kpasswdset(self, i, targetupn=None, newpassword=None): + """ + Use kpasswd in 'Set Password' mode to set the password of an account. + + :param i: the TGT to use. + """ + ticket, sessionkey, upn, _ = self.export_krb(i) + kpasswd( + upn=upn, + targetupn=targetupn, + setpassword=True, + ticket=ticket, + key=sessionkey, + newpassword=newpassword, + ) + + def renew(self, i, ip=None, additional_tickets=[], **kwargs): + """ + Renew a Kerberos TGT or a TS from the local CCache using a TGS-REQ + + :param i: the ticket/sessionkey to renew. + """ + ticket, sessionkey, upn, spn = self.export_krb(i) + + res = krb_tgs_req( + upn, + spn, + sessionkey=sessionkey, + ticket=ticket, + ip=ip, + renew=True, + additional_tickets=additional_tickets, + **kwargs, + ) + if not res: + return + + self.import_krb(res, _inplace=i) + + def enumerate_tickets(self): + """ + Enumerate through the tickets in the ccache + """ + for i, cred in enumerate(self.ccache.credentials): + if cred.is_xcacheconf(): + continue + yield i, self.export_krb(i) + + def iter_tickets(self): + """ + Iterate through the tickets in the ccache + """ + for _, tkt in self.enumerate_tickets(): + yield tkt diff --git a/scapy/modules/voip.py b/scapy/modules/voip.py index 420ed641b89..c0eb1ce006b 100644 --- a/scapy/modules/voip.py +++ b/scapy/modules/voip.py @@ -1,13 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ VoIP (Voice over IP) related functions """ -from __future__ import absolute_import import subprocess ################### # Listen VoIP # @@ -18,7 +17,6 @@ from scapy.layers.rtp import RTP from scapy.consts import WINDOWS from scapy.config import conf -from scapy.modules.six.moves import range sox_base = (["sox", "-t", ".ul"], ["-", "-t", "ossdsp", "/dev/dsp"]) diff --git a/scapy/packet.py b/scapy/packet.py index ab6c44cf384..f75b6e43991 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Packet class @@ -13,9 +13,9 @@ - exploration methods: explore() / ls() """ -from __future__ import absolute_import -from __future__ import print_function from collections import defaultdict + +import json import re import time import itertools @@ -23,18 +23,52 @@ import types import warnings -from scapy.fields import StrField, ConditionalField, Emph, PacketListField, \ - BitField, MultiEnumField, EnumField, FlagsField, MultipleTypeField +from scapy.fields import ( + AnyField, + BitField, + ConditionalField, + Emph, + EnumField, + Field, + FlagsField, + FlagValue, + MayEnd, + MultiEnumField, + MultipleTypeField, + PadField, + PacketListField, + RawVal, + StrField, +) from scapy.config import conf, _version_checker from scapy.compat import raw, orb, bytes_encode from scapy.base_classes import BasePacket, Gen, SetGen, Packet_metaclass, \ _CanvasDumpExtended +from scapy.interfaces import _GlobInterfaceType from scapy.volatile import RandField, VolatileValue from scapy.utils import import_hexcap, tex_escape, colgen, issubtype, \ - pretty_list + pretty_list, EDecimal from scapy.error import Scapy_Exception, log_runtime, warning -from scapy.extlib import PYX -import scapy.modules.six as six +from scapy.libs.test_pyx import PYX + +# Typing imports +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + NoReturn, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + Sequence, + cast, +) +from scapy.compat import Self try: import pyx @@ -42,22 +76,14 @@ pass -class RawVal: - def __init__(self, val=""): - self.val = val - - def __str__(self): - return str(self.val) - - def __bytes__(self): - return raw(self.val) - - def __repr__(self): - return "" % self.val +_T = TypeVar("_T", Dict[str, Any], Optional[Dict[str, Any]]) -class Packet(six.with_metaclass(Packet_metaclass, BasePacket, - _CanvasDumpExtended)): +class Packet( + BasePacket, + _CanvasDumpExtended, + metaclass=Packet_metaclass +): __slots__ = [ "time", "sent_time", "name", "default_fields", "fields", "fieldtype", @@ -65,8 +91,9 @@ class Packet(six.with_metaclass(Packet_metaclass, BasePacket, "packetfields", "original", "explicit", "raw_packet_cache", "raw_packet_cache_fields", "_pkt", "post_transforms", - # then payload and underlayer - "payload", "underlayer", + "stop_dissection_after", + # then payload, underlayer and parent + "payload", "underlayer", "parent", "name", # used for sr() "_answered", @@ -74,80 +101,87 @@ class Packet(six.with_metaclass(Packet_metaclass, BasePacket, "direction", "sniffed_on", # handle snaplen Vs real length "wirelen", + "comments", + "process_information" ] name = None - fields_desc = [] - deprecated_fields = {} - overload_fields = {} - payload_guess = [] + fields_desc = [] # type: List[AnyField] + deprecated_fields = {} # type: Dict[str, Tuple[str, str]] + overload_fields = {} # type: Dict[Type[Packet], Dict[str, Any]] + payload_guess = [] # type: List[Tuple[Dict[str, Any], Type[Packet]]] show_indent = 1 show_summary = True match_subclass = False - class_dont_cache = dict() - class_packetfields = dict() - class_default_fields = dict() - class_default_fields_ref = dict() - class_fieldtype = dict() + class_dont_cache = {} # type: Dict[Type[Packet], bool] + class_packetfields = {} # type: Dict[Type[Packet], Any] + class_default_fields = {} # type: Dict[Type[Packet], Dict[str, Any]] + class_default_fields_ref = {} # type: Dict[Type[Packet], List[str]] + class_fieldtype = {} # type: Dict[Type[Packet], Dict[str, AnyField]] # noqa: E501 @classmethod def from_hexcap(cls): + # type: (Type[Packet]) -> Packet return cls(import_hexcap()) @classmethod def upper_bonds(self): + # type: () -> None for fval, upper in self.payload_guess: - print("%-20s %s" % (upper.__name__, ", ".join("%-12s" % ("%s=%r" % i) for i in six.iteritems(fval)))) # noqa: E501 + print( + "%-20s %s" % ( + upper.__name__, + ", ".join("%-12s" % ("%s=%r" % i) for i in fval.items()), + ) + ) @classmethod def lower_bonds(self): - for lower, fval in six.iteritems(self._overload_fields): - print("%-20s %s" % (lower.__name__, ", ".join("%-12s" % ("%s=%r" % i) for i in six.iteritems(fval)))) # noqa: E501 - - def _unpickle(self, dlist): - """Used to unpack pickling""" - self.__init__(b"".join(dlist)) - return self - - def __reduce__(self): - """Used by pickling methods""" - return (self.__class__, (), (self.build(),)) - - def __reduce_ex__(self, proto): - """Used by pickling methods""" - return self.__reduce__() - - def __getstate__(self): - """Mark object as pickable""" - return self.__reduce__()[2] - - def __setstate__(self, state): - """Rebuild state using pickable methods""" - return self._unpickle(state) - - def __deepcopy__(self, memo): - """Used by copy.deepcopy""" - return self.copy() + # type: () -> None + for lower, fval in self._overload_fields.items(): + print( + "%-20s %s" % ( + lower.__name__, + ", ".join("%-12s" % ("%s=%r" % i) for i in fval.items()), + ) + ) - def __init__(self, _pkt=b"", post_transform=None, _internal=0, _underlayer=None, **fields): # noqa: E501 - self.time = time.time() - self.sent_time = None + def __init__(self, + _pkt=b"", # type: Union[bytes, bytearray] + post_transform=None, # type: Any + _internal=0, # type: int + _underlayer=None, # type: Optional[Packet] + _parent=None, # type: Optional[Packet] + stop_dissection_after=None, # type: Optional[Type[Packet]] + **fields # type: Any + ): + # type: (...) -> None + self.time = time.time() # type: Union[EDecimal, float] + self.sent_time = None # type: Union[EDecimal, float, None] self.name = (self.__class__.__name__ if self._name is None else self._name) - self.default_fields = {} + self.default_fields = {} # type: Dict[str, Any] self.overload_fields = self._overload_fields - self.overloaded_fields = {} - self.fields = {} - self.fieldtype = {} - self.packetfields = [] - self.payload = NoPayload() - self.init_fields() + self.overloaded_fields = {} # type: Dict[str, Any] + self.fields = {} # type: Dict[str, Any] + self.fieldtype = {} # type: Dict[str, AnyField] + self.packetfields = [] # type: List[AnyField] + self.payload = NoPayload() # type: Packet + self.init_fields(bool(_pkt)) self.underlayer = _underlayer + self.parent = _parent + if isinstance(_pkt, bytearray): + _pkt = bytes(_pkt) self.original = _pkt self.explicit = 0 - self.raw_packet_cache = None - self.raw_packet_cache_fields = None - self.wirelen = None + self.raw_packet_cache = None # type: Optional[bytes] + self.raw_packet_cache_fields = None # type: Optional[Dict[str, Any]] # noqa: E501 + self.wirelen = None # type: Optional[int] + self.direction = None # type: Optional[int] + self.sniffed_on = None # type: Optional[_GlobInterfaceType] + self.comments = None # type: Optional[List[bytes]] + self.process_information = None # type: Optional[Dict[str, Any]] + self.stop_dissection_after = stop_dissection_after if _pkt: self.dissect(_pkt) if not _internal: @@ -161,14 +195,16 @@ def __init__(self, _pkt=b"", post_transform=None, _internal=0, _underlayer=None, value = fields.pop(fname) except KeyError: continue - self.fields[fname] = self.get_field(fname).any2i(self, value) + self.fields[fname] = value if isinstance(value, RawVal) else \ + self.get_field(fname).any2i(self, value) # The remaining fields are unknown for fname in fields: if fname in self.deprecated_fields: # Resolve deprecated fields value = fields[fname] fname = self._resolve_alias(fname) - self.fields[fname] = self.get_field(fname).any2i(self, value) + self.fields[fname] = value if isinstance(value, RawVal) else \ + self.get_field(fname).any2i(self, value) continue raise AttributeError(fname) if isinstance(post_transform, list): @@ -178,7 +214,94 @@ def __init__(self, _pkt=b"", post_transform=None, _internal=0, _underlayer=None, else: self.post_transforms = [post_transform] - def init_fields(self): + @property + def comment(self): + # type: () -> Optional[bytes] + """Get the comment of the packet""" + if self.comments and len(self.comments): + return self.comments[0] + return None + + @comment.setter + def comment(self, value): + # type: (Optional[bytes]) -> None + """ + Set the comment of the packet. + If value is None, it will clear the comments. + """ + if value is not None: + self.comments = [value] + else: + self.comments = None + + @classmethod + def _rebuild_pkt( + cls, # type: Type[Packet] + fields, # type: Dict[str, Any] + payload, # type: Optional[Packet] + metadata, # type: Dict[str, Any] + extra_slots={}, # type: Dict[str, Any] + ): + # type: (...) -> Packet + """Helper for unpickling Packet instances via field values.""" + # Create the instance using the field values + pkt = cls(**fields) + if payload is not None: + pkt.add_payload(payload) + # Restore metadata + pkt.time = metadata['time'] + pkt.sent_time = metadata['sent_time'] + pkt.direction = metadata['direction'] + pkt.sniffed_on = metadata['sniffed_on'] + pkt.wirelen = metadata['wirelen'] + pkt.comments = metadata['comments'] + # Restore any extra __slots__ defined by subclasses + for attr, value in extra_slots.items(): + setattr(pkt, attr, value) + return pkt + + def __reduce__(self): + # type: () -> Tuple[Any, ...] + """Used by pickling methods. + + Reconstructs the packet from field values, payload, and metadata. + """ + # Store field values for unpickling + fields = {} + for f in self.fields_desc: + if f.name in self.fields: + fields[f.name] = self.fields[f.name] + payload = self.payload # type: Optional[Packet] + if isinstance(payload, NoPayload): + payload = None + # Store metadata for unpickling + metadata = { + 'time': self.time, + 'sent_time': self.sent_time, + 'direction': self.direction, + 'sniffed_on': self.sniffed_on, + 'wirelen': self.wirelen, + 'comments': self.comments, + } + # Collect any extra __slots__ defined by subclasses + extra_slots = {} + for attr in type(self).__all_slots__ - set(Packet.__slots__): + if hasattr(self, attr): + extra_slots[attr] = getattr(self, attr) + return ( + type(self)._rebuild_pkt, + (fields, payload, metadata, extra_slots), + ) + + def __deepcopy__(self, + memo, # type: Any + ): + # type: (...) -> Packet + """Used by copy.deepcopy""" + return self.copy() + + def init_fields(self, for_dissect_only=False): + # type: (bool) -> None """ Initialize each fields of the fields_desc dict """ @@ -186,9 +309,12 @@ def init_fields(self): if self.class_dont_cache.get(self.__class__, False): self.do_init_fields(self.fields_desc) else: - self.do_init_cached_fields() + self.do_init_cached_fields(for_dissect_only=for_dissect_only) - def do_init_fields(self, flist): + def do_init_fields(self, + flist, # type: Sequence[AnyField] + ): + # type: (...) -> None """ Initialize each fields of the fields_desc dict """ @@ -201,7 +327,8 @@ def do_init_fields(self, flist): # We set default_fields last to avoid race issues self.default_fields = default_fields - def do_init_cached_fields(self): + def do_init_cached_fields(self, for_dissect_only=False): + # type: (bool) -> None """ Initialize each fields of the fields_desc dict, or use the cached fields information @@ -220,6 +347,10 @@ def do_init_cached_fields(self): self.fieldtype = Packet.class_fieldtype[cls_name] self.packetfields = Packet.class_packetfields[cls_name] + # Optimization: no need for references when only dissecting. + if for_dissect_only: + return + # Deepcopy default references for fname in Packet.class_default_fields_ref[cls_name]: value = self.default_fields[fname] @@ -230,6 +361,7 @@ def do_init_cached_fields(self): self.fields[fname] = value[:] def prepare_cached_fields(self, flist): + # type: (Sequence[AnyField]) -> None """ Prepare the cached fields of the fields_desc dict """ @@ -253,8 +385,7 @@ def prepare_cached_fields(self, flist): self.do_init_fields(self.fields_desc) return - tmp_copy = copy.deepcopy(f.default) - class_default_fields[f.name] = tmp_copy + class_default_fields[f.name] = copy.deepcopy(f.default) class_fieldtype[f.name] = f if f.holds_packets: class_packetfields.append(f) @@ -271,19 +402,23 @@ def prepare_cached_fields(self, flist): Packet.class_default_fields[cls_name] = class_default_fields def dissection_done(self, pkt): + # type: (Packet) -> None """DEV: will be called after a dissection is completed""" self.post_dissection(pkt) self.payload.dissection_done(pkt) def post_dissection(self, pkt): + # type: (Packet) -> None """DEV: is called after the dissection of the whole packet""" pass def get_field(self, fld): + # type: (str) -> AnyField """DEV: returns the field instance from the name of the field""" return self.fieldtype[fld] def add_payload(self, payload): + # type: (Union[Packet, bytes]) -> None if payload is None: return elif not isinstance(self.payload, NoPayload): @@ -296,29 +431,47 @@ def add_payload(self, payload): if t in payload.overload_fields: self.overloaded_fields = payload.overload_fields[t] break - elif isinstance(payload, bytes): - self.payload = conf.raw_layer(load=payload) + elif isinstance(payload, (bytes, str, bytearray, memoryview)): + self.payload = conf.raw_layer(load=bytes_encode(payload)) else: - raise TypeError("payload must be either 'Packet' or 'bytes', not [%s]" % repr(payload)) # noqa: E501 + raise TypeError("payload must be 'Packet', 'bytes', 'str', 'bytearray', or 'memoryview', not [%s]" % repr(payload)) # noqa: E501 def remove_payload(self): + # type: () -> None self.payload.remove_underlayer(self) self.payload = NoPayload() self.overloaded_fields = {} def add_underlayer(self, underlayer): + # type: (Packet) -> None self.underlayer = underlayer def remove_underlayer(self, other): + # type: (Packet) -> None self.underlayer = None - def copy(self): + def add_parent(self, parent): + # type: (Packet) -> None + """Set packet parent. + When packet is an element in PacketListField, parent field would + point to the list owner packet.""" + self.parent = parent + + def remove_parent(self, other): + # type: (Packet) -> None + """Remove packet parent. + When packet is an element in PacketListField, parent field would + point to the list owner packet.""" + self.parent = None + + def copy(self) -> Self: """Returns a deep copy of the instance.""" clone = self.__class__() clone.fields = self.copy_fields_dict(self.fields) clone.default_fields = self.copy_fields_dict(self.default_fields) clone.overloaded_fields = self.overloaded_fields.copy() clone.underlayer = self.underlayer + clone.parent = self.parent clone.explicit = self.explicit clone.raw_packet_cache = self.raw_packet_cache clone.raw_packet_cache_fields = self.copy_fields_dict( @@ -329,9 +482,13 @@ def copy(self): clone.payload = self.payload.copy() clone.payload.add_underlayer(clone) clone.time = self.time + clone.comments = self.comments + clone.direction = self.direction + clone.sniffed_on = self.sniffed_on return clone def _resolve_alias(self, attr): + # type: (str) -> str new_attr, version = self.deprecated_fields[attr] warnings.warn( "%s has been deprecated in favor of %s since %s !" % ( @@ -341,6 +498,7 @@ def _resolve_alias(self, attr): return new_attr def getfieldval(self, attr): + # type: (str) -> Any if self.deprecated_fields and attr in self.deprecated_fields: attr = self._resolve_alias(attr) if attr in self.fields: @@ -352,6 +510,7 @@ def getfieldval(self, attr): return self.payload.getfieldval(attr) def getfield_and_val(self, attr): + # type: (str) -> Tuple[AnyField, Any] if self.deprecated_fields and attr in self.deprecated_fields: attr = self._resolve_alias(attr) if attr in self.fields: @@ -360,26 +519,30 @@ def getfield_and_val(self, attr): return self.get_field(attr), self.overloaded_fields[attr] if attr in self.default_fields: return self.get_field(attr), self.default_fields[attr] + raise ValueError def __getattr__(self, attr): + # type: (str) -> Any try: fld, v = self.getfield_and_val(attr) - except TypeError: + except ValueError: return self.payload.__getattr__(attr) if fld is not None: - return fld.i2h(self, v) + return v if isinstance(v, RawVal) else fld.i2h(self, v) return v def setfieldval(self, attr, val): + # type: (str, Any) -> None if self.deprecated_fields and attr in self.deprecated_fields: attr = self._resolve_alias(attr) if attr in self.default_fields: fld = self.get_field(attr) if fld is None: - any2i = lambda x, y: y + any2i = lambda x, y: y # type: Callable[..., Any] else: any2i = fld.any2i - self.fields[attr] = any2i(self, val) + self.fields[attr] = val if isinstance(val, RawVal) else \ + any2i(self, val) self.explicit = 0 self.raw_packet_cache = None self.raw_packet_cache_fields = None @@ -391,9 +554,8 @@ def setfieldval(self, attr, val): self.payload.setfieldval(attr, val) def __setattr__(self, attr, val): + # type: (str, Any) -> None if attr in self.__all_slots__: - if attr == "sent_time": - self.update_sent_time(val) return object.__setattr__(self, attr, val) try: return self.setfieldval(attr, val) @@ -402,8 +564,9 @@ def __setattr__(self, attr, val): return object.__setattr__(self, attr, val) def delfieldval(self, attr): + # type: (str) -> None if attr in self.fields: - del(self.fields[attr]) + del self.fields[attr] self.explicit = 0 # in case a default value must be explicit self.raw_packet_cache = None self.raw_packet_cache_fields = None @@ -416,6 +579,7 @@ def delfieldval(self, attr): self.payload.delfieldval(attr) def __delattr__(self, attr): + # type: (str) -> None if attr == "payload": return self.remove_payload() if attr in self.__all_slots__: @@ -427,10 +591,11 @@ def __delattr__(self, attr): return object.__delattr__(self, attr) def _superdir(self): + # type: () -> Set[str] """ Return a list of slots and methods, including those from subclasses. """ - attrs = set() + attrs = set() # type: Set[str] cls = self.__class__ if hasattr(cls, '__all_slots__'): attrs.update(cls.__all_slots__) @@ -440,12 +605,14 @@ def _superdir(self): return attrs def __dir__(self): + # type: () -> List[str] """ Add fields to tab completion list. """ return sorted(itertools.chain(self._superdir(), self.default_fields)) def __repr__(self): + # type: () -> str s = "" ct = conf.color_theme for f in self.fields_desc: @@ -480,66 +647,88 @@ def __repr__(self): repr(self.payload), ct.punct(">")) - if six.PY2: - def __str__(self): - return self.build() - else: - def __str__(self): - warning("Calling str(pkt) on Python 3 makes no sense!") - return str(self.build()) + def __str__(self): + # type: () -> str + return self.summary() def __bytes__(self): + # type: () -> bytes return self.build() def __div__(self, other): + # type: (Any) -> Self if isinstance(other, Packet): cloneA = self.copy() cloneB = other.copy() cloneA.add_payload(cloneB) return cloneA - elif isinstance(other, (bytes, str)): - return self / conf.raw_layer(load=other) + elif isinstance(other, (bytes, str, bytearray, memoryview)): + return self / conf.raw_layer(load=bytes_encode(other)) else: - return other.__rdiv__(self) + return other.__rdiv__(self) # type: ignore __truediv__ = __div__ def __rdiv__(self, other): - if isinstance(other, (bytes, str)): - return conf.raw_layer(load=other) / self + # type: (Any) -> Packet + if isinstance(other, (bytes, str, bytearray, memoryview)): + return conf.raw_layer(load=bytes_encode(other)) / self else: raise TypeError __rtruediv__ = __rdiv__ def __mul__(self, other): + # type: (Any) -> List[Packet] if isinstance(other, int): return [self] * other else: raise TypeError def __rmul__(self, other): + # type: (Any) -> List[Packet] return self.__mul__(other) def __nonzero__(self): + # type: () -> bool return True __bool__ = __nonzero__ def __len__(self): + # type: () -> int return len(self.__bytes__()) def copy_field_value(self, fieldname, value): + # type: (str, Any) -> Any return self.get_field(fieldname).do_copy(value) def copy_fields_dict(self, fields): + # type: (_T) -> _T if fields is None: return None return {fname: self.copy_field_value(fname, fval) - for fname, fval in six.iteritems(fields)} + for fname, fval in fields.items()} + + def _raw_packet_cache_field_value(self, fld, val, copy=False): + # type: (AnyField, Any, bool) -> Optional[Any] + """Get a value representative of a mutable field to detect changes""" + _cpy = lambda x: fld.do_copy(x) if copy else x # type: Callable[[Any], Any] + if fld.holds_packets: + # avoid copying whole packets (perf: #GH3894) + if fld.islist: + return [ + (_cpy(x.fields), x.payload.raw_packet_cache) for x in val + ] + else: + return (_cpy(val.fields), val.payload.raw_packet_cache) + elif fld.islist or fld.ismutable: + return _cpy(val) + return None def clear_cache(self): + # type: () -> None """Clear the raw packet cache for the field and all its subfields""" self.raw_packet_cache = None - for fld, fval in six.iteritems(self.fields): - fld = self.get_field(fld) + for fname, fval in self.fields.items(): + fld = self.get_field(fname) if fld.holds_packets: if isinstance(fval, Packet): fval.clear_cache() @@ -548,15 +737,16 @@ def clear_cache(self): fsubval.clear_cache() self.payload.clear_cache() - def self_build(self, field_pos_list=None): + def self_build(self): + # type: () -> bytes """ Create the default layer regarding fields_desc dict - - :param field_pos_list: """ - if self.raw_packet_cache is not None: - for fname, fval in six.iteritems(self.raw_packet_cache_fields): - if self.getfieldval(fname) != fval: + if self.raw_packet_cache is not None and \ + self.raw_packet_cache_fields is not None: + for fname, fval in self.raw_packet_cache_fields.items(): + fld, val = self.getfield_and_val(fname) + if self._raw_packet_cache_field_value(fld, val) != fval: self.raw_packet_cache = None self.raw_packet_cache_fields = None self.wirelen = None @@ -567,15 +757,23 @@ def self_build(self, field_pos_list=None): for f in self.fields_desc: val = self.getfieldval(f.name) if isinstance(val, RawVal): - sval = raw(val) - p += sval - if field_pos_list is not None: - field_pos_list.append((f.name, sval.encode("string_escape"), len(p), len(sval))) # noqa: E501 + p += bytes(val) else: - p = f.addfield(self, p, val) + try: + p = f.addfield(self, p, val) + except Exception as ex: + try: + ex.args = ( + "While building field '%s': " % f.name + + ex.args[0], + ) + ex.args[1:] + except (AttributeError, IndexError): + pass + raise ex return p def do_build_payload(self): + # type: () -> bytes """ Create the default version of the payload layer @@ -584,6 +782,7 @@ def do_build_payload(self): return self.payload.do_build() def do_build(self): + # type: () -> bytes """ Create the default version of the layer @@ -601,9 +800,11 @@ def do_build(self): return pkt + pay def build_padding(self): + # type: () -> bytes return self.payload.build_padding() def build(self): + # type: () -> bytes """ Create the current layer @@ -615,19 +816,22 @@ def build(self): return p def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes """ DEV: called right after the current layer is build. - :param str pkt: the current packet (build by self_buil function) + :param str pkt: the current packet (build by self_build function) :param str pay: the packet payload (build by do_build_payload function) :return: a string of the packet with the payload """ return pkt + pay def build_done(self, p): + # type: (bytes) -> bytes return self.payload.build_done(p) def do_build_ps(self): + # type: () -> Tuple[bytes, List[Tuple[Packet, List[Tuple[Field[Any, Any], str, bytes]]]]] # noqa: E501 p = b"" pl = [] q = b"" @@ -649,6 +853,7 @@ def do_build_ps(self): return p, lst def build_ps(self, internal=0): + # type: (int) -> Tuple[bytes, List[Tuple[Packet, List[Tuple[Any, Any, bytes]]]]] # noqa: E501 p, lst = self.do_build_ps() # if not internal: # pkt = self @@ -660,6 +865,7 @@ def build_ps(self, internal=0): return p, lst def canvas_dump(self, layer_shift=0, rebuild=1): + # type: (int, int) -> pyx.canvas.canvas if PYX == 0: raise ImportError("PyX and its dependencies must be installed") canvas = pyx.canvas.canvas() @@ -667,10 +873,10 @@ def canvas_dump(self, layer_shift=0, rebuild=1): _, t = self.__class__(raw(self)).build_ps() else: _, t = self.build_ps() - YTXT = len(t) + YTXTI = len(t) for _, l in t: - YTXT += len(l) - YTXT = float(YTXT) + YTXTI += len(l) + YTXT = float(YTXTI) YDUMP = YTXT XSTART = 1 @@ -685,15 +891,27 @@ def canvas_dump(self, layer_shift=0, rebuild=1): # backcolor=makecol(0.376, 0.729, 0.525, 1.0) def hexstr(x): + # type: (bytes) -> str return " ".join("%02x" % orb(c) for c in x) def make_dump_txt(x, y, txt): - return pyx.text.text(XDSTART + x * XMUL, (YDUMP - y) * YMUL, r"\tt{%s}" % hexstr(txt), [pyx.text.size.Large]) # noqa: E501 + # type: (int, float, bytes) -> pyx.text.text + return pyx.text.text( + XDSTART + x * XMUL, + (YDUMP - y) * YMUL, + r"\tt{%s}" % hexstr(txt), + [pyx.text.size.Large] + ) def make_box(o): - return pyx.box.rect(o.left(), o.bottom(), o.width(), o.height(), relcenter=(0.5, 0.5)) # noqa: E501 + # type: (pyx.bbox.bbox) -> pyx.bbox.bbox + return pyx.box.rect( + o.left(), o.bottom(), o.width(), o.height(), + relcenter=(0.5, 0.5) + ) def make_frame(lst): + # type: (List[Any]) -> pyx.path.path if len(lst) == 1: b = lst[0].bbox() b.enlarge(pyx.unit.u_pt) @@ -730,7 +948,14 @@ def make_frame(lst): pyx.path.lineto(fb.left(), gb.top()), pyx.path.closepath(),) - def make_dump(s, shift=0, y=0, col=None, bkcol=None, large=16): + def make_dump(s, # type: bytes + shift=0, # type: int + y=0., # type: float + col=None, # type: pyx.color.color + bkcol=None, # type: pyx.color.color + large=16 # type: int + ): + # type: (...) -> Tuple[pyx.canvas.canvas, pyx.bbox.bbox, int, float] # noqa: E501 c = pyx.canvas.canvas() tlist = [] while s: @@ -755,7 +980,14 @@ def make_dump(s, shift=0, y=0, col=None, bkcol=None, large=16): bkcol = next(backcolor) proto, fields = t.pop() y += 0.5 - pt = pyx.text.text(XSTART, (YTXT - y) * YMUL, r"\font\cmssfont=cmss10\cmssfont{%s}" % tex_escape(proto.name), [pyx.text.size.Large]) # noqa: E501 + pt = pyx.text.text( + XSTART, + (YTXT - y) * YMUL, + r"\font\cmssfont=cmss10\cmssfont{%s}" % tex_escape( + str(proto.name) + ), + [pyx.text.size.Large] + ) y += 1 ptbb = pt.bbox() ptbb.enlarge(pyx.unit.u_pt * 2) @@ -808,6 +1040,7 @@ def make_dump(s, shift=0, y=0, col=None, bkcol=None, large=16): return canvas def extract_padding(self, s): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] """ DEV: to be overloaded to extract current layer's padding. @@ -817,53 +1050,80 @@ def extract_padding(self, s): return s, None def post_dissect(self, s): + # type: (bytes) -> bytes """DEV: is called right after the current layer has been dissected""" return s def pre_dissect(self, s): + # type: (bytes) -> bytes """DEV: is called right before the current layer is dissected""" return s def do_dissect(self, s): + # type: (bytes) -> bytes _raw = s self.raw_packet_cache_fields = {} for f in self.fields_desc: - if not s: - break s, fval = f.getfield(self, s) + # Skip unused ConditionalField + if isinstance(f, ConditionalField) and fval is None: + continue # We need to track fields with mutable values to discard # .raw_packet_cache when needed. - if f.islist or f.holds_packets or f.ismutable: - self.raw_packet_cache_fields[f.name] = f.do_copy(fval) + if (f.islist or f.holds_packets or f.ismutable) and fval is not None: + self.raw_packet_cache_fields[f.name] = \ + self._raw_packet_cache_field_value(f, fval, copy=True) self.fields[f.name] = fval + # Nothing left to dissect + if not s and (isinstance(f, MayEnd) or + (fval is not None and isinstance(f, ConditionalField) and + isinstance(f.fld, MayEnd))): + break self.raw_packet_cache = _raw[:-len(s)] if s else _raw self.explicit = 1 return s def do_dissect_payload(self, s): + # type: (bytes) -> None """ Perform the dissection of the layer's payload :param str s: the raw layer """ if s: + if ( + self.stop_dissection_after and + isinstance(self, self.stop_dissection_after) + ): + # stop dissection here + p = conf.raw_layer(s, _internal=1, _underlayer=self) + self.add_payload(p) + return cls = self.guess_payload_class(s) try: - p = cls(s, _internal=1, _underlayer=self) + p = cls( + s, + stop_dissection_after=self.stop_dissection_after, + _internal=1, + _underlayer=self, + ) except KeyboardInterrupt: raise except Exception: if conf.debug_dissector: if issubtype(cls, Packet): - log_runtime.error("%s dissector failed" % cls.__name__) + log_runtime.error("%s dissector failed", cls.__name__) else: - log_runtime.error("%s.guess_payload_class() returned [%s]" % (self.__class__.__name__, repr(cls))) # noqa: E501 + log_runtime.error("%s.guess_payload_class() returned " + "[%s]", + self.__class__.__name__, repr(cls)) if cls is not None: raise p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) def dissect(self, s): + # type: (bytes) -> None s = self.pre_dissect(s) s = self.do_dissect(s) @@ -876,6 +1136,7 @@ def dissect(self, s): self.add_payload(conf.padding_layer(pad)) def guess_payload_class(self, payload): + # type: (bytes) -> Type[Packet] """ DEV: Guesses the next payload class from layer bonds. Can be overloaded to use a different mechanism. @@ -887,13 +1148,14 @@ def guess_payload_class(self, payload): for fval, cls in t.payload_guess: try: if all(v == self.getfieldval(k) - for k, v in six.iteritems(fval)): - return cls + for k, v in fval.items()): + return cls # type: ignore except AttributeError: pass return self.default_payload_class(payload) def default_payload_class(self, payload): + # type: (bytes) -> Type[Packet] """ DEV: Returns the default payload class if nothing has been found by the guess_payload_class() method. @@ -904,20 +1166,18 @@ def default_payload_class(self, payload): return conf.raw_layer def hide_defaults(self): + # type: () -> None """Removes fields' values that are the same as default values.""" # use list(): self.fields is modified in the loop - for k, v in list(six.iteritems(self.fields)): + for k, v in list(self.fields.items()): v = self.fields[k] if k in self.default_fields: if self.default_fields[k] == v: del self.fields[k] self.payload.hide_defaults() - def update_sent_time(self, time): - """Use by clone_with to share the sent_time value""" - pass - - def clone_with(self, payload=None, share_time=False, **kargs): + def clone_with(self, payload=None, **kargs): + # type: (Optional[Any], **Any) -> Any pkt = self.__class__() pkt.explicit = 1 pkt.fields = kargs @@ -925,25 +1185,25 @@ def clone_with(self, payload=None, share_time=False, **kargs): pkt.overloaded_fields = self.overloaded_fields.copy() pkt.time = self.time pkt.underlayer = self.underlayer + pkt.parent = self.parent pkt.post_transforms = self.post_transforms pkt.raw_packet_cache = self.raw_packet_cache pkt.raw_packet_cache_fields = self.copy_fields_dict( self.raw_packet_cache_fields ) pkt.wirelen = self.wirelen + pkt.comments = self.comments + pkt.sniffed_on = self.sniffed_on + pkt.direction = self.direction if payload is not None: pkt.add_payload(payload) - if share_time: - # This binds the subpacket .sent_time to this layer - def _up_time(x, parent=self): - parent.sent_time = x - pkt.update_sent_time = _up_time return pkt def __iter__(self): + # type: () -> Iterator[Packet] """Iterates through all sub-packets generated by this Packet.""" - # We use __iterlen__ as low as possible, to lower processing time def loop(todo, done, self=self): + # type: (List[str], Dict[str, Any], Any) -> Iterator[Packet] if todo: eltname = todo.pop() elt = self.getfieldval(eltname) @@ -958,70 +1218,31 @@ def loop(todo, done, self=self): yield x else: if isinstance(self.payload, NoPayload): - payloads = SetGen([None]) + payloads = SetGen([None]) # type: SetGen[Packet] else: payloads = self.payload - share_time = False - if self.fields == done and payloads.__iterlen__() == 1: - # In this case, the packets are identical. Let's bind - # their sent_time attribute for sending purpose - share_time = True for payl in payloads: # Let's make sure subpackets are consistent done2 = done.copy() for k in done2: if isinstance(done2[k], VolatileValue): done2[k] = done2[k]._fix() - pkt = self.clone_with(payload=payl, share_time=share_time, - **done2) + pkt = self.clone_with(payload=payl, **done2) yield pkt if self.explicit or self.raw_packet_cache is not None: todo = [] done = self.fields else: - todo = [k for (k, v) in itertools.chain(six.iteritems(self.default_fields), # noqa: E501 - six.iteritems(self.overloaded_fields)) # noqa: E501 + todo = [k for (k, v) in itertools.chain(self.default_fields.items(), + self.overloaded_fields.items()) if isinstance(v, VolatileValue)] + list(self.fields) done = {} return loop(todo, done) - def __iterlen__(self): - """Predict the total length of the iterator""" - fields = [key for (key, val) in itertools.chain(six.iteritems(self.default_fields), # noqa: E501 - six.iteritems(self.overloaded_fields)) - if isinstance(val, VolatileValue)] + list(self.fields) - length = 1 - - def is_valid_gen_tuple(x): - if not isinstance(x, tuple): - return False - return len(x) == 2 and all(isinstance(z, int) for z in x) - - for field in fields: - fld, val = self.getfield_and_val(field) - if hasattr(val, "__iterlen__"): - length *= val.__iterlen__() - elif is_valid_gen_tuple(val): - length *= (val[1] - val[0] + 1) - elif isinstance(val, list) and not fld.islist: - len2 = 0 - for x in val: - if hasattr(x, "__iterlen__"): - len2 += x.__iterlen__() - elif is_valid_gen_tuple(x): - len2 += (x[1] - x[0] + 1) - elif isinstance(x, list): - len2 += len(x) - else: - len2 += 1 - length *= len2 or 1 - if not isinstance(self.payload, NoPayload): - return length * self.payload.__iterlen__() - return length - def iterpayloads(self): - """Used to iter through the paylods of a Packet. + # type: () -> Iterator[Packet] + """Used to iter through the payloads of a Packet. Useful for DNS or 802.11 for instance. """ yield self @@ -1031,6 +1252,7 @@ def iterpayloads(self): yield current def __gt__(self, other): + # type: (Packet) -> int """True if other is an answer from self (self ==> other).""" if isinstance(other, Packet): return other < self @@ -1040,6 +1262,7 @@ def __gt__(self, other): raise TypeError((self, other)) def __lt__(self, other): + # type: (Packet) -> int """True if self is an answer from other (other ==> self).""" if isinstance(other, Packet): return self.answers(other) @@ -1049,6 +1272,7 @@ def __lt__(self, other): raise TypeError((self, other)) def __eq__(self, other): + # type: (Any) -> bool if not isinstance(other, self.__class__): return False for f in self.fields_desc: @@ -1059,34 +1283,50 @@ def __eq__(self, other): return self.payload == other.payload def __ne__(self, other): + # type: (Any) -> bool return not self.__eq__(other) - __hash__ = None + # Note: setting __hash__ to None is the standard way + # of making an object un-hashable. mypy doesn't know that + __hash__ = None # type: ignore def hashret(self): + # type: () -> bytes """DEV: returns a string that has the same value for a request and its answer.""" return self.payload.hashret() def answers(self, other): + # type: (Packet) -> int """DEV: true if self is an answer from other""" if other.__class__ == self.__class__: return self.payload.answers(other.payload) return 0 def layers(self): + # type: () -> List[Type[Packet]] """returns a list of layer classes (including subclasses) in this packet""" # noqa: E501 layers = [] - lyr = self + lyr = self # type: Optional[Packet] while lyr: layers.append(lyr.__class__) lyr = lyr.payload.getlayer(0, _subclass=True) return layers - def haslayer(self, cls): - """true if self has a layer that is an instance of cls. Superseded by "cls in self" syntax.""" # noqa: E501 - if self.__class__ == cls or cls in [self.__class__.__name__, - self._name]: + def haslayer(self, cls, _subclass=None): + # type: (Union[Type[Packet], str], Optional[bool]) -> int + """ + true if self has a layer that is an instance of cls. + Superseded by "cls in self" syntax. + """ + if _subclass is None: + _subclass = self.match_subclass or None + if _subclass: + match = issubtype + else: + match = lambda x, t: bool(x == t) + if cls is None or match(self.__class__, cls) \ + or cls in [self.__class__.__name__, self._name]: return True for f in self.packetfields: fvalue_gen = self.getfieldval(f.name) @@ -1096,37 +1336,52 @@ def haslayer(self, cls): fvalue_gen = SetGen(fvalue_gen, _iterpacket=0) for fvalue in fvalue_gen: if isinstance(fvalue, Packet): - ret = fvalue.haslayer(cls) + ret = fvalue.haslayer(cls, _subclass=_subclass) if ret: return ret - return self.payload.haslayer(cls) - - def getlayer(self, cls, nb=1, _track=None, _subclass=None, **flt): + return self.payload.haslayer(cls, _subclass=_subclass) + + def getlayer(self, + cls, # type: Union[int, Type[Packet], str] + nb=1, # type: int + _track=None, # type: Optional[List[int]] + _subclass=None, # type: Optional[bool] + **flt # type: Any + ): + # type: (...) -> Optional[Packet] """Return the nb^th layer that is an instance of cls, matching flt values. """ if _subclass is None: _subclass = self.match_subclass or None if _subclass: - match = lambda cls1, cls2: issubclass(cls1, cls2) + match = issubtype else: - match = lambda cls1, cls2: cls1 == cls2 + match = lambda x, t: bool(x == t) + # Note: + # cls can be int, packet, str + # string_class_name can be packet, str (packet or packet+field) + # class_name can be packet, str (packet only) if isinstance(cls, int): nb = cls + 1 - cls = None - if isinstance(cls, str) and "." in cls: - ccls, fld = cls.split(".", 1) + string_class_name = "" # type: Union[Type[Packet], str] else: - ccls, fld = cls, None - if cls is None or match(self.__class__, cls) \ - or ccls in [self.__class__.__name__, self._name]: + string_class_name = cls + class_name = "" # type: Union[Type[Packet], str] + fld = None # type: Optional[str] + if isinstance(string_class_name, str) and "." in string_class_name: + class_name, fld = string_class_name.split(".", 1) + else: + class_name, fld = string_class_name, None + if not class_name or match(self.__class__, class_name) \ + or class_name in [self.__class__.__name__, self._name]: if all(self.getfieldval(fldname) == fldvalue - for fldname, fldvalue in six.iteritems(flt)): + for fldname, fldvalue in flt.items()): if nb == 1: if fld is None: return self else: - return self.getfieldval(fld) + return self.getfieldval(fld) # type: ignore else: nb -= 1 for f in self.packetfields: @@ -1137,22 +1392,24 @@ def getlayer(self, cls, nb=1, _track=None, _subclass=None, **flt): fvalue_gen = SetGen(fvalue_gen, _iterpacket=0) for fvalue in fvalue_gen: if isinstance(fvalue, Packet): - track = [] - ret = fvalue.getlayer(cls, nb=nb, _track=track, + track = [] # type: List[int] + ret = fvalue.getlayer(class_name, nb=nb, _track=track, _subclass=_subclass, **flt) if ret is not None: return ret nb = track[0] - return self.payload.getlayer(cls, nb=nb, _track=_track, + return self.payload.getlayer(class_name, nb=nb, _track=_track, _subclass=_subclass, **flt) def firstlayer(self): + # type: () -> Packet q = self while q.underlayer is not None: q = q.underlayer return q def __getitem__(self, cls): + # type: (Union[Type[Packet], str]) -> Any if isinstance(cls, slice): lname = cls.start if cls.stop: @@ -1163,34 +1420,52 @@ def __getitem__(self, cls): lname = cls ret = self.getlayer(cls) if ret is None: - if isinstance(lname, Packet_metaclass): - lname = lname.__name__ + if isinstance(lname, type): + name = lname.__name__ elif not isinstance(lname, bytes): - lname = repr(lname) - raise IndexError("Layer [%s] not found" % lname) + name = repr(lname) + else: + name = cast(str, lname) + raise IndexError("Layer [%s] not found" % name) return ret def __delitem__(self, cls): - del(self[cls].underlayer.payload) + # type: (Type[Packet]) -> None + del self[cls].underlayer.payload def __setitem__(self, cls, val): + # type: (Type[Packet], Packet) -> None self[cls].underlayer.payload = val def __contains__(self, cls): - """"cls in self" returns true if self has a layer which is an instance of cls.""" # noqa: E501 + # type: (Union[Type[Packet], str]) -> int + """ + "cls in self" returns true if self has a layer which is an + instance of cls. + """ return self.haslayer(cls) def route(self): + # type: () -> Tuple[Optional[str], Optional[str], Optional[str]] return self.payload.route() def fragment(self, *args, **kargs): + # type: (*Any, **Any) -> List[Packet] return self.payload.fragment(*args, **kargs) def display(self, *args, **kargs): # Deprecated. Use show() + # type: (*Any, **Any) -> None """Deprecated. Use show() method.""" self.show(*args, **kargs) - def _show_or_dump(self, dump=False, indent=3, lvl="", label_lvl="", first_call=True): # noqa: E501 + def _show_or_dump(self, + dump=False, # type: bool + indent=3, # type: int + lvl="", # type: str + label_lvl="", # type: str + first_call=True # type: bool + ): + # type: (...) -> Optional[str] """ Internal method that shows or dumps a hierarchical view of a packet. Called by show. @@ -1204,33 +1479,53 @@ def _show_or_dump(self, dump=False, indent=3, lvl="", label_lvl="", first_call=T """ if dump: - from scapy.themes import AnsiColorTheme - ct = AnsiColorTheme() # No color for dump output + from scapy.themes import ColorTheme, AnsiColorTheme + ct: ColorTheme = AnsiColorTheme() # No color for dump output else: ct = conf.color_theme - s = "%s%s %s %s \n" % (label_lvl, - ct.punct("###["), - ct.layer_name(self.name), - ct.punct("]###")) - for f in self.fields_desc: + s = "%s%s %s %s\n" % (label_lvl, + ct.punct("###["), + ct.layer_name(self.name), + ct.punct("]###")) + fields = self.fields_desc.copy() + while fields: + f = fields.pop(0) if isinstance(f, ConditionalField) and not f._evalcond(self): continue + if hasattr(f, "fields"): # Field has subfields + s += "%s %s =\n" % ( + label_lvl + lvl, + ct.depreciate_field_name(f.name), + ) + lvl += " " * indent * self.show_indent + for i, fld in enumerate(x for x in f.fields if hasattr(self, x.name)): + fields.insert(i, fld) + continue if isinstance(f, Emph) or f in conf.emph: ncol = ct.emph_field_name vcol = ct.emph_field_value else: ncol = ct.field_name vcol = ct.field_value + pad = max(0, 10 - len(f.name)) * " " fvalue = self.getfieldval(f.name) if isinstance(fvalue, Packet) or (f.islist and f.holds_packets and isinstance(fvalue, list)): # noqa: E501 - s += "%s \\%-10s\\\n" % (label_lvl + lvl, ncol(f.name)) - fvalue_gen = SetGen(fvalue, _iterpacket=0) + s += "%s %s%s%s%s\n" % (label_lvl + lvl, + ct.punct("\\"), + ncol(f.name), + pad, + ct.punct("\\")) + fvalue_gen = SetGen( + fvalue, + _iterpacket=0 + ) # type: SetGen[Packet] for fvalue in fvalue_gen: s += fvalue._show_or_dump(dump=dump, indent=indent, label_lvl=label_lvl + lvl + " |", first_call=False) # noqa: E501 else: - begn = "%s %-10s%s " % (label_lvl + lvl, - ncol(f.name), - ct.punct("="),) + begn = "%s %s%s%s " % (label_lvl + lvl, + ncol(f.name), + pad, + ct.punct("="),) reprval = f.i2repr(self, fvalue) if isinstance(reprval, str): reprval = reprval.replace("\n", "\n" + " " * (len(label_lvl) + # noqa: E501 @@ -1239,14 +1534,22 @@ def _show_or_dump(self, dump=False, indent=3, lvl="", label_lvl="", first_call=T 4)) s += "%s%s\n" % (begn, vcol(reprval)) if self.payload: - s += self.payload._show_or_dump(dump=dump, indent=indent, lvl=lvl + (" " * indent * self.show_indent), label_lvl=label_lvl, first_call=False) # noqa: E501 + s += self.payload._show_or_dump( # type: ignore + dump=dump, + indent=indent, + lvl=lvl + (" " * indent * self.show_indent), + label_lvl=label_lvl, + first_call=False + ) if first_call and not dump: print(s) + return None else: return s def show(self, dump=False, indent=3, lvl="", label_lvl=""): + # type: (bool, int, str, str) -> Optional[Any] """ Prints or returns (when "dump" is true) a hierarchical view of the packet. @@ -1260,6 +1563,7 @@ def show(self, dump=False, indent=3, lvl="", label_lvl=""): return self._show_or_dump(dump, indent, lvl, label_lvl) def show2(self, dump=False, indent=3, lvl="", label_lvl=""): + # type: (bool, int, str, str) -> Optional[Any] """ Prints or returns (when "dump" is true) a hierarchical view of an assembled version of the packet, so that automatic fields are @@ -1274,6 +1578,7 @@ def show2(self, dump=False, indent=3, lvl="", label_lvl=""): return self.__class__(raw(self)).show(dump, indent, lvl, label_lvl) def sprintf(self, fmt, relax=1): + # type: (str, int) -> str """ sprintf(format, [relax=1]) -> str @@ -1358,27 +1663,33 @@ def sprintf(self, fmt, relax=1): fld = clsfld num = 1 if ":" in cls: - cls, num = cls.split(":") - num = int(num) + cls, snum = cls.split(":") + num = int(snum) fmt = fmt[i + 1:] except Exception: raise Scapy_Exception("Bad format string [%%%s%s]" % (fmt[:25], fmt[25:] and "...")) # noqa: E501 else: if fld == "time": - val = time.strftime("%H:%M:%S.%%06i", time.localtime(self.time)) % int((self.time - int(self.time)) * 1000000) # noqa: E501 + val = time.strftime( + "%H:%M:%S.%%06i", + time.localtime(float(self.time)) + ) % int((self.time - int(self.time)) * 1000000) elif cls == self.__class__.__name__ and hasattr(self, fld): if num > 1: val = self.payload.sprintf("%%%s,%s:%s.%s%%" % (f, cls, num - 1, fld), relax) # noqa: E501 f = "s" - elif f[-1] == "r": # Raw field value - val = getattr(self, fld) - f = f[:-1] - if not f: - f = "s" else: - val = getattr(self, fld) - if fld in self.fieldtype: - val = self.fieldtype[fld].i2repr(self, val) + try: + val = self.getfieldval(fld) + except AttributeError: + val = getattr(self, fld) + if f[-1] == "r": # Raw field value + f = f[:-1] + if not f: + f = "s" + else: + if fld in self.fieldtype: + val = self.fieldtype[fld].i2repr(self, val) else: val = self.payload.sprintf("%%%s%%" % sfclsfld, relax) f = "s" @@ -1388,6 +1699,7 @@ def sprintf(self, fmt, relax=1): return s def mysummary(self): + # type: () -> str """DEV: can be overloaded to return a string that summarizes the layer. Only one mysummary() is used in a whole packet summary: the one of the upper layer, # noqa: E501 except if a mysummary() also returns (as a couple) a list of layers whose # noqa: E501 @@ -1395,6 +1707,7 @@ def mysummary(self): return "" def _do_summary(self): + # type: () -> Tuple[int, str, List[Any]] found, s, needed = self.payload._do_summary() ret = "" if not found or self.__class__ in needed: @@ -1419,14 +1732,17 @@ def _do_summary(self): return found, ret, needed def summary(self, intern=0): + # type: (int) -> str """Prints a one line summary of a packet.""" return self._do_summary()[1] def lastlayer(self, layer=None): + # type: (Optional[Packet]) -> Packet """Returns the uppest layer of the packet""" return self.payload.lastlayer(self) def decode_payload_as(self, cls): + # type: (Type[Packet]) -> None """Reassembles the payload and decode it using another packet class""" s = raw(self.payload) self.payload = cls(s, _internal=1, _underlayer=self) @@ -1435,211 +1751,268 @@ def decode_payload_as(self, cls): pp = pp.underlayer self.payload.dissection_done(pp) - def command(self): + def _command(self, json=False): + # type: (bool) -> List[Tuple[str, Any]] """ - Returns a string representing the command you have to type to - obtain the same packet + Internal method used to generate command() and json() """ f = [] - for fn, fv in six.iteritems(self.fields): + iterator: Iterator[Tuple[str, Any]] + if json: + iterator = ((x.name, self.getfieldval(x.name)) for x in self.fields_desc) + else: + iterator = iter(self.fields.items()) + for fn, fv in iterator: fld = self.get_field(fn) - if isinstance(fv, (list, dict, set)) and len(fv) == 0: + if isinstance(fv, (list, dict, set)) and not fv and not fld.default: continue if isinstance(fv, Packet): - fv = fv.command() + if json: + fv = {k: v for (k, v) in fv._command(json=True)} + else: + fv = fv.command() elif fld.islist and fld.holds_packets and isinstance(fv, list): - fv = "[%s]" % ",".join(map(Packet.command, fv)) - elif isinstance(fld, FlagsField): + if json: + fv = [ + {k: v for (k, v) in x} + for x in map(lambda y: Packet._command(y, json=True), fv) + ] + else: + fv = "[%s]" % ",".join(map(Packet.command, fv)) + elif fld.islist and isinstance(fv, list): + if json: + fv = [ + getattr(x, 'command', lambda: repr(x))() + for x in fv + ] + else: + fv = "[%s]" % ",".join( + getattr(x, 'command', lambda: repr(x))() + for x in fv + ) + elif isinstance(fv, FlagValue): fv = int(fv) + elif callable(getattr(fv, 'command', None)): + fv = fv.command(json=json) else: - fv = repr(fv) - f.append("%s=%s" % (fn, fv)) - c = "%s(%s)" % (self.__class__.__name__, ", ".join(f)) + if json: + if isinstance(fv, bytes): + fv = fv.decode("utf-8", errors="backslashreplace") + else: + fv = fld.i2h(self, fv) + else: + fv = repr(fld.i2h(self, fv)) + f.append((fn, fv)) + return f + + def command(self): + # type: () -> str + """ + Returns a string representing the command you have to type to + obtain the same packet + """ + c = "%s(%s)" % ( + self.__class__.__name__, + ", ".join("%s=%s" % x for x in self._command()) + ) pc = self.payload.command() if pc: c += "/" + pc return c - def convert_to(self, other_cls, **kwargs): - """Converts this Packet to another type. - - This is not guaranteed to be a lossless process. - - By default, this only implements conversion to ``Raw``. - - :param other_cls: Reference to a Packet class to convert to. - :type other_cls: Type[scapy.packet.Packet] - :return: Converted form of the packet. - :rtype: other_cls - :raises TypeError: When conversion is not possible - """ - if not issubtype(other_cls, Packet): - raise TypeError("{} must implement Packet".format(other_cls)) - - if other_cls is Raw: - return Raw(raw(self)) - - if "_internal" not in kwargs: - return other_cls.convert_packet(self, _internal=True, **kwargs) - - raise TypeError("Cannot convert {} to {}".format( - type(self).__name__, other_cls.__name__)) - - @classmethod - def convert_packet(cls, pkt, **kwargs): - """Converts another packet to be this type. - - This is not guaranteed to be a lossless process. - - :param pkt: The packet to convert. - :type pkt: scapy.packet.Packet - :return: Converted form of the packet. - :rtype: cls - :raises TypeError: When conversion is not possible + def json(self): + # type: () -> str """ - if not isinstance(pkt, Packet): - raise TypeError("Can only convert Packets") - - if "_internal" not in kwargs: - return pkt.convert_to(cls, _internal=True, **kwargs) - - raise TypeError("Cannot convert {} to {}".format( - type(pkt).__name__, cls.__name__)) - - @classmethod - def convert_packets(cls, pkts, **kwargs): - """Converts many packets to this type. - - This is implemented as a generator. + Returns a JSON representing the packet. - See ``Packet.convert_packet``. + Please note that this cannot be used for bijective usage: data loss WILL occur, + so it will not make sense to try to rebuild the packet from the output. + This must only be used for a grepping/displaying purpose. """ - for pkt in pkts: - yield cls.convert_packet(pkt, **kwargs) + dump = json.dumps({k: v for (k, v) in self._command(json=True)}) + pc = self.payload.json() + if pc: + dump = dump[:-1] + ", \"payload\": %s}" % pc + return dump class NoPayload(Packet): def __new__(cls, *args, **kargs): + # type: (Type[Packet], *Any, **Any) -> NoPayload singl = cls.__dict__.get("__singl__") if singl is None: cls.__singl__ = singl = Packet.__new__(cls) Packet.__init__(singl) - return singl + return cast(NoPayload, singl) def __init__(self, *args, **kargs): + # type: (*Any, **Any) -> None pass def dissection_done(self, pkt): - return + # type: (Packet) -> None + pass def add_payload(self, payload): + # type: (Union[Packet, bytes]) -> NoReturn raise Scapy_Exception("Can't add payload to NoPayload instance") def remove_payload(self): + # type: () -> None pass def add_underlayer(self, underlayer): + # type: (Any) -> None pass def remove_underlayer(self, other): + # type: (Packet) -> None + pass + + def add_parent(self, parent): + # type: (Any) -> None + pass + + def remove_parent(self, other): + # type: (Packet) -> None pass def copy(self): + # type: () -> NoPayload return self def clear_cache(self): + # type: () -> None pass def __repr__(self): + # type: () -> str return "" def __str__(self): + # type: () -> str return "" def __bytes__(self): + # type: () -> bytes return b"" def __nonzero__(self): + # type: () -> bool return False __bool__ = __nonzero__ def do_build(self): + # type: () -> bytes return b"" def build(self): + # type: () -> bytes return b"" def build_padding(self): + # type: () -> bytes return b"" def build_done(self, p): + # type: (bytes) -> bytes return p def build_ps(self, internal=0): + # type: (int) -> Tuple[bytes, List[Any]] return b"", [] def getfieldval(self, attr): + # type: (str) -> NoReturn raise AttributeError(attr) def getfield_and_val(self, attr): + # type: (str) -> NoReturn raise AttributeError(attr) def setfieldval(self, attr, val): + # type: (str, Any) -> NoReturn raise AttributeError(attr) def delfieldval(self, attr): + # type: (str) -> NoReturn raise AttributeError(attr) def hide_defaults(self): + # type: () -> None pass def __iter__(self): + # type: () -> Iterator[Packet] return iter([]) def __eq__(self, other): + # type: (Any) -> bool if isinstance(other, NoPayload): return True return False def hashret(self): + # type: () -> bytes return b"" def answers(self, other): - return isinstance(other, NoPayload) or isinstance(other, conf.padding_layer) # noqa: E501 + # type: (Packet) -> bool + return isinstance(other, (NoPayload, conf.padding_layer)) # noqa: E501 - def haslayer(self, cls): + def haslayer(self, cls, _subclass=None): + # type: (Union[Type[Packet], str], Optional[bool]) -> int return 0 - def getlayer(self, cls, nb=1, _track=None, **flt): + def getlayer(self, + cls, # type: Union[int, Type[Packet], str] + nb=1, # type: int + _track=None, # type: Optional[List[int]] + _subclass=None, # type: Optional[bool] + **flt # type: Any + ): + # type: (...) -> Optional[Packet] if _track is not None: _track.append(nb) return None def fragment(self, *args, **kargs): + # type: (*Any, **Any) -> List[Packet] raise Scapy_Exception("cannot fragment this packet") - def show(self, indent=3, lvl="", label_lvl=""): + def show(self, dump=False, indent=3, lvl="", label_lvl=""): + # type: (bool, int, str, str) -> None pass - def sprintf(self, fmt, relax): + def sprintf(self, fmt, relax=1): + # type: (str, int) -> str if relax: return "??" else: raise Scapy_Exception("Format not found [%s]" % fmt) def _do_summary(self): + # type: () -> Tuple[int, str, List[Any]] return 0, "", [] def layers(self): + # type: () -> List[Type[Packet]] return [] - def lastlayer(self, layer): - return layer + def lastlayer(self, layer=None): + # type: (Optional[Packet]) -> Packet + return layer or self def command(self): + # type: () -> str + return "" + + def json(self): + # type: () -> str return "" def route(self): + # type: () -> Tuple[None, None, None] return (None, None, None) @@ -1650,17 +2023,24 @@ def route(self): class Raw(Packet): name = "Raw" - fields_desc = [StrField("load", "")] + fields_desc = [StrField("load", b"")] - def __init__(self, _pkt=None, *args, **kwargs): + def __init__(self, _pkt=b"", *args, **kwargs): + # type: (bytes, *Any, **Any) -> None if _pkt and not isinstance(_pkt, bytes): - _pkt = bytes_encode(_pkt) + if isinstance(_pkt, tuple): + _pkt, bn = _pkt + _pkt = bytes_encode(_pkt), bn + else: + _pkt = bytes_encode(_pkt) super(Raw, self).__init__(_pkt, *args, **kwargs) def answers(self, other): + # type: (Packet) -> int return 1 def mysummary(self): + # type: () -> str cs = conf.raw_summary if cs: if callable(cs): @@ -1669,20 +2049,20 @@ def mysummary(self): return "Raw %r" % self.load return Packet.mysummary(self) - @classmethod - def convert_packet(cls, pkt, **kwargs): - return Raw(raw(pkt)) - class Padding(Raw): name = "Padding" def self_build(self): + # type: (Optional[Any]) -> bytes return b"" def build_padding(self): - return (raw(self.load) if self.raw_packet_cache is None - else self.raw_packet_cache) + self.payload.build_padding() + # type: () -> bytes + return ( + bytes_encode(self.load) if self.raw_packet_cache is None + else self.raw_packet_cache + ) + self.payload.build_padding() conf.raw_layer = Raw @@ -1695,11 +2075,16 @@ def build_padding(self): ################# -def bind_bottom_up(lower, upper, __fval=None, **fval): - """Bind 2 layers for dissection. +def bind_bottom_up(lower, # type: Type[Packet] + upper, # type: Type[Packet] + __fval=None, # type: Optional[Any] + **fval # type: Any + ): + # type: (...) -> None + r"""Bind 2 layers for dissection. The upper layer will be chosen for dissection on top of the lower layer, if - ALL the passed arguments are validated. If multiple calls are made with the same # noqa: E501 - layers, the last one will be used as default. + ALL the passed arguments are validated. If multiple calls are made with + the same layers, the last one will be used as default. ex: >>> bind_bottom_up(Ether, SNAP, type=0x1234) @@ -1712,10 +2097,15 @@ def bind_bottom_up(lower, upper, __fval=None, **fval): lower.payload_guess.append((fval, upper)) -def bind_top_down(lower, upper, __fval=None, **fval): +def bind_top_down(lower, # type: Type[Packet] + upper, # type: Type[Packet] + __fval=None, # type: Optional[Any] + **fval # type: Any + ): + # type: (...) -> None """Bind 2 layers for building. - When the upper layer is added as a payload of the lower layer, all the arguments # noqa: E501 - will be applied to them. + When the upper layer is added as a payload of the lower layer, all the + arguments will be applied to them. ex: >>> bind_top_down(Ether, SNAP, type=0x1234) @@ -1724,12 +2114,17 @@ def bind_top_down(lower, upper, __fval=None, **fval): """ if __fval is not None: fval.update(__fval) - upper._overload_fields = upper._overload_fields.copy() + upper._overload_fields = upper._overload_fields.copy() # type: ignore upper._overload_fields[lower] = fval @conf.commands.register -def bind_layers(lower, upper, __fval=None, **fval): +def bind_layers(lower, # type: Type[Packet] + upper, # type: Type[Packet] + __fval=None, # type: Optional[Dict[str, int]] + **fval # type: Any + ): + # type: (...) -> None """Bind 2 layers on some specific fields' values. It makes the packet being built and dissected when the arguments @@ -1748,7 +2143,12 @@ def bind_layers(lower, upper, __fval=None, **fval): bind_bottom_up(lower, upper, **fval) -def split_bottom_up(lower, upper, __fval=None, **fval): +def split_bottom_up(lower, # type: Type[Packet] + upper, # type: Type[Packet] + __fval=None, # type: Optional[Any] + **fval # type: Any + ): + # type: (...) -> None """This call un-links an association that was made using bind_bottom_up. Have a look at help(bind_bottom_up) """ @@ -1756,14 +2156,20 @@ def split_bottom_up(lower, upper, __fval=None, **fval): fval.update(__fval) def do_filter(params, cls): + # type: (Dict[str, int], Type[Packet]) -> bool params_is_invalid = any( - k not in params or params[k] != v for k, v in six.iteritems(fval) + k not in params or params[k] != v for k, v in fval.items() ) return cls != upper or params_is_invalid lower.payload_guess = [x for x in lower.payload_guess if do_filter(*x)] -def split_top_down(lower, upper, __fval=None, **fval): +def split_top_down(lower, # type: Type[Packet] + upper, # type: Type[Packet] + __fval=None, # type: Optional[Any] + **fval # type: Any + ): + # type: (...) -> None """This call un-links an association that was made using bind_top_down. Have a look at help(bind_top_down) """ @@ -1771,14 +2177,19 @@ def split_top_down(lower, upper, __fval=None, **fval): fval.update(__fval) if lower in upper._overload_fields: ofval = upper._overload_fields[lower] - if any(k not in ofval or ofval[k] != v for k, v in six.iteritems(fval)): # noqa: E501 + if any(k not in ofval or ofval[k] != v for k, v in fval.items()): return - upper._overload_fields = upper._overload_fields.copy() - del(upper._overload_fields[lower]) + upper._overload_fields = upper._overload_fields.copy() # type: ignore + del upper._overload_fields[lower] @conf.commands.register -def split_layers(lower, upper, __fval=None, **fval): +def split_layers(lower, # type: Type[Packet] + upper, # type: Type[Packet] + __fval=None, # type: Optional[Any] + **fval # type: Any + ): + # type: (...) -> None """Split 2 layers previously bound. This call un-links calls bind_top_down and bind_bottom_up. It is the opposite of # noqa: E501 bind_layers. @@ -1795,6 +2206,7 @@ def split_layers(lower, upper, __fval=None, **fval): @conf.commands.register def explore(layer=None): + # type: (Optional[str]) -> None """Function used to discover the Scapy layers and protocols. It helps to see which packets exists in contrib or layer files. @@ -1829,55 +2241,48 @@ def explore(layer=None): button_dialog from prompt_toolkit.formatted_text import HTML # Check for prompt_toolkit >= 3.0.0 + call_ptk = lambda x: cast(str, x) # type: Callable[[Any], str] if _version_checker(prompt_toolkit, (3, 0)): call_ptk = lambda x: x.run() - else: - call_ptk = lambda x: x # 1 - Ask for layer or contrib btn_diag = button_dialog( - title=six.text_type("Scapy v%s" % conf.version), + title="Scapy v%s" % conf.version, text=HTML( - six.text_type( - '' - ) + '' ), buttons=[ - (six.text_type("Layers"), "layers"), - (six.text_type("Contribs"), "contribs"), - (six.text_type("Cancel"), "cancel") + ("Layers", "layers"), + ("Contribs", "contribs"), + ("Cancel", "cancel") ]) action = call_ptk(btn_diag) # 2 - Retrieve list of Packets if action == "layers": # Get all loaded layers - values = conf.layers.layers() + lvalues = conf.layers.layers() # Restrict to layers-only (not contribs) + packet.py and asn1*.py - values = [x for x in values if ("layers" in x[0] or - "packet" in x[0] or - "asn1" in x[0])] + values = [x for x in lvalues if ("layers" in x[0] or + "packet" in x[0] or + "asn1" in x[0])] elif action == "contribs": # Get all existing contribs from scapy.main import list_contrib - values = list_contrib(ret=True) + cvalues = cast(List[Dict[str, str]], list_contrib(ret=True)) values = [(x['name'], x['description']) - for x in values] + for x in cvalues] # Remove very specific modules values = [x for x in values if "can" not in x[0]] else: # Escape/Cancel was pressed return - # Python 2 compat - if six.PY2: - values = [(six.text_type(x), six.text_type(y)) - for x, y in values] # Build tree if action == "contribs": # A tree is a dictionary. Each layer contains a keyword # _l which contains the files in the layer, and a _name # argument which is its name. The other keys are the subfolders, # which are similar dictionaries - tree = defaultdict(list) + tree = defaultdict(list) # type: Dict[str, Union[List[Any], Dict[str, Any]]] # noqa: E501 for name, desc in values: if "." in name: # Folder detected parts = name.split(".") @@ -1885,30 +2290,31 @@ def explore(layer=None): for pa in parts[:-1]: if pa not in subtree: subtree[pa] = {} - subtree = subtree[pa] # one layer deeper - subtree["_name"] = pa + # one layer deeper + subtree = subtree[pa] # type: ignore + subtree["_name"] = pa # type: ignore if "_l" not in subtree: subtree["_l"] = [] - subtree["_l"].append((parts[-1], desc)) + subtree["_l"].append((parts[-1], desc)) # type: ignore else: - tree["_l"].append((name, desc)) + tree["_l"].append((name, desc)) # type: ignore elif action == "layers": tree = {"_l": values} # 3 - Ask for the layer/contrib module to explore - current = tree - previous = [] + current = tree # type: Any + previous = [] # type: List[Dict[str, Union[List[Any], Dict[str, Any]]]] # noqa: E501 while True: # Generate tests & form folders = list(current.keys()) _radio_values = [ - ("$" + name, six.text_type('[+] ' + name.capitalize())) + ("$" + name, str('[+] ' + name.capitalize())) for name in folders if not name.startswith("_") - ] + current.get("_l", []) + ] + current.get("_l", []) # type: List[str] cur_path = "" if previous: cur_path = ".".join( itertools.chain( - (x["_name"] for x in previous[1:]), + (x["_name"] for x in previous[1:]), # type: ignore (current["_name"],) ) ) @@ -1918,15 +2324,13 @@ def explore(layer=None): # Show popup rd_diag = radiolist_dialog( values=_radio_values, - title=six.text_type( - "Scapy v%s" % conf.version - ), + title="Scapy v%s" % conf.version, text=HTML( - six.text_type(( + ( '' - ) + extra_text) + ) + extra_text ), cancel_text="Back" if previous else "Cancel" ) @@ -1984,11 +2388,15 @@ def explore(layer=None): raise Scapy_Exception("Unknown scapy module '%s'" % layer) # Print print(conf.color_theme.layer_name("Packets contained in %s:" % result)) - rtlst = [(lay.__name__ or "", lay._name or "") for lay in all_layers] + rtlst = [] # type: List[Tuple[Union[str, List[str]], ...]] + rtlst = [(lay.__name__ or "", cast(str, lay._name) or "") for lay in all_layers] print(pretty_list(rtlst, [("Class", "Name")], borders=True)) -def _pkt_ls(obj, verbose=False): +def _pkt_ls(obj, # type: Union[Packet, Type[Packet]] + verbose=False, # type: bool + ): + # type: (...) -> List[Tuple[str, Type[AnyField], str, str, List[str]]] # noqa: E501 """Internal function used to resolve `fields_desc` to display it. :param obj: a packet object or class @@ -2001,37 +2409,49 @@ def _pkt_ls(obj, verbose=False): fields = [] for f in obj.fields_desc: cur_fld = f - attrs = [] - long_attrs = [] + attrs = [] # type: List[str] + long_attrs = [] # type: List[str] while isinstance(cur_fld, (Emph, ConditionalField)): if isinstance(cur_fld, ConditionalField): attrs.append(cur_fld.__class__.__name__[:4]) cur_fld = cur_fld.fld + name = cur_fld.name + default = cur_fld.default if verbose and isinstance(cur_fld, EnumField) \ - and hasattr(cur_fld, "i2s"): - if len(cur_fld.i2s) < 50: + and hasattr(cur_fld, "i2s") and cur_fld.i2s: + if len(cur_fld.i2s or []) < 50: long_attrs.extend( "%s: %d" % (strval, numval) for numval, strval in - sorted(six.iteritems(cur_fld.i2s)) + sorted(cur_fld.i2s.items()) ) elif isinstance(cur_fld, MultiEnumField): - fld_depend = cur_fld.depends_on(obj.__class__ - if is_pkt else obj) - attrs.append("Depends on %s" % fld_depend.name) + if isinstance(obj, Packet): + obj_pkt = obj + else: + obj_pkt = obj() + fld_depend = cur_fld.depends_on(obj_pkt) + attrs.append("Depends on %s" % fld_depend) if verbose: cur_i2s = cur_fld.i2s_multi.get( - cur_fld.depends_on(obj if is_pkt else obj()), {} + cur_fld.depends_on(obj_pkt), {} ) if len(cur_i2s) < 50: long_attrs.extend( "%s: %d" % (strval, numval) for numval, strval in - sorted(six.iteritems(cur_i2s)) + sorted(cur_i2s.items()) ) elif verbose and isinstance(cur_fld, FlagsField): names = cur_fld.names long_attrs.append(", ".join(names)) + elif isinstance(cur_fld, MultipleTypeField): + default = cur_fld.dflt.default + attrs.append(", ".join( + x[0].__class__.__name__ for x in + itertools.chain(cur_fld.flds, [(cur_fld.dflt,)]) + )) + cls = cur_fld.__class__ class_name_extras = "(%s)" % ( ", ".join(attrs) @@ -2042,32 +2462,37 @@ def _pkt_ls(obj, verbose=False): "s" if cur_fld.size > 1 else "" ) fields.append( - (f.name, + (name, cls, class_name_extras, - f.default, + repr(default), long_attrs) ) return fields @conf.commands.register -def ls(obj=None, case_sensitive=False, verbose=False): +def ls(obj=None, # type: Optional[Union[str, Packet, Type[Packet]]] + case_sensitive=False, # type: bool + verbose=False # type: bool + ): + # type: (...) -> None """List available layers, or infos on a given layer class or name. :param obj: Packet / packet name to use :param case_sensitive: if obj is a string, is it case sensitive? :param verbose: """ - is_string = isinstance(obj, six.string_types) - - if obj is None or is_string: + if obj is None or isinstance(obj, str): tip = False if obj is None: tip = True all_layers = sorted(conf.layers, key=lambda x: x.__name__) else: - pattern = re.compile(obj, 0 if case_sensitive else re.I) + pattern = re.compile( + obj, + 0 if case_sensitive else re.I + ) # We first order by accuracy, then length if case_sensitive: sorter = lambda x: (x.__name__.index(obj), len(x.__name__)) @@ -2088,19 +2513,25 @@ def ls(obj=None, case_sensitive=False, verbose=False): "layers using a clear GUI") else: try: - fields = _pkt_ls(obj, verbose=verbose) + fields = _pkt_ls( + obj, + verbose=verbose + ) is_pkt = isinstance(obj, Packet) # Print for fname, cls, clsne, dflt, long_attrs in fields: - cls = cls.__name__ + " " + clsne - print("%-10s : %-35s =" % (fname, cls), end=' ') + clsinfo = cls.__name__ + " " + clsne + print("%-10s : %-35s =" % (fname, clsinfo), end=' ') if is_pkt: print("%-15r" % (getattr(obj, fname),), end=' ') print("(%r)" % (dflt,)) for attr in long_attrs: print("%-15s%s" % ("", attr)) # Restart for payload if any - if is_pkt and not isinstance(obj.payload, NoPayload): + if is_pkt: + obj = cast(Packet, obj) + if isinstance(obj.payload, NoPayload): + return print("--") ls(obj.payload) except ValueError: @@ -2109,6 +2540,7 @@ def ls(obj=None, case_sensitive=False, verbose=False): @conf.commands.register def rfc(cls, ret=False, legend=True): + # type: (Type[Packet], bool, bool) -> Optional[str] """ Generate an RFC-like representation of a packet def. @@ -2128,15 +2560,27 @@ def rfc(cls, ret=False, legend=True): lines = [] # Get the size (width) that a field will take # when formatted, from its length in bits - clsize = lambda x: 2 * x - 1 + clsize = lambda x: 2 * x - 1 # type: Callable[[int], int] ident = 0 # Fields UUID + # Generate packet groups - for f in cls.fields_desc: - flen = int(f.sz * 8) + def _iterfields() -> Iterator[Tuple[str, int]]: + for f in cls.fields_desc: + # Fancy field name + fname = f.name.upper().replace("_", " ") + fsize = int(f.sz * 8) + yield fname, fsize + # Add padding optionally + if isinstance(f, PadField): + if isinstance(f._align, tuple): + pad = - cur_len % (f._align[0] * 8) + else: + pad = - cur_len % (f._align * 8) + if pad: + yield "padding", pad + for fname, flen in _iterfields(): cur_len += flen ident += 1 - # Fancy field name - fname = f.name.upper().replace("_", " ") # The field might exceed the current line or # take more than one line. Copy it as required while True: @@ -2170,7 +2614,7 @@ def rfc(cls, ret=False, legend=True): # The last field of above is shared with below if above[-1][2] == below[0][2]: # where the field in "above" starts - pos_above = sum(x[1] for x in above[:-1]) + pos_above = sum(x[1] for x in above[:-1]) + len(above[:-1]) - 1 # where the field in "below" ends pos_below = below[0][1] if pos_above < pos_below: @@ -2209,14 +2653,21 @@ def rfc(cls, ret=False, legend=True): if ret: return result print(result) + return None ############# # Fuzzing # ############# +_P = TypeVar('_P', bound=Packet) + + @conf.commands.register -def fuzz(p, _inplace=0): +def fuzz(p, # type: _P + _inplace=0, # type: int + ): + # type: (...) -> _P """ Transform a layer into a fuzzy layer by replacing some default values by random objects. @@ -2226,10 +2677,10 @@ def fuzz(p, _inplace=0): """ if not _inplace: p = p.copy() - q = p + q = cast(Packet, p) while not isinstance(q, NoPayload): new_default_fields = {} - multiple_type_fields = [] + multiple_type_fields = [] # type: List[str] for f in q.fields_desc: if isinstance(f, PacketListField): for r in getattr(q, f.name): @@ -2247,12 +2698,14 @@ def fuzz(p, _inplace=0): # freeze the other random values new_default_fields = { key: (val._fix() if isinstance(val, VolatileValue) else val) - for key, val in six.iteritems(new_default_fields) + for key, val in new_default_fields.items() } q.default_fields.update(new_default_fields) + new_default_fields.clear() # add the random values of the MultipleTypeFields for name in multiple_type_fields: - rnd = q.get_field(name)._find_fld_pkt(q).randval() + fld = cast(MultipleTypeField, q.get_field(name)) + rnd = fld._find_fld_pkt(q).randval() if rnd is not None: new_default_fields[name] = rnd q.default_fields.update(new_default_fields) diff --git a/scapy/pipetool.py b/scapy/pipetool.py index 5c08e4b6964..a28b3534f1e 100644 --- a/scapy/pipetool.py +++ b/scapy/pipetool.py @@ -1,28 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license -from __future__ import print_function import os +import queue import subprocess -import collections import time -import scapy.modules.six as six from threading import Lock, Thread -from scapy.automaton import Message, select_objects, SelectableObject +from scapy.automaton import ( + Message, + ObjectPipe, + select_objects, +) from scapy.consts import WINDOWS -from scapy.error import log_interactive, warning +from scapy.error import log_runtime, warning from scapy.config import conf from scapy.utils import get_temp_file, do_graph +from typing import ( + Any, + Callable, + Dict, + Iterable, + Optional, + Set, + Tuple, + Union, + Type, + TypeVar, + cast, +) -class PipeEngine(SelectableObject): - pipes = {} + +class PipeEngine(ObjectPipe[str]): + pipes = {} # type: Dict[str, Type[Pipe]] @classmethod def list_pipes(cls): + # type: () -> None for pn, pc in sorted(cls.pipes.items()): doc = pc.__doc__ or "" if doc: @@ -31,6 +48,7 @@ def list_pipes(cls): @classmethod def list_pipes_detailed(cls): + # type: () -> None for pn, pc in sorted(cls.pipes.items()): if pc.__doc__: print("###### %s\n %s" % (pn, pc.__doc__)) @@ -38,48 +56,41 @@ def list_pipes_detailed(cls): print("###### %s" % pn) def __init__(self, *pipes): - self.active_pipes = set() - self.active_sources = set() - self.active_drains = set() - self.active_sinks = set() + # type: (*Pipe) -> None + ObjectPipe.__init__(self, "PipeEngine") + self.active_pipes = set() # type: Set[Pipe] + self.active_sources = set() # type: Set[Union[Source, PipeEngine]] + self.active_drains = set() # type: Set[Pipe] + self.active_sinks = set() # type: Set[Pipe] self._add_pipes(*pipes) self.thread_lock = Lock() self.command_lock = Lock() - self.__fd_queue = collections.deque() - self.__fdr, self.__fdw = os.pipe() - self.thread = None - SelectableObject.__init__(self) + self.thread = None # type: Optional[Thread] def __getattr__(self, attr): + # type: (str) -> Callable[..., Pipe] if attr.startswith("spawn_"): dname = attr[6:] if dname in self.pipes: def f(*args, **kargs): + # type: (*Any, **Any) -> Pipe k = self.pipes[dname] - p = k(*args, **kargs) + p = k(*args, **kargs) # type: Pipe self.add(p) return p return f raise AttributeError(attr) - def check_recv(self): - """As select.select is not available, we check if there - is some data to read by using a list that stores pointers.""" - return len(self.__fd_queue) > 0 - - def fileno(self): - return self.__fdr - def _read_cmd(self): - os.read(self.__fdr, 1) - return self.__fd_queue.popleft() + # type: () -> str + return self.recv() # type: ignore def _write_cmd(self, _cmd): - self.__fd_queue.append(_cmd) - os.write(self.__fdw, b"X") - self.call_release() + # type: (str) -> None + self.send(_cmd) def add_one_pipe(self, pipe): + # type: (Pipe) -> None self.active_pipes.add(pipe) if isinstance(pipe, Source): self.active_sources.add(pipe) @@ -89,16 +100,21 @@ def add_one_pipe(self, pipe): self.active_sinks.add(pipe) def get_pipe_list(self, pipe): - def flatten(p, l): - l.add(p) + # type: (Pipe) -> Set[Any] + def flatten(p, # type: Any + li, # type: Set[Pipe] + ): + # type: (...) -> None + li.add(p) for q in p.sources | p.sinks | p.high_sources | p.high_sinks: - if q not in l: - flatten(q, l) - pl = set() + if q not in li: + flatten(q, li) + pl = set() # type: Set[Pipe] flatten(pipe, pl) return pl def _add_pipes(self, *pipes): + # type: (*Pipe) -> Set[Pipe] pl = set() for p in pipes: pl |= self.get_pipe_list(p) @@ -108,17 +124,18 @@ def _add_pipes(self, *pipes): return pl def run(self): - log_interactive.info("Pipe engine thread started.") + # type: () -> None + log_runtime.debug("Pipe engine thread started.") try: for p in self.active_pipes: p.start() sources = self.active_sources sources.add(self) - exhausted = set([]) + exhausted = set([]) # type: Set[Union[Source, PipeEngine]] RUN = True STOP_IF_EXHAUSTED = False while RUN and (not STOP_IF_EXHAUSTED or len(sources) > 1): - fds = select_objects(sources, 2) + fds = select_objects(sources, 0.5) for fd in fds: if fd is self: cmd = self._read_cmd() @@ -131,12 +148,14 @@ def run(self): sources = self.active_sources - exhausted sources.add(self) else: - warning("Unknown internal pipe engine command: %r. Ignoring." % cmd) # noqa: E501 + warning("Unknown internal pipe engine command: %r." + " Ignoring.", cmd) elif fd in sources: try: fd.deliver() except Exception as e: - log_interactive.exception("piping from %s failed: %s" % (fd.name, e)) # noqa: E501 + log_runtime.exception("piping from %s failed: %s", + fd.name, e) else: if fd.exhausted(): exhausted.add(fd) @@ -149,21 +168,24 @@ def run(self): p.stop() finally: self.thread_lock.release() - log_interactive.info("Pipe engine thread stopped.") + log_runtime.debug("Pipe engine thread stopped.") def start(self): - if self.thread_lock.acquire(0): - _t = Thread(target=self.run) - _t.setDaemon(True) + # type: () -> None + if self.thread_lock.acquire(False): + _t = Thread(target=self.run, name="scapy.pipetool.PipeEngine") + _t.daemon = True _t.start() self.thread = _t else: - warning("Pipe engine already running") + log_runtime.debug("Pipe engine already running") def wait_and_stop(self): + # type: () -> None self.stop(_cmd="B") def stop(self, _cmd="X"): + # type: (str) -> None try: with self.command_lock: if self.thread is not None: @@ -174,117 +196,117 @@ def stop(self, _cmd="X"): except Exception: pass else: - warning("Pipe engine thread not running") + log_runtime.debug("Pipe engine thread not running") except KeyboardInterrupt: print("Interrupted by user.") def add(self, *pipes): - pipes = self._add_pipes(*pipes) + # type: (*Pipe) -> None + _pipes = self._add_pipes(*pipes) with self.command_lock: if self.thread is not None: - for p in pipes: + for p in _pipes: p.start() self._write_cmd("A") def graph(self, **kargs): + # type: (Any) -> None g = ['digraph "pipe" {', "\tnode [shape=rectangle];", ] for p in self.active_pipes: g.append('\t"%i" [label="%s"];' % (id(p), p.name)) g.append("") g.append("\tedge [color=blue, arrowhead=vee];") for p in self.active_pipes: - for q in p.sinks: - g.append('\t"%i" -> "%i";' % (id(p), id(q))) + for s in p.sinks: + g.append('\t"%i" -> "%i";' % (id(p), id(s))) g.append("") g.append("\tedge [color=purple, arrowhead=veevee];") for p in self.active_pipes: - for q in p.high_sinks: - g.append('\t"%i" -> "%i";' % (id(p), id(q))) + for hs in p.high_sinks: + g.append('\t"%i" -> "%i";' % (id(p), id(hs))) g.append("") g.append("\tedge [color=red, arrowhead=diamond];") for p in self.active_pipes: - for q in p.trigger_sinks: - g.append('\t"%i" -> "%i";' % (id(p), id(q))) + for ts in p.trigger_sinks: + g.append('\t"%i" -> "%i";' % (id(p), id(ts))) g.append('}') graph = "\n".join(g) do_graph(graph, **kargs) -class _ConnectorLogic(object): - def __init__(self): - self.sources = set() - self.sinks = set() - self.high_sources = set() - self.high_sinks = set() - self.trigger_sources = set() - self.trigger_sinks = set() - - def __lt__(self, other): - other.sinks.add(self) - self.sources.add(other) - return other - - def __gt__(self, other): - self.sinks.add(other) - other.sources.add(self) - return other - - def __eq__(self, other): - self > other - other > self - return other - - def __lshift__(self, other): - self.high_sources.add(other) - other.high_sinks.add(self) - return other - - def __rshift__(self, other): - self.high_sinks.add(other) - other.high_sources.add(self) - return other - - def __floordiv__(self, other): - self >> other - other >> self - return other - - def __xor__(self, other): - self.trigger_sinks.add(other) - other.trigger_sources.add(self) - return other - - def __hash__(self): - return object.__hash__(self) - - class _PipeMeta(type): - def __new__(cls, name, bases, dct): - c = type.__new__(cls, name, bases, dct) + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[Pipe] + c = cast('Type[Pipe]', + super(_PipeMeta, cls).__new__(cls, name, bases, dct)) PipeEngine.pipes[name] = c return c -class Pipe(six.with_metaclass(_PipeMeta, _ConnectorLogic)): +_S = TypeVar("_S", bound="Sink") +_TS = TypeVar("_TS", bound="TriggerSink") + + +class Pipe(metaclass=_PipeMeta): def __init__(self, name=None): - _ConnectorLogic.__init__(self) + # type: (Optional[str]) -> None + self.sources = set() # type: Set['Pipe'] + self.sinks = set() # type: Set['Sink'] + self.high_sources = set() # type: Set['Pipe'] + self.high_sinks = set() # type: Set['Sink'] + self.trigger_sources = set() # type: Set['Pipe'] + self.trigger_sinks = set() # type: Set['TriggerSink'] if name is None: name = "%s" % (self.__class__.__name__) self.name = name def _send(self, msg): + # type: (Any) -> None for s in self.sinks: s.push(msg) def _high_send(self, msg): + # type: (Any) -> None for s in self.high_sinks: s.high_push(msg) def _trigger(self, msg=None): + # type: (Any) -> None for s in self.trigger_sinks: s.on_trigger(msg) + def __gt__(self, other): + # type: (_S) -> _S + self.sinks.add(other) + other.sources.add(self) + return other + + def __rshift__(self, other): + # type: (_S) -> _S + self.high_sinks.add(other) + other.high_sources.add(self) + return other + + def __xor__(self, other): + # type: (_TS) -> _TS + self.trigger_sinks.add(other) + other.trigger_sources.add(self) + return other + + def __hash__(self): + # type: () -> int + return object.__hash__(self) + + def __eq__(self, other): + # type: (Any) -> bool + return object.__eq__(self, other) + def __repr__(self): + # type: () -> str ct = conf.color_theme s = "%s%s" % (ct.punct("<"), ct.layer_name(self.name)) if self.sources or self.sinks: @@ -323,35 +345,35 @@ def __repr__(self): s += ct.punct(">") return s + def start(self): + # type: () -> None + pass -class Source(Pipe, SelectableObject): + def stop(self): + # type: () -> None + pass + + +class Source(Pipe, ObjectPipe[Any]): def __init__(self, name=None): + # type: (Optional[str]) -> None + ObjectPipe.__init__(self, name) Pipe.__init__(self, name=name) - SelectableObject.__init__(self) self.is_exhausted = False def _read_message(self): + # type: () -> Message return Message() def deliver(self): + # type: () -> None msg = self._read_message self._send(msg) - def fileno(self): - return None - - def check_recv(self): - return False - def exhausted(self): + # type: () -> bool return self.is_exhausted - def start(self): - pass - - def stop(self): - pass - class Drain(Pipe): """Repeat messages from low/high entries to (resp.) low/high exits @@ -366,88 +388,129 @@ class Drain(Pipe): """ def push(self, msg): + # type: (Any) -> None self._send(msg) def high_push(self, msg): + # type: (Any) -> None self._high_send(msg) - def start(self): - pass - def stop(self): - pass +class Sink(Pipe): + """ + Does nothing; interface to extend for custom sinks. + All sinks have the following constructor parameters: -class Sink(Pipe): + :param name: a human-readable name for the element + :type name: str + """ def push(self, msg): + # type: (Any) -> None + """ + Called by :py:class:`PipeEngine` when there is a new message for the + low entry. + + :param msg: The message data + :returns: None + :rtype: None + """ pass def high_push(self, msg): + # type: (Any) -> None + """ + Called by :py:class:`PipeEngine` when there is a new message for the + high entry. + + :param msg: The message data + :returns: None + :rtype: None + """ pass - def start(self): - pass + def __lt__(self, other): + # type: (_S) -> _S + other.sinks.add(self) + self.sources.add(other) + return other - def stop(self): + def __lshift__(self, other): + # type: (_S) -> _S + self.high_sources.add(other) + other.high_sinks.add(self) + return other + + def __floordiv__(self, other): + # type: (_S) -> _S + self >> other + other >> self + return other + + def __mod__(self, other): + # type: (_S) -> _S + self > other + other > self + return other + + +class TriggerSink(Sink): + def on_trigger(self, msg): + # type: (Any) -> None pass -class AutoSource(Source, SelectableObject): +class AutoSource(Source): def __init__(self, name=None): - SelectableObject.__init__(self) + # type: (Optional[str]) -> None Source.__init__(self, name=name) - self.__fdr, self.__fdw = os.pipe() - self._queue = collections.deque() - - def fileno(self): - return self.__fdr - - def check_recv(self): - return len(self._queue) > 0 def _gen_data(self, msg): - self._queue.append((msg, False)) - self._wake_up() + # type: (str) -> None + ObjectPipe.send(self, (msg, False, False)) def _gen_high_data(self, msg): - self._queue.append((msg, True)) - self._wake_up() + # type: (str) -> None + ObjectPipe.send(self, (msg, True, False)) - def _wake_up(self): - os.write(self.__fdw, b"X") - self.call_release() + def _exhaust(self): + # type: () -> None + ObjectPipe.send(self, (None, None, True)) def deliver(self): - os.read(self.__fdr, 1) - try: - msg, high = self._queue.popleft() - except IndexError: # empty queue. Exhausted source + # type: () -> None + msg, high, exhaust = self.recv() # type: ignore + if exhaust: pass + if high: + self._high_send(msg) else: - if high: - self._high_send(msg) - else: - self._send(msg) + self._send(msg) class ThreadGenSource(AutoSource): def __init__(self, name=None): + # type: (Optional[str]) -> None AutoSource.__init__(self, name=name) self.RUN = False def generate(self): + # type: () -> None pass def start(self): + # type: () -> None self.RUN = True - Thread(target=self.generate).start() + Thread(target=self.generate, + name="scapy.pipetool.ThreadGenSource").start() def stop(self): + # type: () -> None self.RUN = False class ConsoleSink(Sink): - """Print messages on low and high entries: + """Print messages on low and high entries to ``stdout`` .. code:: @@ -459,14 +522,16 @@ class ConsoleSink(Sink): """ def push(self, msg): + # type: (str) -> None print(">" + repr(msg)) def high_push(self, msg): + # type: (str) -> None print(">>" + repr(msg)) class RawConsoleSink(Sink): - """Print messages on low and high entries, using os.write: + """Print messages on low and high entries, using os.write .. code:: @@ -475,19 +540,26 @@ class RawConsoleSink(Sink): | write | >-|--' |-> +-------+ + + :param newlines: Include a new-line character after printing each packet. + Defaults to True. + :type newlines: bool """ def __init__(self, name=None, newlines=True): + # type: (Optional[str], bool) -> None Sink.__init__(self, name=name) self.newlines = newlines self._write_pipe = 1 def push(self, msg): + # type: (str) -> None if self.newlines: msg += "\n" os.write(self._write_pipe, msg.encode("utf8")) def high_push(self, msg): + # type: (str) -> None if self.newlines: msg += "\n" os.write(self._write_pipe, msg.encode("utf8")) @@ -506,9 +578,12 @@ class CLIFeeder(AutoSource): """ def send(self, msg): + # type: (str) -> int self._gen_data(msg) + return 1 def close(self): + # type: () -> None self.is_exhausted = True @@ -525,11 +600,13 @@ class CLIHighFeeder(CLIFeeder): """ def send(self, msg): + # type: (Any) -> int self._gen_high_data(msg) + return 1 class PeriodicSource(ThreadGenSource): - """Generage messages periodically on low exit: + """Generate messages periodically on low exit: .. code:: @@ -541,14 +618,17 @@ class PeriodicSource(ThreadGenSource): """ def __init__(self, msg, period, period2=0, name=None): + # type: (Union[Iterable[Any], Any], int, int, Optional[str]) -> None ThreadGenSource.__init__(self, name=name) if not isinstance(msg, (list, set, tuple)): - msg = [msg] - self.msg = msg + self.msg = [msg] # type: Iterable[Any] + else: + self.msg = msg self.period = period self.period2 = period2 def generate(self): + # type: () -> None while self.RUN: empty_gen = True for m in self.msg: @@ -557,12 +637,14 @@ def generate(self): time.sleep(self.period) if empty_gen: self.is_exhausted = True - self._wake_up() + self._exhaust() time.sleep(self.period2) class TermSink(Sink): - """Print messages on low and high entries on a separate terminal: + """ + Prints messages on the low and high entries, on a separate terminal (xterm + or cmd). .. code:: @@ -571,9 +653,22 @@ class TermSink(Sink): | print | >-|--' |-> +-------+ + + :param keepterm: Leave the terminal window open after :py:meth:`~Pipe.stop` + is called. Defaults to True. + :type keepterm: bool + :param newlines: Include a new-line character after printing each packet. + Defaults to True. + :type newlines: bool + :param openearly: Automatically starts the terminal when the constructor is + called, rather than waiting for :py:meth:`~Pipe.start`. + Defaults to True. + :type openearly: bool """ - def __init__(self, name=None, keepterm=True, newlines=True, openearly=True): # noqa: E501 + def __init__(self, name=None, keepterm=True, newlines=True, + openearly=True): + # type: (Optional[str], bool, bool, bool) -> None Sink.__init__(self, name=name) self.keepterm = keepterm self.newlines = newlines @@ -582,63 +677,78 @@ def __init__(self, name=None, keepterm=True, newlines=True, openearly=True): # if self.openearly: self.start() - def _start_windows(self): - if not self.opened: - self.opened = True - self.__f = get_temp_file() - open(self.__f, "a").close() - self.name = "Scapy" if self.name is None else self.name - # Start a powershell in a new window and print the PID - cmd = "$app = Start-Process PowerShell -ArgumentList '-command &{$host.ui.RawUI.WindowTitle=\\\"%s\\\";Get-Content \\\"%s\\\" -wait}' -passthru; echo $app.Id" % (self.name, self.__f.replace("\\", "\\\\")) # noqa: E501 - proc = subprocess.Popen([conf.prog.powershell, cmd], stdout=subprocess.PIPE) # noqa: E501 - output, _ = proc.communicate() - # This is the process PID - self.pid = int(output) - print("PID: %d" % self.pid) - - def _start_unix(self): - if not self.opened: - self.opened = True - rdesc, self.wdesc = os.pipe() - cmd = ["xterm"] - if self.name is not None: - cmd.extend(["-title", self.name]) - if self.keepterm: - cmd.append("-hold") - cmd.extend(["-e", "cat <&%d" % rdesc]) - self.proc = subprocess.Popen(cmd, close_fds=False) - os.close(rdesc) + if WINDOWS: + def _start_windows(self): + # type: () -> None + if not self.opened: + self.opened = True + self.__f = get_temp_file() + open(self.__f, "a").close() + self.name = "Scapy" if self.name is None else self.name + # Start a powershell in a new window and print the PID + cmd = "$app = Start-Process PowerShell -ArgumentList '-command &{$host.ui.RawUI.WindowTitle=\\\"%s\\\";Get-Content \\\"%s\\\" -wait}' -passthru; echo $app.Id" % (self.name, self.__f.replace("\\", "\\\\")) # noqa: E501 + proc = subprocess.Popen( + [ + getattr(conf.prog, "powershell"), + cmd + ], + stdout=subprocess.PIPE + ) + output, _ = proc.communicate() + # This is the process PID + self.pid = int(output) + print("PID: %d" % self.pid) + + def _stop_windows(self): + # type: () -> None + if not self.keepterm: + self.opened = False + # Recipe to kill process with PID + # http://code.activestate.com/recipes/347462-terminating-a-subprocess-on-windows/ + import ctypes + PROCESS_TERMINATE = 1 + handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, self.pid) # noqa: E501 + ctypes.windll.kernel32.TerminateProcess(handle, -1) + ctypes.windll.kernel32.CloseHandle(handle) + else: + def _start_unix(self): + # type: () -> None + if not self.opened: + self.opened = True + rdesc, self.wdesc = os.pipe() + os.set_inheritable(rdesc, True) + cmd = ["xterm"] + if self.name is not None: + cmd.extend(["-title", self.name]) + if self.keepterm: + cmd.append("-hold") + cmd.extend(["-e", "cat <&%d" % rdesc]) + self.proc = subprocess.Popen(cmd, close_fds=False) + os.close(rdesc) + + def _stop_unix(self): + # type: () -> None + if not self.keepterm: + self.opened = False + self.proc.kill() + self.proc.wait() def start(self): + # type: () -> None if WINDOWS: return self._start_windows() else: return self._start_unix() - def _stop_windows(self): - if not self.keepterm: - self.opened = False - # Recipe to kill process with PID - # http://code.activestate.com/recipes/347462-terminating-a-subprocess-on-windows/ - import ctypes - PROCESS_TERMINATE = 1 - handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, self.pid) # noqa: E501 - ctypes.windll.kernel32.TerminateProcess(handle, -1) - ctypes.windll.kernel32.CloseHandle(handle) - - def _stop_unix(self): - if not self.keepterm: - self.opened = False - self.proc.kill() - self.proc.wait() - def stop(self): + # type: () -> None if WINDOWS: return self._stop_windows() else: return self._stop_unix() def _print(self, s): + # type: (str) -> None if self.newlines: s += "\n" if WINDOWS: @@ -649,15 +759,19 @@ def _print(self, s): os.write(self.wdesc, s.encode()) def push(self, msg): + # type: (str) -> None self._print(str(msg)) def high_push(self, msg): + # type: (str) -> None self._print(str(msg)) class QueueSink(Sink): - """Collect messages from high and low entries and queue them. - Messages are unqueued with the .recv() method: + """ + Collects messages on the low and high entries into a :py:class:`Queue`. + Messages are dequeued with :py:meth:`recv`. + Both high and low entries share the same :py:class:`Queue`. .. code:: @@ -669,20 +783,38 @@ class QueueSink(Sink): """ def __init__(self, name=None): + # type: (Optional[str]) -> None Sink.__init__(self, name=name) - self.q = six.moves.queue.Queue() + self.q: queue.Queue[Any] = queue.Queue() def push(self, msg): + # type: (Any) -> None self.q.put(msg) def high_push(self, msg): + # type: (Any) -> None self.q.put(msg) def recv(self, block=True, timeout=None): + # type: (bool, Optional[int]) -> Optional[Any] + """ + Reads the next message from the queue. + + If no message is available in the queue, returns None. + + :param block: Blocks execution until a packet is available in the + queue. Defaults to True. + :type block: bool + :param timeout: Controls how long to wait if ``block=True``. If None + (the default), this method will wait forever. If a + non-negative number, this is a number of seconds to + wait before giving up (and returning None). + :type timeout: None, int or float + """ try: return self.q.get(block=block, timeout=timeout) - except six.moves.queue.Empty: - pass + except queue.Empty: + return None class TransformDrain(Drain): @@ -698,13 +830,16 @@ class TransformDrain(Drain): """ def __init__(self, f, name=None): + # type: (Callable[[Any], None], Optional[str]) -> None Drain.__init__(self, name=name) self.f = f def push(self, msg): + # type: (Any) -> None self._send(self.f(msg)) def high_push(self, msg): + # type: (Any) -> None self._high_send(self.f(msg)) @@ -721,9 +856,11 @@ class UpDrain(Drain): """ def push(self, msg): + # type: (Any) -> None self._high_send(msg) def high_push(self, msg): + # type: (Any) -> None pass @@ -740,7 +877,9 @@ class DownDrain(Drain): """ def push(self, msg): + # type: (Any) -> None pass def high_push(self, msg): + # type: (Any) -> None self._send(msg) diff --git a/scapy/plist.py b/scapy/plist.py index d3f05eb51d5..0ea33d91f9d 100644 --- a/scapy/plist.py +++ b/scapy/plist.py @@ -1,40 +1,77 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ PacketList: holds several packets and allows to do operations on them. """ -from __future__ import absolute_import -from __future__ import print_function import os from collections import defaultdict +from typing import Sequence, NamedTuple -from scapy.compat import lambda_tuple_converter from scapy.config import conf -from scapy.base_classes import BasePacket, BasePacketList, _CanvasDumpExtended -from scapy.fields import IPField, ShortEnumField, PacketField +from scapy.base_classes import ( + BasePacket, + BasePacketList, + PacketList_metaclass, + SetGen, + _CanvasDumpExtended, +) from scapy.utils import do_graph, hexdump, make_table, make_lined_table, \ make_tex_table, issubtype -from scapy.extlib import plt, Line2D, \ - MATPLOTLIB_INLINED, MATPLOTLIB_DEFAULT_PLOT_KARGS from functools import reduce -import scapy.modules.six as six -from scapy.modules.six.moves import range, zip -from scapy.compat import Optional, List, Union, Tuple, Dict, Any, Callable + +# typings +from typing import ( + Any, + Callable, + DefaultDict, + Dict, + Generic, + Iterator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + TYPE_CHECKING, +) from scapy.packet import Packet + +try: + import pyx +except ImportError: + pass + +if TYPE_CHECKING: + from scapy.libs.matplot import Line2D + ############# # Results # ############# -class PacketList(BasePacketList, _CanvasDumpExtended): +QueryAnswer = NamedTuple( + "QueryAnswer", + [("query", Packet), ("answer", Packet)] +) + +_Inner = TypeVar("_Inner", Packet, QueryAnswer) + + +class _PacketList(Generic[_Inner], metaclass=PacketList_metaclass): __slots__ = ["stats", "res", "listname"] - def __init__(self, res=None, name="PacketList", stats=None): + def __init__(self, + res=None, # type: Optional[Union[_PacketList[_Inner], List[_Inner]]] # noqa: E501 + name="PacketList", # type: str + stats=None # type: Optional[List[Type[Packet]]] + ): + # type: (...) -> None """create a packet list from a list of packets res: the list of packets stats: a list of classes that will appear in the stats (defaults to [TCP,UDP,ICMP])""" # noqa: E501 @@ -42,10 +79,11 @@ def __init__(self, res=None, name="PacketList", stats=None): stats = conf.stats_classic_protocols self.stats = stats if res is None: - res = [] - elif isinstance(res, PacketList): - res = res.res - self.res = res + self.res = [] # type: List[_Inner] + elif isinstance(res, _PacketList): + self.res = res.res + else: + self.res = res self.listname = name def __len__(self): @@ -53,15 +91,15 @@ def __len__(self): return len(self.res) def _elt2pkt(self, elt): - # type: (Packet) -> Packet - return elt + # type: (_Inner) -> Packet + return elt # type: ignore def _elt2sum(self, elt): - # type: (Packet) -> str - return elt.summary() + # type: (_Inner) -> str + return elt.summary() # type: ignore def _elt2show(self, elt): - # type: (Packet) -> str + # type: (_Inner) -> str return self._elt2sum(elt) def __repr__(self): @@ -93,7 +131,7 @@ def __repr__(self): ct.punct(">")) def __getstate__(self): - # type: () -> Dict[str, Union[List[PacketField], List[Packet], str]] + # type: () -> Dict[str, Any] """ Creates a basic representation of the instance, used in conjunction with __setstate__() e.g. by pickle @@ -108,7 +146,7 @@ def __getstate__(self): return state def __setstate__(self, state): - # type: (Dict[str, Union[List[PacketField], List[Packet], str]]) -> None # noqa: E501 + # type: (Dict[str, Any]) -> None """ Sets instance attributes to values given by state, used in conjunction with __getstate__() e.g. by pickle @@ -119,11 +157,16 @@ def __setstate__(self, state): self.stats = state['stats'] self.listname = state['listname'] + def __iter__(self): + # type: () -> Iterator[_Inner] + return self.res.__iter__() + def __getattr__(self, attr): # type: (str) -> Any return getattr(self.res, attr) def __getitem__(self, item): + # type: (Any) -> Any if issubtype(item, BasePacket): return self.__class__([x for x in self.res if item in self._elt2pkt(x)], # noqa: E501 name="%s from %s" % (item.__name__, self.listname)) # noqa: E501 @@ -132,13 +175,26 @@ def __getitem__(self, item): name="mod %s" % self.listname) return self.res.__getitem__(item) - def __add__(self, other): - # type: (PacketList) -> PacketList - return self.__class__(self.res + other.res, - name="%s+%s" % (self.listname, other.listname)) + _T = TypeVar('_T', 'SndRcvList', 'PacketList') + + # Hinting hack: type self + def __add__(self, # type: _PacketList._T # type: ignore + other # type: _PacketList._T + ): + # type: (...) -> _PacketList._T + return self.__class__( + self.res + other.res, + name="%s+%s" % ( + self.listname, + other.listname + ) + ) - def summary(self, prn=None, lfilter=None): - # type: (Optional[Callable], Optional[Callable]) -> None + def summary(self, + prn=None, # type: Optional[Callable[..., Any]] + lfilter=None # type: Optional[Callable[..., bool]] + ): + # type: (...) -> None """prints a summary of each packet :param prn: function to apply to each packet instead of @@ -148,15 +204,18 @@ def summary(self, prn=None, lfilter=None): """ for r in self.res: if lfilter is not None: - if not lfilter(r): + if not lfilter(*r): continue if prn is None: print(self._elt2sum(r)) else: - print(prn(r)) + print(prn(*r)) - def nsummary(self, prn=None, lfilter=None): - # type: (Optional[Callable], Optional[Callable]) -> None + def nsummary(self, + prn=None, # type: Optional[Callable[..., Any]] + lfilter=None # type: Optional[Callable[..., bool]] + ): + # type: (...) -> None """prints a summary of each packet with the packet's number :param prn: function to apply to each packet instead of @@ -166,57 +225,63 @@ def nsummary(self, prn=None, lfilter=None): """ for i, res in enumerate(self.res): if lfilter is not None: - if not lfilter(res): + if not lfilter(*res): continue print(conf.color_theme.id(i, fmt="%04i"), end=' ') if prn is None: print(self._elt2sum(res)) else: - print(prn(res)) - - def display(self): # Deprecated. Use show() - """deprecated. is show()""" - self.show() + print(prn(*res)) def show(self, *args, **kargs): - # type: (Any, Any) -> None + # type: (*Any, **Any) -> None """Best way to display the packet list. Defaults to nsummary() method""" # noqa: E501 return self.nsummary(*args, **kargs) def filter(self, func): - # type: (Callable) -> PacketList + # type: (Callable[..., bool]) -> _PacketList[_Inner] """Returns a packet list filtered by a truth function. This truth - function has to take a packet as the only argument and return a boolean value.""" # noqa: E501 - return self.__class__([x for x in self.res if func(x)], + function has to take a packet as the only argument and return + a boolean value. + """ + return self.__class__([x for x in self.res if func(*x)], name="filtered %s" % self.listname) def make_table(self, *args, **kargs): - # type: (Any, Any) -> None + # type: (Any, Any) -> Optional[str] """Prints a table using a function that returns for each packet its head column value, head row value and displayed value # noqa: E501 ex: p.make_table(lambda x:(x[IP].dst, x[TCP].dport, x[TCP].sprintf("%flags%")) """ # noqa: E501 return make_table(self.res, *args, **kargs) def make_lined_table(self, *args, **kargs): - # type: (Any, Any) -> None + # type: (Any, Any) -> Optional[str] """Same as make_table, but print a table with lines""" return make_lined_table(self.res, *args, **kargs) def make_tex_table(self, *args, **kargs): - # type: (Any, Any) -> None + # type: (Any, Any) -> Optional[str] """Same as make_table, but print a table with LaTeX syntax""" return make_tex_table(self.res, *args, **kargs) - def plot(self, f, lfilter=None, plot_xy=False, **kargs): - # type: (Callable, Optional[Callable], bool, Any) -> Line2D + def plot(self, + f, # type: Callable[..., Any] + lfilter=None, # type: Optional[Callable[..., bool]] + plot_xy=False, # type: bool + **kargs # type: Any + ): + # type: (...) -> Line2D """Applies a function to each packet to get a value that will be plotted with matplotlib. A list of matplotlib.lines.Line2D is returned. lfilter: a truth function that decides whether a packet must be plotted """ - - # Python 2 backward compatibility - f = lambda_tuple_converter(f) - lfilter = lambda_tuple_converter(lfilter) + # Defer imports of matplotlib until its needed + # because it has a heavy dep chain + from scapy.libs.matplot import ( + plt, + MATPLOTLIB_INLINED, + MATPLOTLIB_DEFAULT_PLOT_KARGS + ) # Get the list of packets if lfilter is None: @@ -238,13 +303,25 @@ def plot(self, f, lfilter=None, plot_xy=False, **kargs): return lines - def diffplot(self, f, delay=1, lfilter=None, **kargs): - # type: (Callable, int, Optional[Callable], Any) -> Line2D + def diffplot(self, + f, # type: Callable[..., Any] + delay=1, # type: int + lfilter=None, # type: Optional[Callable[..., bool]] + **kargs # type: Any + ): + # type: (...) -> Line2D """diffplot(f, delay=1, lfilter=None) Applies a function to couples (l[i],l[i+delay]) A list of matplotlib.lines.Line2D is returned. """ + # Defer imports of matplotlib until its needed + # because it has a heavy dep chain + from scapy.libs.matplot import ( + plt, + MATPLOTLIB_INLINED, + MATPLOTLIB_DEFAULT_PLOT_KARGS + ) # Get the list of packets if lfilter is None: @@ -266,17 +343,25 @@ def diffplot(self, f, delay=1, lfilter=None, **kargs): return lines - def multiplot(self, f, lfilter=None, plot_xy=False, **kargs): - # type: (Callable, Optional[Callable], bool, Any) -> Line2D + def multiplot(self, + f, # type: Callable[..., Any] + lfilter=None, # type: Optional[Callable[..., Any]] + plot_xy=False, # type: bool + **kargs # type: Any + ): + # type: (...) -> Line2D """Uses a function that returns a label and a value for this label, then plots all the values label by label. A list of matplotlib.lines.Line2D is returned. """ - - # Python 2 backward compatibility - f = lambda_tuple_converter(f) - lfilter = lambda_tuple_converter(lfilter) + # Defer imports of matplotlib until its needed + # because it has a heavy dep chain + from scapy.libs.matplot import ( + plt, + MATPLOTLIB_INLINED, + MATPLOTLIB_DEFAULT_PLOT_KARGS + ) # Get the list of packets if lfilter is None: @@ -294,11 +379,11 @@ def multiplot(self, f, lfilter=None, plot_xy=False, **kargs): kargs = MATPLOTLIB_DEFAULT_PLOT_KARGS if plot_xy: - lines = [plt.plot(*zip(*pl), **dict(kargs, label=k)) - for k, pl in six.iteritems(d)] + lines = [plt.plot(*list(zip(*pl)), **dict(kargs, label=k)) + for k, pl in d.items()] else: lines = [plt.plot(pl, **dict(kargs, label=k)) - for k, pl in six.iteritems(d)] + for k, pl in d.items()] plt.legend(loc="center right", bbox_to_anchor=(1.5, 0.5)) # Call show() if matplotlib is not inlined @@ -308,13 +393,13 @@ def multiplot(self, f, lfilter=None, plot_xy=False, **kargs): return lines def rawhexdump(self): - # type: (Optional[Callable]) -> None + # type: () -> None """Prints an hexadecimal dump of each packet in the list""" for p in self: hexdump(self._elt2pkt(p)) def hexraw(self, lfilter=None): - # type: (Optional[Callable]) -> None + # type: (Optional[Callable[..., bool]]) -> None """Same as nsummary(), except that if a packet has a Raw layer, it will be hexdumped # noqa: E501 lfilter: a truth function that decides whether a packet must be displayed""" # noqa: E501 for i, res in enumerate(self.res): @@ -325,10 +410,10 @@ def hexraw(self, lfilter=None): p.sprintf("%.time%"), self._elt2sum(res))) if p.haslayer(conf.raw_layer): - hexdump(p.getlayer(conf.raw_layer).load) + hexdump(p.getlayer(conf.raw_layer).load) # type: ignore def hexdump(self, lfilter=None): - # type: (Optional[Callable]) -> None + # type: (Optional[Callable[..., bool]]) -> None """Same as nsummary(), except that packets are also hexdumped lfilter: a truth function that decides whether a packet must be displayed""" # noqa: E501 for i, res in enumerate(self.res): @@ -341,7 +426,7 @@ def hexdump(self, lfilter=None): hexdump(p) def padding(self, lfilter=None): - # type: (Optional[Callable]) -> None + # type: (Optional[Callable[..., bool]]) -> None """Same as hexraw(), for Padding layer""" for i, res in enumerate(self.res): p = self._elt2pkt(res) @@ -350,24 +435,32 @@ def padding(self, lfilter=None): print("%s %s %s" % (conf.color_theme.id(i, fmt="%04i"), p.sprintf("%.time%"), self._elt2sum(res))) - hexdump(p.getlayer(conf.padding_layer).load) + hexdump( + p.getlayer(conf.padding_layer).load # type: ignore + ) def nzpadding(self, lfilter=None): - # type: (Optional[Callable]) -> None + # type: (Optional[Callable[..., bool]]) -> None """Same as padding() but only non null padding""" for i, res in enumerate(self.res): p = self._elt2pkt(res) if p.haslayer(conf.padding_layer): - pad = p.getlayer(conf.padding_layer).load - if pad == pad[0] * len(pad): + pad = p.getlayer(conf.padding_layer).load # type: ignore + if pad == pad[:1] * len(pad): continue if lfilter is None or lfilter(p): print("%s %s %s" % (conf.color_theme.id(i, fmt="%04i"), p.sprintf("%.time%"), self._elt2sum(res))) - hexdump(p.getlayer(conf.padding_layer).load) - - def conversations(self, getsrcdst=None, **kargs): + hexdump( + p.getlayer(conf.padding_layer).load # type: ignore + ) + + def conversations(self, + getsrcdst=None, # type: Optional[Callable[[Packet], Tuple[Any, ...]]] # noqa: E501 + **kargs # type: Any + ): + # type: (...) -> Any """Graphes a conversations between sources and destinations and display it (using graphviz and imagemagick) @@ -382,7 +475,8 @@ def conversations(self, getsrcdst=None, **kargs): :param prog: which graphviz program to use """ if getsrcdst is None: - def getsrcdst(pkt): + def _getsrcdst(pkt): + # type: (Packet) -> Tuple[str, str] """Extract src and dst addresses""" if 'IP' in pkt: return (pkt['IP'].src, pkt['IP'].dst) @@ -391,9 +485,10 @@ def getsrcdst(pkt): if 'ARP' in pkt: return (pkt['ARP'].psrc, pkt['ARP'].pdst) raise TypeError() - conv = {} - for p in self.res: - p = self._elt2pkt(p) + getsrcdst = _getsrcdst + conv = {} # type: Dict[Tuple[Any, ...], Any] + for elt in self.res: + p = self._elt2pkt(elt) try: c = getsrcdst(p) except Exception: @@ -408,27 +503,32 @@ def getsrcdst(pkt): else: conv[c] = conv.get(c, 0) + 1 gr = 'digraph "conv" {\n' - for (s, d), l in six.iteritems(conv): + for (s, d), l in conv.items(): gr += '\t "%s" -> "%s" [label="%s"]\n' % ( s, d, ', '.join(str(x) for x in l) if isinstance(l, set) else l ) gr += "}\n" return do_graph(gr, **kargs) - def afterglow(self, src=None, event=None, dst=None, **kargs): - # type: (Optional[Callable], Optional[Callable], Optional[Callable], Any) -> None # noqa: E501 + def afterglow(self, + src=None, # type: Optional[Callable[[_Inner], Any]] + event=None, # type: Optional[Callable[[_Inner], Any]] + dst=None, # type: Optional[Callable[[_Inner], Any]] + **kargs # type: Any + ): + # type: (...) -> Any """Experimental clone attempt of http://sourceforge.net/projects/afterglow each datum is reduced as src -> event -> dst and the data are graphed. by default we have IP.src -> IP.dport -> IP.dst""" if src is None: - src = lambda x: x['IP'].src + src = lambda *x: x[0]['IP'].src if event is None: - event = lambda x: x['IP'].dport + event = lambda *x: x[0]['IP'].dport if dst is None: - dst = lambda x: x['IP'].dst - sl = {} # type: Dict[IPField, Tuple[int, List[ShortEnumField]]] - el = {} # type: Dict[ShortEnumField, Tuple[int, List[IPField]]] - dl = {} # type: Dict[IPField, ShortEnumField] + dst = lambda *x: x[0]['IP'].dst + sl = {} # type: Dict[Any, Tuple[Union[float, int], List[Any]]] + el = {} # type: Dict[Any, Tuple[Union[float, int], List[Any]]] + dl = {} # type: Dict[Any, int] for i in self.res: try: s, e, d = src(i), event(i), dst(i) @@ -453,6 +553,7 @@ def afterglow(self, src=None, event=None, dst=None, **kargs): continue def minmax(x): + # type: (Any) -> Tuple[int, int] m, M = reduce(lambda a, b: (min(a[0], b[0]), max(a[1], b[1])), ((a, a) for a in x)) if m == M: @@ -461,9 +562,9 @@ def minmax(x): M = 1 return m, M - mins, maxs = minmax(x for x, _ in six.itervalues(sl)) - mine, maxe = minmax(x for x, _ in six.itervalues(el)) - mind, maxd = minmax(six.itervalues(dl)) + mins, maxs = minmax(x for x, _ in sl.values()) + mine, maxe = minmax(x for x, _ in el.values()) + mind, maxd = minmax(dl.values()) gr = 'digraph "afterglow" {\n\tedge [len=2.5];\n' @@ -484,24 +585,24 @@ def minmax(x): gr += "###\n" for s in sl: - n, lst = sl[s] - for e in lst: + n, lst1 = sl[s] + for e in lst1: gr += ' "src.%s" -> "evt.%s";\n' % (repr(s), repr(e)) for e in el: - n, lst = el[e] - for d in lst: + n, lst2 = el[e] + for d in lst2: gr += ' "evt.%s" -> "dst.%s";\n' % (repr(e), repr(d)) gr += "}" return do_graph(gr, **kargs) - def canvas_dump(self, **kargs): - # type: (Any) -> Any # Using Any since pyx is imported later - import pyx + def canvas_dump(self, layer_shift=0, rebuild=1): + # type: (int, int) -> 'pyx.canvas.canvas' d = pyx.document.document() len_res = len(self.res) for i, res in enumerate(self.res): - c = self._elt2pkt(res).canvas_dump(**kargs) + c = self._elt2pkt(res).canvas_dump(layer_shift=layer_shift, + rebuild=rebuild) cbb = c.bbox() c.text(cbb.left(), cbb.top() + 1, r"\font\cmssfont=cmss12\cmssfont{Frame %i/%i}" % (i, len_res), [pyx.text.size.LARGE]) # noqa: E501 if conf.verb >= 2: @@ -511,37 +612,14 @@ def canvas_dump(self, **kargs): fittosize=1)) return d - def sr(self, multi=0): - # type: (int) -> Tuple[SndRcvList, PacketList] - """sr([multi=1]) -> (SndRcvList, PacketList) - Matches packets in the list and return ( (matched couples), (unmatched packets) )""" # noqa: E501 - remain = self.res[:] - sr = [] - i = 0 - while i < len(remain): - s = remain[i] - j = i - while j < len(remain) - 1: - j += 1 - r = remain[j] - if r.answers(s): - sr.append((s, r)) - if multi: - remain[i]._answered = 1 - remain[j]._answered = 2 - continue - del(remain[j]) - del(remain[i]) - i -= 1 - break - i += 1 - if multi: - remain = [x for x in remain if not hasattr(x, "_answered")] - return SndRcvList(sr), PacketList(remain) - - def sessions(self, session_extractor=None): + def sessions( + self, + session_extractor=None # type: Optional[Callable[[Packet], str]] + ): + # type: (...) -> Dict[str, _PacketList[_Inner]] if session_extractor is None: - def session_extractor(p): + def _session_extractor(p): + # type: (Packet) -> str """Extract sessions from packets""" if 'Ether' in p: if 'IP' in p or 'IPv6' in p: @@ -568,9 +646,12 @@ def session_extractor(p): else: return p.sprintf("Ethernet type=%04xr,Ether.type%") return "Other" - sessions = defaultdict(self.__class__) + session_extractor = _session_extractor + sessions = defaultdict(self.__class__) # type: DefaultDict[str, _PacketList[_Inner]] # noqa: E501 for p in self.res: - sess = session_extractor(self._elt2pkt(p)) + sess = session_extractor( + self._elt2pkt(p) + ) sessions[sess].append(p) return dict(sessions) @@ -589,8 +670,8 @@ def replace(self, *args, **kargs): x = PacketList(name="Replaced %s" % self.listname) if not isinstance(args[0], tuple): args = (args,) - for p in self.res: - p = self._elt2pkt(p) + for _p in self.res: + p = self._elt2pkt(_p) copied = False for scheme in args: fld = scheme[0] @@ -612,7 +693,7 @@ def getlayer(self, cls, # type: Packet nb=None, # type: Optional[int] flt=None, # type: Optional[Dict[str, Any]] name=None, # type: Optional[str] - stats=None # type: Optional[List[Packet]] + stats=None # type: Optional[List[Type[Packet]]] ): # type: (...) -> PacketList """Returns the packet list from a given layer. @@ -650,56 +731,79 @@ def getlayer(self, cls, # type: Packet # Only return non-None getlayer results return PacketList([ - pc for pc in (p.getlayer(**getlayer_arg) for p in self.res) - if pc is not None], + pc for pc in ( + self._elt2pkt(p).getlayer(**getlayer_arg) for p in self.res + ) if pc is not None], name, stats ) - def convert_to(self, other_cls, name=None, stats=None): - # type: (Packet, Optional[str], Optional[List[Packet]]) -> PacketList - """Converts all packets to another type. - - See ``Packet.convert_to`` for more info. - - :param other_cls: reference to a Packet class to convert to - :type other_cls: Type[scapy.packet.Packet] - :param name: optional name for the new PacketList - :type name: Optional[str] - - :param stats: optional list of protocols to give stats on; - if not specified, inherits from this PacketList. - :type stats: Optional[List[Type[scapy.packet.Packet]]] +class PacketList(_PacketList[Packet], + BasePacketList[Packet], + _CanvasDumpExtended): + def sr(self, multi=False, lookahead=None): + # type: (bool, Optional[int]) -> Tuple[SndRcvList, PacketList] + """ + Matches packets in the list - :rtype: scapy.plist.PacketList + :param multi: True if a packet can have multiple answers + :param lookahead: Maximum number of packets between packet and answer. + If 0 or None, full remaining list is + scanned for answers + :return: ( (matched couples), (unmatched packets) ) """ - if name is None: - name = "{} converted to {}".format( - self.listname, other_cls.__name__) - if stats is None: - stats = self.stats + remain = self.res[:] + sr = [] # type: List[QueryAnswer] + i = 0 + if lookahead is None or lookahead == 0: + lookahead = len(remain) + while i < len(remain): + s = remain[i] + j = i + while j < min(lookahead + i, len(remain) - 1): + j += 1 + r = remain[j] + if r.answers(s): + sr.append(QueryAnswer(s, r)) + if multi: + remain[i]._answered = 1 + remain[j]._answered = 2 + continue + del remain[j] + del remain[i] + i -= 1 + break + i += 1 + if multi: + remain = [x for x in remain if not hasattr(x, "_answered")] + return SndRcvList(sr), PacketList(remain) - return PacketList( - [p.convert_to(other_cls) for p in self.res], - name, stats - ) + +_PacketIterable = Union[ + Sequence[Packet], + Packet, + SetGen[Packet], + _PacketList[Packet] +] -class SndRcvList(PacketList): +class SndRcvList(_PacketList[QueryAnswer], + BasePacketList[QueryAnswer], + _CanvasDumpExtended): __slots__ = [] # type: List[str] def __init__(self, - res=None, # type: Optional[Union[List[Packet], PacketList]] + res=None, # type: Optional[Union[_PacketList[QueryAnswer], List[QueryAnswer]]] # noqa: E501 name="Results", # type: str - stats=None # type: Optional[List[Packet]] + stats=None # type: Optional[List[Type[Packet]]] ): # type: (...) -> None - PacketList.__init__(self, res, name, stats) + super(SndRcvList, self).__init__(res, name, stats) def _elt2pkt(self, elt): - # type: (Tuple[Packet, Packet]) -> Packet + # type: (QueryAnswer) -> Packet return elt[1] def _elt2sum(self, elt): - # type: (Tuple[Packet, Packet]) -> str + # type: (QueryAnswer) -> str return "%s ==> %s" % (elt[0].summary(), elt[1].summary()) diff --git a/scapy/pton_ntop.py b/scapy/pton_ntop.py index e57da17dd26..9fa13e89012 100644 --- a/scapy/pton_ntop.py +++ b/scapy/pton_ntop.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Convert IPv6 addresses between textual representation and binary. @@ -10,18 +10,20 @@ without IPv6 support, on Windows for instance. """ -from __future__ import absolute_import import socket import re import binascii -from scapy.modules.six.moves import range from scapy.compat import plain_str, hex_bytes, bytes_encode, bytes_hex +# Typing imports +from typing import Union + _IP6_ZEROS = re.compile('(?::|^)(0(?::0)+)(?::|$)') _INET6_PTON_EXC = socket.error("illegal IP address string passed to inet_pton") def _inet6_pton(addr): + # type: (str) -> bytes """Convert an IPv6 address from text representation into binary form, used when socket.inet_pton is not available. @@ -79,11 +81,14 @@ def _inet6_pton(addr): def inet_pton(af, addr): + # type: (socket.AddressFamily, Union[bytes, str]) -> bytes """Convert an IP address from text representation into binary form.""" # Will replace Net/Net6 objects addr = plain_str(addr) # Use inet_pton if available try: + if not socket.has_ipv6: + raise AttributeError return socket.inet_pton(af, addr) except AttributeError: try: @@ -93,6 +98,7 @@ def inet_pton(af, addr): def _inet6_ntop(addr): + # type: (bytes) -> str """Convert an IPv6 address from binary form into text representation, used when socket.inet_pton is not available. @@ -125,10 +131,13 @@ def _inet6_ntop(addr): def inet_ntop(af, addr): + # type: (socket.AddressFamily, bytes) -> str """Convert an IP address from binary form into text representation.""" # Use inet_ntop if available addr = bytes_encode(addr) try: + if not socket.has_ipv6: + raise AttributeError return socket.inet_ntop(af, addr) except AttributeError: try: diff --git a/doc/scapy/_templates/_dummy b/scapy/py.typed similarity index 100% rename from doc/scapy/_templates/_dummy rename to scapy/py.typed diff --git a/scapy/route.py b/scapy/route.py index 8fd043604d2..52806488cfb 100644 --- a/scapy/route.py +++ b/scapy/route.py @@ -1,21 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Routing and handling of network interfaces. """ -from __future__ import absolute_import - - -import scapy.consts +from scapy.compat import plain_str from scapy.config import conf from scapy.error import Scapy_Exception, warning -from scapy.modules import six -from scapy.utils import atol, ltoa, itom, plain_str, pretty_list +from scapy.interfaces import resolve_iface +from scapy.utils import atol, ltoa, itom, pretty_list + +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + Union, +) ############################## @@ -24,36 +30,50 @@ class Route: def __init__(self): - self.resync() + # type: () -> None + self.routes = [] # type: List[Tuple[int, int, str, str, str, int]] + self.invalidate_cache() + if conf.route_autoload: + self.resync() def invalidate_cache(self): - self.cache = {} + # type: () -> None + self.cache = {} # type: Dict[Tuple[str, Optional[str]], Tuple[str, str, str]] def resync(self): + # type: () -> None from scapy.arch import read_routes self.invalidate_cache() self.routes = read_routes() def __repr__(self): - rtlst = [] + # type: () -> str + rtlst = [] # type: List[Tuple[Union[str, List[str]], ...]] for net, msk, gw, iface, addr, metric in self.routes: + if_repr = resolve_iface(iface).description rtlst.append((ltoa(net), ltoa(msk), gw, - (iface.description if not isinstance(iface, six.string_types) else iface), # noqa: E501 + if_repr, addr, str(metric))) return pretty_list(rtlst, [("Network", "Netmask", "Gateway", "Iface", "Output IP", "Metric")]) # noqa: E501 - def make_route(self, host=None, net=None, gw=None, dev=None, metric=1): - from scapy.arch import get_if_addr + def make_route(self, + host=None, # type: Optional[str] + net=None, # type: Optional[str] + gw=None, # type: Optional[str] + dev=None, # type: Optional[str] + metric=1, # type: int + ): + # type: (...) -> Tuple[int, int, str, str, str, int] if host is not None: thenet, msk = host, 32 elif net is not None: - thenet, msk = net.split("/") - msk = int(msk) + thenet, msk_b = net.split("/") + msk = int(msk_b) else: raise Scapy_Exception("make_route: Incorrect parameters. You should specify a host or a net") # noqa: E501 if gw is None: @@ -65,39 +85,60 @@ def make_route(self, host=None, net=None, gw=None, dev=None, metric=1): nhop = thenet dev, ifaddr, _ = self.route(nhop) else: - ifaddr = get_if_addr(dev) + ifaddr = "0.0.0.0" # acts as a 'via' in `ip addr add` return (atol(thenet), itom(msk), gw, dev, ifaddr, metric) def add(self, *args, **kargs): - """Ex: - add(net="192.168.1.0/24",gw="1.2.3.4") + # type: (*Any, **Any) -> None + """Add a route to Scapy's IPv4 routing table. + add(host|net, gw|dev) + + :param host: single IP to consider (/32) + :param net: range to consider + :param gw: gateway + :param dev: force the interface to use + :param metric: route metric + + Examples: + + - `ip route add 192.168.1.0/24 via 192.168.0.254`:: + >>> conf.route.add(net="192.168.1.0/24", gw="192.168.0.254") + + - `ip route add 192.168.1.0/24 dev eth0`:: + >>> conf.route.add(net="192.168.1.0/24", dev="eth0") + + - `ip route add 192.168.1.0/24 via 192.168.0.254 metric 1`:: + >>> conf.route.add(net="192.168.1.0/24", gw="192.168.0.254", metric=1) """ self.invalidate_cache() self.routes.append(self.make_route(*args, **kargs)) def delt(self, *args, **kargs): - """delt(host|net, gw|dev)""" + # type: (*Any, **Any) -> None + """Remove a route from Scapy's IPv4 routing table. + delt(host|net, gw|dev) + + Same syntax as add() + """ self.invalidate_cache() route = self.make_route(*args, **kargs) try: i = self.routes.index(route) - del(self.routes[i]) + del self.routes[i] except ValueError: - warning("no matching route found") + raise ValueError("No matching route found!") def ifchange(self, iff, addr): + # type: (str, str) -> None self.invalidate_cache() - the_addr, the_msk = (addr.split("/") + ["32"])[:2] - the_msk = itom(int(the_msk)) + the_addr, the_msk_b = (addr.split("/") + ["32"])[:2] + the_msk = itom(int(the_msk_b)) the_rawaddr = atol(the_addr) the_net = the_rawaddr & the_msk for i, route in enumerate(self.routes): net, msk, gw, iface, addr, metric = route - if scapy.consts.WINDOWS: - if iff.guid != iface.guid: - continue - elif iff != iface: + if iff != iface: continue if gw == '0.0.0.0': self.routes[i] = (the_net, the_msk, gw, iface, the_addr, metric) # noqa: E501 @@ -106,34 +147,36 @@ def ifchange(self, iff, addr): conf.netcache.flush() def ifdel(self, iff): + # type: (str) -> None self.invalidate_cache() new_routes = [] for rt in self.routes: - if scapy.consts.WINDOWS: - if iff.guid == rt[3].guid: - continue - elif iff == rt[3]: + if iff == rt[3]: continue new_routes.append(rt) self.routes = new_routes def ifadd(self, iff, addr): + # type: (str, str) -> None self.invalidate_cache() - the_addr, the_msk = (addr.split("/") + ["32"])[:2] - the_msk = itom(int(the_msk)) + the_addr, the_msk_b = (addr.split("/") + ["32"])[:2] + the_msk = itom(int(the_msk_b)) the_rawaddr = atol(the_addr) the_net = the_rawaddr & the_msk self.routes.append((the_net, the_msk, '0.0.0.0', iff, the_addr, 1)) - def route(self, dst=None, verbose=conf.verb): + def route(self, dst=None, dev=None, verbose=conf.verb, _internal=False): + # type: (Optional[str], Optional[str], int, bool) -> Tuple[str, str, str] """Returns the IPv4 routes to a host. - parameters: - - dst: the IPv4 of the destination host - returns: (iface, output_ip, gateway_ip) - - iface: the interface used to connect to the host - - output_ip: the outgoing IP that will be used - - gateway_ip: the gateway IP that will be used + :param dst: the IPv4 of the destination host + :param dev: (optional) filtering is performed to limit search to route + associated to that interface. + + :returns: tuple (iface, output_ip, gateway_ip) where + - ``iface``: the interface used to connect to the host + - ``output_ip``: the outgoing IP that will be used + - ``gateway_ip``: the gateway IP that will be used """ dst = dst or "0.0.0.0" # Enable route(None) to return default route if isinstance(dst, bytes): @@ -141,8 +184,8 @@ def route(self, dst=None, verbose=conf.verb): dst = plain_str(dst) except UnicodeDecodeError: raise TypeError("Unknown IP address input (bytes)") - if dst in self.cache: - return self.cache[dst] + if (dst, dev) in self.cache: + return self.cache[(dst, dev)] # Transform "192.168.*.1-5" to one IP of the set _dst = dst.split("/")[0].replace("*", "0") while True: @@ -157,51 +200,54 @@ def route(self, dst=None, verbose=conf.verb): for d, m, gw, i, a, me in self.routes: if not a: # some interfaces may not currently be connected continue + if dev is not None and i != dev: + continue aa = atol(a) - if aa == atol_dst: + if aa == atol_dst and aa != 0: paths.append( - (0xffffffff, 1, (scapy.consts.LOOPBACK_INTERFACE, a, "0.0.0.0")) # noqa: E501 + (0xffffffff, 1, (conf.loopback_name, a, "0.0.0.0")) # noqa: E501 ) if (atol_dst & m) == (d & m): paths.append((m, me, (i, a, gw))) if not paths: if verbose: - warning("No route found (no default route?)") - return scapy.consts.LOOPBACK_INTERFACE, "0.0.0.0", "0.0.0.0" + warning("No route found for IPv4 destination %s " + "(no default route?)", dst) + return (dev or conf.loopback_name, "0.0.0.0", "0.0.0.0") # Choose the more specific route # Sort by greatest netmask and use metrics as a tie-breaker paths.sort(key=lambda x: (-x[0], x[1])) # Return interface ret = paths[0][2] - self.cache[dst] = ret + # Check if source is 0.0.0.0. This is a 'via' route with no src. + if ret[1] == "0.0.0.0" and not _internal: + # Then get the source from route(gw) + ret = (ret[0], self.route(ret[2], _internal=True)[1], ret[2]) + self.cache[(dst, dev)] = ret return ret def get_if_bcast(self, iff): - for net, msk, gw, iface, addr, metric in self.routes: + # type: (str) -> List[str] + """ + Return the list of broadcast addresses of an interface. + """ + bcast_list = [] + for net, msk, _, iface, _, _ in self.routes: if net == 0: + continue # Ignore default route "0.0.0.0" + elif msk == 0xffffffff: + continue # Ignore host-specific routes + if iff != iface: continue - if scapy.consts.WINDOWS: - if iff.guid != iface.guid: - continue - elif iff != iface: - continue - bcast = atol(addr) | (~msk & 0xffffffff) # FIXME: check error in atol() # noqa: E501 - return ltoa(bcast) - warning("No broadcast address found for iface %s\n", iff) + bcast = net | (~msk & 0xffffffff) + bcast_list.append(ltoa(bcast)) + if not bcast_list: + warning("No broadcast address found for iface %s\n", iff) + return bcast_list conf.route = Route() -iface = conf.route.route(None, verbose=0)[0] - -# Warning: scapy.consts.LOOPBACK_INTERFACE must always be used statically, because it # noqa: E501 -# may be changed by scapy/arch/windows during execution - -if getattr(iface, "name", iface) == scapy.consts.LOOPBACK_INTERFACE: - from scapy.arch import get_working_if - conf.iface = get_working_if() -else: - conf.iface = iface - -del iface +# Update conf.iface +conf.ifaces.load_confiface() diff --git a/scapy/route6.py b/scapy/route6.py index 3365bc0c66a..4062359fea7 100644 --- a/scapy/route6.py +++ b/scapy/route6.py @@ -1,8 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license - # Copyright (C) 2005 Guillaume Valadon # Arnaud Ebalard @@ -14,10 +13,9 @@ # Routing/Interfaces stuff # ############################################################################# -from __future__ import absolute_import import socket -import scapy.consts from scapy.config import conf +from scapy.interfaces import resolve_iface, NetworkInterface from scapy.utils6 import in6_ptop, in6_cidr2mask, in6_and, \ in6_islladdr, in6_ismlladdr, in6_isincluded, in6_isgladdr, \ in6_isaddr6to4, in6_ismaddr, construct_source_candidate_set, \ @@ -25,25 +23,41 @@ from scapy.arch import read_routes6, in6_getifaddr from scapy.pton_ntop import inet_pton, inet_ntop from scapy.error import warning, log_loading -import scapy.modules.six as six from scapy.utils import pretty_list +from typing import ( + Any, + Dict, + List, + Optional, + Set, + Tuple, + Union, +) + class Route6: def __init__(self): - self.resync() + # type: () -> None + self.routes = [] # type: List[Tuple[str, int, str, str, List[str], int]] # noqa: E501 + self.ipv6_ifaces = set() # type: Set[Union[str, NetworkInterface]] self.invalidate_cache() + if conf.route6_autoload: + self.resync() def invalidate_cache(self): - self.cache = {} + # type: () -> None + self.cache = {} # type: Dict[str, Tuple[str, str, str]] def flush(self): + # type: () -> None self.invalidate_cache() - self.ipv6_ifaces = set() - self.routes = [] + self.routes.clear() + self.ipv6_ifaces.clear() def resync(self): + # type: () -> None # TODO : At the moment, resync will drop existing Teredo routes # if any. Change that ... self.invalidate_cache() @@ -55,14 +69,15 @@ def resync(self): log_loading.info("No IPv6 support in kernel") def __repr__(self): - rtlst = [] + # type: () -> str + rtlst = [] # type: List[Tuple[Union[str, List[str]], ...]] for net, msk, gw, iface, cset, metric in self.routes: + if_repr = resolve_iface(iface).description rtlst.append(('%s/%i' % (net, msk), gw, - (iface if isinstance(iface, six.string_types) - else iface.description), - ", ".join(cset) if len(cset) > 0 else "", + if_repr, + cset, str(metric))) return pretty_list(rtlst, @@ -72,22 +87,25 @@ def __repr__(self): # Unlike Scapy's Route.make_route() function, we do not have 'host' and 'net' # noqa: E501 # parameters. We only have a 'dst' parameter that accepts 'prefix' and # 'prefix/prefixlen' values. - # WARNING: Providing a specific device will at the moment not work correctly. # noqa: E501 - def make_route(self, dst, gw=None, dev=None): + def make_route(self, + dst, # type: str + gw=None, # type: Optional[str] + dev=None, # type: Optional[str] + ): + # type: (...) -> Tuple[str, int, str, str, List[str], int] """Internal function : create a route for 'dst' via 'gw'. """ - prefix, plen = (dst.split("/") + ["128"])[:2] - plen = int(plen) + prefix, plen_b = (dst.split("/") + ["128"])[:2] + plen = int(plen_b) if gw is None: gw = "::" if dev is None: - dev, ifaddr, x = self.route(gw) + dev, ifaddr_uniq, x = self.route(gw) + ifaddr = [ifaddr_uniq] else: - # TODO: do better than that - # replace that unique address by the list of all addresses lifaddr = in6_getifaddr() - devaddrs = [x for x in lifaddr if x[2] == dev] + devaddrs = (x for x in lifaddr if x[2] == dev) ifaddr = construct_source_candidate_set(prefix, plen, devaddrs) self.ipv6_ifaces.add(dev) @@ -95,6 +113,7 @@ def make_route(self, dst, gw=None, dev=None): return (prefix, plen, gw, dev, ifaddr, 1) def add(self, *args, **kargs): + # type: (*Any, **Any) -> None """Ex: add(dst="2001:db8:cafe:f000::/56") add(dst="2001:db8:cafe:f000::/56", gw="2001:db8:cafe::1") @@ -104,6 +123,7 @@ def add(self, *args, **kargs): self.routes.append(self.make_route(*args, **kargs)) def remove_ipv6_iface(self, iface): + # type: (str) -> None """ Remove the network interface 'iface' from the list of interfaces supporting IPv6. @@ -116,15 +136,16 @@ def remove_ipv6_iface(self, iface): pass def delt(self, dst, gw=None): + # type: (str, Optional[str]) -> None """ Ex: delt(dst="::/0") delt(dst="2001:db8:cafe:f000::/56") delt(dst="2001:db8:cafe:f000::/56", gw="2001:db8:deca::1") """ tmp = dst + "/128" - dst, plen = tmp.split('/')[:2] + dst, plen_b = tmp.split('/')[:2] dst = in6_ptop(dst) - plen = int(plen) + plen = int(plen_b) to_del = [x for x in self.routes if in6_ptop(x[0]) == dst and x[1] == plen] if gw: @@ -138,18 +159,19 @@ def delt(self, dst, gw=None): i = self.routes.index(to_del[0]) self.invalidate_cache() self.remove_ipv6_iface(self.routes[i][3]) - del(self.routes[i]) + del self.routes[i] def ifchange(self, iff, addr): - the_addr, the_plen = (addr.split("/") + ["128"])[:2] - the_plen = int(the_plen) + # type: (str, str) -> None + the_addr, the_plen_b = (addr.split("/") + ["128"])[:2] + the_plen = int(the_plen_b) naddr = inet_pton(socket.AF_INET6, the_addr) nmask = in6_cidr2mask(the_plen) the_net = inet_ntop(socket.AF_INET6, in6_and(nmask, naddr)) for i, route in enumerate(self.routes): - net, plen, gw, iface, addr, metric = route + net, plen, gw, iface, _, metric = route if iface != iff: continue @@ -160,9 +182,10 @@ def ifchange(self, iff, addr): else: self.routes[i] = (net, plen, gw, iface, [the_addr], metric) self.invalidate_cache() - conf.netcache.in6_neighbor.flush() + conf.netcache.in6_neighbor.flush() # type: ignore def ifdel(self, iff): + # type: (str) -> None """ removes all route entries that uses 'iff' interface. """ new_routes = [] for rt in self.routes: @@ -173,6 +196,7 @@ def ifdel(self, iff): self.remove_ipv6_iface(iff) def ifadd(self, iff, addr): + # type: (str, str) -> None """ Add an interface 'iff' with provided address into routing table. @@ -185,9 +209,9 @@ def ifadd(self, iff, addr): prefix length value can be omitted. In that case, a value of 128 will be used. """ - addr, plen = (addr.split("/") + ["128"])[:2] + addr, plen_b = (addr.split("/") + ["128"])[:2] addr = in6_ptop(addr) - plen = int(plen) + plen = int(plen_b) naddr = inet_pton(socket.AF_INET6, addr) nmask = in6_cidr2mask(plen) prefix = inet_ntop(socket.AF_INET6, in6_and(nmask, naddr)) @@ -195,7 +219,8 @@ def ifadd(self, iff, addr): self.routes.append((prefix, plen, '::', iff, [addr], 1)) self.ipv6_ifaces.add(iff) - def route(self, dst=None, dev=None, verbose=conf.verb): + def route(self, dst="", dev=None, verbose=conf.verb): + # type: (str, Optional[str], int) -> Tuple[str, str, str] """ Provide best route to IPv6 destination address, based on Scapy internal routing table content. @@ -227,43 +252,14 @@ def route(self, dst=None, dev=None, verbose=conf.verb): dst = socket.getaddrinfo(savedst, None, socket.AF_INET6)[0][-1][0] # TODO : Check if name resolution went well - # Choose a valid IPv6 interface while dealing with link-local addresses - if dev is None and (in6_islladdr(dst) or in6_ismlladdr(dst)): - dev = conf.iface # default interface - - # Check if the default interface supports IPv6! - if dev not in self.ipv6_ifaces and self.ipv6_ifaces: - - tmp_routes = [route for route in self.routes - if route[3] != conf.iface] - - default_routes = [route for route in tmp_routes - if (route[0], route[1]) == ("::", 0)] - - ll_routes = [route for route in tmp_routes - if (route[0], route[1]) == ("fe80::", 64)] - - if default_routes: - # Fallback #1 - the first IPv6 default route - dev = default_routes[0][3] - elif ll_routes: - # Fallback #2 - the first link-local prefix - dev = ll_routes[0][3] - else: - # Fallback #3 - the loopback - dev = scapy.consts.LOOPBACK_INTERFACE - - warning("The conf.iface interface (%s) does not support IPv6! " - "Using %s instead for routing!" % (conf.iface, dev)) - # Deal with dev-specific request for cache search k = dst if dev is not None: - k = dst + "%%" + (dev if isinstance(dev, six.string_types) else dev.pcap_name) # noqa: E501 + k = dst + "%%" + dev if k in self.cache: return self.cache[k] - paths = [] + paths = [] # type: List[Tuple[int, int, Tuple[str, List[str], str]]] # TODO : review all kinds of addresses (scope and *cast) to see # if we are able to cope with everything possible. I'm convinced @@ -279,12 +275,12 @@ def route(self, dst=None, dev=None, verbose=conf.verb): if not paths: if dst == "::1": - return (scapy.consts.LOOPBACK_INTERFACE, "::1", "::") + return (conf.loopback_name, "::1", "::") else: if verbose: warning("No route found for IPv6 destination %s " "(no default route?)", dst) - return (scapy.consts.LOOPBACK_INTERFACE, "::", "::") + return (dev or conf.loopback_name, "::", "::") # Sort with longest prefix first then use metrics as a tie-breaker paths.sort(key=lambda x: (-x[0], x[1])) @@ -292,35 +288,35 @@ def route(self, dst=None, dev=None, verbose=conf.verb): best_plen = (paths[0][0], paths[0][1]) paths = [x for x in paths if (x[0], x[1]) == best_plen] - res = [] - for p in paths: # Here we select best source address for every route - tmp = p[2] - srcaddr = get_source_addr_from_candidate_set(dst, tmp[1]) + res = [] # type: List[Tuple[int, int, Tuple[str, str, str]]] + for path in paths: # we select best source address for every route + tmp_c = path[2] + srcaddr = get_source_addr_from_candidate_set(dst, tmp_c[1]) if srcaddr is not None: - res.append((p[0], p[1], (tmp[0], srcaddr, tmp[2]))) + res.append((path[0], path[1], (tmp_c[0], srcaddr, tmp_c[2]))) if res == []: warning("Found a route for IPv6 destination '%s', but no possible source address.", dst) # noqa: E501 - return (scapy.consts.LOOPBACK_INTERFACE, "::", "::") + return (conf.loopback_name, "::", "::") # Symptom : 2 routes with same weight (our weight is plen) # Solution : # - dst is unicast global. Check if it is 6to4 and we have a source # 6to4 address in those available # - dst is link local (unicast or multicast) and multiple output - # interfaces are available. Take main one (conf.iface6) + # interfaces are available. Take main one (conf.iface) # - if none of the previous or ambiguity persists, be lazy and keep # first one if len(res) > 1: - tmp = [] + tmp = [] # type: List[Tuple[int, int, Tuple[str, str, str]]] if in6_isgladdr(dst) and in6_isaddr6to4(dst): # TODO : see if taking the longest match between dst and # every source addresses would provide better results tmp = [x for x in res if in6_isaddr6to4(x[2][1])] elif in6_ismaddr(dst) or in6_islladdr(dst): # TODO : I'm sure we are not covering all addresses. Check that - tmp = [x for x in res if x[2][0] == conf.iface6] + tmp = [x for x in res if x[2][0] == conf.iface] if tmp: res = tmp @@ -328,14 +324,10 @@ def route(self, dst=None, dev=None, verbose=conf.verb): # Fill the cache (including dev-specific request) k = dst if dev is not None: - k = dst + "%%" + (dev if isinstance(dev, six.string_types) else dev.pcap_name) # noqa: E501 + k = dst + "%%" + dev self.cache[k] = res[0][2] return res[0][2] conf.route6 = Route6() -try: - conf.iface6 = conf.route6.route(None)[0] -except Exception: - pass diff --git a/scapy/scapypipes.py b/scapy/scapypipes.py index 9c9c335c94e..9311eda22f2 100644 --- a/scapy/scapypipes.py +++ b/scapy/scapypipes.py @@ -1,19 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license -from __future__ import print_function +from queue import Queue, Empty import socket import subprocess -from scapy.modules.six.moves.queue import Queue, Empty -from scapy.pipetool import Source, Drain, Sink +from scapy.automaton import ObjectPipe from scapy.config import conf from scapy.compat import raw +from scapy.interfaces import _GlobInterfaceType +from scapy.packet import Packet +from scapy.pipetool import Source, Drain, Sink from scapy.utils import ContextManagerSubprocess, PcapReader, PcapWriter -from scapy.automaton import recv_error -from scapy.consts import WINDOWS + +from scapy.supersocket import SuperSocket +from typing import ( + Any, + Callable, + List, + Optional, + cast, +) class SniffSource(Source): @@ -37,37 +46,45 @@ class SniffSource(Source): :param socket: A ``SuperSocket`` to sniff packets from. """ - def __init__(self, iface=None, filter=None, socket=None, name=None): + def __init__(self, + iface=None, # type: Optional[str] + filter=None, # type: Optional[Any] + socket=None, # type: Optional[SuperSocket] + name=None, # type: Optional[Any] + ): + # type: (...) -> None Source.__init__(self, name=name) if (iface or filter) and socket: raise ValueError("iface and filter options are mutually exclusive " "with socket") - self.s = socket + self.s = cast(SuperSocket, socket) self.iface = iface self.filter = filter def start(self): + # type: () -> None if not self.s: self.s = conf.L2listen(iface=self.iface, filter=self.filter) def stop(self): + # type: () -> None if self.s: self.s.close() def fileno(self): + # type: () -> int return self.s.fileno() - def check_recv(self): - return True - def deliver(self): + # type: () -> None try: - self._send(self.s.recv()) - except recv_error: - if not WINDOWS: - raise + pkt = self.s.recv() + if pkt is not None: + self._send(pkt) + except EOFError: + self.is_exhausted = True class RdpcapSource(Source): @@ -83,24 +100,26 @@ class RdpcapSource(Source): """ def __init__(self, fname, name=None): + # type: (str, Optional[Any]) -> None Source.__init__(self, name=name) self.fname = fname self.f = PcapReader(self.fname) def start(self): + # type: () -> None self.f = PcapReader(self.fname) self.is_exhausted = False def stop(self): + # type: () -> None self.f.close() def fileno(self): + # type: () -> int return self.f.fileno() - def check_recv(self): - return True - def deliver(self): + # type: () -> None try: p = self.f.recv() self._send(p) @@ -121,28 +140,41 @@ class InjectSink(Sink): """ def __init__(self, iface=None, name=None): + # type: (Optional[_GlobInterfaceType], Optional[str]) -> None Sink.__init__(self, name=name) if iface is None: iface = conf.iface self.iface = iface def start(self): + # type: () -> None self.s = conf.L2socket(iface=self.iface) def stop(self): + # type: () -> None self.s.close() def push(self, msg): + # type: (Packet) -> None self.s.send(msg) class Inject3Sink(InjectSink): def start(self): + # type: () -> None self.s = conf.L3socket(iface=self.iface) class WrpcapSink(Sink): - """Packets received on low input are written to PCAP file + """ + Writes :py:class:`Packet` on the low entry to a ``pcap`` file. + Ignores all messages on the high entry. + + .. note:: + + Due to limitations of the ``pcap`` format, all packets **must** be of + the same link type. This class will not mutate packets to conform with + the expected link type. .. code:: @@ -151,29 +183,62 @@ class WrpcapSink(Sink): | | >-|--[pcap] |-> +----------+ + + :param fname: Filename to write packets to. + :type fname: str + :param linktype: See :py:attr:`linktype`. + :type linktype: None or int + + .. py:attribute:: linktype + + Set an explicit link-type (``DLT_``) for packets. This must be an + ``int`` or ``None``. + + This is the same as the :py:func:`wrpcap` ``linktype`` parameter. + + If ``None`` (the default), the linktype will be auto-detected on the + first packet. This field will *not* be updated with the result of this + auto-detection. + + This attribute has no effect after calling :py:meth:`PipeEngine.start`. """ - def __init__(self, fname, name=None, linktype=None): + def __init__(self, fname, name=None, linktype=None, **kwargs): + # type: (str, Optional[str], Optional[int], **Any) -> None Sink.__init__(self, name=name) self.fname = fname - self.f = None + self.f = None # type: Optional[PcapWriter] self.linktype = linktype + self.kwargs = kwargs def start(self): - self.f = PcapWriter(self.fname, linktype=self.linktype) + # type: () -> None + self.f = PcapWriter(self.fname, linktype=self.linktype, **self.kwargs) def stop(self): + # type: () -> None if self.f: self.f.flush() self.f.close() def push(self, msg): - if msg: + # type: (Packet) -> None + if msg and self.f: self.f.write(msg) class WiresharkSink(WrpcapSink): - """Packets received on low input are pushed to Wireshark. + """ + Streams :py:class:`Packet` from the low entry to Wireshark. + + Packets are written into a ``pcap`` stream (like :py:class:`WrpcapSink`), + and streamed to a new Wireshark process on its ``stdin``. + + Wireshark is run with the ``-ki -`` arguments, which cause it to treat + ``stdin`` as a capture device. Arguments in :py:attr:`args` will be + appended after this. + + Extends :py:mod:`WrpcapSink`. .. code:: @@ -182,17 +247,34 @@ class WiresharkSink(WrpcapSink): | | >-|--[pcap] |-> +----------+ + + :param linktype: See :py:attr:`WrpcapSink.linktype`. + :type linktype: None or int + :param args: See :py:attr:`args`. + :type args: None or list[str] + + .. py:attribute:: args + + Additional arguments for the Wireshark process. + + This must be either ``None`` (the default), or a ``list`` of ``str``. + + This attribute has no effect after calling :py:meth:`PipeEngine.start`. + + See :manpage:`wireshark(1)` for more details. """ def __init__(self, name=None, linktype=None, args=None): - WrpcapSink.__init__(self, fname=None, name=name, linktype=linktype) + # type: (Optional[Any], Optional[int], Optional[List[str]]) -> None + WrpcapSink.__init__(self, fname="", name=name, linktype=linktype) self.args = args def start(self): + # type: () -> None # Wireshark must be running first, because PcapWriter will block until # data has been read! with ContextManagerSubprocess(conf.prog.wireshark): - args = [conf.prog.wireshark, "-ki", "-"] + args = [conf.prog.wireshark, "-Slki", "-"] if self.args: args.extend(self.args) @@ -203,7 +285,7 @@ def start(self): stderr=None, ) - self.fname = proc.stdin + self.fname = proc.stdin # type: ignore WrpcapSink.start(self) @@ -220,17 +302,20 @@ class UDPDrain(Drain): """ def __init__(self, ip="127.0.0.1", port=1234): + # type: (str, int) -> None Drain.__init__(self) self.ip = ip self.port = port def push(self, msg): + # type: (Packet) -> None from scapy.layers.inet import IP, UDP if IP in msg and msg[IP].proto == 17 and UDP in msg: payload = msg[UDP].payload self._high_send(raw(payload)) def high_push(self, msg): + # type: (Packet) -> None from scapy.layers.inet import IP, UDP p = IP(dst=self.ip) / UDP(sport=1234, dport=self.port) / msg self._send(p) @@ -249,16 +334,20 @@ class FDSourceSink(Source): """ def __init__(self, fd, name=None): + # type: (ObjectPipe[Any], Optional[Any]) -> None Source.__init__(self, name=name) self.fd = fd def push(self, msg): + # type: (str) -> None self.fd.write(msg) def fileno(self): + # type: () -> int return self.fd.fileno() def deliver(self): + # type: () -> None self._send(self.fd.read()) @@ -276,26 +365,32 @@ class TCPConnectPipe(Source): __selectable_force_select__ = True def __init__(self, addr="", port=0, name=None): + # type: (str, int, Optional[str]) -> None Source.__init__(self, name=name) self.addr = addr self.port = port - self.fd = None + self.fd = cast(socket.socket, None) def start(self): + # type: () -> None self.fd = socket.socket() self.fd.connect((self.addr, self.port)) def stop(self): + # type: () -> None if self.fd: self.fd.close() def push(self, msg): + # type: (bytes) -> None self.fd.send(msg) def fileno(self): + # type: () -> int return self.fd.fileno() def deliver(self): + # type: () -> None try: msg = self.fd.recv(65536) except socket.error: @@ -320,11 +415,13 @@ class TCPListenPipe(TCPConnectPipe): __selectable_force_select__ = True def __init__(self, addr="", port=0, name=None): + # type: (str, int, Optional[str]) -> None TCPConnectPipe.__init__(self, addr, port, name) self.connected = False - self.q = Queue() + self.q: Queue[Any] = Queue() def start(self): + # type: () -> None self.connected = False self.fd = socket.socket() self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -332,12 +429,14 @@ def start(self): self.fd.listen(1) def push(self, msg): + # type: (bytes) -> None if self.connected: self.fd.send(msg) else: self.q.put(msg) def deliver(self): + # type: () -> None if self.connected: try: msg = self.fd.recv(65536) @@ -360,6 +459,102 @@ def deliver(self): break +class UDPClientPipe(TCPConnectPipe): + """UDP send packets to addr:port and use it as source and sink + Start trying to receive only once a packet has been send + + .. code:: + + +-------------+ + >>-| |->> + | | + >-|-[addr:port]-|-> + +-------------+ + """ + + def __init__(self, addr="", port=0, name=None): + # type: (str, int, Optional[str]) -> None + TCPConnectPipe.__init__(self, addr, port, name) + self.connected = False + + def start(self): + # type: () -> None + self.fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.fd.connect((self.addr, self.port)) + self.connected = True + + def push(self, msg): + # type: (bytes) -> None + self.fd.send(msg) + + def deliver(self): + # type: () -> None + if not self.connected: + return + try: + msg = self.fd.recv(65536) + except socket.error: + self.stop() + raise + if msg: + self._send(msg) + + +class UDPServerPipe(TCPListenPipe): + """UDP bind to [addr:]port and use as source and sink + Use (ip, port) from first received IP packet as destination for all data + + .. code:: + + +------^------+ + >>-| +-[peer]-|->> + | / | + >-|-[addr:port]-|-> + +-------------+ + """ + + def __init__(self, addr="", port=0, name=None): + # type: (str, int, Optional[str]) -> None + TCPListenPipe.__init__(self, addr, port, name) + self._destination = None # type: Any + + def start(self): + # type: () -> None + self.fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.fd.bind((self.addr, self.port)) + + def push(self, msg): + # type: (bytes) -> None + if self._destination: + self.fd.sendto(msg, self._destination) + else: + self.q.put(msg) + + def deliver(self): + # type: () -> None + if self._destination: + try: + msg = self.fd.recv(65536) + except socket.error: + self.stop() + raise + if msg: + self._send(msg) + else: + msg, dest = self.fd.recvfrom(65536) + if msg: + self._send(msg) + self._destination = dest + self._trigger(dest) + self._high_send(dest) + while True: + try: + msg = self.q.get(block=False) + self.fd.sendto(msg, self._destination) + except Empty: + break + + class TriggeredMessage(Drain): """Send a preloaded message when triggered and trigger in chain @@ -373,10 +568,12 @@ class TriggeredMessage(Drain): """ def __init__(self, msg, name=None): + # type: (str, Optional[Any]) -> None Drain.__init__(self, name=name) self.msg = msg def on_trigger(self, trigmsg): + # type: (bool) -> None self._send(self.msg) self._high_send(self.msg) self._trigger(trigmsg) @@ -395,16 +592,19 @@ class TriggerDrain(Drain): """ def __init__(self, f, name=None): + # type: (Callable[..., None], Optional[str]) -> None Drain.__init__(self, name=name) self.f = f def push(self, msg): + # type: (str) -> None v = self.f(msg) if v: self._trigger(v) self._send(msg) def high_push(self, msg): + # type: (str) -> None v = self.f(msg) if v: self._trigger(v) @@ -424,18 +624,22 @@ class TriggeredValve(Drain): """ def __init__(self, start_state=True, name=None): + # type: (bool, Optional[Any]) -> None Drain.__init__(self, name=name) self.opened = start_state def push(self, msg): + # type: (str) -> None if self.opened: self._send(msg) def high_push(self, msg): + # type: (str) -> None if self.opened: self._high_send(msg) def on_trigger(self, msg): + # type: (bool) -> None self.opened ^= True self._trigger(msg) @@ -453,26 +657,31 @@ class TriggeredQueueingValve(Drain): """ def __init__(self, start_state=True, name=None): + # type: (bool, Optional[Any]) -> None Drain.__init__(self, name=name) self.opened = start_state - self.q = Queue() + self.q: Queue[Any] = Queue() def start(self): + # type: () -> None self.q = Queue() def push(self, msg): + # type: (str) -> None if self.opened: self._send(msg) else: self.q.put((True, msg)) def high_push(self, msg): + # type: (str) -> None if self.opened: self._send(msg) else: self.q.put((False, msg)) def on_trigger(self, msg): + # type: (bool) -> None self.opened ^= True self._trigger(msg) while True: @@ -500,10 +709,12 @@ class TriggeredSwitch(Drain): """ def __init__(self, start_state=True, name=None): + # type: (bool, Optional[Any]) -> None Drain.__init__(self, name=name) self.low = start_state def push(self, msg): + # type: (str) -> None if self.low: self._send(msg) else: @@ -511,34 +722,6 @@ def push(self, msg): high_push = push def on_trigger(self, msg): + # type: (bool) -> None self.low ^= True self._trigger(msg) - - -class ConvertPipe(Drain): - """Packets sent on entry are converted to another type of packet. - - .. code:: - - +-------------+ - >>-|--[convert]--|->> - | | - >-|--[convert]--|-> - +-------------+ - - See ``Packet.convert_packet``. - """ - def __init__(self, low_type=None, high_type=None, name=None): - Drain.__init__(self, name=name) - self.low_type = low_type - self.high_type = high_type - - def push(self, msg): - if self.low_type: - msg = self.low_type.convert_packet(msg) - self._send(msg) - - def high_push(self, msg): - if self.high_type: - msg = self.high_type.convert_packet(msg) - self._high_send(msg) diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index fd2f11da238..2ab413eb2c7 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -1,37 +1,62 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Functions to send and receive packets. """ -from __future__ import absolute_import, print_function import itertools from threading import Thread, Event import os import re +import socket import subprocess import time -import types +import warnings from scapy.compat import plain_str from scapy.data import ETH_P_ALL from scapy.config import conf from scapy.error import warning -from scapy.packet import Gen, Packet +from scapy.interfaces import ( + network_name, + resolve_iface, + NetworkInterface, +) +from scapy.packet import Packet +from scapy.pton_ntop import inet_pton from scapy.utils import get_temp_file, tcpdump, wrpcap, \ - ContextManagerSubprocess, PcapReader -from scapy.plist import PacketList, SndRcvList + ContextManagerSubprocess, PcapReader, EDecimal +from scapy.plist import ( + PacketList, + QueryAnswer, + SndRcvList, +) from scapy.error import log_runtime, log_interactive, Scapy_Exception -from scapy.base_classes import SetGen -from scapy.modules import six -from scapy.modules.six.moves import map +from scapy.base_classes import Gen, SetGen from scapy.sessions import DefaultSession -from scapy.supersocket import SuperSocket +from scapy.supersocket import SuperSocket, IterSocket + +# Typing imports +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Tuple, + Type, + Union, + cast +) +from scapy.interfaces import _GlobInterfaceType +from scapy.plist import _PacketIterable + if conf.route is None: - # unused import, only to initialize conf.route + # unused import, only to initialize conf.route and conf.iface* import scapy.route # noqa: F401 ################# @@ -40,10 +65,10 @@ class debug: - recv = [] - sent = [] - match = [] - crashed_on = None + recv = PacketList([], "Received") + sent = PacketList([], "Sent") + match = SndRcvList([], "Matched") + crashed_on = None # type: Optional[Tuple[Type[Packet], bytes]] #################### @@ -53,29 +78,32 @@ class debug: _DOC_SNDRCV_PARAMS = """ :param pks: SuperSocket instance to send/receive packets :param pkt: the packet to send - :param rcv_pks: if set, will be used instead of pks to receive packets. - packets will still be sent through pks - :param nofilter: put 1 to avoid use of BPF filters + :param timeout: how much time to wait after the last packet has been sent + :param inter: delay between two packets during sending + :param verbose: set verbosity level + :param chainCC: if True, KeyboardInterrupts will be forwarded :param retry: if positive, how many times to resend unanswered packets if negative, how many times to retry when no more packets are answered - :param timeout: how much time to wait after the last packet has been sent - :param verbose: set verbosity level :param multi: whether to accept multiple answers for the same stimulus - :param store_unanswered: whether to store not-answered packets or not. - setting it to False will increase speed, and will return - None as the unans list. - :param process: if specified, only result from process(pkt) will be stored. - the function should follow the following format: - ``lambda sent, received: (func(sent), func2(received))`` - if the packet is unanswered, `received` will be None. - if `store_unanswered` is False, the function won't be called on - un-answered packets. + :param first: stop after receiving the first response of any sent packet + :param rcv_pks: if set, will be used instead of pks to receive packets. + packets will still be sent through pks :param prebuild: pre-build the packets before starting to send them. Automatically enabled when a generator is passed as the packet + :param _flood: + :param threaded: if True, packets are sent in a thread and received in another. + Defaults to True. + :param session: a flow decoder used to handle stream of packets + :param chainEX: if True, exceptions during send will be forwarded + :param stop_filter: Python function applied to each packet to determine if + we have to stop the capture after this packet. """ +_GlobSessionType = Union[Type[DefaultSession], DefaultSession] + + class SndRcvHandler(object): """ Util to send/receive packets, used by sr*(). @@ -84,19 +112,31 @@ class SndRcvHandler(object): This matches the requests and answers. Notes:: - - threaded mode: enabling threaded mode will likely - break packet timestamps, but might result in a speedup - when sending a big amount of packets. Disabled by default + - threaded: if you're planning to send/receive many packets, it's likely + a good idea to use threaded mode. - DEVS: store the outgoing timestamp right BEFORE sending the packet to avoid races that could result in negative latency. We aren't Stadia """ - def __init__(self, pks, pkt, - timeout=None, inter=0, verbose=None, - chainCC=False, - retry=0, multi=False, rcv_pks=None, - prebuild=False, _flood=None, - threaded=False, - session=None): + def __init__(self, + pks, # type: SuperSocket + pkt, # type: _PacketIterable + timeout=None, # type: Optional[int] + inter=0, # type: int + verbose=None, # type: Optional[int] + chainCC=False, # type: bool + retry=0, # type: int + multi=False, # type: bool + first=False, # type: bool + rcv_pks=None, # type: Optional[SuperSocket] + prebuild=False, # type: bool + _flood=None, # type: Optional[_FloodGenerator] + threaded=True, # type: bool + session=None, # type: Optional[_GlobSessionType] + chainEX=False, # type: bool + stop_filter=None, # type: Optional[Callable[[Packet], bool]] + **send_kwargs, # type: Any + ): + # type: (...) -> None # Instantiate all arguments if verbose is None: verbose = conf.verb @@ -105,7 +145,7 @@ def __init__(self, pks, pkt, debug.sent = PacketList([], "Sent") debug.match = SndRcvList([], "Matched") self.nbrecv = 0 - self.ans = [] + self.ans = [] # type: List[QueryAnswer] self.pks = pks self.rcv_pks = rcv_pks or pks self.inter = inter @@ -113,20 +153,22 @@ def __init__(self, pks, pkt, self.chainCC = chainCC self.multi = multi self.timeout = timeout + self.first = first self.session = session + self.chainEX = chainEX + self.stop_filter = stop_filter + self._send_done = False + self.notans = 0 + self.noans = 0 + self._flood = _flood + self.threaded = threaded + self.breakout = Event() + self.send_kwargs = send_kwargs # Instantiate packet holders - if _flood: - self.tobesent = pkt - self.notans = _flood[0] + if prebuild and not self._flood: + self.tobesent = list(pkt) # type: _PacketIterable else: - if isinstance(pkt, types.GeneratorType) or prebuild: - self.tobesent = [p for p in pkt] - self.notans = len(self.tobesent) - else: - self.tobesent = ( - SetGen(pkt) if not isinstance(pkt, Gen) else pkt - ) - self.notans = self.tobesent.__iterlen__() + self.tobesent = pkt if retry < 0: autostop = retry = -retry @@ -137,34 +179,48 @@ def __init__(self, pks, pkt, self.timeout = None while retry >= 0: - self.hsent = {} + self.breakout.clear() + self.hsent = {} # type: Dict[bytes, List[Packet]] - if threaded or _flood: + if threaded or self._flood: # Send packets in thread. - # https://github.com/secdev/scapy/issues/1791 snd_thread = Thread( target=self._sndrcv_snd ) - snd_thread.setDaemon(True) + snd_thread.daemon = True # Start routine with callback - self._sndrcv_rcv(snd_thread.start) + interrupted = None + try: + self._sndrcv_rcv(snd_thread.start) + except KeyboardInterrupt as ex: + interrupted = ex + + self.breakout.set() # Ended. Let's close gracefully - if _flood: + if self._flood: # Flood: stop send thread - _flood[1]() + self._flood.stop() snd_thread.join() + + if interrupted and self.chainCC: + raise interrupted else: - self._sndrcv_rcv(self._sndrcv_snd) + # Send packets, then receive. + try: + self._sndrcv_rcv(self._sndrcv_snd) + except KeyboardInterrupt: + if self.chainCC: + raise if multi: remain = [ - p for p in itertools.chain(*six.itervalues(self.hsent)) + p for p in itertools.chain(*self.hsent.values()) if not hasattr(p, '_answered') ] else: - remain = list(itertools.chain(*six.itervalues(self.hsent))) + remain = list(itertools.chain(*self.hsent.values())) if autostop and len(remain) > 0 and \ len(remain) != len(self.tobesent): @@ -189,7 +245,8 @@ def __init__(self, pks, pkt, print( "\nReceived %i packets, got %i answers, " "remaining %i packets" % ( - self.nbrecv + len(self.ans), len(self.ans), self.notans + self.nbrecv + len(self.ans), len(self.ans), + max(0, self.notans - self.noans) ) ) @@ -197,31 +254,66 @@ def __init__(self, pks, pkt, self.unans_result = PacketList(remain, "Unanswered") def results(self): + # type: () -> Tuple[SndRcvList, PacketList] return self.ans_result, self.unans_result + def _stop_sniffer_if_done(self) -> None: + """Close the sniffer if all expected answers have been received""" + if ( + self._send_done and self.noans >= self.notans and not self.multi or + self.first and self.noans + ): + if self.sniffer and self.sniffer.running: + self.sniffer.stop(join=False) + def _sndrcv_snd(self): + # type: () -> None """Function used in the sending thread of sndrcv()""" + i = 0 + p = None try: if self.verbose: - print("Begin emission:") - i = 0 + os.write(1, b"Begin emission\n") for p in self.tobesent: # Populate the dictionary of _sndrcv_rcv # _sndrcv_rcv won't miss the answer of a packet that # has not been sent self.hsent.setdefault(p.hashret(), []).append(p) # Send packet - self.pks.send(p) + self.pks.send(p, **self.send_kwargs) time.sleep(self.inter) + if self.breakout.is_set(): + break i += 1 if self.verbose: - print("Finished sending %i packets." % i) + os.write(1, b"\nFinished sending %i packets\n" % i) except SystemExit: pass except Exception: - log_runtime.exception("--- Error sending packets") + if self.chainEX: + raise + else: + log_runtime.exception("--- Error sending packets") + finally: + try: + cast(Packet, self.tobesent).sent_time = \ + cast(Packet, p).sent_time + except AttributeError: + pass + if self._flood: + self.notans = self._flood.iterlen + elif not self._send_done: + self.notans = i + self._send_done = True + self._stop_sniffer_if_done() + # In threaded mode, timeout + if self.threaded and self.timeout is not None and not self.breakout.is_set(): + self.breakout.wait(timeout=self.timeout) + if self.sniffer and self.sniffer.running: + self.sniffer.stop() def _process_packet(self, r): + # type: (Packet) -> None """Internal function used to process each packet.""" if r is None: return @@ -231,20 +323,19 @@ def _process_packet(self, r): hlst = self.hsent[h] for i, sentpkt in enumerate(hlst): if r.answers(sentpkt): - self.ans.append((sentpkt, r)) + self.ans.append(QueryAnswer(sentpkt, r)) if self.verbose > 1: os.write(1, b"*") ok = True if not self.multi: del hlst[i] - self.notans -= 1 + self.noans += 1 else: if not hasattr(sentpkt, '_answered'): - self.notans -= 1 + self.noans += 1 sentpkt._answered = 1 break - if self.notans <= 0 and not self.multi: - self.sniffer.stop(join=False) + self._stop_sniffer_if_done() if not ok: if self.verbose > 1: os.write(1, b".") @@ -253,24 +344,25 @@ def _process_packet(self, r): debug.recv.append(r) def _sndrcv_rcv(self, callback): + # type: (Callable[[], None]) -> None """Function used to receive packets and check their hashret""" - self.sniffer = None - try: - self.sniffer = AsyncSniffer() - self.sniffer._run( - prn=self._process_packet, - timeout=self.timeout, - store=False, - opened_socket=self.pks, - session=self.session, - started_callback=callback - ) - except KeyboardInterrupt: - if self.chainCC: - raise + # This is blocking. + self.sniffer = None # type: Optional[AsyncSniffer] + self.sniffer = AsyncSniffer() + self.sniffer._run( + prn=self._process_packet, + timeout=None if self.threaded and not self._flood else self.timeout, + store=False, + opened_socket=self.rcv_pks, + session=self.session, + stop_filter=self.stop_filter, + started_callback=callback, + chainCC=True, + ) def sndrcv(*args, **kwargs): + # type: (*Any, **Any) -> Tuple[SndRcvList, PacketList] """Scapy raw function to send a packet and receive its answer. WARNING: This is an internal function. Using sr/srp/sr1/srp is more appropriate in many cases. @@ -279,7 +371,24 @@ def sndrcv(*args, **kwargs): return sndrcver.results() -def __gen_send(s, x, inter=0, loop=0, count=None, verbose=None, realtime=None, return_packets=False, *args, **kargs): # noqa: E501 +def __gen_send(s, # type: SuperSocket + x, # type: _PacketIterable + inter=0, # type: int + loop=0, # type: int + count=None, # type: Optional[int] + verbose=None, # type: Optional[int] + realtime=False, # type: bool + return_packets=False, # type: bool + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Optional[PacketList] + """ + An internal function used by send/sendp to actually send the packets, + implement the send logic... + + It will take care of iterating through the different packets + """ if isinstance(x, str): x = conf.raw_layer(load=x) if not isinstance(x, Gen): @@ -291,8 +400,8 @@ def __gen_send(s, x, inter=0, loop=0, count=None, verbose=None, realtime=None, r loop = -count elif not loop: loop = -1 - if return_packets: - sent_packets = PacketList() + sent_packets = PacketList() if return_packets else None + p = None try: while loop: dt0 = None @@ -306,7 +415,7 @@ def __gen_send(s, x, inter=0, loop=0, count=None, verbose=None, realtime=None, r else: dt0 = ct - float(p.time) s.send(p) - if return_packets: + if sent_packets is not None: sent_packets.append(p) n += 1 if verbose: @@ -316,53 +425,96 @@ def __gen_send(s, x, inter=0, loop=0, count=None, verbose=None, realtime=None, r loop += 1 except KeyboardInterrupt: pass + finally: + try: + cast(Packet, x).sent_time = cast(Packet, p).sent_time + except AttributeError: + pass if verbose: print("\nSent %i packets." % n) - if return_packets: - return sent_packets + return sent_packets + + +def _send(x, # type: _PacketIterable + _func, # type: Callable[[NetworkInterface], Type[SuperSocket]] + inter=0, # type: int + loop=0, # type: int + iface=None, # type: Optional[_GlobInterfaceType] + count=None, # type: Optional[int] + verbose=None, # type: Optional[int] + realtime=False, # type: bool + return_packets=False, # type: bool + socket=None, # type: Optional[SuperSocket] + **kargs # type: Any + ): + # type: (...) -> Optional[PacketList] + """Internal function used by send and sendp""" + need_closing = socket is None + iface = resolve_iface(iface or conf.iface) + socket = socket or _func(iface)(iface=iface, **kargs) + results = __gen_send(socket, x, inter=inter, loop=loop, + count=count, verbose=verbose, + realtime=realtime, return_packets=return_packets) + if need_closing: + socket.close() + return results @conf.commands.register -def send(x, inter=0, loop=0, count=None, - verbose=None, realtime=None, - return_packets=False, socket=None, *args, **kargs): +def send(x, # type: _PacketIterable + **kargs # type: Any + ): + # type: (...) -> Optional[PacketList] """ Send packets at layer 3 + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 + :param x: the packets :param inter: time (in s) between two packets (default 0) - :param loop: send packet indefinetly (default 0) + :param loop: send packet indefinitely (default 0) :param count: number of packets to send (default None=1) - :param verbose: verbose mode (default None=conf.verbose) + :param verbose: verbose mode (default None=conf.verb) :param realtime: check that a packet was sent before sending the next one :param return_packets: return the sent packets :param socket: the socket to use (default is conf.L3socket(kargs)) - :param iface: the interface to send the packets on :param monitor: (not on linux) send in monitor mode :returns: None """ - need_closing = socket is None - socket = socket or conf.L3socket(*args, **kargs) - results = __gen_send(socket, x, inter=inter, loop=loop, - count=count, verbose=verbose, - realtime=realtime, return_packets=return_packets) - if need_closing: - socket.close() - return results + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O send(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) + return _send( + x, + lambda iface: iface.l3socket(ipv6), + iface=iface, + **kargs + ) @conf.commands.register -def sendp(x, inter=0, loop=0, iface=None, iface_hint=None, count=None, - verbose=None, realtime=None, - return_packets=False, socket=None, *args, **kargs): +def sendp(x, # type: _PacketIterable + iface=None, # type: Optional[_GlobInterfaceType] + iface_hint=None, # type: Optional[str] + socket=None, # type: Optional[SuperSocket] + **kargs # type: Any + ): + # type: (...) -> Optional[PacketList] """ Send packets at layer 2 :param x: the packets :param inter: time (in s) between two packets (default 0) - :param loop: send packet indefinetly (default 0) + :param loop: send packet indefinitely (default 0) :param count: number of packets to send (default None=1) - :param verbose: verbose mode (default None=conf.verbose) + :param verbose: verbose mode (default None=conf.verb) :param realtime: check that a packet was sent before sending the next one :param return_packets: return the sent packets :param socket: the socket to use (default is conf.L3socket(kargs)) @@ -372,25 +524,35 @@ def sendp(x, inter=0, loop=0, iface=None, iface_hint=None, count=None, """ if iface is None and iface_hint is not None and socket is None: iface = conf.route.route(iface_hint)[0] - need_closing = socket is None - socket = socket or conf.L2socket(iface=iface, *args, **kargs) - results = __gen_send(socket, x, inter=inter, loop=loop, - count=count, verbose=verbose, - realtime=realtime, return_packets=return_packets) - if need_closing: - socket.close() - return results + return _send( + x, + lambda iface: iface.l2socket(), + iface=iface, + socket=socket, + **kargs + ) @conf.commands.register -def sendpfast(x, pps=None, mbps=None, realtime=None, loop=0, file_cache=False, iface=None, replay_args=None, # noqa: E501 - parse_results=False): +def sendpfast(x: _PacketIterable, + pps: Optional[float] = None, + mbps: Optional[float] = None, + realtime: bool = False, + count: Optional[int] = None, + loop: int = 0, + file_cache: bool = False, + iface: Optional[_GlobInterfaceType] = None, + replay_args: Optional[List[str]] = None, + parse_results: bool = False, + ): + # type: (...) -> Optional[Dict[str, Any]] """Send packets at layer 2 using tcpreplay for performance :param pps: packets per second - :param mpbs: MBits per second + :param mbps: MBits per second :param realtime: use packet's timestamp, bending time with real-time value - :param loop: number of times to process the packet list + :param loop: send the packet indefinitely (default 0) + :param count: number of packets to send (default None=1) :param file_cache: cache packets in RAM instead of reading from disk at each iteration :param iface: output interface @@ -401,9 +563,9 @@ def sendpfast(x, pps=None, mbps=None, realtime=None, loop=0, file_cache=False, i """ if iface is None: iface = conf.iface - argv = [conf.prog.tcpreplay, "--intf1=%s" % iface] + argv = [conf.prog.tcpreplay, "--intf1=%s" % network_name(iface)] if pps is not None: - argv.append("--pps=%i" % pps) + argv.append("--pps=%f" % pps) elif mbps is not None: argv.append("--mbps=%f" % mbps) elif realtime is not None: @@ -411,8 +573,11 @@ def sendpfast(x, pps=None, mbps=None, realtime=None, loop=0, file_cache=False, i else: argv.append("--topspeed") - if loop: - argv.append("--loop=%i" % loop) + if count: + assert not loop, "Can't use loop and count at the same time in sendpfast" + argv.append("--loop=%i" % count) + elif loop: + argv.append("--loop=0") if file_cache: argv.append("--preload-pcap") @@ -428,12 +593,15 @@ def sendpfast(x, pps=None, mbps=None, realtime=None, loop=0, file_cache=False, i try: cmd = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd.wait() except KeyboardInterrupt: + if cmd: + cmd.terminate() log_interactive.info("Interrupted by user") except Exception: os.unlink(f) raise - else: + finally: stdout, stderr = cmd.communicate() if stderr: log_runtime.warning(stderr.decode()) @@ -441,11 +609,13 @@ def sendpfast(x, pps=None, mbps=None, realtime=None, loop=0, file_cache=False, i results = _parse_tcpreplay_result(stdout, stderr, argv) elif conf.verb > 2: log_runtime.info(stdout.decode()) - os.unlink(f) + if os.path.exists(f): + os.unlink(f) return results -def _parse_tcpreplay_result(stdout, stderr, argv): +def _parse_tcpreplay_result(stdout_b, stderr_b, argv): + # type: (bytes, bytes, List[str]) -> Dict[str, Any] """ Parse the output of tcpreplay and modify the results_dict to populate output information. # noqa: E501 Tested with tcpreplay v3.4.4 @@ -457,8 +627,8 @@ def _parse_tcpreplay_result(stdout, stderr, argv): """ try: results = {} - stdout = plain_str(stdout).lower() - stderr = plain_str(stderr).strip().split("\n") + stdout = plain_str(stdout_b).lower() + stderr = plain_str(stderr_b).strip().split("\n") elements = { "actual": (int, int, float), "rated": (float, float, float), @@ -489,70 +659,113 @@ def _parse_tcpreplay_result(stdout, stderr, argv): matches = re.search(regex, line) for i, typ in enumerate(_types): name = multi.get(elt, [elt])[i] - results[name] = typ(matches.group(i + 1)) + if matches: + results[name] = typ(matches.group(i + 1)) results["command"] = " ".join(argv) results["warnings"] = stderr[:-1] return results except Exception as parse_exception: if not conf.interactive: raise - log_runtime.error("Error parsing output: " + str(parse_exception)) + log_runtime.error("Error parsing output: %s", parse_exception) return {} +def _interface_selection(packet: _PacketIterable) -> Tuple[NetworkInterface, bool]: + """ + Select the network interface according to the layer 3 destination + """ + _iff, src, _ = next(packet.__iter__()).route() + ipv6 = False + if src: + try: + inet_pton(socket.AF_INET6, src) + ipv6 = True + except (ValueError, OSError): + pass + try: + iff = resolve_iface(_iff or conf.iface) + except AttributeError: + iff = None + return iff or conf.iface, ipv6 + + @conf.commands.register -def sr(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs): +def sr(x, # type: _PacketIterable + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + nofilter=0, # type: int + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Tuple[SndRcvList, PacketList] """ Send and receive packets at layer 3 + + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 """ - s = conf.L3socket(promisc=promisc, filter=filter, - iface=iface, nofilter=nofilter) + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O sr(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) + s = iface.l3socket(ipv6)( + promisc=promisc, filter=filter, + iface=iface, nofilter=nofilter, + ) result = sndrcv(s, x, *args, **kargs) s.close() return result -def _interface_selection(iface, packet): - """ - Select the network interface according to the layer 3 destination - """ - - if iface is None: - try: - iff = packet.route()[0] - except AttributeError: - iff = None - return iff or conf.iface - - return iface - - @conf.commands.register -def sr1(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs): +def sr1(*args, **kargs): + # type: (*Any, **Any) -> Optional[Packet] """ Send packets at layer 3 and return only the first answer + + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 """ - iface = _interface_selection(iface, x) - s = conf.L3socket(promisc=promisc, filter=filter, - nofilter=nofilter, iface=iface) - ans, _ = sndrcv(s, x, *args, **kargs) - s.close() - if len(ans) > 0: - return ans[0][1] - else: - return None + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O sr1(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + ans, _ = sr(*args, **kargs) + if ans: + return cast(Packet, ans[0][1]) + return None @conf.commands.register -def srp(x, promisc=None, iface=None, iface_hint=None, filter=None, - nofilter=0, type=ETH_P_ALL, *args, **kargs): +def srp(x, # type: _PacketIterable + promisc=None, # type: Optional[bool] + iface=None, # type: Optional[_GlobInterfaceType] + iface_hint=None, # type: Optional[str] + filter=None, # type: Optional[str] + nofilter=0, # type: int + type=ETH_P_ALL, # type: int + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Tuple[SndRcvList, PacketList] """ Send and receive packets at layer 2 """ if iface is None and iface_hint is not None: iface = conf.route.route(iface_hint)[0] - s = conf.L2socket(promisc=promisc, iface=iface, - filter=filter, nofilter=nofilter, type=type) + iface = resolve_iface(iface or conf.iface) + s = iface.l2socket()(promisc=promisc, iface=iface, + filter=filter, nofilter=nofilter, type=type) result = sndrcv(s, x, *args, **kargs) s.close() return result @@ -560,14 +773,14 @@ def srp(x, promisc=None, iface=None, iface_hint=None, filter=None, @conf.commands.register def srp1(*args, **kargs): + # type: (*Any, **Any) -> Optional[Packet] """ Send and receive packets at layer 2 and return only the first answer """ ans, _ = srp(*args, **kargs) if len(ans) > 0: - return ans[0][1] - else: - return None + return cast(Packet, ans[0][1]) + return None # Append doc @@ -579,18 +792,27 @@ def srp1(*args, **kargs): # SEND/RECV LOOP METHODS -def __sr_loop(srfunc, pkts, prn=lambda x: x[1].summary(), - prnfail=lambda x: x.summary(), - inter=1, timeout=None, count=None, verbose=None, store=1, - *args, **kargs): +def __sr_loop(srfunc, # type: Callable[..., Tuple[SndRcvList, PacketList]] + pkts, # type: _PacketIterable + prn=lambda x: x[1].summary(), # type: Optional[Callable[[QueryAnswer], Any]] # noqa: E501 + prnfail=lambda x: x.summary(), # type: Optional[Callable[[Packet], Any]] + inter=1, # type: int + timeout=None, # type: Optional[int] + count=None, # type: Optional[int] + verbose=None, # type: Optional[int] + store=1, # type: int + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Tuple[SndRcvList, PacketList] n = 0 r = 0 ct = conf.color_theme if verbose is None: verbose = conf.verb parity = 0 - ans = [] - unans = [] + ans = [] # type: List[QueryAnswer] + unans = [] # type: List[Packet] if timeout is None: timeout = min(2 * inter, 5) try: @@ -601,7 +823,7 @@ def __sr_loop(srfunc, pkts, prn=lambda x: x[1].summary(), if count == 0: break count -= 1 - start = time.time() + start = time.monotonic() if verbose > 1: print("\rsend...\r", end=' ') res = srfunc(pkts, timeout=timeout, verbose=0, chainCC=True, *args, **kargs) # noqa: E501 @@ -610,21 +832,28 @@ def __sr_loop(srfunc, pkts, prn=lambda x: x[1].summary(), if verbose > 1 and prn and len(res[0]) > 0: msg = "RECV %i:" % len(res[0]) print("\r" + ct.success(msg), end=' ') - for p in res[0]: - print(col(prn(p))) + for rcv in res[0]: + print(col(prn(rcv))) print(" " * len(msg), end=' ') if verbose > 1 and prnfail and len(res[1]) > 0: msg = "fail %i:" % len(res[1]) print("\r" + ct.fail(msg), end=' ') - for p in res[1]: - print(col(prnfail(p))) + for fail in res[1]: + print(col(prnfail(fail))) print(" " * len(msg), end=' ') if verbose > 1 and not (prn or prnfail): - print("recv:%i fail:%i" % tuple(map(len, res[:2]))) + print("recv:%i fail:%i" % tuple( + map(len, res[:2]) # type: ignore + )) + if verbose == 1: + if res[0]: + os.write(1, b"*") + if res[1]: + os.write(1, b".") if store: ans += res[0] unans += res[1] - end = time.time() + end = time.monotonic() if end - start < inter: time.sleep(inter + start - end) except KeyboardInterrupt: @@ -636,82 +865,181 @@ def __sr_loop(srfunc, pkts, prn=lambda x: x[1].summary(), @conf.commands.register -def srloop(pkts, *args, **kargs): - """Send a packet at layer 3 in loop and print the answer each time -srloop(pkts, [prn], [inter], [count], ...) --> None""" +def srloop(pkts, # type: _PacketIterable + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Tuple[SndRcvList, PacketList] + """ + Send a packet at layer 3 in loop and print the answer each time + srloop(pkts, [prn], [inter], [count], ...) --> None + """ return __sr_loop(sr, pkts, *args, **kargs) @conf.commands.register -def srploop(pkts, *args, **kargs): - """Send a packet at layer 2 in loop and print the answer each time -srloop(pkts, [prn], [inter], [count], ...) --> None""" +def srploop(pkts, # type: _PacketIterable + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Tuple[SndRcvList, PacketList] + """ + Send a packet at layer 2 in loop and print the answer each time + srloop(pkts, [prn], [inter], [count], ...) --> None + """ return __sr_loop(srp, pkts, *args, **kargs) # SEND/RECV FLOOD METHODS -def sndrcvflood(pks, pkt, inter=0, verbose=None, chainCC=False, timeout=None): - """sndrcv equivalent for flooding.""" - stopevent = Event() +class _FloodGenerator(object): + def __init__(self, tobesent, maxretries): + # type: (_PacketIterable, Optional[int]) -> None + self.tobesent = tobesent + self.maxretries = maxretries + self.stopevent = Event() + self.iterlen = 0 - def send_in_loop(tobesent, stopevent): - """Infinite generator that produces the same - packet until stopevent is triggered.""" + def __iter__(self): + # type: () -> Iterator[Packet] + i = 0 while True: - for p in tobesent: - if stopevent.is_set(): + i += 1 + j = 0 + if self.maxretries and i >= self.maxretries: + return + for p in self.tobesent: + if self.stopevent.is_set(): return + j += 1 yield p + if self.iterlen == 0: + self.iterlen = j + + @property + def sent_time(self): + # type: () -> Union[EDecimal, float, None] + return cast(Packet, self.tobesent).sent_time + + @sent_time.setter + def sent_time(self, val): + # type: (Union[EDecimal, float, None]) -> None + cast(Packet, self.tobesent).sent_time = val + + def stop(self): + # type: () -> None + self.stopevent.set() + + +def sndrcvflood(pks, # type: SuperSocket + pkt, # type: _PacketIterable + inter=0, # type: int + maxretries=None, # type: Optional[int] + verbose=None, # type: Optional[int] + chainCC=False, # type: bool + timeout=None # type: Optional[int] + ): + # type: (...) -> Tuple[SndRcvList, PacketList] + """sndrcv equivalent for flooding.""" - infinite_gen = send_in_loop(pkt, stopevent) - _flood_len = pkt.__iterlen__() if isinstance(pkt, Gen) else len(pkt) - _flood = [_flood_len, stopevent.set] + flood_gen = _FloodGenerator(pkt, maxretries) return sndrcv( - pks, infinite_gen, + pks, flood_gen, inter=inter, verbose=verbose, - chainCC=chainCC, timeout=None, - _flood=_flood + chainCC=chainCC, timeout=timeout, + _flood=flood_gen ) @conf.commands.register -def srflood(x, promisc=None, filter=None, iface=None, nofilter=None, *args, **kargs): # noqa: E501 +def srflood(x, # type: _PacketIterable + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + iface=None, # type: Optional[_GlobInterfaceType] + nofilter=None, # type: Optional[bool] + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Tuple[SndRcvList, PacketList] """Flood and receive packets at layer 3 + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 + :param prn: function applied to packets received :param unique: only consider packets whose print :param nofilter: put 1 to avoid use of BPF filters :param filter: provide a BPF filter - :param iface: listen answers only on the given interface """ - s = conf.L3socket(promisc=promisc, filter=filter, iface=iface, nofilter=nofilter) # noqa: E501 + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O srflood(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) + s = iface.l3socket(ipv6)( + promisc=promisc, filter=filter, + iface=iface, nofilter=nofilter, + ) r = sndrcvflood(s, x, *args, **kargs) s.close() return r @conf.commands.register -def sr1flood(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs): # noqa: E501 +def sr1flood(x, # type: _PacketIterable + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + nofilter=0, # type: int + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Optional[Packet] """Flood and receive packets at layer 3 and return only the first answer + This determines the interface (or L2 source to use) based on the routing + table: conf.route / conf.route6 + :param prn: function applied to packets received :param verbose: set verbosity level :param nofilter: put 1 to avoid use of BPF filters :param filter: provide a BPF filter :param iface: listen answers only on the given interface """ - s = conf.L3socket(promisc=promisc, filter=filter, nofilter=nofilter, iface=iface) # noqa: E501 + if "iface" in kargs: + # Warn that it isn't used. + warnings.warn( + "'iface' has no effect on L3 I/O sr1flood(). For multicast/link-local " + "see https://scapy.readthedocs.io/en/latest/usage.html#multicast", + SyntaxWarning, + ) + del kargs["iface"] + iface, ipv6 = _interface_selection(x) + s = iface.l3socket(ipv6)( + promisc=promisc, filter=filter, + nofilter=nofilter, iface=iface, + ) ans, _ = sndrcvflood(s, x, *args, **kargs) s.close() if len(ans) > 0: - return ans[0][1] - else: - return None + return cast(Packet, ans[0][1]) + return None @conf.commands.register -def srpflood(x, promisc=None, filter=None, iface=None, iface_hint=None, nofilter=None, *args, **kargs): # noqa: E501 +def srpflood(x, # type: _PacketIterable + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + iface=None, # type: Optional[_GlobInterfaceType] + iface_hint=None, # type: Optional[str] + nofilter=None, # type: Optional[bool] + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Tuple[SndRcvList, PacketList] """Flood and receive packets at layer 2 :param prn: function applied to packets received @@ -722,14 +1050,23 @@ def srpflood(x, promisc=None, filter=None, iface=None, iface_hint=None, nofilter """ if iface is None and iface_hint is not None: iface = conf.route.route(iface_hint)[0] - s = conf.L2socket(promisc=promisc, filter=filter, iface=iface, nofilter=nofilter) # noqa: E501 + iface = resolve_iface(iface or conf.iface) + s = iface.l2socket()(promisc=promisc, filter=filter, iface=iface, nofilter=nofilter) # noqa: E501 r = sndrcvflood(s, x, *args, **kargs) s.close() return r @conf.commands.register -def srp1flood(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs): # noqa: E501 +def srp1flood(x, # type: _PacketIterable + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + iface=None, # type: Optional[_GlobInterfaceType] + nofilter=0, # type: int + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> Optional[Packet] """Flood and receive packets at layer 2 and return only the first answer :param prn: function applied to packets received @@ -738,13 +1075,13 @@ def srp1flood(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kar :param filter: provide a BPF filter :param iface: listen answers only on the given interface """ - s = conf.L2socket(promisc=promisc, filter=filter, nofilter=nofilter, iface=iface) # noqa: E501 + iface = resolve_iface(iface or conf.iface) + s = iface.l2socket()(promisc=promisc, filter=filter, nofilter=nofilter, iface=iface) # noqa: E501 ans, _ = sndrcvflood(s, x, *args, **kargs) s.close() if len(ans) > 0: - return ans[0][1] - else: - return None + return cast(Packet, ans[0][1]) + return None # SNIFF METHODS @@ -760,13 +1097,16 @@ class AsyncSniffer(object): is displayed. --Ex: prn = lambda x: x.summary() session: a session = a flow decoder used to handle stream of packets. - e.g: IPSession (to defragment on-the-flow) or NetflowSession + --Ex: session=TCPSession + See below for more details. filter: BPF filter to apply. lfilter: Python function applied to each packet to determine if further action may be done. --Ex: lfilter = lambda x: x.haslayer(Padding) offline: PCAP file (or list of PCAP files) to read packets from, instead of sniffing them + quiet: when set to True, the process stderr is discarded + (default: False). timeout: stop sniffing after a given time (default: None). L2socket: use the provided L2socket (default: use conf.L2listen). opened_socket: provide an object (or a list of objects) ready to use @@ -775,7 +1115,7 @@ class AsyncSniffer(object): we have to stop the capture after this packet. --Ex: stop_filter = lambda x: x.haslayer(TCP) iface: interface or list of interfaces (default: None for sniffing - on all interfaces). + on the default interface). monitor: use monitor mode. May not be available on all OS started_callback: called as soon as the sniffer starts sniffing (default: None). @@ -784,6 +1124,9 @@ class AsyncSniffer(object): element, a list of elements, or a dict object mapping an element to a label (see examples below). + For more information about the session argument, see + https://scapy.rtfd.io/en/latest/usage.html#advanced-sniffing-sniffing-sessions + Examples: synchronous >>> sniff(filter="arp") >>> sniff(filter="tcp", @@ -805,41 +1148,63 @@ class AsyncSniffer(object): >>> print("nice weather today") >>> t.stop() """ + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None # Store keyword arguments self.args = args self.kwargs = kwargs self.running = False - self.thread = None - self.results = None + self.thread = None # type: Optional[Thread] + self.results = None # type: Optional[PacketList] + self.exception = None # type: Optional[Exception] + self.stop_cb = lambda: None # type: Callable[[], None] def _setup_thread(self): + # type: () -> None + def _run_catch(self=self, *args, **kwargs): + # type: (Any, *Any, **Any) -> None + try: + self._run(*args, **kwargs) + except Exception as ex: + self.exception = ex # Prepare sniffing thread self.thread = Thread( - target=self._run, + target=_run_catch, args=self.args, - kwargs=self.kwargs + kwargs=self.kwargs, + name="AsyncSniffer" ) - self.thread.setDaemon(True) + self.thread.daemon = True def _run(self, - count=0, store=True, offline=None, - prn=None, lfilter=None, - L2socket=None, timeout=None, opened_socket=None, - stop_filter=None, iface=None, started_callback=None, - session=None, session_args=[], session_kwargs={}, - *arg, **karg): + count=0, # type: int + store=True, # type: bool + offline=None, # type: Any + quiet=False, # type: bool + prn=None, # type: Optional[Callable[[Packet], Any]] + lfilter=None, # type: Optional[Callable[[Packet], bool]] + L2socket=None, # type: Optional[Type[SuperSocket]] + timeout=None, # type: Optional[int] + opened_socket=None, # type: Optional[SuperSocket] + stop_filter=None, # type: Optional[Callable[[Packet], bool]] + iface=None, # type: Optional[_GlobInterfaceType] + started_callback=None, # type: Optional[Callable[[], Any]] + session=None, # type: Optional[_GlobSessionType] + chainCC=False, # type: bool + **karg # type: Any + ): + # type: (...) -> None self.running = True + self.count = 0 + lst = [] # Start main thread # instantiate session if not isinstance(session, DefaultSession): session = session or DefaultSession - session = session(prn, store, *session_args, **session_kwargs) - else: - session.prn = prn - session.store = store + session = session() # sniff_sockets follows: {socket: label} - sniff_sockets = {} + sniff_sockets = {} # type: Dict[SuperSocket, _GlobInterfaceType] if opened_socket is not None: if isinstance(opened_socket, list): sniff_sockets.update( @@ -849,117 +1214,149 @@ def _run(self, elif isinstance(opened_socket, dict): sniff_sockets.update( (s, label) - for s, label in six.iteritems(opened_socket) + for s, label in opened_socket.items() ) else: sniff_sockets[opened_socket] = "socket0" if offline is not None: flt = karg.get('filter') + if isinstance(offline, str): + # Single file + offline = [offline] if isinstance(offline, list) and \ all(isinstance(elt, str) for elt in offline): - sniff_sockets.update((PcapReader( + # List of files + sniff_sockets.update((PcapReader( # type: ignore fname if flt is None else - tcpdump(fname, args=["-w", "-", flt], getfd=True) + tcpdump(fname, + args=["-w", "-"], + flt=flt, + getfd=True, + quiet=quiet) ), fname) for fname in offline) elif isinstance(offline, dict): - sniff_sockets.update((PcapReader( + # Dict of files + sniff_sockets.update((PcapReader( # type: ignore fname if flt is None else - tcpdump(fname, args=["-w", "-", flt], getfd=True) - ), label) for fname, label in six.iteritems(offline)) + tcpdump(fname, + args=["-w", "-"], + flt=flt, + getfd=True, + quiet=quiet) + ), label) for fname, label in offline.items()) + elif isinstance(offline, (Packet, PacketList, list)): + # Iterables (list of packets, PacketList..) + offline = IterSocket(offline) + sniff_sockets[offline if flt is None else PcapReader( + tcpdump(offline, + args=["-w", "-"], + flt=flt, + getfd=True, + quiet=quiet) + )] = offline else: - # Write Scapy Packet objects to a pcap file - def _write_to_pcap(packets_list): - filename = get_temp_file(autoext=".pcap") - wrpcap(filename, offline) - return filename, filename - - if isinstance(offline, Packet): - tempfile_written, offline = _write_to_pcap([offline]) - elif isinstance(offline, list) and \ - all(isinstance(elt, Packet) for elt in offline): - tempfile_written, offline = _write_to_pcap(offline) - - sniff_sockets[PcapReader( + # Other (file descriptors...) + sniff_sockets[PcapReader( # type: ignore offline if flt is None else - tcpdump(offline, args=["-w", "-", flt], getfd=True) + tcpdump(offline, + args=["-w", "-"], + flt=flt, + getfd=True, + quiet=quiet) )] = offline if not sniff_sockets or iface is not None: - if L2socket is None: - L2socket = conf.L2listen + # The _RL2 function resolves the L2socket of an iface + _RL2 = lambda i: L2socket or resolve_iface(i).l2listen() # type: Callable[[_GlobInterfaceType], Callable[..., SuperSocket]] # noqa: E501 if isinstance(iface, list): sniff_sockets.update( - (L2socket(type=ETH_P_ALL, iface=ifname, *arg, **karg), + (_RL2(ifname)(type=ETH_P_ALL, iface=ifname, **karg), ifname) for ifname in iface ) elif isinstance(iface, dict): sniff_sockets.update( - (L2socket(type=ETH_P_ALL, iface=ifname, *arg, **karg), + (_RL2(ifname)(type=ETH_P_ALL, iface=ifname, **karg), iflabel) - for ifname, iflabel in six.iteritems(iface) + for ifname, iflabel in iface.items() ) else: - sniff_sockets[L2socket(type=ETH_P_ALL, iface=iface, - *arg, **karg)] = iface + iface = iface or conf.iface + sniff_sockets[_RL2(iface)(type=ETH_P_ALL, iface=iface, + **karg)] = iface # Get select information from the sockets _main_socket = next(iter(sniff_sockets)) - read_allowed_exceptions = _main_socket.read_allowed_exceptions select_func = _main_socket.select - _backup_read_func = _main_socket.__class__.recv - nonblocking_socket = _main_socket.nonblocking_socket + nonblocking_socket = getattr(_main_socket, "nonblocking_socket", False) # We check that all sockets use the same select(), or raise a warning if not all(select_func == sock.select for sock in sniff_sockets): warning("Warning: inconsistent socket types ! " "The used select function " "will be the one of the first socket") - # Fill if empty - if not read_allowed_exceptions: - read_allowed_exceptions = (IOError,) + close_pipe = None # type: Optional[ObjectPipe[None]] + if not nonblocking_socket: + # select is blocking: Add special control socket + from scapy.automaton import ObjectPipe + close_pipe = ObjectPipe[None]("control_socket") + sniff_sockets[close_pipe] = "control_socket" # type: ignore - if nonblocking_socket: - # select is non blocking def stop_cb(): + # type: () -> None + if self.running and close_pipe: + close_pipe.send(None) self.continue_sniff = False self.stop_cb = stop_cb - close_pipe = None else: - # select is blocking: Add special control socket - from scapy.automaton import ObjectPipe - close_pipe = ObjectPipe() - sniff_sockets[close_pipe] = "control_socket" - + # select is non blocking def stop_cb(): - if self.running: - close_pipe.send(None) + # type: () -> None self.continue_sniff = False self.stop_cb = stop_cb try: + self.continue_sniff = True if started_callback: started_callback() - self.continue_sniff = True # Start timeout if timeout is not None: - stoptime = time.time() + timeout + stoptime = time.monotonic() + timeout remain = None while sniff_sockets and self.continue_sniff: if timeout is not None: - remain = stoptime - time.time() + remain = stoptime - time.monotonic() if remain <= 0: break - sockets, read_func = select_func(sniff_sockets, remain) - read_func = read_func or _backup_read_func + sockets = select_func(list(sniff_sockets.keys()), remain) dead_sockets = [] for s in sockets: - if s is close_pipe: + if s is close_pipe: # type: ignore break + # The session object is passed the socket to call recv() on, + # and may perform additional processing (ip defrag, etc.) try: - p = read_func(s) + packets = session.recv(s) + # A session can return multiple objects + for p in packets: + if lfilter and not lfilter(p): + continue + p.sniffed_on = sniff_sockets.get(s, None) + # post-processing + self.count += 1 + if store: + lst.append(p) + if prn: + result = prn(p) + if result is not None: + print(result) + # check + if (stop_filter and stop_filter(p)) or \ + (0 < count <= self.count): + self.continue_sniff = False + break except EOFError: # End of stream try: @@ -968,15 +1365,13 @@ def stop_cb(): pass dead_sockets.append(s) continue - except read_allowed_exceptions: - continue except Exception as ex: msg = " It was closed." try: # Make sure it's closed s.close() - except Exception as ex: - msg = " close() failed with '%s'" % ex + except Exception as ex2: + msg = " close() failed with '%s'" % ex2 warning( "Socket %s failed with '%s'." % (s, ex) + msg ) @@ -984,88 +1379,97 @@ def stop_cb(): if conf.debug_dissector >= 2: raise continue - if p is None: - continue - if lfilter and not lfilter(p): - continue - p.sniffed_on = sniff_sockets[s] - # on_packet_received handles the prn/storage - session.on_packet_received(p) - # check - if (stop_filter and stop_filter(p)) or \ - (0 < count <= session.count): - self.continue_sniff = False - break # Removed dead sockets for s in dead_sockets: del sniff_sockets[s] + if len(sniff_sockets) == 1 and \ + close_pipe in sniff_sockets: # type: ignore + # Only the close_pipe left + del sniff_sockets[close_pipe] # type: ignore except KeyboardInterrupt: - pass + if chainCC: + raise self.running = False if opened_socket is None: for s in sniff_sockets: s.close() elif close_pipe: close_pipe.close() - self.results = session.toPacketList() + self.results = PacketList(lst, "Sniffed") def start(self): + # type: () -> None """Starts AsyncSniffer in async mode""" self._setup_thread() - self.thread.start() + if self.thread: + self.thread.start() def stop(self, join=True): + # type: (bool) -> Optional[PacketList] """Stops AsyncSniffer if not in async mode""" if self.running: - try: - self.stop_cb() - except AttributeError: + self.stop_cb() + if not hasattr(self, "continue_sniff"): + # Never started -> is there an exception? + if self.exception is not None: + raise self.exception + return None + if self.continue_sniff: raise Scapy_Exception( "Unsupported (offline or unsupported socket)" ) if join: self.join() return self.results + return None else: - raise Scapy_Exception("Not started !") + raise Scapy_Exception("Not running ! (check .running attr)") def join(self, *args, **kwargs): + # type: (*Any, **Any) -> None if self.thread: self.thread.join(*args, **kwargs) + if self.exception is not None: + raise self.exception @conf.commands.register def sniff(*args, **kwargs): + # type: (*Any, **Any) -> PacketList sniffer = AsyncSniffer() sniffer._run(*args, **kwargs) - return sniffer.results + return cast(PacketList, sniffer.results) sniff.__doc__ = AsyncSniffer.__doc__ @conf.commands.register -def bridge_and_sniff(if1, if2, xfrm12=None, xfrm21=None, prn=None, L2socket=None, # noqa: E501 - *args, **kargs): +def bridge_and_sniff(if1, # type: _GlobInterfaceType + if2, # type: _GlobInterfaceType + xfrm12=None, # type: Optional[Callable[[Packet], Union[Packet, bool]]] # noqa: E501 + xfrm21=None, # type: Optional[Callable[[Packet], Union[Packet, bool]]] # noqa: E501 + prn=None, # type: Optional[Callable[[Packet], Any]] + L2socket=None, # type: Optional[Type[SuperSocket]] + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> PacketList """Forward traffic between interfaces if1 and if2, sniff and return -the exchanged packets. - -Arguments: - - if1, if2: the interfaces to use (interface names or opened sockets). - - xfrm12: a function to call when forwarding a packet from if1 to - if2. If it returns True, the packet is forwarded as it. If it - returns False or None, the packet is discarded. If it returns a - packet, this packet is forwarded instead of the original packet - one. - - xfrm21: same as xfrm12 for packets forwarded from if2 to if1. - - The other arguments are the same than for the function sniff(), - except for offline, opened_socket and iface that are ignored. - See help(sniff) for more. - + the exchanged packets. + + :param if1: the interfaces to use (interface names or opened sockets). + :param if2: + :param xfrm12: a function to call when forwarding a packet from if1 to + if2. If it returns True, the packet is forwarded as it. If it + returns False or None, the packet is discarded. If it returns a + packet, this packet is forwarded instead of the original packet + one. + :param xfrm21: same as xfrm12 for packets forwarded from if2 to if1. + + The other arguments are the same than for the function sniff(), + except for offline, opened_socket and iface that are ignored. + See help(sniff) for more. """ for arg in ['opened_socket', 'offline', 'iface']: if arg in kargs: @@ -1073,11 +1477,18 @@ def bridge_and_sniff(if1, if2, xfrm12=None, xfrm21=None, prn=None, L2socket=None "bridge_and_sniff() -- ignoring it.", arg) del kargs[arg] - def _init_socket(iface, count): + def _init_socket(iface, # type: _GlobInterfaceType + count, # type: int + L2socket=L2socket # type: Optional[Type[SuperSocket]] + ): + # type: (...) -> Tuple[SuperSocket, _GlobInterfaceType] if isinstance(iface, SuperSocket): return iface, "iface%d" % count else: - return (L2socket or conf.L2socket)(iface=iface), iface + if not L2socket: + iface = resolve_iface(iface or conf.iface) + L2socket = iface.l2socket() + return L2socket(iface=iface), iface sckt1, if1 = _init_socket(if1, 1) sckt2, if2 = _init_socket(if2, 2) peers = {if1: sckt2, if2: sckt1} @@ -1088,13 +1499,14 @@ def _init_socket(iface, count): xfrms[if2] = xfrm21 def prn_send(pkt): + # type: (Packet) -> None try: - sendsock = peers[pkt.sniffed_on] + sendsock = peers[pkt.sniffed_on or ""] except KeyError: return if pkt.sniffed_on in xfrms: try: - newpkt = xfrms[pkt.sniffed_on](pkt) + _newpkt = xfrms[pkt.sniffed_on](pkt) except Exception: log_runtime.warning( 'Exception in transformation function for packet [%s] ' @@ -1103,12 +1515,14 @@ def prn_send(pkt): ) return else: - if newpkt is True: - newpkt = pkt.original - elif not newpkt: - return + if isinstance(_newpkt, bool): + if not _newpkt: + return + newpkt = pkt + else: + newpkt = _newpkt else: - newpkt = pkt.original + newpkt = pkt try: sendsock.send(newpkt) except Exception: @@ -1120,6 +1534,7 @@ def prn_send(pkt): prn_orig = prn def prn(pkt): + # type: (Packet) -> Any prn_send(pkt) return prn_orig(pkt) @@ -1129,13 +1544,14 @@ def prn(pkt): @conf.commands.register def tshark(*args, **kargs): + # type: (Any, Any) -> None """Sniff packets and print them calling pkt.summary(). This tries to replicate what text-wireshark (tshark) would look like""" if 'iface' in kargs: iface = kargs.get('iface') elif 'opened_socket' in kargs: - iface = kargs.get('opened_socket').iface + iface = cast(SuperSocket, kargs.get('opened_socket')).iface else: iface = conf.iface print("Capturing on '%s'" % iface) @@ -1145,6 +1561,7 @@ def tshark(*args, **kargs): i = [0] def _cb(pkt): + # type: (Packet) -> None print("%5d\t%s" % (i[0], pkt.summary())) i[0] += 1 diff --git a/scapy/sessions.py b/scapy/sessions.py index fa1ae8e276f..3c58dc2c6a3 100644 --- a/scapy/sessions.py +++ b/scapy/sessions.py @@ -1,87 +1,66 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information """ Sessions: decode flow of packets when sniffing """ from collections import defaultdict -from scapy.compat import raw +import socket +import struct + +from scapy.compat import orb from scapy.config import conf -from scapy.packet import NoPayload -from scapy.plist import PacketList +from scapy.packet import Packet +from scapy.pton_ntop import inet_pton + +# Typing imports +from typing import ( + Any, + Callable, + DefaultDict, + Dict, + Iterator, + List, + Optional, + Tuple, + Type, + cast, + TYPE_CHECKING, +) +from scapy.compat import Self +if TYPE_CHECKING: + from scapy.supersocket import SuperSocket class DefaultSession(object): """Default session: no stream decoding""" - def __init__(self, prn=None, store=False, supersession=None, - *args, **karg): - self.__prn = prn - self.__store = store - self.lst = [] - self.__count = 0 - self._supersession = supersession - if self._supersession: - self._supersession.prn = self.__prn - self._supersession.store = self.__store - self.__store = False - self.__prn = None - - @property - def store(self): - return self.__store - - @store.setter - def store(self, val): - if self._supersession: - self._supersession.store = val - else: - self.__store = val - - @property - def prn(self): - return self.__prn - - @prn.setter - def prn(self, f): - if self._supersession: - self._supersession.prn = f - else: - self.__prn = f - - @property - def count(self): - if self._supersession: - return self._supersession.count - else: - return self.__count + def __init__(self, supersession: Optional[Self] = None): + if supersession and not isinstance(supersession, DefaultSession): + supersession = supersession() + self.supersession = supersession - def toPacketList(self): - if self._supersession: - return PacketList(self._supersession.lst, "Sniffed") - else: - return PacketList(self.lst, "Sniffed") + def process(self, pkt: Packet) -> Optional[Packet]: + """ + Called to pre-process the packet + """ + # Optionally handle supersession + if self.supersession: + return self.supersession.process(pkt) + return pkt - def on_packet_received(self, pkt): - """DEV: entry point. Will be called by sniff() for each - received packet (that passes the filters). + def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: + """ + Will be called by sniff() to ask for a packet """ + pkt = sock.recv() if not pkt: return - if isinstance(pkt, list): - for p in pkt: - DefaultSession.on_packet_received(self, p) - return - self.__count += 1 - if self.store: - self.lst.append(pkt) - if self.prn: - result = self.prn(pkt) - if result is not None: - print(result) + pkt = self.process(pkt) + if pkt: + yield pkt class IPSession(DefaultSession): @@ -92,37 +71,17 @@ class IPSession(DefaultSession): """ def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None DefaultSession.__init__(self, *args, **kwargs) - self.fragments = defaultdict(list) + self.fragments = defaultdict(list) # type: DefaultDict[Tuple[Any, ...], List[Packet]] # noqa: E501 - def _ip_process_packet(self, packet): - from scapy.layers.inet import _defrag_list, IP + def process(self, packet: Packet) -> Optional[Packet]: + from scapy.layers.inet import IP, _defrag_ip_pkt + if not packet: + return None if IP not in packet: return packet - ip = packet[IP] - packet._defrag_pos = 0 - if ip.frag != 0 or ip.flags.MF: - uniq = (ip.id, ip.src, ip.dst, ip.proto) - self.fragments[uniq].append(packet) - if not ip.flags.MF: # end of frag - try: - if self.fragments[uniq][0].frag == 0: - # Has first fragment (otherwise ignore) - defrag, missfrag = [], [] - _defrag_list(self.fragments[uniq], defrag, missfrag) - defragmented_packet = defrag[0] - defragmented_packet = defragmented_packet.__class__( - raw(defragmented_packet) - ) - return defragmented_packet - finally: - del self.fragments[uniq] - else: - return packet - - def on_packet_received(self, pkt): - pkt = self._ip_process_packet(pkt) - DefaultSession.on_packet_received(self, pkt) + return _defrag_ip_pkt(packet, self.fragments)[1] # type: ignore class StringBuffer(object): @@ -136,18 +95,33 @@ class StringBuffer(object): If a TCP fragment is missed, this class will fill the missing space with zeros. """ + def __init__(self): + # type: () -> None self.content = bytearray(b"") self.content_len = 0 - self.incomplete = [] + self.noff = 0 # negative offset + self.incomplete = [] # type: List[Tuple[int, int]] - def append(self, data, seq): + def append(self, data: bytes, seq: Optional[int] = None) -> None: + if not data: + return data_len = len(data) - seq = seq - 1 + if seq is None: + seq = self.content_len + seq = seq - 1 - self.noff + if seq < 0: + # Data is located before the start of the current buffer + # (e.g. the first fragment was missing) + self.content = bytearray(b"\x00" * (-seq)) + self.content + self.content_len += (-seq) + self.noff += seq + seq = 0 if seq + data_len > self.content_len: + # Data is located after the end of the current buffer self.content += b"\x00" * (seq - self.content_len + data_len) - # If data was missing, mark it. - self.incomplete.append((self.content_len, seq)) + # As data was missing, mark it. + # self.incomplete.append((self.content_len, seq)) self.content_len = seq + data_len assert len(self.content) == self.content_len # XXX removes empty space marker. @@ -156,36 +130,70 @@ def append(self, data, seq): # self.incomplete.remove([???]) memoryview(self.content)[seq:seq + data_len] = data + def shiftleft(self, i: int) -> None: + self.content = self.content[i:] + self.content_len -= i + def full(self): + # type: () -> bool # Should only be true when all missing data was filled up, # (or there never was missing data) - return True # XXX + return bool(self) def clear(self): - self.__init__() + # type: () -> None + self.__init__() # type: ignore def __bool__(self): + # type: () -> bool return bool(self.content_len) __nonzero__ = __bool__ def __len__(self): + # type: () -> int return self.content_len def __bytes__(self): + # type: () -> bytes return bytes(self.content) - __str__ = __bytes__ + + def __str__(self): + # type: () -> str + return cast(str, self.__bytes__()) + + +def streamcls(cls: Type[Packet]) -> Callable[ + [bytes, Dict[str, Any], Dict[str, Any]], + Optional[Packet], +]: + """ + Wraps a class for use when dissecting streams. + """ + if hasattr(cls, "tcp_reassemble"): + return cls.tcp_reassemble # type: ignore + else: + # There is no tcp_reassemble. Just dissect the packet + return lambda data, *_: data and cls(data) class TCPSession(IPSession): - """A Session that matches seq/ack packets together to dissect - special protocols, such as HTTP. + """A Session that reconstructs TCP streams. + + NOTE: this has the same effect as wrapping a real socket.socket into StreamSocket, + but for all concurrent TCP streams (can be used on pcaps or sniffed sessions). + + NOTE: only protocols that implement a ``tcp_reassemble`` function will be processed + by this session. Other protocols will not be reconstructed. DEV: implement a class-function `tcp_reassemble` in your Packet class:: @classmethod - def tcp_reassemble(cls, data, metadata): + def tcp_reassemble(cls, data, metadata, session): # data = the reassembled data from the same request/flow # metadata = empty dictionary, that can be used to store data + # during TCP reassembly + # session = a dictionary proper to the bidirectional TCP session, + # that can be used to store anything [...] # If the packet is available, return it. Otherwise don't. # Whenever you return a packet, the buffer will be discarded. @@ -194,78 +202,226 @@ def tcp_reassemble(cls, data, metadata): # as you need additional data. return None - A (hard to understand) example can be found in scapy/layers/http.py - """ + For more details and a real example, see: + https://scapy.readthedocs.io/en/latest/usage.html#how-to-use-tcpsession-to-defragment-tcp-packets - fmt = ('TCP {IP:%IP.src%}{IPv6:%IPv6.src%}:%r,TCP.sport% > ' + - '{IP:%IP.dst%}{IPv6:%IPv6.dst%}:%r,TCP.dport%') + :param app: Whether the socket is on application layer = has no TCP + layer. This is identical to StreamSocket so only use this if your + underlying source of data isn't a socket.socket. + """ - def __init__(self, *args, **kwargs): + def __init__(self, app=False, *args, **kwargs): + # type: (bool, *Any, **Any) -> None super(TCPSession, self).__init__(*args, **kwargs) - # The StringBuffer() is used to build a global - # string from fragments and their seq nulber - self.tcp_frags = defaultdict( - lambda: (StringBuffer(), {}) - ) + self.app = app + if app: + self.data = StringBuffer() + self.metadata = {} # type: Dict[str, Any] + self.session = {} # type: Dict[str, Any] + else: + # The StringBuffer() is used to build a global + # string from fragments and their seq nulber + self.tcp_frags = defaultdict( + lambda: (StringBuffer(), {}) + ) # type: DefaultDict[bytes, Tuple[StringBuffer, Dict[str, Any]]] + self.tcp_sessions = defaultdict( + dict + ) # type: DefaultDict[bytes, Dict[str, Any]] + # Setup stopping dissection condition + from scapy.layers.inet import TCP + self.stop_dissection_after = TCP + + def _get_ident(self, pkt, session=False): + # type: (Packet, bool) -> bytes + underlayer = pkt["TCP"].underlayer + af = socket.AF_INET6 if "IPv6" in pkt else socket.AF_INET + src = underlayer and inet_pton(af, underlayer.src) or b"" + dst = underlayer and inet_pton(af, underlayer.dst) or b"" + if session: + # Bidirectional + def xor(x, y): + # type: (bytes, bytes) -> bytes + return bytes(orb(a) ^ orb(b) for a, b in zip(x, y)) + return struct.pack("!4sH", xor(src, dst), pkt.dport ^ pkt.sport) + else: + # Uni-directional + return src + dst + struct.pack("!HH", pkt.dport, pkt.sport) - def _process_packet(self, pkt): + def _strip_padding(self, pkt: Packet) -> Optional[bytes]: + """Strip the packet of any padding, and return the padding. + """ + if isinstance(pkt, conf.padding_layer): + return cast(bytes, pkt.load) + pad = pkt.getlayer(conf.padding_layer) + if pad is not None and pad.underlayer is not None: + # strip padding + del pad.underlayer.payload + return cast(bytes, pad.load) + return None + + def process(self, + pkt: Packet, + cls: Optional[Type[Packet]] = None) -> Optional[Packet]: """Process each packet: matches the TCP seq/ack numbers to follow the TCP streams, and orders the fragments. """ + packet = None # type: Optional[Packet] + if self.app: + # Special mode: Application layer. Use on top of TCP + self.data.append(bytes(pkt)) + if cls is None and not isinstance(pkt, bytes): + cls = pkt.__class__ + if "tcp_reassemble" in self.metadata: + tcp_reassemble = self.metadata["tcp_reassemble"] + elif cls is not None: + self.metadata["tcp_reassemble"] = tcp_reassemble = streamcls(cls) + else: + return None + if self.data.full(): + packet = tcp_reassemble( + bytes(self.data), + self.metadata, + self.session, + ) + if packet: + padding = self._strip_padding(packet) + if padding: + # There is remaining data for the next payload. + self.data.shiftleft(len(self.data) - len(padding)) + # Skip full-padding + if isinstance(packet, conf.padding_layer): + return None + else: + # No padding (data) left. Clear + self.data.clear() + self.metadata.clear() + return packet + return None + + _pkt = super(TCPSession, self).process(pkt) + if _pkt is None: + return None + else: # Python 3.8 := would be nice + pkt = _pkt + from scapy.layers.inet import IP, TCP + if not pkt: + return None if TCP not in pkt: return pkt pay = pkt[TCP].payload - if isinstance(pay, (NoPayload, conf.padding_layer)): - return pkt - new_data = raw(pay) - # Match packets by a uniqute TCP identifier - seq = pkt[TCP].seq - ident = pkt.sprintf(self.fmt) + new_data = pay.original + # Match packets by a unique TCP identifier + ident = self._get_ident(pkt) data, metadata = self.tcp_frags[ident] + tcp_session = self.tcp_sessions[self._get_ident(pkt, True)] + # Handle TCP sequence numbers + seq = pkt[TCP].seq + if "seq" not in metadata: + metadata["seq"] = seq + if "next_seq" in metadata and seq < metadata["next_seq"]: + # Retransmitted data (that we already returned) + new_data = new_data[metadata["next_seq"] - seq:] + if not new_data: + return None + seq = metadata["next_seq"] # Let's guess which class is going to be used if "pay_class" not in metadata: - pay_class = pay.__class__ - if not hasattr(pay_class, "tcp_reassemble"): - # Cannot tcp-reassemble - return pkt - metadata["pay_class"] = pay_class + metadata["pay_class"] = pay_class = pkt[TCP].guess_payload_class(new_data) + metadata["tcp_reassemble"] = tcp_reassemble = streamcls(pay_class) else: - pay_class = metadata["pay_class"] - # Get a relative sequence number for a storage purpose - relative_seq = metadata.get("relative_seq", None) - if not relative_seq: - relative_seq = metadata["relative_seq"] = seq - 1 - seq = seq - relative_seq - # Add the data to the buffer - # Note that this take care of retransmission packets. - data.append(new_data, seq) + tcp_reassemble = metadata["tcp_reassemble"] + + if pay: + # Get a relative sequence number for a storage purpose + relative_seq = metadata.get("relative_seq", None) + if relative_seq is None: + relative_seq = metadata["relative_seq"] = seq - 1 + seq = seq - relative_seq + # Add the data to the buffer + data.append(new_data, seq) + # Check TCP FIN or TCP RESET - if pkt[TCP].flags.F or pkt[TCP].flags.R or pkt[TCP].flags.P: + if pkt[TCP].flags.F or pkt[TCP].flags.R: metadata["tcp_end"] = True + elif not pay: + # If there's no payload and the stream isn't ending, ignore. + return pkt + + # In case any app layer protocol requires it, + # allow the parser to inspect TCP PSH flag + if pkt[TCP].flags.P: + metadata["tcp_psh"] = True # XXX TODO: check that no empty space is missing in the buffer. # XXX Currently, if a TCP fragment was missing, we won't notice it. - packet = None if data.full(): # Reassemble using all previous packets - packet = pay_class.tcp_reassemble(bytes(data), metadata) + metadata["original"] = pkt + metadata["ident"] = ident + packet = tcp_reassemble( + bytes(data), + metadata, + tcp_session + ) # Stack the result on top of the previous frames if packet: - data.clear() - del self.tcp_frags[ident] - pay.underlayer.remove_payload() + if "seq" in metadata: + pkt[TCP].seq = metadata["seq"] + # Clear TCP reassembly metadata + metadata.clear() + # Check for padding + padding = self._strip_padding(packet) + while padding: + # There is remaining data for the next payload. + full_length = data.content_len - len(padding) + metadata["relative_seq"] = relative_seq + full_length + data.shiftleft(full_length) + # There might be a sub-payload hidden in the padding + sub_packet = tcp_reassemble( + bytes(data), + metadata, + tcp_session + ) + if sub_packet: + packet /= sub_packet + padding = self._strip_padding(sub_packet) + else: + break + else: + # No padding (data) left. Clear + data.clear() + del self.tcp_frags[ident] + # Minimum next seq + metadata["next_seq"] = pkt[TCP].seq + len(new_data) + # Skip full-padding + if isinstance(packet, conf.padding_layer): + return None + # Rebuild resulting packet + if pay: + pay.underlayer.remove_payload() if IP in pkt: pkt[IP].len = None pkt[IP].chksum = None - return pkt / packet + pkt = pkt / packet + pkt.wirelen = None + return pkt + return None - def on_packet_received(self, pkt): - """Hook to the Sessions API: entry point of the dissection. - This will defragment IP if necessary, then process to - TCP reassembly. + def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: + """ + Will be called by sniff() to ask for a packet """ - # First, defragment IP if necessary - pkt = self._ip_process_packet(pkt) + pkt = sock.recv(stop_dissection_after=self.stop_dissection_after) # Now handle TCP reassembly - pkt = self._process_packet(pkt) - DefaultSession.on_packet_received(self, pkt) + if self.app: + while pkt is not None: + pkt = self.process(pkt) + if pkt: + yield pkt + # keep calling process as there might be more + pkt = b"" # type: ignore + else: + pkt = self.process(pkt) # type: ignore + if pkt: + yield pkt + return None diff --git a/scapy/supersocket.py b/scapy/supersocket.py index 5906315c2d0..628920b8a62 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -1,35 +1,67 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ SuperSocket. """ -from __future__ import absolute_import from select import select, error as select_error import ctypes import errno -import os import socket import struct import time from scapy.config import conf -from scapy.consts import LINUX, DARWIN, WINDOWS -from scapy.data import MTU, ETH_P_IP, SOL_PACKET, SO_TIMESTAMPNS -from scapy.compat import raw, bytes_encode +from scapy.consts import DARWIN, WINDOWS +from scapy.data import ( + MTU, + ETH_P_IP, + ETH_P_IPV6, + SOL_PACKET, + SO_TIMESTAMPNS, +) +from scapy.compat import raw from scapy.error import warning, log_runtime -import scapy.modules.six as six -import scapy.packet +from scapy.interfaces import network_name +from scapy.packet import Packet, NoPayload +from scapy.plist import ( + PacketList, + SndRcvList, + _PacketIterable, +) from scapy.utils import PcapReader, tcpdump +# Typing imports +from scapy.interfaces import _GlobInterfaceType +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Tuple, + Type, + TypeVar, + cast, + TYPE_CHECKING, +) +from scapy.compat import Self + +if TYPE_CHECKING: + from scapy.ansmachine import AnsweringMachine + # Utils + class _SuperSocket_metaclass(type): + desc = None # type: Optional[str] + def __repr__(self): + # type: () -> str if self.desc is not None: return "<%s: %s>" % (self.__name__, self.desc) else: @@ -40,6 +72,7 @@ def __repr__(self): PACKET_AUXDATA = 8 ETH_P_8021Q = 0x8100 TP_STATUS_VLAN_VALID = 1 << 4 +TP_STATUS_VLAN_TPID_VALID = 1 << 6 class tpacket_auxdata(ctypes.Structure): @@ -50,41 +83,71 @@ class tpacket_auxdata(ctypes.Structure): ("tp_mac", ctypes.c_ushort), ("tp_net", ctypes.c_ushort), ("tp_vlan_tci", ctypes.c_ushort), - ("tp_padding", ctypes.c_ushort), - ] + ("tp_vlan_tpid", ctypes.c_ushort), + ] # type: List[Tuple[str, Any]] # SuperSocket -class SuperSocket(six.with_metaclass(_SuperSocket_metaclass)): - desc = None - closed = 0 - nonblocking_socket = False - read_allowed_exceptions = () - auxdata_available = False +_T = TypeVar("_T", Packet, PacketList) + - def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0): # noqa: E501 - self.ins = socket.socket(family, type, proto) - self.outs = self.ins - self.promisc = None +class SuperSocket(metaclass=_SuperSocket_metaclass): + closed = False # type: bool + nonblocking_socket = False # type: bool + auxdata_available = False # type: bool + + def __init__(self, + family=socket.AF_INET, # type: int + type=socket.SOCK_STREAM, # type: int + proto=0, # type: int + iface=None, # type: Optional[_GlobInterfaceType] + **kwargs # type: Any + ): + # type: (...) -> None + self.ins = socket.socket(family, type, proto) # type: socket.socket + self.outs = self.ins # type: Optional[socket.socket] + self.promisc = conf.sniff_promisc + self.iface = iface or conf.iface def send(self, x): + # type: (Packet) -> int + """Sends a `Packet` object + + :param x: `Packet` to be send + :return: Number of bytes that have been sent + """ sx = raw(x) try: x.sent_time = time.time() except AttributeError: pass - return self.outs.send(sx) - if six.PY2: + if self.outs: + return self.outs.send(sx) + else: + return 0 + + if WINDOWS: def _recv_raw(self, sock, x): - """Internal function to receive a Packet""" + # type: (socket.socket, int) -> Tuple[bytes, Any, Optional[float]] + """Internal function to receive a Packet. + + :param sock: Socket object from which data are received + :param x: Number of bytes to be received + :return: Received bytes, address information and no timestamp + """ pkt, sa_ll = sock.recvfrom(x) return pkt, sa_ll, None else: def _recv_raw(self, sock, x): + # type: (socket.socket, int) -> Tuple[bytes, Any, Optional[float]] """Internal function to receive a Packet, and process ancillary data. + + :param sock: Socket object from which data are received + :param x: Number of bytes to be received + :return: Received bytes, address information and an optional timestamp """ timestamp = None if not self.auxdata_available: @@ -109,9 +172,12 @@ def _recv_raw(self, sock, x): if auxdata.tp_vlan_tci != 0 or \ auxdata.tp_status & TP_STATUS_VLAN_VALID: # Insert VLAN tag + tpid = ETH_P_8021Q + if auxdata.tp_status & TP_STATUS_VLAN_TPID_VALID: + tpid = auxdata.tp_vlan_tpid tag = struct.pack( "!HH", - ETH_P_8021Q, + tpid, auxdata.tp_vlan_tci ) pkt = pkt[:12] + tag + pkt[12:] @@ -129,15 +195,28 @@ def _recv_raw(self, sock, x): return pkt, sa_ll, timestamp def recv_raw(self, x=MTU): - """Returns a tuple containing (cls, pkt_data, time)""" + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + """Returns a tuple containing (cls, pkt_data, time) + + + :param x: Maximum number of bytes to be received, defaults to MTU + :return: A tuple, consisting of a Packet type, the received data, + and a timestamp + """ return conf.raw_layer, self.ins.recv(x), None - def recv(self, x=MTU): + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + """Receive a Packet according to the `basecls` of this socket + + :param x: Maximum number of bytes to be received, defaults to MTU + :return: The received `Packet` object, or None + """ cls, val, ts = self.recv_raw(x) if not val or not cls: - return + return None try: - pkt = cls(val) + pkt = cls(val, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -151,42 +230,72 @@ def recv(self, x=MTU): return pkt def fileno(self): + # type: () -> int return self.ins.fileno() def close(self): + # type: () -> None + """Gracefully close this socket + """ if self.closed: return self.closed = True if getattr(self, "outs", None): if getattr(self, "ins", None) != self.outs: - if WINDOWS or self.outs.fileno() != -1: + if self.outs and self.outs.fileno() != -1: self.outs.close() if getattr(self, "ins", None): - if WINDOWS or self.ins.fileno() != -1: + if self.ins.fileno() != -1: self.ins.close() def sr(self, *args, **kargs): + # type: (Any, Any) -> Tuple[SndRcvList, PacketList] + """Send and Receive multiple packets + """ from scapy import sendrecv return sendrecv.sndrcv(self, *args, **kargs) def sr1(self, *args, **kargs): + # type: (Any, Any) -> Optional[Packet] + """Send one packet and receive one answer + """ from scapy import sendrecv - a, b = sendrecv.sndrcv(self, *args, **kargs) - if len(a) > 0: - return a[0][1] + # if not explicitly specified by the user, + # set threaded to False in sr1 to remove the overhead + # for a Thread creation + kargs.setdefault("threaded", False) + ans = sendrecv.sndrcv(self, *args, **kargs)[0] # type: SndRcvList + if len(ans) > 0: + pkt = ans[0][1] # type: Packet + return pkt else: return None def sniff(self, *args, **kargs): + # type: (Any, Any) -> PacketList from scapy import sendrecv return sendrecv.sniff(opened_socket=self, *args, **kargs) def tshark(self, *args, **kargs): + # type: (Any, Any) -> None from scapy import sendrecv - return sendrecv.tshark(opened_socket=self, *args, **kargs) + sendrecv.tshark(opened_socket=self, *args, **kargs) + + def am(self, + cls, # type: Type[AnsweringMachine[_T]] + **kwargs # type: Any + ): + # type: (...) -> AnsweringMachine[_T] + """ + Creates an AnsweringMachine associated with this socket. + + :param cls: A subclass of AnsweringMachine to instantiate + """ + return cls(opened_socket=self, socket=self, **kwargs) @staticmethod def select(sockets, remain=conf.recv_poll_rate): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] """This function is called during sendrecv() routine to select the available sockets. @@ -194,36 +303,51 @@ def select(sockets, remain=conf.recv_poll_rate): :returns: an array of sockets that were selected and the function to be called next to get the packets (i.g. recv) """ + inp = [] # type: List[SuperSocket] try: inp, _, _ = select(sockets, [], [], remain) except (IOError, select_error) as exc: # select.error has no .errno attribute - if exc.args[0] != errno.EINTR: + if not exc.args or exc.args[0] != errno.EINTR: raise - return inp, None + return inp def __del__(self): + # type: () -> None """Close the socket""" self.close() def __enter__(self): + # type: () -> Self return self def __exit__(self, exc_type, exc_value, traceback): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> None # noqa: E501 """Close the socket""" self.close() -class L3RawSocket(SuperSocket): - desc = "Layer 3 using Raw sockets (PF_INET/SOCK_RAW)" - - def __init__(self, type=ETH_P_IP, filter=None, iface=None, promisc=None, nofilter=0): # noqa: E501 - self.outs = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) # noqa: E501 - self.outs.setsockopt(socket.SOL_IP, socket.IP_HDRINCL, 1) - self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type)) # noqa: E501 - if iface is not None: - self.ins.bind((iface, type)) - if not six.PY2: +if not WINDOWS: + class L3RawSocket(SuperSocket): + desc = "Layer 3 using Raw sockets (PF_INET/SOCK_RAW)" + + def __init__(self, + type=ETH_P_IP, # type: int + filter=None, # type: Optional[str] + iface=None, # type: Optional[_GlobInterfaceType] + promisc=None, # type: Optional[bool] + nofilter=0 # type: int + ): + # type: (...) -> None + self.outs = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) # noqa: E501 + self.outs.setsockopt(socket.SOL_IP, socket.IP_HDRINCL, 1) + self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type)) # noqa: E501 + if iface is not None: + iface = network_name(iface) + self.iface = iface + self.ins.bind((iface, type)) + else: + self.iface = "any" try: # Receive Auxiliary Data (VLAN tags) self.ins.setsockopt(SOL_PACKET, PACKET_AUXDATA, 1) @@ -239,136 +363,231 @@ def __init__(self, type=ETH_P_IP, filter=None, iface=None, promisc=None, nofilte msg = "Your Linux Kernel does not support Auxiliary Data!" log_runtime.info(msg) - def recv(self, x=MTU): - pkt, sa_ll, ts = self._recv_raw(self.ins, x) - if sa_ll[2] == socket.PACKET_OUTGOING: - return None - if sa_ll[3] in conf.l2types: - cls = conf.l2types[sa_ll[3]] - lvl = 2 - elif sa_ll[1] in conf.l3types: - cls = conf.l3types[sa_ll[1]] - lvl = 3 - else: - cls = conf.default_l2 - warning("Unable to guess type (interface=%s protocol=%#x family=%i). Using %s", sa_ll[0], sa_ll[1], sa_ll[3], cls.name) # noqa: E501 - lvl = 3 + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + data, sa_ll, ts = self._recv_raw(self.ins, x) + if sa_ll[2] == socket.PACKET_OUTGOING: + return None + if sa_ll[3] in conf.l2types: + cls = conf.l2types.num2layer[sa_ll[3]] # type: Type[Packet] + lvl = 2 + elif sa_ll[1] in conf.l3types: + cls = conf.l3types.num2layer[sa_ll[1]] + lvl = 3 + else: + cls = conf.default_l2 + warning("Unable to guess type (interface=%s protocol=%#x family=%i). Using %s", sa_ll[0], sa_ll[1], sa_ll[3], cls.name) # noqa: E501 + lvl = 3 - try: - pkt = cls(pkt) - except KeyboardInterrupt: - raise - except Exception: - if conf.debug_dissector: + try: + pkt = cls(data, **kwargs) + except KeyboardInterrupt: raise - pkt = conf.raw_layer(pkt) - if lvl == 2: - pkt = pkt.payload - - if pkt is not None: - if ts is None: - from scapy.arch import get_last_packet_timestamp - ts = get_last_packet_timestamp(self.ins) - pkt.time = ts - return pkt - - def send(self, x): - try: - sx = raw(x) - x.sent_time = time.time() - return self.outs.sendto(sx, (x.dst, 0)) - except socket.error as msg: - log_runtime.error(msg) + except Exception: + if conf.debug_dissector: + raise + pkt = conf.raw_layer(data) + + if lvl == 2: + pkt = pkt.payload + + if pkt is not None: + if ts is None: + from scapy.arch.linux import get_last_packet_timestamp + ts = get_last_packet_timestamp(self.ins) + pkt.time = ts + return pkt + + def send(self, x): + # type: (Packet) -> int + try: + sx = raw(x) + if self.outs: + x.sent_time = time.time() + return self.outs.sendto( + sx, + (x.dst, 0) + ) + except AttributeError: + raise ValueError( + "Missing 'dst' attribute in the first layer to be " + "sent using a native L3 socket ! (make sure you passed the " + "IP layer)" + ) + except socket.error as msg: + log_runtime.error(msg) + return 0 + + class L3RawSocket6(L3RawSocket): + def __init__(self, + type: int = ETH_P_IPV6, + filter: Optional[str] = None, + iface: Optional[_GlobInterfaceType] = None, + promisc: Optional[bool] = None, + nofilter: bool = False) -> None: + # NOTE: if fragmentation is needed, it will be done by the kernel (RFC 2292) # noqa: E501 + self.outs = socket.socket( + socket.AF_INET6, + socket.SOCK_RAW, + socket.IPPROTO_RAW + ) + self.ins = socket.socket( + socket.AF_PACKET, + socket.SOCK_RAW, + socket.htons(type) + ) + self.iface = cast(_GlobInterfaceType, iface) class SimpleSocket(SuperSocket): desc = "wrapper around a classic socket" + __selectable_force_select__ = True - def __init__(self, sock): + def __init__(self, sock, basecls=None): + # type: (socket.socket, Optional[Type[Packet]]) -> None self.ins = sock self.outs = sock + if basecls is None: + basecls = conf.raw_layer + self.basecls = basecls + + def recv_raw(self, x=MTU): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] + return self.basecls, self.ins.recv(x), None + + if WINDOWS: + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + from scapy.automaton import select_objects + return select_objects(sockets, remain) class StreamSocket(SimpleSocket): + """ + Wrap a stream socket into a layer 2 SuperSocket + + :param sock: the socket to wrap + :param basecls: the base class packet to use to dissect the packet + """ desc = "transforms a stream socket into a layer 2" - nonblocking_socket = True - def __init__(self, sock, basecls=None): - if basecls is None: - basecls = conf.raw_layer - SimpleSocket.__init__(self, sock) - self.basecls = basecls + def __init__(self, + sock, # type: socket.socket + basecls=None, # type: Optional[Type[Packet]] + ): + # type: (...) -> None + from scapy.sessions import streamcls + self.rcvcls = streamcls(basecls or conf.raw_layer) + self.metadata: Dict[str, Any] = {} + self.streamsession: Dict[str, Any] = {} + self._buf = b"" + super(StreamSocket, self).__init__(sock, basecls=basecls) + + def recv(self, x=None, **kwargs): + # type: (Optional[int], Any) -> Optional[Packet] + if x is None: + x = MTU + + while True: + # Block but in PEEK mode + data = self.ins.recv(x, socket.MSG_PEEK) + if data == b"": + raise EOFError + x = len(data) + pkt = self.rcvcls(self._buf + data, self.metadata, self.streamsession) + if pkt is None: # Incomplete packet. + self._buf += self.ins.recv(x) + else: + break - def recv(self, x=MTU): - pkt = self.ins.recv(x, socket.MSG_PEEK) - x = len(pkt) - if x == 0: - return None - pkt = self.basecls(pkt) + self.metadata.clear() + # Strip any madding pad = pkt.getlayer(conf.padding_layer) if pad is not None and pad.underlayer is not None: - del(pad.underlayer.payload) - from scapy.packet import NoPayload + del pad.underlayer.payload while pad is not None and not isinstance(pad, NoPayload): x -= len(pad.load) pad = pad.payload + # Only receive the packet length self.ins.recv(x) + self._buf = b"" return pkt -class SSLStreamSocket(StreamSocket): - desc = "similar usage than StreamSocket but specialized for handling SSL-wrapped sockets" # noqa: E501 +class StreamSocketPeekless(StreamSocket): + desc = "StreamSocket that doesn't use MSG_PEEK" def __init__(self, sock, basecls=None): - self._buf = b"" - super(SSLStreamSocket, self).__init__(sock, basecls) + # type: (socket.socket, Optional[Type[Packet]]) -> None + from scapy.sessions import TCPSession + self.sess = TCPSession(app=True) + super(StreamSocketPeekless, self).__init__(sock, basecls) # 65535, the default value of x is the maximum length of a TLS record - def recv(self, x=65535): - pkt = None - if self._buf != b"": - try: - pkt = self.basecls(self._buf) - except Exception: - # We assume that the exception is generated by a buffer underflow # noqa: E501 - pass - + def recv(self, x=None, **kwargs): + # type: (Optional[int], **Any) -> Optional[Packet] + if x is None: + x = MTU + # Block + try: + data = self.ins.recv(x) + except OSError: + raise EOFError + try: + pkt = self.sess.process(data, cls=self.basecls) # type: ignore + except struct.error: + # Buffer underflow + pkt = None + if data == b"" and not pkt: + raise EOFError if not pkt: - buf = self.ins.recv(x) - if len(buf) == 0: - raise socket.error((100, "Underlying stream socket tore down")) - self._buf += buf + return self.recv(x) + return pkt - x = len(self._buf) - pkt = self.basecls(self._buf) - pad = pkt.getlayer(conf.padding_layer) + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + queued = [ + x + for x in sockets + if isinstance(x, StreamSocketPeekless) and x.sess.data + ] + if queued: + return queued # type: ignore + return super(StreamSocketPeekless, StreamSocketPeekless).select( + sockets, + remain=remain, + ) - if pad is not None and pad.underlayer is not None: - del(pad.underlayer.payload) - while pad is not None and not isinstance(pad, scapy.packet.NoPayload): - x -= len(pad.load) - pad = pad.payload - self._buf = self._buf[x:] - return pkt + +# Old name: SSLStreamSocket +SSLStreamSocket = StreamSocketPeekless class L2ListenTcpdump(SuperSocket): desc = "read packets at layer 2 using tcpdump" - def __init__(self, iface=None, promisc=None, filter=None, nofilter=False, - prog=None, *arg, **karg): + def __init__(self, + iface=None, # type: Optional[_GlobInterfaceType] + promisc=None, # type: Optional[bool] + filter=None, # type: Optional[str] + nofilter=False, # type: bool + prog=None, # type: Optional[str] + quiet=False, # type: bool + *arg, # type: Any + **karg # type: Any + ): + # type: (...) -> None self.outs = None args = ['-w', '-', '-s', '65535'] + self.iface = "any" + if iface is None and (WINDOWS or DARWIN): + self.iface = iface = conf.iface + if promisc is None: + promisc = conf.sniff_promisc if iface is not None: - if WINDOWS: - try: - args.extend(['-i', iface.pcap_name]) - except AttributeError: - args.extend(['-i', iface]) - else: - args.extend(['-i', iface]) - elif WINDOWS or DARWIN: - args.extend(['-i', conf.iface.pcap_name if WINDOWS else conf.iface]) # noqa: E501 + args.extend(['-i', network_name(iface)]) if not promisc: args.append('-p') if not nofilter: @@ -379,83 +598,71 @@ def __init__(self, iface=None, promisc=None, filter=None, nofilter=False, filter = "not (%s)" % conf.except_filter if filter is not None: args.append(filter) - self.tcpdump_proc = tcpdump(None, prog=prog, args=args, getproc=True) - self.ins = PcapReader(self.tcpdump_proc.stdout) + self.tcpdump_proc = tcpdump( + None, prog=prog, args=args, getproc=True, quiet=quiet) + self.reader = PcapReader(self.tcpdump_proc.stdout) + self.ins = self.reader # type: ignore - def recv(self, x=MTU): - return self.ins.recv(x) + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + return self.reader.recv(x, **kwargs) def close(self): + # type: () -> None SuperSocket.close(self) self.tcpdump_proc.kill() + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + if (WINDOWS or DARWIN): + return sockets + return SuperSocket.select(sockets, remain=remain) -class TunTapInterface(SuperSocket): - """A socket to act as the host's peer of a tun / tap interface. - """ - desc = "Act as the host's peer of a tun / tap interface" +# More abstract objects - def __init__(self, iface=None, mode_tun=None, *arg, **karg): - self.iface = conf.iface if iface is None else iface - self.mode_tun = ("tun" in self.iface) if mode_tun is None else mode_tun - self.closed = True - self.open() - - def open(self): - """Open the TUN or TAP device.""" - if not self.closed: - return - self.outs = self.ins = open( - "/dev/net/tun" if LINUX else ("/dev/%s" % self.iface), "r+b", - buffering=0 - ) - if LINUX: - from fcntl import ioctl - # TUNSETIFF = 0x400454ca - # IFF_TUN = 0x0001 - # IFF_TAP = 0x0002 - # IFF_NO_PI = 0x1000 - ioctl(self.ins, 0x400454ca, struct.pack( - "16sH", bytes_encode(self.iface), - 0x0001 if self.mode_tun else 0x1002, - )) - self.closed = False - - def __call__(self, *arg, **karg): - """Needed when using an instantiated TunTapInterface object for -conf.L2listen, conf.L2socket or conf.L3socket. +class IterSocket(SuperSocket): + desc = "wrapper around an iterable" + nonblocking_socket = True - """ - return self + def __init__(self, obj): + # type: (_PacketIterable) -> None + if not obj: + self.iter = iter([]) # type: Iterator[Packet] + elif isinstance(obj, IterSocket): + self.iter = obj.iter + elif isinstance(obj, SndRcvList): + def _iter(obj=cast(SndRcvList, obj)): + # type: (SndRcvList) -> Iterator[Packet] + for s, r in obj: + if s.sent_time: + s.time = s.sent_time + yield s + yield r + + self.iter = _iter() + elif isinstance(obj, (list, PacketList)): + if isinstance(obj[0], bytes): + self.iter = iter(obj) + else: + self.iter = (y for x in obj for y in x) + else: + self.iter = obj.__iter__() - def recv(self, x=MTU): - if self.mode_tun: - data = os.read(self.ins.fileno(), x + 4) - proto = struct.unpack('!H', data[2:4])[0] - return conf.l3types.get(proto, conf.raw_layer)(data[4:]) - return conf.l2types.get(1, conf.raw_layer)( - os.read(self.ins.fileno(), x) - ) + @staticmethod + def select(sockets, remain=None): + # type: (List[SuperSocket], Any) -> List[SuperSocket] + return sockets - def send(self, x): - sx = raw(x) - if self.mode_tun: - try: - proto = conf.l3types[type(x)] - except KeyError: - log_runtime.warning( - "Cannot find layer 3 protocol value to send %s in " - "conf.l3types, using 0", - x.name if hasattr(x, "name") else type(x).__name__ - ) - proto = 0 - sx = struct.pack('!HH', 0, proto) + sx + def recv(self, x=None, **kwargs): + # type: (Optional[int], Any) -> Optional[Packet] try: - try: - x.sent_time = time.time() - except AttributeError: - pass - return os.write(self.outs.fileno(), sx) - except socket.error: - log_runtime.error("%s send", self.__class__.__name__, exc_info=True) # noqa: E501 + pkt = next(self.iter) + return pkt.__class__(bytes(pkt), **kwargs) + except StopIteration: + raise EOFError + + def close(self): + # type: () -> None + pass diff --git a/scapy/themes.py b/scapy/themes.py index f9cbc300648..7124bf0c26d 100644 --- a/scapy/themes.py +++ b/scapy/themes.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Color themes for the interactive console. @@ -11,9 +11,18 @@ # Color themes # ################## -import cgi +import html import sys +from typing import ( + Any, + List, + Optional, + Tuple, + cast, +) +from scapy.compat import Protocol + class ColorTable: colors = { # Format: (ansi, pygments) @@ -25,7 +34,8 @@ class ColorTable: "blue": ("\033[34m", "#ansiblue"), "purple": ("\033[35m", "#ansipurple"), "cyan": ("\033[36m", "#ansicyan"), - "grey": ("\033[37m", "#ansiwhite"), + "white": ("\033[37m", "#ansiwhite"), + "grey": ("\033[38;5;246m", "#ansiwhite"), "reset": ("\033[39m", "noinherit"), # background "bg_black": ("\033[40m", "bg:#ansiblack"), @@ -35,7 +45,7 @@ class ColorTable: "bg_blue": ("\033[44m", "bg:#ansiblue"), "bg_purple": ("\033[45m", "bg:#ansipurple"), "bg_cyan": ("\033[46m", "bg:#ansicyan"), - "bg_grey": ("\033[47m", "bg:#ansiwhite"), + "bg_white": ("\033[47m", "bg:#ansiwhite"), "bg_reset": ("\033[49m", "noinherit"), # specials "normal": ("\033[0m", "noinherit"), # color & brightness @@ -44,16 +54,22 @@ class ColorTable: "blink": ("\033[5m", ""), "invert": ("\033[7m", ""), } + inv_map = {v[0]: v[1] for k, v in colors.items()} def __repr__(self): + # type: () -> str return "" def __getattr__(self, attr): + # type: (str) -> str return self.colors.get(attr, [""])[0] - def ansi_to_pygments(self, x): # Transform ansi encoded text to Pygments text # noqa: E501 - inv_map = {v[0]: v[1] for k, v in self.colors.items()} - for k, v in inv_map.items(): + def ansi_to_pygments(self, x): + # type: (str) -> str + """ + Transform ansi encoded text to Pygments text + """ + for k, v in self.inv_map.items(): x = x.replace(k, " " + v) return x.strip() @@ -61,31 +77,78 @@ def ansi_to_pygments(self, x): # Transform ansi encoded text to Pygments text Color = ColorTable() -def create_styler(fmt=None, before="", after="", fmt2="%s"): - def do_style(val, fmt=fmt, before=before, after=after, fmt2=fmt2): +class _ColorFormatterType(Protocol): + def __call__(self, + val: Any, + fmt: Optional[str] = None, + fmt2: str = "", + before: str = "", + after: str = "") -> str: + pass + + +def create_styler(fmt=None, # type: Optional[str] + before="", # type: str + after="", # type: str + fmt2="%s" # type: str + ): + # type: (...) -> _ColorFormatterType + def do_style(val: Any, + fmt: Optional[str] = fmt, + fmt2: str = fmt2, + before: str = before, + after: str = after) -> str: if fmt is None: - if not isinstance(val, str): - val = str(val) + sval = str(val) else: - val = fmt % val - return fmt2 % (before + val + after) + sval = fmt % val + return fmt2 % (before + sval + after) return do_style class ColorTheme: + style_normal = "" + style_prompt = "" + style_punct = "" + style_id = "" + style_not_printable = "" + style_layer_name = "" + style_field_name = "" + style_field_value = "" + style_emph_field_name = "" + style_emph_field_value = "" + style_depreciate_field_name = "" + style_packetlist_name = "" + style_packetlist_proto = "" + style_packetlist_value = "" + style_fail = "" + style_success = "" + style_odd = "" + style_even = "" + style_opening = "" + style_active = "" + style_closed = "" + style_left = "" + style_right = "" + style_logo = "" + def __repr__(self): + # type: () -> str return "<%s>" % self.__class__.__name__ def __reduce__(self): + # type: () -> Tuple[type, Any, Any] return (self.__class__, (), ()) def __getattr__(self, attr): + # type: (str) -> _ColorFormatterType if attr in ["__getstate__", "__setstate__", "__getinitargs__", "__reduce_ex__"]: raise AttributeError() return create_styler() def format(self, string, fmt): + # type: (str, str) -> str for style in fmt.split("+"): string = getattr(self, style)(string) return string @@ -97,6 +160,7 @@ class NoTheme(ColorTheme): class AnsiColorTheme(ColorTheme): def __getattr__(self, attr): + # type: (str) -> _ColorFormatterType if attr.startswith("__"): raise AttributeError(attr) s = "style_%s" % attr @@ -111,30 +175,6 @@ def __getattr__(self, attr): return create_styler(before=before, after=after) - style_normal = "" - style_prompt = "" - style_punct = "" - style_id = "" - style_not_printable = "" - style_layer_name = "" - style_field_name = "" - style_field_value = "" - style_emph_field_name = "" - style_emph_field_value = "" - style_packetlist_name = "" - style_packetlist_proto = "" - style_packetlist_value = "" - style_fail = "" - style_success = "" - style_odd = "" - style_even = "" - style_opening = "" - style_active = "" - style_closed = "" - style_left = "" - style_right = "" - style_logo = "" - class BlackAndWhite(AnsiColorTheme, NoTheme): pass @@ -145,7 +185,8 @@ class DefaultTheme(AnsiColorTheme): style_prompt = Color.blue + Color.bold style_punct = Color.normal style_id = Color.blue + Color.bold - style_not_printable = Color.grey + style_not_printable = Color.white + style_depreciate_field_name = Color.grey style_layer_name = Color.red + Color.bold style_field_name = Color.blue style_field_value = Color.purple @@ -160,7 +201,7 @@ class DefaultTheme(AnsiColorTheme): style_odd = Color.black style_opening = Color.yellow style_active = Color.black - style_closed = Color.grey + style_closed = Color.white style_left = Color.blue + Color.invert style_right = Color.red + Color.invert style_logo = Color.green + Color.bold @@ -228,9 +269,9 @@ class ColorOnBlackTheme(AnsiColorTheme): style_fail = Color.red + Color.bold style_success = Color.green style_even = Color.black + Color.bold - style_odd = Color.grey + style_odd = Color.white style_opening = Color.yellow - style_active = Color.grey + Color.bold + style_active = Color.white + Color.bold style_closed = Color.black + Color.bold style_left = Color.cyan + Color.bold style_right = Color.red + Color.bold @@ -238,7 +279,7 @@ class ColorOnBlackTheme(AnsiColorTheme): class FormatTheme(ColorTheme): - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> _ColorFormatterType: if attr.startswith("__"): raise AttributeError(attr) colfmt = self.__class__.__dict__.get("style_%s" % attr, "%s") @@ -246,6 +287,10 @@ def __getattr__(self, attr): class LatexTheme(FormatTheme): + r""" + You can prepend the output from this theme with + \tt\obeyspaces\obeylines\tiny\noindent + """ style_prompt = r"\textcolor{blue}{%s}" style_not_printable = r"\textcolor{gray}{%s}" style_layer_name = r"\textcolor{red}{\bf %s}" @@ -264,6 +309,14 @@ class LatexTheme(FormatTheme): # style_odd = "" style_logo = r"\textcolor{green}{\bf %s}" + def __getattr__(self, attr: str) -> _ColorFormatterType: + from scapy.utils import tex_escape + styler = super(LatexTheme, self).__getattr__(attr) + return cast( + _ColorFormatterType, + lambda x, *args, **kwargs: styler(tex_escape(str(x)), *args, **kwargs), + ) + class LatexTheme2(FormatTheme): style_prompt = r"@`@textcolor@[@blue@]@@[@%s@]@" @@ -277,7 +330,7 @@ class LatexTheme2(FormatTheme): style_packetlist_proto = r"@`@textcolor@[@blue@]@@[@%s@]@" style_packetlist_value = r"@`@textcolor@[@purple@]@@[@%s@]@" style_fail = r"@`@textcolor@[@red@]@@[@@`@bfseries@[@@]@%s@]@" - style_success = r"@`@textcolor@[@blue@]@@[@@`@bfserices@[@@]@%s@]@" + style_success = r"@`@textcolor@[@blue@]@@[@@`@bfseries@[@@]@%s@]@" style_even = r"@`@textcolor@[@gray@]@@[@@`@bfseries@[@@]@%s@]@" # style_odd = r"@`@textcolor@[@black@]@@[@@`@bfseries@[@@]@%s@]@" style_left = r"@`@textcolor@[@blue@]@@[@%s@]@" @@ -324,6 +377,7 @@ class HTMLTheme2(HTMLTheme): def apply_ipython_style(shell): + # type: (Any) -> None """Updates the specified IPython console shell with the conf.color_theme scapy theme.""" try: @@ -349,7 +403,7 @@ def apply_ipython_style(shell): # default shell.colors = 'neutral' try: - get_ipython() + get_ipython() # type: ignore # This function actually contains tons of hacks color_magic = shell.magics_manager.magics["line"]["colors"] color_magic(shell.colors) @@ -363,7 +417,7 @@ def apply_ipython_style(shell): if isinstance(conf.color_theme, (FormatTheme, NoTheme)): # Formatable if isinstance(conf.color_theme, HTMLTheme): - prompt = cgi.escape(conf.prompt) + prompt = html.escape(conf.prompt) elif isinstance(conf.color_theme, LatexTheme): from scapy.utils import tex_escape prompt = tex_escape(conf.prompt) @@ -379,9 +433,11 @@ def apply_ipython_style(shell): class ClassicPrompt(Prompts): def in_prompt_tokens(self, cli=None): + # type: (Any) -> List[Tuple[Any, str]] return [(Token.Prompt, prompt), ] def out_prompt_tokens(self): + # type: () -> List[Tuple[Any, str]] return [(Token.OutPrompt, ''), ] # Apply classic prompt style shell.prompts_class = ClassicPrompt @@ -390,6 +446,6 @@ def out_prompt_tokens(self): shell.highlighting_style_overrides = scapy_style # Apply if Live try: - get_ipython().refresh_style() + get_ipython().refresh_style() # type: ignore except NameError: pass diff --git a/scapy/tools/UTscapy.py b/scapy/tools/UTscapy.py index f1577e33a0b..f724a845fc0 100644 --- a/scapy/tools/UTscapy.py +++ b/scapy/tools/UTscapy.py @@ -1,36 +1,62 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Unit testing infrastructure for Scapy """ -from __future__ import absolute_import -from __future__ import print_function -import sys +import builtins +import bz2 +import copy import getopt import glob -import importlib import hashlib -import copy -import code -import bz2 +import importlib +import json +import logging +import os import os.path +import sys +import threading import time import traceback import warnings import zlib -from scapy.consts import WINDOWS -import scapy.modules.six as six -from scapy.modules.six.moves import range +from scapy.consts import WINDOWS, BIG_ENDIAN from scapy.config import conf -from scapy.compat import base64_bytes, bytes_hex, plain_str +from scapy.compat import base64_bytes +from scapy.themes import DefaultTheme, BlackAndWhite +from scapy.utils import tex_escape -# Util class # +# Check UTF-8 support # + +def _utf8_support(): + """ + Check UTF-8 support for the output + """ + try: + if WINDOWS: + return (sys.stdout.encoding == "utf-8") + return True + except AttributeError: + return False + + +if _utf8_support(): + arrow = "\u2514" + dash = "\u2501" + checkmark = "\u2713" +else: + arrow = "->" + dash = "--" + checkmark = "OK" + + +# Util class # class Bunch: __init__ = lambda self, **kw: setattr(self, '__dict__', kw) @@ -38,21 +64,40 @@ class Bunch: def retry_test(func): """Retries the passed function 3 times before failing""" - success = False - ex = Exception("Unknown") - for _ in six.moves.range(3): + v = None + tb = None + for _ in range(3): try: - result = func() - except Exception as e: + return func() + except Exception: + t, v, tb = sys.exc_info() time.sleep(1) - ex = e - else: - success = True - break - if not success: - raise ex - assert success - return result + + if v and tb: + raise v.with_traceback(tb) + + +def scapy_path(fname): + """Resolves a path relative to scapy's root folder""" + if fname.startswith('/'): + fname = fname[1:] + return os.path.abspath(os.path.join( + os.path.dirname(__file__), '../../', fname + )) + + +class no_debug_dissector: + """Context object used to disable conf.debug_dissector""" + def __init__(self, reverse=False): + self.new_value = reverse + + def __enter__(self): + self.old_dbg = conf.debug_dissector + conf.debug_dissector = self.new_value + + def __exit__(self, exc_type, exc_value, traceback): + conf.debug_dissector = self.old_dbg + # Import tool # @@ -128,12 +173,12 @@ class External_Files: /i7kinChIXSAmRgA==\n""") def get_local_dict(cls): - return {x: y.name for (x, y) in six.iteritems(cls.__dict__) + return {x: y.name for (x, y) in cls.__dict__.items() if isinstance(y, File)} get_local_dict = classmethod(get_local_dict) def get_URL_dict(cls): - return {x: y.URL for (x, y) in six.iteritems(cls.__dict__) + return {x: y.URL for (x, y) in cls.__dict__.items() if isinstance(y, File)} get_URL_dict = classmethod(get_URL_dict) @@ -162,7 +207,7 @@ def __getitem__(self, item): return getattr(self, item) def add_keywords(self, kws): - if isinstance(kws, six.string_types): + if isinstance(kws, str): kws = [kws.lower()] for kwd in kws: kwd = kwd.lower() @@ -239,6 +284,7 @@ def __init__(self, name): self.test = "" self.comments = "" self.result = "passed" + self.fresult = "" # make instance True at init to have a different truth value than None self.duration = 0 self.output = "" @@ -247,12 +293,11 @@ def __init__(self, name): self.crc = None self.expand = 1 - def decode(self): - if six.PY2: - self.test = self.test.decode("utf8", "ignore") - self.output = self.output.decode("utf8", "ignore") - self.comments = self.comments.decode("utf8", "ignore") - self.result = self.result.decode("utf8", "ignore") + def prepare(self, theme): + if self.result == "passed": + self.fresult = theme.success(self.result) + else: + self.fresult = theme.fail(self.result) def __nonzero__(self): return self.result == "passed" @@ -272,24 +317,23 @@ def parse_config_file(config_path, verb=3): "dump": 0, "docs": 0, "crc": true, - "scapy": "scapy", "preexec": {}, "global_preexec": "", "outputfile": null, "local": true, "format": "ansi", "num": null, + "extensions": [], "modules": [], "kw_ok": [], "kw_ko": [] } """ - import json - with open(config_path) as config_file: + with open(config_path, encoding='utf-8') as config_file: data = json.load(config_file) if verb > 2: - print("### Loaded config file", config_path, file=sys.stderr) + print(" %s Loaded config file" % arrow, config_path) def get_if_exist(key, default): return data[key] if key in data else default @@ -300,13 +344,13 @@ def get_if_exist(key, default): verb=get_if_exist("verb", 3), dump=get_if_exist("dump", 0), crc=get_if_exist("crc", 1), docs=get_if_exist("docs", 0), - scapy=get_if_exist("scapy", "scapy"), preexec=get_if_exist("preexec", {}), global_preexec=get_if_exist("global_preexec", ""), outfile=get_if_exist("outputfile", sys.stdout), local=get_if_exist("local", False), num=get_if_exist("num", None), modules=get_if_exist("modules", []), + extensions=get_if_exist("extensions", []), kw_ok=get_if_exist("kw_ok", []), kw_ko=get_if_exist("kw_ko", []), format=get_if_exist("format", "ansi")) @@ -321,38 +365,38 @@ def parse_campaign_file(campaign_file): test = None testnb = 0 - for l in campaign_file.readlines(): - if l[0] == '#': + for line in campaign_file.readlines(): + if line[0] == '#': continue - if l[0] == "~": - (test or testset or test_campaign).add_keywords(l[1:].split()) - elif l[0] == "%": - test_campaign.title = l[1:].strip() - elif l[0] == "+": - testset = TestSet(l[1:].strip()) + if line[0] == "~": + (test or testset or test_campaign).add_keywords(line[1:].split()) + elif line[0] == "%": + test_campaign.title = line[1:].strip() + elif line[0] == "+": + testset = TestSet(line[1:].strip()) test_campaign.add_testset(testset) test = None - elif l[0] == "=": - test = UnitTest(l[1:].strip()) + elif line[0] == "=": + test = UnitTest(line[1:].strip()) test.num = testnb testnb += 1 if testset is None: error_m = "Please create a test set (i.e. '+' section)." raise getopt.GetoptError(error_m) testset.add_test(test) - elif l[0] == "*": + elif line[0] == "*": if test is not None: - test.comments += l[1:] + test.comments += line[1:] elif testset is not None: - testset.comments += l[1:] + testset.comments += line[1:] else: - test_campaign.headcomments += l[1:] + test_campaign.headcomments += line[1:] else: if test is None: - if l.strip(): - print("Unknown content [%s]" % l.strip(), file=sys.stderr) + if line.strip(): + raise ValueError("Unknown content [%s]" % line.strip()) else: - test.test += l + test.test += line return test_campaign @@ -404,24 +448,18 @@ def docs_campaign(test_campaign): print("%s" % t.comments.strip().replace("\n", "")) print() print("Usage example::") - for l in t.test.split('\n'): - if not l.rstrip().endswith('# no_docs'): - print("\t%s" % l) + for line in t.test.split('\n'): + if not line.rstrip().endswith('# no_docs'): + print("\t%s" % line) # COMPUTE CAMPAIGN DIGESTS # -if six.PY2: - def crc32(x): - return "%08X" % (0xffffffff & zlib.crc32(x)) +def crc32(x): + return "%08X" % (0xffffffff & zlib.crc32(bytearray(x, "utf8"))) - def sha1(x): - return hashlib.sha1(x).hexdigest().upper() -else: - def crc32(x): - return "%08X" % (0xffffffff & zlib.crc32(bytearray(x, "utf8"))) - def sha1(x): - return hashlib.sha1(x.encode("utf8")).hexdigest().upper() +def sha1(x): + return hashlib.sha1(x.encode("utf8")).hexdigest().upper() def compute_campaign_digests(test_campaign): @@ -435,7 +473,7 @@ def compute_campaign_digests(test_campaign): ts.crc = crc32(dts) dc += "\0\x01" + dts test_campaign.crc = crc32(dc) - with open(test_campaign.filename) as fdesc: + with open(test_campaign.filename, encoding='utf-8') as fdesc: test_campaign.sha = sha1(fdesc.read()) @@ -477,10 +515,23 @@ def remove_empty_testsets(test_campaign): # RUN TEST # -def run_test(test, get_interactive_session, verb=3, ignore_globals=None, my_globals=None): +def _run_test_timeout(test, get_interactive_session, verb=3, my_globals=None): + """Run a test with timeout""" + from scapy.autorun import StopAutorunTimeout + try: + return get_interactive_session(test, + timeout=5 * 60, # 5 min + verb=verb, + my_globals=my_globals) + except StopAutorunTimeout as ex: + return "@@@@@@@@@@@@@@@@@ Test timed out ! @@@@@@@@@@@@@@@@@\n" + ex.code_run, False + + +def run_test(test, get_interactive_session, theme, verb=3, + my_globals=None): """An internal UTScapy function to run a single test""" start_time = time.time() - test.output, res = get_interactive_session(test.test.strip(), ignore_globals=ignore_globals, verb=verb, my_globals=my_globals) + test.output, res = _run_test_timeout(test.test.strip(), get_interactive_session, verb=verb, my_globals=my_globals) test.result = "failed" try: if res is None or res: @@ -498,13 +549,13 @@ def run_test(test, get_interactive_session, verb=3, ignore_globals=None, my_glob # Add optional debugging data to log if debug.crashed_on: cls, val = debug.crashed_on - test.output += "\n\nPACKET DISSECTION FAILED ON:\n %s(hex_bytes('%s'))" % (cls.__name__, plain_str(bytes_hex(val))) + test.output += "\n\nPACKET DISSECTION FAILED ON:\n %s(bytes.fromhex('%s'))" % (cls.__name__, val.hex()) debug.crashed_on = None - test.decode() + test.prepare(theme) if verb > 2: - print("%(result)6s %(crc)s %(duration)06.2fs %(name)s" % test, file=sys.stderr) + print("%(fresult)6s %(crc)s %(duration)06.2fs %(name)s" % test) elif verb > 1: - print("%(result)6s %(crc)s %(name)s" % test, file=sys.stderr) + print("%(fresult)6s %(crc)s %(name)s" % test) return bool(test) @@ -513,34 +564,48 @@ def run_test(test, get_interactive_session, verb=3, ignore_globals=None, my_glob def import_UTscapy_tools(ses): """Adds UTScapy tools directly to a session""" - ses["retry_test"] = retry_test ses["Bunch"] = Bunch - - -def run_campaign(test_campaign, get_interactive_session, drop_to_interpreter=False, verb=3, ignore_globals=None): # noqa: E501 + ses["retry_test"] = retry_test + ses["scapy_path"] = scapy_path + ses["no_debug_dissector"] = no_debug_dissector + if WINDOWS: + from scapy.arch.windows import _route_add_loopback + _route_add_loopback() + ses["conf"].ifaces = conf.ifaces + ses["conf"].route.routes = conf.route.routes + ses["conf"].route6.routes = conf.route6.routes + + +def run_campaign(test_campaign, get_interactive_session, theme, + drop_to_interpreter=False, verb=3, + scapy_ses=None): passed = failed = 0 - scapy_ses = importlib.import_module(".all", "scapy").__dict__ - import_UTscapy_tools(scapy_ses) if test_campaign.preexec: - test_campaign.preexec_output = get_interactive_session(test_campaign.preexec.strip(), ignore_globals=ignore_globals, my_globals=scapy_ses)[0] - # Drop + test_campaign.preexec_output = get_interactive_session( + test_campaign.preexec.strip(), + my_globals=scapy_ses + )[0] - def drop(scapy_ses): - code.interact(banner="Test '%s' failed. " - "exit() to stop, Ctrl-D to leave " - "this interpreter and continue " - "with the current test campaign" - % t.name, local=scapy_ses) + # Drop + def drop(t, scapy_ses): + from scapy.main import interact + interact( + mybanner="Test '%s' failed.\n\n%s" % (t.name, t.output), + mybanneronly=True, + mydict=scapy_ses, + argv=[None, "-H"], + ) try: for i, testset in enumerate(test_campaign): for j, t in enumerate(testset): - if run_test(t, get_interactive_session, verb, my_globals=scapy_ses): + if run_test(t, get_interactive_session, theme, + verb=verb, my_globals=scapy_ses): passed += 1 else: failed += 1 if drop_to_interpreter: - drop(scapy_ses) + drop(t, scapy_ses) test_campaign.duration += t.duration except KeyboardInterrupt: failed += 1 @@ -548,29 +613,40 @@ def drop(scapy_ses): test_campaign.trunc(i + 1) test_campaign.interrupted = True if verb: - print("Campaign interrupted!", file=sys.stderr) - if drop_to_interpreter: - drop(scapy_ses) + print("Campaign interrupted!") test_campaign.passed = passed test_campaign.failed = failed + style = [theme.success, theme.fail][bool(failed)] if verb > 2: - print("Campaign CRC=%(crc)s in %(duration)06.2fs SHA=%(sha)s" % test_campaign, file=sys.stderr) # noqa: E501 - print("PASSED=%i FAILED=%i" % (passed, failed), file=sys.stderr) + print("Campaign CRC=%(crc)s in %(duration)06.2fs SHA=%(sha)s" % test_campaign) + print(style("PASSED=%i FAILED=%i" % (passed, failed))) elif verb: - print("Campaign CRC=%(crc)s SHA=%(sha)s" % test_campaign, file=sys.stderr) # noqa: E501 - print("PASSED=%i FAILED=%i" % (passed, failed), file=sys.stderr) + print("Campaign CRC=%(crc)s SHA=%(sha)s" % test_campaign) + print(style("PASSED=%i FAILED=%i" % (passed, failed))) return failed # INFO LINES # -def info_line(test_campaign): +def info_line(test_campaign, theme): filename = test_campaign.filename + duration = test_campaign.duration + if duration > 10: + duration = theme.format(duration, "bg_red+white") + elif duration > 5: + duration = theme.format(duration, "red") if filename is None: - return "Run %s by UTscapy" % time.ctime() + return "Run at %s by UTscapy in %s" % ( + time.strftime("%H:%M:%S"), + duration + ) else: - return "Run %s from [%s] by UTscapy" % (time.ctime(), filename) + return "Run at %s from [%s] by UTscapy in %s" % ( + time.strftime("%H:%M:%S"), + filename, + duration + ) def html_info_line(test_campaign): @@ -581,12 +657,25 @@ def html_info_line(test_campaign): return """Run %s from [%s] by UTscapy
""" % (time.ctime(), filename) # noqa: E501 +def latex_info_line(test_campaign): + filename = test_campaign.filename + if filename is None: + return """by UTscapy""", """%s""" % time.ctime() + else: + return """from %s by UTscapy""" % tex_escape(filename), """%s""" % time.ctime() + + # CAMPAIGN TO something # -def campaign_to_TEXT(test_campaign): - output = "%(title)s\n" % test_campaign - output += "-- " + info_line(test_campaign) + "\n\n" - output += "Passed=%(passed)i\nFailed=%(failed)i\n\n%(headcomments)s\n" % test_campaign +def campaign_to_TEXT(test_campaign, theme): + ptheme = [lambda x: x, theme.success][bool(test_campaign.passed)] + ftheme = [lambda x: x, theme.fail][bool(test_campaign.failed)] + + output = theme.green("\n%(title)s\n" % test_campaign) + output += dash + " " + info_line(test_campaign, theme) + "\n" + output += ptheme(" " + arrow + " Passed=%(passed)i\n" % test_campaign) + output += ftheme(" " + arrow + " Failed=%(failed)i\n" % test_campaign) + output += "%(headcomments)s\n" % test_campaign for testset in test_campaign: if any(t.expand for t in testset): @@ -598,16 +687,16 @@ def campaign_to_TEXT(test_campaign): return output -def campaign_to_ANSI(test_campaign): - return campaign_to_TEXT(test_campaign) +def campaign_to_ANSI(test_campaign, theme): + return campaign_to_TEXT(test_campaign, theme) def campaign_to_xUNIT(test_campaign): output = '\n\n' for testset in test_campaign: for t in testset: - output += ' \t: only tests whose numbers are given (eg. 1,3-7,12) +-N\t\t: force non root -m \t: additional module to put in the namespace -k ,,...\t: include only tests with one of those keywords (can be used many times) -K ,,...\t: remove tests with one of those keywords (can be used many times) -P -""", file=sys.stderr) +""") raise SystemExit # MAIN # def execute_campaign(TESTFILE, OUTPUTFILE, PREEXEC, NUM, KW_OK, KW_KO, DUMP, DOCS, - FORMAT, VERB, ONLYFAILED, CRC, INTERPRETER, autorun_func, pos_begin=0, ignore_globals=None): # noqa: E501 + FORMAT, VERB, ONLYFAILED, CRC, INTERPRETER, + autorun_func, theme, pos_begin=0, + scapy_ses=None): # noqa: E501 # Parse test file - test_campaign = parse_campaign_file(TESTFILE) + try: + test_campaign = parse_campaign_file(TESTFILE) + except ValueError as ex: + print( + theme.red("Error while parsing '%s': '%s'" % (TESTFILE.name, ex)) + ) + sys.exit(1) # Report parameters if PREEXEC: @@ -814,7 +926,12 @@ def execute_campaign(TESTFILE, OUTPUTFILE, PREEXEC, NUM, KW_OK, KW_KO, DUMP, DOC # Run tests test_campaign.output_file = OUTPUTFILE - result = run_campaign(test_campaign, autorun_func[FORMAT], drop_to_interpreter=INTERPRETER, verb=VERB, ignore_globals=None) # noqa: E501 + result = run_campaign( + test_campaign, autorun_func[FORMAT], theme, + drop_to_interpreter=INTERPRETER, + verb=VERB, + scapy_ses=scapy_ses + ) # Shrink passed if ONLYFAILED: @@ -826,9 +943,9 @@ def execute_campaign(TESTFILE, OUTPUTFILE, PREEXEC, NUM, KW_OK, KW_KO, DUMP, DOC # Generate report if FORMAT == Format.TEXT: - output = campaign_to_TEXT(test_campaign) + output = campaign_to_TEXT(test_campaign, theme) elif FORMAT == Format.ANSI: - output = campaign_to_ANSI(test_campaign) + output = campaign_to_ANSI(test_campaign, theme) elif FORMAT == Format.HTML: test_campaign.startNum(pos_begin) output = campaign_to_HTML(test_campaign) @@ -846,20 +963,31 @@ def resolve_testfiles(TESTFILES): for tfile in TESTFILES[:]: if "*" in tfile: TESTFILES.remove(tfile) - TESTFILES.extend(glob.glob(tfile)) + TESTFILES.extend(sorted(glob.glob(tfile))) return TESTFILES def main(): argv = sys.argv[1:] - ignore_globals = list(six.moves.builtins.__dict__) + logger = logging.getLogger("scapy") + logger.addHandler(logging.StreamHandler()) + + # Treat SyntaxWarning as errors + warnings.filterwarnings("error", category=SyntaxWarning) + + import scapy + print(dash + " UTScapy - Scapy %s - %s" % ( + scapy.__version__, sys.version.split(" ")[0] + )) # Parse arguments FORMAT = Format.ANSI OUTPUTFILE = sys.stdout + OUTPUTFILE.reconfigure(encoding='utf-8') LOCAL = 0 NUM = None + NON_ROOT = False KW_OK = [] KW_KO = [] DUMP = 0 @@ -871,11 +999,12 @@ def main(): GLOB_PREEXEC = "" PREEXEC_DICT = {} MODULES = [] + EXTENSIONS = [] TESTFILES = [] ANNOTATIONS_MODE = False INTERPRETER = False try: - opts = getopt.getopt(argv, "o:t:T:c:f:hbln:m:k:K:DRdCiFqP:s:x") + opts = getopt.getopt(argv, "o:t:T:c:f:hbln:m:k:K:DRdCiFqNP:s:x") for opt, optarg in opts[0]: if opt == "-h": usage() @@ -923,6 +1052,7 @@ def main(): LOCAL = 1 if data.local else 0 NUM = data.num MODULES = data.modules + EXTENSIONS = data.extensions KW_OK.extend(data.kw_ok) KW_KO.extend(data.kw_ko) try: @@ -950,6 +1080,8 @@ def main(): except ValueError: v1, v2 = [int(e) for e in v.split('-', 1)] NUM.extend(range(v1, v2 + 1)) + elif opt == "-N": + NON_ROOT = True elif opt == "-m": MODULES.append(optarg) elif opt == "-k": @@ -957,66 +1089,65 @@ def main(): elif opt == "-K": KW_KO.extend(optarg.split(",")) - # Disable tests if needed + except getopt.GetoptError as msg: + print("ERROR:", msg) + raise SystemExit - # Discard Python3 tests when using Python2 - if six.PY2: - KW_KO.append("python3_only") - if VERB > 2: - print("### Python 2 mode ###") - try: - if os.getuid() != 0: # Non root - # Discard root tests - KW_KO.append("netaccess") - KW_KO.append("needs_root") - if VERB > 2: - print("### Non-root mode ###") - except AttributeError: - pass - - if conf.use_pcap: - KW_KO.append("not_pcapdnet") - if VERB > 2: - print("### libpcap mode ###") + if FORMAT in [Format.LIVE, Format.ANSI]: + theme = DefaultTheme() + else: + theme = BlackAndWhite() - # Process extras - if six.PY3: - KW_KO.append("FIXME_py3") + # Disable tests if needed - if ANNOTATIONS_MODE: - try: - from pyannotate_runtime import collect_types - except ImportError: - raise ImportError("Please install pyannotate !") - collect_types.init_types_collection() - collect_types.start() + try: + if NON_ROOT or os.getuid() != 0: # Non root + # Discard root tests + KW_KO.append("needs_root") + if VERB > 2: + print(" " + arrow + " Non-root mode") + except AttributeError: + pass + + if BIG_ENDIAN: + KW_KO.append("little_endian_only") + if conf.use_pcap or WINDOWS: + KW_KO.append("not_libpcap") if VERB > 2: - print("### Booting scapy...", file=sys.stderr) + print(" " + arrow + " libpcap mode") + + if sys.version_info < (3, 8): + KW_KO.append("needs_py38plus") + + KW_KO.append("disabled") + + if ANNOTATIONS_MODE: try: - from scapy import all as scapy - except Exception as e: - print("[CRITICAL]: Cannot import Scapy: %s" % e, file=sys.stderr) - traceback.print_exc() - sys.exit(1) # Abort the tests - - for m in MODULES: - try: - mod = import_module(m) - six.moves.builtins.__dict__.update(mod.__dict__) - except ImportError as e: - raise getopt.GetoptError("cannot import [%s]: %s" % (m, e)) + from pyannotate_runtime import collect_types + except ImportError: + raise ImportError("Please install pyannotate !") + collect_types.init_types_collection() + collect_types.start() - if WINDOWS: - from scapy.arch.windows import route_add_loopback - route_add_loopback() + if VERB > 2: + print(" " + arrow + " Booting scapy...") + try: + from scapy import all as scapy + except Exception as e: + print("[CRITICAL]: Cannot import Scapy: %s" % e) + traceback.print_exc() + sys.exit(1) # Abort the tests - # Add SCAPY_ROOT_DIR environment variable, used for tests - os.environ['SCAPY_ROOT_DIR'] = os.environ.get("PWD", os.getcwd()) + for m in MODULES: + try: + mod = import_module(m) + builtins.__dict__.update(mod.__dict__) + except ImportError as e: + raise getopt.GetoptError("cannot import [%s]: %s" % (m, e)) - except getopt.GetoptError as msg: - print("ERROR:", msg, file=sys.stderr) - raise SystemExit + for ext in EXTENSIONS: + conf.exts.load(ext) autorun_func = { Format.TEXT: scapy.autorun_get_text_interactive_session, @@ -1028,7 +1159,7 @@ def main(): } if VERB > 2: - print("### Starting tests...", file=sys.stderr) + print(" " + arrow + " Discovering tests files...") glob_output = "" glob_result = 0 @@ -1037,7 +1168,7 @@ def main(): UNIQUE = len(TESTFILES) == 1 # Resolve tags and asterix - for prex in six.iterkeys(copy.copy(PREEXEC_DICT)): + for prex in copy.copy(PREEXEC_DICT).keys(): if "*" in prex: pycode = PREEXEC_DICT[prex] del PREEXEC_DICT[prex] @@ -1048,17 +1179,24 @@ def main(): pos_begin = 0 runned_campaigns = [] + + from scapy.main import _scapy_builtins + scapy_ses = _scapy_builtins() + import_UTscapy_tools(scapy_ses) + # Execute all files for TESTFILE in TESTFILES: if VERB > 2: - print("### Loading:", TESTFILE, file=sys.stderr) + print(theme.green(dash + " Loading: %s" % TESTFILE)) PREEXEC = PREEXEC_DICT[TESTFILE] if TESTFILE in PREEXEC_DICT else GLOB_PREEXEC - with open(TESTFILE) as testfile: - output, result, campaign = execute_campaign(testfile, OUTPUTFILE, - PREEXEC, NUM, KW_OK, KW_KO, - DUMP, DOCS, FORMAT, VERB, ONLYFAILED, - CRC, INTERPRETER, autorun_func, pos_begin, - ignore_globals) + with open(TESTFILE, encoding='utf-8') as testfile: + output, result, campaign = execute_campaign( + testfile, OUTPUTFILE, PREEXEC, NUM, KW_OK, KW_KO, DUMP, DOCS, + FORMAT, VERB, ONLYFAILED, CRC, INTERPRETER, + autorun_func, theme, + pos_begin=pos_begin, + scapy_ses=copy.copy(scapy_ses) + ) runned_campaigns.append(campaign) pos_begin = campaign.end_pos if UNIQUE: @@ -1070,7 +1208,9 @@ def main(): break if VERB > 2: - print("### Writing output...", file=sys.stderr) + print( + checkmark + " All campaigns executed. Writing output..." + ) if ANNOTATIONS_MODE: collect_types.stop() @@ -1079,20 +1219,38 @@ def main(): # Concenate outputs if FORMAT == Format.HTML: glob_output = pack_html_campaigns(runned_campaigns, glob_output, LOCAL, glob_title) + if FORMAT == Format.LATEX: + glob_output = pack_latex_campaigns(runned_campaigns, glob_output, LOCAL, glob_title) # Write the final output # Note: on Python 2, we force-encode to ignore ascii errors # on Python 3, we need to detect the type of stream if OUTPUTFILE == sys.stdout: - OUTPUTFILE.write(glob_output.encode("utf8", "ignore") - if 'b' in OUTPUTFILE.mode or six.PY2 else glob_output) + print(glob_output, file=OUTPUTFILE) else: with open(OUTPUTFILE, "wb") as f: f.write(glob_output.encode("utf8", "ignore") - if 'b' in f.mode or six.PY2 else glob_output) + if 'b' in f.mode else glob_output) + + # Print end message + if VERB > 2: + if glob_result == 0: + print(theme.green("UTscapy ended successfully")) + else: + print(theme.red("UTscapy ended with error code %s" % glob_result)) - # Delete scapy's test environment vars - del os.environ['SCAPY_ROOT_DIR'] + # Check active threads + if VERB > 2: + if threading.active_count() > 1: + print("\nWARNING: UNFINISHED THREADS") + print(threading.enumerate()) + import multiprocessing + processes = multiprocessing.active_children() + if processes: + print("\nWARNING: UNFINISHED PROCESSES") + print(processes) + + sys.stdout.flush() # Return state return glob_result diff --git a/scapy/tools/__init__.py b/scapy/tools/__init__.py index f625678a0f2..a9c3091ec13 100644 --- a/scapy/tools/__init__.py +++ b/scapy/tools/__init__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ Additional tools to be run separately diff --git a/scapy/tools/automotive/__init__.py b/scapy/tools/automotive/__init__.py index bcf585e7e16..6911e5c59df 100644 --- a/scapy/tools/automotive/__init__.py +++ b/scapy/tools/automotive/__init__.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license """ Automotive related tools to be run separately diff --git a/scapy/tools/automotive/isotpscanner.py b/scapy/tools/automotive/isotpscanner.py index 4d9ff463476..e66d7122375 100755 --- a/scapy/tools/automotive/isotpscanner.py +++ b/scapy/tools/automotive/isotpscanner.py @@ -1,35 +1,37 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Nils Weiss # Copyright (C) Alexander Schroeder -# This program is published under a GPLv2 license -from __future__ import print_function import getopt import sys import signal import re +import threading from ast import literal_eval -import scapy.modules.six as six from scapy.config import conf from scapy.consts import LINUX -if six.PY2 or not LINUX or conf.use_pypy: +# Typing imports +from typing import ( + Tuple, + Optional, + Any, +) + +if not LINUX or conf.use_pypy: conf.contribs['CANSocket'] = {'use-python-can': True} from scapy.contrib.cansocket import CANSocket, PYTHON_CAN # noqa: E402 -from scapy.contrib.isotp import ISOTPScan # noqa: E402 - - -def signal_handler(sig, frame): - print('Interrupting scan!') - sys.exit(0) +from scapy.contrib.isotp import isotp_scan # noqa: E402 def usage(is_error): + # type: (bool) -> None print('''usage:\tisotpscanner [-i interface] [-c channel] [-a python-can_args] [-n NOISE_LISTEN_TIME] [-t SNIFF_TIME] [-x|--extended] [-C|--piso] [-v|--verbose] [-h|--help] @@ -71,7 +73,33 @@ def usage(is_error): file=sys.stderr if is_error else sys.stdout) +def create_socket(python_can_args, interface, channel): + # type: (Optional[str], Optional[str], str) -> Tuple[CANSocket, str] + + if PYTHON_CAN: + if python_can_args: + interface_string = "CANSocket(bustype=" \ + "'%s', channel='%s', %s)" % \ + (interface, channel, python_can_args) + arg_dict = dict((k, literal_eval(v)) for k, v in + (pair.split('=') for pair in + re.split(', | |,', python_can_args))) + sock = CANSocket(bustype=interface, channel=channel, + **arg_dict) + else: + interface_string = "CANSocket(bustype=" \ + "'%s', channel='%s')" % \ + (interface, channel) + sock = CANSocket(bustype=interface, channel=channel) + else: + sock = CANSocket(channel=channel) + interface_string = "\"%s\"" % channel + + return sock, interface_string + + def main(): + # type: () -> None extended = False piso = False verbose = False @@ -83,6 +111,7 @@ def main(): channel = None interface = None python_can_args = None + conf.verb = -1 options = getopt.getopt( sys.argv[1:], @@ -147,42 +176,33 @@ def main(): print("start must be equal or smaller than end.", file=sys.stderr) sys.exit(1) - sock = None - try: - if PYTHON_CAN: - if python_can_args: - interface_string = "CANSocket(bustype=" \ - "'%s', channel='%s', %s)" % \ - (interface, channel, python_can_args) - arg_dict = dict((k, literal_eval(v)) for k, v in - (pair.split('=') for pair in - re.split(', | |,', python_can_args))) - sock = CANSocket(bustype=interface, channel=channel, - **arg_dict) - else: - interface_string = "CANSocket(bustype=" \ - "'%s', channel='%s')" % \ - (interface, channel) - sock = CANSocket(bustype=interface, channel=channel) - else: - sock = CANSocket(channel=channel) - interface_string = "\"%s\"" % channel + sock, interface_string = \ + create_socket(python_can_args, interface, channel) if verbose: print("Start scan (%s - %s)" % (hex(start), hex(end))) - signal.signal(signal.SIGINT, signal_handler) + stop_event = threading.Event() + + def signal_handler(*args): + # type: (Any) -> None + print('Interrupting scan!') + stop_event.set() - result = ISOTPScan(sock, - range(start, end + 1), - extended_addressing=extended, - noise_listen_time=noise_listen_time, - sniff_time=float(sniff_time) / 1000, - output_format="code" if piso else "text", - can_interface=interface_string, - extended_can_id=extended_can_id, - verbose=verbose) + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + result = isotp_scan(sock, + range(start, end + 1), + extended_addressing=extended, + noise_listen_time=noise_listen_time, + sniff_time=float(sniff_time) / 1000, + output_format="code" if piso else "text", + can_interface=interface_string, + extended_can_id=extended_can_id, + verbose=verbose, + stop_event=stop_event) print("Scan: \n%s" % result) @@ -194,7 +214,7 @@ def main(): sys.exit(1) finally: - if sock is not None: + if sock is not None and not sock.closed: sock.close() diff --git a/scapy/tools/automotive/obdscanner.py b/scapy/tools/automotive/obdscanner.py index 3ba633d886a..5318fb82bf4 100755 --- a/scapy/tools/automotive/obdscanner.py +++ b/scapy/tools/automotive/obdscanner.py @@ -1,32 +1,32 @@ -#! /usr/bin/env python - +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Andreas Korb # Copyright (C) Friedrich Feigel # Copyright (C) Nils Weiss -# This program is published under a GPLv2 license -from __future__ import print_function import getopt import sys import signal import re +import traceback from ast import literal_eval -import scapy.modules.six as six from scapy.config import conf from scapy.consts import LINUX -if six.PY2 or not LINUX or conf.use_pypy: +if not LINUX or conf.use_pypy: conf.contribs['CANSocket'] = {'use-python-can': True} -from scapy.contrib.isotp import ISOTPSocket # noqa: E402 -from scapy.contrib.cansocket import CANSocket, PYTHON_CAN # noqa: E402 -from scapy.contrib.automotive.obd.obd import OBD # noqa: E402 -from scapy.contrib.automotive.obd.scanner import obd_scan # noqa: E402 +from scapy.contrib.isotp import ISOTPSocket # noqa: E402 +from scapy.contrib.cansocket import CANSocket, PYTHON_CAN # noqa: E402 +from scapy.contrib.automotive.obd.obd import OBD # noqa: E402 +from scapy.contrib.automotive.obd.scanner import OBD_Scanner, \ + OBD_S01_Enumerator, OBD_S02_Enumerator, OBD_S03_Enumerator, \ + OBD_S06_Enumerator, OBD_S07_Enumerator, OBD_S08_Enumerator, \ + OBD_S09_Enumerator, OBD_S0A_Enumerator # noqa: E402 def signal_handler(sig, frame): @@ -38,8 +38,8 @@ def usage(is_error): print('''usage:\tobdscanner [-i|--interface] [-c|--channel] [-b|--bitrate] [-a|--python-can_args] [-h|--help] [-s|--source] [-d|--destination] - [-t|--timeout] [-r|--supported] - [-u|--unsupported] [-v|--verbose]\n + [-t|--timeout] [-f|--full] + [-v|--verbose]\n Scan for all possible obd service classes and their subfunctions.\n optional arguments: -c, --channel python-can channel or Linux SocketCAN interface name\n @@ -56,9 +56,16 @@ def usage(is_error): -s, --source ISOTP-socket source id (hex) -d, --destination ISOTP-socket destination id (hex) -t, --timeout Timeout after which the scanner proceeds to next service [seconds] - -r, --supported Check for supported id services - -u, --unsupported Check for unsupported id services - -v, --verbose Display information during scan\n + -f, --full Full scan on id services + -v, --verbose Display information during scan + -1 Scan OBD Service 01 + -2 Scan OBD Service 02 + -3 Scan OBD Service 03 + -6 Scan OBD Service 06 + -7 Scan OBD Service 07 + -8 Scan OBD Service 08 + -9 Scan OBD Service 09 + -A Scan OBD Service 0A\n Example of use:\n Python2 or Windows: python2 -m scapy.tools.automotive.obdscanner --interface=pcan --channel=PCAN_USBBUS1 --source=0x070 --destination 0x034 @@ -70,6 +77,32 @@ def usage(is_error): file=sys.stderr if is_error else sys.stdout) +def get_can_socket(channel, interface, python_can_args): + if PYTHON_CAN: + if python_can_args: + arg_dict = dict((k, literal_eval(v)) for k, v in + (pair.split('=') for pair in + re.split(', | |,', python_can_args))) + return CANSocket(bustype=interface, channel=channel, **arg_dict) + else: + return CANSocket(bustype=interface, channel=channel) + else: + return CANSocket(channel=channel) + + +def get_isotp_socket(csock, source, destination): + return ISOTPSocket(csock, source, destination, basecls=OBD, padding=True) + + +def run_scan(isock, enumerators, full_scan, verbose, timeout): + s = OBD_Scanner(isock, test_cases=enumerators, full_scan=full_scan, + debug=verbose, + timeout=timeout) + print("Starting OBD-Scan...") + s.scan() + s.show_testcases() + + def main(): channel = None @@ -77,16 +110,17 @@ def main(): source = 0x7e0 destination = 0x7df timeout = 0.1 - supported = False - unsupported = False + full_scan = False verbose = False python_can_args = None + enumerators = [] + conf.verb = -1 options = getopt.getopt( sys.argv[1:], - 'i:c:s:d:a:t:hruv', + 'i:c:s:d:a:t:hfv1236789A', ['interface=', 'channel=', 'source=', 'destination=', - 'help', 'timeout=', 'python-can_args=', 'supported', 'unsupported', + 'help', 'timeout=', 'python-can_args=', 'full', 'verbose']) try: @@ -106,10 +140,24 @@ def main(): sys.exit(0) elif opt in ('-t', '--timeout'): timeout = float(arg) - elif opt in ('-r', '--supported'): - supported = True - elif opt in ('-u', '--unsupported'): - unsupported = True + elif opt in ('-f', '--full'): + full_scan = True + elif opt == '-1': + enumerators += [OBD_S01_Enumerator] + elif opt == '-2': + enumerators += [OBD_S02_Enumerator] + elif opt == '-3': + enumerators += [OBD_S03_Enumerator] + elif opt == '-6': + enumerators += [OBD_S06_Enumerator] + elif opt == '-7': + enumerators += [OBD_S07_Enumerator] + elif opt == '-8': + enumerators += [OBD_S08_Enumerator] + elif opt == '-9': + enumerators += [OBD_S09_Enumerator] + elif opt == '-A': + enumerators += [OBD_S0A_Enumerator] elif opt in ('-v', '--verbose'): verbose = True except getopt.GetoptError as msg: @@ -135,33 +183,27 @@ def main(): sys.exit(1) csock = None + isock = None try: - if PYTHON_CAN: - if python_can_args: - arg_dict = dict((k, literal_eval(v)) for k, v in - (pair.split('=') for pair in - re.split(', | |,', python_can_args))) - csock = CANSocket(bustype=interface, channel=channel, - **arg_dict) - else: - csock = CANSocket(bustype=interface, channel=channel) - else: - csock = CANSocket(channel=channel) + csock = get_can_socket(channel, interface, python_can_args) + isock = get_isotp_socket(csock, source, destination) - with ISOTPSocket(csock, source, destination, - basecls=OBD, padding=True) as isock: - signal.signal(signal.SIGINT, signal_handler) - obd_scan(isock, timeout, supported, unsupported, verbose) + signal.signal(signal.SIGINT, signal_handler) + run_scan(isock, enumerators, full_scan, verbose, timeout) except Exception as e: usage(True) print("\nSocket couldn't be created. Check your arguments.\n", file=sys.stderr) print(e, file=sys.stderr) + if verbose: + traceback.print_exc(file=sys.stderr) sys.exit(1) finally: - if csock is not None: + if isock: + isock.close() + if csock: csock.close() diff --git a/scapy/tools/automotive/xcpscanner.py b/scapy/tools/automotive/xcpscanner.py new file mode 100755 index 00000000000..48774032cd8 --- /dev/null +++ b/scapy/tools/automotive/xcpscanner.py @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Fabian Wiche +# Copyright (C) Tabea Spahn + +import argparse +import signal +import sys + +from scapy.contrib.automotive.xcp.scanner import XCPOnCANScanner +from scapy.contrib.automotive.xcp.xcp import XCPOnCAN +from scapy.contrib.cansocket import CANSocket + + +class ScannerParams: + def __init__(self): + self.id_range = None + self.sniff_time = None + self.verbose = False + self.channel = None + self.broadcast = False + + +def signal_handler(sig, _frame): + sys.stderr.write("Interrupting scan!\n") + # Use same convention as the bash shell + # 128+n where n is the fatal error signal + # https://tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF + sys.exit(128 + sig) + + +def init_socket(scan_params): + print("Initializing socket for " + scan_params.channel) + try: + sock = CANSocket(scan_params.channel) + except Exception as e: + sys.stderr.write("\nSocket could not be created: " + str(e) + "\n") + sys.exit(1) + sock.basecls = XCPOnCAN + return sock + + +def parse_inputs(): + scanner_params = ScannerParams() + + parser = argparse.ArgumentParser() + parser.description = "Finds XCP slaves using the XCP Broadcast-CAN " \ + "identifier." + parser.add_argument('--start', '-s', + help='Start ID CAN (in hex).\n' + 'If actual ID is unknown the scan will ' + 'test broadcast ids between --start and --end ' + '(inclusive). Default: 0x00') + parser.add_argument('--end', '-e', + help='End ID CAN (in hex).\n' + 'If actual ID is unknown the scan will test ' + 'broadcast ids between --start and --end ' + '(inclusive). Default: 0x7ff') + parser.add_argument('--sniff_time', '-t', + help='Duration in milliseconds a sniff is waiting ' + 'for a response.', type=int, default=100) + parser.add_argument('channel', + help='Linux SocketCAN interface name, e.g.: vcan0') + parser.add_argument('--verbose', '-v', action="store_true", + help='Display information during scan') + parser.add_argument('--broadcast', '-b', action="store_true", + help='Use Broadcast-message GetSlaveId instead of ' + 'default "Connect"') + + args = parser.parse_args() + scanner_params.channel = args.channel + scanner_params.verbose = args.verbose + scanner_params.use_broadcast = args.broadcast + scanner_params.sniff_time = float(args.sniff_time) / 1000 + + start_id = int(args.start, 16) if args.start is not None else 0 + end_id = int(args.end, 16) if args.end is not None else 0x7ff + + if start_id > end_id: + parser.error( + "End identifier must not be smaller than the start identifier.") + sys.exit(1) + scanner_params.id_range = range(start_id, end_id + 1) + + return scanner_params + + +def main(): + scanner_params = parse_inputs() + can_socket = init_socket(scanner_params) + + try: + scanner = XCPOnCANScanner(can_socket, + id_range=scanner_params.id_range, + sniff_time=scanner_params.sniff_time, + verbose=scanner_params.verbose) + + signal.signal(signal.SIGINT, signal_handler) + + results = scanner.scan_with_get_slave_id() \ + if scanner_params.broadcast \ + else scanner.scan_with_connect() # Blocking + + if isinstance(results, list) and len(results) > 0: + for r in results: + print(r) + else: + print("Detected no XCP slave.") + except Exception as err: + sys.stderr.write(str(err) + "\n") + sys.exit(1) + finally: + can_socket.close() + + +if __name__ == "__main__": + main() diff --git a/scapy/tools/check_asdis.py b/scapy/tools/check_asdis.py index 468aa81bfac..abcf3e47477 100755 --- a/scapy/tools/check_asdis.py +++ b/scapy/tools/check_asdis.py @@ -1,4 +1,8 @@ -from __future__ import print_function +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Philippe Biondi + import getopt @@ -19,14 +23,14 @@ def main(argv): VERBOSE = 0 try: opts = getopt.getopt(argv, "hi:o:azdv") - for opt, parm in opts[0]: + for opt, param in opts[0]: if opt == "-h": usage() raise SystemExit elif opt == "-i": - PCAP_IN = parm + PCAP_IN = param elif opt == "-o": - PCAP_OUT = parm + PCAP_OUT = param elif opt == "-v": VERBOSE += 1 elif opt == "-d": diff --git a/scapy/tools/check_spdx.sh b/scapy/tools/check_spdx.sh new file mode 100755 index 00000000000..890619c1172 --- /dev/null +++ b/scapy/tools/check_spdx.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# Check that all Scapy files have a SPDX + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$SCRIPT_DIR/../.. + +# http://mywiki.wooledge.org/BashFAQ/024 +# This documents an absolutely WTF behavior of bash. +set +m +shopt -s lastpipe + +function check_path() { + cd $ROOT_DIR + RCODE=0 + for ext in "${@:2}"; do + find $1 -name "*.$ext" | while read f; do + if [[ -z $(grep "SPDX" $f) ]]; then + echo "$f" + RCODE=1 + fi + done + done + return $RCODE +} + +check_path scapy py || exit $? diff --git a/scapy/tools/generate_bluetooth.py b/scapy/tools/generate_bluetooth.py new file mode 100644 index 00000000000..9d534a0197c --- /dev/null +++ b/scapy/tools/generate_bluetooth.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Generate the bluetoothids.py file based on blueooth_sig's public listing +""" + +import yaml +import json +import gzip +import urllib.request + +from base64 import b85encode + +URL = "https://bitbucket.org/bluetooth-SIG/public/raw/main/assigned_numbers/company_identifiers/company_identifiers.yaml" # noqa: E501 + +with urllib.request.urlopen(URL) as stream: + DATA = yaml.safe_load(stream.read()) + +COMPILED = {} + +for company in DATA["company_identifiers"]: + COMPILED[company["value"]] = company["name"] + +# Compress properly +COMPILED = gzip.compress(json.dumps(COMPILED).encode()) +# Encode in Base85 +COMPILED = b85encode(COMPILED).decode() +# Split +COMPILED = "\n".join(COMPILED[i : i + 79] for i in range(0, len(COMPILED), 79)) + "\n" + + +with open("../libs/bluetoothids.py", "r") as inp: + data = inp.read() + +with open("../libs/bluetoothids.py", "w") as out: + ini, sep, _ = data.partition("DATA = _d(\"\"\"") + COMPILED = ini + sep + "\n" + COMPILED + "\"\"\")\n" + print("Written: %s" % out.write(COMPILED)) diff --git a/scapy/tools/generate_ethertypes.py b/scapy/tools/generate_ethertypes.py index 47c38c4f973..92f6d1996df 100644 --- a/scapy/tools/generate_ethertypes.py +++ b/scapy/tools/generate_ethertypes.py @@ -1,27 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter -"""Generate the ethertypes file (/etc/ethertypes) -based on the OpenBSD source. +"""Generate the ethertypes file (/etc/ethertypes) based on the OpenBSD source +https://github.com/openbsd/src/blob/master/sys/net/ethertypes.h It allows to have a file with the format of http://git.netfilter.org/ebtables/plain/ethertypes but up-to-date. """ +import gzip import re import urllib.request +from base64 import b85encode +from scapy.error import log_loading + URL = "https://raw.githubusercontent.com/openbsd/src/master/sys/net/ethertypes.h" # noqa: E501 with urllib.request.urlopen(URL) as stream: DATA = stream.read() -reg = br".*ETHERTYPE_([^\s]+)\s.0x([0-9A-Fa-f]+).*\/\*(.*)\*\/" -COMPILED = b"""# +reg = r".*ETHERTYPE_([^\s]+)\s.0x([0-9A-Fa-f]+).*\/\*(.*)\*\/" +COMPILED = """# # Ethernet frame types # This file describes some of the various Ethernet # protocol types that are used on Ethernet networks. @@ -33,16 +36,33 @@ # ... #Comment # """ +ALIASES = {"IP": "IPv4", "IPV6": "IPv6"} + for line in DATA.split(b"\n"): - match = re.match(reg, line) - if match: - name = match.group(1).ljust(16) - number = match.group(2).upper() - comment = match.group(3).strip() - compiled_line = (b"%b%b" + b" " * 25 + b"# %b\n") % ( - name, number, comment + try: + match = re.match(reg, line.decode("utf8", errors="backslashreplace")) + if match: + name = match.group(1) + name = ALIASES.get(name, name).ljust(16) + number = match.group(2).upper() + comment = match.group(3).strip() + COMPILED += ("%s%s" + " " * 25 + "# %s\n") % (name, number, comment) + except Exception: + log_loading.warning( + "Couldn't parse one line from [%s] [%r]", URL, line, exc_info=True ) - COMPILED += compiled_line -with open("ethertypes", "wb") as output: - print("Written: %s" % output.write(COMPILED)) +# Compress properly +COMPILED = gzip.compress(COMPILED.encode()) +# Encode in Base85 +COMPILED = b85encode(COMPILED).decode() +# Split +COMPILED = "\n".join(COMPILED[i : i + 79] for i in range(0, len(COMPILED), 79)) + "\n" + +with open("../libs/ethertypes.py", "r") as inp: + data = inp.read() + +with open("../libs/ethertypes.py", "w") as out: + ini, sep, _ = data.partition("DATA = _d(\"\"\"") + COMPILED = ini + sep + "\n" + COMPILED + "\"\"\")\n" + print("Written: %s" % out.write(COMPILED)) diff --git a/scapy/tools/generate_manuf.py b/scapy/tools/generate_manuf.py new file mode 100644 index 00000000000..6d16e1d339a --- /dev/null +++ b/scapy/tools/generate_manuf.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Generate the manuf.py file based on wireshark's manuf +""" + +import gzip +import urllib.request + +from base64 import b85encode + +URL = "https://www.wireshark.org/download/automated/data/manuf" + +with urllib.request.urlopen(URL) as stream: + DATA = stream.read() + +COMPILED = "" + +for line in DATA.split(b"\n"): + # We decode to strip any non-UTF8 characters. + line = line.strip().decode("utf8", errors="backslashreplace") + if not line or line.startswith("#"): + continue + COMPILED += line + "\n" + +# Compress properly +COMPILED = gzip.compress(COMPILED.encode()) +# Encode in Base85 +COMPILED = b85encode(COMPILED).decode() +# Split +COMPILED = "\n".join(COMPILED[i : i + 79] for i in range(0, len(COMPILED), 79)) + "\n" + + +with open("../libs/manuf.py", "r") as inp: + data = inp.read() + +with open("../libs/manuf.py", "w") as out: + ini, sep, _ = data.partition("DATA = _d(\"\"\"") + COMPILED = ini + sep + "\n" + COMPILED + "\"\"\")\n" + print("Written: %s" % out.write(COMPILED)) diff --git a/scapy/tools/scapy_pyannotate.py b/scapy/tools/scapy_pyannotate.py index f7d12825a29..2ef16cf76fe 100644 --- a/scapy/tools/scapy_pyannotate.py +++ b/scapy/tools/scapy_pyannotate.py @@ -1,7 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information -# Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license +# See https://scapy.net/ for more information """ Wrap Scapy's shell in pyannotate. diff --git a/scapy/utils.py b/scapy/utils.py index caf8b2dbe30..2ad2fc12426 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -1,54 +1,113 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license """ General utility functions. """ -from __future__ import absolute_import -from __future__ import print_function + from decimal import Decimal +from io import StringIO +from itertools import zip_longest +from uuid import UUID -import os -import sys -import socket +import argparse +import array import collections -import random -import time +import decimal +import difflib +import enum import gzip +import inspect +import locale +import math +import os +import random import re +import shutil +import socket import struct -import array import subprocess +import sys import tempfile import threading - -import scapy.modules.six as six -from scapy.modules.six.moves import range, input +import time +import traceback +import warnings from scapy.config import conf -from scapy.consts import DARWIN, WINDOWS, WINDOWS_XP, OPENBSD -from scapy.data import MTU, DLT_EN10MB -from scapy.compat import orb, raw, plain_str, chb, bytes_base64,\ - base64_bytes, hex_bytes, lambda_tuple_converter, bytes_encode -from scapy.error import log_runtime, Scapy_Exception, warning +from scapy.consts import DARWIN, OPENBSD, WINDOWS +from scapy.data import MTU, DLT_EN10MB, DLT_RAW +from scapy.compat import ( + orb, + plain_str, + chb, + hex_bytes, + bytes_encode, +) +from scapy.error import ( + log_interactive, + log_runtime, + Scapy_Exception, + warning, +) from scapy.pton_ntop import inet_pton +# Typing imports +from typing import ( + Any, + AnyStr, + Callable, + cast, + Dict, + IO, + Iterator, + List, + Optional, + overload, + Tuple, + TYPE_CHECKING, + Type, + Union, +) +from scapy.compat import ( + DecoratorCallable, + Literal, +) + +if TYPE_CHECKING: + from scapy.packet import Packet + from scapy.plist import _PacketIterable, PacketList + from scapy.supersocket import SuperSocket + import prompt_toolkit + +_ByteStream = Union[IO[bytes], gzip.GzipFile] + ########### # Tools # ########### -def issubtype(x, t): +def issubtype(x, # type: Any + t, # type: Union[type, str] + ): + # type: (...) -> bool """issubtype(C, B) -> bool Return whether C is a class and if it is a subclass of class B. When using a tuple as the second argument issubtype(X, (A, B, ...)), is a shortcut for issubtype(X, A) or issubtype(X, B) or ... (etc.). """ - return isinstance(x, type) and issubclass(x, t) + if isinstance(t, str): + return t in (z.__name__ for z in x.__bases__) + if isinstance(x, type) and issubclass(x, t): + return True + return False + + +_Decimal = Union[Decimal, int] class EDecimal(Decimal): @@ -58,59 +117,83 @@ class EDecimal(Decimal): backward compatibility """ - def __add__(self, other, **kwargs): - return EDecimal(Decimal.__add__(self, Decimal(other), **kwargs)) + def __add__(self, other, context=None): + # type: (_Decimal, Any) -> EDecimal + return EDecimal(Decimal.__add__(self, Decimal(other))) - def __radd__(self, other, **kwargs): - return EDecimal(Decimal.__add__(self, Decimal(other), **kwargs)) + def __radd__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__add__(self, Decimal(other))) - def __sub__(self, other, **kwargs): - return EDecimal(Decimal.__sub__(self, Decimal(other), **kwargs)) + def __sub__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__sub__(self, Decimal(other))) - def __rsub__(self, other, **kwargs): - return EDecimal(Decimal.__rsub__(self, Decimal(other), **kwargs)) + def __rsub__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__rsub__(self, Decimal(other))) - def __mul__(self, other, **kwargs): - return EDecimal(Decimal.__mul__(self, Decimal(other), **kwargs)) + def __mul__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__mul__(self, Decimal(other))) - def __rmul__(self, other, **kwargs): - return EDecimal(Decimal.__mul__(self, Decimal(other), **kwargs)) + def __rmul__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__mul__(self, Decimal(other))) - def __truediv__(self, other, **kwargs): - return EDecimal(Decimal.__truediv__(self, Decimal(other), **kwargs)) + def __truediv__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__truediv__(self, Decimal(other))) - def __floordiv__(self, other, **kwargs): - return EDecimal(Decimal.__floordiv__(self, Decimal(other), **kwargs)) + def __floordiv__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__floordiv__(self, Decimal(other))) - def __div__(self, other, **kwargs): - return EDecimal(Decimal.__div__(self, Decimal(other), **kwargs)) + def __divmod__(self, other): + # type: (_Decimal) -> Tuple[EDecimal, EDecimal] + r = Decimal.__divmod__(self, Decimal(other)) + return EDecimal(r[0]), EDecimal(r[1]) - def __rdiv__(self, other, **kwargs): - return EDecimal(Decimal.__rdiv__(self, Decimal(other), **kwargs)) + def __mod__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__mod__(self, Decimal(other))) - def __mod__(self, other, **kwargs): - return EDecimal(Decimal.__mod__(self, Decimal(other), **kwargs)) + def __rmod__(self, other): + # type: (_Decimal) -> EDecimal + return EDecimal(Decimal.__rmod__(self, Decimal(other))) - def __rmod__(self, other, **kwargs): - return EDecimal(Decimal.__rmod__(self, Decimal(other), **kwargs)) + def __pow__(self, other, modulo=None): + # type: (_Decimal, Optional[_Decimal]) -> EDecimal + return EDecimal(Decimal.__pow__(self, Decimal(other), modulo)) + + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, Decimal): + return super(EDecimal, self).__eq__(other) + else: + return bool(float(self) == other) - def __divmod__(self, other, **kwargs): - return EDecimal(Decimal.__divmod__(self, Decimal(other), **kwargs)) + def normalize(self, precision): # type: ignore + # type: (int) -> EDecimal + with decimal.localcontext() as ctx: + ctx.prec = precision + return EDecimal(super(EDecimal, self).normalize(ctx)) - def __rdivmod__(self, other, **kwargs): - return EDecimal(Decimal.__rdivmod__(self, Decimal(other), **kwargs)) - def __pow__(self, other, **kwargs): - return EDecimal(Decimal.__pow__(self, Decimal(other), **kwargs)) +@overload +def get_temp_file(keep, autoext, fd): + # type: (bool, str, Literal[True]) -> IO[bytes] + pass - def __rpow__(self, other, **kwargs): - return EDecimal(Decimal.__rpow__(self, Decimal(other), **kwargs)) - def __eq__(self, other, **kwargs): - return super(EDecimal, self).__eq__(other) or float(self) == other +@overload +def get_temp_file(keep=False, autoext="", fd=False): + # type: (bool, str, Literal[False]) -> str + pass def get_temp_file(keep=False, autoext="", fd=False): + # type: (bool, str, bool) -> Union[IO[bytes], str] """Creates a temporary file. :param keep: If False, automatically delete the file when Scapy exits. @@ -132,6 +215,7 @@ def get_temp_file(keep=False, autoext="", fd=False): def get_temp_dir(keep=False): + # type: (bool) -> str """Creates a temporary file, and returns its name. :param keep: If False (default), the directory will be recursively @@ -147,23 +231,44 @@ def get_temp_dir(keep=False): return dname -def sane_color(x): - r = "" - for i in x: - j = orb(i) - if (j < 32) or (j >= 127): - r += conf.color_theme.not_printable(".") - else: - r += chr(j) - return r +def _create_fifo() -> Tuple[str, Any]: + """Creates a temporary fifo. + + You must then use open_fifo() on the server_fd once + the client is connected to use it. + + :returns: (client_file, server_fd) + """ + if WINDOWS: + from scapy.arch.windows.structures import _get_win_fifo + return _get_win_fifo() + else: + f = get_temp_file() + os.unlink(f) + os.mkfifo(f) + return f, f + + +def _open_fifo(fd: Any, mode: str = "rb") -> IO[bytes]: + """Open the server_fd (see create_fifo) + """ + if WINDOWS: + from scapy.arch.windows.structures import _win_fifo_open + return _win_fifo_open(fd) + else: + return open(fd, mode) -def sane(x): +def sane(x, color=False): + # type: (AnyStr, bool) -> str r = "" for i in x: j = orb(i) if (j < 32) or (j >= 127): - r += "." + if color: + r += conf.color_theme.not_printable(".") + else: + r += "." else: r += chr(j) return r @@ -171,43 +276,44 @@ def sane(x): @conf.commands.register def restart(): + # type: () -> None """Restarts scapy""" if not conf.interactive or not os.path.isfile(sys.argv[0]): raise OSError("Scapy was not started from console") if WINDOWS: + res_code = 1 try: res_code = subprocess.call([sys.executable] + sys.argv) - except KeyboardInterrupt: - res_code = 1 finally: os._exit(res_code) os.execv(sys.executable, [sys.executable] + sys.argv) def lhex(x): + # type: (Any) -> str from scapy.volatile import VolatileValue if isinstance(x, VolatileValue): return repr(x) - if type(x) in six.integer_types: + if isinstance(x, int): return hex(x) - elif isinstance(x, tuple): - return "(%s)" % ", ".join(map(lhex, x)) - elif isinstance(x, list): - return "[%s]" % ", ".join(map(lhex, x)) - else: - return x + if isinstance(x, tuple): + return "(%s)" % ", ".join(lhex(v) for v in x) + if isinstance(x, list): + return "[%s]" % ", ".join(lhex(v) for v in x) + return str(x) @conf.commands.register -def hexdump(x, dump=False): +def hexdump(p, dump=False): + # type: (Union[Packet, AnyStr], bool) -> Optional[str] """Build a tcpdump like hexadecimal view - :param x: a Packet + :param p: a Packet :param dump: define if the result must be printed or returned in a variable :return: a String only when dump=True """ s = "" - x = bytes_encode(x) + x = bytes_encode(p) x_len = len(x) i = 0 while i < x_len: @@ -217,7 +323,7 @@ def hexdump(x, dump=False): s += "%02X " % orb(x[i + j]) else: s += " " - s += " %s\n" % sane_color(x[i:i + 16]) + s += " %s\n" % sane(x[i:i + 16], color=True) i += 16 # remove trailing \n s = s[:-1] if s.endswith("\n") else s @@ -225,99 +331,187 @@ def hexdump(x, dump=False): return s else: print(s) + return None @conf.commands.register -def linehexdump(x, onlyasc=0, onlyhex=0, dump=False): +def linehexdump(p, onlyasc=0, onlyhex=0, dump=False): + # type: (Union[Packet, AnyStr], int, int, bool) -> Optional[str] """Build an equivalent view of hexdump() on a single line Note that setting both onlyasc and onlyhex to 1 results in a empty output - :param x: a Packet + :param p: a Packet :param onlyasc: 1 to display only the ascii view :param onlyhex: 1 to display only the hexadecimal view :param dump: print the view if False :return: a String only when dump=True """ s = "" - s = hexstr(x, onlyasc=onlyasc, onlyhex=onlyhex, color=not dump) + s = hexstr(p, onlyasc=onlyasc, onlyhex=onlyhex, color=not dump) if dump: return s else: print(s) + return None @conf.commands.register -def chexdump(x, dump=False): +def chexdump(p, dump=False): + # type: (Union[Packet, AnyStr], bool) -> Optional[str] """Build a per byte hexadecimal representation Example: >>> chexdump(IP()) 0x45, 0x00, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x40, 0x00, 0x7c, 0xe7, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01 # noqa: E501 - :param x: a Packet + :param p: a Packet :param dump: print the view if False :return: a String only if dump=True """ - x = bytes_encode(x) + x = bytes_encode(p) s = ", ".join("%#04x" % orb(x) for x in x) if dump: return s else: print(s) + return None @conf.commands.register -def hexstr(x, onlyasc=0, onlyhex=0, color=False): +def hexstr(p, onlyasc=0, onlyhex=0, color=False): + # type: (Union[Packet, AnyStr], int, int, bool) -> str """Build a fancy tcpdump like hex from bytes.""" - x = bytes_encode(x) - _sane_func = sane_color if color else sane + x = bytes_encode(p) s = [] if not onlyasc: s.append(" ".join("%02X" % orb(b) for b in x)) if not onlyhex: - s.append(_sane_func(x)) + s.append(sane(x, color=color)) return " ".join(s) def repr_hex(s): + # type: (bytes) -> str """ Convert provided bitstring to a simple string of hex digits """ return "".join("%02x" % orb(x) for x in s) @conf.commands.register -def hexdiff(x, y): - """Show differences between 2 binary strings""" - x = bytes_encode(x)[::-1] - y = bytes_encode(y)[::-1] - SUBST = 1 - INSERT = 1 - d = {(-1, -1): (0, (-1, -1))} - for j in range(len(y)): - d[-1, j] = d[-1, j - 1][0] + INSERT, (-1, j - 1) - for i in range(len(x)): - d[i, -1] = d[i - 1, -1][0] + INSERT, (i - 1, -1) - - for j in range(len(y)): - for i in range(len(x)): - d[i, j] = min((d[i - 1, j - 1][0] + SUBST * (x[i] != y[j]), (i - 1, j - 1)), # noqa: E501 - (d[i - 1, j][0] + INSERT, (i - 1, j)), - (d[i, j - 1][0] + INSERT, (i, j - 1))) +def hexdiff( + a: Union['Packet', AnyStr], + b: Union['Packet', AnyStr], + algo: Optional[str] = None, + autojunk: bool = False, +) -> None: + """ + Show differences between 2 binary strings, Packets... + + Available algorithms: + - wagnerfischer: Use the Wagner and Fischer algorithm to compute the + Levenstein distance between the strings then backtrack. + - difflib: Use the difflib.SequenceMatcher implementation. This based on a + modified version of the Ratcliff and Obershelp algorithm. + This is much faster, but far less accurate. + https://docs.python.org/3.8/library/difflib.html#difflib.SequenceMatcher + + :param a: + :param b: The binary strings, packets... to compare + :param algo: Force the algo to be 'wagnerfischer' or 'difflib'. + By default, this is chosen depending on the complexity, optimistically + preferring wagnerfischer unless really necessary. + :param autojunk: (difflib only) See difflib documentation. + """ + xb = bytes_encode(a) + yb = bytes_encode(b) + + if algo is None: + # Choose the best algorithm + complexity = len(xb) * len(yb) + if complexity < 1e7: + # Comparing two (non-jumbos) Ethernet packets is ~2e6 which is manageable. + # Anything much larger than this shouldn't be attempted by default. + algo = "wagnerfischer" + if complexity > 1e6: + log_interactive.info( + "Complexity is a bit high. hexdiff will take a few seconds." + ) + else: + algo = "difflib" backtrackx = [] backtracky = [] - i = len(x) - 1 - j = len(y) - 1 - while not (i == j == -1): - i2, j2 = d[i, j][1] - backtrackx.append(x[i2 + 1:i + 1]) - backtracky.append(y[j2 + 1:j + 1]) - i, j = i2, j2 + + if algo == "wagnerfischer": + xb = xb[::-1] + yb = yb[::-1] + + # costs for the 3 operations + INSERT = 1 + DELETE = 1 + SUBST = 1 + + # Typically, d[i,j] will hold the distance between + # the first i characters of xb and the first j characters of yb. + # We change the Wagner Fischer to also store pointers to all + # the intermediate steps taken while calculating the Levenstein distance. + d = {(-1, -1): (0, (-1, -1))} + for j in range(len(yb)): + d[-1, j] = (j + 1) * INSERT, (-1, j - 1) + for i in range(len(xb)): + d[i, -1] = (i + 1) * INSERT + 1, (i - 1, -1) + + # Compute the Levenstein distance between the two strings, but + # store all the steps to be able to backtrack at the end. + for j in range(len(yb)): + for i in range(len(xb)): + d[i, j] = min( + (d[i - 1, j - 1][0] + SUBST * (xb[i] != yb[j]), (i - 1, j - 1)), + (d[i - 1, j][0] + DELETE, (i - 1, j)), + (d[i, j - 1][0] + INSERT, (i, j - 1)), + ) + + # Iterate through the steps backwards to create the diff + i = len(xb) - 1 + j = len(yb) - 1 + while not (i == j == -1): + i2, j2 = d[i, j][1] + backtrackx.append(xb[i2 + 1:i + 1]) + backtracky.append(yb[j2 + 1:j + 1]) + i, j = i2, j2 + elif algo == "difflib": + sm = difflib.SequenceMatcher(a=xb, b=yb, autojunk=autojunk) + xarr = [xb[i:i + 1] for i in range(len(xb))] + yarr = [yb[i:i + 1] for i in range(len(yb))] + # Iterate through opcodes to build the backtrack + for opcode in sm.get_opcodes(): + typ, x0, x1, y0, y1 = opcode + if typ == 'delete': + backtrackx += xarr[x0:x1] + backtracky += [b''] * (x1 - x0) + elif typ == 'insert': + backtrackx += [b''] * (y1 - y0) + backtracky += yarr[y0:y1] + elif typ in ['equal', 'replace']: + backtrackx += xarr[x0:x1] + backtracky += yarr[y0:y1] + # Some lines may have been considered as junk. Check the sizes + if autojunk: + lbx = len(backtrackx) + lby = len(backtracky) + backtrackx += [b''] * (max(lbx, lby) - lbx) + backtracky += [b''] * (max(lbx, lby) - lby) + else: + raise ValueError("Unknown algorithm '%s'" % algo) + + # Print the diff x = y = i = 0 - colorize = {0: lambda x: x, - -1: conf.color_theme.left, - 1: conf.color_theme.right} + colorize: Dict[int, Callable[[str], str]] = { + 0: lambda x: x, + -1: conf.color_theme.left, + 1: conf.color_theme.right + } dox = 1 doy = 0 @@ -360,12 +554,12 @@ def hexdiff(x, y): cl = "" for j in range(16): - if i + j < btx_len: + if i + j < min(len(backtrackx), len(backtracky)): if line[j]: col = colorize[(linex[j] != liney[j]) * (doy - dox)] print(col("%02X" % orb(line[j])), end=' ') if linex[j] == liney[j]: - cl += sane_color(line[j]) + cl += sane(line[j], color=True) else: cl += col(sane(line[j])) else: @@ -391,12 +585,13 @@ def hexdiff(x, y): if struct.pack("H", 1) == b"\x00\x01": # big endian - checksum_endian_transform = lambda chk: chk + checksum_endian_transform = lambda chk: chk # type: Callable[[int], int] else: checksum_endian_transform = lambda chk: ((chk >> 8) & 0xff) | chk << 8 def checksum(pkt): + # type: (bytes) -> int if len(pkt) % 2 == 1: pkt += b"\0" s = sum(array.array("H", pkt)) @@ -407,10 +602,11 @@ def checksum(pkt): def _fletcher16(charbuf): + # type: (bytes) -> Tuple[int, int] # This is based on the GPLed C implementation in Zebra # noqa: E501 c0 = c1 = 0 for char in charbuf: - c0 += orb(char) + c0 += char c1 += c0 c0 %= 255 @@ -420,6 +616,7 @@ def _fletcher16(charbuf): @conf.commands.register def fletcher16_checksum(binbuf): + # type: (bytes) -> int """Calculates Fletcher-16 checksum of the given buffer. Note: @@ -432,6 +629,7 @@ def fletcher16_checksum(binbuf): @conf.commands.register def fletcher16_checkbytes(binbuf, offset): + # type: (bytes, int) -> bytes """Calculates the Fletcher-16 checkbytes returned as 2 byte binary-string. Including the bytes into the buffer (at the position marked by offset) the # noqa: E501 @@ -461,10 +659,12 @@ def fletcher16_checkbytes(binbuf, offset): def mac2str(mac): + # type: (str) -> bytes return b"".join(chb(int(x, 16)) for x in plain_str(mac).split(':')) def valid_mac(mac): + # type: (str) -> bool try: return len(mac2str(mac)) == 6 except ValueError: @@ -473,65 +673,96 @@ def valid_mac(mac): def str2mac(s): + # type: (bytes) -> str if isinstance(s, str): - return ("%02x:" * 6)[:-1] % tuple(map(ord, s)) - return ("%02x:" * 6)[:-1] % tuple(s) + return ("%02x:" * len(s))[:-1] % tuple(map(ord, s)) + return ("%02x:" * len(s))[:-1] % tuple(s) + + +def randstring(length): + # type: (int) -> bytes + """ + Returns a random string of length (length >= 0) + """ + return b"".join(struct.pack('B', random.randint(0, 255)) + for _ in range(length)) -def randstring(l): +def zerofree_randstring(length): + # type: (int) -> bytes """ - Returns a random string of length l (l >= 0) + Returns a random string of length (length >= 0) without zero in it. """ - return b"".join(struct.pack('B', random.randint(0, 255)) for _ in range(l)) + return b"".join(struct.pack('B', random.randint(1, 255)) + for _ in range(length)) -def zerofree_randstring(l): +def stror(s1, s2): + # type: (bytes, bytes) -> bytes """ - Returns a random string of length l (l >= 0) without zero in it. + Returns the binary OR of the 2 provided strings s1 and s2. s1 and s2 + must be of same length. """ - return b"".join(struct.pack('B', random.randint(1, 255)) for _ in range(l)) + return b"".join(map(lambda x, y: struct.pack("!B", x | y), s1, s2)) def strxor(s1, s2): + # type: (bytes, bytes) -> bytes """ Returns the binary XOR of the 2 provided strings s1 and s2. s1 and s2 must be of same length. """ - return b"".join(map(lambda x, y: chb(orb(x) ^ orb(y)), s1, s2)) + return b"".join(map(lambda x, y: struct.pack("!B", x ^ y), s1, s2)) def strand(s1, s2): + # type: (bytes, bytes) -> bytes """ Returns the binary AND of the 2 provided strings s1 and s2. s1 and s2 must be of same length. """ - return b"".join(map(lambda x, y: chb(orb(x) & orb(y)), s1, s2)) + return b"".join(map(lambda x, y: struct.pack("!B", x & y), s1, s2)) + + +def strrot(s1, count, right=True): + # type: (bytes, int, bool) -> bytes + """ + Rotate the binary by 'count' bytes + """ + off = count % len(s1) + if right: + return s1[-off:] + s1[:-off] + else: + return s1[off:] + s1[:off] # Workaround bug 643005 : https://sourceforge.net/tracker/?func=detail&atid=105470&aid=643005&group_id=5470 # noqa: E501 try: socket.inet_aton("255.255.255.255") except socket.error: - def inet_aton(x): - if x == "255.255.255.255": + def inet_aton(ip_string): + # type: (str) -> bytes + if ip_string == "255.255.255.255": return b"\xff" * 4 else: - return socket.inet_aton(x) + return socket.inet_aton(ip_string) else: - inet_aton = socket.inet_aton + inet_aton = socket.inet_aton # type: ignore inet_ntoa = socket.inet_ntoa def atol(x): + # type: (str) -> int try: ip = inet_aton(x) except socket.error: - ip = inet_aton(socket.gethostbyname(x)) - return struct.unpack("!I", ip)[0] + raise ValueError("Bad IP format: %s" % x) + return cast(int, struct.unpack("!I", ip)[0]) def valid_ip(addr): + # type: (str) -> bool try: addr = plain_str(addr) except UnicodeDecodeError: @@ -544,6 +775,7 @@ def valid_ip(addr): def valid_net(addr): + # type: (str) -> bool try: addr = plain_str(addr) except UnicodeDecodeError: @@ -555,6 +787,7 @@ def valid_net(addr): def valid_ip6(addr): + # type: (str) -> bool try: addr = plain_str(addr) except UnicodeDecodeError: @@ -562,14 +795,12 @@ def valid_ip6(addr): try: inet_pton(socket.AF_INET6, addr) except socket.error: - try: - socket.getaddrinfo(addr, None, socket.AF_INET6)[0][4][0] - except socket.error: - return False + return False return True def valid_net6(addr): + # type: (str) -> bool try: addr = plain_str(addr) except UnicodeDecodeError: @@ -580,19 +811,112 @@ def valid_net6(addr): return valid_ip6(addr) -if WINDOWS_XP: - # That is a hell of compatibility :( - def ltoa(x): - return inet_ntoa(struct.pack(" str + return inet_ntoa(struct.pack("!I", x & 0xffffffff)) def itom(x): + # type: (int) -> int return (0xffffffff00000000 >> x) & 0xffffffff +def in4_cidr2mask(m): + # type: (int) -> bytes + """ + Return the mask (bitstring) associated with provided length + value. For instance if function is called on 20, return value is + b'\xff\xff\xf0\x00'. + """ + if m > 32 or m < 0: + raise Scapy_Exception("value provided to in4_cidr2mask outside [0, 32] domain (%d)" % m) # noqa: E501 + + return strxor( + b"\xff" * 4, + struct.pack(">I", 2**(32 - m) - 1) + ) + + +def in4_isincluded(addr, prefix, mask): + # type: (str, str, int) -> bool + """ + Returns True when 'addr' belongs to prefix/mask. False otherwise. + """ + temp = inet_pton(socket.AF_INET, addr) + pref = in4_cidr2mask(mask) + zero = inet_pton(socket.AF_INET, prefix) + return zero == strand(temp, pref) + + +def in4_ismaddr(str): + # type: (str) -> bool + """ + Returns True if provided address in printable format belongs to + allocated Multicast address space (224.0.0.0/4). + """ + return in4_isincluded(str, "224.0.0.0", 4) + + +def in4_ismlladdr(str): + # type: (str) -> bool + """ + Returns True if address belongs to link-local multicast address + space (224.0.0.0/24) + """ + return in4_isincluded(str, "224.0.0.0", 24) + + +def in4_ismgladdr(str): + # type: (str) -> bool + """ + Returns True if address belongs to global multicast address + space (224.0.1.0-238.255.255.255). + """ + return ( + in4_isincluded(str, "224.0.0.0", 4) and + not in4_isincluded(str, "224.0.0.0", 24) and + not in4_isincluded(str, "239.0.0.0", 8) + ) + + +def in4_ismlsaddr(str): + # type: (str) -> bool + """ + Returns True if address belongs to limited scope multicast address + space (239.0.0.0/8). + """ + return in4_isincluded(str, "239.0.0.0", 8) + + +def in4_isaddrllallnodes(str): + # type: (str) -> bool + """ + Returns True if address is the link-local all-nodes multicast + address (224.0.0.1). + """ + return (inet_pton(socket.AF_INET, "224.0.0.1") == + inet_pton(socket.AF_INET, str)) + + +def in4_getnsmac(a): + # type: (bytes) -> str + """ + Return the multicast mac address associated with provided + IPv4 address. Passed address must be in network format. + """ + + return "01:00:5e:%.2x:%.2x:%.2x" % (a[1] & 0x7f, a[2], a[3]) + + +def decode_locale_str(x): + # type: (bytes) -> str + """ + Decode bytes into a string using the system locale. + Useful on Windows where it can be unusual (e.g. cp1252) + """ + return x.decode(encoding=locale.getlocale()[1] or "utf-8", errors="replace") + + class ContextManagerSubprocess(object): """ Context manager that eases checking for unknown command, without @@ -606,15 +930,22 @@ class ContextManagerSubprocess(object): """ def __init__(self, prog, suppress=True): + # type: (str, bool) -> None self.prog = prog self.suppress = suppress def __enter__(self): + # type: () -> None pass - def __exit__(self, exc_type, exc_value, traceback): - if exc_value is None: - return + def __exit__(self, + exc_type, # type: Optional[type] + exc_value, # type: Optional[Exception] + traceback, # type: Optional[Any] + ): + # type: (...) -> Optional[bool] + if exc_value is None or exc_type is None: + return None # Errored if isinstance(exc_value, EnvironmentError): msg = "Could not execute %s, is it installed?" % self.prog @@ -640,16 +971,15 @@ class ContextManagerCaptureOutput(object): """ def __init__(self): + # type: () -> None self.result_export_object = "" - try: - import mock # noqa: F401 - except Exception: - raise ImportError("The mock module needs to be installed !") def __enter__(self): - import mock + # type: () -> ContextManagerCaptureOutput + from unittest import mock def write(s, decorator=self): + # type: (str, ContextManagerCaptureOutput) -> None decorator.result_export_object += s mock_stdout = mock.Mock() mock_stdout.write = write @@ -658,17 +988,27 @@ def write(s, decorator=self): return self def __exit__(self, *exc): + # type: (*Any) -> Literal[False] sys.stdout = self.bck_stdout return False def get_output(self, eval_bytes=False): + # type: (bool) -> str if self.result_export_object.startswith("b'") and eval_bytes: return plain_str(eval(self.result_export_object)) return self.result_export_object -def do_graph(graph, prog=None, format=None, target=None, type=None, - string=None, options=None): +def do_graph( + graph, # type: str + prog=None, # type: Optional[str] + format=None, # type: Optional[str] + target=None, # type: Optional[Union[IO[bytes], str]] + type=None, # type: Optional[str] + string=None, # type: Optional[bool] + options=None # type: Optional[List[str]] +): + # type: (...) -> Optional[str] """Processes graph description using an external software. This method is used to convert a graphviz format to an image. @@ -683,14 +1023,14 @@ def do_graph(graph, prog=None, format=None, target=None, type=None, """ if format is None: - if WINDOWS: - format = "png" # use common format to make sure a viewer is installed # noqa: E501 - else: - format = "svg" + format = "svg" if string: return graph if type is not None: - warning("type is deprecated, and was renamed format") + warnings.warn( + "type is deprecated, and was renamed format", + DeprecationWarning + ) format = type if prog is None: prog = conf.prog.dot @@ -713,11 +1053,18 @@ def do_graph(graph, prog=None, format=None, target=None, type=None, target = open(target[1:].lstrip(), "wb") else: target = open(os.path.abspath(target), "wb") - proc = subprocess.Popen("\"%s\" %s %s" % (prog, options or "", format or ""), # noqa: E501 - shell=True, stdin=subprocess.PIPE, stdout=target) - proc.stdin.write(bytes_encode(graph)) - proc.stdin.close() - proc.wait() + target = cast(IO[bytes], target) + proc = subprocess.Popen( + "\"%s\" %s %s" % (prog, options or "", format or ""), + shell=True, stdin=subprocess.PIPE, stdout=target, + stderr=subprocess.PIPE + ) + _, stderr = proc.communicate(bytes_encode(graph)) + if proc.returncode != 0: + raise OSError( + "GraphViz call failed (is it installed?):\n" + + plain_str(stderr) + ) try: target.close() except Exception: @@ -731,11 +1078,12 @@ def do_graph(graph, prog=None, format=None, target=None, type=None, warning("Temporary file '%s' could not be written. Graphic will not be displayed.", tempfile) # noqa: E501 break else: - if conf.prog.display == conf.prog._default: + if WINDOWS and conf.prog.display == conf.prog._default: os.startfile(target.name) else: with ContextManagerSubprocess(conf.prog.display): subprocess.Popen([conf.prog.display, target.name]) + return None _TEX_TR = { @@ -756,13 +1104,17 @@ def do_graph(graph, prog=None, format=None, target=None, type=None, def tex_escape(x): + # type: (str) -> str s = "" for c in x: s += _TEX_TR.get(c, c) return s -def colgen(*lstcol, **kargs): +def colgen(*lstcol, # type: Any + **kargs # type: Any + ): + # type: (...) -> Iterator[Any] """Returns a generator that mixes provided quantities forever trans: a function to convert the three arguments into a color. lambda x,y,z:(x,y,z) by default""" # noqa: E501 if len(lstcol) < 2: @@ -777,16 +1129,19 @@ def colgen(*lstcol, **kargs): def incremental_label(label="tag%05i", start=0): + # type: (str, int) -> Iterator[str] while True: yield label % start start += 1 def binrepr(val): + # type: (int) -> str return bin(val)[2:] def long_converter(s): + # type: (str) -> int return int(s.replace('\n', '').replace(' ', ''), 16) ######################### @@ -795,34 +1150,41 @@ def long_converter(s): class EnumElement: - _value = None - def __init__(self, key, value): + # type: (str, int) -> None self._key = key self._value = value def __repr__(self): + # type: () -> str return "<%s %s[%r]>" % (self.__dict__.get("_name", self.__class__.__name__), self._key, self._value) # noqa: E501 def __getattr__(self, attr): + # type: (str) -> Any return getattr(self._value, attr) def __str__(self): + # type: () -> str return self._key def __bytes__(self): + # type: () -> bytes return bytes_encode(self.__str__()) def __hash__(self): + # type: () -> int return self._value def __int__(self): + # type: () -> int return int(self._value) def __eq__(self, other): + # type: (Any) -> bool return self._value == int(other) def __neq__(self, other): + # type: (Any) -> bool return not self.__eq__(other) @@ -830,8 +1192,9 @@ class Enum_metaclass(type): element_class = EnumElement def __new__(cls, name, bases, dct): + # type: (Any, str, Any, Dict[str, Any]) -> Any rdict = {} - for k, v in six.iteritems(dct): + for k, v in dct.items(): if isinstance(v, int): v = cls.element_class(k, v) dct[k] = v @@ -840,68 +1203,56 @@ def __new__(cls, name, bases, dct): return super(Enum_metaclass, cls).__new__(cls, name, bases, dct) def __getitem__(self, attr): - return self.__rdict__[attr] + # type: (int) -> Any + return self.__rdict__[attr] # type: ignore def __contains__(self, val): - return val in self.__rdict__ + # type: (int) -> bool + return val in self.__rdict__ # type: ignore def get(self, attr, val=None): - return self.__rdict__.get(attr, val) + # type: (str, Optional[Any]) -> Any + return self.__rdict__.get(attr, val) # type: ignore def __repr__(self): + # type: () -> str return "<%s>" % self.__dict__.get("name", self.__name__) -################### -# Object saving # -################### - - -def export_object(obj): - print(bytes_base64(gzip.zlib.compress(six.moves.cPickle.dumps(obj, 2), 9))) - - -def import_object(obj=None): - if obj is None: - obj = sys.stdin.read() - return six.moves.cPickle.loads(gzip.zlib.decompress(base64_bytes(obj.strip()))) # noqa: E501 - - -def save_object(fname, obj): - """Pickle a Python object""" - - fd = gzip.open(fname, "wb") - six.moves.cPickle.dump(obj, fd) - fd.close() - - -def load_object(fname): - """unpickle a Python object""" - return six.moves.cPickle.load(gzip.open(fname, "rb")) - +################## +# Corrupt data # +################## @conf.commands.register -def corrupt_bytes(s, p=0.01, n=None): - """Corrupt a given percentage or number of bytes from a string""" - s = array.array("B", bytes_encode(s)) +def corrupt_bytes(data, p=0.01, n=None): + # type: (str, float, Optional[int]) -> bytes + """ + Corrupt a given percentage (at least one byte) or number of bytes + from a string + """ + s = array.array("B", bytes_encode(data)) s_len = len(s) if n is None: n = max(1, int(s_len * p)) for i in random.sample(range(s_len), n): s[i] = (s[i] + random.randint(1, 255)) % 256 - return s.tostring() if six.PY2 else s.tobytes() + return s.tobytes() @conf.commands.register -def corrupt_bits(s, p=0.01, n=None): - """Flip a given percentage or number of bits from a string""" - s = array.array("B", bytes_encode(s)) +def corrupt_bits(data, p=0.01, n=None): + # type: (str, float, Optional[int]) -> bytes + """ + Flip a given percentage (at least one bit) or number of bits + from a string + """ + s = array.array("B", bytes_encode(data)) s_len = len(s) * 8 if n is None: n = max(1, int(s_len * p)) for i in random.sample(range(s_len), n): s[i // 8] ^= 1 << (i % 8) - return s.tostring() if six.PY2 else s.tobytes() + return s.tobytes() ############################# @@ -909,7 +1260,12 @@ def corrupt_bits(s, p=0.01, n=None): ############################# @conf.commands.register -def wrpcap(filename, pkt, *args, **kargs): +def wrpcap(filename, # type: Union[IO[bytes], str] + pkt, # type: _PacketIterable + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> None """Write a list of packets to a pcap file :param filename: the name of the file to write packets to, or an open, @@ -926,79 +1282,137 @@ def wrpcap(filename, pkt, *args, **kargs): fdesc.write(pkt) +@conf.commands.register +def wrpcapng(filename, # type: str + pkt, # type: _PacketIterable + ): + # type: (...) -> None + """Write a list of packets to a pcapng file + + :param filename: the name of the file to write packets to, or an open, + writable file-like object. The file descriptor will be + closed at the end of the call, so do not use an object you + do not want to close (e.g., running wrpcapng(sys.stdout, []) + in interactive mode will crash Scapy). + :param pkt: packets to write + """ + with PcapNgWriter(filename) as fdesc: + fdesc.write(pkt) + + @conf.commands.register def rdpcap(filename, count=-1): + # type: (Union[IO[bytes], str], int) -> PacketList """Read a pcap or pcapng file and return a packet list :param count: read only packets """ - with PcapReader(filename) as fdesc: + # Rant: Our complicated use of metaclasses and especially the + # __call__ function is, of course, not supported by MyPy. + # One day we should simplify this mess and use a much simpler + # layout that will actually be supported and properly dissected. + with PcapReader(filename) as fdesc: # type: ignore return fdesc.read_all(count=count) +# NOTE: Type hinting +# Mypy doesn't understand the following metaclass, and thinks each +# constructor (PcapReader...) needs 3 arguments each. To avoid this, +# we add a fake (=None) to the last 2 arguments then force the value +# to not be None in the signature and pack the whole thing in an ignore. +# This allows to not have # type: ignore every time we call those +# constructors. + class PcapReader_metaclass(type): """Metaclass for (Raw)Pcap(Ng)Readers""" def __new__(cls, name, bases, dct): + # type: (Any, str, Any, Dict[str, Any]) -> Any """The `alternative` class attribute is declared in the PcapNg variant, and set here to the Pcap variant. """ - newcls = super(PcapReader_metaclass, cls).__new__(cls, name, bases, dct) # noqa: E501 + newcls = super(PcapReader_metaclass, cls).__new__( + cls, name, bases, dct + ) if 'alternative' in dct: dct['alternative'].alternative = newcls return newcls def __call__(cls, filename): + # type: (Union[IO[bytes], str]) -> Any """Creates a cls instance, use the `alternative` if that fails. """ - i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) + i = cls.__new__( + cls, + cls.__name__, + cls.__bases__, + cls.__dict__ # type: ignore + ) filename, fdesc, magic = cls.open(filename) + if not magic: + raise Scapy_Exception( + "No data could be read!" + ) try: i.__init__(filename, fdesc, magic) - except Scapy_Exception: - if "alternative" in cls.__dict__: - cls = cls.__dict__["alternative"] - i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) - try: - i.__init__(filename, fdesc, magic) - except Scapy_Exception: - try: - i.f.seek(-4, 1) - except Exception: - pass - raise Scapy_Exception("Not a supported capture file") + return i + except (Scapy_Exception, EOFError): + pass + + if "alternative" in cls.__dict__: + cls = cls.__dict__["alternative"] + i = cls.__new__( + cls, + cls.__name__, + cls.__bases__, + cls.__dict__ # type: ignore + ) + try: + i.__init__(filename, fdesc, magic) + return i + except (Scapy_Exception, EOFError): + pass - return i + raise Scapy_Exception("Not a supported capture file") @staticmethod - def open(filename): + def open(fname # type: Union[IO[bytes], str] + ): + # type: (...) -> Tuple[str, _ByteStream, bytes] """Open (if necessary) filename, and read the magic.""" - if isinstance(filename, six.string_types): - try: - fdesc = gzip.open(filename, "rb") - magic = fdesc.read(4) - except IOError: - fdesc = open(filename, "rb") - magic = fdesc.read(4) + if isinstance(fname, str): + filename = fname + fdesc = open(filename, "rb") # type: _ByteStream + magic = fdesc.read(2) + if magic == b"\x1f\x8b": + # GZIP header detected. + fdesc.seek(0) + fdesc = gzip.GzipFile(fileobj=fdesc) + magic = fdesc.read(2) + magic += fdesc.read(2) else: - fdesc = filename + fdesc = fname filename = getattr(fdesc, "name", "No name") magic = fdesc.read(4) return filename, fdesc, magic -class RawPcapReader(six.with_metaclass(PcapReader_metaclass)): +class RawPcapReader(metaclass=PcapReader_metaclass): """A stateful pcap reader. Each packet is returned as a string""" - read_allowed_exceptions = () # emulate SuperSocket + # TODO: use Generics to properly type the various readers. + # As of right now, RawPcapReader is typed as if it returned packets + # because all of its child do. Fix that + nonblocking_socket = True PacketMetadata = collections.namedtuple("PacketMetadata", ["sec", "usec", "wirelen", "caplen"]) # noqa: E501 - def __init__(self, filename, fdesc, magic): + def __init__(self, filename, fdesc=None, magic=None): # type: ignore + # type: (str, _ByteStream, bytes) -> None self.filename = filename self.f = fdesc if magic == b"\xa1\xb2\xc3\xd4": # big endian @@ -1024,23 +1438,28 @@ def __init__(self, filename, fdesc, magic): self.endian + "HHIIII", hdr ) self.linktype = linktype + self.snaplen = snaplen - def __iter__(self): + def __enter__(self): + # type: () -> RawPcapReader return self - def next(self): - """implement the iterator protocol on a set of packets in a pcap file - pkt is a tuple (pkt_data, pkt_metadata) as defined in - RawPcapReader.read_packet() + def __iter__(self): + # type: () -> RawPcapReader + return self + def __next__(self): + # type: () -> Tuple[bytes, RawPcapReader.PacketMetadata] + """ + implement the iterator protocol on a set of packets in a pcap file """ try: - return self.read_packet() + return self._read_packet() except EOFError: raise StopIteration - __next__ = next - def read_packet(self, size=MTU): + def _read_packet(self, size=MTU): + # type: (int) -> Tuple[bytes, RawPcapReader.PacketMetadata] """return a single packet read from the file as a tuple containing (pkt_data, pkt_metadata) @@ -1050,11 +1469,28 @@ def read_packet(self, size=MTU): if len(hdr) < 16: raise EOFError sec, usec, caplen, wirelen = struct.unpack(self.endian + "IIII", hdr) - return (self.f.read(caplen)[:size], + + try: + data = self.f.read(caplen)[:size] + except OverflowError as e: + warning(f"Pcap: {e}") + raise EOFError + + return (data, RawPcapReader.PacketMetadata(sec=sec, usec=usec, wirelen=wirelen, caplen=caplen)) - def dispatch(self, callback): + def read_packet(self, size=MTU): + # type: (int) -> Packet + raise Exception( + "Cannot call read_packet() in RawPcapReader. Use " + "_read_packet()" + ) + + def dispatch(self, + callback # type: Callable[[Tuple[bytes, RawPcapReader.PacketMetadata]], Any] # noqa: E501 + ): + # type: (...) -> None """call the specified callback routine for each packet read This is just a convenience function for the main loop @@ -1064,59 +1500,77 @@ def dispatch(self, callback): for p in self: callback(p) - def read_all(self, count=-1): + def _read_all(self, count=-1): + # type: (int) -> List[Packet] """return a list of all packets in the pcap file """ - res = [] + res = [] # type: List[Packet] while count != 0: count -= 1 try: - p = self.read_packet() + p = self.read_packet() # type: Packet except EOFError: break res.append(p) return res def recv(self, size=MTU): + # type: (int) -> bytes """ Emulate a socket """ - return self.read_packet(size=size)[0] + return self._read_packet(size=size)[0] def fileno(self): - return self.f.fileno() + # type: () -> int + return -1 if WINDOWS else self.f.fileno() def close(self): - return self.f.close() - - def __enter__(self): - return self + # type: () -> None + if isinstance(self.f, gzip.GzipFile): + self.f.fileobj.close() # type: ignore + self.f.close() def __exit__(self, exc_type, exc_value, tracback): + # type: (Optional[Any], Optional[Any], Optional[Any]) -> None self.close() # emulate SuperSocket @staticmethod - def select(sockets, remain=None): - return sockets, None + def select(sockets, # type: List[SuperSocket] + remain=None, # type: Optional[float] + ): + # type: (...) -> List[SuperSocket] + return sockets class PcapReader(RawPcapReader): - def __init__(self, filename, fdesc, magic): + def __init__(self, filename, fdesc=None, magic=None): # type: ignore + # type: (str, IO[bytes], bytes) -> None RawPcapReader.__init__(self, filename, fdesc, magic) try: - self.LLcls = conf.l2types[self.linktype] + self.LLcls = conf.l2types.num2layer[ + self.linktype + ] # type: Type[Packet] except KeyError: warning("PcapReader: unknown LL type [%i]/[%#x]. Using Raw packets" % (self.linktype, self.linktype)) # noqa: E501 + if conf.raw_layer is None: + # conf.raw_layer is set on import + import scapy.packet # noqa: F401 self.LLcls = conf.raw_layer - def read_packet(self, size=MTU): - rp = super(PcapReader, self).read_packet(size=size) + def __enter__(self): + # type: () -> PcapReader + return self + + def read_packet(self, size=MTU, **kwargs): + # type: (int, **Any) -> Packet + rp = super(PcapReader, self)._read_packet(size=size) if rp is None: raise EOFError s, pkt_info = rp try: - p = self.LLcls(s) + p = self.LLcls(s, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -1124,20 +1578,36 @@ def read_packet(self, size=MTU): from scapy.sendrecv import debug debug.crashed_on = (self.LLcls, s) raise + if conf.raw_layer is None: + # conf.raw_layer is set on import + import scapy.packet # noqa: F401 p = conf.raw_layer(s) power = Decimal(10) ** Decimal(-9 if self.nano else -6) p.time = EDecimal(pkt_info.sec + power * pkt_info.usec) p.wirelen = pkt_info.wirelen return p + def recv(self, size=MTU, **kwargs): # type: ignore + # type: (int, **Any) -> Packet + return self.read_packet(size=size, **kwargs) + + def __iter__(self): + # type: () -> PcapReader + return self + + def __next__(self): # type: ignore + # type: () -> Packet + try: + return self.read_packet() + except EOFError: + raise StopIteration + def read_all(self, count=-1): - res = RawPcapReader.read_all(self, count) + # type: (int) -> PacketList + res = self._read_all(count) from scapy import plist return plist.PacketList(res, name=os.path.basename(self.filename)) - def recv(self, size=MTU): - return self.read_packet(size=size) - class RawPcapNgReader(RawPcapReader): """A stateful pcapng reader. Each packet is returned as @@ -1145,186 +1615,667 @@ class RawPcapNgReader(RawPcapReader): """ - alternative = RawPcapReader + alternative = RawPcapReader # type: Type[Any] - PacketMetadata = collections.namedtuple("PacketMetadata", + PacketMetadata = collections.namedtuple("PacketMetadataNg", # type: ignore ["linktype", "tsresol", - "tshigh", "tslow", "wirelen"]) + "tshigh", "tslow", "wirelen", + "comments", "ifname", "direction", + "process_information"]) - def __init__(self, filename, fdesc, magic): + def __init__(self, filename, fdesc=None, magic=None): # type: ignore + # type: (str, IO[bytes], bytes) -> None self.filename = filename self.f = fdesc # A list of (linktype, snaplen, tsresol); will be populated by IDBs. - self.interfaces = [] + self.interfaces = [] # type: List[Tuple[int, int, Dict[str, Any]]] self.default_options = { "tsresol": 1000000 } - self.blocktypes = { - 1: self.read_block_idb, - 2: self.read_block_pkt, - 3: self.read_block_spb, - 6: self.read_block_epb, + self.blocktypes: Dict[ + int, + Callable[ + [bytes, int], + Optional[Tuple[bytes, RawPcapNgReader.PacketMetadata]] + ]] = { + 1: self._read_block_idb, + 2: self._read_block_pkt, + 3: self._read_block_spb, + 6: self._read_block_epb, + 10: self._read_block_dsb, + 0x80000001: self._read_block_pib, } + self.endian = "!" # Will be overwritten by first SHB + self.process_information = [] # type: List[Dict[str, Any]] + if magic != b"\x0a\x0d\x0d\x0a": # PcapNg: raise Scapy_Exception( "Not a pcapng capture file (bad magic: %r)" % magic ) - # see https://github.com/pcapng/pcapng - blocklen, magic = self.f.read(4), self.f.read(4) # noqa: F841 - if magic == b"\x1a\x2b\x3c\x4d": + + try: + self._read_block_shb() + except EOFError: + raise Scapy_Exception( + "The first SHB of the pcapng file is malformed !" + ) + + def _read_block(self, size=MTU): + # type: (int) -> Optional[Tuple[bytes, RawPcapNgReader.PacketMetadata]] # noqa: E501 + try: + blocktype = struct.unpack(self.endian + "I", self.f.read(4))[0] + except struct.error: + raise EOFError + if blocktype == 0x0A0D0D0A: + # This function updates the endianness based on the block content. + self._read_block_shb() + return None + try: + blocklen = struct.unpack(self.endian + "I", self.f.read(4))[0] + except struct.error: + warning("PcapNg: Error reading blocklen before block body") + raise EOFError + if blocklen < 12: + warning("PcapNg: Invalid block length !") + raise EOFError + + _block_body_length = blocklen - 12 + block = self.f.read(_block_body_length) + if len(block) != _block_body_length: + raise Scapy_Exception("PcapNg: Invalid Block body length " + "(too short)") + self._read_block_tail(blocklen) + if blocktype in self.blocktypes: + return self.blocktypes[blocktype](block, size) + return None + + def _read_block_tail(self, blocklen): + # type: (int) -> None + if blocklen % 4: + pad = self.f.read(-blocklen % 4) + warning("PcapNg: bad blocklen %d (MUST be a multiple of 4. " + "Ignored padding %r" % (blocklen, pad)) + try: + if blocklen != struct.unpack(self.endian + 'I', + self.f.read(4))[0]: + raise EOFError("PcapNg: Invalid pcapng block (bad blocklen)") + except struct.error: + warning("PcapNg: Could not read blocklen after block body") + raise EOFError + + def _read_block_shb(self): + # type: () -> None + """Section Header Block""" + _blocklen = self.f.read(4) + endian = self.f.read(4) + if endian == b"\x1a\x2b\x3c\x4d": self.endian = ">" - elif magic == b"\x4d\x3c\x2b\x1a": + elif endian == b"\x4d\x3c\x2b\x1a": self.endian = "<" else: - raise Scapy_Exception("Not a pcapng capture file (bad magic)") - self.f.read(12) - blocklen = struct.unpack("!I", blocklen)[0] - # Read default options - self.default_options = self.read_options( - self.f.read(blocklen - 24) - ) + warning("PcapNg: Bad magic in Section Header Block" + " (not a pcapng file?)") + raise EOFError + try: - self.f.seek(0) - except Exception: - pass + blocklen = struct.unpack(self.endian + "I", _blocklen)[0] + except struct.error: + warning("PcapNg: Could not read blocklen") + raise EOFError + if blocklen < 28: + warning(f"PcapNg: Invalid Section Header Block length ({blocklen})!") # noqa: E501 + raise EOFError - def read_packet(self, size=MTU): + # Major version must be 1 + _major = self.f.read(2) + try: + major = struct.unpack(self.endian + "H", _major)[0] + except struct.error: + warning("PcapNg: Could not read major value") + raise EOFError + if major != 1: + warning(f"PcapNg: SHB Major version {major} unsupported !") + raise EOFError + + # Skip minor version & section length + skipped = self.f.read(10) + if len(skipped) != 10: + warning("PcapNg: Could not read minor value & section length") + raise EOFError + + _options_len = blocklen - 28 + options = self.f.read(_options_len) + if len(options) != _options_len: + raise Scapy_Exception("PcapNg: Invalid Section Header Block " + " options (too short)") + self._read_block_tail(blocklen) + self._read_options(options) + + def _read_packet(self, size=MTU): # type: ignore + # type: (int) -> Tuple[bytes, RawPcapNgReader.PacketMetadata] """Read blocks until it reaches either EOF or a packet, and returns None or (packet, (linktype, sec, usec, wirelen)), where packet is a string. """ while True: - try: - blocktype, blocklen = struct.unpack(self.endian + "2I", - self.f.read(8)) - except struct.error: - raise EOFError - block = self.f.read(blocklen - 12) - if blocklen % 4: - pad = self.f.read(4 - (blocklen % 4)) - warning("PcapNg: bad blocklen %d (MUST be a multiple of 4. " - "Ignored padding %r" % (blocklen, pad)) - try: - if (blocklen,) != struct.unpack(self.endian + 'I', - self.f.read(4)): - warning("PcapNg: Invalid pcapng block (bad blocklen)") - except struct.error: - raise EOFError - res = self.blocktypes.get(blocktype, - lambda block, size: None)(block, size) + res = self._read_block(size=size) if res is not None: return res - def read_options(self, options): - """Section Header Block""" - opts = self.default_options.copy() + def _read_options(self, options): + # type: (bytes) -> Dict[int, Union[bytes, List[bytes]]] + opts = dict() # type: Dict[int, Union[bytes, List[bytes]]] while len(options) >= 4: - code, length = struct.unpack(self.endian + "HH", options[:4]) - # PCAP Next Generation (pcapng) Capture File Format - # 4.2. - Interface Description Block - # http://xml2rfc.tools.ietf.org/cgi-bin/xml2rfc.cgi?url=https://raw.githubusercontent.com/pcapng/pcapng/master/draft-tuexen-opsawg-pcapng.xml&modeAsFormat=html/ascii&type=ascii#rfc.section.4.2 - if code == 9 and length == 1 and len(options) >= 5: - tsresol = orb(options[4]) - opts["tsresol"] = (2 if tsresol & 128 else 10) ** ( - tsresol & 127 - ) + try: + code, length = struct.unpack(self.endian + "HH", options[:4]) + except struct.error: + warning("PcapNg: options header is too small " + "%d !" % len(options)) + raise EOFError + if code != 0 and 4 + length <= len(options): + # https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-05.html#name-options-format + if code in [1, 2988, 2989, 19372, 19373]: + if code not in opts: + opts[code] = [] + opts[code].append(options[4:4 + length]) # type: ignore + else: + opts[code] = options[4:4 + length] if code == 0: if length != 0: - warning("PcapNg: invalid option length %d for end-of-option" % length) # noqa: E501 + warning("PcapNg: invalid option " + "length %d for end-of-option" % length) break if length % 4: length += (4 - (length % 4)) options = options[4 + length:] return opts - def read_block_idb(self, block, _): + def _read_block_idb(self, block, _): + # type: (bytes, int) -> None """Interface Description Block""" - options = self.read_options(block[16:]) - self.interfaces.append(struct.unpack(self.endian + "HxxI", block[:8]) + - (options["tsresol"],)) + # 2 bytes LinkType + 2 bytes Reserved + # 4 bytes Snaplen + options_raw = self._read_options(block[8:]) + options = self.default_options.copy() # type: Dict[str, Any] + for c, v in options_raw.items(): + if isinstance(v, list): + # Spec allows multiple occurrences (see + # https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-05.html#section-4.2-8.6) + # but does not define which to use. We take the first for + # backward compatibility. + v = v[0] + if c == 9: + length = len(v) + if length == 1: + tsresol = orb(v) + options["tsresol"] = (2 if tsresol & 128 else 10) ** ( + tsresol & 127 + ) + else: + warning("PcapNg: invalid options " + "length %d for IDB tsresol" % length) + elif c == 2: + options["name"] = v + elif c == 1: + options["comment"] = v + try: + interface: Tuple[int, int, Dict[str, Any]] = struct.unpack( + self.endian + "HxxI", + block[:8] + ) + (options,) + except struct.error: + warning("PcapNg: IDB is too small %d/8 !" % len(block)) + raise EOFError + self.interfaces.append(interface) + + def _check_interface_id(self, intid): + # type: (int) -> None + """Check the interface id value and raise EOFError if invalid.""" + tmp_len = len(self.interfaces) + if intid >= tmp_len: + warning("PcapNg: invalid interface id %d/%d" % (intid, tmp_len)) + raise EOFError - def read_block_epb(self, block, size): + def _read_block_epb(self, block, size): + # type: (bytes, int) -> Tuple[bytes, RawPcapNgReader.PacketMetadata] """Enhanced Packet Block""" - intid, tshigh, tslow, caplen, wirelen = struct.unpack( - self.endian + "5I", - block[:20], - ) - return (block[20:20 + caplen][:size], - RawPcapNgReader.PacketMetadata(linktype=self.interfaces[intid][0], # noqa: E501 - tsresol=self.interfaces[intid][2], # noqa: E501 - tshigh=tshigh, - tslow=tslow, - wirelen=wirelen)) + try: + intid, tshigh, tslow, caplen, wirelen = struct.unpack( + self.endian + "5I", + block[:20], + ) + except struct.error: + warning("PcapNg: EPB is too small %d/20 !" % len(block)) + raise EOFError - def read_block_spb(self, block, size): - """Simple Packet Block""" - # "it MUST be assumed that all the Simple Packet Blocks have - # been captured on the interface previously specified in the - # first Interface Description Block." - intid = 0 - wirelen, = struct.unpack(self.endian + "I", block[:4]) - caplen = min(wirelen, self.interfaces[intid][1]) - return (block[4:4 + caplen][:size], - RawPcapNgReader.PacketMetadata(linktype=self.interfaces[intid][0], # noqa: E501 - tsresol=self.interfaces[intid][2], # noqa: E501 - tshigh=None, - tslow=None, - wirelen=wirelen)) + # Compute the options offset taking padding into account + if caplen % 4: + opt_offset = 20 + caplen + (-caplen) % 4 + else: + opt_offset = 20 + caplen + + # Parse options + options = self._read_options(block[opt_offset:]) + + process_information = {} + for code, value in options.items(): + # PCAPNG_EPB_PIB_INDEX, PCAPNG_EPB_E_PIB_INDEX + if code in [0x8001, 0x8003]: + try: + proc_index = struct.unpack( + self.endian + "I", value)[0] # type: ignore + except struct.error: + warning("PcapNg: EPB invalid proc index " + "(expected 4 bytes, got %d) !" % len(value)) + raise EOFError + if proc_index < len(self.process_information): + key = "proc" if code == 0x8001 else "eproc" + process_information[key] = self.process_information[proc_index] + else: + warning("PcapNg: EPB invalid process information index " + "(%d/%d) !" % (proc_index, len(self.process_information))) + + comments = options.get(1, None) + epb_flags_raw = options.get(2, None) + if epb_flags_raw and isinstance(epb_flags_raw, bytes): + try: + epb_flags, = struct.unpack(self.endian + "I", epb_flags_raw) + except struct.error: + warning("PcapNg: EPB invalid flags size" + "(expected 4 bytes, got %d) !" % len(epb_flags_raw)) + raise EOFError + direction = epb_flags & 3 + + else: + direction = None + + self._check_interface_id(intid) + ifname = self.interfaces[intid][2].get('name', None) + + return (block[20:20 + caplen][:size], + RawPcapNgReader.PacketMetadata(linktype=self.interfaces[intid][0], # noqa: E501 + tsresol=self.interfaces[intid][2]['tsresol'], # noqa: E501 + tshigh=tshigh, + tslow=tslow, + wirelen=wirelen, + ifname=ifname, + direction=direction, + process_information=process_information, + comments=comments)) + + def _read_block_spb(self, block, size): + # type: (bytes, int) -> Tuple[bytes, RawPcapNgReader.PacketMetadata] + """Simple Packet Block""" + # "it MUST be assumed that all the Simple Packet Blocks have + # been captured on the interface previously specified in the + # first Interface Description Block." + intid = 0 + self._check_interface_id(intid) + + try: + wirelen, = struct.unpack(self.endian + "I", block[:4]) + except struct.error: + warning("PcapNg: SPB is too small %d/4 !" % len(block)) + raise EOFError - def read_block_pkt(self, block, size): + caplen = min(wirelen, self.interfaces[intid][1]) + return (block[4:4 + caplen][:size], + RawPcapNgReader.PacketMetadata(linktype=self.interfaces[intid][0], # noqa: E501 + tsresol=self.interfaces[intid][2]['tsresol'], # noqa: E501 + tshigh=None, + tslow=None, + wirelen=wirelen, + ifname=None, + direction=None, + process_information={}, + comments=None)) + + def _read_block_pkt(self, block, size): + # type: (bytes, int) -> Tuple[bytes, RawPcapNgReader.PacketMetadata] """(Obsolete) Packet Block""" - intid, drops, tshigh, tslow, caplen, wirelen = struct.unpack( - self.endian + "HH4I", - block[:20], - ) + try: + intid, drops, tshigh, tslow, caplen, wirelen = struct.unpack( + self.endian + "HH4I", + block[:20], + ) + except struct.error: + warning("PcapNg: PKT is too small %d/20 !" % len(block)) + raise EOFError + + self._check_interface_id(intid) return (block[20:20 + caplen][:size], RawPcapNgReader.PacketMetadata(linktype=self.interfaces[intid][0], # noqa: E501 - tsresol=self.interfaces[intid][2], # noqa: E501 + tsresol=self.interfaces[intid][2]['tsresol'], # noqa: E501 tshigh=tshigh, tslow=tslow, - wirelen=wirelen)) + wirelen=wirelen, + ifname=None, + direction=None, + process_information={}, + comments=None)) + + def _read_block_dsb(self, block, size): + # type: (bytes, int) -> None + """Decryption Secrets Block""" + + # Parse the secrets type and length fields + try: + secrets_type, secrets_length = struct.unpack( + self.endian + "II", + block[:8], + ) + block = block[8:] + except struct.error: + warning("PcapNg: DSB is too small %d!", len(block)) + raise EOFError + + # Compute the secrets length including the padding + padded_secrets_length = secrets_length + (-secrets_length) % 4 + if len(block) < padded_secrets_length: + warning("PcapNg: invalid DSB secrets length!") + raise EOFError + + # Extract secrets data and options + secrets_data = block[:padded_secrets_length][:secrets_length] + if block[padded_secrets_length:]: + warning("PcapNg: DSB options are not supported!") + + # TLS Key Log + if secrets_type == 0x544c534b: + if getattr(conf, "tls_sessions", False) is False: + warning("PcapNg: TLS Key Log available, but " + "the TLS layer is not loaded! Scapy won't be able " + "to decrypt the packets.") + else: + from scapy.layers.tls.session import load_nss_keys + + # Write Key Log to a file and parse it + filename = get_temp_file() + with open(filename, "wb") as fd: + fd.write(secrets_data) + fd.close() + keys = load_nss_keys(filename) + if not keys: + warning("PcapNg: invalid TLS Key Log in DSB!") + else: + # Note: these attributes are only available when the TLS + # layer is loaded. + conf.tls_nss_keys = keys + conf.tls_session_enable = True + else: + warning("PcapNg: Unknown DSB secrets type (0x%x)!", secrets_type) -class PcapNgReader(RawPcapNgReader): + def _read_block_pib(self, block, _): + # type: (bytes, int) -> None + """Apple Process Information Block""" + + # Get the Process ID + try: + dpeb_pid = struct.unpack(self.endian + "I", block[:4])[0] + process_information = {"id": dpeb_pid} + block = block[4:] + except struct.error: + warning("PcapNg: DPEB is too small (%d). Cannot get PID!", + len(block)) + raise EOFError + + # Get Options + options = self._read_options(block) + for code, value in options.items(): + if code == 2: + process_information["name"] = value.decode( # type: ignore + "ascii", "backslashreplace") + elif code == 4: + if len(value) == 16: + process_information["uuid"] = str(UUID(bytes=value)) # type: ignore + else: + warning("PcapNg: DPEB UUID length is invalid (%d)!", + len(value)) + + # Store process information + self.process_information.append(process_information) + + +class PcapNgReader(RawPcapNgReader, PcapReader): alternative = PcapReader - def __init__(self, filename, fdesc, magic): + def __init__(self, filename, fdesc=None, magic=None): # type: ignore + # type: (str, IO[bytes], bytes) -> None RawPcapNgReader.__init__(self, filename, fdesc, magic) - def read_packet(self, size=MTU): - rp = super(PcapNgReader, self).read_packet(size=size) + def __enter__(self): + # type: () -> PcapNgReader + return self + + def read_packet(self, size=MTU, **kwargs): + # type: (int, **Any) -> Packet + rp = super(PcapNgReader, self)._read_packet(size=size) if rp is None: raise EOFError - s, (linktype, tsresol, tshigh, tslow, wirelen) = rp + s, (linktype, tsresol, tshigh, tslow, wirelen, comments, ifname, direction, process_information) = rp # noqa: E501 try: - p = conf.l2types[linktype](s) + cls = conf.l2types.num2layer[linktype] # type: Type[Packet] + p = cls(s, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: if conf.debug_dissector: raise + if conf.raw_layer is None: + # conf.raw_layer is set on import + import scapy.packet # noqa: F401 p = conf.raw_layer(s) if tshigh is not None: p.time = EDecimal((tshigh << 32) + tslow) / tsresol p.wirelen = wirelen + p.comments = comments + p.direction = direction + p.process_information = process_information.copy() + if ifname is not None: + p.sniffed_on = ifname.decode('utf-8', 'backslashreplace') return p - def read_all(self, count=-1): - res = RawPcapNgReader.read_all(self, count) - from scapy import plist - return plist.PacketList(res, name=os.path.basename(self.filename)) + def recv(self, size: int = MTU, **kwargs: Any) -> 'Packet': # type: ignore + return self.read_packet(size=size, **kwargs) - def recv(self, size=MTU): - return self.read_packet() +class GenericPcapWriter(object): + nano = False + linktype: int + + def _write_header(self, pkt): + # type: (Optional[Union[Packet, bytes]]) -> None + raise NotImplementedError + + def _write_packet(self, + packet, # type: Union[bytes, Packet] + linktype, # type: int + sec=None, # type: Optional[float] + usec=None, # type: Optional[int] + caplen=None, # type: Optional[int] + wirelen=None, # type: Optional[int] + ifname=None, # type: Optional[bytes] + direction=None, # type: Optional[int] + comments=None, # type: Optional[List[bytes]] + ): + # type: (...) -> None + raise NotImplementedError + + def _get_time(self, + packet, # type: Union[bytes, Packet] + sec, # type: Optional[float] + usec # type: Optional[int] + ): + # type: (...) -> Tuple[float, int] + if hasattr(packet, "time"): + if sec is None: + packet_time = packet.time + tmp = int(packet_time) + usec = int(round((packet_time - tmp) * + (1000000000 if self.nano else 1000000))) + sec = float(packet_time) + if sec is not None and usec is None: + usec = 0 + return sec, usec # type: ignore + + def write_header(self, pkt): + # type: (Optional[Union[Packet, bytes]]) -> None + if not hasattr(self, 'linktype'): + try: + if pkt is None or isinstance(pkt, bytes): + # Can't guess LL + raise KeyError + self.linktype = conf.l2types.layer2num[ + pkt.__class__ + ] + except KeyError: + msg = "%s: unknown LL type for %s. Using type 1 (Ethernet)" + warning(msg, self.__class__.__name__, pkt.__class__.__name__) + self.linktype = DLT_EN10MB + self._write_header(pkt) + + def write_packet(self, + packet, # type: Union[bytes, Packet] + sec=None, # type: Optional[float] + usec=None, # type: Optional[int] + caplen=None, # type: Optional[int] + wirelen=None, # type: Optional[int] + ): + # type: (...) -> None + """ + Writes a single packet to the pcap file. + + :param packet: Packet, or bytes for a single packet + :type packet: scapy.packet.Packet or bytes + :param sec: time the packet was captured, in seconds since epoch. If + not supplied, defaults to now. + :type sec: float + :param usec: If ``nano=True``, then number of nanoseconds after the + second that the packet was captured. If ``nano=False``, + then the number of microseconds after the second the + packet was captured. If ``sec`` is not specified, + this value is ignored. + :type usec: int or long + :param caplen: The length of the packet in the capture file. If not + specified, uses ``len(raw(packet))``. + :type caplen: int + :param wirelen: The length of the packet on the wire. If not + specified, tries ``packet.wirelen``, otherwise uses + ``caplen``. + :type wirelen: int + :return: None + :rtype: None + """ + f_sec, usec = self._get_time(packet, sec, usec) + + rawpkt = bytes_encode(packet) + caplen = len(rawpkt) if caplen is None else caplen + + if wirelen is None: + if hasattr(packet, "wirelen"): + wirelen = packet.wirelen + if wirelen is None: + wirelen = caplen + + comments = getattr(packet, "comments", None) + ifname = getattr(packet, "sniffed_on", None) + direction = getattr(packet, "direction", None) + if not isinstance(packet, bytes): + linktype: int = conf.l2types.layer2num[ + packet.__class__ + ] + else: + linktype = self.linktype + if ifname is not None: + ifname = str(ifname).encode('utf-8') + self._write_packet( + rawpkt, + sec=f_sec, usec=usec, + caplen=caplen, wirelen=wirelen, + ifname=ifname, + direction=direction, + linktype=linktype, + comments=comments, + ) + + +class GenericRawPcapWriter(GenericPcapWriter): + header_present = False + nano = False + sync = False + f = None # type: Union[IO[bytes], gzip.GzipFile] + + def fileno(self): + # type: () -> int + return -1 if WINDOWS else self.f.fileno() + + def flush(self): + # type: () -> Optional[Any] + return self.f.flush() + + def close(self): + # type: () -> Optional[Any] + if not self.header_present: + self.write_header(None) + return self.f.close() + + def __enter__(self): + # type: () -> GenericRawPcapWriter + return self + + def __exit__(self, exc_type, exc_value, tracback): + # type: (Optional[Any], Optional[Any], Optional[Any]) -> None + self.flush() + self.close() + + def write(self, pkt): + # type: (Union[_PacketIterable, bytes]) -> None + """ + Writes a Packet, a SndRcvList object, or bytes to a pcap file. + + :param pkt: Packet(s) to write (one record for each Packet), or raw + bytes to write (as one record). + :type pkt: iterable[scapy.packet.Packet], scapy.packet.Packet or bytes + """ + if isinstance(pkt, bytes): + if not self.header_present: + self.write_header(pkt) + self.write_packet(pkt) + else: + # Import here to avoid circular dependency + from scapy.supersocket import IterSocket + for p in IterSocket(pkt).iter: + if not self.header_present: + self.write_header(p) + + if not isinstance(p, bytes) and \ + self.linktype != conf.l2types.get(type(p), None): + warning("Inconsistent linktypes detected!" + " The resulting file might contain" + " invalid packets." + ) -class RawPcapWriter: + self.write_packet(p) + + +class RawPcapWriter(GenericRawPcapWriter): """A stream PCAP writer with more control than wrpcap()""" - def __init__(self, filename, linktype=None, gz=False, endianness="", - append=False, sync=False, nano=False): + def __init__(self, + filename, # type: Union[IO[bytes], str] + linktype=None, # type: Optional[int] + gz=False, # type: bool + endianness="", # type: str + append=False, # type: bool + sync=False, # type: bool + nano=False, # type: bool + snaplen=MTU, # type: int + bufsz=4096, # type: int + ): + # type: (...) -> None """ :param filename: the name of the file to write packets to, or an open, writable file-like object. @@ -1340,88 +2291,81 @@ def __init__(self, filename, linktype=None, gz=False, endianness="", """ - self.linktype = linktype - self.header_present = 0 + if linktype: + self.linktype = linktype + self.snaplen = snaplen self.append = append self.gz = gz self.endian = endianness self.sync = sync self.nano = nano - bufsz = 4096 if sync: bufsz = 0 - if isinstance(filename, six.string_types): + if isinstance(filename, str): self.filename = filename - self.f = [open, gzip.open][gz](filename, append and "ab" or "wb", gz and 9 or bufsz) # noqa: E501 + if gz: + self.f = cast(_ByteStream, gzip.open( + filename, append and "ab" or "wb", 9 + )) + else: + self.f = open(filename, append and "ab" or "wb", bufsz) else: self.f = filename self.filename = getattr(filename, "name", "No name") - def fileno(self): - return self.f.fileno() - def _write_header(self, pkt): - self.header_present = 1 + # type: (Optional[Union[Packet, bytes]]) -> None + self.header_present = True if self.append: # Even if prone to race conditions, this seems to be # safest way to tell whether the header is already present # because we have to handle compressed streams that # are not as flexible as basic files - g = [open, gzip.open][self.gz](self.filename, "rb") - if g.read(16): - return - - self.f.write(struct.pack(self.endian + "IHHIIII", 0xa1b23c4d if self.nano else 0xa1b2c3d4, # noqa: E501 - 2, 4, 0, 0, MTU, self.linktype)) - self.f.flush() - - def write(self, pkt): - """ - Writes a Packet, a SndRcvList object, or bytes to a pcap file. - - :param pkt: Packet(s) to write (one record for each Packet), or raw - bytes to write (as one record). - :type pkt: iterable[scapy.packet.Packet], scapy.packet.Packet or bytes - """ - if isinstance(pkt, bytes): - if not self.header_present: - self._write_header(pkt) - self._write_packet(pkt) - else: - # Import here to avoid a circular dependency - from scapy.plist import SndRcvList - if isinstance(pkt, SndRcvList): - pkt = (p for t in pkt for p in t) + if self.gz: + g = gzip.open(self.filename, "rb") # type: _ByteStream else: - pkt = pkt.__iter__() - for p in pkt: - - if not self.header_present: - self._write_header(p) + g = open(self.filename, "rb") + try: + if g.read(16): + return + finally: + g.close() - if self.linktype != conf.l2types.get(type(p), None): - warning("Inconsistent linktypes detected!" - " The resulting PCAP file might contain" - " invalid packets." - ) + if not hasattr(self, 'linktype'): + raise ValueError( + "linktype could not be guessed. " + "Please pass a linktype while creating the writer" + ) - self._write_packet(p) + self.f.write(struct.pack(self.endian + "IHHIIII", 0xa1b23c4d if self.nano else 0xa1b2c3d4, # noqa: E501 + 2, 4, 0, 0, self.snaplen, self.linktype)) + self.f.flush() - def _write_packet(self, packet, sec=None, usec=None, caplen=None, - wirelen=None): + def _write_packet(self, + packet, # type: Union[bytes, Packet] + linktype, # type: int + sec=None, # type: Optional[float] + usec=None, # type: Optional[int] + caplen=None, # type: Optional[int] + wirelen=None, # type: Optional[int] + ifname=None, # type: Optional[bytes] + direction=None, # type: Optional[int] + comments=None, # type: Optional[List[bytes]] + ): + # type: (...) -> None """ Writes a single packet to the pcap file. :param packet: bytes for a single packet :type packet: bytes + :param linktype: linktype value associated with the packet + :type linktype: int :param sec: time the packet was captured, in seconds since epoch. If not supplied, defaults to now. - :type sec: int or long - :param usec: If ``nano=True``, then number of nanoseconds after the - second that the packet was captured. If ``nano=False``, - then the number of microseconds after the second the + :type sec: float + :param usec: not used with pcapng packet was captured :type usec: int or long :param caplen: The length of the packet in the capture file. If not @@ -1448,106 +2392,527 @@ def _write_packet(self, packet, sec=None, usec=None, caplen=None, usec = 0 self.f.write(struct.pack(self.endian + "IIII", - sec, usec, caplen, wirelen)) - self.f.write(packet) + int(sec), usec, caplen, wirelen)) + self.f.write(bytes(packet)) if self.sync: self.f.flush() - def flush(self): - return self.f.flush() - def close(self): - if not self.header_present: - self._write_header(None) - return self.f.close() +class RawPcapNgWriter(GenericRawPcapWriter): + """A stream pcapng writer with more control than wrpcapng()""" - def __enter__(self): - return self + def __init__(self, + filename, # type: str + ): + # type: (...) -> None - def __exit__(self, exc_type, exc_value, tracback): - self.flush() - self.close() + self.header_present = False + self.tsresol = 1000000 + # A dict to keep if_name to IDB id mapping. + # unknown if_name(None) id=0 + self.interfaces2id: Dict[Optional[bytes], int] = {None: 0} + # tcpdump only support little-endian in PCAPng files + self.endian = "<" + self.endian_magic = b"\x4d\x3c\x2b\x1a" -class PcapWriter(RawPcapWriter): - """A stream PCAP writer with more control than wrpcap()""" + self.filename = filename + self.f = open(filename, "wb", 4096) + + def _get_time(self, + packet, # type: Union[bytes, Packet] + sec, # type: Optional[float] + usec # type: Optional[int] + ): + # type: (...) -> Tuple[float, int] + if hasattr(packet, "time"): + if sec is None: + sec = float(packet.time) - def _write_header(self, pkt): - if self.linktype is None: - try: - self.linktype = conf.l2types[pkt.__class__] - # Import here to prevent import loops - from scapy.layers.inet import IP - from scapy.layers.inet6 import IPv6 - if OPENBSD and isinstance(pkt, (IP, IPv6)): - self.linktype = 14 # DLT_RAW - except KeyError: - warning("PcapWriter: unknown LL type for %s. Using type 1 (Ethernet)", pkt.__class__.__name__) # noqa: E501 - self.linktype = DLT_EN10MB - RawPcapWriter._write_header(self, pkt) + if usec is None: + usec = 0 + + return sec, usec # type: ignore + + def _add_padding(self, raw_data): + # type: (bytes) -> bytes + raw_data += ((-len(raw_data)) % 4) * b"\x00" + return raw_data + + def build_block(self, block_type, block_body, options=None): + # type: (bytes, bytes, Optional[bytes]) -> bytes + + # Pad Block Body to 32 bits + block_body = self._add_padding(block_body) + + if options: + block_body += options + + # An empty block is 12 bytes long + block_total_length = 12 + len(block_body) + + # Block Type + block = block_type + # Block Total Length$ + block += struct.pack(self.endian + "I", block_total_length) + # Block Body + block += block_body + # Block Total Length$ + block += struct.pack(self.endian + "I", block_total_length) - def _write_packet(self, packet, sec=None, usec=None, caplen=None, - wirelen=None): + return block + + def _write_header(self, pkt): + # type: (Optional[Union[Packet, bytes]]) -> None + if not self.header_present: + self.header_present = True + self._write_block_shb() + self._write_block_idb(linktype=self.linktype) + + def _write_block_shb(self): + # type: () -> None + + # Block Type + block_type = b"\x0A\x0D\x0D\x0A" + # Byte-Order Magic + block_shb = self.endian_magic + # Major Version + block_shb += struct.pack(self.endian + "H", 1) + # Minor Version + block_shb += struct.pack(self.endian + "H", 0) + # Section Length + block_shb += struct.pack(self.endian + "q", -1) + + self.f.write(self.build_block(block_type, block_shb)) + + def _write_block_idb(self, + linktype, # type: int + ifname=None # type: Optional[bytes] + ): + # type: (...) -> None + + # Block Type + block_type = struct.pack(self.endian + "I", 1) + # LinkType + block_idb = struct.pack(self.endian + "H", linktype) + # Reserved + block_idb += struct.pack(self.endian + "H", 0) + # SnapLen + block_idb += struct.pack(self.endian + "I", 262144) + + # if_name option + opts = None + if ifname is not None: + opts = struct.pack(self.endian + "HH", 2, len(ifname)) + # Pad Option Value to 32 bits + opts += self._add_padding(ifname) + opts += struct.pack(self.endian + "HH", 0, 0) + + self.f.write(self.build_block(block_type, block_idb, options=opts)) + + def _write_block_spb(self, raw_pkt): + # type: (bytes) -> None + + # Block Type + block_type = struct.pack(self.endian + "I", 3) + # Original Packet Length + block_spb = struct.pack(self.endian + "I", len(raw_pkt)) + # Packet Data + block_spb += raw_pkt + + self.f.write(self.build_block(block_type, block_spb)) + + def _write_block_epb(self, + raw_pkt, # type: bytes + ifid, # type: int + timestamp=None, # type: Optional[Union[EDecimal, float]] # noqa: E501 + caplen=None, # type: Optional[int] + orglen=None, # type: Optional[int] + comments=None, # type: Optional[List[bytes]] + flags=None, # type: Optional[int] + ): + # type: (...) -> None + + if timestamp: + tmp_ts = int(timestamp * self.tsresol) + ts_high = tmp_ts >> 32 + ts_low = tmp_ts & 0xFFFFFFFF + else: + ts_high = ts_low = 0 + + if not caplen: + caplen = len(raw_pkt) + + if not orglen: + orglen = len(raw_pkt) + + # Block Type + block_type = struct.pack(self.endian + "I", 6) + # Interface ID + block_epb = struct.pack(self.endian + "I", ifid) + # Timestamp (High) + block_epb += struct.pack(self.endian + "I", ts_high) + # Timestamp (Low) + block_epb += struct.pack(self.endian + "I", ts_low) + # Captured Packet Length + block_epb += struct.pack(self.endian + "I", caplen) + # Original Packet Length + block_epb += struct.pack(self.endian + "I", orglen) + # Packet Data + block_epb += raw_pkt + + # Options + opts = b'' + if comments and len(comments): + for c in comments: + comment = bytes_encode(c) + opts += struct.pack(self.endian + "HH", 1, len(comment)) + # Pad Option Value to 32 bits + opts += self._add_padding(comment) + if type(flags) == int: + opts += struct.pack(self.endian + "HH", 2, 4) + opts += struct.pack(self.endian + "I", flags) + if opts: + opts += struct.pack(self.endian + "HH", 0, 0) + + self.f.write(self.build_block(block_type, block_epb, + options=opts)) + + def _write_packet(self, # type: ignore + packet, # type: bytes + linktype, # type: int + sec=None, # type: Optional[float] + usec=None, # type: Optional[int] + caplen=None, # type: Optional[int] + wirelen=None, # type: Optional[int] + ifname=None, # type: Optional[bytes] + direction=None, # type: Optional[int] + comments=None, # type: Optional[List[bytes]] + ): + # type: (...) -> None """ Writes a single packet to the pcap file. - :param packet: Packet, or bytes for a single packet - :type packet: scapy.packet.Packet or bytes + :param packet: bytes for a single packet + :type packet: bytes + :param linktype: linktype value associated with the packet + :type linktype: int :param sec: time the packet was captured, in seconds since epoch. If not supplied, defaults to now. - :type sec: int or long - :param usec: If ``nano=True``, then number of nanoseconds after the - second that the packet was captured. If ``nano=False``, - then the number of microseconds after the second the - packet was captured. If ``sec`` is not specified, - this value is ignored. - :type usec: int or long + :type sec: float :param caplen: The length of the packet in the capture file. If not - specified, uses ``len(raw(packet))``. + specified, uses ``len(packet)``. :type caplen: int :param wirelen: The length of the packet on the wire. If not - specified, tries ``packet.wirelen``, otherwise uses - ``caplen``. + specified, uses ``caplen``. :type wirelen: int + :param comment: UTF-8 string containing human-readable comment text + that is associated to the current block. Line separators + SHOULD be a carriage-return + linefeed ('\r\n') or + just linefeed ('\n'); either form may appear and + be considered a line separator. The string is not + zero-terminated. + :type bytes + :param ifname: UTF-8 string containing the + name of the device used to capture data. + The string is not zero-terminated. + :type bytes + :param direction: 0 = information not available, + 1 = inbound, + 2 = outbound + :type int :return: None :rtype: None """ + if caplen is None: + caplen = len(packet) + if wirelen is None: + wirelen = caplen + + ifid = self.interfaces2id.get(ifname, None) + if ifid is None: + ifid = max(self.interfaces2id.values()) + 1 + self.interfaces2id[ifname] = ifid + self._write_block_idb(linktype=linktype, ifname=ifname) + + # EPB flags (32 bits). + # currently only direction is implemented (least 2 significant bits) + if type(direction) == int: + flags = direction & 0x3 + else: + flags = None + + self._write_block_epb(packet, timestamp=sec, caplen=caplen, + orglen=wirelen, comments=comments, ifid=ifid, flags=flags) + if self.sync: + self.f.flush() + + +class PcapWriter(RawPcapWriter): + """A stream PCAP writer with more control than wrpcap()""" + pass + + +class PcapNgWriter(RawPcapNgWriter): + """A stream pcapng writer with more control than wrpcapng()""" + + def _get_time(self, + packet, # type: Union[bytes, Packet] + sec, # type: Optional[float] + usec # type: Optional[int] + ): + # type: (...) -> Tuple[float, int] if hasattr(packet, "time"): if sec is None: - sec = int(packet.time) - usec = int(round((packet.time - sec) * - (1000000000 if self.nano else 1000000))) + sec = float(packet.time) + if usec is None: usec = 0 - rawpkt = raw(packet) - caplen = len(rawpkt) if caplen is None else caplen + return sec, usec # type: ignore + +@conf.commands.register +def rderf(filename, count=-1): + # type: (Union[IO[bytes], str], int) -> PacketList + """Read a ERF file and return a packet list + + :param count: read only packets + """ + with ERFEthernetReader(filename) as fdesc: + return fdesc.read_all(count=count) + + +class ERFEthernetReader_metaclass(PcapReader_metaclass): + def __call__(cls, filename): + # type: (Union[IO[bytes], str]) -> Any + i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) # type: ignore + filename, fdesc = cls.open(filename) + try: + i.__init__(filename, fdesc) + return i + except (Scapy_Exception, EOFError): + pass + + if "alternative" in cls.__dict__: + cls = cls.__dict__["alternative"] + i = cls.__new__( + cls, + cls.__name__, + cls.__bases__, + cls.__dict__ # type: ignore + ) + try: + i.__init__(filename, fdesc) + return i + except (Scapy_Exception, EOFError): + pass + + raise Scapy_Exception("Not a supported capture file") + + @staticmethod + def open(fname # type: ignore + ): + # type: (...) -> Tuple[str, _ByteStream] + """Open (if necessary) filename""" + if isinstance(fname, str): + filename = fname + try: + with gzip.open(filename, "rb") as tmp: + tmp.read(1) + fdesc = gzip.open(filename, "rb") # type: _ByteStream + except IOError: + fdesc = open(filename, "rb") + + else: + fdesc = fname + filename = getattr(fdesc, "name", "No name") + return filename, fdesc + + +class ERFEthernetReader(PcapReader, + metaclass=ERFEthernetReader_metaclass): + + def __init__(self, filename, fdesc=None): # type: ignore + # type: (Union[IO[bytes], str], IO[bytes]) -> None + self.filename = filename # type: ignore + self.f = fdesc + self.power = Decimal(10) ** Decimal(-9) + + # time is in 64-bits Endace's format which can be see here: + # https://www.endace.com/erf-extensible-record-format-types.pdf + def _convert_erf_timestamp(self, t): + # type: (int) -> EDecimal + sec = t >> 32 + frac_sec = t & 0xffffffff + frac_sec *= 10**9 + frac_sec += (frac_sec & 0x80000000) << 1 + frac_sec >>= 32 + return EDecimal(sec + self.power * frac_sec) + + # The details of ERF Packet format can be see here: + # https://www.endace.com/erf-extensible-record-format-types.pdf + def read_packet(self, size=MTU, **kwargs): + # type: (int, **Any) -> Packet + + # General ERF Header have exactly 16 bytes + hdr = self.f.read(16) + if len(hdr) < 16: + raise EOFError + + # The timestamp is in little-endian byte-order. + time = struct.unpack('BBHHH', hdr[8:]) + # Check if the type != 0x02, type Ethernet + if type & 0x02 == 0: + raise Scapy_Exception("Invalid ERF Type (Not TYPE_ETH)") + + # If there are extended headers, ignore it because Packet object does + # not support it. Extended headers size is 8 bytes before the payload. + if type & 0x80: + _ = self.f.read(8) + s = self.f.read(rlen - 24) + else: + s = self.f.read(rlen - 16) + + # Ethernet has 2 bytes of padding containing `offset` and `pad`. Both + # of the fields are disregarded by Endace. + pb = s[2:size] + from scapy.layers.l2 import Ether + try: + p = Ether(pb, **kwargs) # type: Packet + except KeyboardInterrupt: + raise + except Exception: + if conf.debug_dissector: + from scapy.sendrecv import debug + debug.crashed_on = (Ether, s) + raise + if conf.raw_layer is None: + # conf.raw_layer is set on import + import scapy.packet # noqa: F401 + p = conf.raw_layer(s) + + p.time = self._convert_erf_timestamp(time) + p.wirelen = wlen + + return p + + +@conf.commands.register +def wrerf(filename, # type: Union[IO[bytes], str] + pkt, # type: _PacketIterable + *args, # type: Any + **kargs # type: Any + ): + # type: (...) -> None + """Write a list of packets to a ERF file + + :param filename: the name of the file to write packets to, or an open, + writable file-like object. The file descriptor will be + closed at the end of the call, so do not use an object you + do not want to close (e.g., running wrerf(sys.stdout, []) + in interactive mode will crash Scapy). + :param gz: set to 1 to save a gzipped capture + :param append: append packets to the capture file instead of + truncating it + :param sync: do not bufferize writes to the capture file + """ + with ERFEthernetWriter(filename, *args, **kargs) as fdesc: + fdesc.write(pkt) + + +class ERFEthernetWriter(PcapWriter): + """A stream ERF Ethernet writer with more control than wrerf()""" + + def __init__(self, + filename, # type: Union[IO[bytes], str] + gz=False, # type: bool + append=False, # type: bool + sync=False, # type: bool + ): + # type: (...) -> None + """ + :param filename: the name of the file to write packets to, or an open, + writable file-like object. + :param gz: compress the capture on the fly + :param append: append packets to the capture file instead of + truncating it + :param sync: do not bufferize writes to the capture file + """ + super(ERFEthernetWriter, self).__init__(filename, + gz=gz, + append=append, + sync=sync) + + def write(self, pkt): # type: ignore + # type: (_PacketIterable) -> None + """ + Writes a Packet, a SndRcvList object, or bytes to a ERF file. + + :param pkt: Packet(s) to write (one record for each Packet) + :type pkt: iterable[scapy.packet.Packet], scapy.packet.Packet + """ + # Import here to avoid circular dependency + from scapy.supersocket import IterSocket + for p in IterSocket(pkt).iter: + self.write_packet(p) + + def write_packet(self, pkt): # type: ignore + # type: (Packet) -> None + + if hasattr(pkt, "time"): + sec = int(pkt.time) + usec = int((int(round((pkt.time - sec) * 10**9)) << 32) / 10**9) + t = (sec << 32) + usec + else: + t = int(time.time()) << 32 + + # There are 16 bytes of headers + 2 bytes of padding before the packets + # payload. + rlen = len(pkt) + 18 + + if hasattr(pkt, "wirelen"): + wirelen = pkt.wirelen if wirelen is None: - if hasattr(packet, "wirelen"): - wirelen = packet.wirelen - if wirelen is None: - wirelen = caplen + wirelen = rlen + + self.f.write(struct.pack("BBHHHH", 2, 0, rlen, 0, wirelen, 0)) + self.f.write(bytes(pkt)) + self.f.flush() - RawPcapWriter._write_packet( - self, rawpkt, sec=sec, usec=usec, caplen=caplen, wirelen=wirelen) + def close(self): + # type: () -> Optional[Any] + return self.f.close() @conf.commands.register -def import_hexcap(): +def import_hexcap(input_string=None): + # type: (Optional[str]) -> bytes """Imports a tcpdump like hexadecimal view e.g: exported via hexdump() or tcpdump or wireshark's "export as hex" + + :param input_string: String containing the hexdump input to parse. If None, + read from standard input. """ re_extract_hexcap = re.compile(r"^((0x)?[0-9a-fA-F]{2,}[ :\t]{,3}|) *(([0-9a-fA-F]{2} {,2}){,16})") # noqa: E501 p = "" try: + if input_string: + input_function = StringIO(input_string).readline + else: + input_function = input while True: - line = input().strip() + line = input_function().strip() if not line: break try: - p += re_extract_hexcap.match(line).groups()[2] + p += re_extract_hexcap.match(line).groups()[2] # type: ignore except Exception: warning("Parsing error during hexcap") continue @@ -1560,6 +2925,7 @@ def import_hexcap(): @conf.commands.register def wireshark(pktlist, wait=False, **kwargs): + # type: (List[Packet], bool, **Any) -> Optional[Any] """ Runs Wireshark on a list of packets. @@ -1571,7 +2937,12 @@ def wireshark(pktlist, wait=False, **kwargs): @conf.commands.register -def tdecode(pktlist, args=None, **kwargs): +def tdecode( + pktlist, # type: Union[IO[bytes], None, str, _PacketIterable] + args=None, # type: Optional[List[str]] + **kwargs # type: Any +): + # type: (...) -> Any """ Run tshark on a list of packets. @@ -1585,27 +2956,40 @@ def tdecode(pktlist, args=None, **kwargs): def _guess_linktype_name(value): + # type: (int) -> str """Guess the DLT name from its value.""" - import scapy.data - return next( - k[4:] for k, v in six.iteritems(scapy.data.__dict__) - if k.startswith("DLT") and v == value - ) + from scapy.libs.winpcapy import pcap_datalink_val_to_name + return cast(bytes, pcap_datalink_val_to_name(value)).decode() def _guess_linktype_value(name): + # type: (str) -> int """Guess the value of a DLT name.""" - import scapy.data - if not name.startswith("DLT_"): - name = "DLT_" + name - return scapy.data.__dict__[name] + from scapy.libs.winpcapy import pcap_datalink_name_to_val + val = cast(int, pcap_datalink_name_to_val(name.encode())) + if val == -1: + warning("Unknown linktype: %s. Using EN10MB", name) + return DLT_EN10MB + return val @conf.commands.register -def tcpdump(pktlist=None, dump=False, getfd=False, args=None, - prog=None, getproc=False, quiet=False, use_tempfile=None, - read_stdin_opts=None, linktype=None, wait=True, - _suppress=False): +def tcpdump( + pktlist=None, # type: Union[IO[bytes], None, str, _PacketIterable] + dump=False, # type: bool + getfd=False, # type: bool + args=None, # type: Optional[List[str]] + flt=None, # type: Optional[str] + prog=None, # type: Optional[Any] + getproc=False, # type: bool + quiet=False, # type: bool + use_tempfile=None, # type: Optional[Any] + read_stdin_opts=None, # type: Optional[Any] + linktype=None, # type: Optional[Any] + wait=True, # type: bool + _suppress=False # type: bool +): + # type: (...) -> Any """Run tcpdump or tshark on a list of packets. When using ``tcpdump`` on OSX (``prog == conf.prog.tcpdump``), this uses a @@ -1630,7 +3014,7 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, Packet instances. Can also be a filename (as a string), an open file-like object that must be a file format readable by tshark (Pcap, PcapNg, etc.) or None (to sniff) - + :param flt: a filter to use with tcpdump :param dump: when set to True, returns a string instead of displaying it. :param getfd: when set to True, returns a file-like object to read data from tcpdump or tshark from. @@ -1696,14 +3080,12 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, "tcpdump is not available" ) prog = [conf.prog.tcpdump] - elif isinstance(prog, six.string_types): + elif isinstance(prog, str): prog = [prog] else: raise ValueError("prog must be a string") if linktype is not None: - # Tcpdump does not support integers in -y (yet) - # https://github.com/the-tcpdump-group/tcpdump/issues/758 if isinstance(linktype, int): # Guess name from value try: @@ -1732,6 +3114,28 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, # Make a copy of args args = list(args) + if flt is not None: + # Check the validity of the filter + if linktype is None and isinstance(pktlist, str): + # linktype is unknown but required. Read it from file + with PcapReader(pktlist) as rd: + if isinstance(rd, PcapNgReader): + # Get the linktype from the first packet + try: + _, metadata = rd._read_packet() + linktype = metadata.linktype + if OPENBSD and linktype == 228: + linktype = DLT_RAW + except EOFError: + raise ValueError( + "Cannot get linktype from a PcapNg packet." + ) + else: + linktype = rd.linktype + from scapy.arch.common import compile_filter + compile_filter(flt, linktype=linktype) + args.append(flt) + stdout = subprocess.PIPE if dump or getfd else None stderr = open(os.devnull) if quiet else None proc = None @@ -1745,6 +3149,9 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, if prog[0] == conf.prog.wireshark: # Start capturing immediately (-k) from stdin (-i -) read_stdin_opts = ["-ki", "-"] + elif prog[0] == conf.prog.tcpdump and not OPENBSD: + # Capture in packet-buffered mode (-U) from stdin (-r -) + read_stdin_opts = ["-U", "-r", "-"] else: read_stdin_opts = ["-r", "-"] else: @@ -1759,7 +3166,7 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, stdout=stdout, stderr=stderr, ) - elif isinstance(pktlist, six.string_types): + elif isinstance(pktlist, str): # file with ContextManagerSubprocess(prog[0], suppress=_suppress): proc = subprocess.Popen( @@ -1768,10 +3175,16 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, stderr=stderr, ) elif use_tempfile: - tmpfile = get_temp_file(autoext=".pcap", fd=True) + tmpfile = get_temp_file( # type: ignore + autoext=".pcap", + fd=True + ) # type: IO[bytes] try: - tmpfile.writelines(iter(lambda: pktlist.read(1048576), b"")) + tmpfile.writelines( + iter(lambda: pktlist.read(1048576), b"") # type: ignore + ) except AttributeError: + pktlist = cast("_PacketIterable", pktlist) wrpcap(tmpfile, pktlist, linktype=linktype) else: tmpfile.close() @@ -1782,31 +3195,48 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, stderr=stderr, ) else: - # pass the packet stream - with ContextManagerSubprocess(prog[0], suppress=_suppress): - proc = subprocess.Popen( - prog + read_stdin_opts + args, - stdin=subprocess.PIPE, - stdout=stdout, - stderr=stderr, - ) + try: + pktlist.fileno() # type: ignore + # pass the packet stream + with ContextManagerSubprocess(prog[0], suppress=_suppress): + proc = subprocess.Popen( + prog + read_stdin_opts + args, + stdin=pktlist, # type: ignore + stdout=stdout, + stderr=stderr, + ) + except (AttributeError, ValueError): + # write the packet stream to stdin + with ContextManagerSubprocess(prog[0], suppress=_suppress): + proc = subprocess.Popen( + prog + read_stdin_opts + args, + stdin=subprocess.PIPE, + stdout=stdout, + stderr=stderr, + ) if proc is None: # An error has occurred return - try: - proc.stdin.writelines(iter(lambda: pktlist.read(1048576), b"")) - except AttributeError: - wrpcap(proc.stdin, pktlist, linktype=linktype) - except UnboundLocalError: - # The error was handled by ContextManagerSubprocess - pass - else: - proc.stdin.close() + try: + proc.stdin.writelines( # type: ignore + iter(lambda: pktlist.read(1048576), b"") # type: ignore + ) + except AttributeError: + wrpcap(proc.stdin, pktlist, linktype=linktype) # type: ignore + except UnboundLocalError: + # The error was handled by ContextManagerSubprocess + pass + else: + proc.stdin.close() # type: ignore if proc is None: # An error has occurred return if dump: - return b"".join(iter(lambda: proc.stdout.read(1048576), b"")) + data = b"".join( + iter(lambda: proc.stdout.read(1048576), b"") # type: ignore + ) + proc.terminate() + return data if getproc: return proc if getfd: @@ -1817,30 +3247,28 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, @conf.commands.register def hexedit(pktlist): + # type: (_PacketIterable) -> PacketList """Run hexedit on a list of packets, then return the edited packets.""" f = get_temp_file() wrpcap(f, pktlist) with ContextManagerSubprocess(conf.prog.hexedit): subprocess.call([conf.prog.hexedit, f]) - pktlist = rdpcap(f) + rpktlist = rdpcap(f) os.unlink(f) - return pktlist + return rpktlist def get_terminal_width(): + # type: () -> Optional[int] """Get terminal width (number of characters) if in a window. Notice: this will try several methods in order to support as many terminals and OS as possible. """ - # Let's first try using the official API - # (Python 3.3+) - if not six.PY2: - import shutil - sizex = shutil.get_terminal_size(fallback=(0, 0))[0] - if sizex != 0: - return sizex - # Backups / Python 2.7 + sizex = shutil.get_terminal_size(fallback=(0, 0))[0] + if sizex != 0: + return sizex + # Backups if WINDOWS: from ctypes import windll, create_string_buffer # http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/ @@ -1853,44 +3281,79 @@ def get_terminal_width(): sizex = right - left + 1 # sizey = bottom - top + 1 return sizex - return None - else: - # We have various methods - sizex = None - # COLUMNS is set on some terminals - try: - sizex = int(os.environ['COLUMNS']) - except Exception: - pass - if sizex: - return sizex - # We can query TIOCGWINSZ - try: - import fcntl - import termios - s = struct.pack('HHHH', 0, 0, 0, 0) - x = fcntl.ioctl(1, termios.TIOCGWINSZ, s) - sizex = struct.unpack('HHHH', x)[1] - except IOError: - pass return sizex + # We have various methods + # COLUMNS is set on some terminals + try: + sizex = int(os.environ['COLUMNS']) + except Exception: + pass + if sizex: + return sizex + # We can query TIOCGWINSZ + try: + import fcntl + import termios + s = struct.pack('HHHH', 0, 0, 0, 0) + x = fcntl.ioctl(1, termios.TIOCGWINSZ, s) + sizex = struct.unpack('HHHH', x)[1] + except (IOError, ModuleNotFoundError): + # If everything failed, return default terminal size + sizex = 79 + return sizex + + +def pretty_list(rtlst, # type: List[Tuple[Union[str, List[str]], ...]] + header, # type: List[Tuple[str, ...]] + sortBy=0, # type: Optional[int] + borders=False, # type: bool + ): + # type: (...) -> str + """ + Pretty list to fit the terminal, and add header. - -def pretty_list(rtlst, header, sortBy=0, borders=False): - """Pretty list to fit the terminal, and add header""" + :param rtlst: a list of tuples. each tuple contains a value which can + be either a string or a list of string. + :param sortBy: the column id (starting with 0) which will be used for + ordering + :param borders: whether to put borders on the table or not + """ if borders: _space = "|" else: _space = " " + cols = len(header[0]) # Windows has a fat terminal border - _spacelen = len(_space) * (len(header) - 1) + (10 if WINDOWS else 0) + _spacelen = len(_space) * (cols - 1) + int(WINDOWS) _croped = False - # Sort correctly - rtlst.sort(key=lambda x: x[sortBy]) + if sortBy is not None: + # Sort correctly + rtlst.sort(key=lambda x: x[sortBy]) + # Resolve multi-values + for i, line in enumerate(rtlst): + ids = [] # type: List[int] + values = [] # type: List[Union[str, List[str]]] + for j, val in enumerate(line): + if isinstance(val, list): + ids.append(j) + values.append(val or " ") + if values: + del rtlst[i] + k = 0 + for ex_vals in zip_longest(*values, fillvalue=" "): + if k: + extra_line = [" "] * cols + else: + extra_line = list(line) # type: ignore + for j, h in enumerate(ids): + extra_line[h] = ex_vals[j] + rtlst.insert(i + k, tuple(extra_line)) + k += 1 + rtslst = cast(List[Tuple[str, ...]], rtlst) # Append tag - rtlst = header + rtlst + rtslst = header + rtslst # Detect column's width - colwidth = [max([len(y) for y in x]) for x in zip(*rtlst)] + colwidth = [max(len(y) for y in x) for x in zip(*rtslst)] # Make text fit in box (if required) width = get_terminal_width() if conf.auto_crop_tables and width: @@ -1901,13 +3364,13 @@ def pretty_list(rtlst, header, sortBy=0, borders=False): # Get the longest row i = colwidth.index(max(colwidth)) # Get all elements of this row - row = [len(x[i]) for x in rtlst] + row = [len(x[i]) for x in rtslst] # Get biggest element of this row: biggest of the array j = row.index(max(row)) # Re-build column tuple with the edited element - t = list(rtlst[j]) + t = list(rtslst[j]) t[i] = t[i][:-2] + "_" - rtlst[j] = tuple(t) + rtslst[j] = tuple(t) # Update max size row[j] = len(t[i]) colwidth[i] = max(row) @@ -1917,21 +3380,42 @@ def pretty_list(rtlst, header, sortBy=0, borders=False): fmt = _space.join(["%%-%ds" % x for x in colwidth]) # Append separation line if needed if borders: - rtlst.insert(1, tuple("-" * x for x in colwidth)) + rtslst.insert(1, tuple("-" * x for x in colwidth)) # Compile - rt = "\n".join(((fmt % x).strip() for x in rtlst)) - return rt + return "\n".join(fmt % x for x in rtslst) -def __make_table(yfmtfunc, fmtfunc, endline, data, fxyz, sortx=None, sorty=None, seplinefunc=None): # noqa: E501 +def human_size(x, fmt=".1f"): + # type: (int, str) -> str + """ + Convert a size in octets to a human string representation + """ + units = ['K', 'M', 'G', 'T', 'P', 'E'] + if not x: + return "0B" + i = int(math.log(x, 2**10)) + if i and i < len(units): + return format(x / 2**(10 * i), fmt) + units[i - 1] + return str(x) + "B" + + +def __make_table( + yfmtfunc, # type: Callable[[int], str] + fmtfunc, # type: Callable[[int], str] + endline, # type: str + data, # type: List[Tuple[Packet, Packet]] + fxyz, # type: Callable[[Packet, Packet], Tuple[Any, Any, Any]] + sortx=None, # type: Optional[Callable[[str], Tuple[Any, ...]]] + sorty=None, # type: Optional[Callable[[str], Tuple[Any, ...]]] + seplinefunc=None, # type: Optional[Callable[[int, List[int]], str]] + dump=False # type: bool +): + # type: (...) -> Optional[str] """Core function of the make_table suite, which generates the table""" - vx = {} - vy = {} - vz = {} - vxf = {} - - # Python 2 backward compatibility - fxyz = lambda_tuple_converter(fxyz) + vx = {} # type: Dict[str, int] + vy = {} # type: Dict[str, Optional[int]] + vz = {} # type: Dict[Tuple[str, str], str] + vxf = {} # type: Dict[str, str] tmp_len = 0 for e in data: @@ -1964,39 +3448,73 @@ def __make_table(yfmtfunc, fmtfunc, endline, data, fxyz, sortx=None, sorty=None, except Exception: vyk.sort() + s = "" if seplinefunc: sepline = seplinefunc(tmp_len, [vx[x] for x in vxk]) - print(sepline) + s += sepline + "\n" fmt = yfmtfunc(tmp_len) - print(fmt % "", end=' ') + s += fmt % "" + s += ' ' for x in vxk: vxf[x] = fmtfunc(vx[x]) - print(vxf[x] % x, end=' ') - print(endline) + s += vxf[x] % x + s += ' ' + s += endline + "\n" if seplinefunc: - print(sepline) + s += sepline + "\n" for y in vyk: - print(fmt % y, end=' ') + s += fmt % y + s += ' ' for x in vxk: - print(vxf[x] % vz.get((x, y), "-"), end=' ') - print(endline) + s += vxf[x] % vz.get((x, y), "-") + s += ' ' + s += endline + "\n" if seplinefunc: - print(sepline) + s += sepline + "\n" + + if dump: + return s + else: + print(s, end="") + return None def make_table(*args, **kargs): - __make_table(lambda l: "%%-%is" % l, lambda l: "%%-%is" % l, "", *args, **kargs) # noqa: E501 + # type: (*Any, **Any) -> Optional[Any] + return __make_table( + lambda l: "%%-%is" % l, + lambda l: "%%-%is" % l, + "", + *args, + **kargs + ) def make_lined_table(*args, **kargs): - __make_table(lambda l: "%%-%is |" % l, lambda l: "%%-%is |" % l, "", - seplinefunc=lambda a, x: "+".join('-' * (y + 2) for y in [a - 1] + x + [-2]), # noqa: E501 - *args, **kargs) + # type: (*Any, **Any) -> Optional[str] + return __make_table( # type: ignore + lambda l: "%%-%is |" % l, + lambda l: "%%-%is |" % l, + "", + *args, + seplinefunc=lambda a, x: "+".join( + '-' * (y + 2) for y in [a - 1] + x + [-2] + ), + **kargs + ) def make_tex_table(*args, **kargs): - __make_table(lambda l: "%s", lambda l: "& %s", "\\\\", seplinefunc=lambda a, x: "\\hline", *args, **kargs) # noqa: E501 + # type: (*Any, **Any) -> Optional[str] + return __make_table( # type: ignore + lambda l: "%s", + lambda l: "& %s", + "\\\\", + *args, + seplinefunc=lambda a, x: "\\hline", + **kargs + ) #################### # WHOIS CLIENT # @@ -2004,6 +3522,7 @@ def make_tex_table(*args, **kargs): def whois(ip_address): + # type: (str) -> bytes """Whois client for Python""" whois_ip = str(ip_address) try: @@ -2031,30 +3550,638 @@ def whois(ip_address): break return b"\n".join(lines[3:]) +#################### +# CLI utils # +#################### + + +class _CLIUtilMetaclass(type): + class TYPE(enum.Enum): + COMMAND = 0 + OUTPUT = 1 + COMPLETE = 2 + + def __new__(cls, # type: Type[_CLIUtilMetaclass] + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CLIUtil] + dct["commands"] = { + x.__name__: x + for x in dct.values() + if getattr(x, "cliutil_type", None) == _CLIUtilMetaclass.TYPE.COMMAND + } + dct["commands_output"] = { + x.cliutil_ref.__name__: x + for x in dct.values() + if getattr(x, "cliutil_type", None) == _CLIUtilMetaclass.TYPE.OUTPUT + } + dct["commands_complete"] = { + x.cliutil_ref.__name__: x + for x in dct.values() + if getattr(x, "cliutil_type", None) == _CLIUtilMetaclass.TYPE.COMPLETE + } + newcls = cast(Type['CLIUtil'], type.__new__(cls, name, bases, dct)) + return newcls + + +class CLIUtil(metaclass=_CLIUtilMetaclass): + """ + Provides a Util class to easily create simple CLI tools in Scapy, + that can still be used as an API. + + Doc: + - override the ps1() function + - register commands with the @CLIUtil.addcomment decorator + - call the loop() function when ready + """ + + def _depcheck(self) -> None: + """ + Check that all dependencies are installed + """ + try: + import prompt_toolkit # noqa: F401 + except ImportError: + # okay we lie but prompt_toolkit is a dependency... + raise ImportError("You need to have IPython installed to use the CLI") + + # Okay let's do nice code + commands: Dict[str, Callable[..., Any]] = {} + # print output of command + commands_output: Dict[str, Callable[..., str]] = {} + # provides completion to command + commands_complete: Dict[str, Callable[..., List[str]]] = {} + + def __init__(self, cli: bool = True, debug: bool = False) -> None: + """ + DEV: overwrite + """ + if cli: + self._depcheck() + self.loop(debug=debug) + + @staticmethod + def _inspectkwargs(func: DecoratorCallable) -> None: + """ + Internal function to parse arguments from the kwargs of the functions + """ + func._flagnames = [ # type: ignore + x.name for x in + inspect.signature(func).parameters.values() + if x.kind == inspect.Parameter.KEYWORD_ONLY + ] + func._flags = [ # type: ignore + ("-%s" % x) if len(x) == 1 else ("--%s" % x) + for x in func._flagnames # type: ignore + ] + + @staticmethod + def _parsekwargs( + func: DecoratorCallable, + args: List[str] + ) -> Tuple[List[str], Dict[str, Literal[True]]]: + """ + Internal function to parse CLI arguments of a function. + """ + kwargs: Dict[str, Literal[True]] = {} + if func._flags: # type: ignore + i = 0 + for arg in args: + if arg in func._flags: # type: ignore + i += 1 + kwargs[func._flagnames[func._flags.index(arg)]] = True # type: ignore # noqa: E501 + continue + break + args = args[i:] + return args, kwargs + + @classmethod + def _parseallargs( + cls, + func: DecoratorCallable, + cmd: str, args: List[str] + ) -> Tuple[List[str], Dict[str, Literal[True]], Dict[str, Literal[True]]]: + """ + Internal function to parse CLI arguments of both the function + and its output function. + """ + args, kwargs = cls._parsekwargs(func, args) + outkwargs: Dict[str, Literal[True]] = {} + if cmd in cls.commands_output: + args, outkwargs = cls._parsekwargs(cls.commands_output[cmd], args) + return args, kwargs, outkwargs + + @classmethod + def addcommand( + cls, + mono: bool = False, + globsupport: bool = False, + ) -> Callable[[DecoratorCallable], DecoratorCallable]: + """ + Decorator to register a command + + :param mono: if True, the command takes a single argument even + if there are spaces. + """ + def func(cmd: DecoratorCallable) -> DecoratorCallable: + cmd.cliutil_type = _CLIUtilMetaclass.TYPE.COMMAND # type: ignore + cmd._mono = mono # type: ignore + cmd._globsupport = globsupport # type: ignore + cls._inspectkwargs(cmd) + if cmd._globsupport and not cmd._mono: # type: ignore + raise ValueError("Cannot use globsupport without mono.") + return cmd + return func + + @classmethod + def addoutput(cls, cmd: DecoratorCallable) -> Callable[[DecoratorCallable], DecoratorCallable]: # noqa: E501 + """ + Decorator to register a command output processor + """ + def func(processor: DecoratorCallable) -> DecoratorCallable: + processor.cliutil_type = _CLIUtilMetaclass.TYPE.OUTPUT # type: ignore + processor.cliutil_ref = cmd # type: ignore + cls._inspectkwargs(processor) + return processor + return func + + @classmethod + def addcomplete( + cls, + cmd: DecoratorCallable, + ) -> Callable[[DecoratorCallable], DecoratorCallable]: + """ + Decorator to register a command completor + """ + def func(processor: DecoratorCallable) -> DecoratorCallable: + processor.cliutil_type = _CLIUtilMetaclass.TYPE.COMPLETE # type: ignore + processor.cliutil_ref = cmd # type: ignore + processor._mono = cmd._mono # type: ignore + return processor + return func + + def ps1(self) -> str: + """ + Return the PS1 of the shell + """ + return "> " + + def close(self) -> None: + """ + Function called on exiting + """ + print("Exited") + + def help(self, cmd: Optional[str] = None) -> None: + """ + Return the help related to this CLI util + """ + def _args(func: Any) -> str: + flags = func._flags.copy() + if func.__name__ in self.commands_output: + flags += self.commands_output[func.__name__]._flags # type: ignore + return " %s%s" % ( + ( + "%s " % " ".join("[%s]" % x for x in flags) + if flags else "" + ), + " ".join( + "<%s%s>" % ( + x.name, + "?" if + (x.default is None or x.default != inspect.Parameter.empty) + else "" + ) + for x in list(inspect.signature(func).parameters.values())[1:] + if x.name not in func._flagnames and x.name[0] != "_" + ) + ) + + if cmd: + if cmd not in self.commands: + print("Unknown command '%s'" % cmd) + return + # help for one command + func = self.commands[cmd] + print("%s%s: %s" % ( + cmd, + _args(func), + func.__doc__ and func.__doc__.strip() + )) + else: + header = "│ %s - Help │" % self.__class__.__name__ + print("┌" + "─" * (len(header) - 2) + "â”") + print(header) + print("â””" + "─" * (len(header) - 2) + "┘") + print( + pretty_list( + [ + ( + cmd, + _args(func), + func.__doc__ and func.__doc__.strip().split("\n")[0] or "" + ) + for cmd, func in self.commands.items() + ], + [("Command", "Arguments", "Description")] + ) + ) + + def _split_cmd(self, cmd: str) -> Tuple[List[str], List[int]]: + """ + Split the command in multiple arguments + """ + quoted = None + queue = [""] + offsets = [0] + for i, c in enumerate(cmd): + if c == "'" or c == '"': + # This is a quote. + if quoted is not None and quoted == c: + # We are closing the last quote + quoted = None + elif quoted: + queue[-1] += c + else: + quoted = c + elif c == " ": + # This is a space. + if quoted is not None: + # We're in a quote, append it + queue[-1] += c + elif queue[-1]: + # Not in a quote, this splits the argument. + queue += [""] + offsets.append(i) + else: + # Padding space, advance offset + offsets[-1] += 1 + else: + # This is a char + queue[-1] += c + return queue, offsets + + def _completer(self) -> 'prompt_toolkit.completion.Completer': + """ + Returns a prompt_toolkit custom completer + """ + from prompt_toolkit.completion import Completer, Completion + + class CLICompleter(Completer): + def get_completions(cmpl, document, complete_event): # type: ignore + if not complete_event.completion_requested: + # Only activate when the user does + return + parts, offsets = self._split_cmd(document.text) + cmd = parts[0].lower() + if cmd not in self.commands: + # We are trying to complete the command + for possible_cmd in (x for x in self.commands if x.startswith(cmd)): + yield Completion(possible_cmd, start_position=-len(cmd)) + else: + # We are trying to complete the command content + if len(parts) == 1: + return + args, _, _ = self._parseallargs(self.commands[cmd], cmd, parts[1:]) + if cmd in self.commands_complete: + completer = self.commands_complete[cmd] + # If the completion is 'mono', it's a single argument with + # spaces. Else we pass the list of arguments to complete, + # and we only complete the last argument. + if completer._mono: # type: ignore + arg = " ".join(args) + completions = completer(self, arg) + startpos = offsets[1] + else: + completions = completer(self, args) + startpos = offsets[-1] + + # For each possible completion + for possible_arg in completions: + # If there's a space in the completion, and we're + # not in mono mode, add quotes. + if " " in possible_arg and not completer._mono: # type: ignore # noqa: E501 + possible_arg = '"%s"' % possible_arg + + yield Completion( + possible_arg, + start_position=startpos - len(document.text) + 1 + ) + return + return CLICompleter() + + def loop(self, debug: int = 0) -> None: + """ + Main command handling loop + """ + from prompt_toolkit import PromptSession + session = PromptSession(completer=self._completer()) + + while True: + try: + cmd = session.prompt(self.ps1()).strip() + except KeyboardInterrupt: + continue + except EOFError: + self.close() + break + parts, _ = self._split_cmd(cmd) + args = parts[1:] + cmd = parts[0].strip().lower() + if not cmd: + continue + if cmd in ["help", "h", "?"]: + self.help(" ".join(args)) + continue + if cmd in "exit": + break + if cmd not in self.commands: + print("Unknown command. Type help or ?") + else: + # check the number of arguments + func = self.commands[cmd] + args, kwargs, outkwargs = self._parseallargs(func, cmd, args) + if func._mono: # type: ignore + args = [" ".join(args)] + # if globsupport is set, we might need to do several calls + if func._globsupport and "*" in args[0]: # type: ignore + if args[0].count("*") > 1: + print("More than 1 glob star (*) is currently unsupported.") + continue + before, after = args[0].split("*", 1) + reg = re.compile(re.escape(before) + r".*" + after) + calls = [ + [x] for x in + self.commands_complete[cmd](self, before) + if reg.match(x) + ] + else: + calls = [args] + else: + calls = [args] + # now iterate if required, call the function and print its output + res = None + for args in calls: + try: + res = func(self, *args, **kwargs) + except TypeError as ex: + print("Bad number of arguments !") + if debug: + traceback.print_exception(ex) + self.help(cmd=cmd) + continue + except Exception as ex: + print("Command failed with error: %s" % ex) + if debug: + traceback.print_exception(ex) + try: + if res and cmd in self.commands_output: + self.commands_output[cmd](self, res, **outkwargs) + except Exception as ex: + print("Output processor failed with error: %s" % ex) + + +def AutoArgparse( + func: DecoratorCallable, + _parseonly: bool = False, +) -> Optional[Tuple[List[str], List[str]]]: + """ + Generate an Argparse call from a function, then call this function. + + Notes: + + - for the arguments to have a description, the sphinx docstring format + must be used. See + https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html + - the arguments must be typed in Python (we ignore Sphinx-specific types) + untyped arguments are ignored. + - only types that would be supported by argparse are supported. The others + are omitted. + """ + argsdoc = {} + if func.__doc__: + # Sphinx doc format parser + m = re.match( + r"((?:.|\n)*?)(\n\s*:(?:param|type|raises|return|rtype)(?:.|\n)*)", + func.__doc__.strip(), + ) + if not m: + desc = func.__doc__.strip() + else: + desc = m.group(1) + sphinxargs = re.findall( + r"\s*:(param|type|raises|return|rtype)\s*([^:]*):(.*)", + m.group(2), + ) + for argtype, argparam, argdesc in sphinxargs: + argparam = argparam.strip() + argdesc = argdesc.strip() + if argtype == "param": + if not argparam: + raise ValueError(":param: without a name !") + argsdoc[argparam] = argdesc + else: + desc = "" + + # Process the parameters + positional = [] + noargument = [] + hexarguments = [] + parameters = {} + for param in inspect.signature(func).parameters.values(): + if not param.annotation: + continue + noarg = False + parname = param.name.replace("_", "-") + paramkwargs: Dict[str, Any] = {} + if param.annotation is bool: + if param.default is True: + parname = "no-" + parname + paramkwargs["action"] = "store_false" + else: + paramkwargs["action"] = "store_true" + noarg = True + elif param.annotation is bytes: + paramkwargs["type"] = str + hexarguments.append(parname) + elif param.annotation in [str, int, float]: + paramkwargs["type"] = param.annotation + elif ( + isinstance(param.annotation, type) and + issubclass(param.annotation, enum.Enum) + ): + paramkwargs["type"] = param.annotation + paramkwargs["choices"] = list(param.annotation) + else: + continue + if param.default != inspect.Parameter.empty: + if param.kind == inspect.Parameter.POSITIONAL_ONLY: + positional.append(param.name) + paramkwargs["nargs"] = '?' + else: + parname = "--" + parname + paramkwargs["default"] = param.default + else: + positional.append(param.name) + if param.kind == inspect.Parameter.VAR_POSITIONAL: + paramkwargs["action"] = "append" + if param.name in argsdoc: + paramkwargs["help"] = argsdoc[param.name] + if param.annotation is bytes: + paramkwargs["help"] = "(hex) " + paramkwargs["help"] + elif param.annotation is bool: + paramkwargs["help"] = "(flag) " + paramkwargs["help"] + else: + paramkwargs["help"] = ( + "(%s) " % param.annotation.__name__ + paramkwargs["help"] + ) + # Add to the parameter list + parameters[parname] = paramkwargs + if noarg: + noargument.append(parname) + + if _parseonly: + # An internal mode used to generate bash autocompletion, do it then exit. + return ( + [x for x in parameters if x not in positional] + ["--help"], + [x for x in noargument if x not in positional] + ["--help"], + ) + + # Now build the argparse.ArgumentParser + parser = argparse.ArgumentParser( + prog=func.__name__, + description=desc, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # Add parameters to parser + for parname, paramkwargs in parameters.items(): + parser.add_argument(parname, **paramkwargs) + + # Now parse the sys.argv parameters + params = vars(parser.parse_args()) + + # Convert hex parameters if provided + for p in hexarguments: + if params[p] is not None: + try: + params[p] = bytes.fromhex(params[p]) + except ValueError: + print( + conf.color_theme.fail( + "ERROR: the value of parameter %s " + "'%s' is not valid hexadecimal !" % (p, params[p]) + ) + ) + return None + + # Act as in interactive mode + conf.logLevel = 20 + from scapy.themes import DefaultTheme + conf.color_theme = DefaultTheme() + # And call the function + try: + func( + *[params.pop(x) for x in positional], + **{ + (k[3:] if k.startswith("no_") else k): v + for k, v in params.items() + } + ) + except AssertionError as ex: + print(conf.color_theme.fail("ERROR: " + str(ex))) + parser.print_help() + return None + + ####################### # PERIODIC SENDER # ####################### class PeriodicSenderThread(threading.Thread): - def __init__(self, sock, pkt, interval=0.5): + def __init__(self, sock, pkt, interval=0.5, ignore_exceptions=True): + # type: (Any, _PacketIterable, float, bool) -> None """ Thread to send packets periodically Args: sock: socket where packet is sent periodically - pkt: packet to send + pkt: packet or list of packets to send interval: interval between two packets """ - self._pkt = pkt + if not isinstance(pkt, list): + self._pkts = [cast("Packet", pkt)] # type: _PacketIterable + else: + self._pkts = pkt self._socket = sock self._stopped = threading.Event() + self._enabled = threading.Event() + self._enabled.set() self._interval = interval + self._ignore_exceptions = ignore_exceptions threading.Thread.__init__(self) + def enable(self): + # type: () -> None + self._enabled.set() + + def disable(self): + # type: () -> None + self._enabled.clear() + def run(self): - while not self._stopped.is_set(): - self._socket.send(self._pkt) - time.sleep(self._interval) + # type: () -> None + while not self._stopped.is_set() and not self._socket.closed: + for p in self._pkts: + try: + if self._enabled.is_set(): + self._socket.send(p) + except (OSError, TimeoutError) as e: + if self._ignore_exceptions: + return + else: + raise e + self._stopped.wait(timeout=self._interval) + if self._stopped.is_set() or self._socket.closed: + break def stop(self): + # type: () -> None self._stopped.set() + self.join(self._interval * 2) + + +class SingleConversationSocket(object): + def __init__(self, o): + # type: (Any) -> None + self._inner = o + self._tx_mutex = threading.RLock() + + @property + def __dict__(self): # type: ignore + return self._inner.__dict__ + + def __getattr__(self, name): + # type: (str) -> Any + return getattr(self._inner, name) + + def sr1(self, *args, **kargs): + # type: (*Any, **Any) -> Any + with self._tx_mutex: + return self._inner.sr1(*args, **kargs) + + def sr(self, *args, **kargs): + # type: (*Any, **Any) -> Any + with self._tx_mutex: + return self._inner.sr(*args, **kargs) + + def send(self, x): + # type: (Packet) -> Any + with self._tx_mutex: + try: + return self._inner.send(x) + except (ConnectionError, OSError) as e: + self._inner.close() + raise e diff --git a/scapy/utils6.py b/scapy/utils6.py index c1c0c83ef13..0ee7ee89886 100644 --- a/scapy/utils6.py +++ b/scapy/utils6.py @@ -1,38 +1,49 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# This program is published under a GPLv2 license - # Copyright (C) 2005 Guillaume Valadon # Arnaud Ebalard """ Utility functions for IPv6. """ -from __future__ import absolute_import -import operator -import random import socket import struct import time -import re from scapy.config import conf -import scapy.consts -from scapy.base_classes import Gen +from scapy.base_classes import Net from scapy.data import IPV6_ADDR_GLOBAL, IPV6_ADDR_LINKLOCAL, \ IPV6_ADDR_SITELOCAL, IPV6_ADDR_LOOPBACK, IPV6_ADDR_UNICAST,\ IPV6_ADDR_MULTICAST, IPV6_ADDR_6TO4, IPV6_ADDR_UNSPECIFIED -from scapy.utils import strxor +from scapy.utils import ( + strxor, + stror, + strand, +) from scapy.compat import orb, chb from scapy.pton_ntop import inet_pton, inet_ntop -from scapy.volatile import RandMAC +from scapy.volatile import RandMAC, RandBin from scapy.error import warning, Scapy_Exception from functools import reduce, cmp_to_key -from scapy.modules.six.moves import range, zip + +from typing import ( + Iterator, + List, + Optional, + Tuple, + Union, + cast, +) -def construct_source_candidate_set(addr, plen, laddr): +def construct_source_candidate_set( + addr, # type: str + plen, # type: int + laddr # type: Iterator[Tuple[str, int, str]] +): + # type: (...) -> List[str] """ Given all addresses assigned to a specific interface ('laddr' parameter), this function returns the "candidate set" associated with 'addr/plen'. @@ -45,6 +56,7 @@ def construct_source_candidate_set(addr, plen, laddr): with some specific destination that uses this prefix. """ def cset_sort(x, y): + # type: (str, str) -> int x_global = 0 if in6_isgladdr(x): x_global = 1 @@ -59,7 +71,7 @@ def cset_sort(x, y): return -1 return -res - cset = [] + cset = iter([]) # type: Iterator[Tuple[str, int, str]] if in6_isgladdr(addr) or in6_isuladdr(addr): cset = (x for x in laddr if x[1] == IPV6_ADDR_GLOBAL) elif in6_islladdr(addr): @@ -68,7 +80,7 @@ def cset_sort(x, y): cset = (x for x in laddr if x[1] == IPV6_ADDR_SITELOCAL) elif in6_ismaddr(addr): if in6_ismnladdr(addr): - cset = [('::1', 16, scapy.consts.LOOPBACK_INTERFACE)] + cset = (x for x in [('::1', 16, conf.loopback_name)]) elif in6_ismgladdr(addr): cset = (x for x in laddr if x[1] == IPV6_ADDR_GLOBAL) elif in6_ismlladdr(addr): @@ -77,13 +89,16 @@ def cset_sort(x, y): cset = (x for x in laddr if x[1] == IPV6_ADDR_SITELOCAL) elif addr == '::' and plen == 0: cset = (x for x in laddr if x[1] == IPV6_ADDR_GLOBAL) - cset = [x[0] for x in cset] + elif addr == '::1': + cset = (x for x in laddr if x[1] == IPV6_ADDR_LOOPBACK) + addrs = [x[0] for x in cset] # TODO convert the cmd use into a key - cset.sort(key=cmp_to_key(cset_sort)) # Sort with global addresses first - return cset + addrs.sort(key=cmp_to_key(cset_sort)) # Sort with global addresses first + return addrs def get_source_addr_from_candidate_set(dst, candidate_set): + # type: (str, List[str]) -> str """ This function implement a limited version of source address selection algorithm defined in section 5 of RFC 3484. The format is very different @@ -92,6 +107,7 @@ def get_source_addr_from_candidate_set(dst, candidate_set): """ def scope_cmp(a, b): + # type: (str, str) -> int """ Given two addresses, returns -1, 0 or 1 based on comparison of their scope @@ -117,6 +133,7 @@ def scope_cmp(a, b): return -1 def rfc3484_cmp(source_a, source_b): + # type: (str, str) -> int """ The function implements a limited version of the rules from Source Address selection algorithm defined section of RFC 3484. @@ -158,7 +175,7 @@ def rfc3484_cmp(source_a, source_b): if not candidate_set: # Should not happen - return None + return "" candidate_set.sort(key=cmp_to_key(rfc3484_cmp), reverse=True) @@ -169,6 +186,7 @@ def rfc3484_cmp(source_a, source_b): # there are many others like that. # TODO : integrate Unique Local Addresses def in6_getAddrType(addr): + # type: (str) -> int naddr = inet_pton(socket.AF_INET6, addr) paddr = inet_ntop(socket.AF_INET6, naddr) # normalize addrType = 0 @@ -201,6 +219,7 @@ def in6_getAddrType(addr): def in6_mactoifaceid(mac, ulbit=None): + # type: (str, Optional[int]) -> str """ Compute the interface ID in modified EUI-64 format associated to the Ethernet address provided as input. @@ -209,20 +228,21 @@ def in6_mactoifaceid(mac, ulbit=None): to a specific value by using optional 'ulbit' parameter. """ if len(mac) != 17: - return None + raise ValueError("Invalid MAC") m = "".join(mac.split(':')) if len(m) != 12: - return None + raise ValueError("Invalid MAC") first = int(m[0:2], 16) if ulbit is None or not (ulbit == 0 or ulbit == 1): - ulbit = [1, '-', 0][first & 0x02] + ulbit = [1, 0, 0][first & 0x02] ulbit *= 2 - first = "%.02x" % ((first & 0xFD) | ulbit) - eui64 = first + m[2:4] + ":" + m[4:6] + "FF:FE" + m[6:8] + ":" + m[8:12] + first_b = "%.02x" % ((first & 0xFD) | ulbit) + eui64 = first_b + m[2:4] + ":" + m[4:6] + "FF:FE" + m[6:8] + ":" + m[8:12] return eui64.upper() -def in6_ifaceidtomac(ifaceid): +def in6_ifaceidtomac(ifaceid_s): + # type: (str) -> Optional[str] """ Extract the mac address from provided iface ID. Iface ID is provided in printable format ("XXXX:XXFF:FEXX:XXXX", eventually compressed). None @@ -230,9 +250,10 @@ def in6_ifaceidtomac(ifaceid): """ try: # Set ifaceid to a binary form - ifaceid = inet_pton(socket.AF_INET6, "::" + ifaceid)[8:16] + ifaceid = inet_pton(socket.AF_INET6, "::" + ifaceid_s)[8:16] except Exception: return None + if ifaceid[3:5] != b'\xff\xfe': # Check for burned-in MAC address return None @@ -249,6 +270,7 @@ def in6_ifaceidtomac(ifaceid): def in6_addrtomac(addr): + # type: (str) -> Optional[str] """ Extract the mac address from provided address. None is returned on error. @@ -260,6 +282,7 @@ def in6_addrtomac(addr): def in6_addrtovendor(addr): + # type: (str) -> Optional[str] """ Extract the MAC address from a modified EUI-64 constructed IPv6 address provided and use the IANA oui.txt file to get the vendor. @@ -280,6 +303,7 @@ def in6_addrtovendor(addr): def in6_getLinkScopedMcastAddr(addr, grpid=None, scope=2): + # type: (str, Optional[Union[bytes, str, int]], int) -> Optional[str] """ Generate a Link-Scoped Multicast Address as described in RFC 4489. Returned value is in printable notation. @@ -305,68 +329,81 @@ def in6_getLinkScopedMcastAddr(addr, grpid=None, scope=2): try: if not in6_islladdr(addr): return None - addr = inet_pton(socket.AF_INET6, addr) + baddr = inet_pton(socket.AF_INET6, addr) except Exception: warning("in6_getLinkScopedMcastPrefix(): Invalid address provided") return None - iid = addr[8:] + iid = baddr[8:] if grpid is None: - grpid = b'\x00\x00\x00\x00' + b_grpid = b'\x00\x00\x00\x00' else: - if isinstance(grpid, (bytes, str)): - if len(grpid) == 8: - try: - grpid = int(grpid, 16) & 0xffffffff - except Exception: - warning("in6_getLinkScopedMcastPrefix(): Invalid group id provided") # noqa: E501 - return None - elif len(grpid) == 4: - try: - grpid = struct.unpack("!I", grpid)[0] - except Exception: - warning("in6_getLinkScopedMcastPrefix(): Invalid group id provided") # noqa: E501 - return None - grpid = struct.pack("!I", grpid) + b_grpid = b'' + # Is either bytes, str or int + if isinstance(grpid, (str, bytes)): + try: + if isinstance(grpid, str) and len(grpid) == 8: + i_grpid = int(grpid, 16) & 0xffffffff + elif isinstance(grpid, bytes) and len(grpid) == 4: + i_grpid = struct.unpack("!I", grpid)[0] + else: + raise ValueError + except Exception: + warning( + "in6_getLinkScopedMcastPrefix(): Invalid group id " + "provided" + ) + return None + elif isinstance(grpid, int): + i_grpid = grpid + else: + warning( + "in6_getLinkScopedMcastPrefix(): Invalid group id " + "provided" + ) + return None + b_grpid = struct.pack("!I", i_grpid) flgscope = struct.pack("B", 0xff & ((0x3 << 4) | scope)) plen = b'\xff' res = b'\x00' - a = b'\xff' + flgscope + res + plen + iid + grpid + a = b'\xff' + flgscope + res + plen + iid + b_grpid return inet_ntop(socket.AF_INET6, a) def in6_get6to4Prefix(addr): + # type: (str) -> Optional[str] """ Returns the /48 6to4 prefix associated with provided IPv4 address On error, None is returned. No check is performed on public/private status of the address """ try: - addr = inet_pton(socket.AF_INET, addr) - addr = inet_ntop(socket.AF_INET6, b'\x20\x02' + addr + b'\x00' * 10) + baddr = inet_pton(socket.AF_INET, addr) + return inet_ntop(socket.AF_INET6, b'\x20\x02' + baddr + b'\x00' * 10) except Exception: return None - return addr def in6_6to4ExtractAddr(addr): + # type: (str) -> Optional[str] """ Extract IPv4 address embedded in 6to4 address. Passed address must be a 6to4 address. None is returned on error. """ try: - addr = inet_pton(socket.AF_INET6, addr) + baddr = inet_pton(socket.AF_INET6, addr) except Exception: return None - if addr[:2] != b" \x02": + if baddr[:2] != b" \x02": return None - return inet_ntop(socket.AF_INET, addr[2:6]) + return inet_ntop(socket.AF_INET, baddr[2:6]) def in6_getLocalUniquePrefix(): + # type: () -> str """ Returns a pseudo-randomly generated Local Unique prefix. Function follows recommendation of Section 3.2.2 of RFC 4193 for prefix @@ -386,16 +423,17 @@ def in6_getLocalUniquePrefix(): tod = time.time() # time of day. Will bother with epoch later i = int(tod) j = int((tod - i) * (2**32)) - tod = struct.pack("!II", i, j) + btod = struct.pack("!II", i, j) mac = RandMAC() # construct modified EUI-64 ID - eui64 = inet_pton(socket.AF_INET6, '::' + in6_mactoifaceid(mac))[8:] + eui64 = inet_pton(socket.AF_INET6, '::' + in6_mactoifaceid(str(mac)))[8:] import hashlib - globalid = hashlib.sha1(tod + eui64).digest()[:5] + globalid = hashlib.sha1(btod + eui64).digest()[:5] return inet_ntop(socket.AF_INET6, b'\xfd' + globalid + b'\x00' * 10) def in6_getRandomizedIfaceId(ifaceid, previous=None): + # type: (str, Optional[str]) -> Tuple[str, str] """ Implements the interface ID generation algorithm described in RFC 3041. The function takes the Modified EUI-64 interface identifier generated @@ -416,18 +454,17 @@ def in6_getRandomizedIfaceId(ifaceid, previous=None): s = b"" if previous is None: - d = b"".join(chb(x) for x in range(256)) - for _ in range(8): - s += chb(random.choice(d)) - previous = s - s = inet_pton(socket.AF_INET6, "::" + ifaceid)[8:] + previous + b_previous = bytes(RandBin(8)) + else: + b_previous = inet_pton(socket.AF_INET6, "::" + previous)[8:] + s = inet_pton(socket.AF_INET6, "::" + ifaceid)[8:] + b_previous import hashlib s = hashlib.md5(s).digest() s1, s2 = s[:8], s[8:] - s1 = chb(orb(s1[0]) | 0x04) + s1[1:] - s1 = inet_ntop(socket.AF_INET6, b"\xff" * 8 + s1)[20:] - s2 = inet_ntop(socket.AF_INET6, b"\xff" * 8 + s2)[20:] - return (s1, s2) + s1 = chb(orb(s1[0]) & (~0x04)) + s1[1:] # set bit 6 to 0 + bs1 = inet_ntop(socket.AF_INET6, b"\xff" * 8 + s1)[20:] + bs2 = inet_ntop(socket.AF_INET6, b"\xff" * 8 + s2)[20:] + return (bs1, bs2) _rfc1924map = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', # noqa: E501 @@ -439,6 +476,7 @@ def in6_getRandomizedIfaceId(ifaceid, previous=None): def in6_ctop(addr): + # type: (str) -> Optional[str] """ Convert an IPv6 address in Compact Representation Notation (RFC 1924) to printable representation ;-) @@ -460,6 +498,7 @@ def in6_ctop(addr): def in6_ptoc(addr): + # type: (str) -> Optional[str] """ Converts an IPv6 address in printable representation to RFC 1924 Compact Representation ;-) @@ -469,12 +508,11 @@ def in6_ptoc(addr): d = struct.unpack("!IIII", inet_pton(socket.AF_INET6, addr)) except Exception: return None - res = 0 + rem = 0 m = [2**96, 2**64, 2**32, 1] for i in range(4): - res += d[i] * m[i] - rem = res - res = [] + rem += d[i] * m[i] + res = [] # type: List[str] while rem: res.append(_rfc1924map[rem % 85]) rem = rem // 85 @@ -483,12 +521,13 @@ def in6_ptoc(addr): def in6_isaddr6to4(x): + # type: (str) -> bool """ Return True if provided address (in printable format) is a 6to4 address (being in 2002::/16). """ - x = inet_pton(socket.AF_INET6, x) - return x[:2] == b' \x02' + bx = inet_pton(socket.AF_INET6, x) + return bx[:2] == b' \x02' conf.teredoPrefix = "2001::" # old one was 3ffe:831f (it is a /32) @@ -496,6 +535,7 @@ def in6_isaddr6to4(x): def in6_isaddrTeredo(x): + # type: (str) -> bool """ Return True if provided address is a Teredo, meaning it is under the /32 conf.teredoPrefix prefix value (by default, 2001::). @@ -508,6 +548,7 @@ def in6_isaddrTeredo(x): def teredoAddrExtractInfo(x): + # type: (str) -> Tuple[str, int, str, int] """ Extract information from a Teredo address. Return value is a 4-tuple made of IPv4 address of Teredo server, flag value (int), @@ -516,13 +557,14 @@ def teredoAddrExtractInfo(x): """ addr = inet_pton(socket.AF_INET6, x) server = inet_ntop(socket.AF_INET, addr[4:8]) - flag = struct.unpack("!H", addr[8:10])[0] + flag = struct.unpack("!H", addr[8:10])[0] # type: int mappedport = struct.unpack("!H", strxor(addr[10:12], b'\xff' * 2))[0] mappedaddr = inet_ntop(socket.AF_INET, strxor(addr[12:16], b'\xff' * 4)) return server, flag, mappedaddr, mappedport def in6_iseui64(x): + # type: (str) -> bool """ Return True if provided address has an interface identifier part created in modified EUI-64 format (meaning it matches ``*::*:*ff:fe*:*``). @@ -530,11 +572,12 @@ def in6_iseui64(x): format. """ eui64 = inet_pton(socket.AF_INET6, '::ff:fe00:0') - x = in6_and(inet_pton(socket.AF_INET6, x), eui64) - return x == eui64 + bx = in6_and(inet_pton(socket.AF_INET6, x), eui64) + return bx == eui64 def in6_isanycast(x): # RFC 2526 + # type: (str) -> bool if in6_iseui64(x): s = '::fdff:ffff:ffff:ff80' packed_x = inet_pton(socket.AF_INET6, x) @@ -549,48 +592,41 @@ def in6_isanycast(x): # RFC 2526 # +---------------------------------+------------------+------------+ # | interface identifier field | warning('in6_isanycast(): TODO not EUI-64') - return 0 - - -def _in6_bitops(a1, a2, operator=0): - a1 = struct.unpack('4I', a1) - a2 = struct.unpack('4I', a2) - fop = [lambda x, y: x | y, - lambda x, y: x & y, - lambda x, y: x ^ y - ] - ret = map(fop[operator % len(fop)], a1, a2) - return b"".join(struct.pack('I', x) for x in ret) + return False def in6_or(a1, a2): + # type: (bytes, bytes) -> bytes """ Provides a bit to bit OR of provided addresses. They must be passed in network format. Return value is also an IPv6 address in network format. """ - return _in6_bitops(a1, a2, 0) + return stror(a1, a2) def in6_and(a1, a2): + # type: (bytes, bytes) -> bytes """ Provides a bit to bit AND of provided addresses. They must be passed in network format. Return value is also an IPv6 address in network format. """ - return _in6_bitops(a1, a2, 1) + return strand(a1, a2) def in6_xor(a1, a2): + # type: (bytes, bytes) -> bytes """ Provides a bit to bit XOR of provided addresses. They must be passed in network format. Return value is also an IPv6 address in network format. """ - return _in6_bitops(a1, a2, 2) + return strxor(a1, a2) def in6_cidr2mask(m): + # type: (int) -> bytes """ Return the mask (bitstring) associated with provided length value. For instance if function is called on 48, return value is @@ -608,7 +644,24 @@ def in6_cidr2mask(m): return b"".join(struct.pack('!I', x) for x in t) +def in6_mask2cidr(m): + # type: (bytes) -> int + """ + Opposite of in6_cidr2mask + """ + if len(m) != 16: + raise Scapy_Exception("value must be 16 octets long") + + for i in range(0, 4): + s = struct.unpack('!I', m[i * 4:(i + 1) * 4])[0] + for j in range(32): + if not s & (1 << (31 - j)): + return i * 32 + j + return 128 + + def in6_getnsma(a): + # type: (bytes) -> bytes """ Return link-local solicited-node multicast address for given address. Passed address must be provided in network format. @@ -620,19 +673,21 @@ def in6_getnsma(a): return r -def in6_getnsmac(a): # return multicast Ethernet address associated with multicast v6 destination # noqa: E501 +def in6_getnsmac(a): + # type: (bytes) -> str """ Return the multicast mac address associated with provided IPv6 address. Passed address must be in network format. """ - a = struct.unpack('16B', a)[-4:] + ba = struct.unpack('16B', a)[-4:] mac = '33:33:' - mac += ':'.join("%.2x" % x for x in a) + mac += ':'.join("%.2x" % x for x in ba) return mac def in6_getha(prefix): + # type: (str) -> str """ Return the anycast address associated with all home agents on a given subnet. @@ -643,6 +698,7 @@ def in6_getha(prefix): def in6_ptop(str): + # type: (str) -> str """ Normalizes IPv6 addresses provided in printable format, returning the same address in printable format. (2001:0db8:0:0::1 -> 2001:db8::1) @@ -651,6 +707,7 @@ def in6_ptop(str): def in6_isincluded(addr, prefix, plen): + # type: (str, str, int) -> bool """ Returns True when 'addr' belongs to prefix/plen. False otherwise. """ @@ -661,6 +718,7 @@ def in6_isincluded(addr, prefix, plen): def in6_isllsnmaddr(str): + # type: (str) -> bool """ Return True if provided address is a link-local solicited node multicast address, i.e. belongs to ff02::1:ff00:0/104. False is @@ -672,6 +730,7 @@ def in6_isllsnmaddr(str): def in6_isdocaddr(str): + # type: (str) -> bool """ Returns True if provided address in printable format belongs to 2001:db8::/32 address space reserved for documentation (as defined @@ -681,6 +740,7 @@ def in6_isdocaddr(str): def in6_islladdr(str): + # type: (str) -> bool """ Returns True if provided address in printable format belongs to _allocated_ link-local unicast address space (fe80::/10) @@ -689,6 +749,7 @@ def in6_islladdr(str): def in6_issladdr(str): + # type: (str) -> bool """ Returns True if provided address in printable format belongs to _allocated_ site-local address space (fec0::/10). This prefix has @@ -699,6 +760,7 @@ def in6_issladdr(str): def in6_isuladdr(str): + # type: (str) -> bool """ Returns True if provided address in printable format belongs to Unique local address space (fc00::/7). @@ -712,6 +774,7 @@ def in6_isuladdr(str): def in6_isgladdr(str): + # type: (str) -> bool """ Returns True if provided address in printable format belongs to _allocated_ global address space (2000::/3). Please note that, @@ -722,6 +785,7 @@ def in6_isgladdr(str): def in6_ismaddr(str): + # type: (str) -> bool """ Returns True if provided address in printable format belongs to allocated Multicast address space (ff00::/8). @@ -730,6 +794,7 @@ def in6_ismaddr(str): def in6_ismnladdr(str): + # type: (str) -> bool """ Returns True if address belongs to node-local multicast address space (ff01::/16) as defined in RFC @@ -738,6 +803,7 @@ def in6_ismnladdr(str): def in6_ismgladdr(str): + # type: (str) -> bool """ Returns True if address belongs to global multicast address space (ff0e::/16). @@ -746,6 +812,7 @@ def in6_ismgladdr(str): def in6_ismlladdr(str): + # type: (str) -> bool """ Returns True if address belongs to link-local multicast address space (ff02::/16) @@ -754,6 +821,7 @@ def in6_ismlladdr(str): def in6_ismsladdr(str): + # type: (str) -> bool """ Returns True if address belongs to site-local multicast address space (ff05::/16). Site local address space has been deprecated. @@ -763,6 +831,7 @@ def in6_ismsladdr(str): def in6_isaddrllallnodes(str): + # type: (str) -> bool """ Returns True if address is the link-local all-nodes multicast address (ff02::1). @@ -772,6 +841,7 @@ def in6_isaddrllallnodes(str): def in6_isaddrllallservers(str): + # type: (str) -> bool """ Returns True if address is the link-local all-servers multicast address (ff02::2). @@ -781,6 +851,7 @@ def in6_isaddrllallservers(str): def in6_getscope(addr): + # type: (str) -> int """ Returns the scope of the address. """ @@ -809,10 +880,12 @@ def in6_getscope(addr): def in6_get_common_plen(a, b): + # type: (str, str) -> int """ Return common prefix length of IPv6 addresses a and b. """ def matching_bits(byte1, byte2): + # type: (int, int) -> int for i in range(8): cur_mask = 0x80 >> i if (byte1 & cur_mask) != (byte2 & cur_mask): @@ -829,78 +902,35 @@ def matching_bits(byte1, byte2): def in6_isvalid(address): + # type: (str) -> bool """Return True if 'address' is a valid IPv6 address string, False otherwise.""" try: - socket.inet_pton(socket.AF_INET6, address) + inet_pton(socket.AF_INET6, address) return True except Exception: return False -class Net6(Gen): # syntax ex. fec0::/126 - """Generate a list of IPv6s from a network address or a name""" - name = "ipv6" - ip_regex = re.compile(r"^([a-fA-F0-9:]+)(/[1]?[0-3]?[0-9])?$") - - def __init__(self, net): - self.repr = net - - tmp = net.split('/') + ["128"] - if not self.ip_regex.match(net): - tmp[0] = socket.getaddrinfo(tmp[0], None, socket.AF_INET6)[0][-1][0] # noqa: E501 - - netmask = int(tmp[1]) - self.net = inet_pton(socket.AF_INET6, tmp[0]) - self.mask = in6_cidr2mask(netmask) - self.plen = netmask - - def _parse(self): - def parse_digit(value, netmask): - netmask = min(8, max(netmask, 0)) - value = int(value) - return (value & (0xff << netmask), - (value | (0xff >> (8 - netmask))) + 1) - - self.parsed = [ - parse_digit(x, y) for x, y in zip( - struct.unpack("16B", in6_and(self.net, self.mask)), - (x - self.plen for x in range(8, 129, 8)), - ) - ] - - def __iter__(self): - self._parse() - - def rec(n, l): - sep = ':' if n and n % 2 == 0 else '' - if n == 16: - return l - return rec(n + 1, [y + sep + '%.2x' % i - # faster than '%s%s%.2x' % (y, sep, i) - for i in range(*self.parsed[n]) - for y in l]) - - return (in6_ptop(addr) for addr in iter(rec(0, ['']))) - - def __iterlen__(self): - self._parse() - return reduce(operator.mul, ((y - x) for (x, y) in self.parsed), 1) - - def __str__(self): - try: - return next(self.__iter__()) - except (StopIteration, RuntimeError): - return None - - def __eq__(self, other): - return str(other) == str(self) - - def __ne__(self, other): - return not self == other - - __hash__ = None - - def __repr__(self): - return "Net6(%r)" % self.repr +class Net6(Net): # syntax ex. 2011:db8::/126 + """Network object from an IP address or hostname and mask""" + name = "Net6" # type: str + family = socket.AF_INET6 # type: int + max_mask = 128 # type: int + + @classmethod + def ip2int(cls, addr): + # type: (str) -> int + val1, val2 = struct.unpack( + '!QQ', inet_pton(socket.AF_INET6, cls.name2addr(addr)) + ) + return cast(int, (val1 << 64) + val2) + + @staticmethod + def int2ip(val): + # type: (int) -> str + return inet_ntop( + socket.AF_INET6, + struct.pack('!QQ', val >> 64, val & 0xffffffffffffffff), + ) diff --git a/scapy/volatile.py b/scapy/volatile.py index 85c3aa13b5e..1500840c909 100644 --- a/scapy/volatile.py +++ b/scapy/volatile.py @@ -1,15 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Philippe Biondi # Copyright (C) Michael Farrell # Copyright (C) Gauthier Sebaux -# This program is published under a GPLv2 license """ Fields that hold random numbers. """ -from __future__ import absolute_import import copy import random import time @@ -17,11 +16,24 @@ import re import uuid import struct +import string from scapy.base_classes import Net from scapy.compat import bytes_encode, chb, plain_str from scapy.utils import corrupt_bits, corrupt_bytes -from scapy.modules.six.moves import range + +from typing import ( + List, + TypeVar, + Generic, + Set, + Union, + Any, + Dict, + Optional, + Tuple, + cast, +) #################### # Random numbers # @@ -35,6 +47,7 @@ class RandomEnumeration: number will be drawn in not less than the number of integers of the sequence""" # noqa: E501 def __init__(self, inf, sup, seed=None, forever=1, renewkeys=0): + # type: (int, int, Optional[int], int, int) -> None self.forever = forever self.renewkeys = renewkeys self.inf = inf @@ -55,9 +68,11 @@ def __init__(self, inf, sup, seed=None, forever=1, renewkeys=0): self.i = 0 def __iter__(self): + # type: () -> RandomEnumeration return self def next(self): + # type: () -> int while True: if self.turns == 0 or (self.i == 0 and self.renewkeys): self.cnt_key = self.rnd.randint(0, 2**self.n - 1) @@ -81,161 +96,247 @@ def next(self): __next__ = next -class VolatileValue(object): +_T = TypeVar('_T') + + +class VolatileValue(Generic[_T]): def __repr__(self): + # type: () -> str return "<%s>" % self.__class__.__name__ + def _command_args(self): + # type: () -> str + return '' + + def command(self, json=False): + # type: (bool) -> Union[Dict[str, str], str] + if json: + return {"type": self.__class__.__name__, "value": self._command_args()} + else: + return "%s(%s)" % (self.__class__.__name__, self._command_args()) + def __eq__(self, other): + # type: (Any) -> bool x = self._fix() y = other._fix() if isinstance(other, VolatileValue) else other if not isinstance(x, type(y)): return False - return x == y + return bool(x == y) def __ne__(self, other): + # type: (Any) -> bool # Python 2.7 compat return not self == other - __hash__ = None + __hash__ = None # type: ignore def __getattr__(self, attr): + # type: (str) -> Any if attr in ["__setstate__", "__getstate__"]: raise AttributeError(attr) return getattr(self._fix(), attr) def __str__(self): + # type: () -> str return str(self._fix()) def __bytes__(self): + # type: () -> bytes return bytes_encode(self._fix()) def __len__(self): - return len(self._fix()) + # type: () -> int + # Does not work for some types (int?) + return len(self._fix()) # type: ignore def copy(self): + # type: () -> Any return copy.copy(self) def _fix(self): - return None + # type: () -> _T + return cast(_T, None) -class RandField(VolatileValue): +class RandField(VolatileValue[_T], Generic[_T]): pass -class _RandNumeral(RandField): +_I = TypeVar("_I", int, float) + + +class _RandNumeral(RandField[_I]): """Implements integer management in RandField""" def __int__(self): + # type: () -> int return int(self._fix()) def __index__(self): + # type: () -> int return int(self) def __nonzero__(self): + # type: () -> bool return bool(self._fix()) __bool__ = __nonzero__ def __add__(self, other): + # type: (_I) -> _I return self._fix() + other def __radd__(self, other): + # type: (_I) -> _I return other + self._fix() def __sub__(self, other): + # type: (_I) -> _I return self._fix() - other def __rsub__(self, other): + # type: (_I) -> _I return other - self._fix() def __mul__(self, other): + # type: (_I) -> _I return self._fix() * other def __rmul__(self, other): + # type: (_I) -> _I return other * self._fix() def __floordiv__(self, other): + # type: (_I) -> float return self._fix() / other __div__ = __floordiv__ def __lt__(self, other): + # type: (_I) -> bool return self._fix() < other def __le__(self, other): + # type: (_I) -> bool return self._fix() <= other def __ge__(self, other): + # type: (_I) -> bool return self._fix() >= other def __gt__(self, other): + # type: (_I) -> bool return self._fix() > other + +class RandNum(_RandNumeral[int]): + """Instances evaluate to random integers in selected range""" + min = 0 + max = 0 + + def __init__(self, min, max): + # type: (int, int) -> None + self.min = min + self.max = max + + def _command_args(self): + # type: () -> str + if self.__class__.__name__ == 'RandNum': + return "min=%r, max=%r" % (self.min, self.max) + return super(RandNum, self)._command_args() + + def _fix(self): + # type: () -> int + return random.randrange(self.min, self.max + 1) + def __lshift__(self, other): + # type: (int) -> int return self._fix() << other def __rshift__(self, other): + # type: (int) -> int return self._fix() >> other def __and__(self, other): + # type: (int) -> int return self._fix() & other def __rand__(self, other): + # type: (int) -> int return other & self._fix() def __or__(self, other): + # type: (int) -> int return self._fix() | other def __ror__(self, other): + # type: (int) -> int return other | self._fix() -class RandNum(_RandNumeral): - """Instances evaluate to random integers in selected range""" - min = 0 - max = 0 - +class RandFloat(_RandNumeral[float]): def __init__(self, min, max): + # type: (int, int) -> None self.min = min self.max = max def _fix(self): - return random.randrange(self.min, self.max + 1) - - -class RandFloat(RandNum): - def _fix(self): + # type: () -> float return random.uniform(self.min, self.max) -class RandBinFloat(RandNum): +class RandBinFloat(RandFloat): def _fix(self): - return struct.unpack("!f", bytes(RandBin(4)))[0] + # type: () -> float + return cast( + float, + struct.unpack("!f", bytes(RandBin(4)))[0] + ) -class RandNumGamma(_RandNumeral): +class RandNumGamma(RandNum): def __init__(self, alpha, beta): + # type: (int, int) -> None self.alpha = alpha self.beta = beta + def _command_args(self): + # type: () -> str + return "alpha=%r, beta=%r" % (self.alpha, self.beta) + def _fix(self): + # type: () -> int return int(round(random.gammavariate(self.alpha, self.beta))) -class RandNumGauss(_RandNumeral): +class RandNumGauss(RandNum): def __init__(self, mu, sigma): + # type: (int, int) -> None self.mu = mu self.sigma = sigma + def _command_args(self): + # type: () -> str + return "mu=%r, sigma=%r" % (self.mu, self.sigma) + def _fix(self): + # type: () -> int return int(round(random.gauss(self.mu, self.sigma))) -class RandNumExpo(_RandNumeral): +class RandNumExpo(RandNum): def __init__(self, lambd, base=0): + # type: (float, int) -> None self.lambd = lambd self.base = base + def _command_args(self): + # type: () -> str + ret = "lambd=%r" % self.lambd + if self.base != 0: + ret += ", base=%r" % self.base + return ret + def _fix(self): + # type: () -> int return self.base + int(round(random.expovariate(self.lambd))) @@ -243,90 +344,116 @@ class RandEnum(RandNum): """Instances evaluate to integer sampling without replacement from the given interval""" # noqa: E501 def __init__(self, min, max, seed=None): + # type: (int, int, Optional[int]) -> None + self._seed = seed self.seq = RandomEnumeration(min, max, seed) super(RandEnum, self).__init__(min, max) + def _command_args(self): + # type: () -> str + ret = "min=%r, max=%r" % (self.min, self.max) + if self._seed: + ret += ", seed=%r" % self._seed + return ret + def _fix(self): + # type: () -> int return next(self.seq) class RandByte(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, 0, 2**8 - 1) class RandSByte(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, -2**7, 2**7 - 1) class RandShort(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, 0, 2**16 - 1) class RandSShort(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, -2**15, 2**15 - 1) class RandInt(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, 0, 2**32 - 1) class RandSInt(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, -2**31, 2**31 - 1) class RandLong(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, 0, 2**64 - 1) class RandSLong(RandNum): def __init__(self): + # type: () -> None RandNum.__init__(self, -2**63, 2**63 - 1) class RandEnumByte(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, 0, 2**8 - 1) class RandEnumSByte(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, -2**7, 2**7 - 1) class RandEnumShort(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, 0, 2**16 - 1) class RandEnumSShort(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, -2**15, 2**15 - 1) class RandEnumInt(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, 0, 2**32 - 1) class RandEnumSInt(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, -2**31, 2**31 - 1) class RandEnumLong(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, 0, 2**64 - 1) class RandEnumSLong(RandEnum): def __init__(self): + # type: () -> None RandEnum.__init__(self, -2**63, 2**63 - 1) @@ -334,77 +461,164 @@ class RandEnumKeys(RandEnum): """Picks a random value from dict keys list. """ def __init__(self, enum, seed=None): + # type: (Dict[Any, Any], Optional[int]) -> None self.enum = list(enum) RandEnum.__init__(self, 0, len(self.enum) - 1, seed) + def _command_args(self): + # type: () -> str + # Note: only outputs the list of keys, but values are irrelevant anyway + ret = "enum=%r" % self.enum + if self._seed: + ret += ", seed=%r" % self._seed + return ret + def _fix(self): + # type: () -> Any return self.enum[next(self.seq)] -class RandChoice(RandField): +class RandChoice(RandField[Any]): def __init__(self, *args): + # type: (*Any) -> None if not args: raise TypeError("RandChoice needs at least one choice") self._choice = list(args) + def _command_args(self): + # type: () -> str + return ", ".join(self._choice) + def _fix(self): + # type: () -> Any return random.choice(self._choice) -class RandString(RandField): - def __init__(self, size=None, chars=b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"): # noqa: E501 - if size is None: - size = RandNumExpo(0.01) - self.size = size - self.chars = chars +_S = TypeVar("_S", bytes, str) - def _fix(self): - s = b"" - for _ in range(self.size): - rdm_chr = random.choice(self.chars) - s += rdm_chr if isinstance(rdm_chr, str) else chb(rdm_chr) - return s +class _RandString(RandField[_S], Generic[_S]): def __str__(self): + # type: () -> str return plain_str(self._fix()) def __bytes__(self): + # type: () -> bytes return bytes_encode(self._fix()) def __mul__(self, n): + # type: (int) -> _S return self._fix() * n -class RandBin(RandString): - def __init__(self, size=None): - super(RandBin, self).__init__(size=size, chars=b"".join(chb(c) for c in range(256))) # noqa: E501 +class RandString(_RandString[str]): + _DEFAULT_CHARS = (string.ascii_uppercase + string.ascii_lowercase + + string.digits) + + def __init__(self, size=None, chars=_DEFAULT_CHARS): + # type: (Optional[Union[int, RandNum]], str) -> None + if size is None: + size = RandNumExpo(0.01) + self.size = size + self.chars = chars + + def _command_args(self): + # type: () -> str + ret = "" + if isinstance(self.size, VolatileValue): + if self.size.lambd != 0.01 or self.size.base != 0: + ret += "size=%r" % self.size.command() + else: + ret += "size=%r" % self.size + + if self.chars != self._DEFAULT_CHARS: + ret += ", chars=%r" % self.chars + return ret + + def _fix(self): + # type: () -> str + s = "" + for _ in range(int(self.size)): + s += random.choice(self.chars) + return s + + +class RandBin(_RandString[bytes]): + _DEFAULT_CHARS = b"".join(chb(c) for c in range(256)) + + def __init__(self, size=None, chars=_DEFAULT_CHARS): + # type: (Optional[Union[int, RandNum]], bytes) -> None + if size is None: + size = RandNumExpo(0.01) + self.size = size + self.chars = chars + + def _command_args(self): + # type: () -> str + if not isinstance(self.size, VolatileValue): + return "size=%r" % self.size + + if isinstance(self.size, RandNumExpo) and \ + self.size.lambd == 0.01 and self.size.base == 0: + # Default size for RandString, skip + return "" + return "size=%r" % self.size.command() + + def _fix(self): + # type: () -> bytes + s = b"" + for _ in range(int(self.size)): + s += struct.pack("!B", random.choice(self.chars)) + return s class RandTermString(RandBin): def __init__(self, size, term): + # type: (Union[int, RandNum], bytes) -> None self.term = bytes_encode(term) super(RandTermString, self).__init__(size=size) + self.chars = self.chars.replace(self.term, b"") + + def _command_args(self): + # type: () -> str + return ", ".join((super(RandTermString, self)._command_args(), + "term=%r" % self.term)) def _fix(self): + # type: () -> bytes return RandBin._fix(self) + self.term -class RandIP(RandString): - def __init__(self, iptemplate="0.0.0.0/0"): - RandString.__init__(self) +class RandIP(_RandString[str]): + _DEFAULT_IPTEMPLATE = "0.0.0.0/0" + + def __init__(self, iptemplate=_DEFAULT_IPTEMPLATE): + # type: (str) -> None + super(RandIP, self).__init__() self.ip = Net(iptemplate) + def _command_args(self): + # type: () -> str + rep = "%s/%s" % (self.ip.net, self.ip.mask) + if rep == self._DEFAULT_IPTEMPLATE: + return "" + return "iptemplate=%r" % rep + def _fix(self): + # type: () -> str return self.ip.choice() -class RandMAC(RandString): - def __init__(self, template="*"): - RandString.__init__(self) - template += ":*:*:*:*:*" - template = template.split(":") - self.mac = () +class RandMAC(_RandString[str]): + def __init__(self, _template="*"): + # type: (str) -> None + super(RandMAC, self).__init__() + self._template = _template + _template += ":*:*:*:*:*" + template = _template.split(":") + self.mac = () # type: Tuple[Union[int, RandNum], ...] for i in range(6): + v = 0 # type: Union[int, RandNum] if template[i] == "*": v = RandByte() elif "-" in template[i]: @@ -414,17 +628,26 @@ def __init__(self, template="*"): v = int(template[i], 16) self.mac += (v,) + def _command_args(self): + # type: () -> str + if self._template == "*": + return "" + return "template=%r" % self._template + def _fix(self): - return "%02x:%02x:%02x:%02x:%02x:%02x" % self.mac + # type: () -> str + return "%02x:%02x:%02x:%02x:%02x:%02x" % self.mac # type: ignore -class RandIP6(RandString): +class RandIP6(_RandString[str]): def __init__(self, ip6template="**"): - RandString.__init__(self) + # type: (str) -> None + super(RandIP6, self).__init__() self.tmpl = ip6template - self.sp = self.tmpl.split(":") - for i, v in enumerate(self.sp): + self.sp = [] # type: List[Union[int, RandNum, str]] + for v in self.tmpl.split(":"): if not v or v == "**": + self.sp.append(v) continue if "-" in v: a, b = v.split("-") @@ -438,15 +661,22 @@ def __init__(self, ip6template="**"): if not b: b = "ffff" if a == b: - self.sp[i] = int(a, 16) + self.sp.append(int(a, 16)) else: - self.sp[i] = RandNum(int(a, 16), int(b, 16)) + self.sp.append(RandNum(int(a, 16), int(b, 16))) self.variable = "" in self.sp self.multi = self.sp.count("**") + def _command_args(self): + # type: () -> str + if self.tmpl == "**": + return "" + return "ip6template=%r" % self.tmpl + def _fix(self): + # type: () -> str nbm = self.multi - ip = [] + ip = [] # type: List[str] for i, n in enumerate(self.sp): if n == "**": nbm -= 1 @@ -458,13 +688,13 @@ def _fix(self): for j in range(remain): ip.append("%04x" % random.randint(0, 65535)) elif isinstance(n, RandNum): - ip.append("%04x" % n) + ip.append("%04x" % int(n)) elif n == 0: ip.append("0") elif not n: ip.append("") else: - ip.append("%04x" % n) + ip.append("%04x" % int(n)) if len(ip) == 9: ip.remove("") if ip[-1] == "": @@ -472,26 +702,49 @@ def _fix(self): return ":".join(ip) -class RandOID(RandString): +class RandOID(_RandString[str]): def __init__(self, fmt=None, depth=RandNumExpo(0.1), idnum=RandNumExpo(0.01)): # noqa: E501 - RandString.__init__(self) + # type: (Optional[str], RandNumExpo, RandNumExpo) -> None + super(RandOID, self).__init__() self.ori_fmt = fmt + self.fmt = None # type: Optional[List[Union[str, Tuple[int, ...]]]] if fmt is not None: - fmt = fmt.split(".") - for i in range(len(fmt)): - if "-" in fmt[i]: - fmt[i] = tuple(map(int, fmt[i].split("-"))) - self.fmt = fmt + self.fmt = [ + tuple(map(int, x.split("-"))) if "-" in x else x + for x in fmt.split(".") + ] self.depth = depth self.idnum = idnum + def _command_args(self): + # type: () -> str + ret = [] + if self.fmt: + ret.append("fmt=%r" % self.ori_fmt) + + if not isinstance(self.depth, VolatileValue): + ret.append("depth=%r" % self.depth) + elif not isinstance(self.depth, RandNumExpo) or \ + self.depth.lambd != 0.1 or self.depth.base != 0: + ret.append("depth=%s" % self.depth.command()) + + if not isinstance(self.idnum, VolatileValue): + ret.append("idnum=%r" % self.idnum) + elif not isinstance(self.idnum, RandNumExpo) or \ + self.idnum.lambd != 0.01 or self.idnum.base != 0: + ret.append("idnum=%s" % self.idnum.command()) + + return ", ".join(ret) + def __repr__(self): + # type: () -> str if self.ori_fmt is None: return "<%s>" % self.__class__.__name__ else: return "<%s [%s]>" % (self.__class__.__name__, self.ori_fmt) def _fix(self): + # type: () -> str if self.fmt is None: return ".".join(str(self.idnum) for _ in range(1 + self.depth)) else: @@ -508,13 +761,39 @@ def _fix(self): return ".".join(oid) -class RandRegExp(RandField): - def __init__(self, regexp, lambda_=0.3,): +class RandRegExp(RandField[str]): + def __init__(self, regexp, lambda_=0.3): + # type: (str, float) -> None self._regexp = regexp self._lambda = lambda_ + def _command_args(self): + # type: () -> str + ret = "regexp=%r" % self._regexp + if self._lambda != 0.3: + ret += ", lambda_=%r" % self._lambda + return ret + + special_sets = { + "[:alnum:]": "[a-zA-Z0-9]", + "[:alpha:]": "[a-zA-Z]", + "[:ascii:]": "[\x00-\x7F]", + "[:blank:]": "[ \t]", + "[:cntrl:]": "[\x00-\x1F\x7F]", + "[:digit:]": "[0-9]", + "[:graph:]": "[\x21-\x7E]", + "[:lower:]": "[a-z]", + "[:print:]": "[\x20-\x7E]", + "[:punct:]": "[!\"\\#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_{|}~]", + "[:space:]": "[ \t\r\n\v\f]", + "[:upper:]": "[A-Z]", + "[:word:]": "[A-Za-z0-9_]", + "[:xdigit:]": "[A-Fa-f0-9]", + } + @staticmethod - def choice_expand(s): # XXX does not support special sets like (ex ':alnum:') # noqa: E501 + def choice_expand(s): + # type: (str) -> str m = "" invert = s and s[0] == "^" while True: @@ -539,6 +818,7 @@ def choice_expand(s): # XXX does not support special sets like (ex ':alnum:') @staticmethod def stack_fix(lst, index): + # type: (List[Any], List[Any]) -> str r = "" mul = 1 for e in lst: @@ -576,14 +856,19 @@ def stack_fix(lst, index): return r def _fix(self): + # type: () -> str stack = [None] index = [] - current = stack + # Give up on typing this + current = stack # type: Any i = 0 - ln = len(self._regexp) + regexp = self._regexp + for k, v in self.special_sets.items(): + regexp = regexp.replace(k, v) + ln = len(regexp) interp = True while i < ln: - c = self._regexp[i] + c = regexp[i] i += 1 if c == '(': @@ -618,8 +903,7 @@ def _fix(self): num = "".join(current.pop()[1:]) e = current.pop() if "," not in num: - n = int(num) - current.append([current] + [e] * n) + current.append([current] + [e] * int(num)) else: num_min, num_max = num.split(",") if not num_min: @@ -632,12 +916,11 @@ def _fix(self): current.append(e) interp = True elif c == '\\': - c = self._regexp[i] + c = regexp[i] if c == "s": - c = RandChoice(" ", "\t") + current.append(RandChoice(" ", "\t")) elif c in "0123456789": - c = ("cite", ord(c) - 0x30) - current.append(c) + current.append("cite", ord(c) - 0x30) i += 1 elif not interp: current.append(c) @@ -660,6 +943,7 @@ def _fix(self): return RandRegExp.stack_fix(stack[1:], index) def __repr__(self): + # type: () -> str return "<%s [%r]>" % (self.__class__.__name__, self._regexp) @@ -670,6 +954,7 @@ class RandSingularity(RandChoice): class RandSingNum(RandSingularity): @staticmethod def make_power_of_two(end): + # type: (int) -> Set[int] sign = 1 if end == 0: end = 1 @@ -680,6 +965,9 @@ def make_power_of_two(end): return {sign * 2**i for i in range(end_n)} def __init__(self, mn, mx): + # type: (int, int) -> None + self._mn = mn + self._mx = mx sing = {0, mn, mx, int((mn + mx) / 2)} sing |= self.make_power_of_two(mn) sing |= self.make_power_of_two(mx) @@ -692,49 +980,64 @@ def __init__(self, mn, mx): super(RandSingNum, self).__init__(*sing) self._choice.sort() + def _command_args(self): + # type: () -> str + if self.__class__.__name__ == 'RandSingNum': + return "mn=%r, mx=%r" % (self._mn, self._mx) + return super(RandSingNum, self)._command_args() + class RandSingByte(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, 0, 2**8 - 1) class RandSingSByte(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, -2**7, 2**7 - 1) class RandSingShort(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, 0, 2**16 - 1) class RandSingSShort(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, -2**15, 2**15 - 1) class RandSingInt(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, 0, 2**32 - 1) class RandSingSInt(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, -2**31, 2**31 - 1) class RandSingLong(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, 0, 2**64 - 1) class RandSingSLong(RandSingNum): def __init__(self): + # type: () -> None RandSingNum.__init__(self, -2**63, 2**63 - 1) class RandSingString(RandSingularity): def __init__(self): + # type: () -> None choices_list = ["", "%x", "%%", @@ -791,30 +1094,49 @@ def __init__(self): "foo.exe\\", ] super(RandSingString, self).__init__(*choices_list) + def _command_args(self): + # type: () -> str + return "" + def __str__(self): + # type: () -> str return str(self._fix()) def __bytes__(self): + # type: () -> bytes return bytes_encode(self._fix()) -class RandPool(RandField): +class RandPool(RandField[VolatileValue[Any]]): def __init__(self, *args): + # type: (*Tuple[VolatileValue[Any], int]) -> None """Each parameter is a volatile object or a couple (volatile object, weight)""" # noqa: E501 - pool = [] + self._args = args + pool = [] # type: List[VolatileValue[Any]] for p in args: w = 1 if isinstance(p, tuple): - p, w = p - pool += [p] * w + p, w = p # type: ignore + pool += [cast(VolatileValue[Any], p)] * w self._pool = pool + def _command_args(self): + # type: () -> str + ret = [] + for p in self._args: + if isinstance(p, tuple): + ret.append("(%s, %r)" % (p[0].command(), p[1])) + else: + ret.append(p.command()) + return ", ".join(ret) + def _fix(self): + # type: () -> Any r = random.choice(self._pool) return r._fix() -class RandUUID(RandField): +class RandUUID(RandField[uuid.UUID]): """Generates a random UUID. By default, this generates a RFC 4122 version 4 UUID (totally random). @@ -850,12 +1172,22 @@ class RandUUID(RandField): ) VERSIONS = [1, 3, 4, 5] - def __init__(self, template=None, node=None, clock_seq=None, - namespace=None, name=None, version=None): + def __init__(self, + template=None, # type: Optional[Any] + node=None, # type: Optional[int] + clock_seq=None, # type: Optional[int] + namespace=None, # type: Optional[uuid.UUID] + name=None, # type: Optional[str] + version=None, # type: Optional[Any] + ): + # type: (...) -> None + self._template = template + self._ori_version = version + self.uuid_template = None - self.node = None self.clock_seq = None self.namespace = None + self.name = None self.node = None self.version = None @@ -869,18 +1201,18 @@ def __init__(self, template=None, node=None, clock_seq=None, else: # Invalid template raise ValueError("UUID template is invalid") - rnd_f = [RandInt] + [RandShort] * 2 + [RandByte] * 8 - uuid_template = [] + uuid_template = [] # type: List[Union[int, RandNum]] for i, t in enumerate(template): if t == "*": - val = rnd_f[i]() + uuid_template.append(rnd_f[i]()) elif ":" in t: mini, maxi = t.split(":") - val = RandNum(int(mini, 16), int(maxi, 16)) + uuid_template.append( + RandNum(int(mini, 16), int(maxi, 16)) + ) else: - val = int(t, 16) - uuid_template.append(val) + uuid_template.append(int(t, 16)) self.uuid_template = tuple(uuid_template) else: @@ -921,17 +1253,39 @@ def __init__(self, template=None, node=None, clock_seq=None, "did not specify version, you need to " "specify it explicitly.") + def _command_args(self): + # type: () -> str + ret = [] + if self._template: + ret.append("template=%r" % self._template) + if self.node: + ret.append("node=%r" % self.node) + if self.clock_seq: + ret.append("clock_seq=%r" % self.clock_seq) + if self.namespace: + ret.append("namespace=%r" % self.namespace) + if self.name: + ret.append("name=%r" % self.name) + if self._ori_version: + ret.append("version=%r" % self._ori_version) + return ", ".join(ret) + def _fix(self): + # type: () -> uuid.UUID if self.uuid_template: return uuid.UUID(("%08x%04x%04x" + ("%02x" * 8)) % self.uuid_template) elif self.version == 1: return uuid.uuid1(self.node, self.clock_seq) elif self.version == 3: + if not self.namespace or not self.name: + raise ValueError("Missing namespace or name") return uuid.uuid3(self.namespace, self.name) elif self.version == 4: return uuid.uuid4() elif self.version == 5: + if not self.namespace or not self.name: + raise ValueError("Missing namespace or name") return uuid.uuid5(self.namespace, self.name) else: raise ValueError("Unhandled version") @@ -940,59 +1294,100 @@ def _fix(self): # Automatic timestamp -class AutoTime(_RandNumeral): +class _AutoTime(_RandNumeral[_T], # type: ignore + Generic[_T]): def __init__(self, base=None, diff=None): + # type: (Optional[int], Optional[float]) -> None + self._base = base + self._ori_diff = diff + if diff is not None: self.diff = diff elif base is None: - self.diff = 0 + self.diff = 0. else: self.diff = time.time() - base + def _command_args(self): + # type: () -> str + ret = [] + if self._base: + ret.append("base=%r" % self._base) + if self._ori_diff: + ret.append("diff=%r" % self._ori_diff) + return ", ".join(ret) + + +class AutoTime(_AutoTime[float]): def _fix(self): + # type: () -> float return time.time() - self.diff -class IntAutoTime(AutoTime): +class IntAutoTime(_AutoTime[int]): def _fix(self): + # type: () -> int return int(time.time() - self.diff) -class ZuluTime(AutoTime): +class ZuluTime(_AutoTime[str]): def __init__(self, diff=0): + # type: (int) -> None super(ZuluTime, self).__init__(diff=diff) def _fix(self): + # type: () -> str return time.strftime("%y%m%d%H%M%SZ", time.gmtime(time.time() + self.diff)) -class GeneralizedTime(AutoTime): +class GeneralizedTime(_AutoTime[str]): def __init__(self, diff=0): + # type: (int) -> None super(GeneralizedTime, self).__init__(diff=diff) def _fix(self): + # type: () -> str return time.strftime("%Y%m%d%H%M%SZ", time.gmtime(time.time() + self.diff)) -class DelayedEval(VolatileValue): +class DelayedEval(VolatileValue[Any]): """ Example of usage: DelayedEval("time.time()") """ def __init__(self, expr): + # type: (str) -> None self.expr = expr + def _command_args(self): + # type: () -> str + return "expr=%r" % self.expr + def _fix(self): + # type: () -> Any return eval(self.expr) -class IncrementalValue(VolatileValue): +class IncrementalValue(VolatileValue[int]): def __init__(self, start=0, step=1, restart=-1): + # type: (int, int, int) -> None self.start = self.val = start self.step = step self.restart = restart + def _command_args(self): + # type: () -> str + ret = [] + if self.start: + ret.append("start=%r" % self.start) + if self.step != 1: + ret.append("step=%r" % self.step) + if self.restart != -1: + ret.append("restart=%r" % self.restart) + return ", ".join(ret) + def _fix(self): + # type: () -> int v = self.val if self.val == self.restart: self.val = self.start @@ -1001,16 +1396,29 @@ def _fix(self): return v -class CorruptedBytes(VolatileValue): +class CorruptedBytes(VolatileValue[bytes]): def __init__(self, s, p=0.01, n=None): + # type: (str, float, Optional[Any]) -> None self.s = s self.p = p self.n = n + def _command_args(self): + # type: () -> str + ret = [] + ret.append("s=%r" % self.s) + if self.p != 0.01: + ret.append("p=%r" % self.p) + if self.n: + ret.append("n=%r" % self.n) + return ", ".join(ret) + def _fix(self): + # type: () -> bytes return corrupt_bytes(self.s, self.p, self.n) class CorruptedBits(CorruptedBytes): def _fix(self): + # type: () -> bytes return corrupt_bits(self.s, self.p, self.n) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index cbb75683e6c..00000000000 --- a/setup.cfg +++ /dev/null @@ -1,26 +0,0 @@ -[bdist_wheel] -universal = 1 - -[metadata] -description-file = README.md -license_file = LICENSE - -[sdist] -formats=gztar -owner=root -group=root - -[coverage:run] -concurrency = multiprocessing -omit = - # Scapy specific paths - scapy/tools/UTscapy.py - test/* - # Scapy external modules - scapy/modules/six.py - scapy/libs/winpcapy.py - scapy/libs/ethertypes.py - # .tox specific path - .tox/* - # OS specific paths - /private/* diff --git a/setup.py b/setup.py index 2c60986cb80..dc719faece1 100755 --- a/setup.py +++ b/setup.py @@ -1,19 +1,29 @@ #! /usr/bin/env python """ -Distutils setup file for Scapy. +Setuptools setup file for Scapy. """ +import io +import os +import sys + +if sys.version_info[0] <= 2: + raise OSError("Scapy no longer supports Python 2 ! Please use Scapy 2.5.0") + try: - from setuptools import setup, find_packages + import setuptools + from setuptools import setup + from setuptools.command.sdist import sdist + from setuptools.command.build_py import build_py except: raise ImportError("setuptools is required to install scapy !") -import io -import os def get_long_description(): - """Extract description from README.md, for PyPI's usage""" + """ + Extract description from README.md, for PyPI's usage + """ def process_ignore_tags(buffer): return "\n".join( x for x in buffer.split("\n") if "" not in x @@ -29,75 +39,77 @@ def process_ignore_tags(buffer): return None -# https://packaging.python.org/guides/distributing-packages-using-setuptools/ +# Note: why do we bother including a 'scapy/VERSION' file and doing our +# own versioning stuff, instead of using more standard methods? +# Because it's all garbage. + +# If you remain fully standard, there's no way +# of adding the version dynamically, even less when using archives +# (currently, we're able to add the version anytime someone exports Scapy +# on github). + +# If you use setuptools_scm, you'll be able to have the git tag set into +# the wheel (therefore the metadata), that you can then retrieve using +# importlib.metadata, BUT it breaks sdist (source packages), as those +# don't include metadata. + + +def _build_version(path): + """ + This adds the scapy/VERSION file when creating a sdist and a wheel + """ + fn = os.path.join(path, 'scapy', 'VERSION') + with open(fn, 'w') as f: + f.write(__import__('scapy').VERSION) + + +class SDist(sdist): + """ + Modified sdist to create scapy/VERSION file + """ + def make_release_tree(self, base_dir, *args, **kwargs): + super(SDist, self).make_release_tree(base_dir, *args, **kwargs) + # ensure there's a scapy/VERSION file + _build_version(base_dir) + + +class BuildPy(build_py): + """ + Modified build_py to create scapy/VERSION file + """ + def build_package_data(self): + super(BuildPy, self).build_package_data() + # ensure there's a scapy/VERSION file + _build_version(self.build_lib) + + +# Patch so that for setuptools < 77 understands the 'license' version required +# by modern setuptools. See https://github.com/secdev/scapy/issues/4849. +# This allow us to keep support for Python 3.7 +try: + major = int(setuptools.__version__.split(".")[0]) + if major < 77: + # We replace setuptools.dist.pyprojecttoml.apply_configuration with goo + from setuptools.config.pyprojecttoml import read_configuration, _apply + + def _patched_apply_configuration(dist, filepath, *_): + # 1. We force ignore option errors regarding 'license' + config = read_configuration(filepath, True, ignore_option_errors=True, dist=dist) + + # 2. We replace the license with the one it expected + if isinstance(config["project"]["license"], str): + config["project"]["license"] = {'text': config["project"]["license"]} + + return _apply(dist, config, filepath) + + setuptools.dist.pyprojecttoml.apply_configuration = _patched_apply_configuration +except Exception: + pass + + setup( - name='scapy', - version=__import__('scapy').VERSION, - packages=find_packages(), + cmdclass={'sdist': SDist, 'build_py': BuildPy}, data_files=[('share/man/man1', ["doc/scapy.1"])], - package_data={ - 'scapy': ['VERSION'], - }, - # Build starting scripts automatically - entry_points={ - 'console_scripts': [ - 'scapy = scapy.main:interact', - 'UTscapy = scapy.tools.UTscapy:main' - ] - }, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', - # pip > 9 handles all the versioning - extras_require={ - 'basic': ["ipython"], - 'complete': [ - 'ipython', - 'pyx', - 'cryptography>=2.0', - 'matplotlib' - ], - 'docs': [ - 'sphinx>=2.2.0', - 'sphinx_rtd_theme>=0.4.3', - 'tox>=3.0.0' - ] - }, - # We use __file__ in scapy/__init__.py, therefore Scapy isn't zip safe - zip_safe=False, - - # Metadata - author='Philippe BIONDI', - author_email='phil(at)secdev.org', - maintainer='Pierre LALET, Gabriel POTTER, Guillaume VALADON', - description='Scapy: interactive packet manipulation tool', long_description=get_long_description(), long_description_content_type='text/markdown', - license='GPLv2', - url='https://scapy.net', - project_urls={ - 'Documentation': 'https://scapy.readthedocs.io', - 'Source Code': 'https://github.com/secdev/scapy/', - }, - download_url='https://github.com/secdev/scapy/tarball/master', - keywords=["network"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "Intended Audience :: Science/Research", - "Intended Audience :: System Administrators", - "Intended Audience :: Telecommunications Industry", - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Topic :: Security", - "Topic :: System :: Networking", - "Topic :: System :: Networking :: Monitoring", - ] ) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/answering_machines.uts b/test/answering_machines.uts index bdc69e47f77..7aa36eee261 100644 --- a/test/answering_machines.uts +++ b/test/answering_machines.uts @@ -8,21 +8,27 @@ + Answering Machines = Generic answering machine mocker -import mock +from unittest import mock @mock.patch("scapy.ansmachine.sniff") def test_am(cls_name, packet_query, check_reply, mock_sniff, **kargs): + packet_query = packet_query.__class__(bytes(packet_query)) def sniff(*args,**kargs): kargs["prn"](packet_query) mock_sniff.side_effect = sniff am = cls_name(**kargs) - am.send_reply = check_reply + called = [False] + def _sndrpl(x): + called[0] = True + check_reply(x.__class__(bytes(x))) + am.send_reply = _sndrpl am() + assert called[0], "Filter never passed for AnsweringMachine !" = BOOT_am def check_BOOTP_am_reply(packet): - assert(BOOTP in packet and packet[BOOTP].op == 2) - assert(packet[BOOTP].yiaddr == "192.168.1.128" and packet[BOOTP].giaddr == "192.168.1.1") + assert BOOTP in packet and packet[BOOTP].op == 2 + assert packet[BOOTP].yiaddr == "192.168.1.128" and packet[BOOTP].giaddr == "192.168.1.1" test_am(BOOTP_am, Ether()/IP()/UDP()/BOOTP(op=1), @@ -31,18 +37,30 @@ test_am(BOOTP_am, = DHCP_am def check_DHCP_am_reply(packet): - assert(DHCP in packet and len(packet[DHCP].options)) - assert(("domain", "localnet") in packet[DHCP].options) + assert DHCP in packet and len(packet[DHCP].options) + assert ("domain", b"localnet") in packet[DHCP].options + assert ('name_server', '192.168.1.1') in packet[DHCP].options + +def check_ns_DHCP_am_reply(packet): + assert DHCP in packet and len(packet[DHCP].options) + assert ("domain", b"localnet") in packet[DHCP].options + assert ('name_server', '1.1.1.1', '2.2.2.2') in packet[DHCP].options test_am(DHCP_am, - Ether()/IP()/UDP()/BOOTP(op=1)/DHCP(), - check_DHCP_am_reply) + Ether()/IP()/UDP()/BOOTP(op=1)/DHCP(options=[('message-type', 'request')]), + check_DHCP_am_reply, + domain="localnet") +test_am(DHCP_am, + Ether()/IP()/UDP()/BOOTP(op=1)/DHCP(options=[('message-type', 'request')]), + check_ns_DHCP_am_reply, + domain="localnet", + nameserver=["1.1.1.1", "2.2.2.2"]) = ARP_am def check_ARP_am_reply(packet): - assert(ARP in packet and packet[ARP].psrc == "10.28.7.1") - assert(packet[ARP].hwsrc == "00:01:02:03:04:05") + assert ARP in packet and packet[ARP].psrc == "10.28.7.1" + assert packet[ARP].hwsrc == "00:01:02:03:04:05" test_am(ARP_am, Ether()/ARP(pdst="10.28.7.1"), @@ -50,15 +68,139 @@ test_am(ARP_am, IP_addr="10.28.7.1", ARP_addr="00:01:02:03:04:05") += ICMPEcho_am +def check_ICMP_am_reply(packet): + packet.show() + assert packet[Ether].src != "ff:ff:ff:ff:ff:ff" + assert packet[Ether].dst == "aa:aa:aa:aa:aa:aa" + assert IP in packet and ICMP in packet + assert packet[IP].dst == "1.1.1.1" + assert packet[IP].src == "2.2.2.2" + assert packet[ICMP].seq == 12 + +test_am(ICMPEcho_am, + Ether(src="aa:aa:aa:aa:aa:aa", dst="ff:ff:ff:ff:ff:ff")/IP(src="1.1.1.1", dst="2.2.2.2")/ICMP(seq=12), + check_ICMP_am_reply) = DNS_am def check_DNS_am_reply(packet): - assert(DNS in packet and packet[DNS].ancount == 1) - assert(packet[DNS].an.rdata == "192.168.1.1") + assert packet[Ether].src == "bb:bb:bb:bb:bb:bb" + assert packet[Ether].dst == "aa:aa:aa:aa:aa:aa" + assert packet[IP].src == "127.0.0.2" + assert packet[IP].dst == "127.0.0.1" + assert DNS in packet and packet[DNS].ancount == 1 + assert packet[DNS].an[0].rdata == "192.168.1.1" + assert packet[DNS].qd[0].qname == b"www.secdev.org." + +test_am(DNS_am, + Ether(src="aa:aa:aa:aa:aa:aa", dst="bb:bb:bb:bb:bb:bb")/IP(src="127.0.0.1", dst="127.0.0.2")/UDP()/DNS(qd=DNSQR(qname="www.secdev.org")), + check_DNS_am_reply, + joker="192.168.1.1") + +def check_DNS_am_reply_srvmatch(packet): + assert DNS in packet and packet[DNS].ancount == 1 + assert isinstance(packet[DNS].an[0], DNSRRSRV) + assert packet[DNS].an[0].rrname == b'_ldap._tcp.dc._msdcs.scapy.fr.' + assert packet[DNS].an[0].port == 389 + assert packet[DNS].an[0].target == b'dc.scapy.fr.' + +test_am(DNS_am, + Ether()/IP()/UDP()/DNS(qd=DNSQR(qname=b'_ldap._tcp.dc._msdcs.scapy.fr.', qtype="SRV")), + check_DNS_am_reply_srvmatch, + srvmatch={"_ldap._tcp.dc._msdcs.scapy.fr": (389, "dc.scapy.fr")}) + +def check_DNS_am_reply_arpa(packet): + assert DNS in packet and packet[DNS].ancount == 1 + assert packet[DNS].an[0].rdata == b"scapy." + assert packet[DNS].an[0].rrname == b"1.0.16.172.in-addr.arpa." + +test_am(DNS_am, + Ether()/IP()/UDP()/DNS(qd=DNSQR(qname=b"1.0.16.172.in-addr.arpa.", qtype="PTR")), + check_DNS_am_reply_arpa, + jokerarpa="scapy") + +def check_DNS_am_reply2(packet): + assert DNS in packet and packet[DNS].ancount == 2 + assert packet[DNS].an[0].rdata == "128.0.0.1" + assert packet[DNS].an[1].rdata == "::1" test_am(DNS_am, - IP()/UDP()/DNS(qd=DNSQR(qname="www.secdev.org")), - check_DNS_am_reply) + Ether()/IP(b'E\x00\x00H\x00\x01\x00\x00@\x11|\xa2\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x004\xe8\x9a\x00\x00\x01\x00\x00\x02\x00\x00\x00\x00\x00\x00\x06gaagle\x03com\x00\x00\x01\x00\x01\x06google\x03com\x00\x00\x1c\x00\x01'), + check_DNS_am_reply2, + match={"google.com": ("127.0.0.1", "::1"), "gaagle.com": "128.0.0.1"}, + joker=False) + +assert DNS_am().make_reply(Ether()) is None +assert DNS_am().make_reply(Ether()/IP()) is None +assert DNS_am().make_reply(Ether()/IP()/UDP()) is None +assert DNS_am().make_reply( + Ether()/IP()/UDP()/DNS(b'q\xa04\x00\x00\xa0\x01\x00\xf3\x00\x01\x04\x01y') +) is None + += LLMNR_am +def check_LLMNR_am_am_reply(packet): + # assert packet[Ether].src == get_if_hwaddr(conf.iface) + assert packet[Ether].dst == "aa:aa:aa:aa:aa:aa" + # assert packet[IP].src == get_if_addr(conf.iface) + assert packet[IP].dst == "192.168.0.1" + assert packet[UDP].dport == 51938 + assert packet[UDP].sport == 5355 + assert LLMNRResponse in packet and packet[LLMNRResponse].ancount == 1 and packet[LLMNRResponse].qdcount == 1 + assert packet[LLMNRResponse].qd[0].qname == b"TEST." + assert packet[LLMNRResponse].an[0].rdata == "192.168.1.1" + assert packet[LLMNRResponse].an[0].rrname == b"TEST." + assert packet[LLMNRResponse].an[0].ttl == 60 + +test_am(LLMNR_am, + Ether(src="aa:aa:aa:aa:aa:aa", dst="01:00:5e:00:00:fc")/IP(src="192.168.0.1", dst="224.0.0.252")/UDP(dport=5355, sport=51938)/LLMNRQuery(qd=DNSQR(qname=b"TEST.", qtype="A")), + check_LLMNR_am_am_reply, + ttl=60, + match={"TEST": "192.168.1.1"}) + += mDNS_am +def check_mDNS_am_reply(packet): + packet.show() + # assert packet[Ether].src == get_if_hwaddr(conf.iface) + assert packet[Ether].dst == "01:00:5e:00:00:fb" + # assert packet[IP].src == get_if_addr(conf.iface) + assert packet[IP].dst == "224.0.0.251" + assert packet[IP].ttl == 255 + assert packet[UDP].dport == 5353 + assert packet[UDP].sport == 5353 + assert DNS in packet and packet[DNS].ancount == 1 and packet[DNS].qdcount == 0 + assert packet[DNS].an[0].rdata == "192.168.1.1" + assert packet[DNS].an[0].rrname == b"TEST.local." + assert packet[DNS].an[0].ttl == 10 + +test_am(mDNS_am, + Ether(src="aa:aa:aa:aa:aa:aa", dst="01:00:5e:00:00:fb")/IP(src="192.168.0.1", dst="224.0.0.251", ttl=1)/UDP(dport=5353, sport=5353)/DNS(qd=DNSQR(qname=b"TEST.local.", qtype="A")), + check_mDNS_am_reply, + joker="192.168.1.1") + + +def check_mDNS_am_reply2(packet): + # $ avahi-resolve -n bonjour.local + packet.show() + # assert packet[Ether].src == get_if_hwaddr(conf.iface) + assert packet[Ether].dst == "01:00:5e:00:00:fb" + # assert packet[IP].src == get_if_addr(conf.iface) + assert packet[IP].dst == "224.0.0.251" + assert packet[IP].ttl == 255 + assert packet[UDP].dport == 5353 + assert packet[UDP].sport == 5353 + assert DNS in packet and packet[DNS].ancount == 2 and packet[DNS].qdcount == 0 + assert packet[DNS].an[0].rdata == "192.168.1.1" + assert packet[DNS].an[0].rrname == b"bonjour.local." + assert packet[DNS].an[0].ttl == 120 + assert packet[DNS].an[1].type == 47 + assert packet[DNS].an[1].rrname == b"bonjour.local." + assert packet[DNS].an[1].ttl == 120 + +test_am(mDNS_am, + Ether(b'\x01\x00^\x00\x00\xfb\xaa\xaa\xaa\xaa\xaa\xaa\x08\x00E\x00\x00A\xce}@\x00\xff\x11\x0b\x89\xc0\xa8\x00\x01\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x00-\xdbl\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x07bonjour\x05local\x00\x00\x01\x00\x01\xc0\x0c\x00\x1c\x00\x01'), + check_mDNS_am_reply2, + joker="192.168.1.1", + ttl=120) = DHCPv6_am - Basic Instantiaion ~ osx netaccess @@ -66,7 +208,7 @@ a = DHCPv6_am() a.usage() a.parse_options(dns="2001:500::1035", domain="localdomain, local", duid=None, - iface=conf.iface6, advpref=255, sntpservers=None, + iface=conf.iface, advpref=255, sntpservers=None, sipdomains=None, sipservers=None, nisdomain=None, nisservers=None, nispdomain=None, nispservers=None, @@ -101,11 +243,11 @@ assert a.is_request(req) res = a.make_reply(req) assert not a.is_request(res) assert res[UDP].dport == 546 -assert res[DHCP6_Solicit] +assert res[DHCP6_Reply] a.print_reply(req, res) = WiFi_am -import mock +from unittest import mock @mock.patch("scapy.layers.dot11.sniff") def test_WiFi_am(packet_query, check_reply, mock_sniff, **kargs): def sniff(*args,**kargs): @@ -116,9 +258,167 @@ def test_WiFi_am(packet_query, check_reply, mock_sniff, **kargs): am() def check_WiFi_am_reply(packet): - assert(isinstance(packet, list) and len(packet) == 2) - assert(TCP in packet[0] and Raw in packet[0] and raw(packet[0][Raw]) == b"5c4pY") + assert isinstance(packet, list) and len(packet) == 2 + assert TCP in packet[0] and Raw in packet[0] and raw(packet[0][Raw]) == b"5c4pY" -test_WiFi_am(Dot11(FCfield="to-DS")/IP()/TCP()/"Scapy", +test_WiFi_am(Dot11(FCfield="to_DS")/IP()/TCP()/"Scapy", check_WiFi_am_reply, iffrom="scapy0", ifto="scapy1", replace="5c4pY", pattern="Scapy") + + += NBNS_am +def check_NBNS_am_reply(name): + def check(packet): + packet.show() + assert packet[Ether].src != "ff:ff:ff:ff:ff:ff" + assert packet[Ether].dst == "aa:aa:aa:aa:aa:aa" + assert NBNSQueryResponse in packet and packet[NBNSQueryResponse].RR_NAME == name + return check + +for server_name in (None, "", b"test", "test"): + test_am(NBNS_am, + Ether(src="aa:aa:aa:aa:aa:aa", dst="ff:ff:ff:ff:ff:ff")/IP()/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME="test"), + check_NBNS_am_reply(b"test"), + server_name=server_name) + +test_am(NBNS_am, + Ether(src="aa:aa:aa:aa:aa:aa", dst="ff:ff:ff:ff:ff:ff")/IP()/UDP()/NBNSHeader()/NBNSQueryRequest(QUESTION_NAME=b"\x85"), + check_NBNS_am_reply(b"\x85"), + server_name=b"\x85") + += LdapPing_am +def check_LdapPing_am_reply(packet): + nlogon = packet[CLDAP].protocolOp.attributes[0] + assert nlogon.type == b"Netlogon" + logonresp = NETLOGON(nlogon.values[0].value.val) + assert isinstance(logonresp, NETLOGON_SAM_LOGON_RESPONSE_EX) + logonresp.show() + assert logonresp.DnsForestName == b'scapy.fr.', "DnsForestName" + assert logonresp.DnsDomainName == b'scapy.fr.', "DnsDomainName" + assert logonresp.DnsHostName == b'DC.scapy.fr.', "DnsHostName" + assert logonresp.NetbiosDomainName == b'SCAPY.', "NetbiosDomainName" + assert logonresp.NetbiosComputerName == b'DC.', "NetbiosComputerName" + assert logonresp.NtVersion == 3, "NtVersion" + assert logonresp.Flags == 0x3f3fd, "Flags" + assert logonresp.ClientSiteName == b'Default-First-Site-Name.', "ClientSiteName" + +test_am(LdapPing_am, + Ether(b'\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\x08\x00E\x00\x00\xaf\x9d\xb1\x00\x00\x80\x11\x9c\x89\xac\x13P\x01\xac\x13W\xdb\xc7{\x01\x85\x00\x9bV[0q\x02\x01\x01cl\x04\x00\n\x01\x00\n\x01\x00\x02\x01\x00\x02\x01\x00\x01\x01\x00\xa0M\xa3\x15\x04\tDnsDomain\x04\x08scapy.fr\xa3\x0e\x04\x04Host\x04\x06HOST01\xa3\r\x04\x05NtVer\x04\x04\x16\x00\x00 \xa3\x15\x04\x0bDnsHostName\x04\x06HOST010\n\x04\x08Netlogon'), + check_LdapPing_am_reply, + NetbiosComputerName="DC", + NetbiosDomainName="SCAPY", + DnsForestName="scapy.fr") + + +def check_NBNS_LdapPing_am_reply(packet): + packet.show() + assert SMBMailslot_Write in packet, "SMBMailslot_Write" + assert packet[SMBMailslot_Write].Name == b'\\MAILSLOT\\NET\\GETDC510CC0AD', "SMBMailslot_Write.Name" + logonresp = NETLOGON(packet[SMBMailslot_Write].Data.load) + logonresp.show() + assert logonresp.DcSockAddrSize == 16, "DcSockAddrSize" + assert isinstance(logonresp.DcSockAddr, DcSockAddr) + assert logonresp.DcSockAddr.sin_family == 2, "sin_family" + assert logonresp.DcSockAddr.sin_port == 0, "sin_port" + assert logonresp.DcSockAddr.sin_zero == 0, "sin_zero" + assert logonresp.DcSockAddr.sin_addr == get_if_addr(conf.iface) + assert logonresp.DnsForestName == b'scapy.fr.', "DnsForestName" + assert logonresp.DnsDomainName == b'scapy.fr.', "DnsDomainName" + assert logonresp.DnsHostName == b'DC.scapy.fr.', "DnsHostName" + assert logonresp.NetbiosDomainName == b'SCAPY.', "NetbiosDomainName" + assert logonresp.NetbiosComputerName == b'DC.', "NetbiosComputerName" + assert logonresp.NtVersion == 13, "NtVersion" + assert logonresp.Flags == 0x3f3fd, "Flags" + assert logonresp.ClientSiteName == b'Default-First-Site-Name.', "ClientSiteName" + +test_am(LdapPing_am, + Ether(b'\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\x08\x00E\x00\x01\n\xff\x82\x00\x00\x80\x11:]\xac\x13P\x01\xac\x13W\xdb\x00\x8a\x00\x8a\x00\xf6\xd5\xcb\x10\x02\xde\x9d\xac\x13P\x01\x00\x8a\x00\xe0\x00\x00 EIEPFDFEDADBCACACACACACACACACAAA\x00 FDEDEBFAFJCACACACACACACACACACABM\x00\xffSMB%\x00\x00\x00\x00\x18\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x00\x00\x11\x00\x00@\x00\x02\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\\\x00@\x00\\\x00\x03\x00\x01\x00\x00\x00\x02\x00W\x00\\MAILSLOT\\NET\\NETLOGON\x00\x12\x00\x00\x00H\x00O\x00S\x00T\x000\x001\x00\x00\x00\x00\x00\\MAILSLOT\\NET\\GETDC510CC0AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00 \xff\xff\xff\xff'), + check_NBNS_LdapPing_am_reply, + NetbiosComputerName="DC", + NetbiosDomainName="SCAPY", + DnsForestName="scapy.fr") + ++ Radius_am +~ crypto + += Radius_am PAP - Test Access-Success + +def check_radius_pap_reply_success(x): + x.show() + assert x[Radius].code == 2 + assert len(x.attributes) == 1 + assert isinstance(x.attributes[0], RadiusAttr_Message_Authenticator) + assert x.attributes[0].value == bytes.fromhex("75c0da1e492f6f51771a7a49b9136a6d") + assert x.authenticator == bytes.fromhex("3dd94c06bc90accfab8168437821ded4") + +test_am( + Radius_am, + Ether(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00Z\x00\x8e\x00\x00@\x11|\x03\x7f\x00\x00\x01\x7f\x00\x00\x01\x9f<\x07\x14\x00F\xfeY\x01\xfb\x00>s0\x00\x13\x86x\xd7\x11\xc4\x9e\xe1=\xce&r\x15\xa7J\x8an+\xe2\x8a\xe9Lx\xa0h\x0e\r\xbaP\x12%\x87Sg;\xab\x93\x95\xb5o\x925\xc7h\x88\x01\x01\x06user\x02\x12\x99\xbc\x970\x847\x95L\x86JeD\xf8\xea\x87\x00'), + check_radius_pap_reply_fail, + secret="SECRET", + IDENTITIES={"user": "password"} +) + += Radius_am MS-CHAP2 - Test Access-Success + +def check_radius_mschap2_reply_success(x): + x.show() + assert x[Radius].code == 2 + assert len(x.attributes) == 2 + assert isinstance(x.attributes[0], RadiusAttr_Message_Authenticator) + assert x.attributes[0].value == bytes.fromhex("5ab34c3b0554fb14f2d5bf7f521914eb") + assert x.authenticator == bytes.fromhex("c40000ef60fb3c413e2112afb3c7c7d5") + assert isinstance(x.attributes[1], RadiusAttr_Vendor_Specific) + chap2_success = x.attributes[1].value + assert isinstance(chap2_success, MS_CHAP2_Success) + assert chap2_success.String == b'S=46317A3248777BF4D9FAFF4BF4034DC996B740D9' + assert bytes(x[Radius]) == b'\x02\x01\x00Y\xc4\x00\x00\xef`\xfb!\x12\xaf\xb3\xc7\xc7\xd5P\x12Z\xb3L;\x05T\xfb\x14\xf2\xd5\xbf\x7fR\x19\x14\xeb\x1a3\x00\x00\x017\x1a-\x00S=46317A3248777BF4D9FAFF4BF4034DC996B740D9' + +test_am( + Radius_am, + Ether(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00\xado\x90@\x00@\x11\xcc\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\xe1\xea\x07\x14\x00\x99\xfe\xac\x01\x01\x00\x91\xe3\x99\x1b\xec\x1e\x82\x8a\xfcb\xf6\xbf\x824\x13\xc8\x1d\x04\x06\x7f\x00\x01\x01 \x07mynas\x01\x06user\x06\x06\x00\x00\x00\x01\x1a\x18\x00\x00\x017\x0b\x12(\xa0\x18u\x0c\x13\x8c~@\xb71\xa1\xe9\xfd\x1e\xdc\x1a:\x00\x00\x017\x194\x00\x00\xe2\x1fY\xd4O8\x8b\xc6\xf3\x07\xd6\xe5?:3!\x00\x00\x00\x00\x00\x00\x00\x00g-\xd8%\x03\x04\xed\xa7\xc6O\x83"\xdc\xe2\x07\xaa\xf8\x15\xed\xc3~\x08GHP\x12/)\xa2\t\x9dA8\xf9>\xa7V\xba\xf6\xf0LG'), + check_radius_mschap2_reply_success, + secret="SECRET", + IDENTITIES={"user": "password"} +) + += Radius_am MS-CHAP2 - Test Access-Reject + +def check_radius_mschap2_reply_fail(x): + x.show() + assert x[Radius].code == 3 + assert len(x.attributes) == 2 + assert isinstance(x.attributes[0], RadiusAttr_Message_Authenticator) + assert x.attributes[0].value == bytes.fromhex("df430d94a4992ca0d38acf02a1fa94f0") + assert x.authenticator == bytes.fromhex("e0d5cf468ffdf714ed4a40aea1a5715f") + assert isinstance(x.attributes[1], RadiusAttr_Vendor_Specific) + chap2_error = x.attributes[1].value + assert isinstance(chap2_error, MS_CHAP_Error) + assert chap2_error.String == b'E=691 R=0 V=3' + assert bytes(x[Radius]) == b'\x03\x01\x00<\xe0\xd5\xcfF\x8f\xfd\xf7\x14\xedJ@\xae\xa1\xa5q_P\x12\xdfC\r\x94\xa4\x99,\xa0\xd3\x8a\xcf\x02\xa1\xfa\x94\xf0\x1a\x16\x00\x00\x017\x02\x10\x00E=691 R=0 V=3' + +test_am( + Radius_am, + Ether(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00\xad\xca\xd1@\x00@\x11ql\x7f\x00\x00\x01\x7f\x00\x00\x01\xe9\x1b\x07\x14\x00\x99\xfe\xac\x01\x01\x00\x91\xc0{%t\xdd\x8eQC\xda\x861\x11\xf9\xd0\xb2j\x04\x06\x7f\x00\x01\x01 \x07mynas\x01\x06user\x06\x06\x00\x00\x00\x01\x1a\x18\x00\x00\x017\x0b\x12\xd8\x07\xbf\x15N\xfb\x9a;\x0f\xd8\x14\x7f\xae\xe2\xe3e\x1a:\x00\x00\x017\x194\x00\x00\x8e\x8d\xe0\x81\x15]8\xb5j\x7f`\x14\xe0f]\xa6\x00\x00\x00\x00\x00\x00\x00\x00\x88\x07\xfb\xf9\x08H\xb5\x81\x87\xdc\x02\x90\x04\xb0\xaf\x11\x0c\x9a\rwQ\xd4\xcaiP\x12\x85\xfeMzd\xaf\x00\xaa\x12\xe2\x910\xea\xea\xb6\xf3'), + check_radius_mschap2_reply_fail, + secret="SECRET", + IDENTITIES={"user": "password"} +) diff --git a/test/benchmark/common.py b/test/benchmark/common.py index d95777f2772..6796fedf05a 100644 --- a/test/benchmark/common.py +++ b/test/benchmark/common.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Guillaume Valadon -# This program is published under a GPLv2 license import os import sys diff --git a/test/benchmark/dissection_and_build.py b/test/benchmark/dissection_and_build.py index ac352a6209b..5178fbd0fff 100644 --- a/test/benchmark/dissection_and_build.py +++ b/test/benchmark/dissection_and_build.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Guillaume Valadon -# This program is published under a GPLv2 license from common import * import time diff --git a/test/benchmark/latency_router.py b/test/benchmark/latency_router.py index c1a51fac4f2..24e2861b881 100644 --- a/test/benchmark/latency_router.py +++ b/test/benchmark/latency_router.py @@ -1,7 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy -# See http://www.secdev.org/projects/scapy for more information +# See https://scapy.net/ for more information # Copyright (C) Gabriel Potter -# This program is published under a GPLv2 license # https://github.com/secdev/scapy/issues/1791 diff --git a/test/bluetooth.uts b/test/bluetooth.uts deleted file mode 100644 index 46d4f931abb..00000000000 --- a/test/bluetooth.uts +++ /dev/null @@ -1,367 +0,0 @@ -% Scapy Bluetooth layer tests - -+ Bluetooth tests - -= HCI layers -# a huge packet with all classes in it! -pkt = HCI_ACL_Hdr()/HCI_Cmd_Complete_Read_BD_Addr()/HCI_Cmd_Connect_Accept_Timeout()/HCI_Cmd_Disconnect()/HCI_Cmd_LE_Connection_Update()/HCI_Cmd_LE_Create_Connection()/HCI_Cmd_LE_Create_Connection_Cancel()/HCI_Cmd_LE_Host_Supported()/HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply()/HCI_Cmd_LE_Long_Term_Key_Request_Reply()/HCI_Cmd_LE_Read_Buffer_Size()/HCI_Cmd_LE_Set_Advertise_Enable()/HCI_Cmd_LE_Set_Advertising_Data()/HCI_Cmd_LE_Set_Advertising_Parameters()/HCI_Cmd_LE_Set_Random_Address()/HCI_Cmd_LE_Set_Scan_Enable()/HCI_Cmd_LE_Set_Scan_Parameters()/HCI_Cmd_LE_Start_Encryption_Request()/HCI_Cmd_Read_BD_Addr()/HCI_Cmd_Reset()/HCI_Cmd_Set_Event_Filter()/HCI_Cmd_Set_Event_Mask()/HCI_Command_Hdr()/HCI_Event_Command_Complete()/HCI_Event_Command_Status()/HCI_Event_Disconnection_Complete()/HCI_Event_Encryption_Change()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_Event_Number_Of_Completed_Packets()/HCI_Hdr()/HCI_LE_Meta_Advertising_Reports()/HCI_LE_Meta_Connection_Complete()/HCI_LE_Meta_Connection_Update_Complete()/HCI_LE_Meta_Long_Term_Key_Request() -assert HCI_ACL_Hdr in pkt.layers() -assert HCI_Cmd_Complete_Read_BD_Addr in pkt.layers() -assert HCI_Cmd_Connect_Accept_Timeout in pkt.layers() -assert HCI_Cmd_Disconnect in pkt.layers() -assert HCI_Cmd_LE_Connection_Update in pkt.layers() -assert HCI_Cmd_LE_Create_Connection in pkt.layers() -assert HCI_Cmd_LE_Create_Connection_Cancel in pkt.layers() -assert HCI_Cmd_LE_Host_Supported in pkt.layers() -assert HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply in pkt.layers() -assert HCI_Cmd_LE_Long_Term_Key_Request_Reply in pkt.layers() -assert HCI_Cmd_LE_Read_Buffer_Size in pkt.layers() -assert HCI_Cmd_LE_Set_Advertise_Enable in pkt.layers() -assert HCI_Cmd_LE_Set_Advertising_Data in pkt.layers() -assert HCI_Cmd_LE_Set_Advertising_Parameters in pkt.layers() -assert HCI_Cmd_LE_Set_Random_Address in pkt.layers() -assert HCI_Cmd_LE_Set_Scan_Enable in pkt.layers() -assert HCI_Cmd_LE_Set_Scan_Parameters in pkt.layers() -assert HCI_Cmd_LE_Start_Encryption_Request in pkt.layers() -assert HCI_Cmd_Read_BD_Addr in pkt.layers() -assert HCI_Cmd_Reset in pkt.layers() -assert HCI_Cmd_Set_Event_Filter in pkt.layers() -assert HCI_Cmd_Set_Event_Mask in pkt.layers() -assert HCI_Command_Hdr in pkt.layers() -assert HCI_Event_Command_Complete in pkt.layers() -assert HCI_Event_Command_Status in pkt.layers() -assert HCI_Event_Disconnection_Complete in pkt.layers() -assert HCI_Event_Encryption_Change in pkt.layers() -assert HCI_Event_Hdr in pkt.layers() -assert HCI_Event_LE_Meta in pkt.layers() -assert HCI_Event_Number_Of_Completed_Packets in pkt.layers() -assert HCI_Hdr in pkt.layers() -assert HCI_LE_Meta_Advertising_Reports in pkt.layers() -assert HCI_LE_Meta_Connection_Complete in pkt.layers() -assert HCI_LE_Meta_Connection_Update_Complete in pkt.layers() -assert HCI_LE_Meta_Long_Term_Key_Request in pkt.layers() - -+ Bluetooth Transport Layers -= Test HCI_PHDR_Hdr - -pkt = HCI_PHDR_Hdr()/HCI_Hdr()/HCI_ACL_Hdr()/L2CAP_Hdr()/L2CAP_CmdHdr()/L2CAP_InfoReq() -assert raw(pkt) == b'\x00\x00\x00\x00\x02\x00\x00\n\x00\x06\x00\x05\x00\n\x00\x02\x00\x00\x00' -pkt = HCI_PHDR_Hdr(raw(pkt)) - -assert HCI_Hdr in pkt -assert L2CAP_InfoReq in pkt - -+ HCI Commands - -= LE Create Connection - -# Request data -cmd = HCI_Hdr(hex_bytes("010d2019600060000001123456677890001800280000002a0000000000")) -assert(HCI_Cmd_LE_Create_Connection in cmd) -assert(cmd[HCI_Cmd_LE_Create_Connection].paddr == '90:78:67:56:34:12') -assert(cmd[HCI_Cmd_LE_Create_Connection].patype == 1) - -# Response data -pending = HCI_Hdr(hex_bytes("040f0400020d20")) -assert(pending.answers(cmd)) - -complete = HCI_Hdr(hex_bytes("043e1301020000000112345667789000000000000000")) -assert(HCI_LE_Meta_Connection_Complete in complete) -assert(complete[HCI_LE_Meta_Connection_Complete].paddr == '90:78:67:56:34:12') -assert(complete.answers(cmd)) - -# Invalid combinations -assert(not cmd.answers(cmd)) -assert(not pending.answers(pending)) -assert(not complete.answers(complete)) -assert(not pending.answers(complete)) -assert(not complete.answers(pending)) - -= LE Create Connection Cancel - -# Craft a request... -expected_cmd_raw_data = hex_bytes("010e2000") -cmd = HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_LE_Create_Connection_Cancel() -assert(expected_cmd_raw_data == raw(cmd)) -assert(raw(HCI_Hdr(expected_cmd_raw_data)) == expected_cmd_raw_data) - -other_raw_data = hex_bytes("01060403341213") -other_cmd = HCI_Hdr(other_raw_data) - -# Craft a response... -for p in ( - HCI_Event_Command_Complete(opcode=0x200e), - HCI_Event_Command_Status(opcode=0x200e), -): - res = HCI_Hdr() / HCI_Event_Hdr() / p - # For debugging - res - # Check that the response packet thinks it is an answer to the request - assert(res.answers(cmd)) - # Check that it self isn't a match - assert(not res.answers(res)) - # Check that another request wouldn't match - assert(not res.answers(other_cmd)) - "OK!" - - -= Disconnect -expected_cmd_raw_data = hex_bytes("01060403341213") -cmd_raw_data = raw(HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_Disconnect(handle=0x1234)) -assert(expected_cmd_raw_data == cmd_raw_data) - -= LE Connection Update Command -expected_cmd_raw_data = hex_bytes("0113200e47000a00140001003c000100ffff") -cmd_raw_data = raw( - HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_LE_Connection_Update( - handle=0x47, min_interval=10, max_interval=20, latency=1, timeout=60, - min_ce=1, max_ce=0xffff)) -assert(expected_cmd_raw_data == cmd_raw_data) - - -+ HCI Events -= LE Connection Update Event -evt_raw_data = hex_bytes("043e0a03004800140001003c00") -evt_pkt = HCI_Hdr(evt_raw_data) -assert(evt_pkt[HCI_LE_Meta_Connection_Update_Complete].handle == 0x48) -assert(evt_pkt[HCI_LE_Meta_Connection_Update_Complete].interval == 20) -assert(evt_pkt[HCI_LE_Meta_Connection_Update_Complete].latency == 1) -assert(evt_pkt[HCI_LE_Meta_Connection_Update_Complete].timeout == 60) - - -+ Bluetooth LE Advertising / Scan Response Data Parsing -= Parse EIR_Flags, EIR_CompleteList16BitServiceUUIDs, EIR_CompleteLocalName and EIR_TX_Power_Level - -ad_report_raw_data = \ - hex_bytes("043e2b020100016522c00181781f0201020303d9fe1409" \ - "506562626c652054696d65204c452037314536020a0cde") -scapy_packet = HCI_Hdr(ad_report_raw_data) - -assert(scapy_packet[EIR_Flags].flags == 0x02) -assert(scapy_packet[EIR_CompleteList16BitServiceUUIDs].svc_uuids == [0xfed9]) -assert(scapy_packet[EIR_CompleteLocalName].local_name == b'Pebble Time LE 71E6') -assert(scapy_packet[EIR_TX_Power_Level].level == 12) - -= Parse EIR_Manufacturer_Specific_Data - -scan_resp_raw_data = \ - hex_bytes("043e2302010401be5e0eb9f04f1716ff5401005f423331" \ - "3134374432343631fc00030c0000de") -scapy_packet = HCI_Hdr(scan_resp_raw_data) - -assert(raw(scapy_packet[EIR_Manufacturer_Specific_Data].payload) == b'\x00_B31147D2461\xfc\x00\x03\x0c\x00\x00') -assert(scapy_packet[EIR_Manufacturer_Specific_Data].company_id == 0x154) - -= Parse EIR_Manufacturer_Specific_Data with magic - -class ScapyManufacturerPacket(Packet): - magic = b'SCAPY!' - fields_desc = [ - StrFixedLenField("header", magic, len(magic)), - ShortField("x", None), - ] - -class ScapyManufacturerPacket2(Packet): - magic = b'!SCAPY' - fields_desc = [ - StrFixedLenField("header", magic, len(magic)), - ShortField("y", None), - ] - @classmethod - def magic_check(cls, payload): - return payload.startswith(cls.magic) - -EIR_Manufacturer_Specific_Data.register_magic_payload( - ScapyManufacturerPacket, lambda p: p.startswith(ScapyManufacturerPacket.magic)) -EIR_Manufacturer_Specific_Data.register_magic_payload(ScapyManufacturerPacket2) - -# Test decode -p = EIR_Hdr(b'\x0b\xff\xff\xffSCAPY!\xab\x12') - -p.show() -assert p[EIR_Manufacturer_Specific_Data].company_id == 0xffff -assert p[ScapyManufacturerPacket].x == 0xab12 - -p = EIR_Hdr(b'\x0b\xff\xff\xff!SCAPY\x12\x34') - -p.show() -assert p[EIR_Manufacturer_Specific_Data].company_id == 0xffff -assert p[ScapyManufacturerPacket2].y == 0x1234 - -# Test encode -p = EIR_Hdr()/EIR_Manufacturer_Specific_Data(company_id=0xffff)/ScapyManufacturerPacket(x=0x5678) -assert raw(p) == b'\x0b\xff\xff\xffSCAPY!\x56\x78' - -# Test bad setup -try: - EIR_Manufacturer_Specific_Data.register_magic_payload(conf.raw_layer) -except TypeError: - pass -else: - assert False, "expected exception" - -= Parse EIR_ServiceData16BitUUID - -d = hex_bytes("043e1902010001abcdef7da97f0d020102030350fe051650fee6c2ac") -p = HCI_Hdr(d) - -p.show() -assert p[EIR_CompleteList16BitServiceUUIDs].svc_uuids == [0xfe50] -assert p[EIR_ServiceData16BitUUID].svc_uuid == 0xfe50 -assert raw(p[EIR_ServiceData16BitUUID].payload) == hex_bytes("e6c2") - -= Basic L2CAP dissect -a = L2CAP_Hdr(b'\x08\x00\x06\x00\t\x00\xf6\xe5\xd4\xc3\xb2\xa1') -assert a[SM_Identity_Address_Information].address == 'a1:b2:c3:d4:e5:f6' -assert a[SM_Identity_Address_Information].atype == 0 -a.show() - -= Basic HCI_ACL_Hdr build & dissect -a = HCI_Hdr()/HCI_ACL_Hdr(handle=0xf4c, PB=2, BC=2, len=20)/L2CAP_Hdr(len=16)/L2CAP_CmdHdr(code=8, len=12)/Raw("A"*12) -assert raw(a) == b'\x02L\xaf\x14\x00\x10\x00\x05\x00\x08\x00\x0c\x00AAAAAAAAAAAA' -b = HCI_Hdr(raw(a)) -assert a == b - -= Complex HCI - L2CAP build -a = HCI_Hdr()/HCI_ACL_Hdr()/L2CAP_Hdr()/L2CAP_CmdHdr()/L2CAP_ConnReq(scid=1) -assert raw(a) == b'\x02\x00\x00\x0c\x00\x08\x00\x05\x00\x02\x00\x04\x00\x00\x00\x01\x00' -a.show() - -= Complex HCI - L2CAP dissect -a = HCI_Hdr(b'\x02\x00\x00\x11\x00\r\x00\x05\x00\x0b\x00\t\x00\x01\x00\x00\x00debug') -assert a[L2CAP_InfoResp].result == 0 -assert a[L2CAP_InfoResp].data == b"debug" - -= Answers -a = HCI_Hdr(b'\x02\x00\x00\x0c\x00\x08\x00\x05\x00\x02\x00\x04\x00\x00\x00\x9a;') -b = HCI_Hdr(b'\x02\x00\x00\x10\x00\x0c\x00\x05\x00\x03\x00\x08\x00\xff\xff\x9a;\x00\x00\x01\x00') -assert b.answers(a) -assert not a.answers(b) - -a = HCI_Hdr(b'\x02\x00\x00\x0c\x00\x08\x00\x05\x00\x04\x00\x04\x00\x15\x00\x00\x00') -b = HCI_Hdr(b'\x02\x00\x00\x0e\x00\n\x00\x05\x00\x05\x00\x06\x00\x15\x00\x00\x00\x02\x00') -assert b.answers(a) -assert not a.answers(b) - -= EIR_Hdr - HCI_LE_Meta_Advertising_Report (single report) -a = HCI_Hdr()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_LE_Meta_Advertising_Reports(reports=[ - HCI_LE_Meta_Advertising_Report( - addr="a1:b2:c3:d4:e5:f6", - data=[ - EIR_Hdr()/EIR_Flags(flags=['br_edr_not_supported']), - EIR_Hdr()/EIR_CompleteLocalName(local_name="scapy"), - ] - ) -]) -assert raw(a) == b'\x04>\x16\x02\x01\x00\x00\xf6\xe5\xd4\xc3\xb2\xa1\n\x02\x01\x04\x06\tscapy\x00' -b = HCI_Hdr(raw(a)) -b.show() -assert b[HCI_Event_Hdr].len > 0 -assert b[EIR_CompleteLocalName].local_name == b"scapy" -assert b[HCI_LE_Meta_Advertising_Report].addr == "a1:b2:c3:d4:e5:f6" - -assert a.summary() == "HCI Event / HCI_Event_Hdr / HCI_Event_LE_Meta / HCI_LE_Meta_Advertising_Reports" - -= EIR_Hdr - HCI_LE_Meta_Advertising_Report (duplicate reports) -# When duplicate reports are allowed, there are "Connectable Unidirected -# Advertising" reports, and "Scan Responses", for the same device/MAC, in the -# same packet. - -a = HCI_Hdr()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_LE_Meta_Advertising_Reports(reports=[ - HCI_LE_Meta_Advertising_Report( - addr="a1:b2:c3:d4:e5:f6", - data=[ - EIR_Hdr()/EIR_Flags(flags=['br_edr_not_supported']), - EIR_Hdr()/EIR_CompleteLocalName(local_name="scapy"), - ] - ), - HCI_LE_Meta_Advertising_Report( - type=4, # Scan Response - addr="a1:b2:c3:d4:e5:f6", - data=[ - EIR_Hdr()/EIR_Manufacturer_Specific_Data( - company_id=0xffff, - )/Raw(b"ypacs"), - EIR_Hdr()/EIR_TX_Power_Level(level=10), - EIR_Hdr()/EIR_CompleteList128BitServiceUUIDs(svc_uuids=[ - "01234567-89ab-cdef-1023-456789abcdfe", - ]) - ] - ) -]) -assert raw(a) == b'\x04>>\x02\x02\x00\x00\xf6\xe5\xd4\xc3\xb2\xa1\n\x02\x01\x04\x06\tscapy\x00\x04\x00\xf6\xe5\xd4\xc3\xb2\xa1\x1e\x08\xff\xff\xffypacs\x02\n\n\x11\x07\xfe\xcd\xab\x89gE#\x10\xef\xcd\xab\x89gE#\x01\x00' - -b = HCI_Hdr(raw(a)) -b.show() -assert b[HCI_Event_Hdr].len > 0 -assert b[EIR_CompleteLocalName].local_name == b"scapy" -assert b[HCI_LE_Meta_Advertising_Report].addr == "a1:b2:c3:d4:e5:f6" -assert b[EIR_Manufacturer_Specific_Data].company_id == 0xffff -assert raw(b[EIR_Manufacturer_Specific_Data].payload) == b"ypacs" -assert b[EIR_TX_Power_Level].level == 10 -assert b[EIR_CompleteList128BitServiceUUIDs].svc_uuids[0] == UUID("01234567-89ab-cdef-1023-456789abcdfe") - -assert a.summary() == "HCI Event / HCI_Event_Hdr / HCI_Event_LE_Meta / HCI_LE_Meta_Advertising_Reports" - -= ATT_Hdr - misc -a = HCI_Hdr()/HCI_ACL_Hdr()/L2CAP_Hdr()/ATT_Hdr()/ATT_Read_By_Type_Request_128bit(uuid1=0xa14, uuid2=0xa24) -a = HCI_Hdr(raw(a)) -a.show() -a.mysummary() -assert ATT_Read_By_Type_Request_128bit in a -assert not Raw in a - -b = HCI_Hdr()/HCI_ACL_Hdr()/L2CAP_Hdr()/ATT_Hdr()/ATT_Read_By_Type_Request(uuid=0xa14) -b = HCI_Hdr(raw(b)) -b.show() -b.mysummary() -assert ATT_Read_By_Type_Request in b -assert not Raw in b - -= ATT Read_By_Type_Response - -pkt = HCI_Hdr(hex_bytes('0248201b001700040009070200020300002a0400020500012a0600020700042a')) - -assert pkt[ATT_Read_By_Type_Response].len == 7 -assert len(pkt.handles) == 3 -assert pkt.handles[0].handle == 0x2 -assert pkt.handles[1].handle == 0x4 -assert pkt.handles[2].handle == 0x6 - -pkt.handles[0].value == b'\x02\x03\x00\x00*' -pkt.handles[1].value == b'\x02\x05\x00\x01*' -pkt.handles[2].value == b'\x02\x07\x00\x04*' - -= L2CAP layers - -# a crazy packet with all classes in it! -pkt = L2CAP_CmdHdr()/L2CAP_CmdRej()/L2CAP_ConfReq()/L2CAP_ConfResp()/L2CAP_ConnReq()/L2CAP_ConnResp()/L2CAP_Connection_Parameter_Update_Request()/L2CAP_Connection_Parameter_Update_Response()/L2CAP_DisconnReq()/L2CAP_DisconnResp()/L2CAP_Hdr()/L2CAP_InfoReq()/L2CAP_InfoResp() -assert L2CAP_CmdHdr in pkt.layers() -assert L2CAP_CmdRej in pkt.layers() -assert L2CAP_ConfReq in pkt.layers() -assert L2CAP_ConfResp in pkt.layers() -assert L2CAP_ConnReq in pkt.layers() -assert L2CAP_ConnResp in pkt.layers() -assert L2CAP_Connection_Parameter_Update_Request in pkt.layers() -assert L2CAP_Connection_Parameter_Update_Response in pkt.layers() -assert L2CAP_DisconnReq in pkt.layers() -assert L2CAP_DisconnResp in pkt.layers() -assert L2CAP_Hdr in pkt.layers() -assert L2CAP_InfoReq in pkt.layers() -assert L2CAP_InfoResp in pkt.layers() - -= SM_Public_Key() tests - -r = raw(SM_Hdr()/SM_Public_Key(key_x="sca", key_y="py")) -assert r == b'\x0csca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00py\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -p = SM_Hdr(r) -assert SM_Public_Key in p and p.key_x[:3] == b"sca" and p.key_y[:2] == b"py" - -= SM_DHKey_Check() tests - -r = raw(SM_Hdr()/SM_DHKey_Check(dhkey_check="scapy")) -assert r == b'\rscapy\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -p = SM_Hdr(r) -assert SM_DHKey_Check in p and p.dhkey_check[:5] == b"scapy" diff --git a/test/bluetooth4LE.uts b/test/bluetooth4LE.uts deleted file mode 100644 index 6a358975dfc..00000000000 --- a/test/bluetooth4LE.uts +++ /dev/null @@ -1,106 +0,0 @@ -% Regression tests for the bluetooth4LE layer - -################################## -#### Bluetooth 4.0 Low Energy #### -################################## - -+ BTLE tests - -= Default build - -a = BTLE()/BTLE_ADV()/BTLE_ADV_IND() -assert raw(a) == b'\xd6\xbe\x89\x8e\x00\x06\x00\x00\x00\x00\x00\x00Z9`' - -= Basic dissection - -b = BTLE(raw(a)) -assert b.crc == 0x5a3960 -assert b[BTLE_ADV_IND].AdvA == '00:00:00:00:00:00' - -= BTLE_DATA build - -a = BTLE(access_addr=0)/BTLE_DATA()/"toto" -a = BTLE(raw(a)) -assert a[BTLE_DATA].len == 4 -assert a[Raw].load == b"toto" - -= BTLE_DATA + EIR_ShortenedLocalName - -test1 = BTLE() / BTLE_ADV() / BTLE_ADV_IND() / EIR_Hdr() / EIR_ShortenedLocalName(local_name= 'wussa') -test1e = BTLE(raw(test1)) -assert test1e[EIR_ShortenedLocalName].local_name == b"wussa" - -= BTLE_DATA + CtrlPDU - -test2 = BTLE(access_addr=1) / BTLE_DATA() / CtrlPDU(version=7) -test2e = BTLE(raw(test2)) -assert test2e[CtrlPDU].version == 7 - -= BTLE_DATA + ATT_PrepareWriteReq - -test3 = BTLE(access_addr=1) / BTLE_DATA() / L2CAP_Hdr() / ATT_Hdr() / ATT_Prepare_Write_Request(gatt_handle = 0xa, data="test") -test3e = BTLE(raw(test3)) -assert test3e[ATT_Prepare_Write_Request].data == b"test" -assert test3e[ATT_Prepare_Write_Request].gatt_handle == 0xa -assert test3e[ATT_Hdr].opcode == 0x16 - - -= BTLE layers -# a crazy packet with all classes in it! -pkt = BTLE()/BTLE_ADV()/BTLE_ADV_DIRECT_IND()/BTLE_ADV_IND()/BTLE_ADV_NONCONN_IND()/BTLE_ADV_SCAN_IND()/BTLE_CONNECT_REQ()/BTLE_DATA()/BTLE_PPI()/BTLE_SCAN_REQ()/BTLE_SCAN_RSP() -assert BTLE in pkt.layers() -assert BTLE_ADV in pkt.layers() -assert BTLE_ADV_DIRECT_IND in pkt.layers() -assert BTLE_ADV_IND in pkt.layers() -assert BTLE_ADV_NONCONN_IND in pkt.layers() -assert BTLE_ADV_SCAN_IND in pkt.layers() -assert BTLE_CONNECT_REQ in pkt.layers() -assert BTLE_DATA in pkt.layers() -assert BTLE_PPI in pkt.layers() -assert BTLE_SCAN_REQ in pkt.layers() -assert BTLE_SCAN_RSP in pkt.layers() - -= BTLE_RF link - -a = BTLE_RF()/BTLE()/BTLE_ADV()/BTLE_SCAN_REQ() -a.ScanA = "aa:aa:aa:aa:aa:aa" -a.AdvA = "bb:bb:bb:bb:bb:bb" -a.flags = 0x10 -a.reference_access_address = 0x8e89bed6 -a.noise = -90 -a.signal = -75 -a.rf_channel = 6 -a.access_address_offenses = 10 -assert raw(a) == b'\x06\xb5\xa6\n\xd6\xbe\x89\x8e\x10\x00\xd6\xbe\x89\x8e\x03\x0c\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\x07\xb2a' - -a = BTLE_RF(raw(a)) - -assert a.noise == -90 -assert a.signal == -75 -assert a.flags == "reference_access_address_valid" -assert a[BTLE_SCAN_REQ].ScanA == "aa:aa:aa:aa:aa:aa" - -+ Specific tests after issue GH#1673 - -= DLT_USER0 with PPI - -pkt = PPI(b'\x00\x00\x18\x00\x93\x00\x00\x006u\x0c\x00\x00b\t\x00\xe1\xcf\x01\x00\xf1\xe3\x92\x00\xd6\xbe\x89\x8e@\x14M\x95P\x16\xfev\x02\x01\x1a\n\xffL\x00\x10\x05\x0b\x1c\x0e\xa86Z\xf0\x04') -assert BTLE_PPI in pkt.headers[0].payload - -# We MUST NOT detect the BLTE link-layer at this point. This is intentionally -# counter to issue 1673 -- Ubertooth One emits incorrect PCAP files. -assert BTLE not in pkt - -= DLT_BLUETOOTH_LE_LL with PPI - -pkt = PPI(b'\x00\x00\x18\x00\xfb\x00\x00\x006u\x0c\x00\x00b\t\x00\xe1\xcf\x01\x00\xf1\xe3\x92\x00\xd6\xbe\x89\x8e@\x14M\x95P\x16\xfev\x02\x01\x1a\n\xffL\x00\x10\x05\x0b\x1c\x0e\xa86Z\xf0\x04') -assert BTLE_PPI in pkt.headers[0].payload - -# Only now must we detect BTLE. -assert BTLE in pkt - -= DLT_BLUETOOTH_LE_LL without PPI - -pkt = BTLE_RF(b'\x00\xc6\x80\x00\xd6\xbe\x89\x8e7\x00\xd6\xbe\x89\x8e@\x14\x03g\xa6+\xcbi\x00\x01\x1a\n\xffL\x00\x12E\x03\x18y\x9e\x96\x07\xfa%') -assert BTLE_RF in pkt -assert BTLE in pkt diff --git a/test/bpf.uts b/test/bpf.uts index 4c5a638321b..5c7fb861973 100644 --- a/test/bpf.uts +++ b/test/bpf.uts @@ -12,19 +12,23 @@ get_if_raw_addr(conf.iface) -= Get the packed MAC address of conf.iface += Get the MAC address of conf.iface -get_if_raw_hwaddr(conf.iface) +get_if_hwaddr(conf.iface) -= Get the packed MAC address of LOOPBACK_NAME += Get the MAC address of conf.loopback_name -get_if_raw_hwaddr(LOOPBACK_NAME) == (ARPHDR_LOOPBACK, b'\x00'*6) +get_if_hwaddr(conf.loopback_name) == "00:00:00:00:00:00" ############ ############ + BPF related functions += Imports + +from scapy.arch.bpf.supersocket import L3bpfSocket, L2bpfListenSocket, L2bpfSocket + = Get a BPF handler ~ needs_root @@ -32,7 +36,7 @@ from scapy.arch.bpf.supersocket import get_dev_bpf fd, _ = get_dev_bpf() = Attach a BPF filter -~ needs_root +~ needs_root libpcap from scapy.arch.bpf.supersocket import attach_filter attach_filter(fd, "arp or icmp", conf.iface) @@ -43,48 +47,10 @@ attach_filter(fd, "arp or icmp", conf.iface) iflist = get_if_list() len(iflist) > 0 - -= Get working network interfaces -~ needs_root - -from scapy.arch.bpf.core import get_working_if, get_working_ifaces -ifworking = get_working_ifaces() -assert len(ifworking) -assert get_working_if() == ifworking[0] - - -= Get working network interfaces order - -import mock -from scapy.arch.bpf.core import get_working_ifaces - -@mock.patch("scapy.arch.bpf.core.os.close") -@mock.patch("scapy.arch.bpf.core.fcntl.ioctl") -@mock.patch("scapy.arch.bpf.core.get_dev_bpf") -@mock.patch("scapy.arch.bpf.core.get_if") -@mock.patch("scapy.arch.bpf.core.get_if_list") -@mock.patch("scapy.arch.bpf.core.os.getuid") -def test_get_working_ifaces(mock_getuid, mock_get_if_list, mock_get_if, - mock_get_dev_bpf, mock_ioctl, mock_close): - mock_getuid.return_value = 0 - mock_get_if_list.return_value = ['igb0', 'em0', 'msk0', 'epair0a', 'igb1', - 'vlan20', 'igb10', 'igb2'] - mock_get_if.return_value = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') - mock_get_dev_bpf.return_value = (31337,) - mock_ioctl.return_value = 0 - mock_close.return_value = 0 - return get_working_ifaces() - -assert test_get_working_ifaces() == ['em0', 'igb0', 'msk0', 'epair0a', 'igb1', - 'igb2', 'igb10', 'vlan20'] - = Misc functions ~ needs_root -from scapy.arch.bpf.supersocket import isBPFSocket, bpf_select -isBPFSocket(L2bpfListenSocket()) and isBPFSocket(L2bpfSocket()) and isBPFSocket(L3bpfSocket()) +from scapy.arch.bpf.supersocket import bpf_select l = bpf_select([L2bpfSocket()]) l = bpf_select([L2bpfSocket(), sys.stdin.fileno()]) @@ -148,7 +114,7 @@ s.close() = L2bpfListenSocket - read failure ~ needs_root -import mock +from unittest import mock @mock.patch("scapy.arch.bpf.supersocket.os.read") def _test_osread(osread): @@ -176,5 +142,25 @@ s = L3bpfSocket() s.send(IP(dst="8.8.8.8")/ICMP()) s = L3bpfSocket() -s.assigned_interface = LOOPBACK_NAME +s.assigned_interface = conf.loopback_name s.send(IP(dst="8.8.8.8")/ICMP()) + += L3bpfSocket - send and sniff on loopback +~ needs_root + +localhost_ip = conf.ifaces[conf.loopback_name].ips[4][0] + +def cb(): + # Send a ping to the loopback IP. + s = L3bpfSocket(iface=conf.loopback_name) + s.send(IP(dst=localhost_ip)/ICMP(seq=1001)) + +t = AsyncSniffer(iface=conf.loopback_name, started_callback=cb) +t.start() +time.sleep(1) +t.stop() +t.join(timeout=1) + +# We expect to see our packet and kernel's response. +len(t.results.filter(lambda p: ( + IP in p and ICMP in p and (p[IP].src == localhost_ip or p[IP].dst == localhost_ip) and p[ICMP].seq == 1001))) == 2 diff --git a/test/cert.uts b/test/cert.uts deleted file mode 100644 index 49e80ca8905..00000000000 --- a/test/cert.uts +++ /dev/null @@ -1,675 +0,0 @@ -# Cert extension - Regression Test Campaign - -# Try me with: -# bash test/run_tests -t test/cert.uts -F - -~ crypto - -########### PKCS helpers ############################################### - -+ PKCS helpers tests - -= PKCS os2ip basic tests -pkcs_os2ip(b'\x00\x00\xff\xff') == 0xffff and pkcs_os2ip(b'\xff\xff\xff\xff\xff') == 0xffffffffff - -= PKCS i2osp basic tests -pkcs_i2osp(0xffff, 4) == b'\x00\x00\xff\xff' and pkcs_i2osp(0xffff, 2) == b'\xff\xff' and pkcs_i2osp(0xffffeeee, 3) == b'\xff\xff\xee\xee' - - -########### PubKey class ############################################### - -+ PubKey class tests - -= PubKey class : Importing PEM-encoded RSA public key -x = PubKey(""" ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmFdqP+nTEZukS0lLP+yj -1gNImsEIf7P2ySTunceYxwkm4VE5QReDbb2L5/HLA9pPmIeQLSq/BgO1meOcbOSJ -2YVHQ28MQ56+8Crb6n28iycX4hp0H3AxRAjh0edX+q3yilvYJ4W9/NnIb/wAZwS0 -oJif/tTkVF77HybAfJde5Eqbp+bCKIvMWnambh9DRUyjrBBZo5dA1o32zpuFBrJd -I8dmUpw9gtf0F0Ba8lGZm8Uqc0GyXeXOJUE2u7CiMu3M77BM6ZLLTcow5+bQImkm -TL1SGhzwfinME1e6p3Hm//pDjuJvFaY22k05LgLuyqc59vFiB3Toldz8+AbMNjvz -AwIDAQAB ------END PUBLIC KEY----- -""") -x_pubNum = x.pubkey.public_numbers() -type(x) is PubKeyRSA - -= PubKey class : Verifying PEM key format -x.frmt == "PEM" - -= PubKey class : Importing DER-encoded RSA Key -y = PubKey(b'0\x82\x01\"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x000\x82\x01\n\x02\x82\x01\x01\x00\x98Wj?\xe9\xd3\x11\x9b\xa4KIK?\xec\xa3\xd6\x03H\x9a\xc1\x08\x7f\xb3\xf6\xc9$\xee\x9d\xc7\x98\xc7\t&\xe1Q9A\x17\x83m\xbd\x8b\xe7\xf1\xcb\x03\xdaO\x98\x87\x90-*\xbf\x06\x03\xb5\x99\xe3\x9cl\xe4\x89\xd9\x85GCo\x0cC\x9e\xbe\xf0*\xdb\xea}\xbc\x8b\'\x17\xe2\x1at\x1fp1D\x08\xe1\xd1\xe7W\xfa\xad\xf2\x8a[\xd8\'\x85\xbd\xfc\xd9\xc8o\xfc\x00g\x04\xb4\xa0\x98\x9f\xfe\xd4\xe4T^\xfb\x1f&\xc0|\x97^\xe4J\x9b\xa7\xe6\xc2(\x8b\xccZv\xa6n\x1fCEL\xa3\xac\x10Y\xa3\x97@\xd6\x8d\xf6\xce\x9b\x85\x06\xb2]#\xc7fR\x9c=\x82\xd7\xf4\x17@Z\xf2Q\x99\x9b\xc5*sA\xb2]\xe5\xce%A6\xbb\xb0\xa22\xed\xcc\xef\xb0L\xe9\x92\xcbM\xca0\xe7\xe6\xd0\"i&L\xbdR\x1a\x1c\xf0~)\xcc\x13W\xba\xa7q\xe6\xff\xfaC\x8e\xe2o\x15\xa66\xdaM9.\x02\xee\xca\xa79\xf6\xf1b\x07t\xe8\x95\xdc\xfc\xf8\x06\xcc6;\xf3\x03\x02\x03\x01\x00\x01') -y_pubNum = y.pubkey.public_numbers() -type(y) is PubKeyRSA - -= PubKey class : Verifying DER key format -y.frmt == "DER" - -= PubKey class : Checking modulus value -x_pubNum.n == y_pubNum.n and x_pubNum.n == 19231328316532061413420367242571475005688288081144416166988378525696075445024135424022026378563116068168327239354659928492979285632474448448624869172454076124150405352043642781483254546569202103296262513098482624188672299255268092629150366527784294463900039290024710152521604731213565912934889752122898104556895316819303096201441834849255370122572613047779766933573375974464479123135292080801384304131606933504677232323037116557327478512106367095125103346134248056463878553619525193565824925835325216545121044922690971718737998420984924512388011040969150550056783451476150234324593710633552558175109683813482739004163 - -= PubKey class : Checking public exponent value -x_pubNum.e == y_pubNum.e and x_pubNum.e == 65537 - -= PubKey class : Importing PEM-encoded ECDSA public key -z = PubKey(""" ------BEGIN PUBLIC KEY----- -MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE55WjbZjS/88K1kYagsO9wtKifw0IKLp4 -Jd5qtmDF2Zu+xrwrBRT0HBnPweDU+RsFxcyU/QxD9WYORzYarqxbcA== ------END PUBLIC KEY----- -""") -type(z) is PubKeyECDSA - -= PubKey class : Checking curve -z.pubkey.curve.name == "secp256k1" - -= PubKey class : Checking point value -z.pubkey.public_numbers().x == 104748656174769496952370005421566518252704263000192720134585149244759951661467 - -= PubKeyRSA class : Generate without modulus -t = PubKeyRSA() -t.fill_and_store(modulus=None, pubExp=32769, modulusLen=1024) -assert t.pubkey.key_size == 1024 -assert t.pubkey.public_numbers().e == 32769 - -########### PrivKey class ############################################### - -+ PrivKey class tests - -= PrivKey class : Importing PEM-encoded RSA private key -x = PrivKey(""" ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAmFdqP+nTEZukS0lLP+yj1gNImsEIf7P2ySTunceYxwkm4VE5 -QReDbb2L5/HLA9pPmIeQLSq/BgO1meOcbOSJ2YVHQ28MQ56+8Crb6n28iycX4hp0 -H3AxRAjh0edX+q3yilvYJ4W9/NnIb/wAZwS0oJif/tTkVF77HybAfJde5Eqbp+bC -KIvMWnambh9DRUyjrBBZo5dA1o32zpuFBrJdI8dmUpw9gtf0F0Ba8lGZm8Uqc0Gy -XeXOJUE2u7CiMu3M77BM6ZLLTcow5+bQImkmTL1SGhzwfinME1e6p3Hm//pDjuJv -FaY22k05LgLuyqc59vFiB3Toldz8+AbMNjvzAwIDAQABAoIBAH3KeJZL2hhI/1GX -NMaU/PfDgFkgmYbxMA8JKusnm/SFjxAwBGnGI6UjBXpBgpQs2Nqm3ZseF9u8hmCK -vGiCEX2GesCo2mSfmSQxD6RBrMTuQ99UXpxzBIscFnM/Zrs8lPBARGzmF2nI3qPx -Xtex4ABX5o0Cd4NfZlZjpj96skUoO8+bd3I4OPUFYFFFuv81LoSQ6Hew0a8xtJXt -KkDp9h1jTGGUOc189WACNoBLH0MGeVoSUfc1++RcC3cypUZ8fNP1OO6GBfv06f5o -XES4ZbxGYpa+nCfNwb6V2gWbkvaYm7aFn0KWGNZXS1P3OcWv6IWdOmg2CI7MMBLJ -0LyWVCECgYEAyMJYw195mvHl8VyxJ3HkxeQaaozWL4qhNQ0Kaw+mzD+jYdkbHb3a -BYghsgEDZjnyOVblC7I+4smvAZJLWJaf6sZ5HAw3zmj1ibCkXx7deoRc/QVcOikl -3dE/ymO0KGJNiGzJZmxbRS3hTokmVPuxSWW4p5oSiMupFHKa18Uv8DECgYEAwkJ7 -iTOUL6b4e3lQuHQnJbsiQpd+P/bsIPP7kaaHObewfHpfOOtIdtN4asxVFf/PgW5u -WmBllqAHZYR14DEYIdL+hdLrdvk5nYQ3YfhOnp+haHUPCdEiXrRZuGXjmMA4V0hL -3HPF5ZM8H80fLnN8Pgn2rIC7CZQ46y4PnoV1nXMCgYBBwCUCF8rkDEWa/ximKo8a -oNJmAypC98xEa7j1x3KBgnYoHcrbusok9ajTe7F5UZEbZnItmnsuG4/Nm/RBV1OY -uNgBb573YzjHl6q93IX9EkzCMXc7NS7JrzaNOopOj6OFAtwTR3m89oHMDu8W9jfi -KgaIHdXkJ4+AuugrstE4gQKBgFK0d1/8g7SeA+Cdz84YNaqMt5NeaDPXbsTA23Qx -UBU0rYDxoKTdFybv9a6SfA83sCLM31K/A8FTNJL2CDGA9WNBL3fOSs2GYg88AVBG -pUJHeDK+0748OcPUSPaG+pVIETSn5RRgffq16r0nWYUvSdAn8cuTqw3y+yC1pZS6 -AU8dAoGBAL5QCi0dTWKN3kf3cXaCAnYiWe4Qg2S+SgLE+F1U4Xws2rqAuSvIiuT5 -i5+Mqk9ZCGdoReVbAovJFoRqe7Fj9yWM+b1awGjL0bOTtnqx0iljob6uFyhpl1xg -W3a3ICJ/ZYLvkgb4IBEteOwWpp37fX57vzhW8EmUV2UX7ve1uNRI ------END RSA PRIVATE KEY----- -""") -x_privNum = x.key.private_numbers() -x_pubNum = x.pubkey.public_numbers() -type(x) is PrivKeyRSA - -= PrivKey class : Checking public attributes -assert(x_pubNum.n == 19231328316532061413420367242571475005688288081144416166988378525696075445024135424022026378563116068168327239354659928492979285632474448448624869172454076124150405352043642781483254546569202103296262513098482624188672299255268092629150366527784294463900039290024710152521604731213565912934889752122898104556895316819303096201441834849255370122572613047779766933573375974464479123135292080801384304131606933504677232323037116557327478512106367095125103346134248056463878553619525193565824925835325216545121044922690971718737998420984924512388011040969150550056783451476150234324593710633552558175109683813482739004163) -x_pubNum.e == 65537 - -= PrivKey class : Checking private attributes -assert(x_privNum.p == 140977881300857803928857666115326329496639762170623218602431133528876162476487960230341078724702018316260690172014674492782486113504117653531825010840338251572887403113276393351318549036549656895326851872473595350667293402676143426484331639796163189182788306480699144107905869179435145810212051656274284113969) -assert(x_privNum.q == 136413798668820291889092636919077529673097927884427227010121877374504825870002258140616512268521246045642663981036167305976907058413796938050224182519965099316625879807962173794483933183111515251808827349718943344770056106787713032506379905031673992574818291891535689493330517205396872699985860522390496583027) -assert(x_privNum.dmp1 == 46171616708754015342920807261537213121074749458020000367465429453038710215532257783908950878847126373502288079285334594398328912526548076894076506899568491565992572446455658740752572386903609191774044411412991906964352741123956581870694330173563737928488765282233340389888026245745090096745219902501964298369) -assert(x_privNum.dmq1 == 58077388505079936284685944662039782610415160654764308528562806086690474868010482729442634318267235411531220690585030443434512729356878742778542733733189895801341155353491318998637269079682889033003797865508917973141494201620317820971253064836562060222814287812344611566640341960495346782352037479526674026269) -x_privNum.d == 15879630313397508329451198152673380989865598204237760057319927734227125481903063742175442230739018051313441697936698689753842471306305671266572085925009572141819112648211571007521954312641597446020984266846581125287547514750428503480880603089110687015181510081018160579576523796170439894692640171752302225125980423560965987469457505107324833137678663960560798216976668670722016960863268272661588745006387723814962668678285659376534048525020951633874488845649968990679414325096323920666486328886913648207836459784281744709948801682209478580185160477801656666089536527545026197569990716720623647770979759861119273292833 - -= PrivKey class : Importing PEM-encoded ECDSA private key -y = PrivKey(""" ------BEGIN EC PRIVATE KEY----- -MHQCAQEEIMiRlFoy6046m1NXu911ukXyjDLVgmOXWCKWdQMd8gCRoAcGBSuBBAAK -oUQDQgAE55WjbZjS/88K1kYagsO9wtKifw0IKLp4Jd5qtmDF2Zu+xrwrBRT0HBnP -weDU+RsFxcyU/QxD9WYORzYarqxbcA== ------END EC PRIVATE KEY----- -""") -type(y) is PrivKeyECDSA - -= PrivKeyECDSA sign & verify -~ crypto_advanced -a = PrivKeyECDSA() -a.fill_and_store() -msg = b"Scapy test message" -data = a.sign(msg) -assert a.verify(msg, data) -assert not a.verify(b"Hello", data) - -= PubKeyECDSA verify -~ crypto_advanced -b = PubKeyECDSA() -b.pubkey = a.pubkey -assert b.verify(msg, data) -assert not b.verify(b"Hello", data) - -= PrivKey class : Importing DER-encoded RSA private key -a = PrivKeyRSA(b'0\x82\x04\xa3\x02\x01\x00\x02\x82\x01\x01\x00\x98Wj?\xe9\xd3\x11\x9b\xa4KIK?\xec\xa3\xd6\x03H\x9a\xc1\x08\x7f\xb3\xf6\xc9$\xee\x9d\xc7\x98\xc7\t&\xe1Q9A\x17\x83m\xbd\x8b\xe7\xf1\xcb\x03\xdaO\x98\x87\x90-*\xbf\x06\x03\xb5\x99\xe3\x9cl\xe4\x89\xd9\x85GCo\x0cC\x9e\xbe\xf0*\xdb\xea}\xbc\x8b\'\x17\xe2\x1at\x1fp1D\x08\xe1\xd1\xe7W\xfa\xad\xf2\x8a[\xd8\'\x85\xbd\xfc\xd9\xc8o\xfc\x00g\x04\xb4\xa0\x98\x9f\xfe\xd4\xe4T^\xfb\x1f&\xc0|\x97^\xe4J\x9b\xa7\xe6\xc2(\x8b\xccZv\xa6n\x1fCEL\xa3\xac\x10Y\xa3\x97@\xd6\x8d\xf6\xce\x9b\x85\x06\xb2]#\xc7fR\x9c=\x82\xd7\xf4\x17@Z\xf2Q\x99\x9b\xc5*sA\xb2]\xe5\xce%A6\xbb\xb0\xa22\xed\xcc\xef\xb0L\xe9\x92\xcbM\xca0\xe7\xe6\xd0"i&L\xbdR\x1a\x1c\xf0~)\xcc\x13W\xba\xa7q\xe6\xff\xfaC\x8e\xe2o\x15\xa66\xdaM9.\x02\xee\xca\xa79\xf6\xf1b\x07t\xe8\x95\xdc\xfc\xf8\x06\xcc6;\xf3\x03\x02\x03\x01\x00\x01\x02\x82\x01\x00}\xcax\x96K\xda\x18H\xffQ\x974\xc6\x94\xfc\xf7\xc3\x80Y \x99\x86\xf10\x0f\t*\xeb\'\x9b\xf4\x85\x8f\x100\x04i\xc6#\xa5#\x05zA\x82\x94,\xd8\xda\xa6\xdd\x9b\x1e\x17\xdb\xbc\x86`\x8a\xbch\x82\x11}\x86z\xc0\xa8\xdad\x9f\x99$1\x0f\xa4A\xac\xc4\xeeC\xdfT^\x9cs\x04\x8b\x1c\x16s?f\xbb<\x94\xf0@Dl\xe6\x17i\xc8\xde\xa3\xf1^\xd7\xb1\xe0\x00W\xe6\x8d\x02w\x83_fVc\xa6?z\xb2E(;\xcf\x9bwr88\xf5\x05`QE\xba\xff5.\x84\x90\xe8w\xb0\xd1\xaf1\xb4\x95\xed*@\xe9\xf6\x1dcLa\x949\xcd|\xf5`\x026\x80K\x1fC\x06yZ\x12Q\xf75\xfb\xe4\\\x0bw2\xa5F||\xd3\xf58\xee\x86\x05\xfb\xf4\xe9\xfeh\\D\xb8e\xbcFb\x96\xbe\x9c\'\xcd\xc1\xbe\x95\xda\x05\x9b\x92\xf6\x98\x9b\xb6\x85\x9fB\x96\x18\xd6WKS\xf79\xc5\xaf\xe8\x85\x9d:h6\x08\x8e\xcc0\x12\xc9\xd0\xbc\x96T!\x02\x81\x81\x00\xc8\xc2X\xc3_y\x9a\xf1\xe5\xf1\\\xb1\'q\xe4\xc5\xe4\x1aj\x8c\xd6/\x8a\xa15\r\nk\x0f\xa6\xcc?\xa3a\xd9\x1b\x1d\xbd\xda\x05\x88!\xb2\x01\x03f9\xf29V\xe5\x0b\xb2>\xe2\xc9\xaf\x01\x92KX\x96\x9f\xea\xc6y\x1c\x0c7\xceh\xf5\x89\xb0\xa4_\x1e\xddz\x84\\\xfd\x05\\:)%\xdd\xd1?\xcac\xb4(bM\x88l\xc9fl[E-\xe1N\x89&T\xfb\xb1Ie\xb8\xa7\x9a\x12\x88\xcb\xa9\x14r\x9a\xd7\xc5/\xf01\x02\x81\x81\x00\xc2B{\x893\x94/\xa6\xf8{yP\xb8t\'%\xbb"B\x97~?\xf6\xec \xf3\xfb\x91\xa6\x879\xb7\xb0|z_8\xebHv\xd3xj\xccU\x15\xff\xcf\x81nnZ`e\x96\xa0\x07e\x84u\xe01\x18!\xd2\xfe\x85\xd2\xebv\xf99\x9d\x847a\xf8N\x9e\x9f\xa1hu\x0f\t\xd1"^\xb4Y\xb8e\xe3\x98\xc08WHK\xdcs\xc5\xe5\x93<\x1f\xcd\x1f.s|>\t\xf6\xac\x80\xbb\t\x948\xeb.\x0f\x9e\x85u\x9ds\x02\x81\x80A\xc0%\x02\x17\xca\xe4\x0cE\x9a\xff\x18\xa6*\x8f\x1a\xa0\xd2f\x03*B\xf7\xccDk\xb8\xf5\xc7r\x81\x82v(\x1d\xca\xdb\xba\xca$\xf5\xa8\xd3{\xb1yQ\x91\x1bfr-\x9a{.\x1b\x8f\xcd\x9b\xf4AWS\x98\xb8\xd8\x01o\x9e\xf7c8\xc7\x97\xaa\xbd\xdc\x85\xfd\x12L\xc21w;5.\xc9\xaf6\x8d:\x8aN\x8f\xa3\x85\x02\xdc\x13Gy\xbc\xf6\x81\xcc\x0e\xef\x16\xf67\xe2*\x06\x88\x1d\xd5\xe4\'\x8f\x80\xba\xe8+\xb2\xd18\x81\x02\x81\x80R\xb4w_\xfc\x83\xb4\x9e\x03\xe0\x9d\xcf\xce\x185\xaa\x8c\xb7\x93^h3\xd7n\xc4\xc0\xdbt1P\x154\xad\x80\xf1\xa0\xa4\xdd\x17&\xef\xf5\xae\x92|\x0f7\xb0"\xcc\xdfR\xbf\x03\xc1S4\x92\xf6\x081\x80\xf5cA/w\xceJ\xcd\x86b\x0f<\x01PF\xa5BGx2\xbe\xd3\xbe<9\xc3\xd4H\xf6\x86\xfa\x95H\x114\xa7\xe5\x14`}\xfa\xb5\xea\xbd\'Y\x85/I\xd0\'\xf1\xcb\x93\xab\r\xf2\xfb \xb5\xa5\x94\xba\x01O\x1d\x02\x81\x81\x00\xbeP\n-\x1dMb\x8d\xdeG\xf7qv\x82\x02v"Y\xee\x10\x83d\xbeJ\x02\xc4\xf8]T\xe1|,\xda\xba\x80\xb9+\xc8\x8a\xe4\xf9\x8b\x9f\x8c\xaaOY\x08ghE\xe5[\x02\x8b\xc9\x16\x84j{\xb1c\xf7%\x8c\xf9\xbdZ\xc0h\xcb\xd1\xb3\x93\xb6z\xb1\xd2)c\xa1\xbe\xae\x17(i\x97\\`[v\xb7 "\x7fe\x82\xef\x92\x06\xf8 \x11-x\xec\x16\xa6\x9d\xfb}~{\xbf8V\xf0I\x94We\x17\xee\xf7\xb5\xb8\xd4H') -a_privNum = a.key.private_numbers() -a_pubNum = a.pubkey.public_numbers() - -assert(x_pubNum.n == x_pubNum.n) -assert(x_pubNum.e == x_pubNum.e) - -assert(x_privNum.p == a_privNum.p) -assert(x_privNum.q == a_privNum.q) -assert(x_privNum.dmp1 == a_privNum.dmp1) -assert(x_privNum.dmq1 == a_privNum.dmq1) -assert(x_privNum.d == a_privNum.d) - -= PrivKey class : Checking public attributes -assert(y.key.curve.name == "secp256k1") -y.key.public_key().public_numbers().y == 86290575637772818452062569410092503179882738810918951913926481113065456425840 - -= PrivKey class : Checking private attributes -y.key.private_numbers().private_value == 90719786431263082134670936670180839782031078050773732489701961692235185651857 - -+ PrivKey/Pubkey test signatures - -= PrivKey class : sign tbs cert - -pkey_sign = PrivKey(""" ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA1L8KacejlbFJ18bvAz5/W9mF+0GglJs6qyv8pAPPiX1mWaLZ -Y42Kf/axHYrxUPXEqitRG3VkOy1HONAZhl90rY0jVUyYps94om4S98NECbY3eiVc -02ZqQng5HyzBYJQeTh+EYrDaxPUXcVXjthmrt/6vbUHI1Kgk/gok8IBFMSzilxeO -ZMJJ+dQigeDiaJGwHb3U5KzOm+hFb/IbwjdXJm3CG/58bCQp0rp6RD2qI/D6Xtvj -pc/ms6q7vfBVpquSLeEIt4Jq2XC9RKGR7TGHaVe8vmU5rb/Y36ReYCw5+fMJqcP4 -fFlC6iexBDhgy1sqV0o0tu4TzJodn8n3SFResQIDAQABAoIBAHcXEe8w0AOloJ5n -P7hjLcvusi96BzfoxSi4kM4HTA+84KRgoqw1uUf0giT1eCxHx3Uylk52okr2B55n -70HnAVt9XEANho4qKW9Tis6iwd1l4RxA+ftkoyrePauT1BQKFgTJY8QTGAOU5zCM -UdHIAPYYXX8dihxwm3SRnSf7xb/GSRkj5sMr0ioiBOZ91fwzbtOEbVXE58DyPNJm -w/tBCFbibpr4iCU/6US8OyCxR/X4heRyKCcANXlHyE/eUO6TY8J2RaKbSQi+c3/y -Y11ypSboyM3cGJ/URS5wRd0oQMQMANck4w+MlNU5jxsfN9wF32HWII8wq/6n3hHR -M+H+3YECgYEA79nc8BLzFPrzuJud9JvCFEh0pNb0gLRb/MvIsaVUT7ac8/89tfvQ -6qxWgP81ldJ7S+d/uh80CKg0lVwaxF4sQ6yNn/cvebW8tCCm0RkD8q3R9kxOd3Q/ -kLNeeBS/gPzh2xOmVuTE0ruv7ovYowU8WfJG2z20lv7WNsrN/Jm526kCgYEA4xH+ -EBVqoPYxzKoa0LNxSPfVOBO7wT19pS5Ny7yjI9oy724cNXn39H5KaCHC3ZnR0mII -0znf7cbtbFHLSkR2MNzy1MC1VhIxFQ5yHLRCjZcKkjd+gZuJp0tCgY/r2dNYsBCR -7W1vMz/wNsbufkOhi/DqC0Ru7onFbouGBdpID8kCgYEAjamr6NAIarfeA4dGQBdP -BhPVcRbUyr+8JQ9ntiTkK0C8axCyLi5RMooffYk+6QKseCR/ODr9zK8sf5sq5BiL -JF1iOL0SeVxx3CH85TtVLZykikh/f+ZVNO38OghnI5Q5AeAVOvVbmuvn+Yj3pzGM -d8O1PgCwDQ7vDuWxzCQvtiECgYAGWA9YFbEX9CjqBeqf4BOPLVVorqx1NqmW/tcv -lQKd0s/Pfq0NFW5HB2w+woq2NED3dsO2WwyVkRQ7DYH3fjgrH1EtfoDSecmjQ/cO -ND8Tw5+I/EHtjxHmeaTPB91YBZ6ZtKzPDFqp/ORSM3agUnVl+oIfdHcA9Rpt/zns -We/feQKBgGimvdIrurKPTrV49ltAKdkHmglpYeCaDr6aZKwWMcsrLmTZ6a4uRPFF -TdK+rCyGyjmibTVRjdg5+7KXshSlBleNR3v+AySAxzpjwySVhTfRirCogHRFHrnK -kXqy5xUkg11ETv6v91n3u5NVBlXVN4iwFRGSKsecw0qxSgKjbP4n ------END RSA PRIVATE KEY----- -""") - -c_tosign = Cert(""" ------BEGIN CERTIFICATE----- -MIIC/TCCAeWgAwIBAgIJALkQBZa7rCRFMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV -BAMMCnNlY2Rldi5vcmcwHhcNMTgwMjI3MTY1NjIyWhcNMjgwMjI1MTY1NjIyWjAV -MRMwEQYDVQQDDApzZWNkZXYub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEA1L8KacejlbFJ18bvAz5/W9mF+0GglJs6qyv8pAPPiX1mWaLZY42Kf/ax -HYrxUPXEqitRG3VkOy1HONAZhl90rY0jVUyYps94om4S98NECbY3eiVc02ZqQng5 -HyzBYJQeTh+EYrDaxPUXcVXjthmrt/6vbUHI1Kgk/gok8IBFMSzilxeOZMJJ+dQi -geDiaJGwHb3U5KzOm+hFb/IbwjdXJm3CG/58bCQp0rp6RD2qI/D6Xtvjpc/ms6q7 -vfBVpquSLeEIt4Jq2XC9RKGR7TGHaVe8vmU5rb/Y36ReYCw5+fMJqcP4fFlC6iex -BDhgy1sqV0o0tu4TzJodn8n3SFResQIDAQABo1AwTjAdBgNVHQ4EFgQUf98kGOpM -CVBFdHxFb8DaL6tPe+8wHwYDVR0jBBgwFoAUf98kGOpMCVBFdHxFb8DaL6tPe+8w -DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAmw0lTyEVH8YfytbVS9AW -rTJ1wWhDGf+9jHHEjX/OIq5ii0Ks38WyybhD7cMQNfkZCgIjrutrLHN/m/wn9aDx -y9vuubWvrcbqhur82YZbVnlvEiqEEyY/ULqCaW2X7UC2K/2NAy14oF6bClLX8LBq -3G/lc6GUOToN6i4OuKeB9xxvJaBxsVIdnUW9IqesHatqV4yIhH1/flhqWM47LjHP -a/uIGboyhl8p5bt3aVbXFwm/NeqsOVPDcQsBdWGldCN6loLE7b4eJDhjHbsuR2C3 -aomWcyGW1mRxNJUI0GQ5EHB5Vvy4mcxKG1DMYxG/rGf/EHk+xPJXpITIugbispbm -uA== ------END CERTIFICATE----- -""") -tbs_signed = pkey_sign.signTBSCert(c_tosign.tbsCertificate) -assert raw(tbs_signed.signatureValue) == b"BH\xdb@>\x82\x08b\xbc\xaf\x04%_\xeaV\xf5_\xa8\xf4\xf3\xd1\x0f\x86\xbd\x1b\xe2U\xfb\xf5/\rN\xc2\r\xbc\xa0Hn\xed\xb7\x18\xb2\xb3\xa5\x08m9\x9fY\xa6\xb32\xcd:\xd7\xab\xac\x8c\xcf@\xbb\x08Gt2\xb7\x93\x95\x92\x17\xa7j\x99\xa7)\xab\xbc\x07HP\xca\x00M$\xfb.\xb9\xb8\xac%i\x8c\xa2+\xe7ny!\xa1\xd2l\x0f>j\xd6\xb0\x9e\xcat)+\xbc\x16'\x9d\x1e\x80\x89\x01.\x9dS\xbb\xa0-\xb8\x0c\xe9\xe9:a\xbe\x14p\xd1\xbb\xf0I\xa2\x8fio`2\x1b7\xb8]\t3\xced`\x86\x97\x01\x82t\xd0\xc3c%\xa7\xda\\[]9\xfa\xba\r\x83\x8b\r\xa2(\x87\xe87C\xb7\\\x11\x163\x8e\xbf\xe2\x80\x7f\xf2\x93\xa4\x04w\xddG\x88\x1e#\xa6l\x15\xa1\xc6\xda\x1f\xd4\xb4$T\xa1\xd0\xe9\xd5t\xc4\xe4q\xbe\xa2\xd2\xba\x1b!/\x1dK\x17}\xc6.\xba\x81;\x00ft\x8du)\x15\n\t\x08\x1b\xb2Ol\xe1\x94g\xc8\xc0\xd6>" -pkey_sign.resignCert(c_tosign) -assert pkey_sign.verifyCert(c_tosign) - - -########### Keys crypto tests ####################################### - -+ PubKey/PrivKey classes crypto tests - -= PrivKey/PubKey classes : Signing/Verifying with MD5_SHA1 hash -m = "Testing our PKCS #1 legacy methods" # ignore this string -s = x.sign(m, t="pkcs", h="md5-sha1") -assert(s == b"\x0cm\x8a\x8f\xae`o\xcdC=\xfea\xf4\xff\xf0i\xfe\xa3!\xfd\xa5=*\x99?\x08!\x03A~\xa3-B\xe8\xca\xaf\xb4H|\xa3\x98\xe9\xd5U\xfdL\xb1\x9c\xd8\xb2{\xa1/\xfcr\x8c\xa7\xd3\xa9%\xde\x13\xa8\xf6\xc6<\xc7\xdb\xe3\xa62\xeb\xe9?\xe5by\xc2\x9e\xad\xec\x92:\x14\xd96\xa8\xc0+\xea8'{=\x91$\xdf\xed\xe1+eF8\x9fI\x1f\xa1\xcb4s\xd1#\xdf\xa11\x88o\x050i Hg\x0690\xe6\xe8?\\<:k\x94\x82\x91\x0f\x06\xc7>ZQ\xc2\xcdn\xdb\xf4\x9d\x7f!\xa9>\xe8\xea\xb3\xd83]\x8d\x90\xd4\xa0b\xe6\xe6$d[\xe4\xb4 |W\xb2t\x8c\xb2\xd5>>+\xf1\xa6W'\xaf\xc2CU\x82\x13\xc4\x0b\xc4vD*\xc3\xef\xa6s\nQ\xe6\rS@B\xd2\xa4V\xdc\xd1D\x7f\x00\xaa\xac\xac\x96i\xf1kg*\xe9*\x90a@\xc8uDy\x16\xe2\x03\xd1\x9fa\xe2s\xdb\xees\xa4\x8cna\xba\xdaE\x006&\xa4") -x_pub = PubKey((x._pubExp, x._modulus, x._modulusLen)) -x_pub.verify(m, s, t="pkcs", h="md5-sha1") - -= PrivKey/PubKey classes : Signing/Verifying with MD5_SHA1 hash with legacy support -m = "Testing our PKCS #1 legacy methods" -s = x._legacy_sign_md5_sha1(m) -assert(s == b"\x0cm\x8a\x8f\xae`o\xcdC=\xfea\xf4\xff\xf0i\xfe\xa3!\xfd\xa5=*\x99?\x08!\x03A~\xa3-B\xe8\xca\xaf\xb4H|\xa3\x98\xe9\xd5U\xfdL\xb1\x9c\xd8\xb2{\xa1/\xfcr\x8c\xa7\xd3\xa9%\xde\x13\xa8\xf6\xc6<\xc7\xdb\xe3\xa62\xeb\xe9?\xe5by\xc2\x9e\xad\xec\x92:\x14\xd96\xa8\xc0+\xea8\'{=\x91$\xdf\xed\xe1+eF8\x9fI\x1f\xa1\xcb4s\xd1#\xdf\xa11\x88o\x050i Hg\x0690\xe6\xe8?\\<:k\x94\x82\x91\x0f\x06\xc7>ZQ\xc2\xcdn\xdb\xf4\x9d\x7f!\xa9>\xe8\xea\xb3\xd83]\x8d\x90\xd4\xa0b\xe6\xe6$d[\xe4\xb4 |W\xb2t\x8c\xb2\xd5>>+\xf1\xa6W\'\xaf\xc2CU\x82\x13\xc4\x0b\xc4vD*\xc3\xef\xa6s\nQ\xe6\rS@B\xd2\xa4V\xdc\xd1D\x7f\x00\xaa\xac\xac\x96i\xf1kg*\xe9*\x90a@\xc8uDy\x16\xe2\x03\xd1\x9fa\xe2s\xdb\xees\xa4\x8cna\xba\xdaE\x006&\xa4") -x_pub = PubKey((x._pubExp, x._modulus, x._modulusLen)) -x_pub._legacy_verify_md5_sha1(m, s) - - -########### Cert class ############################################## - -+ Cert class tests - -= Cert class : Importing PEM-encoded X.509 Certificate -x = Cert(""" ------BEGIN CERTIFICATE----- -MIIFEjCCA/qgAwIBAgIJALRecEPnCQtxMA0GCSqGSIb3DQEBBQUAMIG2MQswCQYD -VQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMRcwFQYDVQQK -Ew5NdXNocm9vbSBDb3JwLjEeMBwGA1UECxMVTXVzaHJvb20gVlBOIFNlcnZpY2Vz -MSUwIwYDVQQDExxJS0V2MiBYLjUwOSBUZXN0IGNlcnRpZmljYXRlMScwJQYJKoZI -hvcNAQkBFhhpa2V2Mi10ZXN0QG11c2hyb29tLmNvcnAwHhcNMDYwNzEzMDczODU5 -WhcNMjYwMzMwMDczODU5WjCBtjELMAkGA1UEBhMCRlIxDjAMBgNVBAgTBVBhcmlz -MQ4wDAYDVQQHEwVQYXJpczEXMBUGA1UEChMOTXVzaHJvb20gQ29ycC4xHjAcBgNV -BAsTFU11c2hyb29tIFZQTiBTZXJ2aWNlczElMCMGA1UEAxMcSUtFdjIgWC41MDkg -VGVzdCBjZXJ0aWZpY2F0ZTEnMCUGCSqGSIb3DQEJARYYaWtldjItdGVzdEBtdXNo -cm9vbS5jb3JwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmFdqP+nT -EZukS0lLP+yj1gNImsEIf7P2ySTunceYxwkm4VE5QReDbb2L5/HLA9pPmIeQLSq/ -BgO1meOcbOSJ2YVHQ28MQ56+8Crb6n28iycX4hp0H3AxRAjh0edX+q3yilvYJ4W9 -/NnIb/wAZwS0oJif/tTkVF77HybAfJde5Eqbp+bCKIvMWnambh9DRUyjrBBZo5dA -1o32zpuFBrJdI8dmUpw9gtf0F0Ba8lGZm8Uqc0GyXeXOJUE2u7CiMu3M77BM6ZLL -Tcow5+bQImkmTL1SGhzwfinME1e6p3Hm//pDjuJvFaY22k05LgLuyqc59vFiB3To -ldz8+AbMNjvzAwIDAQABo4IBHzCCARswHQYDVR0OBBYEFPPYTt6Q9+Zd0s4zzVxW -jG+XFDFLMIHrBgNVHSMEgeMwgeCAFPPYTt6Q9+Zd0s4zzVxWjG+XFDFLoYG8pIG5 -MIG2MQswCQYDVQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlz -MRcwFQYDVQQKEw5NdXNocm9vbSBDb3JwLjEeMBwGA1UECxMVTXVzaHJvb20gVlBO -IFNlcnZpY2VzMSUwIwYDVQQDExxJS0V2MiBYLjUwOSBUZXN0IGNlcnRpZmljYXRl -MScwJQYJKoZIhvcNAQkBFhhpa2V2Mi10ZXN0QG11c2hyb29tLmNvcnCCCQC0XnBD -5wkLcTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQA2zt0BvXofiVvH -MWlftZCstQaawej1SmxrAfDB4NUM24NsG+UZI88XA5XM6QolmfyKnNromMLC1+6C -aFxjq3jC/qdS7ifalFLQVo7ik/te0z6Olo0RkBNgyagWPX2LR5kHe9RvSDuoPIsb -SHMmJA98AZwatbvEhmzMINJNUoHVzhPeHZnIaBgUBg02XULk/ElidO51Rf3gh8dR -/kgFQSQT687vs1x9TWD00z0Q2bs2UF3Ob3+NYkEGEo5F9RePQm0mY94CT2xs6WpH -o060Fo7fVpAFktMWx1vpu+wsEbQAhgGqV0fCR2QwKDIbTrPW/p9HJtJDYVjYdAFx -r3s7V77y ------END CERTIFICATE----- -""") - -= Cert class : Checking version -x.version == 3 - -= Cert class : Checking certificate serial number extraction -x.serial == 0xB45E7043E7090B71 - -= Cert class : Checking signature algorithm -x.sigAlg == 'sha1-with-rsa-signature' - -= Cert class : Checking issuer extraction in basic format (/C=FR ...) -x.issuer_str == '/C=FR/ST=Paris/L=Paris/O=Mushroom Corp./OU=Mushroom VPN Services/CN=IKEv2 X.509 Test certificate/emailAddress=ikev2-test@mushroom.corp' - -= Cert class : Checking subject extraction in basic format (/C=FR ...) -x.subject_str == '/C=FR/ST=Paris/L=Paris/O=Mushroom Corp./OU=Mushroom VPN Services/CN=IKEv2 X.509 Test certificate/emailAddress=ikev2-test@mushroom.corp' - -= Cert class : Checking start date extraction in simple and tuple formats -assert(x.notBefore_str_simple == '07/13/06') -x.notBefore == (2006, 7, 13, 7, 38, 59, 3, 194, -1) - -= Cert class : Checking end date extraction in simple and tuple formats -assert(x.notAfter_str_simple == '03/30/26') -x.notAfter == (2026, 3, 30, 7, 38, 59, 0, 89, -1) - -= Cert class : test remainingDays -assert abs(x.remainingDays("02/12/11")) > 5000 -assert abs(x.remainingDays("Feb 12 10:00:00 2011 Paris, Madrid")) > 1 - -= Cert class : Checking RSA public key -assert(type(x.pubKey) is PubKeyRSA) -x_pubNum = x.pubKey.pubkey.public_numbers() -assert(x_pubNum.n == 19231328316532061413420367242571475005688288081144416166988378525696075445024135424022026378563116068168327239354659928492979285632474448448624869172454076124150405352043642781483254546569202103296262513098482624188672299255268092629150366527784294463900039290024710152521604731213565912934889752122898104556895316819303096201441834849255370122572613047779766933573375974464479123135292080801384304131606933504677232323037116557327478512106367095125103346134248056463878553619525193565824925835325216545121044922690971718737998420984924512388011040969150550056783451476150234324593710633552558175109683813482739004163) -x_pubNum.e == 0x10001 - -= Cert class : Checking extensions -x.show() -x.tbsCertificate.show() -assert(x.cA) -assert(x.authorityKeyID == b'\xf3\xd8N\xde\x90\xf7\xe6]\xd2\xce3\xcd\\V\x8co\x97\x141K') -not hasattr(x, "keyUsage") - -= Cert class : encrypt - -assert len(x.encrypt(b"Scapy")) == 256 - -= Cert class : export - -import tempfile, os -filename = tempfile.mktemp() -x.export(filename) -fstat = os.stat(filename) -assert fstat.st_size == 1302 -os.remove(filename) - -= Cert class : isIssuerCert - -assert x.isIssuerCert(x) - -= Cert class : Importing another PEM-encoded X.509 Certificate -y = Cert(""" ------BEGIN CERTIFICATE----- -MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg -RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf -Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q -RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD -AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY -JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv -6pZjamVFkpUBtA== ------END CERTIFICATE----- -""") - -= Cert class : Checking ECDSA public key -assert(type(y.pubKey) is PubKeyECDSA) -pubkey = y.pubKey.pubkey -assert(pubkey.curve.name == 'secp384r1') -pubkey.public_numbers().x == 3987178688175281746349180015490646948656137448666005327832107126183726641822596270780616285891030558662603987311874 - -= Cert class : Checking ECDSA signature -raw(y.signatureValue) == b'0d\x020%\xa4\x81E\x02k\x12KutO\xc8#\xe3p\xf2ur\xde|\x89\xf0\xcf\x91ra\x9e^\x10\x92YV\xb9\x83\xc7\x10\xe78\xe9X&6}\xd5\xe44\x869\x020|6S\xf00\xe5bc:\x99\xe2\xb6\xa3;\x9b4\xfa\x1e\xda\x10\x92q^\x91\x13\xa7\xdd\xa4n\x92\xcc2\xd6\xf5!f\xc7/\xea\x96cjeE\x92\x95\x01\xb4' - -= Cert class : Test show -awaited = """ -Serial: 15459312981008553731928384953135426796 -Issuer: /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root G3 -Subject: /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root G3 -Validity: Aug 01 12:00:00 2013 GMT to Jan 15 12:00:00 2038 GMT -""" - -with ContextManagerCaptureOutput() as cmco: - y.show() - assert cmco.get_output().strip() == awaited.strip() - -########### CRL class ############################################### - -+ CRL class tests - -= CRL class : Importing PEM-encoded CRL -x = CRL(""" ------BEGIN X509 CRL----- -MIICHjCCAYcwDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMxFzAVBgNVBAoT -DlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5Fw0wNjExMDIwMDAwMDBaFw0wNzAyMTcy -MzU5NTlaMIH2MCECECzSS2LEl6QXzW6jyJx6LcgXDTA0MDQwMTE3NTYxNVowIQIQ -OkXeVssCzdzcTndjIhvU1RcNMDEwNTA4MTkyMjM0WjAhAhBBXYg2gRUg1YCDRqhZ -kngsFw0wMTA3MDYxNjU3MjNaMCECEEc5gf/9hIHxlfnrGMJ8DfEXDTAzMDEwOTE4 -MDYxMlowIQIQcFR+auK62HZ/R6mZEEFeZxcNMDIwOTIzMTcwMDA4WjAhAhB+C13e -GPI5ZoKmj2UiOCPIFw0wMTA1MDgxOTA4MjFaMCICEQDQVEhgGGfTrTXKLw1KJ5Ve -Fw0wMTEyMTExODI2MjFaMA0GCSqGSIb3DQEBBQUAA4GBACLJ9rsdoaU9JMf/sCIR -s3AGW8VV3TN2oJgiCGNEac9PRyV3mRKE0hmuIJTKLFSaa4HSAzimWpWNKuJhztsZ -zXUnWSZ8VuHkgHEaSbKqzUlb2g+o/848CvzJrcbeyEBkDCYJI5C3nLlQA49LGJ+w -4GUPYBwaZ+WFxCX1C8kzglLm ------END X509 CRL----- -""") - -= CRL class : Checking version -x.version == 1 - -= CRL class : Checking issuer extraction in basic format (/C=FR ...) -x.issuer_str == '/C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority' - -= CRL class : Checking lastUpdate date extraction in tuple format -x.lastUpdate == (2006, 11, 2, 0, 0, 0, 3, 306, -1) - -= CRL class : Checking nextUpdate date extraction in tuple format -x.nextUpdate == (2007, 2, 17, 23, 59, 59, 5, 48, -1) - -= CRL class : Checking number of revoked certificates -len(x.revoked_cert_serials) == 7 - -= CRL class : Checking presence of one revoked certificate -(94673785334145723688625287778885438961, '030109180612') in x.revoked_cert_serials - -= Cert/CRL class : Checking isRevoked -cx = X509_Cert() -cx.tbsCertificate.serialNumber.val = 59577943160751197113872490992424857032 -cx.tbsCertificate.issuer = x.x509CRL.tbsCertList.issuer -cx = Cert(raw(cx)) -assert cx.isRevoked([x]) - -= CRL class : Test show -awaited = """ -Version: 1 -sigAlg: sha1-with-rsa-signature -Issuer: /C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority -lastUpdate: Nov 02 00:00:00 2006 GMT -nextUpdate: Feb 17 23:59:59 2007 GMT -""" - -with ContextManagerCaptureOutput() as cmco: - x.show() - assert cmco.get_output().strip() == awaited.strip() - -########### High-level methods ############################################### - -= Cert class : Checking isIssuerCert() -c0 = Cert(""" ------BEGIN CERTIFICATE----- -MIIFVjCCBD6gAwIBAgIJAJmDv7HOC+iUMA0GCSqGSIb3DQEBCwUAMIHGMQswCQYD -VQQGEwJVUzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEl -MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEzMDEGA1UECxMq -aHR0cDovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkvMTQwMgYD -VQQDEytTdGFyZmllbGQgU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcy -MB4XDTE1MTAxMzE2NDIzOFoXDTE2MTEzMDIzMzQxOVowPjEhMB8GA1UECxMYRG9t -YWluIENvbnRyb2wgVmFsaWRhdGVkMRkwFwYDVQQDDBAqLnRvb2xzLmlldGYub3Jn -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAseE36OuC1on62/XCS3fw -LErecm4+E2DRqGYexK09MmDl8Jm19Hp6SFUh7g45EvnODcr1aWHHBO1uDx07HlCI -eToOMUEW8bECZGilzfVKCsqZljUIw34nXdCpz/PnKK832LZ73fN+rm6Xf/fKaU7M -0AbfXSebOxLn5v4Ia1J7ghF8crNG68HoeLgPy+HrvQZEWNyDULKgYlvcgbg24558 -ebKpU4rgC8lKKhM5MRO9LM+ocM+MjT0Bo4iuEgA2HR4kK9152FMBJu0oT8mGlINO -yOEULoWzr9Ru3WlGr0ElDnqti/KSynnZezJP93fo+bRPI1zUXAOu2Ks6yhNfXV1d -oQIDAQABo4IBzDCCAcgwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcD -AQYIKwYBBQUHAwIwDgYDVR0PAQH/BAQDAgWgMDwGA1UdHwQ1MDMwMaAvoC2GK2h0 -dHA6Ly9jcmwuc3RhcmZpZWxkdGVjaC5jb20vc2ZpZzJzMS0xNy5jcmwwWQYDVR0g -BFIwUDBOBgtghkgBhv1uAQcXATA/MD0GCCsGAQUFBwIBFjFodHRwOi8vY2VydGlm -aWNhdGVzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkvMIGCBggrBgEFBQcB -AQR2MHQwKgYIKwYBBQUHMAGGHmh0dHA6Ly9vY3NwLnN0YXJmaWVsZHRlY2guY29t -LzBGBggrBgEFBQcwAoY6aHR0cDovL2NlcnRpZmljYXRlcy5zdGFyZmllbGR0ZWNo -LmNvbS9yZXBvc2l0b3J5L3NmaWcyLmNydDAfBgNVHSMEGDAWgBQlRYFoUCY4PTst -LL7Natm2PbNmYzArBgNVHREEJDAighAqLnRvb2xzLmlldGYub3Jngg50b29scy5p -ZXRmLm9yZzAdBgNVHQ4EFgQUrYq0HAdR15KJB7C3hGIvNlV6X00wDQYJKoZIhvcN -AQELBQADggEBAAxfzShHiatHrWnTGuRX9BmFpHOFGmLs3PtRRPoOUEbZrcTbaJ+i -EZpjj4R3eiLITgObcib8+NR1eZsN6VkswZ+rr54aeQ1WzWlsVwBP1t0h9lIbaonD -wDV6ME3KzfFwwsZWqMBgLin8TcoMadAkXhdfcEKNndKSMsowgEjigP677l24nHf/ -OcnMftgErmTm+jEdW1wUooJoWgbt8TT2uWD8MC62sIIgSQ6miKtg7LhCC1ScyVuN -Erk3YzF8mPwouOcnNOKsUnkDXLA2REMedVp48c4ikjLClu6AcIg03ZU+o8fLNqcZ -zd1s7DbacrRSSQ+nXDTodqw1HB+77u0RFs0= ------END CERTIFICATE----- -""") -c1 = Cert(""" ------BEGIN CERTIFICATE----- -MIIFADCCA+igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAw -MFoXDTMxMDUwMzA3MDAwMFowgcYxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydHMuc3RhcmZpZWxk -dGVjaC5jb20vcmVwb3NpdG9yeS8xNDAyBgNVBAMTK1N0YXJmaWVsZCBTZWN1cmUg -Q2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDlkGZL7PlGcakgg77pbL9KyUhpgXVObST2yxcT+LBxWYR6ayuF -pDS1FuXLzOlBcCykLtb6Mn3hqN6UEKwxwcDYav9ZJ6t21vwLdGu4p64/xFT0tDFE -3ZNWjKRMXpuJyySDm+JXfbfYEh/JhW300YDxUJuHrtQLEAX7J7oobRfpDtZNuTlV -Bv8KJAV+L8YdcmzUiymMV33a2etmGtNPp99/UsQwxaXJDgLFU793OGgGJMNmyDd+ -MB5FcSM1/5DYKp2N57CSTTx/KgqT3M0WRmX3YISLdkuRJ3MUkuDq7o8W6o0OPnYX -v32JgIBEQ+ct4EMJddo26K3biTr1XRKOIwSDAgMBAAGjggEsMIIBKDAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUJUWBaFAmOD07LSy+ -zWrZtj2zZmMwHwYDVR0jBBgwFoAUfAwyH6fZMH/EfWijYqihzqsHWycwOgYIKwYB -BQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5zdGFyZmllbGR0ZWNo -LmNvbS8wOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zdGFyZmllbGR0ZWNo -LmNvbS9zZnJvb3QtZzIuY3JsMEwGA1UdIARFMEMwQQYEVR0gADA5MDcGCCsGAQUF -BwIBFitodHRwczovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkv -MA0GCSqGSIb3DQEBCwUAA4IBAQBWZcr+8z8KqJOLGMfeQ2kTNCC+Tl94qGuc22pN -QdvBE+zcMQAiXvcAngzgNGU0+bE6TkjIEoGIXFs+CFN69xpk37hQYcxTUUApS8L0 -rjpf5MqtJsxOYUPl/VemN3DOQyuwlMOS6eFfqhBJt2nk4NAfZKQrzR9voPiEJBjO -eT2pkb9UGBOJmVQRDVXFJgt5T1ocbvlj2xSApAer+rKluYjdkf5lO6Sjeb6JTeHQ -sPTIFwwKlhR8Cbds4cLYVdQYoKpBaXAko7nv6VrcPuuUSvC33l8Odvr7+2kDRUBQ -7nIMpBKGgc0T0U7EPMpODdIm8QC3tKai4W56gf0wrHofx1l7 ------END CERTIFICATE----- -""") -c2 = Cert(""" ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- -""") -c0.isIssuerCert(c1) and c1.isIssuerCert(c2) and not c0.isIssuerCert(c2) - -= Cert class : Checking isSelfSigned() -c2.isSelfSigned() and not c1.isSelfSigned() and not c0.isSelfSigned() - -= PubKey class : Checking verifyCert() -c2.pubKey.verifyCert(c2) and c1.pubKey.verifyCert(c0) - -= Chain class : Checking chain construction -assert(len(Chain([c0, c1, c2])) == 3) -assert(len(Chain([c0], c1)) == 2) -len(Chain([c0], c2)) == 1 - -= Chain class : repr - -expected_repr = """__ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 [Self Signed] - _ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certs.starfieldtech.com/repository//CN=Starfield Secure Certificate Authority - G2 - _ /OU=Domain Control Validated/CN=*.tools.ietf.org""" -assert str(Chain([c0, c1, c2])) == expected_repr - -= Chain class : Checking chain verification -assert(Chain([], c0).verifyChain([c2], [c1])) -not Chain([c1]).verifyChain([c0]) - -= Chain class: Checking chain verification with file - -import tempfile - -tf_folder = tempfile.mkdtemp() - -try: - os.makedirs(tf_folder) -except: - pass - -tf = os.path.join(tf_folder, "trusted") -utf = os.path.join(tf_folder, "untrusted") - -tf -utf - -# Create files -trusted = open(tf, "w") -trusted.write(""" ------BEGIN CERTIFICATE----- -MIIFADCCA+igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAw -MFoXDTMxMDUwMzA3MDAwMFowgcYxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydHMuc3RhcmZpZWxk -dGVjaC5jb20vcmVwb3NpdG9yeS8xNDAyBgNVBAMTK1N0YXJmaWVsZCBTZWN1cmUg -Q2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDlkGZL7PlGcakgg77pbL9KyUhpgXVObST2yxcT+LBxWYR6ayuF -pDS1FuXLzOlBcCykLtb6Mn3hqN6UEKwxwcDYav9ZJ6t21vwLdGu4p64/xFT0tDFE -3ZNWjKRMXpuJyySDm+JXfbfYEh/JhW300YDxUJuHrtQLEAX7J7oobRfpDtZNuTlV -Bv8KJAV+L8YdcmzUiymMV33a2etmGtNPp99/UsQwxaXJDgLFU793OGgGJMNmyDd+ -MB5FcSM1/5DYKp2N57CSTTx/KgqT3M0WRmX3YISLdkuRJ3MUkuDq7o8W6o0OPnYX -v32JgIBEQ+ct4EMJddo26K3biTr1XRKOIwSDAgMBAAGjggEsMIIBKDAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUJUWBaFAmOD07LSy+ -zWrZtj2zZmMwHwYDVR0jBBgwFoAUfAwyH6fZMH/EfWijYqihzqsHWycwOgYIKwYB -BQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5zdGFyZmllbGR0ZWNo -LmNvbS8wOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zdGFyZmllbGR0ZWNo -LmNvbS9zZnJvb3QtZzIuY3JsMEwGA1UdIARFMEMwQQYEVR0gADA5MDcGCCsGAQUF -BwIBFitodHRwczovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkv -MA0GCSqGSIb3DQEBCwUAA4IBAQBWZcr+8z8KqJOLGMfeQ2kTNCC+Tl94qGuc22pN -QdvBE+zcMQAiXvcAngzgNGU0+bE6TkjIEoGIXFs+CFN69xpk37hQYcxTUUApS8L0 -rjpf5MqtJsxOYUPl/VemN3DOQyuwlMOS6eFfqhBJt2nk4NAfZKQrzR9voPiEJBjO -eT2pkb9UGBOJmVQRDVXFJgt5T1ocbvlj2xSApAer+rKluYjdkf5lO6Sjeb6JTeHQ -sPTIFwwKlhR8Cbds4cLYVdQYoKpBaXAko7nv6VrcPuuUSvC33l8Odvr7+2kDRUBQ -7nIMpBKGgc0T0U7EPMpODdIm8QC3tKai4W56gf0wrHofx1l7 ------END CERTIFICATE----- -""") -trusted.close() - -untrusted = open(utf, "w") -untrusted.write(""" ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- -""") -untrusted.close() - -assert Chain([], c0).verifyChainFromCAFile(tf, untrusted_file=utf) -assert Chain([], c0).verifyChainFromCAPath(tf_folder, untrusted_file=utf) - -= Clear files - -try: - os.remove("./certs_test_ca/trusted") - os.remove("./certs_test_ca/untrusted") -except: - pass - -try: - os.rmdir("././certs_test_ca") -except: - pass - -= Test __repr__ - -repr_str = Chain([], c0).__repr__() -assert repr_str == '__ /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' - -= Test GeneralizedTime - -data = b"MHAwXAIBADANBgkqhkiG9w0BAQ0FADAAMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMAAwHDANBgkqhkiG9w0BAQEFAAMLADAIAgEAAgMBAAGjAjAAMA0GCSqGSIb3DQEBDQUAAwEA" -import tempfile, os -_, filename = tempfile.mkstemp() -fd = open(filename, "wb") -fd.write(b"-----BEGIN CERTIFICATE-----\n") -fd.write(data) -fd.write(b"-----END CERTIFICATE-----\n") -fd.close() -cert = Cert(filename) -assert "2011" in cert.notBefore_str and "2046" in cert.notAfter_str diff --git a/test/configs/README.md b/test/configs/README.md new file mode 100644 index 00000000000..99d85493c68 --- /dev/null +++ b/test/configs/README.md @@ -0,0 +1,5 @@ +### UTscapy configs + +- OS specifics: bsd, linux, solaris, windows +- Other: + - cryptography -> used for downstream testing by pyca/cryptography diff --git a/test/configs/bsd.utsc b/test/configs/bsd.utsc index 4f90de423ae..194466f989f 100644 --- a/test/configs/bsd.utsc +++ b/test/configs/bsd.utsc @@ -1,29 +1,41 @@ { "testfiles": [ "test/*.uts", + "test/scapy/layers/*.uts", + "test/scapy/layers/tls/*.uts", + "test/scapy/layers/msrpce/*.uts", "test/contrib/automotive/*.uts", "test/contrib/automotive/obd/*.uts", + "test/contrib/automotive/scanner/*.uts", "test/contrib/automotive/gm/*.uts", "test/contrib/automotive/bmw/*.uts", + "test/contrib/automotive/xcp/*.uts", + "test/contrib/automotive/autosar/*.uts", "test/contrib/*.uts" ], "remove_testfiles": [ "test/linux.uts", - "test/windows.uts" + "test/windows.uts", + "test/contrib/automotive/ecu_am.uts", + "test/contrib/automotive/gm/gmlanutils.uts", + "test/contrib/isotp_packet.uts", + "test/contrib/isotpscan.uts", + "test/contrib/isotp_soft_socket.uts" ], "onlyfailed": true, + "extensions": ["scapy-rpc"], "preexec": { "test/contrib/*.uts": "load_contrib(\"%name%\")", "test/cert.uts": "load_layer(\"tls\")", "test/sslv2.uts": "load_layer(\"tls\")", - "test/tls*.uts": "load_layer(\"tls\")" + "test/scapy/layers/tls/*.uts": "load_layer(\"tls\")" }, - "format": "text", "kw_ko": [ "linux", "windows", - "crypto_advanced", "ipv6", - "vcan_socket" + "vcan_socket", + "tun", + "tap" ] } diff --git a/test/configs/cryptography.utsc b/test/configs/cryptography.utsc new file mode 100644 index 00000000000..8408f9fdb2b --- /dev/null +++ b/test/configs/cryptography.utsc @@ -0,0 +1,21 @@ +{ + "testfiles": [ + "test/contrib/macsec.uts", + "test/scapy/layers/dot11.uts", + "test/scapy/layers/ipsec.uts", + "test/scapy/layers/kerberos.uts", + "test/scapy/layers/ntlm.uts", + "test/scapy/layers/msrpce/msnrpc.uts", + "test/scapy/layers/tls/cert.uts", + "test/scapy/layers/tls/tls*.uts" + ], + "breakfailed": true, + "onlyfailed": true, + "preexec": { + "test/contrib/*.uts": "load_contrib(\"%name%\")", + "test/scapy/layers/tls/*.uts": "load_layer(\"tls\")" + }, + "kw_ko": [ + "needs_root" + ] +} diff --git a/test/configs/linux.utsc b/test/configs/linux.utsc index 242602bc2b9..b26e9166c85 100644 --- a/test/configs/linux.utsc +++ b/test/configs/linux.utsc @@ -1,13 +1,18 @@ { "testfiles": [ "test/*.uts", + "test/scapy/layers/*.uts", + "test/scapy/layers/tls/*.uts", + "test/scapy/layers/msrpce/*.uts", "test/contrib/*.uts", + "test/tools/*.uts", "test/contrib/automotive/*.uts", "test/contrib/automotive/obd/*.uts", + "test/contrib/automotive/scanner/*.uts", "test/contrib/automotive/gm/*.uts", "test/contrib/automotive/bmw/*.uts", - "test/tools/*.uts", - "test/tls/tests_tls_netaccess.uts" + "test/contrib/automotive/xcp/*.uts", + "test/contrib/automotive/autosar/*.uts" ], "remove_testfiles": [ "test/windows.uts", @@ -15,17 +20,14 @@ ], "breakfailed": true, "onlyfailed": true, + "extensions": ["scapy-rpc"], "preexec": { "test/contrib/*.uts": "load_contrib(\"%name%\")", - "test/cert.uts": "load_layer(\"tls\")", - "test/sslv2.uts": "load_layer(\"tls\")", - "test/tls*.uts": "load_layer(\"tls\")" + "test/scapy/layers/tls/*.uts": "load_layer(\"tls\")" }, - "format": "text", "kw_ko": [ "osx", "windows", - "crypto_advanced", "ipv6" ] } diff --git a/test/configs/scapy-rpc.utsc b/test/configs/scapy-rpc.utsc new file mode 100644 index 00000000000..1d600b2b7b6 --- /dev/null +++ b/test/configs/scapy-rpc.utsc @@ -0,0 +1,9 @@ +{ + "testfiles": [ + "test/scapy/layers/dcerpc.uts", + "test/scapy/layers/msrpce/*.uts" + ], + "extensions": ["scapy-rpc"], + "breakfailed": true, + "onlyfailed": true +} diff --git a/test/configs/solaris.utsc b/test/configs/solaris.utsc index 5d8cb4e3879..d76e8b77a4f 100644 --- a/test/configs/solaris.utsc +++ b/test/configs/solaris.utsc @@ -1,16 +1,24 @@ { "testfiles": [ "test/*.uts", + "test/scapy/layers/*.uts", "test/contrib/automotive/*.uts", "test/contrib/automotive/obd/*.uts", + "test/contrib/automotive/scanner/*.uts", "test/contrib/automotive/gm/*.uts", "test/contrib/automotive/bmw/*.uts", + "test/contrib/automotive/xcp/*.uts", + "test/contrib/automotive/autosar/*.uts", "test/contrib/*.uts" ], "remove_testfiles": [ "test/linux.uts", "test/bpf.uts", - "test/windows.uts" + "test/windows.uts", + "test/contrib/automotive/ecu_am.uts", + "test/contrib/automotive/gm/gmlanutils.uts", + "test/contrib/isotp.uts", + "test/contrib/isotpscan.uts" ], "onlyfailed": true, "preexec": { @@ -19,13 +27,14 @@ "test/sslv2.uts": "load_layer(\"tls\")", "test/tls*.uts": "load_layer(\"tls\")" }, - "format": "text", "kw_ko": [ "osx", "linux", "windows", "crypto_advanced", "ipv6", + "tap", + "tun", "vcan_socket" ] } diff --git a/test/configs/windows.utsc b/test/configs/windows.utsc index 5b266fea781..a38f065e8ca 100644 --- a/test/configs/windows.utsc +++ b/test/configs/windows.utsc @@ -1,11 +1,16 @@ { "testfiles": [ "test\\*.uts", - "test\\tls\\tests_tls_netaccess.uts", + "test\\scapy\\layers\\*.uts", + "test\\scapy\\layers\\tls\\*.uts", + "test\\scapy\\layers\\msrpce\\*.uts", "test\\contrib\\automotive\\obd\\*.uts", + "test\\contrib\\automotive\\scanner\\*.uts", "test\\contrib\\automotive\\gm\\*.uts", "test\\contrib\\automotive\\bmw\\*.uts", + "test\\contrib\\automotive\\xcp\\*.uts", "test\\contrib\\automotive\\*.uts", + "test\\contrib\\automotive\\autosar\\*.uts", "test\\contrib\\*.uts" ], "remove_testfiles": [ @@ -14,21 +19,25 @@ ], "breakfailed": true, "onlyfailed": true, + "extensions": ["scapy-rpc"], "preexec": { "test\\contrib\\*.uts": "load_contrib(\"%name%\")", - "test\\cert.uts": "load_layer(\"tls\")", - "test\\sslv2.uts": "load_layer(\"tls\")", - "test\\tls*.uts": "load_layer(\"tls\")" + "test\\scapy\\layers\\tls\\*.uts": "load_layer(\"tls\")" }, - "format": "text", "kw_ko": [ - "osx", + "as_resolvers", + "brotli", + "broken_windows", + "ipv6", "linux", - "crypto_advanced", + "native_tls13", "mock_read_routes_bsd", - "require_gui", "open_ssl_client", + "osx", + "require_gui", + "tap", + "tun", "vcan_socket", - "ipv6" + "zstd" ] } diff --git a/test/configs/windows2.utsc b/test/configs/windows2.utsc index cb7ac56266a..8d284880dd0 100644 --- a/test/configs/windows2.utsc +++ b/test/configs/windows2.utsc @@ -1,11 +1,14 @@ { "testfiles": [ "*.uts", - "test\\contrib\\automotive\\obd\\*.uts", - "test\\contrib\\automotive\\gm\\*.uts", - "test\\contrib\\automotive\\bmw\\*.uts", - "test\\contrib\\automotive\\*.uts", - "tls\\tests_tls_netaccess.uts", + "scapy\\layers\\*.uts", + "scapy\\layers\\tls\\*.uts", + "scapy\\layers\\msrpce\\*.uts", + "contrib\\automotive\\obd\\*.uts", + "contrib\\automotive\\gm\\*.uts", + "contrib\\automotive\\bmw\\*.uts", + "contrib\\automotive\\*.uts", + "contrib\\automotive\\autosar\\*.uts", "contrib\\*.uts" ], "remove_testfiles": [ @@ -14,24 +17,26 @@ ], "breakfailed": true, "onlyfailed": true, + "extensions": ["scapy-rpc"], "preexec": { "contrib\\*.uts": "load_contrib(\"%name%\")", - "cert.uts": "load_layer(\"tls\")", - "sslv2.uts": "load_layer(\"tls\")", - "tls*.uts": "load_layer(\"tls\")" + "scapy\\layers\\tls\\*.uts": "load_layer(\"tls\")" }, "format": "html", "kw_ko": [ "osx", "linux", + "broken_windows", "crypto_advanced", "mock_read_routes_bsd", - "appveyor_only", + "ci_only", "open_ssl_client", "vcan_socket", "ipv6", "manufdb", "tcpdump", + "tap", + "tun", "tshark" ] } diff --git a/test/contrib/aoe.uts b/test/contrib/aoe.uts index 92d2c21ff25..c66bb93c52a 100644 --- a/test/contrib/aoe.uts +++ b/test/contrib/aoe.uts @@ -8,37 +8,37 @@ a = Ether(src="00:01:02:03:04:05") b = AOE() c = a / b -assert(c[Ether].type == 0x88a2) +assert c[Ether].type == 0x88a2 = Build - Check default p = AOE() -assert(hasattr(p, "q_conf_info")) +assert hasattr(p, "q_conf_info") = Build - Check Issue ATA command p = AOE() p.cmd = 0 -assert(hasattr(p, "i_ata_cmd")) +assert hasattr(p, "i_ata_cmd") = Build - Check Query Config Information p = AOE() p.cmd = 1 -assert(hasattr(p, "q_conf_info")) +assert hasattr(p, "q_conf_info") = Build - Check Mac Mask List p = AOE() p.cmd = 2 -assert(hasattr(p, "mac_m_list")) +assert hasattr(p, "mac_m_list") = Build - Check ReserveRelease p = AOE() p.cmd = 3 -assert(hasattr(p, "res_rel")) +assert hasattr(p, "res_rel") diff --git a/test/contrib/automotive/autosar/pdu.uts b/test/contrib/automotive/autosar/pdu.uts new file mode 100644 index 00000000000..67278216b87 --- /dev/null +++ b/test/contrib/automotive/autosar/pdu.uts @@ -0,0 +1,71 @@ +% Regression tests for the PDUTransport / PDU layer + + +# More information at http://www.secdev.org/projects/UTscapy/ + + +############ +############ + ++ PDUTransport contrib tests + += Load Contrib Layer + +load_contrib("automotive.autosar.pdu", globals_dict=globals()) + += Defaults test + +p = PDUTransport() +assert p.pdus == [PDU()] + +p = PDU() +assert p.pdu_id == 0 +assert p.pdu_payload_len == None + += Build test pdu_id +p = PDU(bytes(PDU(pdu_id=0x11))) +assert len(bytes(p)) == 8 +assert p.pdu_id == 0x11 +assert p.pdu_payload_len == 0 + += Build test pdu_payload_len +p = PDU(bytes(PDU(pdu_payload_len=12))) +assert len(p) == 8 +assert p.pdu_id == 0 +assert p.pdu_payload_len == 12 + += Build test id and payload len with data +p = PDU(bytes(PDU(pdu_id=0x12, pdu_payload_len=2) / Raw(b'\x22\x33'))) +assert len(p) == 10 +assert p.pdu_id == 0x12 +assert p.pdu_payload_len == 2 +assert len(p['Raw']) == 2 +assert bytes(p['Raw']) == b'\x22\x33' + += Build PDUTransport with multiple PDU packets +p1 = PDUTransport(b'\x00\x00\x00\x01\x00\x00\x00\x01\x11' +b'\x00\x00\x00\x02\x00\x00\x00\x02\x11\x44' +b'\x00\x00\x00\x03\x00\x00\x00\x03\x11\x33\x91') +p2 = PDUTransport(bytes(PDUTransport(pdus=[PDU(pdu_id=0x1,pdu_payload_len=1)/Raw(b'\x11'), # noqa: E501 +PDU(pdu_id=0x2, pdu_payload_len=2) / Raw(b'\x11\x44'), +PDU(pdu_id=0x3, pdu_payload_len=3) / Raw(b'\x11\x33\x91')]))) +# Check if packets are the same +assert p1 == p2 +# Check if fields are set correctly within PDU list +assert p1.pdus[0].pdu_id == 0x1 +assert p1.pdus[0].pdu_payload_len == 1 +assert p1.pdus[1].pdu_id == 0x2 +assert p1.pdus[1].pdu_payload_len == 2 +assert p1.pdus[2].pdu_id == 0x3 +assert p1.pdus[2].pdu_payload_len == 3 + += Build PDUTransport with one PDU packet +p1 = PDUTransport(b'\x00\x00\x00\x01\x00\x00\x00\x03\x11\x22\x33') +p2 = PDUTransport(bytes(PDUTransport(pdus=[ +PDU(pdu_id=0x1, pdu_payload_len=0x3) / Raw(b'\x11\x22\x33')]))) + +# Check if packets are the same +assert p1 == p2 +# Check if fields are set correctly within PDU list +assert p1.pdus[0].pdu_id == 0x1 +assert p1.pdus[0].pdu_payload_len == 3 diff --git a/test/contrib/automotive/autosar/secoc.uts b/test/contrib/automotive/autosar/secoc.uts new file mode 100644 index 00000000000..a39011fdf26 --- /dev/null +++ b/test/contrib/automotive/autosar/secoc.uts @@ -0,0 +1,194 @@ +% Regression tests for the SecOC_PDUTransport / SecOC_PDU layer + + +# More information at http://www.secdev.org/projects/UTscapy/ + + +############ +############ + ++ SecOC_PDUTransport contrib tests + += Load Contrib Layer + +load_contrib("automotive.autosar.secoc_pdu") + += Prepare SecOC keys + +SecOC_PDU.secoc_protected_pdus_by_identifier = {0, 1, 2, 3, 17, 18} +SecOC_PDU.register_secoc_protected_pdu(0xdeadbeef) + +class PDU_Payload(Packet): + fields_desc = [ + ByteField("a", 0), + ByteField("b", 0), + ByteField("c", 0) + ] + + +class PDU_Payload2(Packet): + fields_desc = [ + ByteField("x", 0), + ByteField("y", 0), + ByteField("z", 0) + ] + + +SecOC_PDUTransport.register_secoc_protected_pdu(32, PDU_Payload) +SecOC_PDUTransport.register_secoc_protected_pdu(64, PDU_Payload2) + + += Defaults test +p = SecOC_PDUTransport() +p.show() +assert p.pdus == [SecOC_PDU()] + +p = SecOC_PDU() +assert p.pdu_id == 0 +assert p.pdu_payload_len == None + + += Build test pdu_id +p = SecOC_PDU(bytes(SecOC_PDU(pdu_id=0x11))) +assert len(bytes(p)) == 12 +assert p.pdu_id == 0x11 +assert p.pdu_payload_len == 4 + + += Build test pdu_payload_len +p1 = bytes(SecOC_PDU(pdu_payload_len=12, pdu_payload=bytes.fromhex("1122334455667788"))) +print(p1.hex()) +p = SecOC_PDU(p1) +p.show() +assert len(p) == 20 +assert p.pdu_id == 0 +assert p.pdu_payload_len == 12 +assert bytes(p.pdu_payload) == bytes.fromhex("1122334455667788") +assert p.tfv == 0 +assert p.tmac == b"\x00\x00\x00" + + += Build test pdu_payload_len2 +p1 = bytes(SecOC_PDU(pdu_id=0xdeadbeef, pdu_payload_len=12, pdu_payload=bytes.fromhex("1122334455667788"), tfv=42)) +print(p1.hex()) +p = SecOC_PDU(p1) +p.show() +assert len(p) == 20 +assert p.pdu_id == 0xdeadbeef +assert p.pdu_payload_len == 12 +assert bytes(p.pdu_payload) == bytes.fromhex("1122334455667788") +assert p.tfv == 42 +assert p.tmac == b"\x00\x00\x00" + + += Build test id and payload len with data +p = SecOC_PDU(bytes(SecOC_PDU(pdu_id=0x12, pdu_payload=b'\x22\x33\x22\x33'))) +assert len(p) == 16 +assert p.pdu_id == 0x12 +print(p.pdu_payload) +p.show() +assert p.pdu_payload_len == 8 +assert len(p.pdu_payload) == 4 +assert bytes(p.pdu_payload) == b'\x22\x33\x22\x33' + + += Build SecOC_PDUTransport with multiple SecOC_PDU packets +p1 = SecOC_PDUTransport( + b'\x00\x00\x00\x01\x00\x00\x00\x05\x11\x00\x00\x00\x00' + b'\x00\x00\x00\x02\x00\x00\x00\x06\x11\x44\x00\x00\x00\x00' + b'\x00\x00\x00\x03\x00\x00\x00\x07\x11\x33\x91\x00\x00\x00\x00') + +# Check if fields are set correctly within SecOC_PDU list +assert p1.pdus[0].pdu_id == 0x1 +assert p1.pdus[0].pdu_payload_len == 5 +assert p1.pdus[1].pdu_id == 0x2 +assert p1.pdus[1].pdu_payload_len == 6 +assert p1.pdus[2].pdu_id == 0x3 +assert p1.pdus[2].pdu_payload_len == 7 + +p2 = SecOC_PDUTransport(bytes(SecOC_PDUTransport( + pdus=[ + SecOC_PDU(pdu_id=0x1,pdu_payload_len=5, pdu_payload=Raw(b'\x11')), + SecOC_PDU(pdu_id=0x2, pdu_payload_len=6, pdu_payload=Raw(b'\x11\x44')), + SecOC_PDU(pdu_id=0x3, pdu_payload_len=7, pdu_payload=Raw(b'\x11\x33\x91')) + ]))) +# Check if packets are the same +assert p1 == p2 + + += Build SecOC_PDUTransport with one SecOC_PDU packet +p1 = SecOC_PDUTransport(b'\x00\x00\x00\x01\x00\x00\x00\x08\xaa\xaa\xaa\xaa\x11\x22\x33\x44') +p2 = SecOC_PDUTransport(bytes(SecOC_PDUTransport(pdus=[SecOC_PDU(pdu_id=0x1, pdu_payload=Raw(b'\xaa\xaa\xaa\xaa'), tfv=0x11, tmac=b"\x22\x33\x44")]))) + +# Check if packets are the same +assert p1 == p2 +# Check if fields are set correctly within SecOC_PDU list +assert p1.pdus[0].pdu_id == 0x1 +assert p1.pdus[0].pdu_payload_len == 8 + + += Build SecOC_PDUTransport with one SecOC_PDU packet and custom class +p1 = SecOC_PDUTransport(b'\x00\x00\x00\x20\x00\x00\x00\x07\xaa\xbb\xcc\x11\x22\x33\x44') + +# Check if packets are the same +assert p1 +# Check if fields are set correctly within SecOC_PDU list +assert p1.pdus[0].pdu_id == 0x20 +assert p1.pdus[0].pdu_payload_len == 7 +assert p1.pdus[0].tmac == b"\x22\x33\x44" +pdu = p1.pdus[0] +pdu.show() +assert pdu.pdu_payload.a == 0xaa +assert pdu.pdu_payload.b == 0xbb +assert pdu.pdu_payload.c == 0xcc + + += Build SecOC_PDUTransport with multiple SecOC_PDU packets +p1 = SecOC_PDUTransport(bytes.fromhex("00000020 00000007 aabbcc 11223344 00000040 00000007 ddeeff 55667788 000000ff 00000008 01234567 11223344 000000ff 00000008 01234567 11223344")) +p1.show() +# Check if packets are the same +assert p1 +# Check if fields are set correctly within SecOC_PDU list +assert p1.pdus[0].pdu_id == 0x20 +assert p1.pdus[1].pdu_id == 0x40 +assert p1.pdus[2].pdu_id == 0xff +assert p1.pdus[3].pdu_id == 0xff +assert p1.pdus[0].pdu_payload_len == 7 +assert p1.pdus[1].pdu_payload_len == 7 +assert p1.pdus[2].pdu_payload_len == 8 +assert p1.pdus[3].pdu_payload_len == 8 +assert p1.pdus[0].tmac == b"\x22\x33\x44" + +try: + assert p1.pdus[2].tmac == b"\x22\x33\x44" + assert False +except AttributeError: + pass + +assert p1.pdus[1].tmac == b"\x66\x77\x88" + +pdu = p1.pdus[0] +pdu.show() +assert pdu.pdu_payload.a == 0xaa +assert pdu.pdu_payload.b == 0xbb +assert pdu.pdu_payload.c == 0xcc + +pdu = p1.pdus[1] +pdu.show() +assert pdu.pdu_payload.x == 0xdd +assert pdu.pdu_payload.y == 0xee +assert pdu.pdu_payload.z == 0xff + +pdu = p1.pdus[2] +assert "PDU" in pdu.__class__.__name__ +assert pdu.payload.__class__.__name__ == "Raw" +assert pdu.load == bytes.fromhex("0123456711223344") + + +pdu = p1.pdus[3] +assert "PDU" in pdu.__class__.__name__ +assert pdu.payload.__class__.__name__ == "Raw" +assert pdu.load == bytes.fromhex("0123456711223344") + + + diff --git a/test/contrib/automotive/bmw/enet.uts b/test/contrib/automotive/bmw/enet.uts deleted file mode 100644 index 13b69b881ce..00000000000 --- a/test/contrib/automotive/bmw/enet.uts +++ /dev/null @@ -1,69 +0,0 @@ -+ ENET Contrib tests - -= Load Contrib Layer - -load_contrib("automotive.bmw.enet") - -= Basic Test 1 - -pkt = ENET(type=1, src=0xf4, dst=0x10)/Raw(b'\x11\x22\x33') -assert bytes(pkt) == b'\x00\x00\x00\x05\x00\x01\xf4\x10\x11"3' - -= Basic Test 2 - -pkt = ENET(type=1, src=0xf4, dst=0x10)/Raw(b'\x11\x22\x33\x11\x11\x11\x11\x11') -assert bytes(pkt) == b'\x00\x00\x00\x0a\x00\x01\xf4\x10\x11"3\x11\x11\x11\x11\x11' - -= Basic Dissect Test - -pkt = ENET(b'\x00\x00\x00\x0a\x00\x01\xf4\x10\x11"3\x11\x11\x11\x11\x11') -assert pkt.length == 10 -assert pkt.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 -assert pkt[1].service == 17 -assert pkt[2].resetType == 34 - -= Build Test - -pkt = ENET(src=0xf4, dst=0x10)/Raw(b"0" * 20) -assert bytes(pkt) == b'\x00\x00\x00\x16\x00\x01\xf4\x10' + b"0" * 20 - -= Dissect Test - -pkt = ENET(b'\x00\x00\x00\x18\x00\x01\xf4\x10\x67\x01' + b"0" * 20) -assert pkt.length == 24 -assert pkt.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 -assert pkt.securitySeed == b"0" * 20 -assert len(pkt[1]) == pkt.length - 2 - -= Dissect Test with padding - -pkt = ENET(b'\x00\x00\x00\x18\x00\x01\xf4\x10\x67\x01' + b"0" * 20 + b"p" * 100) -assert pkt.length == 24 -assert pkt.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 -assert pkt.securitySeed == b"0" * 20 -assert pkt.load == b'p' * 100 - -= Dissect Test to short packet - -pkt = ENET(b'\x00\x00\x00\x18\x00\x01\xf4\x10\x67\x01' + b"0" * 19) -assert pkt.length == 24 -assert pkt.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 -assert pkt.securitySeed == b"0" * 19 - -= Dissect Test very long packet - -pkt = ENET(b'\x00\x0f\xff\x04\x00\x01\xf4\x10\x67\x01' + b"0" * 0xfff00) -assert pkt.length == 0xfff04 -assert pkt.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 -assert pkt.securitySeed == b"0" * 0xfff00 - diff --git a/test/contrib/automotive/bmw/hsfz.uts b/test/contrib/automotive/bmw/hsfz.uts new file mode 100644 index 00000000000..9f199b38012 --- /dev/null +++ b/test/contrib/automotive/bmw/hsfz.uts @@ -0,0 +1,162 @@ +% Regression tests for the HSFZ layer + +# More information at http://www.secdev.org/projects/UTscapy/ + + +############ +############ + ++ HSFZ Contrib tests + += Load Contrib Layer +load_contrib("automotive.bmw.hsfz", globals_dict=globals()) + += Basic Test 1 + +pkt = HSFZ(control=1, source=0xf4, target=0x10)/Raw(b'\x11\x22\x33') +assert bytes(pkt) == b'\x00\x00\x00\x05\x00\x01\xf4\x10\x11"3' + += Basic Test 2 + +pkt = HSFZ(control=1, source=0xf4, target=0x10)/Raw(b'\x11\x22\x33\x11\x11\x11\x11\x11') +assert bytes(pkt) == b'\x00\x00\x00\x0a\x00\x01\xf4\x10\x11"3\x11\x11\x11\x11\x11' + += Basic Dissect Test + +pkt = HSFZ(b'\x00\x00\x00\x0a\x00\x01\xf4\x10\x11"3\x11\x11\x11\x11\x11') +assert pkt.length == 10 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 +assert pkt.control == 1 +assert pkt[1].service == 17 +assert pkt[2].resetType == 34 + += Build Test + +pkt = HSFZ(source=0xf4, target=0x10)/Raw(b"0" * 20) +assert bytes(pkt) == b'\x00\x00\x00\x16\x00\x01\xf4\x10' + b"0" * 20 + += Dissect Test + +pkt = HSFZ(b'\x00\x00\x00\x18\x00\x01\xf4\x10\x67\x01' + b"0" * 20) +assert pkt.length == 24 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 +assert pkt.control == 1 +assert pkt.securitySeed == b"0" * 20 +assert len(pkt[1]) == pkt.length - 2 + += Dissect Test with padding + +pkt = HSFZ(b'\x00\x00\x00\x18\x00\x01\xf4\x10\x67\x01' + b"0" * 20 + b"p" * 100) +assert pkt.length == 24 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 +assert pkt.control == 1 +assert pkt.securitySeed == b"0" * 20 +assert pkt.load == b'p' * 100 + += Dissect Test to short packet + +pkt = HSFZ(b'\x00\x00\x00\x18\x00\x01\xf4\x10\x67\x01' + b"0" * 19) +assert pkt.length == 24 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 +assert pkt.control == 1 +assert pkt.securitySeed == b"0" * 19 + + += Dissect Test very long packet + +pkt = HSFZ(b'\x00\x0f\xff\x04\x00\x01\xf4\x10\x67\x01' + b"0" * 0xfff00) +assert pkt.length == 0xfff04 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 +assert pkt.control == 1 +assert pkt.securitySeed == b"0" * 0xfff00 + + += Dissect diagnostic request + +pkt = HSFZ(hex_bytes("000000050001f41022f150")) +assert pkt.length == 5 +assert pkt.control == 0x01 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 + + += Dissect acknowledgment transfer + +pkt = HSFZ(hex_bytes("000000050002f41022f150")) +assert pkt.length == 5 +assert pkt.control == 0x02 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 + + += Dissect identification + +pkt = HSFZ(bytes.fromhex("000000320011444941474144523130424d574d4143374346436343463837393343424d5756494e5742413558373333333246483735373334")) +assert pkt.length == 50 +assert pkt.control == 0x11 +assert b"BMW" in pkt.identification_string + +pkt = UDP(bytes.fromhex("1a9be2d90040d67d000000320011444941474144523130424d574d4143374346436343463837393343424d5756494e5742413558373333333246483735373334")) +assert pkt.length == 50 +assert pkt.control == 0x11 +assert b"BMW" in pkt.identification_string + +pkt = UDP(hex_bytes("e9811a9b000ea98f000000000011")) +assert pkt.length == 0 +assert pkt.control == 0x11 + + += Dissect alive check +pkt = HSFZ(bytes.fromhex("000000200012444941474144523130424d5756494e5858585858585858585858585858585858")) +assert pkt.length == 32 +assert pkt.control == 0x12 +assert b"BMW" in pkt.identification_string + +pkt = HSFZ(bytes.fromhex("00000002001200f4")) +assert pkt.length == 2 +assert pkt.control == 0x12 +assert pkt.source == 0x00 +assert pkt.target == 0xf4 + + += Dissect incorrect tester address +pkt = HSFZ(bytes.fromhex("000000020040fff4")) +assert pkt.length == 2 +assert pkt.control == 0x40 +assert pkt.expected == 0xff +assert pkt.received == 0xf4 + + += Test HSFZSocket + +server_up = threading.Event() +def server(): + buffer = bytes(HSFZ(control=1, source=0xf4, target=0x10) / Raw(b'\x11\x22\x33' * 1024)) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('127.0.0.1', 6801)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + connection.send(buffer[:1024]) + time.sleep(0.1) + connection.send(buffer[1024:]) + connection.close() + finally: + sock.close() + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = HSFZSocket() + +pkts = sock.sniff(timeout=1, count=1) +assert len(pkts) == 1 +assert len(pkts[0]) > 2048 diff --git a/test/contrib/automotive/ccp.uts b/test/contrib/automotive/ccp.uts index b160ba1948c..b267317a580 100644 --- a/test/contrib/automotive/ccp.uts +++ b/test/contrib/automotive/ccp.uts @@ -4,86 +4,8 @@ ~ conf = Imports -load_layer("can") -conf.contribs['CAN']['swap-bytes'] = False -import threading, six -from subprocess import call -from scapy.consts import LINUX - -= Definition of constants, utility functions and mock classes -iface0 = "vcan0" -iface1 = "vcan1" - - -= Initialize a virtual CAN interface -~ vcan_socket needs_root linux -if 0 != call(["cansend", iface0, "000#"]): - # vcan0 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface0, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface0) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface0, "up"]): - raise Exception("could not bring up %s" % iface0) - -if 0 != call(["cansend", iface0, "000#"]): - raise Exception("cansend doesn't work") - -if 0 != call(["cansend", iface1, "000#"]): - # vcan1 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface1, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface1) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface1, "up"]): - raise Exception("could not bring up %s" % iface1) - -if 0 != call(["cansend", iface1, "000#"]): - raise Exception("cansend doesn't work") - -print("CAN should work now") - -= Import CANSocket - -from scapy.contrib.cansocket_python_can import * - -import can as python_can -new_can_socket = lambda iface: CANSocket(bustype='virtual', channel=iface) -new_can_socket0 = lambda: CANSocket(bustype='virtual', channel=iface0, timeout=0.01) -new_can_socket1 = lambda: CANSocket(bustype='virtual', channel=iface1, timeout=0.01) - -# utility function for draining a can interface, asserting that no packets are there -def drain_bus(iface=iface0, assert_empty=True): - with new_can_socket(iface) as s: - pkts = s.sniff(timeout=0.1) - if assert_empty: - assert len(pkts) == 0 - -print("CAN sockets should work now") - -= Overwrite definition for vcan_socket systems native sockets -~ vcan_socket not_pypy needs_root linux - -if six.PY3 and LINUX: - from scapy.contrib.cansocket_native import * - new_can_socket = lambda iface: CANSocket(iface) - new_can_socket0 = lambda: CANSocket(iface0) - new_can_socket1 = lambda: CANSocket(iface1) - - -= Overwrite definition for vcan_socket systems python-can sockets -~ vcan_socket needs_root linux - -if "python_can" in CANSocket.__module__: - new_can_socket = lambda iface: CANSocket(bustype='socketcan', channel=iface, bitrate=250000, timeout=0.01) - new_can_socket0 = lambda: CANSocket(bustype='socketcan', channel=iface0, bitrate=250000, timeout=0.01) - new_can_socket1 = lambda: CANSocket(bustype='socketcan', channel=iface1, bitrate=250000, timeout=0.01) - - -= Verify that a CAN socket can be created and closed -s = new_can_socket(iface0) -s.close() +from test.testsocket import TestSocket, cleanup_testsockets ############ ############ @@ -91,9 +13,7 @@ s.close() + Basic operations = Load module - -load_contrib("automotive.ccp") -from scapy.contrib.automotive.ccp import CONNECT, DISCONNECT +load_contrib("automotive.ccp", globals_dict=globals()) = Build CRO CONNECT @@ -957,23 +877,24 @@ assert dto.hashret() == cro.hashret() + Tests on a virtual CAN-Bus -= CAN Socket sr1 with dto.ansers(cro) == True - -with new_can_socket0() as sock1, new_can_socket0() as sock2: - sock1.basecls = CCP - started = threading.Event() - def ecu(): - pkts = sock2.sniff(count=1, timeout=1, started_callback=started.set) - if len(pkts) == 1: - cro = CRO(pkts[0].data) - assert cro.cmd == 0x22 - assert cro.data == b"\x10\x11\x12\x10\x11\x12" - sock2.send(CCP(b'\x00\x00\x07\x00\x08\x00\x00\x00\xff\x00\x53\x02\x34\x00\x20\x06')) - thread = threading.Thread(target=ecu) - thread.start() - started.wait(timeout=5) - dto = sock1.sr1(CCP(identifier=0x700)/CRO(ctr=0x53)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12"), timeout=1) - thread.join(timeout=5) += CAN Socket sr1 with dto.answers(cro) == True + +sock1 = TestSocket(CCP) +sock2 = TestSocket(CAN) +sock1.pair(sock2) + +def answer(pkt): + cro = CRO(pkt.data) + assert cro.cmd == 0x22 + assert cro.data == b"\x10\x11\x12\x10\x11\x12" + sock2.send(CCP(b'\x00\x00\x07\x00\x08\x00\x00\x00\xff\x00\x53\x02\x34\x00\x20\x06')) + +sniffer = AsyncSniffer(opened_socket=sock2, count=1, timeout=5, prn=answer) +sniffer.start() +dto = sock1.sr1(CCP(identifier=0x700)/CRO(ctr=0x53)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12"), timeout=1, verbose=False) +sniffer.join(timeout=5) +sock1.close() +sock2.close() assert dto.ctr == 83 assert dto.packet_id == 0xff @@ -982,46 +903,45 @@ assert hasattr(dto, "load") == False assert dto.MTA0_extension == 2 assert dto.MTA0_address == 0x34002006 -= CAN Socket sr1 with dto.ansers(cro) == False - -with new_can_socket0() as sock1, new_can_socket0() as sock2: - sock1.basecls = CCP - started = threading.Event() - def ecu(): - pkts = sock2.sniff(count=1, timeout=1, started_callback=started.set) - if len(pkts) == 1: - cro = CRO(pkts[0].data) - assert cro.cmd == 0x22 - assert cro.data == b"\x10\x11\x12\x10\x11\x12" - sock2.send(CCP(b'\x00\x00\x07\x00\x08\x00\x00\x00\xff\x00\x55\x02\x34\x00\x20\x06')) - thread = threading.Thread(target=ecu) - thread.start() - started.wait(timeout=5) - gotTimeout = False - dto = sock1.sr1(CCP(identifier=0x700)/CRO(ctr=0x54)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12"), timeout=1) - print(dto) - if dto is None: - gotTimeout = True - assert gotTimeout - thread.join(timeout=5) += CAN Socket sr1 with dto.answers(cro) == False + +sock1 = TestSocket(CCP) +sock2 = TestSocket(CAN) +sock1.pair(sock2) + +def answer(pkt): + cro = CRO(pkt.data) + assert cro.cmd == 0x22 + assert cro.data == b"\x10\x11\x12\x10\x11\x12" + sock2.send(CCP(b'\x00\x00\x07\x00\x08\x00\x00\x00\xff\x00\x55\x02\x34\x00\x20\x06')) + +sniffer = AsyncSniffer(opened_socket=sock2, count=1, timeout=5, prn=answer) +sniffer.start() +dto = sock1.sr1(CCP(identifier=0x700)/CRO(ctr=0x54)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12"), timeout=0.1, verbose=False) +sniffer.join(timeout=5) +sock1.close() +sock2.close() +assert dto is None + = CAN Socket sr1 with error code -with new_can_socket0() as sock1, new_can_socket0() as sock2: - sock1.basecls = CCP - started = threading.Event() - def ecu(): - pkts = sock2.sniff(count=1, timeout=1, started_callback=started.set) - if len(pkts) == 1: - cro = CRO(pkts[0].data) - assert cro.cmd == 0x22 - assert cro.data == b"\x10\x11\x12\x10\x11\x12" - sock2.send(CCP(b'\x00\x00\x07\x00\x08\x00\x00\x00\xff\x01\x55\xff\xff\xff\xff\xff')) - thread = threading.Thread(target=ecu) - thread.start() - started.wait(timeout=5) - dto = sock1.sr1(CCP(identifier=0x700)/CRO(ctr=0x55)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12"), timeout=1) - thread.join(timeout=5) +sock1 = TestSocket(CCP) +sock2 = TestSocket(CAN) +sock1.pair(sock2) + +def answer(pkt): + cro = CRO(pkt.data) + assert cro.cmd == 0x22 + assert cro.data == b"\x10\x11\x12\x10\x11\x12" + sock2.send(CCP(b'\x00\x00\x07\x00\x08\x00\x00\x00\xff\x01\x55\xff\xff\xff\xff\xff')) + +sniffer = AsyncSniffer(opened_socket=sock2, count=1, timeout=5, prn=answer) +sniffer.start() +dto = sock1.sr1(CCP(identifier=0x700)/CRO(ctr=0x55)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12"), timeout=1, verbose=False) +sniffer.join(timeout=5) +sock1.close() +sock2.close() assert dto.ctr == 85 assert dto.packet_id == 0xff @@ -1032,11 +952,7 @@ assert dto.MTA0_address == 0xffffffff + Cleanup -= Delete vcan interfaces -~ vcan_socket needs_root linux += Delete TestSockets -if 0 != call(["sudo", "ip", "link", "delete", iface0]): - raise Exception("%s could not be deleted" % iface0) +cleanup_testsockets() -if 0 != call(["sudo", "ip", "link", "delete", iface1]): - raise Exception("%s could not be deleted" % iface1) diff --git a/test/contrib/automotive/doip.uts b/test/contrib/automotive/doip.uts new file mode 100644 index 00000000000..f224e39a9c6 --- /dev/null +++ b/test/contrib/automotive/doip.uts @@ -0,0 +1,924 @@ +% Regression tests for the DoIP layer + +# More information at http://www.secdev.org/projects/UTscapy/ + + +############ +############ + ++ Doip contrib tests + += Load Contrib Layer + +from test.testsocket import TestSocket, cleanup_testsockets, UnstableSocket + +load_contrib("automotive.doip", globals_dict=globals()) +load_contrib("automotive.uds", globals_dict=globals()) + += Defaults test + +p = DoIP(payload_type=1) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == None +assert p.payload_type == 1 + += Build test 0 + +p = DoIP(bytes(DoIP(payload_type=0))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 1 +assert p.payload_type == 0 +assert p.nack == 0 + += Build test 1 + +p = DoIP(bytes(DoIP(payload_type=1))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 0 +assert p.payload_type == 1 + += Build test 2 + +p = DoIP(bytes(DoIP(payload_type=2))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 6 +assert p.payload_type == 2 +assert bytes(p.eid) == b"\x00" * 6 + += Build test 3 + +p = DoIP(bytes(DoIP(payload_type=3))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 17 +assert p.payload_type == 3 +assert bytes(p.vin) == b"\x00" * 17 + += Build test 4 + +p = DoIP(bytes(DoIP(payload_type=4))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 33 +assert p.payload_type == 4 +assert bytes(p.vin) == b"\x00" * 17 +assert p.logical_address == 0 +assert bytes(p.eid) == b"\x00" * 6 +assert bytes(p.gid) == b"\x00" * 6 +assert p.further_action == 0 +assert p.vin_gid_status == 0 + += Build test 5 + +p = DoIP(bytes(DoIP(payload_type=5))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 7 +assert p.payload_type == 5 +assert p.source_address == 0 +assert p.activation_type == 0 +assert p.reserved_iso == 0 +assert p.reserved_oem == b"" + += Build test 5.1 + +p = DoIP(bytes(DoIP(payload_type=5, reserved_oem=b"1234"))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 11 +assert p.payload_type == 5 +assert p.source_address == 0 +assert p.activation_type == 0 +assert p.reserved_iso == 0 +p.show() +print(p.reserved_oem) +assert p.reserved_oem == b"1234" + += Build test 5.2 + +p = DoIP(bytes(DoIP(payload_type=5, reserved_oem=b"12"))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 9 +assert p.payload_type == 5 +assert p.source_address == 0 +assert p.activation_type == 0 +assert p.reserved_iso == 0 +assert p.reserved_oem == b"12" + += Build test 6 + +p = DoIP(bytes(DoIP(payload_type=6))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 9 +assert p.payload_type == 6 +assert p.logical_address_tester == 0 +assert p.logical_address_doip_entity == 0 +assert p.reserved_iso == 0 +assert p.reserved_oem == b"" + += Build test 7 + +p = DoIP(bytes(DoIP(payload_type=7))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 0 +assert p.payload_type == 7 + += Build test 8 + +p = DoIP(bytes(DoIP(payload_type=8))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 2 +assert p.payload_type == 8 +assert p.source_address == 0 + += Build test 4001 + +p = DoIP(bytes(DoIP(payload_type=0x4001))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 0 +assert p.payload_type == 0x4001 + + += Build test 4002 + +p = DoIP(bytes(DoIP(payload_type=0x4002))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 7 +assert p.payload_type == 0x4002 +assert p.node_type == 0 +assert p.max_open_sockets == 1 +assert p.cur_open_sockets == 0 +assert p.max_data_size == 0 + + += Build test 4003 + +p = DoIP(bytes(DoIP(payload_type=0x4003))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 0 +assert p.payload_type == 0x4003 + + += Build test 4004 + +p = DoIP(bytes(DoIP(payload_type=0x4004))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 1 +assert p.payload_type == 0x4004 +assert p.diagnostic_power_mode == 0 + += Build test 8001 + +p = DoIP(bytes(DoIP(payload_type=0x8001))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 4 +assert p.payload_type == 0x8001 +assert p.source_address == 0 +assert p.target_address == 0 + += Build test 8002 + +p = DoIP(bytes(DoIP(payload_type=0x8002))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 5 +assert p.payload_type == 0x8002 +assert p.source_address == 0 +assert p.target_address == 0 +assert p.ack_code == 0 +assert p.previous_msg == b'' + +p = DoIP(bytes(DoIP(payload_type=0x8002, previous_msg=b'\x22\xfd\x32'))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 8 +assert p.payload_type == 0x8002 +assert p.source_address == 0 +assert p.target_address == 0 +assert p.ack_code == 0 +assert p.previous_msg == b'\x22\xfd\x32' + +p = DoIP(bytes(DoIP(payload_type=0x8002, previous_msg=b'\x19\x02\x09\x9C\x00'))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 10 +assert p.payload_type == 0x8002 +assert p.source_address == 0 +assert p.target_address == 0 +assert p.ack_code == 0 +assert p.previous_msg == b'\x19\x02\t\x9c\x00' + +p = DoIP(b'\x02\xfd\x80\x02\x00\x00\x00\x07\x00\x08\x00\x0e\x00\x10\x01') +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xFD +assert p.payload_length == 7 +assert p.payload_type == 0x8002 +assert p.source_address == 0x8 +assert p.target_address == 0xE +assert p.ack_code == 0 +assert p.previous_msg == b'\x10\x01' + += Build test 8003 + +p = DoIP(bytes(DoIP(payload_type=0x8003))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 5 +assert p.payload_type == 0x8003 +assert p.source_address == 0 +assert p.target_address == 0 +assert p.nack_code == 0 + + +p = DoIP(bytes(DoIP(payload_type=0x8003, previous_msg=b'\x2E\xfd\x32\x01\x02'))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 10 +assert p.payload_type == 0x8003 +assert p.source_address == 0 +assert p.target_address == 0 +assert p.nack_code == 0 +assert p.previous_msg == b'.\xfd2\x01\x02' + +p = DoIP(bytes(DoIP(payload_type=0x8003, previous_msg=b'\x19\x02\x09\x9A\x00'))) + +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 10 +assert p.payload_type == 0x8003 +assert p.source_address == 0 +assert p.target_address == 0 +assert p.nack_code == 0 +assert p.previous_msg == b'\x19\x02\t\x9a\x00' + +p = DoIP(b'\x02\xfd\x80\x03\x00\x00\x00\x07\x00\x0A\x00\x0C\x00\x10\x03') +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xFD +assert p.payload_length == 7 +assert p.payload_type == 0x8003 +assert p.source_address == 0xA +assert p.target_address == 0xC +assert p.nack_code == 0 +assert p.previous_msg == b'\x10\x03' + ++ pcap based tests + += read diag_ack pcap file +pkt = rdpcap(scapy_path("test/pcaps/doip_ack.pcap")).res[0] + +assert len(pkt) == 70 + += dissect test of diag ACK with previous_msg field filled +assert pkt.protocol_version == 0x02 +assert pkt.inverse_version == 0xFD +assert pkt.payload_length == 8 +assert pkt.source_address == 0x4B +assert pkt.target_address == 0xE00 +assert pkt.ack_code == 0 +assert pkt.previous_msg == b'\x22\xFD\x31' + + += read main pcap file + +pkts = rdpcap(scapy_path("test/pcaps/doip.pcap.gz")) +ips = [p for p in pkts if p.proto == 6] + +assert len(ips) > 1 + += dissect test of routing activation pkts req + +req = ips[0] +p = req +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 11 +assert p.payload_type == 0x5 +assert p.source_address == 0xe80 +assert p.activation_type == 0 +assert p.reserved_iso == 0 +assert p.reserved_oem == b"\x00\x00\x00\x00" + += dissect test of routing activation pkts resp + +resp = ips[1] +p = resp +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 9 +assert p.payload_type == 0x6 +assert p.logical_address_tester == 0xe80 +assert p.logical_address_doip_entity == 0x4010 +assert p.routing_activation_response == 16 +assert p.reserved_iso == 0 + += answers test of routing activation pkts + +assert resp.answers(req) +assert resp.hashret() == req.hashret() + += dissect diagnostic message + +req = ips[-4] +resp = ips[-1] + +p = req +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 6 +assert p.payload_type == 0x8001 +assert p.source_address == 0xe80 +assert p.target_address == 0x4010 +assert bytes(p)[-2:] == bytes(UDS()/UDS_DSC(b"\x02")) +assert p.service == 0x10 +assert p.diagnosticSessionType == 2 + +p = resp +assert p.protocol_version == 0x02 +assert p.inverse_version == 0xfd +assert p.payload_length == 10 +assert p.payload_type == 0x8001 +assert p.target_address == 0xe80 +assert p.source_address == 0x4010 +assert bytes(p)[-6:] == bytes(UDS()/UDS_DSCPR(b"\x02\x002\x01\xf4")) +assert p.service == 0x50 +assert p.diagnosticSessionType == 2 + +assert req.hashret() == resp.hashret() +# exclude TCP layer from answers check +assert resp[3].answers(req[3]) +assert not req[3].answers(resp[3]) + += TCPSession Test + +tmp_file = get_temp_file() + +wrpcap(tmp_file, [ + IP(src="10.10.10.10", dst="10.10.10.11") / TCP(sport=61000, seq=1) / DoIP(payload_type=0x8001, payload_length=6) / b"\x3E", + IP(src="10.10.10.10", dst="10.10.10.11") / TCP(sport=61000, dport=13400, seq=14) / Raw(load=b"\xff") +]) + +pkts = sniff(offline=tmp_file, session=TCPSession) +assert pkts[0].haslayer(UDS_TP) +assert pkts[0].service == 0x3E + += TCPSession Test multiple DoIP messages + +filename = scapy_path("/test/pcaps/multiple_doip_layers.pcap.gz") + +pkts = sniff(offline=filename, session=TCPSession) +print(repr(pkts[0])) +print(repr(pkts[1])) +assert len(pkts) == 2 +assert pkts[0][DoIP].payload_length == 2 +assert pkts[0][DoIP:2].payload_length == 7 +assert pkts[1][DoIP].payload_length == 103 + += Doip logical addressing + +filename = scapy_path("/test/pcaps/doip_functional_request.pcap.gz") +tx_sock = TestSocket(DoIP) +rx_sock = TestSocket(DoIP) +tx_sock.pair(rx_sock) + +for pkt in PcapReader(filename): + if pkt.haslayer(DoIP): + tx_sock.send(pkt[DoIP]) + +ans, unans = rx_sock.sr(DoIP(bytes(DoIP(payload_type=0x8001, source_address=0xe80, target_address=0xe400) / UDS() / UDS_TP())), multi=True, timeout=0.1, verbose=False) + +cleanup_testsockets() + +ans.summary() +if unans: + unans.summary() + +assert len(ans) == 8 +ans.summary() +assert len(unans) == 0 + + ++ DoIP Communication tests + += Load libraries +import base64 +import ssl +import tempfile + += Test DoIPSocket + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('127.0.0.1', 13400)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + sniff_up.wait(timeout=1) + connection.send(buffer) + connection.close() + finally: + sock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = DoIPSocket(activate_routing=False) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_thread.join(timeout=1) +assert len(pkts) == 2 + + += Test DoIPSocket 2 +~ linux + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('127.0.0.1', 13400)) + sock.listen(1) + server_up.set() + try: + connection, address = sock.accept() + sniff_up.wait(timeout=1) + for i in range(len(buffer)): + connection.send(buffer[i:i+1]) + time.sleep(0.01) + finally: + connection.close() + finally: + sock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = DoIPSocket(activate_routing=False) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_thread.join(timeout=1) +assert len(pkts) == 2 + += Test DoIPSocket 2 enforce protocol_version +~ linux + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('127.0.0.1', 13400)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + try: + sniff_up.wait(timeout=1) + connection.send(buffer) + doip_sock = DoIPSSLStreamSocket(connection) + pkts = doip_sock.sniff(timeout=2, count=1) + doip_sock.send(pkts[0]) + finally: + connection.close() + finally: + sock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = DoIPSocket(activate_routing=False, doip_version=3, enforce_doip_version=True) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +sock.send(DoIP(payload_type=0x8001, source_address=0xe80, target_address=0xe400) / UDS() / UDS_TP()) +pkts2 = sock.sniff(timeout=1, count=1) +server_thread.join(timeout=1) +assert len(pkts) == 2 +assert len(pkts2) == 1 +assert pkts2[0].protocol_version == 0x03 +assert pkts2[0].inverse_version == 0xfc +assert pkts2[0].payload_type == 0x8001 +assert pkts2[0].service == 0x3E + += Test DoIPSocket 3 + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('127.0.0.1', 13400)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + sniff_up.wait(timeout=1) + while buffer: + randlen = random.randint(0, len(buffer)) + connection.send(buffer[:randlen]) + buffer = buffer[randlen:] + time.sleep(0.01) + connection.close() + finally: + sock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = DoIPSocket(activate_routing=False) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_thread.join(timeout=1) +assert len(pkts) == 2 + + += Test DoIPSocket6 + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('::1', 13400)) + sock.listen(1) + server_up.set() + connection, address = sock.accept() + sniff_up.wait(timeout=1) + connection.send(buffer) + connection.close() + finally: + sock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +sock = DoIPSocket(ip="::1", activate_routing=False) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_thread.join(timeout=1) +assert len(pkts) == 2 + += Test DoIPSslSocket +~ broken_windows + +certstring = """ +LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZB +QVNDQktZd2dnU2lBZ0VBQW9JQkFRRFUvK0hRbVpzSDl2QVcKQ3ZMQjRxalpnZFJSSXE1b2JBanB4 +YUhoUGxCVEMvUlBzMHIxRVF0V0FtbXNEZFE3UGlLaCtYa1hES3pNY3lJSQp1a0ZpNThUQW1idGFj +N0U5VmJHSnNlTWp2RkJKSkFqQXVtbFdRZk5XcSs2TkZhdmRkTDQrSTNBTVJ5TldJTkJYCjhHMzRo +dldIbDdTOGhhSFFZN0FXcUZWVTNVL2xKR2pubnF3MEJraEIvVGRCTWIwM0habzkrVjIrWU9RZmk5 +QWsKTVRSRXpSeWVObWJqT0sxbHpXdFJXWkZZU0RnMEtqUVh4SkdFNVc5MzFPWitHL1NkbytTM1ZW +SVRPdWxQbHRmVwpXMEdjeCsvZERSNFIxNG5mcUl5L1daMElHUVNXMlRsQytmeGJ0dURDUkFqelRz +b0J3YjJ0cnpoR0VtYVFveUtNCnpBKzVSUHNyQWdNQkFBRUNnZ0VBRUJHaEoyWm5OVHh5YVY5TnZY +QjI1NDNZQnRUMGVSUHBhanJLMXg0bk1OU3oKNE9LNFVzWlo1MnBnTHRHT1EzZm1aS0l0cEo1WlY1 +cVBUejdwN3VjUzhnQWNZUnNJUnpCMHA5d3FpWExMK3h0RApxUjB4dnR4VDJpUGlFblVNNndudHpr +SHpKK0g0QkZLT2FvdjNaK3Fha2E1UmFCcmhheGRuaDBDNklLQmZtM3cyCm5zUWI2N0lCYWwrSnBs +L1g5TENWRkdRT2owb0lmVWI5ZFp3OWQ3MCthSGVVb2xvMGdYZmxxcXFFcnl3ZDlPN2QKNnp4dGlx +cnRyZUJhK1IraWs3NE1SK0xvaFNVR3o2VTRQaXhWQ3l1SnQ2U0hvRHR2L3dtSnltWDd2a0FRS2w1 +RQplK1JqUGVyakpUWTNzNXNXbEd2V21UTEtEbnVyS2pBYzZUOHhKb0pXWlFLQmdRRHdsd2RRdmww +S28wNHhDUmtiCklYRGVJZE1jZkp2ejRGZEtka1BmVnZVT2xHVEpNZkRzbWNoUzZhcEJCQUdQMUU2 +VkN2VzJmUFdjaXhScHE3MW8KR2xtbWZ5RnlJRW0rL08yamMvSFRXWHp6Qjdoc0JISEltQklHczFU +TC9iWFU3amhVQW5kWDdMK3RSRDBKNWRGVwpiN1VOOXNxaWdtRG42REJWZkxaUHgxRnlWUUtCZ1FE +aXBIT1BhNmVMSlk5R1FZdkw3OTIyTHNoU3ZYSUFVMERGCjBabTlqbjM2b3ZIY0kvWEZDdHVXank2 +WG9wbk9pbjlycmtUY2FDUnBvSEFNb00ycHdiR0tFY0dVVEY2RHQ3akYKRHVnd2srR21sbDkrbjM2 +M3Iwb09YNktSbWFhRStiZHoyNjNQVEhMaktYUnFyc3h5WEtMT3ZyTXhVNWNzMXJCeQpTMWI2ZGhr +M2Z3S0JnRjlONUliMnNkS3ArQ3B5aVRCM0ljZk1yRjBuZTN1ekRjRWdjaWlCd05lQ3J4NElHNEVP +Ck5nMnFKRmhXNXV0NzFaa3kyenpyNlR1VzJJSTNsdk1ySlFKUWNBWk9oZ2dURjJ2ZFhSazA1TXM4 +N3JCVFhtTncKNGdzbmROck42UDZ0VTBEc0xTeDJTME91dVdNM1Y2S2U0NkRoZDBuQ3pmSnZ4dDNH +WmszYURnaDFBb0dBWFhIcQpoNDZlZEx1V3VDUGNUTWhvUkc1RGdBSEdHQ1k3UlpTbTY4WHRZVUov +c0FGUG10OWdMRko2cG1DUFE5NU1yUXdjCkxqZnVFM0xuMy8wSTd0NENvbWV4eGNBN0U5blRIOFNH +clVpN3QrQzJITklNQUJZUTFaNU91L042K2Nhd0FkL28KYU5rZllWTzlRU015L2svOWZIcWFEVk5t +dUVFSVhRZDlKQ1UvUG1jQ2dZQWI0RTBRWTdDZmlrV293OFIzSlhoZgo0MHFVVkdud09QKzJNbXE5 +d2ZmWkpTRHNFSTQvb2g0VGRnN0sybHNNazVsWnRaMyszTjljSDVUc1pMYlJtd2FMCm9sRVl6K1BB +WU91MlMrY1l2bFlNL0V2WmlpRHJybjZuTStNbTNnaXJPYkNwMzcxd1ZxRFVsUnB4OUlwWVdYcnAK +T3YxUXFHdXkwODdyQkk1cStWL3hqQT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KLS0tLS1C +RUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ3VENDQXRXZ0F3SUJBZ0lVVTNsendsTVNSa294Tkdk +SFJzZllIcUtxcDAwd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZVXhDekFKQmdOVkJBWVRBa1JGTVJN +d0VRWURWUVFJREFwVGIyMWxMVk4wWVhSbE1Rd3dDZ1lEVlFRSApEQU5TUlVjeEVUQVBCZ05WQkFv +TUNHUnBjM05sWTNSdk1Rd3dDZ1lEVlFRTERBTkVSVll4RFRBTEJnTlZCQU1NCkJGUkZVMVF4SXpB +aEJna3Foa2lHOXcwQkNRRVdGR052Ym5SaFkzUXRkWE5BWkdsemMyVmpMblJ2TUI0WERUSTAKTURN +eE9ERTVNek13TlZvWERUSTBNRFF4TnpFNU16TXdOVm93Z1lVeEN6QUpCZ05WQkFZVEFrUkZNUk13 +RVFZRApWUVFJREFwVGIyMWxMVk4wWVhSbE1Rd3dDZ1lEVlFRSERBTlNSVWN4RVRBUEJnTlZCQW9N +Q0dScGMzTmxZM1J2Ck1Rd3dDZ1lEVlFRTERBTkVSVll4RFRBTEJnTlZCQU1NQkZSRlUxUXhJekFo +QmdrcWhraUc5dzBCQ1FFV0ZHTnYKYm5SaFkzUXRkWE5BWkdsemMyVmpMblJ2TUlJQklqQU5CZ2tx +aGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBMVAvaDBKbWJCL2J3RmdyeXdlS28yWUhV +VVNLdWFHd0k2Y1doNFQ1UVV3djBUN05LOVJFTFZnSnByQTNVCk96NGlvZmw1Rnd5c3pITWlDTHBC +WXVmRXdKbTdXbk94UFZXeGliSGpJN3hRU1NRSXdMcHBWa0h6VnF2dWpSV3IKM1hTK1BpTndERWNq +VmlEUVYvQnQrSWIxaDVlMHZJV2gwR093RnFoVlZOMVA1U1JvNTU2c05BWklRZjAzUVRHOQpOeDJh +UGZsZHZtRGtINHZRSkRFMFJNMGNualptNHppdFpjMXJVVm1SV0VnNE5DbzBGOFNSaE9WdmQ5VG1m +aHYwCm5hUGt0MVZTRXpycFQ1YlgxbHRCbk1mdjNRMGVFZGVKMzZpTXYxbWRDQmtFbHRrNVF2bjhX +N2Jnd2tRSTgwN0sKQWNHOXJhODRSaEpta0tNaWpNd1B1VVQ3S3dJREFRQUJvMU13VVRBZEJnTlZI +UTRFRmdRVVZhbUFkUjR1ZW8zQgpmV0RjUlMyUkQ3OEtlZXd3SHdZRFZSMGpCQmd3Rm9BVVZhbUFk +UjR1ZW8zQmZXRGNSUzJSRDc4S2Vld3dEd1lEClZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5 +dzBCQVFzRkFBT0NBUUVBRjE1TTNvL3RyUVdYeHdHamlxZjgKNXBUTEM0bHJwQkZaTFZDbStQdHd4 +aENlN1ZSd2dLMElBb01EMW0vSjNEYnVJSjVURXlTVElnR2N0WHVNbG5pWgpsY3IwekZOZVVhQ08w +YkdhaExYUXpCWTRxSkhTTUNWNnhiNXNqUDlEdk9HYnFxbHVTbk51ZFJ5UWNIbkd4SE0rCk1adXpO +WUNseklOMEtYbFJuSTZqRXUrcG9XZ0pEMGN1NFM2b1lwT2R3bElRYmtaNnIrUE1jQ3hpRmhRd3E2 +em4KcE1nQzB0WlpSM3pCOEpVcTJwRHlGVy9jVlFjWkp5YUhnQkkwWlJWWG5wbDFqYng2YlNIOCts +cnMxVk1xZDlkcQozd1BMcjBheWI2VkpNa29WMjNWSXAzLzlYQVpTR3Z6Y0dadnM2VThSUTdFbUtx +akJibWxudm1CTkpUMk9xbFFRCllRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=""" + +certstring = certstring.replace('\n', '') + +def _load_certificate_chain(context) -> None: + with tempfile.NamedTemporaryFile(delete=False) as fp: + fp.write(base64.b64decode(certstring)) + fp.close() + context.load_cert_chain(fp.name) + + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + _load_certificate_chain(context) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + ssock = context.wrap_socket(sock) + try: + ssock.bind(('127.0.0.1', 3496)) + ssock.listen(1) + server_up.set() + connection, address = ssock.accept() + sniff_up.wait(timeout=1) + connection.send(buffer) + connection.close() + finally: + ssock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.check_hostname = False +context.verify_mode = ssl.CERT_NONE +sock = DoIPSocket(activate_routing=False, force_tls=True, context=context) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_thread.join(timeout=1) +assert len(pkts) == 2 + += Test DoIPSslSocket6 +~ broken_windows + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + _load_certificate_chain(context) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + ssock = context.wrap_socket(sock) + try: + ssock.bind(('::1', 3496)) + ssock.listen(1) + server_up.set() + connection, address = ssock.accept() + sniff_up.wait(timeout=1) + connection.send(buffer) + connection.close() + finally: + ssock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.check_hostname = False +context.verify_mode = ssl.CERT_NONE +sock = DoIPSocket(ip="::1", activate_routing=False, force_tls=True, context=context) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_thread.join(timeout=1) +assert len(pkts) == 2 + += Test UDS_DoIPSslSocket6 +~ broken_windows + +server_up = threading.Event() +sniff_up = threading.Event() +def server(): + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + _load_certificate_chain(context) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + buffer = b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + ssock = context.wrap_socket(sock) + try: + ssock.bind(('::1', 3496)) + ssock.listen(1) + server_up.set() + connection, address = ssock.accept() + sniff_up.wait(timeout=1) + connection.send(buffer) + connection.close() + finally: + ssock.close() + + +server_thread = threading.Thread(target=server) +server_thread.start() +server_up.wait(timeout=1) +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.check_hostname = False +context.verify_mode = ssl.CERT_NONE +sock = UDS_DoIPSocket(ip="::1", activate_routing=False, force_tls=True, context=context) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_thread.join(timeout=1) +assert len(pkts) == 2 + += Test UDS_DualDoIPSslSocket6 +~ broken_windows not_pypy + +server_tcp_up = threading.Event() +server_tls_up = threading.Event() +sniff_up = threading.Event() +def server_tls(): + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + _load_certificate_chain(context) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + buffer = bytes.fromhex("02fd0006000000090e8011061000000000") + buffer += b'\x02\xfd\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x02\xfd\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + ssock = context.wrap_socket(sock) + try: + ssock.bind(('::1', 3496)) + ssock.listen(1) + server_tls_up.set() + connection, address = ssock.accept() + sniff_up.wait(timeout=1) + connection.send(buffer) + connection.close() + finally: + ssock.close() + +def server_tcp(): + buffer = bytes.fromhex("02fd0006000000090e8011060700000000") + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('::1', 13400)) + sock.listen(1) + server_tcp_up.set() + connection, address = sock.accept() + connection.send(buffer) + connection.shutdown(socket.SHUT_RDWR) + connection.close() + finally: + sock.close() + + +server_tcp_thread = threading.Thread(target=server_tcp) +server_tcp_thread.start() +server_tcp_up.wait(timeout=1) +server_tls_thread = threading.Thread(target=server_tls) +server_tls_thread.start() +server_tls_up.wait(timeout=1) +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.check_hostname = False +context.verify_mode = ssl.CERT_NONE + + +sock = UDS_DoIPSocket(ip="::1", context=context) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_tcp_thread.join(timeout=1) +server_tls_thread.join(timeout=1) +assert len(pkts) == 2 + += Test UDS_DualDoIPSslSocket6 force version 3 +~ broken_windows not_pypy + +server_tcp_up = threading.Event() +server_tls_up = threading.Event() +sniff_up = threading.Event() +def server_tls(): + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + _load_certificate_chain(context) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + buffer = bytes.fromhex("03fc0006000000090e8011061000000000") + buffer += b'\x03\xfc\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00\x03\xfc\x80\x01\x00\x00\x00\n\x10\x10\x0e\x80P\x03\x002\x01\xf4' + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + ssock = context.wrap_socket(sock) + try: + ssock.bind(('::1', 3496)) + ssock.listen(1) + server_tls_up.set() + connection, address = ssock.accept() + sniff_up.wait(timeout=1) + connection.send(buffer) + connection.close() + finally: + ssock.close() + +def server_tcp(): + buffer = bytes.fromhex("03fc0006000000090e8011060700000000") + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('::1', 13400)) + sock.listen(1) + server_tcp_up.set() + connection, address = sock.accept() + connection.send(buffer) + connection.shutdown(socket.SHUT_RDWR) + connection.close() + finally: + sock.close() + + +server_tcp_thread = threading.Thread(target=server_tcp) +server_tcp_thread.start() +server_tcp_up.wait(timeout=1) +server_tls_thread = threading.Thread(target=server_tls) +server_tls_thread.start() +server_tls_up.wait(timeout=1) +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.check_hostname = False +context.verify_mode = ssl.CERT_NONE + +conf.debug_dissector = True + +sock = UDS_DoIPSocket(ip="::1", context=context, doip_version=3, enforce_doip_version=True) + +pkts = sock.sniff(timeout=1, count=2, started_callback=sniff_up.set) +server_tcp_thread.join(timeout=1) +server_tls_thread.join(timeout=1) +assert len(pkts) == 2 \ No newline at end of file diff --git a/test/contrib/automotive/ecu.uts b/test/contrib/automotive/ecu.uts index fb6bb51c7db..4ced520357b 100644 --- a/test/contrib/automotive/ecu.uts +++ b/test/contrib/automotive/ecu.uts @@ -1,4 +1,4 @@ -% Regression tests for the ECU utility +% Regression tests for the Ecu utility # More information at http://www.secdev.org/projects/UTscapy/ @@ -10,215 +10,702 @@ = Load modules -load_contrib('isotp') -load_contrib("automotive.uds") -load_contrib("automotive.gm.gmlan") -load_layer("can") +import copy +import itertools + +load_contrib("isotp", globals_dict=globals()) +load_contrib("automotive.uds", globals_dict=globals()) +load_contrib("automotive.gm.gmlan", globals_dict=globals()) +load_layer("can", globals_dict=globals()) conf.contribs["CAN"]["swap-bytes"] = True -= Load ECU module += Load Ecu module + +load_contrib("automotive.ecu", globals_dict=globals()) +from scapy.contrib.automotive.uds_ecu_states import * +from scapy.contrib.automotive.uds_logging import * +from scapy.contrib.automotive.gm.gmlan_ecu_states import * +from scapy.contrib.automotive.gm.gmlan_logging import * + ++ EcuState Basic checks + += Check EcuState basic functionality + +state = EcuState() +state["session"] = 2 +state["securityAccess"] = 4 +print(repr(state)) +assert repr(state) == "securityAccess4session2" + += More complex tests + +state = EcuState(ses=4) +assert state.ses == 4 +state.ses = 5 +assert state.ses == 5 + += Even more complex tests + +state = EcuState(myinfo="42") + +state.ses = 5 +assert state.ses == 5 + +state["ses"] = None +assert state.ses is None + +state.ses = 5 +assert 5 == state.ses + +assert "42" == state.myinfo +assert repr(state) == "myinfo42ses5" + += Delete Attribute Test + +state = EcuState(myinfo="42") + +state.ses = 5 +assert state.ses == 5 + +del state.ses + +try: + x = state.ses + assert False +except (KeyError, AttributeError): + assert state.myinfo == "42" + += Copy tests + +state = EcuState(myinfo="42") +state.ses = 5 + +ns = copy.copy(state) + +ns.ses = 6 + +assert ns.ses == 6 +assert state.ses == 5 +assert ns.myinfo == "42" + + += Move tests + +state = EcuState(myinfo="42") +state.ses = 5 + +ns = state + +ns.ses = 6 + +assert ns.ses == 6 +assert state.ses == 6 +assert ns.myinfo == "42" + += equal tests + +state = EcuState(myinfo="42") +state.ses = 5 + +ns = copy.copy(state) + +assert state == ns +assert hash(state) == hash(ns) + +ns.ses = 6 + +assert state != ns +assert hash(state) != hash(ns) + +ns.ses = 5 + +assert state == ns +assert hash(state) == hash(ns) + +ns.sa = 5 + +assert state != ns +assert hash(state) != hash(ns) + + += hash tests + +state = EcuState(myinfo="42") +state.ses = 5 + +ns = copy.copy(state) + +assert hash(state) == hash(ns) + +ns.ses = 6 + +assert hash(state) != hash(ns) + +ns.ses = 5 + +assert hash(state) == hash(ns) + +ns.sa = 5 + +assert hash(state) != hash(ns) + += command tests + +state = EcuState(myinfo="42") +state.ses = 5 + +state.command() +assert "EcuState(myinfo='42', ses=5)" == state.command() + += less than tests + +s1 = EcuState() +s2 = EcuState() + +s1.a = 1 +s2.a = 2 + +assert s1 < s2 + +s1.b = 4 + +assert s1 > s2 + +s2.b = 1 + +assert s1 < s2 + +s1.a = 2 + +assert s1 > s2 + += less than tests 2 + +s1 = EcuState() +s2 = EcuState() + +s1.c = "x" +s2.c = 4 +exception = False + +try: + assert s1 < s2 +except TypeError: + exception = True + +assert exception + += less than tests 3 + +s1 = EcuState() +s2 = EcuState() + + +s1.A = 1 +s1.a = 2 + +s2.A = 2 +s2.a = 1 + +assert s1 < s2 + += less than tests 4 + +s1 = EcuState() +s2 = EcuState() + + +s1.A = 1 +s1.a = 2 + +s2.A = 2 +s2.b = 100 + +assert s1 < s2 + += less than tests 5 + +s1 = EcuState() +s2 = EcuState() + + +s1.A = 100 +s1.a = 2 + +s2.A = 2 +s2.b = 100 + +assert s1 > s2 +assert not s1 > s1 +assert not s1 < s1 + += less than tests 6 + +s1 = EcuState() +s2 = EcuState() + + +s1.A = 100 +s1.B = 200 + +s2.a = 2 +s2.b = 1 + +assert s1 < s2 + += contains test + +s1 = EcuState(ses=[1,2,3]) +s2 = EcuState(ses=1) -load_contrib("automotive.ecu") +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 -+ Basic checks +s1 = EcuState(ses=[2,3]) +s2 = EcuState(ses=1) -= Check default init parameters +assert s1 != s2 +assert s2 not in s1 +assert s1 not in s2 -ecu = ECU() -assert ecu.current_session == 1 -assert ecu.current_security_level == 0 -assert ecu.communication_control == 0 -= Check init parameters +s1 = EcuState(ses=[1,2,3], security=5) +s2 = EcuState(ses=1) -ecu = ECU(init_session=5, init_security_level=4, init_communication_control=2) -assert ecu.current_session == 5 -assert ecu.current_security_level == 4 -assert ecu.communication_control == 2 +assert s1 != s2 +assert s2 not in s1 +assert s1 not in s2 -= Check reset +s1 = EcuState(ses=range(4), security=[None, 5]) +s2 = EcuState(ses=1) -ecu = ECU(init_session=5, init_security_level=4, init_communication_control=2) -ecu.reset() -assert ecu.current_session == 1 -assert ecu.current_security_level == 0 -assert ecu.communication_control == 0 +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 -+ Simple operations +s1 = EcuState(ses=range(4), security=[None, 5]) +s2 = EcuState(ses=range(2)) -= Log all commands applied to an ECU -~ docs -* This example shows the logging mechanism of an ECU object. -* The log of an ECU is a dictionary of applied UDS commands. -* The key for this dictionary the UDS service name. The value consists of a list -* of tuples, containing a timestamp and a log value +assert s1 != s2 +assert s2 < s1 +assert s2 in s1 +assert s1 not in s2 -msgs = [UDS(service=16) / UDS_DSC(diagnosticSessionType=3), # no_docs - UDS(service=16) / UDS_DSC(diagnosticSessionType=4), # no_docs - UDS(service=16) / UDS_DSC(diagnosticSessionType=5), # no_docs - UDS(service=16) / UDS_DSC(diagnosticSessionType=6), # no_docs - UDS(service=16) / UDS_DSC(diagnosticSessionType=2)] # no_docs +s1 = EcuState(ses=range(4), security=[None, 5]) +s2 = EcuState(ses=range(2), security=5) -ecu = ECU(verbose=False, store_supported_responses=False) +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 + +s1 = EcuState(ses=range(4), security=[None, 5]) +s2 = EcuState(ses=range(5)) + +assert s1 != s2 +assert s2 not in s1 +assert s1 not in s2 + +s1 = EcuState(ses=range(4), security=[None, range(5)]) +s2 = EcuState(ses=3) + +print(s1._expand()) +print(s2._expand()) + +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 + +s1 = EcuState(ses=range(4), security=[None, range(5), [5, 7, range(4), [range(10), 10]]]) +s2 = EcuState(ses=3, security=10) + +print(s1._expand()) +print(s2._expand()) + +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 + +s1 = EcuState(ses=range(4), security=[None, range(5), [5, 7, range(4), [range(10), "B"]]]) +s2 = EcuState(ses=3, security="B") + +print(s1._expand()) +print(s2._expand()) + +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 + +s1 = EcuState(ses=range(4), security=[None, range(5), [5, 7, range(4), [range(10), "B"]]]) +s2 = EcuState(ses=3, security="C") + +print(s1._expand()) +print(s2._expand()) + +assert s1 != s2 +assert s2 not in s1 +assert s1 not in s2 + +s1 = EcuState(ses=range(3), security=5) +s2 = EcuState(ses=1, security=5) + +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 + +s1 = EcuState(ses=range(3), security=(x for x in range(1, 10, 2))) +s2 = EcuState(ses=1, security=5) + +assert s1 != s2 +assert s2 in s1 +assert s1 not in s2 + +s1 = EcuState(ses=[1,2,3]) +s2 = EcuState(ses=[1,2,3]) + +assert s1 in s2 +assert s2 in s1 +assert s1 == s2 + +s1 = EcuState(ses=1) +s2 = EcuState(ses=1) + +assert s1 in s2 +assert s2 in s1 +assert s1 == s2 + +s1 = EcuState(ses=range(3), security=range(5)) +for ses, sec in itertools.product(range(3), range(5)): + s2 = EcuState(ses=ses, security=sec) + assert s1 != s2 + assert s2 in s1 + assert s1 not in s2 + + +s1 = EcuState(ses=[0, 1, 2], security=[43, 44]) +for ses, sec in itertools.product(range(3), range(43, 45)): + s2 = EcuState(ses=ses, security=sec) + assert s1 != s2 + assert s2 in s1 + assert s1 not in s2 + +s1 = EcuState(ses=[0, 1, 2], security=["a", "b"]) +for ses, sec in itertools.product(range(3), (x for x in "ab")): + s2 = EcuState(ses=ses, security=sec) + assert s1 != s2 + assert s2 in s1 + try: + assert s1 not in s2 + except TypeError: + assert True + +s1 = [EcuState(ses=1), EcuState(ses=2), EcuState(ses=3)] +s2 = EcuState(ses=3) + +assert s2 in s1 +assert s1 not in s2 + + +s1 = EcuState(ses=1, sa="SEC") +s2 = EcuState(ses=1, sa="SOC") + +assert s1 not in s2 +assert s2 not in s1 +assert s1 != s2 + +s1 = EcuState(ses=1, sa="SEC") +s2 = EcuState(ses=1, sa="SEC") + +assert s1 in s2 +assert s2 in s1 +assert s1 == s2 + + +s1 = EcuState(ses=1, sa="SEC") +s2 = EcuState(ses=1, sa=["SEC", "SOL"]) + + +assert s1 in s2 +assert s2 not in s1 +assert s1 != s2 + + +s1 = EcuState(ses=1, sa=b"SEC") +s2 = EcuState(ses=1, sa=[b"SEC", "SOL"]) + +assert s1 in s2 +assert s2 not in s1 +assert s1 != s2 + + ++ EcuState modification tests + += Basic definitions for tests + +class myPack1(Packet): + fields_desc = [ + IntField("fakefield", 1) + ] + +class myPack2(Packet): + fields_desc = [ + IntField("statefield", 1) + ] + +@EcuState.extend_pkt_with_modifier(myPack2) +def modify_ecu_state(self, req, ecustate): + # type: (Packet, Packet, EcuState) -> None + ecustate.state = self.statefield + +pkt = myPack1()/myPack2() +st = EcuState() +exception = False + +try: + assert st.state == 1 +except AttributeError: + exception = True + +assert exception == True +assert EcuState.is_modifier_pkt(pkt) +assert not EcuState.is_modifier_pkt(myPack1()) + +mod = EcuState.get_modified_ecu_state(pkt, Raw(), st) +assert mod != st +assert mod.state ==1 + +pkt2 = myPack1()/myPack1()/myPack1()/myPack2(statefield=5) +mod2 = EcuState.get_modified_ecu_state(pkt2, Raw(), mod) + +assert mod != mod2 +assert mod < mod2 + +pkt2 = myPack1()/myPack1()/myPack1()/myPack2(statefield=4)/myPack2(statefield=5) +mod2 = EcuState.get_modified_ecu_state(pkt2, Raw(), mod) +mod.state = 5 +assert mod != mod2 +assert mod > mod2 + ++ EcuResponse tests + += Basic checks + +resp = EcuResponse(EcuState(session=1), UDS()/UDS_DSCPR(b"\x03")) + +assert not resp.supports_state(EcuState()) +assert not resp.supports_state(EcuState(session=2)) +assert resp.supports_state(EcuState(session=1)) +assert resp.answers(UDS()/UDS_DSC(b"\x03")) + += Command checks + +resp = EcuResponse(EcuState(session=1), UDS()/UDS_DSCPR(b"\x03")) +cmd = resp.command() + +print(cmd) +resp1 = eval(cmd) +assert resp1 == resp + += Command checks 2 + +p1 = UDS(bytes(UDS()/UDS_NR(b"\x10\x00"))) +p2 = UDS(bytes(UDS()/UDS_DSCPR(b"\x03"))) + +resp = EcuResponse([EcuState(session=1), EcuState(session=3)], [p1, p2]) +cmd = resp.command() + +print(cmd) +resp1 = eval(cmd) +assert any(resp1.supports_state(s) for s in resp.states) +assert any(resp.supports_state(s) for s in resp1.states) +assert len(resp.responses) == len(resp1.responses) +assert all(bytes(x) == bytes(y) for x, y in zip(resp.responses, resp1.responses)) +assert resp1 == resp + += Compare check + +p1 = UDS(bytes(UDS()/UDS_NR(b"\x10\x00"))) +p2 = UDS(bytes(UDS()/UDS_DSCPR(b"\x03"))) + +resp = EcuResponse([EcuState(session=1), EcuState(session=3)], [p1, p2]) + +resp1 = EcuResponse([EcuState(session=1)], [p1, p2]) + +resp2 = EcuResponse([EcuState(session=2)], [p1, p2]) +resp3 = EcuResponse([EcuState(session=1)], [p2]) + + +assert resp == resp1 +assert resp != resp2 +assert resp != resp3 + += Key response check + +req = UDS()/UDS_DSC(b"\x03") +p1 = UDS(bytes(UDS()/UDS_NR(b"\x10\x00"))) +p2 = UDS(bytes(UDS()/UDS_DSCPR(b"\x03"))) + +resp = EcuResponse([EcuState(session=1), EcuState(session=3)], [p1, p2]) + +assert resp.answers(req) +assert resp.key_response.answers(req) + + + ++ Ecu Simple operations + += Log all commands applied to an Ecu + +msgs = [UDS(service=16) / UDS_DSC(diagnosticSessionType=3), + UDS(service=16) / UDS_DSC(diagnosticSessionType=4), + UDS(service=16) / UDS_DSC(diagnosticSessionType=5), + UDS(service=16) / UDS_DSC(diagnosticSessionType=6), + UDS(service=16) / UDS_DSC(diagnosticSessionType=2)] + +ecu = Ecu(verbose=False, store_supported_responses=False) ecu.update(PacketList(msgs)) -print(ecu.log) -assert len(ecu.log["DiagnosticSessionControl"]) == 5 # no_docs +assert len(ecu.log["DiagnosticSessionControl"]) == 5 timestamp, value = ecu.log["DiagnosticSessionControl"][0] -assert timestamp > 0 # no_docs -assert value == "extendedDiagnosticSession" # no_docs -assert ecu.log["DiagnosticSessionControl"][-1][1] == "programmingSession" # no_docs +assert timestamp > 0 +assert value == "extendedDiagnosticSession" +assert ecu.log["DiagnosticSessionControl"][-1][1] == "programmingSession" -= Trace all commands applied to an ECU -~ docs -* This example shows the trace mechanism of an ECU object. -* Traces of the current state of the ECU object and the received message are -* print on stdout. Some messages, depending on the protocol, will change the -* internal state of the ECU. += Trace all commands applied to an Ecu -msgs = [UDS(service=16) / UDS_DSC(diagnosticSessionType=3), # no_docs - UDS(service=80) / UDS_DSCPR(diagnosticSessionType=3, sessionParameterRecord=b'\\x002\\x01\\xf4')] # no_docs +msgs = [UDS(service=16) / UDS_DSC(diagnosticSessionType=3), + UDS(service=80) / UDS_DSCPR(diagnosticSessionType=3, sessionParameterRecord=b'\\x002\\x01\\xf4')] -ecu = ECU(verbose=True, logging=False, store_supported_responses=False) +ecu = Ecu(verbose=True, logging=False, store_supported_responses=False) ecu.update(PacketList(msgs)) -print(ecu.current_session) -assert ecu.current_session == 3 # no_docs -assert ecu.current_security_level == 0 # no_docs -assert ecu.communication_control == 0 # no_docs +assert ecu.state.session == 3 -= Generate supported responses of an ECU -~ docs -* This example shows a mechanism to clone a real world ECU by analyzing a list of Packets. += Generate supported responses of an Ecu -msgs = [UDS(service=16) / UDS_DSC(diagnosticSessionType=3), # no_docs - UDS(service=80) / UDS_DSCPR(diagnosticSessionType=3, sessionParameterRecord=b'\\x002\\x01\\xf4'), # no_docs - UDS(service=16) / UDS_DSC(diagnosticSessionType=4)] # no_docs +msgs = [UDS(service=16) / UDS_DSC(diagnosticSessionType=3), + UDS(service=80) / UDS_DSCPR(diagnosticSessionType=3, sessionParameterRecord=b'\\x002\\x01\\xf4'), + UDS(service=16) / UDS_DSC(diagnosticSessionType=4)] -ecu = ECU(verbose=False, logging=False, store_supported_responses=True) +ecu = Ecu(verbose=False, logging=False, store_supported_responses=True) ecu.update(PacketList(msgs)) supported_responses = ecu.supported_responses unanswered_packets = ecu.unanswered_packets -print(supported_responses) -print(unanswered_packets) -assert ecu.current_session == 3 # no_docs -assert ecu.current_security_level == 0 # no_docs -assert ecu.communication_control == 0 # no_docs -assert len(supported_responses) == 1 # no_docs -assert len(unanswered_packets) == 1 # no_docs -response = supported_responses[0] # no_docs -assert response.in_correct_session(1) # no_docs -assert response.has_security_access(0) # no_docs -assert response.responses[-1].service == 80 # no_docs -assert unanswered_packets[0].diagnosticSessionType == 4 # no_docs - - -+ Advanced checks +assert ecu.state.session == 3 +assert len(supported_responses) == 1 +assert len(unanswered_packets) == 1 + +response = supported_responses[0] +print(response.command()) +assert response.supports_state(EcuState()) +assert response.key_response.service == 80 +assert unanswered_packets[0].diagnosticSessionType == 4 + + ++ Ecu Advanced checks = Analyze multiple UDS messages -~ docs -* This example shows how to load ``UDS`` messages from a ``.pcap`` file containing ``CAN`` messages -* A ``PcapReader`` object is used as socket and an ``ISOTPSession`` parses ``CAN`` frames to ``ISOTP`` frames -* which are then casted to ``UDS`` objects through the ``basecls`` parameter -with PcapReader("test/contrib/automotive/ecu_trace.pcap") as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"use_ext_addr":False, "basecls":UDS}, count=50, opened_socket=sock) +udsmsgs = sniff(offline=scapy_path("test/pcaps/ecu_trace.pcap.gz"), + session=ISOTPSession(use_ext_address=False, basecls=UDS), + count=50, timeout=3) -assert len(udsmsgs) == 50 # no_docs +assert len(udsmsgs) == 50 -ecu = ECU() +ecu = Ecu() ecu.update(udsmsgs) -print(ecu.log) -print(ecu.supported_responses) -response = ecu.supported_responses[0] # no_docs -assert response.in_correct_session(1) # no_docs -assert response.has_security_access(0) # no_docs -assert response.responses[0].service == 80 # no_docs -assert response.responses[0].diagnosticSessionType == 3 # no_docs -response = ecu.supported_responses[1] # no_docs -assert response.in_correct_session(3) # no_docs -assert response.has_security_access(0) # no_docs -assert response.responses[0].service == 80 # no_docs -assert response.responses[0].diagnosticSessionType == 2 # no_docs -response = ecu.supported_responses[4] # no_docs -assert response.in_correct_session(2) # no_docs -assert response.has_security_access(18) # no_docs -assert response.responses[0].service == 110 # no_docs -assert response.responses[0].dataIdentifier == 61786 # no_docs +response = ecu.supported_responses[0] +assert response.supports_state(EcuState()) +assert response.key_response.service == 80 +assert response.key_response.diagnosticSessionType == 3 +response = ecu.supported_responses[1] +assert response.supports_state(EcuState(session=3)) +assert response.key_response.service == 80 +assert response.key_response.diagnosticSessionType == 2 +response = ecu.supported_responses[4] +print(response) +state = EcuState(session=2, security_level=18) +print(state) +assert response.supports_state(state) +assert response.key_response.service == 110 +assert response.key_response.dataIdentifier == 61786 assert len(ecu.log["TransferData"]) == 2 ++ EcuSession tests -= Analyze on the fly with ECUSession -~ docs -* This example shows the usage of a ECUSession in sniff. An ISOTPSocket or any -* socket like object which returns entire messages of the right protocol can be used. -* A ``ECUSession`` is used as supersession in an ``ISOTPSession``. To obtain the ``ECU`` object from a ``ECUSession``, -* the ``ECUSession`` has to be created outside of sniff. += Analyze on the fly with EcuSession -session = ECUSession() +session = EcuSession() -with PcapReader("test/contrib/automotive/ecu_trace.pcap") as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "use_ext_addr":False, "basecls":UDS}, count=50, opened_socket=sock) +with PcapReader(scapy_path("test/pcaps/ecu_trace.pcap.gz")) as sock: + udsmsgs = sniff(session=ISOTPSession(supersession=session, use_ext_address=False, basecls=UDS), count=50, opened_socket=sock, timeout=3) -assert len(udsmsgs) == 50 # no_docs +assert len(udsmsgs) == 50 ecu = session.ecu -print(ecu.log) -print(ecu.supported_responses) -response = ecu.supported_responses[0] # no_docs -assert response.in_correct_session(1) # no_docs -assert response.has_security_access(0) # no_docs -assert response.responses[0].service == 80 # no_docs -assert response.responses[0].diagnosticSessionType == 3 # no_docs -response = ecu.supported_responses[1] # no_docs -assert response.in_correct_session(3) # no_docs -assert response.has_security_access(0) # no_docs -assert response.responses[0].service == 80 # no_docs -assert response.responses[0].diagnosticSessionType == 2 # no_docs -response = ecu.supported_responses[4] # no_docs -assert response.in_correct_session(2) # no_docs -assert response.has_security_access(18) # no_docs -assert response.responses[0].service == 110 # no_docs -assert response.responses[0].dataIdentifier == 61786 # no_docs -assert len(ecu.log["TransferData"]) == 2 # no_docs - - -= Analyze on the fly with ECUSession GMLAN1 - -session = ECUSession() - -with CandumpReader("test/contrib/automotive/gmlan_trace.candump") as sock: - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "did":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=2, opened_socket=sock) +response = ecu.supported_responses[0] +assert response.supports_state(EcuState()) +assert response.key_response.service == 80 +assert response.key_response.diagnosticSessionType == 3 +response = ecu.supported_responses[1] +assert response.supports_state(EcuState(session=3)) +assert response.key_response.service == 80 +assert response.key_response.diagnosticSessionType == 2 +response = ecu.supported_responses[4] +print(response) +state = EcuState(session=2, security_level=18) +print(state) +assert response.supports_state(state) +assert response.key_response.service == 110 +assert response.key_response.dataIdentifier == 61786 +assert len(ecu.log["TransferData"]) == 2 + + += Analyze on the fly with EcuSession GMLAN1 + +session = EcuSession() + +conf.contribs['CAN']['swap-bytes'] = True + +with PcapReader(scapy_path("test/pcaps/gmlan_trace.pcap.gz")) as sock: + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=2, opened_socket=sock, timeout=3) ecu = session.ecu print("Check 1 after change to diagnostic mode") assert len(ecu.supported_responses) == 1 - assert ecu.current_session == 3 - assert ecu.current_security_level == 0 - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "did":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=8, opened_socket=sock) + assert ecu.state == EcuState(session=3) + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=6, opened_socket=sock) ecu = session.ecu - print("Check 2 after some more messages were read") - assert len(ecu.supported_responses) == 4 - assert ecu.current_session == 3 - assert ecu.current_security_level == 0 - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "did":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=10, opened_socket=sock) + print("Check 2 after some more messages were read1") + assert len(ecu.supported_responses) == 3 + print("Check 2 after some more messages were read2") + assert ecu.state.session == 3 + print("assert 1") + assert ecu.state.communication_control == 1 + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=2, opened_socket=sock) ecu = session.ecu print("Check 3 after change to programming mode (bootloader)") - assert len(ecu.supported_responses) == 5 - assert ecu.current_session == 2 - assert ecu.current_security_level == 0 - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "did":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=16, opened_socket=sock) + assert len(ecu.supported_responses) == 4 + assert ecu.state.session == 2 + assert ecu.state.communication_control == 1 + gmlanmsgs = sniff(session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), count=6, opened_socket=sock) ecu = session.ecu print("Check 4 after gaining security access") - assert len(ecu.supported_responses) == 7 - assert ecu.current_session == 2 - assert ecu.current_security_level == 2 + assert len(ecu.supported_responses) == 6 + assert ecu.state.session == 2 + assert ecu.state.security_level == 2 + assert ecu.state.communication_control == 1 + += Analyze on the fly with EcuSession GMLAN logging test -= Analyze on the fly with ECUSession GMLAN logging test +session = EcuSession(verbose=False, store_supported_responses=False) -session = ECUSession(verbose=False, store_supported_responses=False) +conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 +conf.contribs['CAN']['swap-bytes'] = True -with CandumpReader("test/contrib/automotive/gmlan_trace.candump") as sock: - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "did":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=200, opened_socket=sock) +conf.debug_dissector = True +gmlanmsgs = sniff(offline=scapy_path("test/pcaps/gmlan_trace.pcap.gz"), + session=ISOTPSession(supersession=session, rx_id=[0x241, 0x641, 0x101], basecls=GMLAN), + count=200, timeout=6) ecu = session.ecu assert len(ecu.supported_responses) == 0 @@ -230,4 +717,4 @@ assert len([m for m in gmlanmsgs if m.sprintf("%GMLAN.service%") == "ReadDataByI assert len(ecu.log["SecurityAccess"]) == 2 assert len(ecu.log["SecurityAccessPositiveResponse"]) == 2 -assert ecu.log["TransferData"][-1][1][0] == "downloadAndExecuteOrExecute" \ No newline at end of file +assert ecu.log["TransferData"][-1][1][0] == "downloadAndExecuteOrExecute" diff --git a/test/contrib/automotive/ecu_am.uts b/test/contrib/automotive/ecu_am.uts index 8f8eb867289..d2eea1e3aeb 100644 --- a/test/contrib/automotive/ecu_am.uts +++ b/test/contrib/automotive/ecu_am.uts @@ -1,122 +1,11 @@ -% Regression tests for ECU_am +% Regression tests for EcuAnsweringMachine + Configuration ~ conf = Imports -load_layer("can") -conf.contribs['CAN']['swap-bytes'] = False -import os, threading, six, subprocess, sys -from subprocess import call -from scapy.consts import LINUX - -= Definition of constants, utility functions and mock classes -iface0 = "vcan0" -iface1 = "vcan1" - -# function to exit when the can-isotp kernel module is not available -ISOTP_KERNEL_MODULE_AVAILABLE = False -def exit_if_no_isotp_module(): - if not ISOTP_KERNEL_MODULE_AVAILABLE: - sys.stderr.write("TEST SKIPPED: can-isotp not available" + os.linesep) - warning("Can't test ISOTP native socket because kernel module is not loaded") - exit(0) - - -= Initialize a virtual CAN interface -~ vcan_socket needs_root linux -if 0 != call(["cansend", iface0, "000#"]): - # vcan0 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface0, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface0) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface0, "up"]): - raise Exception("could not bring up %s" % iface0) - -if 0 != call(["cansend", iface0, "000#"]): - raise Exception("cansend doesn't work") - -if 0 != call(["cansend", iface1, "000#"]): - # vcan1 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface1, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface1) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface1, "up"]): - raise Exception("could not bring up %s" % iface1) - -if 0 != call(["cansend", iface1, "000#"]): - raise Exception("cansend doesn't work") - -print("CAN should work now") - -= Import CANSocket - -from scapy.contrib.cansocket_python_can import * - -new_can_socket = lambda iface: CANSocket(bustype='virtual', channel=iface) -new_can_socket0 = lambda: CANSocket(bustype='virtual', channel=iface0, timeout=0.01) -new_can_socket1 = lambda: CANSocket(bustype='virtual', channel=iface1, timeout=0.01) - -# utility function for draining a can interface, asserting that no packets are there -def drain_bus(iface=iface0, assert_empty=True): - with new_can_socket(iface) as s: - pkts = s.sniff(timeout=0.1) - if assert_empty: - assert len(pkts) == 0 - -print("CAN sockets should work now") - -= Overwrite definition for vcan_socket systems native sockets -~ vcan_socket not_pypy needs_root linux - -if six.PY3 and LINUX: - from scapy.contrib.cansocket_native import * - new_can_socket = lambda iface: CANSocket(iface) - new_can_socket0 = lambda: CANSocket(iface0) - new_can_socket1 = lambda: CANSocket(iface1) - - -= Overwrite definition for vcan_socket systems python-can sockets -~ vcan_socket needs_root linux -if "python_can" in CANSocket.__module__: - new_can_socket = lambda iface: CANSocket(bustype='socketcan', channel=iface, bitrate=250000, timeout=0.01) - new_can_socket0 = lambda: CANSocket(bustype='socketcan', channel=iface0, bitrate=250000, timeout=0.01) - new_can_socket1 = lambda: CANSocket(bustype='socketcan', channel=iface1, bitrate=250000, timeout=0.01) - -= Verify that a CAN socket can be created and closed -s = new_can_socket(iface0) -s.close() - - -= Check if can-isotp and can-utils are installed on this system -~ linux -p1 = subprocess.Popen(['lsmod'], stdout = subprocess.PIPE) -p2 = subprocess.Popen(['grep', '^can_isotp'], stdout = subprocess.PIPE, stdin=p1.stdout) -p1.stdout.close() -if p1.wait() == 0 and p2.wait() == 0 and b"can_isotp" in p2.stdout.read(): - p = subprocess.Popen(["isotpsend", "-s1", "-d0", iface0], stdin = subprocess.PIPE) - p.communicate(b"01") - if p.returncode == 0: - ISOTP_KERNEL_MODULE_AVAILABLE = True - - -+ Syntax check - -= Import isotp - -conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': ISOTP_KERNEL_MODULE_AVAILABLE} -load_contrib("isotp") - -if six.PY3 and ISOTP_KERNEL_MODULE_AVAILABLE: - from scapy.contrib.isotp import ISOTPNativeSocket - ISOTPSocket = ISOTPNativeSocket - assert ISOTPSocket == ISOTPNativeSocket -else: - from scapy.contrib.isotp import ISOTPSoftSocket - ISOTPSocket = ISOTPSoftSocket - assert ISOTPSocket == ISOTPSoftSocket + +from test.testsocket import TestSocket, cleanup_testsockets ############ ############ @@ -124,175 +13,165 @@ else: = Load contribution layer -load_contrib('automotive.uds') -load_contrib('automotive.ecu') +load_contrib("automotive.uds", globals_dict=globals()) +load_contrib("automotive.ecu", globals_dict=globals()) +load_contrib("automotive.uds_ecu_states", globals_dict=globals()) +ecu = TestSocket(UDS) +tester = TestSocket(UDS) +ecu.pair(tester) + Simulator tests = Simple check with RDBI and Negative Response -drain_bus(iface0) example_responses = \ - [ECUResponse(session=1, security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=0x1234) / Raw(b"deadbeef"))] + [EcuResponse([EcuState(session=1)], responses=UDS() / UDS_RDBIPR(dataIdentifier=0x1234) / Raw(b"deadbeef"))] success = False - -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x700, did=0x600, basecls=UDS) as ecu, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x600, did=0x700, basecls=UDS) as tester: - answering_machine = ECU_am(supported_responses=example_responses, main_socket=ecu, basecls=UDS, verbose=False) - sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) - sim.start() - time.sleep(0.1) - try: - resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[0x123]), timeout=1, verbose=False) - assert resp.negativeResponseCode == 0x10 - assert resp.requestServiceId == 34 - resp = tester.sr1(UDS(service=0x22), timeout=1, verbose=False) - assert resp.negativeResponseCode == 0x10 - assert resp.requestServiceId == 34 - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[0x1234]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 0x1234 - assert resp.load == b"deadbeef" - success = True - except Exception as ex: - print(ex) - finally: - tester.send(UDS(service=0xff)) - sim.join(timeout=10) +answering_machine = EcuAnsweringMachine(supported_responses=example_responses, main_socket=ecu, basecls=UDS, verbose=False) +sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) +sim.start() +try: + resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[0x123]), timeout=1, verbose=False) + assert resp.negativeResponseCode == 0x10 + assert resp.requestServiceId == 34 + resp = tester.sr1(UDS(service=0x22), timeout=1, verbose=False) + assert resp.negativeResponseCode == 0x10 + assert resp.requestServiceId == 34 + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[0x1234]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 0x1234 + assert resp.load == b"deadbeef" + success = True +except Exception as ex: + print(ex) +finally: + tester.send(UDS(service=0xff)) + sim.join(timeout=10) assert success = Simple check with different Sessions -drain_bus(iface0) example_responses = \ - [ECUResponse(session=2, security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"deadbeef1")), - ECUResponse(session=range(3,5), security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"deadbeef2")), - ECUResponse(session=[5,6,7], security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef3")), - ECUResponse(session=lambda x: 8 < x <= 10, security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=9) / Raw(b"deadbeef4"))] + [EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"deadbeef1")), + EcuResponse(EcuState(session=[3, 4]), responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"deadbeef2")), + EcuResponse(EcuState(session=[5, 6, 7]), responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef3")), + EcuResponse(EcuState(session=[8, 9]), responses=UDS() / UDS_RDBIPR(dataIdentifier=9) / Raw(b"deadbeef4"))] success = False -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x700, did=0x600, basecls=UDS) as ecu, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x600, did=0x700, basecls=UDS) as tester: - answering_machine = ECU_am(supported_responses=example_responses, main_socket=ecu, basecls=UDS) - sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) - sim.start() - time.sleep(0.1) - try: - resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.negativeResponseCode == 0x10 - assert resp.requestServiceId == 34 - answering_machine.ecu_state.current_session = 2 - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 2 - assert resp.load == b"deadbeef1" - answering_machine.ecu_state.current_session = 4 - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 3 - assert resp.load == b"deadbeef2" - answering_machine.ecu_state.current_session = 6 - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 5 - assert resp.load == b"deadbeef3" - answering_machine.ecu_state.current_session = 9 - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 9 - assert resp.load == b"deadbeef4" - success = True - except Exception as ex: - print(ex) - finally: - tester.send(UDS(service=0xff)) - sim.join(timeout=10) +answering_machine = EcuAnsweringMachine(supported_responses=example_responses, main_socket=ecu, basecls=UDS) +sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) +sim.start() +try: + resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.negativeResponseCode == 0x10 + assert resp.requestServiceId == 34 + answering_machine.state.session = 2 + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 2 + assert resp.load == b"deadbeef1" + answering_machine.state.session = 4 + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 3 + assert resp.load == b"deadbeef2" + answering_machine.state.session = 6 + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 5 + assert resp.load == b"deadbeef3" + answering_machine.state.session = 9 + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 9 + assert resp.load == b"deadbeef4" + success = True +except Exception as ex: + print(ex) +finally: + tester.send(UDS(service=0xff)) + sim.join(timeout=10) assert success = Simple check with different Sessions and diagnosticSessionControl -drain_bus(iface0) example_responses = \ - [ECUResponse(session=2, security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"deadbeef1")), - ECUResponse(session=range(3,5), security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"deadbeef2")), - ECUResponse(session=[5,6,7], security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef3")), - ECUResponse(session=lambda x: 8 < x <= 10, security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=9) / Raw(b"deadbeef4")), - ECUResponse(session=range(0,8), security_level=lambda x: x==0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=1, sessionParameterRecord=b"dead")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=2, sessionParameterRecord=b"dead")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=3, sessionParameterRecord=b"dead")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=4, sessionParameterRecord=b"dead")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=5, sessionParameterRecord=b"dead")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=6, sessionParameterRecord=b"dead")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=7, sessionParameterRecord=b"dead")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=8, sessionParameterRecord=b"dead")), - ECUResponse(session=range(8,10), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead1")), - ECUResponse(session=range(8,10), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead2")), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] + [EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"deadbeef1")), + EcuResponse(EcuState(session=range(3,5)), responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"deadbeef2")), + EcuResponse(EcuState(session=[5,6,7]), responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef3")), + EcuResponse(EcuState(session=9), responses=UDS() / UDS_RDBIPR(dataIdentifier=9) / Raw(b"deadbeef4")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=1, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=2, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=3, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=4, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=5, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=6, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=7, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(0,8))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=8, sessionParameterRecord=b"dead")), + EcuResponse([EcuState(), EcuState(session=range(8,10))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead1")), + EcuResponse([EcuState(), EcuState(session=range(8,10))], responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead2")), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] success = False -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x700, did=0x600, basecls=UDS) as ecu, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x600, did=0x700, basecls=UDS) as tester: - answering_machine = ECU_am(supported_responses=example_responses, main_socket=ecu, basecls=UDS) - sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) - sim.start() - time.sleep(0.1) - try: - resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.negativeResponseCode == 0x10 - assert resp.requestServiceId == 34 - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=2), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 2 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 2 - assert resp.load == b"deadbeef1" - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=4), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 4 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 3 - assert resp.load == b"deadbeef2" - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=6), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 6 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 5 - assert resp.load == b"deadbeef3" - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=8), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 8 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_DSC(diagnosticSessionType=9), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 9 - assert resp.sessionParameterRecord == b"dead1" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 9 - assert resp.load == b"deadbeef4" - success = True - except Exception as ex: - print(ex) - finally: - tester.send(UDS(service=0xff)) - sim.join(timeout=10) +answering_machine = EcuAnsweringMachine(supported_responses=example_responses, main_socket=ecu, basecls=UDS) +sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) +sim.start() +try: + resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.negativeResponseCode == 0x10 + assert resp.requestServiceId == 34 + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=2), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 2 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 2 + assert resp.load == b"deadbeef1" + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=4), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 4 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 3 + assert resp.load == b"deadbeef2" + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=6), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 6 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 5 + assert resp.load == b"deadbeef3" + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=8), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 8 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_DSC(diagnosticSessionType=9), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 9 + assert resp.sessionParameterRecord == b"dead1" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 9 + assert resp.load == b"deadbeef4" + success = True +except Exception as ex: + print(ex) +finally: + tester.send(UDS(service=0xff)) + sim.join(timeout=10) assert success = Simple check with different Sessions and diagnosticSessionControl and answers hook -drain_bus(iface0) def custom_answers(resp, req): if req.service + 0x40 != resp.service: @@ -304,75 +183,70 @@ def custom_answers(resp, req): return False example_responses = \ - [ECUResponse(session=2, security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"deadbeef1")), - ECUResponse(session=range(3,5), security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"deadbeef2")), - ECUResponse(session=[5,6,7], security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef3")), - ECUResponse(session=lambda x: 8 < x <= 10, security_level=0, responses=UDS() / UDS_RDBIPR(dataIdentifier=9) / Raw(b"deadbeef4")), - ECUResponse(session=range(0,8), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=1, sessionParameterRecord=b"dead"), answers=custom_answers), - ECUResponse(session=range(8,10), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead1")), - ECUResponse(session=range(8,10), security_level=0, responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead2")), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] + [EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"deadbeef1")), + EcuResponse(EcuState(session=range(3,5)), responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"deadbeef2")), + EcuResponse(EcuState(session=[5,6,7]), responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef3")), + EcuResponse(EcuState(session=[9, 10]), responses=UDS() / UDS_RDBIPR(dataIdentifier=9) / Raw(b"deadbeef4")), + EcuResponse(EcuState(session=range(0,8)), responses=UDS() / UDS_DSCPR(diagnosticSessionType=1, sessionParameterRecord=b"dead"), answers=custom_answers), + EcuResponse(EcuState(session=range(8,10)), responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead1")), + EcuResponse(EcuState(session=range(8,10)), responses=UDS() / UDS_DSCPR(diagnosticSessionType=9, sessionParameterRecord=b"dead2")), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] success = False -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x700, did=0x600, basecls=UDS) as ecu, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x600, did=0x700, basecls=UDS) as tester: - answering_machine = ECU_am(supported_responses=example_responses, - main_socket=ecu, basecls=UDS) - sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) - sim.start() - time.sleep(0.1) - try: - resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.negativeResponseCode == 0x10 - assert resp.requestServiceId == 34 - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=2), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 2 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 2 - assert resp.load == b"deadbeef1" - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=4), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 4 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 3 - assert resp.load == b"deadbeef2" - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=6), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 6 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 5 - assert resp.load == b"deadbeef3" - resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=8), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 8 - assert resp.sessionParameterRecord == b"dead" - resp = tester.sr1(UDS() / UDS_DSC(diagnosticSessionType=9), timeout=1, verbose=False) - assert resp.service == 0x50 - assert resp.diagnosticSessionType == 9 - assert resp.sessionParameterRecord == b"dead1" - resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) - assert resp.service == 0x62 - assert resp.dataIdentifier == 9 - assert resp.load == b"deadbeef4" - success = True - except Exception as ex: - print(ex) - finally: - tester.send(UDS(service=0xff)) - sim.join(timeout=10) +answering_machine = EcuAnsweringMachine(supported_responses=example_responses, main_socket=ecu, basecls=UDS) +sim = threading.Thread(target=answering_machine, kwargs={'timeout': 60, 'stop_filter': lambda p: p.service==0xff}) +sim.start() +try: + resp = tester.sr1(UDS()/UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.negativeResponseCode == 0x10 + assert resp.requestServiceId == 34 + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=2), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 2 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 2 + assert resp.load == b"deadbeef1" + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=4), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 4 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 3 + assert resp.load == b"deadbeef2" + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=6), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 6 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 5 + assert resp.load == b"deadbeef3" + resp = tester.sr1(UDS()/UDS_DSC(diagnosticSessionType=8), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 8 + assert resp.sessionParameterRecord == b"dead" + resp = tester.sr1(UDS() / UDS_DSC(diagnosticSessionType=9), timeout=1, verbose=False) + assert resp.service == 0x50 + assert resp.diagnosticSessionType == 9 + assert resp.sessionParameterRecord == b"dead1" + resp = tester.sr1(UDS() / UDS_RDBI(identifiers=[2, 3, 5, 9]), timeout=1, verbose=False) + assert resp.service == 0x62 + assert resp.dataIdentifier == 9 + assert resp.load == b"deadbeef4" + success = True +except Exception as ex: + print(ex) +finally: + tester.send(UDS(service=0xff)) + sim.join(timeout=10) assert success = Simple check with security access and answers hook -drain_bus(iface0) security_seed = b"abcd" @@ -388,43 +262,38 @@ def custom_answers(resp, req): return False example_responses = \ - [ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_SAPR(securityAccessType=1, securitySeed=b"1234"), answers=custom_answers), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_SAPR(securityAccessType=2), answers=custom_answers), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x35, requestServiceId=0x27)), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] + [EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_SAPR(securityAccessType=1, securitySeed=b"1234"), answers=custom_answers), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_SAPR(securityAccessType=2), answers=custom_answers), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x35, requestServiceId=0x27)), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] success = False -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x700, did=0x600, basecls=UDS) as ecu, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x600, did=0x700, basecls=UDS) as tester: - answering_machine = ECU_am(supported_responses=example_responses, - main_socket=ecu, basecls=UDS) - sim = threading.Thread(target=answering_machine, kwargs={'timeout': 10, 'stop_filter': lambda p: p.service==0xff}) - sim.start() - time.sleep(0.1) - try: - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) - assert resp.service == 0x67 - assert resp.securitySeed == b"abcd" - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed), timeout=1, verbose=False) - assert resp.service == 0x7f - assert resp.negativeResponseCode == 0x35 - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) - assert resp.service == 0x67 - assert resp.securitySeed == b"abcd" - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed+resp.securitySeed), timeout=1, verbose=False) - assert resp.service == 0x67 - success = True - except Exception as ex: - print(ex) - finally: - tester.send(UDS(service=0xff)) - sim.join(timeout=10) +answering_machine = EcuAnsweringMachine(supported_responses=example_responses, main_socket=ecu, basecls=UDS) +sim = threading.Thread(target=answering_machine, kwargs={'timeout': 10, 'stop_filter': lambda p: p.service==0xff}) +sim.start() +try: + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) + assert resp.service == 0x67 + assert resp.securitySeed == b"abcd" + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed), timeout=1, verbose=False) + assert resp.service == 0x7f + assert resp.negativeResponseCode == 0x35 + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) + assert resp.service == 0x67 + assert resp.securitySeed == b"abcd" + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed+resp.securitySeed), timeout=1, verbose=False) + assert resp.service == 0x67 + success = True +except Exception as ex: + print(ex) +finally: + tester.send(UDS(service=0xff)) + sim.join(timeout=10) assert success = Simple check with security access and answers hook and request-correctly-received message -drain_bus(iface0) security_seed = b"abcd" @@ -440,43 +309,38 @@ def custom_answers(resp, req): return False example_responses = \ - [ECUResponse(session=range(0,255), security_level=0, responses=[UDS()/UDS_NR(negativeResponseCode=0x78, requestServiceId=0x27), UDS() / UDS_SAPR(securityAccessType=1, securitySeed=b"1234")], answers=custom_answers), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_SAPR(securityAccessType=2), answers=custom_answers), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x35, requestServiceId=0x27)), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] + [EcuResponse(EcuState(session=range(0,255)), responses=[UDS()/UDS_NR(negativeResponseCode=0x78, requestServiceId=0x27), UDS() / UDS_SAPR(securityAccessType=1, securitySeed=b"1234")], answers=custom_answers), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_SAPR(securityAccessType=2), answers=custom_answers), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x35, requestServiceId=0x27)), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] success = False -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x700, did=0x600, basecls=UDS) as ecu, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x600, did=0x700, basecls=UDS) as tester: - answering_machine = ECU_am(supported_responses=example_responses, - main_socket=ecu, basecls=UDS) - sim = threading.Thread(target=answering_machine, kwargs={'timeout': 10, 'stop_filter': lambda p: p.service==0xff}) - sim.start() - time.sleep(0.1) - try: - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=2, verbose=False) - assert resp.service == 0x67 - assert resp.securitySeed == b"abcd" - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed), timeout=2, verbose=False) - assert resp.service == 0x7f - assert resp.negativeResponseCode == 0x35 - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=2, verbose=False) - assert resp.service == 0x67 - assert resp.securitySeed == b"abcd" - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed+resp.securitySeed), timeout=2, verbose=False) - assert resp.service == 0x67 - success = True - except Exception as ex: - print(ex) - finally: - tester.send(UDS(service=0xff)) - sim.join(timeout=10) +answering_machine = EcuAnsweringMachine(supported_responses=example_responses, main_socket=ecu, basecls=UDS) +sim = threading.Thread(target=answering_machine, kwargs={'timeout': 10, 'stop_filter': lambda p: p.service==0xff}) +sim.start() +try: + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=2, verbose=False) + assert resp.service == 0x67 + assert resp.securitySeed == b"abcd" + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed), timeout=2, verbose=False) + assert resp.service == 0x7f + assert resp.negativeResponseCode == 0x35 + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=2, verbose=False) + assert resp.service == 0x67 + assert resp.securitySeed == b"abcd" + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed+resp.securitySeed), timeout=2, verbose=False) + assert resp.service == 0x67 + success = True +except Exception as ex: + print(ex) +finally: + tester.send(UDS(service=0xff)) + sim.join(timeout=10) assert success = Simple check with security access and answers hook and request-correctly-received message 2 -drain_bus(iface0) security_seed = b"abcd" @@ -492,46 +356,43 @@ def custom_answers(resp, req): return False example_responses = \ - [ECUResponse(session=range(0,255), security_level=0, responses=[UDS()/UDS_NR(negativeResponseCode=0x78, requestServiceId=0x27), UDS() / UDS_SAPR(securityAccessType=1, securitySeed=b"1234")], answers=custom_answers), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_SAPR(securityAccessType=2), answers=custom_answers), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x35, requestServiceId=0x27)), - ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] + [EcuResponse(EcuState(session=range(0,255)), responses=[UDS()/UDS_NR(negativeResponseCode=0x78, requestServiceId=0x27), UDS() / UDS_SAPR(securityAccessType=1, securitySeed=b"1234")], answers=custom_answers), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_SAPR(securityAccessType=2), answers=custom_answers), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x35, requestServiceId=0x27)), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10))] conf.contribs['UDS']['treat-response-pending-as-answer'] = True success = False -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x700, did=0x600, basecls=UDS) as ecu, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x600, did=0x700, basecls=UDS) as tester: - answering_machine = ECU_am(supported_responses=example_responses, - main_socket=ecu, basecls=UDS) - sim = threading.Thread(target=answering_machine, kwargs={'timeout':5, 'stop_filter': lambda p: p.service==0xff}) - sim.start() - time.sleep(0.1) - try: - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) - assert resp.service == 0x7f - assert resp.negativeResponseCode == 0x78 - resp = tester.sniff(timeout=2, count=1, verbose=False)[0] - assert resp.service == 0x67 - assert resp.securitySeed == b"abcd" - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed), timeout=3, verbose=False) - assert resp.service == 0x7f - assert resp.negativeResponseCode == 0x35 - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) - assert resp.service == 0x7f - assert resp.negativeResponseCode == 0x78 - resp = tester.sniff(timeout=2, count=1, verbose=False)[0] - assert resp.service == 0x67 - assert resp.securitySeed == b"abcd" - resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed+resp.securitySeed), timeout=1, verbose=False) - assert resp.service == 0x67 - success = True - except Exception as ex: - print(ex) - finally: - tester.send(UDS(service=0xff)) - sim.join(timeout=10) +answering_machine = EcuAnsweringMachine(supported_responses=example_responses, + main_socket=ecu, basecls=UDS) +sim = threading.Thread(target=answering_machine, kwargs={'timeout':5, 'stop_filter': lambda p: p.service==0xff}) +sim.start() +try: + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) + assert resp.service == 0x7f + assert resp.negativeResponseCode == 0x78 + resp = tester.sniff(timeout=2, count=1, verbose=False)[0] + assert resp.service == 0x67 + assert resp.securitySeed == b"abcd" + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed), timeout=3, verbose=False) + assert resp.service == 0x7f + assert resp.negativeResponseCode == 0x35 + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=1), timeout=1, verbose=False) + assert resp.service == 0x7f + assert resp.negativeResponseCode == 0x78 + resp = tester.sniff(timeout=2, count=1, verbose=False)[0] + assert resp.service == 0x67 + assert resp.securitySeed == b"abcd" + resp = tester.sr1(UDS() / UDS_SA(securityAccessType=2, securityKey=resp.securitySeed+resp.securitySeed), timeout=1, verbose=False) + assert resp.service == 0x67 + success = True +except Exception as ex: + print(ex) +finally: + tester.send(UDS(service=0xff)) + sim.join(timeout=10) assert success @@ -539,11 +400,6 @@ conf.contribs['UDS']['treat-response-pending-as-answer'] = False + Cleanup -= Delete vcan interfaces -~ vcan_socket needs_root linux - -if 0 != call(["sudo", "ip", "link", "delete", iface0]): - raise Exception("%s could not be deleted" % iface0) += Delete TestSockets -if 0 != call(["sudo", "ip", "link", "delete", iface1]): - raise Exception("%s could not be deleted" % iface1) +cleanup_testsockets() \ No newline at end of file diff --git a/test/contrib/automotive/ecu_trace.pcap b/test/contrib/automotive/ecu_trace.pcap deleted file mode 100644 index d7eaa7db171..00000000000 Binary files a/test/contrib/automotive/ecu_trace.pcap and /dev/null differ diff --git a/test/contrib/automotive/gm/gmlan.uts b/test/contrib/automotive/gm/gmlan.uts index cf53321f1b6..9b8d0a175aa 100644 --- a/test/contrib/automotive/gm/gmlan.uts +++ b/test/contrib/automotive/gm/gmlan.uts @@ -7,17 +7,18 @@ + Configuration of scapy = Load gmlan layer -~ conf command +~ conf -from scapy.contrib.automotive.ecu import ECU +load_contrib("automotive.ecu", globals_dict=globals()) +load_contrib("automotive.gm.gmlan", globals_dict=globals()) -load_contrib('automotive.gm.gmlan') +from scapy.contrib.automotive.gm.gmlan_ecu_states import * +from scapy.contrib.automotive.gm.gmlan_logging import * + Basic Packet Tests() = Set GMLAN ECU AddressingScheme conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 2 - assert conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] == 2 = Craft Packet @@ -245,9 +246,10 @@ x.service == 0x67 x.subfunction == 2 x.answers(b) -ecu = ECU() +ecu = Ecu() +ecu.update(b) ecu.update(x) -assert ecu.current_security_level == 2 +assert ecu.state.security_level == 2 = Craft Packet for GMLAN_SAPR2 @@ -439,18 +441,299 @@ assert x.answers(y) y.hashret() == x.hashret() = Check modifies ecu state -ecu = ECU() +ecu = Ecu() +ecu.update(GMLAN(service="InitiateDiagnosticOperation")) ecu.update(GMLAN(service="InitiateDiagnosticOperationPositiveResponse")) -assert ecu.current_session == 3 +assert ecu.state.session == 3 +ecu.update(GMLAN(service="ReturnToNormalOperation")) ecu.update(GMLAN(service="ReturnToNormalOperationPositiveResponse")) -assert ecu.current_session == 1 +assert ecu.state.session == 1 +ecu.update(GMLAN(service="ProgrammingMode")) ecu.update(GMLAN(service="ProgrammingModePositiveResponse")) -assert ecu.current_session == 2 +assert ecu.state.session == 2 +ecu.update(GMLAN(service="DisableNormalCommunication")) ecu.update(GMLAN(service="DisableNormalCommunicationPositiveResponse")) -assert ecu.communication_control == 1 +assert ecu.state.communication_control == 1 +ecu.update(GMLAN(service="ReturnToNormalOperation")) ecu.update(GMLAN(service="ReturnToNormalOperationPositiveResponse")) -assert ecu.current_session == 1 -assert ecu.communication_control == 0 +assert ecu.state.session == 1 + += Craft GMLAN_DC + +req = GMLAN()/GMLAN_DC(CPIDNumber=0x11, CPIDControlBytes=b"\xbe\xefabc") +assert bytes(req) == b"\xAE\x11\xbe\xefabc" + +req2 = GMLAN()/GMLAN_DC(CPIDNumber=0x12) +assert bytes(req2) == b"\xAE\x12\x00\x00\x00\x00\x00" + +resp = GMLAN()/GMLAN_DCPR(CPIDNumber=0x11) +assert bytes(resp) == b"\xEE\x11" + + +assert resp.answers(req) +assert not resp.answers(req2) + += Dissect test GMLAN_DC + +req = GMLAN(b"\xAE\x14caffe") +assert req.service == 0xAE +assert req.CPIDNumber == 20 +assert req.CPIDControlBytes == b"caffe" + +resp = GMLAN(b"\xEE\x14") +assert resp.service == 0xEE +assert resp.CPIDNumber == 20 +assert resp.answers(req) +assert resp.hashret() == req.hashret() + += Logging tests + + +def get_log(pkt): + for layer in pkt.layers(): + if not hasattr(layer, "get_log"): + continue + try: + return layer.get_log(pkt) + except TypeError: + return layer.get_log.im_func(pkt) + +pkt = GMLAN()/GMLAN_RFRD(subfunction=1) +log = get_log(pkt) +assert len(log) == 2 +assert log[1] == "readFailureRecordIdentifiers" +assert log[0] == "ReadFailureRecordData" + +pkt = GMLAN()/GMLAN_RFRDPR(subfunction=1) +log = get_log(pkt) +assert len(log) == 2 +assert log[1] == "readFailureRecordIdentifiers" +assert log[0] == "ReadFailureRecordDataPositiveResponse" + +pkt = GMLAN()/GMLAN_RDBPI(identifiers=[5]) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == '[OBD_EngineCoolantTemperature]' +assert log[0] == "ReadDataByParameterIdentifier" + +pkt = GMLAN()/GMLAN_RDBPIPR(parameterIdentifier=5) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == 'OBD_EngineCoolantTemperature' +assert log[0] == "ReadDataByParameterIdentifierPositiveResponse" + + +pkt = GMLAN()/GMLAN_RDBPKTI(subfunction=0) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == 'stopSending' +assert log[0] == "ReadDataByPacketIdentifier" + +pkt = GMLAN()/GMLAN_RMBA(memoryAddress=0) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == '0x0' +assert log[0] == "ReadMemoryByAddress" + +pkt = GMLAN()/GMLAN_RMBAPR(memoryAddress=0, dataRecord=b"deadbeef") +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1][0] == '0x0' +assert log[1][1] == b'deadbeef' +assert log[0] == "ReadMemoryByAddressPositiveResponse" + +pkt = GMLAN()/GMLAN_DDM(DPIDIdentifier=0, PIDData=b"deadbeef") +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1][0] == '0x0' +assert log[1][1] == b'deadbeef' +assert log[0] == "DynamicallyDefineMessage" + +pkt = GMLAN()/GMLAN_DDMPR(DPIDIdentifier=0) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == '0x0' +assert log[0] == "DynamicallyDefineMessagePositiveResponse" + +pkt = GMLAN()/GMLAN_DPBA(parameterIdentifier=0, memoryAddress=1, memorySize=3) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1][0] == 0 +assert log[1][1] == 1 +assert log[1][2] == 3 +assert log[0] == "DefinePIDByAddress" + +pkt = GMLAN()/GMLAN_DPBAPR(parameterIdentifier=0) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == 0 +assert log[0] == "DefinePIDByAddressPositiveResponse" + +pkt = GMLAN()/GMLAN_WDBI(dataIdentifier=0, dataRecord=b"deadbeef") +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1][0] == "0x0" +assert log[1][1] == b"deadbeef" +assert log[0] == "WriteDataByIdentifier" + +pkt = GMLAN()/GMLAN_WDBIPR(dataIdentifier=0) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == "0x0" +assert log[0] == "WriteDataByIdentifierPositiveResponse" + +pkt = GMLAN()/GMLAN_RDI(subfunction=0x80) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == "readStatusOfDTCByDTCNumber" +assert log[0] == "ReadDiagnosticInformation" + +pkt = GMLAN()/GMLAN_DC(CPIDNumber=0x80) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == "0x80" +assert log[0] == "DeviceControl" + +pkt = GMLAN()/GMLAN_DCPR(CPIDNumber=0x80) +log = get_log(pkt) +print(log) +assert len(log) == 2 +assert log[1] == "0x80" +assert log[0] == "DeviceControlPositiveResponse" ++ Single layer GMLAN mode + += Single layer mode: enable and basic dissect + +conf.contribs['GMLAN']['single_layer_mode'] = True + +ido = GMLAN(b'\x10\x02') +assert isinstance(ido, GMLAN_IDO), "Expected GMLAN_IDO, got %s" % type(ido) +assert ido.service == 0x10 +assert ido.subfunction == 0x02 + += Single layer mode: build GMLAN_IDO + +ido_built = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_built) == b'\x10\x02', "Expected b'\\x10\\x02', got %s" % bytes(ido_built).hex() + += Single layer mode: dissect positive response (using SA which has a PR class) + +sapr = GMLAN(b'\x67\x01\xde\xad') +assert isinstance(sapr, GMLAN_SAPR), "Expected GMLAN_SAPR, got %s" % type(sapr) +assert sapr.service == 0x67 +assert sapr.subfunction == 0x01 + += Single layer mode: NegativeResponse dissect + +nr = GMLAN(b'\x7f\x10\x22') +assert isinstance(nr, GMLAN_NR), "Expected GMLAN_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.returnCode == 0x22 + += Single layer mode: NegativeResponse answers() + +ido2 = GMLAN_IDO(subfunction=0x02) +nr2 = GMLAN_NR(requestServiceId=0x10, returnCode=0x22) +assert nr2.answers(ido2) + += Single layer mode: hashret consistency between request and positive response (SA) + +sa3 = GMLAN_SA(subfunction=0x01) +sapr3 = GMLAN_SAPR(subfunction=0x01) +assert sa3.hashret() == sapr3.hashret(), \ + "hashret mismatch: %s vs %s" % (sa3.hashret().hex(), sapr3.hashret().hex()) + += Single layer mode: sub-subpacket bindings are unaffected + +rfrdpr = GMLAN(b'\x52\x01\x00\x01\x02\x03\x04') +assert isinstance(rfrdpr, GMLAN_RFRDPR), "Expected GMLAN_RFRDPR, got %s" % type(rfrdpr) + += Single layer mode: unknown service falls back to GMLAN + +unknown = GMLAN(b'\xBB\x01\x02') +assert isinstance(unknown, GMLAN), "Expected GMLAN fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['GMLAN']['single_layer_mode'] = False + +ido4 = GMLAN(b'\x10\x02') +assert ido4.__class__ == GMLAN +assert ido4.service == 0x10 +assert ido4[GMLAN_IDO].subfunction == 0x02 + += Single layer mode: cleanup + +conf.contribs['GMLAN']['single_layer_mode'] = False +assert not conf.contribs['GMLAN']['single_layer_mode'] + ++ Compatibility mode GMLAN + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['GMLAN']['single_layer_mode'] = True +conf.contribs['GMLAN']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +ido_sa = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_sa) == b'\x10\x02', \ + "Standalone GMLAN_IDO should include service byte, got %s" % bytes(ido_sa).hex() +assert ido_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked) == b'\x10\x02', \ + "Stacked GMLAN/GMLAN_IDO should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +ido_dis = GMLAN(b'\x10\x02') +assert isinstance(ido_dis, GMLAN_IDO) +assert ido_dis.service == 0x10 +assert ido_dis.subfunction == 0x02 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['GMLAN']['compatibility_mode'] = False + +stacked_nc = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked_nc) == b'\x10\x10\x02', \ + "With compat OFF, stacked GMLAN/GMLAN_IDO should produce 3 bytes, got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +ido_nc = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_nc) == b'\x10\x02', \ + "Standalone GMLAN_IDO should include service byte even with compat OFF, got %s" % bytes(ido_nc).hex() + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['GMLAN']['single_layer_mode'] = False +conf.contribs['GMLAN']['compatibility_mode'] = False +stacked_slm_off = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked_slm_off) == b'\x10\x02', \ + "With SLM OFF, no service field in GMLAN_IDO regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() += Compatibility mode: cleanup +conf.contribs['GMLAN']['single_layer_mode'] = False +conf.contribs['GMLAN']['compatibility_mode'] = True +assert not conf.contribs['GMLAN']['single_layer_mode'] +assert conf.contribs['GMLAN']['compatibility_mode'] diff --git a/test/contrib/automotive/gm/gmlanutils.uts b/test/contrib/automotive/gm/gmlanutils.uts index 244adaa192e..68ee1c99480 100644 --- a/test/contrib/automotive/gm/gmlanutils.uts +++ b/test/contrib/automotive/gm/gmlanutils.uts @@ -1,122 +1,15 @@ % Regression tests for gmlanutil +~ scanner + Configuration ~ conf + = Imports -load_layer("can") -conf.contribs['CAN']['swap-bytes'] = False -import os, threading, six, subprocess, time, sys -from subprocess import call -from scapy.consts import LINUX - -= Definition of constants, utility functions and mock classes -iface0 = "vcan0" -iface1 = "vcan1" - -# function to exit when the can-isotp kernel module is not available -ISOTP_KERNEL_MODULE_AVAILABLE = False -def exit_if_no_isotp_module(): - if not ISOTP_KERNEL_MODULE_AVAILABLE: - sys.stderr.write("TEST SKIPPED: can-isotp not available" + os.linesep) - warning("Can't test ISOTP native socket because kernel module is not loaded") - exit(0) - - -= Initialize a virtual CAN interface -~ vcan_socket needs_root linux -if 0 != call(["cansend", iface0, "000#"]): - # vcan0 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface0, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface0) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface0, "up"]): - raise Exception("could not bring up %s" % iface0) - -if 0 != call(["cansend", iface0, "000#"]): - raise Exception("cansend doesn't work") - -if 0 != call(["cansend", iface1, "000#"]): - # vcan1 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface1, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface1) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface1, "up"]): - raise Exception("could not bring up %s" % iface1) - -if 0 != call(["cansend", iface1, "000#"]): - raise Exception("cansend doesn't work") - -print("CAN should work now") - -= Import CANSocket - -from scapy.contrib.cansocket_python_can import * - -import can as python_can -new_can_socket = lambda iface: CANSocket(bustype='virtual', channel=iface, timeout=0.01) -new_can_socket0 = lambda: CANSocket(bustype='virtual', channel=iface0, timeout=0.01) -new_can_socket1 = lambda: CANSocket(bustype='virtual', channel=iface1, timeout=0.01) - -# utility function for draining a can interface, asserting that no packets are there -def drain_bus(iface=iface0, assert_empty=True): - with new_can_socket(iface) as s: - pkts = s.sniff(timeout=0.1) - if assert_empty: - assert len(pkts) == 0 - -print("CAN sockets should work now") - -= Overwrite definition for vcan_socket systems native sockets -~ vcan_socket not_pypy needs_root linux - -if six.PY3 and LINUX: - from scapy.contrib.cansocket_native import * - new_can_socket = lambda iface: CANSocket(iface) - new_can_socket0 = lambda: CANSocket(iface0) - new_can_socket1 = lambda: CANSocket(iface1) - - -= Overwrite definition for vcan_socket systems python-can sockets -~ vcan_socket needs_root linux -if "python_can" in CANSocket.__module__: - new_can_socket = lambda iface: CANSocket(bustype='socketcan', channel=iface, timeout=0.001) - new_can_socket0 = lambda: CANSocket(bustype='socketcan', channel=iface0, timeout=0.001) - new_can_socket1 = lambda: CANSocket(bustype='socketcan', channel=iface1, timeout=0.001) - -= Verify that a CAN socket can be created and closed -s = new_can_socket(iface0) -s.close() - - -= Check if can-isotp and can-utils are installed on this system -~ linux -p1 = subprocess.Popen(['lsmod'], stdout = subprocess.PIPE) -p2 = subprocess.Popen(['grep', '^can_isotp'], stdout = subprocess.PIPE, stdin=p1.stdout) -p1.stdout.close() -if p1.wait() == 0 and p2.wait() == 0 and b"can_isotp" in p2.stdout.read(): - p = subprocess.Popen(["isotpsend", "-s1", "-d0", iface0], stdin = subprocess.PIPE) - p.communicate(b"01") - if p.returncode == 0: - ISOTP_KERNEL_MODULE_AVAILABLE = True - - -+ Syntax check - -= Import isotp -conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': ISOTP_KERNEL_MODULE_AVAILABLE} -load_contrib("isotp") - -if six.PY3 and ISOTP_KERNEL_MODULE_AVAILABLE: - from scapy.contrib.isotp import ISOTPNativeSocket - ISOTPSocket = ISOTPNativeSocket - assert ISOTPSocket == ISOTPNativeSocket -else: - from scapy.contrib.isotp import ISOTPSoftSocket - ISOTPSocket = ISOTPSoftSocket - assert ISOTPSocket == ISOTPSoftSocket + +from scapy.contrib.automotive import log_automotive +from test.testsocket import TestSocket, cleanup_testsockets +import logging ############ ############ @@ -124,176 +17,189 @@ else: = Load contribution layer -load_contrib("automotive.gm.gmlan") -load_contrib("automotive.gm.gmlanutils") +load_layer("can", globals_dict=globals()) +load_contrib("automotive.gm.gmlan", globals_dict=globals()) +load_contrib("automotive.gm.gmlanutils", globals_dict=globals()) + +log_automotive.setLevel(logging.DEBUG) + += Define test sockets + +isotpsock2 = TestSocket(GMLAN) +isotpsock = TestSocket(GMLAN) +isotpsock2.pair(isotpsock) ############################################################################## + GMLAN_RequestDownload Tests ############################################################################## = Positive, immediate positive response -ecusimSuccessfullyExecuted = True + +ecusimSuccessfullyExecuted = False started = threading.Event() + def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN()/GMLAN_RD(memorySize=4) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x74" - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN()/GMLAN_RD(memorySize=4) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + else: + ecusimSuccessfullyExecuted = True + ack = b"\x74" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1) == True - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=1) == True thread.join(timeout=5) + +assert res assert ecusimSuccessfullyExecuted == True = Negative, immediate negative response + started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - nr = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x22) - isotpsock2.send(nr) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + nr = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x22) + isotpsock2.send(nr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1) == False - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=1) == False thread.join(timeout=5) +assert res = Negative, timeout -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1) == False +assert GMLAN_RequestDownload(isotpsock, 4, timeout=0.01) == False ############################ Response pending = Positive, after response pending started = threading.Event() + def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) - isotpsock2.send(pending) - ack = b"\x74" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=2, started_callback=started.set) + pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) + isotpsock2.send(pending) + ack = b"\x74" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1) == True - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=2) == True thread.join(timeout=5) +assert res = Positive, hold response pending for several messages -tout = 0.3 +tout = 0.1 repeats = 4 started = threading.Event() + def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) - for i in range(repeats): - isotpsock2.send(ack) - time.sleep(tout) - ack = b"\x74" + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) + for i in range(repeats): isotpsock2.send(ack) + time.sleep(tout) + ack = b"\x74" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - starttime = time.time() # may be inaccurate -> on some systems only seconds precision - assert GMLAN_RequestDownload(isotpsock, 4, timeout=repeats*tout+0.5) == True - endtime = time.time() - assert (endtime - starttime) >= tout*repeats - +starttime = time.time() # may be inaccurate -> on some systems only seconds precision +result = GMLAN_RequestDownload(isotpsock, 4, timeout=repeats*tout+0.5) +endtime = time.time() + 1 thread.join(timeout=5) +assert result +print(endtime - starttime) +print(tout * (repeats - 1)) +assert (endtime - starttime) >= tout * (repeats - 1) + = Negative, negative response after response pending started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) - isotpsock2.send(pending) - nr = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x22) - isotpsock2.send(nr) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) + isotpsock2.send(pending) + nr = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x22) + isotpsock2.send(nr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1) == False - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=0.1) == False thread.join(timeout=5) +assert res + = Negative, timeout after response pending started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) - isotpsock2.send(pending) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) + isotpsock2.send(pending) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=0.3) == False - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=0.1) == False thread.join(timeout=5) +assert res + + = Positive, pending message from different service interferes while pending started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) - isotpsock2.send(pending) - wrongservice = GMLAN()/GMLAN_NR(requestServiceId=0x36, returnCode=0x78) - isotpsock2.send(wrongservice) - isotpsock2.send(pending) - ack = b"\x74" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) + isotpsock2.send(pending) + wrongservice = GMLAN()/GMLAN_NR(requestServiceId=0x36, returnCode=0x78) + isotpsock2.send(wrongservice) + isotpsock2.send(pending) + ack = b"\x74" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1) == True - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=0.1) == True thread.join(timeout=5) +assert res = Positive, negative response from different service interferes while pending started = threading.Event() + def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) - isotpsock2.send(pending) - wrongservice = GMLAN()/GMLAN_NR(requestServiceId=0x36, returnCode=0x22) - isotpsock2.send(wrongservice) - isotpsock2.send(pending) - ack = b"\x74" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pending = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x78) + isotpsock2.send(pending) + wrongservice = GMLAN()/GMLAN_NR(requestServiceId=0x36, returnCode=0x22) + isotpsock2.send(wrongservice) + isotpsock2.send(pending) + ack = b"\x74" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1) == True - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=0.1) == True thread.join(timeout=5) +assert res ################### RETRY = Positive, first: immediate negative response, retry: Positive @@ -301,31 +207,31 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # negative - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN()/GMLAN_RD(memorySize=4) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - nr = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x22) - # positive retry - print("retry") - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(nr)) - pkt = GMLAN()/GMLAN_RD(memorySize=4) - print(requ) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x74" - isotpsock2.send(ack) + # negative + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN()/GMLAN_RD(memorySize=4) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + nr = GMLAN()/GMLAN_NR(requestServiceId=0x34, returnCode=0x22) + # positive retry + print("retry") + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(nr)) + pkt = GMLAN()/GMLAN_RD(memorySize=4) + print(requ) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x74" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_RequestDownload(isotpsock, 4, timeout=1, retry=1) == True - assert ecusimSuccessfullyExecuted == True - +res = GMLAN_RequestDownload(isotpsock, 4, timeout=0.1, retry=1) == True thread.join(timeout=5) +assert res +assert ecusimSuccessfullyExecuted == True + ############################################################################## @@ -339,22 +245,21 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, - dataRecord=payload) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x76" - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, + dataRecord=payload) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x40000000, payload, timeout=1) == True - +res = GMLAN_TransferData(isotpsock, 0x40000000, payload, timeout=1) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True = Positive, short payload, scheme = 3 @@ -365,22 +270,21 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x400000, - dataRecord=payload) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x76" - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x400000, + dataRecord=payload) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x400000, payload, timeout=1) == True - +res = GMLAN_TransferData(isotpsock, 0x400000, payload, timeout=1) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True = Positive, short payload, scheme = 2 @@ -391,22 +295,20 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted = True - with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - time.sleep(0) - requ = isotpsock2.sniff(count=1, timeout=2, started_callback=started.set) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x4000, dataRecord=payload) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x76" - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=2, started_callback=started.set) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x4000, dataRecord=payload) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x4000, payload, timeout=2) == True - +res = GMLAN_TransferData(isotpsock, 0x4000, payload, timeout=2) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True = Negative, short payload @@ -417,23 +319,22 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - nr = GMLAN() / GMLAN_NR(requestServiceId=0x36, returnCode=0x22) - isotpsock2.send(nr) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + nr = GMLAN() / GMLAN_NR(requestServiceId=0x36, returnCode=0x22) + isotpsock2.send(nr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x40000000, payload, timeout=1) == False - +res = GMLAN_TransferData(isotpsock, 0x40000000, payload, timeout=1) == False thread.join(timeout=5) +assert res = Negative, timeout -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x4000, payload, timeout=1) == False +assert GMLAN_TransferData(isotpsock, 0x4000, payload, timeout=0.1) == False + = Positive, long payload conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 @@ -443,29 +344,28 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, - dataRecord=payload*2) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x76" - # second package with inscreased address - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000010, - dataRecord=payload * 2) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x76" - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, + dataRecord=payload*2) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x76" + # second package with inscreased address + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000010, + dataRecord=payload * 2) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x40000000, payload*4, maxmsglen=16, timeout=1) == True - +res = GMLAN_TransferData(isotpsock, 0x40000000, payload*4, maxmsglen=16, timeout=1) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True @@ -475,66 +375,60 @@ conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 payload = b"\x00\x11\x22\x33\x44\x55\x66\x77" started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x76" - # second package with inscreased address - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pending = GMLAN() / GMLAN_NR(requestServiceId=0x36, returnCode=0x78) - isotpsock2.send(pending) - time.sleep(0.1) - nr = GMLAN() / GMLAN_NR(requestServiceId=0x36, returnCode=0x22) - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(nr)) - ack = b"\x76" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = b"\x76" + # second package with inscreased address + isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pending = GMLAN() / GMLAN_NR(requestServiceId=0x36, returnCode=0x78) + isotpsock2.send(pending) + nr = GMLAN() / GMLAN_NR(requestServiceId=0x36, returnCode=0x22) + isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(nr)) + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x40000000, payload*4, maxmsglen=16, timeout=1, retry=1) == True - +res = GMLAN_TransferData(isotpsock, 0x40000000, payload*4, maxmsglen=16, timeout=0.1, retry=1) == True thread.join(timeout=5) +assert res ############ = Positive, maxmsglen length check -> message is split automatically -* TODO: This test causes an error in ISOTPSoftSockets -exit_if_no_isotp_module() conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 payload = b"\x00\x11\x22\x33\x44\x55\x66\x77" ecusimSuccessfullyExecuted = True sim_started = threading.Event() -started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=3, started_callback=sim_started.set) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, - dataRecord=payload*511+payload[:1]) - if len(requ) == 0 or bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - return - ack = b"\x76" - # second package with inscreased address - requ = isotpsock2.sniff(count=1, timeout=3, started_callback=lambda: isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000FF9, - dataRecord=payload[1:]) - if len(requ) == 0 or bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - return - ack = b"\x76" - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=3, started_callback=sim_started.set) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, + dataRecord=payload*511+payload[:1]) + if len(requ) == 0 or bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + return + ack = b"\x76" + # second package with inscreased address + requ = isotpsock2.sniff(count=1, timeout=3, started_callback=lambda: isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000FF9, + dataRecord=payload[1:]) + if len(requ) == 0 or bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + return + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) -thread.name = "ECUSimulator" + thread.name +thread.name = "EcuSimulator" + thread.name +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - sim_started.wait(timeout=5) - assert GMLAN_TransferData(isotpsock, 0x40000000, payload*512, maxmsglen=0x1000000, timeout=8) == True - +sim_started.wait(timeout=5) +res = GMLAN_TransferData(isotpsock, 0x40000000, payload*512, maxmsglen=0x1000000, timeout=8) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True ############ Address boundary checks @@ -543,72 +437,68 @@ conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 payload = b"\x00\x11\x22\x33\x44\x55\x66\x77" started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x76" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 2**32 - 1, payload, timeout=1) == True - +res = GMLAN_TransferData(isotpsock, 2**32 - 1, payload, timeout=1) == True thread.join(timeout=5) +assert res = Negative, invalid address (too large for addressing scheme) conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 payload = b"\x00\x11\x22\x33\x44\x55\x66\x77" started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x76" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=0.1, started_callback=started.set) + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 2**32, payload, timeout=1) == False - +res = GMLAN_TransferData(isotpsock, 2**32, payload, timeout=0.1) == False thread.join(timeout=5) +assert res = Positive, address zero conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 ecusimSuccessfullyExecuted = True started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x76" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, 0x00, payload, timeout=1) == True - +res = GMLAN_TransferData(isotpsock, 0x00, payload, timeout=1) == True thread.join(timeout=5) +assert res = Negative, negative address conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 payload = b"\x00\x11\x22\x33\x44\x55\x66\x77" started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x76" - isotpsock2.send(ack) + isotpsock2.sniff(count=1, timeout=0.1, started_callback=started.set) + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferData(isotpsock, -1, payload, timeout=1) == False - +res = GMLAN_TransferData(isotpsock, -1, payload, timeout=0.1) == False thread.join(timeout=5) +assert res ############################################ + GMLAN_TransferPayload Tests @@ -621,27 +511,26 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN()/GMLAN_RD(memorySize=len(payload)) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x74" - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, - dataRecord=payload) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x76" - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN()/GMLAN_RD(memorySize=len(payload)) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x74" + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_TD(startingAddress=0x40000000, + dataRecord=payload) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x76" + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_TransferPayload(isotpsock, 0x40000000, payload, timeout=1) == True - +res = GMLAN_TransferPayload(isotpsock, 0x40000000, payload, timeout=1) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True @@ -657,32 +546,30 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # wait for request - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN()/GMLAN_SA(subfunction=1) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) - # wait for key - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) - pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbeef) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x35) - isotpsock2.send(nr) - else: - pr = GMLAN()/GMLAN_SAPR(subfunction=2) - isotpsock2.send(pr) + # wait for request + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN()/GMLAN_SA(subfunction=1) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) + # wait for key + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) + pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbeef) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x35) + isotpsock2.send(nr) + else: + pr = GMLAN()/GMLAN_SAPR(subfunction=2) + isotpsock2.send(pr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=1) == True - +res = GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=0.1) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True = Positive scenario, level 3 @@ -691,31 +578,30 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # wait for request - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN()/GMLAN_SA(subfunction=3) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=3, securitySeed=0xdead) - # wait for key - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) - pkt = GMLAN()/GMLAN_SA(subfunction=4, securityKey=0xbeef) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x35) - isotpsock2.send(nr) - else: - pr = GMLAN()/GMLAN_SAPR(subfunction=4) - isotpsock2.send(pr) + # wait for request + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN()/GMLAN_SA(subfunction=3) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=3, securitySeed=0xdead) + # wait for key + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) + pkt = GMLAN()/GMLAN_SA(subfunction=4, securityKey=0xbeef) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x35) + isotpsock2.send(nr) + else: + pr = GMLAN()/GMLAN_SAPR(subfunction=4) + isotpsock2.send(pr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=3, timeout=1) == True - +res = GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=3, timeout=0.1) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True @@ -725,127 +611,121 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # wait for request - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN()/GMLAN_SA(subfunction=1) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) - # wait for key - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) - pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbabe) - if bytes(requ[0]) != bytes(pkt): - nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x35) - isotpsock2.send(nr) - else: - ecusimSuccessfullyExecuted = False - pr = GMLAN()/GMLAN_SAPR(subfunction=2) - isotpsock2.send(pr) + # wait for request + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN()/GMLAN_SA(subfunction=1) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) + # wait for key + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) + pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbabe) + if bytes(requ[0]) != bytes(pkt): + nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x35) + isotpsock2.send(nr) + else: + ecusimSuccessfullyExecuted = False + pr = GMLAN()/GMLAN_SAPR(subfunction=2) + isotpsock2.send(pr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=1) == False - +res = GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=0.1) == False thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True = invalid level (not an odd number) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=2, timeout=1) == False +assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=2, timeout=1) == False = zero seed started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # wait for request - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0x0000) - isotpsock2.send(seedmsg) + # wait for request + isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0x0000) + isotpsock2.send(seedmsg) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=1) == True - +res = GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=0.1) == True thread.join(timeout=5) +assert res ############### retry = Positive scenario, request timeout, retry works started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # timeout - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - # wait for request - requ = isotpsock2.sniff(count=1, timeout=3) - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) - # wait for key - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) - pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbeef) - pr = GMLAN()/GMLAN_SAPR(subfunction=2) - isotpsock2.send(pr) + # timeout + requ = isotpsock2.sniff(count=1, timeout=0.1, started_callback=started.set) + # wait for request + requ = isotpsock2.sniff(count=1, timeout=3) + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) + # wait for key + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) + pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbeef) + pr = GMLAN()/GMLAN_SAPR(subfunction=2) + isotpsock2.send(pr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=1, retry=1) == True - +res = GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=0.1, retry=1) == True thread.join(timeout=5) +assert res = Positive scenario, keysend timeout, retry works started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # wait for request - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) - # timeout - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) - # retry from start - requ = isotpsock2.sniff(count=1, timeout=3) - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) - # wait for key - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) - pr = GMLAN()/GMLAN_SAPR(subfunction=2) - isotpsock2.send(pr) + # wait for request + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) + # timeout + requ = isotpsock2.sniff(count=1, timeout=0.1, started_callback=lambda:isotpsock2.send(seedmsg)) + # retry from start + requ = isotpsock2.sniff(count=1, timeout=3) + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) + # wait for key + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) + pr = GMLAN()/GMLAN_SAPR(subfunction=2) + isotpsock2.send(pr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=1, retry=1) == True - +res = GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=0.1, retry=1) == True thread.join(timeout=5) +assert res = Positive scenario, request error, retry works started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # wait for request - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x37) - # wait for request - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(nr)) - seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) - # wait for key - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) - pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbeef) - pr = GMLAN()/GMLAN_SAPR(subfunction=2) - isotpsock2.send(pr) + # wait for request + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + nr = GMLAN() / GMLAN_NR(requestServiceId=0x27, returnCode=0x37) + # wait for request + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(nr)) + seedmsg = GMLAN()/GMLAN_SAPR(subfunction=1, securitySeed=0xdead) + # wait for key + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(seedmsg)) + pkt = GMLAN()/GMLAN_SA(subfunction=2, securityKey=0xbeef) + pr = GMLAN()/GMLAN_SAPR(subfunction=2) + isotpsock2.send(pr) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=1, retry=1) == True - +res = GMLAN_GetSecurityAccess(isotpsock, keyfunc, level=1, timeout=0.1, retry=1) == True thread.join(timeout=5) +assert res ############################################################################## @@ -857,85 +737,81 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN(b"\x28") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN(b"\xa2") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - # ProgrammingMode requestProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_PM(subfunction=0x1) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN(b"\xe5") - # InitiateProgramming enableProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_PM(subfunction=0x3) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN(b"\x28") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN(b"\xa2") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + # ProgrammingMode requestProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_PM(subfunction=0x1) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN(b"\xe5") + # InitiateProgramming enableProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_PM(subfunction=0x3) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False thread = threading.Thread(target=ecusim) + +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1) == True - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, unittest=True) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True -= sequence of the correct messages, disablenormalcommunication as broadcast -* TODO: This test errors if executed with ISOTPSoftSockets -exit_if_no_isotp_module() += sequence of the correct messages, disablenormalcommunication as broadcast ecusimSuccessfullyExecuted = True started = threading.Event() + +broadcastsender = TestSocket(CAN) +broadcastrcv = TestSocket(CAN) +broadcastsender.pair(broadcastrcv) def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2, \ - new_can_socket(iface0) as broadcastrcv: - print("DisableNormalCommunication") - requ = broadcastrcv.sniff(count=1, timeout=2, started_callback=started.set) - pkt = GMLAN(b"\x28") - print(requ) - assert len(requ) >= 1 - if bytes(requ[0].data) != b"\xfe\x01" + bytes(pkt): - ecusimSuccessfullyExecuted = False - print("ReportProgrammedState") - requ = isotpsock2.sniff(count=1, timeout=2) - pkt = GMLAN(b"\xa2") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - print("ProgrammingMode requestProgramming") - requ = isotpsock2.sniff(count=1, timeout=2, started_callback=lambda: isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_PM(subfunction=0x1) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN(b"\xe5") - print("InitiateProgramming enableProgramming") - requ = isotpsock2.sniff(count=1, timeout=2, started_callback=lambda: isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_PM(subfunction=0x3) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False + print("DisableNormalCommunication") + requ = broadcastrcv.sniff(count=1, timeout=2, started_callback=started.set) + assert len(requ) >= 1 + if bytes(requ[0].data)[0:3] != b"\xfe\x01\x28": + ecusimSuccessfullyExecuted = False + print("ReportProgrammedState") + requ = isotpsock2.sniff(count=1, timeout=2) + pkt = GMLAN(b"\xa2") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + print("ProgrammingMode requestProgramming") + requ = isotpsock2.sniff(count=1, timeout=3, started_callback=lambda: isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_PM(subfunction=0x1) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN(b"\xe5") + print("InitiateProgramming enableProgramming") + requ = isotpsock2.sniff(count=1, timeout=3, started_callback=lambda: isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_PM(subfunction=0x3) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, broadcastsocket=GMLAN_BroadcastSocket(new_can_socket(iface0)), timeout=8, verbose=1) == True - +res = GMLAN_InitDiagnostics(isotpsock, broadcast_socket=GMLAN_BroadcastSocket(broadcastsender), timeout=5, unittest=True) == True thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True @@ -946,21 +822,19 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN(b"\x28") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN(b"\x28") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1) == False - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, unittest=True) == False thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True @@ -970,29 +844,27 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN(b"\x28") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN(b"\xa2") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - isotpsock2.send(ack) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN(b"\x28") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN(b"\xa2") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1) == False - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, unittest=True) == False thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True @@ -1002,85 +874,80 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN(b"\x28") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN(b"\xa2") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - # ProgrammingMode requestProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - pkt = GMLAN() / GMLAN_PM(subfunction=0x1) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN(b"\x28") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN(b"\xa2") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + # ProgrammingMode requestProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + pkt = GMLAN() / GMLAN_PM(subfunction=0x1) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1) == False - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, unittest=True) == False thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True -###### negative respone +###### negative response = timeout DisableNormalCommunication ecusimSuccessfullyExecuted = True started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN(b"\x28") - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) - isotpsock2.send(ack) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN(b"\x28") + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) + +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1) == False - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, unittest=True) == False thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True ###### retry tests = sequence of the correct messages, retry set started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - # ProgrammingMode requestProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN(b"\xe5") - # InitiateProgramming enableProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + # ProgrammingMode requestProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN(b"\xe5") + # InitiateProgramming enableProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1, retry=0) == True - +res = GMLAN_InitDiagnostics(isotpsock, timeout=1, retry=0, unittest=True) == True +assert res thread.join(timeout=5) @@ -1090,116 +957,108 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - if len(requ) != 0: - ecusimSuccessfullyExecuted = False + requ = isotpsock2.sniff(count=1, timeout=0.1, started_callback=started.set) + ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) + requ = isotpsock2.sniff(count=1, timeout=0.1, started_callback=lambda:isotpsock2.send(ack)) + if len(requ) != 0: + ecusimSuccessfullyExecuted = False thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1, retry=0) == False - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, retry=0, unittest=True) == False thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True = first fail at DisableNormalCommunication, then sequence of the correct messages started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - # ProgrammingMode requestProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN(b"\xe5") - # InitiateProgramming enableProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + # ProgrammingMode requestProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN(b"\xe5") + # InitiateProgramming enableProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1, retry=1) == True - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, retry=1, unittest=True) == True thread.join(timeout=5) +assert res = first fail at ReportProgrammedState, then sequence of the correct messages started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x68" - # Fail - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN() / GMLAN_NR(requestServiceId=0xA2, returnCode=0x12) - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - # ProgrammingMode requestProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN(b"\xe5") - # InitiateProgramming enableProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = b"\x68" + # Fail + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN() / GMLAN_NR(requestServiceId=0xA2, returnCode=0x12) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + # ProgrammingMode requestProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN(b"\xe5") + # InitiateProgramming enableProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1, retry=1) == True - +res = GMLAN_InitDiagnostics(isotpsock, timeout=1, retry=1, unittest=True) == True thread.join(timeout=5) +assert res = first fail at ProgrammingMode requestProgramming, then sequence of the correct messages started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - # Fail - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN() / GMLAN_NR(requestServiceId=0xA5, returnCode=0x12) - # DisableNormalCommunication - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = b"\x68" - # ReportProgrammedState - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN()/GMLAN_RPSPR(programmedState=0) - # ProgrammingMode requestProgramming - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN(b"\xe5") - isotpsock2.send(ack) - # InitiateProgramming enableProgramming - requ = isotpsock2.sniff(count=1, timeout=1) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + # Fail + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN() / GMLAN_NR(requestServiceId=0xA5, returnCode=0x12) + # DisableNormalCommunication + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = b"\x68" + # ReportProgrammedState + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN()/GMLAN_RPSPR(programmedState=0) + # ProgrammingMode requestProgramming + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN(b"\xe5") + isotpsock2.send(ack) + # InitiateProgramming enableProgramming + requ = isotpsock2.sniff(count=1, timeout=1) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1, retry=1) == True - +res = GMLAN_InitDiagnostics(isotpsock, timeout=1, retry=1, unittest=True) == True thread.join(timeout=5) +assert res = fail twice ecusimSuccessfullyExecuted = True @@ -1207,23 +1066,21 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - if len(requ) != 0: - ecusimSuccessfullyExecuted = False + requ = isotpsock2.sniff(count=1, timeout=0.1, started_callback=started.set) + ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) + requ = isotpsock2.sniff(count=1, timeout=0.1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN() / GMLAN_NR(requestServiceId=0x28, returnCode=0x12) + requ = isotpsock2.sniff(count=1, timeout=0.1, started_callback=lambda:isotpsock2.send(ack)) + if len(requ) != 0: + ecusimSuccessfullyExecuted = False thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_InitDiagnostics(isotpsock, timeout=1, verbose=1, retry=1) == False - +res = GMLAN_InitDiagnostics(isotpsock, timeout=0.1, retry=1, unittest=True) == False thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True ############################################################################## @@ -1237,21 +1094,20 @@ started = threading.Event() def ecusim(): global ecusimSuccessfullyExecuted ecusimSuccessfullyExecuted= True - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - pkt = GMLAN() / GMLAN_RMBA(memoryAddress=0x0, memorySize=0x8) - if bytes(requ[0]) != bytes(pkt): - ecusimSuccessfullyExecuted = False - ack = GMLAN() / GMLAN_RMBAPR(memoryAddress=0x0, dataRecord=payload) - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + pkt = GMLAN() / GMLAN_RMBA(memoryAddress=0x0, memorySize=0x8) + if bytes(requ[0]) != bytes(pkt): + ecusimSuccessfullyExecuted = False + ack = GMLAN() / GMLAN_RMBAPR(memoryAddress=0x0, dataRecord=payload) + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=1) == payload - +res = GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=1) == payload thread.join(timeout=5) +assert res assert ecusimSuccessfullyExecuted == True @@ -1260,22 +1116,20 @@ conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 payload = b"\x00\x11\x22\x33\x44\x55\x66\x77" started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = GMLAN() / GMLAN_NR(requestServiceId=0x23, returnCode=0x31) - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = GMLAN() / GMLAN_NR(requestServiceId=0x23, returnCode=0x31) + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=1) is None - +res = GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=1) is None thread.join(timeout=5) +assert res = Negative, timeout -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=1) is None +assert GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=0.01) is None ###### RETRY = Positive, negative response, retry succeeds @@ -1283,28 +1137,22 @@ conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 payload = b"\x00\x11\x22\x33\x44\x55\x66\x77" started = threading.Event() def ecusim(): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242, basecls=GMLAN) as isotpsock2: - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) - ack = GMLAN() / GMLAN_NR(requestServiceId=0x23, returnCode=0x31) - requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) - ack = GMLAN() / GMLAN_RMBAPR(memoryAddress=0x0, dataRecord=payload) - isotpsock2.send(ack) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=started.set) + ack = GMLAN() / GMLAN_NR(requestServiceId=0x23, returnCode=0x31) + requ = isotpsock2.sniff(count=1, timeout=1, started_callback=lambda:isotpsock2.send(ack)) + ack = GMLAN() / GMLAN_RMBAPR(memoryAddress=0x0, dataRecord=payload) + isotpsock2.send(ack) thread = threading.Thread(target=ecusim) +sniff(timeout=0.01, opened_socket=[isotpsock, isotpsock2]) thread.start() started.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x242, did=0x642, basecls=GMLAN) as isotpsock: - assert GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=1, retry=1) == payload - +res = GMLAN_ReadMemoryByAddress(isotpsock, 0x0, 0x8, timeout=1, retry=1) == payload thread.join(timeout=5) +assert res + Cleanup -= Delete vcan interfaces -~ vcan_socket needs_root linux - -if 0 != call(["sudo", "ip", "link", "delete", iface0]): - raise Exception("%s could not be deleted" % iface0) += Delete TestSockets -if 0 != call(["sudo", "ip", "link", "delete", iface1]): - raise Exception("%s could not be deleted" % iface1) +cleanup_testsockets() diff --git a/test/contrib/automotive/gm/scanner.uts b/test/contrib/automotive/gm/scanner.uts new file mode 100644 index 00000000000..59a5e265dc6 --- /dev/null +++ b/test/contrib/automotive/gm/scanner.uts @@ -0,0 +1,540 @@ +% Regression tests for GMLAN Scanners +~ scanner + ++ Configuration +~ conf + += Imports + +import itertools +import logging +import threading +import time +from scapy.contrib.isotp import ISOTPMessageBuilder + +from test.testsocket import TestSocket, cleanup_testsockets, open_test_sockets + + +############ +############ ++ Load general modules + += Load contribution layer + +from scapy.contrib.automotive.gm.gmlan import * +conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 +from scapy.contrib.automotive.gm.gmlan_scanner import * +from scapy.contrib.automotive.ecu import * +load_layer("can") + +log_automotive.setLevel(logging.DEBUG) + + += Define Testfunction + +def executeScannerInVirtualEnvironment(supported_responses, enumerators, **kwargs): + tester = TestSocket(GMLAN) + ecu = TestSocket(GMLAN) + tester.pair(ecu) + answering_machine = EcuAnsweringMachine(supported_responses=supported_responses, main_socket=ecu, basecls=GMLAN, verbose=False) + def reset(): + answering_machine.reset_state() + sniff(timeout=0.001, opened_socket=[ecu, tester]) + sim = threading.Thread(target=answering_machine, kwargs={ + "timeout": 30, "stop_filter": lambda x: bytes(x) == b"\xff\xff\xff"}) + sim.start() + try: + scanner = GMLAN_Scanner( + tester, reset_handler=reset, + test_cases=enumerators, timeout=0.2, retry_if_none_received=True, + unittest=True, **kwargs) + def scanner_thread(): + for i in range(3): + print("Starting scan") + scanner.scan(timeout=10) + if scanner.scan_completed: + print("Scan completed after %d iterations" % i) + return + scanner_t = threading.Thread(target=scanner_thread) + scanner_t.start() + scanner_t.join(timeout=120) + if scanner_t.is_alive(): + scanner.stop_scan() + finally: + tester.send(Raw(b"\xff\xff\xff")) + sim.join(timeout=2) + assert not sim.is_alive() + cleanup_testsockets() + return scanner + += Load packets from candump + +conf.contribs['CAN']['swap-bytes'] = True +pkts = rdpcap(scapy_path("test/pcaps/candump_gmlan_scanner.pcap.gz")) +assert len(pkts) + += Create GMLAN messages from packets + +builder = ISOTPMessageBuilder(basecls=GMLAN, use_ext_address=False, rx_id=[0x241, 0x641]) +msgs = list() + +for p in pkts: + if p.data == b"ECURESET": + msgs.append(p) + else: + builder.feed(p) + if len(builder): + msgs.append(builder.pop()) + +assert len(msgs) + += Create ECU-Clone from packets + +mEcu = Ecu(logging=False, verbose=False, store_supported_responses=True) + +for p in msgs: + if isinstance(p, CAN) and p.data == b"ECURESET": + mEcu.reset() + else: + mEcu.update(p) + +assert len(mEcu.supported_responses) + += Test GMLAN_SAEnumerator evaluate_response + +e = GMLAN_SAEnumerator() + +config = {} + +s = EcuState(session=1) + +debug_dissector_backup = conf.debug_dissector + +# This tests involves corrupted Packets, therefore we need to disable the debug_dissector +conf.debug_dissector = False + +assert False == e._evaluate_response(s, GMLAN(b"\x27\x01"), None, **config) +config = {"exit_if_service_not_supported": True} +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x7f\x27\x11"), **config) +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x7f\x27\x22"), **config) +assert e._retry_pkt[s] == GMLAN(b"\x27\x01") +assert False == e._evaluate_response(s, GMLAN(b"\x27\x02"), GMLAN(b"\x7f\x27\x22"), **config) +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x7f\x27\x37"), **config) +assert e._retry_pkt[s] == GMLAN(b"\x27\x01") +assert False == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x7f\x27\x37"), **config) +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x67\x01ab"), **config) +assert not e._retry_pkt[s] +assert False == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x67\x02ab"), **config) +assert not e._retry_pkt[s] +conf.debug_dissector = debug_dissector_backup + + += Simulate ECU and run Scanner + +def securityAccess_Algorithm1(seed): + return 0x5F51 + +keyfunction = securityAccess_Algorithm1 + +scanner = executeScannerInVirtualEnvironment( + mEcu.supported_responses, + [GMLAN_IDOEnumerator, GMLAN_PMEnumerator, GMLAN_RDEnumerator, GMLAN_SAEnumerator], + GMLAN_SAEnumerator_kwargs={"keyfunction": keyfunction, "scan_range": range(2)}, + GMLAN_PMEnumerator_kwargs={"unittest": True}) + +assert len(scanner.state_paths) == 9 +assert scanner.scan_completed + +assert EcuState(session=1) in scanner.final_states +assert EcuState(session=1, security_level=2) in scanner.final_states +assert EcuState(session=3, tp=1) in scanner.final_states +assert EcuState(session=2, tp=1, communication_control=1) in scanner.final_states +assert EcuState(session=2, tp=1, communication_control=1, security_level=2) in scanner.final_states +assert EcuState(session=3, tp=1, security_level=2) in scanner.final_states +assert EcuState(session=2, tp=1, communication_control=1, security_level=2, request_download=1) in scanner.final_states + += Simulate ECU and test GMLAN_RDBIEnumerator + +resps = [EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=2)/Raw(b"beef2")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=0xff)/Raw(b"beefff")]), + EcuResponse(None, [GMLAN()/GMLAN_NR(returnCode="SubFunctionNotSupported", requestServiceId="ReadDataByIdentifier")])] + +es = [GMLAN_RDBIEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) == 256 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "beef2" in result +assert "beef3" in result +assert "beefff" in result +assert "SubFunctionNotSupported received" in result + +ids = [t.req.dataIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + + += Simulate ECU and test GMLAN_WDBIEnumerator + +def wdbi_handler(resp, req): + if req.service != 0x3b: + return False + assert req.dataIdentifier in [1, 2, 3, 0xff] + resp.dataIdentifier = req.dataIdentifier + if req.dataIdentifier == 1: + assert req.dataRecord == b'asdfbeef1' + return True + if req.dataIdentifier == 2: + assert req.dataRecord == b'beef2' + return True + if req.dataIdentifier == 3: + assert req.dataRecord == b"beef3" + return True + if req.dataIdentifier == 0xff: + assert req.dataRecord == b"beefff" + return True + return False + +resps = [EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=2)/Raw(b"beef2")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBIPR(dataIdentifier=0xff)/Raw(b"beefff")]), + EcuResponse(None, [GMLAN()/GMLAN_WDBIPR()], answers=wdbi_handler), + EcuResponse(None, [GMLAN()/GMLAN_NR(returnCode="SubFunctionNotSupported", requestServiceId="ReadDataByIdentifier")])] + +es = [GMLAN_WDBISelectiveEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +tc = scanner.configuration.test_cases[0][0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) == 256 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "beef2" in result +assert "beef3" in result +assert "beefff" in result +assert "SubFunctionNotSupported received" in result + +ids = [t.req.dataIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + +######################### WDBI ############################# +tc = scanner.configuration.test_cases[0][1] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) == 0 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +ids = [t.req.dataIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + + += Simulate ECU and test GMLAN_RDBPIEnumerator + +resps = [EcuResponse(None, [GMLAN()/GMLAN_RDBPIPR(parameterIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBPIPR(parameterIdentifier=2)/Raw(b"asdfbeef2")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBPIPR(parameterIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [GMLAN()/GMLAN_RDBPIPR(parameterIdentifier=0xffff)/Raw(b"beefffff")]), + EcuResponse(None, [GMLAN()/GMLAN_NR(returnCode="SubFunctionNotSupported", requestServiceId="ReadDataByParameterIdentifier")])] + +es = [GMLAN_RDBPIEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es, GMLAN_RDBPIEnumerator_kwargs={"scan_range":list(range(0x100)) + list(range(0xff00, 0x10000))}) + +assert scanner.scan_completed +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) == 0x200 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "asdfbeef2" in result +assert "beef3" in result +assert "beefffff" in result +assert "SubFunctionNotSupported received" in result + +ids = [t.req.identifiers[0] for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xffff in ids + += Simulate ECU and test GMLAN_TPEnumerator + +resps = [EcuResponse(None, [GMLAN(service=0x7e)])] + +es = [GMLAN_TPEnumerator] +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) == 0 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 2 + + += Simulate ECU and test GMLAN_DCEnumerator + +resps = [EcuResponse(None, [GMLAN()/GMLAN_DCPR(CPIDNumber=1)]), + EcuResponse(None, [GMLAN()/GMLAN_DCPR(CPIDNumber=2)]), + EcuResponse(None, [GMLAN()/GMLAN_DCPR(CPIDNumber=3)/Raw(b"beef3")]), + EcuResponse(None, [GMLAN()/GMLAN_DCPR(CPIDNumber=0xff)/Raw(b"beefff")]), + EcuResponse(None, [GMLAN()/GMLAN_NR(returnCode="SubFunctionNotSupported", requestServiceId="DeviceControl")])] + +es = [GMLAN_DCEnumerator] +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) == 256 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +ids = [t.req.CPIDNumber for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 255 in ids + +result = tc.show(dump=True) + +assert "SubFunctionNotSupported received " in result + += Simulate ECU and test GMLAN_TDEnumerator + +conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 + +positive_responses_left = 4 + +def answers_td(resp, req): + global positive_responses_left + if req.service != 0x36: + return False + if not positive_responses_left: + return False + positive_responses_left -= 1 + resp.service = 0x76 + return True + +resps = [EcuResponse(None, [GMLAN(service="TransferDataPositiveResponse")], answers=answers_td), + EcuResponse(None, [GMLAN()/GMLAN_NR(returnCode="RequestOutOfRange", requestServiceId="TransferData")])] + +es = [GMLAN_TDEnumerator] +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) == 0x1ff - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "RequestOutOfRange received " in result + += Simulate ECU and test GMLAN_RMBAEnumerator 1 +~ not_pypy + +conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 2 + +memory = dict() + +mem_areas = [(0x100, 0x1f00), (0xd000, 0xff00), (0xa000, 0xc000), (0x3000, 0x5f00)] + +mem_ranges = [range(s, e) for s, e in mem_areas] + +mem_inner_borders = [s for s, _ in mem_areas] +mem_inner_borders += [e - 1 for _, e in mem_areas] + +mem_outer_borders = [s - 1 for s, _ in mem_areas] +mem_outer_borders += [e for _, e in mem_areas] + +mem_random_test_points = [] +for _ in range(100): + mem_random_test_points += [random.choice(list(itertools.chain(*mem_ranges)))] + +for addr in itertools.chain(*mem_ranges): + memory[addr] = addr & 0xff + +def answers_rmba(resp, req): + global memory + if req.service != 0x23: + return False + if req.memoryAddress not in memory.keys(): + return False + out_mem = list() + for i in range(req.memoryAddress, req.memoryAddress + req.memorySize): + try: + out_mem.append(memory[i]) + except KeyError: + pass + resp.memoryAddress = req.memoryAddress + resp.dataRecord = bytes(out_mem) + return True + +resps = [EcuResponse(None, [GMLAN()/GMLAN_RMBAPR(memoryAddress=0, dataRecord=b'')], answers=answers_rmba), + EcuResponse(None, [GMLAN()/GMLAN_NR(returnCode="RequestOutOfRange", requestServiceId="ReadMemoryByAddress")])] + +####################################################### +scanner = executeScannerInVirtualEnvironment(resps, [GMLAN_RMBAEnumerator]) + +assert scanner.scan_completed +tc1 = scanner.configuration.test_cases[0] + +assert len(tc1.results_without_response) < 10 +assert len(tc1.results_with_negative_response) > 10 +assert len(tc1.results_with_positive_response) > 50 +assert len(tc1.scanned_states) == 1 + +result = tc1.show(dump=True) + +assert "RequestOutOfRange received " in result + + +def _get_memory_addresses_from_results(results): + mem_areas = [ + range(tup.req.memoryAddress, tup.req.memoryAddress + tup.req.memorySize) + for tup in results] + return set(list(itertools.chain.from_iterable(mem_areas))) + +############################################################ + +addrs = _get_memory_addresses_from_results(tc1.results_with_positive_response) + +print([tp in addrs for tp in mem_inner_borders].count(True) / len(mem_inner_borders)) +assert [tp in addrs for tp in mem_inner_borders].count(True) / len(mem_inner_borders) > 0.8 +print([tp in addrs for tp in mem_random_test_points].count(True) / len(mem_random_test_points)) +assert [tp in addrs for tp in mem_random_test_points].count(True) / len(mem_random_test_points) > 0.8 +print([tp not in addrs for tp in mem_outer_borders].count(True) / len(mem_outer_borders)) +assert [tp not in addrs for tp in mem_outer_borders].count(True) / len(mem_outer_borders) > 0.8 + + += Simulate ECU and test GMLAN_RMBAEnumerator 2 +* This test takes very long to execute + +~ disabled + +conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 3 + +memory = dict() + +for addr in itertools.chain(range(0x10000), range(0xf00000, 0xf0f000)): + memory[addr] = addr & 0xff + +resps = [EcuResponse(None, [GMLAN()/GMLAN_RMBAPR(memoryAddress=0, dataRecord=b'')], answers=answers_rmba), + EcuResponse(None, [GMLAN()/GMLAN_NR(returnCode="RequestOutOfRange", requestServiceId="ReadMemoryByAddress")])] + +scanner = executeScannerInVirtualEnvironment(resps, [GMLAN_RMBAEnumerator]) + +assert scanner.scan_completed +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + + +assert len(tc.results_with_negative_response) > 350 +assert len(tc.results_with_positive_response) > 50 +assert len(tc.scanned_states) == 1 + +addrs = [t.req.memoryAddress for t in tc.results_with_positive_response] + +assert 0 in addrs +assert 0x10 in addrs +assert 0xf0 in addrs +assert 0x3000 in addrs +assert 0x3090 in addrs +assert 0xa100 in addrs +assert 0xa1f0 in addrs +assert 0xa200 in addrs +assert 0xa2f0 in addrs +assert 0xf000 in addrs +assert 0xf0f0 in addrs + +result = tc.show(dump=True) + +assert "RequestOutOfRange received " in result + ++ Cleanup + += Delete TestSockets + +cleanup_testsockets() diff --git a/test/contrib/automotive/gmlan_trace.candump b/test/contrib/automotive/gmlan_trace.candump deleted file mode 100755 index 04d88c6aabe..00000000000 --- a/test/contrib/automotive/gmlan_trace.candump +++ /dev/null @@ -1,1087 +0,0 @@ -(0.297000) vcan2 12A#122010FF00001080 -(2.713000) vcan2 1E1#00000400000000 -(7.647000) vcan2 0F1#B10000400000 -(10.240000) vcan2 135#0400000200000000 -(12.778000) vcan2 1F3#000000 -(15.153000) vcan2 451#000000000000 -(17.670000) vcan2 0F1#8D0000400000 -(20.251000) vcan2 137#0000000030000000 -(22.741000) vcan2 3F1#00FF5F0C00FF0100 -(27.657000) vcan2 0F1#990000400000 -(30.210000) vcan2 139#0000000000000000 -(32.718000) vcan2 1E1#000004000104F0 -(35.159000) vcan2 451#000000000000 -(37.676000) vcan2 0F1#A50000400000 -(40.144000) vcan2 1F3#404000 -(42.773000) vcan2 32A#0000000000000000 -(50.172000) vcan2 0F1#B10000400000 -(52.666000) vcan2 451#000000000000 -(57.674000) vcan2 0F1#8D0000400000 -(60.231000) vcan2 1E1#000004000208E0 -(67.675000) vcan2 0F1#990000400000 -(70.316000) vcan2 1F1#050A000008000070 -(72.627000) vcan2 1F3#808000 -(75.172000) vcan2 451#000000000000 -(77.694000) vcan2 0F1#A50000400000 -(80.201000) vcan2 3C9#0766000000000000 -(87.680000) vcan2 0F1#B10000400000 -(90.180000) vcan2 160#1C2A2E8703 -(92.751000) vcan2 1E1#00000400030CD0 -(95.236000) vcan2 451#000000000000 -(97.704000) vcan2 0F1#8D0000400000 -(100.345000) vcan2 12A#122010FF00001090 -(102.640000) vcan2 1F3#C0C000 -(107.691000) vcan2 0F1#990000400000 -(110.282000) vcan2 135#0400000200000000 -(112.861000) vcan2 451#000000000000 -(117.698000) vcan2 0F1#A50000400000 -(120.295000) vcan2 137#0000000030000000 -(122.762000) vcan2 1E1#00000400000000 -(127.700000) vcan2 0F1#B10000400000 -(130.256000) vcan2 139#0000000000000000 -(132.649000) vcan2 1F3#000000 -(135.202000) vcan2 451#000000000000 -(137.724000) vcan2 0F1#8D0000400000 -(140.325000) vcan2 32A#0000000000000000 -(147.710000) vcan2 0F1#990000400000 -(150.282000) vcan2 1E1#000004000104F0 -(152.717000) vcan2 451#000000000000 -(157.713000) vcan2 0F1#A50000400000 -(160.167000) vcan2 1F3#404000 -(167.718000) vcan2 0F1#B10000400000 -(170.359000) vcan2 1F1#050A000008000070 -(172.715000) vcan2 451#000000000000 -(177.723000) vcan2 0F1#8D0000400000 -(180.284000) vcan2 1E1#000004000208E0 -(182.747000) vcan2 3C9#0766000000000000 -(187.724000) vcan2 0F1#990000400000 -(190.227000) vcan2 160#1C2A2E8703 -(192.710000) vcan2 1F3#808000 -(195.262000) vcan2 451#000000000000 -(197.745000) vcan2 0F1#A50000400000 -(200.434000) vcan2 12A#122010FF000010A0 -(207.736000) vcan2 0F1#B10000400000 -(210.325000) vcan2 135#0400000200000000 -(212.973000) vcan2 1E1#00000400030CD0 -(215.240000) vcan2 451#000000000000 -(217.757000) vcan2 0F1#8D0000400000 -(220.337000) vcan2 137#0000000030000000 -(222.688000) vcan2 1F3#C0C000 -(227.744000) vcan2 0F1#990000400000 -(230.299000) vcan2 139#0000000000000000 -(232.737000) vcan2 451#000000000000 -(237.749000) vcan2 0F1#A50000400000 -(240.366000) vcan2 1E1#00000400000000 -(242.874000) vcan2 32A#0000000000000000 -(247.752000) vcan2 0F1#B10000400000 -(250.253000) vcan2 1F3#000000 -(252.755000) vcan2 451#000000000000 -(257.761000) vcan2 0F1#8D0000400000 -(267.758000) vcan2 0F1#990000400000 -(270.235000) vcan2 120#000160F100 -(272.822000) vcan2 1E1#000004000104F0 -(275.403000) vcan2 1F1#050A000008000070 -(277.777000) vcan2 0F1#A50000400000 -(280.218000) vcan2 1F3#404000 -(282.789000) vcan2 3C9#0766000000000000 -(285.355000) vcan2 3F1#00FF5F0C00FF0100 -(287.784000) vcan2 0F1#B10000400000 -(290.287000) vcan2 140#000A00 -(292.793000) vcan2 160#1C2A2E8703 -(295.316000) vcan2 451#000000000000 -(297.791000) vcan2 0F1#8D0000400000 -(300.442000) vcan2 12A#122010FF000010B0 -(302.836000) vcan2 1E1#000004000208E0 -(305.271000) vcan2 4C5#0000000000 -(307.792000) vcan2 0F1#990000400000 -(310.368000) vcan2 135#0400000200000000 -(312.911000) vcan2 1F3#808000 -(315.282000) vcan2 451#000000000000 -(317.798000) vcan2 0F1#A50000400000 -(320.381000) vcan2 137#0000000030000000 -(322.782000) vcan2 4E1#4843373933343934 -(325.284000) vcan2 4E9#112000000300 -(327.801000) vcan2 0F1#B10000400000 -(330.342000) vcan2 139#0000000000000000 -(332.845000) vcan2 1E1#00000400030CD0 -(335.287000) vcan2 451#000000000000 -(337.810000) vcan2 0F1#8D0000400000 -(340.313000) vcan2 1F3#C0C000 -(342.881000) vcan2 32A#0000000000000000 -(345.348000) vcan2 514#304C444436453733 -(347.809000) vcan2 0F1#990000400000 -(350.319000) vcan2 451#000000000000 -(352.862000) vcan2 52A#000037373737 -(355.297000) vcan2 530#00000000 -(357.810000) vcan2 0F1#A50000400000 -(360.368000) vcan2 1E1#00000400000000 -(367.801000) vcan2 0F1#B10000400000 -(370.447000) vcan2 1F1#050A000008000070 -(372.756000) vcan2 1F3#000000 -(375.307000) vcan2 451#000000000000 -(377.826000) vcan2 0F1#8D0000400000 -(380.325000) vcan2 3C9#0766000000000000 -(387.813000) vcan2 0F1#990000400000 -(390.330000) vcan2 160#1C2A2E8703 -(392.923000) vcan2 1E1#000004000104F0 -(395.407000) vcan2 451#000000000000 -(397.832000) vcan2 0F1#A50000400000 -(400.473000) vcan2 12A#122010FF00001080 -(402.769000) vcan2 1F3#404000 -(407.821000) vcan2 0F1#B10000400000 -(410.410000) vcan2 135#0400000200000000 -(412.991000) vcan2 451#000000000000 -(417.830000) vcan2 0F1#8D0000400000 -(420.423000) vcan2 137#0000000030000000 -(422.886000) vcan2 1E1#000004000208E0 -(427.831000) vcan2 0F1#990000400000 -(430.386000) vcan2 139#0000000000000000 -(432.781000) vcan2 1F3#808000 -(435.329000) vcan2 451#000000000000 -(437.848000) vcan2 0F1#A50000400000 -(440.451000) vcan2 32A#0000000000000000 -(447.837000) vcan2 0F1#B10000400000 -(450.406000) vcan2 1E1#00000400030CD0 -(452.861000) vcan2 451#000000000000 -(457.844000) vcan2 0F1#8D0000400000 -(460.297000) vcan2 1F3#C0C000 -(467.846000) vcan2 0F1#990000400000 -(470.486000) vcan2 1F1#050A000008000070 -(472.845000) vcan2 451#000000000000 -(477.851000) vcan2 0F1#A50000400000 -(480.421000) vcan2 1E1#00000400000000 -(482.876000) vcan2 3C9#0766000000000000 -(487.856000) vcan2 0F1#B10000400000 -(490.356000) vcan2 160#1C2A2E8703 -(492.839000) vcan2 1F3#000000 -(495.388000) vcan2 451#000000000000 -(497.878000) vcan2 0F1#8D0000400000 -(500.557000) vcan2 12A#122010FF00001090 -(507.864000) vcan2 0F1#990000400000 -(510.454000) vcan2 135#0400000200000000 -(513.101000) vcan2 1E1#000004000104F0 -(515.366000) vcan2 451#000000000000 -(517.884000) vcan2 0F1#A50000400000 -(520.467000) vcan2 137#0000000030000000 -(522.820000) vcan2 1F3#404000 -(525.461000) vcan2 3F1#00FF5F0C00FF0100 -(527.887000) vcan2 0F1#B10000400000 -(530.428000) vcan2 139#0000000000000000 -(532.871000) vcan2 451#000000000000 -(537.880000) vcan2 0F1#8D0000400000 -(540.487000) vcan2 1E1#000004000208E0 -(542.986000) vcan2 32A#0000000000000000 -(547.879000) vcan2 0F1#990000400000 -(550.352000) vcan2 1F3#808000 -(552.889000) vcan2 451#000000000000 -(557.886000) vcan2 0F1#A50000400000 -(567.890000) vcan2 0F1#B10000400000 -(570.451000) vcan2 1E1#00000400030CD0 -(573.035000) vcan2 1F1#050A000008000070 -(575.390000) vcan2 451#000000000000 -(577.913000) vcan2 0F1#8D0000400000 -(580.346000) vcan2 1F3#C0C000 -(582.916000) vcan2 3C9#0766000000000000 -(587.900000) vcan2 0F1#990000400000 -(590.399000) vcan2 160#1C2A2E8703 -(592.903000) vcan2 451#000000000000 -(597.905000) vcan2 0F1#A50000400000 -(600.546000) vcan2 12A#122010FF000010A0 -(602.971000) vcan2 1E1#00000400000000 -(607.908000) vcan2 0F1#B10000400000 -(610.497000) vcan2 135#0400000200000000 -(613.037000) vcan2 1F3#000000 -(615.408000) vcan2 451#000000000000 -(617.929000) vcan2 0F1#8D0000400000 -(620.510000) vcan2 137#0000000030000000 -(627.914000) vcan2 0F1#990000400000 -(630.471000) vcan2 139#0000000000000000 -(632.976000) vcan2 1E1#000004000104F0 -(635.419000) vcan2 451#000000000000 -(637.933000) vcan2 0F1#A50000400000 -(640.442000) vcan2 1F3#404000 -(642.993000) vcan2 32A#0000000000000000 -(647.924000) vcan2 0F1#B10000400000 -(650.479000) vcan2 451#000000000000 -(657.935000) vcan2 0F1#8D0000400000 -(660.492000) vcan2 1E1#000004000208E0 -(667.933000) vcan2 0F1#990000400000 -(670.572000) vcan2 1F1#050A000008000070 -(672.882000) vcan2 1F3#808000 -(675.435000) vcan2 451#000000000000 -(677.952000) vcan2 0F1#A50000400000 -(680.456000) vcan2 3C9#0766000000000000 -(687.845000) vcan2 0F1#B10000400000 -(690.354000) vcan2 160#1C2A2E8703 -(692.944000) vcan2 1E1#00000400030CD0 -(695.431000) vcan2 451#000000000000 -(697.866000) vcan2 0F1#8D0000400000 -(700.508000) vcan2 12A#122010FF000010B0 -(702.801000) vcan2 1F3#C0C000 -(707.853000) vcan2 0F1#990000400000 -(710.442000) vcan2 135#0400000200000000 -(713.024000) vcan2 451#000000000000 -(717.854000) vcan2 0F1#A50000400000 -(720.455000) vcan2 137#0000000030000000 -(722.925000) vcan2 1E1#00000400000000 -(727.859000) vcan2 0F1#B10000400000 -(730.416000) vcan2 139#0000000000000000 -(732.814000) vcan2 1F3#000000 -(735.363000) vcan2 451#000000000000 -(737.884000) vcan2 0F1#8D0000400000 -(740.472000) vcan2 32A#0000000000000000 -(747.871000) vcan2 0F1#990000400000 -(750.462000) vcan2 1E1#000004000104F0 -(752.896000) vcan2 451#000000000000 -(757.874000) vcan2 0F1#A50000400000 -(760.325000) vcan2 1F3#404000 -(767.881000) vcan2 0F1#B10000400000 -(770.516000) vcan2 1F1#050A000008000070 -(772.967000) vcan2 3F1#00FF5F0C00FF0100 -(775.376000) vcan2 451#000000000000 -(777.902000) vcan2 0F1#8D0000400000 -(780.445000) vcan2 1E1#000004000208E0 -(782.904000) vcan2 3C9#0766000000000000 -(785.374000) vcan2 4C5#0000000000 -(787.901000) vcan2 0F1#990000400000 -(790.398000) vcan2 160#1C2A2E8703 -(792.864000) vcan2 1F3#808000 -(795.413000) vcan2 451#000000000000 -(797.906000) vcan2 0F1#A50000400000 -(800.582000) vcan2 12A#122010FF00001080 -(807.893000) vcan2 0F1#B10000400000 -(810.486000) vcan2 135#0400000200000000 -(813.130000) vcan2 1E1#00000400030CD0 -(815.401000) vcan2 451#000000000000 -(817.916000) vcan2 0F1#8D0000400000 -(820.501000) vcan2 137#0000000030000000 -(822.852000) vcan2 1F3#C0C000 -(827.903000) vcan2 0F1#990000400000 -(830.456000) vcan2 139#0000000000000000 -(832.901000) vcan2 451#000000000000 -(837.910000) vcan2 0F1#A50000400000 -(840.521000) vcan2 1E1#00000400000000 -(843.016000) vcan2 32A#0000000000000000 -(847.913000) vcan2 0F1#B10000400000 -(850.386000) vcan2 1F3#000000 -(852.913000) vcan2 451#000000000000 -(857.922000) vcan2 0F1#8D0000400000 -(867.922000) vcan2 0F1#990000400000 -(870.484000) vcan2 1E1#000004000104F0 -(873.063000) vcan2 1F1#050A000008000070 -(875.416000) vcan2 451#000000000000 -(877.939000) vcan2 0F1#A50000400000 -(880.379000) vcan2 1F3#404000 -(882.948000) vcan2 3C9#0766000000000000 -(887.930000) vcan2 0F1#B10000400000 -(890.404000) vcan2 160#1C2A2E8703 -(893.001000) vcan2 451#000000000000 -(897.935000) vcan2 0F1#8D0000400000 -(900.573000) vcan2 12A#122010FF00001090 -(902.998000) vcan2 1E1#000004000208E0 -(907.936000) vcan2 0F1#990000400000 -(910.530000) vcan2 135#0400000200000000 -(913.067000) vcan2 1F3#808000 -(915.442000) vcan2 451#000000000000 -(917.958000) vcan2 0F1#A50000400000 -(920.541000) vcan2 137#0000000030000000 -(927.948000) vcan2 0F1#B10000400000 -(930.500000) vcan2 139#0000000000000000 -(933.007000) vcan2 1E1#00000400030CD0 -(935.450000) vcan2 451#000000000000 -(937.969000) vcan2 0F1#8D0000400000 -(940.467000) vcan2 1F3#C0C000 -(943.036000) vcan2 32A#0000000000000000 -(947.956000) vcan2 0F1#990000400000 -(950.546000) vcan2 451#000000000000 -(957.961000) vcan2 0F1#A50000400000 -(960.529000) vcan2 1E1#00000400000000 -(967.966000) vcan2 0F1#B10000400000 -(970.607000) vcan2 1F1#050A000008000070 -(972.916000) vcan2 1F3#000000 -(975.464000) vcan2 451#000000000000 -(977.987000) vcan2 0F1#8D0000400000 -(980.490000) vcan2 3C9#0766000000000000 -(987.970000) vcan2 0F1#990000400000 -(990.471000) vcan2 160#1C2A2E8703 -(993.044000) vcan2 1E1#000004000104F0 -(995.528000) vcan2 451#000000000000 -(997.991000) vcan2 0F1#A50000400000 -(1000.634000) vcan2 12A#122010FF000010A0 -(1002.932000) vcan2 1F3#404000 -(1007.982000) vcan2 0F1#B10000400000 -(1010.571000) vcan2 135#0400000200000000 -(1013.153000) vcan2 451#000000000000 -(1017.991000) vcan2 0F1#8D0000400000 -(1020.584000) vcan2 137#0000000030000000 -(1023.048000) vcan2 1E1#000004000208E0 -(1025.579000) vcan2 3F1#00FF5F0C00FF0100 -(1028.004000) vcan2 0F1#990000400000 -(1030.546000) vcan2 139#0000000000000000 -(1032.939000) vcan2 1F3#808000 -(1035.492000) vcan2 451#000000000000 -(1038.009000) vcan2 0F1#A50000400000 -(1040.601000) vcan2 32A#0000000000000000 -(1048.000000) vcan2 0F1#B10000400000 -(1050.563000) vcan2 1E1#00000400030CD0 -(1052.999000) vcan2 451#000000000000 -(1058.007000) vcan2 0F1#8D0000400000 -(1060.456000) vcan2 1F3#C0C000 -(1068.008000) vcan2 0F1#990000400000 -(1070.651000) vcan2 1F1#050A000008000070 -(1073.006000) vcan2 451#000000000000 -(1078.009000) vcan2 0F1#A50000400000 -(1080.580000) vcan2 1E1#00000400000000 -(1083.037000) vcan2 3C9#0766000000000000 -(1088.014000) vcan2 0F1#B10000400000 -(1090.523000) vcan2 160#1C2A2E8703 -(1093.010000) vcan2 1F3#000000 -(1095.576000) vcan2 451#000000000000 -(1098.039000) vcan2 0F1#8D0000400000 -(1100.702000) vcan2 12A#122010FF000010B0 -(1108.026000) vcan2 0F1#990000400000 -(1110.615000) vcan2 135#0400000200000000 -(1113.264000) vcan2 1E1#000004000104F0 -(1115.529000) vcan2 451#000000000000 -(1118.043000) vcan2 0F1#A50000400000 -(1120.628000) vcan2 137#0000000030000000 -(1122.977000) vcan2 1F3#404000 -(1128.035000) vcan2 0F1#B10000400000 -(1130.591000) vcan2 139#0000000000000000 -(1133.028000) vcan2 451#000000000000 -(1138.044000) vcan2 0F1#8D0000400000 -(1140.656000) vcan2 1E1#000004000208E0 -(1143.099000) vcan2 32A#0000000000000000 -(1148.043000) vcan2 0F1#990000400000 -(1150.579000) vcan2 1F3#808000 -(1153.058000) vcan2 451#000000000000 -(1158.048000) vcan2 0F1#A50000400000 -(1168.049000) vcan2 0F1#B10000400000 -(1170.612000) vcan2 1E1#00000400030CD0 -(1173.197000) vcan2 1F1#050A000008000070 -(1175.551000) vcan2 451#000000000000 -(1178.072000) vcan2 0F1#8D0000400000 -(1180.509000) vcan2 1F3#C0C000 -(1183.081000) vcan2 3C9#0766000000000000 -(1188.059000) vcan2 0F1#990000400000 -(1190.562000) vcan2 160#1C2A2E8703 -(1193.088000) vcan2 451#000000000000 -(1198.064000) vcan2 0F1#A50000400000 -(1200.723000) vcan2 12A#122010FF00001080 -(1203.132000) vcan2 1E1#00000400000000 -(1208.069000) vcan2 0F1#B10000400000 -(1210.658000) vcan2 135#0400000200000000 -(1213.199000) vcan2 1F3#000000 -(1215.573000) vcan2 451#000000000000 -(1218.092000) vcan2 0F1#8D0000400000 -(1220.671000) vcan2 137#0000000030000000 -(1228.079000) vcan2 0F1#990000400000 -(1230.632000) vcan2 139#0000000000000000 -(1233.137000) vcan2 1E1#000004000104F0 -(1235.577000) vcan2 451#000000000000 -(1238.096000) vcan2 0F1#A50000400000 -(1240.595000) vcan2 1F3#404000 -(1243.173000) vcan2 32A#0000000000000000 -(1248.087000) vcan2 0F1#B10000400000 -(1250.626000) vcan2 451#000000000000 -(1258.092000) vcan2 0F1#8D0000400000 -(1260.653000) vcan2 1E1#000004000208E0 -(1268.092000) vcan2 0F1#990000400000 -(1270.736000) vcan2 1F1#050A000008000070 -(1273.047000) vcan2 1F3#808000 -(1275.686000) vcan2 3F1#00FF5F0C00FF0100 -(1278.113000) vcan2 0F1#A50000400000 -(1280.617000) vcan2 3C9#0766000000000000 -(1283.096000) vcan2 451#000000000000 -(1285.591000) vcan2 4C5#0000000000 -(1288.117000) vcan2 0F1#B10000400000 -(1290.646000) vcan2 140#000A00 -(1293.077000) vcan2 160#1C2A2E8703 -(1295.739000) vcan2 1E1#00000400030CD0 -(1298.130000) vcan2 0F1#8D0000400000 -(1300.817000) vcan2 12A#122010FF00001090 -(1303.058000) vcan2 1F3#C0C000 -(1305.610000) vcan2 451#000000000000 -(1308.127000) vcan2 0F1#990000400000 -(1310.700000) vcan2 135#0400000200000000 -(1313.286000) vcan2 4E9#112000000300 -(1315.631000) vcan2 52A#000037373737 -(1318.130000) vcan2 0F1#A50000400000 -(1320.714000) vcan2 137#0000000030000000 -(1323.183000) vcan2 1E1#00000400000000 -(1325.616000) vcan2 451#000000000000 -(1328.134000) vcan2 0F1#B10000400000 -(1330.677000) vcan2 139#0000000000000000 -(1333.072000) vcan2 1F3#000000 -(1335.623000) vcan2 4E1#4843373933343934 -(1338.143000) vcan2 0F1#8D0000400000 -(1340.738000) vcan2 32A#0000000000000000 -(1343.157000) vcan2 451#000000000000 -(1345.663000) vcan2 514#304C444436453733 -(1348.140000) vcan2 0F1#990000400000 -(1350.699000) vcan2 1E1#000004000104F0 -(1353.136000) vcan2 451#000000000000 -(1355.630000) vcan2 530#00000000 -(1358.145000) vcan2 0F1#A50000400000 -(1360.586000) vcan2 1F3#404000 -(1368.136000) vcan2 0F1#B10000400000 -(1370.775000) vcan2 1F1#050A000008000070 -(1373.134000) vcan2 451#000000000000 -(1378.147000) vcan2 0F1#8D0000400000 -(1380.704000) vcan2 1E1#000004000208E0 -(1383.165000) vcan2 3C9#0766000000000000 -(1388.146000) vcan2 0F1#990000400000 -(1390.663000) vcan2 160#1C2A2E8703 -(1393.146000) vcan2 1F3#808000 -(1395.695000) vcan2 451#000000000000 -(1398.165000) vcan2 0F1#A50000400000 -(1400.854000) vcan2 12A#122010FF000010A0 -(1408.155000) vcan2 0F1#B10000400000 -(1410.747000) vcan2 135#0400000200000000 -(1413.388000) vcan2 1E1#00000400030CD0 -(1415.655000) vcan2 451#000000000000 -(1418.177000) vcan2 0F1#8D0000400000 -(1420.758000) vcan2 137#0000000030000000 -(1423.111000) vcan2 1F3#C0C000 -(1428.164000) vcan2 0F1#990000400000 -(1430.719000) vcan2 139#0000000000000000 -(1433.160000) vcan2 451#000000000000 -(1438.167000) vcan2 0F1#A50000400000 -(1440.790000) vcan2 1E1#00000400000000 -(1443.245000) vcan2 32A#0000000000000000 -(1448.170000) vcan2 0F1#B10000400000 -(1450.663000) vcan2 1F3#000000 -(1453.188000) vcan2 451#000000000000 -(1458.178000) vcan2 0F1#8D0000400000 -(1468.181000) vcan2 0F1#990000400000 -(1470.742000) vcan2 1E1#000004000104F0 -(1473.326000) vcan2 1F1#050A000008000070 -(1475.679000) vcan2 451#000000000000 -(1478.198000) vcan2 0F1#A50000400000 -(1480.637000) vcan2 1F3#404000 -(1483.205000) vcan2 3C9#0766000000000000 -(1488.189000) vcan2 0F1#B10000400000 -(1490.692000) vcan2 160#1C2A2E8703 -(1493.216000) vcan2 451#000000000000 -(1498.198000) vcan2 0F1#8D0000400000 -(1500.845000) vcan2 12A#122010FF000010B0 -(1503.256000) vcan2 1E1#000004000208E0 -(1508.199000) vcan2 0F1#990000400000 -(1510.788000) vcan2 135#0400000200000000 -(1513.326000) vcan2 1F3#808000 -(1515.699000) vcan2 451#000000000000 -(1518.216000) vcan2 0F1#A50000400000 -(1520.801000) vcan2 137#0000000030000000 -(1523.291000) vcan2 3F1#00FF5F0C00FF0100 -(1528.203000) vcan2 0F1#B10000400000 -(1530.762000) vcan2 139#0000000000000000 -(1533.266000) vcan2 1E1#00000400030CD0 -(1535.709000) vcan2 451#000000000000 -(1538.228000) vcan2 0F1#8D0000400000 -(1540.716000) vcan2 1F3#C0C000 -(1543.283000) vcan2 32A#0000000000000000 -(1548.213000) vcan2 0F1#990000400000 -(1550.717000) vcan2 451#000000000000 -(1558.220000) vcan2 0F1#A50000400000 -(1560.787000) vcan2 1E1#00000400000000 -(1568.225000) vcan2 0F1#B10000400000 -(1570.862000) vcan2 1F1#050A000008000070 -(1573.173000) vcan2 1F3#000000 -(1575.727000) vcan2 451#000000000000 -(1578.246000) vcan2 0F1#8D0000400000 -(1580.747000) vcan2 3C9#0766000000000000 -(1588.233000) vcan2 0F1#990000400000 -(1590.736000) vcan2 160#1C2A2E8703 -(1593.307000) vcan2 1E1#000004000104F0 -(1595.790000) vcan2 451#000000000000 -(1598.250000) vcan2 0F1#A50000400000 -(1600.901000) vcan2 12A#122010FF00001080 -(1603.190000) vcan2 1F3#404000 -(1608.241000) vcan2 0F1#B10000400000 -(1610.830000) vcan2 135#0400000200000000 -(1613.411000) vcan2 451#000000000000 -(1618.247000) vcan2 0F1#8D0000400000 -(1620.843000) vcan2 137#0000000030000000 -(1623.306000) vcan2 1E1#000004000208E0 -(1628.246000) vcan2 0F1#990000400000 -(1630.806000) vcan2 139#0000000000000000 -(1633.201000) vcan2 1F3#808000 -(1635.752000) vcan2 451#000000000000 -(1638.268000) vcan2 0F1#A50000400000 -(1640.871000) vcan2 32A#0000000000000000 -(1648.258000) vcan2 0F1#B10000400000 -(1650.842000) vcan2 1E1#00000400030CD0 -(1653.275000) vcan2 451#000000000000 -(1658.267000) vcan2 0F1#8D0000400000 -(1660.715000) vcan2 1F3#C0C000 -(1668.268000) vcan2 0F1#990000400000 -(1670.905000) vcan2 1F1#050A000008000070 -(1673.261000) vcan2 451#000000000000 -(1678.271000) vcan2 0F1#A50000400000 -(1680.840000) vcan2 1E1#00000400000000 -(1683.292000) vcan2 3C9#0766000000000000 -(1688.376000) vcan2 0F1#B10000400000 -(1690.883000) vcan2 160#1C2A2E8703 -(1693.367000) vcan2 1F3#000000 -(1695.918000) vcan2 451#000000000000 -(1698.397000) vcan2 0F1#8D0000400000 -(1701.083000) vcan2 12A#122010FF00001090 -(1708.382000) vcan2 0F1#990000400000 -(1710.973000) vcan2 135#0400000200000000 -(1713.621000) vcan2 1E1#000004000104F0 -(1715.888000) vcan2 451#000000000000 -(1718.401000) vcan2 0F1#A50000400000 -(1720.990000) vcan2 137#0000000030000000 -(1723.341000) vcan2 1F3#404000 -(1728.392000) vcan2 0F1#B10000400000 -(1730.945000) vcan2 139#0000000000000000 -(1733.391000) vcan2 451#000000000000 -(1738.403000) vcan2 0F1#8D0000400000 -(1741.008000) vcan2 1E1#000004000208E0 -(1743.509000) vcan2 32A#0000000000000000 -(1748.402000) vcan2 0F1#990000400000 -(1750.873000) vcan2 1F3#808000 -(1753.404000) vcan2 451#000000000000 -(1758.407000) vcan2 0F1#A50000400000 -(1768.411000) vcan2 0F1#B10000400000 -(1770.971000) vcan2 1E1#00000400030CD0 -(1773.552000) vcan2 1F1#050A000008000070 -(1775.999000) vcan2 3F1#00FF5F0C00FF0100 -(1778.432000) vcan2 0F1#8D0000400000 -(1780.868000) vcan2 1F3#C0C000 -(1783.437000) vcan2 3C9#0766000000000000 -(1785.910000) vcan2 451#000000000000 -(1788.432000) vcan2 0F1#990000400000 -(1790.923000) vcan2 160#1C2A2E8703 -(1793.426000) vcan2 4C5#0000000000 -(1798.421000) vcan2 0F1#A50000400000 -(1801.080000) vcan2 12A#122010FF000010A0 -(1803.491000) vcan2 1E1#00000400000000 -(1805.927000) vcan2 451#000000000000 -(1808.440000) vcan2 0F1#B10000400000 -(1811.019000) vcan2 135#0400000200000000 -(1813.556000) vcan2 1F3#000000 -(1818.435000) vcan2 0F1#8D0000400000 -(1821.030000) vcan2 137#0000000030000000 -(1823.429000) vcan2 451#000000000000 -(1828.436000) vcan2 0F1#990000400000 -(1830.989000) vcan2 139#0000000000000000 -(1833.496000) vcan2 1E1#000004000104F0 -(1835.940000) vcan2 451#000000000000 -(1838.455000) vcan2 0F1#A50000400000 -(1840.952000) vcan2 1F3#404000 -(1843.520000) vcan2 32A#0000000000000000 -(1848.446000) vcan2 0F1#B10000400000 -(1850.959000) vcan2 451#000000000000 -(1858.455000) vcan2 0F1#8D0000400000 -(1861.012000) vcan2 1E1#000004000208E0 -(1868.453000) vcan2 0F1#990000400000 -(1871.095000) vcan2 1F1#050A000008000070 -(1873.404000) vcan2 1F3#808000 -(1875.951000) vcan2 451#000000000000 -(1878.471000) vcan2 0F1#A50000400000 -(1880.978000) vcan2 3C9#0766000000000000 -(1888.459000) vcan2 0F1#B10000400000 -(1890.969000) vcan2 160#1C2A2E8703 -(1893.558000) vcan2 1E1#00000400030CD0 -(1896.043000) vcan2 451#000000000000 -(1898.482000) vcan2 0F1#8D0000400000 -(1901.156000) vcan2 12A#122010FF000010B0 -(1903.433000) vcan2 1F3#C0C000 -(1908.470000) vcan2 0F1#990000400000 -(1911.059000) vcan2 135#0400000200000000 -(1913.640000) vcan2 451#000000000000 -(1918.475000) vcan2 0F1#A50000400000 -(1921.074000) vcan2 137#0000000030000000 -(1923.541000) vcan2 1E1#00000400000000 -(1928.480000) vcan2 0F1#B10000400000 -(1931.035000) vcan2 139#0000000000000000 -(1933.426000) vcan2 1F3#000000 -(1935.980000) vcan2 451#000000000000 -(1938.501000) vcan2 0F1#8D0000400000 -(1941.064000) vcan2 32A#0000000000000000 -(1948.490000) vcan2 0F1#990000400000 -(1951.139000) vcan2 1E1#000004000104F0 -(1953.496000) vcan2 451#000000000000 -(1958.493000) vcan2 0F1#A50000400000 -(1960.946000) vcan2 1F3#404000 -(1968.497000) vcan2 0F1#B10000400000 -(1971.139000) vcan2 1F1#050A000008000070 -(1973.494000) vcan2 451#000000000000 -(1978.502000) vcan2 0F1#8D0000400000 -(1979.724000) vcan2 101#FE021002FE021002 -(1981.067000) vcan2 1E1#000004000208E0 -(1983.526000) vcan2 3C9#0766000000000000 -(1988.502000) vcan2 0F1#990000400000 -(1990.982000) vcan2 160#1C2A2E8703 -(1993.485000) vcan2 1F3#808000 -(1996.114000) vcan2 451#000000000000 -(1998.523000) vcan2 0F1#A50000400000 -(2001.229000) vcan2 12A#122010FF00001080 -(2003.522000) vcan2 641#01501A3186418231 -(2008.515000) vcan2 0F1#B10000400000 -(2011.104000) vcan2 135#0400000200000000 -(2013.751000) vcan2 1E1#00000400030CD0 -(2016.019000) vcan2 451#000000000000 -(2018.536000) vcan2 0F1#8D0000400000 -(2021.117000) vcan2 137#0000000030000000 -(2023.467000) vcan2 1F3#C0C000 -(2026.110000) vcan2 3F1#00FF5F0C00FF0100 -(2028.537000) vcan2 0F1#990000400000 -(2031.078000) vcan2 139#0000000000000000 -(2033.518000) vcan2 451#000000000000 -(2038.528000) vcan2 0F1#A50000400000 -(2041.141000) vcan2 1E1#00000400000000 -(2043.639000) vcan2 32A#0000000000000000 -(2048.531000) vcan2 0F1#B10000400000 -(2051.010000) vcan2 1F3#000000 -(2053.536000) vcan2 451#000000000000 -(2058.540000) vcan2 0F1#8D0000400000 -(2060.444000) vcan2 101#FE021002FE021002 -(2068.538000) vcan2 0F1#990000400000 -(2071.103000) vcan2 1E1#000004000104F0 -(2073.684000) vcan2 1F1#050A000008000070 -(2076.037000) vcan2 451#000000000000 -(2078.557000) vcan2 0F1#A50000400000 -(2080.998000) vcan2 1F3#404000 -(2083.567000) vcan2 3C9#0766000000000000 -(2086.057000) vcan2 641#01501A3186418231 -(2088.568000) vcan2 0F1#B10000400000 -(2091.021000) vcan2 160#1C2A2E8703 -(2093.569000) vcan2 451#000000000000 -(2098.557000) vcan2 0F1#8D0000400000 -(2101.208000) vcan2 12A#122010FF00001090 -(2103.615000) vcan2 1E1#000004000208E0 -(2108.558000) vcan2 0F1#990000400000 -(2111.147000) vcan2 135#0400000200000000 -(2113.688000) vcan2 1F3#808000 -(2116.062000) vcan2 451#000000000000 -(2118.575000) vcan2 0F1#A50000400000 -(2121.160000) vcan2 137#0000000030000000 -(2128.566000) vcan2 0F1#B10000400000 -(2131.121000) vcan2 139#0000000000000000 -(2133.624000) vcan2 1E1#00000400030CD0 -(2136.064000) vcan2 451#000000000000 -(2138.589000) vcan2 0F1#8D0000400000 -(2141.062000) vcan2 1F3#C0C000 -(2143.298000) vcan2 101#FE012803FE012800 -(2143.658000) vcan2 32A#0000000000000000 -(2148.575000) vcan2 0F1#990000400000 -(2151.090000) vcan2 641#01681A3186418231 -(2207.192000) vcan2 101#FE01A203FE01A200 -(2221.098000) vcan2 641#02E2003186418231 -(2278.067000) vcan2 101#FE02A501FE02A501 -(2291.161000) vcan2 641#01E5003186418231 -(2347.941000) vcan2 101#FE02A503FE02A503 -(2605.377000) vcan2 241#0227013186418253 -(2611.267000) vcan2 641#046701D6C6418231 -(2625.512000) vcan2 101#FE013E4000604D40 -(2681.230000) vcan2 241#0427021238418253 -(2691.229000) vcan2 641#026702D6C6418231 -(2697.126000) vcan2 241#021AB01230000000 -(2701.230000) vcan2 641#035AB040C0000000 -(2707.008000) vcan2 241#021AC04030000000 -(2711.254000) vcan2 641#065AC000C0000000 -(2716.999000) vcan2 241#021ACB00C0000000 -(2721.211000) vcan2 641#065ACB00C0000000 -(2727.009000) vcan2 241#021ACC00C0000000 -(2731.221000) vcan2 641#065ACC00C0000000 -(2737.004000) vcan2 241#021AD000C0000000 -(2741.228000) vcan2 641#045AD04740000000 -(2803.759000) vcan2 241#0634000000139453 -(2811.253000) vcan2 641#037F3478446FC831 -(3366.134000) vcan2 641#0174 -(3422.745000) vcan2 101#FE013E4000604D40 -(3426.673000) vcan2 241#10333600FEDF15CD -(3426.871000) vcan2 641#300001 -(3427.103000) vcan2 241#2106000000000000 -(3431.863000) vcan2 241#22F7000000000000 -(3436.861000) vcan2 241#2323000000000000 -(3441.860000) vcan2 241#2434000000000000 -(3446.862000) vcan2 241#259D000000000000 -(3451.857000) vcan2 241#2636000000000000 -(3456.861000) vcan2 241#2764000000000000 -(3457.041000) vcan2 641#0176 -(3465.541000) vcan2 101#FE013E4000604D40 -(3469.528000) vcan2 241#10333600FEDF1438 -(3469.725000) vcan2 641#300001 -(3469.954000) vcan2 241#2164000000000000 -(3474.854000) vcan2 241#223D000000000000 -(3479.860000) vcan2 241#23F6000000000000 -(3484.857000) vcan2 241#2463000000000000 -(3489.859000) vcan2 241#25CA000000000000 -(3494.858000) vcan2 241#2677000000000000 -(3499.864000) vcan2 241#2738000000000000 -(3500.044000) vcan2 641#0176 -(3508.546000) vcan2 101#FE013E4000604D40 -(3512.531000) vcan2 241#10333600FEDF15A0 -(3512.728000) vcan2 641#300001 -(3512.962000) vcan2 241#2167000000000000 -(3517.859000) vcan2 241#2256000000000000 -(3522.855000) vcan2 241#2394000000000000 -(3527.858000) vcan2 241#248F000000000000 -(3532.862000) vcan2 241#25C5000000000000 -(3537.866000) vcan2 241#26E2000000000000 -(3542.863000) vcan2 241#2791000000000000 -(3543.044000) vcan2 641#0176 -(3551.415000) vcan2 101#FE013E4000604D40 -(3555.419000) vcan2 241#10333600FEDF12FD -(3555.617000) vcan2 641#300001 -(3555.845000) vcan2 241#21E0000000000000 -(3560.854000) vcan2 241#2213000000000000 -(3565.854000) vcan2 241#235B000000000000 -(3570.854000) vcan2 241#2484000000000000 -(3575.859000) vcan2 241#25D0000000000000 -(3580.857000) vcan2 241#260F000000000000 -(3585.856000) vcan2 241#27CF000000000000 -(3586.037000) vcan2 641#0176 -(3594.290000) vcan2 101#FE013E4000604D40 -(3598.280000) vcan2 241#10333600FEDF110E -(3598.478000) vcan2 641#300001 -(3598.716000) vcan2 241#2108000000000000 -(3603.857000) vcan2 241#2267000000000000 -(3608.861000) vcan2 241#2308000000000000 -(3613.865000) vcan2 241#2400000000000000 -(3618.860000) vcan2 241#2504000000000000 -(3623.862000) vcan2 241#2647000000000000 -(3628.860000) vcan2 241#2780000000000000 -(3629.038000) vcan2 641#0176 -(3637.295000) vcan2 101#FE013E4000604D40 -(3641.287000) vcan2 241#10333600FEDF14BF -(3641.483000) vcan2 641#300001 -(3641.713000) vcan2 241#21A6000000000000 -(3646.853000) vcan2 241#2238000000000000 -(3651.852000) vcan2 241#2320000000000000 -(3656.856000) vcan2 241#2410000000000000 -(3661.851000) vcan2 241#2566000000000000 -(3666.849000) vcan2 241#26C8000000000000 -(3671.853000) vcan2 241#2713000000000000 -(3672.031000) vcan2 641#0176 -(3680.174000) vcan2 101#FE013E4000604D40 -(3684.167000) vcan2 241#10333600FEDF12D0 -(3684.361000) vcan2 641#300001 -(3684.590000) vcan2 241#2139000000000000 -(3688.860000) vcan2 241#22AB000000000000 -(3693.859000) vcan2 241#2390000000000000 -(3698.861000) vcan2 241#24EB000000000000 -(3703.861000) vcan2 241#25B7000000000000 -(3708.860000) vcan2 241#26C9000000000000 -(3713.860000) vcan2 241#2731000000000000 -(3714.038000) vcan2 641#0176 -(3722.181000) vcan2 101#FE013E4000604D40 -(3726.173000) vcan2 241#10333600FEDF113B -(3726.371000) vcan2 641#300001 -(3726.609000) vcan2 241#2147000000000000 -(3730.861000) vcan2 241#2280000000000000 -(3735.867000) vcan2 241#2300000000000000 -(3740.858000) vcan2 241#2434000000000000 -(3745.866000) vcan2 241#2505000000000000 -(3750.867000) vcan2 241#2674000000000000 -(3755.863000) vcan2 241#2706000000000000 -(3756.041000) vcan2 641#0176 -(3765.049000) vcan2 101#FE013E4000604D40 -(3769.048000) vcan2 241#10333600FEDF11EF -(3769.247000) vcan2 641#300001 -(3769.476000) vcan2 241#2196000000000000 -(3773.854000) vcan2 241#2249000000000000 -(3778.854000) vcan2 241#2324000000000000 -(3783.855000) vcan2 241#24FC000000000000 -(3788.859000) vcan2 241#255B000000000000 -(3793.853000) vcan2 241#26E3000000000000 -(3798.856000) vcan2 241#2712000000000000 -(3799.039000) vcan2 641#0176 -(3807.976000) vcan2 101#FE013E4000604D40 -(3811.973000) vcan2 241#10333600FEDF1276 -(3812.170000) vcan2 641#300001 -(3812.398000) vcan2 241#214F000000000000 -(3816.859000) vcan2 241#220D000000000000 -(3821.859000) vcan2 241#2344000000000000 -(3826.854000) vcan2 241#2466000000000000 -(3831.856000) vcan2 241#25AA000000000000 -(3836.858000) vcan2 241#2694000000000000 -(3841.853000) vcan2 241#2722000000000000 -(3842.032000) vcan2 641#0176 -(3850.799000) vcan2 101#FE013E4000604D40 -(3854.793000) vcan2 241#10333600FEDF10E1 -(3854.989000) vcan2 641#300001 -(3855.223000) vcan2 241#21FF000000000000 -(3859.860000) vcan2 241#2205000000000000 -(3864.856000) vcan2 241#2359000000000000 -(3869.850000) vcan2 241#242C000000000000 -(3874.861000) vcan2 241#2500000000000000 -(3879.859000) vcan2 241#26C8000000000000 -(3884.858000) vcan2 241#2700000000000000 -(3885.037000) vcan2 641#0176 -(3893.804000) vcan2 101#FE013E4000604D40 -(3897.682000) vcan2 241#10333600FEDF12A3 -(3897.880000) vcan2 641#300001 -(3898.114000) vcan2 241#218F000000000000 -(3902.850000) vcan2 241#2298000000000000 -(3907.847000) vcan2 241#2342000000000000 -(3912.855000) vcan2 241#24BB000000000000 -(3917.850000) vcan2 241#252A000000000000 -(3922.854000) vcan2 241#263F000000000000 -(3927.850000) vcan2 241#271E000000000000 -(3928.030000) vcan2 641#0176 -(3936.681000) vcan2 101#FE013E4000604D40 -(3940.673000) vcan2 241#10333600FEDF132A -(3940.871000) vcan2 641#300001 -(3941.101000) vcan2 241#216B000000000000 -(3945.853000) vcan2 241#22B5000000000000 -(3950.848000) vcan2 241#2359000000000000 -(3955.850000) vcan2 241#2488000000000000 -(3960.854000) vcan2 241#2530000000000000 -(3965.851000) vcan2 241#2674000000000000 -(3970.855000) vcan2 241#27EB000000000000 -(3971.035000) vcan2 641#0176 -(3979.552000) vcan2 101#FE013E4000604D40 -(3983.536000) vcan2 241#101F3600FEDF1627 -(3983.734000) vcan2 641#300001 -(3983.960000) vcan2 241#214D5A4C2B2C2D36 -(3988.864000) vcan2 241#223035FFFFFFFFFF -(3993.871000) vcan2 241#23FFFFFFFFFFFFFF -(3998.855000) vcan2 241#24FFFFFFFF8A2977 -(3999.035000) vcan2 641#0176 -(4007.559000) vcan2 101#FE013E4000604D40 -(4011.544000) vcan2 241#10333600FEDF102D -(4011.741000) vcan2 641#300001 -(4011.978000) vcan2 241#2100000000000000 -(4016.856000) vcan2 241#2280000000000000 -(4021.856000) vcan2 241#2305000000000000 -(4026.851000) vcan2 241#247F000000000000 -(4031.857000) vcan2 241#25FD000000000000 -(4036.853000) vcan2 241#26B5000000000000 -(4041.856000) vcan2 241#27FE000000000000 -(4042.036000) vcan2 641#0176 -(4050.438000) vcan2 101#FE013E4000604D40 -(4054.441000) vcan2 241#10333600FEDF15FA -(4054.638000) vcan2 641#300001 -(4054.864000) vcan2 241#21B8000000000000 -(4059.845000) vcan2 241#22A6000000000000 -(4064.847000) vcan2 241#232E000000000000 -(4069.844000) vcan2 241#24CC000000000000 -(4074.848000) vcan2 241#255E000000000000 -(4079.850000) vcan2 241#26FF000000000000 -(4084.847000) vcan2 241#2763000000000000 -(4085.026000) vcan2 641#0176 -(4093.303000) vcan2 101#FE013E4000604D40 -(4097.299000) vcan2 241#10333600FEDF1087 -(4097.495000) vcan2 641#300001 -(4097.735000) vcan2 241#215F000000000000 -(4102.850000) vcan2 241#2280000000000000 -(4107.848000) vcan2 241#2300000000000000 -(4112.848000) vcan2 241#24EA000000000000 -(4117.849000) vcan2 241#2500000000000000 -(4122.855000) vcan2 241#2608000000000000 -(4127.846000) vcan2 241#2700000000000000 -(4128.027000) vcan2 641#0176 -(4136.304000) vcan2 101#FE013E4000604D40 -(4140.292000) vcan2 241#10333600FEDF14EC -(4140.488000) vcan2 641#300001 -(4140.720000) vcan2 241#21DF000000000000 -(4145.842000) vcan2 241#222A000000000000 -(4150.847000) vcan2 241#2365000000000000 -(4155.841000) vcan2 241#2426000000000000 -(4160.840000) vcan2 241#25E9000000000000 -(4165.846000) vcan2 241#2616000000000000 -(4170.846000) vcan2 241#27CA000000000000 -(4171.024000) vcan2 641#0176 -(4179.149000) vcan2 101#FE013E4000604D40 -(4183.177000) vcan2 241#10333600FEDF10B4 -(4183.373000) vcan2 641#300001 -(4183.611000) vcan2 241#2101000000000000 -(4187.849000) vcan2 241#2200000000000000 -(4192.844000) vcan2 241#23C5000000000000 -(4197.846000) vcan2 241#2477000000000000 -(4202.838000) vcan2 241#25CE000000000000 -(4207.847000) vcan2 241#266E000000000000 -(4212.843000) vcan2 241#270B000000000000 -(4213.021000) vcan2 641#0176 -(4221.190000) vcan2 101#FE013E4000604D40 -(4225.168000) vcan2 241#10333600FEDF1573 -(4225.366000) vcan2 641#300001 -(4225.600000) vcan2 241#21AF000000000000 -(4229.842000) vcan2 241#222F000000000000 -(4234.842000) vcan2 241#23FF000000000000 -(4239.843000) vcan2 241#241A000000000000 -(4244.841000) vcan2 241#2566000000000000 -(4249.842000) vcan2 241#2605000000000000 -(4254.842000) vcan2 241#27A0000000000000 -(4255.022000) vcan2 641#0176 -(4263.058000) vcan2 101#FE013E4000604D40 -(4267.039000) vcan2 241#10333600FEDF1465 -(4267.238000) vcan2 641#300001 -(4267.469000) vcan2 241#219F000000000000 -(4271.837000) vcan2 241#2206000000000000 -(4276.835000) vcan2 241#2390000000000000 -(4281.838000) vcan2 241#241F000000000000 -(4286.840000) vcan2 241#2555000000000000 -(4291.838000) vcan2 241#26FA000000000000 -(4296.841000) vcan2 241#274F000000000000 -(4297.021000) vcan2 641#0176 -(4305.977000) vcan2 101#FE013E4000604D40 -(4309.970000) vcan2 241#10333600FEDF1195 -(4310.167000) vcan2 641#300001 -(4310.397000) vcan2 241#217E000000000000 -(4314.834000) vcan2 241#2268000000000000 -(4319.840000) vcan2 241#23C2000000000000 -(4324.837000) vcan2 241#2400000000000000 -(4329.837000) vcan2 241#2518000000000000 -(4334.831000) vcan2 241#26AF000000000000 -(4339.842000) vcan2 241#27AF000000000000 -(4340.023000) vcan2 641#0176 -(4348.978000) vcan2 101#FE013E4000604D40 -(4352.800000) vcan2 241#10333600FEDF1249 -(4352.996000) vcan2 641#300001 -(4353.224000) vcan2 241#210F000000000000 -(4357.833000) vcan2 241#22F6000000000000 -(4362.837000) vcan2 241#2377000000000000 -(4367.835000) vcan2 241#24AA000000000000 -(4372.832000) vcan2 241#25D6000000000000 -(4377.834000) vcan2 241#2627000000000000 -(4382.841000) vcan2 241#27E7000000000000 -(4383.020000) vcan2 641#0176 -(4391.803000) vcan2 101#FE013E4000604D40 -(4395.793000) vcan2 241#10333600FEDF11C2 -(4395.993000) vcan2 641#300001 -(4396.229000) vcan2 241#21AF000000000000 -(4400.833000) vcan2 241#224C000000000000 -(4405.834000) vcan2 241#237F000000000000 -(4410.832000) vcan2 241#246F000000000000 -(4415.833000) vcan2 241#2589000000000000 -(4420.839000) vcan2 241#2615000000000000 -(4425.835000) vcan2 241#27D4000000000000 -(4426.015000) vcan2 641#0176 -(4434.698000) vcan2 101#FE013E4000604D40 -(4438.674000) vcan2 241#10333600FEDF1168 -(4438.872000) vcan2 641#300001 -(4439.108000) vcan2 241#2140000000000000 -(4443.840000) vcan2 241#2246000000000000 -(4448.833000) vcan2 241#2370000000000000 -(4453.829000) vcan2 241#248E000000000000 -(4458.835000) vcan2 241#2572000000000000 -(4463.828000) vcan2 241#26B2000000000000 -(4468.832000) vcan2 241#2763000000000000 -(4469.012000) vcan2 641#0176 -(4477.677000) vcan2 101#FE013E4000604D40 -(4481.547000) vcan2 241#10333600FEDF1492 -(4481.743000) vcan2 641#300001 -(4481.973000) vcan2 241#21CA000000000000 -(4486.829000) vcan2 241#22D5000000000000 -(4491.830000) vcan2 241#23D2000000000000 -(4496.828000) vcan2 241#2433000000000000 -(4501.828000) vcan2 241#25ED000000000000 -(4506.833000) vcan2 241#26F0000000000000 -(4511.833000) vcan2 241#27D3000000000000 -(4512.013000) vcan2 641#0176 -(4520.554000) vcan2 101#FE013E4000604D40 -(4524.546000) vcan2 241#10333600FEDF1546 -(4524.744000) vcan2 641#300001 -(4524.974000) vcan2 241#2154000000000000 -(4529.830000) vcan2 241#2245000000000000 -(4534.826000) vcan2 241#23D6000000000000 -(4539.829000) vcan2 241#240C000000000000 -(4544.829000) vcan2 241#2536000000000000 -(4549.828000) vcan2 241#2690000000000000 -(4554.828000) vcan2 241#279C000000000000 -(4555.010000) vcan2 641#0176 -(4563.430000) vcan2 101#FE013E4000604D40 -(4567.415000) vcan2 241#10333600FEDF121C -(4567.614000) vcan2 641#300001 -(4567.843000) vcan2 241#21EC000000000000 -(4572.823000) vcan2 241#225D000000000000 -(4577.835000) vcan2 241#233F000000000000 -(4582.824000) vcan2 241#2474000000000000 -(4587.822000) vcan2 241#25A9000000000000 -(4592.826000) vcan2 241#2672000000000000 -(4597.827000) vcan2 241#277F000000000000 -(4598.007000) vcan2 641#0176 -(4606.357000) vcan2 101#FE013E4000604D40 -(4610.302000) vcan2 241#10333600FEDF1000 -(4610.529000) vcan2 641#300001 -(4610.767000) vcan2 241#21E0000000000000 -(4615.832000) vcan2 241#2202000000000000 -(4620.830000) vcan2 241#2324000000000000 -(4625.833000) vcan2 241#2406000000000000 -(4630.833000) vcan2 241#2540000000000000 -(4635.833000) vcan2 241#260D000000000000 -(4640.826000) vcan2 241#2725000000000000 -(4641.004000) vcan2 641#0176 -(4649.300000) vcan2 101#FE013E4000604D40 -(4653.290000) vcan2 241#10333600FEDF1519 -(4653.486000) vcan2 641#300001 -(4653.718000) vcan2 241#219B000000000000 -(4658.821000) vcan2 241#222D000000000000 -(4663.827000) vcan2 241#230A000000000000 -(4668.831000) vcan2 241#24A0000000000000 -(4673.828000) vcan2 241#25F5000000000000 -(4678.822000) vcan2 241#2695000000000000 -(4683.813000) vcan2 241#27CC000000000000 -(4683.994000) vcan2 641#0176 -(4692.166000) vcan2 101#FE013E4000604D40 -(4696.170000) vcan2 241#10333600FEDF1384 -(4696.368000) vcan2 641#300001 -(4696.602000) vcan2 241#21D4000000000000 -(4701.807000) vcan2 241#2262000000000000 -(4706.807000) vcan2 241#2327000000000000 -(4711.809000) vcan2 241#2466000000000000 -(4716.816000) vcan2 241#25B3000000000000 -(4721.810000) vcan2 241#2646000000000000 -(4726.816000) vcan2 241#27FE000000000000 -(4726.994000) vcan2 641#0176 -(4735.157000) vcan2 101#FE013E4000604D40 -(4739.155000) vcan2 241#10333600FEDF13B1 -(4739.351000) vcan2 641#300001 -(4739.585000) vcan2 241#21FD000000000000 -(4744.809000) vcan2 241#225A000000000000 -(4749.806000) vcan2 241#23BA000000000000 -(4754.810000) vcan2 241#2466000000000000 -(4759.805000) vcan2 241#2500000000000000 -(4764.809000) vcan2 241#26A9000000000000 -(4769.815000) vcan2 241#27D9000000000000 -(4769.997000) vcan2 641#0176 -(4778.044000) vcan2 101#FE013E4000604D40 -(4782.034000) vcan2 241#10333600FEDF13DE -(4782.232000) vcan2 641#300001 -(4782.466000) vcan2 241#21FF000000000000 -(4786.804000) vcan2 241#22D9000000000000 -(4791.805000) vcan2 241#2382000000000000 -(4796.803000) vcan2 241#2454000000000000 -(4801.807000) vcan2 241#25E4000000000000 -(4806.800000) vcan2 241#2661000000000000 -(4811.810000) vcan2 241#276F000000000000 -(4811.990000) vcan2 641#0176 -(4820.963000) vcan2 101#FE013E4000604D40 -(4824.963000) vcan2 241#10333600FEDF140B -(4825.161000) vcan2 641#300001 -(4825.391000) vcan2 241#2157000000000000 -(4829.805000) vcan2 241#228A000000000000 -(4834.803000) vcan2 241#2333000000000000 -(4839.804000) vcan2 241#2444000000000000 -(4844.802000) vcan2 241#2520000000000000 -(4849.801000) vcan2 241#268B000000000000 -(4854.805000) vcan2 241#279A000000000000 -(4854.983000) vcan2 641#0176 -(4863.795000) vcan2 101#FE013E4000604D40 -(4867.772000) vcan2 241#10333600FEDF1357 -(4867.971000) vcan2 641#300001 -(4868.198000) vcan2 241#21DE000000000000 -(4872.808000) vcan2 241#228F000000000000 -(4877.802000) vcan2 241#23F2000000000000 -(4882.801000) vcan2 241#24EA000000000000 -(4887.801000) vcan2 241#2551000000000000 -(4892.801000) vcan2 241#26C9000000000000 -(4897.806000) vcan2 241#2732000000000000 -(4897.986000) vcan2 641#0176 -(4906.796000) vcan2 101#FE013E4000604D40 -(4910.781000) vcan2 241#10333600FEDF105A -(4910.976000) vcan2 641#300001 -(4911.212000) vcan2 241#2101000000000000 -(4915.815000) vcan2 241#2200000000000000 -(4920.801000) vcan2 241#230A000000000000 -(4925.808000) vcan2 241#2406000000000000 -(4930.812000) vcan2 241#25EC000000000000 -(4935.812000) vcan2 241#2600000000000000 -(4940.803000) vcan2 241#27CB000000000000 -(4940.982000) vcan2 641#0176 -(4951.645000) vcan2 241#063680FEDF100001 diff --git a/test/contrib/automotive/interface_mockup.py b/test/contrib/automotive/interface_mockup.py new file mode 100644 index 00000000000..1e7b2388afd --- /dev/null +++ b/test/contrib/automotive/interface_mockup.py @@ -0,0 +1,196 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Nils Weiss + + +# """ Default imports required for setup of CAN interfaces """ + +import os +import subprocess +import sys + +from platform import python_implementation + +from scapy.main import load_layer, load_contrib +from scapy.config import conf +from scapy.error import log_runtime, Scapy_Exception +from scapy.consts import LINUX + +load_layer("can", globals_dict=globals()) +conf.contribs['CAN']['swap-bytes'] = False + +# ############################################################################ +# """ Define interface names for automotive tests """ +# ############################################################################ +iface0 = "vcan0" +iface1 = "vcan1" + +try: + _root = os.geteuid() == 0 +except AttributeError: + _root = False + +_not_pypy = "pypy" not in python_implementation().lower() +_socket_can_support = False + + +def test_and_setup_socket_can(iface_name): + # type: (str) -> None + if 0 != subprocess.call(("cansend %s 000#" % iface_name).split()): + # iface_name is not enabled + if 0 != subprocess.call("modprobe vcan".split()): + raise Exception("modprobe vcan failed") + if 0 != subprocess.call( + ("ip link add name %s type vcan" % iface_name).split()): + log_runtime.debug( + "add %s failed: Maybe it was already up?" % iface_name) + if 0 != subprocess.call( + ("ip link set dev %s up" % iface_name).split()): + raise Exception("could not bring up %s" % iface_name) + + if 0 != subprocess.call(("cansend %s 000#12" % iface_name).split()): + raise Exception("cansend doesn't work") + + sys.__stderr__.write("SocketCAN setup done!\n") + + +if LINUX and _root and _not_pypy: + try: + test_and_setup_socket_can(iface0) + test_and_setup_socket_can(iface1) + log_runtime.debug("CAN should work now") + _socket_can_support = True + except Exception as e: + sys.__stderr__.write("ERROR %s!\n" % e) + + +sys.__stderr__.write("SocketCAN support: %s\n" % _socket_can_support) + + +# ############################################################################ +# """ Define helper functions for CANSocket creation on all platforms """ +# ############################################################################ +if _socket_can_support: + from scapy.contrib.cansocket_native import * # noqa: F403 + new_can_socket = NativeCANSocket + new_can_socket0 = lambda: NativeCANSocket(iface0) + new_can_socket1 = lambda: NativeCANSocket(iface1) + can_socket_string_list = ["-c", iface0] + sys.__stderr__.write("Using NativeCANSocket\n") + +else: + from scapy.contrib.cansocket_python_can import * # noqa: F403 + new_can_socket = lambda iface: PythonCANSocket(bustype='virtual', channel=iface) # noqa: E501 + new_can_socket0 = lambda: PythonCANSocket(bustype='virtual', channel=iface0, timeout=0.01) # noqa: E501 + new_can_socket1 = lambda: PythonCANSocket(bustype='virtual', channel=iface1, timeout=0.01) # noqa: E501 + sys.__stderr__.write("Using PythonCANSocket virtual\n") + + +# ############################################################################ +# """ Test if socket creation functions work """ +# ############################################################################ +s = new_can_socket(iface0) +s.close() +del s + +s = new_can_socket(iface1) +s.close() +del s + + +def cleanup_interfaces(): + # type: () -> bool + """ + Helper function to remove virtual CAN interfaces after test + + :return: True on success + """ + if _socket_can_support: + if 0 != subprocess.call(["ip", "link", "delete", iface0]): + raise Exception("%s could not be deleted" % iface0) + if 0 != subprocess.call(["ip", "link", "delete", iface1]): + raise Exception("%s could not be deleted" % iface1) + return True + + +def drain_bus(iface=iface0, assert_empty=True): + # type: (str, bool) -> None + """ + Utility function for draining a can interface, + asserting that no packets are there + + :param iface: Interface name to drain + :param assert_empty: If true, raise exception in case packets were received + """ + with new_can_socket(iface) as s: + pkts = s.sniff(timeout=0.1) + if assert_empty and not len(pkts) == 0: + raise Scapy_Exception( + "Error in drain_bus. Packets found but no packets expected!") + + +drain_bus(iface0) +drain_bus(iface1) + +log_runtime.debug("CAN sockets should work now") + +# ############################################################################ +# """ Setup and definitions for ISOTP related stuff """ +# ############################################################################ + +# ############################################################################ +# function to exit when the can-isotp kernel module is not available +# ############################################################################ +ISOTP_KERNEL_MODULE_AVAILABLE = False + + +def exit_if_no_isotp_module(): + # type: () -> None + """ + Helper function to exit a test case if ISOTP kernel module is not available + """ + if not ISOTP_KERNEL_MODULE_AVAILABLE: + err = "TEST SKIPPED: can-isotp not available\n" + sys.__stderr__.write(err) + warning("Can't test ISOTPNativeSocket because " + "kernel module isn't loaded") + sys.exit(0) + + +# ############################################################################ +# """ Evaluate if ISOTP kernel module is installed and available """ +# ############################################################################ +if LINUX and _root and _socket_can_support: + p1 = subprocess.Popen(['lsmod'], stdout=subprocess.PIPE) + p2 = subprocess.Popen(['grep', '^can_isotp'], + stdout=subprocess.PIPE, stdin=p1.stdout) + p1.stdout.close() + if p1.wait() == 0 and p2.wait() == 0 and b"can_isotp" in p2.stdout.read(): + p = subprocess.Popen(["isotpsend", "-s1", "-d0", iface0], + stdin=subprocess.PIPE) + p.communicate(b"01") + if p.returncode == 0: + ISOTP_KERNEL_MODULE_AVAILABLE = True + +# ############################################################################ +# """ Save configuration """ +# ############################################################################ +conf.contribs['ISOTP'] = \ + {'use-can-isotp-kernel-module': ISOTP_KERNEL_MODULE_AVAILABLE} + +# ############################################################################ +# """ reload ISOTP kernel module in case configuration changed """ +# ############################################################################ +import importlib +if "scapy.contrib.isotp" in sys.modules: + importlib.reload(scapy.contrib.isotp) # type: ignore # noqa: F405 + +load_contrib("isotp", globals_dict=globals()) + +if ISOTP_KERNEL_MODULE_AVAILABLE: + if ISOTPSocket is not ISOTPNativeSocket: # type: ignore + raise Scapy_Exception("Error in ISOTPSocket import!") +else: + if ISOTPSocket is not ISOTPSoftSocket: # type: ignore + raise Scapy_Exception("Error in ISOTPSocket import!") diff --git a/test/contrib/automotive/kwp.uts b/test/contrib/automotive/kwp.uts new file mode 100644 index 00000000000..73c99dff1a2 --- /dev/null +++ b/test/contrib/automotive/kwp.uts @@ -0,0 +1,650 @@ +% Regression tests for the KWP2000 layer + +# More information at http://www.secdev.org/projects/UTscapy/ + + +############ +############ + ++ Basic operations + += Load module +load_contrib("automotive.kwp", globals_dict=globals()) + += Check if positive response answers + +sds = KWP(b'\x10') +sdspr = KWP(b'\x50') +assert sdspr.answers(sds) + += Check hashret +sds.hashret() == sdspr.hashret() + += Check if negative response answers + +sds = KWP(b'\x10') +neg = KWP(b'\x7f\x10') +assert neg.answers(sds) + += CHECK hashret NEG +sds.hashret() == neg.hashret() + += Check if negative response answers + +conf.contribs['KWP']['treat-response-pending-as-answer'] = False + +sds = KWP(b'\x10') +neg = KWP(b'\x7f\x10\x78') +assert not neg.answers(sds) + +conf.contribs['KWP']['treat-response-pending-as-answer'] = True + += CHECK hashret NEG +sds.hashret() == neg.hashret() + += Check if negative response answers not + +sds = KWP(b'\x10') +neg = KWP(b'\x7f\x11') +assert not neg.answers(sds) + += Check if positive response answers not + +sds = KWP(b'\x10') +somePacket = KWP(b'\x49') +assert not somePacket.answers(sds) + += Check KWP_SDS + +sds = KWP(b'\x10\x01') +assert sds.service == 0x10 +assert sds.diagnosticSession == 0x01 + += Check KWP_SDS + +sds = KWP()/KWP_SDS(b'\x01') +assert sds.service == 0x10 +assert sds.diagnosticSession == 0x01 + += Check KWP_SDSPR + +sdspr = KWP(b'\x50\x02beef') +assert sdspr.service == 0x50 +assert sdspr.diagnosticSession == 0x02 + +assert not sdspr.answers(sds) + += Check KWP_SDSPR + +sdspr = KWP()/KWP_SDSPR(b'\x01beef') +assert sdspr.service == 0x50 +assert sdspr.diagnosticSession == 0x01 + +assert sdspr.answers(sds) + += Check KWP_SDS + +sds = KWP()/KWP_SDS(b'\x01') +assert sds.service == 0x10 +assert sds.diagnosticSession == 0x01 + += Check KWP_SDSPR + +sdspr = KWP()/KWP_SDSPR(b'\x01beef') +assert sdspr.service == 0x50 +assert sdspr.diagnosticSession == 0x01 + +assert sdspr.answers(sds) + += Check KWP_ER + +er = KWP(b'\x11\x01') +assert er.service == 0x11 +assert er.resetMode == 0x01 + += Check KWP_ER + +er = KWP()/KWP_ER(resetMode="powerOnReset") +assert er.service == 0x11 +assert er.resetMode == 0x01 + += Check KWP_ERPR + +erpr = KWP(b'\x51') +assert erpr.service == 0x51 + +assert erpr.answers(er) + += Check KWP_SA + +sa = KWP(b'\x27\x00c0ffee') +assert sa.service == 0x27 +assert sa.accessMode == 0x0 +assert sa.key == b'c0ffee' + += Check KWP_SAPR + +sapr = KWP(b'\x67') +assert sapr.service == 0x67 + +assert sapr.answers(sa) + += Check KWP_SA + +sa = KWP(b'\x27\x01') +assert sa.service == 0x27 +assert sa.accessMode == 0x1 + += Check KWP_SAPR + +sapr = KWP(b'\x67\x01c0ffee') +assert sapr.service == 0x67 +assert sapr.accessMode == 0x1 +assert sapr.seed == b'c0ffee' + +assert sapr.answers(sa) + += Check KWP_SA + +sa = KWP(b'\x27\x00c0ffee') +assert sa.service == 0x27 +assert sa.accessMode == 0x0 +assert sa.key == b'c0ffee' + += Check KWP_SA + +sa = KWP(b'\x27\x01c0ffee') +assert sa.service == 0x27 +assert sa.accessMode == 0x1 + += Check KWP_SAPR + +sapr = KWP(b'\x67\x01c0ffee') +assert sapr.service == 0x67 +assert sapr.accessMode == 0x1 +assert sapr.seed == b'c0ffee' + + += Check KWP_DNT + +cc = KWP(b'\x28\x01') +assert cc.service == 0x28 +assert cc.responseRequired == 0x1 + += Check KWP_DNMTPR + +ccpr = KWP(b'\x68') +assert ccpr.service == 0x68 +assert ccpr.answers(cc) + += Check KWP_DNMTPR + +ccpr = KWP(b'\x68abcd') +assert ccpr.service == 0x68 +assert ccpr.answers(cc) + +assert (KWP()/KWP_DNMTPR()).answers(cc) + += Check KWP_TP + +tp = KWP(b'\x3E\x01') +assert tp.service == 0x3e +assert tp.responseRequired == 1 + += Check KWP_TPPR + +tppr = KWP(b'\x7E') +assert tppr.service == 0x7e + +assert tppr.answers(tp) + +assert (KWP()/KWP_TPPR()).answers(tp) + += Check KWP_CDTCS + +cdtcs = KWP(b'\x85\x01\x40\x00\x01') +assert cdtcs.service == 0x85 +assert cdtcs.responseRequired == 1 +assert cdtcs.groupOfDTC == 0x4000 +assert cdtcs.DTCSettingMode == 1 + += Check KWP_CDTCSPR + +cdtcspr = KWP(b'\xC5\x00') +assert cdtcspr.service == 0xC5 + +assert cdtcspr.answers(cdtcs) + += Check KWP_ROE + +roe = KWP(b'\x86\x01\x10\x00') +assert roe.service == 0x86 +assert roe.responseRequired == 1 +assert roe.eventType == 0 +assert roe.eventWindowTime == 16 + += Check KWP_ROEPR + +roepr = KWP(b'\xC6\x00\x01') +assert roepr.service == 0xC6 +assert roepr.numberOfActivatedEvents == 0 +assert roepr.eventType == 0 + +assert roepr.answers(roe) + += Check KWP_RDBI + +rdbi = KWP(b'\x22\x01\x02') +assert rdbi.service == 0x22 +assert rdbi.identifier == 0x0102 + += Build KWP_RDBI + +rdbi = KWP()/KWP_RDBI(identifier=0x102) +assert rdbi.service == 0x22 +assert rdbi.identifier == 0x0102 +assert bytes(rdbi) == b'\x22\x01\x02' + += Check KWP_RDBI2 + +rdbi = KWP(b'\x22\x01\x02') +assert rdbi.service == 0x22 +assert rdbi.identifier == 0x0102 +assert raw(rdbi) == b'\x22\x01\x02' + += Build KWP_RDBI2 + +rdbi = KWP()/KWP_RDBI(identifier=0x304) +assert rdbi.service == 0x22 +assert rdbi.identifier == 0x0304 +assert raw(rdbi) == b'\x22\x03\x04' + += Test observable dict used in KWP_RDBI, setter + +KWP_RDBI.dataIdentifiers[0x102] = "turbo" +KWP_RDBI.dataIdentifiers[0x103] = "fullspeed" + +rdbi = KWP()/KWP_RDBI(identifier=0x102) + +assert "turbo" in plain_str(repr(rdbi)) + +rdbi = KWP()/KWP_RDBI(identifier=0x103) + +assert "fullspeed" in plain_str(repr(rdbi)) + += Test observable dict used in KWP_RDBI, deleter + +KWP_RDBI.dataIdentifiers[0x102] = "turbo" + +rdbi = KWP()/KWP_RDBI(identifier=0x102) +assert "turbo" in plain_str(repr(rdbi)) + +del KWP_RDBI.dataIdentifiers[0x102] +KWP_RDBI.dataIdentifiers[0x103] = "slowspeed" + +rdbi = KWP()/KWP_RDBI(identifier=0x102) + +assert "turbo" not in plain_str(repr(rdbi)) + +rdbi = KWP()/KWP_RDBI(identifier=0x103) + +assert "slowspeed" in plain_str(repr(rdbi)) + += Check KWP_RDBIPR + +rdbipr = KWP(b'\x62\x01\x03dieselgate') +assert rdbipr.service == 0x62 +assert rdbipr.identifier == 0x0103 +assert rdbipr.load == b'dieselgate' + +assert rdbipr.answers(rdbi) + += Check KWP_RMBA + +rmba = KWP(b'\x23\x11\x02\x02\x11') +assert rmba.service == 0x23 +assert rmba.memoryAddress == 0x110202 +assert rmba.memorySize == 17 + += Check KWP_RMBAPR + +rmbapr = KWP(b'\x63muchData') +assert rmbapr.service == 0x63 +assert rmbapr.dataRecord == b'muchData' + +assert rmbapr.answers(rmba) + += Check KWP_DDLI + +dddi = KWP(b'\x2c\x12\x44coffee') +assert dddi.service == 0x2c +assert dddi.dynamicallyDefineLocalIdentifier == 0x12 +assert dddi.definitionMode == 0x44 +assert dddi.dataRecord == b'coffee' + += Check KWP_DDLIPR + +dddipr = KWP(b'\x6c\x12\x44\x01') +assert dddipr.service == 0x6c +assert dddipr.dynamicallyDefineLocalIdentifier == 0x12 + +assert dddipr.answers(dddi) + += Check KWP_WDBI + +wdbi = KWP(b'\x2e\x01\x02dieselgate') +assert wdbi.service == 0x2e +assert wdbi.identifier == 0x0102 +assert wdbi.load == b'dieselgate' + += Build KWP_WDBI + +wdbi = KWP()/KWP_WDBI(identifier=0x0102)/Raw(load=b'dieselgate') +assert wdbi.service == 0x2e +assert wdbi.identifier == 0x0102 +assert wdbi.load == b'dieselgate' +assert bytes(wdbi) == b'\x2e\x01\x02dieselgate' + += Check KWP_WDBI + +wdbi = KWP(b'\x2e\x01\x02dieselgate') +assert wdbi.service == 0x2e +assert wdbi.identifier == 0x0102 +assert wdbi.load == b'dieselgate' + +wdbi = KWP(b'\x2e\x02\x02benzingate') +assert wdbi.service == 0x2e +assert wdbi.identifier == 0x0202 +assert wdbi.load == b'benzingate' + + += Check KWP_WDBIPR + +wdbipr = KWP(b'\x6e\x02\x02') +assert wdbipr.service == 0x6e +assert wdbipr.identifier == 0x0202 + +assert wdbipr.answers(wdbi) + += Check KWP_WMBA + +wmba = KWP(b'\x3d\x11\x02\x02\x02muchData') +assert wmba.service == 0x3d +assert wmba.memoryAddress == 0x110202 +assert wmba.memorySize == 2 +assert wmba.dataRecord == b'muchData' + += Check KWP_WMBAPR + +wmbapr = KWP(b'\x7d\x11\x02\x02') +assert wmbapr.service == 0x7d +assert wmbapr.memoryAddress == 0x110202 + +assert wmbapr.answers(wmba) + += Check KWP_CDI + +cdtci = KWP(b'\x14\x44\x02\x03') +assert cdtci.service == 0x14 +assert cdtci.groupOfDTC == 0x4402 + +cdtcipr = KWP(b'\x54\x44\x02\x03') +assert cdtcipr.service == 0x54 +assert cdtcipr.groupOfDTC == 0x4402 + +assert cdtcipr.answers(cdtci) + += Check KWP_RC + +rc = KWP(b'\x31\xff\xee\xdd\xaa') +assert rc.service == 0x31 +assert rc.routineLocalIdentifier == 0xff +assert rc.load == b'\xee\xdd\xaa' + += Check KWP_RC + +rc = KWP(b'\x31\xff\xee\xdd\xaa') +assert rc.service == 0x31 +assert rc.routineLocalIdentifier == 0xff +assert rc.load == b'\xee\xdd\xaa' + + += Check KWP_RCPR + +rcpr = KWP(b'\x71\xff\xee\xdd\xaa') +assert rcpr.service == 0x71 +assert rcpr.routineLocalIdentifier == 0xff +assert rcpr.load == b'\xee\xdd\xaa' + +assert rcpr.answers(rc) + += Check KWP_RD + +rd = KWP(b'\x34\xaa\x11\x02\x02\x10\x00\x00') + +assert rd.service == 0x34 +assert rd.memoryAddress == 0xaa1102 +assert rd.compression == 0 +assert rd.encryption == 2 +assert rd.uncompressedMemorySize == 0x100000 + += Check KWP_RDPR + +rdpr = KWP(b'\x74\x02\x02\x02\x02\x03\x03\x03\x03') +assert rdpr.service == 0x74 +assert rdpr.maxNumberOfBlockLength == b'\x02\x02\x02\x02\x03\x03\x03\x03' +rdpr.show() +assert rdpr.answers(rd) + += Check KWP_RU + +ru = KWP(b'\x35\x11\x02\x02\xa0\xff\xff\xff') +assert ru.service == 0x35 +assert ru.memoryAddress == 0x110202 +assert ru.compression == 10 +assert ru.encryption == 0 +assert ru.uncompressedMemorySize == 0xffffff + += Check KWP_RUPR + +rupr = KWP(b'\x75\x02\x02\x02\x02\x03\x03\x03\x03') +assert rupr.service == 0x75 +assert rupr.maxNumberOfBlockLength == b'\x02\x02\x02\x02\x03\x03\x03\x03' + +assert rupr.answers(ru) + += Check KWP_TD + +td = KWP(b'\x36\xaapayload') +assert td.service == 0x36 +assert td.blockSequenceCounter == 0xaa +assert td.transferDataRequestParameter == b'payload' + += Check KWP_TDPR + +tdpr = KWP(b'\x76\xaapayload') +assert tdpr.service == 0x76 +assert tdpr.blockSequenceCounter == 0xaa +assert tdpr.transferDataRequestParameter == b'payload' + +assert tdpr.answers(td) + += Check KWP_RTE + +rte = KWP(b'\x37payload') +assert rte.service == 0x37 +assert rte.transferDataRequestParameter == b'payload' + += Check KWP_RTEPR + +rtepr = KWP(b'\x77payload') +assert rtepr.service == 0x77 +assert rtepr.transferDataRequestParameter == b'payload' + +assert rtepr.answers(rte) + += Check KWP_IOCBI + +iocbi = KWP(b'\x30\x23\xffcoffee') +assert iocbi.service == 0x30 +assert iocbi.localIdentifier == 0x23 +assert iocbi.inputOutputControlParameter == 255 +assert iocbi.controlState == b'coffee' + += Check KWP_IOCBIPR + +iocbipr = KWP(b'\x70\x23\xffcoffee') +assert iocbipr.service == 0x70 +assert iocbipr.localIdentifier == 0x23 +assert iocbipr.inputOutputControlParameter == 255 +assert iocbipr.controlState == b'coffee' + +assert iocbipr.answers(iocbi) + += Check KWP_NRC + +nrc = KWP(b'\x7f\x22\x33') +assert nrc.service == 0x7f +assert nrc.requestServiceId == 0x22 +assert nrc.negativeResponseCode == 0x33 + ++ Single layer KWP mode + += Single layer mode: enable and basic dissect + +conf.contribs['KWP']['single_layer_mode'] = True + +sds = KWP(b'\x10\x01') +assert isinstance(sds, KWP_SDS), "Expected KWP_SDS, got %s" % type(sds) +assert sds.service == 0x10 +assert sds.diagnosticSession == 0x01 + += Single layer mode: build KWP_SDS + +sds_built = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_built) == b'\x10\x01', "Expected b'\\x10\\x01', got %s" % bytes(sds_built).hex() + += Single layer mode: dissect positive response + +sdspr = KWP(b'\x50\x01\xbe\xef') +assert isinstance(sdspr, KWP_SDSPR), "Expected KWP_SDSPR, got %s" % type(sdspr) +assert sdspr.service == 0x50 +assert sdspr.diagnosticSession == 0x01 + += Single layer mode: answers() between subpackets + +sds2 = KWP_SDS(diagnosticSession=0x01) +sdspr2 = KWP_SDSPR(diagnosticSession=0x01) +assert sdspr2.answers(sds2) + += Single layer mode: NegativeResponse dissect + +nr = KWP(b'\x7f\x10\x22') +assert isinstance(nr, KWP_NR), "Expected KWP_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.negativeResponseCode == 0x22 + += Single layer mode: NegativeResponse answers() + +sds3 = KWP_SDS(diagnosticSession=0x01) +nr2 = KWP_NR(requestServiceId=0x10, negativeResponseCode=0x22) +assert nr2.answers(sds3) + += Single layer mode: hashret consistency between request and positive response + +sds4 = KWP_SDS(diagnosticSession=0x01) +sdspr4 = KWP_SDSPR(diagnosticSession=0x01) +assert sds4.hashret() == sdspr4.hashret(), \ + "hashret mismatch: %s vs %s" % (sds4.hashret().hex(), sdspr4.hashret().hex()) + += Single layer mode: unknown service falls back to KWP + +unknown = KWP(b'\xAA\x01\x02') +assert isinstance(unknown, KWP), "Expected KWP fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['KWP']['single_layer_mode'] = False + +sds5 = KWP(b'\x10\x01') +assert sds5.__class__ == KWP +assert sds5.service == 0x10 +assert sds5[KWP_SDS].diagnosticSession == 0x01 + += Single layer mode: idempotency + +conf.contribs['KWP']['single_layer_mode'] = True +conf.contribs['KWP']['single_layer_mode'] = True +sds6 = KWP(b'\x10\x01') +assert isinstance(sds6, KWP_SDS) + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['single_layer_mode'] = False +sds7 = KWP(b'\x10\x01') +assert sds7.__class__ == KWP +count = sum(1 for fval, cls in KWP.payload_guess + if fval.get('service') == 0x10 and cls == KWP_SDS) +assert count == 1, "Expected 1 binding for KWP_SDS, got %d" % count + += Single layer mode: cleanup + +conf.contribs['KWP']['single_layer_mode'] = False +assert not conf.contribs['KWP']['single_layer_mode'] + ++ Compatibility mode KWP + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['KWP']['single_layer_mode'] = True +conf.contribs['KWP']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +sds_sa = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_sa) == b'\x10\x01', \ + "Standalone KWP_SDS should include service byte, got %s" % bytes(sds_sa).hex() +assert sds_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked) == b'\x10\x01', \ + "Stacked KWP/KWP_SDS should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +sds_dis = KWP(b'\x10\x01') +assert isinstance(sds_dis, KWP_SDS) +assert sds_dis.service == 0x10 +assert sds_dis.diagnosticSession == 0x01 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['KWP']['compatibility_mode'] = False + +stacked_nc = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked_nc) == b'\x10\x10\x01', \ + "With compat OFF, stacked KWP/KWP_SDS should produce 3 bytes, got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +sds_nc = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_nc) == b'\x10\x01', \ + "Standalone KWP_SDS should include service byte even with compat OFF, got %s" % bytes(sds_nc).hex() + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['compatibility_mode'] = False + +stacked_slm_off = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked_slm_off) == b'\x10\x01', \ + "With SLM OFF, no service field in KWP_SDS regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() + += Compatibility mode: cleanup + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['compatibility_mode'] = True +assert not conf.contribs['KWP']['single_layer_mode'] +assert conf.contribs['KWP']['compatibility_mode'] diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index f04fa89a0c8..2c5e8e71e11 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -9,9 +9,7 @@ + Basic operations = Load module - -load_contrib("automotive.obd.obd") - +load_contrib("automotive.obd.obd", globals_dict=globals()) = Check if positive response answers @@ -447,7 +445,7 @@ assert p.data_records[0].turbocharger_a_turbine_inlet_temperature == \ round((0x2233 * 0.1) - 40, 3) assert p.data_records[0].turbocharger_a_turbine_outlet_temperature == \ round((0x4455 * 0.1) - 40, 3) -r = OBD(b'\x02\x75') +r = OBD(b'\x02\x75\x00') assert p.answers(r) @@ -467,7 +465,7 @@ assert p.data_records[0].sensor2 == 1707.7 assert p.data_records[0].sensor3 == 1759.1 assert p.data_records[0].sensor4 == 1810.5 -r = OBD(b'\x02\x78') +r = OBD(b'\x02\x78\x00') assert p.answers(r) = Check dissecting a response for Service 02 PID 7F @@ -487,7 +485,7 @@ assert p.data_records[0].total == 0xFFFFFFFFFFFFFFFF assert p.data_records[0].total_idle == 0x0102030405060708 assert p.data_records[0].total_with_pto_active == 0x0011223344556677 -r = OBD(b'\x02\x7F') +r = OBD(b'\x02\x7F\x00') assert p.answers(r) @@ -499,7 +497,7 @@ assert p.data_records[0].pid == 0x89 assert p.data_records[0].frame_no == 0x01 assert p.data_records[0].data == b'ABCDEFGHIKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOP' -r = OBD(b'\x02\x89') +r = OBD(b'\x02\x89\x00') assert p.answers(r) = Check dissecting a response for Service 02 PID 0C, 05, 04 @@ -1033,3 +1031,127 @@ assert b[22:] == b'ABCDEFGHIJKLMNOP' r = OBD(b'\x09\x02\x04') assert p.answers(r) + ++ Single layer OBD mode + += Single layer mode: enable and basic dissect + +conf.contribs['OBD']['single_layer_mode'] = True + +s01 = OBD(b'\x01\x0c') +assert isinstance(s01, OBD_S01), "Expected OBD_S01, got %s" % type(s01) +assert s01.service == 0x01 + += Single layer mode: build OBD_S01 + +s01_built = OBD_S01(pid=[0x0c]) +assert bytes(s01_built) == b'\x01\x0c', "Expected b'\\x01\\x0c', got %s" % bytes(s01_built).hex() + += Single layer mode: dissect positive response + +s01pr = OBD(b'\x41\x0c\x0f\xa0') +assert isinstance(s01pr, OBD_S01_PR), "Expected OBD_S01_PR, got %s" % type(s01pr) +assert s01pr.service == 0x41 + += Single layer mode: NegativeResponse dissect + +nr = OBD(b'\x7f\x01\x22') +assert isinstance(nr, OBD_NR), "Expected OBD_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.request_service_id == 0x01 +assert nr.response_code == 0x22 + += Single layer mode: NegativeResponse answers() + +s01_2 = OBD_S01(pid=[0x0c]) +nr2 = OBD_NR(request_service_id=0x01, response_code=0x22) +assert nr2.answers(s01_2) + += Single layer mode: hashret consistency between request and positive response + +s09 = OBD_S09(iid=[0x02]) +s09pr = OBD_S09_PR() +assert s09.hashret() == s09pr.hashret(), \ + "hashret mismatch: %s vs %s" % (s09.hashret().hex(), s09pr.hashret().hex()) + += Single layer mode: unknown service falls back to OBD + +unknown = OBD(b'\xBB\x01\x02') +assert isinstance(unknown, OBD), "Expected OBD fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['OBD']['single_layer_mode'] = False + +s01_3 = OBD(b'\x01\x0c') +assert s01_3.__class__ == OBD +assert s01_3.service == 0x01 +assert isinstance(s01_3[OBD_S01], OBD_S01) + += Single layer mode: cleanup + +conf.contribs['OBD']['single_layer_mode'] = False +assert not conf.contribs['OBD']['single_layer_mode'] + ++ Compatibility mode OBD + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['OBD']['single_layer_mode'] = True +conf.contribs['OBD']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +s01_sa = OBD_S01(pid=[0x0c]) +assert bytes(s01_sa)[0:1] == b'\x01', \ + "Standalone OBD_S01 should include service byte 0x01, got %s" % bytes(s01_sa).hex() + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = OBD() / OBD_S01(pid=[0x0c]) +stacked_bytes = bytes(stacked) +assert stacked_bytes[0:1] == b'\x01', \ + "Stacked OBD/OBD_S01 first byte should be 0x01 (OBD service), got %s" % stacked_bytes.hex() +assert stacked_bytes[1:2] != b'\x01', \ + "No duplicate service byte expected, got %s" % stacked_bytes.hex() +assert len(stacked_bytes) == 2, \ + "Stacked OBD/OBD_S01(pid=[0x0c]) should be 2 bytes, got %s" % stacked_bytes.hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +s01_dis = OBD(b'\x01\x0c') +assert isinstance(s01_dis, OBD_S01) + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['OBD']['compatibility_mode'] = False + +stacked_nc = OBD() / OBD_S01(pid=[0x0c]) +stacked_nc_bytes = bytes(stacked_nc) +assert len(stacked_nc_bytes) == 3, \ + "With compat OFF, stacked OBD/OBD_S01 should produce 3 bytes (duplicate service), got %s" % stacked_nc_bytes.hex() +assert stacked_nc_bytes[0:1] == b'\x01' and stacked_nc_bytes[1:2] == b'\x01', \ + "With compat OFF, first two bytes should both be service 0x01, got %s" % stacked_nc_bytes.hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +s01_nc = OBD_S01(pid=[0x0c]) +assert bytes(s01_nc)[0:1] == b'\x01', \ + "Standalone OBD_S01 should include service byte even with compat OFF" + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['OBD']['single_layer_mode'] = False +conf.contribs['OBD']['compatibility_mode'] = False + +stacked_slm_off = OBD() / OBD_S01(pid=[0x0c]) +slm_off_bytes = bytes(stacked_slm_off) +assert len(slm_off_bytes) == 2, \ + "With SLM OFF, no service field in OBD_S01 regardless of compat mode, got %s" % slm_off_bytes.hex() + += Compatibility mode: cleanup + +conf.contribs['OBD']['single_layer_mode'] = False +conf.contribs['OBD']['compatibility_mode'] = True +assert not conf.contribs['OBD']['single_layer_mode'] +assert conf.contribs['OBD']['compatibility_mode'] diff --git a/test/contrib/automotive/obd/scanner.uts b/test/contrib/automotive/obd/scanner.uts index 57102fed9de..ad90095b739 100644 --- a/test/contrib/automotive/obd/scanner.uts +++ b/test/contrib/automotive/obd/scanner.uts @@ -1,335 +1,213 @@ % Regression tests for obd_scan +~ scanner + Configuration ~ conf = Imports -load_layer("can") -conf.contribs['CAN']['swap-bytes'] = False -import os, six, subprocess, sys -from subprocess import call -from scapy.consts import LINUX - -= Definition of constants, utility functions and mock classes -iface0 = "vcan0" -iface1 = "vcan1" - -# function to exit when the can-isotp kernel module is not available -ISOTP_KERNEL_MODULE_AVAILABLE = False -def exit_if_no_isotp_module(): - if not ISOTP_KERNEL_MODULE_AVAILABLE: - sys.stderr.write("TEST SKIPPED: can-isotp not available" + os.linesep) - warning("Can't test ISOTP native socket because kernel module is not loaded") - exit(0) - - -= Initialize a virtual CAN interface -~ vcan_socket needs_root linux -if 0 != call(["cansend", iface0, "000#"]): - # vcan0 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface0, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface0) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface0, "up"]): - raise Exception("could not bring up %s" % iface0) - -if 0 != call(["cansend", iface0, "000#"]): - raise Exception("cansend doesn't work") - -if 0 != call(["cansend", iface1, "000#"]): - # vcan1 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface1, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface1) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface1, "up"]): - raise Exception("could not bring up %s" % iface1) - -if 0 != call(["cansend", iface1, "000#"]): - raise Exception("cansend doesn't work") - -print("CAN should work now") - -= Import CANSocket - -from scapy.contrib.cansocket_python_can import * - -import can as python_can -new_can_socket = lambda iface: CANSocket(bustype='virtual', channel=iface) -new_can_socket0 = lambda: CANSocket(bustype='virtual', channel=iface0, timeout=0.01) -new_can_socket1 = lambda: CANSocket(bustype='virtual', channel=iface1, timeout=0.01) - -# utility function for draining a can interface, asserting that no packets are there -def drain_bus(iface=iface0, assert_empty=True): - with new_can_socket(iface) as s: - pkts = s.sniff(timeout=0.1) - if assert_empty: - assert len(pkts) == 0 - -= Overwrite definition for vcan_socket systems native sockets -~ vcan_socket not_pypy needs_root linux - -if six.PY3 and LINUX: - from scapy.contrib.cansocket_native import * - new_can_socket = lambda iface: CANSocket(iface) - new_can_socket0 = lambda: CANSocket(iface0) - new_can_socket1 = lambda: CANSocket(iface1) - - -= Overwrite definition for vcan_socket systems python-can sockets -~ vcan_socket needs_root linux -if "python_can" in CANSocket.__module__: - new_can_socket = lambda iface: CANSocket(bustype='socketcan', channel=iface, bitrate=250000, timeout=0.01) - new_can_socket0 = lambda: CANSocket(bustype='socketcan', channel=iface0, bitrate=250000, timeout=0.01) - new_can_socket1 = lambda: CANSocket(bustype='socketcan', channel=iface1, bitrate=250000, timeout=0.01) - -= Verify that a CAN socket can be created and closed -s = new_can_socket(iface0) -s.close() - - -= Check if can-isotp and can-utils are installed on this system -~ linux -p1 = subprocess.Popen(['lsmod'], stdout = subprocess.PIPE) -p2 = subprocess.Popen(['grep', '^can_isotp'], stdout = subprocess.PIPE, stdin=p1.stdout) -p1.stdout.close() -if p1.wait() == 0 and p2.wait() == 0 and b"can_isotp" in p2.stdout.read(): - p = subprocess.Popen(["isotpsend", "-s1", "-d0", iface0], stdin = subprocess.PIPE) - p.communicate(b"01") - if p.returncode == 0: - ISOTP_KERNEL_MODULE_AVAILABLE = True - - -+ Syntax check - -= Import isotp -conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': ISOTP_KERNEL_MODULE_AVAILABLE} -load_contrib("isotp") - -if six.PY3 and ISOTP_KERNEL_MODULE_AVAILABLE: - from scapy.contrib.isotp import ISOTPNativeSocket - ISOTPSocket = ISOTPNativeSocket - assert ISOTPSocket == ISOTPNativeSocket -else: - from scapy.contrib.isotp import ISOTPSoftSocket - ISOTPSocket = ISOTPSoftSocket - assert ISOTPSocket == ISOTPSoftSocket - -############ -############ -+ Load general modules -= Load contribution layer +from test.testsocket import TestSocket, cleanup_testsockets +from scapy.contrib.automotive.ecu import * # noqa: F403 -load_contrib('automotive.obd.obd') += Load contribution layer -+ Load OBD_scan -= imports +load_contrib("automotive.obd.obd", globals_dict=globals()) +load_contrib("automotive.obd.scanner", globals_dict=globals()) -from subprocess import call += Create sockets -from scapy.contrib.automotive.obd.scanner import obd_scan -from scapy.contrib.automotive.obd.scanner import _supported_id_numbers -from scapy.contrib.automotive.ecu import * +ecu = TestSocket(OBD) +tester = TestSocket(OBD) +ecu.pair(tester) = Create answers -s3 = OBD()/OBD_S03_PR(dtcs=[OBD_DTC()]) - -s1_pid00 = OBD() / OBD_S01_PR(data_records=[OBD_S01_PR_Record() / OBD_PID00(supported_pids="PID03+PID0B+PID0F")]) -s6_mid00 = OBD() / OBD_S06_PR(data_records=[OBD_S06_PR_Record() / OBD_MID00(supported_mids="")]) -s8_tid00 = OBD() / OBD_S08_PR(data_records=[OBD_S08_PR_Record() / OBD_TID00(supported_tids="")]) -s9_iid00 = OBD() / OBD_S09_PR(data_records=[OBD_S09_PR_Record() / OBD_IID00(supported_iids="")]) - - -s1_pid01 = OBD()/OBD_S01_PR(data_records=[OBD_S01_PR_Record()/OBD_PID01()]) -s1_pid03 = OBD()/OBD_S01_PR(data_records=[OBD_S01_PR_Record()/OBD_PID03(fuel_system1=0, fuel_system2=2)]) -s1_pid0B = OBD()/OBD_S01_PR(data_records=[OBD_S01_PR_Record()/OBD_PID0B(data=100)]) -s1_pid0F = OBD()/OBD_S01_PR(data_records=[OBD_S01_PR_Record()/OBD_PID0F(data=50)]) - -example_responses = \ - [ECUResponse(session=range(0, 255), security_level=0, responses=s3), - ECUResponse(session=range(0, 255), security_level=0, responses=s1_pid00), - ECUResponse(session=range(0, 255), security_level=0, responses=s6_mid00), - ECUResponse(session=range(0, 255), security_level=0, responses=s8_tid00), - ECUResponse(session=range(0, 255), security_level=0, responses=s9_iid00), - ECUResponse(session=range(0, 255), security_level=0, responses=s1_pid01), - ECUResponse(session=range(0, 255), security_level=0, responses=s1_pid03), - ECUResponse(session=range(0, 255), security_level=0, responses=s1_pid0B), - ECUResponse(session=range(0, 255), security_level=0, responses=s1_pid0F)] - responses = [ - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=0)/OBD_PID00(supported_pids=3191777299)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=1)/OBD_PID01(mil=0, dtc_count=0, reserved1=0, continuous_tests_ready=0, reserved2=0, continuous_tests_supported=7, once_per_trip_tests_supported=225, once_per_trip_tests_ready=0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=11)/OBD_PID0B(data=44)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=12)/OBD_PID0C(data=857.0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=13)/OBD_PID0D(data=0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=14)/OBD_PID0E(data=3.5)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=15)/OBD_PID0F(data=22.0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=17)/OBD_PID11(data=14.51)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=19)/OBD_PID13(sensors_present=3)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=21)/OBD_PID15(outputVoltage=1.275, trim=99.219)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=28)/OBD_PID1C(data=6)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=3)/OBD_PID03(fuel_system1=2, fuel_system2=0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=31)/OBD_PID1F(data=13)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=32)/OBD_PID20(supported_pids=2684465153)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=33)/OBD_PID21(data=0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=35)/OBD_PID23(data=24910)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=4)/OBD_PID04(data=9.804)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=48)/OBD_PID30(data=19)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=49)/OBD_PID31(data=3587)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=5)/OBD_PID05(data=41.0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=51)/OBD_PID33(data=97)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=52)/OBD_PID34(equivalence_ratio=1.001, current=128.004)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=6)/OBD_PID06(data=0.0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=64)/OBD_PID40(supported_pids=244352000)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=69)/OBD_PID45(data=3.922)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=7)/OBD_PID07(data=-0.781)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=70)/OBD_PID46(data=20.0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=71)/OBD_PID47(data=12.549)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=73)/OBD_PID49(data=5.49)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=76)/OBD_PID4C(data=3.922)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=81)/OBD_PID51(data=1)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=86)/OBD_PID56(bank1=0.0)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=67)/OBD_S03_PR(count=0)), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=0)/OBD_MID00(supported_mids=3221225473)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=1)/OBD_MIDXX(standardized_test_id=131, unit_and_scaling_id=4, test_value=0.0, min_limit=0.0, max_limit=1.0),OBD_S06_PR_Record(mid=1)/OBD_MIDXX(standardized_test_id=138, unit_and_scaling_id=132, test_value=0.996, min_limit=-32.768, max_limit=1.06),OBD_S06_PR_Record(mid=1)/OBD_MIDXX(standardized_test_id=139, unit_and_scaling_id=132, test_value=0.996, min_limit=0.94, max_limit=32.767)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=128)/OBD_MID80(supported_mids=1)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=160)/OBD_MIDA0(supported_mids=4160749568)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=161)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=162)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=162)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=2, min_limit=0, max_limit=65535)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=163)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=163)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=1, min_limit=0, max_limit=65535)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=164)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=164)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=1, min_limit=0, max_limit=65535)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=165)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=165)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=1, min_limit=0, max_limit=65535)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=2)/OBD_MIDXX(standardized_test_id=145, unit_and_scaling_id=177, test_value=3944, min_limit=900, max_limit=65534),OBD_S06_PR_Record(mid=2)/OBD_MIDXX(standardized_test_id=149, unit_and_scaling_id=10, test_value=764.696, min_limit=719.556, max_limit=7995.27),OBD_S06_PR_Record(mid=2)/OBD_MIDXX(standardized_test_id=150, unit_and_scaling_id=10, test_value=115.412, min_limit=0.0, max_limit=179.95)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=32)/OBD_MID20(supported_mids=2147485697)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=33)/OBD_MIDXX(standardized_test_id=132, unit_and_scaling_id=3, test_value=2.63, min_limit=1.0, max_limit=655.35)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=128, unit_and_scaling_id=28, test_value=32.42, min_limit=10.0, max_limit=655.35),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=129, unit_and_scaling_id=28, test_value=25.41, min_limit=10.0, max_limit=655.35),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=130, unit_and_scaling_id=28, test_value=0.21, min_limit=0.0, max_limit=10.0),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=131, unit_and_scaling_id=28, test_value=0.0, min_limit=0.0, max_limit=10.0),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=132, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=133, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=134, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=135, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=64)/OBD_MID40(supported_mids=3221225473)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=65)/OBD_MIDXX(standardized_test_id=133, unit_and_scaling_id=22, test_value=720.0, min_limit=700.0, max_limit=6513.5)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=66)/OBD_MIDXX(standardized_test_id=144, unit_and_scaling_id=20, test_value=401, min_limit=0, max_limit=800)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=96)/OBD_MID60(supported_mids=1)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=71)/OBD_S07_PR(count=0)), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=0)/OBD_IID00(supported_iids=1430405120)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=10)/OBD_IID0A(ecu_names=[b'ECM\x00-EngineControl\x00\x00'], count=1)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=15)/Raw(load=b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00HM0876')])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=18)/Raw(load=b'\x01\x00\xd5')])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=2)/OBD_IID02(vehicle_identification_numbers=[b'WDD1xxxxxxxxxxx11'], count=1)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=4)/OBD_IID04(calibration_identifications=[b'282xxxxxxx300044', b'00090xxxxxx00031'], count=2)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=6)/OBD_IID06(calibration_verification_numbers=[b'\xf9\x10\xb9\xfb', b'&6"e'], count=2)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=8)/OBD_IID08(data=[9, 189, 8, 9, 0, 0, 8, 9, 0, 0, 22, 9, 0, 0, 0, 0, 8, 9, 0, 0], count=20)])), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=127)/OBD_NR(request_service_id=1, response_code=49)), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=127)/OBD_NR(request_service_id=10, response_code=49)), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=127)/OBD_NR(request_service_id=6, response_code=49)), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=127)/OBD_NR(request_service_id=8, response_code=17)), - ECUResponse(session=range(0, 255), security_level=0, responses=OBD(service=127)/OBD_NR(request_service_id=9, response_code=49))] + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=0)/OBD_PID00(supported_pids=3191777299)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=1)/OBD_PID01(mil=0, dtc_count=0, reserved1=0, continuous_tests_ready=0, reserved2=0, continuous_tests_supported=7, once_per_trip_tests_supported=225, once_per_trip_tests_ready=0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=11)/OBD_PID0B(data=44)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=12)/OBD_PID0C(data=857.0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=13)/OBD_PID0D(data=0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=14)/OBD_PID0E(data=3.5)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=15)/OBD_PID0F(data=22.0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=17)/OBD_PID11(data=14.51)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=19)/OBD_PID13(sensors_present=3)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=21)/OBD_PID15(outputVoltage=1.275, trim=99.219)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=28)/OBD_PID1C(data=6)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=3)/OBD_PID03(fuel_system1=2, fuel_system2=0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=31)/OBD_PID1F(data=13)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=32)/OBD_PID20(supported_pids=2684465153)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=33)/OBD_PID21(data=0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=35)/OBD_PID23(data=24910)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=4)/OBD_PID04(data=9.804)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=48)/OBD_PID30(data=19)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=49)/OBD_PID31(data=3587)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=5)/OBD_PID05(data=41.0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=51)/OBD_PID33(data=97)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=52)/OBD_PID34(equivalence_ratio=1.001, current=128.004)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=6)/OBD_PID06(data=0.0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=64)/OBD_PID40(supported_pids=244352000)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=69)/OBD_PID45(data=3.922)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=7)/OBD_PID07(data=-0.781)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=70)/OBD_PID46(data=20.0)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=71)/OBD_PID47(data=12.549)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=73)/OBD_PID49(data=5.49)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=76)/OBD_PID4C(data=3.922)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=81)/OBD_PID51(data=1)])), + EcuResponse(responses=OBD(service=65)/OBD_S01_PR(data_records=[OBD_S01_PR_Record(pid=86)/OBD_PID56(bank1=0.0)])), + EcuResponse(responses=OBD(service=67)/OBD_S03_PR(count=0)), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=0)/OBD_MID00(supported_mids=3221225473)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=1)/OBD_MIDXX(standardized_test_id=131, unit_and_scaling_id=4, test_value=0.0, min_limit=0.0, max_limit=1.0),OBD_S06_PR_Record(mid=1)/OBD_MIDXX(standardized_test_id=138, unit_and_scaling_id=132, test_value=0.996, min_limit=-32.768, max_limit=1.06),OBD_S06_PR_Record(mid=1)/OBD_MIDXX(standardized_test_id=139, unit_and_scaling_id=132, test_value=0.996, min_limit=0.94, max_limit=32.767)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=128)/OBD_MID80(supported_mids=1)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=160)/OBD_MIDA0(supported_mids=4160749568)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=161)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=162)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=162)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=2, min_limit=0, max_limit=65535)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=163)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=163)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=1, min_limit=0, max_limit=65535)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=164)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=164)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=1, min_limit=0, max_limit=65535)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=165)/OBD_MIDXX(standardized_test_id=12, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=65535),OBD_S06_PR_Record(mid=165)/OBD_MIDXX(standardized_test_id=11, unit_and_scaling_id=36, test_value=1, min_limit=0, max_limit=65535)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=2)/OBD_MIDXX(standardized_test_id=145, unit_and_scaling_id=177, test_value=3944, min_limit=900, max_limit=65534),OBD_S06_PR_Record(mid=2)/OBD_MIDXX(standardized_test_id=149, unit_and_scaling_id=10, test_value=764.696, min_limit=719.556, max_limit=7995.27),OBD_S06_PR_Record(mid=2)/OBD_MIDXX(standardized_test_id=150, unit_and_scaling_id=10, test_value=115.412, min_limit=0.0, max_limit=179.95)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=32)/OBD_MID20(supported_mids=2147485697)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=33)/OBD_MIDXX(standardized_test_id=132, unit_and_scaling_id=3, test_value=2.63, min_limit=1.0, max_limit=655.35)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=128, unit_and_scaling_id=28, test_value=32.42, min_limit=10.0, max_limit=655.35),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=129, unit_and_scaling_id=28, test_value=25.41, min_limit=10.0, max_limit=655.35),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=130, unit_and_scaling_id=28, test_value=0.21, min_limit=0.0, max_limit=10.0),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=131, unit_and_scaling_id=28, test_value=0.0, min_limit=0.0, max_limit=10.0),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=132, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=133, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=134, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3),OBD_S06_PR_Record(mid=53)/OBD_MIDXX(standardized_test_id=135, unit_and_scaling_id=36, test_value=0, min_limit=0, max_limit=3)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=64)/OBD_MID40(supported_mids=3221225473)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=65)/OBD_MIDXX(standardized_test_id=133, unit_and_scaling_id=22, test_value=720.0, min_limit=700.0, max_limit=6513.5)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=66)/OBD_MIDXX(standardized_test_id=144, unit_and_scaling_id=20, test_value=401, min_limit=0, max_limit=800)])), + EcuResponse(responses=OBD(service=70)/OBD_S06_PR(data_records=[OBD_S06_PR_Record(mid=96)/OBD_MID60(supported_mids=1)])), + EcuResponse(responses=OBD(service=71)/OBD_S07_PR(count=0)), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=0)/OBD_IID00(supported_iids=1430405120)])), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=10)/OBD_IID0A(ecu_names=[b'ECM\x00-EngineControl\x00\x00'], count=1)])), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=15)/Raw(load=b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00HM0876')])), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=18)/Raw(load=b'\x01\x00\xd5')])), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=2)/OBD_IID02(vehicle_identification_numbers=[b'WDD1xxxxxxxxxxx11'], count=1)])), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=4)/OBD_IID04(calibration_identifications=[b'282xxxxxxx300044', b'00090xxxxxx00031'], count=2)])), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=6)/OBD_IID06(calibration_verification_numbers=[b'\xf9\x10\xb9\xfb', b'&6"e'], count=2)])), + EcuResponse(responses=OBD(service=73)/OBD_S09_PR(data_records=[OBD_S09_PR_Record(iid=8)/OBD_IID08(data=[9, 189, 8, 9, 0, 0, 8, 9, 0, 0, 22, 9, 0, 0, 0, 0, 8, 9, 0, 0], count=20)])), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=1, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=2, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=3, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=4, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=5, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=6, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=7, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=8, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=9, response_code=0x12)), + EcuResponse(responses=OBD(service=127)/OBD_NR(request_service_id=10, response_code=0x12))] + Simulate scanner -= Run scanner with real world responses - -exit_if_no_isotp_module() - -drain_bus(iface0) - -with new_can_socket0() as isocan_ecu, ISOTPSocket(isocan_ecu, 0x7e8, 0x7e0, basecls=OBD, padding=True) as ecu: - answering_machine = ECU_am(supported_responses=responses, main_socket=ecu, basecls=OBD) - sim = threading.Thread(target=answering_machine, kwargs={"timeout": 3}) - sim.start() - try: - with new_can_socket0() as isocan, ISOTPSocket(isocan, 0x7e0, 0x7e8, basecls=OBD, padding=True) as socket: - data = obd_scan(socket, 0.01, True, False, verbose=False) - dtc = data[0] - print(data) - supported = data[1] - unsupported = data[2] - - finally: - sim.join(timeout=10) - -= Run scanner - -exit_if_no_isotp_module() - -drain_bus(iface0) - -with new_can_socket0() as isocan_ecu, ISOTPSocket(isocan_ecu, 0x7e8, 0x7e0, basecls=OBD, padding=True) as ecu: - answering_machine = ECU_am(supported_responses=example_responses, main_socket=ecu, basecls=OBD) - sim = threading.Thread(target=answering_machine, kwargs={'count': 12}) - sim.start() - try: - with new_can_socket0() as isocan, ISOTPSocket(isocan, 0x7e0, 0x7e8, basecls=OBD, padding=True) as socket: - all_ids_set = set(range(1, 256)) - supported_ids = _supported_id_numbers(socket, 0.1, OBD_S01, 'pid', False) - unsupported_ids = all_ids_set - supported_ids - drain_bus(iface0) - # timeout to avoid a deadlock if the test which sets this event fails - with new_can_socket0() as isocan, ISOTPSocket(isocan, 0x7e0, 0x7e8, basecls=OBD, padding=True) as socket: - data = obd_scan(socket, 0.01, True, True, verbose=False) - dtc = data[0] - print(data) - supported = data[1] - unsupported = data[2] - - finally: - sim.join(timeout=10) - - -+ Check results - -= Check supported ids += Run scanner with real world responses short scan + +sniff(timeout=0.001, opened_socket=[ecu, tester]) + +answering_machine = EcuAnsweringMachine(supported_responses=responses, main_socket=ecu, basecls=OBD) +sim = threading.Thread(target=answering_machine, kwargs={"timeout": 100, "stop_filter": lambda x: bytes(x) == b"\xff\xff\xff"}) +sim.start() +try: + s = OBD_Scanner(tester, full_scan=False, timeout=1, retry_if_none_received=True) + s.scan(timeout=100) + tester.send(b"\xff\xff\xff") +finally: + sim.join(timeout=10) + +s.show_testcases() + +assert len(s.enumerators) == 8 +assert s.enumerators[0].__class__ == OBD_S01_Enumerator +assert s.enumerators[1].__class__ == OBD_S02_Enumerator +assert s.enumerators[2].__class__ == OBD_S06_Enumerator +assert s.enumerators[3].__class__ == OBD_S08_Enumerator +assert s.enumerators[4].__class__ == OBD_S09_Enumerator +assert s.enumerators[5].__class__ == OBD_S03_Enumerator +assert s.enumerators[6].__class__ == OBD_S07_Enumerator +assert s.enumerators[7].__class__ == OBD_S0A_Enumerator + +print(len(s.enumerators[0].results_with_response)) + +assert len(s.enumerators[0].results_with_response) == 33 # 32 pos resps + 1 NR +assert len(s.enumerators[0].results_with_negative_response) == 1 +assert len(s.enumerators[1].results_with_response) == 1 # 1 NR +assert len(s.enumerators[1].results_with_negative_response) == 1 +assert len(s.enumerators[2].results_with_response) == 18 # 17 pos resps + 1 NR +assert len(s.enumerators[2].results_with_negative_response) == 1 +assert len(s.enumerators[3].results_with_response) == 1 # 1 NR +assert len(s.enumerators[3].results_with_negative_response) == 1 +assert len(s.enumerators[4].results_with_response) == 9 # 8 pos resps + 1 NR +assert len(s.enumerators[4].results_with_negative_response) == 1 +assert len(s.enumerators[5].results_with_response) == 1 # 1 PR +assert len(s.enumerators[6].results_with_response) == 1 # 1 PR +assert len(s.enumerators[7].results_with_response) == 1 # 1 PR + + += Run scanner with real world responses full scan + +sniff(timeout=0.001, opened_socket=[ecu, tester]) + +answering_machine = EcuAnsweringMachine(supported_responses=responses, main_socket=ecu, basecls=OBD) +sim = threading.Thread(target=answering_machine, kwargs={"timeout": 100, "stop_filter": lambda x: bytes(x) == b"\xff\xff\xff"}) +sim.start() +try: + s = OBD_Scanner(tester, full_scan=True, timeout=1, retry_if_none_received=True) + s.scan(timeout=100) + tester.send(b"\xff\xff\xff") +finally: + sim.join(timeout=10) + +s.show_testcases() + +assert len(s.enumerators) == 8 +assert s.enumerators[0].__class__ == OBD_S01_Enumerator +assert s.enumerators[1].__class__ == OBD_S02_Enumerator +assert s.enumerators[2].__class__ == OBD_S06_Enumerator +assert s.enumerators[3].__class__ == OBD_S08_Enumerator +assert s.enumerators[4].__class__ == OBD_S09_Enumerator +assert s.enumerators[5].__class__ == OBD_S03_Enumerator +assert s.enumerators[6].__class__ == OBD_S07_Enumerator +assert s.enumerators[7].__class__ == OBD_S0A_Enumerator + +assert len(s.enumerators[0].results_with_response) == 0x100 # 32 pos resps + 1 NR +print( len(s.enumerators[0].results_with_negative_response)) +assert len(s.enumerators[0].results_with_negative_response) == 0x100 - 32 +print( len(s.enumerators[1].results_with_response)) +assert len(s.enumerators[1].results_with_response) == 0x100 +print( len(s.enumerators[1].results_with_negative_response)) +assert len(s.enumerators[1].results_with_negative_response) == 0x100 +assert len(s.enumerators[2].results_with_response) == 0x100 # 17 pos resps +assert len(s.enumerators[2].results_with_negative_response) == 0x100 - 17 +assert len(s.enumerators[3].results_with_response) == 0x100 +assert len(s.enumerators[3].results_with_negative_response) == 0x100 +assert len(s.enumerators[4].results_with_response) == 0x100 # 8 pos resps +assert len(s.enumerators[4].results_with_negative_response) == 0x100 - 8 +assert len(s.enumerators[5].results_with_response) == 1 # 1 PR +assert len(s.enumerators[6].results_with_response) == 1 # 1 PR +assert len(s.enumerators[7].results_with_response) == 1 # 1 PR + + += Run scanner only for Service 01 real world responses + +sniff(timeout=0.001, opened_socket=[ecu, tester]) + +answering_machine = EcuAnsweringMachine(supported_responses=responses, main_socket=ecu, basecls=OBD) +sim = threading.Thread(target=answering_machine, kwargs={"timeout": 100, "stop_filter": lambda x: bytes(x) == b"\xff\xff\xff"}) +sim.start() +try: + s = OBD_Scanner(tester, test_cases=[OBD_S01_Enumerator], full_scan=False, retry_if_none_received=True, timeout=1) + s.scan(timeout=100) + tester.send(b"\xff\xff\xff") +finally: + sim.join(timeout=10) + +s.show_testcases() + +assert len(s.enumerators) == 1 +assert s.enumerators[0].__class__ == OBD_S01_Enumerator + +assert len(s.enumerators[0].results_with_response) == 33 # 32 pos resps + 1 NR +assert len(s.enumerators[0].results_with_negative_response) == 1 -exit_if_no_isotp_module() - -supported_ids_set = set([3, 11, 15]) -assert supported_ids == supported_ids_set - -= Check unsupported ids - -exit_if_no_isotp_module() - -unsupported_ids_set = all_ids_set - supported_ids_set -assert unsupported_ids == unsupported_ids_set - -= Check service 1 - -exit_if_no_isotp_module() - -assert len(supported[1]) == 3 - -= Check service 3 - -exit_if_no_isotp_module() - -assert dtc[3] == bytes(s3) - -= Check empty services - -exit_if_no_isotp_module() - -assert len(supported[6]) == 0 -assert len(supported[8]) == 0 -assert len(supported[9]) == 0 - -print(dtc) -assert dtc[7] == b'\x7f\x07\x10' -assert dtc[10] == b'\x7f\n\x10' - -= Check unsupported service 1 - -exit_if_no_isotp_module() - -assert unsupported[1][1] == bytes(s1_pid01) + Cleanup -= Delete vcan interfaces -~ vcan_socket needs_root linux - -if 0 != call(["sudo", "ip", "link", "delete", iface0]): - raise Exception("%s could not be deleted" % iface0) += Delete TestSockets -if 0 != call(["sudo", "ip", "link", "delete", iface1]): - raise Exception("%s could not be deleted" % iface1) +cleanup_testsockets() diff --git a/test/contrib/automotive/scanner/configuration.uts b/test/contrib/automotive/scanner/configuration.uts new file mode 100644 index 00000000000..1db1b8706f1 --- /dev/null +++ b/test/contrib/automotive/scanner/configuration.uts @@ -0,0 +1,144 @@ +% Regression tests for automotive scanner configuration + ++ Load general modules + += Load contribution layer + +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCase +from scapy.contrib.automotive.scanner.configuration import AutomotiveTestCaseExecutorConfiguration +from scapy.contrib.automotive.scanner.staged_test_case import StagedAutomotiveTestCase + ++ Basic checks + += Definition of Test classes + +class MyTestCase1(AutomotiveTestCase): + _description = "MyTestCase1" + def supported_responses(self): + return [] + + +class MyTestCase2(AutomotiveTestCase): + _description = "MyTestCase2" + def supported_responses(self): + return [] + +class MyTestCase3(AutomotiveTestCase): + _description = "MyTestCase3" + def supported_responses(self): + return [] + +class MyTestCase4(AutomotiveTestCase): + _description = "MyTestCase4" + def supported_responses(self): + return [] + += creation of config with classes + +config = AutomotiveTestCaseExecutorConfiguration( + [MyTestCase1, MyTestCase2, MyTestCase3, MyTestCase4]) + +assert len(config.test_cases) == 4 +assert len(config.test_case_clss) == 4 +assert len(config.stages) == 0 +assert len(config.staged_test_cases) == 0 +assert config.verbose == False +assert config.debug == False + + += creation of config with instances + +config = AutomotiveTestCaseExecutorConfiguration( + [MyTestCase1(), MyTestCase2(), MyTestCase3(), MyTestCase4()]) + +assert len(config.test_cases) == 4 +assert len(config.test_case_clss) == 4 +assert len(config.stages) == 0 +assert len(config.staged_test_cases) == 0 +assert config.verbose == False +assert config.debug == False + + += creation of config with instances and classes + +config = AutomotiveTestCaseExecutorConfiguration( + [MyTestCase2(), MyTestCase2(), MyTestCase3, MyTestCase4]) + +assert len(config.test_cases) == 4 +assert len(config.test_case_clss) == 3 +assert len(config.stages) == 0 +assert len(config.staged_test_cases) == 0 +assert config.verbose == False +assert config.debug == False + + += creation of config with instances and classes and global configuration and local configuration + +config = AutomotiveTestCaseExecutorConfiguration( + [MyTestCase2(), MyTestCase2(), MyTestCase3, MyTestCase4], + global_config=42, verbose=True, MyTestCase2_kwargs={"local_config": 41}) + +assert len(config.test_cases) == 4 +assert len(config.test_case_clss) == 3 +assert len(config.stages) == 0 +assert len(config.staged_test_cases) == 0 +assert config.verbose == True +assert config.debug == False +assert config["MyTestCase2"]["global_config"] == 42 +assert config["MyTestCase2"]["local_config"] == 41 +assert config["MyTestCase2"]["verbose"] == True +try: + print(config["MyTestCase1"]["global_config"]) + raise AssertionError +except KeyError: + pass + +assert len(config["MyTestCase3"]) == 3 +assert len(config["MyTestCase2"]) == 4 + +try: + print(config["MyTestCase3"]["local_config"]) + raise AssertionError +except KeyError: + pass + + += creation of config with stages + +st = StagedAutomotiveTestCase([MyTestCase1(), MyTestCase2()]) + +config = AutomotiveTestCaseExecutorConfiguration( + [MyTestCase2(), MyTestCase2, MyTestCase3, MyTestCase4, st]) + +assert len(config.test_cases) == 5 +assert len(config.test_case_clss) == 5 +assert len(config.stages) == 1 +assert len(config.staged_test_cases) == 2 +assert config.verbose == False +assert config.debug == False +assert config.staged_test_cases[0].__class__ == MyTestCase1 +assert config.staged_test_cases[1].__class__ == MyTestCase2 +assert config.stages[0].__class__ == StagedAutomotiveTestCase + += creation of config with stages class + +class myStagedTestCase(StagedAutomotiveTestCase): + def __init__(self): + # type: () -> None + super(myStagedTestCase, self).__init__( + [MyTestCase1(), MyTestCase2()], + None) + + +config = AutomotiveTestCaseExecutorConfiguration( + [MyTestCase2(), MyTestCase2, MyTestCase3, MyTestCase4, myStagedTestCase]) + +assert len(config.test_cases) == 5 +assert len(config.test_case_clss) == 5 +assert len(config.stages) == 1 +assert len(config.staged_test_cases) == 2 +assert config.staged_test_cases[0].__class__ == MyTestCase1 +assert config.staged_test_cases[1].__class__ == MyTestCase2 +assert config.stages[0].__class__ == myStagedTestCase +assert config.verbose == False +assert config.debug == False diff --git a/test/contrib/automotive/scanner/enumerator.uts b/test/contrib/automotive/scanner/enumerator.uts new file mode 100644 index 00000000000..b1ac0cc8716 --- /dev/null +++ b/test/contrib/automotive/scanner/enumerator.uts @@ -0,0 +1,1091 @@ +% Regression tests for enumerators +~ linux + ++ Load general modules + += Load contribution layer + +from scapy.contrib.automotive.scanner.enumerator import _AutomotiveTestCaseScanResult, ServiceEnumerator, StateGenerator, StateGeneratingServiceEnumerator +from scapy.contrib.automotive.scanner.test_case import TestCaseGenerator, AutomotiveTestCase +from scapy.contrib.automotive.scanner.executor import AutomotiveTestCaseExecutor +from scapy.contrib.isotp import ISOTP +from scapy.contrib.automotive.uds import * +from scapy.contrib.automotive.scanner.staged_test_case import StagedAutomotiveTestCase +from scapy.utils import SingleConversationSocket +from scapy.contrib.automotive.ecu import EcuState, EcuResponse +from scapy.contrib.automotive.uds_ecu_states import * +import copy + ++ Basic checks += ServiceEnumerator basecls checks + +pkts = [ + _AutomotiveTestCaseScanResult(EcuState(session=1), UDS(b"\x20abcd"), UDS(b"\x60abcd"), 1.0, 1.9), + _AutomotiveTestCaseScanResult(EcuState(session=2), UDS(b"\x20abcd"), None, 2.0, None), + _AutomotiveTestCaseScanResult(EcuState(session=1), UDS(b"\x21abcd"), UDS(b"\x7fabcd"), 3.0, 3.1), + _AutomotiveTestCaseScanResult(EcuState(session=2), UDS(b"\x21abcd"), UDS(b"\x7fa\x10cd"), 4.0, 4.5), +] + +class MyTestCase(ServiceEnumerator): + _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) + _supported_kwargs.update({ + 'local_kwarg': ((int, str), None), + 'verbose': (bool, None), + 'global_arg': (str, None) + }) + def _get_initial_requests(self, **kwargs): + # type: (Any) -> Iterable[Packet] + return UDS(service=range(1, 11)) + def _get_table_entry_y(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return "0x%02x: %s" % (tup[1].service, tup[1].sprintf("%UDS.service%")) + def _get_table_entry_z(self, tup): + # type: (_AutomotiveTestCaseScanResult) -> str + return self._get_label(tup[2], "PR: Supported") + @staticmethod + def _get_negative_response_label(response): + # type: (Packet) -> str + return response.sprintf("NR: %UDS_NR.negativeResponseCode%") + @staticmethod + def _get_negative_response_code(resp): + # type: (Packet) -> int + return resp.negativeResponseCode + @staticmethod + def _get_negative_response_desc(nrc): + # type: (int) -> str + return UDS_NR(negativeResponseCode=nrc).sprintf( + "%UDS_NR.negativeResponseCode%") + + +e = MyTestCase() +for p in pkts: + p.req.time = p.req_ts + p.req.sent_time = p.req_ts + if p.resp is not None: + p.resp.time = p.resp_ts + e._store_result(p.state, p.req, p.resp) + + += ServiceEnumerator not completed check + +assert e.completed == False + += ServiceEnumerator completed + +e._state_completed[EcuState(session=1)] = True +e._state_completed[EcuState(session=2)] = True + +assert e.completed + += ServiceEnumerator stats check + +stat_list = e._compute_statistics() + +stats = {label: value for state, label, value in stat_list if state == "all"} +print(stats) + +assert stats["num_answered"] == '3' +assert stats["num_unanswered"] == '1' +assert stats["answertime_max"] == '0.9' +assert stats["answertime_min"] == '0.1' +assert stats["answertime_avg"] == '0.5' +assert stats["num_negative_resps"] == '2' + += ServiceEnumerator scanned states + +assert len(e.scanned_states) == 2 +assert {EcuState(session=1), EcuState(session=2)} == e.scanned_states + += ServiceEnumerator scanned results + +assert len(e.results_with_positive_response) == 1 +assert len(e.results_with_negative_response) == 2 +assert len(e.results_without_response) == 1 +assert len(e.results_with_response) == 3 + += ServiceEnumerator get_label +assert e._get_label(pkts[0].resp) == "PR: PositiveResponse" +assert e._get_label(pkts[0].resp, lambda _: "positive") == "positive" +assert e._get_label(pkts[0].resp, lambda _: "positive" + hex(pkts[0].req.service)) == "positive" + "0x20" +assert e._get_label(pkts[1].resp) == "Timeout" +assert e._get_label(pkts[2].resp) == "NR: 98" +assert e._get_label(pkts[3].resp) == "NR: generalReject" + += ServiceEnumerator show + +e.show(filtered=False) + +dump = e.show(dump=True, filtered=False) +assert "NR: 98" in dump +assert "NR: generalReject" in dump +assert "PR: Supported" in dump +assert "Timeout" in dump +assert "session1" in dump +assert "session2" in dump +assert "0x20" in dump +assert "0x21" in dump + += ServiceEnumerator filtered results before show + +print(len(e.filtered_results)) +assert len(e.filtered_results) == 2 +assert e.filtered_results[0] == pkts[0] +assert e.filtered_results[1] == pkts[2] + += ServiceEnumerator show filtered + +e.show(filtered=True) + +dump = e.show(dump=True, filtered=True) +assert "NR: 98" in dump +assert "NR: generalReject" in dump +assert "PR: Supported" in dump +assert "Timeout" not in dump +assert "session1" in dump +assert "session2" in dump +assert "all" in dump +assert "0x20" in dump +assert "0x21" in dump +assert "The following negative response codes are blacklisted: ['serviceNotSupported']" in dump + += ServiceEnumerator filtered results after show + +assert len(e.filtered_results) == 3 +assert e.filtered_results[0] == pkts[0] +assert e.filtered_results[1] == pkts[2] + += ServiceEnumerator supported responses + +assert len(e.supported_responses) == 3 + += ServiceEnumerator evaluate response + +conf = {} + +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), None, **conf) +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x10"), **conf) +conf = {"exit_if_service_not_supported": True, "retry_if_busy_returncode": False} +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x21"), **conf) +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x10"), **conf) +assert True == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x11"), **conf) +assert True == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x7f"), **conf) +conf = {"exit_if_service_not_supported": False, "retry_if_busy_returncode": True} +assert not e._retry_pkt[EcuState(session=1)] +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x10"), **conf) +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x11"), **conf) +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x7f"), **conf) +assert not e._retry_pkt[EcuState(session=1)] +assert True == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x21"), **conf) +assert e._retry_pkt[EcuState(session=1)] == UDS(b"\x10\x03abcd") +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x7f\x10\x21"), **conf) +assert not e._retry_pkt[EcuState(session=1)] + +assert True == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), UDS(b"\x50\x03\x00"), **conf) +assert False == e._evaluate_response(EcuState(session=1), UDS(b"\x11\x03abcd"), UDS(b"\x51\x03\x00"), **conf) +conf = {"retry_if_none_received": True} +assert True == e._evaluate_response(EcuState(session=1), UDS(b"\x10\x03abcd"), None, **conf) +assert e._retry_pkt[EcuState(session=1)] + + += ServiceEnumerator execute + +from queue import Queue +from scapy.supersocket import SuperSocket + +class MockISOTPSocket(SuperSocket): + nonblocking_socket = True + @property + def closed(self): + return False + @closed.setter + def closed(self, var): + pass + def __init__(self, rcvd_queue=None): + self.rcvd_queue = Queue() + self.sent_queue = Queue() + if rcvd_queue is not None: + for c in rcvd_queue: + self.rcvd_queue.put(c) + def recv_raw(self, x=MTU): + pkt = bytes(self.rcvd_queue.get(True, 0.01)) + return UDS, pkt, 10.0 + def send(self, x): + sx = raw(x) + try: + x.sent_time = 9.0 + except AttributeError: + pass + self.sent_queue.put(sx) + return len(sx) + @staticmethod + def select(sockets, remain=None): + time.sleep(0) + return sockets + def sr(self, *args, **kargs): + from scapy import sendrecv + return sendrecv.sndrcv(self, *args, threaded=False, **kargs) + def sr1(self, *args, **kargs): + from scapy import sendrecv + ans = sendrecv.sndrcv(self, *args, threaded=False, **kargs)[0] # type: SndRcvList + if len(ans) > 0: + pkt = ans[0][1] # type: Packet + return pkt + else: + return None + +sock = MockISOTPSocket() +sock.rcvd_queue.put(b"\x41") +sock.rcvd_queue.put(b"\x42") +sock.rcvd_queue.put(b"\x43") +sock.rcvd_queue.put(b"\x44") +sock.rcvd_queue.put(b"\x45") +sock.rcvd_queue.put(b"\x46") +sock.rcvd_queue.put(b"\x47") +sock.rcvd_queue.put(b"\x48") +sock.rcvd_queue.put(b"\x49") +sock.rcvd_queue.put(b"\x4A") + +e = MyTestCase() + +e.execute(sock, EcuState(session=1)) + +assert len(e.filtered_results) == 10 +assert len(e.results_with_response) == 10 +assert len(e.results_without_response) == 0 + +assert e.has_completed(EcuState(session=1)) +assert e.completed + +e.execute(sock, EcuState(session=2), timeout=0.01) + +assert len(e.filtered_results) == 10 +assert len(e.results_with_response) == 10 +assert len(e.results_without_response) == 10 + +assert e.has_completed(EcuState(session=2)) + +e.execute(sock, EcuState(session=3), timeout=0.01, exit_if_no_answer_received=True) + +assert not e.has_completed(EcuState(session=3)) +assert not e.completed +assert len(e.scanned_states) == 3 + +e.execute(sock, EcuState(session=42), state_block_list=[EcuState(session=42)]) + +assert e.has_completed(EcuState(session=42)) +assert len(e.scanned_states) == 3 + +e.execute(sock, EcuState(session=13), state_block_list=EcuState(session=13)) + +assert e.has_completed(EcuState(session=13)) +assert len(e.scanned_states) == 3 + +e.execute(sock, EcuState(session=41), state_allow_list=[EcuState(session=42)]) + +assert e.has_completed(EcuState(session=41)) +assert len(e.scanned_states) == 3 + +e.execute(sock, EcuState(session=12), state_allow_list=EcuState(session=13)) + +assert e.has_completed(EcuState(session=12)) +assert len(e.scanned_states) == 3 + += Test negative response code service not supported + +sock.rcvd_queue.put(b"\x7f\x01\x11") +sock.rcvd_queue.put(b"\x7f\x01\x7f") + +e = MyTestCase() + +e.execute(sock, EcuState(session=1), exit_if_service_not_supported=True) + +assert not e._retry_pkt[EcuState(session=1)] +assert len(e.results_with_response) == 1 +assert len(e.results_with_negative_response) == 1 +assert e.completed + +e.execute(sock, EcuState(session=2), exit_if_service_not_supported=True) + +assert not e._retry_pkt[EcuState(session=2)] +assert len(e.results_with_response) == 2 +assert len(e.results_with_negative_response) == 2 +assert e.completed + += Test negative response code retry if busy + +sock.rcvd_queue.put(b"\x7f\x01\x21") +sock.rcvd_queue.put(b"\x7f\x01\x10") + +e = MyTestCase() + +e.execute(sock, EcuState(session=1)) + +assert e._retry_pkt[EcuState(session=1)] +assert len(e.results_with_response) == 1 +assert len(e.results_with_negative_response) == 1 +assert len(e.results_without_response) == 0 +assert not e.completed + +e.execute(sock, EcuState(session=1)) + +assert not e._retry_pkt[EcuState(session=1)] +assert len(e.results_with_response) == 2 +assert len(e.results_with_negative_response) == 2 +assert len(e.results_without_response) == 9 +assert e.completed +assert e.has_completed(EcuState(session=1)) + += Test negative response code don't retry if busy + +sock.rcvd_queue.put(b"\x7f\x01\x21") + +e = MyTestCase() + +e.execute(sock, EcuState(session=1), retry_if_busy_returncode=False) + +assert not e._retry_pkt[EcuState(session=1)] +assert len(e.results_with_response) == 1 +assert len(e.results_with_negative_response) == 1 +assert len(e.results_without_response) == 9 +assert e.completed +assert e.has_completed(EcuState(session=1)) + += Test execution time + +sock.rcvd_queue.put(b"\x7f\x01\x10") + +e = MyTestCase() + +e.execute(sock, EcuState(session=1), execution_time=-1) + +assert not e._retry_pkt[EcuState(session=1)] +assert len(e.results_with_response) == 1 +assert len(e.results_with_negative_response) == 1 +assert len(e.results_without_response) == 0 +assert not e.completed +assert not e.has_completed(EcuState(session=1)) + + ++ AutomotiveTestCaseExecutorConfiguration tests + += Definitions + +class MockSock(object): + closed = False + def sr1(self, *args, **kwargs): + raise OSError + +class TestCase1(MyTestCase): + pass + +class TestCase2(MyTestCase): + pass + +class Scanner(AutomotiveTestCaseExecutor): + @property + def default_test_case_clss(self): + # type: () -> List[Type[AutomotiveTestCaseABC]] + return [MyTestCase] + += Basic tests + +tce = Scanner(MockSock(), test_cases=[TestCase1, TestCase2, MyTestCase], + verbose=True, debug=True, + global_arg="Whatever", TestCase1_kwargs={"local_kwarg": 42}) + +config = tce.configuration # type: AutomotiveTestCaseExecutorConfiguration +assert config.verbose +assert config.debug +assert len(config.test_cases) == 3 +assert len(config.stages) == 0 +assert len(config.staged_test_cases) == 0 +assert len(config.test_case_clss) == 3 +assert len(config.TestCase1.items()) == 5 +assert len(config.TestCase2.items()) == 4 +assert len(config["TestCase1"].items()) == 5 +assert len(config.MyTestCase.items()) == 4 +assert config.TestCase1["verbose"] +assert config.TestCase1["debug"] +assert config.TestCase1["local_kwarg"] == 42 +assert config.TestCase1["global_arg"] == "Whatever" +assert config.TestCase2["global_arg"] == "Whatever" +assert config.MyTestCase["global_arg"] == "Whatever" +assert isinstance(tce.socket, SingleConversationSocket) + + += Basic tests with default values + +tce = Scanner(MockSock()) + +config = tce.configuration # type: AutomotiveTestCaseExecutorConfiguration +assert not config.verbose +assert not config.debug +assert len(config.test_cases) == 1 +assert len(config.MyTestCase.items()) == 1 +assert isinstance(tce.socket, SingleConversationSocket) + + += Basic test with stages + +def connector(testcase1, _): + scan_range = len(testcase1.results) + return {"verbose": True, "scan_range": scan_range} + +tc1 = TestCase1() +tc2 = TestCase2() + +pipeline = StagedAutomotiveTestCase([tc1, tc2], [None, connector]) + +tce = Scanner(MockSock(), test_cases=[pipeline]) + +config = tce.configuration # type: AutomotiveTestCaseExecutorConfiguration +assert not config.verbose +assert not config.debug +assert len(config.test_cases) == 1 +assert len(config.stages) == 1 +assert len(config.staged_test_cases) == 2 +assert len(config.test_case_clss) == 3 +assert len(config.StagedAutomotiveTestCase.items()) == 1 +assert isinstance(tce.socket, SingleConversationSocket) + += Basic tests with two stages + +def connector(testcase1, testcase2): + scan_range = len(testcase1.results) + return {"verbose": True, "scan_range": scan_range} + +tc1 = TestCase1() +tc2 = TestCase2() + +pipeline = StagedAutomotiveTestCase([tc1, tc2], [None, connector]) + +class StagedTest(StagedAutomotiveTestCase): + pass + +pipeline2 = StagedTest([MyTestCase(), MyTestCase()]) + +tce = Scanner(MockSock(), test_cases=[pipeline, pipeline2], verbose=True) + +config = tce.configuration # type: AutomotiveTestCaseExecutorConfiguration +assert config.verbose +assert not config.debug +assert len(config.test_cases) == 2 +assert len(config.stages) == 2 +assert len(config.staged_test_cases) == 4 +assert len(config.test_case_clss) == 5 +assert len(config.StagedAutomotiveTestCase.items()) == 2 +assert len(config.StagedTest.items()) == 2 +assert len(config.TestCase1.items()) == 2 +assert len(config.TestCase2.items()) == 2 +assert len(config.MyTestCase.items()) == 2 + +assert isinstance(tce.socket, SingleConversationSocket) + +assert len(tce.state_paths) == 1 +assert len(tce.final_states) == 1 + +tce.state_graph.add_edge((tce.final_states[0], EcuState(session=2))) + +assert len(tce.state_paths) == 2 +assert len(tce.final_states) == 2 + +assert not tce.scan_completed + + += Reset Handler tests + +reset_flag = False + +def reset_func(): + global reset_flag + reset_flag = True + +tce = Scanner(MockSock(), reset_handler=reset_func) +tce.target_state = EcuState(session=2) +tce.reset_target() + +assert reset_flag +assert tce.target_state == EcuState(session=1) + += Reset Handler tests 2 + +tce = Scanner(MockSock()) +tce.target_state = EcuState(session=2) +tce.reset_target() + +assert tce.target_state == EcuState(session=1) + += Reconnect Handler tests + +class MockSocket2: + closed = False + +def reconnect_func(): + return MockSocket2() + +tce = Scanner(MockSock(), reconnect_handler=reconnect_func) + +print(tce.socket) +print(repr(tce.socket)) +assert isinstance(tce.socket._inner, MockSock) +tce.reconnect() +assert isinstance(tce.socket._inner, MockSocket2) + += Reconnect Handler tests 2 + +closed = False + +class MockSocket1: + closed = False + def close(self): + global closed + closed = True + +class MockSocket2: + closed = False + +def reconnect_func(): + return MockSocket2() + +tce = Scanner(MockSocket1(), reconnect_handler=reconnect_func) + +print(tce.socket) +print(repr(tce.socket)) +assert isinstance(tce.socket._inner, MockSocket1) +tce.reconnect() +assert isinstance(tce.socket._inner, MockSocket2) +assert closed + += TestCase execute + +pre_exec = False +execute = False +post_exec = False + +class TestCase42(MyTestCase): + def pre_execute(self, + socket, # type: _SocketUnion + state, # type: EcuState + global_configuration # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + ): # type: (...) -> None + global pre_exec + assert state == EcuState(session=1) + assert global_configuration.TestCase42["local_kwarg"] == 42 + assert global_configuration.TestCase42["verbose"] + assert global_configuration.TestCase42["debug"] + global_configuration.TestCase42["local_kwarg"] = 1 + pre_exec = True + def execute(self, socket, state, local_kwarg, verbose, debug, **kwargs): + global execute + assert verbose + assert debug + assert local_kwarg == 1 + execute = True + def post_execute(self, + socket, # type: _SocketUnion + state, # type: EcuState + global_configuration # type: AutomotiveTestCaseExecutorConfiguration # noqa: E501 + ): # type: (...) -> None + global post_exec + assert global_configuration.TestCase42["local_kwarg"] == 1 + assert global_configuration.TestCase42["verbose"] + assert global_configuration.TestCase42["debug"] + post_exec = True + + +tce = Scanner(MockSock(), test_cases=[TestCase42], + verbose=True, debug=True, + TestCase42_kwargs={"local_kwarg": 42}) + +tce.execute_test_case(TestCase42()) +assert pre_exec == execute == post_exec == True + + += TestCase execute StateGenerator + +transition_done = False + +def transition_func(sock, conf, kwargs): + assert kwargs["arg42"] == "hello" + assert conf.TestCase43["local_kwarg"] == "world" + global transition_done + transition_done = True + return True + +class TestCase43(MyTestCase, StateGenerator): + def get_new_edge(self, socket, config): + assert config.TestCase43["local_kwarg"] == "world" + return EcuState(session=1), EcuState(session=2) + def get_transition_function(self, socket, edge): + assert edge[0] == EcuState(session=1) + assert edge[1] == EcuState(session=2) + return transition_func, {"arg42": "hello"}, None + def execute(self, socket, state, **kwargs): + return True + + +tce = Scanner(MockSock(), test_cases=[TestCase43], + TestCase43_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 + +tce.execute_test_case(TestCase43()) + +assert len(tce.final_states) == 2 +assert EcuState(session=1) in tce.final_states and EcuState(session=2) in tce.final_states +assert tce.enter_state(EcuState(session=1), EcuState(session=2)) +assert transition_done + + += TestCase execute StateGenerator no edge + +class TestCase43(MyTestCase, StateGenerator): + def get_new_edge(self, socket, config): + assert config.TestCase43["local_kwarg"] == "world" + return None + def execute(self, socket, state, **kwargs): + return True + def get_transition_function(self, socket, edge): + raise NotImplementedError() + +tce = Scanner(MockSock(), test_cases=[TestCase43], + TestCase43_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 + +tce.execute_test_case(TestCase43()) + +assert len(tce.final_states) == 1 +assert EcuState(session=1) in tce.final_states +assert not tce.enter_state(EcuState(session=1), EcuState(session=2)) + + += TestCase execute StateGenerator with cleanupfunc + +transition_done = False +cleanup_done = False + +def transition_func(sock, conf, kwargs): + assert kwargs["arg42"] == "hello" + assert conf.TestCase43["local_kwarg"] == "world" + global transition_done + transition_done = True + return True + +def cleanup_func(sock, conf): + assert conf.TestCase43["local_kwarg"] == "world" + global cleanup_done + cleanup_done = True + return True + +class TestCase43(MyTestCase, StateGenerator): + def get_new_edge(self, socket, config): + assert config.TestCase43["local_kwarg"] == "world" + return EcuState(session=1), EcuState(session=2) + def get_transition_function(self, socket, edge): + assert edge[0] == EcuState(session=1) + assert edge[1] == EcuState(session=2) + return transition_func, {"arg42": "hello"}, cleanup_func + def execute(self, socket, state, **kwargs): + return True + + +tce = Scanner(MockSock(), test_cases=[TestCase43], + TestCase43_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 + +tce.execute_test_case(TestCase43()) + +assert len(tce.final_states) == 2 +assert EcuState(session=1) in tce.final_states and EcuState(session=2) in tce.final_states +assert not len(tce.cleanup_functions) +assert tce.enter_state(EcuState(session=1), EcuState(session=2)) +assert transition_done +assert len(tce.cleanup_functions) +tce.cleanup_state() +assert not len(tce.cleanup_functions) +assert cleanup_done + + += TestCase execute StateGenerator with not callable cleanupfunc + +transition_done = False + +def transition_func(sock, conf, kwargs): + assert kwargs["arg42"] == "hello" + assert conf.TestCase43["local_kwarg"] == "world" + global transition_done + transition_done = True + return True + +class TestCase43(MyTestCase, StateGenerator): + def get_new_edge(self, socket, config): + assert config.TestCase43["local_kwarg"] == "world" + return EcuState(session=1), EcuState(session=2) + def get_transition_function(self, socket, edge): + assert edge[0] == EcuState(session=1) + assert edge[1] == EcuState(session=2) + return transition_func, {"arg42": "hello"}, "fake" + def execute(self, socket, state, **kwargs): + return True + + +tce = Scanner(MockSock(), test_cases=[TestCase43], + TestCase43_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 + +tce.execute_test_case(TestCase43()) + +assert len(tce.final_states) == 2 +assert EcuState(session=1) in tce.final_states and EcuState(session=2) in tce.final_states +assert not len(tce.cleanup_functions) +assert tce.enter_state(EcuState(session=1), EcuState(session=2)) +assert transition_done +assert len(tce.cleanup_functions) +tce.cleanup_state() +assert not len(tce.cleanup_functions) + += TestCase execute StateGenerator with cleanupfunc negative return + +transition_done = False +cleanup_done = False + +def transition_func(sock, conf, kwargs): + assert kwargs["arg42"] == "hello" + assert conf.TestCase43["local_kwarg"] == "world" + global transition_done + transition_done = True + return True + +def cleanup_func(sock, conf): + assert conf.TestCase43["local_kwarg"] == "world" + global cleanup_done + cleanup_done = True + return False + +class TestCase43(MyTestCase, StateGenerator): + def get_new_edge(self, socket, config): + assert config.TestCase43["local_kwarg"] == "world" + return EcuState(session=1), EcuState(session=2) + def get_transition_function(self, socket, edge): + assert edge[0] == EcuState(session=1) + assert edge[1] == EcuState(session=2) + return transition_func, {"arg42": "hello"}, cleanup_func + def execute(self, socket, state, **kwargs): + return True + + +tce = Scanner(MockSock(), test_cases=[TestCase43], + TestCase43_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 + +tce.execute_test_case(TestCase43()) + +assert len(tce.final_states) == 2 +assert EcuState(session=1) in tce.final_states and EcuState(session=2) in tce.final_states +assert not len(tce.cleanup_functions) +assert tce.enter_state(EcuState(session=1), EcuState(session=2)) +assert transition_done +assert len(tce.cleanup_functions) +tce.cleanup_state() +assert not len(tce.cleanup_functions) +assert cleanup_done + + += TestCase execute StateGenerator with cleanupfunc and path + +transition_done1 = False +cleanup_done1 = False +transition_done2 = False +cleanup_done2 = False + +transition_error = False + + +def transition_func1(sock, conf, kwargs): + global transition_done1 + transition_done1 = True + return True + +def cleanup_func1(sock, conf): + global cleanup_done1 + cleanup_done1 = True + return True + +def transition_func2(sock, conf, kwargs): + global transition_done2 + transition_done2 = True + return not transition_error + +def cleanup_func2(sock, conf): + global cleanup_done2 + cleanup_done2 = True + return True + +class TestCase43(MyTestCase, StateGenerator): + def get_new_edge(self, socket, config): + return EcuState(session=1), EcuState(session=2) + def get_transition_function(self, socket, edge): + return transition_func1, {"arg42": "hello"}, cleanup_func1 + def execute(self, socket, state, **kwargs): + return True + +class TestCase44(MyTestCase, StateGenerator): + def get_new_edge(self, socket, config): + return EcuState(session=2), EcuState(session=3) + def get_transition_function(self, socket, edge): + return transition_func2, None, cleanup_func2 + def execute(self, socket, state, **kwargs): + return True + +reset_done = False + +def reset_func(): + global reset_done + reset_done = True + +reconnect_done = False + +def reconnect_func(): + global reconnect_done + reconnect_done = True + return MockSock() + +tce = Scanner(MockSock(), test_cases=[TestCase43, TestCase44], + reset_handler=reset_func, reconnect_handler=reconnect_func) + +assert len(tce.final_states) == 1 + +tce.execute_test_case(TestCase43()) + +assert len(tce.final_states) == 2 +assert EcuState(session=1) in tce.final_states and EcuState(session=2) in tce.final_states +tce.execute_test_case(TestCase44()) +assert len(tce.final_states) == 3 +assert EcuState(session=3) in tce.final_states and EcuState(session=2) in tce.final_states + +assert not len(tce.cleanup_functions) +assert tce.enter_state_path([EcuState(session=1), EcuState(session=2), EcuState(session=3)]) +assert transition_done1 +assert transition_done2 +assert len(tce.cleanup_functions) == 2 +assert reconnect_done +assert reset_done +tce.cleanup_state() +assert cleanup_done1 +assert cleanup_done2 + +try: + tce.enter_state_path([EcuState(session=3)]) + assert False +except Scapy_Exception: + assert True + += Test downrate edge + +transition_done1 = False +cleanup_done1 = False + +tce = Scanner(MockSock(), test_cases=[TestCase43, TestCase44], + reset_handler=reset_func, reconnect_handler=reconnect_func) + +assert len(tce.final_states) == 1 +tce.execute_test_case(TestCase43()) +assert len(tce.final_states) == 2 +assert EcuState(session=1) in tce.final_states and EcuState(session=2) in tce.final_states +tce.execute_test_case(TestCase44()) +assert len(tce.final_states) == 3 +assert EcuState(session=3) in tce.final_states and EcuState(session=2) in tce.final_states + +assert not len(tce.cleanup_functions) +transition_error = True +assert not tce.enter_state_path([EcuState(session=1), EcuState(session=2), EcuState(session=3)]) +assert transition_done1 +assert cleanup_done1 +assert len(tce.cleanup_functions) == 0 +assert tce.state_graph.weights[(EcuState(session=1), EcuState(session=2))] == 1 +assert tce.state_graph.weights[(EcuState(session=2), EcuState(session=3))] == 2 + + += TestCase execute TestCaseGenerator + +tc_executed = False + +class GeneratedTestCase(MyTestCase): + def execute(self, socket, state, **kwargs): + assert kwargs["local_kwarg"] == "world" + global tc_executed + tc_executed = True + return True + + +class TestCase43(MyTestCase, TestCaseGenerator): + def execute(self, socket, state, **kwargs): + return True + def get_generated_test_case(self): + return GeneratedTestCase() + + +tce = Scanner(MockSock(), test_cases=[TestCase43], + GeneratedTestCase_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 +assert len(tce.configuration.test_cases) == 1 + +tce.execute_test_case(tce.configuration.test_cases[0]) + +assert len(tce.configuration.test_cases) == 2 + +tce.execute_test_case(tce.configuration.test_cases[1]) + +assert tc_executed + += TestCase scan timeout + +tc_executed = False + +class GeneratedTestCase(MyTestCase): + def execute(self, socket, state, **kwargs): + assert kwargs["local_kwarg"] == "world" + global tc_executed + tc_executed = True + return True + + +tce = Scanner(MockSock(), test_cases=[GeneratedTestCase], + GeneratedTestCase_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 +assert len(tce.configuration.test_cases) == 1 + +tce.scan(-1) + +assert not tc_executed + + += TestCase scan + +tc_executed = False + +class GeneratedTestCase(MyTestCase): + def execute(self, socket, state, **kwargs): + assert kwargs["local_kwarg"] == "world" + global tc_executed + tc_executed = True + self._state_completed[state] = True + return True + + +class TestCase43(MyTestCase, TestCaseGenerator): + def execute(self, socket, state, **kwargs): + self._state_completed[state] = True + return True + def get_generated_test_case(self): + return GeneratedTestCase() + + +tce = Scanner(MockSock(), test_cases=[TestCase43], + GeneratedTestCase_kwargs={"local_kwarg": "world"}) + +assert len(tce.final_states) == 1 +assert len(tce.configuration.test_cases) == 1 + +tce.scan() + +assert len(tce.configuration.test_cases) == 2 +assert tc_executed +assert tce.scan_completed + += Test supported responses + +class MyTestCase1(AutomotiveTestCase): + _description = "MyTestCase1" + _supported_kwargs = copy.copy(AutomotiveTestCase._supported_kwargs) + _supported_kwargs.update({ + 'stop_event': (threading.Event, None), # type: ignore + }) + @property + def supported_responses(self): + return [EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"de")), + EcuResponse([EcuState(session=2), EcuState(security_level=6)], responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"dea2")), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x13))] + + +class MyTestCase2(AutomotiveTestCase): + _description = "MyTestCase2" + _supported_kwargs = copy.copy(AutomotiveTestCase._supported_kwargs) + _supported_kwargs.update({ + 'stop_event': (threading.Event, None), # type: ignore + }) + @property + def supported_responses(self): + return [EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef1")), + EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=6) / Raw(b"deadbeef2")), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x10, requestServiceId=0x11))] + + +tce = Scanner(MockSock(), test_cases=[MyTestCase1(), MyTestCase2()]) + +resps = tce.supported_responses + +assert len(resps) == 6 +assert resps[0].responses[0].service != 0x7f +assert resps[1].responses[0].service != 0x7f +assert resps[2].responses[0].service != 0x7f +assert resps[3].responses[0].service != 0x7f + +assert resps[4].responses[0].service == 0x7f +assert resps[5].responses[0].service == 0x7f + +assert resps[0].responses[0].load == b"dea2" +assert resps[1].responses[0].load == b"deadbeef1" +assert resps[2].responses[0].load == b"deadbeef2" +assert resps[3].responses[0].load == b"de" +assert resps[4].responses[0].requestServiceId == 0x13 +assert resps[5].responses[0].requestServiceId == 0x11 + += Test show testcases + +try: + tce.show_testcases() + assert True +except Exception: + assert False + + +try: + tce.show_testcases_status() + assert True +except Exception: + assert False + += Test StateGeneratingServiceEnumerator + +class TestCase43(MyTestCase, StateGeneratingServiceEnumerator): + def execute(self, socket, state, **kwargs): + return True + @property + def results(self): # type: () -> List[_AutomotiveTestCaseScanResult] + return [_AutomotiveTestCaseScanResult(EcuState(session=1), UDS()/UDS_DSC(b"\x03"), UDS()/UDS_DSCPR(b"\x03"), 1.1, 1.2)] + +tce = Scanner(MockSock(), test_cases=[TestCase43]) + +assert len(tce.final_states) == 1 + +tce.execute_test_case(TestCase43()) + +assert len(tce.final_states) == 2 +assert EcuState(session=1) in tce.final_states and EcuState(session=3) in tce.final_states + +tf, args, cf = tce.state_graph.get_transition_tuple_for_edge((EcuState(session=1), EcuState(session=3))) + +assert cf is None +assert tf is not None +assert len(args) == 2 +assert args["req"] == UDS()/UDS_DSC(b"\x03") +assert "diagnosticSessionType" in args["desc"] and "extendedDiagnosticSession" in args["desc"] + +assert not tce.enter_state(EcuState(session=1), EcuState(session=3)) diff --git a/test/contrib/automotive/scanner/graph.uts b/test/contrib/automotive/scanner/graph.uts new file mode 100644 index 00000000000..8cd11dcd97d --- /dev/null +++ b/test/contrib/automotive/scanner/graph.uts @@ -0,0 +1,75 @@ +% Regression tests for graph + ++ Load general modules + += Load contribution layer + +from scapy.contrib.automotive.scanner.graph import * +import pickle +import io + ++ Graph tests + += Basic test + +g = Graph() +g.add_edge(("1", "1")) +g.add_edge(("1", "2")) +g.add_edge(("2", "3")) +g.add_edge(("3", "4")) +g.add_edge(("4", "4")) + +assert "1" in g.nodes +assert "2" in g.nodes +assert "3" in g.nodes +assert "4" in g.nodes +assert len(g.nodes) == 4 +assert g.dijkstra(g, "1", "4") == ["1", "2", "3", "4"] + += Shortest path test + +g = Graph() +g.add_edge(("1", "1")) +g.add_edge(("1", "2")) +g.add_edge(("2", "3")) +g.add_edge(("3", "4")) +g.add_edge(("4", "4")) + +assert g.dijkstra(g, "1", "4") == ["1", "2", "3", "4"] + +g.add_edge(("1", "4")) + +assert g.dijkstra(g, "1", "4") == ["1", "4"] + +g.add_edge(("3", "5")) +g.add_edge(("5", "6")) + +print(g.dijkstra(g, "1", "6")) + +assert g.dijkstra(g, "1", "6") == ["1", "2", "3", "5", "6"] or \ + g.dijkstra(g, "1", "6") == ['1', '4', '3', '5', '6'] + +g.add_edge(("2", "5")) + +print(g.dijkstra(g, "1", "6")) + +assert g.dijkstra(g, "1", "6") == ["1", "2", "5", "6"] + += graph add transition function + +g.add_edge(("4", "6"), transition_function=(str, str)) + +assert g.dijkstra(g, "1", "6") == ["1", "4", "6"] + += graph pickle + +f = io.BytesIO() + +pickle.dump(g, f) +unp = pickle.loads(f.getvalue()) + +assert unp.dijkstra(g, "1", "6") == ["1", "4", "6"] + +f1, f2 = unp.get_transition_tuple_for_edge(("4", "6")) +assert f1==f2 +assert "1" == f1(1) diff --git a/test/contrib/automotive/scanner/staged_test_case.uts b/test/contrib/automotive/scanner/staged_test_case.uts new file mode 100644 index 00000000000..cc6d37d3144 --- /dev/null +++ b/test/contrib/automotive/scanner/staged_test_case.uts @@ -0,0 +1,251 @@ +% Regression tests for automotive scanner staged test_case + ++ Load general modules + += Load contribution layer + +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCase +from scapy.contrib.automotive.ecu import EcuState, EcuResponse +from scapy.contrib.automotive.scanner.staged_test_case import StagedAutomotiveTestCase +from scapy.contrib.automotive.uds import UDS, UDS_RDBIPR, UDS_NR +from scapy.packet import Raw + ++ Basic checks + += Definition of Test classes + + +class MyTestCase1(AutomotiveTestCase): + _description = "MyTestCase1" + @property + def supported_responses(self): + return [EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=2) / Raw(b"de")), + EcuResponse([EcuState(session=2), EcuState(security_level=6)], responses=UDS() / UDS_RDBIPR(dataIdentifier=3) / Raw(b"dea2")), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x13))] + + +class MyTestCase2(AutomotiveTestCase): + _description = "MyTestCase2" + @property + def supported_responses(self): + return [EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=5) / Raw(b"deadbeef1")), + EcuResponse(EcuState(session=2), responses=UDS() / UDS_RDBIPR(dataIdentifier=6) / Raw(b"deadbeef2")), + EcuResponse(EcuState(session=range(0,255)), responses=UDS() / UDS_NR(negativeResponseCode=0x10, requestServiceId=0x11))] + + += Create instance of stage test + +tc1 = MyTestCase1() +tc2 = MyTestCase2() + +mt = StagedAutomotiveTestCase([tc1, tc2]) + +assert len(mt.test_cases) == 2 +assert mt.current_test_case == tc1 +assert mt.current_connector == None +assert mt.previous_test_case == None +assert mt[0] == tc1 +assert mt[1] == tc2 + += Check completion + +tc1 = MyTestCase1() +tc2 = MyTestCase2() + +mt = StagedAutomotiveTestCase([tc1, tc2]) + +tc1._state_completed[EcuState(session=1)] = False +tc2._state_completed[EcuState(session=1)] = False + +assert not mt.completed +assert not mt.has_completed(EcuState(session=1)) + +tc1._state_completed[EcuState(session=1)] = True +assert mt.current_test_case == tc1 +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert mt.current_test_case == tc2 +assert not mt.completed + +tc2._state_completed[EcuState(session=1)] = True +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert mt.completed +assert mt.has_completed(EcuState(session=1)) + += Check completion 2 + +tc1 = MyTestCase1() +tc2 = MyTestCase2() + +mt = StagedAutomotiveTestCase([tc1, tc2]) + +tc1._state_completed[EcuState(session=1)] = False +tc2._state_completed[EcuState(session=1)] = False + +assert not mt.completed +assert not mt.has_completed(EcuState(session=1)) + +tc1._state_completed[EcuState(session=1)] = True +assert mt.current_test_case == tc1 +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +tc1._state_completed[EcuState(session=1)] = False +assert not mt.has_completed(EcuState(session=1)) +tc1._state_completed[EcuState(session=1)] = True +assert mt.current_test_case == tc1 +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) + +assert mt.current_test_case == tc2 +assert not mt.completed + +tc2._state_completed[EcuState(session=1)] = True +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert mt.completed +assert mt.has_completed(EcuState(session=1)) + += Check supported responses + +tc1 = MyTestCase1() +tc2 = MyTestCase2() +tx = StagedAutomotiveTestCase([tc1, tc2]) +resps = tx.supported_responses + +assert len(resps) == 6 +assert resps[0].responses[0].service != 0x7f +assert resps[1].responses[0].service != 0x7f +assert resps[2].responses[0].service != 0x7f +assert resps[3].responses[0].service != 0x7f + +assert resps[4].responses[0].service == 0x7f +assert resps[5].responses[0].service == 0x7f + +assert resps[0].responses[0].load == b"dea2" +assert resps[1].responses[0].load == b"deadbeef1" +assert resps[2].responses[0].load == b"deadbeef2" +assert resps[3].responses[0].load == b"de" +assert resps[4].responses[0].requestServiceId == 0x13 +assert resps[5].responses[0].requestServiceId == 0x11 + + += Check connector + +test_storage_tc2 = None + +class MyTestCase2(AutomotiveTestCase): + _description = "MyTestCase2" + def pre_execute(self, socket, state, global_configuration): + global test_storage_tc2 + print(global_configuration) + test_storage_tc2 = global_configuration + def supported_responses(self): + return [] + +test_storage_tc3 = None + +class MyTestCase3(AutomotiveTestCase): + _description = "MyTestCase3" + def pre_execute(self, socket, state, global_configuration): + global test_storage_tc3 + print(global_configuration) + test_storage_tc3 = global_configuration + def supported_responses(self): + return [] + +def con1(tc1, tc2): + assert isinstance(tc1, MyTestCase1) + assert isinstance(tc2, MyTestCase2) + return {"tc2_con_config": 42} + +def con2(tc2, tc3): + assert isinstance(tc2, MyTestCase2) + assert isinstance(tc3, MyTestCase3) + return {"tc3_con_config": "deadbeef"} + +tc1 = MyTestCase1() +tc2 = MyTestCase2() +tc3 = MyTestCase3() + +assert test_storage_tc2 is None +assert test_storage_tc3 is None + +mt = StagedAutomotiveTestCase([tc1, tc2, tc3], [None, con1, con2]) + +assert mt.current_test_case == tc1 +assert mt.current_connector == None + +#Move stage forward +tc1._state_completed[EcuState(session=1)] = True +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) + +assert mt.current_test_case == tc2 +assert mt.current_connector == con1 + +mt.pre_execute(None, None, {"MyTestCase2": {"verbose": True, "config": "whatever"}}) + +assert test_storage_tc2["MyTestCase2"]["verbose"] +assert test_storage_tc2["MyTestCase2"]["tc2_con_config"] == 42 +assert test_storage_tc2["MyTestCase2"]["config"] == "whatever" + +#Move stage forward +tc2._state_completed[EcuState(session=1)] = True +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) +assert not mt.has_completed(EcuState(session=1)) + +assert mt.current_test_case == tc3 +assert mt.current_connector == con2 + +mt.pre_execute(None, None, {}) + +assert test_storage_tc3["MyTestCase3"]["tc3_con_config"] == "deadbeef" + += Check show + +dump = mt.show(dump=True) + +assert "MyTestCase1" in dump +assert "MyTestCase2" in dump +assert "MyTestCase3" in dump + += Check len + +assert len(mt) == 3 + += Check generator functions + +assert mt.get_generated_test_case() == None +assert mt.get_new_edge(None, None) == None +assert mt.get_transition_function(None, None) == None + + + + + diff --git a/test/contrib/automotive/scanner/test_case.uts b/test/contrib/automotive/scanner/test_case.uts new file mode 100644 index 00000000000..f910be6e457 --- /dev/null +++ b/test/contrib/automotive/scanner/test_case.uts @@ -0,0 +1,101 @@ +% Regression tests for automotive scanner test_case + ++ Load general modules + += Load contribution layer + +from scapy.contrib.automotive.scanner.test_case import AutomotiveTestCase +from scapy.contrib.automotive.ecu import EcuState + ++ Basic checks + += Definition of Test class + +class MyTestCase(AutomotiveTestCase): + _description = "MyTestCase" + _supported_kwargs = {"testarg": (int, None)} + def supported_responses(self): + return [] + += Check supported kwargs + +try: + MyTestCase.check_kwargs({"testarg": 5}) +except Scapy_Exception as e: + assert False + +try: + MyTestCase.check_kwargs({"test": 5}) + assert False +except Scapy_Exception as e: + assert "Keyword-Argument test not supported" in str(e) + +try: + MyTestCase.check_kwargs({"testarg": 5.5}) + assert False +except Scapy_Exception as e: + assert "Keyword-Value" in str(e) + assert "is not instance of type " in str(e) or \ + "is not instance of type " in str(e) + += Create instance of test class + +mt = MyTestCase() + +mt._state_completed[EcuState(session=1)] = True +mt._state_completed[EcuState(session=2)] = True +mt._state_completed[EcuState(session=3)] = False + += Tests of has_completed + +assert mt.completed is False +assert mt.has_completed(EcuState(session=1)) +assert mt.has_completed(EcuState(session=3)) is False + +assert len(mt.scanned_states) == 3 + += Tests of has_completed with new state + +assert mt.completed is False +assert mt.has_completed(EcuState(session=4)) is False +assert mt.has_completed(EcuState(session=3)) is False + +assert len(mt.scanned_states) == 4 + += Tests of completed + +mt._state_completed[EcuState(session=3)] = True +mt._state_completed[EcuState(session=4)] = True + +assert mt.completed + += Test of show + +header = mt._show_header(dump=True) + +assert "MyTestCase" in header + +state_info = mt._show_state_information(dump=True) + +assert "session" in state_info +assert "False" not in state_info +assert "True" in state_info + +mt._state_completed[EcuState(session=3)] = False +state_info = mt._show_state_information(dump=True) + +assert "session" in state_info +assert "False" in state_info +assert "True" in state_info + +dump = mt.show(dump=True, verbose=True) + +assert "session" in dump +assert "MyTestCase" in dump + + + + + + + diff --git a/test/contrib/automotive/scanner/uds_scanner.uts b/test/contrib/automotive/scanner/uds_scanner.uts new file mode 100644 index 00000000000..49649b52d4c --- /dev/null +++ b/test/contrib/automotive/scanner/uds_scanner.uts @@ -0,0 +1,1331 @@ +% Regression tests for Simulated ECUs and UDS Scanners +~ scanner + ++ Configuration +~ conf + += Imports +import io +import pickle +from scapy.contrib.isotp import ISOTPMessageBuilder +from test.testsocket import TestSocket, cleanup_testsockets, UnstableSocket +from scapy.automaton import ObjectPipe + +############ +############ ++ Load general modules + += Load contribution layer + + +from scapy.contrib.automotive.uds import * +from scapy.contrib.automotive.uds_ecu_states import * +from scapy.contrib.automotive.uds_scan import * +from scapy.contrib.automotive.ecu import * + +load_layer("can") + +conf.debug_dissector = False + + += Define Testfunction + +def executeScannerInVirtualEnvironment(supported_responses, enumerators, unstable_socket=True, software_reset=False, **kwargs): + tester_obj_pipe = ObjectPipe(name="TesterPipe") + ecu_obj_pipe = ObjectPipe(name="ECUPipe") + TesterSocket = UnstableSocket if unstable_socket else TestSocket + tester = TesterSocket(UDS, tester_obj_pipe) + ecu = TestSocket(UDS, ecu_obj_pipe) + tester.pair(ecu) + answering_machine = EcuAnsweringMachine( + supported_responses=supported_responses, main_socket=ecu, + basecls=UDS, verbose=False) + def reset(): + answering_machine.state.reset() + answering_machine.state["session"] = 1 + sniff(timeout=0.001, opened_socket=[ecu, tester]) + def reconnect(): + try: + tester.close() + except Exception: + pass + tester = TesterSocket(UDS, tester_obj_pipe) + ecu.pair(tester) + return tester + def answering_machine_thread(): + answering_machine( + timeout=120, stop_filter=lambda x: bytes(x) == b"\xff\xff\xff") + sim = threading.Thread(target=answering_machine_thread) + try: + sim.start() + if software_reset: + scanner = UDS_Scanner( + tester, + software_reset_handler=uds_software_reset, + reconnect_handler=reconnect, + test_cases=enumerators, + timeout=0.1, + retry_if_none_received=True, + unittest=True, + **kwargs) + else: + scanner = UDS_Scanner( + tester, + reset_handler=reset, + reconnect_handler=reconnect, + test_cases=enumerators, + timeout=0.1, + retry_if_none_received=True, + unittest=True, + **kwargs) + for i in range(12): + print("Starting scan") + scanner.scan(timeout=10) + if scanner.scan_completed: + print("Scan completed after %d iterations" % i) + break + finally: + ecu.ins.send(Raw(b"\xff\xff\xff")) + sim.join(timeout=2) + assert not sim.is_alive() + cleanup_testsockets() + tester_obj_pipe.close() + ecu_obj_pipe.close() + if LINUX: + pickle_test(scanner) + return scanner + +def pickle_test(scanner): + f = io.BytesIO() + pickle.dump(scanner, f) + unp = pickle.loads(f.getvalue()) + assert scanner.scan_completed == unp.scan_completed + assert scanner.state_paths == unp.state_paths + += Load packets from pcap + +conf.contribs['CAN']['swap-bytes'] = True +pkts = rdpcap(scapy_path("test/pcaps/candump_uds_scanner.pcap.gz")) +assert len(pkts) + += Create UDS messages from packets + +builder = ISOTPMessageBuilder(basecls=UDS, use_ext_address=False, rx_id=[0x641, 0x651]) +msgs = list() + +for p in pkts: + if p.data == b"ECURESET": + msgs.append(p) + else: + builder.feed(p) + if len(builder): + msgs.append(builder.pop()) + +assert len(msgs) + += Create ECU-Clone from packets + +mEcu = Ecu(logging=False, verbose=False, store_supported_responses=True, lookahead=3) + +for p in msgs: + if isinstance(p, CAN) and p.data == b"ECURESET": + mEcu.reset() + else: + mEcu.update(p) + +assert len(mEcu.supported_responses) + += Test UDS_SAEnumerator evaluate_response + +e = UDS_SAEnumerator() + +config = {} + +s = EcuState(session=1) + +assert False == e._evaluate_response(s, UDS(b"\x27\x01"), None, **config) +config = {"exit_if_service_not_supported": True} +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, UDS(b"\x27\x01"), UDS(b"\x7f\x27\x11"), **config) +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, UDS(b"\x27\x01"), UDS(b"\x7f\x27\x24"), **config) +assert e._retry_pkt[s] == UDS(b"\x27\x01") +assert False == e._evaluate_response(s, UDS(b"\x27\x02"), UDS(b"\x7f\x27\x24"), **config) +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, UDS(b"\x27\x01"), UDS(b"\x7f\x27\x37"), **config) +assert e._retry_pkt[s] == UDS(b"\x27\x01") +assert False == e._evaluate_response(s, UDS(b"\x27\x01"), UDS(b"\x7f\x27\x37"), **config) +assert not e._retry_pkt[s] +assert True == e._evaluate_response(s, UDS(b"\x27\x01"), UDS(b"\x67\x01ab"), **config) +assert not e._retry_pkt[s] +assert False == e._evaluate_response(s, UDS(b"\x27\x01"), UDS(b"\x67\x02ab"), **config) +assert not e._retry_pkt[s] + + += Test UDS_SA_XOR_Enumerator stand alone mode + +TesterSocket = TestSocket +ecu_sock = TestSocket(UDS) +mTester = TesterSocket(UDS) +ecu_sock.pair(mTester) +answering_machine = EcuAnsweringMachine(supported_responses=mEcu.supported_responses, main_socket=ecu_sock, basecls=UDS, verbose=False) +sim = threading.Thread(target=answering_machine, kwargs={'timeout': 1000, "stop_filter": lambda x: bytes(x) == b"\xff\xff\xff"}) +sim.start() +try: + resp = mTester.sr1(UDS()/UDS_TP(b"\x00"), verbose=False, timeout=1) + print(repr(resp)) + assert resp and resp.service != 0x7f + resp = mTester.sr1(UDS()/UDS_DSC(diagnosticSessionType=3), verbose=False, timeout=1) + print(repr(resp)) + assert resp and resp.service != 0x7f + assert UDS_SA_XOR_Enumerator().get_security_access(mTester, 1) +finally: + mTester.send(Raw(b"\xff\xff\xff")) + sim.join(timeout=2) + cleanup_testsockets() + + += Test configuration validation + +try: + scanner = UDS_Scanner(TestSocket(UDS), + test_cases=[UDS_SA_XOR_Enumerator, UDS_DSCEnumerator, UDS_ServiceEnumerator], + UDS_DSCEnumerator_kwargs={"scan_range": range(0x1000), "delay_state_change": 0, + "overwrite_timeout": False}) + assert False +except Scapy_Exception: + pass + += Simulate ECU and run Scanner + +scanner = executeScannerInVirtualEnvironment( + mEcu.supported_responses, + [UDS_SA_XOR_Enumerator, UDS_DSCEnumerator, UDS_ServiceEnumerator], + UDS_DSCEnumerator_kwargs={"scan_range": range(5), "delay_state_change": 0, + "overwrite_timeout": False}, + UDS_SA_XOR_Enumerator_kwargs={"scan_range": range(5)}, + UDS_ServiceEnumerator_kwargs={"scan_range": [0x10, 0x11, 0x14, 0x19, 0x22, + 0x23, 0x24, 0x27, 0x28, 0x29, + 0x2A, 0x2C, 0x2E, 0x2F, 0x31, + 0x34, 0x35, 0x36, 0x37, 0x38, + 0x3D, 0x3E, 0x83, 0x84, 0x85, + 0x87], + "request_length": 1}) + +scanner.show_testcases() +scanner.show_testcases_status() +assert len(scanner.state_paths) == 5 +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +assert EcuState(session=1) in scanner.final_states +assert EcuState(session=2, tp=1) in scanner.final_states +assert EcuState(session=3, tp=1) in scanner.final_states +assert EcuState(session=2, tp=1, security_level=2) in scanner.final_states +assert EcuState(session=3, tp=1, security_level=2) in scanner.final_states + +#################### UDS_SA_XOR_Enumerator ################ +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 19 +assert len(tc.results_with_positive_response) >= 6 +assert len(tc.scanned_states) == 5 + +result = tc.show(dump=True) + +assert "serviceNotSupportedInActiveSession received 5 times" in result +assert "incorrectMessageLengthOrInvalidFormat received 14 times" in result + +################# UDS_DSCEnumerator ##################### +tc = scanner.configuration.test_cases[1] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 20 +assert len(tc.results_with_positive_response) == 5 +assert len(tc.scanned_states) == 5 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat received 20 times" in result + +###################### UDS_ServiceEnumerator ################### +tc = scanner.configuration.test_cases[2] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 130 +assert len(tc.results_with_positive_response) == 0 +assert len(tc.scanned_states) == 5 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat received 34 times" in result +assert "serviceNotSupported received 75 times" in result +assert "serviceNotSupportedInActiveSession received 19 times" in result +assert "securityAccessDenied received 2 times" in result + += Simulate ECU and run Scanner with software resert + +responses = ([EcuResponse(None, [UDS()/UDS_DSCPR(b"\x01")])] + + mEcu.supported_responses) + +scanner = executeScannerInVirtualEnvironment( + responses, + [UDS_SA_XOR_Enumerator, UDS_DSCEnumerator, UDS_ServiceEnumerator], + software_reset=True, + UDS_DSCEnumerator_kwargs={"scan_range": range(5), "delay_state_change": 0, + "overwrite_timeout": False}, + UDS_SA_XOR_Enumerator_kwargs={"scan_range": range(5)}, + UDS_ServiceEnumerator_kwargs={"scan_range": [0x10, 0x11, 0x14, 0x19, 0x22, + 0x23, 0x24, 0x27, 0x28, 0x29, + 0x2A, 0x2C, 0x2E, 0x2F, 0x31, + 0x34, 0x35, 0x36, 0x37, 0x38, + 0x3D, 0x3E, 0x83, 0x84, 0x85, + 0x87], + "request_length": 1}) + +scanner.show_testcases() +scanner.show_testcases_status() +assert len(scanner.state_paths) == 6 +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +assert EcuState(session=1) in scanner.final_states +assert EcuState(session=2, tp=1) in scanner.final_states +assert EcuState(session=1, tp=1) in scanner.final_states +assert EcuState(session=3, tp=1) in scanner.final_states +assert EcuState(session=2, tp=1, security_level=2) in scanner.final_states +assert EcuState(session=3, tp=1, security_level=2) in scanner.final_states + +#################### UDS_SA_XOR_Enumerator ################ +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 24 +assert len(tc.results_with_positive_response) >= 6 +assert len(tc.scanned_states) == 6 + +result = tc.show(dump=True) + +assert "serviceNotSupportedInActiveSession received 5 times" in result +assert "incorrectMessageLengthOrInvalidFormat received 14 times" in result + +################# UDS_DSCEnumerator ##################### +tc = scanner.configuration.test_cases[1] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 17 +assert len(tc.results_with_positive_response) == 13 +assert len(tc.scanned_states) == 6 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat received 14 times" in result + +###################### UDS_ServiceEnumerator ################### +tc = scanner.configuration.test_cases[2] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 156 +assert len(tc.results_with_positive_response) == 0 +assert len(tc.scanned_states) == 6 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat received 34 times" in result +assert "serviceNotSupported received 75 times" in result +assert "serviceNotSupportedInActiveSession received 19 times" in result +assert "securityAccessDenied received 2 times" in result + += Simulate ECU and run Scanner with software resert 2 + +responses = ([EcuResponse(None, [UDS()/UDS_ERPR(b"\x01")])] + + mEcu.supported_responses) + +scanner = executeScannerInVirtualEnvironment( + responses, + [UDS_SA_XOR_Enumerator, UDS_DSCEnumerator, UDS_ServiceEnumerator], + software_reset=True, + UDS_DSCEnumerator_kwargs={"scan_range": range(5), "delay_state_change": 0, + "overwrite_timeout": False}, + UDS_SA_XOR_Enumerator_kwargs={"scan_range": range(5)}, + UDS_ServiceEnumerator_kwargs={"scan_range": [0x10, 0x11, 0x14, 0x19, 0x22, + 0x23, 0x24, 0x27, 0x28, 0x29, + 0x2A, 0x2C, 0x2E, 0x2F, 0x31, + 0x34, 0x35, 0x36, 0x37, 0x38, + 0x3D, 0x3E, 0x83, 0x84, 0x85, + 0x87], + "request_length": 1}) + +scanner.show_testcases() +scanner.show_testcases_status() +assert len(scanner.state_paths) == 5 +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +assert EcuState(session=1) in scanner.final_states +assert EcuState(session=2, tp=1) in scanner.final_states +assert EcuState(session=3, tp=1) in scanner.final_states +assert EcuState(session=2, tp=1, security_level=2) in scanner.final_states +assert EcuState(session=3, tp=1, security_level=2) in scanner.final_states + +#################### UDS_SA_XOR_Enumerator ################ +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 19 +assert len(tc.results_with_positive_response) >= 6 +assert len(tc.scanned_states) == 5 + +result = tc.show(dump=True) + +assert "serviceNotSupportedInActiveSession received 5 times" in result +assert "incorrectMessageLengthOrInvalidFormat received 14 times" in result + +################# UDS_DSCEnumerator ##################### +tc = scanner.configuration.test_cases[1] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 20 +assert len(tc.results_with_positive_response) == 5 +assert len(tc.scanned_states) == 5 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat received 20 times" in result + +###################### UDS_ServiceEnumerator ################### +tc = scanner.configuration.test_cases[2] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 130 +assert len(tc.results_with_positive_response) == 0 +assert len(tc.scanned_states) == 5 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat received 34 times" in result +assert "serviceNotSupported received 75 times" in result +assert "serviceNotSupportedInActiveSession received 19 times" in result +assert "securityAccessDenied received 2 times" in result + + += UDS_ServiceEnumerator + +def req_handler(resp, req): + if req.service != 0x22: + return False + if len(req) == 1: + resp.negativeResponseCode="generalReject" + return True + if len(req) == 2: + resp.negativeResponseCode="incorrectMessageLengthOrInvalidFormat" + return True + if len(req) == 3: + resp.negativeResponseCode="requestOutOfRange" + return True + return False + +resps = [EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ReadDataByIdentifier")], req_handler)] + +es = [UDS_ServiceEnumerator] + +debug_dissector_backup = conf.debug_dissector + +# This Enumerator is sending corrupted Packets, therefore we need to disable the debug_dissector +conf.debug_dissector = False +scanner = executeScannerInVirtualEnvironment( + resps, es, UDS_ServiceEnumerator_kwargs={"request_length": 3}, unstable_socket=False) +conf.debug_dissector = debug_dissector_backup + +assert scanner.scan_completed +assert scanner.progress() > 0.95 +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +tc.show() + +assert len(tc.results_with_negative_response) == 128 * 3 +assert len(tc.results_with_positive_response) == 0 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat" in result +assert "requestOutOfRange" in result + += UDS_RDBIEnumerator + +resps = [EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=2)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0xff)/Raw(b"beefff")]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ReadDataByIdentifier")])] + +es = [UDS_RDBIEnumerator] + +scanner = executeScannerInVirtualEnvironment( + resps, es, UDS_RDBIEnumerator_kwargs={"scan_range": range(0x100)}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 256 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "beef2" in result +assert "beef3" in result +assert "beefff" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.identifiers[0] for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + += UDS_RDBISelectiveEnumerator +~ not_pypy + +resps = [EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x101)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x102)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x103)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x104)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x105)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x106)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x107)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x108)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x109)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x110)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x111)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x112)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x113)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x114)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x115)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x116)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x117)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x118)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x119)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x120)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x121)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x122)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x123)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x124)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x125)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x126)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x127)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x128)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x129)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x130)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x131)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x132)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x133)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x134)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0x135)/Raw(b"beef35")]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ReadDataByIdentifier")])] + +es = [UDS_RDBISelectiveEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es, UDS_RDBIRandomEnumerator_kwargs={"probe_start": 0, "probe_end": 0x500}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.stages[0][0] + +tc.show() + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) > 100 +assert len(tc.results_with_positive_response) >= 1 +assert len(tc.scanned_states) == 1 + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.stages[0][1] + +tc.show() + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 29 +assert len(tc.results_with_positive_response) == 35 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "beef2" in result +assert "beef3" in result +assert "beef35" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.identifiers[0] for t in tc.results_with_positive_response] + +assert 0x101 in ids +assert 0x102 in ids +assert 0x103 in ids +assert 0x135 in ids + += UDS_WDBIEnumerator + +def wdbi_handler(resp, req): + if req.service != 0x2E: + return False + assert req.dataIdentifier in [1, 2, 3, 0xff] + resp.dataIdentifier = req.dataIdentifier + if req.dataIdentifier == 1: + assert req.load == b'asdfbeef1' + return True + if req.dataIdentifier == 2: + assert req.load == b'beef2' + return True + if req.dataIdentifier == 3: + assert req.load == b"beef3" + return True + if req.dataIdentifier == 0xff: + assert req.load == b"beefff" + return True + return False + +resps = [EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=2)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0xff)/Raw(b"beefff")]), + EcuResponse(None, [UDS()/UDS_WDBIPR()], answers=wdbi_handler), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ReadDataByIdentifier")])] + +es = [UDS_WDBISelectiveEnumerator()] + +scanner = executeScannerInVirtualEnvironment( + resps, es, UDS_RDBIEnumerator_kwargs={"scan_range": range(0x100)}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.stages[0][0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 256 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "beef2" in result +assert "beef3" in result +assert "beefff" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.identifiers[0] for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + +######################### WDBI ############################# +tc = scanner.configuration.stages[0][1] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 0 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +ids = [t.req.dataIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + += UDS_WDBIEnumerator 2 + +def wdbi_handler(resp, req): + if req.service != 0x2E: + return False + assert req.dataIdentifier in [1, 2, 3, 0xff] + resp.dataIdentifier = req.dataIdentifier + if req.dataIdentifier == 1: + assert req.load == b'asdfbeef1' + return True + if req.dataIdentifier == 2: + assert req.load == b'beef2' + return True + if req.dataIdentifier == 3: + assert req.load == b"beef3" + return True + if req.dataIdentifier == 0xff: + assert req.load == b"beefff" + return True + return False + +resps = [EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=2)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RDBIPR(dataIdentifier=0xff)/Raw(b"beefff")]), + EcuResponse(None, [UDS()/UDS_WDBIPR()], answers=wdbi_handler), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ReadDataByIdentifier")])] + +es = [UDS_WDBISelectiveEnumerator] + +scanner = executeScannerInVirtualEnvironment( + resps, es, UDS_RDBIEnumerator_kwargs={"scan_range": range(0x100)}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0][0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 256 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "beef2" in result +assert "beef3" in result +assert "beefff" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.identifiers[0] for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + +######################### WDBI ############################# +tc = scanner.configuration.test_cases[0][1] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 0 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +ids = [t.req.dataIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + + += UDS_TPEnumerator + +resps = [EcuResponse(None, [UDS()/UDS_TPPR()]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="serviceNotSupported", requestServiceId="TesterPresent")])] + +es = [UDS_TPEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 0 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 2 + +assert tc.show(dump=True) + += UDS_EREnumerator + +resps = [EcuResponse(None, [UDS()/UDS_ERPR(resetType=1)]), + EcuResponse(None, [UDS()/UDS_ERPR(resetType=3)]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ECUReset")])] + +es = [UDS_EREnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 256 - 2 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "hardReset" in result +assert "softReset" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.resetType for t in tc.results_with_positive_response] + +assert 1 in ids +assert 3 in ids + + += UDS_CCEnumerator + +resps = [EcuResponse(None, [UDS()/UDS_CCPR(controlType=1)]), + EcuResponse(None, [UDS()/UDS_CCPR(controlType=3)]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="CommunicationControl")])] + +es = [UDS_CCEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es, inter=0.001) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 256 - 2 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "enableRxAndDisableTx" in result +assert "disableRxAndTx" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.controlType for t in tc.results_with_positive_response] + +assert 1 in ids +assert 3 in ids + += UDS_RDBPIEnumerator + +UDS_RDBPI.periodicDataIdentifiers[1] = "identifierElectric" +UDS_RDBPI.periodicDataIdentifiers[3] = "identifierGas" + +resps = [EcuResponse(None, [UDS()/UDS_RDBPIPR(periodicDataIdentifier=1, dataRecord=b'electric')]), + EcuResponse(None, [UDS()/UDS_RDBPIPR(periodicDataIdentifier=3, dataRecord=b'gas')]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ReadDataPeriodicIdentifier")])] + +es = [UDS_RDBPIEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 256 - 2 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "electric" in result +assert "gas" in result +assert "0x01 identifierElectric" in result +assert "0x03 identifierGas" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.periodicDataIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 3 in ids + + += UDS_RCEnumerator + +resps = [EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=1, routineIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=2, routineIdentifier=2)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=3, routineIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=1, routineIdentifier=0x10)/Raw(b"beefff")]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="RoutineControl")])] + +es = [UDS_RCEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es, UDS_RCEnumerator_kwargs={"scan_range": range(0x11)}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 0x11 * 3 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "subFunctionNotSupported received" in result + +ids = [t.req.routineIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0x10 in ids + + += UDS_RCSelectiveEnumerator + +resps = [EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=1, routineIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=2, routineIdentifier=1)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=3, routineIdentifier=1)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_RCPR(routineControlType=1, routineIdentifier=0x10)/Raw(b"beefff")]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="RoutineControl")])] + +es = [UDS_RCSelectiveEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es, UDS_RCStartEnumerator_kwargs={"scan_range": range(0x11)}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.stages[0][0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 0x11 - 2 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "subFunctionNotSupported received" in result + +ids = [t.req.routineIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 0x10 in ids + +tc = scanner.configuration.stages[0][1] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 538 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "subFunctionNotSupported received" in result + +ids = [t.req.routineIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids + += UDS_IOCBIEnumerator + +resps = [EcuResponse(None, [UDS()/UDS_IOCBIPR(dataIdentifier=1)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/UDS_IOCBIPR(dataIdentifier=2)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/UDS_IOCBIPR(dataIdentifier=3)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/UDS_IOCBIPR(dataIdentifier=0xff)/Raw(b"beefff")]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="InputOutputControlByIdentifier")])] + +es = [UDS_IOCBIEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es, UDS_IOCBIEnumerator_kwargs={"scan_range": range(0x100)}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 0x100 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "asdfbeef1" in result +assert "beef2" in result +assert "beef3" in result +assert "beefff" in result +assert "subFunctionNotSupported received" in result + +ids = [t.req.dataIdentifier for t in tc.results_with_positive_response] + +assert 1 in ids +assert 2 in ids +assert 3 in ids +assert 0xff in ids + + += UDS_RDEnumerator + +memory = dict() + +for addr in itertools.chain(range(0x1f00), range(0xd000, 0xfff2), range(0xa000, 0xcf00), range(0x2000, 0x5f00)): + memory[addr] = addr & 0xff + +def answers_rd(resp, req): + global memory + if req.service != 0x34: + return False + if req.memorySizeLen in [1, 3, 4]: + return False + if req.memoryAddressLen in [1, 3, 4]: + return False + addr = getattr(req, "memoryAddress%d" % req.memoryAddressLen) + if addr not in memory.keys(): + return False + resp.memorySizeLen = req.memorySizeLen + return True + +resps = [EcuResponse(None, [UDS()/UDS_RDPR()], answers=answers_rd), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="requestOutOfRange", requestServiceId="RequestDownload")])] + +####################################################### +scanner = executeScannerInVirtualEnvironment( + resps, [UDS_RDEnumerator], unstable_socket=False, + UDS_RDEnumerator_kwargs={"unittest": True}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc1 = scanner.configuration.test_cases[0] + +assert len(tc1.results_without_response) < 10 +if len(tc1.results_without_response): + tc1.show() + +assert len(tc1.results_with_negative_response) > 400 +assert len(tc1.results_with_positive_response) > 40 +assert len(tc1.scanned_states) == 1 + +result = tc1.show(dump=True) + +assert "requestOutOfRange received " in result + += UDS_RMBARandomEnumerator + +pkt = UDS_RMBARandomEnumerator._random_memory_addr_pkt(4, 4, 10) + +assert pkt.memorySizeLen == 4 +assert pkt.memoryAddressLen == 4 +assert pkt.memorySize4 == 10 +assert pkt.memoryAddress4 is not None + +pkt = UDS_RMBARandomEnumerator._random_memory_addr_pkt() + +assert pkt.memorySizeLen in [1, 2, 3, 4] +assert pkt.memoryAddressLen in [1, 2, 3, 4] + +pkt2 = UDS_RMBARandomEnumerator._random_memory_addr_pkt() + +assert pkt != pkt2 + + += UDS_RMBAEnumerator +~ linux not_pypy + +memory = dict() + +mem_areas = [(0x100, 0x1f00), (0xd000, 0xff00), (0xa000, 0xc000), (0x3000, 0x5f00)] + +mem_ranges = [range(s, e) for s, e in mem_areas] + +mem_inner_borders = [s for s, _ in mem_areas] +mem_inner_borders += [e - 1 for _, e in mem_areas] + +mem_outer_borders = [s - 1 for s, _ in mem_areas] +mem_outer_borders += [e for _, e in mem_areas] + +mem_random_test_points = [] +for _ in range(100): + mem_random_test_points += [random.choice(list(itertools.chain(*mem_ranges)))] + +for addr in itertools.chain(*mem_ranges): + memory[addr] = addr & 0xff + +def answers_rmba(resp, req): + global memory + if req.service != 0x23: + return False + if req.memorySizeLen in [1, 3, 4]: + return False + if req.memoryAddressLen in [1, 3, 4]: + return False + addr = getattr(req, "memoryAddress%d" % req.memoryAddressLen) + if addr not in memory.keys(): + return False + out_mem = list() + size = getattr(req, "memorySize%d" % req.memorySizeLen) + for i in range(addr, addr + size): + try: + out_mem.append(memory[i]) + except KeyError: + pass + resp.dataRecord = bytes(out_mem) + return True + +resps = [EcuResponse(None, [UDS()/UDS_RMBAPR(dataRecord=b'')], answers=answers_rmba), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="requestOutOfRange", requestServiceId="ReadMemoryByAddress")])] + +####################################################### +scanner = executeScannerInVirtualEnvironment( + resps, [UDS_RMBAEnumerator], unstable_socket=False, + UDS_RMBARandomEnumerator_kwargs={"unittest": True}) + +assert scanner.scan_completed +tc1 = scanner.configuration.stages[0][1] + +assert len(tc1.results_without_response) < 30 +if len(tc1.results_without_response): + tc1.show() + +assert len(tc1.results_with_negative_response) > 10 +assert len(tc1.results_with_positive_response) > 300 +assert len(tc1.scanned_states) == 1 + +result = tc1.show(dump=True) + +assert "requestOutOfRange received " in result + +############################################################ + +addrs = tc1._get_memory_addresses_from_results(tc1.results_with_positive_response) + +print(float([tp in addrs for tp in mem_inner_borders].count(True)) / len(mem_inner_borders)) +assert float([tp in addrs for tp in mem_inner_borders].count(True)) / len(mem_inner_borders) > 0.8 +print(float([tp in addrs for tp in mem_random_test_points].count(True)) / len(mem_random_test_points)) +assert float([tp in addrs for tp in mem_random_test_points].count(True)) / len(mem_random_test_points) > 0.8 +print(float([tp not in addrs for tp in mem_outer_borders].count(True)) / len(mem_outer_borders)) +assert float([tp not in addrs for tp in mem_outer_borders].count(True)) / len(mem_outer_borders) > 0.7 + + += UDS_TDEnumerator + +resps = [EcuResponse(None, [UDS()/UDS_TDPR(blockSequenceCounter=1)]), + EcuResponse(None, [UDS()/UDS_TDPR(blockSequenceCounter=3)]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="TransferData")])] + +es = [UDS_TDEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 256 - 2 +assert len(tc.results_with_positive_response) == 2 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "subFunctionNotSupported received" in result + +ids = [t.req.blockSequenceCounter for t in tc.results_with_positive_response] + +assert 1 in ids +assert 3 in ids + += BMW_DevJobEnumerator + +load_contrib("automotive.bmw.definitions") +load_contrib("automotive.bmw.enumerator") + +resps = [EcuResponse(None, [UDS()/DEV_JOB_PR(identifier=0xff00)/Raw(b"asdfbeef1")]), + EcuResponse(None, [UDS()/DEV_JOB_PR(identifier=0xff02)/Raw(b"beef2")]), + EcuResponse(None, [UDS()/DEV_JOB_PR(identifier=0xff03)/Raw(b"beef3")]), + EcuResponse(None, [UDS()/DEV_JOB_PR(identifier=0xffff)/Raw(b"beefff")]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="DevelopmentJob")])] + +es = [BMW_DevJobEnumerator] + +scanner = executeScannerInVirtualEnvironment(resps, es, BMW_DevJobEnumerator_kwargs={"scan_range": range(0xFF00, 0x10000)}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 + +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +assert len(tc.results_with_negative_response) == 0x100 - 4 +assert len(tc.results_with_positive_response) == 4 +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "ReadTransportMessageStatus" in result +assert "65282" in result +assert "65283" in result +assert "ReadMemory" in result +assert "subFunctionNotSupported received" in result +assert "PR: Supported" in result + +ids = [t.req.identifier for t in tc.results_with_positive_response] + +assert 0xff00 in ids +assert 0xff02 in ids +assert 0xff03 in ids +assert 0xffff in ids + += UDS_ServiceEnumerator weird issue + +resps = [EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode=0x13, requestServiceId=0x40)]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId=0x41)]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId=0x11)]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId=0x42)]), + EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId=0x43)])] + +es = [UDS_ServiceEnumerator] + +scanner = executeScannerInVirtualEnvironment( + resps, es, UDS_ServiceEnumerator_kwargs={"scan_range": [0x11, 0x40, 0x41, 0x42]}) + +assert scanner.scan_completed +assert scanner.progress() > 0.95 +tc = scanner.configuration.test_cases[0] +tc.show() + +assert len(tc.results_with_negative_response) == 4 + += UDS_ServiceEnumerator, all range + +def req_handler(resp, req): + if req.service != 0x22: + return False + if len(req) == 1: + resp.negativeResponseCode="generalReject" + return True + if len(req) == 2: + resp.negativeResponseCode="incorrectMessageLengthOrInvalidFormat" + return True + if len(req) == 3: + resp.negativeResponseCode="requestOutOfRange" + return True + return False + +resps = [EcuResponse(None, [UDS()/UDS_NR(negativeResponseCode="subFunctionNotSupported", requestServiceId="ReadDataByIdentifier")], req_handler)] + +es = [UDS_ServiceEnumerator] + +debug_dissector_backup = conf.debug_dissector + +# This Enumerator is sending corrupted Packets, therefore we need to disable the debug_dissector +conf.debug_dissector = False +scanner = executeScannerInVirtualEnvironment( + resps, es, UDS_ServiceEnumerator_kwargs={"request_length": 3, "scan_range": range(256)}, unstable_socket=False) +conf.debug_dissector = debug_dissector_backup + +assert scanner.scan_completed +assert scanner.progress() > 0.95 +tc = scanner.configuration.test_cases[0] + +assert len(tc.results_without_response) < 10 +if tc.results_without_response: + tc.show() + +tc.show() + +assert len(tc.scanned_states) == 1 + +result = tc.show(dump=True) + +assert "incorrectMessageLengthOrInvalidFormat" in result +assert "requestOutOfRange" in result + ++ Cleanup + += Delete testsockets + + +cleanup_testsockets() diff --git a/test/contrib/automotive/someip.uts b/test/contrib/automotive/someip.uts index 375b17e39e0..ec1a38f780d 100644 --- a/test/contrib/automotive/someip.uts +++ b/test/contrib/automotive/someip.uts @@ -32,7 +32,7 @@ + Basic operations = Load module -load_contrib("automotive.someip") +load_contrib("automotive.someip", globals_dict=globals()) + SOME/IP operation @@ -40,47 +40,44 @@ load_contrib("automotive.someip") p = SOMEIP() pstr = bytes(p) binstr = b"\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x01\x01\x00\x00" -assert(pstr == binstr) +assert pstr == binstr = Build with empty payload p.payload = Raw(b"") pstr = bytes(p) binstr = b"\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x01\x01\x00\x00" -assert(pstr == binstr) +assert pstr == binstr = Build with non-empty payload p.payload = Raw(b"\xde\xad\xbe\xef") pstr = bytes(p) binstr = b"\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x01\x00\x00\xde\xad\xbe\xef" -assert(pstr == binstr) +assert pstr == binstr = Dissect EVENT_ID packet -p = SOMEIP(b"\x11\x11\x81\x11\x00\x00\x00\x04\x33\x33\x44\x44\x02\x03\x04\x05") - -assert(p.srv_id == 0x1111) -assert(p.sub_id == 0x1) -assert(p.method_id == None) -assert(p.event_id == 0x0111) -assert(p.client_id == 0x3333) -assert(p.session_id == 0x4444) -assert(p.proto_ver == 0x02) -assert(p.iface_ver == 0x03) -assert(p.msg_type == 0x04) -assert(p.retcode == 0x05) +p = SOMEIP(b"\x11\x11\x81\x11\x00\x00\x00\x08\x33\x33\x44\x44\x02\x03\x04\x05") + +assert p.srv_id == 0x1111 +assert p.sub_id == 0x8111 +assert p.client_id == 0x3333 +assert p.session_id == 0x4444 +assert p.proto_ver == 0x02 +assert p.iface_ver == 0x03 +assert p.msg_type == 0x04 +assert p.retcode == 0x05 + = Dissect METHOD_ID packet -p = SOMEIP(b"\x11\x11\x01\x11\x00\x00\x00\x04\x33\x33\x44\x44\x02\x03\x04\x05") - -assert(p.srv_id == 0x1111) -assert(p.sub_id == 0x0) -assert(p.method_id == 0x0111) -assert(p.event_id == None) -assert(p.client_id == 0x3333) -assert(p.session_id == 0x4444) -assert(p.proto_ver == 0x02) -assert(p.iface_ver == 0x03) -assert(p.msg_type == 0x04) -assert(p.retcode == 0x05) +p = SOMEIP(b"\x11\x11\x01\x11\x00\x00\x00\x08\x33\x33\x44\x44\x02\x03\x04\x05") + +assert p.srv_id == 0x1111 +assert p.sub_id == 0x0111 +assert p.client_id == 0x3333 +assert p.session_id == 0x4444 +assert p.proto_ver == 0x02 +assert p.iface_ver == 0x03 +assert p.msg_type == 0x04 +assert p.retcode == 0x05 + SOME/IP-TP operation @@ -91,50 +88,64 @@ p.msg_type = 0x20 pstr = bytes(p) print(pstr) binstr = b'\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x01\x20\x00\x00\x00\x00\x00' -assert(pstr == binstr) +assert pstr == binstr p.more_seg = 1 pstr = bytes(p) binstr = b'\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x01\x20\x00\x00\x00\x00\x01' -assert(pstr == binstr) +assert pstr == binstr p.msg_type = 0x00 pstr = bytes(p) binstr = b'\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x01\x01\x00\x00' -assert(pstr == binstr) +assert pstr == binstr = Dissect TP p = SOMEIP(b'\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x01\x21\x00\x00\x00\x00\x01') -assert(p.msg_type == 0x21) -assert(p.more_seg == 1) -assert(p.len == 12) +assert p.msg_type == 0x21 +assert p.more_seg == 1 +assert p.len == 12 p.msg_type = 0x00 pstr = bytes(p) binstr = b"\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x01\x00\x00" -assert(pstr == binstr) +assert pstr == binstr -= Build TP fragmented += Build TP fragmented payload p = SOMEIP() p.msg_type = 0x20 p.add_payload(Raw("A"*1400)) f = p.fragment() -assert(f[0].len == 1404) -assert(f[1].len == 20) -assert(f[0].payload == Raw("A"*1392)) -assert(f[1].payload == Raw("A"*8)) -assert(f[0].more_seg == 1) -assert(f[1].more_seg == 0) +assert f[0].len == 1404 +assert f[1].len == 20 +assert f[0].payload == Raw("A"*1392) +assert f[1].payload == Raw("A"*8) +assert f[0].more_seg == 1 +assert f[1].more_seg == 0 + += Build TP fragmented data +p = SOMEIP() +p.msg_type = 0x20 +p.data = [Raw("A"*1400)] + +f = p.fragment() + +assert f[0].len == 1404 +assert f[1].len == 20 +assert f[0].data[0] == Raw("A"*1392) +assert f[1].data[0] == Raw("A"*8) +assert f[0].more_seg == 1 +assert f[1].more_seg == 0 + SD Entry Service = Check packet length on empty build p = SDEntry_Service() -assert(len(bytes(p)) == SDENTRY_OVERALL_LEN) +assert len(bytes(p)) == SDENTRY_OVERALL_LEN = Build 1 p = SDEntry_Service(type = SDENTRY_TYPE_SRV_OFFERSERVICE, @@ -143,33 +154,33 @@ p = SDEntry_Service(type = SDENTRY_TYPE_SRV_OFFERSERVICE, ttl = 0x666666, minor_ver = 0xdeadbeef) p_str = bytes(p) bin_str = b"\x01\x11\x22\x00\x33\x33\x44\x44\x55\x66\x66\x66\xde\xad\xbe\xef" -assert(p_str == bin_str) -assert(len(p_str) == SDENTRY_OVERALL_LEN) +assert p_str == bin_str +assert len(p_str) == SDENTRY_OVERALL_LEN = Build 2 p = SDEntry_Service(n_opt_1 = 0xf1, n_opt_2 = 0xf2) p_str = bytes(p) bin_str = b"\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" -assert(p_str == bin_str) -assert(len(p_str) == SDENTRY_OVERALL_LEN) +assert p_str == bin_str +assert len(p_str) == SDENTRY_OVERALL_LEN = Dissect p = SDEntry_Service( b"\x01\x22\x33\x00\x44\x44\x55\x55\x66\x77\x77\x77\xde\xad\xbe\xef") -assert(p.type == SDENTRY_TYPE_SRV_OFFERSERVICE) -assert(p.index_1 == 0x22) -assert(p.index_2 == 0x33) -assert(p.srv_id == 0x4444) -assert(p.inst_id == 0x5555) -assert(p.major_ver == 0x66) -assert(p.ttl == 0x777777) -assert(p.minor_ver == 0xdeadbeef) +assert p.type == SDENTRY_TYPE_SRV_OFFERSERVICE +assert p.index_1 == 0x22 +assert p.index_2 == 0x33 +assert p.srv_id == 0x4444 +assert p.inst_id == 0x5555 +assert p.major_ver == 0x66 +assert p.ttl == 0x777777 +assert p.minor_ver == 0xdeadbeef + SD Entry Eventgroup = Check packet length on empty build p = SDEntry_EventGroup() -assert(len(bytes(p)) == SDENTRY_OVERALL_LEN) +assert len(bytes(p)) == SDENTRY_OVERALL_LEN = Build p = SDEntry_EventGroup(index_1 = 0x11, index_2 = 0x22, srv_id = 0x3333, @@ -177,69 +188,72 @@ p = SDEntry_EventGroup(index_1 = 0x11, index_2 = 0x22, srv_id = 0x3333, cnt = 0x7, eventgroup_id = 0x8888) p_str = bytes(p) bin_str = b"\x06\x11\x22\x00\x33\x33\x44\x44\x55\x66\x66\x66\x00\x07\x88\x88" -assert(p_str == bin_str) -assert(len(bytes(p)) == SDENTRY_OVERALL_LEN) +assert p_str == bin_str +assert len(bytes(p)) == SDENTRY_OVERALL_LEN = Dissect p = SDEntry_EventGroup( b"\x06\x11\x22\x00\x33\x33\x44\x44\x55\x66\x66\x66\x00\x07\x88\x88") -assert(p.index_1 == 0x11) -assert(p.index_2 == 0x22) -assert(p.srv_id == 0x3333) -assert(p.inst_id == 0x4444) -assert(p.major_ver == 0x55) -assert(p.ttl == 0x666666) -assert(p.cnt == 0x7) -assert(p.eventgroup_id == 0x8888) +assert p.index_1 == 0x11 +assert p.index_2 == 0x22 +assert p.srv_id == 0x3333 +assert p.inst_id == 0x4444 +assert p.major_ver == 0x55 +assert p.ttl == 0x666666 +assert p.cnt == 0x7 +assert p.eventgroup_id == 0x8888 + SD Flags = Build and check flags p = SD() -p.set_flag("REBOOT", 1) -assert(p.flags == 0x80) +p.flags = "REBOOT" +assert p.flags == 0x80 + +p.flags = "" +assert p.flags == 0x00 -p.set_flag("REBOOT", 0) -assert(p.flags == 0x00) +p.flags = "UNICAST" +assert p.flags == 0x40 -p.set_flag("UNICAST", 1) -assert(p.flags == 0x40) +p.flags = "" +assert p.flags == 0x00 -p.set_flag("UNICAST", 0) -assert(p.flags == 0x00) +p.flags = "EXPLICIT_INITIAL_DATA_CONTROL" +assert p.flags == 0x20 -p.set_flag("REBOOT", 1) -p.set_flag("UNICAST", 1) -assert(p.flags == 0xc0) +p.flags = "" +assert p.flags == 0x00 + +p.flags = "REBOOT+UNICAST+EXPLICIT_INITIAL_DATA_CONTROL" +assert p.flags == 0xe0 + SD Get SOME/IP Packet = Build empty p = SOMEIP() / SD() -assert(len(bytes(p)) == SOMEIP._OVERALL_LEN_NOPAYLOAD + 12) +assert len(bytes(p)) == SOMEIP._OVERALL_LEN_NOPAYLOAD + 12 = Verify constants against spec TR_SOMEIP_00250 -assert(SD.SOMEIP_MSGID_SRVID == 0xffff) -assert(SD.SOMEIP_MSGID_SUBID == 0x1) -assert(SD.SOMEIP_MSGID_EVENTID == 0x0100) -assert(SD.SOMEIP_CLIENT_ID == 0x0000) -assert(SD.SOMEIP_MINIMUM_SESSION_ID == 0x0001) -assert(SD.SOMEIP_PROTO_VER == 0x01) -assert(SD.SOMEIP_IFACE_VER == 0x01) -assert(SD.SOMEIP_MSG_TYPE == 0x02) -assert(SD.SOMEIP_RETCODE == 0x00) +assert SD.SOMEIP_MSGID_SRVID == 0xffff +assert SD.SOMEIP_MSGID_SUBID == 0x8100 +assert SD.SOMEIP_CLIENT_ID == 0x0000 +assert SD.SOMEIP_MINIMUM_SESSION_ID == 0x0001 +assert SD.SOMEIP_PROTO_VER == 0x01 +assert SD.SOMEIP_IFACE_VER == 0x01 +assert SD.SOMEIP_MSG_TYPE == 0x02 +assert SD.SOMEIP_RETCODE == 0x00 = check that values are bound -assert(p[SOMEIP].srv_id == SD.SOMEIP_MSGID_SRVID) -assert(p[SOMEIP].sub_id == SD.SOMEIP_MSGID_SUBID) -assert(p[SOMEIP].event_id == SD.SOMEIP_MSGID_EVENTID) -assert(p[SOMEIP].client_id == SD.SOMEIP_CLIENT_ID) -assert(p[SOMEIP].session_id != 0x0000) -assert(p[SOMEIP].session_id >= SD.SOMEIP_MINIMUM_SESSION_ID) -assert(p[SOMEIP].proto_ver == SD.SOMEIP_PROTO_VER) -assert(p[SOMEIP].iface_ver == SD.SOMEIP_IFACE_VER) -assert(p[SOMEIP].msg_type == SD.SOMEIP_MSG_TYPE) -assert(p[SOMEIP].retcode == SD.SOMEIP_RETCODE) +assert p[SOMEIP].srv_id == SD.SOMEIP_MSGID_SRVID +assert p[SOMEIP].sub_id == SD.SOMEIP_MSGID_SUBID +assert p[SOMEIP].client_id == SD.SOMEIP_CLIENT_ID +assert p[SOMEIP].session_id != 0x0000 +assert p[SOMEIP].session_id >= SD.SOMEIP_MINIMUM_SESSION_ID +assert p[SOMEIP].proto_ver == SD.SOMEIP_PROTO_VER +assert p[SOMEIP].iface_ver == SD.SOMEIP_IFACE_VER +assert p[SOMEIP].msg_type == SD.SOMEIP_MSG_TYPE +assert p[SOMEIP].retcode == SD.SOMEIP_RETCODE # FIXME: Service Discovery messages shell be transported over UDP # (TR_SOMEIP_00248) @@ -250,28 +264,28 @@ assert(p[SOMEIP].retcode == SD.SOMEIP_RETCODE) = Check length of package without entries nor options p = SD() -assert(len(bytes(p)) == 12) +assert len(bytes(p)) == 12 = Check entries to array and size check p.set_entryArray([SDEntry_Service(), SDEntry_EventGroup()]) -assert(struct.unpack("!L", bytes(p)[4:8])[0] == 32) +assert struct.unpack("!L", bytes(p)[4:8])[0] == 32 p.set_entryArray([]) -assert(struct.unpack("!L", bytes(p)[4:8])[0] == 0) +assert struct.unpack("!L", bytes(p)[4:8])[0] == 0 = Check Options to array and size check p.set_optionArray([SDOption_IP4_EndPoint(), SDOption_IP4_EndPoint()]) -assert(struct.unpack("!L", bytes(p)[8:12])[0] == 24) +assert struct.unpack("!L", bytes(p)[8:12])[0] == 24 p.set_optionArray([]) -assert(struct.unpack("!L", bytes(p)[8:12])[0] == 0) +assert struct.unpack("!L", bytes(p)[8:12])[0] == 0 = Check Entries & Options to array and size check p.set_entryArray([SDEntry_Service(), SDEntry_EventGroup()]) p.set_optionArray([SDOption_IP4_EndPoint(), SDOption_IP4_EndPoint()]) -assert(struct.unpack("!L", bytes(p)[4:8])[0] == 32) -assert(struct.unpack("!L", bytes(p)[40:44])[0] == 24) +assert struct.unpack("!L", bytes(p)[4:8])[0] == 32 +assert struct.unpack("!L", bytes(p)[40:44])[0] == 24 + Git issue 2348: SOME/IP-SD Entry-Array is broken by building it from RAW @@ -294,25 +308,25 @@ ea2.eventgroup_id = 0x1357 sd1 = SD() sd1.set_entryArray([ea1]) -# this is computed on build, but we need it sooner for the assert() +# this is computed on build, but we need it sooner for the assert sd1.len_entry_array = 16 sd1.len_option_array = 0 -assert(sd1.show(dump=True) == SD(sd1.build()).show(dump=True)) +assert sd1.show(dump=True) == SD(sd1.build()).show(dump=True) = Double SD entry sd2 = SD() sd2.set_entryArray([ea2,ea1]) -# this is computed on build, but we need it sooner for the assert() +# this is computed on build, but we need it sooner for the assert sd2.len_entry_array = 32 sd2.len_option_array = 0 -assert(sd2.show(dump=True) == SD(sd2.build()).show(dump=True)) +assert sd2.show(dump=True) == SD(sd2.build()).show(dump=True) = Flipped double SD entry # flip the order sd2.set_entryArray([ea1,ea2]) -assert(sd2.show(dump=True) == SD(sd2.build()).show(dump=True)) +assert sd2.show(dump=True) == SD(sd2.build()).show(dump=True) @@ -320,27 +334,27 @@ assert(sd2.show(dump=True) == SD(sd2.build()).show(dump=True)) + SD Options (individual) = Verifying constants against spec -assert(SDOPTION_CFG_TYPE == 0x01) -assert(SDOPTION_LOADBALANCE_TYPE == 0x02) -assert(SDOPTION_LOADBALANCE_LEN == 0x05) -assert(SDOPTION_IP4_ENDPOINT_TYPE == 0x04) -assert(SDOPTION_IP4_ENDPOINT_LEN == 0x0009) -assert(SDOPTION_IP4_MCAST_TYPE == 0x14) -assert(SDOPTION_IP4_MCAST_LEN == 0x0009) -assert(SDOPTION_IP4_SDENDPOINT_TYPE == 0x24) -assert(SDOPTION_IP4_SDENDPOINT_LEN == 0x0009) -assert(SDOPTION_IP6_ENDPOINT_TYPE == 0x06) -assert(SDOPTION_IP6_ENDPOINT_LEN == 0x0015) -assert(SDOPTION_IP6_MCAST_TYPE == 0x16) -assert(SDOPTION_IP6_MCAST_LEN == 0x0015) -assert(SDOPTION_IP6_SDENDPOINT_TYPE == 0x26) -assert(SDOPTION_IP6_SDENDPOINT_LEN == 0x0015) +assert SDOPTION_CFG_TYPE == 0x01 +assert SDOPTION_LOADBALANCE_TYPE == 0x02 +assert SDOPTION_LOADBALANCE_LEN == 0x05 +assert SDOPTION_IP4_ENDPOINT_TYPE == 0x04 +assert SDOPTION_IP4_ENDPOINT_LEN == 0x0009 +assert SDOPTION_IP4_MCAST_TYPE == 0x14 +assert SDOPTION_IP4_MCAST_LEN == 0x0009 +assert SDOPTION_IP4_SDENDPOINT_TYPE == 0x24 +assert SDOPTION_IP4_SDENDPOINT_LEN == 0x0009 +assert SDOPTION_IP6_ENDPOINT_TYPE == 0x06 +assert SDOPTION_IP6_ENDPOINT_LEN == 0x0015 +assert SDOPTION_IP6_MCAST_TYPE == 0x16 +assert SDOPTION_IP6_MCAST_LEN == 0x0015 +assert SDOPTION_IP6_SDENDPOINT_TYPE == 0x26 +assert SDOPTION_IP6_SDENDPOINT_LEN == 0x0015 ### SDOption_Config = SDOption_Config: Verify make_string() method from dict data = { "hello": "world" } out = SDOption_Config.make_string(data) -assert(out == b"\x0bhello=world\x00") +assert out == b"\x0bhello=world\x00" = SDOption_Config: Verify make_string() method from list data = [ @@ -349,349 +363,349 @@ data = [ ("123", "456") ] out = SDOption_Config.make_string(data) -assert(out == b"\x03x=y\x07abc=def\x07123=456\x00") +assert out == b"\x03x=y\x07abc=def\x07123=456\x00" = SDOption_Config: Build and dissect empty opt = SDOption_Config() optraw = opt.build() -assert(optraw == b"\x00\x02\x01\x00\x00") +assert optraw == b"\x00\x02\x01\x00\x00" opt = SDOption_Config(optraw) -assert(opt.len == 0x2) -assert(opt.type == SDOPTION_CFG_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.cfg_str == b"\x00") +assert opt.len == 0x2 +assert opt.type == SDOPTION_CFG_TYPE +assert opt.res_hdr == 0x0 +assert opt.cfg_str == b"\x00" = SDOption_Config: Build and dissect spec example tststr = b"\x05abc=x\x07def=123\x00" opt = SDOption_Config(cfg_str=tststr) optraw = opt.build() -assert(optraw == b"\x00\x10\x01\x00" + tststr) +assert optraw == b"\x00\x10\x01\x00" + tststr opt = SDOption_Config(optraw) -assert(opt.len == 0x10) -assert(opt.type == SDOPTION_CFG_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.cfg_str == tststr) +assert opt.len == 0x10 +assert opt.type == SDOPTION_CFG_TYPE +assert opt.res_hdr == 0x00 +assert opt.cfg_str == tststr = SDOption_Config: Build and dissect fully populated tststr = b"abcdefghijklmnopqrstuvwxyz" opt = SDOption_Config(len=0x1234, type=0x56, res_hdr=0x78, cfg_str=tststr) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78" + tststr) +assert optraw == b"\x12\x34\x56\x78" + tststr opt = SDOption_Config(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.cfg_str == tststr) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.cfg_str == tststr ### SDOption_LoadBalance = SDOption_LoadBalance: Build and dissect empty opt = SDOption_LoadBalance() optraw = opt.build() -assert(optraw == b"\x00\x05\x02\x00\x00\x00\x00\x00") +assert optraw == b"\x00\x05\x02\x00\x00\x00\x00\x00" opt = SDOption_LoadBalance(optraw) -assert(opt.len == SDOPTION_LOADBALANCE_LEN) -assert(opt.type == SDOPTION_LOADBALANCE_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.priority == 0x0) -assert(opt.weight == 0x0) +assert opt.len == SDOPTION_LOADBALANCE_LEN +assert opt.type == SDOPTION_LOADBALANCE_TYPE +assert opt.res_hdr == 0x0 +assert opt.priority == 0x0 +assert opt.weight == 0x0 = SDOption_LoadBalance: Build and dissect example opt = SDOption_LoadBalance(priority=0x1234, weight=0x5678) optraw = opt.build() -assert(optraw == b"\x00\x05\x02\x00\x12\x34\x56\x78") +assert optraw == b"\x00\x05\x02\x00\x12\x34\x56\x78" opt = SDOption_LoadBalance(optraw) -assert(opt.len == SDOPTION_LOADBALANCE_LEN) -assert(opt.type == SDOPTION_LOADBALANCE_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.priority == 0x1234) -assert(opt.weight == 0x5678) +assert opt.len == SDOPTION_LOADBALANCE_LEN +assert opt.type == SDOPTION_LOADBALANCE_TYPE +assert opt.res_hdr == 0x00 +assert opt.priority == 0x1234 +assert opt.weight == 0x5678 = SDOption_LoadBalance: Build and dissect fully populated opt = SDOption_LoadBalance(len=0x1234, type=0x56, res_hdr=0x78, priority=0x9abc, weight=0xdef0) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78\x9a\xbc\xde\xf0") +assert optraw == b"\x12\x34\x56\x78\x9a\xbc\xde\xf0" opt = SDOption_LoadBalance(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.priority == 0x9abc) -assert(opt.weight == 0xdef0) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.priority == 0x9abc +assert opt.weight == 0xdef0 ### SDOption_IP4_EndPoint = SDOption_IP4_EndPoint: Build and dissect empty opt = SDOption_IP4_EndPoint() optraw = opt.build() -assert(optraw == b"\x00\x09\x04\x00\x00\x00\x00\x00\x00\x11\x00\x00") +assert optraw == b"\x00\x09\x04\x00\x00\x00\x00\x00\x00\x11\x00\x00" opt = SDOption_IP4_EndPoint(optraw) -assert(opt.len == SDOPTION_IP4_ENDPOINT_LEN) -assert(opt.type == SDOPTION_IP4_ENDPOINT_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.addr == "0.0.0.0") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x11) -assert(opt.port == 0x0) +assert opt.len == SDOPTION_IP4_ENDPOINT_LEN +assert opt.type == SDOPTION_IP4_ENDPOINT_TYPE +assert opt.res_hdr == 0x0 +assert opt.addr == "0.0.0.0" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x11 +assert opt.port == 0x0 = SDOption_IP4_EndPoint: Build and dissect example opt = SDOption_IP4_EndPoint(addr = "192.168.123.45", l4_proto = "TCP", port = 0x1234) optraw = opt.build() -assert(optraw == b"\x00\x09\x04\x00\xc0\xa8\x7b\x2d\x00\x06\x12\x34") +assert optraw == b"\x00\x09\x04\x00\xc0\xa8\x7b\x2d\x00\x06\x12\x34" opt = SDOption_IP4_EndPoint(optraw) -assert(opt.len == SDOPTION_IP4_ENDPOINT_LEN) -assert(opt.type == SDOPTION_IP4_ENDPOINT_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.addr == "192.168.123.45") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x06) -assert(opt.port == 0x1234) +assert opt.len == SDOPTION_IP4_ENDPOINT_LEN +assert opt.type == SDOPTION_IP4_ENDPOINT_TYPE +assert opt.res_hdr == 0x00 +assert opt.addr == "192.168.123.45" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x06 +assert opt.port == 0x1234 = SDOption_IP4_EndPoint: Build and dissect fully populated opt = SDOption_IP4_EndPoint(len=0x1234, type=0x56, res_hdr=0x78, addr = "11.22.33.44", res_tail = 0x9a, l4_proto = 0xbc, port = 0xdef0) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78\x0b\x16\x21\x2c\x9a\xbc\xde\xf0") +assert optraw == b"\x12\x34\x56\x78\x0b\x16\x21\x2c\x9a\xbc\xde\xf0" opt = SDOption_IP4_EndPoint(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.addr == "11.22.33.44") -assert(opt.res_tail == 0x9a) -assert(opt.l4_proto == 0xbc) -assert(opt.port == 0xdef0) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.addr == "11.22.33.44" +assert opt.res_tail == 0x9a +assert opt.l4_proto == 0xbc +assert opt.port == 0xdef0 ### SDOption_IP4_Multicast = SDOption_IP4_Multicast: Build and dissect empty opt = SDOption_IP4_Multicast() optraw = opt.build() -assert(optraw == b"\x00\x09\x14\x00\x00\x00\x00\x00\x00\x11\x00\x00") +assert optraw == b"\x00\x09\x14\x00\x00\x00\x00\x00\x00\x11\x00\x00" opt = SDOption_IP4_Multicast(optraw) -assert(opt.len == SDOPTION_IP4_MCAST_LEN) -assert(opt.type == SDOPTION_IP4_MCAST_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.addr == "0.0.0.0") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x11) -assert(opt.port == 0x0) +assert opt.len == SDOPTION_IP4_MCAST_LEN +assert opt.type == SDOPTION_IP4_MCAST_TYPE +assert opt.res_hdr == 0x0 +assert opt.addr == "0.0.0.0" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x11 +assert opt.port == 0x0 = SDOption_IP4_Multicast: Build and dissect example opt = SDOption_IP4_Multicast(addr = "192.168.123.45", l4_proto = "TCP", port = 0x1234) optraw = opt.build() -assert(optraw == b"\x00\x09\x14\x00\xc0\xa8\x7b\x2d\x00\x06\x12\x34") +assert optraw == b"\x00\x09\x14\x00\xc0\xa8\x7b\x2d\x00\x06\x12\x34" opt = SDOption_IP4_Multicast(optraw) -assert(opt.len == SDOPTION_IP4_MCAST_LEN) -assert(opt.type == SDOPTION_IP4_MCAST_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.addr == "192.168.123.45") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x06) -assert(opt.port == 0x1234) +assert opt.len == SDOPTION_IP4_MCAST_LEN +assert opt.type == SDOPTION_IP4_MCAST_TYPE +assert opt.res_hdr == 0x00 +assert opt.addr == "192.168.123.45" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x06 +assert opt.port == 0x1234 = SDOption_IP4_Multicast: Build and dissect fully populated opt = SDOption_IP4_Multicast(len=0x1234, type=0x56, res_hdr=0x78, addr = "11.22.33.44", res_tail = 0x9a, l4_proto = 0xbc, port = 0xdef0) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78\x0b\x16\x21\x2c\x9a\xbc\xde\xf0") +assert optraw == b"\x12\x34\x56\x78\x0b\x16\x21\x2c\x9a\xbc\xde\xf0" opt = SDOption_IP4_Multicast(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.addr == "11.22.33.44") -assert(opt.res_tail == 0x9a) -assert(opt.l4_proto == 0xbc) -assert(opt.port == 0xdef0) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.addr == "11.22.33.44" +assert opt.res_tail == 0x9a +assert opt.l4_proto == 0xbc +assert opt.port == 0xdef0 ### SDOption_IP4_SD_EndPoint = SDOption_IP4_SD_EndPoint: Build and dissect empty opt = SDOption_IP4_SD_EndPoint() optraw = opt.build() -assert(optraw == b"\x00\x09\x24\x00\x00\x00\x00\x00\x00\x11\x00\x00") +assert optraw == b"\x00\x09\x24\x00\x00\x00\x00\x00\x00\x11\x00\x00" opt = SDOption_IP4_SD_EndPoint(optraw) -assert(opt.len == SDOPTION_IP4_SDENDPOINT_LEN) -assert(opt.type == SDOPTION_IP4_SDENDPOINT_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.addr == "0.0.0.0") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x11) -assert(opt.port == 0x0) +assert opt.len == SDOPTION_IP4_SDENDPOINT_LEN +assert opt.type == SDOPTION_IP4_SDENDPOINT_TYPE +assert opt.res_hdr == 0x0 +assert opt.addr == "0.0.0.0" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x11 +assert opt.port == 0x0 = SDOption_IP4_SD_EndPoint: Build and dissect example opt = SDOption_IP4_SD_EndPoint(addr = "192.168.123.45", l4_proto = "TCP", port = 0x1234) optraw = opt.build() -assert(optraw == b"\x00\x09\x24\x00\xc0\xa8\x7b\x2d\x00\x06\x12\x34") +assert optraw == b"\x00\x09\x24\x00\xc0\xa8\x7b\x2d\x00\x06\x12\x34" opt = SDOption_IP4_SD_EndPoint(optraw) -assert(opt.len == SDOPTION_IP4_SDENDPOINT_LEN) -assert(opt.type == SDOPTION_IP4_SDENDPOINT_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.addr == "192.168.123.45") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x06) -assert(opt.port == 0x1234) +assert opt.len == SDOPTION_IP4_SDENDPOINT_LEN +assert opt.type == SDOPTION_IP4_SDENDPOINT_TYPE +assert opt.res_hdr == 0x00 +assert opt.addr == "192.168.123.45" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x06 +assert opt.port == 0x1234 = SDOption_IP4_SD_EndPoint: Build and dissect fully populated opt = SDOption_IP4_SD_EndPoint(len=0x1234, type=0x56, res_hdr=0x78, addr = "11.22.33.44", res_tail = 0x9a, l4_proto = 0xbc, port = 0xdef0) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78\x0b\x16\x21\x2c\x9a\xbc\xde\xf0") +assert optraw == b"\x12\x34\x56\x78\x0b\x16\x21\x2c\x9a\xbc\xde\xf0" opt = SDOption_IP4_SD_EndPoint(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.addr == "11.22.33.44") -assert(opt.res_tail == 0x9a) -assert(opt.l4_proto == 0xbc) -assert(opt.port == 0xdef0) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.addr == "11.22.33.44" +assert opt.res_tail == 0x9a +assert opt.l4_proto == 0xbc +assert opt.port == 0xdef0 ### SDOption_IP6_EndPoint = SDOption_IP6_EndPoint: Build and dissect empty opt = SDOption_IP6_EndPoint() optraw = opt.build() -assert(optraw == b"\x00\x15\x06\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x11\x00\x00") +assert optraw == b"\x00\x15\x06\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x11\x00\x00" opt = SDOption_IP6_EndPoint(optraw) -assert(opt.len == SDOPTION_IP6_ENDPOINT_LEN) -assert(opt.type == SDOPTION_IP6_ENDPOINT_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.addr == "::") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x11) -assert(opt.port == 0x0) +assert opt.len == SDOPTION_IP6_ENDPOINT_LEN +assert opt.type == SDOPTION_IP6_ENDPOINT_TYPE +assert opt.res_hdr == 0x0 +assert opt.addr == "::" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x11 +assert opt.port == 0x0 = SDOption_IP6_EndPoint: Build and dissect example opt = SDOption_IP6_EndPoint(addr = "2001:cdba::3257:9652", l4_proto = "TCP", port = 0x1234) optraw = opt.build() -assert(optraw == b"\x00\x15\x06\x00" + b"\x20\x01\xcd\xba\x00\x00\x00\x00\x00\x00\x00\x00\x32\x57\x96\x52" + b"\x00\x06\x12\x34") +assert optraw == b"\x00\x15\x06\x00" + b"\x20\x01\xcd\xba\x00\x00\x00\x00\x00\x00\x00\x00\x32\x57\x96\x52" + b"\x00\x06\x12\x34" opt = SDOption_IP6_EndPoint(optraw) -assert(opt.len == SDOPTION_IP6_ENDPOINT_LEN) -assert(opt.type == SDOPTION_IP6_ENDPOINT_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.addr == "2001:cdba::3257:9652") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x06) -assert(opt.port == 0x1234) +assert opt.len == SDOPTION_IP6_ENDPOINT_LEN +assert opt.type == SDOPTION_IP6_ENDPOINT_TYPE +assert opt.res_hdr == 0x00 +assert opt.addr == "2001:cdba::3257:9652" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x06 +assert opt.port == 0x1234 = SDOption_IP6_EndPoint: Build and dissect fully populated opt = SDOption_IP6_EndPoint(len=0x1234, type=0x56, res_hdr=0x78, addr = "1234:5678:9abc:def0:0fed:cba9:8765:4321", res_tail = 0x9a, l4_proto = 0xbc, port = 0xdef0) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78" + b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x0f\xed\xcb\xa9\x87\x65\x43\x21" + b"\x9a\xbc\xde\xf0") +assert optraw == b"\x12\x34\x56\x78" + b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x0f\xed\xcb\xa9\x87\x65\x43\x21" + b"\x9a\xbc\xde\xf0" opt = SDOption_IP6_EndPoint(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.addr == "1234:5678:9abc:def0:fed:cba9:8765:4321") -assert(opt.res_tail == 0x9a) -assert(opt.l4_proto == 0xbc) -assert(opt.port == 0xdef0) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.addr == "1234:5678:9abc:def0:fed:cba9:8765:4321" +assert opt.res_tail == 0x9a +assert opt.l4_proto == 0xbc +assert opt.port == 0xdef0 ### SDOption_IP6_Multicast = SDOption_IP6_Multicast: Build and dissect empty opt = SDOption_IP6_Multicast() optraw = opt.build() -assert(optraw == b"\x00\x15\x16\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x11\x00\x00") +assert optraw == b"\x00\x15\x16\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x11\x00\x00" opt = SDOption_IP6_Multicast(optraw) -assert(opt.len == SDOPTION_IP6_MCAST_LEN) -assert(opt.type == SDOPTION_IP6_MCAST_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.addr == "::") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x11) -assert(opt.port == 0x0) +assert opt.len == SDOPTION_IP6_MCAST_LEN +assert opt.type == SDOPTION_IP6_MCAST_TYPE +assert opt.res_hdr == 0x0 +assert opt.addr == "::" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x11 +assert opt.port == 0x0 = SDOption_IP6_Multicast: Build and dissect example opt = SDOption_IP6_Multicast(addr = "2001:cdba::3257:9652", l4_proto = "TCP", port = 0x1234) optraw = opt.build() -assert(optraw == b"\x00\x15\x16\x00" + b"\x20\x01\xcd\xba\x00\x00\x00\x00\x00\x00\x00\x00\x32\x57\x96\x52" + b"\x00\x06\x12\x34") +assert optraw == b"\x00\x15\x16\x00" + b"\x20\x01\xcd\xba\x00\x00\x00\x00\x00\x00\x00\x00\x32\x57\x96\x52" + b"\x00\x06\x12\x34" opt = SDOption_IP6_Multicast(optraw) -assert(opt.len == SDOPTION_IP6_MCAST_LEN) -assert(opt.type == SDOPTION_IP6_MCAST_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.addr == "2001:cdba::3257:9652") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x06) -assert(opt.port == 0x1234) +assert opt.len == SDOPTION_IP6_MCAST_LEN +assert opt.type == SDOPTION_IP6_MCAST_TYPE +assert opt.res_hdr == 0x00 +assert opt.addr == "2001:cdba::3257:9652" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x06 +assert opt.port == 0x1234 = SDOption_IP6_Multicast: Build and dissect fully populated opt = SDOption_IP6_Multicast(len=0x1234, type=0x56, res_hdr=0x78, addr = "1234:5678:9abc:def0:0fed:cba9:8765:4321", res_tail = 0x9a, l4_proto = 0xbc, port = 0xdef0) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78" + b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x0f\xed\xcb\xa9\x87\x65\x43\x21" + b"\x9a\xbc\xde\xf0") +assert optraw == b"\x12\x34\x56\x78" + b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x0f\xed\xcb\xa9\x87\x65\x43\x21" + b"\x9a\xbc\xde\xf0" opt = SDOption_IP6_Multicast(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.addr == "1234:5678:9abc:def0:fed:cba9:8765:4321") -assert(opt.res_tail == 0x9a) -assert(opt.l4_proto == 0xbc) -assert(opt.port == 0xdef0) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.addr == "1234:5678:9abc:def0:fed:cba9:8765:4321" +assert opt.res_tail == 0x9a +assert opt.l4_proto == 0xbc +assert opt.port == 0xdef0 ### SDOption_IP6_SD_EndPoint = SDOption_IP6_SD_EndPoint: Build and dissect empty opt = SDOption_IP6_SD_EndPoint() optraw = opt.build() -assert(optraw == b"\x00\x15\x26\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x11\x00\x00") +assert optraw == b"\x00\x15\x26\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x11\x00\x00" opt = SDOption_IP6_SD_EndPoint(optraw) -assert(opt.len == SDOPTION_IP6_SDENDPOINT_LEN) -assert(opt.type == SDOPTION_IP6_SDENDPOINT_TYPE) -assert(opt.res_hdr == 0x0) -assert(opt.addr == "::") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x11) -assert(opt.port == 0x0) +assert opt.len == SDOPTION_IP6_SDENDPOINT_LEN +assert opt.type == SDOPTION_IP6_SDENDPOINT_TYPE +assert opt.res_hdr == 0x0 +assert opt.addr == "::" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x11 +assert opt.port == 0x0 = SDOption_IP6_SD_EndPoint: Build and dissect example opt = SDOption_IP6_SD_EndPoint(addr = "2001:cdba::3257:9652", l4_proto = "TCP", port = 0x1234) optraw = opt.build() -assert(optraw == b"\x00\x15\x26\x00" + b"\x20\x01\xcd\xba\x00\x00\x00\x00\x00\x00\x00\x00\x32\x57\x96\x52" + b"\x00\x06\x12\x34") +assert optraw == b"\x00\x15\x26\x00" + b"\x20\x01\xcd\xba\x00\x00\x00\x00\x00\x00\x00\x00\x32\x57\x96\x52" + b"\x00\x06\x12\x34" opt = SDOption_IP6_SD_EndPoint(optraw) -assert(opt.len == SDOPTION_IP6_SDENDPOINT_LEN) -assert(opt.type == SDOPTION_IP6_SDENDPOINT_TYPE) -assert(opt.res_hdr == 0x00) -assert(opt.addr == "2001:cdba::3257:9652") -assert(opt.res_tail == 0x0) -assert(opt.l4_proto == 0x06) -assert(opt.port == 0x1234) +assert opt.len == SDOPTION_IP6_SDENDPOINT_LEN +assert opt.type == SDOPTION_IP6_SDENDPOINT_TYPE +assert opt.res_hdr == 0x00 +assert opt.addr == "2001:cdba::3257:9652" +assert opt.res_tail == 0x0 +assert opt.l4_proto == 0x06 +assert opt.port == 0x1234 = SDOption_IP6_SD_EndPoint: Build and dissect fully populated opt = SDOption_IP6_SD_EndPoint(len=0x1234, type=0x56, res_hdr=0x78, addr = "1234:5678:9abc:def0:0fed:cba9:8765:4321", res_tail = 0x9a, l4_proto = 0xbc, port = 0xdef0) optraw = opt.build() -assert(optraw == b"\x12\x34\x56\x78" + b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x0f\xed\xcb\xa9\x87\x65\x43\x21" + b"\x9a\xbc\xde\xf0") +assert optraw == b"\x12\x34\x56\x78" + b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x0f\xed\xcb\xa9\x87\x65\x43\x21" + b"\x9a\xbc\xde\xf0" opt = SDOption_IP6_SD_EndPoint(optraw) -assert(opt.len == 0x1234) -assert(opt.type == 0x56) -assert(opt.res_hdr == 0x78) -assert(opt.addr == "1234:5678:9abc:def0:fed:cba9:8765:4321") -assert(opt.res_tail == 0x9a) -assert(opt.l4_proto == 0xbc) -assert(opt.port == 0xdef0) +assert opt.len == 0x1234 +assert opt.type == 0x56 +assert opt.res_hdr == 0x78 +assert opt.addr == "1234:5678:9abc:def0:fed:cba9:8765:4321" +assert opt.res_tail == 0x9a +assert opt.l4_proto == 0xbc +assert opt.port == 0xdef0 = verify building and parsing of multiple SDOptions def _opts_check(opts): @@ -702,7 +716,7 @@ def _opts_check(opts): sd.len_option_array = optslen sd.show() SD(sd.build()).show() - assert(sd.show(dump=True) == SD(sd.build()).show(dump=True)) + assert sd.show(dump=True) == SD(sd.build()).show(dump=True) # options are built and reparsed, to make sure all is calculated opts = [ @@ -721,3 +735,64 @@ _opts_check(opts) _opts_check(opts[::-1]) _opts_check(opts + opts[::-1]) + += build test SOMEIP/TP + +p = SOMEIP(srv_id=1234, sub_id=4321, msg_type=0xff, retcode=0xff, offset=4294967040, data=[Raw(b"deadbeef")]) + +assert p.data[0].load == b"deadbeef" + += test fragment + +msg = bytes.fromhex("aabbccdd0003aabbccdd20608100a5dc0800450005a050ad400040117ee9c0a87262c0a872037725e107058c6b54402f801e0000057c0000000e0101220000000001123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210123456789abcdef0fedcba9876543210fedcba9876543210a1b2c3d4e5f678901234567890abcdef0f1e2d3c4b5a697889abcdef01234567f0e1d2c3b4a59687111122223333444455556666777788889999aaaabbbbccccdeadbeafbaddcafecafebabedeafbeef1122334455667788a1b2c3d4e5f6f7f823456789abcdef0199887766554433221a2b3c4d5e6f7a8beaf1234567890deffedcba987654321001f23e45d6789abce1f2d3c4b5a6d7e8c9a1b2f3e4d5a6b7d8e1f0a2b3c4d5e623a1b2c3d4e5f678f23456789abcdef09876543210abcdefabcdef012345678987654321f0e1d2c312f34d56a78b9c019a8b7c6d5e4f3a2b56789abcdef0123423456789abcdef01a1b2c3d4e5f678909876543210abcdefabcdef0123456789f23456789abcdef099887766554433221a2b3c4d5e6f7a8bf0e1d2c3b4a59687abcdef9876543210234567890abcdef19999aaaabbbbccccdeadbeafbaddcafecafebabedeafbeef111122223333444455556666777788889999aaaabbbbccccdeadbeafbaddcafecafebabedeafbeef1122334455667788a1b2c3d4e5f6f7f823456789abcdef0199887766554433221a2b3c4d5e6f7a8beaf1234567890deffedcba987654321001f23e45d6789abce1f2d3c4b5a6d7e8c9a1b2f3e4d5a6b7d8e1f0a2b3c4d5e623a1b2c3d4e5f678f23456789abcdef09876543210abcdefabcdef012345678987654321f0e1d2c312f34d56a78b9c019a8b7c6d5e4f3a2b56789abcdef0123423456789abcdef01a1b2c3d4e5f678909876543210abcdefabcdef0123456789f23456789abcdef099887766554433221a2b3c4d5e6f7a8bf0e1d2c3b4a59687abcdef9876543210234567890abcdef19999aaaabbbbccccdeadbeafbaddcafecafebabedeafbeef111122223333444455556666777788889999aaaabbbbccccdeadbeafbaddcafecafebabedeafbeef1122334455667788a1b2c3d4e5f6f7f823456789abcdef0199887766554433221a2b3c4d5e6f7a8beaf1234567890deffedcba987654321001f23e45d6789abce1f2d3c4b5a6d7e8c9a1b2f3e4d5a6b7d8e1f0a2b3c4d5e623a1b2c3d4e5f678f23456789abcdef09876543210abcdefabcdef0123456789123456789abcdef01a2b3c4d5e6f70819a8b7c6d5e4f3a21d1c2b3a4f5e60798a9b8c7d6e5f4f3d2123456789abcdef01f2e3d4c5b6a7980a4b3c2d1e0f1f8a9456789abcdef0123f1e2d3c4b5a60789d6c5b4a3f2e1f0a91e2d3c4b5a6078f09c8b7a6d5e4f3b212b1a3c4d5e6f7081a7b8c9d6e5f4f0d2f5e4d3c2b1a0798a8123456789abcdef1f2e3d4c5b6a7981a3b2c1d0f1e607929081726354abcdef0f1e2d3c4b5a60788b7a6c5d4e3f2109d4c3b2a1f0e6078a4f5e6d7c8b9a1234e9d8c7b6a5f4e308a1b2c3d4e5f678909c8b7a6d5e4f32103b2a1c0d5e6f7098a0b1c2d3e4f5e6176d5e4f3c2b1a7890d7c8b9a0f1e2f390f1e2d3c4b5a607899b8a7c6d5e4f3211d3c2b1a0f1e6078b8f9e6d7c5b4a3210b2c1a3d4e5f6f8090e1d2c3b4a5f6789c9b8a7d6e5f4e3087d6c5b4a3f2e10989a8b7c6d5e4f32106e5d4c3b2a1f70980a9b8c7d6e5f4d023e1f2d4c5b6a70988f9e7d6c5b4a3102") +pkt = Ether(msg)[SOMEIP] + +x = pkt.fragment(fragsize=100) +for i, p in enumerate(x): + if i == len(x) -1: + assert p.more_seg == 0 + assert len(p.data[0]) < 100 + else: + assert p.more_seg == 1 + assert len(p.data[0]) == 100 + += SOMEIP multiple frames in one TCP/UDP + +payload_3 = bytes.fromhex("deadbeef") +someip_3 = SOMEIP(srv_id=0xabcd, sub_id=0x8001, len=8 + len(payload_3)) +someip_3.payload = Raw(load=payload_3) + +payload_2 = bytes.fromhex("ff") +someip_23 = SOMEIP(srv_id=0x5678, sub_id=0x8002, len=8 + len(payload_2)) +someip_23.payload = Raw(load=payload_2 + bytes(someip_3)) + +payload_1 = bytes.fromhex("0000") +someip_123 = SOMEIP(srv_id=0x1234, sub_id=0x8001, len=8 + len(payload_1)) +someip_123.payload = Raw(load=payload_1 + bytes(someip_23)) + +eth_frame = ( + Ether(src="00:11:22:33:44:55", dst="AA:BB:CC:DD:EE:FF") + / IP(src="192.168.0.10", dst="192.168.0.20") + / UDP(sport=30501, dport=30491) + / someip_123 +) + +pkt = Ether(bytes(eth_frame)) + + +pkt.show() +layers = pkt.layers() +assert len(layers) == 6 +assert layers[-1] == SOMEIP +assert layers[-2] == SOMEIP +assert layers[-3] == SOMEIP + + +someip_123_x = pkt[SOMEIP] + +assert someip_123_x.data[0].load == payload_1 +someip_23_x = someip_123_x.payload +assert someip_23_x.data[0].load == payload_2 +someip_3_x = someip_23_x.payload +assert someip_3_x.data[0].load == payload_3 + diff --git a/test/contrib/automotive/testsocket.uts b/test/contrib/automotive/testsocket.uts new file mode 100644 index 00000000000..005daf99d54 --- /dev/null +++ b/test/contrib/automotive/testsocket.uts @@ -0,0 +1,109 @@ +% Regression tests for TestSocket + ++ Configuration +~ conf + += Imports + +from test.testsocket import TestSocket, cleanup_testsockets + += Create Dummy Packet + +class TestPacket(Packet): + fields_desc = [ + IntField("identifier", 0), + StrField("data", b"") + ] + def answers(self, other): + if other.__class__ != self.__class__: + return False + if self.identifier % 2: + return False + if self.identifier == (other.identifier + 1): + return True + return False + def hashret(self): + return struct.pack('I', self.identifier + (self.identifier % 2)) + + += Create Sockets + +sender = TestSocket(TestPacket) +receiver = TestSocket(TestPacket) +sender.pair(receiver) + ++ Basic tests + += Simple ping pong + +def create_answer(p): + ans = TestPacket(identifier=p.identifier + 1, data=p.data + b"_answer") + receiver.send(ans) + +t = AsyncSniffer(timeout=50, prn=create_answer, opened_socket=receiver) +t.start() + +pks = PacketList() + +for i in range(1, 2000, 2): + txp = TestPacket(identifier=i, data=b"hello"*i) + rxp = sender.sr1(txp, verbose=False, timeout=0.5) + pks.append(txp) + pks.append(rxp) + +t.stop(join=True) +convs = pks.sr() + +sender.close() +receiver.close() + +assert len(t.results) == 1000 +assert len(pks) == 2000 +assert len(convs[0]) == 1000 + += Simple ping pong with sr with packet generator 500 + +testlen = 500 + +sender = TestSocket(TestPacket) +receiver = TestSocket(TestPacket) +sender.pair(receiver) + +t = AsyncSniffer(timeout=10, prn=create_answer, opened_socket=receiver) +t.start() + +txp = TestPacket(identifier=range(1, testlen * 2, 2), data=b"test1") +rxp = sender.sr(txp, timeout=10, verbose=False, prebuild=True) +t.stop(join=True) + +print(rxp) +print(rxp[0].summary()) + +sender.close() +receiver.close() + +assert len(t.results) == testlen +assert len(rxp[0]) == testlen + += Simple ping pong with sr with generated packets + +sender = TestSocket(TestPacket) +receiver = TestSocket(TestPacket) +sender.pair(receiver) + +t = AsyncSniffer(timeout=10, prn=create_answer, opened_socket=receiver) +t.start() + +txp = [TestPacket(identifier=i, data=b"hello") for i in range(1, 2000, 2)] +rxp = sender.sr(txp, timeout=10, verbose=False) +t.stop(join=True) + +print(rxp) +assert len(t.results) == 1000 +assert len(rxp[0]) == 1000 + ++ Cleanup + += Delete TestSockets + +cleanup_testsockets() \ No newline at end of file diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index 51d0a840ec4..e4a6bf07ba8 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -9,10 +9,10 @@ + Basic operations = Load module +load_contrib("automotive.uds", globals_dict=globals()) +load_contrib("automotive.ecu", globals_dict=globals()) -load_contrib("automotive.uds") - -from scapy.contrib.automotive.ecu import ECU +from scapy.contrib.automotive.uds_ecu_states import * = Check if positive response answers @@ -26,7 +26,7 @@ dsc.hashret() == dscpr.hashret() = Check if negative response answers dsc = UDS(b'\x10') -neg = UDS(b'\x7f\x10') +neg = UDS(b'\x7f\x10\x00') assert neg.answers(dsc) = CHECK hashret NEG @@ -35,7 +35,7 @@ dsc.hashret() == neg.hashret() = Check if negative response answers not dsc = UDS(b'\x10') -neg = UDS(b'\x7f\x11') +neg = UDS(b'\x7f\x11\x00') assert not neg.answers(dsc) = Check if positive response answers not @@ -101,9 +101,10 @@ assert dscpr.service == 0x50 assert dscpr.diagnosticSessionType == 0x09 assert dscpr.sessionParameterRecord == b"beef" -ecu = ECU() +ecu = Ecu() +ecu.update(dsc) ecu.update(dscpr) -assert ecu.current_session == 9 +assert ecu.state.session == 9 = Check UDS_ER @@ -134,21 +135,18 @@ assert erpr.powerDownTime == 0x10 = Check UDS_ERPR modifies ecu state -erpr = UDS(b'\x51\x04\x10') +erpr = UDS(b'\x51\x01') assert erpr.service == 0x51 -assert erpr.resetType == 0x04 -assert erpr.powerDownTime == 0x10 - -ecu = ECU() -ecu.current_security_level = 5 -ecu.current_session = 3 -ecu.communication_control = 4 +assert erpr.resetType == 0x01 +ecu = Ecu() +ecu.state.security_level = 5 +ecu.state.session = 3 +ecu.state.communication_control = 4 +ecu.update(er) ecu.update(erpr) -assert ecu.current_session == 1 -assert ecu.current_security_level == 0 -assert ecu.communication_control == 0 +assert ecu.state.session == 1 = Check UDS_SA @@ -183,9 +181,9 @@ assert sapr.answers(sa) = Check UDS_SA -sa = UDS(b'\x27\x00c0ffee') +sa = UDS(b'\x27\x06c0ffee') assert sa.service == 0x27 -assert sa.securityAccessType == 0x0 +assert sa.securityAccessType == 0x6 assert sa.securityKey == b'c0ffee' @@ -195,9 +193,10 @@ sapr = UDS(b'\x67\x06') assert sapr.service == 0x67 assert sapr.securityAccessType == 0x6 -ecu = ECU() +ecu = Ecu() +ecu.update(sa) ecu.update(sapr) -assert ecu.current_security_level == 6 +assert ecu.state.security_level == 6 = Check UDS_SA @@ -237,9 +236,349 @@ ccpr = UDS(b'\x68\x01') assert ccpr.service == 0x68 assert ccpr.controlType == 0x1 -ecu = ECU() +ecu = Ecu() +ecu.update(cc) ecu.update(ccpr) -assert ecu.communication_control == 1 +assert ecu.state.communication_control == 1 + += Check UDS_AUTH + +auth = UDS(b"\x29\x00") +assert auth.service == 0x29 +assert auth.subFunction == 0x0 + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x0) +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x00\x00") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x0 +assert authpr.returnValue == 0x0 + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x0, returnValue=0x0) +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x01\x01\x00\x01\xFF\x00\x01\xFF") +assert auth.service == 0x29 +assert auth.subFunction == 0x1 +assert auth.communicationConfiguration == 0x1 +assert auth.lengthOfCertificateClient == 0x1 +assert auth.certificateClient == b"\xFF" +assert auth.lengthOfChallengeClient == 0x1 +assert auth.challengeClient == b"\xFF" + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x1, communicationConfiguration=0x1, + certificateClient=b"\xFF", challengeClient=b"\xFF") +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x01\x00\x00\x01\xFF\x00\x01\xFE") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x1 +assert authpr.returnValue == 0x0 +assert authpr.lengthOfChallengeServer == 0x1 +assert authpr.challengeServer == b"\xFF" +assert authpr.lengthOfEphemeralPublicKeyServer == 0x1 +assert authpr.ephemeralPublicKeyServer == b"\xFE" + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x1, returnValue=0x0, + challengeServer=b"\xFF", + ephemeralPublicKeyServer=b"\xFE") +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x02\x01\x00\x01\xFF\x00\x01\xFF") +assert auth.service == 0x29 +assert auth.subFunction == 0x2 +assert auth.communicationConfiguration == 0x1 +assert auth.lengthOfCertificateClient == 0x1 +assert auth.certificateClient == b"\xFF" +assert auth.lengthOfChallengeClient == 0x1 +assert auth.challengeClient == b"\xFF" + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x2, communicationConfiguration=0x1, + certificateClient=b"\xFF", challengeClient=b"\xFF") +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x02\x00\x00\x01\xFF\x00\x03\xC0\xFF\xEE\x00\x01\x56\x00" + + b"\x01\xFE") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x2 +assert authpr.returnValue == 0x0 +assert authpr.lengthOfChallengeServer == 0x1 +assert authpr.challengeServer == b"\xFF" +assert authpr.lengthOfCertificateServer == 0x3 +assert authpr.certificateServer == b"\xC0\xFF\xEE" +assert authpr.lengthOfProofOfOwnershipServer == 0x1 +assert authpr.proofOfOwnershipServer == b"\x56" +assert authpr.lengthOfEphemeralPublicKeyServer == 0x1 +assert authpr.ephemeralPublicKeyServer == b"\xFE" + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x2, returnValue=0x0, + challengeServer=b"\xFF", + certificateServer=b"\xC0\xFF\xEE", + proofOfOwnershipServer=b"\x56", + ephemeralPublicKeyServer=b"\xFE") +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x03\x00\x01\xFF\x00\x02\xFF\xFE") +assert auth.service == 0x29 +assert auth.subFunction == 0x3 +assert auth.lengthOfProofOfOwnershipClient == 0x1 +assert auth.proofOfOwnershipClient == b"\xFF" +assert auth.lengthOfEphemeralPublicKeyClient == 0x2 +assert auth.ephemeralPublicKeyClient == b"\xFF\xFE" + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x3, proofOfOwnershipClient=b"\xFF", + ephemeralPublicKeyClient=b"\xFF\xFE") +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x03\x00\x00\x01\xFE") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x3 +assert authpr.returnValue == 0x0 +assert authpr.lengthOfSessionKeyInfo == 0x1 +assert authpr.sessionKeyInfo == b"\xFE" + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x3, returnValue=0x0, + sessionKeyInfo=b"\xFE") +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x04\x00\x03\x00\x05\xFF\x00\x02\xFF\xFE") +assert auth.service == 0x29 +assert auth.subFunction == 0x4 +assert auth.certificateEvaluationId == 0x3 +assert auth.lengthOfCertificateData == 0x5 +assert auth.certificateData == b"\xFF\x00\x02\xFF\xFE" + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x4, certificateEvaluationId=0x3, + certificateData=b"\xFF\x00\x02\xFF\xFE") +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x04\x00") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x4 +assert authpr.returnValue == 0x0 + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x4, returnValue=0x0) +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x05\x01\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE\x34\x56\x03" + + b"\xFF\xEE\x20\x01") +assert auth.service == 0x29 +assert auth.subFunction == 0x5 +assert auth.communicationConfiguration == 0x1 +assert auth.algorithmIndicator == (b"\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE" + + b"\x34\x56\x03\xFF\xEE\x20\x01") + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x5, communicationConfiguration=0x1, + algorithmIndicator=(b"\x03\x00\x05\xFF\x00\x02" + + b"\xFF\xFE\xBE\x34\x56\x03" + + b"\xFF\xEE\x20\x01")) +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x05\x00\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE\x34\x56\x03" + + b"\xFF\xEE\x20\x01\x00\x01\xFF\x00\x00") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x5 +assert authpr.returnValue == 0x0 +assert authpr.algorithmIndicator == (b"\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE" + + b"\x34\x56\x03\xFF\xEE\x20\x01") +assert authpr.lengthOfChallengeServer == 0x1 +assert authpr.challengeServer == b"\xFF" +assert authpr.lengthOfNeededAdditionalParameter == 0x0 +assert authpr.neededAdditionalParameter == b"" + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x5, returnValue=0x0, + algorithmIndicator=(b"\x03\x00\x05\xFF\x00" + + b"\x02\xFF\xFE\xBE\x34" + + b"\x56\x03\xFF\xEE\x20" + + b"\x01"), + challengeServer=b"\xFF") +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x06\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE\x34\x56\x03\xFF" + + b"\xEE\x20\x01\x00\x01\xFF\x00\x01\xFF\x00\x00") +assert auth.service == 0x29 +assert auth.subFunction == 0x6 +assert auth.algorithmIndicator == (b"\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE" + + b"\x34\x56\x03\xFF\xEE\x20\x01") +assert auth.lengthOfProofOfOwnershipClient == 0x1 +assert auth.proofOfOwnershipClient == b"\xFF" +assert auth.lengthOfChallengeClient == 0x1 +assert auth.challengeClient == b"\xFF" +assert auth.lengthOfAdditionalParameter == 0x0 +assert auth.additionalParameter == b"" + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x6, + algorithmIndicator=(b"\x03\x00\x05\xFF\x00\x02" + + b"\xFF\xFE\xBE\x34\x56\x03" + + b"\xFF\xEE\x20\x01"), + proofOfOwnershipClient=b"\xFF", + challengeClient=b"\xFF") +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x06\x00\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE\x34\x56\x03" + + b"\xFF\xEE\x20\x01\x00\x01\xFE") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x6 +assert authpr.returnValue == 0x0 +assert auth.algorithmIndicator == (b"\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE" + + b"\x34\x56\x03\xFF\xEE\x20\x01") +assert authpr.lengthOfSessionKeyInfo == 0x1 +assert authpr.sessionKeyInfo == b"\xFE" + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x6, returnValue=0x0, + algorithmIndicator=(b"\x03\x00\x05\xFF\x00" + + b"\x02\xFF\xFE\xBE\x34" + + b"\x56\x03\xFF\xEE\x20\x01" + ), + sessionKeyInfo=b"\xFE") +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x07\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE\x34\x56\x03\xFF" + + b"\xEE\x20\x01\x00\x01\xFF\x00\x01\xFF\x00\x02\xC0\xCA") +assert auth.service == 0x29 +assert auth.subFunction == 0x7 +assert auth.algorithmIndicator == (b"\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE" + + b"\x34\x56\x03\xFF\xEE\x20\x01") +assert auth.lengthOfProofOfOwnershipClient == 0x1 +assert auth.proofOfOwnershipClient == b"\xFF" +assert auth.lengthOfChallengeClient == 0x1 +assert auth.challengeClient == b"\xFF" +assert auth.lengthOfAdditionalParameter == 0x2 +assert auth.additionalParameter == b"\xC0\xCA" + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x7, + algorithmIndicator=(b"\x03\x00\x05\xFF\x00\x02" + + b"\xFF\xFE\xBE\x34\x56\x03" + + b"\xFF\xEE\x20\x01"), + proofOfOwnershipClient=b"\xFF", + challengeClient=b"\xFF", + additionalParameter=b"\xC0\xCA") +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x07\x00\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE\x34\x56\x03" + + b"\xFF\xEE\x20\x01\x00\x02\xFE\x20\x00\x01\xFE") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x7 +assert authpr.returnValue == 0x0 +assert auth.algorithmIndicator == (b"\x03\x00\x05\xFF\x00\x02\xFF\xFE\xBE" + + b"\x34\x56\x03\xFF\xEE\x20\x01") +assert authpr.lengthOfProofOfOwnershipServer == 0x2 +assert authpr.proofOfOwnershipServer == b"\xFE\x20" +assert authpr.lengthOfSessionKeyInfo == 0x1 +assert authpr.sessionKeyInfo == b"\xFE" + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x7, returnValue=0x0, + algorithmIndicator=(b"\x03\x00\x05\xFF\x00" + + b"\x02\xFF\xFE\xBE\x34" + + b"\x56\x03\xFF\xEE\x20\x01" + ), + proofOfOwnershipServer=b"\xFE\x20", + sessionKeyInfo=b"\xFE") +assert bytes(authpr_build) == bytes(authpr) + += Check UDS_AUTH + +auth = UDS(b"\x29\x08") +assert auth.service == 0x29 +assert auth.subFunction == 0x8 + += Build UDS_AUTH + +auth_build = UDS()/UDS_AUTH(subFunction=0x8) +assert bytes(auth_build) == bytes(auth) + += Check UDS_AUTHPR + +authpr = UDS(b"\x69\x08\x00") +assert authpr.service == 0x69 +assert authpr.subFunction == 0x8 +assert authpr.returnValue == 0x0 + +assert authpr.answers(auth) + += Build UDS_AUTHPR + +authpr_build = UDS()/UDS_AUTHPR(subFunction=0x8) +assert bytes(authpr_build) == bytes(authpr) = Check UDS_TP @@ -285,15 +624,50 @@ assert atppr.timingParameterResponseRecord == b'coffee' = Check UDS_SDT -sdt = UDS(b'\x84coffee') +sdt = UDS(b'\x84\x80\x00\x01\x12\x34\x13\x37\x01coffee') +assert sdt.service == 0x84 +assert sdt.requestMessage == 0x1 +assert sdt.preEstablishedKeyUsed == 0x0 +assert sdt.encryptedMessage == 0x0 +assert sdt.signedMessage == 0x0 +assert sdt.signedResponseRequested == 0x0 +assert sdt.signatureEncryptionCalculation == 0x1 +assert sdt.signatureLength == 0x1234 +assert sdt.antiReplayCounter == 0x1337 +assert sdt.internalMessageServiceRequestId == 0x1 +assert sdt.dataRecord == b'coffee' + += Build UDS_SDT + +sdt = UDS()/UDS_SDT(requestMessage=0x1, signatureEncryptionCalculation=0x1, + signatureLength=0x1234, antiReplayCounter=0x1337, + internalMessageServiceRequestId=0x1, dataRecord=b'coffee') assert sdt.service == 0x84 -assert sdt.securityDataRequestRecord == b'coffee' +assert sdt.requestMessage == 0x1 +assert sdt.preEstablishedKeyUsed == 0x0 +assert sdt.encryptedMessage == 0x0 +assert sdt.signedMessage == 0x0 +assert sdt.signedResponseRequested == 0x0 +assert sdt.signatureEncryptionCalculation == 0x1 +assert sdt.signatureLength == 0x1234 +assert sdt.antiReplayCounter == 0x1337 +assert sdt.internalMessageServiceRequestId == 0x1 +assert sdt.dataRecord == b'coffee' = Check UDS_SDTPR -sdtpr = UDS(b'\xC4coffee') +sdtpr = UDS(b'\xC4\x04\x00\x01\x12\x34\x13\x37\x01coffee') assert sdtpr.service == 0xC4 -assert sdtpr.securityDataResponseRecord == b'coffee' +assert sdtpr.requestMessage == 0x0 +assert sdtpr.preEstablishedKeyUsed == 0x0 +assert sdtpr.encryptedMessage == 0x0 +assert sdtpr.signedMessage == 0x1 +assert sdtpr.signedResponseRequested == 0x0 +assert sdtpr.signatureEncryptionCalculation == 0x1 +assert sdtpr.signatureLength == 0x1234 +assert sdtpr.antiReplayCounter == 0x1337 +assert sdtpr.internalMessageServiceResponseId == 0x1 +assert sdtpr.dataRecord == b'coffee' assert sdtpr.answers(sdt) @@ -384,6 +758,30 @@ assert rdbi.identifiers[0] == 0x0102 assert rdbi.identifiers[1] == 0x0304 assert raw(rdbi) == b'\x22\x01\x02\x03\x04' += Test observable dict used in UDS_RDBI, setter + +UDS_RDBI.dataIdentifiers[0x102] = "turbo" +UDS_RDBI.dataIdentifiers[0x103] = "fullspeed" + +rdbi = UDS()/UDS_RDBI(identifiers=[0x102, 0x103]) + +assert "turbo" in plain_str(repr(rdbi)) +assert "fullspeed" in plain_str(repr(rdbi)) + += Test observable dict used in UDS_RDBI, deleter + +UDS_RDBI.dataIdentifiers[0x102] = "turbo" + +rdbi = UDS()/UDS_RDBI(identifiers=[0x102, 0x103]) +assert "turbo" in plain_str(repr(rdbi)) + +del UDS_RDBI.dataIdentifiers[0x102] +UDS_RDBI.dataIdentifiers[0x103] = "slowspeed" + +rdbi = UDS()/UDS_RDBI(identifiers=[0x102, 0x103]) + +assert "turbo" not in plain_str(repr(rdbi)) +assert "slowspeed" in plain_str(repr(rdbi)) = Check UDS_RDBIPR @@ -648,6 +1046,20 @@ assert rdtcipr.DTCCount == 0xddaa assert rdtcipr.answers(rdtci) +rdtcipr1 = UDS(b'\x59\x02\xff\x11\x07\x11\'\x022\x12\'\x01\x07\x11\'\x01\x18\x12\'\x01\x13\x12\'\x01"\x11\'\x06C\x00\'\x06S\x00\'\x161\x00\'\x14\x03\x12\'') + +assert len(rdtcipr1.DTCAndStatusRecord) == 10 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.system == 0 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.type == 1 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.numeric_value_code == 263 +assert rdtcipr1.DTCAndStatusRecord[0].dtc.additional_information_code == 17 +assert rdtcipr1.DTCAndStatusRecord[0].status == 0x27 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.system == 0 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.type == 1 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.numeric_value_code == 1027 +assert rdtcipr1.DTCAndStatusRecord[-1].dtc.additional_information_code == 18 +assert rdtcipr1.DTCAndStatusRecord[-1].status == 0x27 + = Check UDS_RDTCI rdtci = UDS(b'\x19\x02\xff') @@ -688,9 +1100,7 @@ assert rdtci.DTCStatusMask == 0xff rdtci = UDS(b'\x19\x03\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x03 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCSnapshotRecordNumber == 0xaa = Check UDS_RDTCI @@ -698,9 +1108,7 @@ assert rdtci.DTCSnapshotRecordNumber == 0xaa rdtci = UDS(b'\x19\x04\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x04 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCSnapshotRecordNumber == 0xaa = Check UDS_RDTCI @@ -715,9 +1123,7 @@ assert rdtci.DTCSnapshotRecordNumber == 0xaa rdtci = UDS(b'\x19\x06\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x06 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCExtendedDataRecordNumber == 0xaa = Check UDS_RDTCI @@ -741,31 +1147,47 @@ assert rdtci.DTCStatusMask == 0xbb rdtci = UDS(b'\x19\x09\xff\xee\xdd') assert rdtci.service == 0x19 assert rdtci.reportType == 0x09 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) = Check UDS_RDTCI rdtci = UDS(b'\x19\x10\xff\xee\xdd\xaa') assert rdtci.service == 0x19 assert rdtci.reportType == 0x10 -assert rdtci.DTCHighByte == 0xff -assert rdtci.DTCMiddleByte == 0xee -assert rdtci.DTCLowByte == 0xdd +assert rdtci.dtc == DTC(bytes.fromhex("ffeedd")) assert rdtci.DTCExtendedDataRecordNumber == 0xaa = Check UDS_RDTCIPR -rdtcipr = UDS(b'\x59\x02\xff\xee\xdd\xaa') +rdtcipr = UDS(b'\x59\x02\xff\xee\xdd\xaa\x02') +rdtcipr.show() assert rdtcipr.service == 0x59 assert rdtcipr.reportType == 2 assert rdtcipr.DTCStatusAvailabilityMask == 0xff -assert rdtcipr.DTCAndStatusRecord == b'\xee\xdd\xaa' +assert rdtcipr.DTCAndStatusRecord[0].dtc.system == 3 +assert rdtcipr.DTCAndStatusRecord[0].dtc.type == 2 +assert rdtcipr.DTCAndStatusRecord[0].dtc.numeric_value_code == 3805 +assert rdtcipr.DTCAndStatusRecord[0].dtc.additional_information_code == 170 +assert rdtcipr.DTCAndStatusRecord[0].status == 2 assert not rdtcipr.answers(rdtci) += Check UDS_RDTCIPR extended data + +p = UDS(b'Y\x06\x80SV`\x01\x00\x02\x01\x03\x15') + +assert len(p.extendedDataRecord.extendedData) == 3 + +assert p.extendedDataRecord.extendedData[0].data_type == 1 +assert p.extendedDataRecord.extendedData[1].data_type == 2 +assert p.extendedDataRecord.extendedData[2].data_type == 3 + +assert p.extendedDataRecord.extendedData[0].record == 0 +assert p.extendedDataRecord.extendedData[1].record == 1 +assert p.extendedDataRecord.extendedData[2].record == 0x15 + + = Check UDS_RDTCIPR rdtcipr = UDS(b'\x59\x03\xff\xee\xdd\xaa') @@ -773,13 +1195,30 @@ assert rdtcipr.service == 0x59 assert rdtcipr.reportType == 3 assert rdtcipr.dataRecord == b'\xff\xee\xdd\xaa' + += Check UDS_RDTCIPR 2 +req = UDS(bytes.fromhex("1904480a46ff")) +resp = UDS(bytes.fromhex("5904480a46af000b170002ff6417010a8278fa170c2ff1800000800104800200028003400a8004808005054002400a400004010b170002ff6417010a82ec69170c2f2c800000800100800200028003400a80048080050540024017400004")) + +assert resp.answers(req) + +req = UDS(bytes.fromhex("1904480a47ff")) +resp = UDS(bytes.fromhex("5904480a46af000b170002ff6417010a8278fa170c2ff1800000800104800200028003400a8004808005054002400a400004010b170002ff6417010a82ec69170c2f2c800000800100800200028003400a80048080050540024017400004")) + +assert not resp.answers(req) + +req = UDS(bytes.fromhex("1906480a46ff")) +resp = UDS(bytes.fromhex("5906480a46af010002070328")) + +assert resp.answers(req) + = Check UDS_RC rc = UDS(b'\x31\x03\xff\xee\xdd\xaa') assert rc.service == 0x31 assert rc.routineControlType == 3 assert rc.routineIdentifier == 0xffee -assert rc.routineControlOptionRecord == b'\xdd\xaa' +assert rc.load == b'\xdd\xaa' = Check UDS_RC @@ -787,7 +1226,7 @@ rc = UDS(b'\x31\x03\xff\xee\xdd\xaa') assert rc.service == 0x31 assert rc.routineControlType == 3 assert rc.routineIdentifier == 0xffee -assert rc.routineControlOptionRecord == b'\xdd\xaa' +assert rc.load == b'\xdd\xaa' = Check UDS_RCPR @@ -796,7 +1235,7 @@ rcpr = UDS(b'\x71\x03\xff\xee\xdd\xaa') assert rcpr.service == 0x71 assert rcpr.routineControlType == 3 assert rcpr.routineIdentifier == 0xffee -assert rcpr.routineStatusRecord == b'\xdd\xaa' +assert rcpr.load == b'\xdd\xaa' = Check UDS_RD @@ -945,8 +1384,53 @@ assert rtepr.answers(rte) iocbi = UDS(b'\x2f\x23\x34\xffcoffee') assert iocbi.service == 0x2f assert iocbi.dataIdentifier == 0x2334 -assert iocbi.controlOptionRecord == 255 -assert iocbi.controlEnableMaskRecord == b'coffee' +assert iocbi.load == b'\xffcoffee' + += Check UDS_RFT + +rft = UDS(b'\x38\x01\x00\x1ED:\\mapdata\\europe\\germany1.yxz\x11\x02\xC3\x50\x75\x30') +assert rft.service == 0x38 +assert rft.modeOfOperation == 0x01 +assert rft.filePathAndNameLength == 0x001e +assert rft.filePathAndName == b'D:\\mapdata\\europe\\germany1.yxz' +assert rft.compressionMethod == 1 +assert rft.encryptingMethod == 1 +assert rft.fileSizeParameterLength == 0x02 +assert rft.fileSizeUnCompressed == b'\xc3\x50' +assert rft.fileSizeCompressed == b'\x75\x30' + += Build UDS_RFT + +rft_build = UDS()/UDS_RFT(modeOfOperation=0x1, + filePathAndName=(b'D:\\mapdata\\europe\\' + + b'germany1.yxz'), + compressionMethod=1, encryptingMethod=1, + fileSizeUnCompressed=b'\xc3\x50', + fileSizeCompressed=b'\x75\x30') +assert bytes(rft_build) == bytes(rft) + += Check UDS_RFTPR + +rftpr = UDS(b'\x78\x01\x02\xc3\x50\x11') +assert rftpr.service == 0x78 +assert rftpr.modeOfOperation == 0x01 +assert rftpr.lengthFormatIdentifier == 0x02 +assert rftpr.maxNumberOfBlockLength == b'\xc3\x50' +assert rftpr.compressionMethod == 1 +assert rftpr.encryptingMethod == 1 + +assert rftpr.answers(rft) + += Build UDS_RFTPR +rftpr_build = UDS()/UDS_RFTPR(modeOfOperation=0x1, + maxNumberOfBlockLength=b'\xc3\x50', + compressionMethod=1, encryptingMethod=1) +assert bytes(rftpr_build) == bytes(rftpr) + += Check (invalid) UDS_NRC, no reply-to service + +nrc = UDS(b'\x7f') +assert nrc.service == 0x7f = Check UDS_NRC @@ -955,6 +1439,178 @@ assert nrc.service == 0x7f assert nrc.requestServiceId == 0x22 assert nrc.negativeResponseCode == 0x33 ++ Single layer UDS mode + += Single layer mode: enable and basic dissect + +conf.contribs['UDS']['single_layer_mode'] = True + +dsc = UDS(b'\x10\x01') +assert isinstance(dsc, UDS_DSC), "Expected UDS_DSC, got %s" % type(dsc) +assert dsc.service == 0x10 +assert dsc.diagnosticSessionType == 0x01 + += Single layer mode: build UDS_DSC + +dsc_built = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_built) == b'\x10\x01', "Expected b'\\x10\\x01', got %s" % bytes(dsc_built).hex() + += Single layer mode: UDS() / UDS_DSC() still works in single layer mode + +dsc_two = UDS_DSC(service=0x10, diagnosticSessionType=0x01) +assert dsc_two.service == 0x10 +assert dsc_two.diagnosticSessionType == 0x01 + += Single layer mode: dissect positive response + +dscpr = UDS(b'\x50\x01beef') +assert isinstance(dscpr, UDS_DSCPR), "Expected UDS_DSCPR, got %s" % type(dscpr) +assert dscpr.service == 0x50 +assert dscpr.diagnosticSessionType == 0x01 +assert dscpr.sessionParameterRecord == b"beef" + += Single layer mode: answers() between subpackets + +dsc = UDS_DSC(diagnosticSessionType=0x01) +dscpr = UDS_DSCPR(diagnosticSessionType=0x01, sessionParameterRecord=b"beef") +assert dscpr.answers(dsc) + += Single layer mode: answers() negative (different session type) + +dsc2 = UDS_DSC(diagnosticSessionType=0x02) +dscpr2 = UDS_DSCPR(diagnosticSessionType=0x01) +assert not dscpr2.answers(dsc2) + += Single layer mode: NegativeResponse dissect + +nr = UDS(b'\x7f\x10\x22') +assert isinstance(nr, UDS_NR), "Expected UDS_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.negativeResponseCode == 0x22 + += Single layer mode: NegativeResponse answers() + +dsc3 = UDS_DSC(diagnosticSessionType=0x01) +nr2 = UDS_NR(requestServiceId=0x10, negativeResponseCode=0x22) +assert nr2.answers(dsc3) + += Single layer mode: NegativeResponse does not answer wrong service + +er = UDS_ER(resetType=0x01) +assert not nr2.answers(er) + += Single layer mode: hashret consistency between request and positive response + +dsc4 = UDS_DSC(diagnosticSessionType=0x01) +dscpr4 = UDS_DSCPR(diagnosticSessionType=0x01) +assert dsc4.hashret() == dscpr4.hashret(), \ + "hashret mismatch: %s vs %s" % (dsc4.hashret().hex(), dscpr4.hashret().hex()) + += Single layer mode: UDS_RDBI dissect + +rdbi = UDS(b'\x22\x01\x02\x03\x04') +assert isinstance(rdbi, UDS_RDBI), "Expected UDS_RDBI, got %s" % type(rdbi) +assert rdbi.service == 0x22 + += Single layer mode: unknown service falls back to UDS + +unknown = UDS(b'\xAA\x01\x02') +assert isinstance(unknown, UDS), "Expected UDS fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['UDS']['single_layer_mode'] = False + +dsc5 = UDS(b'\x10\x01') +assert dsc5.__class__ == UDS +assert dsc5.service == 0x10 +assert dsc5[UDS_DSC].diagnosticSessionType == 0x01 + +dscpr5 = UDS(b'\x50\x01beef') +assert dscpr5.__class__ == UDS +assert dscpr5.service == 0x50 +assert dscpr5[UDS_DSCPR].diagnosticSessionType == 0x01 + += Single layer mode: enable via conf directly + +conf.contribs['UDS']['single_layer_mode'] = True + +er6 = UDS(b'\x11\x01') +assert isinstance(er6, UDS_ER), "Expected UDS_ER, got %s" % type(er6) +assert er6.service == 0x11 +assert er6.resetType == 0x01 + += Single layer mode: final cleanup - restore default multi-layer mode + +conf.contribs['UDS']['single_layer_mode'] = False +assert not conf.contribs['UDS']['single_layer_mode'] + ++ Compatibility mode UDS + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['UDS']['single_layer_mode'] = True +conf.contribs['UDS']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +dsc_sa = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_sa) == b'\x10\x01', \ + "Standalone UDS_DSC should include service byte, got %s" % bytes(dsc_sa).hex() +assert dsc_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked) == b'\x10\x01', \ + "Stacked UDS/UDS_DSC should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: positive response stacked suppresses service + +stacked_pr = UDS() / UDS_DSCPR(diagnosticSessionType=0x01, sessionParameterRecord=b"") +assert bytes(stacked_pr) == b'\x50\x01', \ + "Stacked UDS/UDS_DSCPR should produce 2 bytes, got %s" % bytes(stacked_pr).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +dsc_dis = UDS(b'\x10\x01') +assert isinstance(dsc_dis, UDS_DSC) +assert dsc_dis.service == 0x10 +assert dsc_dis.diagnosticSessionType == 0x01 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['UDS']['compatibility_mode'] = False + +stacked_nc = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked_nc) == b'\x10\x10\x01', \ + "With compat OFF, stacked UDS/UDS_DSC should produce 3 bytes (duplicate service), got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +dsc_nc = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_nc) == b'\x10\x01', \ + "Standalone UDS_DSC should include service byte even with compat OFF, got %s" % bytes(dsc_nc).hex() + += Compatibility mode OFF + SLM ON: dissect standalone still works + +dsc_dis2 = UDS(b'\x10\x01') +assert isinstance(dsc_dis2, UDS_DSC) +assert dsc_dis2.service == 0x10 + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['UDS']['single_layer_mode'] = False +conf.contribs['UDS']['compatibility_mode'] = False +stacked_slm_off = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked_slm_off) == b'\x10\x01', \ + "With SLM OFF, no service field in UDS_DSC regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() += Compatibility mode: cleanup +conf.contribs['UDS']['single_layer_mode'] = False +conf.contribs['UDS']['compatibility_mode'] = True +assert not conf.contribs['UDS']['single_layer_mode'] +assert conf.contribs['UDS']['compatibility_mode'] diff --git a/test/contrib/automotive/uds_utils.uts b/test/contrib/automotive/uds_utils.uts deleted file mode 100644 index 11be0d586a0..00000000000 --- a/test/contrib/automotive/uds_utils.uts +++ /dev/null @@ -1,211 +0,0 @@ -% Regression tests for uds_utils - -+ Configuration -~ conf - -= Imports -load_layer("can") -conf.contribs['CAN']['swap-bytes'] = False -import os, threading, six, subprocess, sys -from subprocess import call -from scapy.consts import LINUX - -= Definition of constants, utility functions and mock classes -iface0 = "vcan0" -iface1 = "vcan1" - -# function to exit when the can-isotp kernel module is not available -ISOTP_KERNEL_MODULE_AVAILABLE = False -def exit_if_no_isotp_module(): - if not ISOTP_KERNEL_MODULE_AVAILABLE: - sys.stderr.write("TEST SKIPPED: can-isotp not available" + os.linesep) - warning("Can't test ISOTP native socket because kernel module is not loaded") - exit(0) - - -= Initialize a virtual CAN interface -~ vcan_socket needs_root linux -if 0 != call(["cansend", iface0, "000#"]): - # vcan0 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface0, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface0) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface0, "up"]): - raise Exception("could not bring up %s" % iface0) - -if 0 != call(["cansend", iface0, "000#"]): - raise Exception("cansend doesn't work") - -if 0 != call(["cansend", iface1, "000#"]): - # vcan1 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface1, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface1) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface1, "up"]): - raise Exception("could not bring up %s" % iface1) - -if 0 != call(["cansend", iface1, "000#"]): - raise Exception("cansend doesn't work") - -print("CAN should work now") - -= Import CANSocket - -from scapy.contrib.cansocket_python_can import * - -new_can_socket = lambda iface: CANSocket(bustype='virtual', channel=iface) -new_can_socket0 = lambda: CANSocket(bustype='virtual', channel=iface0, timeout=0.01) -new_can_socket1 = lambda: CANSocket(bustype='virtual', channel=iface1, timeout=0.01) - -# utility function for draining a can interface, asserting that no packets are there -def drain_bus(iface=iface0, assert_empty=True): - with new_can_socket(iface) as s: - pkts = s.sniff(timeout=0.1) - if assert_empty: - assert len(pkts) == 0 - -print("CAN sockets should work now") - -= Overwrite definition for vcan_socket systems native sockets -~ vcan_socket not_pypy needs_root linux - -if six.PY3 and LINUX: - from scapy.contrib.cansocket_native import * - new_can_socket = lambda iface: CANSocket(iface) - new_can_socket0 = lambda: CANSocket(iface0) - new_can_socket1 = lambda: CANSocket(iface1) - - -= Overwrite definition for vcan_socket systems python-can sockets -~ vcan_socket needs_root linux -if "python_can" in CANSocket.__module__: - new_can_socket = lambda iface: CANSocket(bustype='socketcan', channel=iface, bitrate=250000, timeout=0.01) - new_can_socket0 = lambda: CANSocket(bustype='socketcan', channel=iface0, bitrate=250000, timeout=0.01) - new_can_socket1 = lambda: CANSocket(bustype='socketcan', channel=iface1, bitrate=250000, timeout=0.01) - -= Verify that a CAN socket can be created and closed -s = new_can_socket(iface0) -s.close() - - -= Check if can-isotp and can-utils are installed on this system -~ linux -p1 = subprocess.Popen(['lsmod'], stdout = subprocess.PIPE) -p2 = subprocess.Popen(['grep', '^can_isotp'], stdout = subprocess.PIPE, stdin=p1.stdout) -p1.stdout.close() -if p1.wait() == 0 and p2.wait() == 0 and b"can_isotp" in p2.stdout.read(): - p = subprocess.Popen(["isotpsend", "-s1", "-d0", iface0], stdin = subprocess.PIPE) - p.communicate(b"01") - if p.returncode == 0: - ISOTP_KERNEL_MODULE_AVAILABLE = True - - -+ Syntax check - -= Import isotp -conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': ISOTP_KERNEL_MODULE_AVAILABLE} -load_contrib("isotp") - -if six.PY3 and ISOTP_KERNEL_MODULE_AVAILABLE: - from scapy.contrib.isotp import ISOTPNativeSocket - ISOTPSocket = ISOTPNativeSocket - assert ISOTPSocket == ISOTPNativeSocket -else: - from scapy.contrib.isotp import ISOTPSoftSocket - ISOTPSocket = ISOTPSoftSocket - assert ISOTPSocket == ISOTPSoftSocket - -############ -############ -+ Load general modules - -= Load contribution layer -load_contrib('automotive.uds') - - -= Test Session Enumerator -drain_bus(iface0) -drain_bus(iface1) - -packet = ISOTP('Request') -succ = False - -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x241, did=0x641, basecls=UDS) as sendSock, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x641, did=0x241, basecls=UDS) as recvSock: - def answer(pkt): - pkt.service = pkt.service + 0x40 - recvSock.send(pkt) - def sniffer(): - global sniffed, succ - sniffed = 0 - pkts = recvSock.sniff(timeout=10, prn=answer) - sniffed = len(pkts) - succ = True - threadSniffer = threading.Thread(target=sniffer) - threadSniffer.start() - sessions = UDS_SessionEnumerator(sendSock, session_range=range(3)) - threadSniffer.join() - -assert sniffed == 3*2 -assert succ - - -= Test Service Enumerator -drain_bus(iface0) -drain_bus(iface1) - -packet = ISOTP('Request') -succ = False - -with new_can_socket0() as isocan1, ISOTPSocket(isocan1, sid=0x241, did=0x641, basecls=UDS) as sendSock, \ - new_can_socket0() as isocan2, ISOTPSocket(isocan2, sid=0x641, did=0x241, basecls=UDS) as recvSock: - def answer(pkt): - pkt.service = pkt.service + 0x40 - if pkt.service == 0x7f: - pkt = UDS()/UDS_NR(requestServiceId=0x3f) - recvSock.send(pkt) - def sniffer(): - global sniffed, succ - sniffed = 0 - pkts = recvSock.sniff(timeout=10, prn=answer) - sniffed = len(pkts) - succ = True - threadSniffer = threading.Thread(target=sniffer) - threadSniffer.start() - services = UDS_ServiceEnumerator(sendSock) - threadSniffer.join() - -assert sniffed == 128 -assert succ - - -= Test getTableEntry -a = ('DefaultSession', UDS()/UDS_SAPR()) -b = ('ProgrammingSession', UDS()/UDS_NR(requestServiceId=0x10, negativeResponseCode=0x13)) -c = ('ExtendedDiagnosticSession', UDS()/UDS_IOCBI()) - -res_a = getTableEntry(a) -res_b = getTableEntry(b) -res_c = getTableEntry(c) - -print(res_a) -print(res_b) -print(res_c) -#make_lined_table([a, b, c], getTableEntry) - -assert res_a == ('DefaultSession', '0x27: SecurityAccess', 'PositiveResponse') -assert res_b == ('ProgrammingSession', '0x10: DiagnosticSessionControl', 'incorrectMessageLengthOrInvalidFormat') -assert res_c == ('ExtendedDiagnosticSession', '0x2f: InputOutputControlByIdentifier', 'PositiveResponse') - -+ Cleanup - -= Delete vcan interfaces -~ vcan_socket needs_root linux - -if 0 != call(["sudo", "ip", "link", "delete", iface0]): - raise Exception("%s could not be deleted" % iface0) - -if 0 != call(["sudo", "ip", "link", "delete", iface1]): - raise Exception("%s could not be deleted" % iface1) diff --git a/test/contrib/automotive/xcp/xcp.uts b/test/contrib/automotive/xcp/xcp.uts new file mode 100644 index 00000000000..69175764ded --- /dev/null +++ b/test/contrib/automotive/xcp/xcp.uts @@ -0,0 +1,691 @@ +% Regression tests for the XCP +# More information at http://www.secdev.org/projects/UTscapy/ + +############ +############ + ++ Basic operations + += Load module + +load_layer("can", globals_dict=globals()) +conf.contribs['CAN']['swap-bytes'] = False +load_contrib("automotive.xcp.xcp", globals_dict=globals()) + + += Test padding + +conf.contribs["XCP"]["add_padding_for_can"] = True + +pkt = XCPOnCAN(identifier=0x700) / CTORequest() / Connect() +build_pkt = bytes(pkt) +hexdump(build_pkt) +assert build_pkt == b'\x00\x00\x07\x00\x08\x00\x00\x00\xff\x00\xcc\xcc\xcc\xcc\xcc\xcc' +conf.contribs["XCP"]["add_padding_for_can"] = False + += test_get_com_mode_info +conf.contribs["XCP"]["add_padding_for_can"] = False + +cto_request = CTORequest() / GetCommModeInfo() +assert cto_request.pid == 0xfb +assert bytes(cto_request) == b'\xfb' + +cto_response = CTOResponse(b'\xff\x00\x01\x00\x02\x00\x00\x64') +assert cto_response.packet_code == 0xFF + +assert cto_response.answers(cto_request) + +get_comm_mode_info_response = cto_response["CommonModeInfoPositiveResponse"] +assert "master_block_mode" in get_comm_mode_info_response.comm_mode_optional +assert get_comm_mode_info_response.max_bs == 0x02 +assert get_comm_mode_info_response.min_st == 0x00 +assert get_comm_mode_info_response.xcp_driver_version_number == 0x64 + += test_get_status + +cto_request = CTORequest() / GetStatus() +assert cto_request.pid == 0xfd +assert bytes(cto_request) == b'\xfd' + +cto_response = CTOResponse(b'\xff\x00\x15\x00\x00\x00') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +get_comm_mode_info_response = cto_response["StatusPositiveResponse"] +assert get_comm_mode_info_response.current_session_status == 0x00 +assert "cal_pag" in get_comm_mode_info_response.current_resource_protection_status +assert "x1" not in get_comm_mode_info_response.current_resource_protection_status +assert "daq" in get_comm_mode_info_response.current_resource_protection_status +assert "stim" not in get_comm_mode_info_response.current_resource_protection_status +assert "pgm" in get_comm_mode_info_response.current_resource_protection_status +assert "x5" not in get_comm_mode_info_response.current_resource_protection_status +assert "x6" not in get_comm_mode_info_response.current_resource_protection_status +assert "x7" not in get_comm_mode_info_response.current_resource_protection_status + +assert get_comm_mode_info_response.session_configuration_id == 0x0000 + += test_get_seed + +conf.contribs['XCP']['MAX_CTO'] = 8 +cto_request = CTORequest() / GetSeed(b'\x00\x01') +assert cto_request.pid == 0xf8 +assert bytes(cto_request) == b'\xf8\x00\x01' + +cto_response = CTOResponse(b'\xff\x06\x00\x01\x02\x03\x04\x05') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +get_seed_response = cto_response["SeedPositiveResponse"] +assert get_seed_response.seed_length == 0x06 +assert get_seed_response.seed == b'\x00\x01\x02\x03\x04\x05' + += test_unlock + +conf.contribs['XCP']['MAX_CTO'] = 8 +cto_request = CTORequest() / Unlock(b'\x06\x69\xAB\xA6\x00\x00\x00') +assert cto_request.pid == 0xf7 +assert cto_request['Unlock'].len == 0x06 +assert cto_request['Unlock'].seed == b'\x69\xAB\xA6\x00\x00\x00' +assert bytes(cto_request) == b'\xf7\x06\x69\xAB\xA6\x00\x00\x00' + +cto_response = CTOResponse(b'\xff\x14') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +unlock_response = cto_response["UnlockPositiveResponse"] +assert unlock_response.current_resource_protection_status == 0x14 +assert "cal_pag" not in unlock_response.current_resource_protection_status +assert "x1" not in unlock_response.current_resource_protection_status +assert "daq" in unlock_response.current_resource_protection_status +assert "stim" not in unlock_response.current_resource_protection_status +assert "pgm" in unlock_response.current_resource_protection_status +assert "x5" not in unlock_response.current_resource_protection_status +assert "x6" not in unlock_response.current_resource_protection_status +assert "x7" not in unlock_response.current_resource_protection_status + += test_get_id + +conf.contribs['XCP']['byte_order'] = 0 +cto_request = CTORequest() / GetId(b'\x01') +assert cto_request.pid == 0xfa +assert bytes(cto_request) == b'\xfa\x01' +assert cto_request['GetId'].identification_type == 0x01 + +cto_response = CTOResponse(b'\xff\x00\x00\x00\x06\x00\x00\x00') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +get_id_response = cto_response["IdPositiveResponse"] +assert get_id_response.mode == 0x00 +assert get_id_response.length == 6 + + += test_upload + +conf.contribs['XCP']['MAX_CTO'] = 8 +conf.contribs['XCP']['Address_Granularity_Byte'] = 1 + +cto_request = CTORequest() / Upload(b'\x06') +assert cto_request.pid == 0xf5 +assert bytes(cto_request) == b'\xf5\x06' +assert cto_request['Upload'].nr_of_data_elements == 0x06 + +cto_response = CTOResponse(b'\xff\x58\x43\x50\x53\x49\x4D') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +upload_response = cto_response["UploadPositiveResponse"] +assert upload_response.element == b'\x58\x43\x50\x53\x49\x4D' + += test_cal_page + +cto_request = CTORequest() / GetCalPage(b'\x01\x00') +assert cto_request.pid == 0xea +assert bytes(cto_request) == b'\xea\x01\x00' + +cto_response = CTOResponse(b'\xff\x00\x00\x01') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +get_cal_page_response = cto_response["CalPagePositiveResponse"] +assert get_cal_page_response.logical_data_page_number == 0x01 + += test_set_mta + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / SetMta(b'\xff\xff\x00\x3c\x00\x00\x00') +assert cto_request.pid == 0xf6 +assert bytes(cto_request) == b'\xf6\xff\xff\x00\x3c\x00\x00\x00' +assert cto_request['SetMta'].address_extension == 0x00 +assert cto_request['SetMta'].address == 0x3C + +cto_response = CTOResponse(b'\xff') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_build_checksum + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / BuildChecksum(b'\xff\xff\xff\xad\x0d\x00\x00') +assert cto_request.pid == 0xf3 +assert bytes(cto_request) == b'\xf3\xff\xff\xff\xad\x0d\x00\x00' +assert hex(cto_request['BuildChecksum'].block_size) == '0xdad' + +cto_response = CTOResponse(b'\xff\x02\xff\xff\x2C\x87\x00\x00') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +build_checksum_response = cto_response["ChecksumPositiveResponse"] +assert build_checksum_response.checksum_type == 0x02 +assert hex(build_checksum_response.checksum) == '0x872c' + += test_download + +conf.contribs['XCP']['byte_order'] = 0 +conf.contribs['XCP']['MAX_CTO'] = 8 +conf.contribs['XCP']['Address_Granularity_Byte'] = 1 + +cto_request = CTORequest() / Download(b'\x04\x00\x00\x80\x3f') +assert cto_request.pid == 0xf0 +assert bytes(cto_request) == b'\xf0\x04\x00\x00\x80\x3f' +assert cto_request['Download'].nr_of_data_elements == 0x04 +assert cto_request['Download'].data_elements == b'\x00\x00\x80\x3f' + +cto_response = CTOResponse(b'\xff') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_short_upload + +conf.contribs['XCP']['byte_order'] = 0 +conf.contribs['XCP']['MAX_CTO'] = 8 +conf.contribs['XCP']['Address_Granularity_Byte'] = 1 + +cto_request = CTORequest() / ShortUpload(b'\04\xff\x00\x60\x00\x00\x00') +assert cto_request.pid == 0xf4 +assert bytes(cto_request) == b'\xf4\x04\xff\x00\x60\x00\x00\x00' +assert cto_request['ShortUpload'].nr_of_data_elements == 0x04 +assert cto_request['ShortUpload'].address_extension == 0x00 +assert hex(cto_request['ShortUpload'].address) == '0x60' + +cto_response = CTOResponse(b'\xff\x00\x00\x80\x3F') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +upload_response = cto_response["ShortUploadPositiveResponse"] +assert upload_response.element == b'\x00\x00\x80\x3F' + += test_copy_cal_page + +cto_request = CTORequest() / CopyCalPage(b'\00\x01\x02\x03') +assert cto_request.pid == 0xe4 +assert bytes(cto_request) == b'\xe4\00\x01\x02\x03' +assert cto_request['CopyCalPage'].segment_num_src == 0x00 +assert cto_request['CopyCalPage'].page_num_src == 0x01 +assert cto_request['CopyCalPage'].segment_num_dst == 0x02 +assert cto_request['CopyCalPage'].page_num_dst == 0x03 + +cto_response = CTOResponse(b'\xff') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_get_daq_processor_info + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / GetDaqProcessorInfo() +assert cto_request.pid == 0xda +assert bytes(cto_request) == b'\xda' +cto_response = CTOResponse(b'\xff\x11\x00\x00\x01\x00\x00\x40') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +processor_info_response = cto_response["DAQProcessorInfoPositiveResponse"] +assert processor_info_response.daq_properties == 0x11 +assert "daq_config_type" in processor_info_response.daq_properties +assert "timestamp_supported" in processor_info_response.daq_properties + +assert "prescaler_supported" not in processor_info_response.daq_properties +assert "resume_supported" not in processor_info_response.daq_properties +assert "bit_stim_supported" not in processor_info_response.daq_properties +assert "pid_off_supported" not in processor_info_response.daq_properties +assert "overload_msb" not in processor_info_response.daq_properties +assert "overload_event" not in processor_info_response.daq_properties + +assert processor_info_response.max_daq == 0x0000 +assert processor_info_response.max_event_channel == 0x0001 +assert processor_info_response.min_daq == 0x00 +assert processor_info_response.daq_key_byte == 0x40 +assert "optimisation_type_0" not in processor_info_response.daq_key_byte +assert "optimisation_type_1" not in processor_info_response.daq_key_byte +assert "optimisation_type_2" not in processor_info_response.daq_key_byte +assert "optimisation_type_3" not in processor_info_response.daq_key_byte +assert "identification_field_type_0" in processor_info_response.daq_key_byte +assert "identification_field_type_1" not in processor_info_response.daq_key_byte + +assert "address_extension_odt" not in processor_info_response.daq_key_byte +assert "address_extension_daq" not in processor_info_response.daq_key_byte +assert "address_extension_daq" not in processor_info_response.daq_key_byte + += test_daq_resolution_info + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / GetDaqResolutionInfo() +assert cto_request.pid == 0xd9 +assert bytes(cto_request) == b'\xd9' + +cto_response = CTOResponse(b'\xff\x02\xfd\xff\xff\x62\x0a\x00') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +resolution_info_response = cto_response["DAQResolutionInfoPositiveResponse"] +assert resolution_info_response.granularity_odt_entry_size_daq == 0x02 +assert resolution_info_response.max_odt_entry_size_daq == 0xfd +assert resolution_info_response.granularity_odt_entry_size_stim == 0xff +assert resolution_info_response.max_odt_entry_size_stim == 0xff +assert resolution_info_response.timestamp_mode == 0x62 +assert "size_0" not in resolution_info_response.timestamp_mode +assert "size_1" in resolution_info_response.timestamp_mode +assert "size_2" not in resolution_info_response.timestamp_mode +assert "timestamp_fixed" not in resolution_info_response.timestamp_mode +assert "unit_0" not in resolution_info_response.timestamp_mode +assert "unit_1" in resolution_info_response.timestamp_mode +assert "unit_2" in resolution_info_response.timestamp_mode +assert "unit_3" not in resolution_info_response.timestamp_mode + +assert resolution_info_response.timestamp_ticks == 0x000A + += test_daq_event_info + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / GetDaqEventInfo(b'\xff\x00\x00') +assert cto_request.pid == 0xd7 +assert bytes(cto_request) == b'\xd7\xff\x00\x00' +assert cto_request['GetDaqEventInfo'].event_channel_num == 0x0000 + +cto_response = CTOResponse(b'\xFF\x04\x01\x05\x0A\x60\x00') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +event_info_response = cto_response["DAQEventInfoPositiveResponse"] +assert event_info_response.daq_event_properties == 0x04 +assert "x_0" not in event_info_response.daq_event_properties +assert "x_1" not in event_info_response.daq_event_properties +assert "daq" in event_info_response.daq_event_properties +assert "stim" not in event_info_response.daq_event_properties +assert "x_4" not in event_info_response.daq_event_properties +assert "x_5" not in event_info_response.daq_event_properties +assert "x_6" not in event_info_response.daq_event_properties +assert "x_7" not in event_info_response.daq_event_properties + +assert event_info_response.max_daq_list == 0x01 +assert event_info_response.event_channel_name_length == 0x05 +assert event_info_response.event_channel_time_cycle == 0x0a +assert event_info_response.event_channel_time_unit == 0x60 +assert event_info_response.event_channel_priority == 0x00 + += test_daq_list_info + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / GetDaqListInfo(b'\xff\x00\x00') +assert cto_request.pid == 0xd8 +assert bytes(cto_request) == b'\xd8\xff\x00\x00' +assert cto_request['GetDaqListInfo'].daq_list_num == 0x0000 + +cto_response = CTOResponse(b'\xFF\x04\x03\x0a\x00\x00') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +list_info_response = cto_response["DAQListInfoPositiveResponse"] +assert list_info_response.daq_list_properties == 0x04 +assert "predefined" not in list_info_response.daq_list_properties +assert "event_fixed" not in list_info_response.daq_list_properties +assert "daq" in list_info_response.daq_list_properties +assert "stim" not in list_info_response.daq_list_properties +assert "x_4" not in list_info_response.daq_list_properties +assert "x_5" not in list_info_response.daq_list_properties +assert "x_6" not in list_info_response.daq_list_properties +assert "x_7" not in list_info_response.daq_list_properties + +assert list_info_response.max_odt == 0x03 +assert list_info_response.max_odt_entries == 0x0a +assert list_info_response.fixed_event == 0x00 + += test_clear_daq_list + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / ClearDaqList(b'\xff\x00\x00') +assert cto_request.pid == 0xe3 +assert bytes(cto_request) == b'\xe3\xff\x00\x00' +assert cto_request['ClearDaqList'].daq_list_num == 0x0000 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_alloc_daq + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / AllocDaq(b'\xff\x01\x00') +assert cto_request.pid == 0xd5 +assert bytes(cto_request) == b'\xd5\xff\x01\x00' +assert cto_request['AllocDaq'].daq_count == 0x0001 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_alloc_odt + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / AllocOdt(b'\xff\x00\x00\x01') +assert cto_request.pid == 0xd4 +assert bytes(cto_request) == b'\xd4\xff\x00\x00\x01' +assert cto_request['AllocOdt'].daq_list_num == 0x0000 +assert cto_request['AllocOdt'].odt_count == 0x01 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_alloc_odt_entry + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / AllocOdtEntry(b'\xff\x00\x00\x00\x02') +assert cto_request.pid == 0xd3 +assert bytes(cto_request) == b'\xd3\xff\x00\x00\x00\x02' +assert cto_request['AllocOdtEntry'].daq_list_num == 0x0000 +assert cto_request['AllocOdtEntry'].odt_num == 0x00 +assert cto_request['AllocOdtEntry'].odt_entries_count == 0x02 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_set_daq_ptr + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / SetDaqPtr(b'\xff\x00\x00\x00\x00') +assert cto_request.pid == 0xe2 +assert bytes(cto_request) == b'\xe2\xff\x00\x00\x00\x00' +assert cto_request['SetDaqPtr'].daq_list_num == 0x0000 +assert cto_request['SetDaqPtr'].odt_num == 0x00 +assert cto_request['SetDaqPtr'].odt_entry_num == 0x00 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_write_daq + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / WriteDaq(b'\xFF\x04\x00\x08\x55\x0C\x00') +assert cto_request.pid == 0xe1 +assert bytes(cto_request) == b'\xe1\xFF\x04\x00\x08\x55\x0C\x00' +assert cto_request['WriteDaq'].bit_offset == 0xff +assert cto_request['WriteDaq'].size_of_daq_element == 0x04 +assert cto_request['WriteDaq'].address_extension == 0x00 +assert cto_request['WriteDaq'].address == 0x000C5508 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_set_daq_list_mode(self): +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / SetDaqListMode(b'\x10\x00\x00\x00\x00\x01\x00') +assert cto_request.pid == 0xe0 +assert bytes(cto_request) == b'\xe0\x10\x00\x00\x00\x00\x01\x00' +set_daq_list_mode_request = cto_request['SetDaqListMode'] +assert set_daq_list_mode_request.mode == 0x10 +assert "x0" not in set_daq_list_mode_request.mode +assert "direction" not in set_daq_list_mode_request.mode +assert "x2" not in set_daq_list_mode_request.mode +assert "x3" not in set_daq_list_mode_request.mode +assert "timestamp" in set_daq_list_mode_request.mode +assert "pid_off" not in set_daq_list_mode_request.mode +assert "x6" not in set_daq_list_mode_request.mode +assert "x7" not in set_daq_list_mode_request.mode + +assert set_daq_list_mode_request.daq_list_num == 0x0000 +assert set_daq_list_mode_request.event_channel_num == 0x0000 +assert set_daq_list_mode_request.transmission_rate_prescaler == 0x01 +assert set_daq_list_mode_request.daq_list_prio == 0x00 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_start_stop_daq_list + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / StartStopDaqList(b'\x02\x00\x00') +assert cto_request.pid == 0xde +assert bytes(cto_request) == b'\xde\x02\x00\x00' +assert cto_request['StartStopDaqList'].mode == 0x02 +assert cto_request['StartStopDaqList'].daq_list_number == 0x0000 + +cto_response = CTOResponse(b'\xFF\xbb') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +assert cto_response['StartStopDAQListPositiveResponse'].first_pid == 0xbb + += test_get_daq_clock + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / GetDaqClock() +assert cto_request.pid == 0xdc +assert bytes(cto_request) == b'\xdc' + +cto_response = CTOResponse(b'\xFF\xFF\xFF\xFF\xAA\xC5\x00\x00') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +get_daq_clock_response = cto_response["DAQClockListPositiveResponse"] + +assert get_daq_clock_response.receive_timestamp == 0x0000C5AA + += Test negative response + +cto_request = CTORequest() / GetCommModeInfo() +cto_response = CTOResponse() / NegativeResponse() +assert cto_response.packet_code == 0xFE +assert cto_response.answers(cto_request) + += test_start_stop_synch + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / StartStopSynch(b'\x01') +assert cto_request.pid == 0xdd +assert bytes(cto_request) == b'\xdd\x01' +assert cto_request['StartStopSynch'].mode == 0x01 + +cto_response = CTOResponse(b'\xFF') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_program_start + +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / ProgramStart() +assert cto_request.pid == 0xd2 +assert bytes(cto_request) == b'\xd2' + +cto_response = CTOResponse(b'\xFF\xff\x01\x08\x2A\xFF\xdd') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + +program_start_response = cto_response['ProgramStartPositiveResponse'] + +assert program_start_response.comm_mode_pgm == 0x01 +assert "master_block_mode" in program_start_response.comm_mode_pgm +assert "interleaved_mode" not in program_start_response.comm_mode_pgm +assert "x2" not in program_start_response.comm_mode_pgm +assert "x3" not in program_start_response.comm_mode_pgm +assert "x4" not in program_start_response.comm_mode_pgm +assert "x5" not in program_start_response.comm_mode_pgm +assert "slave_block_mode" not in program_start_response.comm_mode_pgm +assert "x7" not in program_start_response.comm_mode_pgm + +assert program_start_response.max_cto_pgm == 0x08 +assert program_start_response.max_bs_pgm == 0x2a +assert program_start_response.min_bs_pgm == 0xff +assert program_start_response.queue_size_pgm == 0xdd + += test_program_clear(self): +conf.contribs['XCP']['byte_order'] = 0 + +cto_request = CTORequest() / ProgramClear(b'\x00\xff\xff\x00\x01\x00\x00') +assert cto_request.pid == 0xd1 +assert bytes(cto_request) == b'\xd1\x00\xff\xff\x00\x01\x00\x00' + +assert cto_request['ProgramClear'].mode == 0x00 +assert cto_request['ProgramClear'].clear_range == 0x00000100 + +cto_response = CTOResponse(b'\xff') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + += test_program + +conf.contribs['XCP']['byte_order'] = 0 +conf.contribs['XCP']['MAX_CTO'] = 8 +conf.contribs['XCP']['Address_Granularity_Byte'] = 1 + +cto_request = CTORequest() / Program(b'\x06\x00\x01\x02\x03\x04\x05') +assert cto_request.pid == 0xd0 +assert bytes(cto_request) == b'\xd0\x06\x00\x01\x02\x03\x04\x05' + +assert cto_request['Program'].nr_of_data_elements == 0x06 +assert cto_request['Program'].data_elements == b"\x00\x01\x02\x03\x04\x05" + +cto_response = CTOResponse(b'\xff') +assert cto_response.packet_code == 0xFF +assert cto_response.answers(cto_request) + + ++ Tests for XCPonUDP + += CONNECT + +cto_request = XCPOnUDP(ctr=0, sport=1, dport=1) / CTORequest() / Connect() + +assert cto_request.length is None +assert cto_request.ctr == 0 + +assert cto_request.pid == 0xFF +assert cto_request.connection_mode == 0 +assert bytes(cto_request).endswith(b'\x00\x02\x00\x00\xff\x00') +xcp_on_udp = XCPOnUDP(b'\x00\x01\x00\x01\x00\x0c\x00\x00\x00\x08\x00\x01\xff\x15\xC0\x08\x08\x00\x10\x10') +assert xcp_on_udp.length == 8 +assert xcp_on_udp.ctr == 1 + +assert xcp_on_udp.answers(cto_request) +cto_response = xcp_on_udp["CTOResponse"] +assert cto_response.packet_code == 0xFF + +connect_response = cto_response["ConnectPositiveResponse"] +assert connect_response.resource == 0x15 +assert connect_response.comm_mode_basic == 0xC0 +assert connect_response.max_cto == 8 +assert connect_response.max_cto == 8 + +assert connect_response.xcp_protocol_layer_version_number_msb == 0x10 +assert connect_response.xcp_transport_layer_version_number_msb == 0x10 + +assert conf.contribs['XCP']['byte_order'] == 0 +assert conf.contribs['XCP']['MAX_CTO'] == 8 +assert conf.contribs['XCP']['MAX_DTO'] == 8 +assert conf.contribs['XCP']['Address_Granularity_Byte'] == 1 + += CONNECT 2 + +prt1, prt2 = 12345, 54321 +xcp_on_udp_request = XCPOnUDP(sport=prt1, dport=prt2, ctr=0) / CTORequest() / Connect() + +assert xcp_on_udp_request.length is None +assert xcp_on_udp_request.ctr == 0 +assert xcp_on_udp_request.pid == 0xFF +assert xcp_on_udp_request.connection_mode == 0 +assert bytes(xcp_on_udp_request).endswith(b'\x00\x02\x00\x00\xff\x00') + +xcp_on_udp_response = XCPOnUDP(b'\xd4109\x00\x0c\x00\x00\x00\x08\x00\x01\xff\x15\xC0\x08\x08\x00\x10\x10') +assert xcp_on_udp_response.length == 8 +assert xcp_on_udp_response.ctr == 1 +assert xcp_on_udp_response.answers(xcp_on_udp_request) + +cto_response = xcp_on_udp_response["CTOResponse"] +assert cto_response.packet_code == 0xFF + +connect_response = cto_response["ConnectPositiveResponse"] +assert connect_response.resource == 0x15 +assert connect_response.comm_mode_basic == 0xC0 +assert connect_response.max_cto == 8 +assert connect_response.max_cto == 8 +assert connect_response.xcp_protocol_layer_version_number_msb == 0x10 +assert connect_response.xcp_transport_layer_version_number_msb == 0x10 +assert conf.contribs['XCP']['byte_order'] == 0 +assert conf.contribs['XCP']['MAX_CTO'] == 8 +assert conf.contribs['XCP']['MAX_DTO'] == 8 +assert conf.contribs['XCP']['Address_Granularity_Byte'] == 1 + += XCPOnUDP post build length + +xcp_on_udp_request = XCPOnUDP(sport=1, dport=2, ctr=0) / CTORequest() / Connect() +assert bytes(xcp_on_udp_request)[8:10] == b'\x00\x02' + ++ Tests XCPonTCP + += CONNECT + +prt1, prt2 = 12345, 54321 + +xcp_on_tcp_request = XCPOnTCP(sport=prt1, dport=prt2, ctr=0) / CTORequest() / Connect() +assert xcp_on_tcp_request.length is None +assert xcp_on_tcp_request.ctr == 0 +assert xcp_on_tcp_request.pid == 0xFF +assert xcp_on_tcp_request.connection_mode == 0 +assert bytes(xcp_on_tcp_request).endswith(b'\x00\x02\x00\x00\xff\x00') + +xcp_on_tcp_response = XCPOnTCP(b'\xd4109\x00\x00\x00\x00\x00\x00\x00\x00P\x12 \x00\x00\x00\x00\x00\x00\x08\x00\x01\xff\x15\xC0\x08\x08\x00\x10\x10') +assert xcp_on_tcp_response.length == 8 +assert xcp_on_tcp_response.ctr == 1 +assert xcp_on_tcp_response.answers(xcp_on_tcp_request) + +cto_response = xcp_on_tcp_response["CTOResponse"] +assert cto_response.packet_code == 0xFF + +connect_response = cto_response["ConnectPositiveResponse"] +assert connect_response.resource == 0x15 +assert connect_response.comm_mode_basic == 0xC0 +assert connect_response.max_cto == 8 +assert connect_response.max_cto == 8 +assert connect_response.xcp_protocol_layer_version_number_msb == 0x10 +assert connect_response.xcp_transport_layer_version_number_msb == 0x10 +assert conf.contribs['XCP']['byte_order'] == 0 +assert conf.contribs['XCP']['MAX_CTO'] == 8 +assert conf.contribs['XCP']['MAX_DTO'] == 8 +assert conf.contribs['XCP']['Address_Granularity_Byte'] == 1 + + += XCPOnTCP post build length + +xcp_on_tcp_request = XCPOnTCP(sport=prt1, dport=prt2, ctr=0) / CTORequest() / Connect() +assert bytes(xcp_on_tcp_request)[20:22] == b'\x00\x02' diff --git a/test/contrib/automotive/xcp/xcp_comm.uts b/test/contrib/automotive/xcp/xcp_comm.uts new file mode 100644 index 00000000000..2b933ca49e2 --- /dev/null +++ b/test/contrib/automotive/xcp/xcp_comm.uts @@ -0,0 +1,101 @@ +% Regression tests for the XCP using CANSockets + +############ +############ + ++ Configuration +~ conf + += Imports + +from test.testsocket import TestSocket, cleanup_testsockets + += Load module + +load_contrib("automotive.xcp.xcp", globals_dict=globals()) + += Connect + +sock1 = TestSocket(XCPOnCAN) +sock2 = TestSocket(XCPOnCAN) +sock1.pair(sock2) + +response = XCPOnCAN(identifier=0x700) / CTOResponse() / ConnectPositiveResponse(b'\x15\xC0\x08\x08\x00\x10\x10') +sniffer = AsyncSniffer(opened_socket=sock2, count=1, timeout=5, prn=lambda x: sock2.send(response)) +sniffer.start() +pkt = XCPOnCAN(identifier=0x700) / CTORequest() / Connect() +ans = sock1.sr1(pkt, timeout=0.5, verbose=False) +sniffer.join(timeout=1) + +assert ans.identifier == 0x700 +cto_response = ans["CTOResponse"] +assert cto_response.packet_code == 0xff + +connect_response = cto_response["ConnectPositiveResponse"] + +assert connect_response.resource == 0x15 +assert connect_response.comm_mode_basic == 0xC0 +assert connect_response.max_cto == 8 +assert connect_response.max_dto is None +assert connect_response.max_dto_le == 8 + +assert connect_response.xcp_protocol_layer_version_number_msb == 0x10 +assert connect_response.xcp_transport_layer_version_number_msb == 0x10 + + +cto_request = XCPOnCAN(identifier=0x700) / CTORequest() / Connect() + +assert cto_request.identifier == 0x700 +assert cto_request.pid == 0xFF +assert cto_request.connection_mode == 0 +assert bytes(cto_request) == b'\x00\x00\x07\x00\x02\x00\x00\x00\xff\x00' + +xcp_on_can = XCPOnCAN(b'\x00\x00\x05\x00\x08\x00\x00\x00\xff\x15\xC0\x08\x08\x00\x10\x10') +assert xcp_on_can.identifier == 0x500 +assert xcp_on_can.answers(cto_request) + +cto_response = xcp_on_can["CTOResponse"] +assert cto_response.packet_code == 0xFF + +connect_response = cto_response["ConnectPositiveResponse"] +assert connect_response.resource == 0x15 +assert connect_response.comm_mode_basic == 0xC0 +assert connect_response.max_cto == 8 +assert connect_response.max_cto == 8 + +assert connect_response.xcp_protocol_layer_version_number_msb == 0x10 +assert connect_response.xcp_transport_layer_version_number_msb == 0x10 + +assert conf.contribs['XCP']['byte_order'] == 0 +assert conf.contribs['XCP']['MAX_CTO'] == 8 +assert conf.contribs['XCP']['MAX_DTO'] == 8 +assert conf.contribs['XCP']['Address_Granularity_Byte'] == 1 + + += Endianness test for ConnectPositiveResponse + +p = ConnectPositiveResponse(b"\x00\xFF\x01\x00\xFF\x05\x05") +assert p.max_dto_le is None +assert p.max_dto == 0xff + +p = ConnectPositiveResponse(b"\x00\x00\x01\xFF\x00\x05\x05") +assert p.max_dto_le == 0xff +assert p.max_dto is None + + += Wrong answer + +request = XCPOnCAN(identifier=0x700) / CTORequest() / Connect() + +# This response has not enough bytes for a ConnectPositiveResponse +response = XCPOnCAN(identifier=0x90) / CTOResponse() / Raw(b'\x01\x02\x03\x04') + +assert not response.answers(request) + + ++ Cleanup + += Delete TestSockets + +cleanup_testsockets() + diff --git a/test/contrib/bfd.uts b/test/contrib/bfd.uts new file mode 100644 index 00000000000..39a467b115a --- /dev/null +++ b/test/contrib/bfd.uts @@ -0,0 +1,77 @@ ++ BFD + += BFD, basic instantiation + +from scapy.contrib.bfd import * +a = UDP(sport=3784, dport=3784)/BFD() +assert raw(a) == b'\x0e\xc8\x0e\xc8\x00 \x00\x00 \xc0\x03\x18\x11\x11\x11\x11"""";\x9a\xca\x00;\x9a\xca\x00;\x9a\xca\x00' + += BFD - dissection + +assert BFD in UDP(raw(a)) + += BFD with OptionalAuth [Simple Password Auth] [dissection] +p = UDP(b'\x04\x00\x0e\xc8\x00\x29\x72\x31\x20\x44\x05\x21\x00\x00\x00\x01\x00\x00\x00\x00\x00\x0f\x42\x40\x00\x0f\x42\x40\x00\x00\x00\x00\x01\x09\x02\x73\x65\x63\x72\x65\x74\x4e\x0a\x90\x40') +assert(isinstance(p[1], BFD)) +assert(p[1].len == 33) +assert(isinstance(p[2], OptionalAuth)) +assert(p[2].auth_type == 1) +assert(p[2].auth_len == 9) + += BFD with OptionalAuth [Keyed MD5 Auth] [dissection] +p = UDP(b'\x04\x00\x0e\xc8\x00\x38\x6a\xcc\x20\x44\x05\x30\x00\x00\x00\x01\x00\x00\x00\x00\x00\x0f\x42\x40\x00\x0f\x42\x40\x00\x00\x00\x00\x02\x18\x02\x00\x00\x00\x00\x05\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x3c\xc3\xf8\x21') +assert(isinstance(p[1], BFD)) +assert(p[1].len == 48) +assert(isinstance(p[2], OptionalAuth)) +assert(p[2].auth_type ==2) +assert(p[2].auth_len == 24) + += BFD with OptionalAuth [Meticulous Keyed SHA1 Auth] [dissection] +p = UDP(b'\x04\x00\x0e\xc8\x00\x3c\x37\x8a\x20\x44\x05\x34\x00\x00\x00\x01\x00\x00\x00\x00\x00\x0f\x42\x40\x00\x0f\x42\x40\x00\x00\x00\x00\x05\x1c\x02\x00\x00\x00\x00\x05\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\xea\x6d\x1f\x21') +assert(isinstance(p[1], BFD)) +assert(p[1].len == 52) +assert(isinstance(p[2], OptionalAuth)) +assert(p[2].auth_type ==5) +assert(p[2].auth_len == 28) + += BFD with OptionalAuth [Simple Password Auth] [Build] +p = UDP(sport=3784, dport=3784)/BFD(flags="A", optional_auth=OptionalAuth(auth_type=1)) +assert raw(p) == b'\x0e\xc8\x0e\xc8\x00+\x00\x00 \xc4\x03#\x11\x11\x11\x11"""";\x9a\xca\x00;\x9a\xca\x00;\x9a\xca\x00\x01\x0b\x01password' + += BFD with OptionalAuth [Keyed MD5 Auth] [Build] +p = UDP(sport=3784, dport=3784)/BFD(flags="A", optional_auth=OptionalAuth(auth_type=2)) +assert raw(p) == b'\x0e\xc8\x0e\xc8\x008\x00\x00 \xc4\x030\x11\x11\x11\x11"""";\x9a\xca\x00;\x9a\xca\x00;\x9a\xca\x00\x02\x18\x01\x00\x00\x00\x00\x00_M\xcc;Z\xa7e\xd6\x1d\x83\'\xde\xb8\x82\xcf\x99' + += BFD with OptionalAuth [Meticulous Keyed SHA1 Auth] [Build] +p = UDP(sport=3784, dport=3784)/BFD(flags="A", optional_auth=OptionalAuth(auth_type=5)) +assert raw(p) == b'\x0e\xc8\x0e\xc8\x00<\x00\x00 \xc4\x034\x11\x11\x11\x11"""";\x9a\xca\x00;\x9a\xca\x00;\x9a\xca\x00\x05\x1c\x01\x00\x00\x00\x00\x00[\xaaa\xe4\xc9\xb9??\x06\x82%\x0bl\xf83\x1b~\xe6\x8f\xd8' + += BFD without Auth flag - dissection should not inject phantom optional_auth (Issue #4937) + +a = UDP(sport=3784, dport=3784)/BFD() +p = UDP(raw(a)) +assert p[BFD].optional_auth is None +assert not p[BFD].flags.A + += BFD with non-Auth flags set - optional_auth should still be None + +a = UDP(sport=3784, dport=3784)/BFD(flags="DF") +p = UDP(raw(a)) +assert p[BFD].flags.D +assert p[BFD].flags.F +assert not p[BFD].flags.A +assert p[BFD].optional_auth is None + += BFD round-trip without auth preserves raw bytes + +a = UDP(sport=3784, dport=3784)/BFD() +raw1 = raw(a) +raw2 = raw(UDP(raw1)) +assert raw1 == raw2 + += BFD with Auth flag set - optional_auth should be present + +p = UDP(b'\x04\x00\x0e\xc8\x00\x29\x72\x31\x20\x44\x05\x21\x00\x00\x00\x01\x00\x00\x00\x00\x00\x0f\x42\x40\x00\x0f\x42\x40\x00\x00\x00\x00\x01\x09\x02\x73\x65\x63\x72\x65\x74\x4e\x0a\x90\x40') +assert p[BFD].flags.A +assert p[BFD].optional_auth is not None +assert isinstance(p[BFD].optional_auth, OptionalAuth) \ No newline at end of file diff --git a/test/contrib/bgp.uts b/test/contrib/bgp.uts index 004548c0f62..3d8e30f2431 100644 --- a/test/contrib/bgp.uts +++ b/test/contrib/bgp.uts @@ -1,7 +1,9 @@ #################################### bgp.py ################################## % Regression tests for the bgp module -# Default configuration : OLD speaker (see RFC 6793) ++ Default configuration + += OLD speaker (see RFC 6793) bgp_module_conf.use_2_bytes_asn = True ################################ BGPNLRI_IPv4 ################################ @@ -54,8 +56,8 @@ nlri.prefix == '2001:db8::/32' = BGP - Instantiation (Should be a KEEPALIVE) m = BGP() -assert(raw(m) == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x13\x04') -assert(m.type == BGP.KEEPALIVE_TYPE) +assert raw(m) == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x13\x04' +assert m.type == BGP.KEEPALIVE_TYPE = BGP - Instantiation with specific values (1) raw(BGP(type = 0)) == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x13\x00' @@ -77,13 +79,20 @@ raw(BGP(type = 5)) == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff = BGP - Basic dissection h = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x13\x04') -assert(h.type == BGP.KEEPALIVE_TYPE) -assert(h.len == 19) +assert h.type == BGP.KEEPALIVE_TYPE +assert h.len == 19 = BGP - Dissection with specific values h = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x13\x01') -assert(h.type == BGP.OPEN_TYPE) -assert(h.len == 19) +assert h.type == BGP.OPEN_TYPE +assert h.len == 19 + += BGP - Test TCP reassembly +pkts = sniff(offline=scapy_path("/test/pcaps/bgp_fragmented.pcap.gz"), session=TCPSession) +assert len(pkts) == 1 +assert BGPUpdate in pkts[0] +assert len(pkts[0].nlri) == 512 +assert pkts[0].nlri[511].prefix == '91.0.177.0/24' ############################### BGPKeepAlive ################################# + BGPKeepAlive class tests @@ -108,7 +117,7 @@ raw(BGPCapability()) == b'\x00\x00' = BGPCapability - Instantiation with specific values (1) c = BGPCapability(code = 70) -assert(raw(c) == b'F\x00') +assert raw(c) == b'F\x00' = BGPCapability - Check exception from scapy.contrib.bgp import _BGPInvalidDataException @@ -144,7 +153,7 @@ assert len(s) == 1 = BGPCapMultiprotocol - Inheritance c = BGPCapMultiprotocol() -assert(isinstance(c, BGPCapability)) +assert isinstance(c, BGPCapability) = BGPCapMultiprotocol - Instantiation raw(BGPCapMultiprotocol()) == b'\x01\x04\x00\x00\x00\x00' @@ -157,11 +166,11 @@ raw(BGPCapMultiprotocol(afi = 2, safi = 1)) == b'\x01\x04\x00\x02\x00\x01' = BGPCapMultiprotocol - Dissection with specific values c = BGPCapMultiprotocol(b'\x01\x04\x00\x02\x00\x01') -assert(c.code == 1) -assert(c.length == 4) -assert(c.afi == 2) -assert(c.reserved == 0) -assert(c.safi == 1) +assert c.code == 1 +assert c.length == 4 +assert c.afi == 2 +assert c.reserved == 0 +assert c.safi == 1 ############################### BGPCapORFBlock ############################### + BGPCapORFBlock class tests @@ -207,7 +216,7 @@ c.orf_type == 64 and c.send_receive == 3 = BGPCapORF - Inheritance c = BGPCapORF() -assert(isinstance(c, BGPCapability)) +assert isinstance(c, BGPCapability) = BGPCapORF - Instantiation raw(BGPCapORF()) == b'\x03\x00' @@ -228,7 +237,7 @@ c.code == 3 and c.orf[0].afi == 1 and c.orf[0].safi == 1 and c.orf[0].entries[0] = BGPCapORF - Dissection p = BGPCapORF(b'\x03\x07\x00\x01\x00\x01\x01@\x03') -assert(len(p.orf) == 1) +assert len(p.orf) == 1 ####################### BGPCapGracefulRestart.GRTuple ######################### @@ -254,7 +263,7 @@ c.afi == 1 and c.safi == 1 and c.flags == 128 = BGPCapGracefulRestart - Inheritance c = BGPCapGracefulRestart() -assert(isinstance(c, BGPCapGracefulRestart)) +assert isinstance(c, BGPCapGracefulRestart) = BGPCapGracefulRestart - Instantiation raw(BGPCapGracefulRestart()) == b'@\x02\x00\x00' @@ -285,7 +294,7 @@ c.code == 64 and c.restart_time == 120 and c.restart_flags == 0x8 and c.entries[ = BGPCapFourBytesASN - Inheritance c = BGPCapFourBytesASN() -assert(isinstance(c, BGPCapFourBytesASN)) +assert isinstance(c, BGPCapFourBytesASN) = BGPCapFourBytesASN - Instantiation raw(BGPCapFourBytesASN()) == b'A\x04\x00\x00\x00\x00' @@ -313,7 +322,7 @@ raw(BGPAuthenticationInformation()) == b'\x00' = BGPAuthenticationInformation - Basic dissection c = BGPAuthenticationInformation(b'\x00') -c.authentication_code == 0 and c.authentication_data == None +c.authentication_code == 0 and not c.authentication_data ################################# BGPOptParam ################################# @@ -375,18 +384,18 @@ raw(BGPOpen(my_as = 64503, bgp_id = "192.168.100.3", hold_time = 30, opt_params = BGPOpen - Dissection with specific values (1) m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00?\x01\x04\xfb\xf7\x00\x1e\xc0\xa8d\x03"\x02\x06\x01\x04\x00\x01\x00\x01\x02\x02\x80\x00\x02\x02\x02\x00\x02\x08@\x06\x00x\x00\x01\x01\x80\x02\x06A\x04\x00\x00\xfb\xf7') -assert(BGPHeader in m and BGPOpen in m) -assert(m.len == 63) -assert(m.type == BGP.OPEN_TYPE) -assert(m.version == 4) -assert(m.my_as == 64503) -assert(m.hold_time == 30) -assert(m.bgp_id == "192.168.100.3") -assert(m.opt_param_len == 34) -assert(isinstance(m.opt_params[0].param_value, BGPCapMultiprotocol)) -assert(isinstance(m.opt_params[1].param_value, BGPCapability)) -assert(isinstance(m.opt_params[2].param_value, BGPCapability)) -assert(isinstance(m.opt_params[3].param_value, BGPCapGracefulRestart)) +assert BGPHeader in m and BGPOpen in m +assert m.len == 63 +assert m.type == BGP.OPEN_TYPE +assert m.version == 4 +assert m.my_as == 64503 +assert m.hold_time == 30 +assert m.bgp_id == "192.168.100.3" +assert m.opt_param_len == 34 +assert isinstance(m.opt_params[0].param_value, BGPCapMultiprotocol) +assert isinstance(m.opt_params[1].param_value, BGPCapability) +assert isinstance(m.opt_params[2].param_value, BGPCapability) +assert isinstance(m.opt_params[3].param_value, BGPCapGracefulRestart) = BGPOpen - Dissection with specific values (2) (followed by a KEEPALIVE) messages = b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00=\x01\x04\xfb\xf6\x00\xb4\xc0\xa8ze \x02\x06\x01\x04\x00\x01\x00\x01\x02\x06\x01\x04\x00\x02\x00\x01\x02\x02\x80\x00\x02\x02\x02\x00\x02\x06A\x04\x00\x00\xfb\xf6\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x13\x04' @@ -395,11 +404,11 @@ raw(m) == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00 = BGPOpen - Dissection with specific values (3) m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x8f\x01\x04\xfd\xe8\x00\xb4\n\xff\xff\x01r\x02\x06\x01\x04\x00\x01\x00\x84\x02\x06\x01\x04\x00\x19\x00A\x02\x06\x01\x04\x00\x02\x00\x02\x02\x06\x01\x04\x00\x01\x00\x02\x02\x06\x01\x04\x00\x02\x00\x80\x02\x06\x01\x04\x00\x01\x00\x80\x02\x06\x01\x04\x00\x01\x00B\x02\x06\x01\x04\x00\x02\x00\x01\x02\x06\x01\x04\x00\x02\x00\x04\x02\x06\x01\x04\x00\x01\x00\x01\x02\x06\x01\x04\x00\x01\x00\x04\x02\x02\x80\x00\x02\x02\x02\x00\x02\x04@\x02\x80x\x02\x02F\x00\x02\x06A\x04\x00\x00\xfd\xe8') -assert(BGPHeader in m and BGPOpen in m) +assert BGPHeader in m and BGPOpen in m = BGPOpen - Dissection with specific values (4) m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x8f\x01\x04\xfd\xe8\x00\xb4\n\xff\xff\x02r\x02\x06\x01\x04\x00\x01\x00\x84\x02\x06\x01\x04\x00\x19\x00A\x02\x06\x01\x04\x00\x02\x00\x02\x02\x06\x01\x04\x00\x01\x00\x02\x02\x06\x01\x04\x00\x02\x00\x80\x02\x06\x01\x04\x00\x01\x00\x80\x02\x06\x01\x04\x00\x01\x00B\x02\x06\x01\x04\x00\x02\x00\x01\x02\x06\x01\x04\x00\x02\x00\x04\x02\x06\x01\x04\x00\x01\x00\x01\x02\x06\x01\x04\x00\x01\x00\x04\x02\x02\x80\x00\x02\x02\x02\x00\x02\x04@\x02\x00x\x02\x02F\x00\x02\x06A\x04\x00\x00\xfd\xe8') -assert(BGPHeader in m and BGPOpen in m) +assert BGPHeader in m and BGPOpen in m ################################# BGPPAOrigin ################################# @@ -595,6 +604,20 @@ a = BGPPAAS4Aggregator(b'&kN%\xc0\x00\x02\x01') a.aggregator_asn == 644566565 and a.speaker_address == "192.0.2.1" +############################# BGPPALargeCommunity ############################ ++ BGPPALargeCommunity class tests + += BGPPALargeCommunity - Instantiation +raw(BGPPALargeCommunity()) == b'' + += BGPPALargeCommunity - Instantiation with specific values +raw(BGPPALargeCommunity(segments=BGPLargeCommunitySegment(global_administrator=161,local_data_part1=0,local_data_part2=0))) == b'\x00\x00\x00\xa1\x00\x00\x00\x00\x00\x00\x00\x00' + += BGPPALargeCommunity - Dissection +a = BGPPALargeCommunity(b'\x00\x00\x00\xa1\x00\x00\x00\x00\x00\x00\x00\x00') +a.segments[0].global_administrator == 161 and a.segments[0].local_data_part1 == 0 and a.segments[0].local_data_part2 == 0 + + ################################ BGPPathAttr ################################# + BGPPathAttr class tests @@ -640,44 +663,53 @@ raw(BGPUpdate()) == b'\x00\x00\x00\x00' = BGPUpdate - Dissection (1) bgp_module_conf.use_2_bytes_asn = True m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x000\x02\x00\x19\x18\xc0\xa8\x96\x18\x07\x07\x07\x18\xc63d\x18\xc0\xa8\x01\x19\x06\x06\x06\x00\x18\xc0\xa8\x1a\x00\x00') -assert(BGPHeader in m and BGPUpdate in m) -assert(m.withdrawn_routes_len == 25) -assert(m.withdrawn_routes[0].prefix == "192.168.150.0/24") -assert(m.withdrawn_routes[5].prefix == "192.168.26.0/24") -assert(m.path_attr_len == 0) +assert BGPHeader in m and BGPUpdate in m +assert m.withdrawn_routes_len == 25 +assert m.withdrawn_routes[0].prefix == "192.168.150.0/24" +assert m.withdrawn_routes[5].prefix == "192.168.26.0/24" +assert m.path_attr_len == 0 = BGPUpdate - Behave like a NEW speaker (RFC 6793) - Dissection (2) bgp_module_conf.use_2_bytes_asn = False m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00=\x02\x00\x00\x00"@\x01\x01\x00@\x02\x06\x02\x01\x00\x00\xfb\xfa@\x03\x04\xc0\xa8\x10\x06\x80\x04\x04\x00\x00\x00\x00\xc0\x08\x04\xff\xff\xff\x01\x18\xc0\xa8\x01') -assert(BGPHeader in m and BGPUpdate in m) -assert(m.path_attr[1].attribute.segments[0].segment_value == [64506]) -assert(m.path_attr[4].attribute.community == 0xFFFFFF01) -assert(m.nlri[0].prefix == "192.168.1.0/24") +assert BGPHeader in m and BGPUpdate in m +assert m.path_attr[1].attribute.segments[0].segment_value == [64506] +assert m.path_attr[4].attribute.community == 0xFFFFFF01 +assert m.nlri[0].prefix == "192.168.1.0/24" = BGPUpdate - Dissection (MP_REACH_NLRI) m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\xd8\x02\x00\x00\x00\xc1@\x01\x01\x00@\x02\x06\x02\x01\x00\x00\xfb\xf6\x90\x0e\x00\xb0\x00\x02\x01 \xfe\x80\x00\x00\x00\x00\x00\x00\xfa\xc0\x01\x00\x15\xde\x15\x81\xfe\x80\x00\x00\x00\x00\x00\x00\xfa\xc0\x01\x00\x15\xde\x15\x81\x00\x06\x04\x05\x08\x04\x10\x03`\x03\x80\x03\xa0\x03\xc0\x04\xe0\x05\xf0\x06\xf8\t\xfe\x00\x16 \x01<\x08-\x07.\x040\x10?\xfe\x10 \x02\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff@\x01\x00\x00\x00\x00\x00\x00\x00\x17 \x01\x00 \x01\x00\x000 \x01\x00\x02\x00\x00 \x01\r\xb8\x1c \x01\x00\x10\x07\xfc\n\xfe\x80\x08\xff\n\xfe\xc0\x03 \x03@\x08_`\x00d\xff\x9b\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x08\x01\x07\x02') -assert(BGPHeader in m and BGPUpdate in m) -assert(m.path_attr[2].attribute.afi == 2) -assert(m.path_attr[2].attribute.safi == 1) -assert(m.path_attr[2].attribute.nh_addr_len == 32) -assert(m.path_attr[2].attribute.nh_v6_global == "fe80::fac0:100:15de:1581") -assert(m.path_attr[2].attribute.nh_v6_link_local == "fe80::fac0:100:15de:1581") -assert(m.path_attr[2].attribute.nlri[0].prefix == "400::/6") -assert(m.nlri == []) +assert BGPHeader in m and BGPUpdate in m +assert m.path_attr[2].attribute.afi == 2 +assert m.path_attr[2].attribute.safi == 1 +assert m.path_attr[2].attribute.nh_addr_len == 32 +assert m.path_attr[2].attribute.nh_v6_global == "fe80::fac0:100:15de:1581" +assert m.path_attr[2].attribute.nh_v6_link_local == "fe80::fac0:100:15de:1581" +assert m.path_attr[2].attribute.nlri[0].prefix == "400::/6" +assert m.nlri == [] = BGPUpdate - Dissection (MP_UNREACH_NLRI) m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00s\x02\x00\x00\x00\\\x90\x0f\x00X\x00\x02\x01\x03`\x03\x80\x03\xa0\x03\xc0\x04\xe0\x05\xf0\x06\xf8\x10 \x02`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff@\x01\x00\x00\x00\x00\x00\x00\x00\x17 \x01\x00 \x01\x00\x000 \x01\x00\x02\x00\x00 \x01\r\xb8\n\xfe\xc0\x07\xfc\n\xfe\x80\x1c \x01\x00\x10\x03 \x06\x04\x03@\x08_\x05\x08\x04\x10') -assert(BGPHeader in m and BGPUpdate in m) -assert(m.path_attr[0].attribute.afi == 2) -assert(m.path_attr[0].attribute.safi == 1) -assert(m.path_attr[0].attribute.afi_safi_specific.withdrawn_routes[0].prefix == "6000::/3") -assert(m.nlri == []) +assert BGPHeader in m and BGPUpdate in m +assert m.path_attr[0].attribute.afi == 2 +assert m.path_attr[0].attribute.safi == 1 +assert m.path_attr[0].attribute.afi_safi_specific.withdrawn_routes[0].prefix == "6000::/3" +assert m.nlri == [] + += BGPUpdate - Dissection (with BGP Additional Path) +m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x17\x05\x00\x01\x01\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\xd0\x02\x00\xb9\x00\x00\x00\x02\x00\x00\x00\x00\x04 \n\xe9\x19\xb2\x00\x00\x00\x04 \n\xe9\x19\x90\x00\x00\x00\x04 \n\xe9\x19\x93\x00\x00\x00\x04 \n\xe9\x19\xbb\x00\x00\x00\x04 \n\xe9\x19\x9f\x00\x00\x00\x04 \n\xe9\x19\x8c\x00\x00\x00\x04 \n\xe9\x19\xb1\x00\x00\x00\x04 \n\xe9\x19\x8f\x00\x00\x00\x04 \n\xe9\x19\x98\x00\x00\x00\x04 \n\xe9\x19\x9b\x00\x00\x00\x04 \n\xe9\x19\x8b\x00\x00\x00\x04 \n\xe9\x19\xb3\x00\x00\x00\x04 \n\xe9\x19\x91\x00\x00\x00\x04 \n\xe9\x19\xb6\x00\x00\x00\x04 \n\xe9\x19\x94\x00\x00\x00\x04 \n\xe9\x19\x97\x00\x00\x00\x04 \n\xe9\x19\xbc\x00\x00\x00\x04 \n\xe9\x19\x9d\x00\x00\x00\x04 \n\xe9\x19\xa3\x00\x00\x00\x04 \n\xe9\x19\x84\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x005\x02\x00\x00\x00\x15@\x01\x01\x00@\x02\x00@\x03\x04\n\x16\x0cX@\x05\x04\x00\x00\x00d\x00\x00\x00\x02 \n\xe9\x00\x16\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x17\x05\x00\x01\x02\x01') +assert m.withdrawn_routes[0].nlri_path_id == 2 +assert len(m.withdrawn_routes) == 21 +assert m.withdrawn_routes[-1].sprintf("%prefix%") == "10.233.25.132/32" +assert len(m.getlayer(BGPUpdate, 2).path_attr) == 4 +assert m.getlayer(BGPUpdate, 2).nlri[0].nlri_path_id == 2 +assert m.getlayer(BGPUpdate, 2).nlri[0].sprintf("%prefix%") == "10.233.0.22/32" = BGPUpdate - with BGPHeader p = BGP(raw(BGPHeader()/BGPUpdate())) -assert(BGPHeader in p and BGPUpdate in p) +assert BGPHeader in p and BGPUpdate in p ########## BGPNotification Class ################################### @@ -715,22 +747,26 @@ m.type == BGP.ROUTEREFRESH_TYPE and m.len == 23 and m.afi == 2 and m.subtype == = BGPRouteRefresh - Dissection (2) - With ORFs m = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00.\x05\x00\x01\x00\x01\x01\x80\x00\x13 \x00\x00\x00\x05\x18\x18\x15\x01\x01\x00\x00\x00\x00\x00\n\x00 \x00') -assert(m.type == BGP.ROUTEREFRESH_TYPE) -assert(m.len == 46) -assert(m.afi == 1) -assert(m.subtype == 0) -assert(m.safi == 1) -assert(m.orf_data[0].when_to_refresh == 1) -assert(m.orf_data[0].orf_type == 128) -assert(m.orf_data[0].orf_len == 19) -assert(len(m.orf_data[0].entries) == 2) -assert(m.orf_data[0].entries[0].action == 0) -assert(m.orf_data[0].entries[0].match == 1) -assert(m.orf_data[0].entries[0].prefix.prefix == "1.1.0.0/21") -assert(m.orf_data[0].entries[1].action == 0) -assert(m.orf_data[0].entries[1].match == 0) -assert(m.orf_data[0].entries[1].prefix.prefix == "0.0.0.0/0") - +assert m.type == BGP.ROUTEREFRESH_TYPE +assert m.len == 46 +assert m.afi == 1 +assert m.subtype == 0 +assert m.safi == 1 +assert m.orf_data[0].when_to_refresh == 1 +assert m.orf_data[0].orf_type == 128 +assert m.orf_data[0].orf_len == 19 +assert len(m.orf_data[0].entries) == 2 +assert m.orf_data[0].entries[0].action == 0 +assert m.orf_data[0].entries[0].match == 1 +assert m.orf_data[0].entries[0].prefix.prefix == "1.1.0.0/21" +assert m.orf_data[0].entries[1].action == 0 +assert m.orf_data[0].entries[1].match == 0 +assert m.orf_data[0].entries[1].prefix.prefix == "0.0.0.0/0" + += BGPRouteRefresh - Dissection (3) - bad ORFS (GH3345) +m = BGPRouteRefresh(b'\x00\x01\x00\x01\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00') +assert m.orf_data.orf_type == 0 +assert m.orf_data.entries[0].load == b'\x00\x00\x00\x00\x00\x00\x00' ########## BGPCapGeneric fuzz() ################################### + BGPCapGeneric fuzz() diff --git a/test/contrib/bier.uts b/test/contrib/bier.uts index 5cc05c9cc14..0559c7dd742 100644 --- a/test/contrib/bier.uts +++ b/test/contrib/bier.uts @@ -10,9 +10,9 @@ from scapy.contrib.mpls import MPLS p1 = MPLS()/BIER(length=BIERLength.BIER_LEN_256)/IP()/UDP() -assert(p1[MPLS].s == 1) +assert p1[MPLS].s == 1 p2 = BIFT()/BIER(length=BIERLength.BIER_LEN_64)/IP()/UDP() -assert(p2[BIFT].s == 1) +assert p2[BIFT].s == 1 p1[MPLS] p1[BIER] diff --git a/test/contrib/canfdsocket_native.uts b/test/contrib/canfdsocket_native.uts new file mode 100644 index 00000000000..ac2ed3ea19e --- /dev/null +++ b/test/contrib/canfdsocket_native.uts @@ -0,0 +1,152 @@ +% Regression tests for nativecanfdsocket +~ not_pypy vcan_socket needs_root linux + +# More information at http://www.secdev.org/projects/UTscapy/ + + +############ +############ ++ Configuration of CAN virtual sockets +~ conf + += Load module +load_layer("can", globals_dict=globals()) +conf.contribs['CANSocket'] = {'use-python-can': False} +from scapy.contrib.cansocket_native import * +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} + + += Setup string for vcan +bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" + += Load os +import os +import threading +from time import sleep +from subprocess import call + += Setup vcan0 +assert 0 == os.system(bashCommand) + ++ Basic Packet Tests() += CAN FD Packet init +canfdframe = CANFD(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08\xaa') +assert bytes(canfdframe) == b'\x00\x00\x07\xff\x08\x04\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\xaa' + ++ Basic Socket Tests() += CAN FD Socket Init +sock1 = CANSocket(channel="vcan0", fd=True) + += CAN Socket send recv small packet without remove padding + +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': False} + +sock2 = CANSocket(channel="vcan0", fd=True) +sock2.send(CANFD(identifier=0x7ff,length=9,data=b'\x01\x02\x03\x04\x05\x06\x07\x08\xaa')) +sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() + +rx = sock1.recv() +assert rx == CANFD(identifier=0x7ff,length=12,data=b'\x01\x02\x03\x04\x05\x06\x07\x08\xaa\x00\x00\x00') / Padding(b"\x00" * (64 - 12)) +rx = sock1.recv() +# different Kernel Versions produce different packets +hexdump(rx) +test = CANFD(identifier=0x7ff, fd_flags=0, length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') / Padding(b"\x00" * (64 - 8)) +hexdump(test) +test2 = CANFD(identifier=0x7ff,fd_flags=4, length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') / Padding(b"\x00" * (64 - 8)) +hexdump(test2) +assert bytes(rx) in [bytes(test), bytes(test2)] + += CAN Socket send recv + +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} + +sock2 = CANSocket(channel="vcan0", fd=True) +sock2.send(CANFD(identifier=0x7ff,length=9,data=b'\x01\x02\x03\x04\x05\x06\x07\x08\xaa')) +sock2.close() + +rx = sock1.recv() +assert rx == CANFD(identifier=0x7ff,length=12, fd_flags=4, data=b'\x01\x02\x03\x04\x05\x06\x07\x08\xaa\x00\x00\x00') + += CAN Socket basecls test + + +sock2 = CANSocket(channel="vcan0", fd=True) +sock2.send(CANFD(identifier=0x7ff,length=9,data=b'\x01\x02\x03\x04\x05\x06\x07\x08\xaa')) +sock2.close() + +sock1.basecls = Raw +rx = sock1.recv() +assert rx.load == bytes(CANFD(identifier=0x7ff, fd_flags=4, length=12,data=b'\x01\x02\x03\x04\x05\x06\x07\x08\xaa\x00\x00\x00' + b'\x00' * (64 - 12))) + += sniff with filtermask 0x1FFFFFFF and inverse filter + + +sock1 = CANSocket(channel='vcan0', fd=True, can_filters=[{'can_id': 0x10000000 | CAN_INV_FILTER, 'can_mask': 0x1fffffff}]) + +sock2 = CANSocket(channel="vcan0", fd=True) +sock2.send(CANFD(flags='extended', identifier=0x10010000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x08ab')) +sock2.send(CANFD(flags='extended', identifier=0x10020000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x08ab')) +sock2.send(CANFD(flags='extended', identifier=0x10000000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x08ab')) +sock2.send(CANFD(flags='extended', identifier=0x10030000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x08ab')) +sock2.send(CANFD(flags='extended', identifier=0x10040000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x08ab')) +sock2.send(CANFD(flags='extended', identifier=0x10000000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x08ab')) +sock2.close() + +packets = sock1.sniff(timeout=0.1, verbose=False, count=4) +assert len(packets) == 4 + +sock1.close() + ++ bridge and sniff tests + += bridge and sniff setup vcan1 package forwarding + + +bashCommand = "/bin/bash -c 'sudo ip link add name vcan1 type vcan; sudo ip link set dev vcan1 up'" +assert 0 == os.system(bashCommand) + +sock0 = CANSocket(channel='vcan0', fd=True) +sock1 = CANSocket(channel='vcan1', fd=True) + +bridgeStarted = threading.Event() + +def bridge(): + global bridgeStarted + bSock0 = CANSocket(channel="vcan0", fd=True) + bSock1 = CANSocket(channel='vcan1', fd=True) + def pnr(pkt): + return pkt + bridgeStarted.set() + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False, count=6) + bSock0.close() + bSock1.close() + +threadBridge = threading.Thread(target=bridge) +threadBridge.start() +bridgeStarted.wait(timeout=5) +sock0.send(CANFD(flags='extended', identifier=0x10010000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x0842')) +sock0.send(CANFD(flags='extended', identifier=0x10020000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x0842')) +sock0.send(CANFD(flags='extended', identifier=0x10000000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x0842')) +sock0.send(CANFD(flags='extended', identifier=0x10030000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x0842')) +sock0.send(CANFD(flags='extended', identifier=0x10040000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x0842')) +sock0.send(CANFD(flags='extended', identifier=0x10000000, length=10, data=b'\x01\x02\x03\x04\x05\x06\x07\x0842')) + +packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False, count=6) +assert len(packetsVCan1) == 6 + +threadBridge.join(timeout=5) +assert not threadBridge.is_alive() + +sock1.close() +sock0.close() + + += Delete vcan interfaces + +if 0 != call(["sudo", "ip", "link", "delete", "vcan0"]): + raise Exception("vcan0 could not be deleted") + +if 0 != call(["sudo", "ip", "link", "delete", "vcan1"]): + raise Exception("vcan1 could not be deleted") + diff --git a/test/contrib/canfdsocket_python_can.uts b/test/contrib/canfdsocket_python_can.uts new file mode 100644 index 00000000000..6ae7b526bfa --- /dev/null +++ b/test/contrib/canfdsocket_python_can.uts @@ -0,0 +1,177 @@ +% Regression tests for the CANSocket +~ vcan_socket linux needs_root not_pypy + +# More information at http://www.secdev.org/projects/UTscapy/ + + +############ +############ ++ Configuration of CAN virtual sockets + += Load module +~ conf + +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} +load_layer("can", globals_dict=globals()) +conf.contribs['CANSocket'] = {'use-python-can': True} +from scapy.contrib.cansocket_python_can import * + += Setup string for vcan +~ conf command +bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" + += Load os +~ conf command + +import os +import threading +from subprocess import call + += Setup vcan0 +~ conf command + +0 == os.system(bashCommand) + += Define common used functions + +send_done = threading.Event() + +def sender(sock, msg): + if not hasattr(msg, "__iter__"): + msg = [msg] + for m in msg: + sock.send(m) + send_done.set() + ++ Basic Packet Tests() += CAN Packet init + +canframe = CANFD(identifier=0x7ff,length=10,data=b'\x01\x02\x03\x04\x05\x06\x07\x08ab') +bytes(canframe) == b'\x00\x00\x07\xff\x0c\x04\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08ab\x00\x00' + ++ Basic Socket Tests() += CAN Socket Init + +sock1 = CANSocket(bustype='socketcan', channel='vcan0', fd=True) +sock1.close() +del sock1 +sock1 = None +assert sock1 == None + += CAN Socket send recv small packet + +sock1 = CANSocket(bustype='socketcan', channel='vcan0', fd=True) +sock2 = CANSocket(bustype='socketcan', channel='vcan0', fd=True) + +sock2.send(CANFD(identifier=0x7ff,length=10,data=b'\x01'*10)) +sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) +rx1 = sock1.recv() +rx2 = sock1.recv() +sock1.close() +sock2.close() + +assert rx1 == CANFD(identifier=0x7ff,length=10,data=b'\x01'*10) +assert rx2 == CAN(identifier=0x7ff,length=1,data=b'\x01') + + += CAN Socket send recv small packet test with + +with CANSocket(bustype='socketcan', channel='vcan0', fd=True) as sock1, \ + CANSocket(bustype='socketcan', channel='vcan0', fd=True) as sock2: + sock2.send(CANFD(identifier=0x7ff,length=1,data=b'\x01')) + rx = sock1.recv() + +assert rx == CANFD(identifier=0x7ff,length=1,data=b'\x01') + += CAN Socket basecls test + +with CANSocket(bustype='socketcan', channel='vcan0', fd=True) as sock1, \ + CANSocket(bustype='socketcan', channel='vcan0', fd=True) as sock2: + sock1.basecls = Raw + sock2.send(CANFD(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) + rx = sock1.recv() + assert rx == Raw(bytes(CANFD(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'))) + += CAN Socket send recv swapped + +conf.contribs['CAN']['swap-bytes'] = True + +with CANSocket(bustype='socketcan', channel='vcan0', fd=True) as sock1, \ + CANSocket(bustype='socketcan', channel='vcan0', fd=True) as sock2: + sock2.send(CANFD(identifier=0x7ff,length=64,data=b'\x01' * 64)) + sock1.basecls = CAN + rx = sock1.recv() + assert rx == CANFD(identifier=0x7ff,length=64,data=b'\x01' * 64) + +conf.contribs['CAN']['swap-bytes'] = False + += sniff with filtermask 0x7ff + +msgs = [CANFD(identifier=0x200, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8), + CANFD(identifier=0x300, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8), + CANFD(identifier=0x300, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8), + CANFD(identifier=0x200, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8), + CANFD(identifier=0x100, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8), + CANFD(identifier=0x200, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8)] + +with CANSocket(bustype='socketcan', channel='vcan0', fd=True, can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}]) as sock1, \ + CANSocket(bustype='socketcan', channel='vcan0', fd=True) as sock2: + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=3) + assert len(packets) == 3 + + ++ bridge and sniff tests += bridge and sniff setup vcan1 package forwarding + +bashCommand = "/bin/bash -c 'sudo ip link add name vcan1 type vcan; sudo ip link set dev vcan1 up'" +assert 0 == os.system(bashCommand) + +sock0 = CANSocket(bustype='socketcan', channel='vcan0', fd=True) +sock1 = CANSocket(bustype='socketcan', channel='vcan1', fd=True) + +bridgeStarted = threading.Event() +def bridge(): + global bridgeStarted + bSock0 = CANSocket( + bustype='socketcan', channel='vcan0', bitrate=250000, fd=True) + bSock1 = CANSocket( + bustype='socketcan', channel='vcan1', bitrate=250000, fd=True) + def pnr(pkt): + return pkt + bSock0.timeout = 0.01 + bSock1.timeout = 0.01 + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=6) + bSock0.close() + bSock1.close() + +threadBridge = threading.Thread(target=bridge) +threadBridge.start() +bridgeStarted.wait(timeout=1) + +sock0.send(CANFD(flags='extended', identifier=0x10010000, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8)) +sock0.send(CANFD(flags='extended', identifier=0x10020000, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8)) +sock0.send(CANFD(flags='extended', identifier=0x10000000, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8)) +sock0.send(CANFD(flags='extended', identifier=0x10030000, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8)) +sock0.send(CANFD(flags='extended', identifier=0x10040000, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8)) +sock0.send(CANFD(flags='extended', identifier=0x10000000, length=64, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' * 8)) + +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) +assert len(packetsVCan1) == 6 + +sock1.close() +sock0.close() + +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() + + += Delete vcan interfaces +~ needs_root linux vcan_socket + +if 0 != call(["sudo", "ip" ,"link", "delete", "vcan0"]): + raise Exception("vcan0 could not be deleted") + +if 0 != call(["sudo", "ip" ,"link", "delete", "vcan1"]): + raise Exception("vcan1 could not be deleted") diff --git a/test/contrib/cansocket.uts b/test/contrib/cansocket.uts index 4d649c696e2..52165634c9a 100644 --- a/test/contrib/cansocket.uts +++ b/test/contrib/cansocket.uts @@ -1,5 +1,5 @@ % Regression tests for compatibility between NativeCANSocket and PythonCANSocket -~ python3_only not_pypy vcan_socket needs_root linux +~ not_pypy vcan_socket needs_root linux # More information at http://www.secdev.org/projects/UTscapy/ @@ -10,20 +10,16 @@ ~ conf = Load module - -load_layer("can") +load_layer("can", globals_dict=globals()) from scapy.contrib.cansocket_python_can import PythonCANSocket from scapy.contrib.cansocket_native import NativeCANSocket -conf.contribs['CAN'] = {'swap-bytes': False} +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} = Setup string for vcan -~ conf command - bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" = Load os - import os import threading from subprocess import call @@ -50,13 +46,10 @@ def sender(sock, msg): sock1 = NativeCANSocket(bustype='socketcan', channel='vcan0') sock2 = NativeCANSocket(bustype='socketcan', channel='vcan0') -thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=1,data=b'\x01'), )) -thread.start() -send_done.wait(timeout=1) +sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) rx = sock1.recv() sock1.close() sock2.close() -thread.join(timeout=5) assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') @@ -64,11 +57,8 @@ assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') with NativeCANSocket(bustype='socketcan', channel='vcan0') as sock1, \ NativeCANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=1,data=b'\x01'),)) - thread.start() - send_done.wait(timeout=1) + sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) rx = sock1.recv() - thread.join(timeout=5) assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') @@ -77,13 +67,10 @@ assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') sock1 = PythonCANSocket(bustype='socketcan', channel='vcan0') sock2 = PythonCANSocket(bustype='socketcan', channel='vcan0') -thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=1,data=b'\x01'), )) -thread.start() -send_done.wait(timeout=1) +sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) rx = sock1.recv() sock1.close() sock2.close() -thread.join(timeout=5) assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') @@ -91,11 +78,8 @@ assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') with PythonCANSocket(bustype='socketcan', channel='vcan0') as sock1, \ PythonCANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=1,data=b'\x01'),)) - thread.start() - send_done.wait(timeout=1) + sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) rx = sock1.recv() - thread.join(timeout=5) assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') @@ -105,12 +89,10 @@ conf.contribs['CAN']['swap-bytes'] = True with NativeCANSocket(bustype='socketcan', channel='vcan0') as sock1, \ NativeCANSocket(bustype='socketcan', channel='vcan0') as sock2: - time.sleep(0) - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'),)) - rx = sock1.sniff(count=1, timeout=1, started_callback=thread.start) + sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) + rx = sock1.sniff(count=1, timeout=1) assert len(rx) == 1 assert rx[0] == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread.join(timeout=5) conf.contribs['CAN']['swap-bytes'] = False @@ -120,10 +102,9 @@ conf.contribs['CAN']['swap-bytes'] = True with PythonCANSocket(bustype='socketcan', channel='vcan0') as sock1, \ PythonCANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'),)) - rx = sock1.sniff(count=1, timeout=1, started_callback=thread.start) + sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) + rx = sock1.sniff(count=1, timeout=1) assert rx[0] == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread.join(timeout=5) conf.contribs['CAN']['swap-bytes'] = False @@ -138,10 +119,10 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with NativeCANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}]) as sock1, \ NativeCANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - packets = sock1.sniff(timeout=0.1, started_callback=thread.start) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=3) assert len(packets) == 3 - thread.join(timeout=5) = PythonCANSocket sniff with filtermask 0x7ff @@ -154,10 +135,10 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with PythonCANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}]) as sock1, \ PythonCANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - packets = sock1.sniff(timeout=0.1, started_callback=thread.start) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=3) assert len(packets) == 3 - thread.join(timeout=5) = NativeCANSocket sniff with multiple filters @@ -171,10 +152,10 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with NativeCANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}, {'can_id': 0x400, 'can_mask': 0x7ff}, {'can_id': 0x600, 'can_mask': 0x7ff}, {'can_id': 0x7ff, 'can_mask': 0x7ff}]) as sock1, \ NativeCANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - packets = sock1.sniff(timeout=0.1, started_callback=thread.start) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=4) assert len(packets) == 4 - thread.join(timeout=5) = PythonCANSocket sniff with multiple filters @@ -188,10 +169,10 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with PythonCANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}, {'can_id': 0x400, 'can_mask': 0x7ff}, {'can_id': 0x600, 'can_mask': 0x7ff}, {'can_id': 0x7ff, 'can_mask': 0x7ff}]) as sock1, \ PythonCANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - packets = sock1.sniff(timeout=0.1, started_callback=thread.start) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=4) assert len(packets) == 4 - thread.join(timeout=5) + bridge and sniff tests @@ -205,14 +186,6 @@ assert 0 == os.system(bashCommand) sock0 = NativeCANSocket(bustype='socketcan', channel='vcan0') sock1 = NativeCANSocket(bustype='socketcan', channel='vcan1') -def senderVCan0(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridge(): global bridgeStarted @@ -226,37 +199,34 @@ def bridge(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) bridgeStarted.wait() -packetsVCan1 = sock1.sniff(timeout=0.5, started_callback=threadSender.start) +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) + +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) assert len(packetsVCan1) == 6 sock1.close() sock0.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) = PythonCANSocket bridge and sniff setup vcan1 package forwarding sock0 = PythonCANSocket(bustype='socketcan', channel='vcan0') sock1 = PythonCANSocket(bustype='socketcan', channel='vcan1') -def senderVCan0(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridge(): global bridgeStarted @@ -270,24 +240,28 @@ def bridge(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) bridgeStarted.wait() -packetsVCan1 = sock1.sniff(timeout=0.5, started_callback=threadSender.start) +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) + +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) assert len(packetsVCan1) == 6 sock1.close() sock0.close() -threadBridge.join() -threadSender.join() - +threadBridge.join(timeout=3) + Cleanup diff --git a/test/contrib/cansocket_native.uts b/test/contrib/cansocket_native.uts index fa504850342..fedf063eef6 100644 --- a/test/contrib/cansocket_native.uts +++ b/test/contrib/cansocket_native.uts @@ -1,5 +1,5 @@ % Regression tests for nativecansocket -~ python3_only not_pypy vcan_socket needs_root linux +~ not_pypy vcan_socket needs_root linux # More information at http://www.secdev.org/projects/UTscapy/ @@ -7,313 +7,269 @@ ############ ############ + Configuration of CAN virtual sockets +~ conf = Load module -~ conf command needs_root linux - -load_layer("can") +load_layer("can", globals_dict=globals()) conf.contribs['CANSocket'] = {'use-python-can': False} from scapy.contrib.cansocket_native import * -conf.contribs['CAN'] = {'swap-bytes': False} +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} = Setup string for vcan -~ conf command - bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" +bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" = Load os -~ conf command needs_root linux - import os import threading from time import sleep from subprocess import call = Setup vcan0 -~ conf command needs_root linux - -0 == os.system(bashCommand) +assert 0 == os.system(bashCommand) + Basic Packet Tests() = CAN Packet init - - canframe = CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') -bytes(canframe) == b'\x00\x00\x07\xff\x08\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08' +assert bytes(canframe) == b'\x00\x00\x07\xff\x08\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08' + Basic Socket Tests() = CAN Socket Init +sock1 = CANSocket(channel="vcan0") += CAN Socket send recv small packet without remove padding + +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': False} + +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) +sock2.close() + +rx = sock1.recv() +print(repr(rx)) +assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') / Padding(b"\x00" * 7) -sock1 = CANSocket(channel="vcan0") = CAN Socket send recv small packet -def sender(): - sleep(0.1) - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) - sock2.close() +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} + +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) +sock2.close() -thread = threading.Thread(target=sender) -thread.start() rx = sock1.recv() -rx == CAN(identifier=0x7ff,length=1,data=b'\x01') -thread.join(timeout=5) +print(repr(rx)) +assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') = CAN Socket send recv -def sender(): - sleep(0.1) - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() -thread = threading.Thread(target=sender) -thread.start() rx = sock1.recv() -rx == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') -thread.join(timeout=5) +assert rx == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') = CAN Socket basecls test -def sender(): - sleep(0.1) - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() sock1.basecls = Raw -thread = threading.Thread(target=sender) -thread.start() rx = sock1.recv() -rx == Raw(bytes(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'))) +assert rx == Raw(bytes(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'))) sock1.basecls = CAN -thread.join(timeout=5) + Advanced Socket Tests() = CAN Socket sr1 - - tx = CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') = CAN Socket sr1 init time +assert tx.sent_time == None +sock2 = CANSocket(channel="vcan0") +sock2.send(tx) +sock2.close() -tx.sent_time == None - -def sender(): - sleep(0.1) - sock2 = CANSocket(channel="vcan0") - sock2.send(tx) - sock2.close() - -thread = threading.Thread(target=sender) -thread.start() rx = None -rx = sock1.sr1(tx, verbose=False) -rx == tx +rx = sock1.sr1(tx, verbose=False, timeout=3) +assert rx == tx sock1.close() -thread.join(timeout=5) = CAN Socket sr1 time check - - -assert tx.sent_time < rx.time and rx.time > 0 +assert abs(tx.sent_time - rx.time) < 0.1 +assert rx.time > 0 = sr can - - tx = CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') = sr can check init time - - assert tx.sent_time == None -def sender(): - sleep(0.1) - sock2 = CANSocket(channel="vcan0") - sock2.send(tx) - sock2.close() - sock1 = CANSocket(channel="vcan0") -thread = threading.Thread(target=sender) -thread.start() + +sock2 = CANSocket(channel="vcan0") +sock2.send(tx) +sock2.close() + rx = None rx = sock1.sr(tx, timeout=1, verbose=False) rx = rx[0][0][1] assert tx == rx -thread.join(timeout=5) + = srcan check init time basecls +sock1 = CANSocket(channel="vcan0", basecls=Raw) -def sender(): - sleep(0.1) - sock2 = CANSocket(channel="vcan0") - sock2.send(tx) - sock2.close() +sock2 = CANSocket(channel="vcan0") +sock2.send(tx) +sock2.close() -sock1 = CANSocket(channel="vcan0", basecls=Raw) -thread = threading.Thread(target=sender) -thread.start() rx = None rx = sock1.sr(tx, timeout=1, verbose=False) rx = rx[0][0][1] assert Raw(bytes(tx)) == rx -thread.join(timeout=5) -= sr can check rx and tx +sock1.close() += sr can check rx and tx -assert tx.sent_time > 0 and rx.time > 0 and tx.sent_time < rx.time +assert tx.sent_time > 0 and rx.time > 0 = sniff with filtermask 0x7ff - sock1 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}]) -def sender(): - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x100, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() - -thread = threading.Thread(target=sender) -packets = sock1.sniff(timeout=0.2, started_callback=thread.start, verbose=False) -len(packets) == 3 - +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x100, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() + +packets = sock1.sniff(timeout=0.1, verbose=False, count=3) +assert len(packets) == 3 sock1.close() -thread.join(timeout=5) = sniff with filtermask 0x700 - sock1 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x700}]) -def sender(): - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x212, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x2ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x1ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x2aa, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x212, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x2ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x1ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x2aa, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() -thread = threading.Thread(target=sender) -packets = sock1.sniff(timeout=0.2, started_callback=thread.start, verbose=False) -len(packets) == 4 +packets = sock1.sniff(timeout=0.1, verbose=False, count=4) +assert len(packets) == 4 sock1.close() -thread.join(timeout=5) = sniff with filtermask 0x0ff sock1 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x0ff}]) -def sender(): - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x301, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x1ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x700, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x100, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x301, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x1ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x700, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x100, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() -thread = threading.Thread(target=sender) -packets = sock1.sniff(timeout=0.2, started_callback=thread.start, verbose=False) -len(packets) == 4 +packets = sock1.sniff(timeout=0.1, verbose=False, count=4) +assert len(packets) == 4 sock1.close() -thread.join(timeout=5) = sniff with multiple filters sock1 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}, {'can_id': 0x400, 'can_mask': 0x7ff}, {'can_id': 0x600, 'can_mask': 0x7ff}, {'can_id': 0x7ff, 'can_mask': 0x7ff}]) -def sender(): - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x400, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x500, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x600, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x700, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x7ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() - -thread = threading.Thread(target=sender) -packets = sock1.sniff(timeout=0.2, started_callback=thread.start, verbose=False) -len(packets) == 4 +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x400, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x500, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x600, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x700, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x7ff, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() + +packets = sock1.sniff(timeout=0.1, verbose=False, count=4) +assert len(packets) == 4 sock1.close() -thread.join(timeout=5) = sniff with filtermask 0x7ff and inverse filter sock1 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x200 | CAN_INV_FILTER, 'can_mask': 0x7ff}]) -def sender(): - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x100, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x300, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x100, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() -thread = threading.Thread(target=sender) -packets = sock1.sniff(timeout=0.2, started_callback=thread.start, verbose=False) -len(packets) == 2 +packets = sock1.sniff(timeout=0.1, verbose=False, count=2) +assert len(packets) == 2 sock1.close() -thread.join(timeout=5) = sniff with filtermask 0x1FFFFFFF sock1 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x10000000, 'can_mask': 0x1fffffff}]) -def sender(): - sock2 = CANSocket(channel="vcan0") - sock2.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock2.close() +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() -thread = threading.Thread(target=sender) -packets = sock1.sniff(timeout=0.2, started_callback=thread.start, verbose=False) -len(packets) == 2 +packets = sock1.sniff(timeout=0.1, verbose=False, count=2) +assert len(packets) == 2 sock1.close() -thread.join(timeout=5) = sniff with filtermask 0x1FFFFFFF and inverse filter sock1 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x10000000 | CAN_INV_FILTER, 'can_mask': 0x1fffffff}]) -if six.PY3: - thread = threading.Thread(target=sender) - packets = sock1.sniff(timeout=0.2, started_callback=thread.start, verbose=False) - len(packets) == 4 +sock2 = CANSocket(channel="vcan0") +sock2.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock2.close() + +packets = sock1.sniff(timeout=0.1, verbose=False, count=4) +assert len(packets) == 4 sock1.close() @@ -323,9 +279,9 @@ sock1.close() sock1 = CANSocket(channel="vcan0", receive_own_messages=True) tx = CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') rx = None -rx = sock1.sr1(tx, verbose=False) -tx == rx -tx.sent_time < rx.time and tx == rx and rx.time > 0 +rx = sock1.sr1(tx, verbose=False, timeout=3) +assert tx == rx +assert tx.sent_time < rx.time and tx == rx and rx.time > 0 sock1.close() @@ -334,8 +290,8 @@ sock1.close() sock1 = CANSocket(channel="vcan0", receive_own_messages=True) tx = CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') rx = None -rx = sock1.sr(tx, timeout=1, verbose=False) -tx == rx[0][0][1] +rx = sock1.sr(tx, timeout=0.1, verbose=False) +assert tx == rx[0][0][1] + bridge and sniff tests @@ -343,19 +299,11 @@ tx == rx[0][0][1] bashCommand = "/bin/bash -c 'sudo ip link add name vcan1 type vcan; sudo ip link set dev vcan1 up'" -0 == os.system(bashCommand) +assert 0 == os.system(bashCommand) sock0 = CANSocket(channel='vcan0') sock1 = CANSocket(channel='vcan1') -def senderVCan0(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridge(): @@ -365,20 +313,25 @@ def bridge(): def pnr(pkt): return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) bridgeStarted.wait(timeout=5) +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) -packetsVCan1 = sock1.sniff(timeout=0.2, started_callback=threadSender.start, verbose=False) -len(packetsVCan1) == 6 +packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False, count=6) +assert len(packetsVCan1) == 6 -threadSender.join(timeout=5) threadBridge.join(timeout=5) +assert not threadBridge.is_alive() sock1.close() sock0.close() @@ -389,12 +342,6 @@ sock0.close() sock0 = CANSocket(channel='vcan0') sock1 = CANSocket(channel='vcan1') -def senderVCan1(): - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridge(): @@ -404,23 +351,27 @@ def bridge(): def pnr(pkt): return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False, count=4) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderVCan1) bridgeStarted.wait(timeout=5) -packetsVCan0 = sock0.sniff(timeout=0.2, started_callback=threadSender.start, verbose=False) -len(packetsVCan0) == 4 +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.1, verbose=False, count=4) +assert len(packetsVCan0) == 4 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) +assert not threadBridge.is_alive() =bridge and sniff setup vcan0 vcan1 package forwarding both directions @@ -428,18 +379,6 @@ threadBridge.join(timeout=5) sock0 = CANSocket(channel='vcan0') sock1 = CANSocket(channel='vcan1') -def senderBothVCans(): - sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x30, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridge(): @@ -449,27 +388,36 @@ def bridge(): def pnr(pkt): return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False, count=10) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderBothVCans) - bridgeStarted.wait(timeout=5) -packetsVCan0 = sock0.sniff(timeout=0.1, count=6, started_callback=threadSender.start, verbose=False) -packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False) +sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x30, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.1, count=4, verbose=False) +packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False, count=6) -len(packetsVCan0) == 4 -len(packetsVCan1) == 6 +assert len(packetsVCan0) == 4 +assert len(packetsVCan1) == 6 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) +assert not threadBridge.is_alive() =bridge and sniff setup vcan1 package change @@ -477,15 +425,6 @@ threadBridge.join(timeout=5) sock0 = CANSocket(channel='vcan0') sock1 = CANSocket(channel='vcan1', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) -def senderVCan0(): - sleep(0.1) - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridgeWithPackageChangeVCan0ToVCan1(): @@ -497,24 +436,29 @@ def bridgeWithPackageChangeVCan0ToVCan1(): pkt.identifier = 0x10010000 return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.2, verbose=False, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithPackageChangeVCan0ToVCan1) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) bridgeStarted.wait(timeout=5) +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) -packetsVCan1 = sock1.sniff(timeout=0.2, started_callback=threadSender.start, verbose=False) -len(packetsVCan1) == 6 +packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False, count=6) +assert len(packetsVCan1) == 6 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) +assert not threadBridge.is_alive() =bridge and sniff setup vcan0 package change @@ -522,13 +466,6 @@ threadBridge.join(timeout=5) sock0 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) sock1 = CANSocket(channel='vcan1') -def senderVCan1(): - sleep(0.1) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithPackageChangeVCan1ToVCan0(): @@ -540,23 +477,25 @@ def bridgeWithPackageChangeVCan1ToVCan0(): pkt.identifier = 0x10010000 return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.2, verbose=False, count=4) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithPackageChangeVCan1ToVCan0) threadBridge.start() -threadSender = threading.Thread(target=senderVCan1) bridgeStarted.wait(timeout=5) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) -packetsVCan0 = sock0.sniff(timeout=0.2, started_callback=threadSender.start, verbose=False) -len(packetsVCan0) == 4 +packetsVCan0 = sock0.sniff(timeout=0.1, verbose=False, count=4) +assert len(packetsVCan0) == 4 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) =bridge and sniff setup vcan0 and vcan1 package change in both directions @@ -565,18 +504,6 @@ threadBridge.join(timeout=5) sock0 = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) sock1 = CANSocket(channel='vcan1', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) -def senderBothVCans(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithPackageChangeBothDirections(): @@ -588,26 +515,33 @@ def bridgeWithPackageChangeBothDirections(): pkt.identifier = 0x10010000 return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.2, verbose=False, count=10) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithPackageChangeBothDirections) threadBridge.start() -threadSender = threading.Thread(target=senderBothVCans) bridgeStarted.wait(timeout=5) -threadSender.start() - -packetsVCan0 = sock0.sniff(timeout=0.1, verbose=False) -packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False) -len(packetsVCan0) == 4 -len(packetsVCan1) == 6 +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.1, verbose=False, count=4) +packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False, count=6) +assert len(packetsVCan0) == 4 +assert len(packetsVCan1) == 6 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) =bridge and sniff setup vcan0 package remove @@ -616,14 +550,6 @@ threadBridge.join(timeout=5) sock0 = CANSocket(channel='vcan0') sock1 = CANSocket(channel='vcan1') -def senderVCan0(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridgeWithRemovePackageFromVCan0ToVCan1(): @@ -637,25 +563,27 @@ def bridgeWithRemovePackageFromVCan0ToVCan1(): pkt = pkt return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.2, verbose=False, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithRemovePackageFromVCan0ToVCan1) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) - bridgeStarted.wait(timeout=5) -threadSender.start() +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) -packetsVCan1 = sock1.sniff(timeout=0.2, verbose=False) -len(packetsVCan1) == 5 +packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False, count=5) +assert len(packetsVCan1) == 5 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) =bridge and sniff setup vcan1 package remove @@ -664,12 +592,6 @@ threadBridge.join(timeout=5) sock0 = CANSocket(channel='vcan0') sock1 = CANSocket(channel='vcan1') -def senderVCan1(): - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithRemovePackageFromVCan1ToVCan0(): @@ -683,24 +605,25 @@ def bridgeWithRemovePackageFromVCan1ToVCan0(): pkt = pkt return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.2, verbose=False, count=4) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithRemovePackageFromVCan1ToVCan0) threadBridge.start() -threadSender = threading.Thread(target=senderVCan1) bridgeStarted.wait(timeout=5) -threadSender.start() +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) -packetsVCan0 = sock0.sniff(timeout=0.2, verbose=False) -len(packetsVCan0) == 3 +packetsVCan0 = sock0.sniff(timeout=0.1, verbose=False, count=3) +assert len(packetsVCan0) == 3 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) =bridge and sniff setup vcan0 and vcan1 package remove both directions @@ -709,18 +632,6 @@ threadBridge.join(timeout=5) sock0 = CANSocket(channel="vcan0") sock1 = CANSocket(channel="vcan1") -def senderBothVCans(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithRemovePackageInBothDirections(): @@ -740,26 +651,34 @@ def bridgeWithRemovePackageInBothDirections(): pkt = pkt return pkt bridgeStarted.set() - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnrA, xfrm21=pnrB, timeout=0.2, verbose=False) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnrA, xfrm21=pnrB, timeout=0.2, verbose=False, count=10) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithRemovePackageInBothDirections) threadBridge.start() -threadSender = threading.Thread(target=senderBothVCans) - bridgeStarted.wait(timeout=5) -packetsVCan0 = sock0.sniff(timeout=0.1, started_callback=threadSender.start, verbose=False) -packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False) +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.1, verbose=False, count=3) +packetsVCan1 = sock1.sniff(timeout=0.1, verbose=False, count=5) -len(packetsVCan0) == 3 -len(packetsVCan1) == 5 +assert len(packetsVCan0) == 3 +assert len(packetsVCan1) == 5 sock0.close() sock1.close() -threadSender.join(timeout=5) threadBridge.join(timeout=5) = Delete vcan interfaces diff --git a/test/contrib/cansocket_python_can.uts b/test/contrib/cansocket_python_can.uts index 658ec49302b..78fa349d899 100644 --- a/test/contrib/cansocket_python_can.uts +++ b/test/contrib/cansocket_python_can.uts @@ -1,5 +1,5 @@ % Regression tests for the CANSocket -~ vcan_socket linux needs_root +~ vcan_socket linux needs_root not_pypy # More information at http://www.secdev.org/projects/UTscapy/ @@ -9,13 +9,12 @@ + Configuration of CAN virtual sockets = Load module -~ conf command - -conf.contribs['CAN'] = {'swap-bytes': False} -load_layer("can") +~ conf + +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} +load_layer("can", globals_dict=globals()) conf.contribs['CANSocket'] = {'use-python-can': True} from scapy.contrib.cansocket_python_can import * -import can = Setup string for vcan ~ conf command @@ -63,27 +62,21 @@ sock1 = None sock1 = CANSocket(bustype='socketcan', channel='vcan0') sock2 = CANSocket(bustype='socketcan', channel='vcan0') -thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=1,data=b'\x01'), )) -thread.start() -send_done.wait(timeout=1) -send_done.clear() +sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) rx = sock1.recv() sock1.close() sock2.close() -rx == CAN(identifier=0x7ff,length=1,data=b'\x01') +assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') = CAN Socket send recv small packet test with with CANSocket(bustype='socketcan', channel='vcan0') as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=1,data=b'\x01'),)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() + sock2.send(CAN(identifier=0x7ff,length=1,data=b'\x01')) rx = sock1.recv() -rx == CAN(identifier=0x7ff,length=1,data=b'\x01') +assert rx == CAN(identifier=0x7ff,length=1,data=b'\x01') = CAN Socket send recv ISOTP_Packet @@ -92,37 +85,28 @@ from scapy.contrib.isotp import ISOTPHeader, ISOTP_FF with CANSocket(bustype='socketcan', channel='vcan0') as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, ISOTPHeader(identifier=0x7ff)/ISOTP_FF(message_size=100, data=b'abcdef'),)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() + sock2.send(ISOTPHeader(identifier=0x7ff)/ISOTP_FF(message_size=100, data=b'abcdef')) rx = sock1.recv() - rx == CAN(identifier=0x7ff,length=8,data=b'\x10\x64abcdef') + assert rx == CAN(identifier=0x7ff,length=8,data=b'\x10\x64abcdef') = CAN Socket basecls test with CANSocket(bustype='socketcan', channel='vcan0') as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'),)) - thread.start() sock1.basecls = Raw - send_done.wait(timeout=1) - send_done.clear() + sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) rx = sock1.recv() - rx == Raw(bytes(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'))) + assert rx == Raw(bytes(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'))) = CAN Socket send recv with CANSocket(bustype='socketcan', channel='vcan0') as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'),)) - thread.start() + sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) sock1.basecls = CAN - send_done.wait(timeout=1) - send_done.clear() rx = sock1.recv() - rx == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + assert rx == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') = CAN Socket send recv swapped @@ -130,13 +114,10 @@ conf.contribs['CAN']['swap-bytes'] = True with CANSocket(bustype='socketcan', channel='vcan0') as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08'),)) - thread.start() + sock2.send(CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) sock1.basecls = CAN - send_done.wait(timeout=1) - send_done.clear() rx = sock1.recv() - rx == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + assert rx == CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08') conf.contribs['CAN']['swap-bytes'] = False @@ -151,11 +132,9 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}]) as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() - packets = sock1.sniff(timeout=0.1) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=3) assert len(packets) == 3 @@ -170,11 +149,9 @@ msgs = [CAN(identifier=0x212, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x700}]) as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() - packets = sock1.sniff(timeout=0.1) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=4) assert len(packets) == 4 = sniff with filtermask 0x0ff @@ -188,12 +165,10 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0xff}]) as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() - packets = sock1.sniff(timeout=0.1) - len(packets) == 4 + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=4) + assert len(packets) == 4 = sniff with multiple filters @@ -207,11 +182,9 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}, {'can_id': 0x400, 'can_mask': 0x7ff}, {'can_id': 0x600, 'can_mask': 0x7ff}, {'can_id': 0x7ff, 'can_mask': 0x7ff}]) as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() - packets = sock1.sniff(timeout=0.1) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=4) assert len(packets) == 4 = sniff with filtermask 0x7ff @@ -226,11 +199,9 @@ msgs = [CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08' with CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff}]) as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() - packets = sock1.sniff(timeout=0.1) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=4) assert len(packets) == 4 = sniff with filtermask 0x1FFFFFFF @@ -244,11 +215,9 @@ msgs = [CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x with CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x10000000, 'can_mask': 0x1fffffff}]) as sock1, \ CANSocket(bustype='socketcan', channel='vcan0') as sock2: - thread = threading.Thread(target=sender, args=(sock2, msgs,)) - thread.start() - send_done.wait(timeout=1) - send_done.clear() - packets = sock1.sniff(timeout=0.1) + for m in msgs: + sock2.send(m) + packets = sock1.sniff(timeout=0.1, count=2) assert len(packets) == 2 @@ -256,19 +225,11 @@ with CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x1 = bridge and sniff setup vcan1 package forwarding bashCommand = "/bin/bash -c 'sudo ip link add name vcan1 type vcan; sudo ip link set dev vcan1 up'" -0 == os.system(bashCommand) +assert 0 == os.system(bashCommand) sock0 = CANSocket(bustype='socketcan', channel='vcan0') sock1 = CANSocket(bustype='socketcan', channel='vcan1') -def senderVCan0(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridge(): global bridgeStarted @@ -282,35 +243,35 @@ def bridge(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) -bridgeStarted.wait() +bridgeStarted.wait(timeout=1) + +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) -packetsVCan1 = sock1.sniff(timeout=0.5, started_callback=threadSender.start) +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) assert len(packetsVCan1) == 6 sock1.close() sock0.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() = bridge and sniff setup vcan0 package forwarding sock0 = CANSocket(bustype='socketcan', channel='vcan0') sock1 = CANSocket(bustype='socketcan', channel='vcan1') -def senderVCan1(): - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridge(): global bridgeStarted @@ -320,42 +281,33 @@ def bridge(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=4) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderVCan1) -bridgeStarted.wait() +bridgeStarted.wait(timeout=1) -packetsVCan0 = sock0.sniff(timeout=0.3, started_callback=threadSender.start) -len(packetsVCan0) == 4 +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) +assert len(packetsVCan0) == 4 sock0.close() sock1.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() =bridge and sniff setup vcan0 vcan1 package forwarding both directions sock0 = CANSocket(bustype='socketcan', channel='vcan0') sock1 = CANSocket(bustype='socketcan', channel='vcan1') - -def senderBothVCans(): - sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x30, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridge(): global bridgeStarted @@ -365,39 +317,41 @@ def bridge(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=10) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridge) threadBridge.start() -threadSender = threading.Thread(target=senderBothVCans) -bridgeStarted.wait() - -packetsVCan0 = sock0.sniff(timeout=0.3, started_callback=threadSender.start) -packetsVCan1 = sock1.sniff(timeout=0.3) -len(packetsVCan0) == 4 -len(packetsVCan1) == 6 +bridgeStarted.wait(timeout=1) + +sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x25, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x20, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x30, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x80, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) +assert len(packetsVCan0) == 4 +assert len(packetsVCan1) == 6 sock0.close() sock1.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() =bridge and sniff setup vcan1 package change sock0 = CANSocket(bustype='socketcan', channel='vcan0') sock1 = CANSocket(bustype='socketcan', channel='vcan1', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) -def senderVCan0(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridgeWithPackageChangeVCan0ToVCan1(): global bridgeStarted @@ -409,35 +363,34 @@ def bridgeWithPackageChangeVCan0ToVCan1(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithPackageChangeVCan0ToVCan1) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) -bridgeStarted.wait() - -packetsVCan1 = sock1.sniff(timeout=0.3, started_callback=threadSender.start) -len(packetsVCan1) == 6 +bridgeStarted.wait(timeout=1) +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) + +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) +assert len(packetsVCan1) == 6 sock0.close() sock1.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() =bridge and sniff setup vcan0 package change sock1 = CANSocket(bustype='socketcan', channel='vcan1') sock0 = CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) -def senderVCan1(): - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithPackageChangeVCan1ToVCan0(): global bridgeStarted @@ -449,41 +402,33 @@ def bridgeWithPackageChangeVCan1ToVCan0(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=4) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithPackageChangeVCan1ToVCan0) threadBridge.start() -threadSender = threading.Thread(target=senderVCan1) -bridgeStarted.wait() +bridgeStarted.wait(timeout=1) -packetsVCan0 = sock0.sniff(timeout=0.3, started_callback=threadSender.start) -len(packetsVCan0) == 4 +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) +assert len(packetsVCan0) == 4 sock0.close() sock1.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() =bridge and sniff setup vcan0 and vcan1 package change in both directions sock0 = CANSocket(bustype='socketcan', channel='vcan0', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) sock1 = CANSocket(bustype='socketcan', channel='vcan1', can_filters=[{'can_id': 0x10010000, 'can_mask': 0x1fffffff}]) -def senderBothVCans(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithPackageChangeBothDirections(): global bridgeStarted @@ -495,39 +440,41 @@ def bridgeWithPackageChangeBothDirections(): return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=10) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithPackageChangeBothDirections) threadBridge.start() -threadSender = threading.Thread(target=senderBothVCans) -bridgeStarted.wait() - -packetsVCan0 = sock0.sniff(timeout=0.3, started_callback=threadSender.start) -packetsVCan1 = sock1.sniff(timeout=0.3) -len(packetsVCan0) == 4 -len(packetsVCan1) == 6 +bridgeStarted.wait(timeout=1) + +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) +assert len(packetsVCan0) == 4 +assert len(packetsVCan1) == 6 sock0.close() sock1.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() =bridge and sniff setup vcan0 package remove sock0 = CANSocket(bustype='socketcan', channel='vcan0') sock1 = CANSocket(bustype='socketcan', channel='vcan1') -def senderVCan0(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - bridgeStarted = threading.Event() def bridgeWithRemovePackageFromVCan0ToVCan1(): global bridgeStarted @@ -535,42 +482,42 @@ def bridgeWithRemovePackageFromVCan0ToVCan1(): bSock1 = CANSocket(bustype='socketcan', channel='vcan1') def pnr(pkt): if(pkt.identifier == 0x10020000): - pkt = None + pkt = False else: pkt = pkt return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=6) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithRemovePackageFromVCan0ToVCan1) threadBridge.start() -threadSender = threading.Thread(target=senderVCan0) -bridgeStarted.wait() +bridgeStarted.wait(timeout=1) -packetsVCan1 = sock1.sniff(timeout=0.3, started_callback=threadSender.start) +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) -len(packetsVCan1) == 5 +packetsVCan1 = sock1.sniff(timeout=0.5, count=5) + +assert len(packetsVCan1) == 5 sock0.close() sock1.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() =bridge and sniff setup vcan1 package remove sock0 = CANSocket(bustype='socketcan', channel='vcan0') sock1 = CANSocket(bustype='socketcan', channel='vcan1') -def senderVCan1(): - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithRemovePackageFromVCan1ToVCan0(): global bridgeStarted @@ -578,30 +525,34 @@ def bridgeWithRemovePackageFromVCan1ToVCan0(): bSock1 = CANSocket(bustype='socketcan', channel='vcan1') def pnr(pkt): if(pkt.identifier == 0x10050000): - pkt = None + pkt = False else: pkt = pkt return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm21=pnr, timeout=0.5, started_callback=bridgeStarted.set, count=4) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithRemovePackageFromVCan1ToVCan0) threadBridge.start() -threadSender = threading.Thread(target=senderVCan1) -bridgeStarted.wait() +bridgeStarted.wait(timeout=1) + +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) -packetsVCan0 = sock0.sniff(timeout=0.3, started_callback=threadSender.start) +packetsVCan0 = sock0.sniff(timeout=0.5, count=3) -len(packetsVCan0) == 3 +assert len(packetsVCan0) == 3 sock0.close() sock1.close() -threadBridge.join() -threadSender.join() +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() =bridge and sniff setup vcan0 and vcan1 package remove both directions @@ -609,18 +560,6 @@ threadSender.join() sock0 = CANSocket(bustype='socketcan', channel='vcan0') sock1 = CANSocket(bustype='socketcan', channel='vcan1') -def senderBothVCans(): - sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) - bridgeStarted = threading.Event() def bridgeWithRemovePackageInBothDirections(): global bridgeStarted @@ -628,32 +567,45 @@ def bridgeWithRemovePackageInBothDirections(): bSock1 = CANSocket(bustype='socketcan', channel='vcan1') def pnrA(pkt): if(pkt.identifier == 0x10020000): - pkt = None + pkt = False else: pkt = pkt return pkt def pnrB(pkt): if (pkt.identifier == 0x10050000): - pkt = None + pkt = False else: pkt = pkt return pkt bSock0.timeout = 0.01 bSock1.timeout = 0.01 - bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnrA, xfrm21=pnrB, timeout=0.5, started_callback=bridgeStarted.set) + bridge_and_sniff(if1=bSock0, if2=bSock1, xfrm12=pnrA, xfrm21=pnrB, timeout=0.5, started_callback=bridgeStarted.set, count=10) bSock0.close() bSock1.close() threadBridge = threading.Thread(target=bridgeWithRemovePackageInBothDirections) threadBridge.start() -threadSender = threading.Thread(target=senderBothVCans) -bridgeStarted.wait() - -packetsVCan0 = sock0.sniff(timeout=0.3, started_callback=threadSender.start) -packetsVCan1 = sock1.sniff(timeout=0.3) - -len(packetsVCan0) == 3 -len(packetsVCan1) == 5 +bridgeStarted.wait(timeout=1) + +sock0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10020000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10040000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock0.send(CAN(flags='extended', identifier=0x10000000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10050000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) +sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x04\x05\x06')) + +packetsVCan0 = sock0.sniff(timeout=0.5, count=3) +packetsVCan1 = sock1.sniff(timeout=0.5, count=5) + +assert len(packetsVCan0) == 3 +assert len(packetsVCan1) == 5 + +threadBridge.join(timeout=3) +assert not threadBridge.is_alive() sock0.close() sock1.close() diff --git a/test/contrib/cdp.uts b/test/contrib/cdp.uts index d4e3dacd41a..9a6601bcc72 100644 --- a/test/contrib/cdp.uts +++ b/test/contrib/cdp.uts @@ -8,34 +8,34 @@ = CDPv2 - Dissection (1) s = b'\x02\xb4\x8c\xfa\x00\x01\x00\x0cmyswitch\x00\x02\x00\x11\x00\x00\x00\x01\x01\x01\xcc\x00\x04\xc0\xa8\x00\xfd\x00\x03\x00\x13FastEthernet0/1\x00\x04\x00\x08\x00\x00\x00(\x00\x05\x01\x14Cisco Internetwork Operating System Software \nIOS (tm) C2950 Software (C2950-I6K2L2Q4-M), Version 12.1(22)EA14, RELEASE SOFTWARE (fc1)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2010 by cisco Systems, Inc.\nCompiled Tue 26-Oct-10 10:35 by nburra\x00\x06\x00\x15cisco WS-C2950-12\x00\x08\x00$\x00\x00\x0c\x01\x12\x00\x00\x00\x00\xff\xff\xff\xff\x01\x02!\xff\x00\x00\x00\x00\x00\x00\x00\x0b\xbe\x18\x9a@\xff\x00\x00\x00\t\x00\x0cMYDOMAIN\x00\n\x00\x06\x00\x01\x00\x0b\x00\x05\x01\x00\x0e\x00\x07\x01\x00\n\x00\x12\x00\x05\x00\x00\x13\x00\x05\x00\x00\x16\x00\x11\x00\x00\x00\x01\x01\x01\xcc\x00\x04\xc0\xa8\x00\xfd' cdpv2 = CDPv2_HDR(s) -assert(len(cdpv2) == 450) -assert(cdpv2.vers == 2) -assert(cdpv2.ttl == 180) -assert(cdpv2.cksum == 0x8cfa) -assert(cdpv2.haslayer(CDPMsgDeviceID)) -assert(cdpv2.haslayer(CDPMsgAddr)) -assert(cdpv2.haslayer(CDPAddrRecordIPv4)) -assert(cdpv2.haslayer(CDPMsgPortID)) -assert(cdpv2.haslayer(CDPMsgCapabilities)) -assert(cdpv2.haslayer(CDPMsgSoftwareVersion)) -assert(cdpv2.haslayer(CDPMsgPlatform)) -assert(cdpv2.haslayer(CDPMsgProtoHello)) -assert(cdpv2.haslayer(CDPMsgVTPMgmtDomain)) -assert(cdpv2.haslayer(CDPMsgNativeVLAN)) -assert(cdpv2.haslayer(CDPMsgDuplex)) -assert(cdpv2.haslayer(CDPMsgVoIPVLANReply)) -assert(cdpv2.haslayer(CDPMsgTrustBitmap)) -assert(cdpv2.haslayer(CDPMsgUntrustedPortCoS)) -assert(cdpv2.haslayer(CDPMsgMgmtAddr)) -assert(cdpv2[CDPMsgProtoHello].len == 36) -assert(cdpv2[CDPMsgProtoHello].oui == 0xc) -assert(cdpv2[CDPMsgProtoHello].protocol_id == 0x112) -assert(cdpv2[CDPMsgTrustBitmap].type == 0x0012) -assert(cdpv2[CDPMsgTrustBitmap].len == 5) -assert(cdpv2[CDPMsgTrustBitmap].trust_bitmap == 0x0) -assert(cdpv2[CDPMsgUntrustedPortCoS].type == 0x0013) -assert(cdpv2[CDPMsgUntrustedPortCoS].len == 5) -assert(cdpv2[CDPMsgUntrustedPortCoS].untrusted_port_cos == 0x0) +assert len(cdpv2) == 450 +assert cdpv2.vers == 2 +assert cdpv2.ttl == 180 +assert cdpv2.cksum == 0x8cfa +assert cdpv2.haslayer(CDPMsgDeviceID) +assert cdpv2.haslayer(CDPMsgAddr) +assert cdpv2.haslayer(CDPAddrRecordIPv4) +assert cdpv2.haslayer(CDPMsgPortID) +assert cdpv2.haslayer(CDPMsgCapabilities) +assert cdpv2.haslayer(CDPMsgSoftwareVersion) +assert cdpv2.haslayer(CDPMsgPlatform) +assert cdpv2.haslayer(CDPMsgProtoHello) +assert cdpv2.haslayer(CDPMsgVTPMgmtDomain) +assert cdpv2.haslayer(CDPMsgNativeVLAN) +assert cdpv2.haslayer(CDPMsgDuplex) +assert cdpv2.haslayer(CDPMsgVoIPVLANReply) +assert cdpv2.haslayer(CDPMsgTrustBitmap) +assert cdpv2.haslayer(CDPMsgUntrustedPortCoS) +assert cdpv2.haslayer(CDPMsgMgmtAddr) +assert cdpv2[CDPMsgProtoHello].len == 36 +assert cdpv2[CDPMsgProtoHello].oui == 0xc +assert cdpv2[CDPMsgProtoHello].protocol_id == 0x112 +assert cdpv2[CDPMsgTrustBitmap].type == 0x0012 +assert cdpv2[CDPMsgTrustBitmap].len == 5 +assert cdpv2[CDPMsgTrustBitmap].trust_bitmap == 0x0 +assert cdpv2[CDPMsgUntrustedPortCoS].type == 0x0013 +assert cdpv2[CDPMsgUntrustedPortCoS].len == 5 +assert cdpv2[CDPMsgUntrustedPortCoS].untrusted_port_cos == 0x0 = CDPv2 - Rebuild (1) @@ -45,23 +45,23 @@ assert raw(cdpv2) == s = CDPv2 - Dissection (2) s = b'\x02\xb4\xd7\xdb\x00\x01\x00\x13SIP001122334455\x00\x02\x00\x11\x00\x00\x00\x01\x01\x01\xcc\x00\x04\xc0\xa8\x01!\x00\x03\x00\nPort 1\x00\x04\x00\x08\x00\x00\x00\x10\x00\x05\x00\x10P003-08-2-00\x00\x06\x00\x17Cisco IP Phone 7960\x00\x0f\x00\x08 \x02\x00\x01\x00\x0b\x00\x05\x01\x00\x10\x00\x06\x18\x9c' cdpv2 = CDPv2_HDR(s) -assert(cdpv2.vers == 2) -assert(cdpv2.ttl == 180) -assert(cdpv2.cksum == 0xd7db) -assert(cdpv2.haslayer(CDPMsgDeviceID)) -assert(cdpv2.haslayer(CDPMsgAddr)) -assert(cdpv2.haslayer(CDPAddrRecordIPv4)) -assert(cdpv2.haslayer(CDPMsgPortID)) -assert(cdpv2.haslayer(CDPMsgCapabilities)) -assert(cdpv2.haslayer(CDPMsgSoftwareVersion)) -assert(cdpv2.haslayer(CDPMsgPlatform)) -assert(cdpv2.haslayer(CDPMsgVoIPVLANQuery)) -assert(cdpv2.haslayer(CDPMsgDuplex)) -assert(cdpv2.haslayer(CDPMsgPower)) -assert(cdpv2[CDPMsgVoIPVLANQuery].type == 0x000f) -assert(cdpv2[CDPMsgVoIPVLANQuery].len == 8) -assert(cdpv2[CDPMsgVoIPVLANQuery].unknown1 == 0x20) -assert(cdpv2[CDPMsgVoIPVLANQuery].vlan == 512) +assert cdpv2.vers == 2 +assert cdpv2.ttl == 180 +assert cdpv2.cksum == 0xd7db +assert cdpv2.haslayer(CDPMsgDeviceID) +assert cdpv2.haslayer(CDPMsgAddr) +assert cdpv2.haslayer(CDPAddrRecordIPv4) +assert cdpv2.haslayer(CDPMsgPortID) +assert cdpv2.haslayer(CDPMsgCapabilities) +assert cdpv2.haslayer(CDPMsgSoftwareVersion) +assert cdpv2.haslayer(CDPMsgPlatform) +assert cdpv2.haslayer(CDPMsgVoIPVLANQuery) +assert cdpv2.haslayer(CDPMsgDuplex) +assert cdpv2.haslayer(CDPMsgPower) +assert cdpv2[CDPMsgVoIPVLANQuery].type == 0x000f +assert cdpv2[CDPMsgVoIPVLANQuery].len == 8 +assert cdpv2[CDPMsgVoIPVLANQuery].unknown1 == 0x20 +assert cdpv2[CDPMsgVoIPVLANQuery].vlan == 512 assert cdpv2[CDPMsgPower].sprintf("%power%") == '6300 mW' @@ -81,3 +81,35 @@ assert CDPMsgPortID in p and CDPMsgIPPrefix in p pkt = CDPv2_HDR(vers=2, ttl=180, msg='123') assert len(pkt) == 7 + += CDPv2 - CDPMsgAddr Packet +cdp_msg_addr = CDPMsgAddr(addr=[CDPAddrRecordIPv4(), CDPAddrRecordIPv6()]) +assert cdp_msg_addr.haslayer(CDPAddrRecordIPv4) +assert cdp_msg_addr.haslayer(CDPAddrRecordIPv6) +assert len(cdp_msg_addr.addr) == 2 + +assert raw(cdp_msg_addr)[4:8] == b'\x00\x00\x00\x02' + += CDPv2 - CDPMsgPowerRequest and CDPMsgPowerAvailable Packet +s = b'\x02\xb4\x39\xfa\x00\x01\x00\x09\x53\x63\x61\x70\x79\x00\x02\x00\x11\x00\x00\x00\x01\x01\x01\xcc\x00\x04\x7f\x00\x00\x01\x00\x10\x00\x06\x00\x10\x00\x19\x00\x18\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x1a\x00\x14\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x07' +cdpv2 = CDPv2_HDR(s) +assert cdpv2.vers == 2 +assert cdpv2.ttl == 180 +assert cdpv2.cksum == 0x39fa +assert cdpv2.haslayer(CDPMsgDeviceID) +assert cdpv2.haslayer(CDPMsgAddr) +assert cdpv2.haslayer(CDPMsgPower) +assert cdpv2.haslayer(CDPMsgPowerRequest) +assert cdpv2.haslayer(CDPMsgPowerAvailable) +assert cdpv2[CDPMsgPowerRequest].type == 0x0019 +assert cdpv2[CDPMsgPowerRequest].len == 24 +assert cdpv2[CDPMsgPowerRequest].req_id == 0 +assert cdpv2[CDPMsgPowerRequest].mgmt_id == 0 +assert len(cdpv2[CDPMsgPowerRequest].power_requested_list) == 4 +assert cdpv2[CDPMsgPowerRequest].power_requested_list == [1, 2, 3, 4] +assert cdpv2[CDPMsgPowerAvailable].type == 0x001a +assert cdpv2[CDPMsgPowerAvailable].len == 20 +assert cdpv2[CDPMsgPowerAvailable].req_id == 0 +assert cdpv2[CDPMsgPowerAvailable].mgmt_id == 0 +assert len(cdpv2[CDPMsgPowerAvailable].power_available_list) == 3 +assert cdpv2[CDPMsgPowerAvailable].power_available_list == [5, 6, 7] diff --git a/test/contrib/coap.uts b/test/contrib/coap.uts index a777735d839..16dfc0f99b7 100644 --- a/test/contrib/coap.uts +++ b/test/contrib/coap.uts @@ -6,39 +6,40 @@ from scapy.contrib.coap import * + Test CoAP = CoAP default values -assert(raw(CoAP()) == b'\x40\x00\x00\x00') +assert raw(CoAP()) == b'\x40\x00\x00\x00' = Token length calculation p = CoAP(token='foobar') -assert(CoAP(raw(p)).tkl == 6) +assert CoAP(raw(p)).tkl == 6 = CON GET dissect p = CoAP(b'\x40\x01\xd9\xe1\xbb\x2e\x77\x65\x6c\x6c\x2d\x6b\x6e\x6f\x77\x6e\x04\x63\x6f\x72\x65') -assert(p.code == 1) -assert(p.ver == 1) -assert(p.tkl == 0) -assert(p.tkl == 0) -assert(p.msg_id == 55777) -assert(p.token == b'') -assert(p.type == 0) -assert(p.options == [('Uri-Path', b'.well-known'), ('Uri-Path', b'core')]) +assert p.code == 1 +assert p.ver == 1 +assert p.tkl == 0 +assert p.tkl == 0 +assert p.msg_id == 55777 +assert p.token == b'' +assert p.type == 0 +assert p.options == [('Uri-Path', b'.well-known'), ('Uri-Path', b'core')] = Extended option delta -assert(raw(CoAP(options=[("Uri-Query", "query")])) == b'\x40\x00\x00\x00\xd5\x02\x71\x75\x65\x72\x79') +assert raw(CoAP(options=[("Uri-Query", "query")])) == b'\x40\x00\x00\x00\xd5\x02\x71\x75\x65\x72\x79' = Extended option length -assert(raw(CoAP(options=[("Location-Path", 'x' * 280)])) == b'\x40\x00\x00\x00\x8e\x0b\x00' + b'\x78' * 280) +assert raw(CoAP(options=[("Location-Path", 'x' * 280)])) == b'\x40\x00\x00\x00\x8e\x00\x0b' + b'\x78' * 280 +assert len(CoAP(b'\x40\x00\x00\x00\x8e\x00\x0b' + b'\x78' * 280 + b'\xff').options[0][1]) == 280 = Options should be ordered by option number -assert(raw(CoAP(options=[("Uri-Query", "b"),("Uri-Path","a")])) == b'\x40\x00\x00\x00\xb1\x61\x41\x62') +assert raw(CoAP(options=[("Uri-Query", "b"),("Uri-Path","a")])) == b'\x40\x00\x00\x00\xb1\x61\x41\x62' = Options of the same type should not be reordered -assert(raw(CoAP(options=[("Uri-Path", "b"),("Uri-Path","a")])) == b'\x40\x00\x00\x00\xb1\x62\x01\x61') +assert raw(CoAP(options=[("Uri-Path", "b"),("Uri-Path","a")])) == b'\x40\x00\x00\x00\xb1\x62\x01\x61' + Test layer binding = Destination port p = UDP()/CoAP() -assert(p[UDP].dport == 5683) +assert p[UDP].dport == 5683 = Source port s = b'\x16\x33\xa0\xa4\x00\x78\xfe\x8b\x60\x45\xd9\xe1\xc1\x28\xff\x3c\x2f\x3e\x3b\x74\x69\x74\x6c\x65\x3d\x22\x47\x65' \ @@ -46,18 +47,18 @@ s = b'\x16\x33\xa0\xa4\x00\x78\xfe\x8b\x60\x45\xd9\xe1\xc1\x28\xff\x3c\x2f\x3e\x b'\x22\x63\x6c\x6f\x63\x6b\x22\x3b\x72\x74\x3d\x22\x54\x69\x63\x6b\x73\x22\x3b\x74\x69\x74\x6c\x65\x3d\x22\x49\x6e' \ b'\x74\x65\x72\x6e\x61\x6c\x20\x43\x6c\x6f\x63\x6b\x22\x3b\x63\x74\x3d\x30\x3b\x6f\x62\x73\x2c\x3c\x2f\x61\x73\x79' \ b'\x6e\x63\x3e\x3b\x63\x74\x3d\x30' -assert(CoAP in UDP(s)) +assert CoAP in UDP(s) = building with a text/plain payload p = CoAP(ver = 1, type = 0, code = 0x42, msg_id = 0xface, options=[("Content-Format", b"\x00")], paymark = b"\xff") p /= Raw(b"\xde\xad\xbe\xef") -assert(raw(p) == b'\x40\x42\xfa\xce\xc1\x00\xff\xde\xad\xbe\xef') +assert raw(p) == b'\x40\x42\xfa\xce\xc1\x00\xff\xde\xad\xbe\xef' = dissection with a text/plain payload p = CoAP(raw(p)) -assert(p.ver == 1) -assert(p.type == 0) -assert(p.code == 0x42) -assert(p.msg_id == 0xface) -assert(isinstance(p.payload, Raw)) -assert(p.payload.load == b'\xde\xad\xbe\xef') +assert p.ver == 1 +assert p.type == 0 +assert p.code == 0x42 +assert p.msg_id == 0xface +assert isinstance(p.payload, Raw) +assert p.payload.load == b'\xde\xad\xbe\xef' diff --git a/test/contrib/dce_rpc.uts b/test/contrib/dce_rpc.uts deleted file mode 100644 index 15ee5c1df3a..00000000000 --- a/test/contrib/dce_rpc.uts +++ /dev/null @@ -1,101 +0,0 @@ -% DCE/RPC layer test campaign - -+ Syntax check -= Import the DCE/RPC layer -import re -from scapy.contrib.dce_rpc import * -from uuid import UUID - - -+ Check EndiannessField - -= Little Endian IntField getfield -f = EndiannessField(IntField('f', 0), lambda p: '<') -f.getfield(None, hex_bytes('0102030405')) == (b'\x05', 0x04030201) - -= Little Endian IntField addfield -f = EndiannessField(IntField('f', 0), lambda p: '<') -f.addfield(None, b'\x01', 0x05040302) == hex_bytes('0102030405') - -= Big Endian IntField getfield -f = EndiannessField(IntField('f', 0), lambda p: '>') -f.getfield(None, hex_bytes('0102030405')) == (b'\x05', 0x01020304) - -= Big Endian IntField addfield -f = EndiannessField(IntField('f', 0), lambda p: '>') -f.addfield(None, b'\x01', 0x02030405) == hex_bytes('0102030405') - -= Little Endian StrField getfield -f = EndiannessField(StrField('f', 0), lambda p: '<') -f.getfield(None, '0102030405') == (b'', '0102030405') - -= Little Endian StrField addfield -f = EndiannessField(StrField('f', 0), lambda p: '<') -f.addfield(None, b'01', '02030405') == b'0102030405' - -= Big Endian StrField getfield -f = EndiannessField(StrField('f', 0), lambda p: '>') -f.getfield(None, '0102030405') == (b'', '0102030405') - -= Big Endian StrField addfield -f = EndiannessField(StrField('f', 0), lambda p: '>') -f.addfield(None, b'01', '02030405') == b'0102030405' - -= Little Endian UUIDField getfield -* The endianness of a UUIDField should be apply by block on each block in -* parenthesis '(01234567)-(89ab)-(cdef)-(01)(23)-(45)(67)(89)(ab)(cd)(ef)' - -f = EndiannessField(UUIDField('f', None), lambda p: '<') -f.getfield(None, hex_bytes('0123456789abcdef0123456789abcdef')) == (b'', UUID('67452301-ab89-efcd-0123-456789abcdef')) - -= Little Endian UUIDField addfield -f = EndiannessField(UUIDField('f', '01234567-89ab-cdef-0123-456789abcdef'), lambda p: '<') -f.addfield(None, b'', f.default) == hex_bytes('67452301ab89efcd0123456789abcdef') - -= Big Endian UUIDField getfield -f = EndiannessField(UUIDField('f', None), lambda p: '>') -f.getfield(None, hex_bytes('0123456789abcdef0123456789abcdef')) == (b'', UUID('01234567-89ab-cdef-0123456789abcdef')) - -= Big Endian UUIDField addfield -f = EndiannessField(UUIDField('f', '01234567-89ab-cdef-0123-456789abcdef'), lambda p: '>') -f.addfield(None, b'', f.default) == hex_bytes('0123456789abcdef0123456789abcdef') - - -+ Check DCE/RPC layer - -= DCE/RPC default values -bytes(DceRpc()) == hex_bytes('04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000ffffffff000000000000') - -= DCE/RPC payload length computation -bytes(DceRpc() / b'\x00\x01\x02\x03') == hex_bytes('04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000ffffffff00040000000000010203') - -= DCE/RPC Guess payload class fallback with no possible payload -p = DceRpc(hex_bytes('04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000ffffffff00040000000000010203')) -p.payload.__class__ == conf.raw_layer - -= DCE/RPC Guess payload class to a registered heuristic payload -* A payload to be valid must implement the method can_handle and be registered to DceRpcPayload -from scapy.contrib.dce_rpc import *; import binascii, re -class DummyPayload(Packet): - fields_desc = [StrField('load', '')] - @classmethod - def can_handle(cls, pkt, dce): - if pkt[0] in [b'\x01', 1]: # support for py3 bytearray - return True - else: - return False - -DceRpcPayload.register_possible_payload(DummyPayload) -p = DceRpc(hex_bytes('04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000ffffffff00040000000001020304')) -p.payload.__class__ == DummyPayload - -= DCE/RPC Guess payload class fallback with possible payload classes -p = DceRpc(hex_bytes('04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000ffffffff00040000000000010203')) -p.payload.__class__ == conf.raw_layer - -= DCE/RPC little-endian build -bytes(DceRpc(type='response', endianness='little', opnum=3) / b'\x00\x01\x02\x03') == hex_bytes('04020000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000300ffffffff04000000000000010203') - -= DCE/RPC little-endian dissection -p = DceRpc(hex_bytes('04020000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000300ffffffff04000000000000010203')) -p.type == 2 and p.opnum == 3 and p.frag_len == 4 diff --git a/test/contrib/diameter.uts b/test/contrib/diameter.uts index e06a055a16d..2f1ce7d6f0e 100644 --- a/test/contrib/diameter.uts +++ b/test/contrib/diameter.uts @@ -253,3 +253,33 @@ r3b = DiamReq ('Multimedia-Auth', drHbHId=0x5478, drEtEId=0x1234, raw(r3b) == raw(r3) + +####################################################################### ++ Diameter over SCTP +####################################################################### + += Diameter decoded from SCTPChunkData via proto_id binding + +from scapy.layers.sctp import SCTP, SCTPChunkData + +diam_pkt = DiamAns('Capabilities-Exchange', drHbHId=0x1234, drEtEId=0x5678, + avpList=[AVP('Origin-Host', val='host.example.com'), + AVP('Origin-Realm', val='example.com')]) + +pkt = SCTP(raw(SCTP() / SCTPChunkData(proto_id=46, beginning=1, ending=1, data=raw(diam_pkt)))) +chunk = pkt[SCTPChunkData] +assert isinstance(chunk.data, DiamG) +assert chunk.proto_id == 46 +assert chunk.data.drHbHId == 0x1234 +assert chunk.data.avpList[0].avpCode == 264 + += SCTPChunkData with unknown proto_id keeps raw bytes + +pkt = SCTP(raw(SCTP() / SCTPChunkData(proto_id=0, data=b"test"))) +assert raw(pkt[SCTPChunkData].data) == b"test" + += SCTPChunkData fragment is not decoded + +pkt = SCTP(raw(SCTP() / SCTPChunkData(proto_id=46, beginning=1, ending=0, data=raw(diam_pkt)))) +assert not isinstance(pkt[SCTPChunkData].data, DiamG) + diff --git a/test/contrib/dicom.uts b/test/contrib/dicom.uts new file mode 100644 index 00000000000..b9490fda332 --- /dev/null +++ b/test/contrib/dicom.uts @@ -0,0 +1,576 @@ +% DICOM (Digital Imaging and Communications in Medicine) tests + +# Type the following command to launch the tests: +# $ test/run_tests -P "load_contrib('dicom')" -t test/contrib/dicom.uts + +############ +############ ++ DICOM module loading + += Import DICOM module +load_contrib("dicom", globals_dict=globals()) + += Verify essential classes are exported +assert DICOM is not None +assert A_ASSOCIATE_RQ is not None +assert A_ASSOCIATE_AC is not None +assert A_ASSOCIATE_RJ is not None +assert P_DATA_TF is not None +assert A_RELEASE_RQ is not None +assert A_RELEASE_RP is not None +assert A_ABORT is not None +assert DICOMVariableItem is not None +assert DICOMApplicationContext is not None +assert DICOMPresentationContextRQ is not None +assert DICOMUserInformation is not None +assert DICOMMaximumLength is not None + += Verify DIMSE packet classes are exported +assert C_ECHO_RQ is not None +assert C_ECHO_RSP is not None +assert C_STORE_RQ is not None +assert C_STORE_RSP is not None +assert C_FIND_RQ is not None +assert C_FIND_RSP is not None +assert C_MOVE_RQ is not None +assert C_MOVE_RSP is not None +assert C_GET_RQ is not None +assert C_GET_RSP is not None + += Verify constants are exported +assert DICOM_PORT == 104 +assert APP_CONTEXT_UID == "1.2.840.10008.3.1.1.1" +assert DEFAULT_TRANSFER_SYNTAX_UID == "1.2.840.10008.1.2" +assert VERIFICATION_SOP_CLASS_UID == "1.2.840.10008.1.1" + += Verify Query/Retrieve SOP Class UIDs are exported +assert PATIENT_ROOT_QR_FIND_SOP_CLASS_UID == "1.2.840.10008.5.1.4.1.2.1.1" +assert PATIENT_ROOT_QR_MOVE_SOP_CLASS_UID == "1.2.840.10008.5.1.4.1.2.1.2" +assert PATIENT_ROOT_QR_GET_SOP_CLASS_UID == "1.2.840.10008.5.1.4.1.2.1.3" +assert STUDY_ROOT_QR_FIND_SOP_CLASS_UID == "1.2.840.10008.5.1.4.1.2.2.1" +assert STUDY_ROOT_QR_MOVE_SOP_CLASS_UID == "1.2.840.10008.5.1.4.1.2.2.2" +assert STUDY_ROOT_QR_GET_SOP_CLASS_UID == "1.2.840.10008.5.1.4.1.2.2.3" + +############ +############ ++ PDU header tests + += DICOM PDU header construction +pkt = DICOM() +assert pkt.pdu_type == 0x01 +assert pkt.reserved1 == 0 + += DICOM PDU type field values +import struct +for pdu_type, expected_class in [(0x01, A_ASSOCIATE_RQ), (0x02, A_ASSOCIATE_AC), + (0x03, A_ASSOCIATE_RJ), (0x04, P_DATA_TF), + (0x05, A_RELEASE_RQ), (0x06, A_RELEASE_RP), + (0x07, A_ABORT)]: + pkt = DICOM() / expected_class() + raw_bytes = bytes(pkt) + assert raw_bytes[0] == pdu_type + +############ +############ ++ LenField auto-calculation tests + += DICOM header auto-calculates payload length +pkt = DICOM() / A_RELEASE_RQ() +raw = bytes(pkt) +length_field = struct.unpack("!I", raw[2:6])[0] +payload_size = len(raw) - 6 +assert length_field == payload_size +assert length_field == 4 + += Variable item length auto-calculated +pkt = DICOMVariableItem() / DICOMApplicationContext() +raw = bytes(pkt) +length_field = struct.unpack("!H", raw[2:4])[0] +payload_size = len(raw) - 4 +assert length_field == payload_size + += Nested items have correct cumulative length +max_len = DICOMVariableItem() / DICOMMaximumLength(max_pdu_length=16384) +user_info = DICOMVariableItem() / DICOMUserInformation(sub_items=[max_len]) +raw = bytes(user_info) +assert len(raw) == 12 +ui_length = struct.unpack("!H", raw[2:4])[0] +assert ui_length == 8 + +############ +############ ++ Variable item bind_layers tests + += Application Context bind_layers (type 0x10) +pkt = DICOMVariableItem() / DICOMApplicationContext() +assert pkt.item_type == 0x10 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0x10 +assert parsed.haslayer(DICOMApplicationContext) + += Abstract Syntax bind_layers (type 0x30) +uid = _uid_to_bytes(VERIFICATION_SOP_CLASS_UID) +pkt = DICOMVariableItem() / DICOMAbstractSyntax(uid=uid) +assert pkt.item_type == 0x30 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0x30 +assert parsed.haslayer(DICOMAbstractSyntax) +assert parsed[DICOMAbstractSyntax].uid == uid + += Transfer Syntax bind_layers (type 0x40) +pkt = DICOMVariableItem() / DICOMTransferSyntax() +assert pkt.item_type == 0x40 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0x40 +assert parsed.haslayer(DICOMTransferSyntax) + += Maximum Length bind_layers (type 0x51) +pkt = DICOMVariableItem() / DICOMMaximumLength(max_pdu_length=32768) +assert pkt.item_type == 0x51 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0x51 +assert parsed.haslayer(DICOMMaximumLength) +assert parsed[DICOMMaximumLength].max_pdu_length == 32768 + += User Information bind_layers (type 0x50) +max_len = DICOMVariableItem() / DICOMMaximumLength(max_pdu_length=16384) +pkt = DICOMVariableItem() / DICOMUserInformation(sub_items=[max_len]) +assert pkt.item_type == 0x50 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0x50 +assert parsed.haslayer(DICOMUserInformation) + += Presentation Context RQ bind_layers (type 0x20) +abs_syn = DICOMVariableItem() / DICOMAbstractSyntax(uid=_uid_to_bytes(VERIFICATION_SOP_CLASS_UID)) +ts = DICOMVariableItem() / DICOMTransferSyntax() +pkt = DICOMVariableItem() / DICOMPresentationContextRQ(context_id=1, sub_items=[abs_syn, ts]) +assert pkt.item_type == 0x20 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0x20 +assert parsed.haslayer(DICOMPresentationContextRQ) +assert parsed[DICOMPresentationContextRQ].context_id == 1 + += Presentation Context AC bind_layers (type 0x21) +ts = DICOMVariableItem() / DICOMTransferSyntax() +pkt = DICOMVariableItem() / DICOMPresentationContextAC(context_id=1, result=0, sub_items=[ts]) +assert pkt.item_type == 0x21 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0x21 +assert parsed.haslayer(DICOMPresentationContextAC) +assert parsed[DICOMPresentationContextAC].result == 0 + += Unknown item type uses guess_payload_class fallback +raw = struct.pack("!BBH", 0xFF, 0, 4) + b"test" +parsed = DICOMVariableItem(raw) +assert parsed.item_type == 0xFF +assert parsed.length == 4 +assert parsed.payload is not None + +############ +############ ++ A-ASSOCIATE-RQ tests + += Build simple A-ASSOCIATE-RQ +app_ctx = DICOMVariableItem() / DICOMApplicationContext() +pctx = build_presentation_context_rq(1, VERIFICATION_SOP_CLASS_UID, [DEFAULT_TRANSFER_SYNTAX_UID]) +user_info = build_user_information(max_pdu_length=16384) +assoc_rq = DICOM() / A_ASSOCIATE_RQ( + called_ae_title=b"TARGET", + calling_ae_title=b"SOURCE", + variable_items=[app_ctx, pctx, user_info] +) +raw = bytes(assoc_rq) +parsed = DICOM(raw) +assert parsed.haslayer(A_ASSOCIATE_RQ) +items = parsed[A_ASSOCIATE_RQ].variable_items +assert len(items) == 3 +assert items[0].item_type == 0x10 +assert items[1].item_type == 0x20 +assert items[2].item_type == 0x50 + += A-ASSOCIATE-RQ AE titles are space-padded by DICOMAETitleField +original = DICOM() / A_ASSOCIATE_RQ( + called_ae_title=b"TARGET", + calling_ae_title=b"SOURCE", +) +serialized = bytes(original) +parsed = DICOM(serialized) +assert parsed.haslayer(A_ASSOCIATE_RQ) +assert parsed[A_ASSOCIATE_RQ].called_ae_title == b"TARGET " +assert parsed[A_ASSOCIATE_RQ].calling_ae_title == b"SOURCE " + += A-ASSOCIATE-RQ with multiple presentation contexts +app_ctx = DICOMVariableItem() / DICOMApplicationContext() +pctx1 = build_presentation_context_rq(1, VERIFICATION_SOP_CLASS_UID, [DEFAULT_TRANSFER_SYNTAX_UID]) +pctx2 = build_presentation_context_rq(3, CT_IMAGE_STORAGE_SOP_CLASS_UID, [DEFAULT_TRANSFER_SYNTAX_UID]) +user_info = build_user_information() +assoc_rq = DICOM() / A_ASSOCIATE_RQ( + called_ae_title=b"TARGET", + calling_ae_title=b"SOURCE", + variable_items=[app_ctx, pctx1, pctx2, user_info] +) +raw = bytes(assoc_rq) +parsed = DICOM(raw) +items = parsed[A_ASSOCIATE_RQ].variable_items +assert len(items) == 4 +pctx_items = [i for i in items if i.item_type == 0x20] +assert len(pctx_items) == 2 +assert pctx_items[0][DICOMPresentationContextRQ].context_id == 1 +assert pctx_items[1][DICOMPresentationContextRQ].context_id == 3 + +############ +############ ++ A-ASSOCIATE-RJ tests + += A-ASSOCIATE-RJ construction and parsing +pkt = DICOM() / A_ASSOCIATE_RJ(result=1, source=2, reason_diag=2) +reparsed = DICOM(bytes(pkt)) +assert reparsed.haslayer(A_ASSOCIATE_RJ) +assert reparsed[A_ASSOCIATE_RJ].source == 2 + +############ +############ ++ A-RELEASE tests + += A-RELEASE-RQ round-trip +original = DICOM() / A_RELEASE_RQ() +serialized = bytes(original) +parsed = DICOM(serialized) +assert parsed.haslayer(A_RELEASE_RQ) + += A-RELEASE-RP round-trip +original = DICOM() / A_RELEASE_RP() +serialized = bytes(original) +parsed = DICOM(serialized) +assert parsed.haslayer(A_RELEASE_RP) + +############ +############ ++ A-ABORT tests + += A-ABORT round-trip +original = DICOM() / A_ABORT(source=2, reason_diag=6) +serialized = bytes(original) +parsed = DICOM(serialized) +assert parsed.haslayer(A_ABORT) +assert parsed[A_ABORT].source == 2 +assert parsed[A_ABORT].reason_diag == 6 + +############ +############ ++ P-DATA-TF tests + += PresentationDataValueItem length linked to data +test_data = b"TEST_DATA_12345" +pdv = PresentationDataValueItem(context_id=1, data=test_data, is_command=1, is_last=1) +raw = bytes(pdv) +length = struct.unpack("!I", raw[:4])[0] +assert length == len(test_data) + 2 + += P-DATA-TF with multiple PDV items - build only +pdv1 = PresentationDataValueItem(context_id=1, data=b'\xDE\xAD', is_command=1, is_last=0) +pdv2 = PresentationDataValueItem(context_id=1, data=b'\xBE\xEF', is_command=0, is_last=1) +pkt = DICOM() / P_DATA_TF(pdv_items=[pdv1, pdv2]) +raw = bytes(pkt) +assert raw[0] == 0x04 +assert len(raw) > 6 + += P-DATA-TF round-trip - build and verify structure +test_data = b"\x01\x02\x03\x04\x05" +pdv = PresentationDataValueItem(context_id=3, data=test_data, is_command=1, is_last=1) +original = DICOM() / P_DATA_TF(pdv_items=[pdv]) +serialized = bytes(original) +assert serialized[0] == 0x04 +length = struct.unpack("!I", serialized[2:6])[0] +assert length > 0 +assert test_data in serialized + += PDV is_command flag encoding +pdv = PresentationDataValueItem(context_id=1, data=b'x', is_command=1, is_last=0) +raw = bytes(pdv) +msg_ctrl = raw[5] +assert msg_ctrl & 0x01 == 1 +assert msg_ctrl & 0x02 == 0 + += PDV is_last flag encoding +pdv = PresentationDataValueItem(context_id=1, data=b'x', is_command=0, is_last=1) +raw = bytes(pdv) +msg_ctrl = raw[5] +assert msg_ctrl & 0x01 == 0 +assert msg_ctrl & 0x02 == 2 + += PDV both flags set +pdv = PresentationDataValueItem(context_id=1, data=b'x', is_command=1, is_last=1) +raw = bytes(pdv) +msg_ctrl = raw[5] +assert msg_ctrl == 0x03 + += PDV flags encoding verification +for is_cmd in [0, 1]: + for is_last in [0, 1]: + pdv = PresentationDataValueItem(context_id=1, data=b'test', is_command=is_cmd, is_last=is_last) + raw = bytes(pdv) + msg_ctrl = raw[5] + assert (msg_ctrl & 0x01) == is_cmd + assert (msg_ctrl & 0x02) == (is_last << 1) + +############ +############ ++ DIMSE packet tests + += C_ECHO_RQ creation with defaults +pkt = C_ECHO_RQ() +raw = bytes(pkt) +assert raw[:4] == b'\x00\x00\x00\x00' +assert b'1.2.840.10008.1.1' in raw + += C_ECHO_RQ custom message_id +pkt = C_ECHO_RQ(message_id=12345) +raw = bytes(pkt) +assert b'\x10\x01' in raw +assert struct.pack("= 1 +assert sub_items[0].item_type == 0x51 +assert sub_items[0][DICOMMaximumLength].max_pdu_length == 32768 + += build_user_information with implementation info +user_info = build_user_information( + max_pdu_length=16384, + implementation_class_uid="1.2.3.4.5", + implementation_version="SCAPY_V1" +) +sub_items = user_info[DICOMUserInformation].sub_items +assert len(sub_items) == 3 +types = [item.item_type for item in sub_items] +assert 0x51 in types +assert 0x52 in types +assert 0x55 in types + += _uid_to_bytes pads odd-length UIDs +assert len(_uid_to_bytes("1.2.3")) % 2 == 0 +assert _uid_to_bytes("1.2.3.4") == b"1.2.3.4\x00" +assert _uid_to_bytes("1.2.3") == b"1.2.3\x00" + +############ +############ ++ User Identity Negotiation tests + += User Identity username only (type 1) +pkt = DICOMVariableItem() / DICOMUserIdentity( + user_identity_type=1, + positive_response_requested=0, + primary_field=b"admin" +) +assert pkt.item_type == 0x58 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.haslayer(DICOMUserIdentity) +assert parsed[DICOMUserIdentity].user_identity_type == 1 +assert parsed[DICOMUserIdentity].primary_field == b"admin" + += User Identity username+password (type 2) +pkt = DICOMVariableItem() / DICOMUserIdentity( + user_identity_type=2, + positive_response_requested=1, + primary_field=b"admin", + secondary_field=b"password123" +) +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.haslayer(DICOMUserIdentity) +assert parsed[DICOMUserIdentity].user_identity_type == 2 +assert parsed[DICOMUserIdentity].primary_field == b"admin" +assert parsed[DICOMUserIdentity].secondary_field == b"password123" + += User Identity Response +pkt = DICOMVariableItem() / DICOMUserIdentityResponse( + server_response=b"auth_token_12345" +) +assert pkt.item_type == 0x59 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.haslayer(DICOMUserIdentityResponse) +assert parsed[DICOMUserIdentityResponse].server_response == b"auth_token_12345" + +############ +############ ++ Async Operations Window tests + += Async operations default values +pkt = DICOMVariableItem() / DICOMAsyncOperationsWindow() +assert pkt.item_type == 0x53 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.haslayer(DICOMAsyncOperationsWindow) +assert parsed[DICOMAsyncOperationsWindow].max_ops_invoked == 1 +assert parsed[DICOMAsyncOperationsWindow].max_ops_performed == 1 + += Async operations custom values +pkt = DICOMVariableItem() / DICOMAsyncOperationsWindow( + max_ops_invoked=8, + max_ops_performed=4 +) +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed[DICOMAsyncOperationsWindow].max_ops_invoked == 8 +assert parsed[DICOMAsyncOperationsWindow].max_ops_performed == 4 + +############ +############ ++ SCP/SCU Role Selection tests + += Role Selection SCU only +pkt = DICOMVariableItem() / DICOMSCPSCURoleSelection( + sop_class_uid=b"1.2.840.10008.5.1.4.1.1.2", + scu_role=1, + scp_role=0 +) +assert pkt.item_type == 0x54 +raw = bytes(pkt) +parsed = DICOMVariableItem(raw) +assert parsed.haslayer(DICOMSCPSCURoleSelection) +assert parsed[DICOMSCPSCURoleSelection].sop_class_uid == b"1.2.840.10008.5.1.4.1.1.2" +assert parsed[DICOMSCPSCURoleSelection].scu_role == 1 +assert parsed[DICOMSCPSCURoleSelection].scp_role == 0 + +############ +############ ++ DICOM extract_padding for StreamSocket + += DICOM extract_padding method exists +pkt = DICOM() +assert hasattr(pkt, 'extract_padding') + += DICOM extract_padding separates payload from trailing data +pkt = DICOM() +pkt.length = 10 +payload, remaining = pkt.extract_padding(b'0123456789EXTRA') +assert payload == b'0123456789' +assert remaining == b'EXTRA' + +############ +############ ++ TCP layer binding + += DICOM binds to TCP port 104 +from scapy.layers.inet import TCP +pkt = TCP(dport=104) / b'\x01\x00\x00\x00\x00\x04' +assert DICOM in pkt or pkt.payload + +############ +############ ++ Edge cases + += Empty variable items list +pkt = DICOM() / A_ASSOCIATE_RQ(variable_items=[]) +serialized = bytes(pkt) +parsed = DICOM(serialized) +assert parsed.haslayer(A_ASSOCIATE_RQ) + += Empty PDV items list +pkt = DICOM() / P_DATA_TF(pdv_items=[]) +serialized = bytes(pkt) +assert len(serialized) == 6 diff --git a/test/contrib/dtp.uts b/test/contrib/dtp.uts index c8880f114cf..205c4864438 100644 --- a/test/contrib/dtp.uts +++ b/test/contrib/dtp.uts @@ -16,7 +16,7 @@ assert pkt[DTP].tlvlist[3].status == b'\x03' = Test negotiate_trunk -import mock +from unittest import mock def test_pkt(pkt): pkt = Ether(raw(pkt)) diff --git a/test/contrib/eddystone.uts b/test/contrib/eddystone.uts index ab8c5ed2d58..8f80276a8ed 100644 --- a/test/contrib/eddystone.uts +++ b/test/contrib/eddystone.uts @@ -45,7 +45,7 @@ assert d == hex_bytes('1000006578616d706c650068656c6c6f0b2e68746d6c') = Eddystone URL (encode unsupported scheme) -assert(expect_exception(Exception, lambda: Eddystone_URL.from_url('gopher://example.com'))) +assert expect_exception(Exception, lambda: Eddystone_URL.from_url('gopher://example.com')) = Eddystone URL (encode advertising report) diff --git a/test/contrib/eigrp.uts b/test/contrib/eigrp.uts index cee8a23003a..70e5ca48be7 100644 --- a/test/contrib/eigrp.uts +++ b/test/contrib/eigrp.uts @@ -85,37 +85,37 @@ assert inet_pton(socket.AF_INET6, f.randval()) = EIGRPGuessPayloadClass function: Return Parameters TLV from scapy.contrib.eigrp import _EIGRPGuessPayloadClass -isinstance(_EIGRPGuessPayloadClass(b"\x00\x01"), EIGRPParam) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x01" + b"\x00" * 50), EIGRPParam) = EIGRPGuessPayloadClass function: Return Authentication Data TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x02"), EIGRPAuthData) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x02" + b"\x00" * 50), EIGRPAuthData) = EIGRPGuessPayloadClass function: Return Sequence TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x03"), EIGRPSeq) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x03" + b"\x00" * 50), EIGRPSeq) = EIGRPGuessPayloadClass function: Return Software Version TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x04"), EIGRPSwVer) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x04" + b"\x00" * 50), EIGRPSwVer) = EIGRPGuessPayloadClass function: Return Next Multicast Sequence TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x05"), EIGRPNms) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x05" + b"\x00" * 50), EIGRPNms) = EIGRPGuessPayloadClass function: Return Stub Router TLV -isinstance(_EIGRPGuessPayloadClass(b"\x00\x06"), EIGRPStub) +isinstance(_EIGRPGuessPayloadClass(b"\x00\x06" + b"\x00" * 50), EIGRPStub) = EIGRPGuessPayloadClass function: Return Internal Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x01\x02"), EIGRPIntRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x01\x02" + b"\x00" * 50), EIGRPIntRoute) = EIGRPGuessPayloadClass function: Return External Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x01\x03"), EIGRPExtRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x01\x03" + b"\x00" * 50), EIGRPExtRoute) = EIGRPGuessPayloadClass function: Return IPv6 Internal Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x04\x02"), EIGRPv6IntRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x04\x02" + b"\x00" * 50), EIGRPv6IntRoute) = EIGRPGuessPayloadClass function: Return IPv6 External Route TLV -isinstance(_EIGRPGuessPayloadClass(b"\x04\x03"), EIGRPv6ExtRoute) +isinstance(_EIGRPGuessPayloadClass(b"\x04\x03" + b"\x00" * 100), EIGRPv6ExtRoute) = EIGRPGuessPayloadClass function: Return EIGRPGeneric -isinstance(_EIGRPGuessPayloadClass(b"\x23\x42"), EIGRPGeneric) +isinstance(_EIGRPGuessPayloadClass(b"\x23\x42" + b"\x00" * 50), EIGRPGeneric) + TLV List @@ -160,7 +160,7 @@ p = IP()/EIGRP(tlvlist=[EIGRPv6ExtRoute(prefixlen=99, dst="2000::")]) struct.unpack("!H", p[EIGRPv6ExtRoute].build()[2:4])[0] == 70 + Stub Flags -* The receive-only flag is always set, when a router anounces itself as stub router. +* The receive-only flag is always set, when a router announces itself as stub router. = Receive-Only p = IP()/EIGRP(tlvlist=[EIGRPStub(flags="receive-only")]) diff --git a/test/contrib/enipTCP.uts b/test/contrib/enipTCP.uts index d6e85ed2943..d2d820c669e 100644 --- a/test/contrib/enipTCP.uts +++ b/test/contrib/enipTCP.uts @@ -6,188 +6,186 @@ from scapy.contrib.enipTCP import * #from scapy.all import * + + Test ENIP/TCP Encapsulation Header = Encapsulation Header Default Values pkt=ENIPTCP() -assert(pkt.commandId == None) -assert(pkt.length == 0) -assert(pkt.session == 0) -assert(pkt.status == None) -assert(pkt.senderContext == 0) -assert(pkt.options == 0) +assert pkt.commandId == None +assert pkt.length == 0 +assert pkt.session == 0 +assert pkt.status == None +assert pkt.senderContext == 0 +assert pkt.options == 0 -+ ENIP List Services ++ ENIP List Services 0x0004 = ENIP List Services Reply Command ID pkt=ENIPTCP() pkt.commandId=0x4 -assert(pkt.commandId == 0x4) +assert pkt.commandId == 0x4 -= ENIP List Services Reply Default Values -pkt=pkt/ENIPListServicesReply() -assert(pkt[ENIPListServicesReply].itemCount == 0) += ENIP List Services Default Values +pkt=ENIPListServices() +assert pkt.itemCount == 0 -= ENIP List Services Reply Items Default Values -pkt=pkt/ENIPListServicesReplyItems() -assert(pkt[ENIPListServicesReplyItems].itemTypeCode == 0) -assert(pkt[ENIPListServicesReplyItems].itemLength == 0) -assert(pkt[ENIPListServicesReplyItems].version == 1) -assert(pkt[ENIPListServicesReplyItems].flag == 0) -assert(pkt[ENIPListServicesReplyItems].serviceName == None) += ENIP List Services Custom Values +pkt.items.append(ENIPListServicesItem(serviceName=b'test')) +assert pkt.items[0].itemTypeCode == 0 +assert pkt.items[0].itemLength == 0 +assert pkt.items[0].protocolVersion == 0 +assert pkt.items[0].flag == 0 +assert pkt.items[0].serviceName == b'test' -+ ENIP List Identity ++ ENIP List Identity 0x0063 = ENIP List Identity Reply Command ID pkt=ENIPTCP() pkt.commandId=0x63 -assert(pkt.commandId == 0x63) +assert pkt.commandId == 0x63 +assert raw(pkt) == b"c\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ +b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" -= ENIP List Identity Reply Default Values -pkt=pkt/ENIPListIdentityReply() -assert(pkt[ENIPListIdentityReply].itemCount == 0) += ENIP List Identity Default Values +pkt=ENIPListIdentity() +assert pkt.itemCount == 0 -= ENIP List Identity Reply Items Default Values -pkt=pkt/ENIPListIdentityReplyItems() -assert(pkt[ENIPListIdentityReplyItems].itemTypeCode == 0) -assert(pkt[ENIPListIdentityReplyItems].itemLength == 0) -assert(pkt[ENIPListIdentityReplyItems].itemData == b'') += ENIP List Identity Custom Values +pkt=ENIPListIdentityItem(sinAddress="192.168.1.1", + productNameLength=4, productName=b"test") +assert pkt.protocolVersion == 0 +assert pkt.sinAddress == "192.168.1.1" +assert pkt.productNameLength == 4 +assert pkt.productName == b'test' + ENIP List Interfaces = ENIP List Interfaces Reply Command ID pkt=ENIPTCP() pkt.commandId=0x64 -assert(pkt.commandId == 0x64) +assert pkt.commandId == 0x64 = ENIP List Interfaces Reply Default Values -pkt=pkt/ENIPListInterfacesReply() -assert(pkt[ENIPListInterfacesReply].itemCount == 0) +pkt=ENIPListInterfaces() +assert pkt.itemCount == 0 = ENIP List Interfaces Reply Items Default Values -pkt=pkt/ENIPListInterfacesReplyItems() -assert(pkt[ENIPListInterfacesReplyItems].itemTypeCode == 0) -assert(pkt[ENIPListInterfacesReplyItems].itemLength == 0) -assert(pkt[ENIPListInterfacesReplyItems].itemData == b'') +pkt=ENIPListInterfacesItem(itemTypeCode=0x0c) +assert pkt.itemTypeCode == 0x0c +assert pkt.itemLength == 0 +assert pkt.itemData == b'' + + ENIP Register Session = ENIP Register Session Command ID pkt=ENIPTCP() pkt.commandId=0x65 -assert(pkt.commandId == 0x65) +assert pkt.commandId == 0x65 = ENIP Register Session Default Values -pkt=pkt/ENIPRegisterSession() -assert(pkt[ENIPRegisterSession].protocolVersion == 1) -assert(pkt[ENIPRegisterSession].options == 0) +pkt=ENIPRegisterSession() +assert pkt.protocolVersion == 1 +assert pkt.options == 0 = ENIP Register Session Request registerSessionReqPkt = b'\x65\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' - pkt = ENIPTCP(registerSessionReqPkt) -assert(pkt.commandId == 0x65) -assert(pkt.length == 4) -assert(pkt.session == 0) -assert(pkt.status == 0) -assert(pkt.senderContext == 0) -assert(pkt.options == 0) -assert(pkt[ENIPRegisterSession].protocolVersion == 1) -assert(pkt[ENIPRegisterSession].options == 0) +assert pkt.commandId == 0x65 +assert pkt.length == 4 +assert pkt.session == 0 +assert pkt.status == 0 +assert pkt.senderContext == 0 +assert pkt.options == 0 +assert pkt[ENIPRegisterSession].protocolVersion == 1 +assert pkt[ENIPRegisterSession].options == 0 = ENIP Register Session Reply registerSessionRepPkt = b'\x65\x00\x04\x00\x7b\x9a\x4e\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' pkt = ENIPTCP(registerSessionRepPkt) -assert(pkt.commandId == 0x65) -assert(pkt.length == 4) -assert(pkt.session == 0xa14e9a7b) -assert(pkt.status == 0) -assert(pkt.senderContext == 0) -assert(pkt.options == 0) -assert(pkt[ENIPRegisterSession].protocolVersion == 1) -assert(pkt[ENIPRegisterSession].options == 0) +assert pkt.commandId == 0x65 +assert pkt.length == 4 +assert pkt.session == 0xa14e9a7b +assert pkt.status == 0 +assert pkt.senderContext == 0 +assert pkt.options == 0 +assert pkt[ENIPRegisterSession].protocolVersion == 1 +assert pkt[ENIPRegisterSession].options == 0 +raw(pkt) + ENIP Send RR Data = ENIP Send RR Data Command ID pkt=ENIPTCP() pkt.commandId=0x6f -assert(pkt.commandId == 0x6f) +assert pkt.commandId == 0x6f = ENIP Send RR Data Default Values -pkt=pkt/ENIPSendRRData() -assert(pkt[ENIPSendRRData].interfaceHandle == 0) -assert(pkt[ENIPSendRRData].timeout == 0) -assert(pkt[ENIPSendRRData].encapsulatedPacket == None) +pkt=ENIPSendRRData() +assert pkt.interface == 0 +assert pkt.timeout == 255 +assert pkt.itemCount == 0 = ENIP Send RR Data Request sendRRDataReqPkt = b'\x6f\x00\x3e\x00\x7b\x9a\x4e\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x2e\x00' pkt = ENIPTCP(sendRRDataReqPkt) -assert(pkt.commandId == 0x6f) -assert(pkt.length == 62) -assert(pkt.session == 0xa14e9a7b) -assert(pkt.status == 0) -assert(pkt.senderContext == 0) -assert(pkt.options == 0) -assert(pkt[ENIPSendRRData].interfaceHandle == 0) -assert(pkt[ENIPSendRRData].timeout == 0) -assert(pkt[EncapsulatedPacket].itemCount == 2) - +assert pkt.commandId == 0x6f +assert pkt.length == 62 +assert pkt.session == 0xa14e9a7b +assert pkt.status == 0 +assert pkt.senderContext == 0 +assert pkt.options == 0 +assert pkt.interface == 0 +assert pkt.timeout == 0 +assert pkt.itemCount == 2 = ENIP Send RR Data Reply sendRRDataRepPkt = b'\x6f\x00\x2e\x00\x7b\x9a\x4e\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00\x00\x00\x00\xb2\x00\x1e\x00' pkt = ENIPTCP(sendRRDataRepPkt) -assert(pkt.commandId == 0x6f) -assert(pkt.length == 46) -assert(pkt.session == 0xa14e9a7b) -assert(pkt.status == 0) -assert(pkt.senderContext == 0) -assert(pkt.options == 0) -assert(pkt[ENIPSendRRData].interfaceHandle == 0) -assert(pkt[ENIPSendRRData].timeout == 1024) -assert(pkt[EncapsulatedPacket].item[0].typeId == 0) -assert(pkt[EncapsulatedPacket].item[0].length == 0) -assert(pkt[EncapsulatedPacket].item[1].typeId == 0x00b2) -assert(pkt[EncapsulatedPacket].item[1].length == 30) +assert pkt.commandId == 0x6f +assert pkt.length == 46 +assert pkt.session == 0xa14e9a7b +assert pkt.status == 0 +assert pkt.senderContext == 0 +assert pkt.options == 0 +assert pkt.interface == 0 +assert pkt.timeout == 1024 +assert pkt.items[0].typeId == 0 +assert pkt.items[0].length == 0 +assert pkt.items[1].typeId == 0x00b2 +assert pkt.items[1].length == 30 + + ENIP Send Unit Data = ENIP Send Unit Data Command ID pkt=ENIPTCP() pkt.commandId=0x70 -assert(pkt.commandId == 0x70) +assert pkt.commandId == 0x70 = ENIP Send Unit Data Default Values -pkt=pkt/ENIPSendUnitData() -assert(pkt[ENIPSendUnitData].interfaceHandle == 0) -assert(pkt[ENIPSendUnitData].timeout == 0) -assert(pkt[ENIPSendUnitData].encapsulatedPacket == None) - +pkt=ENIPSendUnitData() +assert pkt.interface == 0 +assert pkt.timeout == 255 +assert pkt.itemCount == 0 = ENIP Send Unit Data sendUnitDataPkt = b'\x70\x00\x2d\x00\x7b\x9a\x4e\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\xcc\x60\x9a\x7b\xb1\x00\x19\x00\x01\x00' - pkt = ENIPTCP(sendUnitDataPkt) -assert(pkt.commandId == 0x70) -assert(pkt.length == 45) -assert(pkt.session == 0xa14e9a7b) -assert(pkt.status == 0) -assert(pkt.senderContext == 0) -assert(pkt.options == 0) -assert(pkt[ENIPSendUnitData].interfaceHandle == 0) -assert(pkt[ENIPSendUnitData].timeout == 0) -assert(pkt[EncapsulatedPacket].itemCount == 2) - -assert(pkt[EncapsulatedPacket].item[0].typeId == 0x00a1) -assert(pkt[EncapsulatedPacket].item[0].length == 4) -assert(pkt[EncapsulatedPacket].item[0].data == b'\x7b\x9a\x60\xcc') -assert(pkt[EncapsulatedPacket].item[1].typeId == 0x00b1) -assert(pkt[EncapsulatedPacket].item[1].length == 25) -assert(pkt[EncapsulatedPacket].item[1].data == b'\x00\x01') - - - - - - +assert pkt.commandId == 0x70 +assert pkt.length == 45 +assert pkt.session == 0xa14e9a7b +assert pkt.status == 0 +assert pkt.senderContext == 0 +assert pkt.options == 0 +assert pkt.interface == 0 +assert pkt.timeout == 0 +assert pkt.itemCount == 2 + +assert pkt.items[0].typeId == 0x00a1 +assert pkt.items[0].length == 4 +assert pkt.items[0].data == b'\x7b\x9a\x60\xcc' +assert pkt.items[1].typeId == 0x00b1 +assert pkt.items[1].length == 25 +assert pkt.items[1].data == b'\x00\x01' diff --git a/test/contrib/erspan.uts b/test/contrib/erspan.uts new file mode 100644 index 00000000000..e4465b382f1 --- /dev/null +++ b/test/contrib/erspan.uts @@ -0,0 +1,44 @@ +% ERSPAN + ++ ERSPAN I += Build & dissect ERSPAN 1 + +pkt = GRE()/ERSPAN_I()/Ether() +pkt = GRE(bytes(pkt)) +assert ERSPAN in pkt +assert pkt.proto == 0x88be +assert pkt.seqnum_present == 0 + ++ ERSPAN II += Build ERSPAN II + +pkt = GRE()/ERSPAN_II()/Ether(src="11:11:11:11:11:11", dst="ff:ff:ff:ff:ff:ff") +b = bytes(pkt) +assert b == b'\x10\x00\x88\xbe\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\x11\x11\x11\x11\x11\x11\x90\x00' + += Dissect ERSPAN II + +pkt = GRE(b) +assert pkt[GRE].proto == 0x88be +assert pkt[GRE].seqnum_present == 1 +assert pkt[GRE][ERSPAN].ver == 1 +assert pkt[Ether].src == "11:11:11:11:11:11" + ++ ERSPAN III += Build & dissect ERSPAN III with platform specific + +pkt = GRE()/ERSPAN_III()/ERSPAN_PlatformSpecific()/Ether() +pkt = GRE(bytes(pkt)) +assert pkt[GRE].proto == 0x22eb +assert pkt[ERSPAN_III].o == 1 +assert ERSPAN_PlatformSpecific in pkt +assert Ether in pkt + += Build & dissect ERSPAN III without platform specific +pkt = GRE()/ERSPAN_III()/Ether() +pkt = GRE(bytes(pkt)) +assert pkt[GRE].proto == 0x22eb +assert pkt[ERSPAN_III].o == 0 +assert ERSPAN_PlatformSpecific not in pkt +assert Ether in pkt + diff --git a/test/contrib/esmc.uts b/test/contrib/esmc.uts new file mode 100644 index 00000000000..ed01e85466c --- /dev/null +++ b/test/contrib/esmc.uts @@ -0,0 +1,33 @@ +% ESMC unit tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('esmc')" -t test/contrib/esmc.uts + ++ ESMC + += Build & dissect ESMC and QLTLV + +pkt = Ether(src="00:13:c4:12:0f:0d") / SlowProtocol() / ESMC(event=1) / QLTLV(ssmCode=0x2) +pkt.show() +s = raw(pkt) +raw_pkt = b'\x01\x80\xc2\x00\x00\x02\x00\x13\xc4\x12\x0f\x0d\x88\x09\x0a\x00\x19\xa7\x00' \ + b'\x01\x18\x00\x00\x00\x01\x00\x04\x02' +assert s == raw_pkt + +p = Ether(s) +assert SlowProtocol in p and ESMC in p and QLTLV in p +assert raw(p) == raw_pkt + += Build & dissect ESMC and EQLTLV + +pkt = pkt / EQLTLV(clockIdentity=b'\x11\x22\x33\x44\x55\x66\x77\x88') +pkt.show() +s = raw(pkt) +raw_pkt = b'\x01\x80\xc2\x00\x00\x02\x00\x13\xc4\x12\x0f\x0d\x88\x09\x0a\x00\x19\xa7\x00' \ + b'\x01\x18\x00\x00\x00\x01\x00\x04\x02\x02\x00\x14\xff\x11\x22\x33\x44\x55\x66' \ + b'\x77\x88\x00\x01\x00\x00\x00\x00\x00\x00' +assert s == raw_pkt + +p = Ether(s) +assert SlowProtocol in p and ESMC in p and QLTLV in p and EQLTLV in p +assert raw(p) == raw_pkt diff --git a/test/contrib/ethercat.uts b/test/contrib/ethercat.uts index 6f246c77eaa..6e1d872212e 100644 --- a/test/contrib/ethercat.uts +++ b/test/contrib/ethercat.uts @@ -81,7 +81,7 @@ for data in test_data: ''' # compare values for key in data: - assert(getattr(bf,key) == data[key]) + assert getattr(bf,key) == data[key] assert (getattr(bf_le, key) == data[key]) = Avoid mix of LEBitFields and BitFields @@ -159,35 +159,38 @@ frm = Ether() / EtherCat() # even with padding the length must be zero # the Ether(do_build()) forces the calculation of all (post_build generated) fields frm = Ether(frm.do_build()) -assert(frm[EtherCat].length == 0) -assert(len(frm) == 60) +assert frm[EtherCat].length == 0 +assert len(frm) == 60 frm = Ether()/Dot1Q()/Dot1Q()/EtherCat() frm = Ether()/EtherCat() -assert(len(frm) == 60) +assert len(frm) == 60 frm = Ether(frm.do_build()) -assert(frm[EtherCat].length == 0) +assert frm[EtherCat].length == 0 = EtherCat and RawPayload frm=Ether()/EtherCat()/Raw(b'0123456789') -assert(len(frm) == 60) +assert len(frm) == 60 frm = Ether(frm.do_build()) -assert(frm[EtherCat].length == 10) +assert frm[EtherCat].length == 10 frm = Ether()/EtherCat()/Raw(b'012345678901234567890123456789012345678901234567890123456789') frm = Ether(frm.do_build()) -assert(len(frm) == 76) -assert(frm[EtherCat].length == 60) +assert len(frm) == 76 +assert frm[EtherCat].length == 60 = EtherCat - test invalid length detection nums_11_bits = [random.randint(0, 65535) & 0b11111111111 for dummy in range(0, 23)] nums_4_bits = [random.randint(0, 16) & 0b1111 for dummy in range(0, 23)] +old_max_list_count = conf.max_list_count +conf.max_list_count = 3000 + frm = Ether()/EtherCat()/EtherCatAPRD(adp=0x1234, ado=0x5678, irq=0xbad0, wkc=0xbeef, data=[1]*2035, c=1) frm = Ether(frm.do_build()) -assert(frm[EtherCat].length == 2047) -assert(len(frm[EtherCatAPRD].data) == 2035) -assert(frm[EtherCatAPRD].c == 1) +assert frm[EtherCat].length == 2047 +assert len(frm[EtherCatAPRD].data) == 2035 +assert frm[EtherCatAPRD].c == 1 data_oversized = False try: @@ -195,25 +198,27 @@ try: frm = Ether(frm.do_build()) except ValueError as err: data_oversized = True - assert('data size' in str(err)) + assert 'data size' in str(err) -assert(data_oversized == True) +assert data_oversized == True dlpdu_oversized = False try: frm = Ether()/EtherCat()/EtherCatAPRD(adp=0x1234, ado=0x5678, irq=0xbad0, wkc=0xbeef, data=[2]*2036, c=1) frm = Ether(frm.do_build()) except ValueError as err: dlpdu_oversized = True - assert('EtherCat message' in str(err)) + assert 'EtherCat message' in str(err) -assert(dlpdu_oversized == True) +assert dlpdu_oversized == True frm = Ether()/EtherCat(_reserved=1)/EtherCatAPRD(adp=0x1234, ado=0x5678, irq=0xbad0, wkc=0xbeef, data=[3], c=0) frm = Ether(frm.do_build()) -assert(frm[EtherCatAPRD].c == 0) +assert frm[EtherCatAPRD].c == 0 + +assert frm[EtherCat]._reserved == 0 -assert(frm[EtherCat]._reserved == 0) +conf.max_list_count = old_max_list_count = EtherCat and Type12 DLPDU layers @@ -223,7 +228,7 @@ for type_id in EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES: frm = Ether(frm.do_build()) # expect to have one layer of current Type12 DLPDU type dlpdu_lyr = frm[EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES[type_id]] - assert(dlpdu_lyr.data == data) + assert dlpdu_lyr.data == data = EtherCat and Type12 DLPDU layer using structure used for physical and broadcast addressing @@ -232,11 +237,11 @@ test_data = [121,99,110,104,114,109,58,41] frm = Ether()/EtherCat()/EtherCatAPRD(adp=0x1234, ado=0x5678, irq=0xbad0, wkc=0xbeef, data=test_data) frm = Ether(frm.do_build()) aprd_lyr = frm[EtherCatAPRD] -assert(aprd_lyr.adp == 0x1234) -assert(aprd_lyr.ado == 0x5678) -assert(aprd_lyr.irq == 0xbad0) -assert(aprd_lyr.wkc == 0xbeef) -assert(aprd_lyr.data == test_data) +assert aprd_lyr.adp == 0x1234 +assert aprd_lyr.ado == 0x5678 +assert aprd_lyr.irq == 0xbad0 +assert aprd_lyr.wkc == 0xbeef +assert aprd_lyr.data == test_data = EtherCat and Type12 DLPDU layer using structure used for logical addressing @@ -262,5 +267,5 @@ for outer_dummy in range(10): frm = Ether(frm.do_build()) idx = 0 for layer_id in layer_ids: - assert(type(EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES[layer_id]()) == type(frm[2 + idx])) + assert type(EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES[layer_id]()) == type(frm[2 + idx]) idx += 1 diff --git a/test/contrib/exposure_notification.uts b/test/contrib/exposure_notification.uts new file mode 100644 index 00000000000..7930c530dde --- /dev/null +++ b/test/contrib/exposure_notification.uts @@ -0,0 +1,59 @@ +% Exposure Notification System tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('exposure_notification')" -t test/contrib/exposure_notification.uts + ++ ENS tests + += Setup + +def next_eir(p): + return EIR_Hdr(p[Padding].load) + += Presence check + +Exposure_Notification_Frame + += Raw payload copied from BluetoothExplorer.app + +d = hex_bytes('17df1d67405e3395470e62ca4fda6a9303687b31') +p = Exposure_Notification_Frame(d) + +assert p.identifier == hex_bytes('17df1d67405e3395470e62ca4fda6a93') +assert p.metadata == hex_bytes('03687b31') + += Raw captured payload + +d = hex_bytes('02011a03036ffd17166ffde23f352fa09307a85d4194912443180d484dc151') +p = EIR_Hdr(d) + +# First is a flags header +assert EIR_Flags in p + +# Then the 16-bit Service Class ID +p = next_eir(p) +assert p[EIR_CompleteList16BitServiceUUIDs].svc_uuids == [ + EXPOSURE_NOTIFICATION_UUID] + +# Then the ENS +p = next_eir(p) +assert p[EIR_ServiceData16BitUUID].svc_uuid == EXPOSURE_NOTIFICATION_UUID +assert p[Exposure_Notification_Frame].identifier == hex_bytes( + 'e23f352fa09307a85d4194912443180d') +assert p[Exposure_Notification_Frame].metadata == hex_bytes('484dc151') + +# Rebuild the payload. +p2 = p[Exposure_Notification_Frame].build_eir() + +# Our captured payload was from a mobile phone, but build_eir presumes that +# we're broadcasting as an non-connectable, LE-only beacon. We need to adjust +# these flags to match the captured packet. +p2[0] = EIR_Hdr() / EIR_Flags(flags=[ + 'general_disc_mode', 'simul_le_br_edr_ctrl', 'simul_le_br_edr_host']) + +# Ensure we didn't mutate LowEnergyBeaconHelper.base_eir just then. +assert LowEnergyBeaconHelper.base_eir[0][EIR_Flags].flags == [ + 'general_disc_mode', 'br_edr_not_supported'] + +# Assemble all packet bytes +assert b''.join(map(raw, p2)) == d diff --git a/test/contrib/geneve.uts b/test/contrib/geneve.uts index 41c922c1851..5e730dcaf3a 100644 --- a/test/contrib/geneve.uts +++ b/test/contrib/geneve.uts @@ -6,40 +6,55 @@ + GENEVE = Build & dissect - GENEVE encapsulates Ether -if WINDOWS: - route_add_loopback() s = raw(IP()/UDP(sport=10000)/GENEVE()/Ether(dst='00:01:00:11:11:11',src='00:02:00:22:22:22')) -assert(s == b'E\x00\x002\x00\x01\x00\x00@\x11|\xb8\x7f\x00\x00\x01\x7f\x00\x00\x01\'\x10\x17\xc1\x00\x1e\x9a\x1c\x00\x00eX\x00\x00\x00\x00\x00\x01\x00\x11\x11\x11\x00\x02\x00"""\x90\x00') +assert s == b'E\x00\x002\x00\x01\x00\x00@\x11|\xb8\x7f\x00\x00\x01\x7f\x00\x00\x01\'\x10\x17\xc1\x00\x1e\x9a\x1c\x00\x00eX\x00\x00\x00\x00\x00\x01\x00\x11\x11\x11\x00\x02\x00"""\x90\x00' p = IP(s) -assert(GENEVE in p and Ether in p[GENEVE].payload) +assert GENEVE in p and Ether in p[GENEVE].payload = Build & dissect - GENEVE with options encapsulates Ether s = raw(IP()/UDP(sport=10000)/GENEVE(critical=1, options=b'\x00\x01\x81\x02\x0a\x0a\x0b\x0b')/Ether(dst='00:01:00:11:11:11',src='00:02:00:22:22:22')) -assert(s == b'E\x00\x00:\x00\x01\x00\x00@\x11|\xb0\x7f\x00\x00\x01\x7f\x00\x00\x01\'\x10\x17\xc1\x00&\x01\xb4\x02@eX\x00\x00\x00\x00\x00\x01\x81\x02\n\n\x0b\x0b\x00\x01\x00\x11\x11\x11\x00\x02\x00"""\x90\x00') +assert s == b'E\x00\x00:\x00\x01\x00\x00@\x11|\xb0\x7f\x00\x00\x01\x7f\x00\x00\x01\'\x10\x17\xc1\x00&\x01\xb4\x02@eX\x00\x00\x00\x00\x00\x01\x81\x02\n\n\x0b\x0b\x00\x01\x00\x11\x11\x11\x00\x02\x00"""\x90\x00' p = IP(s) -assert(GENEVE in p and Ether in p[GENEVE].payload and p[GENEVE].critical == 1 and p[GENEVE].optionlen == 2) +assert GENEVE in p and Ether in p[GENEVE].payload and p[GENEVE].critical == 1 and p[GENEVE].optionlen == 2 + += Build & dissect - GENEVE with metadata options encapsulates Ether + +s = raw(Ether()/Dot1Q()/IP()/UDP(sport=57025,dport=6081)/GENEVE(proto=0x6558,options=GeneveOptions(classid=0x0102,type=0x80,data=b'\x00\x01\x00\x02'))/Ether()/IP()/ICMP(type=8)) +assert (s == b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x81\x00\x00\x01\x08\x00E\x00\x00V\x00\x01\x00\x00@\x11|\x94\x7f\x00\x00\x01\x7f\x00\x00\x01\xde\xc1\x17\xc1\x00B\x1a\x86\x02\x00eX\x00\x00\x00\x00\x01\x02\x80\x01\x00\x01\x00\x02\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00\x1c\x00\x01\x00\x00@\x01|\xde\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x00\xf7\xff\x00\x00\x00\x00') + +p = Ether(s) +assert GENEVE in p and Ether in p[GENEVE].payload and p[GENEVE].proto == 0x6558 and p[GeneveOptions].length == 1 and p[GeneveOptions].classid == 0x102 and p[GeneveOptions].type == 0x80 + += Build & dissect - GENEVE with multiple options + +s = raw(GENEVE(proto=0x0800,options=[GeneveOptions(classid=0x0102,type=0x1,data=b'\x00\x01\x00\x02'), GeneveOptions(classid=0x0102,type=0x2,data=b'\x00\x01\x00\x02')])) +p = GENEVE(s) +assert p.optionlen == 4 +assert len(p.options) == 2 +assert p.options[0].classid == 0x102 and p.options[0].type == 0x1 +assert p.options[1].classid == 0x102 and p.options[1].type == 0x2 = Build & dissect - GENEVE encapsulates IPv4 s = raw(IP()/UDP(sport=10000)/GENEVE()/IP()) -assert(s == b"E\x00\x008\x00\x01\x00\x00@\x11|\xb2\x7f\x00\x00\x01\x7f\x00\x00\x01'\x10\x17\xc1\x00$\xba\xd2\x00\x00\x08\x00\x00\x00\x00\x00E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01") +assert s == b"E\x00\x008\x00\x01\x00\x00@\x11|\xb2\x7f\x00\x00\x01\x7f\x00\x00\x01'\x10\x17\xc1\x00$\xba\xd2\x00\x00\x08\x00\x00\x00\x00\x00E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01" p = IP(s) -assert(GENEVE in p and IP in p[GENEVE].payload) +assert GENEVE in p and IP in p[GENEVE].payload = Build & dissect - GENEVE encapsulates IPv6 s = raw(IP()/UDP(sport=10000)/GENEVE()/IPv6()) -assert(s == b"E\x00\x00L\x00\x01\x00\x00@\x11|\x9e\x7f\x00\x00\x01\x7f\x00\x00\x01'\x10\x17\xc1\x008\xa0\x8a\x00\x00\x86\xdd\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01") +assert s == b"E\x00\x00L\x00\x01\x00\x00@\x11|\x9e\x7f\x00\x00\x01\x7f\x00\x00\x01'\x10\x17\xc1\x008\xa0\x8a\x00\x00\x86\xdd\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" p = IP(s) -assert(GENEVE in p and IPv6 in p[GENEVE].payload) +assert GENEVE in p and IPv6 in p[GENEVE].payload = GENEVE - Answers @@ -61,3 +76,11 @@ a = GENEVE(proto=0x0800)/b'E\x00\x00\x1c\x00\x01\x00\x00@\x01\xfa$\xc0\xa8\x00w\ a = GENEVE(raw(a)) assert a.summary() == 'GENEVE / IP / ICMP 192.168.0.119 > 172.217.18.195 echo-request 0' assert a.mysummary() in ['GENEVE (vni=0x0,optionlen=0,proto=0x800)', 'GENEVE (vni=0x0,optionlen=0,proto=IPv4)'] + += GENEVE - Optionlen + +for size in range(0, 0x1f, 4): + p = GENEVE(bytes(GENEVE(options=GeneveOptions(data=RandString(size))))) + assert p[GENEVE].optionlen == (size // 4 + 1) + assert len(p[GENEVE].options[0].data) == size + diff --git a/test/contrib/gtp.uts b/test/contrib/gtp.uts index 4240a8753c1..155c258aef3 100644 --- a/test/contrib/gtp.uts +++ b/test/contrib/gtp.uts @@ -22,20 +22,20 @@ a = GTPHeader(raw(GTP_U_Header()/GTPPDUSessionContainer(QFI=3))) assert isinstance(a, GTP_U_Header) assert a[GTP_U_Header].E == 1 and a[GTP_U_Header].next_ex == 0x85 assert a[GTPPDUSessionContainer].ExtHdrLen == 1 -assert a[GTPPDUSessionContainer].P == 0 and a[GTPPDUSessionContainer].R == 0 +assert a[GTPPDUSessionContainer].PPP == 0 and a[GTPPDUSessionContainer].RQI == 0 assert a[GTPPDUSessionContainer].QFI == 3 assert a[GTPPDUSessionContainer].NextExtHdr == 0 = GTP_U_Header with PDU Session Container with QFI/PPI -a = GTPHeader(raw(GTP_U_Header()/GTPPDUSessionContainer(type=1, QFI=3, P=1, PPI=6))) +a = GTPHeader(raw(GTP_U_Header()/GTPPDUSessionContainer(type=0, QFI=3, PPP=1, PPI=6))) assert isinstance(a, GTP_U_Header) assert a[GTP_U_Header].E == 1 and a[GTP_U_Header].next_ex == 0x85 assert a[GTPPDUSessionContainer].ExtHdrLen == 2 -assert a[GTPPDUSessionContainer].P == 1 and a[GTPPDUSessionContainer].R == 0 +assert a[GTPPDUSessionContainer].PPP == 1 and a[GTPPDUSessionContainer].RQI == 0 assert a[GTPPDUSessionContainer].QFI == 3 and a[GTPPDUSessionContainer].PPI == 6 assert a[GTPPDUSessionContainer].NextExtHdr == 0 -assert a[GTPPDUSessionContainer].type == 1 +assert a[GTPPDUSessionContainer].type == 0 = GTP_U_Header sub layers @@ -52,6 +52,22 @@ assert isinstance(d[GTP_U_Header].payload, IP) a = IP(raw(IP()/UDP()/GTP_U_Header()/PPP())) assert isinstance(a[GTP_U_Header].payload, PPP) += GTPPDUSessionContainer(), dissect +h = 'fa163ed6de7bfa163ed82b9408004500008400000000fe114b560a0a2e010a0a2efe086808680070000034ff006000000001fa163e850200ff800000000045000054074d00004001fb490a0a31fe0a0a32010000325600930001c444ca5f00000000759e0a0000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637' +gtp = Ether(hex_bytes(h)) +gtp[GTP_U_Header].ExtHdrLen == 2 and gtp[GTP_U_Header].padding == b'\x00\x00\x00' and gtp[GTP_U_Header][IP].src == '10.10.49.254' and gtp[GTP_U_Header][IP][ICMP].type == 0 and gtp[GTP_U_Header].type == 0 and gtp[GTP_U_Header].QMP == 0 and gtp[GTP_U_Header].PPP == 1 and gtp[GTP_U_Header].RQI == 1 and gtp[GTP_U_Header].QFI == 63 and gtp[GTP_U_Header].PPI == 4 + += GTPPDUSessionContainer with padding +data = b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00^\x00\x01\x00\x00@\x11|\x8c\x7f\x00\x00\x01\x7f\x00\x00\x01\x08h\x08h\x00J\xed^4\xff\x00:\x00\x00\x00\x00\x00\x00\x00\x85\x04\x08\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00E\x00\x00&\x00\x01\x00\x00@\x11|\xc4\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x00\x12\x01^ffffffffff000' +gtp = Ether(data) +assert IP in gtp + += GTPEchoResponse matches GTPEchoRequest by seq +req = GTPHeader(seq=12345)/GTPEchoRequest() +res = GTPHeader(seq=12345)/GTPEchoResponse() +assert req.hashret() == res.hashret() +assert res.answers(req) + = GTPCreatePDPContextRequest(), basic instantiation gtp = IP(src="127.0.0.1", dst="127.0.0.1")/UDP(dport=2123, sport=2123)/GTPHeader(teid=2807)/GTPCreatePDPContextRequest() gtp.dport == 2123 and gtp.teid == 2807 and len(gtp.IE_list) == 5 @@ -61,7 +77,8 @@ random.seed(0x2807) rg = raw(gtp) rg assert rg in [ - b"E\x00\x00K\x00\x01\x00\x00@\x11|\x9f\x7f\x00\x00\x01\x7f\x00\x00\x01\x08K\x08K\x007\x99\xce0\x10\x00'\x00\x00\n\xf7\x10\x12\x05\xf7(\x14\x0b\x85\x00\x04\xb7\xd0\xbf \x85\x00\x04\xe2\xb8\x88\x19\x87\x00\x0ffOTLcIukpXKxV0Z", + b"E\x00\x00K\x00\x01\x00\x00@\x11|\x9f\x7f\x00\x00\x01\x7f\x00\x00\x01\x08K\x08K\x007\x8e\x860\x10\x00'\x00\x00\n\xf7\x10\x12\x05\xf7(\x14\x0b\x85\x00\x04_\xe2,i\x85\x00\x04\xadm\x97\x83\x87\x00\x0f1DfOTLcIukpXKxV", + b'E\x00\x00K\x00\x01\x00\x00@\x11|\x9f\x7f\x00\x00\x01\x7f\x00\x00\x01\x08K\x08K\x007ty0\x10\x00\'\x00\x00\n\xf7\x10\xf0\x84"\x1c\x14\x00\x85\x00\x04\x02D\x81\xe8\x85\x00\x04\xbd\xeb\x92z\x87\x00\x0fv2LUNmjgwdrVOeg', b"E\x00\x00K\x00\x01\x00\x00@\x11|\x9f\x7f\x00\x00\x01\x7f\x00\x00\x01\x08K\x08K\x007n\xb20\x10\x00'\x00\x00\n\xf7\x10\x91\x9f\xbc\xaa\x14\x07\x85\x00\x04<\x7f\x87\x14\x85\x00\x04\xbcU\x14\xcb\x87\x00\x0f9Co27Fbj65eKHyQ", ] @@ -244,15 +261,37 @@ ie = IE_ProtocolConfigurationOptions( length=29, Protocol_Configuration=b'\x80\xc0#\x06\x01\x01\x00\x06\x00\x00\x80!\x10\x01\x01\x00\x10\x81\x06\x00\x00\x00\x00\x83\x06\x00\x00\x00\x00') ie.ietype == 132 and ie.Protocol_Configuration == b'\x80\xc0#\x06\x01\x01\x00\x06\x00\x00\x80!\x10\x01\x01\x00\x10\x81\x06\x00\x00\x00\x00\x83\x06\x00\x00\x00\x00' -= IE_GSNAddress(), dissect += IE_GSNAddress(), simple build/dissect IPv4 +r = raw(IE_GSNAddress(length=4, ipv4_address='10.42.0.1')) +assert r == b'\x85\x00\x04\x0a\x2a\x00\x01' +ie = IE_GSNAddress(r) +ie.ietype == 133 and ie.ipv4_address == '10.42.0.1' + += IE_GSNAddress(), simple build/dissect IPv6 +r = raw(IE_GSNAddress(length=16, ipv6_address='fd01:1::1')) +assert r == b'\x85\x00\x10\xfd\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' +ie = IE_GSNAddress(r) +ie.ietype == 133 and ie.ipv6_address == 'fd01:1::1' + += IE_GSNAddress(), dissect IPv4 h = "3333333333332222222222228100838408004588005400000000fd1182850a2a00010a2a0002084b084b00406b463213003031146413c18000000180109181ba027fcf701a8c8500040a2a00018500040a2a000187000f0213921f7396d1fe7482ffff004a00f7a71e0a" gtp = Ether(hex_bytes(h)) ie = gtp.IE_list[3] -ie.ietype == 133 and ie.address == '10.42.0.1' +ie.ietype == 133 and ie.ipv4_address == '10.42.0.1' + += IE_GSNAddress(), dissect IPv6 +h = "33333333333322222222222286dd60000000002c1140fd010001000000000000000000000001fd01000100000000000000000000000208680868002ce2e9321a001c000000000000000010000004d2850010fd010001000000000000000000000001" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[1] +ie.ietype == 133 and ie.ipv6_address == 'fd01:1::1' + += IE_GSNAddress(), basic instantiation IPv4 +ie = IE_GSNAddress(length=4, ipv4_address='10.42.0.1') +ie.ietype == 133 and ie.ipv4_address == '10.42.0.1' -= IE_GSNAddress(), basic instantiation -ie = IE_GSNAddress(address='10.42.0.1') -ie.ietype == 133 and ie.address == '10.42.0.1' += IE_GSNAddress(), basic instantiation IPv6 +ie = IE_GSNAddress(length=16, ipv6_address='fd01:1::1') +ie.ietype == 133 and ie.ipv6_address == 'fd01:1::1' = IE_MSInternationalNumber(), dissect h = "333333333333222222222222810083840800458800c300000000fc1184e50a2a00010a2a00024a4d084b00af41993210009f79504a3e048e00000202081132547600000332f42004d27b0ffc10a692773d1158da9e2214051a0a00800002f1218300070661616161616184001d80c02306010100060000802110010100108106000000008306000000008500040a2a00018500040a2a00018600079111111111111187000d0213621f73967373741affff0094000120970001029800080032f42004d204d299000240009a0008111111111111000081182fb2" @@ -271,8 +310,7 @@ ie = gtp.IE_list[5] ie.ietype == 135 and ie.allocation_retention_prioiry == 2 and ie.delay_class == 2 and ie.traffic_class == 3 = IE_QoS(), basic instantiation -ie = IE_QoS( - allocation_retention_prioiry=2, delay_class=2, traffic_class=3) +ie = IE_QoS(allocation_retention_prioiry=2, delay_class=2, traffic_class=3, length=50) ie.ietype == 135 and ie.allocation_retention_prioiry == 2 and ie.delay_class == 2 and ie.traffic_class == 3 = IE_CommonFlags(), dissect @@ -352,7 +390,9 @@ ie.ietype == 191 and ie.PCI == 1 = IE_CharginGatewayAddress(), basic instantiation ie = IE_CharginGatewayAddress() -ie.ietype == 251 and ie.ipv4_address == '127.0.0.1' and ie.ipv6_address == '::1' +assert ie.ietype == 251 and ie.ipv4_address == '127.0.0.1' +ie = IE_CharginGatewayAddress(length=16) +assert ie.ietype == 251 and ie.ipv6_address == '::1' = IE_PrivateExtension(), basic instantiation ie = IE_PrivateExtension(extention_value='hello') diff --git a/test/contrib/gtp_v2.uts b/test/contrib/gtp_v2.uts index 41d7e1f0415..71e89fb9121 100644 --- a/test/contrib/gtp_v2.uts +++ b/test/contrib/gtp_v2.uts @@ -16,8 +16,11 @@ gtp.dport == 2123 and gtp.seq == 12345 and gtp.gtp_type == 1 and gtp.T == 0 = GTPV2CreateSessionRequest, basic instantiation gtp = IP() / UDP(dport=2123) / \ GTPHeader(gtp_type="create_session_req", teid=2807, seq=12345) / \ - GTPV2CreateSessionRequest() -gtp.dport == 2123 and gtp.teid == 2807 and gtp.seq == 12345 + GTPV2CreateSessionRequest(IE_list=[IE_IMSI(IMSI=b'001030000000356'),IE_APN(APN=b'super')]) + +assert gtp.dport == 2123 and gtp.teid == 2807 and gtp.seq == 12345 +ie = gtp.IE_list[1] +assert ie.APN == b"super" = GTPV2EchoRequest, dissection h = "333333333333222222222222810080c808004588002937dd0000fd1115490a2a00010a2a0002084b084b00152d0e4001000900000100030001000daa000000003f1f382f" @@ -109,25 +112,25 @@ ie.EBI == 50 ie = IE_EPSBearerID(ietype='EPS Bearer ID', length=1, EBI=50) ie.ietype == 73 and ie.EBI == 50 -= IE_IPv4, dissection += IE_IP_Address, dissection h = "3333333333332222222222228100838408004580006d00000000f31180d20a2a00010a2a0002084b85930059e49a4823004d84530d5a4cdee2000200020010004c00060011111111111149000100b248000800000061a8000249f07f000100005d00130049000100da0200020010005e00040039004f454a0004007f00000436f73a63" gtp = Ether(hex_bytes(h)) ie = gtp.IE_list[6] ie.address == '127.0.0.4' -= IE_IPv4, basic instantiation -ie = IE_IPv4(ietype='IPv4', length=4, address='127.0.0.4') += IE_IP_Address, basic instantiation +ie = IE_IP_Address(ietype='IP Address', length=4, address='127.0.0.4') ie.ietype == 74 and ie.address == '127.0.0.4' = IE_MEI, dissection -h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b000800000000000001e24056000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" +h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b00080071655774980786ff56000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" gtp = Ether(hex_bytes(h)) ie = gtp.IE_list[1] -ie.MEI == 123456 +ie.MEI == b"17567547897068" = IE_MEI, basic instantiation -ie = IE_MEI(ietype='MEI', length=1, MEI=123456) -ie.ietype == 75 and ie.MEI == 123456 +ie = IE_MEI(ietype='MEI', length=1, MEI=175675478970685) +ie.ietype == 75 and ie.MEI == 175675478970685 = IE_MSISDN, dissection h = "3333333333332222222222228100838408004580006d00000000f31180d20a2a00010a2a0002084b85930059e49a4823004d55819f6500ede7000200020010004c000600111111111111490001003248000800000061a8000249f07f000100005d001300490001000b0200020010005e00040039004f454a0004007f00000436f73a63" @@ -141,7 +144,7 @@ ie.ietype == 76 and ie.digits == b'111111111111' assert bytes(ie) == b'L\x00\x06\x00\x11\x11\x11\x11\x11\x11' = IE_Indication, dissection -h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b000800000000000001e24056000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" +h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b00080071655774980786ff56000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" gtp = Ether(hex_bytes(h)) ie = gtp.IE_list[10] ie.DAF == 0 and ie.DTF == 0 and ie.PS == 1 and ie.CCRSI == 0 and ie.CPRAI == 0 and ie.PPON == 0 and ie.CLII == 0 and ie.CPSR == 0 @@ -165,6 +168,48 @@ ie = IE_PCO(ietype='Protocol Configuration Options', length=8, Extension=1, PPP= PCO_DNS_Server_IPv4(type='DNS Server IPv4 Address Request', length=4, address='10.42.0.3')]) ie.Extension == 1 and ie.PPP == 3 and ie.Protocols[0].address == '10.42.0.3' += IE_EPCO, dissection +h = "d89ef3da40e2fa163e956dce08004500003000010000401144e10a0f0f3d0a0f1281084b084b001c0c154821000c0000000100000100c500040080001b00" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0] +ie.Protocols[0].type == 27 + += IE_EPCO, basic instantiation +ie = IE_EPCO(Protocols=[PCO_S_Nssai(type=27, length=0)], ietype=197, length=4, CR_flag=0, instance=0, Extension=1, SPARE=0, PPP=0) +ie.Extension == 1 and ie.ietype == 197 and ie.Protocols[0].type == 27 and ie.Protocols[0].length == 0 + += IE_APCO, dissection +h = "d89ef3da40e2fa163e956dce0800450000360001000040115d650a0f0f3d01020304084b084b00220000482000160000000100000100a3000a0080000c00001200000d00" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0] +ie.Protocols[0].type == 12 and ie.Protocols[1].type == 18 and ie.Protocols[2].type == 13 + += IE_APCO, basic instantiation +ie = IE_APCO(Protocols=[PCO_P_CSCF_IPv4_Address_Request(address=None, type=12, length=0),PCO_P_CSCF_Re_selection_Support(type=18, length=0),PCO_DNS_Server_IPv4(address=None, type=13, length=0)], ietype=163, length=10, CR_flag=0, instance=0, extension=1, SPARE=0, PPP=0) +ie.extension == 1 and ie.ietype == 163 and ie.length == 10 and ie.Protocols[0].type == 12 and ie.Protocols[1].type == 18 and ie.Protocols[2].type == 13 + += IE_MMContext_EPS, dissection +h = "d89ef3da40e2fa163e956dce08004500007f0001000040114bbd0a0a0f3d0a0f0b5b084b084b006b5a234883005f0000180f76d163006b0046008800910000020000021890aa80be385102083701a2907066f8bd9f2a28b717671c71c71c71c71c71c70100003d090002625a00028040000812345678900000000000000000006d000900880005000470677731" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0] +ie.Sec_Mode == 4 and ie.Nhi == 0 and ie.Drxi == 1 and ie.Ksi == 0 and ie.Num_quint == 0 and ie.Num_Quad == 0 and ie.Uambri == 0 and ie.Osci == 0 and ie.Sambri == 1 and ie.Nas_algo == 1 and ie.Nas_cipher == 1 and ie.Nas_dl_count == 2 and ie.Nas_ul_count == 2 and ie.Kasme == 11111111111111111111111111111111111111111111111111111111111111111111111111111 + += IE_MMContext_EPS, basic instantiation +ie = IE_MMContext_EPS(ietype=107, length=70, CR_flag=0, instance=0, Sec_Mode=4, Nhi=0, Drxi=1, Ksi=0, Num_quint=0, Num_Quad=0, Uambri=0, Osci=0, Sambri=1, Nas_algo=1, Nas_cipher=1, Nas_dl_count=2, Nas_ul_count=2, Kasme=11111111111111111111111111111111111111111111111111111111111111111111111111111) +ie.Sec_Mode == 4 and ie.Nhi == 0 and ie.Drxi == 1 and ie.Ksi == 0 and ie.Num_quint == 0 and ie.Num_Quad == 0 and ie.Uambri == 0 and ie.Osci == 0 and ie.Sambri == 1 and ie.Nas_algo == 1 and ie.Nas_cipher == 1 and ie.Nas_dl_count == 2 and ie.Nas_ul_count == 2 and ie.Kasme == 11111111111111111111111111111111111111111111111111111111111111111111111111111 + += IE_PDNConnection, IE_FQDN, dissection +h = "d89ef3da40e2fa163e956dce08004500008a0001000040114bbd0a0a0f3d0a0f0b5b084b084b00765a234883006a0000180f76d163006b0046008800910000020000021890aa80be385102083701a2907066f8bd9f2a28b717671c71c71c71c71c71c70100003d090002625a00028040000812345678900000000000000000006d0014008800100004706777310474657374056c6f63616c" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[1].IE_list[0] +ie.fqdn == b'pgw1.test.local' +gtp.build().hex() == h + += IE_PDNConnection, IE_FQDN, basic instantiation +ie = IE_PDNConnection(IE_list=[IE_FQDN(ietype=136, length=5, CR_flag=0, instance=0, fqdn=b'pgw1.test.local')], ietype=109, length=9, CR_flag=0, instance=0) +ie2 = ie.IE_list[0] +ie2.fqdn == b'pgw1.test.local' + = IE_PAA, dissection h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b000800000000000001e24056000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" gtp = Ether(hex_bytes(h)) @@ -218,6 +263,56 @@ ie = IE_ULI(ietype='ULI', length=13, LAI_Present=0, ECGI_Present=1, TAI_Present= CGI_Present=0, TAI=ULI_TAI(MCC='234', MNC='02', TAC=12345), ECGI=ULI_ECGI(MCC='234', MNC='02', ECI=123456)) ie.ietype == 86 and ie.LAI_Present == 0 and ie.ECGI_Present == 1 and ie.TAI_Present == 1 and ie.RAI_Present == 0 and ie.SAI_Present == 0 and ie.CGI_Present == 0 and ie.TAI.MCC == b'234' and ie.TAI.MNC == b'02' and ie.TAI.TAC == 12345 and ie.ECGI.MCC == b'234' and ie.ECGI.MNC == b'02' and ie.ECGI.ECI == 123456 += IE_UCI, dissection +h = "fe1d70fa717ceeeeeeeeeeee080045000127a4f500003c11e9aec0a8ee80c0a87f50084b23a301131aa1482001070000000001020f009100080021f3540000001602" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0] +ie.CSG_ID == 22 and ie.AccessMode == 0 and ie.LCSG == 1 and ie.CMI == 0 and ie.MCC == b'123' and ie.MNC == b'45' + += IE_UCI, basic instantiation +ie = IE_UCI(ietype='UCI', length=8, CR_flag=0, instance=0, MCC=b'123', MNC=b'45', SPARE1=0, SPARE2=0, CSG_ID=22, AccessMode=0, LCSG=1, CMI=0) +ie.ietype == 145 and ie.CSG_ID == 22 and ie.AccessMode == 0 and ie.LCSG == 1 and ie.CMI == 0 and ie.MCC == b'123' and ie.MNC == b'45' + += IE_BearerFlags, dissection +h = "0026f126c100000c29b131dd81004d040800450000d8a6010000401118680a2180350a212735084b138800c47f8248210011000023f2000001005d006200610001000a" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0].IE_list[0] +ie.ASI == 1 and ie.Vind == 0 and ie.VB == 1 and ie.PPC == 0 + += IE_BearerFlags, basic instantiation +ie = IE_BearerFlags(ietype='Bearer Flags', length=1, CR_flag=0, instance=0, SPARE=0, ASI=1, Vind=0, VB=1, PPC=0) +ie.ietype == 97 and ie.ASI == 1 and ie.Vind == 0 and ie.VB == 1 and ie.PPC == 0 + += IE_UPF_SelInd_Flags, dissection +h = "000c29b131dd0026f126c10081000d04080045000112608940003f111ea60a2127350a2180351388084b00fe0ec44820000d0000000000000100ca00010000" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0] +ie.DCNR == 0 + += IE_UPF_SelInd_Flags, basic instantiation +ie = IE_UPF_SelInd_Flags(ietype='UP Function Selection Indication Flags', length=1, CR_flag=0, instance=0, SPARE=0, DCNR=0) +ie.ietype == 202 and ie.DCNR == 0 + += IE_Ran_Nas_Cause, dissection +h = "00000000000000000000000008004500005a0000000040114d390101010101010102084b084b0046bf694824000e000ba0df00002300ac0002003011" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0] +ie.protocol_type == 3 and ie.cause_type == 0 and ie.cause_value == 17 + += IE_Ran_Nas_Cause, basic instantiation +ie = IE_Ran_Nas_Cause(ietype='RAN/NAS Cause', length=2, CR_flag=0, instance=0, protocol_type=3, cause_type=0, cause_value=17) +ie.ietype == 172 and ie.protocol_type == 3 and ie.cause_type == 0 and ie.cause_value == 17 + += IE_FQCSID, dissection +h = "d89ef3da40e2fa163e956dce0800450000330001000040117a2a0a0f0f3d0a09dd3a084b084b001f454648240013000000010000010084000700010a01010b00c8" +gtp = Ether(hex_bytes(h)) +ie = gtp.IE_list[0] +ie.ietype == 132 and ie.nodeid_type == 0 and ie.num_csid == 1 and ie.nodeid_v4 == '10.1.1.11' and ie.csid == 200 + += IE_FQCSID, basic instantiation +ie = IE_FQCSID(ietype=132, length=19, CR_flag=0, instance=0, nodeid_type=1, num_csid=1, nodeid_v4=None, nodeid_v6=42540578207381523466529575969228128257, nodeid_nonip=None, csid=0) +ie.ietype == 132 and ie.nodeid_type == 1 and ie.num_csid == 1 and ie.nodeid_v6 == 42540578207381523466529575969228128257 and ie.csid == 0 + = IE_FTEID, dissection h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b000800000000000001e24056000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" gtp = Ether(hex_bytes(h)) @@ -256,12 +351,12 @@ ie.ietype == 94 and ie.ChargingID == 956321605 h = "3333333333332222222222228100a384080045b8011800000000fc1193150a2a00010a2a00027be5084b010444c4482000f82fd783953790a2000100080002081132547600004c0006001111111111114b000800000000000001e24056000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a001961616161616161616161616161616161616161616161616161800001000063000100014f000500017f0000034d000400000800007f00010000480008000000c3500002e6304e001a008080211001000010810600000000830600000000000d00000a005d001f00490001000750001600190700000000000000000000000000000000000000007200020014005f0002000a008e80b09f" gtp = Ether(hex_bytes(h)) ie = gtp.IE_list[18] -ie.ChargingCharacteristric == 0xa00 +ie.ChargingCharacteristic == 0xa00 = IE_ChargingCharacteristics, basic instantiation ie = IE_ChargingCharacteristics( - ietype='Charging Characteristics', length=2, ChargingCharacteristric=0xa00) -ie.ietype == 95 and ie.ChargingCharacteristric == 0xa00 + ietype='Charging Characteristics', length=2, ChargingCharacteristic=0xa00) +ie.ietype == 95 and ie.ChargingCharacteristic == 0xa00 = IE_PDN_type, dissection h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b000800000000000001e24056000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" @@ -331,16 +426,31 @@ ie = IE_MMBR(ietype='Max MBR/APN-AMBR (MMBR)', length=8, uplink_rate=5696, downlink_rate=21000) ie.ietype == 161 and ie.uplink_rate == 5696 and ie.downlink_rate == 21000 -= GTPHeader answers to not GTPHeader instance += GTPHeader isn't an answer to not GTPHeader instance GTPHeader(gtp_type=2).answers(Ether()) == False += GTPHeader is an answer to a message with the same sequence number +GTPHeader(seq=42).answers(GTPHeader(seq=42)) == True + += GTPHeader isn't an answer to a message with a different sequence number +GTPHeader(seq=42).answers(GTPHeader(seq=24)) == False + += GTPV2EchoResponse answers +assert (GTPHeader(seq=1)/GTPV2EchoResponse()).answers(GTPHeader(seq=1)/GTPV2EchoRequest()) +assert not (GTPHeader(seq=1)/GTPV2EchoResponse()).answers(GTPHeader(seq=1)/GTPV2EchoResponse()) + = GTPHeader post_build gtp = GTPHeader(gtp_type="create_session_req") / ("X"*32) gtp.show2() += GTPHeader length calculation +h = GTPHeader(seq=12345, version=2, T=1, teid=1234)/("X"*32) +h = GTPHeader(h.do_build()) +h[GTPHeader].length == len(bytes(h)) - 4 + = GTPHeader hashret -req = GTPHeader(gtp_type="create_session_req") / ("X"*32) -res = GTPHeader(gtp_type="create_session_res") / ("Y"*32) +req = GTPHeader(gtp_type="create_session_req", seq=1) / ("X"*32) +res = GTPHeader(gtp_type="create_session_res", seq=1) / ("Y"*32) req.hashret() == res.hashret() = IE_NotImplementedTLV @@ -349,10 +459,16 @@ gtp = Ether(hex_bytes(h)) isinstance(gtp.IE_list[0], IE_NotImplementedTLV) isinstance(gtp.IE_list[0].payload, NoPayload) -= IE_PrivateExtension -h = "5001003500303900ff0031002b79deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678" -gtp = GTPHeader(hex_bytes(h)) -gtp.show2() -(ie,) = gtp.IE_list -ie.enterprisenum == 11129 -bytes_hex(ie.payload) == b"deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678" += IE_PrivateExtension, dissection +h = "d89ef3da40e2fa163e956dce08004500005b0001000040115d400a0f0f3d01020304084b084b00470000482000620000000100000100ff0015000137020046462d46462d46462d46462d46462d4646ff00160001370100000100000000000000000000000000000000" +gtp = Ether(hex_bytes(h)) +ie1 = gtp.IE_list[0] +ie2 = gtp.IE_list[1] +ie1.enterprisenum == 311 and bytes_hex(ie1.proprietaryvalue) == b'020046462d46462d46462d46462d46462d4646' +ie2.enterprisenum == 311 and bytes_hex(ie2.proprietaryvalue) == b'0100000100000000000000000000000000000000' + += IE_PrivateExtension, basic instantiation +ie1 = IE_PrivateExtension(ietype=255, length=21, SPARE=0, instance=0, enterprisenum=311, proprietaryvalue=hex_bytes('020046462d46462d46462d46462d46462d4646')) +ie2 = IE_PrivateExtension(ietype=255, length=22, SPARE=0, instance=0, enterprisenum=311, proprietaryvalue=hex_bytes('0100000100000000000000000000000000000000')) +ie1.enterprisenum == 311 and bytes_hex(ie1.proprietaryvalue) == b'020046462d46462d46462d46462d46462d4646' +ie2.enterprisenum == 311 and bytes_hex(ie2.proprietaryvalue) == b'0100000100000000000000000000000000000000' diff --git a/test/contrib/gxrp.uts b/test/contrib/gxrp.uts new file mode 100644 index 00000000000..38a7a2cb195 --- /dev/null +++ b/test/contrib/gxrp.uts @@ -0,0 +1,137 @@ +# GXRP unit tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('gxrp')" -t test/contrib/gxrp.uts + ++ GVRP test + += Construction test + +pkt = GVRP(vlan=2) +assert pkt.vlan == 2 +assert pkt == GVRP(raw(pkt)) + ++ GMRP test + += GMRP_GROUP Construction test + +pkt = GMRP_GROUP(addr="01:23:45:67:89:00") +assert pkt.addr == "01:23:45:67:89:00" +assert pkt == GMRP_GROUP(raw(pkt)) + += GMRP_SERVICE Construction test + +pkt = GMRP_SERVICE(event="All Groups") +assert pkt.event == 0 +pkt = GMRP_SERVICE(event="All Unregistered Groups") +assert pkt.event == 1 +assert pkt == GMRP_SERVICE(raw(pkt)) + ++ GARP Attribute test + += GMRP_GROUP Construction test + +pkt = GARP_ATTRIBUTE(event='LeaveAll') +assert pkt.event == 0 +assert GARP_ATTRIBUTE(pkt.build()).len == 2 +assert len(pkt.build()) == 2 +pkt = GARP_ATTRIBUTE(event='JoinEmpty')/GVRP() +assert pkt.event == 1 +assert GARP_ATTRIBUTE(pkt.build()).len == 4 +assert len(pkt.build()) == 4 +pkt = GARP_ATTRIBUTE(event='JoinIn')/GVRP() +assert pkt.event == 2 +assert GARP_ATTRIBUTE(pkt.build()).len == 4 +assert len(pkt.build()) == 4 +pkt = GARP_ATTRIBUTE(event='LeaveEmpty')/GVRP() +assert pkt.event == 3 +assert GARP_ATTRIBUTE(pkt.build()).len == 4 +assert len(pkt.build()) == 4 +pkt = GARP_ATTRIBUTE(event='LeaveIn')/GVRP() +assert pkt.event == 4 +assert GARP_ATTRIBUTE(pkt.build()).len == 4 +assert len(pkt.build()) == 4 +pkt = GARP_ATTRIBUTE(event='Empty')/GVRP() +assert pkt.event == 5 +assert GARP_ATTRIBUTE(pkt.build()).len == 4 +assert len(pkt.build()) == 4 +pkt = GARP_ATTRIBUTE(event='JoinEmpty')/GVRP() +del pkt.payload +assert pkt == GARP_ATTRIBUTE(event='JoinEmpty') +assert GARP_ATTRIBUTE(raw(pkt)) == GARP_ATTRIBUTE(raw(GARP_ATTRIBUTE(event='JoinEmpty'))) +assert len(pkt.build()) == 2 + += GVRP Stacking test + +pkt = Dot3(dst="01:80:c2:00:00:21")/LLC_GARP(dsap=0x42, ssap=0x42, ctrl=3)/GARP( + msgs=[GARP_MESSAGE(attrs=[GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=1), + GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=2)]), + GARP_MESSAGE(attrs=[GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=3), + GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=4)])]) +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 1)[GVRP].vlan == 1 +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 2)[GVRP].vlan == 2 +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 1)[GVRP].vlan == 3 +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 2)[GVRP].vlan == 4 +pkt = Dot3(pkt.build()) +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 1)[GVRP].vlan == 1 +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 2)[GVRP].vlan == 2 +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 1)[GVRP].vlan == 3 +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 2)[GVRP].vlan == 4 + += GMRP Stacking test + +pkt = Dot3(dst="01:80:c2:00:00:20")/LLC_GARP(dsap=0x42, ssap=0x42, ctrl=3)/GARP( + msgs=[GARP_MESSAGE(type = 1, attrs=[GARP_ATTRIBUTE(event='JoinIn')/GMRP_GROUP(addr="00:00:00:00:00:01"), + GARP_ATTRIBUTE(event='JoinIn')/GMRP_GROUP(addr="00:00:00:00:00:02")]), + GARP_MESSAGE(type = 2, attrs=[GARP_ATTRIBUTE(event='JoinIn')/GMRP_SERVICE(event="All Groups"), + GARP_ATTRIBUTE(event='JoinIn')/GMRP_SERVICE(event="All Unregistered Groups")])]) +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 1)[GMRP_GROUP].addr == "00:00:00:00:00:01" +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 2)[GMRP_GROUP].addr == "00:00:00:00:00:02" +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 1)[GMRP_SERVICE].event == 0 +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 2)[GMRP_SERVICE].event == 1 +pkt = Dot3(pkt.build()) +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 1)[GMRP_GROUP].addr == "00:00:00:00:00:01" +assert pkt.getlayer(GARP_MESSAGE, 1).getlayer(GARP_ATTRIBUTE, 2)[GMRP_GROUP].addr == "00:00:00:00:00:02" +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 1)[GMRP_SERVICE].event == 0 +assert pkt.getlayer(GARP_MESSAGE, 2).getlayer(GARP_ATTRIBUTE, 2)[GMRP_SERVICE].event == 1 + += GARP from pcap + +pkts = rdpcap(scapy_path("test/pcaps/gvrp.pcapng.gz")) +for p in pkts: + if len(p[GARP_ATTRIBUTE].payload) > 0: + assert p[GVRP] is not None + += GARP tshark check +~ tshark + +import tempfile, os +pkt = Dot3(dst="01:80:c2:00:00:21")/LLC_GARP(dsap=0x42, ssap=0x42, ctrl=3)/GARP( + msgs=[GARP_MESSAGE(attrs=[GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=1), + GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=2)]), + GARP_MESSAGE(attrs=[GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=3), + GARP_ATTRIBUTE(event='JoinIn')/GVRP(vlan=4)])]) + +fd, pcapfilename = tempfile.mkstemp() +wrpcap(pcapfilename, pkt) +rv = tcpdump(pcapfilename, prog=conf.prog.tshark, getfd=True, args=['-Y', 'gvrp'], dump=True, wait=True) +assert rv != b"" +os.close(fd) +os.unlink(pcapfilename) + += GARP tshark check +~ tshark + +import tempfile, os +pkt = Dot3(dst="01:80:c2:00:00:20")/LLC_GARP(dsap=0x42, ssap=0x42, ctrl=3)/GARP( + msgs=[GARP_MESSAGE(type = 1, attrs=[GARP_ATTRIBUTE(event='JoinIn')/GMRP_GROUP(addr="00:00:00:00:00:01"), + GARP_ATTRIBUTE(event='JoinIn')/GMRP_GROUP(addr="00:00:00:00:00:02")]), + GARP_MESSAGE(type = 2, attrs=[GARP_ATTRIBUTE(event='JoinIn')/GMRP_SERVICE(event="All Groups"), + GARP_ATTRIBUTE(event='JoinIn')/GMRP_SERVICE(event="All Unregistered Groups")])]) + +fd, pcapfilename = tempfile.mkstemp() +wrpcap(pcapfilename, pkt) +rv = tcpdump(pcapfilename, prog=conf.prog.tshark, getfd=True, args=['-Y', 'gmrp'], dump=True, wait=True) +assert rv != b"" +os.close(fd) +os.unlink(pcapfilename) diff --git a/test/contrib/hicp.uts b/test/contrib/hicp.uts new file mode 100644 index 00000000000..12d0e4832b4 --- /dev/null +++ b/test/contrib/hicp.uts @@ -0,0 +1,113 @@ +% HICP test campaign + +# +# execute test: +# > test/run_tests -t test/contrib/hicp.uts +# + ++ Syntax check += Import the HICP layer +from scapy.contrib.hicp import * + ++ HICP Module scan request += Build and dissect module scan +pkt = HICPModuleScan() +assert(pkt.hicp_command == b"Module scan") +assert(raw(pkt) == b"MODULE SCAN\x00") +pkt = HICP(b"Module scan\x00") +assert(pkt.hicp_command == b"Module scan") + ++ HICP Module scan response += Build and dissect device description +pkt=HICPModuleScanResponse(fieldbus_type="kwack") +assert(pkt.protocol_version == b"1.00") +assert(pkt.fieldbus_type == b"kwack") +assert(pkt.mac_address == "ff:ff:ff:ff:ff:ff") +pkt=HICP( +b"\x50\x72\x6f\x74\x6f\x63\x6f\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e" \ +b"\x20\x3d\x20\x31\x2e\x30\x30\x3b\x46\x42\x20\x74\x79\x70\x65\x20" \ +b"\x3d\x20\x3b\x4d\x6f\x64\x75\x6c\x65\x20\x76\x65\x72\x73\x69\x6f" \ +b"\x6e\x20\x3d\x20\x3b\x4d\x41\x43\x20\x3d\x20\x65\x65\x3a\x65\x65" \ +b"\x3a\x65\x65\x3a\x65\x65\x3a\x65\x65\x3a\x65\x65\x3b\x49\x50\x20" \ +b"\x3d\x20\x32\x35\x35\x2e\x32\x35\x35\x2e\x32\x35\x35\x2e\x32\x35" \ +b"\x35\x3b\x53\x4e\x20\x3d\x20\x32\x35\x35\x2e\x32\x35\x35\x2e\x32" \ +b"\x35\x35\x2e\x30\x3b\x47\x57\x20\x3d\x20\x30\x2e\x30\x2e\x30\x2e" \ +b"\x30\x3b\x44\x48\x43\x50\x20\x3d\x20\x4f\x46\x46\x3b\x48\x4e\x20" \ +b"\x3d\x20\x3b\x44\x4e\x53\x31\x20\x3d\x20\x30\x2e\x30\x2e\x30\x2e" \ +b"\x30\x3b\x44\x4e\x53\x32\x20\x3d\x20\x30\x2e\x30\x2e\x30\x2e\x30" \ +b"\x3b\x00" +) +assert(pkt.hicp_command == b"Module scan response") +assert(pkt.protocol_version == b"1.00") +assert(pkt.mac_address == "ee:ee:ee:ee:ee:ee") +assert(pkt.subnet_mask == "255.255.255.0") +pkt=HICP(b"Protocol version = 2; FB type = TEST;Module version = 1.0.0;MAC = cc:cc:cc:cc:cc:cc;IP = 192.168.1.1;SN = 255.255.255.0;GW = 192.168.1.254;DHCP=ON;HN = bonjour;DNS1 = 1.1.1.1;DNS2 = 2.2.2.2") +assert(pkt.hicp_command == b"Module scan response") +assert(pkt.protocol_version == b"2") +assert(pkt.fieldbus_type == b"TEST") +assert(pkt.module_version == b"1.0.0") +assert(pkt.mac_address == "cc:cc:cc:cc:cc:cc") +assert(pkt.ip_address == "192.168.1.1") +assert(pkt.subnet_mask == "255.255.255.0") +assert(pkt.gateway_address == "192.168.1.254") +assert(pkt.dhcp == b"ON") +assert(pkt.hostname == b"bonjour") +assert(pkt.dns1 == "1.1.1.1") +assert(pkt.dns2 == "2.2.2.2") + ++ HICP Wink request += Build and dissect Winks +pkt = HICPWink(target="dd:dd:dd:dd:dd:dd") +assert(pkt.target == "dd:dd:dd:dd:dd:dd") +pkt = HICP(b"To: bb:bb:bb:bb:bb:bb;WINK;\x00") +assert(pkt.target == "bb:bb:bb:bb:bb:bb") + ++ HICP Configure request += Build and dissect new network settings +pkt = HICPConfigure(target="aa:aa:aa:aa:aa:aa", hostname="llama") +assert(pkt.target == "aa:aa:aa:aa:aa:aa") +assert(pkt.ip_address == "255.255.255.255") +assert(pkt.hostname == b"llama") +assert(raw(pkt) == b"Configure: aa-aa-aa-aa-aa-aa;IP = 255.255.255.255;SN = 255.255.255.0;GW = 0.0.0.0;DHCP = OFF;HN = llama;DNS1 = 0.0.0.0;DNS2 = 0.0.0.0;\x00") +pkt = HICP(b"Configure: aa-aa-aa-aa-aa-aa;IP = 255.255.255.255;SN = 255.255.255.0;GW = 0.0.0.0;DHCP = OFF;HN = llama;DNS1 = 0.0.0.0;DNS2 = 0.0.0.0;\x00") +assert(pkt.hicp_command == b"Configure") +assert(pkt.target == "aa:aa:aa:aa:aa:aa") +assert(pkt.ip_address == "255.255.255.255") +assert(pkt.hostname == b"llama") + ++ HICP Configure response += Build and dissect successful response to configure request + +pkt = HICPReconfigured(source="11:00:00:00:00:00") +assert(pkt.source == "11:00:00:00:00:00") +assert(raw(pkt) == b"Reconfigured: 11-00-00-00-00-00\x00") +pkt = HICP(b"\x52\x65\x63\x6f\x6e\x66\x69\x67\x75\x72\x65\x64\x3a\x20\x31\x31" \ +b"\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x00") +assert(pkt.hicp_command == b"Reconfigured") +assert(pkt.source == "11:00:00:00:00:00") + ++ HICP Configure error += Build and dissect error response to configure request + +pkt = HICPInvalidConfiguration(source="00:11:00:00:00:00") +assert(pkt.source == "00:11:00:00:00:00") +assert(raw(pkt) == b"Invalid Configuration: 00-11-00-00-00-00\x00") +pkt = HICP( +b"\x49\x6e\x76\x61\x6c\x69\x64\x20\x43\x6f\x6e\x66\x69\x67\x75\x72" \ +b"\x61\x74\x69\x6f\x6e\x3a\x20\x30\x30\x2d\x31\x31\x2d\x30\x30\x2d" \ +b"\x30\x30\x2d\x30\x30\x2d\x30\x30\x00" +) +assert(pkt.hicp_command == b"Invalid Configuration") +assert(pkt.source == "00:11:00:00:00:00") + ++ HICP Configure invalid password += Build and dissect invalid password response to configure request + +pkt = HICPInvalidPassword(source="00:00:11:00:00:00") +assert(pkt.source == "00:00:11:00:00:00") +assert(raw(pkt) == b"Invalid Password: 00-00-11-00-00-00\x00") +pkt = HICP(b"\x49\x6e\x76\x61\x6c\x69" \ +b"\x64\x20\x50\x61\x73\x73\x77\x6f\x72\x64\x3a\x20\x30\x30\x2d\x30" \ +b"\x30\x2d\x31\x31\x2d\x30\x30\x2d\x30\x30\x2d\x30\x30\x00") +assert(pkt.hicp_command == b"Invalid Password") +assert(pkt.source == "00:00:11:00:00:00") diff --git a/test/contrib/http2.uts b/test/contrib/http2.uts index 96150f237a2..3c195ccbe01 100644 --- a/test/contrib/http2.uts +++ b/test/contrib/http2.uts @@ -29,32 +29,32 @@ def expect_exception(e, c): f = h2.UVarIntField('value', 0, 5) expect_exception(AssertionError, 'f.any2i(None, None)') -assert(f.any2i(None, 0) == 0) -assert(f.any2i(None, 3) == 3) -assert(f.any2i(None, 1<<5) == 1<<5) -assert(f.any2i(None, 1<<16) == 1<<16) +assert f.any2i(None, 0) == 0 +assert f.any2i(None, 3) == 3 +assert f.any2i(None, 1<<5) == 1<<5 +assert f.any2i(None, 1<<16) == 1<<16 f = h2.UVarIntField('value', 0, 8) -assert(f.any2i(None, b'\x1E') == 30) +assert f.any2i(None, b'\x1E') == 30 = HTTP/2 UVarIntField.m2i on full byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 8) -assert(f.m2i(None, b'\x00') == 0) -assert(f.m2i(None, b'\x03') == 3) -assert(f.m2i(None, b'\xFE') == 254) -assert(f.m2i(None, b'\xFF\x00') == 255) -assert(f.m2i(None, b'\xFF\xFF\x03') == 766) #0xFF + (0xFF ^ 0x80) + (3<<7) +assert f.m2i(None, b'\x00') == 0 +assert f.m2i(None, b'\x03') == 3 +assert f.m2i(None, b'\xFE') == 254 +assert f.m2i(None, b'\xFF\x00') == 255 +assert f.m2i(None, b'\xFF\xFF\x03') == 766 #0xFF + (0xFF ^ 0x80) + (3<<7 = HTTP/2 UVarIntField.m2i on partial byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 5) -assert(f.m2i(None, (b'\x00', 3)) == 0) -assert(f.m2i(None, (b'\x03', 3)) == 3) -assert(f.m2i(None, (b'\x1e', 3)) == 30) -assert(f.m2i(None, (b'\x1f\x00', 3)) == 31) -assert(f.m2i(None, (b'\x1f\xe1\xff\x03', 3)) == 65536) +assert f.m2i(None, (b'\x00', 3)) == 0 +assert f.m2i(None, (b'\x03', 3)) == 3 +assert f.m2i(None, (b'\x1e', 3)) == 30 +assert f.m2i(None, (b'\x1f\x00', 3)) == 31 +assert f.m2i(None, (b'\x1f\xe1\xff\x03', 3)) == 65536 = HTTP/2 UVarIntField.getfield on full byte ~ http2 frame field uvarintfield @@ -62,24 +62,24 @@ assert(f.m2i(None, (b'\x1f\xe1\xff\x03', 3)) == 65536) f = h2.UVarIntField('value', 0, 8) r = f.getfield(None, b'\x00\x00') -assert(r[0] == b'\x00') -assert(r[1] == 0) +assert r[0] == b'\x00' +assert r[1] == 0 r = f.getfield(None, b'\x03\x00') -assert(r[0] == b'\x00') -assert(r[1] == 3) +assert r[0] == b'\x00' +assert r[1] == 3 r = f.getfield(None, b'\xFE\x00') -assert(r[0] == b'\x00') -assert(r[1] == 254) +assert r[0] == b'\x00' +assert r[1] == 254 r = f.getfield(None, b'\xFF\x00\x00') -assert(r[0] == b'\x00') -assert(r[1] == 255) +assert r[0] == b'\x00' +assert r[1] == 255 r = f.getfield(None, b'\xFF\xFF\x03\x00') -assert(r[0] == b'\x00') -assert(r[1] == 766) +assert r[0] == b'\x00' +assert r[1] == 766 = HTTP/2 UVarIntField.getfield on partial byte ~ http2 frame field uvarintfield @@ -87,88 +87,88 @@ assert(r[1] == 766) f = h2.UVarIntField('value', 0, 5) r = f.getfield(None, (b'\x00\x00', 3)) -assert(r[0] == b'\x00') -assert(r[1] == 0) +assert r[0] == b'\x00' +assert r[1] == 0 r = f.getfield(None, (b'\x03\x00', 3)) -assert(r[0] == b'\x00') -assert(r[1] == 3) +assert r[0] == b'\x00' +assert r[1] == 3 r = f.getfield(None, (b'\x1e\x00', 3)) -assert(r[0] == b'\x00') -assert(r[1] == 30) +assert r[0] == b'\x00' +assert r[1] == 30 r = f.getfield(None, (b'\x1f\x00\x00', 3)) -assert(r[0] == b'\x00') -assert(r[1] == 31) +assert r[0] == b'\x00' +assert r[1] == 31 r = f.getfield(None, (b'\x1f\xe1\xff\x03\x00', 3)) -assert(r[0] == b'\x00') -assert(r[1] == 65536) +assert r[0] == b'\x00' +assert r[1] == 65536 = HTTP/2 UVarIntField.i2m on full byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 8) -assert(f.i2m(None, 0) == b'\x00') -assert(f.i2m(None, 3) == b'\x03') -assert(f.i2m(None, 254).lower() == b'\xfe') -assert(f.i2m(None, 255).lower() == b'\xff\x00') -assert(f.i2m(None, 766).lower() == b'\xff\xff\x03') +assert f.i2m(None, 0) == b'\x00' +assert f.i2m(None, 3) == b'\x03' +assert f.i2m(None, 254).lower() == b'\xfe' +assert f.i2m(None, 255).lower() == b'\xff\x00' +assert f.i2m(None, 766).lower() == b'\xff\xff\x03' = HTTP/2 UVarIntField.i2m on partial byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 5) -assert(f.i2m(None, 0) == b'\x00') -assert(f.i2m(None, 3) == b'\x03') -assert(f.i2m(None, 30).lower() == b'\x1e') -assert(f.i2m(None, 31).lower() == b'\x1f\x00') -assert(f.i2m(None, 65536).lower() == b'\x1f\xe1\xff\x03') +assert f.i2m(None, 0) == b'\x00' +assert f.i2m(None, 3) == b'\x03' +assert f.i2m(None, 30).lower() == b'\x1e' +assert f.i2m(None, 31).lower() == b'\x1f\x00' +assert f.i2m(None, 65536).lower() == b'\x1f\xe1\xff\x03' = HTTP/2 UVarIntField.addfield on full byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 8) -assert(f.addfield(None, b'Toto', 0) == b'Toto\x00') -assert(f.addfield(None, b'Toto', 3) == b'Toto\x03') -assert(f.addfield(None, b'Toto', 254).lower() == b'toto\xfe') -assert(f.addfield(None, b'Toto', 255).lower() == b'toto\xff\x00') -assert(f.addfield(None, b'Toto', 766).lower() == b'toto\xff\xff\x03') +assert f.addfield(None, b'Toto', 0) == b'Toto\x00' +assert f.addfield(None, b'Toto', 3) == b'Toto\x03' +assert f.addfield(None, b'Toto', 254).lower() == b'toto\xfe' +assert f.addfield(None, b'Toto', 255).lower() == b'toto\xff\x00' +assert f.addfield(None, b'Toto', 766).lower() == b'toto\xff\xff\x03' = HTTP/2 UVarIntField.addfield on partial byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 5) -assert(f.addfield(None, (b'Toto', 3, 4), 0) == b'Toto\x80') -assert(f.addfield(None, (b'Toto', 3, 4), 3) == b'Toto\x83') -assert(f.addfield(None, (b'Toto', 3, 4), 30).lower() == b'toto\x9e') -assert(f.addfield(None, (b'Toto', 3, 4), 31).lower() == b'toto\x9f\x00') -assert(f.addfield(None, (b'Toto', 3, 4), 65536).lower() == b'toto\x9f\xe1\xff\x03') +assert f.addfield(None, (b'Toto', 3, 4), 0) == b'Toto\x80' +assert f.addfield(None, (b'Toto', 3, 4), 3) == b'Toto\x83' +assert f.addfield(None, (b'Toto', 3, 4), 30).lower() == b'toto\x9e' +assert f.addfield(None, (b'Toto', 3, 4), 31).lower() == b'toto\x9f\x00' +assert f.addfield(None, (b'Toto', 3, 4), 65536).lower() == b'toto\x9f\xe1\xff\x03' = HTTP/2 UVarIntField.i2len on full byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 8) -assert(f.i2len(None, 0) == 1) -assert(f.i2len(None, 3) == 1) -assert(f.i2len(None, 254) == 1) -assert(f.i2len(None, 255) == 2) -assert(f.i2len(None, 766) == 3) +assert f.i2len(None, 0) == 1 +assert f.i2len(None, 3) == 1 +assert f.i2len(None, 254) == 1 +assert f.i2len(None, 255) == 2 +assert f.i2len(None, 766) == 3 = HTTP/2 UVarIntField.i2len on partial byte ~ http2 frame field uvarintfield f = h2.UVarIntField('value', 0, 5) -assert(f.i2len(None, 0) == 1) -assert(f.i2len(None, 3) == 1) -assert(f.i2len(None, 30) == 1) -assert(f.i2len(None, 31) == 2) -assert(f.i2len(None, 65536) == 4) +assert f.i2len(None, 0) == 1 +assert f.i2len(None, 3) == 1 +assert f.i2len(None, 30) == 1 +assert f.i2len(None, 31) == 2 +assert f.i2len(None, 65536) == 4 + HTTP/2 FieldUVarLenField Test Suite @@ -184,11 +184,11 @@ class TrivialPacket(Packet): StrField('data', '') ] -assert(f.i2m(TrivialPacket(data='a'*5), None) == b'\x05') -assert(f.i2m(TrivialPacket(data='a'*255), None).lower() == b'\xff\x00') -assert(f.i2m(TrivialPacket(data='a'), 2) == b'\x02') -assert(f.i2m(None, 2) == b'\x02') -assert(f.i2m(None, 0) == b'\x00') +assert f.i2m(TrivialPacket(data='a'*5), None) == b'\x05' +assert f.i2m(TrivialPacket(data='a'*255), None).lower() == b'\xff\x00' +assert f.i2m(TrivialPacket(data='a'), 2) == b'\x02' +assert f.i2m(None, 2) == b'\x02' +assert f.i2m(None, 0) == b'\x00' = HTTP/2 FieldUVarLenField.i2m with adjustment ~ http2 frame field fielduvarlenfield @@ -201,10 +201,10 @@ class TrivialPacket(Packet): ] f = h2.FieldUVarLenField('value', None, 8, length_of='data', adjust=lambda x: x-1) -assert(f.i2m(TrivialPacket(data='a'*5), None) == b'\x04') -assert(f.i2m(TrivialPacket(data='a'*255), None).lower() == b'\xfe') +assert f.i2m(TrivialPacket(data='a'*5), None) == b'\x04' +assert f.i2m(TrivialPacket(data='a'*255), None).lower() == b'\xfe' #Adjustment does not affect non-None value! -assert(f.i2m(TrivialPacket(data='a'*3), 2) == b'\x02') +assert f.i2m(TrivialPacket(data='a'*3), 2) == b'\x02' + HTTP/2 HPackZString Test Suite @@ -213,26 +213,26 @@ assert(f.i2m(TrivialPacket(data='a'*3), 2) == b'\x02') string = 'Test' s = h2.HPackZString(string) -assert(len(s) == 3) -assert(raw(s) == b"\xdeT'") -assert(s.origin() == string) +assert len(s) == 3 +assert raw(s) == b"\xdeT'" +assert s.origin() == string string = 'a'*65535 s = h2.HPackZString(string) -assert(len(s) == 40960) -assert(raw(s) == (b'\x18\xc61\x8cc' * 8191) + b'\x18\xc61\x8c\x7f') -assert(s.origin() == string) +assert len(s) == 40960 +assert raw(s) == (b'\x18\xc61\x8cc' * 8191) + b'\x18\xc61\x8c\x7f' +assert s.origin() == string = HTTP/2 HPackZString Decompression ~ http2 hpack huffman s = b"\xdeT'" i, ibl = h2.HPackZString.huffman_conv2bitstring(s) -assert(b'Test' == h2.HPackZString.huffman_decode(i, ibl)) +assert b'Test' == h2.HPackZString.huffman_decode(i, ibl) s = (b'\x18\xc61\x8cc' * 8191) + b'\x18\xc61\x8c\x7f' i, ibl = h2.HPackZString.huffman_conv2bitstring(s) -assert(b'a'*65535 == h2.HPackZString.huffman_decode(i, ibl)) +assert b'a'*65535 == h2.HPackZString.huffman_decode(i, ibl) assert( expect_exception(h2.InvalidEncodingException, @@ -254,12 +254,12 @@ class TrivialPacket(Packet): ] s = f.m2i(TrivialPacket(type=0, len=4), b'Test') -assert(isinstance(s, h2.HPackLiteralString)) -assert(s.origin() == 'Test') +assert isinstance(s, h2.HPackLiteralString) +assert s.origin() == 'Test' s = f.m2i(TrivialPacket(type=1, len=3), b"\xdeT'") -assert(isinstance(s, h2.HPackZString)) -assert(s.origin() == 'Test') +assert isinstance(s, h2.HPackZString) +assert s.origin() == 'Test' = HTTP/2 HPackStrLenField.any2i ~ http2 hpack field hpackstrlenfield @@ -274,33 +274,33 @@ class TrivialPacket(Packet): ] s = f.any2i(TrivialPacket(type=0, len=4), b'Test') -assert(isinstance(s, h2.HPackLiteralString)) -assert(s.origin() == 'Test') +assert isinstance(s, h2.HPackLiteralString) +assert s.origin() == 'Test' s = f.any2i(TrivialPacket(type=1, len=3), b"\xdeT'") -assert(isinstance(s, h2.HPackZString)) -assert(s.origin() == 'Test') +assert isinstance(s, h2.HPackZString) +assert s.origin() == 'Test' s = h2.HPackLiteralString('Test') s2 = f.any2i(TrivialPacket(type=0, len=4), s) -assert(s.origin() == s2.origin()) +assert s.origin() == s2.origin() s = h2.HPackZString('Test') s2 = f.any2i(TrivialPacket(type=1, len=3), s) -assert(s.origin() == s2.origin()) +assert s.origin() == s2.origin() s = h2.HPackLiteralString('Test') s2 = f.any2i(None, s) -assert(s.origin() == s2.origin()) +assert s.origin() == s2.origin() s = h2.HPackZString('Test') s2 = f.any2i(None, s) -assert(s.origin() == s2.origin()) +assert s.origin() == s2.origin() # Verifies that one can fuzz s = h2.HPackLiteralString('Test') s2 = f.any2i(TrivialPacket(type=1, len=1), s) -assert(s.origin() == s2.origin()) +assert s.origin() == s2.origin() = HTTP/2 HPackStrLenField.i2m ~ http2 hpack field hpackstrlenfield @@ -309,11 +309,11 @@ f = h2.HPackStrLenField('data', h2.HPackLiteralString(''), length_from=lambda p: s = b'Test' s2 = f.i2m(None, h2.HPackLiteralString(s)) -assert(s == s2) +assert s == s2 s = b'Test' s2 = f.i2m(None, h2.HPackZString(s)) -assert(s2 == b"\xdeT'") +assert s2 == b"\xdeT'" = HTTP/2 HPackStrLenField.addfield ~ http2 hpack field hpackstrlenfield @@ -322,11 +322,11 @@ f = h2.HPackStrLenField('data', h2.HPackLiteralString(''), length_from=lambda p: s = b'Test' s2 = f.addfield(None, b'Toto', h2.HPackLiteralString(s)) -assert(b'Toto' + s == s2) +assert b'Toto' + s == s2 s = b'Test' s2 = f.addfield(None, b'Toto', h2.HPackZString(s)) -assert(s2 == b"Toto\xdeT'") +assert s2 == b"Toto\xdeT'" = HTTP/2 HPackStrLenField.getfield ~ http2 hpack field hpackstrlenfield @@ -341,16 +341,16 @@ class TrivialPacket(Packet): ] r = f.getfield(TrivialPacket(type=0, len=4), b'TestToto') -assert(isinstance(r, tuple)) -assert(r[0] == b'Toto') -assert(isinstance(r[1], h2.HPackLiteralString)) -assert(r[1].origin() == 'Test') +assert isinstance(r, tuple) +assert r[0] == b'Toto' +assert isinstance(r[1], h2.HPackLiteralString) +assert r[1].origin() == 'Test' r = f.getfield(TrivialPacket(type=1, len=3), b"\xdeT'Toto") -assert(isinstance(r, tuple)) -assert(r[0] == b'Toto') -assert(isinstance(r[1], h2.HPackZString)) -assert(r[1].origin() == 'Test') +assert isinstance(r, tuple) +assert r[0] == b'Toto' +assert isinstance(r[1], h2.HPackZString) +assert r[1].origin() == 'Test' = HTTP/2 HPackStrLenField.i2h / i2repr ~ http2 hpack field hpackstrlenfield @@ -358,11 +358,11 @@ assert(r[1].origin() == 'Test') f = h2.HPackStrLenField('data', h2.HPackLiteralString(''), length_from=lambda p: p.len, type_from='type') s = b'Test' -assert(f.i2h(None, h2.HPackLiteralString(s)) == 'HPackLiteralString(Test)') -assert(f.i2repr(None, h2.HPackLiteralString(s)) == repr('HPackLiteralString(Test)')) +assert f.i2h(None, h2.HPackLiteralString(s)) == 'HPackLiteralString(Test)' +assert f.i2repr(None, h2.HPackLiteralString(s)) == repr('HPackLiteralString(Test)') -assert(f.i2h(None, h2.HPackZString(s)) == 'HPackZString(Test)') -assert(f.i2repr(None, h2.HPackZString(s)) == repr('HPackZString(Test)')) +assert f.i2h(None, h2.HPackZString(s)) == 'HPackZString(Test)' +assert f.i2repr(None, h2.HPackZString(s)) == repr('HPackZString(Test)') = HTTP/2 HPackStrLenField.i2len ~ http2 hpack field hpackstrlenfield @@ -370,8 +370,8 @@ assert(f.i2repr(None, h2.HPackZString(s)) == repr('HPackZString(Test)')) f = h2.HPackStrLenField('data', h2.HPackLiteralString(''), length_from=lambda p: p.len, type_from='type') s = b'Test' -assert(f.i2len(None, h2.HPackLiteralString(s)) == 4) -assert(f.i2len(None, h2.HPackZString(s)) == 3) +assert f.i2len(None, h2.HPackLiteralString(s)) == 4 +assert f.i2len(None, h2.HPackZString(s)) == 3 + HTTP/2 HPackMagicBitField Test Suite # Magic bits are not supposed to be modified and if they are anyway, they must @@ -382,18 +382,18 @@ assert(f.i2len(None, h2.HPackZString(s)) == 3) f = h2.HPackMagicBitField('value', 3, 2) r = f.addfield(None, b'Toto', 3) -assert(isinstance(r, tuple)) -assert(r[0] == b'Toto') -assert(r[1] == 2) -assert(r[2] == 3) +assert isinstance(r, tuple) +assert r[0] == b'Toto' +assert r[1] == 2 +assert r[2] == 3 r = f.addfield(None, (b'Toto', 2, 1) , 3) -assert(isinstance(r, tuple)) -assert(r[0] == b'Toto') -assert(r[1] == 4) -assert(r[2] == 7) +assert isinstance(r, tuple) +assert r[0] == b'Toto' +assert r[1] == 4 +assert r[2] == 7 -assert(expect_exception(AssertionError, 'f.addfield(None, "toto", 2)')) +assert expect_exception(AssertionError, 'f.addfield(None, "toto", 2)') = HTTP/2 HPackMagicBitField.getfield ~ http2 hpack field hpackmagicbitfield @@ -401,20 +401,20 @@ assert(expect_exception(AssertionError, 'f.addfield(None, "toto", 2)')) f = h2.HPackMagicBitField('value', 3, 2) r = f.getfield(None, b'\xc0') -assert(isinstance(r, tuple)) -assert(len(r) == 2) -assert(isinstance(r[0], tuple)) -assert(len(r[0]) == 2) -assert(r[0][0] == b'\xc0') -assert(r[0][1] == 2) -assert(r[1] == 3) +assert isinstance(r, tuple) +assert len(r) == 2 +assert isinstance(r[0], tuple) +assert len(r[0]) == 2 +assert r[0][0] == b'\xc0' +assert r[0][1] == 2 +assert r[1] == 3 r = f.getfield(None, (b'\x03', 6)) -assert(isinstance(r, tuple)) -assert(len(r) == 2) -assert(isinstance(r[0], bytes)) -assert(r[0] == b'') -assert(r[1] == 3) +assert isinstance(r, tuple) +assert len(r) == 2 +assert isinstance(r[0], bytes) +assert r[0] == b'' +assert r[1] == 3 expect_exception(AssertionError, 'f.getfield(None, b"\\x80")') @@ -422,28 +422,28 @@ expect_exception(AssertionError, 'f.getfield(None, b"\\x80")') ~ http2 hpack field hpackmagicbitfield f = h2.HPackMagicBitField('value', 3, 2) -assert(f.h2i(None, 3) == 3) +assert f.h2i(None, 3) == 3 expect_exception(AssertionError, 'f.h2i(None, 2)') = HTTP/2 HPackMagicBitField.m2i ~ http2 hpack field hpackmagicbitfield f = h2.HPackMagicBitField('value', 3, 2) -assert(f.m2i(None, 3) == 3) +assert f.m2i(None, 3) == 3 expect_exception(AssertionError, 'f.m2i(None, 2)') = HTTP/2 HPackMagicBitField.i2m ~ http2 hpack field hpackmagicbitfield f = h2.HPackMagicBitField('value', 3, 2) -assert(f.i2m(None, 3) == 3) +assert f.i2m(None, 3) == 3 expect_exception(AssertionError, 'f.i2m(None, 2)') = HTTP/2 HPackMagicBitField.any2i ~ http2 hpack field hpackmagicbitfield f = h2.HPackMagicBitField('value', 3, 2) -assert(f.any2i(None, 3) == 3) +assert f.any2i(None, 3) == 3 expect_exception(AssertionError, 'f.any2i(None, 2)') + HTTP/2 HPackHdrString Test Suite @@ -452,29 +452,29 @@ expect_exception(AssertionError, 'f.any2i(None, 2)') ~ http2 pack dissect hpackhdrstring p = h2.HPackHdrString(b'\x04Test') -assert(p.type == 0) -assert(p.len == 4) -assert(isinstance(p.getfieldval('data'), h2.HPackLiteralString)) -assert(p.getfieldval('data').origin() == 'Test') +assert p.type == 0 +assert p.len == 4 +assert isinstance(p.getfieldval('data'), h2.HPackLiteralString) +assert p.getfieldval('data').origin() == 'Test' p = h2.HPackHdrString(b"\x83\xdeT'") -assert(p.type == 1) -assert(p.len == 3) -assert(isinstance(p.getfieldval('data'), h2.HPackZString)) -assert(p.getfieldval('data').origin() == 'Test') +assert p.type == 1 +assert p.len == 3 +assert isinstance(p.getfieldval('data'), h2.HPackZString) +assert p.getfieldval('data').origin() == 'Test' = HTTP/2 Build HPackHdrString ~ http2 hpack build hpackhdrstring p = h2.HPackHdrString(data=h2.HPackLiteralString('Test')) -assert(raw(p) == b'\x04Test') +assert raw(p) == b'\x04Test' p = h2.HPackHdrString(data=h2.HPackZString('Test')) -assert(raw(p) == b"\x83\xdeT'") +assert raw(p) == b"\x83\xdeT'" #Fuzzing-able tests p = h2.HPackHdrString(type=1, len=3, data=h2.HPackLiteralString('Test')) -assert(raw(p) == b'\x83Test') +assert raw(p) == b'\x83Test' + HTTP/2 HPackIndexedHdr Test Suite @@ -482,21 +482,21 @@ assert(raw(p) == b'\x83Test') ~ http2 hpack dissect hpackindexedhdr p = h2.HPackIndexedHdr(b'\x80') -assert(p.magic == 1) -assert(p.index == 0) +assert p.magic == 1 +assert p.index == 0 p = h2.HPackIndexedHdr(b'\xFF\x00') -assert(p.magic == 1) -assert(p.index == 127) +assert p.magic == 1 +assert p.index == 127 = HTTP/2 Build HPackIndexedHdr ~ http2 hpack build hpackindexedhdr p = h2.HPackIndexedHdr(index=0) -assert(raw(p) == b'\x80') +assert raw(p) == b'\x80' p = h2.HPackIndexedHdr(index=127) -assert(raw(p) == b'\xFF\x00') +assert raw(p) == b'\xFF\x00' + HTTP/2 HPackLitHdrFldWithIncrIndexing Test Suite @@ -504,28 +504,28 @@ assert(raw(p) == b'\xFF\x00') ~ http2 hpack dissect hpacklithdrfldwithincrindexing p = h2.HPackLitHdrFldWithIncrIndexing(b'\x40\x04Test\x04Toto') -assert(p.magic == 1) -assert(p.index == 0) -assert(isinstance(p.hdr_name, h2.HPackHdrString)) -assert(p.hdr_name.type == 0) -assert(p.hdr_name.len == 4) -assert(p.hdr_name.getfieldval('data').origin() == 'Test') -assert(isinstance(p.hdr_value, h2.HPackHdrString)) -assert(p.hdr_value.type == 0) -assert(p.hdr_value.len == 4) -assert(p.hdr_value.getfieldval('data').origin() == 'Toto') +assert p.magic == 1 +assert p.index == 0 +assert isinstance(p.hdr_name, h2.HPackHdrString) +assert p.hdr_name.type == 0 +assert p.hdr_name.len == 4 +assert p.hdr_name.getfieldval('data').origin() == 'Test' +assert isinstance(p.hdr_value, h2.HPackHdrString) +assert p.hdr_value.type == 0 +assert p.hdr_value.len == 4 +assert p.hdr_value.getfieldval('data').origin() == 'Toto' = HTTP/2 Dissect HPackLitHdrFldWithIncrIndexing with indexed name ~ http2 hpack dissect hpacklithdrfldwithincrindexing p = h2.HPackLitHdrFldWithIncrIndexing(b'\x41\x04Toto') -assert(p.magic == 1) -assert(p.index == 1) -assert(p.hdr_name is None) -assert(isinstance(p.hdr_value, h2.HPackHdrString)) -assert(p.hdr_value.type == 0) -assert(p.hdr_value.len == 4) -assert(p.hdr_value.getfieldval('data').origin() == 'Toto') +assert p.magic == 1 +assert p.index == 1 +assert p.hdr_name is None +assert isinstance(p.hdr_value, h2.HPackHdrString) +assert p.hdr_value.type == 0 +assert p.hdr_value.len == 4 +assert p.hdr_value.getfieldval('data').origin() == 'Toto' = HTTP/2 Build HPackLitHdrFldWithIncrIndexing without indexed name @@ -535,7 +535,7 @@ p = h2.HPackLitHdrFldWithIncrIndexing( hdr_name=h2.HPackHdrString(data=h2.HPackLiteralString('Test')), hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString(b'Toto')) ) -assert(raw(p) == b'\x40\x04Test\x04Toto') +assert raw(p) == b'\x40\x04Test\x04Toto' = HTTP/2 Build HPackLitHdrFldWithIncrIndexing with indexed name ~ http2 hpack build hpacklithdrfldwithincrindexing @@ -544,7 +544,7 @@ p = h2.HPackLitHdrFldWithIncrIndexing( index=1, hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString(b'Toto')) ) -assert(raw(p) == b'\x41\x04Toto') +assert raw(p) == b'\x41\x04Toto' + HTTP/2 HPackLitHdrFldWithoutIndexing Test Suite @@ -552,51 +552,51 @@ assert(raw(p) == b'\x41\x04Toto') ~ http2 hpack dissect hpacklithdrfldwithoutindexing p = h2.HPackLitHdrFldWithoutIndexing(b'\x00\x04Test\x04Toto') -assert(p.magic == 0) -assert(p.never_index == 0) -assert(p.index == 0) -assert(isinstance(p.hdr_name, h2.HPackHdrString)) -assert(p.hdr_name.type == 0) -assert(p.hdr_name.len == 4) -assert(isinstance(p.hdr_name.getfieldval('data'), h2.HPackLiteralString)) -assert(p.hdr_name.getfieldval('data').origin() == 'Test') -assert(isinstance(p.hdr_value, h2.HPackHdrString)) -assert(p.hdr_value.type == 0) -assert(p.hdr_value.len == 4) -assert(isinstance(p.hdr_value.getfieldval('data'), h2.HPackLiteralString)) -assert(p.hdr_value.getfieldval('data').origin() == 'Toto') +assert p.magic == 0 +assert p.never_index == 0 +assert p.index == 0 +assert isinstance(p.hdr_name, h2.HPackHdrString) +assert p.hdr_name.type == 0 +assert p.hdr_name.len == 4 +assert isinstance(p.hdr_name.getfieldval('data'), h2.HPackLiteralString) +assert p.hdr_name.getfieldval('data').origin() == 'Test' +assert isinstance(p.hdr_value, h2.HPackHdrString) +assert p.hdr_value.type == 0 +assert p.hdr_value.len == 4 +assert isinstance(p.hdr_value.getfieldval('data'), h2.HPackLiteralString) +assert p.hdr_value.getfieldval('data').origin() == 'Toto' = HTTP/2 Dissect HPackLitHdrFldWithoutIndexing : never index index and no index ~ http2 hpack dissect hpacklithdrfldwithoutindexing p = h2.HPackLitHdrFldWithoutIndexing(b'\x10\x04Test\x04Toto') -assert(p.magic == 0) -assert(p.never_index == 1) -assert(p.index == 0) -assert(isinstance(p.hdr_name, h2.HPackHdrString)) -assert(p.hdr_name.type == 0) -assert(p.hdr_name.len == 4) -assert(isinstance(p.hdr_name.getfieldval('data'), h2.HPackLiteralString)) -assert(p.hdr_name.getfieldval('data').origin() == 'Test') -assert(isinstance(p.hdr_value, h2.HPackHdrString)) -assert(p.hdr_value.type == 0) -assert(p.hdr_value.len == 4) -assert(isinstance(p.hdr_value.getfieldval('data'), h2.HPackLiteralString)) -assert(p.hdr_value.getfieldval('data').origin() == 'Toto') +assert p.magic == 0 +assert p.never_index == 1 +assert p.index == 0 +assert isinstance(p.hdr_name, h2.HPackHdrString) +assert p.hdr_name.type == 0 +assert p.hdr_name.len == 4 +assert isinstance(p.hdr_name.getfieldval('data'), h2.HPackLiteralString) +assert p.hdr_name.getfieldval('data').origin() == 'Test' +assert isinstance(p.hdr_value, h2.HPackHdrString) +assert p.hdr_value.type == 0 +assert p.hdr_value.len == 4 +assert isinstance(p.hdr_value.getfieldval('data'), h2.HPackLiteralString) +assert p.hdr_value.getfieldval('data').origin() == 'Toto' = HTTP/2 Dissect HPackLitHdrFldWithoutIndexing : never index and indexed name ~ http2 hpack dissect hpacklithdrfldwithoutindexing p = h2.HPackLitHdrFldWithoutIndexing(b'\x11\x04Toto') -assert(p.magic == 0) -assert(p.never_index == 1) -assert(p.index == 1) -assert(p.hdr_name is None) -assert(isinstance(p.hdr_value, h2.HPackHdrString)) -assert(p.hdr_value.type == 0) -assert(p.hdr_value.len == 4) -assert(isinstance(p.hdr_value.getfieldval('data'), h2.HPackLiteralString)) -assert(p.hdr_value.getfieldval('data').origin() == 'Toto') +assert p.magic == 0 +assert p.never_index == 1 +assert p.index == 1 +assert p.hdr_name is None +assert isinstance(p.hdr_value, h2.HPackHdrString) +assert p.hdr_value.type == 0 +assert p.hdr_value.len == 4 +assert isinstance(p.hdr_value.getfieldval('data'), h2.HPackLiteralString) +assert p.hdr_value.getfieldval('data').origin() == 'Toto' = HTTP/2 Build HPackLitHdrFldWithoutIndexing : don't index and no index ~ http2 hpack build hpacklithdrfldwithoutindexing @@ -605,7 +605,7 @@ p = h2.HPackLitHdrFldWithoutIndexing( hdr_name=h2.HPackHdrString(data=h2.HPackLiteralString('Test')), hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString(b'Toto')) ) -assert(raw(p) == b'\x00\x04Test\x04Toto') +assert raw(p) == b'\x00\x04Test\x04Toto' = HTTP/2 Build HPackLitHdrFldWithoutIndexing : never index index and no index ~ http2 hpack build hpacklithdrfldwithoutindexing @@ -615,7 +615,7 @@ p = h2.HPackLitHdrFldWithoutIndexing( hdr_name=h2.HPackHdrString(data=h2.HPackLiteralString('Test')), hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString(b'Toto')) ) -assert(raw(p) == b'\x10\x04Test\x04Toto') +assert raw(p) == b'\x10\x04Test\x04Toto' = HTTP/2 Build HPackLitHdrFldWithoutIndexing : never index and indexed name ~ http2 hpack build hpacklithdrfldwithoutindexing @@ -625,7 +625,7 @@ p = h2.HPackLitHdrFldWithoutIndexing( index=1, hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString(b'Toto')) ) -assert(raw(p) == b'\x11\x04Toto') +assert raw(p) == b'\x11\x04Toto' + HTTP/2 HPackDynamicSizeUpdate Test Suite @@ -633,19 +633,19 @@ assert(raw(p) == b'\x11\x04Toto') ~ http2 hpack dissect hpackdynamicsizeupdate p = h2.HPackDynamicSizeUpdate(b'\x25') -assert(p.magic == 1) -assert(p.max_size == 5) +assert p.magic == 1 +assert p.max_size == 5 p = h2.HPackDynamicSizeUpdate(b'\x3F\x00') -assert(p.magic == 1) -assert(p.max_size == 31) +assert p.magic == 1 +assert p.max_size == 31 = HTTP/2 Build HPackDynamicSizeUpdate ~ http2 hpack build hpackdynamicsizeupdate p = h2.HPackDynamicSizeUpdate(max_size=5) -assert(raw(p) == b'\x25') +assert raw(p) == b'\x25' p = h2.HPackDynamicSizeUpdate(max_size=31) -assert(raw(p) == b'\x3F\x00') +assert raw(p) == b'\x3F\x00' + HTTP/2 Data Frame Test Suite @@ -653,85 +653,85 @@ assert(raw(p) == b'\x3F\x00') ~ http2 frame dissect data pkt = h2.H2Frame(b'\x00\x00\x04\x00\x00\x00\x00\x00\x01ABCD') -assert(pkt.type == 0) -assert(pkt.len == 4) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2DataFrame)) -assert(pkt[h2.H2DataFrame]) -assert(pkt.payload.data == b'ABCD') -assert(isinstance(pkt.payload.payload, scapy.packet.NoPayload)) +assert pkt.type == 0 +assert pkt.len == 4 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2DataFrame) +assert pkt[h2.H2DataFrame] +assert pkt.payload.data == b'ABCD' +assert isinstance(pkt.payload.payload, scapy.packet.NoPayload) = HTTP/2 Build Data Frame: Simple data frame ~ http2 frame build data pkt = h2.H2Frame(stream_id = 1)/h2.H2DataFrame(data='ABCD') -assert(raw(pkt) == b'\x00\x00\x04\x00\x00\x00\x00\x00\x01ABCD') +assert raw(pkt) == b'\x00\x00\x04\x00\x00\x00\x00\x00\x01ABCD' try: pkt.show2(dump=True) - assert(True) + assert True except Exception: - assert(False) + assert False = HTTP/2 Dissect Data Frame: Simple data frame with padding ~ http2 frame dissect data pkt = h2.H2Frame(b'\x00\x00\r\x00\x08\x00\x00\x00\x01\x08ABCD\x00\x00\x00\x00\x00\x00\x00\x00') #Padded data frame -assert(pkt.type == 0) -assert(pkt.len == 13) -assert(len(pkt.flags) == 1) -assert('P' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2PaddedDataFrame)) -assert(pkt[h2.H2PaddedDataFrame]) -assert(pkt.payload.padlen == 8) -assert(pkt.payload.data == b'ABCD') -assert(pkt.payload.padding == b'\x00'*8) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(isinstance(pkt.payload.payload, scapy.packet.NoPayload)) +assert pkt.type == 0 +assert pkt.len == 13 +assert len(pkt.flags) == 1 +assert 'P' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2PaddedDataFrame) +assert pkt[h2.H2PaddedDataFrame] +assert pkt.payload.padlen == 8 +assert pkt.payload.data == b'ABCD' +assert pkt.payload.padding == b'\x00'*8 +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert isinstance(pkt.payload.payload, scapy.packet.NoPayload) = HTTP/2 Build Data Frame: Simple data frame with padding ~ http2 frame build data pkt = h2.H2Frame(flags = {'P'}, stream_id = 1)/h2.H2PaddedDataFrame(data='ABCD', padding=b'\x00'*8) -assert(raw(pkt) == b'\x00\x00\r\x00\x08\x00\x00\x00\x01\x08ABCD\x00\x00\x00\x00\x00\x00\x00\x00') +assert raw(pkt) == b'\x00\x00\r\x00\x08\x00\x00\x00\x01\x08ABCD\x00\x00\x00\x00\x00\x00\x00\x00' try: pkt.show2(dump=True) - assert(True) + assert True except Exception: - assert(False) + assert False = HTTP/2 Dissect Data Frame: Simple data frame with padding and end stream flag ~ http2 frame dissect data pkt = h2.H2Frame(b'\x00\x00\r\x00\t\x00\x00\x00\x01\x08ABCD\x00\x00\x00\x00\x00\x00\x00\x00') #Padded data frame with end stream flag -assert(pkt.type == 0) -assert(pkt.len == 13) -assert(len(pkt.flags) == 2) -assert('P' in pkt.flags) -assert('ES' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2PaddedDataFrame)) -assert(pkt[h2.H2PaddedDataFrame]) -assert(pkt.payload.padlen == 8) -assert(pkt.payload.data == b'ABCD') -assert(pkt.payload.padding == b'\x00'*8) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(isinstance(pkt.payload.payload, scapy.packet.NoPayload)) +assert pkt.type == 0 +assert pkt.len == 13 +assert len(pkt.flags) == 2 +assert 'P' in pkt.flags +assert 'ES' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2PaddedDataFrame) +assert pkt[h2.H2PaddedDataFrame] +assert pkt.payload.padlen == 8 +assert pkt.payload.data == b'ABCD' +assert pkt.payload.padding == b'\x00'*8 +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert isinstance(pkt.payload.payload, scapy.packet.NoPayload) = HTTP/2 Build Data Frame: Simple data frame with padding and end stream flag ~ http2 frame build data pkt = h2.H2Frame(flags = {'P', 'ES'}, stream_id=1)/h2.H2PaddedDataFrame(data='ABCD', padding=b'\x00'*8) -assert(raw(pkt) == b'\x00\x00\r\x00\t\x00\x00\x00\x01\x08ABCD\x00\x00\x00\x00\x00\x00\x00\x00') +assert raw(pkt) == b'\x00\x00\r\x00\t\x00\x00\x00\x01\x08ABCD\x00\x00\x00\x00\x00\x00\x00\x00' try: pkt.show2(dump=True) - assert(True) + assert True except Exception: - assert(False) + assert False + HTTP/2 Headers Frame Test Suite @@ -739,30 +739,30 @@ except Exception: ~ http2 frame dissect headers pkt = h2.H2Frame(b'\x00\x00\x0e\x01\x00\x00\x00\x00\x01\x88\x0f\x10\ntext/plain') #Header frame -assert(pkt.type == 1) -assert(pkt.len == 14) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2HeadersFrame)) -assert(pkt[h2.H2HeadersFrame]) +assert pkt.type == 1 +assert pkt.len == 14 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2HeadersFrame) +assert pkt[h2.H2HeadersFrame] hf = pkt[h2.H2HeadersFrame] -assert(len(hf.hdrs) == 2) -assert(isinstance(hf.hdrs[0], h2.HPackIndexedHdr)) -assert(hf.hdrs[0].magic == 1) -assert(hf.hdrs[0].index == 8) -assert(isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing)) -assert(hf.hdrs[1].magic == 0) -assert(hf.hdrs[1].never_index == 0) -assert(hf.hdrs[1].index == 31) -assert(hf.hdrs[1].hdr_name is None) -assert(expect_exception(AttributeError, 'hf.hdrs[1].non_existant')) -assert(isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString)) +assert len(hf.hdrs) == 2 +assert isinstance(hf.hdrs[0], h2.HPackIndexedHdr) +assert hf.hdrs[0].magic == 1 +assert hf.hdrs[0].index == 8 +assert isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing) +assert hf.hdrs[1].magic == 0 +assert hf.hdrs[1].never_index == 0 +assert hf.hdrs[1].index == 31 +assert hf.hdrs[1].hdr_name is None +assert expect_exception(AttributeError, 'hf.hdrs[1].non_existant') +assert isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString) s = hf.hdrs[1].hdr_value -assert(s.type == 0) -assert(s.len == 10) -assert(s.getfieldval('data').origin() == 'text/plain') -assert(isinstance(hf.payload, scapy.packet.NoPayload)) +assert s.type == 0 +assert s.len == 10 +assert s.getfieldval('data').origin() == 'text/plain' +assert isinstance(hf.payload, scapy.packet.NoPayload) = HTTP/2 Build Headers Frame: Simple header frame ~ http2 frame build headers @@ -775,40 +775,40 @@ p = h2.H2Frame(stream_id=1)/h2.H2HeadersFrame(hdrs=[ ) ] ) -assert(raw(p) == b'\x00\x00\x0e\x01\x00\x00\x00\x00\x01\x88\x0f\x10\ntext/plain') +assert raw(p) == b'\x00\x00\x0e\x01\x00\x00\x00\x00\x01\x88\x0f\x10\ntext/plain' = HTTP/2 Dissect Headers Frame: Header frame with padding ~ http2 frame dissect headers pkt = h2.H2Frame(b'\x00\x00\x17\x01\x08\x00\x00\x00\x01\x08\x88\x0f\x10\ntext/plain\x00\x00\x00\x00\x00\x00\x00\x00') #Header frame with padding -assert(pkt.type == 1) -assert(pkt.len == 23) -assert(len(pkt.flags) == 1) -assert('P' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2PaddedHeadersFrame)) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(pkt[h2.H2PaddedHeadersFrame]) +assert pkt.type == 1 +assert pkt.len == 23 +assert len(pkt.flags) == 1 +assert 'P' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2PaddedHeadersFrame) +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert pkt[h2.H2PaddedHeadersFrame] hf = pkt[h2.H2PaddedHeadersFrame] -assert(hf.padlen == 8) -assert(hf.padding == b'\x00' * 8) -assert(len(hf.hdrs) == 2) -assert(isinstance(hf.hdrs[0], h2.HPackIndexedHdr)) -assert(hf.hdrs[0].magic == 1) -assert(hf.hdrs[0].index == 8) -assert(isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing)) -assert(hf.hdrs[1].magic == 0) -assert(hf.hdrs[1].never_index == 0) -assert(hf.hdrs[1].index == 31) -assert(hf.hdrs[1].hdr_name is None) -assert(expect_exception(AttributeError, 'hf.hdrs[1].non_existant')) -assert(isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString)) +assert hf.padlen == 8 +assert hf.padding == b'\x00' * 8 +assert len(hf.hdrs) == 2 +assert isinstance(hf.hdrs[0], h2.HPackIndexedHdr) +assert hf.hdrs[0].magic == 1 +assert hf.hdrs[0].index == 8 +assert isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing) +assert hf.hdrs[1].magic == 0 +assert hf.hdrs[1].never_index == 0 +assert hf.hdrs[1].index == 31 +assert hf.hdrs[1].hdr_name is None +assert expect_exception(AttributeError, 'hf.hdrs[1].non_existant') +assert isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString) s = hf.hdrs[1].hdr_value -assert(s.type == 0) -assert(s.len == 10) -assert(s.getfieldval('data').origin() == 'text/plain') -assert(isinstance(hf.payload, scapy.packet.NoPayload)) +assert s.type == 0 +assert s.len == 10 +assert s.getfieldval('data').origin() == 'text/plain' +assert isinstance(hf.payload, scapy.packet.NoPayload) = HTTP/2 Build Headers Frame: Header frame with padding ~ http2 frame build headers @@ -823,41 +823,41 @@ p = h2.H2Frame(flags={'P'}, stream_id=1)/h2.H2PaddedHeadersFrame( ], padding=b'\x00'*8, ) -assert(raw(p) == b'\x00\x00\x17\x01\x08\x00\x00\x00\x01\x08\x88\x0f\x10\ntext/plain\x00\x00\x00\x00\x00\x00\x00\x00') +assert raw(p) == b'\x00\x00\x17\x01\x08\x00\x00\x00\x01\x08\x88\x0f\x10\ntext/plain\x00\x00\x00\x00\x00\x00\x00\x00' = HTTP/2 Dissect Headers Frame: Header frame with priority ~ http2 frame dissect headers pkt = h2.H2Frame(b'\x00\x00\x13\x01 \x00\x00\x00\x01\x00\x00\x00\x02d\x88\x0f\x10\ntext/plain') #Header frame with priority -assert(pkt.type == 1) -assert(pkt.len == 19) -assert(len(pkt.flags) == 1) -assert('+' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2PriorityHeadersFrame)) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(pkt[h2.H2PriorityHeadersFrame]) +assert pkt.type == 1 +assert pkt.len == 19 +assert len(pkt.flags) == 1 +assert '+' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2PriorityHeadersFrame) +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert pkt[h2.H2PriorityHeadersFrame] hf = pkt[h2.H2PriorityHeadersFrame] -assert(hf.exclusive == 0) -assert(hf.stream_dependency == 2) -assert(hf.weight == 100) -assert(len(hf.hdrs) == 2) -assert(isinstance(hf.hdrs[0], h2.HPackIndexedHdr)) -assert(hf.hdrs[0].magic == 1) -assert(hf.hdrs[0].index == 8) -assert(isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing)) -assert(hf.hdrs[1].magic == 0) -assert(hf.hdrs[1].never_index == 0) -assert(hf.hdrs[1].index == 31) -assert(hf.hdrs[1].hdr_name is None) -assert(expect_exception(AttributeError, 'hf.hdrs[1].non_existant')) -assert(isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString)) +assert hf.exclusive == 0 +assert hf.stream_dependency == 2 +assert hf.weight == 100 +assert len(hf.hdrs) == 2 +assert isinstance(hf.hdrs[0], h2.HPackIndexedHdr) +assert hf.hdrs[0].magic == 1 +assert hf.hdrs[0].index == 8 +assert isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing) +assert hf.hdrs[1].magic == 0 +assert hf.hdrs[1].never_index == 0 +assert hf.hdrs[1].index == 31 +assert hf.hdrs[1].hdr_name is None +assert expect_exception(AttributeError, 'hf.hdrs[1].non_existant') +assert isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString) s = hf.hdrs[1].hdr_value -assert(s.type == 0) -assert(s.len == 10) -assert(s.getfieldval('data').origin() == 'text/plain') -assert(isinstance(hf.payload, scapy.packet.NoPayload)) +assert s.type == 0 +assert s.len == 10 +assert s.getfieldval('data').origin() == 'text/plain' +assert isinstance(hf.payload, scapy.packet.NoPayload) = HTTP/2 Build Headers Frame: Header frame with priority ~ http2 frame build headers @@ -874,46 +874,46 @@ p = h2.H2Frame(flags={'+'}, stream_id=1)/h2.H2PriorityHeadersFrame( ) ] ) -assert(raw(p) == b'\x00\x00\x13\x01 \x00\x00\x00\x01\x00\x00\x00\x02d\x88\x0f\x10\ntext/plain') +assert raw(p) == b'\x00\x00\x13\x01 \x00\x00\x00\x01\x00\x00\x00\x02d\x88\x0f\x10\ntext/plain' = HTTP/2 Dissect Headers Frame: Header frame with priority and padding and flags ~ http2 frame dissect headers pkt = h2.H2Frame(b'\x00\x00\x1c\x01-\x00\x00\x00\x01\x08\x00\x00\x00\x02d\x88\x0f\x10\ntext/plain\x00\x00\x00\x00\x00\x00\x00\x00') #Header frame with priority and padding and flags ES|EH -assert(pkt.type == 1) -assert(pkt.len == 28) -assert(len(pkt.flags) == 4) -assert('+' in pkt.flags) -assert('P' in pkt.flags) -assert('ES' in pkt.flags) -assert('EH' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2PaddedPriorityHeadersFrame)) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(pkt[h2.H2PaddedPriorityHeadersFrame]) +assert pkt.type == 1 +assert pkt.len == 28 +assert len(pkt.flags) == 4 +assert '+' in pkt.flags +assert 'P' in pkt.flags +assert 'ES' in pkt.flags +assert 'EH' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2PaddedPriorityHeadersFrame) +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert pkt[h2.H2PaddedPriorityHeadersFrame] hf = pkt[h2.H2PaddedPriorityHeadersFrame] -assert(hf.padlen == 8) -assert(hf.padding == b'\x00' * 8) -assert(hf.exclusive == 0) -assert(hf.stream_dependency == 2) -assert(hf.weight == 100) -assert(len(hf.hdrs) == 2) -assert(isinstance(hf.hdrs[0], h2.HPackIndexedHdr)) -assert(hf.hdrs[0].magic == 1) -assert(hf.hdrs[0].index == 8) -assert(isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing)) -assert(hf.hdrs[1].magic == 0) -assert(hf.hdrs[1].never_index == 0) -assert(hf.hdrs[1].index == 31) -assert(hf.hdrs[1].hdr_name is None) -assert(expect_exception(AttributeError, 'hf.hdrs[1].non_existant')) -assert(isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString)) +assert hf.padlen == 8 +assert hf.padding == b'\x00' * 8 +assert hf.exclusive == 0 +assert hf.stream_dependency == 2 +assert hf.weight == 100 +assert len(hf.hdrs) == 2 +assert isinstance(hf.hdrs[0], h2.HPackIndexedHdr) +assert hf.hdrs[0].magic == 1 +assert hf.hdrs[0].index == 8 +assert isinstance(hf.hdrs[1], h2.HPackLitHdrFldWithoutIndexing) +assert hf.hdrs[1].magic == 0 +assert hf.hdrs[1].never_index == 0 +assert hf.hdrs[1].index == 31 +assert hf.hdrs[1].hdr_name is None +assert expect_exception(AttributeError, 'hf.hdrs[1].non_existant') +assert isinstance(hf.hdrs[1].hdr_value, h2.HPackHdrString) s = hf.hdrs[1].hdr_value -assert(s.type == 0) -assert(s.len == 10) -assert(s.getfieldval('data').origin() == 'text/plain') -assert(isinstance(hf.payload, scapy.packet.NoPayload)) +assert s.type == 0 +assert s.len == 10 +assert s.getfieldval('data').origin() == 'text/plain' +assert isinstance(hf.payload, scapy.packet.NoPayload) = HTTP/2 Build Headers Frame: Header frame with priority and padding and flags ~ http2 frame build headers @@ -938,17 +938,17 @@ p = h2.H2Frame(flags={'P', '+', 'ES', 'EH'}, stream_id=1)/h2.H2PaddedPriorityHea ~ http2 frame dissect priority pkt = h2.H2Frame(b'\x00\x00\x05\x02\x00\x00\x00\x00\x03\x80\x00\x00\x01d') -assert(pkt.type == 2) -assert(pkt.len == 5) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 3) -assert(isinstance(pkt.payload, h2.H2PriorityFrame)) -assert(pkt[h2.H2PriorityFrame]) +assert pkt.type == 2 +assert pkt.len == 5 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 3 +assert isinstance(pkt.payload, h2.H2PriorityFrame) +assert pkt[h2.H2PriorityFrame] pp = pkt[h2.H2PriorityFrame] -assert(pp.stream_dependency == 1) -assert(pp.exclusive == 1) -assert(pp.weight == 100) +assert pp.stream_dependency == 1 +assert pp.exclusive == 1 +assert pp.weight == 100 = HTTP/2 Build Priority Frame ~ http2 frame build priority @@ -958,7 +958,7 @@ p = h2.H2Frame(stream_id=3)/h2.H2PriorityFrame( stream_dependency=1, weight=100 ) -assert(raw(p) == b'\x00\x00\x05\x02\x00\x00\x00\x00\x03\x80\x00\x00\x01d') +assert raw(p) == b'\x00\x00\x05\x02\x00\x00\x00\x00\x03\x80\x00\x00\x01d' + HTTP/2 Reset Stream Frame Test Suite @@ -966,46 +966,46 @@ assert(raw(p) == b'\x00\x00\x05\x02\x00\x00\x00\x00\x03\x80\x00\x00\x01d') ~ http2 frame dissect rststream pkt = h2.H2Frame(b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x01') #Reset stream with protocol error -assert(pkt.type == 3) -assert(pkt.len == 4) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2ResetFrame)) -assert(pkt[h2.H2ResetFrame]) +assert pkt.type == 3 +assert pkt.len == 4 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2ResetFrame) +assert pkt[h2.H2ResetFrame] rf = pkt[h2.H2ResetFrame] -assert(rf.error == 1) -assert(isinstance(rf.payload, scapy.packet.NoPayload)) +assert rf.error == 1 +assert isinstance(rf.payload, scapy.packet.NoPayload) = HTTP/2 Build Reset Stream Frame: Protocol Error ~ http2 frame build rststream p = h2.H2Frame(stream_id=1)/h2.H2ResetFrame(error='Protocol error') -assert(raw(p) == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x01') +assert raw(p) == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x01' p = h2.H2Frame(stream_id=1)/h2.H2ResetFrame(error=1) -assert(raw(p) == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x01') +assert raw(p) == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x01' = HTTP/2 Dissect Reset Stream Frame: Raw 123456 error ~ http2 frame dissect rststream pkt = h2.H2Frame(b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x01\xe2@') #Reset stream with raw error -assert(pkt.type == 3) -assert(pkt.len == 4) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2ResetFrame)) -assert(pkt[h2.H2ResetFrame]) +assert pkt.type == 3 +assert pkt.len == 4 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2ResetFrame) +assert pkt[h2.H2ResetFrame] rf = pkt[h2.H2ResetFrame] -assert(rf.error == 123456) -assert(isinstance(rf.payload, scapy.packet.NoPayload)) +assert rf.error == 123456 +assert isinstance(rf.payload, scapy.packet.NoPayload) = HTTP/2 Dissect Reset Stream Frame: Raw 123456 error ~ http2 frame dissect rststream p = h2.H2Frame(stream_id=1)/h2.H2ResetFrame(error=123456) -assert(raw(p) == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x01\xe2@') +assert raw(p) == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x01\xe2@' + HTTP/2 Settings Frame Test Suite @@ -1013,34 +1013,34 @@ assert(raw(p) == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x01\xe2@') ~ http2 frame dissect settings pkt = h2.H2Frame(b'\x00\x00$\x04\x00\x00\x00\x00\x00\x00\x01\x07[\xcd\x15\x00\x02\x00\x00\x00\x01\x00\x03\x00\x00\x00{\x00\x04\x00\x12\xd6\x87\x00\x05\x00\x01\xe2@\x00\x06\x00\x00\x00{') #Settings frame -assert(pkt.type == 4) -assert(pkt.len == 36) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 0) -assert(isinstance(pkt.payload, h2.H2SettingsFrame)) -assert(pkt[h2.H2SettingsFrame]) +assert pkt.type == 4 +assert pkt.len == 36 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 0 +assert isinstance(pkt.payload, h2.H2SettingsFrame) +assert pkt[h2.H2SettingsFrame] sf = pkt[h2.H2SettingsFrame] -assert(len(sf.settings) == 6) -assert(isinstance(sf.settings[0], h2.H2Setting)) -assert(sf.settings[0].id == 1) -assert(sf.settings[0].value == 123456789) -assert(isinstance(sf.settings[1], h2.H2Setting)) -assert(sf.settings[1].id == 2) -assert(sf.settings[1].value == 1) -assert(isinstance(sf.settings[2], h2.H2Setting)) -assert(sf.settings[2].id == 3) -assert(sf.settings[2].value == 123) -assert(isinstance(sf.settings[3], h2.H2Setting)) -assert(sf.settings[3].id == 4) -assert(sf.settings[3].value == 1234567) -assert(isinstance(sf.settings[4], h2.H2Setting)) -assert(sf.settings[4].id == 5) -assert(sf.settings[4].value == 123456) -assert(isinstance(sf.settings[5], h2.H2Setting)) -assert(sf.settings[5].id == 6) -assert(sf.settings[5].value == 123) -assert(isinstance(sf.payload, scapy.packet.NoPayload)) +assert len(sf.settings) == 6 +assert isinstance(sf.settings[0], h2.H2Setting) +assert sf.settings[0].id == 1 +assert sf.settings[0].value == 123456789 +assert isinstance(sf.settings[1], h2.H2Setting) +assert sf.settings[1].id == 2 +assert sf.settings[1].value == 1 +assert isinstance(sf.settings[2], h2.H2Setting) +assert sf.settings[2].id == 3 +assert sf.settings[2].value == 123 +assert isinstance(sf.settings[3], h2.H2Setting) +assert sf.settings[3].id == 4 +assert sf.settings[3].value == 1234567 +assert isinstance(sf.settings[4], h2.H2Setting) +assert sf.settings[4].id == 5 +assert sf.settings[4].value == 123456 +assert isinstance(sf.settings[5], h2.H2Setting) +assert sf.settings[5].id == 6 +assert sf.settings[5].value == 123 +assert isinstance(sf.payload, scapy.packet.NoPayload) = HTTP/2 Build Settings Frame: Settings Frame ~ http2 frame build settings @@ -1054,32 +1054,32 @@ p = h2.H2Frame()/h2.H2SettingsFrame(settings=[ h2.H2Setting(id='Max header list size', value=123) ] ) -assert(raw(p) == b'\x00\x00$\x04\x00\x00\x00\x00\x00\x00\x01\x07[\xcd\x15\x00\x02\x00\x00\x00\x01\x00\x03\x00\x00\x00{\x00\x04\x00\x12\xd6\x87\x00\x05\x00\x01\xe2@\x00\x06\x00\x00\x00{') +assert raw(p) == b'\x00\x00$\x04\x00\x00\x00\x00\x00\x00\x01\x07[\xcd\x15\x00\x02\x00\x00\x00\x01\x00\x03\x00\x00\x00{\x00\x04\x00\x12\xd6\x87\x00\x05\x00\x01\xe2@\x00\x06\x00\x00\x00{' = HTTP/2 Dissect Settings Frame: Incomplete Settings Frame ~ http2 frame dissect settings #We use here the decode('hex') method because null-bytes are rejected by eval() -assert(expect_exception(AssertionError, 'h2.H2Frame(bytes_hex("0000240400000000000001075bcd1500020000000100030000007b00040012d68700050001e2400006000000"))')) +assert expect_exception(AssertionError, 'h2.H2Frame(bytes_hex("0000240400000000000001075bcd1500020000000100030000007b00040012d68700050001e2400006000000"))') = HTTP/2 Dissect Settings Frame: Settings Frame acknowledgement ~ http2 frame dissect settings pkt = h2.H2Frame(b'\x00\x00\x00\x04\x01\x00\x00\x00\x00') #Settings frame w/ ack flag -assert(pkt.type == 4) -assert(pkt.len == 0) -assert(len(pkt.flags) == 1) -assert('A' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 0) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(isinstance(pkt.payload, scapy.packet.NoPayload)) +assert pkt.type == 4 +assert pkt.len == 0 +assert len(pkt.flags) == 1 +assert 'A' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 0 +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert isinstance(pkt.payload, scapy.packet.NoPayload) = HTTP/2 Build Settings Frame: Settings Frame acknowledgement ~ http2 frame build settings p = h2.H2Frame(type=h2.H2SettingsFrame.type_id, flags={'A'}) -assert(raw(p) == b'\x00\x00\x00\x04\x01\x00\x00\x00\x00') +assert raw(p) == b'\x00\x00\x00\x04\x01\x00\x00\x00\x00' + HTTP/2 Push Promise Frame Test Suite @@ -1087,30 +1087,30 @@ assert(raw(p) == b'\x00\x00\x00\x04\x01\x00\x00\x00\x00') ~ http2 frame dissect pushpromise pkt = h2.H2Frame(b'\x00\x00\x15\x05\x00\x00\x00\x00\x01\x00\x00\x00\x03@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me') -assert(pkt.type == 5) -assert(pkt.len == 21) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2PushPromiseFrame)) -assert(pkt[h2.H2PushPromiseFrame]) +assert pkt.type == 5 +assert pkt.len == 21 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2PushPromiseFrame) +assert pkt[h2.H2PushPromiseFrame] pf = pkt[h2.H2PushPromiseFrame] -assert(pf.reserved == 0) -assert(pf.stream_id == 3) -assert(len(pf.hdrs) == 1) -assert(isinstance(pf.payload, scapy.packet.NoPayload)) +assert pf.reserved == 0 +assert pf.stream_id == 3 +assert len(pf.hdrs) == 1 +assert isinstance(pf.payload, scapy.packet.NoPayload) hdr = pf.hdrs[0] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.type == 1) -assert(hdr.hdr_name.len == 12) -assert(hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.type == 0) -assert(hdr.hdr_value.len == 2) -assert(hdr.hdr_value.getfieldval('data').origin() == 'Me') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.type == 1 +assert hdr.hdr_name.len == 12 +assert hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.type == 0 +assert hdr.hdr_value.len == 2 +assert hdr.hdr_value.getfieldval('data').origin() == 'Me' = HTTP/2 Build Push Promise Frame: no flag & headers with compression and hdr_name ~ http2 frame build pushpromise @@ -1121,39 +1121,39 @@ p = h2.H2Frame(stream_id=1)/h2.H2PushPromiseFrame(stream_id=3,hdrs=[ hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString('Me')), ) ]) -assert(raw(p) == b'\x00\x00\x15\x05\x00\x00\x00\x00\x01\x00\x00\x00\x03@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me') +assert raw(p) == b'\x00\x00\x15\x05\x00\x00\x00\x00\x01\x00\x00\x00\x03@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me' = HTTP/2 Dissect Push Promise Frame: with padding, the flag END_Header & headers with compression and hdr_name ~ http2 frame dissect pushpromise pkt = h2.H2Frame(b'\x00\x00\x1e\x05\x0c\x00\x00\x00\x01\x08\x00\x00\x00\x03@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me\x00\x00\x00\x00\x00\x00\x00\x00') -assert(pkt.type == 5) -assert(pkt.len == 30) -assert(len(pkt.flags) == 2) -assert('P' in pkt.flags) -assert('EH' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(isinstance(pkt.payload, h2.H2PaddedPushPromiseFrame)) -assert(pkt[h2.H2PaddedPushPromiseFrame]) +assert pkt.type == 5 +assert pkt.len == 30 +assert len(pkt.flags) == 2 +assert 'P' in pkt.flags +assert 'EH' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert isinstance(pkt.payload, h2.H2PaddedPushPromiseFrame) +assert pkt[h2.H2PaddedPushPromiseFrame] pf = pkt[h2.H2PaddedPushPromiseFrame] -assert(pf.padlen == 8) -assert(pf.padding == b'\x00'*8) -assert(pf.stream_id == 3) -assert(len(pf.hdrs) == 1) +assert pf.padlen == 8 +assert pf.padding == b'\x00'*8 +assert pf.stream_id == 3 +assert len(pf.hdrs) == 1 hdr = pf.hdrs[0] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.type == 1) -assert(hdr.hdr_name.len == 12) -assert(hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.type == 0) -assert(hdr.hdr_value.len == 2) -assert(hdr.hdr_value.getfieldval('data').origin() == 'Me') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.type == 1 +assert hdr.hdr_name.len == 12 +assert hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.type == 0 +assert hdr.hdr_value.len == 2 +assert hdr.hdr_value.getfieldval('data').origin() == 'Me' = HTTP/2 Build Push Promise Frame: with padding, the flag END_Header & headers with compression and hdr_name ~ http2 frame build pushpromise @@ -1168,7 +1168,7 @@ p = h2.H2Frame(flags={'P', 'EH'}, stream_id=1)/h2.H2PaddedPushPromiseFrame( ], padding=b'\x00'*8 ) -assert(raw(p) == b'\x00\x00\x1e\x05\x0c\x00\x00\x00\x01\x08\x00\x00\x00\x03@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me\x00\x00\x00\x00\x00\x00\x00\x00') +assert raw(p) == b'\x00\x00\x1e\x05\x0c\x00\x00\x00\x01\x08\x00\x00\x00\x03@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me\x00\x00\x00\x00\x00\x00\x00\x00' + HTTP/2 Ping Frame Test Suite @@ -1176,45 +1176,45 @@ assert(raw(p) == b'\x00\x00\x1e\x05\x0c\x00\x00\x00\x01\x08\x00\x00\x00\x03@\x8c ~ http2 frame dissect ping pkt = h2.H2Frame(b'\x00\x00\x08\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe2@') #Ping frame with payload -assert(pkt.type == 6) -assert(pkt.len == 8) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 0) -assert(isinstance(pkt.payload, h2.H2PingFrame)) -assert(pkt[h2.H2PingFrame]) +assert pkt.type == 6 +assert pkt.len == 8 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 0 +assert isinstance(pkt.payload, h2.H2PingFrame) +assert pkt[h2.H2PingFrame] pf = pkt[h2.H2PingFrame] -assert(pf.opaque == 123456) -assert(isinstance(pf.payload, scapy.packet.NoPayload)) +assert pf.opaque == 123456 +assert isinstance(pf.payload, scapy.packet.NoPayload) = HTTP/2 Build Ping Frame: Ping frame ~ http2 frame build ping p = h2.H2Frame()/h2.H2PingFrame(opaque=123456) -assert(raw(p) == b'\x00\x00\x08\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe2@') +assert raw(p) == b'\x00\x00\x08\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe2@' = HTTP/2 Dissect Ping Frame: Pong frame ~ http2 frame dissect ping pkt = h2.H2Frame(b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe2@') #Pong frame -assert(pkt.type == 6) -assert(pkt.len == 8) -assert(len(pkt.flags) == 1) -assert('A' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 0) -assert(isinstance(pkt.payload, h2.H2PingFrame)) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(pkt[h2.H2PingFrame]) +assert pkt.type == 6 +assert pkt.len == 8 +assert len(pkt.flags) == 1 +assert 'A' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 0 +assert isinstance(pkt.payload, h2.H2PingFrame) +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert pkt[h2.H2PingFrame] pf = pkt[h2.H2PingFrame] -assert(pf.opaque == 123456) -assert(isinstance(pf.payload, scapy.packet.NoPayload)) +assert pf.opaque == 123456 +assert isinstance(pf.payload, scapy.packet.NoPayload) = HTTP/2 Dissect Ping Frame: Pong frame ~ http2 frame dissect ping p = h2.H2Frame(flags={'A'})/h2.H2PingFrame(opaque=123456) -assert(raw(p) == b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe2@') +assert raw(p) == b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe2@' + HTTP/2 Go Away Frame Test Suite @@ -1222,43 +1222,43 @@ assert(raw(p) == b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\ ~ http2 frame dissect goaway pkt = h2.H2Frame(b'\x00\x00\x08\x07\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00') #Go Away for no particular reason :) -assert(pkt.type == 7) -assert(pkt.len == 8) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 0) -assert(isinstance(pkt.payload, h2.H2GoAwayFrame)) -assert(pkt[h2.H2GoAwayFrame]) +assert pkt.type == 7 +assert pkt.len == 8 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 0 +assert isinstance(pkt.payload, h2.H2GoAwayFrame) +assert pkt[h2.H2GoAwayFrame] gf = pkt[h2.H2GoAwayFrame] -assert(gf.reserved == 0) -assert(gf.last_stream_id == 1) -assert(gf.error == 0) -assert(len(gf.additional_data) == 0) -assert(isinstance(gf.payload, scapy.packet.NoPayload)) +assert gf.reserved == 0 +assert gf.last_stream_id == 1 +assert gf.error == 0 +assert len(gf.additional_data) == 0 +assert isinstance(gf.payload, scapy.packet.NoPayload) = HTTP/2 Build Go Away Frame: No error ~ http2 frame build goaway p = h2.H2Frame()/h2.H2GoAwayFrame(last_stream_id=1, error='No error') -assert(raw(p) == b'\x00\x00\x08\x07\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00') +assert raw(p) == b'\x00\x00\x08\x07\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00' = HTTP/2 Dissect Go Away Frame: Arbitrary error with additional data ~ http2 frame dissect goaway pkt = h2.H2Frame(b'\x00\x00\x10\x07\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\xe2@\x00\x00\x00\x00\x00\x00\x00\x00') #Go Away with debug data -assert(pkt.type == 7) -assert(pkt.len == 16) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 0) -assert(isinstance(pkt.payload, h2.H2GoAwayFrame)) -assert(pkt[h2.H2GoAwayFrame]) +assert pkt.type == 7 +assert pkt.len == 16 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 0 +assert isinstance(pkt.payload, h2.H2GoAwayFrame) +assert pkt[h2.H2GoAwayFrame] gf = pkt[h2.H2GoAwayFrame] -assert(gf.reserved == 0) -assert(gf.last_stream_id == 2) -assert(gf.error == 123456) -assert(gf.additional_data == 8*b'\x00') -assert(isinstance(gf.payload, scapy.packet.NoPayload)) +assert gf.reserved == 0 +assert gf.last_stream_id == 2 +assert gf.error == 123456 +assert gf.additional_data == 8*b'\x00' +assert isinstance(gf.payload, scapy.packet.NoPayload) = HTTP/2 Build Go Away Frame: Arbitrary error with additional data ~ http2 frame build goaway @@ -1268,7 +1268,7 @@ p = h2.H2Frame()/h2.H2GoAwayFrame( error=123456, additional_data=b'\x00'*8 ) -assert(raw(p) == b'\x00\x00\x10\x07\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\xe2@\x00\x00\x00\x00\x00\x00\x00\x00') +assert raw(p) == b'\x00\x00\x10\x07\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\xe2@\x00\x00\x00\x00\x00\x00\x00\x00' + HTTP/2 Window Update Frame Test Suite @@ -1276,45 +1276,45 @@ assert(raw(p) == b'\x00\x00\x10\x07\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\ ~ http2 frame dissect winupdate pkt = h2.H2Frame(b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x01\xe2@') #Window update with increment for connection -assert(pkt.type == 8) -assert(pkt.len == 4) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 0) -assert(isinstance(pkt.payload, h2.H2WindowUpdateFrame)) -assert(pkt[h2.H2WindowUpdateFrame]) +assert pkt.type == 8 +assert pkt.len == 4 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 0 +assert isinstance(pkt.payload, h2.H2WindowUpdateFrame) +assert pkt[h2.H2WindowUpdateFrame] wf = pkt[h2.H2WindowUpdateFrame] -assert(wf.reserved == 0) -assert(wf.win_size_incr == 123456) -assert(isinstance(wf.payload, scapy.packet.NoPayload)) +assert wf.reserved == 0 +assert wf.win_size_incr == 123456 +assert isinstance(wf.payload, scapy.packet.NoPayload) = HTTP/2 Build Window Update Frame: global ~ http2 frame build winupdate p = h2.H2Frame()/h2.H2WindowUpdateFrame(win_size_incr=123456) -assert(raw(p) == b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x01\xe2@') +assert raw(p) == b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x01\xe2@' = HTTP/2 Dissect Window Update Frame: a stream ~ http2 frame dissect winupdate pkt = h2.H2Frame(b'\x00\x00\x04\x08\x00\x00\x00\x00\x01\x00\x01\xe2@') #Window update with increment for a stream -assert(pkt.type == 8) -assert(pkt.len == 4) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2WindowUpdateFrame)) -assert(pkt[h2.H2WindowUpdateFrame]) +assert pkt.type == 8 +assert pkt.len == 4 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2WindowUpdateFrame) +assert pkt[h2.H2WindowUpdateFrame] wf = pkt[h2.H2WindowUpdateFrame] -assert(wf.reserved == 0) -assert(wf.win_size_incr == 123456) -assert(isinstance(wf.payload, scapy.packet.NoPayload)) +assert wf.reserved == 0 +assert wf.win_size_incr == 123456 +assert isinstance(wf.payload, scapy.packet.NoPayload) = HTTP/2 Build Window Update Frame: a stream ~ http2 frame build winupdate p = h2.H2Frame(stream_id=1)/h2.H2WindowUpdateFrame(win_size_incr=123456) -assert(raw(p) == b'\x00\x00\x04\x08\x00\x00\x00\x00\x01\x00\x01\xe2@') +assert raw(p) == b'\x00\x00\x04\x08\x00\x00\x00\x00\x01\x00\x01\xe2@' + HTTP/2 Continuation Frame Test Suite @@ -1322,28 +1322,28 @@ assert(raw(p) == b'\x00\x00\x04\x08\x00\x00\x00\x00\x01\x00\x01\xe2@') ~ http2 frame dissect continuation pkt = h2.H2Frame(b'\x00\x00\x11\t\x00\x00\x00\x00\x01@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me') -assert(pkt.type == 9) -assert(pkt.len == 17) -assert(len(pkt.flags) == 0) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(isinstance(pkt.payload, h2.H2ContinuationFrame)) -assert(pkt[h2.H2ContinuationFrame]) +assert pkt.type == 9 +assert pkt.len == 17 +assert len(pkt.flags) == 0 +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert isinstance(pkt.payload, h2.H2ContinuationFrame) +assert pkt[h2.H2ContinuationFrame] hf = pkt[h2.H2ContinuationFrame] -assert(len(hf.hdrs) == 1) -assert(isinstance(hf.payload, scapy.packet.NoPayload)) +assert len(hf.hdrs) == 1 +assert isinstance(hf.payload, scapy.packet.NoPayload) hdr = hf.hdrs[0] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.type == 1) -assert(hdr.hdr_name.len == 12) -assert(hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.type == 0) -assert(hdr.hdr_value.len == 2) -assert(hdr.hdr_value.getfieldval('data').origin() == 'Me') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.type == 1 +assert hdr.hdr_name.len == 12 +assert hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.type == 0 +assert hdr.hdr_value.len == 2 +assert hdr.hdr_value.getfieldval('data').origin() == 'Me' = HTTP/2 Build Continuation Frame: no flag & headers with compression and hdr_name ~ http2 frame build continuation @@ -1356,37 +1356,37 @@ p = h2.H2Frame(stream_id=1)/h2.H2ContinuationFrame( ) ] ) -assert(raw(p) == b'\x00\x00\x11\t\x00\x00\x00\x00\x01@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me') +assert raw(p) == b'\x00\x00\x11\t\x00\x00\x00\x00\x01@\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me' = HTTP/2 Dissect Continuation Frame: flag END_Header & headers with compression, sensitive flag and hdr_name ~ http2 frame dissect continuation pkt = h2.H2Frame(b'\x00\x00\x11\t\x04\x00\x00\x00\x01\x10\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me') -assert(pkt.type == 9) -assert(pkt.len == 17) -assert(len(pkt.flags) == 1) -assert('EH' in pkt.flags) -assert(pkt.reserved == 0) -assert(pkt.stream_id == 1) -assert(flags_bit_pattern.search(pkt.show(dump=True)) is None) -assert(isinstance(pkt.payload, h2.H2ContinuationFrame)) -assert(pkt[h2.H2ContinuationFrame]) +assert pkt.type == 9 +assert pkt.len == 17 +assert len(pkt.flags) == 1 +assert 'EH' in pkt.flags +assert pkt.reserved == 0 +assert pkt.stream_id == 1 +assert flags_bit_pattern.search(pkt.show(dump=True)) is None +assert isinstance(pkt.payload, h2.H2ContinuationFrame) +assert pkt[h2.H2ContinuationFrame] hf = pkt[h2.H2ContinuationFrame] -assert(len(hf.hdrs) == 1) -assert(isinstance(hf.payload, scapy.packet.NoPayload)) +assert len(hf.hdrs) == 1 +assert isinstance(hf.payload, scapy.packet.NoPayload) hdr = hf.hdrs[0] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 1) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.type == 1) -assert(hdr.hdr_name.len == 12) -assert(hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.type == 0) -assert(hdr.hdr_value.len == 2) -assert(hdr.hdr_value.getfieldval('data').origin() == 'Me') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.type == 1 +assert hdr.hdr_name.len == 12 +assert hdr.hdr_name.getfieldval('data').origin() == 'X-Requested-With' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.type == 0 +assert hdr.hdr_value.len == 2 +assert hdr.hdr_value.getfieldval('data').origin() == 'Me' = HTTP/2 Build Continuation Frame: flag END_Header & headers with compression, sensitive flag and hdr_name ~ http2 frame build continuation @@ -1400,7 +1400,7 @@ p = h2.H2Frame(flags={'EH'}, stream_id=1)/h2.H2ContinuationFrame( ) ] ) -assert(raw(p) == b'\x00\x00\x11\t\x04\x00\x00\x00\x01\x10\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me') +assert raw(p) == b'\x00\x00\x11\t\x04\x00\x00\x00\x01\x10\x8c\xfc[i{ZT$\xb2-\xc8\xc9\x9f\x02Me' + HTTP/2 HPackHdrTable Test Suite @@ -1410,35 +1410,35 @@ assert(raw(p) == b'\x00\x00\x11\t\x04\x00\x00\x00\x01\x10\x8c\xfc[i{ZT$\xb2-\xc8 n = 'X-Requested-With' v = 'Me' h = h2.HPackHdrEntry(n, v) -assert(len(h) == 32 + len(n) + len(v)) -assert(h.name() == n.lower()) -assert(h.value() == v) -assert(str(h) == '{}: {}'.format(n.lower(), v)) +assert len(h) == 32 + len(n) + len(v) +assert h.name() == n.lower() +assert h.value() == v +assert str(h) == '{}: {}'.format(n.lower(), v) n = ':status' v = '200' h = h2.HPackHdrEntry(n, v) -assert(len(h) == 32 + len(n) + len(v)) -assert(h.name() == n.lower()) -assert(h.value() == v) -assert(str(h) == '{} {}'.format(n.lower(), v)) +assert len(h) == 32 + len(n) + len(v) +assert h.name() == n.lower() +assert h.value() == v +assert str(h) == '{} {}'.format(n.lower(), v) = HTTP/2 HPackHdrTable : Querying Static Entries ~ http2 hpack hpackhdrtable # In RFC7541, the table is 1-based -assert(expect_exception(KeyError, 'h2.HPackHdrTable()[0]')) +assert expect_exception(KeyError, 'h2.HPackHdrTable()[0]') h = h2.HPackHdrTable() -assert(h[1].name() == ':authority') -assert(h[7].name() == ':scheme') -assert(h[7].value() == 'https') -assert(str(h[14]) == ':status 500') -assert(str(h[16]) == 'accept-encoding: gzip, deflate') +assert h[1].name() == ':authority' +assert h[7].name() == ':scheme' +assert h[7].value() == 'https' +assert str(h[14]) == ':status 500' +assert str(h[16]) == 'accept-encoding: gzip, deflate' -assert(expect_exception(KeyError, 'h2.HPackHdrTable()[h2.HPackHdrTable._static_entries_last_idx+1]')) +assert expect_exception(KeyError, 'h2.HPackHdrTable()[h2.HPackHdrTable._static_entries_last_idx+1]') -= HTTP/2 HPackHdrTable : Addind Dynamic Entries without overflowing the table += HTTP/2 HPackHdrTable : Adding Dynamic Entries without overflowing the table ~ http2 hpack hpackhdrtable tbl = h2.HPackHdrTable(dynamic_table_max_size=1<<32, dynamic_table_cap_size=1<<32) @@ -1489,16 +1489,16 @@ hdr3 = h2.HPackLitHdrFldWithIncrIndexing( ) tbl.register(hdr3) -assert(tbl.get_idx_by_name('x-requested-by') == h2.HPackHdrTable._static_entries_last_idx+1) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+3].value() == hdrv) +assert tbl.get_idx_by_name('x-requested-by') == h2.HPackHdrTable._static_entries_last_idx+1 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv2 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+3].value() == hdrv -= HTTP/2 HPackHdrTable : Addind already registered Dynamic Entries without overflowing the table += HTTP/2 HPackHdrTable : Adding already registered Dynamic Entries without overflowing the table ~ http2 hpack hpackhdrtable tbl = h2.HPackHdrTable(dynamic_table_max_size=1<<32, dynamic_table_cap_size=1<<32) -assert(len(tbl) == 0) +assert len(tbl) == 0 hdrv = 'PHPSESSID=abcdef0123456789' hdr = h2.HPackLitHdrFldWithIncrIndexing( @@ -1506,7 +1506,7 @@ hdr = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdrv)) ) tbl.register(hdr) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv hdr2v = 'JSESSID=abcdef0123456789' hdr2 = h2.HPackLitHdrFldWithIncrIndexing( @@ -1514,15 +1514,15 @@ hdr2 = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdr2v)) ) tbl.register(hdr2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdr2v) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdr2v l = len(tbl) tbl.register(hdr) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdr2v) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+3].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdr2v +assert tbl[h2.HPackHdrTable._static_entries_last_idx+3].value() == hdrv -= HTTP/2 HPackHdrTable : Addind Dynamic Entries and overflowing the table += HTTP/2 HPackHdrTable : Adding Dynamic Entries and overflowing the table ~ http2 hpack hpackhdrtable tbl = h2.HPackHdrTable(dynamic_table_max_size=80, dynamic_table_cap_size=80) @@ -1532,8 +1532,8 @@ hdr = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdrv)) ) tbl.register(hdr) -assert(len(tbl) <= 80) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv) +assert len(tbl) <= 80 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv hdrv2 = 'JSESSID=abcdef0123456789' hdr2 = h2.HPackLitHdrFldWithIncrIndexing( @@ -1541,15 +1541,15 @@ hdr2 = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdrv2)) ) tbl.register(hdr2) -assert(len(tbl) <= 80) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) +assert len(tbl) <= 80 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 try: tbl[h2.HPackHdrTable._static_entries_last_idx+2] ret = False except Exception: ret = True -assert(ret) +assert ret = HTTP/2 HPackHdrTable : Resizing @@ -1562,7 +1562,7 @@ hdr = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdrv)) ) tbl.register(hdr) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv hdrv2 = 'JSESSID=abcdef0123456789' hdr2 = h2.HPackLitHdrFldWithIncrIndexing( @@ -1570,8 +1570,8 @@ hdr2 = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdrv2)) ) tbl.register(hdr2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv #Resizing to a value higher than cap (default:4096) try: @@ -1580,25 +1580,25 @@ try: except AssertionError: ret = True -assert(ret) +assert ret #Resizing to a lower value by that is not small enough to cause eviction tbl.resize(1024) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv #Resizing to a higher value but thatt is lower than cap tbl.resize(2048) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv #Resizing to a lower value that causes eviction tbl.resize(80) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 try: tbl[h2.HPackHdrTable._static_entries_last_idx+2] ret = False except Exception: ret = True -assert(ret) +assert ret = HTTP/2 HPackHdrTable : Recapping ~ http2 hpack hpackhdrtable @@ -1610,7 +1610,7 @@ hdr = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdrv)) ) tbl.register(hdr) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv hdrv2 = 'JSESSID=abcdef0123456789' hdr2 = h2.HPackLitHdrFldWithIncrIndexing( @@ -1618,29 +1618,29 @@ hdr2 = h2.HPackLitHdrFldWithIncrIndexing( hdr_value=h2.HPackHdrString(data=h2.HPackZString(hdrv2)) ) tbl.register(hdr2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv #Recapping to a higher value tbl.recap(8192) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv #Recapping to a low value but without causing eviction tbl.recap(1024) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 +assert tbl[h2.HPackHdrTable._static_entries_last_idx+2].value() == hdrv #Recapping to a low value that causes evictiontbl.recap(1024) tbl.recap(80) -assert(tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2) +assert tbl[h2.HPackHdrTable._static_entries_last_idx+1].value() == hdrv2 try: tbl[h2.HPackHdrTable._static_entries_last_idx+2] ret = False except Exception: ret = True -assert(ret) +assert ret = HTTP/2 HPackHdrTable : Generating Textual Representation ~ http2 hpack hpackhdrtable helpers @@ -1678,7 +1678,7 @@ x-generation-date: 2016-08-11 user-agent: Mozilla/5.0 Generated by hand X-Generated-By: Me''' -assert(h.gen_txt_repr(p) == expected_output) +assert h.gen_txt_repr(p) == expected_output = HTTP/2 HPackHdrTable : Parsing Textual Representation ~ http2 hpack hpackhdrtable helpers @@ -1693,7 +1693,7 @@ user-agent: Mozilla/5.0 Generated by hand x-generated-by: Me x-generation-date: 2016-08-11 x-generation-software: scapy -'''.format(len(body)).encode() +'''.format(len(body)) h = h2.HPackHdrTable() h.register(h2.HPackLitHdrFldWithIncrIndexing( @@ -1707,87 +1707,182 @@ seq = h.parse_txt_hdrs( should_index=lambda name: name in ['user-agent', 'x-generation-software'], is_sensitive=lambda name, value: name in ['x-generated-by', ':path'] ) -assert(isinstance(seq, h2.H2Seq)) -assert(len(seq.frames) == 2) +assert isinstance(seq, h2.H2Seq) +assert len(seq.frames) == 2 +p = seq.frames[0] +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 1 +assert 'EH' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) +hdrs_frm = p[h2.H2HeadersFrame] +assert len(p.hdrs) == 9 +hdr = p.hdrs[0] +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 +hdr = p.hdrs[1] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' +hdr = p.hdrs[2] +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 +hdr = p.hdrs[3] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 31 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)' +hdr = p.hdrs[4] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 28 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(22)' +hdr = p.hdrs[5] +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' +hdr = p.hdrs[6] +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' +hdr = p.hdrs[7] +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 63 +hdr = p.hdrs[8] +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generation-software)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(scapy)' + +p = seq.frames[1] +assert isinstance(p, h2.H2Frame) +assert p.type == 0 +assert len(p.flags) == 1 +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2DataFrame) +pay = p[h2.H2DataFrame] +assert pay.data == body + +# now with bytes +h = h2.HPackHdrTable() +h.register(h2.HPackLitHdrFldWithIncrIndexing( + hdr_name=h2.HPackHdrString(data=h2.HPackZString('X-Generation-Date')), + hdr_value=h2.HPackHdrString(data=h2.HPackLiteralString('2016-08-11')) +)) +seq = h.parse_txt_hdrs( + hdrs.encode(), + stream_id=1, + body=body, + should_index=lambda name: name in ['user-agent', 'x-generation-software'], + is_sensitive=lambda name, value: name in ['x-generated-by', ':path'] +) +assert isinstance(seq, h2.H2Seq) +assert len(seq.frames) == 2 p = seq.frames[0] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 1) -assert(len(p.flags) == 1) -assert('EH' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2HeadersFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 1 +assert 'EH' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) hdrs_frm = p[h2.H2HeadersFrame] -assert(len(p.hdrs) == 9) +assert len(p.hdrs) == 9 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 3) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 hdr = p.hdrs[1] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 1) -assert(hdr.index in [4, 5]) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(/login.php)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' hdr = p.hdrs[2] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 7) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 hdr = p.hdrs[3] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 31) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 31 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)' hdr = p.hdrs[4] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 28) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(22)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 28 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(22)' hdr = p.hdrs[5] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 58) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' hdr = p.hdrs[6] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 1) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generated-by)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(Me)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' hdr = p.hdrs[7] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 63) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 63 hdr = p.hdrs[8] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generation-software)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(scapy)') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generation-software)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(scapy)' p = seq.frames[1] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 0) -assert(len(p.flags) == 1) -assert('ES' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2DataFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 0 +assert len(p.flags) == 1 +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2DataFrame) pay = p[h2.H2DataFrame] -assert(pay.data == body) +assert pay.data == body = HTTP/2 HPackHdrTable : Parsing Textual Representation without body ~ http2 hpack hpackhdrtable helpers @@ -1808,57 +1903,57 @@ h.register(h2.HPackLitHdrFldWithIncrIndexing( # Without body seq = h.parse_txt_hdrs(hdrs, stream_id=1) -assert(isinstance(seq, h2.H2Seq)) +assert isinstance(seq, h2.H2Seq) #This is the first major difference with the first test -assert(len(seq.frames) == 1) +assert len(seq.frames) == 1 p = seq.frames[0] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 1) -assert(len(p.flags) == 2) -assert('EH' in p.flags) +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 2 +assert 'EH' in p.flags #This is the second major difference with the first test -assert('ES' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2HeadersFrame)) +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) hdrs_frm = p[h2.H2HeadersFrame] -assert(len(p.hdrs) == 6) +assert len(p.hdrs) == 6 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 3) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 hdr = p.hdrs[1] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index in [4, 5]) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(/login.php)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' hdr = p.hdrs[2] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 7) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 hdr = p.hdrs[3] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 58) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' hdr = p.hdrs[4] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generated-by)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(Me)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' hdr = p.hdrs[5] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 62) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 62 = HTTP/2 HPackHdrTable : Parsing Textual Representation with too small max frame @@ -1909,80 +2004,80 @@ h.register(h2.HPackLitHdrFldWithIncrIndexing( # Now trying to parse it with a max frame size large enough for x-long-header to # fit in a frame seq = h.parse_txt_hdrs(hdrs, stream_id=1, max_frm_sz=8192) -assert(isinstance(seq, h2.H2Seq)) -assert(len(seq.frames) == 1) +assert isinstance(seq, h2.H2Seq) +assert len(seq.frames) == 1 p = seq.frames[0] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 1) -assert(len(p.flags) == 2) -assert('EH' in p.flags) -assert('ES' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2HeadersFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 2 +assert 'EH' in p.flags +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) hdrs_frm = p[h2.H2HeadersFrame] -assert(len(p.hdrs) == 9) +assert len(p.hdrs) == 9 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 3) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 hdr = p.hdrs[1] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index in [4, 5]) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(/login.php)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' hdr = p.hdrs[2] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 7) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 hdr = p.hdrs[3] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 31) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 31 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)' hdr = p.hdrs[4] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 28) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(22)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 28 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(22)' hdr = p.hdrs[5] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 58) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' hdr = p.hdrs[6] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generated-by)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(Me)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' hdr = p.hdrs[7] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 62) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 62 hdr = p.hdrs[8] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-long-header)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString({})'.format('a'*5000)) +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-long-header)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString({})'.format('a'*5000) = HTTP/2 HPackHdrTable : Parsing Textual Representation with two very large headers and a large authorized frame size ~ http2 hpack hpackhdrtable helpers @@ -2010,97 +2105,97 @@ h.register(h2.HPackLitHdrFldWithIncrIndexing( # fit in a frame but a maximum header fragment size that is not large enough to # store two x-long-header seq = h.parse_txt_hdrs(hdrs, stream_id=1, max_frm_sz=8192) -assert(isinstance(seq, h2.H2Seq)) -assert(len(seq.frames) == 2) +assert isinstance(seq, h2.H2Seq) +assert len(seq.frames) == 2 p = seq.frames[0] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 1) -assert(len(p.flags) == 1) -assert('ES' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2HeadersFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 1 +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) hdrs_frm = p[h2.H2HeadersFrame] -assert(len(p.hdrs) == 9) +assert len(p.hdrs) == 9 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 3) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 hdr = p.hdrs[1] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index in [4, 5]) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(/login.php)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' hdr = p.hdrs[2] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 7) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 hdr = p.hdrs[3] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 31) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 31 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)' hdr = p.hdrs[4] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 28) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(22)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 28 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(22)' hdr = p.hdrs[5] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 58) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' hdr = p.hdrs[6] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generated-by)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(Me)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' hdr = p.hdrs[7] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 62) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 62 hdr = p.hdrs[8] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-long-header)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString({})'.format('a'*5000)) +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-long-header)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString({})'.format('a'*5000) p = seq.frames[1] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 9) -assert(len(p.flags) == 1) -assert('EH' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2ContinuationFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 9 +assert len(p.flags) == 1 +assert 'EH' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2ContinuationFrame) hdrs_frm = p[h2.H2ContinuationFrame] -assert(len(p.hdrs) == 1) +assert len(p.hdrs) == 1 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-long-header)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString({})'.format('b'*5000)) +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-long-header)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString({})'.format('b'*5000) = HTTP/2 HPackHdrTable : Parsing Textual Representation with two very large headers, a large authorized frame size and a "small" max header list size ~ http2 hpack hpackhdrtable helpers @@ -2128,105 +2223,105 @@ h.register(h2.HPackLitHdrFldWithIncrIndexing( # fit in a frame but and a max header list size that is large enough to fit one # but not two seq = h.parse_txt_hdrs(hdrs, stream_id=1, max_frm_sz=8192, max_hdr_lst_sz=5050) -assert(isinstance(seq, h2.H2Seq)) -assert(len(seq.frames) == 3) +assert isinstance(seq, h2.H2Seq) +assert len(seq.frames) == 3 p = seq.frames[0] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 1) -assert(len(p.flags) == 1) -assert('ES' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2HeadersFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 1 +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) hdrs_frm = p[h2.H2HeadersFrame] -assert(len(p.hdrs) == 8) +assert len(p.hdrs) == 8 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 3) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 hdr = p.hdrs[1] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index in [4, 5]) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(/login.php)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' hdr = p.hdrs[2] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 7) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 hdr = p.hdrs[3] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 31) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 31 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)' hdr = p.hdrs[4] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 28) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(22)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 28 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(22)' hdr = p.hdrs[5] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 58) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' hdr = p.hdrs[6] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generated-by)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(Me)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' hdr = p.hdrs[7] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 62) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 62 p = seq.frames[1] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 9) -assert(len(p.flags) == 0) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2ContinuationFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 9 +assert len(p.flags) == 0 +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2ContinuationFrame) hdrs_frm = p[h2.H2ContinuationFrame] -assert(len(p.hdrs) == 1) +assert len(p.hdrs) == 1 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-long-header)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString({})'.format('a'*5000)) +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-long-header)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString({})'.format('a'*5000) p = seq.frames[2] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 9) -assert(len(p.flags) == 1) -assert('EH' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2ContinuationFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 9 +assert len(p.flags) == 1 +assert 'EH' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2ContinuationFrame) hdrs_frm = p[h2.H2ContinuationFrame] -assert(len(p.hdrs) == 1) +assert len(p.hdrs) == 1 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-long-header)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString({})'.format('b'*5000)) +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-long-header)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString({})'.format('b'*5000) = HTTP/2 HPackHdrTable : Parsing Textual Representation with sensitive headers and non-indexable ones ~ http2 hpack hpackhdrtable helpers @@ -2243,77 +2338,77 @@ x-generation-date: 2016-08-11 h = h2.HPackHdrTable() seq = h.parse_txt_hdrs(hdrs, stream_id=1, body=body, is_sensitive=lambda n,v: n in ['x-generation-date'], should_index=lambda x: x != 'x-generated-by') -assert(isinstance(seq, h2.H2Seq)) -assert(len(seq.frames) == 2) +assert isinstance(seq, h2.H2Seq) +assert len(seq.frames) == 2 p = seq.frames[0] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 1) -assert(len(p.flags) == 1) -assert('EH' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2HeadersFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 1 +assert len(p.flags) == 1 +assert 'EH' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2HeadersFrame) hdrs_frm = p[h2.H2HeadersFrame] -assert(len(p.hdrs) == 8) +assert len(p.hdrs) == 8 hdr = p.hdrs[0] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 3) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 3 hdr = p.hdrs[1] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index in [4, 5]) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(/login.php)') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index in [4, 5] +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(/login.php)' hdr = p.hdrs[2] -assert(isinstance(hdr, h2.HPackIndexedHdr)) -assert(hdr.magic == 1) -assert(hdr.index == 7) +assert isinstance(hdr, h2.HPackIndexedHdr) +assert hdr.magic == 1 +assert hdr.index == 7 hdr = p.hdrs[3] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 31) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 31 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(application/x-www-form-urlencoded)' hdr = p.hdrs[4] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 28) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(22)') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 28 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(22)' hdr = p.hdrs[5] -assert(isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing)) -assert(hdr.magic == 1) -assert(hdr.index == 58) -assert(hdr.hdr_name is None) -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)') +assert isinstance(hdr, h2.HPackLitHdrFldWithIncrIndexing) +assert hdr.magic == 1 +assert hdr.index == 58 +assert hdr.hdr_name is None +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(Mozilla/5.0 Generated by hand)' hdr = p.hdrs[6] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 0) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generated-by)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackLiteralString(Me)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 0 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generated-by)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackLiteralString(Me)' hdr = p.hdrs[7] -assert(isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing)) -assert(hdr.magic == 0) -assert(hdr.never_index == 1) -assert(hdr.index == 0) -assert(isinstance(hdr.hdr_name, h2.HPackHdrString)) -assert(hdr.hdr_name.data == 'HPackZString(x-generation-date)') -assert(isinstance(hdr.hdr_value, h2.HPackHdrString)) -assert(hdr.hdr_value.data == 'HPackZString(2016-08-11)') +assert isinstance(hdr, h2.HPackLitHdrFldWithoutIndexing) +assert hdr.magic == 0 +assert hdr.never_index == 1 +assert hdr.index == 0 +assert isinstance(hdr.hdr_name, h2.HPackHdrString) +assert hdr.hdr_name.data == 'HPackZString(x-generation-date)' +assert isinstance(hdr.hdr_value, h2.HPackHdrString) +assert hdr.hdr_value.data == 'HPackZString(2016-08-11)' p = seq.frames[1] -assert(isinstance(p, h2.H2Frame)) -assert(p.type == 0) -assert(len(p.flags) == 1) -assert('ES' in p.flags) -assert(p.stream_id == 1) -assert(isinstance(p.payload, h2.H2DataFrame)) +assert isinstance(p, h2.H2Frame) +assert p.type == 0 +assert len(p.flags) == 1 +assert 'ES' in p.flags +assert p.stream_id == 1 +assert isinstance(p.payload, h2.H2DataFrame) pay = p[h2.H2DataFrame] -assert(pay.data == body) +assert pay.data == body diff --git a/test/contrib/ibeacon.uts b/test/contrib/ibeacon.uts index df9221db947..a935980b7b8 100644 --- a/test/contrib/ibeacon.uts +++ b/test/contrib/ibeacon.uts @@ -68,3 +68,18 @@ assert p[HCI_LE_Meta_Advertising_Report].addr == 'd6:ee:d4:16:ed:fc' assert len(p[Apple_BLE_Frame].plist) == 1 assert p[IBeacon_Data].uuid == UUID('b9407f30-f5f8-466e-aff9-25556b57fe6d') ++ Overflow area + += Basic overflow area packet + +d = hex_bytes('14ff4c000100000000000000000000000000000080') +p = EIR_Hdr(d) + +assert raw(p) == d +assert len(p[Apple_BLE_Frame].plist) == 1 +assert p[Apple_BLE_Submessage].subtype == 0x01 +assert p[Apple_BLE_Submessage].len == None + +payload = p[Apple_BLE_Submessage].payload +assert isinstance(payload, Raw) +assert raw(payload) == hex_bytes('00000000000000000000000000000080') diff --git a/test/contrib/iec104.uts b/test/contrib/iec104.uts index 6f5d55e2455..4ab561cd030 100644 --- a/test/contrib/iec104.uts +++ b/test/contrib/iec104.uts @@ -13,10 +13,10 @@ load_contrib('scada.iec104') = class attribute generator -assert(IEC104_IE_QOC.QU_FLAG_RESERVED_COMPATIBLE_4 == 4) -assert(IEC104_IE_QOC.QU_FLAG_RESERVED_COMPATIBLE_8 == 8) -assert(IEC104_IE_QOC.QU_FLAG_RESERVED_PREDEFINED_FUNCTION_9 == 9) -assert(IEC104_IE_QOC.QU_FLAG_RESERVED_PREDEFINED_FUNCTION_15 == 15) +assert IEC104_IE_QOC.QU_FLAG_RESERVED_COMPATIBLE_4 == 4 +assert IEC104_IE_QOC.QU_FLAG_RESERVED_COMPATIBLE_8 == 8 +assert IEC104_IE_QOC.QU_FLAG_RESERVED_PREDEFINED_FUNCTION_9 == 9 +assert IEC104_IE_QOC.QU_FLAG_RESERVED_PREDEFINED_FUNCTION_15 == 15 = IEC60870_5_4_NormalizedFixPoint @@ -37,8 +37,8 @@ nfp = IEC60870_5_4_NormalizedFixPoint('foo', 0) for num_raw, num_fp, num_ss in test_data: i_val = nfp.getfield(None, num_raw)[1] - assert(i_val == num_ss) - assert(round(nfp.i2h(None, i_val), 6) == round(num_fp, 6)) + assert i_val == num_ss + assert round(nfp.i2h(None, i_val), 6) == round(num_fp, 6) = Iec104SequenceNumber field @@ -63,8 +63,8 @@ test_data = { } for key in test_data: - assert(iec104_seq_num.getfield(None, test_data[key])[1] == key) - assert(iec104_seq_num.addfield(None, b'', key) == test_data[key]) + assert iec104_seq_num.getfield(None, test_data[key])[1] == key + assert iec104_seq_num.addfield(None, b'', key) == test_data[key] + raw layer dissection @@ -73,14 +73,14 @@ for key in test_data: raw_u_msg = b'\x68\x04\x83\x00\x00\x00' lyr = iec104_decode(b'\x68\x04\x83\x00\x00\x00') -assert(lyr.__class__ == IEC104_U_Message) +assert lyr.__class__ == IEC104_U_Message = IEC104_S_Message raw_s_msg = b'\x68\x04\x01\x00\xa6\x17' lyr = iec104_decode(raw_s_msg) -assert(lyr.__class__ == IEC104_S_Message) +assert lyr.__class__ == IEC104_S_Message = IEC104_I_Message_SeqIOA @@ -88,14 +88,14 @@ raw_i_msg_seq_ioa = b'\x68\x1f\x2c\x00\x04\x00' # APCI raw_i_msg_seq_ioa += b'\x01\x92\x14\x00\x23\x00\x12\x54\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # ASDU lyr = iec104_decode(raw_i_msg_seq_ioa) -assert(lyr.__class__ == IEC104_I_Message_SeqIOA) +assert lyr.__class__ == IEC104_I_Message_SeqIOA = IEC104_I_Message_SingleIOA raw_i_msg_single_ioa = b'\x68\x0e\x00\x00\x00\x00\x64\x01\x06\x00\x0a\x00\x00\x00\x00\x14' lyr = iec104_decode(raw_i_msg_single_ioa) -assert(lyr.__class__ == IEC104_I_Message_SingleIOA) +assert lyr.__class__ == IEC104_I_Message_SingleIOA + IEC104 S Message @@ -105,21 +105,21 @@ s_msg = b'\x68\x04\x01\x00\xa6\x17' s_msg = IEC104_S_Message(s_msg) -assert(s_msg.rx_seq_num == 3027) +assert s_msg.rx_seq_num == 3027 raw_s_message = b'\x00\x14\xab\x00\x3c\x13\x00\x1b\x8d\xf1\xdc\x12\x08\x00\x45\x10\x00\x3a\x8d\xdb\x40\x00\x3d\x06\x54\x46\x1a\x52\x01\xde\xc1\x28\x15\x5c\xaa\x56\x09\x64\x16\x67\x6c\xd7\x53\x07\x28\x98\x80\x18\x79\x5e\x9b\x14\x00\x00\x01\x01\x08\x0a\x9e\x08\xaa\x23\x73\xe8\x6c\xc3\x68\x04\x01\x00\xc2\x3a' frm = Ether(raw_s_message) s_msg = frm.getlayer(IEC104_S_Message) -assert(s_msg) -assert(s_msg.rx_seq_num == 7521) +assert s_msg +assert s_msg.rx_seq_num == 7521 frm = Ether(frm.do_build()) s_msg = frm.getlayer(IEC104_S_Message) -assert(s_msg) -assert(s_msg.rx_seq_num == 7521) +assert s_msg +assert s_msg.rx_seq_num == 7521 = double IEC104 S Message (test layer binding) @@ -128,22 +128,22 @@ raw_double_s_message = b'\x00\x14\xab\x00\x3c\x13\x00\x1b\x8d\xf1\xdc\x12\x08\x0 frm = Ether(raw_double_s_message) s_msg = frm.getlayer(IEC104_S_Message) -assert(s_msg) -assert(s_msg.rx_seq_num == 7521) +assert s_msg +assert s_msg.rx_seq_num == 7521 s_msg = frm.getlayer(IEC104_S_Message, nb=2) -assert(s_msg) -assert(s_msg.rx_seq_num == 7649) +assert s_msg +assert s_msg.rx_seq_num == 7649 frm = Ether(frm.do_build()) s_msg = frm.getlayer(IEC104_S_Message) -assert(s_msg) -assert(s_msg.rx_seq_num == 7521) +assert s_msg +assert s_msg.rx_seq_num == 7521 s_msg = frm.getlayer(IEC104_S_Message, nb=2) -assert(s_msg) -assert(s_msg.rx_seq_num == 7649) +assert s_msg +assert s_msg.rx_seq_num == 7649 + IEC104 U Message @@ -152,37 +152,37 @@ assert(s_msg.rx_seq_num == 7649) frm = Ether()/IP()/TCP()/IEC104_U_Message(startdt_act = 1, stopdt_con = 1, testfr_act=1) frm = Ether(frm.do_build()) u_msg = frm.getlayer(IEC104_U_Message) -assert(u_msg) -assert(u_msg.startdt_act == 1) -assert(u_msg.startdt_con == 0) -assert(u_msg.stopdt_con == 1) -assert(u_msg.stopdt_act == 0) -assert(u_msg.testfr_act == 1) -assert(u_msg.testfr_con == 0) +assert u_msg +assert u_msg.startdt_act == 1 +assert u_msg.startdt_con == 0 +assert u_msg.stopdt_con == 1 +assert u_msg.stopdt_act == 0 +assert u_msg.testfr_act == 1 +assert u_msg.testfr_con == 0 u_msg_tst_act = b'\x68\x04\x43\x00\x00\x00' u_msg = IEC104_U_Message(u_msg_tst_act) -assert(u_msg.testfr_act == 1) +assert u_msg.testfr_act == 1 u_msg_tst_con = b'\x68\x04\x83\x00\x00\x00' u_msg = IEC104_U_Message(u_msg_tst_con) -assert(u_msg.testfr_con == 1) +assert u_msg.testfr_con == 1 u_msg_startdt_act = b'\x68\x04\x07\x00\x00\x00' u_msg = IEC104_U_Message(u_msg_startdt_act) -assert(u_msg.startdt_act == 1) +assert u_msg.startdt_act == 1 u_msg_startdt_con = b'\x68\x04\x0b\x00\x00\x00' u_msg = IEC104_U_Message(u_msg_startdt_con) -assert(u_msg.startdt_con == 1) +assert u_msg.startdt_con == 1 u_msg_stopdt_act = b'\x68\x04\x13\x00\x00\x00' u_msg = IEC104_U_Message(u_msg_stopdt_act) -assert(u_msg.stopdt_act == 1) +assert u_msg.stopdt_act == 1 u_msg_stopdt_con = b'\x68\x04\x23\x00\x00\x00' u_msg = IEC104_U_Message(u_msg_stopdt_con) -assert(u_msg.stopdt_con == 1) +assert u_msg.stopdt_con == 1 = double IEC104 U Message @@ -192,16 +192,16 @@ frm = Ether()/IP()/TCP()/\ frm = Ether(frm.do_build()) u_msg = frm.getlayer(IEC104_U_Message) -assert(u_msg) -assert(u_msg.startdt_act == 1) -assert(u_msg.stopdt_con == 1) -assert(u_msg.testfr_act == 1) +assert u_msg +assert u_msg.startdt_act == 1 +assert u_msg.stopdt_con == 1 +assert u_msg.testfr_act == 1 u_msg = frm.getlayer(IEC104_U_Message, nb=2) -assert(u_msg) -assert(u_msg.startdt_con == 1) -assert(u_msg.stopdt_act == 1) -assert(u_msg.testfr_con == 1) +assert u_msg +assert u_msg.startdt_con == 1 +assert u_msg.stopdt_act == 1 +assert u_msg.testfr_con == 1 + IEC104 I Message @@ -212,7 +212,7 @@ for io_id in IEC104_IO_CLASSES: frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/IEC104_I_Message_SeqIOA(io=io_class()) frm = Ether(frm.do_build()) io_layer = frm.getlayer(io_class) - assert(io_layer) + assert io_layer = Single IOA, single IO - information object types dissection @@ -221,7 +221,7 @@ for io_id in IEC104_IO_WITH_IOA_CLASSES: frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/IEC104_I_Message_SingleIOA(io=io_class()) frm = Ether(frm.do_build()) io_layer = frm.getlayer(io_class) - assert(io_layer) + assert io_layer = Sequence IOA, multiple IOs - information object types dissection @@ -229,9 +229,9 @@ frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/IEC104_I_Message_Se frm = Ether(frm.do_build()) i_msg_lyr = frm.getlayer(IEC104_I_Message_SeqIOA) -assert(i_msg_lyr) +assert i_msg_lyr -assert(i_msg_lyr.information_object_address == 1234) +assert i_msg_lyr.information_object_address == 1234 m_sp_ta_1_lyr = i_msg_lyr.io[0] assert (m_sp_ta_1_lyr.minutes == 1) @@ -249,7 +249,7 @@ frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/\ frm = Ether(frm.do_build()) i_msg_lyr = frm.getlayer(IEC104_I_Message_SingleIOA) -assert(i_msg_lyr) +assert i_msg_lyr m_sp_ta_1_lyr = i_msg_lyr.io[0] assert (m_sp_ta_1_lyr.information_object_address==1111) @@ -274,22 +274,22 @@ frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/\ frm = Ether(frm.do_build()) i_msg_lyr = frm.getlayer(IEC104_I_Message_SeqIOA, nb=1) -assert(i_msg_lyr) +assert i_msg_lyr assert (i_msg_lyr.information_object_address == 1234) -assert(len(i_msg_lyr.io) == 2) -assert(i_msg_lyr.io[0].minutes == 1) -assert(i_msg_lyr.io[0].sec_milli == 2) -assert(i_msg_lyr.io[1].minutes == 3) -assert(i_msg_lyr.io[1].sec_milli == 4) +assert len(i_msg_lyr.io) == 2 +assert i_msg_lyr.io[0].minutes == 1 +assert i_msg_lyr.io[0].sec_milli == 2 +assert i_msg_lyr.io[1].minutes == 3 +assert i_msg_lyr.io[1].sec_milli == 4 i_msg_lyr = frm.getlayer(IEC104_I_Message_SeqIOA, nb=2) -assert(i_msg_lyr) +assert i_msg_lyr assert (i_msg_lyr.information_object_address == 5432) -assert(len(i_msg_lyr.io) == 2) -assert(i_msg_lyr.io[0].minutes == 5) -assert(i_msg_lyr.io[0].sec_milli == 6) -assert(i_msg_lyr.io[1].minutes == 7) -assert(i_msg_lyr.io[1].sec_milli == 8) +assert len(i_msg_lyr.io) == 2 +assert i_msg_lyr.io[0].minutes == 5 +assert i_msg_lyr.io[0].sec_milli == 6 +assert i_msg_lyr.io[1].minutes == 7 +assert i_msg_lyr.io[1].sec_milli == 8 = Single IOA, multiple APDUs @@ -305,24 +305,24 @@ frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/\ frm = Ether(frm.do_build()) i_msg_lyr = frm.getlayer(IEC104_I_Message_SingleIOA, nb=1) -assert(i_msg_lyr) -assert(len(i_msg_lyr.io) == 2) +assert i_msg_lyr +assert len(i_msg_lyr.io) == 2 assert (i_msg_lyr.io[0].information_object_address == 1111) -assert(i_msg_lyr.io[0].minutes == 1) -assert(i_msg_lyr.io[0].sec_milli == 2) +assert i_msg_lyr.io[0].minutes == 1 +assert i_msg_lyr.io[0].sec_milli == 2 assert (i_msg_lyr.io[1].information_object_address == 2222) -assert(i_msg_lyr.io[1].minutes == 3) -assert(i_msg_lyr.io[1].sec_milli == 4) +assert i_msg_lyr.io[1].minutes == 3 +assert i_msg_lyr.io[1].sec_milli == 4 i_msg_lyr = frm.getlayer(IEC104_I_Message_SingleIOA, nb=2) -assert(i_msg_lyr) -assert(len(i_msg_lyr.io) == 2) +assert i_msg_lyr +assert len(i_msg_lyr.io) == 2 assert (i_msg_lyr.io[0].information_object_address == 3333) -assert(i_msg_lyr.io[0].minutes == 5) -assert(i_msg_lyr.io[0].sec_milli == 6) +assert i_msg_lyr.io[0].minutes == 5 +assert i_msg_lyr.io[0].sec_milli == 6 assert (i_msg_lyr.io[1].information_object_address == 4444) -assert(i_msg_lyr.io[1].minutes == 7) -assert(i_msg_lyr.io[1].sec_milli == 8) +assert i_msg_lyr.io[1].minutes == 7 +assert i_msg_lyr.io[1].sec_milli == 8 = Mixed Single and Sequence IOA, multiple APDU @@ -342,15 +342,15 @@ frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/\ frm = Ether(frm.do_build()) i_msg_lyr = frm.getlayer(IEC104_I_Message_SeqIOA, nb=1) -assert(i_msg_lyr) +assert i_msg_lyr assert (i_msg_lyr.information_object_address == 1111) i_msg_lyr = frm.getlayer(IEC104_I_Message_SeqIOA, nb=2) -assert(i_msg_lyr) +assert i_msg_lyr assert (i_msg_lyr.information_object_address == 5555) i_msg_lyr = frm.getlayer(IEC104_I_Message_SingleIOA, nb=1) -assert(i_msg_lyr) +assert i_msg_lyr assert (i_msg_lyr.io[0].information_object_address == 3333) + mixed APDU types in one packet @@ -368,27 +368,27 @@ frm = Ether()/IP()/TCP(sport=IEC_104_IANA_PORT, dport=56780)/\ frm = Ether(frm.do_build()) i_msg = frm.getlayer(IEC104_I_Message_SeqIOA) -assert(i_msg) +assert i_msg u_msg = frm.getlayer(IEC104_U_Message) -assert(u_msg) +assert u_msg s_msg = frm.getlayer(IEC104_S_Message) -assert(s_msg) +assert s_msg + information elements & objects = ASDU allowed in given standard (examples) layer = IEC104_IO_M_SP_NA_1() -assert(layer.defined_for_iec_101() is True) -assert(layer.defined_for_iec_104() is True) +assert layer.defined_for_iec_101() is True +assert layer.defined_for_iec_104() is True layer = IEC104_IO_M_DP_TA_1() -assert(layer.defined_for_iec_101() is True) -assert(layer.defined_for_iec_104() is False) +assert layer.defined_for_iec_101() is True +assert layer.defined_for_iec_104() is False layer = IEC104_IO_C_SC_TA_1() -assert(layer.defined_for_iec_101() is False) -assert(layer.defined_for_iec_104() is True) +assert layer.defined_for_iec_101() is False +assert layer.defined_for_iec_104() is True = BCR - binary counter reading / IEC104_IO_M_IT_NA_1 - integrated totals @@ -402,11 +402,11 @@ for value, sequence in values: frm = Ether()/IP()/TCP()/IEC104_I_Message_SeqIOA(io=m_it_na) frm = Ether(frm.do_build()) i_msg = frm.getlayer(IEC104_I_Message_SeqIOA) -assert(i_msg) +assert i_msg for idx, value in enumerate(values): value, sequence = value - assert(i_msg.io[idx].counter_value == value) + assert i_msg.io[idx].counter_value == value assert (i_msg.io[idx].sq == sequence) = DIQ - double-point information with quality descriptor / IEC104_IO_M_DP_NA_1 - double-point information without time tag @@ -415,8 +415,8 @@ frm = Ether() / IP() / TCP() / IEC104_I_Message_SeqIOA(io=IEC104_IO_M_DP_NA_1(dp frm = Ether(frm.do_build()) i_msg = frm.getlayer(IEC104_I_Message_SeqIOA) -assert(i_msg) -assert(i_msg.io[0].dpi_value==IEC104_IE_DIQ.DPI_FLAG_STATE_UNDEFINED) +assert i_msg +assert i_msg.io[0].dpi_value==IEC104_IE_DIQ.DPI_FLAG_STATE_UNDEFINED = VTI - value with transient state indication / IEC104_IO_M_ST_NA_1 - step position information diff --git a/test/contrib/ife.uts b/test/contrib/ife.uts index f990d673d08..f39ed148fae 100644 --- a/test/contrib/ife.uts +++ b/test/contrib/ife.uts @@ -12,21 +12,21 @@ frm = Ether()/IFE(tlvs=[IFESKBMark(value=3), IFETCIndex(value=5)]) frm = Ether(bytes(frm)) -assert(IFE in frm) -assert(frm[IFE].tlvs[0].type == 1) -assert(frm[IFE].tlvs[0].length == 8) -assert(frm[IFE].tlvs[0].value == 3) -assert(frm[IFE].tlvs[1].type == 5) -assert(frm[IFE].tlvs[1].length == 6) -assert(frm[IFE].tlvs[1].value == 5) +assert IFE in frm +assert frm[IFE].tlvs[0].type == 1 +assert frm[IFE].tlvs[0].length == 8 +assert frm[IFE].tlvs[0].value == 3 +assert frm[IFE].tlvs[1].type == 5 +assert frm[IFE].tlvs[1].length == 6 +assert frm[IFE].tlvs[1].value == 5 = add padding if required frm = Ether()/IFE(tlvs=[IFETCIndex()]) -assert(len(raw(frm)) == 24) +assert len(raw(frm)) == 24 frm = Ether()/IFE(tlvs=[IFESKBMark(), IFETCIndex()]) -assert(len(raw(frm)) == 32) +assert len(raw(frm)) == 32 = variable payload diff --git a/test/contrib/ikev2.uts b/test/contrib/ikev2.uts index d03e055d7f9..21ef35fdfda 100644 --- a/test/contrib/ikev2.uts +++ b/test/contrib/ikev2.uts @@ -1,4 +1,9 @@ -% Ikev2 Tests +% Ikev2 unit tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('ikev2')" -t test/contrib/ikev2.uts + + * Tests for the Ikev2 layer + Basic Layer Tests @@ -11,20 +16,20 @@ assert raw(a) == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ = Ikev2 dissection a = IKEv2(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00! \x00\x00\x00\x00\x00\x00\x00\x00\x000\x00\x00\x00\x14\x00\x00\x00\x10\x01\x01\x00\x00\x00\x00\x00\x08\x02\x00\x00\x03") -assert a[IKEv2_payload_Transform].transform_type == 2 -assert a[IKEv2_payload_Transform].transform_id == 3 +assert a[IKEv2_Transform].transform_type == 2 +assert a[IKEv2_Transform].transform_id == 3 assert a.next_payload == 33 -assert a[IKEv2_payload_SA].next_payload == 0 -assert a[IKEv2_payload_Proposal].next_payload == 0 -assert a[IKEv2_payload_Proposal].proposal == 1 -assert a[IKEv2_payload_Transform].next_payload == 0 -a[IKEv2_payload_Transform].show() +assert a[IKEv2_SA].next_payload == 0 +assert a[IKEv2_Proposal].next_payload == 0 +assert a[IKEv2_Proposal].proposal == 1 +assert a[IKEv2_Transform].next_payload == 0 +a[IKEv2_Transform].show() = Build Ikev2 SA request packet -a = IKEv2(init_SPI="MySPI",exch_type=34)/IKEv2_payload_SA(prop=IKEv2_payload_Proposal()) -assert raw(a) == b'MySPI\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00! "\x00\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x0c\x00\x00\x00\x08\x01\x01\x00\x00' +a = IKEv2(init_SPI="MySPI",exch_type=34)/IKEv2_SA(flags="critical", prop=IKEv2_Proposal()) +assert raw(a) == b'MySPI\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00! "\x00\x00\x00\x00\x00\x00\x00\x00(\x00\x80\x00\x0c\x00\x00\x00\x08\x01\x01\x00\x00' = Build advanced IKEv2 @@ -34,20 +39,20 @@ key_exchange = binascii.unhexlify('bb41bb41cfaf34e3b3209672aef1c51b9d52919f1781d nonce = binascii.unhexlify('8dfcf8384c5c32f1b294c64eab69f98e9d8cf7e7f352971a91ff6777d47dffed') nat_detection_source_ip = binascii.unhexlify('e64c81c4152ad83bd6e035009fbb900406be371f') nat_detection_destination_ip = binascii.unhexlify('28cd99b9fa1267654b53f60887c9c35bcf67a8ff') -transform_1 = IKEv2_payload_Transform(next_payload = 'Transform', transform_type = 'Encryption', transform_id = 12, length = 12, key_length = 0x80) -transform_2 = IKEv2_payload_Transform(next_payload = 'Transform', transform_type = 'PRF', transform_id = 2) -transform_3 = IKEv2_payload_Transform(next_payload = 'Transform', transform_type = 'Integrity', transform_id = 2) -transform_4 = IKEv2_payload_Transform(next_payload = 'last', transform_type = 'GroupDesc', transform_id = 2) +transform_1 = IKEv2_Transform(next_payload = 'Transform', transform_type = 'Encryption', transform_id = 12, length = 12, key_length = 0x80) +transform_2 = IKEv2_Transform(next_payload = 'Transform', transform_type = 'PRF', transform_id = 2) +transform_3 = IKEv2_Transform(next_payload = 'Transform', transform_type = 'Integrity', transform_id = 2) +transform_4 = IKEv2_Transform(next_payload = 'None', transform_type = 'GroupDesc', transform_id = 2) packet = IP(dst = '192.168.1.10', src = '192.168.1.130') /\ UDP(dport = 500) /\ IKEv2(init_SPI = b'KWdxMhjA', next_payload = 'SA', exch_type = 'IKE_SA_INIT', flags='Initiator') /\ - IKEv2_payload_SA(next_payload = 'KE', prop = IKEv2_payload_Proposal(trans_nb = 4, trans = transform_1 / transform_2 / transform_3 / transform_4, )) /\ - IKEv2_payload_KE(next_payload = 'Nonce', group = '1024MODPgr', load = key_exchange) /\ - IKEv2_payload_Nonce(next_payload = 'Notify', load = nonce) /\ - IKEv2_payload_Notify(next_payload = 'Notify', type = 16388, load = nat_detection_source_ip) /\ - IKEv2_payload_Notify(next_payload = 'None', type = 16389, load = nat_detection_destination_ip) + IKEv2_SA(next_payload = 'KE', prop = IKEv2_Proposal(trans_nb = 4, trans = transform_1 / transform_2 / transform_3 / transform_4, )) /\ + IKEv2_KE(next_payload = 'Nonce', group = '1024MODPgr', ke = key_exchange) /\ + IKEv2_Nonce(next_payload = 'Notify', nonce = nonce) /\ + IKEv2_Notify(next_payload = 'Notify', type = 16388, notify = nat_detection_source_ip) /\ + IKEv2_Notify(next_payload = 'None', type = 16389, notify = nat_detection_destination_ip) -assert raw(packet) == b'E\x00\x01L\x00\x01\x00\x00@\x11\xf5\xc3\xc0\xa8\x01\x82\xc0\xa8\x01\n\x11\x94\x01\xf4\x018\x97 KWdxMhjA\x00\x00\x00\x00\x00\x00\x00\x00! "\x08\x00\x00\x00\x00\x00\x00\x010"\x00\x000\x00\x00\x00,\x01\x01\x00\x04\x03\x00\x00\x0c\x01\x00\x00\x0c\x80\x0e\x00\x80\x03\x00\x00\x08\x02\x00\x00\x02\x03\x00\x00\x08\x03\x00\x00\x02\x00\x00\x00\x08\x04\x00\x00\x02(\x00\x00\x88\x00\x02\x00\x00\xbbA\xbbA\xcf\xaf4\xe3\xb3 \x96r\xae\xf1\xc5\x1b\x9dR\x91\x9f\x17\x81\xd0\xb4\xcd\x88\x9dJ\xaf\xe2ah\x87v\x00\x0c=\x901PZ\xef\xc0\x18ig\xea\xf5\xa7f7%\xfb\x10,Y\xc3\x9bzp\xd8\xd9\x16\x1c;\xd0\xebDX\x88\xb5\x02\x8e\xa0c\xba\n\xe0\x1f[?0\x80\x8akg\x10\xdc\x9b\xab`\x1eA\x16\x15}\x7fX\xcf\x83\\\xb63\xc6J\xbc\xb3\xa5\xc6\x1c">\x932S\x8b\xfc\x9f(,\xb6-\x1f\x00\xf4\xee\x88\x02)\x00\x00$\x8d\xfc\xf88L\\2\xf1\xb2\x94\xc6N\xabi\xf9\x8e\x9d\x8c\xf7\xe7\xf3R\x97\x1a\x91\xffgw\xd4}\xff\xed)\x00\x00\x1c\x00\x00@\x04\xe6L\x81\xc4\x15*\xd8;\xd6\xe05\x00\x9f\xbb\x90\x04\x06\xbe7\x1f\x00\x00\x00\x1c\x00\x00@\x05(\xcd\x99\xb9\xfa\x12geKS\xf6\x08\x87\xc9\xc3[\xcfg\xa8\xff' +assert raw(packet) == b'E\x00\x01L\x00\x01\x00\x00@\x11\xf5\xc3\xc0\xa8\x01\x82\xc0\xa8\x01\n\x01\xf4\x01\xf4\x018\xa6\xc0KWdxMhjA\x00\x00\x00\x00\x00\x00\x00\x00! "\x08\x00\x00\x00\x00\x00\x00\x010"\x00\x000\x00\x00\x00,\x01\x01\x00\x04\x03\x00\x00\x0c\x01\x00\x00\x0c\x80\x0e\x00\x80\x03\x00\x00\x08\x02\x00\x00\x02\x03\x00\x00\x08\x03\x00\x00\x02\x00\x00\x00\x08\x04\x00\x00\x02(\x00\x00\x88\x00\x02\x00\x00\xbbA\xbbA\xcf\xaf4\xe3\xb3 \x96r\xae\xf1\xc5\x1b\x9dR\x91\x9f\x17\x81\xd0\xb4\xcd\x88\x9dJ\xaf\xe2ah\x87v\x00\x0c=\x901PZ\xef\xc0\x18ig\xea\xf5\xa7f7%\xfb\x10,Y\xc3\x9bzp\xd8\xd9\x16\x1c;\xd0\xebDX\x88\xb5\x02\x8e\xa0c\xba\n\xe0\x1f[?0\x80\x8akg\x10\xdc\x9b\xab`\x1eA\x16\x15}\x7fX\xcf\x83\\\xb63\xc6J\xbc\xb3\xa5\xc6\x1c">\x932S\x8b\xfc\x9f(,\xb6-\x1f\x00\xf4\xee\x88\x02)\x00\x00$\x8d\xfc\xf88L\\2\xf1\xb2\x94\xc6N\xabi\xf9\x8e\x9d\x8c\xf7\xe7\xf3R\x97\x1a\x91\xffgw\xd4}\xff\xed)\x00\x00\x1c\x00\x00@\x04\xe6L\x81\xc4\x15*\xd8;\xd6\xe05\x00\x9f\xbb\x90\x04\x06\xbe7\x1f\x00\x00\x00\x1c\x00\x00@\x05(\xcd\x99\xb9\xfa\x12geKS\xf6\x08\x87\xc9\xc3[\xcfg\xa8\xff' ## packets taken from ## https://github.com/wireshark/wireshark/blob/master/test/captures/ikev2-decrypt-aes128ccm12.pcap @@ -55,24 +60,24 @@ assert raw(packet) == b'E\x00\x01L\x00\x01\x00\x00@\x11\xf5\xc3\xc0\xa8\x01\x82\ = Dissect Initiator Request a = Ether(b'\x00!k\x91#H\xb8\'\xeb\xa6XI\x08\x00E\x00\x01\x14u\xc2@\x00@\x11@\xb6\xc0\xa8\x01\x02\xc0\xa8\x01\x0e\x01\xf4\x01\xf4\x01\x00=8\xeahM!Yz\xfd6\x00\x00\x00\x00\x00\x00\x00\x00! "\x08\x00\x00\x00\x00\x00\x00\x00\xf8"\x00\x00(\x00\x00\x00$\x01\x01\x00\x03\x03\x00\x00\x0c\x01\x00\x00\x0f\x80\x0e\x00\x80\x03\x00\x00\x08\x02\x00\x00\x05\x00\x00\x00\x08\x04\x00\x00\x13(\x00\x00H\x00\x13\x00\x002\xc6\xdf\xfe\\C\xb0\xd5\x81\x1f~\xaa\xa8L\x9fx\xbf\x99\xb9\x06\x9c+\x07.\x0b\x82\xf4k\xf6\xf6m\xd4_\x97\xef\x89\xee(_\xd5\xdfRzDwkR\x9f\xc9\xd8\xa9\t\xd8B\xa6\xfbY\xb9j\tS\x95ar)\x00\x00$\xb6UF-oKf\xf8r\xcc\xd7\xf0\xf4\xb4\x85w2\x92\x139\xcb\xaaR7\xed\xba$O&+h#)\x00\x00\x1c\x00\x00@\x04\x94\x9c\x9d\xb5s\x9du\xa9t\xa4\x9c\x18F\x186\x9b4\xb7\xf9B)\x00\x00\x1c\x00\x00@\x05>r\x1bF\xbe\x07\xd51\x11B]\x7f\x80\xd2\xc6\xe2 \xc6\x07.\x00\x00\x00\x10\x00\x00@/\x00\x01\x00\x02\x00\x03\x00\x04') -assert a[IKEv2_payload_SA].prop.trans.transform_id == 15 -assert a[IKEv2_payload_Notify].next_payload == 41 -assert IP(a[IKEv2_payload_Notify].load).src == "70.24.54.155" -assert IP(a[IKEv2_payload_Notify].payload.load).dst == "32.198.7.46" +assert a[IKEv2_SA].prop.trans.transform_id == 15 +assert a[IKEv2_Notify].next_payload == 41 +assert IP(a[IKEv2_Notify].notify).src == "70.24.54.155" +assert IP(a[IKEv2_Notify].payload.notify).dst == "32.198.7.46" = Dissect Responder Response b = Ether(b'\xb8\'\xeb\xa6XI\x00!k\x91#H\x08\x00E\x00\x01\x0c\xd2R@\x00@\x11\xe4-\xc0\xa8\x01\x0e\xc0\xa8\x01\x02\x01\xf4\x01\xf4\x00\xf8\x07\xdd\xeahM!Yz\xfd6\xd9\xfe*\xb2-\xac#\xac! " \x00\x00\x00\x00\x00\x00\x00\xf0"\x00\x00(\x00\x00\x00$\x01\x01\x00\x03\x03\x00\x00\x0c\x01\x00\x00\x0f\x80\x0e\x00\x80\x03\x00\x00\x08\x02\x00\x00\x05\x00\x00\x00\x08\x04\x00\x00\x13(\x00\x00H\x00\x13\x00\x00,f\xbe\xad\xb6\xce\x855\xd6!\x8c\xb4\x01\xaaZ\x1e\xb4\x03[\x97\xca\xdd\xaf67J\x97\x9c\x04F\xb8\x80\x05\x06\xbf\x9do\x95\tR2k\xf3\x01\x19\x13\xda\x93\xbb\x8e@\xf8\x157k\xe1\xa0h\x01\xc0\xa6>;T)\x00\x00$\x9e]&sy\xe6\x81\xe7\xd3\x8d\x81\xc7\x10\xd3\x83@\x1d\xe7\xe3`{\x92m\x90\xa9\x95\x8a\xdc\xb5(1\xaa)\x00\x00\x1c\x00\x00@\x04z\x07\x85\'=Y 8)\xa6\x97U\x0f1\xcb\xb9N\xb7+C)\x00\x00\x1c\x00\x00@\x05\xc3\xe5\x8a\x8c\xc9\x93<\xe0\xb7\x8f*P\xe8\xde\x80\x13N\x12\xce1\x00\x00\x00\x08\x00\x00@\x14') assert b[UDP].dport == 500 -assert b[IKEv2_payload_KE].load == b',f\xbe\xad\xb6\xce\x855\xd6!\x8c\xb4\x01\xaaZ\x1e\xb4\x03[\x97\xca\xdd\xaf67J\x97\x9c\x04F\xb8\x80\x05\x06\xbf\x9do\x95\tR2k\xf3\x01\x19\x13\xda\x93\xbb\x8e@\xf8\x157k\xe1\xa0h\x01\xc0\xa6>;T' -assert b[IKEv2_payload_Nonce].payload.type == 16388 -assert b[IKEv2_payload_Nonce].payload.payload.payload.next_payload == 0 +assert b[IKEv2_KE].ke == b',f\xbe\xad\xb6\xce\x855\xd6!\x8c\xb4\x01\xaaZ\x1e\xb4\x03[\x97\xca\xdd\xaf67J\x97\x9c\x04F\xb8\x80\x05\x06\xbf\x9do\x95\tR2k\xf3\x01\x19\x13\xda\x93\xbb\x8e@\xf8\x157k\xe1\xa0h\x01\xc0\xa6>;T' +assert b[IKEv2_Nonce].payload.type == 16388 +assert b[IKEv2_Nonce].payload.payload.payload.next_payload == 0 -= Dissect Encrypted Inititor Request += Dissect Encrypted Initiator Request a = Ether(b"\x00!k\x91#H\xb8'\xeb\xa6XI\x08\x00E\x00\x00Yu\xe2@\x00@\x11AQ\xc0\xa8\x01\x02\xc0\xa8\x01\x0e\x01\xf4\x01\xf4\x00E}\xe0\xeahM!Yz\xfd6\xd9\xfe*\xb2-\xac#\xac. %\x08\x00\x00\x00\x02\x00\x00\x00=*\x00\x00!\xcc\xa0\xb3]\xe5\xab\xc5\x1c\x99\x87\xcb\xf1\xf5\xec\xff!\x0e\xb7g\xcd\xb8Qy8;\x96Mx\xe2") -assert a[IKEv2_payload_Encrypted].next_payload == 42 -assert a[IKEv2_payload_Encrypted].load == b'\xcc\xa0\xb3]\xe5\xab\xc5\x1c\x99\x87\xcb\xf1\xf5\xec\xff!\x0e\xb7g\xcd\xb8Qy8;\x96Mx\xe2' +assert a[IKEv2_Encrypted].next_payload == 42 +assert a[IKEv2_Encrypted].load == b'\xcc\xa0\xb3]\xe5\xab\xc5\x1c\x99\x87\xcb\xf1\xf5\xec\xff!\x0e\xb7g\xcd\xb8Qy8;\x96Mx\xe2' = Dissect Encrypted Responder Response @@ -80,17 +85,28 @@ b = Ether(b"\xb8'\xeb\xa6XI\x00!k\x91#H\x08\x00E\x00\x00Q\xd5y@\x00@\x11\xe1\xc1 assert b[IKEv2].init_SPI == b'\xeahM!Yz\xfd6' assert b[IKEv2].resp_SPI == b'\xd9\xfe*\xb2-\xac#\xac' assert b[IKEv2].next_payload == 46 -assert b[IKEv2_payload_Encrypted].load == b'\xa8\x0c\x95{\xac\x15\xc3\xf8\xaf\xdf1Z\x81\xccK|@\xe8f\rD' +assert b[IKEv2_Encrypted].load == b'\xa8\x0c\x95{\xac\x15\xc3\xf8\xaf\xdf1Z\x81\xccK|@\xe8f\rD' = Test Certs detection -a = IKEv2_payload_CERT(raw(IKEv2_payload_CERT_CRL())) -b = IKEv2_payload_CERT(raw(IKEv2_payload_CERT_STR())) -c = IKEv2_payload_CERT(raw(IKEv2_payload_CERT_CRT())) +a = IKEv2_CERT(raw(IKEv2_CERT(cert_encoding = "X.509 Certificate - Signature"))) +b = IKEv2_CERT(raw(IKEv2_CERT(cert_encoding ="Certificate Revocation List (CRL)"))) +c = IKEv2_CERT(raw(IKEv2_CERT(cert_encoding = 0))) + +assert a.cert_encoding == 4 +assert isinstance(a.cert_data, X509_Cert) +assert b.cert_encoding == 7 +assert isinstance(b.cert_data, X509_CRL) +assert c.cert_encoding == 0 +assert isinstance(c.cert_data, bytes) + -assert isinstance(a, IKEv2_payload_CERT_CRL) -assert isinstance(b, IKEv2_payload_CERT_STR) -assert isinstance(c, IKEv2_payload_CERT_CRT) += Test Certs length calculations +## For the length calculations see Figure 12 in RFC 7296 + +assert a.length == len(a.cert_data) + 5 +assert b.length == len(b.cert_data) + 5 +assert c.length == len(c.cert_data) + 5 = Test TrafficSelector detection @@ -102,10 +118,1620 @@ assert isinstance(a, IPv4TrafficSelector) assert isinstance(b, IPv6TrafficSelector) assert isinstance(c, EncryptedTrafficSelector) -= IKEv2_payload_Encrypted_Fragment, simple tests += Test TSi with multiple TrafficSelector dissection + +a = IKEv2_TSi() +a.traffic_selector.extend(IPv4TrafficSelector() * 2) +a.traffic_selector.extend(IPv6TrafficSelector() * 3) +assert len(a.traffic_selector) == 5 + +b = IKEv2_TSi(raw(a)) +assert len(b.traffic_selector) == 5 + += Test automatic calculation of number_of_TSs field + +a = IKEv2_TSi(traffic_selector=IPv4TrafficSelector() * 2) +b = IKEv2_TSi(raw(a)) +assert b.number_of_TSs == 2 + +c = IKEv2_TSr(traffic_selector=IPv4TrafficSelector() * 2) +d = IKEv2_TSr(raw(c)) +assert d.number_of_TSs == 2 + += IKEv2_Encrypted_Fragment, simple tests s = b"\x00\x00\x00\x08\x00\x01\x00\x01" -assert raw(IKEv2_payload_Encrypted_Fragment()) == s +assert raw(IKEv2_Encrypted_Fragment()) == s -p = IKEv2_payload_Encrypted_Fragment(s) +p = IKEv2_Encrypted_Fragment(s) assert p.length == 8 and p.frag_number == 1 + + += Build and dissect UDP encapsulated IKEv1 packets + +pkt = Ether() / IP() / UDP() / NON_ESP() / ISAKMP(init_cookie = b'\x01\x02\x03\x04\x05\x06\x07\x08', resp_cookie = b'\x08\x07\x06\x05\x04\x03\x02\x01') +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[NON_ESP].non_esp == 0x00 +assert pkt[ISAKMP].version == 0x10 +assert pkt[ISAKMP].init_cookie == b'\x01\x02\x03\x04\x05\x06\x07\x08' +assert pkt[ISAKMP].resp_cookie == b'\x08\x07\x06\x05\x04\x03\x02\x01' + +pkt = Ether(raw(pkt)) +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[NON_ESP].non_esp == 0x00 +assert pkt[ISAKMP].version == 0x10 +assert pkt[ISAKMP].init_cookie == b'\x01\x02\x03\x04\x05\x06\x07\x08' +assert pkt[ISAKMP].resp_cookie == b'\x08\x07\x06\x05\x04\x03\x02\x01' + + +# the IKEv1 and IKEv2 headers are compatible, so changing the version to 0x02... +pkt[ISAKMP].version = 0x20 +# ...should turn the ISAKMP packet into an IKEv2 packet after building and dissecting +pkt = Ether(raw(pkt)) +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[NON_ESP].non_esp == 0x00 +assert pkt[IKEv2].version == 0x20 +assert pkt[IKEv2].init_SPI == b'\x01\x02\x03\x04\x05\x06\x07\x08' +assert pkt[IKEv2].resp_SPI == b'\x08\x07\x06\x05\x04\x03\x02\x01' + + += Build and dissect UDP encapsulated IKEv2 packets + +pkt = Ether() / IP() / UDP() / NON_ESP() / IKEv2(init_SPI = b'\x01\x02\x03\x04\x05\x06\x07\x08', resp_SPI = b'\x08\x07\x06\x05\x04\x03\x02\x01') +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[NON_ESP].non_esp == 0x00 +assert pkt[IKEv2].version == 0x20 +assert pkt[IKEv2].init_SPI == b'\x01\x02\x03\x04\x05\x06\x07\x08' +assert pkt[IKEv2].resp_SPI == b'\x08\x07\x06\x05\x04\x03\x02\x01' + +pkt = Ether(raw(pkt)) +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[NON_ESP].non_esp == 0x00 +assert pkt[IKEv2].version == 0x20 +assert pkt[IKEv2].init_SPI == b'\x01\x02\x03\x04\x05\x06\x07\x08' +assert pkt[IKEv2].resp_SPI == b'\x08\x07\x06\x05\x04\x03\x02\x01' + +# the IKEv1 and IKEv2 headers are compatible, so changing the version to 0x01... +pkt[IKEv2].version = 0x10 +# ...should turn the IKEv2 packet into an ISAKMP packet after building and dissecting +pkt = Ether(raw(pkt)) +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[NON_ESP].non_esp == 0x00 +assert pkt[ISAKMP].version == 0x10 +assert pkt[ISAKMP].init_cookie == b'\x01\x02\x03\x04\x05\x06\x07\x08' +assert pkt[ISAKMP].resp_cookie == b'\x08\x07\x06\x05\x04\x03\x02\x01' + + += Build and dissect UDP encapsulated ESP packets + +pkt = Ether() / IP() / UDP() / ESP(spi = 0x01020304) +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[ESP].spi == 0x01020304 + +pkt = Ether(raw(pkt)) +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[ESP].spi == 0x01020304 + += Build and dissect UDP encapsulated NAT-keepalive packets + +pkt = Ether() / IP() / UDP() / NAT_KEEPALIVE() +pkt.show() +assert pkt[UDP].sport == 4500 +assert pkt[UDP].dport == 4500 +assert pkt[NAT_KEEPALIVE].nat_keepalive == 0xFF + +pkt = Ether(b'DNm\xa4\xf6G`W\x18\x93\x9c\x7f\x08\x00E\x00\x00\x1d\xfb.\x00\x00\x80\x11\x9a\x16\xc0\xa8\x01\x1c>\x99\xa5-*\xca\x11\x94\x00\t\x1e\xf2\xff') +pkt.show() +assert pkt[UDP].dport == 4500 +assert pkt[NAT_KEEPALIVE].nat_keepalive == 0xFF + + ++ Wireshark Captures + += IKEv2 key exchange with NAT-traversal + +* Loads and dissects the four frames of the key exchange from a Wireshark +* capture and compares them with manually built scapy packets. + +pcap = rdpcap(scapy_path("/test/pcaps/ikev2_nat_t.pcapng"), count=4) + +ike_auth_request_encrypted_payload = binascii.unhexlify(''.join(""" + be11 14ab1abe02954640 ce512b03d6527a50 +dd17707ff420b9b5 b02d2874c57afdd3 fa95b15693017a12 8333c8d694f2cd61 +e98b0717f65e1860 430f0699a4174af6 a6c929ff4114b686 f201f471ff9b191e +4d4cbd43dd994ef6 d5179b6845843d2d 1502f16d4356dc3b ad819c1b0549296b +dbe479878dbc8a8b e71f9017946bc198 ef010f83a69a5d81 a312be0df9afa949 +e3f0807bd2785498 c0c492f0bcde5085 b2df1187657cbf23 e11c25558af278d0 +1bceadf5548a8990 a6adea270410cb16 1786e0798ed8f047 3442b43399e42122 +6f2ee1e2b0787dfc f56b7b32f3d0b02d 038764ce8ffee757 b94896763c68c2bb +2a94dec851dcf7e4 489ba8e431d1c63c f5d19a097674b513 58e6b5052a87dd48 +bb3be834b06ab704 579fcac6f6bf647c 87b4c5c0b7353df6 0b55e32a75ac4ced +3c1724d32a068207 226769352b08eefb 195da55e29c3eea1 05f0fd024029e0d7 +8b83757bd1b6052a 64febad6779cfca3 5b9a2529dc15d2a5 ee8825a2ab3e72ed +e84aaeb86e8debd6 2a9b3d6503dd6c1a 7e03b87b81578dc0 fb087a5ad2d6bf6b +d149d108defcabb5 721f8b4ebf1b9b78 80bdd2fc93856afe 4f54a32125964bbc +fd917239f5af1db9 cd3d188ab7165826 7a445c13d2147169 5da3f3a674c2baaf +5fd7636cc8ca4b43 142fd2588bb31fdd d6a42b20ebc03b01 04e8beb1356fc863 +0bd95de8574e16fe 14cfa9a6455e20e9 eb08bf632cea53e7 c614277e32fa81d9 +cb2efed29b04377a 748bfab753058349 f21a03fa5c5f478b c0bd993ca3e982b9 +d19fa8d24306e46a b41d9bbfd1d2e2da 112b6c840cc7b86b 8e005aa71b5339d1 +ff2eabb0124df2bf 910173c17380a7e3 85d22f94fa6e3f78 bce897a9a37e08c1 +1124661701dfd643 bba0c4ab4d8e19bb 95478e272d61c1a1 6d4e562f25c3c0a1 +69d39a84045183e2 684ac80ab6e18f20 dc4cc8d5b1d83293 07766d58695eff56 +14c207e045152933 07f9dbeb621e1c25 665f75f55e1ae90c aa43a500fa1ecf18 +3d7e7d46db8eae03 e1bc7a3aefab0c00 9884ca11e7889841 8459936a02699e5f +7f798d3c81de4933 a7f14f62aa5c31ae 2693089ca1df68a5 2cd338d5d2539053 +5099dd4f0646318f 079822b43f5a47b7 db9eba75ef843a42 98fb9e695a349824 +bef5ee441997f7c5 303c4f8288bb8be1 6cc72fc348c777ec 7ce8b0f032633890 +f01fbeef028f3bb5 ffd1ec663e9304cf 745d4659fc67f32d cffffa9deae65066 +5a2779b742057d71 86bd2603ce0946c4 1589d63fae9c404d 6c7f793a436c775a +d7d34f2dd609a272 4ac70b514a76d248 8eefb6fc2f3bd196 4dfc1a0d652e89a9 +e0b3278bc2c4c961 19df82bdc3b1f99d 399b0dbf62d23ea3 a7e940177525130b +df5960b33b3d2d73 28d98a5fd9bbec2e 71404b77facc8053 a14feafd49bf150f +450384b99d392549 31f06ac18d225368 5c52b4ee6ad50337 dbce7f72bf56e4bf +55fdf3fd42c39c7d 65a48987ad84d1e0 c4e4543463c95a8e 646744240fdc00b6 +0c009f4afd15b800 182a5004e4062557 e7b20115e01d1cc3 5eb8d01e22f0bf2d +bb2db84a970934d0 5f9b0d5e5350a45f 733a747e229eca56 087886a5c09efac8 +0c9545e6d849189b 40d7e7b9da4a9f04 9fb0273c3a2ad370 a84d5e7db14c362c +c84483bbe70f2573 8116b11b877a7939 628a2dec6a590056 fdc7ce849770f12d +0f63a701e672cf93 75c68c4325e60e3e ae46c7dd014df09d 4594339fa5e82ab3 +9de316df933694da e20120886403 +""".split())) + + +ike_auth_response_encrypted_payload = binascii.unhexlify(''.join(""" + 0fb3 4e8905b03a3d9b97 70f3e63428ab00be +1bc29397bec721ef 9bd02e6cc64a309b 0c0dd67e4442f235 c201ccb5f6b8c8b0 +26baaaf0dce597c0 dd610ebbc4aa2d07 8cbd6fdc2dd879a9 f3216edaabd965d8 +5fe04a202615c5c6 08b0caf7db24dc08 4d0d86e560ccb75e 209941a2945bab45 +0795b96cc4f03752 163825f1be62d009 038f29f25956f3e9 3648ea647af4fbea +52a19bbf16074ed3 9161cfd1a1695176 059cbfc48c57755f b1b1b397155171a0 +b11e10d3f476512b 73687912265ccb6f 1fef5aa5dee1ffc3 a5ecc574a76d529b +884f819f859c015a a3977230a69657d7 1d54b5cfebcc135a 4010294fdc98db45 +e933cfeca0d638b1 f3f42c863be5501c 105ebc0efc4a8dd2 e48fdc4f35a59068 +5b1c073f6dd368fa 4ac1af60469f5ac0 d209445259a5ec1c e1ce59fad2dd60bb +11eae2a678095d99 7b69733553933371 b083e1f94d5bd71d b9fc9167068f4565 +1f9de7b7cfa30e6f 54f65e2c9f1a6d88 ff7beff94532af43 ce9067db85fd3679 +5a8ad841889285f4 f27d740d8da1429b 0764f789f314e20f 5a08258b4bdfd75d +7b7b9cb4b0bb7c2b a469ac24545f2fbe 0621bdaa76898cb6 cb3bbd334c6b6394 +ef7e1cf31df2dd0b 86089a654b942f6e fb7ee5ba401200e0 d727791fc3f978dc +f446067cd054e664 69ea05784e61ce67 a1fe98a73d22962d 703ad51ff1091920 +f111c2f1535197f8 72471fc2b482b55b 15bfb7525c4c1b4d 8b9a1b98534dcea5 +8343e35e0ecb0164 953604b8687315b8 86509cc26b8730be f8ef669e77466628 +2da94192b67f0c4a 56ff1f7b3a080e4f 0e9ed767d497e8d3 1807169a7c62b80c +c27c8e4907d59b02 a9d5fd0b9aa8ed96 7bd26a1ad6bce39b 562382ccfc6102d3 +5d4cefd222eadfc4 cffff96f16e69c4a 7b7367dbf48a13c2 1c95ef3b3bf7e1fb +b240854e6c40b8a8 a8e957919e088d36 4e1da0c0130ae87b 83e980f6f14a9cfa +fe8e956d489a03aa c365767ec06cee58 04ed81cfe559a8a5 ed00e0ae964e2705 +d2c9011390ba6afd 262b4527144ce8b6 4d438ebddd94eb2c e39c6c254547f0d4 +27b4abf5217c9588 f96dc393517bfab2 50153321ddced8e2 dbb52454e342a483 +1af575c5420b5d37 42aa9ae79e3e7187 3117fd36c856e1c0 317b4ad2d1d3fe38 +b528eb3438210e14 d10e5d2d9feff9d8 1f6fdefde57da710 db7f72e03d154aba +61bacccd26c0a80f e710f55eb5bb59db 2c0aec7f1003fb4f 1ffd219932bc8e7f +4f7ced086f6c3067 7610e78a6e8e04dc 330cd2da1ffb181a e09b5b52b9ea366b +ea88329e2c2d6f51 68b1b2b7ac118861 a56cdc43402d89d6 26344a127a7cb39a +3f2e1a8ae35b72fa c0b8eb83622cd944 fe86bc8f340ea1a0 81fb980c9e6baa8e +f9c1b37d11b13d51 e0cf72aac6dbfab9 49f8443d4f3098f9 b022ea0fa25dd418 +f9cc26d0b8358ddd 778204fd9da6374a 46c4cc1777485acc b9c3975a1c12d9f3 +ac326a8e37ca3c17 31a0b6f163a4335c 1c589d52d8b82699 c0c1b31b6b58a7d6 +76d3eeca77a0b4ee 289b11494a217031 d464e32c28e7c109 5afdad0297c5dd65 +1ad1a856f330647a 4ba7be0eee67eace e4a8137709b1234e 07909fb464b5b4fe +f63e8829a9f066dc ecb8c12cf91836cd 7b7300b86ecea0f7 467b2991832c8380 +3e5f02e1b663e064 e4bd991caa1bcadb 38d984595233f6aa 5c7079217ea5405e +72a515e9f787d3d9 0a48cb098216f8ff a94ddd0bd8634d48 2f4ffcb96dd81e66 +0a4324eb34f6 +""".split())) + + +frames = [ + ( + # i: frame number + 0, + # title: + "IKE_SA_INIT request", + # data: raw frame data + binascii.unhexlify(''.join(""" + 005056eddb32000c 2930109e08004500 014cedc240004011 da45c0a8f583ac10 + 0f5c2aca11940138 97c9000000008992 2c915f35570e0000 0000000000002120 + 2208000000000000 012c220000280000 0024010100030300 000c01000014800e + 0100030000080200 0005000000080400 0013280000480013 0000db253178440c + e776a794133cb8b6 9e5eb07473353657 0c64d7b630549c89 9c0712d828b37168 + 500885e051024578 afc75c101f73b894 3cad62d74a30f2be 1fca2b00002c09cb + 538b2c3dbd4d0bb0 eec8d318cb801a9b 4715b207828d9b5f f1f4ec64ed588637 + 07bcf14ccf052b00 0014eb4c1b788afd 4a9cb7730a68d56c 53212b000014c61b + aca1f1a60cc10800 0000000000002b00 00184048b7d56ebc e88525e7de7f00d6 + c2d3c00000002900 00144048b7d56ebc e88525e7de7f00d6 c2d3290000080000 + 402e290000080000 4016000000100000 402f000100020003 0004 + """.split())), + # packet: Ether / IP / UDP / NON_ESP / IKEv2 / ... + Ether(dst='00:50:56:ed:db:32', src='00:0c:29:30:10:9e', type='IPv4') / + IP(version=4, ihl=5, tos=0x0, len=332, id=60866, flags='DF', frag=0, ttl=64, proto='udp', chksum=0xda45, src='192.168.245.131', dst='172.16.15.92') / + UDP(sport=10954, dport=4500, len=312, chksum=0x97c9) / + NON_ESP() / + IKEv2( + init_SPI=b'\x89\x92\x2c\x91\x5f\x35\x57\x0e', + resp_SPI=b'\x00\x00\x00\x00\x00\x00\x00\x00', + next_payload='SA', + version=0x20, + exch_type='IKE_SA_INIT', + flags='Initiator', + id=0, + length=300 + ) / + IKEv2_SA( + next_payload='KE', + flags='', + length=40, + prop=IKEv2_Proposal( + next_payload='None', + flags='', + length=36, + proposal=1, + proto='IKE', + trans_nb=3, + trans=( + IKEv2_Transform( + next_payload='Transform', + flags='', + length=12, + transform_type='Encryption', + res2=0, + transform_id='AES-GCM-16ICV', + key_length=256 + ) / + IKEv2_Transform( + next_payload='Transform', + flags='', + length=8, + transform_type='PRF', + res2=0, + transform_id='PRF_HMAC_SHA2_256' + ) / + IKEv2_Transform( + next_payload='None', + flags='', + length=8, + transform_type='GroupDesc', + res2=0, + transform_id='256randECPgr' + ) + ) + ) + ) / + IKEv2_KE( + next_payload='Nonce', + flags='', + length=72, + group='256randECPgr', + res2=0, + ke=b'\xdb%1xD\x0c\xe7v\xa7\x94\x13<\xb8\xb6\x9e^\xb0ts56W\x0cd\xd7\xb60T\x9c\x89\x9c\x07\x12\xd8(\xb3qhP\x08\x85\xe0Q\x02Ex\xaf\xc7\\\x10\x1fs\xb8\x94<\xadb\xd7J0\xf2\xbe\x1f\xca' + ) / + IKEv2_Nonce( + next_payload='VendorID', + flags='', + length=44, + nonce=b'\t\xcbS\x8b,=\xbdM\x0b\xb0\xee\xc8\xd3\x18\xcb\x80\x1a\x9bG\x15\xb2\x07\x82\x8d\x9b_\xf1\xf4\xecd\xedX\x867\x07\xbc\xf1L\xcf\x05' + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=20, + vendorID=b'\xebL\x1bx\x8a\xfdJ\x9c\xb7s\nh\xd5lS!' + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=20, + vendorID=b'\xc6\x1b\xac\xa1\xf1\xa6\x0c\xc1\x08\x00\x00\x00\x00\x00\x00\x00' + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=24, + vendorID=b'@H\xb7\xd5n\xbc\xe8\x85%\xe7\xde\x7f\x00\xd6\xc2\xd3\xc0\x00\x00\x00' + ) / + IKEv2_VendorID( + next_payload='Notify', + flags='', + length=20, + vendorID=b'@H\xb7\xd5n\xbc\xe8\x85%\xe7\xde\x7f\x00\xd6\xc2\xd3' + ) / + IKEv2_Notify( + next_payload='Notify', + flags='', + length=8, + type='IKEV2_FRAGMENTATION_SUPPORTED', + ) / + IKEv2_Notify( + next_payload='Notify', + flags='', + length=8, + type='REDIRECT_SUPPORTED', + ) / + IKEv2_Notify( + next_payload='None', + flags='', + length=16, + type='SIGNATURE_HASH_ALGORITHMS', + notify=b'\x00\x01\x00\x02\x00\x03\x00\x04' + ) + ), + ( + # i: frame number + 1, + # title: + "IKE_SA_INIT response", + # data: raw frame data + binascii.unhexlify(''.join(""" + 000c2930109e0050 56eddb3208004500 0151a5dc00008011 2227ac100f5cc0a8 + f58311942aca013d af99000000008992 2c915f35570e98d5 6d32e2a047422120 + 2220000000000000 0131220000280000 0024010100030300 000c01000014800e + 0100030000080200 0005000000080400 0013280000480013 00001d9cd5974c95 + 0c95e0544483fb1f 7a9132f5fe8959c0 9ab3a54c779ff2bc f4522a030dc33b9d + 5ddfeb99e028c0e8 ba7d80dfdcf12b15 16dbe180e6aec664 428b2600002c1d10 + 7dc5a7463da7d761 014139fb381af9cd 3b8c0181e6cd36a8 ae105e55aa7fe71f + 5db1d36c29152b00 0005042b00001840 48b7d56ebce88525 e7de7f00d6c2d3c0 + 0000002b00001440 48b7d56ebce88525 e7de7f00d6c2d32b 000014c6f57ac398 + f493208145b7581e 8789832900001485 817703c6e320d2ae 5a4dd02056c6d729 + 0000080000402e29 0000100000402f00 0100020003000400 00000800004014 + """.split())), + # packet: Ether / IP / UDP / NON_ESP / IKEv2 / ... + Ether(dst='00:0c:29:30:10:9e', src='00:50:56:ed:db:32', type='IPv4') / + IP(version=4, ihl=5, tos=0x0, len=337, id=42460, flags='', frag=0, ttl=128, + proto='udp', chksum=0x2227, src='172.16.15.92', dst='192.168.245.131') / + UDP(sport=4500, dport=10954, len=317, chksum=0xaf99) / + NON_ESP() / + IKEv2( + init_SPI=b'\x89\x92\x2c\x91\x5f\x35\x57\x0e', + resp_SPI=b'\x98\xd5\x6d\x32\xe2\xa0\x47\x42', + next_payload='SA', + version=0x20, + exch_type='IKE_SA_INIT', + flags='Response', + id=0, + length=305 + ) / + IKEv2_SA( + next_payload='KE', + flags='', + length=40, + prop=IKEv2_Proposal( + next_payload='None', + flags='', + length=36, + proposal=1, + proto='IKE', + trans_nb=3, + trans=( + IKEv2_Transform( + next_payload='Transform', + flags='', + length=12, + transform_type='Encryption', + res2=0, + transform_id='AES-GCM-16ICV', + key_length=256 + ) / + IKEv2_Transform( + next_payload='Transform', + flags='', + length=8, + transform_type='PRF', + res2=0, + transform_id='PRF_HMAC_SHA2_256' + ) / + IKEv2_Transform( + next_payload='None', + flags='', + length=8, + transform_type='GroupDesc', + res2=0, + transform_id='256randECPgr' + ) + ) + ) + ) / + IKEv2_KE( + next_payload='Nonce', + flags='', + length=72, + group='256randECPgr', + res2=0, + ke=b'\x1d\x9c\xd5\x97L\x95\x0c\x95\xe0TD\x83\xfb\x1fz\x912\xf5\xfe\x89Y\xc0\x9a\xb3\xa5Lw\x9f\xf2\xbc\xf4R*\x03\r\xc3;\x9d]\xdf\xeb\x99\xe0(\xc0\xe8\xba}\x80\xdf\xdc\xf1+\x15\x16\xdb\xe1\x80\xe6\xae\xc6dB\x8b' + ) / + IKEv2_Nonce( + next_payload='CERTREQ', + flags='', + length=44, + nonce=b'\x1d\x10}\xc5\xa7F=\xa7\xd7a\x01A9\xfb8\x1a\xf9\xcd;\x8c\x01\x81\xe6\xcd6\xa8\xae\x10^U\xaa\x7f\xe7\x1f]\xb1\xd3l)\x15' + ) / + IKEv2_CERTREQ( + next_payload='VendorID', + flags='', + length=5, + cert_encoding='X.509 Certificate - Signature', + cert_authority=b'' + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=24, + vendorID=b'@H\xb7\xd5n\xbc\xe8\x85%\xe7\xde\x7f\x00\xd6\xc2\xd3\xc0\x00\x00\x00' + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=20, + vendorID=b'@H\xb7\xd5n\xbc\xe8\x85%\xe7\xde\x7f\x00\xd6\xc2\xd3' + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=20, + vendorID=b'\xc6\xf5z\xc3\x98\xf4\x93 \x81E\xb7X\x1e\x87\x89\x83' + ) / + IKEv2_VendorID( + next_payload='Notify', + flags='', + length=20, + vendorID=b'\x85\x81w\x03\xc6\xe3 \xd2\xaeZM\xd0 V\xc6\xd7' + ) / + IKEv2_Notify( + next_payload='Notify', + flags='', + length=8, + type='IKEV2_FRAGMENTATION_SUPPORTED', + ) / + IKEv2_Notify( + next_payload='Notify', + flags='', + length=16, + type='SIGNATURE_HASH_ALGORITHMS', + notify=b'\x00\x01\x00\x02\x00\x03\x00\x04' + ) / + IKEv2_Notify( + next_payload='None', + flags='', + length=8, + type='MULTIPLE_AUTH_SUPPORTED' + ) + ), + ( + # i: frame number + 2, + # title: + "IKE_AUTH request", + # data: raw frame data + binascii.unhexlify(''.join(""" + 005056eddb32000c 2930109e08004500 0520edc640004011 d66dc0a8f583ac10 + 0f5c2aca1194050c 8eb0000000008992 2c915f35570e98d5 6d32e2a047422e20 + 2308000000010000 0500230004e4 + """.split())) + ike_auth_request_encrypted_payload, + # packet: Ether / IP / UDP / NON_ESP / IKEv2 / ... + Ether(dst='00:50:56:ed:db:32', src='00:0c:29:30:10:9e', type='IPv4') / + IP(version=4, ihl=5, tos=0x0, len=1312, id=60870, flags='DF', frag=0, ttl=64, + proto='udp', chksum=0xd66d, src='192.168.245.131', dst='172.16.15.92') / + UDP(sport=10954, dport=4500, len=1292, chksum=0x8eb0) / + NON_ESP() / + IKEv2( + init_SPI=b'\x89\x92\x2c\x91\x5f\x35\x57\x0e', + resp_SPI=b'\x98\xd5\x6d\x32\xe2\xa0\x47\x42', + next_payload='Encrypted', + version=0x20, + exch_type='IKE_AUTH', + flags='Initiator', + id=1, + length=1280 + ) / + IKEv2_Encrypted( + next_payload='IDi', + flags='', + length=1252, + load = ike_auth_request_encrypted_payload + ) + ), + ( + # i: frame number + 3, + # title: + "IKE_AUTH response", + # data: raw frame data + binascii.unhexlify(''.join(""" + 000c2930109e0050 56eddb3208004500 0518a5dd00008011 1e5fac100f5cc0a8 + f58311942aca0504 886e000000008992 2c915f35570e98d5 6d32e2a047422e20 + 2320000000010000 04f8240004dc + """.split())) + ike_auth_response_encrypted_payload, + # packet: Ether / IP / UDP / NON_ESP / IKEv2 / ... + Ether(dst='00:0c:29:30:10:9e', src='00:50:56:ed:db:32', type='IPv4') / + IP(version=4, ihl=5, tos=0x0, len=1304, id=42461, flags='', frag=0, ttl=128, + proto='udp', chksum=0x1e5f, src='172.16.15.92', dst='192.168.245.131') / + UDP(sport=4500, dport=10954, len=1284, chksum=0x886e) / + NON_ESP() / + IKEv2( + init_SPI=b'\x89\x92\x2c\x91\x5f\x35\x57\x0e', + resp_SPI=b'\x98\xd5\x6d\x32\xe2\xa0\x47\x42', + next_payload='Encrypted', + version=0x20, + exch_type='IKE_AUTH', + flags='Response', + id=1, + length=1272 + ) / + IKEv2_Encrypted( + next_payload='IDr', + flags='', + length=1244, + load=ike_auth_response_encrypted_payload + ) + ), + ( + # i: frame number + -2, + # title: + "IKE_AUTH request, decrypted", + binascii.unhexlify(''.join(""" + 005056eddb32000c 2930109e08004500 0520edc640004011 d66dc0a8f583ac10 + 0f5c2aca1194050c 8eb0000000008992 2c915f35570e98d5 6d32e2a047422320 + 2308000000010000 0500250000120300 0000696b6576322d 63657274290002dc + 04308202d3308202 79a0030201020204 01000013300a0608 2a8648ce3d040302 + 304b310b30090603 5504061302444531 0f300d0603550408 130642617965726e + 310c300a06035504 0a13034e4350311d 301b060355040313 144e43502044656d + 6f20434120454343 2032303530302218 0f32303136303830 343038303031335a + 180f323035303038 3035303830303133 5a3074310b300906 0355040613024445 + 311a301806035504 0a0c1144656d6f20 4f7267616e697a61 74696f6e3110300e + 060355040b0c0744 656d6f204f553110 300e06035504030c 07436c69656e7431 + 3125302306092a86 4886f70d01090116 16636c69656e7431 4064656d6f2e6e63 + 702d652e636f6d30 59301306072a8648 ce3d020106082a86 48ce3d0301070342 + 0004b74572a1b5dd 1c4cafdab7f06a92 913cab7ee2a55106 efa4056e2dc17369 + 600510553454e37e 69e9a08c5abae5a0 5a77e01ebb04e4b2 72fe349f12a34088 + ceeaa382011c3082 011830090603551d 1304023000300b06 03551d0f04040302 + 05a0301d0603551d 250416301406082b 0601050507030206 082b060105050703 + 07301d0603551d0e 041604145a5e6aa2 9f89959131c17018 ef64dc2a8a4a4a6a + 30750603551d2304 6e306c801425db6d 44dec7a03eb5f862 3ab18784546a0f04 + 09a14fa44d304b31 0b30090603550406 13024445310f300d 0603550408130642 + 617965726e310c30 0a060355040a1303 4e4350311d301b06 0355040313144e43 + 502044656d6f2043 4120454343203230 3530820302000230 490603551d110442 + 3040a026060a2b06 0104018237140203 a0180c16436c6965 6e74314064656d6f + 2e6e63702d652e63 6f6d8116436c6965 6e74314064656d6f 2e6e63702d652e63 + 6f6d300a06082a86 48ce3d0403020348 0030450220602d76 6db7e07b70d88e38 + 10acc6cd350ccdda 1e60d77bd36ed6e6 0f869ef371022100 d1e3d278fcacf41c + d8380691363ad393 3d6bc293fae9c847 ddf6187bb0f06f49 2900000801004000 + 2600000801004008 270000410491c1dc 0f2a8f0e3bd7da99 1a43a39226355e42 + 29bcb62a0e9de979 fda864e3f06460dc aaff850759f48956 233865214e9a10e6 + 376f4c59b5c02f36 6d2f00005c0e0000 000c300a06082a86 48ce3d0403023045 + 022100c1486ab5b3 db4c8b08f3ae0613 20104c826fb0803b a1e6e30d58c8000b + ac514202205865ea 41bc99e0adfa2856 770efaff530f2e85 50da1d86f8504df0 + 04025fb12d210000 8001000000000100 0000020000000300 00000400004e2200 + 0000080000000900 00000a0000001900 0000070000700000 0070010000700200 + 004e2600004e2700 0070030000700400 0070050000700600 0070070000700800 + 00700900004e2300 004e240000700a00 004e250006646562 69616e700a000664 + 656269616e2c0000 2400000020010304 02c1a9656b030000 0c01000014800e00 + 8000000008050000 002d000018010000 00070000100000ff ff00000000ffffff + ff2b000018010000 00070000100000ff ffc0a8e100c0a8e1 ff2b000014afcad7 + 1368a1f1c96b8696 fc775701002b0000 14c61baca1f1a60c c208000000000000 + 002900001c4e6350 0a09b8e83c80b693 36268ec8f6000c29 30109e0000290000 + 080000400c000000 0800004014 + """.split())), + Ether(dst='00:50:56:ed:db:32', src='00:0c:29:30:10:9e', type='IPv4') / + IP(version=4, ihl=5, tos=0x0, len=1312, id=60870, flags='DF', frag=0, ttl=64, proto='udp', chksum=0xd66d, src='192.168.245.131', dst='172.16.15.92') / + UDP(sport=10954, dport=4500, len=1292, chksum=0x8eb0) / + NON_ESP(non_esp=0x0) / + IKEv2( + init_SPI=b'\x89\x92\x2c\x91\x5f\x35\x57\x0e', + resp_SPI=b'\x98\xd5m2\xe2\xa0GB', + next_payload='IDi', + version=0x20, + exch_type='IKE_AUTH', + flags='Initiator', + id=1, + length=1280 + ) / + IKEv2_IDi( + next_payload='CERT', + flags='', + length=18, + IDtype='Email_addr', + res2=0x0, + ID='ikev2-cert' + ) / + IKEv2_CERT( + next_payload='Notify', flags='', length=732, + cert_encoding='X.509 Certificate - Signature', + cert_data=X509_Cert( + tbsCertificate=X509_TBSCertificate( + version=ASN1_INTEGER(2), + serialNumber=ASN1_INTEGER(0x1000013), + signature=X509_AlgorithmIdentifier( + algorithm=ASN1_OID('ecdsa-with-SHA256'), + parameters=None + ), + issuer=[ + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('countryName'), value=ASN1_PRINTABLE_STRING(b'DE'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('stateOrProvinceName'), value=ASN1_PRINTABLE_STRING(b'Bayern'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('organizationName'), value=ASN1_PRINTABLE_STRING(b'NCP'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('commonName'), value=ASN1_PRINTABLE_STRING(b'NCP Demo CA ECC 2050'))) + ], + validity=X509_Validity( + not_before=ASN1_GENERALIZED_TIME('20160804080013Z'), + not_after=ASN1_GENERALIZED_TIME('20500805080013Z') + ), + subject=[ + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('countryName'), value=ASN1_PRINTABLE_STRING(b'DE'))), + X509_RDN(rdn=(X509_AttributeTypeAndValue(type=ASN1_OID('organizationName'), value=ASN1_UTF8_STRING(b'Demo Organization')))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('organizationUnitName'), value=ASN1_UTF8_STRING(b'Demo OU'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('commonName'), value=ASN1_UTF8_STRING(b'Client1'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('emailAddress'), value=ASN1_IA5_STRING(b'client1@demo.ncp-e.com'))) + ], + subjectPublicKeyInfo=X509_SubjectPublicKeyInfo( + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID('ecPublicKey'), + parameters=ECParameters(curve=ASN1_OID('prime256v1'))), + subjectPublicKey=ECDSAPublicKey( + ecPoint=ASN1_BIT_STRING( + '000001001011011101000101011100101010000110110101110111010001110' + '001001100101011111101101010110111111100000110101010010010100100' + '010011110010101011011111101110001010100101010100010000011011101' + '111101001000000010101101110001011011100000101110011011010010110' + '000000000101000100000101010100110100010101001110001101111110011' + '010011110100110100000100011000101101010111010111001011010000001' + '011010011101111110000000011110101110110000010011100100101100100' + '111001011111110001101001001111100010010101000110100000010001000' + '1100111011101010'))), + issuerUniqueID=None, + subjectUniqueID=None, + extensions=[ + X509_Extension( + extnID=ASN1_OID('basicConstraints'), + critical=None, + extnValue=X509_ExtBasicConstraints(cA=None, pathLenConstraint=None) + ), + X509_Extension( + extnID=ASN1_OID('keyUsage'), + critical=None, + extnValue=X509_ExtKeyUsage(keyUsage=ASN1_BIT_STRING('101')) + ), + X509_Extension( + extnID=ASN1_OID('extKeyUsage'), + critical=None, + extnValue=X509_ExtExtendedKeyUsage( + extendedKeyUsage=[ + ASN1P_OID(oid=ASN1_OID('clientAuth')), + ASN1P_OID(oid=ASN1_OID('ipsecUser')) + ] + ) + ), + X509_Extension( + extnID=ASN1_OID('subjectKeyIdentifier'), + critical=None, + extnValue=X509_ExtSubjectKeyIdentifier( + keyIdentifier=ASN1_STRING(b'Z^j\xa2\x9f\x89\x95\x911\xc1p\x18\xefd\xdc*\x8aJJj') + ) + ), + X509_Extension( + extnID=ASN1_OID('authorityKeyIdentifier'), + critical=None, + extnValue=X509_ExtAuthorityKeyIdentifier( + keyIdentifier=ASN1_STRING(b'%\xdbmD\xde\xc7\xa0>\xb5\xf8b:\xb1\x87\x84Tj\x0f\x04\t'), + authorityCertIssuer=X509_GeneralName( + generalName=X509_DirectoryName( + directoryName=[ + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('countryName'), value=ASN1_PRINTABLE_STRING(b'DE'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('stateOrProvinceName'), value=ASN1_PRINTABLE_STRING(b'Bayern'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('organizationName'), value=ASN1_PRINTABLE_STRING(b'NCP'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('commonName'), value=ASN1_PRINTABLE_STRING(b'NCP Demo CA ECC 2050'))) + ] + ) + ), + authorityCertSerialNumber=ASN1_INTEGER(0x20002) + ) + ), + X509_Extension( + extnID=ASN1_OID('subjectAltName'), + critical=None, + extnValue=X509_ExtSubjectAltName( + subjectAltName=[ + X509_GeneralName( + generalName=X509_OtherName( + type_id=ASN1_OID('.1.3.6.1.4.1.311.20.2.3'), + value=ASN1_UTF8_STRING(b'Client1@demo.ncp-e.com') + ) + ), + X509_GeneralName( + generalName=X509_RFC822Name( + rfc822Name=ASN1_IA5_STRING(b'Client1@demo.ncp-e.com') + ) + ) + ] + ) + ) + ] + ), + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID('ecdsa-with-SHA256'), + parameters=None + ), + signatureValue=ECDSASignature( + r=ASN1_INTEGER(0x602d766db7e07b70d88e3810acc6cd350ccdda1e60d77bd36ed6e60f869ef371), + s=ASN1_INTEGER(0xd1e3d278fcacf41cd8380691363ad3933d6bc293fae9c847ddf6187bb0f06f49) + ) + ) + ) / + IKEv2_Notify( + next_payload='Notify', + flags='', + length=8, + proto='IKE', + type='INITIAL_CONTACT', + notify='' + ) / + IKEv2_Notify( + next_payload='CERTREQ', + flags='', + length=8, + proto='IKE', + type='HTTP_CERT_LOOKUP_SUPPORTED', + notify='' + ) / + IKEv2_CERTREQ( + next_payload='AUTH', + flags='', + length=65, + cert_encoding='X.509 Certificate - Signature', + cert_authority=b'\x91\xc1\xdc\x0f*\x8f\x0e;\xd7\xda\x99\x1aC\xa3\x92&5^B)\xbc\xb6*\x0e\x9d\xe9y\xfd\xa8d\xe3\xf0d`\xdc\xaa\xff\x85\x07Y\xf4\x89V#8e!N\x9a\x10\xe67oLY\xb5\xc0/6m' + ) / + IKEv2_AUTH( + next_payload='CP', + flags='', + length=92, + auth_type='Digital Signature', + res2=0x0, + load=b'\x0c0\n\x06\x08*\x86H\xce=\x04\x03\x020E\x02!\x00\xc1Hj\xb5\xb3\xdbL\x8b\x08\xf3\xae\x06\x13 \x10L\x82o\xb0\x80;\xa1\xe6\xe3\rX\xc8\x00\x0b\xacQB\x02 Xe\xeaA\xbc\x99\xe0\xad\xfa(Vw\x0e\xfa\xffS\x0f.\x85P\xda\x1d\x86\xf8PM\xf0\x04\x02_\xb1-' + ) / + IKEv2_CP( + next_payload='SA', + flags='', + length=128, + CFGType='CFG_REQUEST', + res2=0x0, + attributes=[ + ConfigurationAttribute(type='INTERNAL_IP4_ADDRESS', length=0, value=''), + ConfigurationAttribute(type='INTERNAL_IP4_NETMASK', length=0, value=''), + ConfigurationAttribute(type='INTERNAL_IP4_DNS', length=0, value=''), + ConfigurationAttribute(type='INTERNAL_IP4_NBNS', length=0, value=''), + ConfigurationAttribute(type=20002, length=0, value=''), + ConfigurationAttribute(type='INTERNAL_IP6_ADDRESS', length=0, value=''), + ConfigurationAttribute(type=9, length=0, value=''), + ConfigurationAttribute(type='INTERNAL_IP6_DNS', length=0, value=''), + ConfigurationAttribute(type='INTERNAL_DNS_DOMAIN', length=0, value=''), + ConfigurationAttribute(type='APPLICATION_VERSION', length=0, value=''), + ConfigurationAttribute(type=28672, length=0, value=''), + ConfigurationAttribute(type=28673, length=0, value=''), + ConfigurationAttribute(type=28674, length=0, value=''), + ConfigurationAttribute(type=20006, length=0, value=''), + ConfigurationAttribute(type=20007, length=0, value=''), + ConfigurationAttribute(type=28675, length=0, value=''), + ConfigurationAttribute(type=28676, length=0, value=''), + ConfigurationAttribute(type=28677, length=0, value=''), + ConfigurationAttribute(type=28678, length=0, value=''), + ConfigurationAttribute(type=28679, length=0, value=''), + ConfigurationAttribute(type=28680, length=0, value=''), + ConfigurationAttribute(type=28681, length=0, value=''), + ConfigurationAttribute(type=20003, length=0, value=''), + ConfigurationAttribute(type=20004, length=0, value=''), + ConfigurationAttribute(type=28682, length=0, value=''), + ConfigurationAttribute(type=20005, length=6, value='debian'), + ConfigurationAttribute(type=28682, length=6, value='debian') + ] + ) / + IKEv2_SA( + next_payload='TSi', + flags='', + length=36, + prop=IKEv2_Proposal( + next_payload='None', + flags='', + length=32, + proposal=1, + proto='ESP', + SPIsize=4, + trans_nb=2, + SPI=b'\xc1\xa9ek', + trans=IKEv2_Transform(flags='', length=12, transform_type='Encryption', res2=0, transform_id='AES-GCM-16ICV', key_length=128) / + IKEv2_Transform(flags='', length=8, transform_type='Extended Sequence Number', res2=0, transform_id='No ESN') + ) + ) / + IKEv2_TSi( + next_payload='TSr', + flags='', + length=24, + number_of_TSs=1, + res2=0x0, + traffic_selector=[ + IPv4TrafficSelector(TS_type='TS_IPV4_ADDR_RANGE', + IP_protocol_ID='All protocols', + length=16, + start_port=0, + end_port=65535, + starting_address_v4='0.0.0.0', + ending_address_v4='255.255.255.255') + ] + ) / + IKEv2_TSr( + next_payload='VendorID', + flags='', + length=24, + number_of_TSs=1, + res2=0x0, + traffic_selector=[ + IPv4TrafficSelector( + TS_type='TS_IPV4_ADDR_RANGE', + IP_protocol_ID='All protocols', + length=16, + start_port=0, + end_port=65535, + starting_address_v4='192.168.225.0', + ending_address_v4='192.168.225.255') + ] + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=20, + vendorID=b'\xaf\xca\xd7\x13h\xa1\xf1\xc9k\x86\x96\xfcwW\x01\x00' + ) / + IKEv2_VendorID( + next_payload='VendorID', + flags='', + length=20, + vendorID=b'\xc6\x1b\xac\xa1\xf1\xa6\x0c\xc2\x08\x00\x00\x00\x00\x00\x00\x00' + ) / + IKEv2_VendorID( + next_payload='Notify', + flags='', + length=28, + vendorID=b'NcP\n\t\xb8\xe8<\x80\xb6\x936&\x8e\xc8\xf6\x00\x0c)0\x10\x9e\x00\x00' + ) / + IKEv2_Notify( + next_payload='Notify', + flags='', + length=8, + type='MOBIKE_SUPPORTED', + notify='' + ) / + IKEv2_Notify( + next_payload=None, + flags='', + length=8, + type='MULTIPLE_AUTH_SUPPORTED' + ) + ), + # IKE_AUTH response, decrypted + ( + # i: frame number + -3, + # title: + "IKE_AUTH response, decrypted", + binascii.unhexlify(''.join(""" + 000c2930109e0050 56eddb3208004500 0518a5dd00008011 1e5fac100f5cc0a8 + f58311942aca0504 886e000000008992 2c915f35570e98d5 6d32e2a047422420 + 2320000000010000 04f82500007e0900 00003074310b3009 0603550406130244 + 45311a3018060355 040a0c1144656d6f 204f7267616e697a 6174696f6e311030 + 0e060355040b0c07 44656d6f204f5531 10300e0603550403 0c07536572766572 + 313125302306092a 864886f70d010901 1616736572766572 314064656d6f2e6e + 63702d652e636f6d 270002e604308202 dd30820283a00302 0102020401000016 + 300a06082a8648ce 3d040302304b310b 3009060355040613 024445310f300d06 + 0355040813064261 7965726e310c300a 060355040a13034e 4350311d301b0603 + 55040313144e4350 2044656d6f204341 2045434320323035 303022180f323031 + 3630383034303830 3031355a180f3230 3530303830353038 303031355a307431 + 0b30090603550406 13024445311a3018 060355040a0c1144 656d6f204f726761 + 6e697a6174696f6e 3110300e06035504 0b0c0744656d6f20 4f553110300e0603 + 5504030c07536572 7665723131253023 06092a864886f70d 0109011616736572 + 766572314064656d 6f2e6e63702d652e 636f6d3059301306 072a8648ce3d0201 + 06082a8648ce3d03 010703420004dec7 f4b2c8b2dc4d6345 ea1bc875c1076b55 + d9dbc87d069d189b 3fd6bdffec3ec40a fc74a88583cc541b 46ada5e4040ce77d + 6ab7745987296ec1 d236a878f394a382 0126308201223009 0603551d13040230 + 00300b0603551d0f 0404030205a03027 0603551d25042030 1e06082b06010505 + 07030106082b0601 050507030206082b 0601050507030630 1d0603551d0e0416 + 0414a54698574719 a02a49f01a2c9484 d482d94c27233075 0603551d23046e30 + 6c801425db6d44de c7a03eb5f8623ab1 8784546a0f0409a1 4fa44d304b310b30 + 0906035504061302 4445310f300d0603 5504081306426179 65726e310c300a06 + 0355040a13034e43 50311d301b060355 040313144e435020 44656d6f20434120 + 4543432032303530 8203020002304906 03551d1104423040 a026060a2b060104 + 018237140203a018 0c16536572766572 314064656d6f2e6e 63702d652e636f6d + 8116536572766572 314064656d6f2e6e 63702d652e636f6d 300a06082a8648ce + 3d04030203480030 4502205387d21afa 1bab56fc406f8176 8ae73fe18b93b4cf + f191fd01cda6fd92 020e95022100ee5f 6735a9f6d6b377e7 13cacdddd72fc7fb + a5d48258479ee1ed f2af2da848502f00 005c0e0000000c30 0a06082a8648ce3d + 0403023045022078 d6a7e8b366bde8f9 c12f269f2bf64116 9511ce621a90059a + ed0fea47538b0e02 21008cf30813d135 aafe8e4dc0fdf2fd 595a9867f1a6083d + 1e01a149c905ecf9 bfe62100005c0200 000000010004c0a8 e10a00020004ffff + ff004e240004c0a8 e101000300040000 0000000300040000 00004e220004ac10 + 0f5c4e2200040000 0000000400040000 0000000400040000 00004e2300040000 + 0000700200002800 0024000000200103 0402ac0faf030300 000c01000014800e + 0080000000080500 00002c00002ccf0e 7950765db7f7371d bbdfa1720493c83c + 1ba4dc3617c3192a 57b9285d9a630ac7 164611fdf42c2d00 0018010000000700 + 00100000ffffc0a8 e10ac0a8e10a2b00 0018010000000700 00100000ffffc0a8 + e100c0a8e1ff2900 0014afcad71368a1 f1c96b8696fc7757 0100000000080000 + 400c + """.split())), + Ether(dst='00:0c:29:30:10:9e', src='00:50:56:ed:db:32', type='IPv4') / + IP(version=4, ihl=5, tos=0x0, len=1304, id=42461, flags='', frag=0, ttl=128, proto='udp', chksum=0x1e5f, src='172.16.15.92', dst='192.168.245.131') / + UDP(sport=4500, dport=10954, len=1284, chksum=0x886e) / + NON_ESP(non_esp=0x0) / + IKEv2( + init_SPI=b'\x89\x92\x2c\x91\x5f\x35\x57\x0e', + resp_SPI=b'\x98\xd5m2\xe2\xa0GB', + next_payload='IDr', + version=0x20, + exch_type='IKE_AUTH', + flags='Response', + id=1, + length=1272 + ) / + IKEv2_IDr( + next_payload='CERT', + flags='', + length=126, + IDtype=9, + res2=0x0, + ID=b'0t1\x0b0\t\x06\x03U\x04\x06\x13\x02DE1\x1a0\x18\x06\x03U\x04\n\x0c\x11Demo Organization1\x100\x0e\x06\x03U\x04\x0b\x0c\x07Demo OU1\x100\x0e\x06\x03U\x04\x03\x0c\x07Server11%0#\x06\t*\x86H\x86\xf7\r\x01\t\x01\x16\x16server1@demo.ncp-e.com' + ) / + IKEv2_CERT( + next_payload='AUTH', + flags='', + length=742, + cert_encoding='X.509 Certificate - Signature', + cert_data=X509_Cert( + tbsCertificate=X509_TBSCertificate( + version=ASN1_INTEGER(2), + serialNumber=ASN1_INTEGER(0x1000016), + signature=X509_AlgorithmIdentifier( + algorithm=ASN1_OID('ecdsa-with-SHA256'), + parameters=None + ), + issuer=[ + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('countryName'), value=ASN1_PRINTABLE_STRING(b'DE'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('stateOrProvinceName'), value=ASN1_PRINTABLE_STRING(b'Bayern'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('organizationName'), value=ASN1_PRINTABLE_STRING(b'NCP'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('commonName'), value=ASN1_PRINTABLE_STRING(b'NCP Demo CA ECC 2050'))) + ], + validity=X509_Validity( + not_before=ASN1_GENERALIZED_TIME('20160804080015Z'), + not_after=ASN1_GENERALIZED_TIME('20500805080015Z') + ), + subject=[ + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('countryName'), value=ASN1_PRINTABLE_STRING(b'DE'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('organizationName'), value=ASN1_UTF8_STRING(b'Demo Organization'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('organizationUnitName'), value=ASN1_UTF8_STRING(b'Demo OU'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('commonName'), value=ASN1_UTF8_STRING(b'Server1'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('emailAddress'), value=ASN1_IA5_STRING(b'server1@demo.ncp-e.com'))) + ], + subjectPublicKeyInfo=X509_SubjectPublicKeyInfo( + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID('ecPublicKey'), + parameters=ECParameters(curve=ASN1_OID('prime256v1')), + ), + subjectPublicKey=ECDSAPublicKey( + ecPoint=ASN1_BIT_STRING( + '000001001101111011000111111101001011001011001000101100101101110' + '001001101011000110100010111101010000110111100100001110101110000' + '010000011101101011010101011101100111011011110010000111110100000' + '110100111010001100010011011001111111101011010111101111111111110' + '110000111110110001000000101011111100011101001010100010000101100' + '000111100110001010100000110110100011010101101101001011110010000' + '000100000011001110011101111101011010101011011101110100010110011' + '000011100101001011011101100000111010010001101101010100001111000' + '1111001110010100' + ) + ) + ), + issuerUniqueID=None, + subjectUniqueID=None, + extensions=[ + X509_Extension( + extnID=ASN1_OID('basicConstraints'), + critical=None, + extnValue=X509_ExtBasicConstraints(cA=None, pathLenConstraint=None) + ), + X509_Extension( + extnID=ASN1_OID('keyUsage'), + critical=None, + extnValue=X509_ExtKeyUsage(keyUsage=ASN1_BIT_STRING('101')) + ), + X509_Extension( + extnID=ASN1_OID('extKeyUsage'), + critical=None, + extnValue=X509_ExtExtendedKeyUsage( + extendedKeyUsage=[ + ASN1P_OID(oid=ASN1_OID('serverAuth')), + ASN1P_OID(oid=ASN1_OID('clientAuth')), + ASN1P_OID(oid=ASN1_OID('ipsecTunnel')) + ] + ) + ), + X509_Extension( + extnID=ASN1_OID('subjectKeyIdentifier'), + critical=None, + extnValue=X509_ExtSubjectKeyIdentifier( + keyIdentifier=ASN1_STRING(b"\xa5F\x98WG\x19\xa0*I\xf0\x1a,\x94\x84\xd4\x82\xd9L'#") + ) + ), + X509_Extension( + extnID=ASN1_OID('authorityKeyIdentifier'), + critical=None, + extnValue=X509_ExtAuthorityKeyIdentifier( + keyIdentifier=ASN1_STRING(b'%\xdbmD\xde\xc7\xa0>\xb5\xf8b:\xb1\x87\x84Tj\x0f\x04\t'), + authorityCertIssuer=X509_GeneralName( + generalName=X509_DirectoryName( + directoryName=[ + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('countryName'), value=ASN1_PRINTABLE_STRING(b'DE'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('stateOrProvinceName'), value=ASN1_PRINTABLE_STRING(b'Bayern'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('organizationName'), value=ASN1_PRINTABLE_STRING(b'NCP'))), + X509_RDN(rdn=X509_AttributeTypeAndValue(type=ASN1_OID('commonName'), value=ASN1_PRINTABLE_STRING(b'NCP Demo CA ECC 2050'))) + ] + ) + ), + authorityCertSerialNumber=ASN1_INTEGER(0x20002) + ) + ), + X509_Extension( + extnID=ASN1_OID('subjectAltName'), + critical=None, + extnValue=X509_ExtSubjectAltName( + subjectAltName=[ + X509_GeneralName( + generalName=X509_OtherName( + type_id=ASN1_OID('.1.3.6.1.4.1.311.20.2.3'), + value=ASN1_UTF8_STRING(b'Server1@demo.ncp-e.com') + ) + ), + X509_GeneralName( + generalName=X509_RFC822Name( + rfc822Name=ASN1_IA5_STRING(b'Server1@demo.ncp-e.com') + ) + ) + ] + ) + ) + ] + ), + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID('ecdsa-with-SHA256'), + parameters=None + ), + signatureValue=ECDSASignature( + r=ASN1_INTEGER(0x5387d21afa1bab56fc406f81768ae73fe18b93b4cff191fd01cda6fd92020e95), + s=ASN1_INTEGER(0xee5f6735a9f6d6b377e713cacdddd72fc7fba5d48258479ee1edf2af2da84850) + ) + ) + ) / + IKEv2_AUTH( + next_payload='CP', + flags='', + length=92, + auth_type='Digital Signature', + res2=0x0, + load=b'\x0c0\n\x06\x08*\x86H\xce=\x04\x03\x020E\x02 x\xd6\xa7\xe8\xb3f\xbd\xe8\xf9\xc1/&\x9f+\xf6A\x16\x95\x11\xceb\x1a\x90\x05\x9a\xed\x0f\xeaGS\x8b\x0e\x02!\x00\x8c\xf3\x08\x13\xd15\xaa\xfe\x8eM\xc0\xfd\xf2\xfdYZ\x98g\xf1\xa6\x08=\x1e\x01\xa1I\xc9\x05\xec\xf9\xbf\xe6' + ) / + IKEv2_CP( + next_payload='SA', + flags='', + length=92, + CFGType='CFG_REPLY', + res2=0x0, + attributes=[ + ConfigurationAttribute(type='INTERNAL_IP4_ADDRESS', length=4, value='192.168.225.10'), + ConfigurationAttribute(type='INTERNAL_IP4_NETMASK', length=4, value='255.255.255.0'), + ConfigurationAttribute(type=20004, length=4, value=b'\xc0\xa8\xe1\x01'), + ConfigurationAttribute(type='INTERNAL_IP4_DNS', length=4, value='0.0.0.0'), + ConfigurationAttribute(type='INTERNAL_IP4_DNS', length=4, value='0.0.0.0'), + ConfigurationAttribute(type=20002, length=4, value=b'\xac\x10\x0f\x5c'), + ConfigurationAttribute(type=20002, length=4, value='\x00\x00\x00\x00'), + ConfigurationAttribute(type='INTERNAL_IP4_NBNS', length=4, value='0.0.0.0'), + ConfigurationAttribute(type='INTERNAL_IP4_NBNS', length=4, value='0.0.0.0'), + ConfigurationAttribute(type=20003, length=4, value=b'\x00\x00\x00\x00'), + ConfigurationAttribute(type=28674, length=0) + ] + ) / + IKEv2_SA( + next_payload='Nonce', + flags='', + length=36, + prop=IKEv2_Proposal( + flags='', + length=32, + proposal=1, + proto='ESP', + SPIsize=4, + trans_nb=2, + SPI=b'\xac\x0f\xaf\x03', + trans=IKEv2_Transform(flags='', length=12, transform_type='Encryption', res2=0, transform_id='AES-GCM-16ICV', key_length=128) / + IKEv2_Transform(flags='', length=8, transform_type='Extended Sequence Number', res2=0, transform_id='No ESN') + ) + ) / + IKEv2_Nonce( + next_payload='TSi', + flags='', + length=44, + nonce=b'\xcf\x0eyPv]\xb7\xf77\x1d\xbb\xdf\xa1r\x04\x93\xc8<\x1b\xa4\xdc6\x17\xc3\x19*W\xb9(]\x9ac\n\xc7\x16F\x11\xfd\xf4,' + ) / + IKEv2_TSi( + next_payload='TSr', + flags='', + length=24, + number_of_TSs=1, + res2=0x0, + traffic_selector=[ + IPv4TrafficSelector( + TS_type='TS_IPV4_ADDR_RANGE', + IP_protocol_ID='All protocols', + length=16, + start_port=0, + end_port=65535, + starting_address_v4='192.168.225.10', + ending_address_v4='192.168.225.10' + ) + ] + ) / + IKEv2_TSr( + next_payload='VendorID', + flags='', + length=24, + number_of_TSs=1, + res2=0x0, + traffic_selector=[ + IPv4TrafficSelector( + TS_type='TS_IPV4_ADDR_RANGE', + IP_protocol_ID='All protocols', + length=16, + start_port=0, + end_port=65535, + starting_address_v4='192.168.225.0', + ending_address_v4='192.168.225.255' + ) + ] + ) / + IKEv2_VendorID( + next_payload='Notify', + flags='', + length=20, + vendorID=b'\xaf\xca\xd7\x13h\xa1\xf1\xc9k\x86\x96\xfcwW\x01\x00' + ) / + IKEv2_Notify( + next_payload='None', + flags='', + length=8, + type='MOBIKE_SUPPORTED' + ) + ), + # CREATE_CHILD_SA request, decrypted + ( + # i: frame number + -4, + # title: + "CREATE_CHILD_SA request, decrypted", + binascii.unhexlify(''.join(""" + 00 50 56 99 bf d5 00 50 56 99 69 93 08 00 45 00 + 01 38 60 32 40 00 40 11 c1 0f 0a 05 02 36 0a 05 + 02 34 b8 99 11 94 01 24 19 a9 00 00 00 00 46 b3 + f6 88 4d 37 5f 9a f5 38 82 35 ea 87 5e 8a 29 20 + 24 00 00 00 00 00 00 00 01 18 + + 21 00 00 0c 03 04 40 09 5f c7 ff 5a 28 00 00 2c + 00 00 00 28 01 03 04 03 6b 21 88 20 03 00 00 0c + 01 00 00 14 80 0e 00 80 03 00 00 08 04 00 00 1c + 00 00 00 08 05 00 00 00 22 00 00 2c ea 7e 88 57 + 4a 36 64 cd 67 e3 3c 42 46 66 59 4d df 70 25 03 + b2 00 a3 3f 87 82 f2 3c 94 c0 60 0e ae 7e d9 50 + d7 67 e9 6e 2c 00 00 48 00 1c 00 00 8e 15 b1 f4 + 9a cc 04 ff 12 e3 2f bc 3a f0 57 14 81 f3 b9 6c + 21 1a f7 36 97 6d c2 23 80 74 ef 75 59 d1 99 65 + 5a a5 80 00 87 4a bf 1f 13 f7 e1 6f de 34 80 94 + 28 1c 93 cb 5a ee 30 24 d9 3e b9 55 2d 00 00 18 + 01 00 00 00 07 00 00 10 00 00 ff ff c0 a8 e1 0b + c0 a8 e1 0b 00 00 00 18 01 00 00 00 07 00 00 10 + 00 00 ff ff c0 a8 e1 00 c0 a8 e1 ff + """.split())), + Ether(dst='00:50:56:99:bf:d5', src='00:50:56:99:69:93', type=2048) /\ + IP(version=4, ihl=5, tos=0, len=312, id=24626, flags=2, frag=0, ttl=64, proto=17, chksum=49423, src='10.5.2.54', dst='10.5.2.52') /\ + UDP(sport=47257, dport=4500, len=292, chksum=6569) /\ + NON_ESP(non_esp=0) /\ + IKEv2( + init_SPI=b'F\xb3\xf6\x88M7_\x9a', + resp_SPI=b'\xf58\x825\xea\x87^\x8a', + next_payload=41, + version=32, + exch_type=36, + flags=0, + id=0, + length=280 + ) /\ + IKEv2_Notify( + next_payload=33, + flags=0, + length=12, + proto=3, + SPIsize=4, + type=16393, + SPI=b'_\xc7\xffZ', + notify=b'' + ) /\ + IKEv2_SA( + prop=IKEv2_Proposal( + trans=( + IKEv2_Transform(next_payload=3, flags=0, length=12, transform_type=1, res2=0, transform_id=20, key_length=128) /\ + IKEv2_Transform(next_payload=3, flags=0, length=8, transform_type=4, res2=0, transform_id=28) /\ + IKEv2_Transform(next_payload=0, flags=0, length=8, transform_type=5, res2=0, transform_id=0) + ), + next_payload=0, flags=0, length=40, proposal=1, proto=3, SPIsize=4, trans_nb=3, SPI=b'k!\x88 '), + next_payload=40, + flags=0, + length=44 + ) /\ + IKEv2_Nonce( + next_payload=34, + flags=0, + length=44, + nonce=b'\xea~\x88WJ6d\xcdg\xe3\xb9U' + ) /\ + IKEv2_TSi( + traffic_selector=[ + IPv4TrafficSelector( + TS_type=7, + IP_protocol_ID=0, + length=16, + start_port=0, + end_port=65535, + starting_address_v4='192.168.225.11', + ending_address_v4='192.168.225.11' + ) + ], + next_payload=45, + flags=0, + length=24, + number_of_TSs=1, + res2=0 + ) /\ + IKEv2_TSr( + traffic_selector=[ + IPv4TrafficSelector( + TS_type=7, + IP_protocol_ID=0, + length=16, + start_port=0, + end_port=65535, + starting_address_v4='192.168.225.0', + ending_address_v4='192.168.225.255' + ) + ], + next_payload=0, + flags=0, + length=24, + number_of_TSs=1, + res2=0 + ) + ), +] + + +for i, title, data, packet in frames: + print(title) + if i >= 0: + # the raw frame data coincides with the frame from the packet capture + assert data == raw(pcap[i]) + # the scapy packet correctly describes the frame + assert raw(packet) == data + # reassembling the dissected frame yields the original frame + assert raw(Ether(data)) == data + + + += IKEv2 key exchange with REDIRECT + +* Loads and dissects the four frames of the key exchange from a Wireshark +* capture and compares them with manually built scapy packets. + +pcap = rdpcap(scapy_path("/test/pcaps/ikev2_notify_redirect.pcap")) + + +frames = [ + ( + # i: frame number + 0, + # title: + "IKE_SA_INIT request (redirect_supported)", + # data: raw frame data + binascii.unhexlify(''.join(""" + 00505699bfd50050 56991bcc08004500 012cb73300007f11 6aac0a05023c0a05 + 02342ac801f40118 62b8886948814975 28ad000000000000 0000212022080000 + 0000000001102200 0028000000240101 00030300000c0100 0014800e01000300 + 0008020000050000 00080400001c2800 0048001c00002895 d48e470d8cb88196 + 62f3370c57b26cd3 49c16f5ec1b31959 f9ef695480bc7323 52f96d0a7c4a54f1 + d596bb4fcc2f368e 31985a76ea5a7c77 d4310d372d962900 002c4bf3ea6cd0c6 + afe702c567fe7db3 ff973424bb5e9de6 af123a41975a6ffb 266e9c5b4c915795 + 132b2900001c0100 4005509b01b43dc2 8c9df849fd765c64 8a512959ac502900 + 001c010040045312 0985399e14cf2b79 211f375b439bd030 31ac290000080000 + 402e290000080000 4016000000100000 402f000100020003 0004 + """.split())), + # packet: Ether / IP / UDP / IKEv2 / ... + Ether(dst='00:50:56:99:bf:d5', src='00:50:56:99:1b:cc', type=2048) /\ + IP(version=4, ihl=5, tos=0, id=46899, flags=0, frag=0, ttl=127, proto=17, chksum=27308, src='10.5.2.60', dst='10.5.2.52') /\ + UDP(sport=10952, dport=500, chksum=25272) /\ + IKEv2( + init_SPI=b'\x88iH\x81Iu(\xad', + resp_SPI=b'\x00\x00\x00\x00\x00\x00\x00\x00', + next_payload=33, + version=32, + exch_type=34, + flags=8, + id=0 + ) /\ + IKEv2_SA( + prop=IKEv2_Proposal( + trans=( + IKEv2_Transform(next_payload=3, flags=0, length=12, transform_type=1, res2=0, transform_id=20, key_length=256) /\ + IKEv2_Transform(next_payload=3, flags=0, length=8, transform_type=2, res2=0, transform_id=5) /\ + IKEv2_Transform(next_payload=0, flags=0, length=8, transform_type=4, res2=0, transform_id=28) + ), + next_payload=0, flags=0, length=36, proposal=1, proto='IKE', trans_nb=3), + next_payload=34, + flags=0, + length=40 + ) /\ + IKEv2_KE( + next_payload=40, + flags=0, + length=72, + group=28, + res2=0, + ke=b'(\x95\xd4\x8eG\r\x8c\xb8\x81\x96b\xf37\x0cW\xb2l\xd3I\xc1o^\xc1\xb3\x19Y\xf9\xefiT\x80\xbcs#R\xf9m\n|JT\xf1\xd5\x96\xbbO\xcc/6\x8e1\x98Zv\xeaZ|w\xd41\r7-\x96' + ) /\ + IKEv2_Nonce( + next_payload=41, + flags=0, + length=44, + nonce=b'K\xf3\xeal\xd0\xc6\xaf\xe7\x02\xc5g\xfe}\xb3\xff\x974$\xbb^\x9d\xe6\xaf\x12:A\x97Zo\xfb&n\x9c[L\x91W\x95\x13+' + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=28, + proto='IKE', + type='NAT_DETECTION_DESTINATION_IP', + notify=b'P\x9b\x01\xb4=\xc2\x8c\x9d\xf8I\xfdv\\d\x8aQ)Y\xacP' + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=28, + proto='IKE', + type='NAT_DETECTION_SOURCE_IP', + notify=b'S\x12\t\x859\x9e\x14\xcf+y!\x1f7[C\x9b\xd001\xac' + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=8, + type='IKEV2_FRAGMENTATION_SUPPORTED', + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=8, + type='REDIRECT_SUPPORTED', + ) /\ + IKEv2_Notify( + next_payload=0, + flags=0, + length=16, + type='SIGNATURE_HASH_ALGORITHMS', + notify=b'\x00\x01\x00\x02\x00\x03\x00\x04' + ) + ), + ( + # i: frame number + 1, + # title: + "IKE_SA_INIT response (redirect)", + # data: raw frame data + # data: raw frame data + binascii.unhexlify(''.join(""" + 005056991bcc0050 5699bfd508004500 0086c4d300004011 9d1a0a0502340a05 + 023c01f42ac80072 c9bc886948814975 28ad000000000000 0000292022200000 + 00000000006a0000 004e01004017031c 6d6f6e657962696e 2e6475636b627572 + 672e6469736e6579 2e636f6d4bf3ea6c d0c6afe702c567fe 7db3ff973424bb5e + 9de6af123a41975a 6ffb266e9c5b4c91 5795132b + """.split())), + # packet: Ether / IP / UDP / IKEv2 / ... + Ether(dst='00:50:56:99:1b:cc', src='00:50:56:99:bf:d5', type=2048) /\ + IP(version=4, ihl=5, tos=0, id=50387, flags=0, frag=0, ttl=64, proto=17, src='10.5.2.52', dst='10.5.2.60') /\ + UDP(sport=500, dport=10952) /\ + IKEv2( + init_SPI=b'\x88iH\x81Iu(\xad', + resp_SPI=b'\x00\x00\x00\x00\x00\x00\x00\x00', + next_payload=41, + version=32, + exch_type=34, + flags=32, + id=0 + ) /\ + IKEv2_Notify( + next_payload=0, + flags=0, + length=78, + proto='IKE', + type='REDIRECT', + gw_id_type=3, + gw_id=b'moneybin.duckburg.disney.com', + nonce=b'K\xf3\xeal\xd0\xc6\xaf\xe7\x02\xc5g\xfe}\xb3\xff\x974$\xbb^\x9d\xe6\xaf\x12:A\x97Zo\xfb&n\x9c[L\x91W\x95\x13+' + ) + ), + ( + # i: frame number + 2, + # title: + "IKE_SA_INIT request (redirected_from)", + # data: raw frame data + binascii.unhexlify(''.join(""" + 0050569907660050 56991bcc08004500 013290ac00007f11 91940a05023c0a05 + 02352ac801f4011e cba11c88ee0b7793 d52e000000000000 0000212022080000 + 0000000001162200 0028000000240101 00030300000c0100 0014800e01000300 + 0008020000050000 00080400001c2800 0048001c00004616 8482fe53233fc1e2 + 2f9726b7adfe0dfc f53d1558fd663168 24ceec32d4d33f57 7941d3d52e929b3b + ed0b2eef12886117 cd358655f2f6ffd6 fb54fd48bbc52900 002ca573e33f62cf + 2893f80abed1677c a303249bf90aae99 980052cbdfd9cc6b 6e70605869ef142b + cdfd2900001c0100 40052c07d7519ad8 df23a23027e9e7c2 654b32c4e0f32900 + 001c010040041a1d 001cd4d06f42d1ce 836f7ced61c683b1 87ef290000080000 + 402e2900000e0000 401801040a050234 000000100000402f 0001000200030004 + """.split())), + # packet: Ether / IP / UDP / IKEv2 / ... + Ether(dst='00:50:56:99:07:66', src='00:50:56:99:1b:cc', type=2048) /\ + IP(version=4, ihl=5, tos=0, id=37036, flags=0, frag=0, ttl=127, proto=17, src='10.5.2.60', dst='10.5.2.53') /\ + UDP(sport=10952, dport=500) /\ + IKEv2( + init_SPI=b'\x1c\x88\xee\x0bw\x93\xd5.', + resp_SPI=b'\x00\x00\x00\x00\x00\x00\x00\x00', + next_payload=33, + version=32, + exch_type=34, + flags=8, + id=0) /\ + IKEv2_SA( + prop=IKEv2_Proposal( + trans=( + IKEv2_Transform(next_payload=3, flags=0, length=12, transform_type=1, res2=0, transform_id=20, key_length=256) /\ + IKEv2_Transform(next_payload=3, flags=0, length=8, transform_type=2, res2=0, transform_id=5) /\ + IKEv2_Transform(next_payload=0, flags=0, length=8, transform_type=4, res2=0, transform_id=28) + ), + next_payload=0, + flags=0, + length=36, + proposal=1, + proto='IKE', + trans_nb=3, + ), + next_payload=34, + flags=0, + length=40 + ) /\ + IKEv2_KE( + next_payload=40, + flags=0, + length=72, + group=28, + res2=0, + ke=b'F\x16\x84\x82\xfeS#?\xc1\xe2/\x97&\xb7\xad\xfe\r\xfc\xf5=\x15X\xfdf1h$\xce\xec2\xd4\xd3?\x57\x79\x41\xd3\xd5.\x92\x9b;\xed\x0b.\xef\x12\x88a\x17\xcd5\x86U\xf2\xf6\xff\xd6\xfbT\xfdH\xbb\xc5' + ) /\ + IKEv2_Nonce( + next_payload=41, + flags=0, + length=44, + nonce=b'\xa5s\xe3?b\xcf(\x93\xf8\n\xbe\xd1g|\xa3\x03$\x9b\xf9\n\xae\x99\x98\x00R\xcb\xdf\xd9\xccknp`Xi\xef\x14+\xcd\xfd' + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=28, + proto='IKE', + type='NAT_DETECTION_DESTINATION_IP', + notify=b",\x07\xd7Q\x9a\xd8\xdf#\xa20'\xe9\xe7\xc2eK2\xc4\xe0\xf3" + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=28, + proto='IKE', + type='NAT_DETECTION_SOURCE_IP', + notify=b'\x1a\x1d\x00\x1c\xd4\xd0oB\xd1\xce\x83o|\xeda\xc6\x83\xb1\x87\xef' + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=8, + type='IKEV2_FRAGMENTATION_SUPPORTED' + ) /\ + IKEv2_Notify( + next_payload=41, + flags=0, + length=14, + type='REDIRECTED_FROM', + gw_id_type=1, + gw_id_len=4, + gw_id='10.5.2.52' + ) /\ + IKEv2_Notify( + next_payload=0, + flags=0, + length=16, + type='SIGNATURE_HASH_ALGORITHMS', + notify=b'\x00\x01\x00\x02\x00\x03\x00\x04' + ) + ), + ( + # i: frame number + 3, + # title: + "IKE_SA_INIT response (no_proposal_chosen)", + # data: raw frame data + binascii.unhexlify(''.join(""" + 005056991bcc0050 5699076608004500 0040f24c00004011 6fe60a0502350a05 + 023c01f42ac8002c c8e31c88ee0b7793 d52e63cc9c1919de 33e7292022200000 + 0000000000240000 00080100000e + """.split())), + # packet: Ether / IP / UDP / IKEv2 / ... + Ether(dst='00:50:56:99:1b:cc', src='00:50:56:99:07:66', type=2048) /\ + IP(version=4, ihl=5, tos=0, id=62028, flags=0, frag=0, ttl=64, proto=17, src='10.5.2.53', dst='10.5.2.60') /\ + UDP(sport=500, dport=10952) /\ + IKEv2( + init_SPI=b'\x1c\x88\xee\x0bw\x93\xd5.', + resp_SPI=b'c\xcc\x9c\x19\x19\xde3\xe7', + next_payload=41, + version=32, + exch_type=34, + flags=32, + id=0 + ) /\ + IKEv2_Notify( + next_payload=0, + flags=0, + length=8, + proto='IKE', + type='NO_PROPOSAL_CHOSEN' + ) + ), +] + + +for i, title, data, packet in frames: + print(title) + if i >= 0: + # the raw frame data coincides with the frame from the packet capture + assert data == raw(pcap[i]) + # the scapy packet correctly describes the frame + assert raw(packet) == data + # reassembling the dissected frame yields the original frame + assert raw(Ether(data)) == data diff --git a/test/contrib/isis.uts b/test/contrib/isis.uts index 78302beb99e..69b50ecf004 100644 --- a/test/contrib/isis.uts +++ b/test/contrib/isis.uts @@ -10,12 +10,12 @@ from scapy.contrib.isis import * = Layer Binding p = Dot3()/LLC()/ISIS_CommonHdr()/ISIS_P2P_Hello() -assert(p[LLC].dsap == 0xfe) -assert(p[LLC].ssap == 0xfe) -assert(p[LLC].ctrl == 0x03) -assert(p[ISIS_CommonHdr].nlpid == 0x83) -assert(p[ISIS_CommonHdr].pdutype == 17) -assert(p[ISIS_CommonHdr].hdrlen == 20) +assert p[LLC].dsap == 0xfe +assert p[LLC].ssap == 0xfe +assert p[LLC].ctrl == 0x03 +assert p[ISIS_CommonHdr].nlpid == 0x83 +assert p[ISIS_CommonHdr].pdutype == 17 +assert p[ISIS_CommonHdr].hdrlen == 20 + Package Tests @@ -26,8 +26,8 @@ p = Dot3(dst="09:00:2b:00:00:05",src="00:00:00:aa:00:8c")/LLC()/ISIS_CommonHdr() ISIS_ProtocolsSupportedTlv(nlpids=["IPv4", "IPv6"]) ]) p = p.__class__(raw(p)) -assert(p[ISIS_P2P_Hello].pdulength == 24) -assert(network_layer_protocol_ids[p[ISIS_ProtocolsSupportedTlv].nlpids[1]] == "IPv6") +assert p[ISIS_P2P_Hello].pdulength == 24 +assert network_layer_protocol_ids[p[ISIS_ProtocolsSupportedTlv].nlpids[1]] == "IPv6" = LSP p = Dot3(dst="09:00:2b:00:00:05",src="00:00:00:aa:00:8c")/LLC()/ISIS_CommonHdr()/ISIS_L2_LSP( @@ -68,8 +68,8 @@ p = Dot3(dst="09:00:2b:00:00:05",src="00:00:00:aa:00:8c")/LLC()/ISIS_CommonHdr() ) ]) p = p.__class__(raw(p)) -assert(p[ISIS_L2_LSP].pdulength == 150) -assert(p[ISIS_L2_LSP].checksum == 0x8701) +assert p[ISIS_L2_LSP].pdulength == 150 +assert p[ISIS_L2_LSP].checksum == 0x8701 = LSP with Sub-TLVs p = Dot3(dst="09:00:2b:00:00:05",src="00:00:00:aa:00:8c")/LLC()/ISIS_CommonHdr()/ISIS_L2_LSP( @@ -97,7 +97,12 @@ p = Dot3(dst="09:00:2b:00:00:05",src="00:00:00:aa:00:8c")/LLC()/ISIS_CommonHdr() ISIS_ExtendedIpPrefix(metric=10, pfx="10.1.0.109/30", subtlvindicator=1, subtlvs=[ ISIS_32bitAdministrativeTagSubTlv(tags=[321, 123]), - ISIS_64bitAdministrativeTagSubTlv(tags=[54321, 4294967311]) + ISIS_64bitAdministrativeTagSubTlv(tags=[54321, 4294967311]), + ISIS_PrefixSegmentIdentifierSubTlv(flags="P", algorithm=0, idx=20) + ]), + ISIS_ExtendedIpPrefix(metric=10, pfx="10.20.30.40/32", subtlvindicator=1, + subtlvs=[ + ISIS_PrefixSegmentIdentifierSubTlv(flags=["L", "V", "N"], algorithm=0, sid=1000) ]), ISIS_ExtendedIpPrefix(metric=10, pfx="10.1.0.181/30", subtlvindicator=1, subtlvs=[ @@ -130,16 +135,45 @@ p = Dot3(dst="09:00:2b:00:00:05",src="00:00:00:aa:00:8c")/LLC()/ISIS_CommonHdr() ), ISIS_ExternalIpReachabilityTlv( type=130 + ), + ISIS_RouterCapabilityTlv( + type=242, + routerid="10.20.30.40", + subtlvs=[ + ISIS_SRCapabilitiesSubTLV( + flags='I', + srgb_ranges=[ + ISIS_SRGBDescriptorEntry( + range=1000, + sid_label=ISIS_SIDLabelSubTLV( + sid=10 + ) + ), + ISIS_SRGBDescriptorEntry( + range=5000, + sid_label=ISIS_SIDLabelSubTLV( + idx=20 + ) + ), + ] + ), + ISIS_SRAlgorithmSubTLV(algorithms=[0, 1]) + ] ) ]) p = p.__class__(raw(p)) -assert(p[ISIS_L2_LSP].pdulength == 278) -assert(p[ISIS_L2_LSP].checksum == 0xc0a9) -assert(p[ISIS_ExtendedIpReachabilityTlv].pfxs[1].subtlvs[1].tags[0]==54321) -assert(p[ISIS_Ipv6ReachabilityTlv].pfxs[1].subtlvs[0].len==8) -assert(p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[0].address=='172.16.8.4') -assert(p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[1].localid==418) -assert(p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[3].maxbw==125000000) -assert(p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[4].unrsvbw[0]==125000000) -assert(p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[5].temetric==16777214) -assert(p[ISIS_ExternalIpReachabilityTlv].type==130) +assert p[ISIS_L2_LSP].pdulength == 332 +assert p[ISIS_L2_LSP].checksum == 0x074f +assert p[ISIS_ExtendedIpReachabilityTlv].pfxs[1].subtlvs[1].tags[0]==54321 +assert p[ISIS_ExtendedIpReachabilityTlv].pfxs[2].subtlvs[0].sid==1000 +assert p[ISIS_Ipv6ReachabilityTlv].pfxs[1].subtlvs[0].len==8 +assert p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[0].address=='172.16.8.4' +assert p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[1].localid==418 +assert p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[3].maxbw==125000000 +assert p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[4].unrsvbw[0]==125000000 +assert p[ISIS_ExtendedIsReachabilityTlv].neighbours[0].subtlvs[5].temetric==16777214 +assert p[ISIS_ExternalIpReachabilityTlv].type==130 +assert p[ISIS_RouterCapabilityTlv].type==242 +assert p[ISIS_RouterCapabilityTlv].subtlvs[0].srgb_ranges[0].range==1000 +assert p[ISIS_RouterCapabilityTlv].subtlvs[0].srgb_ranges[0].sid_label.sid==10 +assert p[ISIS_RouterCapabilityTlv].subtlvs[1].algorithms==[0, 1] \ No newline at end of file diff --git a/test/contrib/isotp.uts b/test/contrib/isotp.uts deleted file mode 100644 index eef8c91517f..00000000000 --- a/test/contrib/isotp.uts +++ /dev/null @@ -1,2369 +0,0 @@ -% Regression tests for ISOTP - -+ Configuration -~ conf - -= Imports -load_layer("can") -conf.contribs['CAN']['swap-bytes'] = False -import os, threading, six, subprocess, sys -from six.moves.queue import Queue -from subprocess import call -from io import BytesIO -from scapy.contrib.isotp import get_isotp_packet -from scapy.consts import LINUX - -= Definition of constants, utility functions and mock classes -iface0 = "vcan0" -iface1 = "vcan1" - -class MockCANSocket(SuperSocket): - nonblocking_socket = True - def __init__(self, rcvd_queue=None): - self.rcvd_queue = Queue() - self.sent_queue = Queue() - if rcvd_queue is not None: - for c in rcvd_queue: - self.rcvd_queue.put(c) - def recv_raw(self, x=MTU): - pkt = bytes(self.rcvd_queue.get(True, 2)) - return CAN, pkt, None - def send(self, p): - self.sent_queue.put(p) - @staticmethod - def select(sockets, remain=None): - return sockets, None - - -# utility function that waits on list l for n elements, timing out if nothing is added for 1 second -def list_wait(l, n): - old_len = 0 - c = 0 - while len(l) < n: - if c > 100: - return False - if len(l) == old_len: - time.sleep(0.01) - c += 1 - else: - old_len = len(l) - c = 0 - -# hexadecimal to bytes convenience function -if six.PY2: - dhex = lambda s: "".join(s.split()).decode('hex') -else: - dhex = bytes.fromhex - - -# function to exit when the can-isotp kernel module is not available -ISOTP_KERNEL_MODULE_AVAILABLE = False -def exit_if_no_isotp_module(): - if not ISOTP_KERNEL_MODULE_AVAILABLE: - sys.stderr.write("TEST SKIPPED: can-isotp not available" + os.linesep) - warning("Can't test ISOTP native socket because kernel module is not loaded") - exit(0) - - -= Initialize a virtual CAN interface -~ vcan_socket needs_root linux -if 0 != call(["cansend", iface0, "000#"]): - # vcan0 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface0, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface0) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface0, "up"]): - raise Exception("could not bring up %s" % iface0) - -if 0 != call(["cansend", iface0, "000#"]): - raise Exception("cansend doesn't work") - -if 0 != call(["cansend", iface1, "000#"]): - # vcan1 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface1, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface1) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface1, "up"]): - raise Exception("could not bring up %s" % iface1) - -if 0 != call(["cansend", iface1, "000#"]): - raise Exception("cansend doesn't work") - -print("CAN should work now") - -= Import CANSocket - -from scapy.contrib.cansocket_python_can import * - -new_can_socket = lambda iface: CANSocket(bustype='virtual', channel=iface) -new_can_socket0 = lambda: CANSocket(bustype='virtual', channel=iface0, timeout=0.01) -new_can_socket1 = lambda: CANSocket(bustype='virtual', channel=iface1, timeout=0.01) - -# utility function for draining a can interface, asserting that no packets are there -def drain_bus(iface=iface0, assert_empty=True): - with new_can_socket(iface) as s: - pkts = s.sniff(timeout=0.1) - if assert_empty: - assert len(pkts) == 0 - -print("CAN sockets should work now") - -= Overwrite definition for vcan_socket systems python-can sockets -~ vcan_socket needs_root linux -if "python_can" in CANSocket.__module__: - new_can_socket = lambda iface: CANSocket(bustype='socketcan', channel=iface, bitrate=250000, timeout=0.01) - new_can_socket0 = lambda: CANSocket(bustype='socketcan', channel=iface0, bitrate=250000, timeout=0.01) - new_can_socket1 = lambda: CANSocket(bustype='socketcan', channel=iface1, bitrate=250000, timeout=0.01) - -= Verify that a CAN socket can be created and closed -s = new_can_socket(iface0) -s.close() - - -= Check if can-isotp and can-utils are installed on this system -~ linux -p1 = subprocess.Popen(['lsmod'], stdout = subprocess.PIPE) -p2 = subprocess.Popen(['grep', '^can_isotp'], stdout = subprocess.PIPE, stdin=p1.stdout) -p1.stdout.close() -if p1.wait() == 0 and p2.wait() == 0 and b"can_isotp" in p2.stdout.read(): - p = subprocess.Popen(["isotpsend", "-s1", "-d0", iface0], stdin = subprocess.PIPE) - p.communicate(b"01") - if p.returncode == 0: - ISOTP_KERNEL_MODULE_AVAILABLE = True - - -+ Syntax check - -= Import isotp -conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} -load_contrib("isotp") - -ISOTPSocket = ISOTPSoftSocket - -+ ISOTP packet check - -= Creation of an empty ISOTP packet -p = ISOTP() -assert(p.data == b"") -assert(p.src is None and p.dst is None and p.exsrc is None and p.exdst is None) -assert(bytes(p) == b"") - -= Creation of a simple ISOTP packet with src -p = ISOTP(b"eee", src=0x241) -assert(p.src == 0x241) -assert(p.data == b"eee") -assert(bytes(p) == b"eee") - -= Creation of a simple ISOTP packet with exsrc -p = ISOTP(b"eee", exsrc=0x41) -assert(p.exsrc == 0x41) -assert(p.data == b"eee") -assert(bytes(p) == b"eee") - -= Creation of a simple ISOTP packet with dst -p = ISOTP(b"eee", dst=0x241) -assert(p.dst == 0x241) -assert(p.data == b"eee") -assert(bytes(p) == b"eee") - -= Creation of a simple ISOTP packet with exdst -p = ISOTP(b"eee", exdst=0x41) -assert(p.exdst == 0x41) -assert(p.data == b"eee") -assert(bytes(p) == b"eee") - -= Creation of a simple ISOTP packet with src, dst, exsrc, exdst -p = ISOTP(b"eee", src=1, dst=2, exsrc=3, exdst=4) -assert(p.dst == 2) -assert(p.exdst == 4) -assert(p.src == 1) -assert(p.exsrc == 3) -assert(p.data == b"eee") -assert(bytes(p) == b"eee") - -= Creation of a simple ISOTP packet with src validation error -ex = False -try: - p = ISOTP(b"eee", src=0x1000000000, dst=2, exsrc=3, exdst=4) -except Scapy_Exception: - ex = True - -assert ex - -= Creation of a simple ISOTP packet with dst validation error -ex = False -try: - p = ISOTP(b"eee", src=0x10, dst=0x20000000000, exsrc=3, exdst=4) -except Scapy_Exception: - ex = True - -assert ex - -= Creation of a simple ISOTP packet with exsrc validation error -ex = False -try: - p = ISOTP(b"eee", src=0x10, dst=2, exsrc=3000, exdst=4) -except Scapy_Exception: - ex = True - -assert ex - - -= Creation of a simple ISOTP packet with exdst validation error -ex = False -try: - p = ISOTP(b"eee", src=0x10, dst=2, exsrc=30, exdst=400) -except Scapy_Exception: - ex = True - -assert ex - -+ ISOTPFrame related checks - -= Build a packet with extended addressing -pkt = CAN(identifier=0x123, data=b'\x42\x10\xff\xde\xea\xdd\xaa\xaa') -isotpex = ISOTPHeaderEA(bytes(pkt)) -assert(isotpex.type == 1) -assert(isotpex.message_size == 0xff) -assert(isotpex.extended_address == 0x42) -assert(isotpex.identifier == 0x123) -assert(isotpex.length == 8) - -= Build a packet with normal addressing -pkt = CAN(identifier=0x123, data=b'\x10\xff\xde\xea\xdd\xaa\xaa') -isotpno = ISOTPHeader(bytes(pkt)) -assert(isotpno.type == 1) -assert(isotpno.message_size == 0xff) -assert(isotpno.identifier == 0x123) -assert(isotpno.length == 7) - -= Compare both isotp payloads -assert(isotpno.data == isotpex.data) -assert(isotpno.message_size == isotpex.message_size) - -= Dissect multiple packets -frames = \ - [b'\x00\x00\x00\x00\x08\x00\x00\x00\x10(\xde\xad\xbe\xef\xde\xad', - b'\x00\x00\x00\x00\x08\x00\x00\x00!\xbe\xef\xde\xad\xbe\xef\xde', - b'\x00\x00\x00\x00\x08\x00\x00\x00"\xad\xbe\xef\xde\xad\xbe\xef', - b'\x00\x00\x00\x00\x08\x00\x00\x00#\xde\xad\xbe\xef\xde\xad\xbe', - b'\x00\x00\x00\x00\x08\x00\x00\x00$\xef\xde\xad\xbe\xef\xde\xad', - b'\x00\x00\x00\x00\x07\x00\x00\x00%\xbe\xef\xde\xad\xbe\xef'] - -isotpframes = [ISOTPHeader(x) for x in frames] - -assert(isotpframes[0].type == 1) -assert(isotpframes[0].message_size == 40) -assert(isotpframes[0].length == 8) -assert(isotpframes[1].type == 2) -assert(isotpframes[1].index == 1) -assert(isotpframes[1].length == 8) -assert(isotpframes[2].type == 2) -assert(isotpframes[2].index == 2) -assert(isotpframes[2].length == 8) -assert(isotpframes[3].type == 2) -assert(isotpframes[3].index == 3) -assert(isotpframes[3].length == 8) -assert(isotpframes[4].type == 2) -assert(isotpframes[4].index == 4) -assert(isotpframes[4].length == 8) -assert(isotpframes[5].type == 2) -assert(isotpframes[5].index == 5) -assert(isotpframes[5].length == 7) - -= Build SF frame with constructor, check for correct length assignments -p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_SF(data=b'\xad\xbe\xad\xff'))) -assert(p.length == 5) -assert(p.message_size == 4) -assert(len(p.data) == 4) -assert(p.data == b'\xad\xbe\xad\xff') -assert(p.type == 0) -assert(p.identifier == 0) - -= Build SF frame EA with constructor, check for correct length assignments -p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_SF(data=b'\xad\xbe\xad\xff'))) -assert(p.extended_address == 0) -assert(p.length == 6) -assert(p.message_size == 4) -assert(len(p.data) == 4) -assert(p.data == b'\xad\xbe\xad\xff') -assert(p.type == 0) -assert(p.identifier == 0) - -= Build FF frame with constructor, check for correct length assignments -p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FF(message_size=10, data=b'\xad\xbe\xad\xff'))) -assert(p.length == 6) -assert(p.message_size == 10) -assert(len(p.data) == 4) -assert(p.data == b'\xad\xbe\xad\xff') -assert(p.type == 1) -assert(p.identifier == 0) - -= Build FF frame EA with constructor, check for correct length assignments -p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FF(message_size=10, data=b'\xad\xbe\xad\xff'))) -assert(p.extended_address == 0) -assert(p.length == 7) -assert(p.message_size == 10) -assert(len(p.data) == 4) -assert(p.data == b'\xad\xbe\xad\xff') -assert(p.type == 1) -assert(p.identifier == 0) - -= Build FF frame EA, extended size, with constructor, check for correct length assignments -p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FF(message_size=0, - extended_message_size=2000, - data=b'\xad'))) -assert(p.extended_address == 0) -assert(p.length == 8) -assert(p.message_size == 0) -assert(p.extended_message_size == 2000) -assert(len(p.data) == 1) -assert(p.data == b'\xad') -assert(p.type == 1) -assert(p.identifier == 0) - -= Build FF frame, extended size, with constructor, check for correct length assignments -p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FF(message_size=0, - extended_message_size=2000, - data=b'\xad'))) -assert(p.length == 7) -assert(p.message_size == 0) -assert(p.extended_message_size == 2000) -assert(len(p.data) == 1) -assert(p.data == b'\xad') -assert(p.type == 1) -assert(p.identifier == 0) - -= Build CF frame with constructor, check for correct length assignments -p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_CF(data=b'\xad'))) -assert(p.length == 2) -assert(p.index == 0) -assert(len(p.data) == 1) -assert(p.data == b'\xad') -assert(p.type == 2) -assert(p.identifier == 0) - -= Build CF frame EA with constructor, check for correct length assignments -p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_CF(data=b'\xad'))) -assert(p.length == 3) -assert(p.index == 0) -assert(len(p.data) == 1) -assert(p.data == b'\xad') -assert(p.type == 2) -assert(p.identifier == 0) - -= Build FC frame EA with constructor, check for correct length assignments -p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FC())) -assert(p.length == 4) -assert(p.block_size == 0) -assert(p.separation_time == 0) -assert(p.type == 3) -assert(p.identifier == 0) - -= Build FC frame with constructor, check for correct length assignments -p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FC())) -assert(p.length == 3) -assert(p.block_size == 0) -assert(p.separation_time == 0) -assert(p.type == 3) -assert(p.identifier == 0) - -= Construct some single frames -p = ISOTPHeader(identifier=0x123, length=5)/ISOTP_SF(message_size=4, data=b'abcd') -assert(p.length == 5) -assert(p.identifier == 0x123) -assert(p.type == 0) -assert(p.message_size == 4) -assert(p.data == b'abcd') - -= Construct some single frames EA -p = ISOTPHeaderEA(identifier=0x123, length=6, extended_address=42)/ISOTP_SF(message_size=4, data=b'abcd') -assert(p.length == 6) -assert(p.extended_address == 42) -assert(p.identifier == 0x123) -assert(p.type == 0) -assert(p.message_size == 4) -assert(p.data == b'abcd') - -= Construct ISOTP_packet with extended can frame -p = get_isotp_packet(identifier=0x1234, extended=False, extended_can_id=True) -print(p) -assert (p.identifier == 0x1234) -assert (p.flags == "extended") - -= Construct ISOTPEA_Packet with extended can frame -p = get_isotp_packet(identifier=0x1234, extended=True, extended_can_id=True) -print(p) -assert (p.identifier == 0x1234) -assert (p.flags == "extended") - -+ ISOTP fragment and defragment checks - -= Fragment an empty ISOTP message -fragments = ISOTP().fragment() -assert(len(fragments) == 1) -assert(fragments[0].data == b"\0") - -= Fragment another empty ISOTP message -fragments = ISOTP(b"").fragment() -assert(len(fragments) == 1) -assert(fragments[0].data == b"\0") - -= Fragment a 4 bytes long ISOTP message -fragments = ISOTP(b"data", src=0x241).fragment() -assert(len(fragments) == 1) -assert(isinstance(fragments[0], CAN)) -fragment = CAN(bytes(fragments[0])) -assert(fragment.data == b"\x04data") -assert(fragment.flags == 0) -assert(fragment.length == 5) -assert(fragment.reserved == 0) - -= Fragment a 4 bytes long ISOTP message extended -fragments = ISOTP(b"data", dst=0x1fff0000).fragment() -assert(len(fragments) == 1) -assert(isinstance(fragments[0], CAN)) -fragment = CAN(bytes(fragments[0])) -assert(fragment.data == b"\x04data") -assert(fragment.length == 5) -assert(fragment.reserved == 0) -assert(fragment.flags == 4) - -= Fragment a 7 bytes long ISOTP message -fragments = ISOTP(b"abcdefg").fragment() -assert(len(fragments) == 1) -assert(fragments[0].data == b"\x07abcdefg") - -= Fragment a 8 bytes long ISOTP message -fragments = ISOTP(b"abcdefgh").fragment() -assert(len(fragments) == 2) -assert(fragments[0].data == b"\x10\x08abcdef") -assert(fragments[1].data == b"\x21gh") - -= Fragment an ISOTP message with extended addressing -isotp = ISOTP(b"abcdef", exdst=ord('A')) -fragments = isotp.fragment() -assert(len(fragments) == 1) -assert(fragments[0].data == b"A\x06abcdef") - -= Fragment a 7 bytes ISOTP message with destination identifier -isotp = ISOTP(b"abcdefg", dst=0x64f) -fragments = isotp.fragment() -assert(len(fragments) == 1) -assert(fragments[0].data == b"\x07abcdefg") -assert(fragments[0].identifier == 0x64f) - -= Fragment a 16 bytes ISOTP message with extended addressing -isotp = ISOTP(b"abcdefghijklmnop", dst=0x64f, exdst=ord('A')) -fragments = isotp.fragment() -assert(len(fragments) == 3) -assert(fragments[0].data == b"A\x10\x10abcde") -assert(fragments[1].data == b"A\x21fghijk") -assert(fragments[2].data == b"A\x22lmnop") -assert(fragments[0].identifier == 0x64f) -assert(fragments[1].identifier == 0x64f) -assert(fragments[2].identifier == 0x64f) - -= Fragment a huge ISOTP message, 4997 bytes long -data = b"T" * 4997 -isotp = ISOTP(b"T" * 4997, dst=0x345) -fragments = isotp.fragment() -assert(len(fragments) == 715) -assert(fragments[0].data == dhex("10 00 00 00 13 85") + b"TT") -assert(fragments[1].data == b"\x21TTTTTTT") -assert(fragments[-2].data == b"\x29TTTTTTT") -assert(fragments[-1].data == b"\x2ATTTT") - -= Defragment a single-frame ISOTP message -fragments = [CAN(identifier=0x641, data=b"\x04test")] -isotp = ISOTP.defragment(fragments) -isotp.show() -assert(isotp.data == b"test") -assert(isotp.dst == 0x641) - -= Defragment non ISOTP message -fragments = [CAN(identifier=0x641, data=b"\xa4test")] -isotp = ISOTP.defragment(fragments) -assert isotp is None - -= Defragment exception -fragments = [] -ex = False -try: - isotp = ISOTP.defragment(fragments) - isotp.show() -except Scapy_Exception: - ex = True - -assert ex - -= Defragment an ISOTP message composed of multiple CAN frames -fragments = [ - CAN(identifier=0x641, data=dhex("41 10 10 61 62 63 64 65")), - CAN(identifier=0x641, data=dhex("41 21 66 67 68 69 6A 6B")), - CAN(identifier=0x641, data=dhex("41 22 6C 6D 6E 6F 70 00")) -] -isotp = ISOTP.defragment(fragments) -isotp.show() -assert(isotp.data == dhex("61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70")) -assert(isotp.dst == 0x641) -assert(isotp.exdst == 0x41) - -= Check if fragmenting a message and defragmenting it back yields the original message -isotp1 = ISOTP(b"abcdef", exdst=ord('A')) -fragments = isotp1.fragment() -isotp2 = ISOTP.defragment(fragments) -isotp2.show() -assert(isotp1 == isotp2) - -isotp1 = ISOTP(b"abcdefghijklmnop") -fragments = isotp1.fragment() -isotp2 = ISOTP.defragment(fragments) -isotp2.show() -assert(isotp1 == isotp2) - -isotp1 = ISOTP(b"abcdefghijklmnop", exdst=ord('A')) -fragments = isotp1.fragment() -isotp2 = ISOTP.defragment(fragments) -isotp2.show() -assert(isotp1 == isotp2) - -isotp1 = ISOTP(b"T"*5000, exdst=ord('A')) -fragments = isotp1.fragment() -isotp2 = ISOTP.defragment(fragments) -isotp2.show() -assert(isotp1 == isotp2) - -= Defragment an ambiguous CAN frame -fragments = [CAN(identifier=0x641, data=dhex("02 01 AA"))] -isotp = ISOTP.defragment(fragments, False) -isotp.show() -assert(isotp.data == dhex("01 AA")) -assert(isotp.exdst == None) -isotpex = ISOTP.defragment(fragments, True) -isotpex.show() -assert(isotpex.data == dhex("AA")) -assert(isotpex.exdst == 0x02) - - -+ Testing ISOTPMessageBuilder - -= Create ISOTPMessageBuilder -m = ISOTPMessageBuilder() - -= Feed packets to machine -ff = CAN(identifier=0x241, data=dhex("10 28 01 02 03 04 05 06")) -ff.time = 1000 -m.feed(ff) -m.feed(CAN(identifier=0x641, data=dhex("30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("21 07 08 09 0A 0B 0C 0D"))) -m.feed(CAN(identifier=0x241, data=dhex("22 0E 0F 10 11 12 13 14"))) -m.feed(CAN(identifier=0x241, data=dhex("23 15 16 17 18 19 1A 1B"))) -m.feed(CAN(identifier=0x641, data=dhex("30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("24 1C 1D 1E 1F 20 21 22"))) -m.feed(CAN(identifier=0x241, data=dhex("25 23 24 25 26 27 28" ))) - -= Verify there is a ready message in the machine -assert(m.count == 1) - -= Extract the message from the machine -msg = m.pop() -assert(m.count == 0) -assert(msg.dst == 0x241) -assert(msg.exdst is None) -assert(msg.src == 0x641) -assert(msg.exsrc is None) -assert(msg.time == 1000) -expected = dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28") -assert(msg.data == expected) - -= Verify that no error happens when there is not enough data -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("04 AB CD EF"))) -msg = m.pop() -assert(msg is None) - -= Verify that no error happens when there is no data -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex(""))) -msg = m.pop() -assert(msg is None) - -= Verify a single frame without EA -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("04 AB CD EF 04"))) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is None) -assert(msg.data == dhex("AB CD EF 04")) - -= Single frame without EA, with excessive bytes in CAN frame -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("03 AB CD EF AB CD EF AB"))) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is None) -assert(msg.data == dhex("AB CD EF")) - -= Verify a single frame with EA -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("E2 04 01 02 03 04"))) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0xE2) -assert(msg.data == dhex("01 02 03 04")) - -= Single CAN frame that has 2 valid interpretations -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("04 01 02 03 04"))) -msg = m.pop(0x241, None) -assert(msg.dst == 0x241) -assert(msg.exdst is None) -assert(msg.data == dhex("01 02 03 04")) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst == 0x04) -assert(msg.data == dhex("02")) - -= Verify multiple frames with EA -m = ISOTPMessageBuilder() -ff = CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")) -ff.time = 1005 -m.feed(ff) -m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17"))) -m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0xEA) -assert(msg.src == 0x641) -assert(msg.exsrc is 0xEA) -assert(msg.time == 1005) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28")) - -= Verify multiple frames with EA 2 -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05"))) -m.feed(CAN(identifier=0x641, data=dhex("AE 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17"))) -m.feed(CAN(identifier=0x641, data=dhex("AE 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0xEA) -assert(msg.src == 0x641) -assert(msg.exsrc is 0xAE) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28")) - -= Verify that an EA starting with 1 will still work -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("1A 10 14 01 02 03 04 05"))) -m.feed(CAN(identifier=0x641, data=dhex("1A 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("1A 21 06 07 08 09 0A 0B"))) -m.feed(CAN(identifier=0x241, data=dhex("1A 22 0C 0D 0E 0F 10 11"))) -m.feed(CAN(identifier=0x241, data=dhex("1A 23 12 13 14 15 16 17"))) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0x1A) -assert(msg.src == 0x641) -assert(msg.exsrc is 0x1A) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14")) - -= Verify that an EA of 07 will still work -m = ISOTPMessageBuilder() -m.feed(CAN(identifier=0x241, data=dhex("07 10 0A 01 02 03 04 05"))) -m.feed(CAN(identifier=0x641, data=dhex("07 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("07 21 06 07 08 09 0A 0B"))) -msg = m.pop(0x241, 0x07) -assert(msg.dst == 0x241) -assert(msg.exdst is 0x07) -assert(msg.src == 0x641) -assert(msg.exsrc is 0x07) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09 0A")) - -= Verify that three interleaved messages can be sniffed simultaneously on the same identifier and extended address (very unrealistic) -m = ISOTPMessageBuilder() -ff = CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")) -ff.time = 300 -m.feed(ff) # start of message A -m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17"))) -ff = CAN(identifier=0x241, data=dhex("EA 10 10 31 32 33 34 35")) -ff.time = 400 -m.feed(ff) # start of message B -m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) -sf = CAN(identifier=0x241, data=dhex("EA 03 A6 A7 A8" )) -sf.time = 200 -m.feed(sf) # single-frame message C -m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) -m.feed(CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 21 36 37 38 39 3A 3B"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 22 3C 3D 3E 3F 40" ))) # end of message B -m.feed(CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23"))) -m.feed(CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))) # end of message A -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0xEA) -assert(msg.data == dhex("A6 A7 A8")) -assert(msg.time == 200) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0xEA) -assert(msg.src == 0x641) -assert(msg.exsrc is 0xEA) -assert(msg.time == 400) -assert(msg.data == dhex("31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40")) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0xEA) -assert(msg.src == 0x641) -assert(msg.exsrc is 0xEA) -assert(msg.time == 300) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28")) - - -= Verify multiple frames with EA from list -m = ISOTPMessageBuilder() -msgs = [ - CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")), - CAN(identifier=0x641, data=dhex("EA 30 03 00" )), - CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B")), - CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11")), - CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17")), - CAN(identifier=0x641, data=dhex("EA 30 03 00" )), - CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D")), - CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23")), - CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))] -m.feed(msgs) -msg = m.pop() -assert(msg.dst == 0x241) -assert(msg.exdst is 0xEA) -assert(msg.src == 0x641) -assert(msg.exsrc is 0xEA) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28")) - -= Verify multiple frames with EA from list and iterator -m = ISOTPMessageBuilder() -msgs = [ - CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")), - CAN(identifier=0x641, data=dhex("EA 30 03 00" )), - CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B")), - CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11")), - CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17")), - CAN(identifier=0x641, data=dhex("EA 30 03 00" )), - CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D")), - CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23")), - CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" )), - CAN(identifier=0x241, data=dhex("EA 03 A6 A7 A8" )), - CAN(identifier=0x241, data=dhex("EA 03 A6 A7 A8"))] -m.feed(msgs) -assert m.count == 3 -assert len(m) == 3 - -isotpmsgs = [x for x in m] -assert len(isotpmsgs) == 3 -msg = isotpmsgs[0] -assert(msg.dst == 0x241) -assert(msg.exdst is 0xEA) -assert(msg.src == 0x641) -assert(msg.exsrc is 0xEA) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28")) - -assert isotpmsgs[1] == isotpmsgs[2] - -= Verify a single frame without EA and different basecls -m = ISOTPMessageBuilder(basecls=Raw) -m.feed(CAN(identifier=0x241, data=dhex("04 AB CD EF 04"))) -msg = m.pop() -assert(msg.load == dhex("AB CD EF 04")) -assert(type(msg) == Raw) - -+ Test sniffer -= Test sniffer with multiple frames -~ vcan_socket needs_root linux - -test_frames = [ - (0x241, "EA 10 28 01 02 03 04 05"), - (0x641, "EA 30 03 00" ), - (0x241, "EA 21 06 07 08 09 0A 0B"), - (0x241, "EA 22 0C 0D 0E 0F 10 11"), - (0x241, "EA 23 12 13 14 15 16 17"), - (0x641, "EA 30 03 00" ), - (0x241, "EA 24 18 19 1A 1B 1C 1D"), - (0x241, "EA 25 1E 1F 20 21 22 23"), - (0x241, "EA 26 24 25 26 27 28" ), -] - -succ = False - -def sender(args=None): - global succ - with new_can_socket(iface0) as tx_sock: - for f in test_frames: - tx_sock.send(CAN(identifier=f[0], data=dhex(f[1]))) - succ = True - -with new_can_socket(iface0) as s: - thread = threading.Thread(target=sender) - sniffed = sniff(opened_socket=s, session=ISOTPSession, timeout=1, prn=lambda x: x.show2(), started_callback=thread.start, count=1) - -assert sniffed[0]['ISOTP'].data == bytearray(range(1, 0x29)) -assert(sniffed[0]['ISOTP'].src == 0x641) -assert(sniffed[0]['ISOTP'].exsrc is 0xEA) -assert(sniffed[0]['ISOTP'].dst == 0x241) -assert(sniffed[0]['ISOTP'].exdst is 0xEA) -thread.join(timeout=5) -assert(succ) - -+ ISOTPSocket tests - -= Single-frame receive -cans = MockCANSocket() -cans.rcvd_queue.put(CAN(identifier=0x241, data=dhex("05 01 02 03 04 05"))) -with ISOTPSoftSocket(cans, sid=0x641, did=0x241) as s: - msg = s.recv() - -assert(msg.data == dhex("01 02 03 04 05")) -assert(cans.sent_queue.empty()) - -= Single-frame send -cans = MockCANSocket() -with ISOTPSoftSocket(cans, sid=0x641, did=0x241) as s: - s.send(ISOTP(dhex("01 02 03 04 05"))) - -msg = cans.sent_queue.get(True, 1) -assert(msg.data == dhex("05 01 02 03 04 05")) -assert(cans.sent_queue.empty()) -assert(cans.rcvd_queue.empty()) - -= Two frame receive -cans = MockCANSocket() -with ISOTPSoftSocket(cans, sid=0x641, did=0x241) as s: - ready = threading.Event() - exception = None - succ = False - def sender(): - global exception, succ - try: - cans.rcvd_queue.put(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) - ready.set() - c = cans.sent_queue.get(True, 2) - assert(c.data == dhex("30 00 00")) - cans.rcvd_queue.put(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) - succ = True - except Exception as ex: - exception = ex - raise ex - thread = threading.Thread(target=sender, name="sender") - thread.start() - ready.wait(timeout=5) - msg = s.recv() - thread.join(timeout=5) - -if exception is not None: - raise exception - -assert(succ) -assert(msg.data == dhex("01 02 03 04 05 06 07 08 09")) -assert(cans.sent_queue.empty()) -assert(cans.rcvd_queue.empty()) - -= 20000 bytes receive -data = dhex("01 02 03 04 05")*4000 -cans = MockCANSocket() - -with ISOTPSoftSocket(cans, sid=0x641, did=0x241) as s: - ready = threading.Event() - exception = None - succ = False - def sender(): - global exception, succ - try: - cf = ISOTP(data, dst=0x241).fragment() - ff = cf.pop(0) - cans.rcvd_queue.put(ff) - ready.set() - c = cans.sent_queue.get(True, 2) - assert(c.data == dhex("30 00 00")) - for f in cf: - cans.rcvd_queue.put(f) - succ = True - except Exception as ex: - exception = ex - raise ex - thread = threading.Thread(target=sender, name="sender") - thread.start() - ready.wait(15) - msg = s.recv() - thread.join(15) - -if exception is not None: - raise exception - -assert(succ) -assert(msg.data == data) -assert(cans.sent_queue.empty()) -assert(cans.rcvd_queue.empty()) - -cans = MockCANSocket() -with ISOTPSoftSocket(cans, sid=0x641, did=0x241) as s: - s.send(ISOTP(dhex("01 02 03 04 05"))) - -= 20000 bytes send -data = dhex("01 02 03 04 05")*4000 -cans = MockCANSocket() -msg = ISOTP(data, dst=0x641) -succ = threading.Event() -ready = threading.Event() -fragments = msg.fragment() -ack = CAN(identifier=0x241, data=dhex("30 00 00")) - -def acker(): - ready.set() - ff = cans.sent_queue.get(True, 2) - assert(ff == fragments[0]) - cans.rcvd_queue.put(ack) - for fragment in fragments[1:]: - cf = cans.sent_queue.get(True, 2) - assert(fragment == cf) - succ.set() - -thread = threading.Thread(target=acker, name="acker") -thread.start() -ready.wait(timeout=5) - -with ISOTPSoftSocket(cans, sid=0x641, did=0x241) as s: - s.send(msg) - -thread.join(15) -succ.wait(2) -assert(succ.is_set()) - -= Create and close ISOTP soft socket -with ISOTPSocket(MockCANSocket(), sid=0x641, did=0x241) as s: - assert(s.impl.rx_thread.is_alive()) - s.close() - assert(not s.impl.rx_thread.join(5)) - assert(not s.impl.rx_thread.is_alive()) - - -= Verify that all threads will die when GC collects the socket -import gc -s = ISOTPSocket(MockCANSocket(), sid=0x641, did=0x241) -assert(s.impl.rx_thread.is_alive()) -impl = s.impl -s = None -r = gc.collect() -impl.rx_thread.join(10) # hope that the GC has made a pass -assert(not impl.rx_thread.is_alive()) - -= Test on_recv function with single frame -with ISOTPSocket(MockCANSocket(), sid=0x641, did=0x241) as s: - s.ins.on_recv(CAN(identifier=0x241, data=dhex("05 01 02 03 04 05"))) - msg = s.ins.rx_queue.get(True, 1) - assert(msg == dhex("01 02 03 04 05")) - -= Test on_recv function with empty frame -with ISOTPSocket(MockCANSocket(), sid=0x641, did=0x241) as s: - s.ins.on_recv(CAN(identifier=0x241, data=b"")) - assert(s.ins.rx_queue.empty()) - -= Test on_recv function with single frame and extended addressing -with ISOTPSocket(MockCANSocket(), sid=0x641, did=0x241, extended_rx_addr=0xea) as s: - s.ins.on_recv(CAN(identifier=0x241, data=dhex("EA 05 01 02 03 04 05"))) - msg = s.ins.rx_queue.get(True, 1) - assert(msg == dhex("01 02 03 04 05")) - -= CF is sent when first frame is received -cans = MockCANSocket() -with ISOTPSocket(cans, sid=0x641, did=0x241) as s: - s.ins.on_recv(CAN(identifier=0x241, data=dhex("10 20 01 02 03 04 05 06"))) - can = cans.sent_queue.get(True, 1) - assert(can.identifier == 0x641) - assert(can.data == dhex("30 00 00")) - -cans.close() - -+ Testing ISOTPSocket with an actual CAN socket - -= Verify that packets are not lost if they arrive before the sniff() is called -with new_can_socket(iface0) as ss, new_can_socket0() as sr: - tx_func = lambda: ss.send(CAN(identifier=0x111, data=b"\x01\x23\x45\x67")) - p = sr.sniff(count=1, timeout=0.2, started_callback=tx_func) - assert(len(p)==1) - tx_func = lambda: ss.send(CAN(identifier=0x111, data=b"\x89\xab\xcd\xef")) - p = sr.sniff(count=1, timeout=0.2, started_callback=tx_func) - assert(len(p)==1) - -= Send single frame ISOTP message, using begin_send -with new_can_socket(iface0) as isocan, \ - ISOTPSocket(isocan, sid=0x641, did=0x241) as s, \ - new_can_socket0() as cans: - can = cans.sniff(timeout=2, count=1, started_callback=lambda: s.begin_send(ISOTP(data=dhex("01 02 03 04 05")))) - assert(can[0].identifier == 0x641) - assert(can[0].data == dhex("05 01 02 03 04 05")) - -= Send many single frame ISOTP messages, using begin_send - -with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x641, did=0x241) as s, \ - new_can_socket0() as cans: - for i in range(100): - data = dhex("01 02 03 04 05") + struct.pack("B", i) - expected = struct.pack("B", len(data)) + data - can = cans.sniff(timeout=4, count=1, started_callback=lambda: s.begin_send(ISOTP(data=data))) - assert(can[0].identifier == 0x641) - print(can[0].data, data) - assert(can[0].data == expected) - - -= Send two-frame ISOTP message, using begin_send -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - can = cans.sniff(timeout=1, count=1, started_callback=lambda: s.begin_send(ISOTP(data=dhex("01 02 03 04 05 06 07 08")))) - assert can[0].identifier == 0x641 - assert can[0].data == dhex("10 08 01 02 03 04 05 06") - can = cans.sniff(timeout=1, count=1, started_callback=lambda: cans.send(CAN(identifier = 0x241, data=dhex("30 00 00")))) - assert can[0].identifier == 0x641 - assert can[0].data == dhex("21 07 08") - -cans.close() - -= Send single frame ISOTP message -with new_can_socket(iface0) as cans: - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - s.send(ISOTP(data=dhex("01 02 03 04 05"))) - can = cans.sniff(timeout=1, count=1) - assert(can[0].identifier == 0x641) - assert(can[0].data == dhex("05 01 02 03 04 05")) - - -= Send two-frame ISOTP message -acker_ready = threading.Event() -def acker(): - with new_can_socket(iface0) as acks: - acker_ready.set() - can_pkt = acks.sniff(timeout=1, count=1) - can = can_pkt[0] - acks.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) - -Thread(target=acker).start() -acker_ready.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("10 08 01 02 03 04 05 06")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x241) - assert(can.data == dhex("30 00 00")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("21 07 08")) - - -= Send two-frame ISOTP message with bs - -acker_ready = threading.Event() -def acker(): - with new_can_socket(iface0) as acks: - acker_ready.set() - can_pkt = acks.sniff(timeout=1, count=1) - can = can_pkt[0] - acks.send(CAN(identifier = 0x241, data=dhex("30 20 00"))) - -Thread(target=acker).start() -acker_ready.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("10 08 01 02 03 04 05 06")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x241) - assert(can.data == dhex("30 20 00")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("21 07 08")) - - -= Send two-frame ISOTP message with ST -acker_ready = threading.Event() -def acker(): - with new_can_socket0() as acks: - acker_ready.set() - can_pkt = acks.sniff(timeout=1, count=1) - can = can_pkt[0] - acks.send(CAN(identifier = 0x241, data=dhex("30 00 10"))) - -Thread(target=acker).start() -acker_ready.wait(timeout=5) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("10 08 01 02 03 04 05 06")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x241) - assert(can.data == dhex("30 00 10")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("21 07 08")) - - -= Receive a single frame ISOTP message -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("05 01 02 03 04 05"))) - isotp = s.recv() - assert(isotp.data == dhex("01 02 03 04 05")) - assert(isotp.src == 0x641) - assert(isotp.dst == 0x241) - assert(isotp.exsrc == None) - assert(isotp.exdst == None) - - -= Receive a single frame ISOTP message, with extended addressing -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241, extended_addr=0xc0, extended_rx_addr=0xea) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("EA 05 01 02 03 04 05"))) - isotp = s.recv() - assert(isotp.data == dhex("01 02 03 04 05")) - assert(isotp.src == 0x641) - assert(isotp.dst == 0x241) - assert(isotp.exsrc == 0xc0) - assert(isotp.exdst == 0xea) - - -= Receive frames from CandumpReader -candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA''') - -with ISOTPSocket(CandumpReader(candump_fd), sid=0x241, did=0x541, listen_only=True) as s: - pkts = s.sniff(timeout=1, count=6) - assert(len(pkts) == 6) - isotp = pkts[0] - print(repr(isotp)) - print(hex(isotp.src)) - print(hex(isotp.dst)) - assert(isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA")) - assert(isotp.src == 0x241) - assert(isotp.dst == 0x541) - -= Receive frames from CandumpReader with ISOTPSniffer without extended addressing -candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA''') - -pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession, timeout=1, session_kwargs={"use_ext_addr": False}) -assert(len(pkts) == 6) -isotp = pkts[0] -assert(isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA")) -assert (isotp.dst == 0x541) - -= Receive frames from CandumpReader with ISOTPSniffer -* all flow control frames are detected as single frame with extended address - -candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA''') - -pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession, timeout=1) -assert(len(pkts) == 12) -isotp = pkts[1] -assert(isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA")) -assert (isotp.dst == 0x541) -isotp = pkts[0] -assert(isotp.data == dhex("")) -assert (isotp.dst == 0x241) - -= Receive frames from CandumpReader with ISOTPSniffer and count -* all flow control frames are detected as single frame with extended address - -candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA - vcan0 541 [8] 10 0A DE AD BE EF AA AA - vcan0 241 [3] 30 00 00 - vcan0 541 [5] 21 AA AA AA AA''') - -pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession, timeout=1, count=2) -assert(len(pkts) == 2) -isotp = pkts[1] -assert(isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA")) -assert (isotp.dst == 0x541) -isotp = pkts[0] -assert(isotp.data == dhex("")) -assert (isotp.dst == 0x241) - -= Receive a two-frame ISOTP message -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("10 0B 01 02 03 04 05 06"))) - cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 10 11"))) - isotp = s.recv() - assert(isotp.data == dhex("01 02 03 04 05 06 07 08 09 10 11")) - -= Check what happens when a CAN frame with wrong identifier gets received -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x141, data = dhex("05 01 02 03 04 05"))) - assert(s.ins.rx_queue.empty()) - -+ Testing ISOTPSocket timeouts - -= Check if not sending the last CF will make the socket timeout -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("10 11 01 02 03 04 05 06"))) - cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 0A 0B 0C 0D"))) - isotp = s.sniff(timeout=1) - -assert(len(isotp) == 0) - -= Check if not sending the first CF will make the socket timeout -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("10 11 01 02 03 04 05 06"))) - isotp = s.sniff(timeout=1) - -assert(len(isotp) == 0) - -= Check if not sending the first FC will make the socket timeout -exception = None -isotp = ISOTP(data=dhex("01 02 03 04 05 06 07 08 09 0A")) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - try: - s.send(isotp) - assert(False) - except Scapy_Exception as ex: - exception = ex - -print(exception) -assert(str(exception) == "TX state was reset due to timeout" or str(exception) == "ISOTP send not completed in 30s") - -= Check if not sending the second FC will make the socket timeout -exception = None -isotp = ISOTP(data=b"\xa5" * 120) -test_sem = threading.Semaphore(0) -evt = threading.Event() - -def acker(): - with new_can_socket(iface0) as cans: - evt.set() - can = cans.sniff(timeout=1, count=1)[0] - cans.send(CAN(identifier = 0x241, data=dhex("30 04 00"))) - -thread = Thread(target=acker) -thread.start() -evt.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - try: - s.send(isotp) - except Scapy_Exception as ex: - exception = ex - -cans.close() -thread.join(timeout=5) - -assert(exception is not None) -print(exception) -assert(str(exception) == "TX state was reset due to timeout") - -= Check if reception of an overflow FC will make a send fail -exception = None -isotp = ISOTP(data=b"\xa5" * 120) -test_sem = threading.Semaphore(0) -evt = threading.Event() - -def acker(): - with new_can_socket(iface0) as cans: - evt.set() - can = cans.sniff(timeout=1, count=1)[0] - cans.send(CAN(identifier = 0x241, data=dhex("32 00 00"))) - -thread = Thread(target=acker) -thread.start() -evt.wait(timeout=5) - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - try: - s.send(isotp) - except Scapy_Exception as ex: - exception = ex - -thread.join(timeout=5) - -assert(exception is not None) -print(exception) -assert(str(exception) == "Overflow happened at the receiver side") - -= Close the Socket -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241) as s: - s.close() - -+ More complex operations - -= ISOTPSoftSocket sr1 -drain_bus(iface0) -drain_bus(iface1) - -evt = threading.Event() -msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') -rx2 = None -evt2 = threading.Event() - -def sender(sock): - global evt, rx2, msg - evt2.set() - evt.wait(timeout=5) - rx2 = sock.sr1(msg, timeout=3, verbose=True) - -with new_can_socket0() as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as sock_tx, \ - new_can_socket0() as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x321, 0x123) as sock_rx: - txThread = threading.Thread(target=sender, args=(sock_tx,)) - txThread.start() - evt2.wait(timeout=1) - rx = sock_rx.sniff(timeout=3, count=1, started_callback=evt.set)[0] - sock_rx.send(msg) - sent = True - txThread.join(timeout=5) - -assert(rx == msg) -assert(sent) -assert(rx2 is not None) -assert(rx2 == msg) - -= ISOTPSoftSocket sr1 and ISOTP test vice versa -drain_bus(iface0) -drain_bus(iface1) - -rx2 = None -sent = False -evt = threading.Event() -msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') - -with new_can_socket0() as isocan, ISOTPSoftSocket(isocan, 0x123, 0x321) as txSock: - def receiver(): - global rx2, sent - with new_can_socket0() as isocan, ISOTPSoftSocket(isocan, 0x321, 0x123) as rxSock: - evt.set() - rx2 = rxSock.sniff(count=1) - rxSock.send(msg) - sent = True - rxThread = threading.Thread(target=receiver, name="receiver") - rxThread.start() - evt.wait(timeout=5) - rx = txSock.sr1(msg, timeout=5,verbose=True) - rxThread.join(timeout=5) - -assert(rx is not None) -assert(rx == msg) -assert(len(rx2) == 1) -assert(rx2[0] == msg) -assert(sent) - -= ISOTPSoftSocket sniff -evt = threading.Event() -succ = False - -def receiver(): - global evt, succ, rx - with new_can_socket0() as isocan, \ - ISOTPSoftSocket(isocan, 0x321, 0x123) as sock: - rx = sock.sniff(count=5, timeout=5, started_callback=evt.set) - succ = True - -rxThread = threading.Thread(target=receiver) -rxThread.start() -evt.wait(timeout=5) - -msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') -with new_can_socket0() as isocan, \ - ISOTPSoftSocket(isocan, 0x123, 0x321) as sock: - msg.data += b'0' - sock.send(msg) - msg.data += b'1' - sock.send(msg) - msg.data += b'2' - sock.send(msg) - msg.data += b'3' - sock.send(msg) - msg.data += b'4' - sock.send(msg) - -rxThread.join(timeout=5) -msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') -msg.data += b'0' -assert(rx[0] == msg) -msg.data += b'1' -assert(rx[1] == msg) -msg.data += b'2' -assert(rx[2] == msg) -msg.data += b'3' -assert(rx[3] == msg) -msg.data += b'4' -assert(rx[4] == msg) -assert(succ) - -+ ISOTPSoftSocket MITM attack tests - -= bridge and sniff with isotp soft sockets set up vcan0 and vcan1 for package forwarding vcan1 -drain_bus(iface0) -drain_bus(iface1) - -succ = False - -with new_can_socket0() as can0_0, \ - new_can_socket0() as can0_1, \ - new_can_socket1() as can1_0, \ - new_can_socket1() as can1_1, \ - ISOTPSoftSocket(can0_0, sid=0x241, did=0x641) as isoTpSocket0, \ - ISOTPSoftSocket(can1_0, sid=0x541, did=0x141) as isoTpSocket1, \ - ISOTPSoftSocket(can0_1, sid=0x641, did=0x241) as bSocket0, \ - ISOTPSoftSocket(can1_1, sid=0x141, did=0x141) as bSocket1: - evt = threading.Event() - def forwarding(pkt): - global forwarded - forwarded += 1 - return pkt - def bridge(): - global forwarded, succ - forwarded = 0 - bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=0.5, - started_callback=evt.set) - succ = True - threadBridge = threading.Thread(target=bridge) - threadBridge.start() - evt.wait(timeout=5) - packetsVCan1 = isoTpSocket1.sniff(timeout=0.5, started_callback=lambda: isoTpSocket0.send(ISOTP(b'Request'))) - threadBridge.join(timeout=5) - -assert forwarded == 1 -assert len(packetsVCan1) == 1 -assert succ - -drain_bus(iface0) -drain_bus(iface1) - -= bridge and sniff with isotp soft sockets and multiple long packets -drain_bus(iface0) -drain_bus(iface1) - -N = 3 -T = 20 - -succ = False -with new_can_socket0() as can0_0, \ - new_can_socket0() as can0_1, \ - new_can_socket1() as can1_0, \ - new_can_socket1() as can1_1, \ - ISOTPSoftSocket(can0_0, sid=0x241, did=0x641) as isoTpSocket0, \ - ISOTPSoftSocket(can1_0, sid=0x541, did=0x141) as isoTpSocket1, \ - ISOTPSoftSocket(can0_1, sid=0x641, did=0x241) as bSocket0, \ - ISOTPSoftSocket(can1_1, sid=0x141, did=0x541) as bSocket1: - evt = threading.Event() - def forwarding(pkt): - global forwarded - forwarded += 1 - return pkt - def bridge(): - global forwarded, succ - forwarded = 0 - bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, - timeout=T, count=N, started_callback=evt.set) - succ = True - def sendpkts(): - for _ in range(N): - time.sleep(0.2) - isoTpSocket0.send(ISOTP(b'RequestASDF1234567890')) - threadBridge = threading.Thread(target=bridge) - threadBridge.start() - evt.wait(timeout=5) - sender = threading.Thread(target=sendpkts) - packetsVCan1 = isoTpSocket1.sniff(timeout=T, count=N, started_callback=sender.start) - sender.join(timeout=5) - print("forwarded: %d" % forwarded) - print("len(packetsVCan1): %d" % len(packetsVCan1)) - threadBridge.join(timeout=5) - -assert forwarded == N -assert len(packetsVCan1) == N -assert succ - -drain_bus(iface0) -drain_bus(iface1) - -= bridge and sniff with isotp soft sockets set up vcan0 and vcan1 for package change vcan1 -drain_bus(iface0) -drain_bus(iface1) - -succ = False -with new_can_socket0() as can0_0, \ - new_can_socket0() as can0_1, \ - new_can_socket1() as can1_0, \ - new_can_socket1() as can1_1, \ - ISOTPSoftSocket(can0_0, sid=0x241, did=0x641) as isoTpSocket0, \ - ISOTPSoftSocket(can1_0, sid=0x641, did=0x241) as isoTpSocket1, \ - ISOTPSoftSocket(can0_1, sid=0x641, did=0x241) as bSocket0, \ - ISOTPSoftSocket(can1_1, sid=0x241, did=0x641) as bSocket1: - evt = threading.Event() - def forwarding(pkt): - pkt.data = 'changed' - return pkt - def bridge(): - global succ - bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=5, - started_callback=evt.set) - succ = True - threadBridge = threading.Thread(target=bridge) - threadBridge.start() - evt.wait(timeout=5) - packetsVCan1 = isoTpSocket1.sniff(timeout=2, started_callback=lambda: isoTpSocket0.send(ISOTP(b'Request'))) - threadBridge.join(timeout=5) - -assert len(packetsVCan1) == 1 -assert packetsVCan1[0].data == b'changed' -assert succ - -drain_bus(iface0) -drain_bus(iface1) - -= Two ISOTPSockets at the same time, sending and receiving - -with new_can_socket0() as cs1, ISOTPSocket(cs1, sid=0x641, did=0x241) as s1, \ - new_can_socket0() as cs2, ISOTPSocket(cs2, sid=0x241, did=0x641) as s2: - isotp = ISOTP(data=b"\x10\x25" * 43) - def sender(): - s2.send(isotp) - t = Thread(target=sender) - result = s1.sniff(count=1, timeout=5, started_callback=t.start) - t.join(timeout=5) - -assert len(result) == 1 -assert(result[0].data == isotp.data) - - -= Two ISOTPSockets at the same time, multiple sends/receives -with new_can_socket0() as cs1, ISOTPSocket(cs1, sid=0x641, did=0x241) as s1, \ - new_can_socket0() as cs2, ISOTPSocket(cs2, sid=0x241, did=0x641) as s2: - def sender(p): - s2.send(p) - for i in range(1, 40, 5): - isotp = ISOTP(data=bytearray(range(i, i * 2))) - t = Thread(target=sender, args=(isotp,)) - result = s1.sniff(count=1, timeout=5, started_callback=t.start) - t.join(timeout=5) - assert len(result) - assert (result[0].data == isotp.data) - - -= Send a single frame ISOTP message with padding -with new_can_socket0() as cs1, ISOTPSocket(cs1, sid=0x641, did=0x241, padding=True) as s: - with new_can_socket(iface0) as cans: - s.send(ISOTP(data=dhex("01"))) - res = cans.sniff(timeout=1, count=1)[0] - assert(res.length == 8) - - -= Send a two-frame ISOTP message with padding -acker_ready = threading.Event() -def acker(): - with new_can_socket(iface0) as acks: - acker_ready.set() - can = acks.sniff(timeout=1, count=1)[0] - acks.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) - -with new_can_socket(iface0) as cans: - ack_thread = Thread(target=acker) - ack_thread.start() - acker_ready.wait(timeout=5) - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241, padding=True) as s: - s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("10 08 01 02 03 04 05 06")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x241) - assert(can.data == dhex("30 00 00")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("21 07 08 00 00 00 00 00")) - ack_thread.join(timeout=5) - - -= Receive a padded single frame ISOTP message with padding disabled -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241, padding=False) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) - res = s.recv() - assert(res.data == dhex("05 06")) - - -= Receive a padded single frame ISOTP message with padding enabled -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241, padding=True) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) - res = s.recv() - assert(res.data == dhex("05 06")) - - -= Receive a non-padded single frame ISOTP message with padding enabled -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241, padding=True) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("02 05 06"))) - res = s.recv() - assert(res.data == dhex("05 06")) - - -= Receive a padded two-frame ISOTP message with padding enabled -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241, padding=True) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) - cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) - res = s.recv() - assert(res.data == dhex("01 02 03 04 05 06 07 08 09")) - - -= Receive a padded two-frame ISOTP message with padding disabled -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x641, did=0x241, padding=False) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) - cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) - res = s.recv() - res.show() - print(res.data) - print(raw(res)) - assert(res.data == dhex("01 02 03 04 05 06 07 08 09")) - - -+ Compatibility with can-isotp linux kernel modules -~ vcan_socket needs_root linux - -= Compatibility with isotpsend -exit_if_no_isotp_module() - -message = "01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14" - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x642, did=0x242) as s: - p = subprocess.Popen(["isotpsend", "-s", "242", "-d", "642", iface0], stdin=subprocess.PIPE, universal_newlines=True) - p.communicate(message) - r = p.returncode - print("returncode is %d" % r) - assert(r == 0) - isotp = s.recv() - assert(isotp.data == dhex(message)) - - -= Compatibility with isotpsend - extended addresses -exit_if_no_isotp_module() -message = "01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14" - -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x644, did=0x244, extended_addr=0xaa, extended_rx_addr=0xee) as s: - p = subprocess.Popen(["isotpsend", "-s", "244", "-d", "644", "-x", "ee:aa", iface0], stdin=subprocess.PIPE, universal_newlines=True) - p.communicate(message) - r = p.returncode - print("returncode is %d" % r) - assert(r == 0) - isotp = s.recv() - assert(isotp.data == dhex(message)) - - -= Compatibility with isotprecv -exit_if_no_isotp_module() - -isotp = ISOTP(data=bytearray(range(1,20))) -cmd = ["isotprecv", "-s", "243", "-d", "643", "-b", "3", iface0] -print(" ".join(cmd)) -p = subprocess.Popen(cmd, stdout=subprocess.PIPE) -time.sleep(0.1) -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x643, did=0x243) as s: - s.send(isotp) - -threading.Timer(1, lambda: p.terminate() if p.poll() else p.wait()).start() # Timeout the receiver after 1 second -r = p.wait() -print("returncode is %d" % r) -assert(0 == r) - -result = None -for i in range(10): - time.sleep(0.1) - if p.poll() is not None: - result = p.stdout.readline().decode().strip() - break - -assert(result is not None) -print(result) -result_data = dhex(result) -assert(result_data == isotp.data) - - -= Compatibility with isotprecv - extended addresses -exit_if_no_isotp_module() -isotp = ISOTP(data=bytearray(range(1,20))) -cmd = ["isotprecv", "-s245", "-d645", "-b3", "-x", "ee:aa", iface0] -print(" ".join(cmd)) -p = subprocess.Popen(cmd, stdout=subprocess.PIPE) -time.sleep(0.1) # Give some time for starting reception -with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x645, did=0x245, extended_addr=0xaa, extended_rx_addr=0xee) as s: - s.send(isotp) - -threading.Timer(1, lambda: p.terminate() if p.poll() else p.wait()).start() # Timeout the receiver after 1 second -r = p.wait() -print("returncode is %d" % r) -assert(0 == r) - -result = None -for i in range(10): - time.sleep(0.1) - if p.poll() is not None: - result = p.stdout.readline().decode().strip() - break - -assert(result is not None) -print(result) -result_data = dhex(result) -assert(result_data == isotp.data) - - -+ ISOTPNativeSocket tests -~ python3_only not_pypy vcan_socket needs_root linux - - -= Overwrite definition for vcan_socket systems native sockets -~ conf -if six.PY3 and LINUX: - conf.contribs['CANSocket'] = {'use-python-can': False} - from scapy.contrib.cansocket_native import * - new_can_socket = lambda iface: CANSocket(iface) - new_can_socket0 = lambda: CANSocket(iface0) - new_can_socket1 = lambda: CANSocket(iface1) - - - -= Create ISOTP socket -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) - - -= Send single frame ISOTP message -exit_if_no_isotp_module() - -with new_can_socket(iface0) as cans: - s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) - s.send(ISOTP(data=dhex("01 02 03 04 05"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("05 01 02 03 04 05")) - - -= Send single frame ISOTP message Test init with CANSocket -exit_if_no_isotp_module() -cans = CANSocket(iface0) -s = ISOTPNativeSocket(cans, sid=0x641, did=0x241) -s.send(ISOTP(data=dhex("01 02 03 04 05"))) -can = cans.sniff(timeout=1, count=1)[0] -assert(can.identifier == 0x641) -assert(can.data == dhex("05 01 02 03 04 05")) -cans.close() - - -= Test init with wrong type -exit_if_no_isotp_module() -exception_catched = False -try: - s = ISOTPNativeSocket(42, sid=0x641, did=0x241) -except Scapy_Exception: - exception_catched = True - -assert exception_catched - -= Send two-frame ISOTP message -exit_if_no_isotp_module() - -evt = threading.Event() -def acker(): - with new_can_socket(iface0) as cans: - evt.set() - can = cans.sniff(timeout=1, count=1)[0] - cans.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) - - -with new_can_socket(iface0) as cans: - t = Thread(target=acker) - t.start() - s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) - evt.wait(timeout=5) - s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("10 08 01 02 03 04 05 06")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x241) - assert(can.data == dhex("30 00 00")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("21 07 08")) - t.join(timeout=5) - -= Send a single frame ISOTP message with padding -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, padding=True) - -with new_can_socket(iface0) as cans: - s.send(ISOTP(data=dhex("01"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.length == 8) - - -= Send a two-frame ISOTP message with padding -exit_if_no_isotp_module() - -acker_ready = threading.Event() -def acker(): - with new_can_socket(iface0) as acks: - acker_ready.set() - can = acks.sniff(timeout=1, count=1)[0] - acks.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) - -with new_can_socket(iface0) as cans: - Thread(target=acker).start() - acker_ready.wait(timeout=5) - s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, padding=True) - s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("10 08 01 02 03 04 05 06")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x241) - assert(can.data == dhex("30 00 00")) - can = cans.sniff(timeout=1, count=1)[0] - assert(can.identifier == 0x641) - assert(can.data == dhex("21 07 08 00 00 00 00 00")) - - -= Receive a padded single frame ISOTP message with padding disabled -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, padding=False) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) - res = s.recv() - assert(res.data == dhex("05 06")) - - -= Receive a padded single frame ISOTP message with padding enabled -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, padding=True) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) - res = s.recv() - assert(res.data == dhex("05 06")) - - -= Receive a non-padded single frame ISOTP message with padding enabled -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, padding=True) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("02 05 06"))) - res = s.recv() - assert(res.data == dhex("05 06")) - - -= Receive a padded two-frame ISOTP message with padding enabled -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, padding=True) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) - cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) - res = s.recv() - assert(res.data == dhex("01 02 03 04 05 06 07 08 09")) - - -= Receive a padded two-frame ISOTP message with padding disabled -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, padding=False) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) - cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) - res = s.recv() - assert(res.data == dhex("01 02 03 04 05 06 07 08 09")) - - -= Receive a single frame ISOTP message -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("05 01 02 03 04 05"))) - isotp = s.recv() - assert(isotp.data == dhex("01 02 03 04 05")) - assert(isotp.src == 0x641) - assert(isotp.dst == 0x241) - assert(isotp.exsrc == None) - assert(isotp.exdst == None) - - -= Receive a single frame ISOTP message, with extended addressing -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241, extended_addr=0xc0, extended_rx_addr=0xea) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("EA 05 01 02 03 04 05"))) - isotp = s.recv() - assert(isotp.data == dhex("01 02 03 04 05")) - assert(isotp.src == 0x641) - assert(isotp.dst == 0x241) - assert(isotp.exsrc == 0xc0) - assert(isotp.exdst == 0xea) - - -= Receive a two-frame ISOTP message -exit_if_no_isotp_module() -s = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) -with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("10 0B 01 02 03 04 05 06"))) - cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 10 11"))) - isotp = s.recv() - assert(isotp.data == dhex("01 02 03 04 05 06 07 08 09 10 11")) - -= Receive a two-frame ISOTP message and test python with statement -exit_if_no_isotp_module() -with ISOTPNativeSocket(iface0, sid=0x641, did=0x241) as s: - with new_can_socket(iface0) as cans: - cans.send(CAN(identifier = 0x241, data = dhex("10 0B 01 02 03 04 05 06"))) - cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 10 11"))) - isotp = s.recv() - assert(isotp.data == dhex("01 02 03 04 05 06 07 08 09 10 11")) - - -= ISOTP Socket sr1 test -exit_if_no_isotp_module() - -txSock = ISOTPNativeSocket(iface0, sid=0x123, did=0x321) -txmsg = ISOTP(b'\x11\x22\x33') -rx2 = None - -receiver_up = Event() - -def sender(): - global receiver_up - receiver_up.wait(timeout=5) - global txmsg - global rx2 - rx2 = txSock.sr1(txmsg, timeout=1, verbose=True) - -def receiver(): - global receiver_up - with new_can_socket(iface0) as cans: - rx = cans.sniff(timeout=1, count=1, started_callback=receiver_up.set)[0] - cans.send(CAN(identifier=0x321, length=4, data=b'\x03\x7f\x22\x33')) - expectedrx = CAN(identifier=0x123, length=4, data=b'\x03\x11\x22\x33') - assert(rx.length == expectedrx.length) - assert(rx.data == expectedrx.data) - assert(rx.identifier == expectedrx.identifier) - -txThread = threading.Thread(target=sender) -txThread.start() -receiver() -txThread.join(timeout=5) - -assert(rx2 is not None) -assert(rx2 == ISOTP(b'\x7f\x22\x33')) -assert(rx2.answers(txmsg)) - -= ISOTP Socket sr1 and ISOTP test -exit_if_no_isotp_module() -txSock = ISOTPNativeSocket(iface0, 0x123, 0x321) -rxSock = ISOTPNativeSocket(iface0, 0x321, 0x123) -msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') -rx2 = None - -receiver_up = Event() - -def sender(): - receiver_up.wait(timeout=5) - global rx2 - rx2 = txSock.sr1(msg, timeout=1, verbose=True) - -def receiver(): - global rx - receiver_up.set() - rx = rxSock.recv() - rxSock.send(msg) - -txThread = threading.Thread(target=sender) -txThread.start() -receiver() -txThread.join(timeout=5) - -assert(rx == msg) -assert(rxSock.send(msg)) -assert(rx2 is not None) -assert(rx2 == msg) - -= ISOTP Socket sr1 and ISOTP test vice versa -exit_if_no_isotp_module() - -rxSock = ISOTPNativeSocket(iface0, 0x321, 0x123) -txSock = ISOTPNativeSocket(iface0, 0x123, 0x321) - -msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') - -receiver_up = Event() - -def receiver(): - global rx2, sent - rx2 = rxSock.sniff(count=1, timeout=1, started_callback=receiver_up.set) - sent = rxSock.send(msg) - -def sender(): - global rx - receiver_up.wait(timeout=5) - rx = txSock.sr1(msg, timeout=1,verbose=True) - -rx2 = None -sent = False -rxThread = threading.Thread(target=receiver) -rxThread.start() -sender() -rxThread.join(timeout=5) - -assert(rx == msg) -assert(rx2[0] == msg) -assert(sent) - -= ISOTP Socket sniff -exit_if_no_isotp_module() - -rxSock = ISOTPNativeSocket(iface0, 0x321, 0x123) -txSock = ISOTPNativeSocket(iface0, 0x123, 0x321) -succ = False - -receiver_up = Event() - -def receiver(): - rx = rxSock.sniff(count=5, timeout=1, started_callback=receiver_up.set) - msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') - msg.data += b'0' - assert(rx[0] == msg) - msg.data += b'1' - assert(rx[1] == msg) - msg.data += b'2' - assert(rx[2] == msg) - msg.data += b'3' - assert(rx[3] == msg) - msg.data += b'4' - assert(rx[4] == msg) - global succ - succ = True - -def sender(): - receiver_up.wait(timeout=5) - msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') - msg.data += b'0' - assert(txSock.send(msg)) - msg.data += b'1' - assert(txSock.send(msg)) - msg.data += b'2' - assert(txSock.send(msg)) - msg.data += b'3' - assert(txSock.send(msg)) - msg.data += b'4' - assert(txSock.send(msg)) - -rxThread = threading.Thread(target=receiver) -rxThread.start() -sender() -rxThread.join(timeout=5) - -assert(succ) - -+ ISOTPNativeSocket MITM attack tests -~ python3_only vcan_socket needs_root linux - -= bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package forwarding vcan1 -exit_if_no_isotp_module() - -isoTpSocket0 = ISOTPNativeSocket(iface0, sid=0x241, did=0x641) -isoTpSocket1 = ISOTPNativeSocket(iface1, sid=0x641, did=0x241) -bSocket0 = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) -bSocket1 = ISOTPNativeSocket(iface1, sid=0x241, did=0x641) - -bridgeStarted = threading.Event() -def bridge(): - global bridgeStarted - def forwarding(pkt): - return pkt - bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=2, count=1, started_callback=bridgeStarted.set) - bSocket0.close() - bSocket1.close() - global bSucc - bSucc = True - -def RequestOnBus0(): - global rSucc - isoTpSocket0.send(ISOTP(b'Request')) - rSucc = True - -bSucc = False -rSucc = False - -threadBridge = threading.Thread(target=bridge) -threadBridge.start() -threadSender = threading.Thread(target=RequestOnBus0) -bridgeStarted.wait(timeout=5) - -packetsVCan1 = isoTpSocket1.sniff(timeout=0.5, started_callback=threadSender.start) - -len(packetsVCan1) == 1 - -isoTpSocket0.close() -isoTpSocket1.close() - -threadSender.join(timeout=5) -threadBridge.join(timeout=5) - -assert(bSucc) -assert(rSucc) - -= bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package change to vcan1 -exit_if_no_isotp_module() - -isoTpSocket0 = ISOTPNativeSocket(iface0, sid=0x241, did=0x641) -isoTpSocket1 = ISOTPNativeSocket(iface1, sid=0x641, did=0x241) -bSocket0 = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) -bSocket1 = ISOTPNativeSocket(iface1, sid=0x241, did=0x641) - -bSucc = False -rSucc = False - -bridgeStarted = threading.Event() -def bridge(): - global bridgeStarted - global bSucc - def forwarding(pkt): - pkt.data = 'changed' - return pkt - bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=0.5, started_callback=bridgeStarted.set) - bSocket0.close() - bSocket1.close() - bSucc = True - -def RequestOnBus0(): - global rSucc - isoTpSocket0.send(ISOTP(b'Request')) - rSucc = True - -threadBridge = threading.Thread(target=bridge) -threadBridge.start() -threadSender = threading.Thread(target=RequestOnBus0) -bridgeStarted.wait(timeout=5) -packetsVCan1 = isoTpSocket1.sniff(timeout=0.5, started_callback=threadSender.start) - -packetsVCan1[0].data = b'changed' -len(packetsVCan1) == 1 - -isoTpSocket0.close() -isoTpSocket1.close() - -threadSender.join(timeout=5) -threadBridge.join(timeout=5) - -assert(bSucc) -assert(rSucc) - -= bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package forwarding in both directions -exit_if_no_isotp_module() - -bSucc = False -rSucc = False - -isoTpSocket0 = ISOTPNativeSocket(iface0, sid=0x241, did=0x641) -isoTpSocket1 = ISOTPNativeSocket(iface1, sid=0x641, did=0x241) -bSocket0 = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) -bSocket1 = ISOTPNativeSocket(iface1, sid=0x241, did=0x641) - -bridgeStarted = threading.Event() -def bridge(): - global bridgeStarted - global bSucc - def forwarding(pkt): - return pkt - bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=0.5, started_callback=bridgeStarted.set) - bSocket0.close() - bSocket1.close() - bSucc = True - -def RequestBothVCans(): - global rSucc - packetVcan0 = ISOTP(b'RequestVcan0') - packetVcan1 = ISOTP(b'RequestVcan1') - isoTpSocket0.send(packetVcan0) - isoTpSocket1.send(packetVcan1) - rSucc = True - -threadBridge = threading.Thread(target=bridge) -threadBridge.start() -threadSender = threading.Thread(target=RequestOnBus0) -bridgeStarted.wait(timeout=5) - -packetsVCan0 = isoTpSocket0.sniff(timeout=0.5, started_callback=threadSender.start) -packetsVCan1 = isoTpSocket1.sniff(timeout=0.5) - -len(packetsVCan0) == 1 -len(packetsVCan1) == 1 - -isoTpSocket0.close() -isoTpSocket1.close() - -threadSender.join(timeout=5) -threadBridge.join(timeout=5) - -assert(bSucc) -assert(rSucc) - -= bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package change in both directions -exit_if_no_isotp_module() - -bSucc = False -rSucc = False - -isoTpSocket0 = ISOTPNativeSocket(iface0, sid=0x241, did=0x641) -isoTpSocket1 = ISOTPNativeSocket(iface1, sid=0x641, did=0x241) -bSocket0 = ISOTPNativeSocket(iface0, sid=0x641, did=0x241) -bSocket1 = ISOTPNativeSocket(iface1, sid=0x241, did=0x641) - -bridgeStarted = threading.Event() -def bridge(): - global bridgeStarted - global bSucc - def forwarding(pkt): - pkt.data = 'changed' - return pkt - bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=0.5, started_callback=bridgeStarted.set) - bSocket0.close() - bSocket1.close() - bSucc = True - -def RequestBothVCans(): - global rSucc - packetVcan0 = ISOTP(b'RequestVcan0') - packetVcan1 = ISOTP(b'RequestVcan1') - isoTpSocket0.send(packetVcan0) - isoTpSocket1.send(packetVcan1) - rSucc = True - -threadBridge = threading.Thread(target=bridge) -threadBridge.start() -threadSender = threading.Thread(target=RequestBothVCans) -bridgeStarted.wait(timeout=5) - -packetsVCan0 = isoTpSocket0.sniff(timeout=0.5, started_callback=threadSender.start) -packetsVCan1 = isoTpSocket1.sniff(timeout=0.5) - -packetsVCan0[0].data = b'changed' -assert len(packetsVCan0) == 1 -packetsVCan1[0].data = b'changed' -assert len(packetsVCan1) == 1 - -isoTpSocket0.close() -isoTpSocket1.close() - -threadSender.join(timeout=5) -threadBridge.join(timeout=5) - -assert(bSucc) -assert(rSucc) - -+ Cleanup - -= Cleanup reference to ISOTPSoftSocket to let the thread end -s = None - - -= Delete vcan interfaces -~ vcan_socket needs_root linux - -if 0 != call(["sudo", "ip", "link", "delete", iface0]): - raise Exception("%s could not be deleted" % iface0) - -if 0 != call(["sudo", "ip", "link", "delete", iface1]): - raise Exception("%s could not be deleted" % iface1) diff --git a/test/contrib/isotp_message_builder.uts b/test/contrib/isotp_message_builder.uts new file mode 100644 index 00000000000..8856f88f50c --- /dev/null +++ b/test/contrib/isotp_message_builder.uts @@ -0,0 +1,260 @@ +% Regression tests for ISOTP Message Builder + ++ Configuration +~ conf + += Definition of utility functions + +# hexadecimal to bytes convenience function +dhex = bytes.fromhex + += Import isotp + +conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} + +load_layer("can", globals_dict=globals()) +load_contrib("isotp", globals_dict=globals()) + + ++ Testing ISOTPMessageBuilder + += Create ISOTPMessageBuilder +m = ISOTPMessageBuilder() + += Feed packets to machine +ff = CAN(identifier=0x241, data=dhex("10 28 01 02 03 04 05 06")) +ff.time = 1000 +m.feed(ff) +m.feed(CAN(identifier=0x641, data=dhex("30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("21 07 08 09 0A 0B 0C 0D"))) +m.feed(CAN(identifier=0x241, data=dhex("22 0E 0F 10 11 12 13 14"))) +m.feed(CAN(identifier=0x241, data=dhex("23 15 16 17 18 19 1A 1B"))) +m.feed(CAN(identifier=0x641, data=dhex("30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("24 1C 1D 1E 1F 20 21 22"))) +m.feed(CAN(identifier=0x241, data=dhex("25 23 24 25 26 27 28" ))) + += Verify there is a ready message in the machine +assert m.count == 1 + += Extract the message from the machine +msg = m.pop() +assert m.count == 0 +assert msg.rx_id == 0x241 +assert msg.rx_ext_address is None +assert msg.tx_id == 0x641 +assert msg.ext_address is None +assert msg.time == 1000 +expected = dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28") +assert msg.data == expected + += Verify that no error happens when there is not enough data +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("04 AB CD EF"))) +msg = m.pop() +assert msg is None + += Verify that no error happens when there is no data +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex(""))) +msg = m.pop() +assert msg is None + += Verify a single frame without EA +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("04 AB CD EF 04"))) +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address is None +assert msg.data == dhex("AB CD EF 04") + += Single frame without EA, with excessive bytes in CAN frame +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("03 AB CD EF AB CD EF AB"))) +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address is None +assert msg.data == dhex("AB CD EF") + += Verify a single frame with EA +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("E2 04 01 02 03 04"))) +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xE2 +assert msg.data == dhex("01 02 03 04") + += Single CAN frame that has 2 valid interpretations +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("04 01 02 03 04"))) +msg = m.pop(0x241, None) +assert msg.rx_id == 0x241 +assert msg.rx_ext_address is None +assert msg.data == dhex("01 02 03 04") +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0x04 +assert msg.data == dhex("02") + += Verify multiple frames with EA +m = ISOTPMessageBuilder() +ff = CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")) +ff.time = 1005 +m.feed(ff) +m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17"))) +m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))) +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xEA +assert msg.tx_id == 0x641 +assert msg.ext_address == 0xEA +assert msg.time == 1005 +assert msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28") + += Verify multiple frames with EA 2 +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05"))) +m.feed(CAN(identifier=0x641, data=dhex("AE 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17"))) +m.feed(CAN(identifier=0x641, data=dhex("AE 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))) +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xEA +assert msg.tx_id == 0x641 +assert msg.ext_address == 0xAE +assert msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28") + += Verify that an EA starting with 1 will still work +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("1A 10 14 01 02 03 04 05"))) +m.feed(CAN(identifier=0x641, data=dhex("1A 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("1A 21 06 07 08 09 0A 0B"))) +m.feed(CAN(identifier=0x241, data=dhex("1A 22 0C 0D 0E 0F 10 11"))) +m.feed(CAN(identifier=0x241, data=dhex("1A 23 12 13 14 15 16 17"))) +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0x1A +assert msg.tx_id == 0x641 +assert msg.ext_address == 0x1A +assert msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14") + += Verify that an EA of 07 will still work +m = ISOTPMessageBuilder() +m.feed(CAN(identifier=0x241, data=dhex("07 10 0A 01 02 03 04 05"))) +m.feed(CAN(identifier=0x641, data=dhex("07 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("07 21 06 07 08 09 0A 0B"))) +msg = m.pop(0x241, 0x07) +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0x07 +assert msg.tx_id == 0x641 +assert msg.ext_address == 0x07 +assert msg.data == dhex("01 02 03 04 05 06 07 08 09 0A") + += Verify that three interleaved messages can be sniffed simultaneously on the same identifier and extended address (very unrealistic) +m = ISOTPMessageBuilder() +ff = CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")) +ff.time = 300 +m.feed(ff) # start of message A +m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17"))) +ff = CAN(identifier=0x241, data=dhex("EA 10 10 31 32 33 34 35")) +ff.time = 400 +m.feed(ff) # start of message B +m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) +sf = CAN(identifier=0x241, data=dhex("EA 03 A6 A7 A8" )) +sf.time = 200 +m.feed(sf) # single-frame message C +m.feed(CAN(identifier=0x641, data=dhex("EA 30 03 00" ))) +m.feed(CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 21 36 37 38 39 3A 3B"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 22 3C 3D 3E 3F 40" ))) # end of message B +m.feed(CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23"))) +m.feed(CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))) # end of message A +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xEA +assert msg.data == dhex("A6 A7 A8") +assert msg.time == 200 +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xEA +assert msg.tx_id == 0x641 +assert msg.ext_address == 0xEA +assert msg.time == 400 +assert msg.data == dhex("31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40") +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xEA +assert msg.tx_id == 0x641 +assert msg.ext_address == 0xEA +assert msg.time == 300 +assert msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28") + + += Verify multiple frames with EA from list +m = ISOTPMessageBuilder() +msgs = [ + CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")), + CAN(identifier=0x641, data=dhex("EA 30 03 00" )), + CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B")), + CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11")), + CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17")), + CAN(identifier=0x641, data=dhex("EA 30 03 00" )), + CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D")), + CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23")), + CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" ))] +m.feed(msgs) +msg = m.pop() +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xEA +assert msg.tx_id == 0x641 +assert msg.ext_address == 0xEA +assert msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28") + += Verify multiple frames with EA from list and iterator +m = ISOTPMessageBuilder() +msgs = [ + CAN(identifier=0x241, data=dhex("EA 10 28 01 02 03 04 05")), + CAN(identifier=0x641, data=dhex("EA 30 03 00" )), + CAN(identifier=0x241, data=dhex("EA 21 06 07 08 09 0A 0B")), + CAN(identifier=0x241, data=dhex("EA 22 0C 0D 0E 0F 10 11")), + CAN(identifier=0x241, data=dhex("EA 23 12 13 14 15 16 17")), + CAN(identifier=0x641, data=dhex("EA 30 03 00" )), + CAN(identifier=0x241, data=dhex("EA 24 18 19 1A 1B 1C 1D")), + CAN(identifier=0x241, data=dhex("EA 25 1E 1F 20 21 22 23")), + CAN(identifier=0x241, data=dhex("EA 26 24 25 26 27 28" )), + CAN(identifier=0x241, data=dhex("EA 03 A6 A7 A8" )), + CAN(identifier=0x241, data=dhex("EA 03 A6 A7 A8"))] +m.feed(msgs) +assert m.count == 3 +assert len(m) == 3 + +isotpmsgs = [x for x in m] +assert len(isotpmsgs) == 3 +msg = isotpmsgs[0] +assert msg.rx_id == 0x241 +assert msg.rx_ext_address == 0xEA +assert msg.tx_id == 0x641 +assert msg.ext_address == 0xEA +assert msg.data == dhex("01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28") + +assert isotpmsgs[1] == isotpmsgs[2] + += Verify a single frame without EA and different basecls +m = ISOTPMessageBuilder(basecls=Raw) +m.feed(CAN(identifier=0x241, data=dhex("04 AB CD EF 04"))) +msg = m.pop() +assert msg.load == dhex("AB CD EF 04") +assert type(msg) == Raw diff --git a/test/contrib/isotp_native_socket.uts b/test/contrib/isotp_native_socket.uts new file mode 100644 index 00000000000..4a61c2e41b1 --- /dev/null +++ b/test/contrib/isotp_native_socket.uts @@ -0,0 +1,690 @@ +% Regression tests for ISOTPNativeSocket +~ automotive_comm + ++ Configuration +~ conf + += Imports + +with open(scapy_path("test/contrib/automotive/interface_mockup.py")) as f: + exec(f.read()) + += Definition of constants, utility functions and mock classes + +# hexadecimal to bytes convenience function +dhex = bytes.fromhex + ++ Compatibility with can-isotp linux kernel modules + += Compatibility with isotpsend +exit_if_no_isotp_module() + +message = "01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14" + +with new_can_socket0() as isocan, ISOTPSoftSocket(isocan, tx_id=0x642, rx_id=0x242) as s: + p = subprocess.Popen(["isotpsend", "-s", "242", "-d", "642", iface0], stdin=subprocess.PIPE, universal_newlines=True) + p.communicate(message) + r = p.returncode + assert r == 0 + isotp = s.recv() + assert isotp.data == dhex(message) + + += Compatibility with isotpsend - extended addresses +exit_if_no_isotp_module() +message = "01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14" + +with new_can_socket0() as isocan, ISOTPSoftSocket(isocan, tx_id=0x644, rx_id=0x244, ext_address=0xaa, rx_ext_address=0xee) as s: + p = subprocess.Popen(["isotpsend", "-s", "244", "-d", "644", "-x", "ee:aa", iface0], stdin=subprocess.PIPE, universal_newlines=True) + p.communicate(message) + r = p.returncode + assert r == 0 + isotp = s.recv() + assert isotp.data == dhex(message) + + += Compatibility with isotprecv +exit_if_no_isotp_module() + +isotp = ISOTP(data=bytearray(range(1,20))) +p = subprocess.Popen(["isotprecv", "-s", "243", "-d", "643", "-b", "3", iface0], stdout=subprocess.PIPE) +time.sleep(0.1) +with new_can_socket0() as isocan, ISOTPSoftSocket(isocan, tx_id=0x643, rx_id=0x243) as s: + s.send(isotp) + +timer = threading.Timer(1, lambda: p.terminate() if p.poll() else p.wait()) +timer.start() # Timeout the receiver after 1 second +r = p.wait() +assert 0 == r + +result = None +for i in range(10): + time.sleep(0.1) + if p.poll() is not None: + result = p.stdout.readline().decode().strip() + break + +assert result is not None +result_data = dhex(result) +assert result_data == isotp.data + +timer.join(5) +assert not timer.is_alive() + + += Compatibility with isotprecv - extended addresses +exit_if_no_isotp_module() +isotp = ISOTP(data=bytearray(range(1,20))) +cmd = ["isotprecv", "-s245", "-d645", "-b3", "-x", "ee:aa", iface0] +p = subprocess.Popen(cmd, stdout=subprocess.PIPE) +time.sleep(0.1) # Give some time for starting reception +with new_can_socket0() as isocan, ISOTPSoftSocket(isocan, tx_id=0x645, rx_id=0x245, ext_address=0xaa, rx_ext_address=0xee) as s: + s.send(isotp) + +timer = threading.Timer(1, lambda: p.terminate() if p.poll() else p.wait()) +timer.start() # Timeout the receiver after 1 second +r = p.wait() +assert 0 == r + +result = None +for i in range(10): + time.sleep(0.1) + if p.poll() is not None: + result = p.stdout.readline().decode().strip() + break + +assert result is not None +result_data = dhex(result) +assert result_data == isotp.data + +timer.join(5) +assert not timer.is_alive() + += Compatibility ISOTPSoftSocket ISOTPNativeSocket various configs +exit_if_no_isotp_module() + +message = "01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14" * 5 + +kwargs = [({"tx_id": 0x242, "rx_id": 0x642, "bs": 0, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 2, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 5, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 0, "stmin": 5, "padding": False, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 0, "stmin": 10, "padding": False, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 4, "stmin": 130, "padding": False, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": None, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 3, "stmin": 0, "padding": True, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": True, "ext_address": None, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 0, "stmin": 0, "padding": False, "ext_address": 0xfe, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": 0xfe, "rx_ext_address": None}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 0, "stmin": 0, "padding": False, "ext_address": 0xfe, "rx_ext_address": 0xef}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 0, "padding": False, "ext_address": 0xef, "rx_ext_address": 0xfe}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 6, "stmin": 10, "padding": True, "ext_address": 0x12, "rx_ext_address": 0x23}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 6, "stmin": 5, "padding": True, "ext_address": 0x23, "rx_ext_address": 0x12}), + ({"tx_id": 0x242, "rx_id": 0x642, "bs": 0, "stmin": 0, "padding": True, "ext_address": 0x45, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x242, "bs": 0, "stmin": 40, "padding": True, "ext_address": 0x45, "rx_ext_address": None}), + ({"tx_id": 0x123, "rx_id": 0x642, "bs": 1, "stmin": 1, "padding": False, "ext_address": None, "rx_ext_address": None}, + {"tx_id": 0x642, "rx_id": 0x123, "bs": 1, "stmin": 1, "padding": False, "ext_address": None, "rx_ext_address": None}),] + +for kwargs1, kwargs2 in kwargs: + print("Testing config %s, %s" % (kwargs1, kwargs2)) + with NativeCANSocket(iface0) as cs: + cs.sniff(timeout=0.01) + with ISOTPSoftSocket(iface0, **kwargs1) as s, ISOTPNativeSocket(iface0, **kwargs2) as ns: + ns.send(ISOTP(bytes.fromhex(message))) + isotp = s.recv() + assert isotp.data == dhex(message) + ns.send(ISOTP(bytes.fromhex("00 11 22"))) + isotp = s.recv() + assert (isotp.data == dhex("00 11 22")) + pks1 = cs.sniff(timeout=0.01) + with ISOTPNativeSocket(iface0, **kwargs1) as s, ISOTPSoftSocket(iface0, **kwargs2) as ns: + ns.send(ISOTP(bytes.fromhex(message))) + isotp = s.recv() + assert isotp.data == dhex(message) + ns.send(ISOTP(bytes.fromhex("00 11 22"))) + isotp = s.recv() + assert (isotp.data == dhex("00 11 22")) + pks2 = cs.sniff(timeout=0.01) + assert len(pks1) == len(pks2) and len(pks2) > 0 + for p1, p2 in zip(pks1, pks2): + assert bytes(p1) == bytes(p2) + + ++ ISOTPNativeSocket tests + += Create ISOTP socket +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) + += Send single frame ISOTP message +exit_if_no_isotp_module() + +with new_can_socket(iface0) as cans: + s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) + s.send(ISOTP(data=dhex("01 02 03 04 05"))) + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x641 + assert can.data == dhex("05 01 02 03 04 05") + + += Send single frame ISOTP message Test init with CANSocket +exit_if_no_isotp_module() +cans = CANSocket(iface0) +s = ISOTPNativeSocket(cans, tx_id=0x641, rx_id=0x241) +s.send(ISOTP(data=dhex("01 02 03 04 05"))) +can = cans.sniff(timeout=1, count=1)[0] +assert can.identifier == 0x641 +assert can.data == dhex("05 01 02 03 04 05") +cans.close() + + += Test init with wrong type +exit_if_no_isotp_module() +exception_catched = False +try: + s = ISOTPNativeSocket(42, tx_id=0x641, rx_id=0x241) +except Scapy_Exception: + exception_catched = True + +assert exception_catched + += Send two-frame ISOTP message +exit_if_no_isotp_module() + +evt = threading.Event() +def acker(): + with new_can_socket(iface0) as cans: + evt.set() + can = cans.sniff(timeout=1, count=1)[0] + cans.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) + + +with new_can_socket(iface0) as cans: + t = Thread(target=acker) + t.start() + s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) + evt.wait(timeout=5) + s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 08 01 02 03 04 05 06") + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 00") + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 07 08") + t.join(timeout=5) + assert not t.is_alive() + += Send a single frame ISOTP message with padding +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, padding=True) + +with new_can_socket(iface0) as cans: + s.send(ISOTP(data=dhex("01"))) + can = cans.sniff(timeout=1, count=1)[0] + assert can.length == 8 + + += Send a two-frame ISOTP message with padding +exit_if_no_isotp_module() + +acker_ready = threading.Event() +def acker(): + with new_can_socket(iface0) as acks: + acker_ready.set() + can = acks.sniff(timeout=1, count=1)[0] + acks.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) + +with new_can_socket(iface0) as cans: + thread = Thread(target=acker) + thread.start() + acker_ready.wait(timeout=5) + s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, padding=True) + s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 08 01 02 03 04 05 06") + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 00") + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 07 08 CC CC CC CC CC") + thread.join(5) + assert not thread.is_alive() + + += Receive a padded single frame ISOTP message with padding disabled +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, padding=False) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) + res = s.recv() + assert res.data == dhex("05 06") + + += Receive a padded single frame ISOTP message with padding enabled +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, padding=True) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) + res = s.recv() + assert res.data == dhex("05 06") + + += Receive a non-padded single frame ISOTP message with padding enabled +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, padding=True) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier=0x241, data=dhex("02 05 06"))) + res = s.recv() + assert res.data == dhex("05 06") + + += Receive a padded two-frame ISOTP message with padding enabled +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, padding=True) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) + cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) + res = s.recv() + assert res.data == dhex("01 02 03 04 05 06 07 08 09") + + += Receive a padded two-frame ISOTP message with padding disabled +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, padding=False) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) + cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) + res = s.recv() + assert res.data == dhex("01 02 03 04 05 06 07 08 09") + + += Receive a single frame ISOTP message +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier = 0x241, data = dhex("05 01 02 03 04 05"))) + isotp = s.recv() + assert isotp.data == dhex("01 02 03 04 05") + assert isotp.tx_id == 0x641 + assert isotp.rx_id == 0x241 + assert isotp.ext_address == None + assert isotp.rx_ext_address == None + + += Receive a single frame ISOTP message, with extended addressing +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, ext_address=0xc0, rx_ext_address=0xea) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier = 0x241, data = dhex("EA 05 01 02 03 04 05"))) + isotp = s.recv() + assert isotp.data == dhex("01 02 03 04 05") + assert isotp.tx_id == 0x641 + assert isotp.rx_id == 0x241 + assert isotp.ext_address == 0xc0 + assert isotp.rx_ext_address == 0xea + + += Receive a two-frame ISOTP message +exit_if_no_isotp_module() +s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) +with new_can_socket(iface0) as cans: + cans.send(CAN(identifier = 0x241, data = dhex("10 0B 01 02 03 04 05 06"))) + cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 10 11"))) + isotp = s.recv() + assert isotp.data == dhex("01 02 03 04 05 06 07 08 09 10 11") + += Receive a two-frame ISOTP message and test python with statement +exit_if_no_isotp_module() +with ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) as s: + with new_can_socket(iface0) as cans: + cans.send(CAN(identifier = 0x241, data = dhex("10 0B 01 02 03 04 05 06"))) + cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 10 11"))) + isotp = s.recv() + assert isotp.data == dhex("01 02 03 04 05 06 07 08 09 10 11") + + += Send single CANFD frame ISOTP message +exit_if_no_isotp_module() + +with new_can_socket(iface0, fd=True) as cans: + s = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241, fd=True) + s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08 09"))) + can = cans.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x641 + assert can.data == dhex("09 01 02 03 04 05 06 07 08 09") + + += ISOTP Socket sr1 test +exit_if_no_isotp_module() + +txSock = ISOTPNativeSocket(iface0, tx_id=0x123, rx_id=0x321, basecls=ISOTP) +txmsg = ISOTP(b'\x11\x22\x33') +rx2 = None + +receiver_up = Event() + +def sender(): + global receiver_up + receiver_up.wait(timeout=5) + global txmsg + global rx2 + rx2 = txSock.sr1(txmsg, timeout=1, verbose=True) + +def receiver(): + global receiver_up + with new_can_socket(iface0) as cans: + rx = cans.sniff(timeout=1, count=1, started_callback=receiver_up.set)[0] + cans.send(CAN(identifier=0x321, length=4, data=b'\x03\x7f\x22\x33')) + expectedrx = CAN(identifier=0x123, length=4, data=b'\x03\x11\x22\x33') + assert rx.length == expectedrx.length + assert rx.data == expectedrx.data + assert rx.identifier == expectedrx.identifier + +txThread = threading.Thread(target=sender) +txThread.start() +receiver() +txThread.join(timeout=5) +assert not txThread.is_alive() + +assert rx2 is not None +assert rx2 == ISOTP(b'\x7f\x22\x33') +assert rx2.answers(txmsg) + += ISOTP Socket sr1 and ISOTP test +exit_if_no_isotp_module() +txSock = ISOTPNativeSocket(iface0, 0x123, 0x321, basecls=ISOTP) +rxSock = ISOTPNativeSocket(iface0, 0x321, 0x123, basecls=ISOTP) +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') +rx2 = None + +receiver_up = Event() + +def sender(): + receiver_up.wait(timeout=5) + global rx2 + rx2 = txSock.sr1(msg, timeout=1, verbose=True) + +def receiver(): + global rx + receiver_up.set() + rx = rxSock.recv() + rxSock.send(msg) + +txThread = threading.Thread(target=sender) +txThread.start() +receiver() +txThread.join(timeout=5) +assert not txThread.is_alive() + +assert rx == msg +assert rxSock.send(msg) +assert rx2 is not None +assert rx2 == msg + += ISOTP Socket sr1 and ISOTP test vice versa +exit_if_no_isotp_module() + +rxSock = ISOTPNativeSocket(iface0, 0x321, 0x123, basecls=ISOTP) +txSock = ISOTPNativeSocket(iface0, 0x123, 0x321, basecls=ISOTP) + +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + +receiver_up = Event() + +def receiver(): + global rx2, sent + rx2 = rxSock.sniff(count=1, timeout=1, started_callback=receiver_up.set) + sent = rxSock.send(msg) + +def sender(): + global rx + receiver_up.wait(timeout=5) + rx = txSock.sr1(msg, timeout=1,verbose=True) + +rx2 = None +sent = False +rxThread = threading.Thread(target=receiver) +rxThread.start() +sender() +rxThread.join(timeout=5) +assert not rxThread.is_alive() + +assert rx == msg +assert rx2[0] == msg +assert sent + += ISOTP Socket sniff +exit_if_no_isotp_module() + +rxSock = ISOTPNativeSocket(iface0, 0x321, 0x123, basecls=ISOTP) +txSock = ISOTPNativeSocket(iface0, 0x123, 0x321, basecls=ISOTP) +succ = False + +receiver_up = Event() + +def receiver(): + rx = rxSock.sniff(count=5, timeout=1, started_callback=receiver_up.set) + msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + msg.data += b'0' + assert rx[0] == msg + msg.data += b'1' + assert rx[1] == msg + msg.data += b'2' + assert rx[2] == msg + msg.data += b'3' + assert rx[3] == msg + msg.data += b'4' + assert rx[4] == msg + global succ + succ = True + +def sender(): + receiver_up.wait(timeout=5) + msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + msg.data += b'0' + assert txSock.send(msg) + msg.data += b'1' + assert txSock.send(msg) + msg.data += b'2' + assert txSock.send(msg) + msg.data += b'3' + assert txSock.send(msg) + msg.data += b'4' + assert txSock.send(msg) + +rxThread = threading.Thread(target=receiver) +rxThread.start() +sender() +rxThread.join(timeout=5) +assert not rxThread.is_alive() + +assert succ + ++ ISOTPNativeSocket MITM attack tests +~ vcan_socket needs_root linux + += bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package forwarding vcan1 +exit_if_no_isotp_module() + +isoTpSocket0 = ISOTPNativeSocket(iface0, tx_id=0x241, rx_id=0x641) +isoTpSocket1 = ISOTPNativeSocket(iface1, tx_id=0x641, rx_id=0x241) +bSocket0 = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) +bSocket1 = ISOTPNativeSocket(iface1, tx_id=0x241, rx_id=0x641) + +bridgeStarted = threading.Event() +def bridge(): + global bridgeStarted + def forwarding(pkt): + return pkt + bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=2, count=1, started_callback=bridgeStarted.set) + bSocket0.close() + bSocket1.close() + global bSucc + bSucc = True + +bSucc = False + +threadBridge = threading.Thread(target=bridge) +threadBridge.start() +bridgeStarted.wait(timeout=5) +isoTpSocket0.send(ISOTP(b'Request')) +packetsVCan1 = isoTpSocket1.sniff(timeout=0.5, count=1) + +assert len(packetsVCan1) == 1 + +isoTpSocket0.close() +isoTpSocket1.close() + +threadBridge.join(timeout=5) +assert not threadBridge.is_alive() + +assert bSucc + += bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package change to vcan1 +exit_if_no_isotp_module() + +isoTpSocket0 = ISOTPNativeSocket(iface0, tx_id=0x241, rx_id=0x641) +isoTpSocket1 = ISOTPNativeSocket(iface1, tx_id=0x641, rx_id=0x241) +bSocket0 = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) +bSocket1 = ISOTPNativeSocket(iface1, tx_id=0x241, rx_id=0x641) + +bSucc = False + +bridgeStarted = threading.Event() +def bridge(): + global bridgeStarted + global bSucc + def forwarding(pkt): + pkt.data = 'changed' + return pkt + bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=0.5, started_callback=bridgeStarted.set) + bSocket0.close() + bSocket1.close() + bSucc = True + +threadBridge = threading.Thread(target=bridge) +threadBridge.start() +bridgeStarted.wait(timeout=5) +isoTpSocket0.send(ISOTP(b'Request')) +packetsVCan1 = isoTpSocket1.sniff(timeout=0.5, count=1) + +packetsVCan1[0].data = b'changed' +assert len(packetsVCan1) == 1 + +isoTpSocket0.close() +isoTpSocket1.close() + +threadBridge.join(timeout=5) +assert not threadBridge.is_alive() + +assert bSucc + += bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package forwarding in both directions +exit_if_no_isotp_module() + +bSucc = False + +isoTpSocket0 = ISOTPNativeSocket(iface0, tx_id=0x241, rx_id=0x641) +isoTpSocket1 = ISOTPNativeSocket(iface1, tx_id=0x641, rx_id=0x241) +bSocket0 = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) +bSocket1 = ISOTPNativeSocket(iface1, tx_id=0x241, rx_id=0x641) + +bridgeStarted = threading.Event() +def bridge(): + global bridgeStarted + global bSucc + def forwarding(pkt): + return pkt + bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=0.5, started_callback=bridgeStarted.set, count=2) + bSocket0.close() + bSocket1.close() + bSucc = True + +threadBridge = threading.Thread(target=bridge) +threadBridge.start() +bridgeStarted.wait(timeout=5) + +packetVcan0 = ISOTP(b'RequestVcan0') +packetVcan1 = ISOTP(b'RequestVcan1') +isoTpSocket0.send(packetVcan0) +isoTpSocket1.send(packetVcan1) + +packetsVCan0 = isoTpSocket0.sniff(timeout=0.5, count=1) +packetsVCan1 = isoTpSocket1.sniff(timeout=0.5, count=1) + +len(packetsVCan0) == 1 +len(packetsVCan1) == 1 + +isoTpSocket0.close() +isoTpSocket1.close() + +threadBridge.join(timeout=5) +assert not threadBridge.is_alive() + +assert bSucc + += bridge and sniff with isotp native sockets set up vcan0 and vcan1 for package change in both directions +exit_if_no_isotp_module() + +bSucc = False + +isoTpSocket0 = ISOTPNativeSocket(iface0, tx_id=0x241, rx_id=0x641) +isoTpSocket1 = ISOTPNativeSocket(iface1, tx_id=0x641, rx_id=0x241) +bSocket0 = ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) +bSocket1 = ISOTPNativeSocket(iface1, tx_id=0x241, rx_id=0x641) + +bridgeStarted = threading.Event() +def bridge(): + global bridgeStarted + global bSucc + def forwarding(pkt): + pkt.data = 'changed' + return pkt + bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=0.5, started_callback=bridgeStarted.set, count=2) + bSocket0.close() + bSocket1.close() + bSucc = True + +threadBridge = threading.Thread(target=bridge) +threadBridge.start() +bridgeStarted.wait(timeout=5) + +packetVcan0 = ISOTP(b'RequestVcan0') +packetVcan1 = ISOTP(b'RequestVcan1') +isoTpSocket0.send(packetVcan0) +isoTpSocket1.send(packetVcan1) + +packetsVCan0 = isoTpSocket0.sniff(timeout=0.5, count=1) +packetsVCan1 = isoTpSocket1.sniff(timeout=0.5, count=1) + +packetsVCan0[0].data = b'changed' +assert len(packetsVCan0) == 1 +packetsVCan1[0].data = b'changed' +assert len(packetsVCan1) == 1 + +isoTpSocket0.close() +isoTpSocket1.close() + +threadBridge.join(timeout=5) +assert not threadBridge.is_alive() + +assert bSucc + ++ Cleanup + += Cleanup reference to ISOTPSoftSocket to let the thread end +s = None + += Delete vcan interfaces + +assert cleanup_interfaces() diff --git a/test/contrib/isotp_packet.uts b/test/contrib/isotp_packet.uts new file mode 100644 index 00000000000..e0225eb09ef --- /dev/null +++ b/test/contrib/isotp_packet.uts @@ -0,0 +1,514 @@ +% Regression tests for ISOTP packet definitions + ++ Configuration +~ conf + += Import isotp + +conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': False} + +load_layer("can", globals_dict=globals()) +load_contrib("isotp", globals_dict=globals()) +from scapy.contrib.isotp.isotp_scanner import get_isotp_packet + += Define helpers + +# hexadecimal to bytes convenience function +dhex = bytes.fromhex + + ++ ISOTP packet check + += Creation of an empty ISOTP packet +p = ISOTP() +assert p.data == b"" +assert p.tx_id is None and p.rx_id is None and p.ext_address is None and p.rx_ext_address is None +assert bytes(p) == b"" + += Creation of a simple ISOTP packet with tx_id +p = ISOTP(b"eee", tx_id=0x241) +assert p.tx_id == 0x241 +assert p.data == b"eee" +assert bytes(p) == b"eee" + += Creation of a simple ISOTP packet with ext_address +p = ISOTP(b"eee", ext_address=0x41) +assert p.ext_address == 0x41 +assert p.data == b"eee" +assert bytes(p) == b"eee" + += Creation of a simple ISOTP packet with rx_id +p = ISOTP(b"eee", rx_id=0x241) +assert p.rx_id == 0x241 +assert p.data == b"eee" +assert bytes(p) == b"eee" + += Creation of a simple ISOTP packet with rx_ext_address +p = ISOTP(b"eee", rx_ext_address=0x41) +assert p.rx_ext_address == 0x41 +assert p.data == b"eee" +assert bytes(p) == b"eee" + += Creation of a simple ISOTP packet with tx_id, rx_id, ext_address, rx_ext_address +p = ISOTP(b"eee", tx_id=1, rx_id=2, ext_address=3, rx_ext_address=4) +assert p.rx_id == 2 +assert p.rx_ext_address == 4 +assert p.tx_id == 1 +assert p.ext_address == 3 +assert p.data == b"eee" +assert bytes(p) == b"eee" + += ISOTP answers test +p = ISOTP() +r = ISOTP() +assert p.data == b"" +assert p.answers(r) +assert not p.answers(Raw()) + + += Creation of a simple ISOTP packet with tx_id validation error +ex = False +try: + p = ISOTP(b"eee", tx_id=0x1000000000, rx_id=2, ext_address=3, rx_ext_address=4) +except Scapy_Exception: + ex = True + +assert ex + += Creation of a simple ISOTP packet with rx_id validation error +ex = False +try: + p = ISOTP(b"eee", tx_id=0x10, rx_id=0x20000000000, ext_address=3, rx_ext_address=4) +except Scapy_Exception: + ex = True + +assert ex + += Creation of a simple ISOTP packet with ext_address validation error +ex = False +try: + p = ISOTP(b"eee", tx_id=0x10, rx_id=2, ext_address=3000, rx_ext_address=4) +except Scapy_Exception: + ex = True + +assert ex + + += Creation of a simple ISOTP packet with rx_ext_address validation error +ex = False +try: + p = ISOTP(b"eee", tx_id=0x10, rx_id=2, ext_address=30, rx_ext_address=400) +except Scapy_Exception: + ex = True + +assert ex + ++ ISOTPFrame related checks + += Build a packet with extended addressing +pkt = CAN(identifier=0x123, data=b'\x42\x10\xff\xde\xea\xdd\xaa\xaa') +isotpex = ISOTPHeaderEA(bytes(pkt)) +assert isotpex.type == 1 +assert isotpex.message_size == 0xff +assert isotpex.extended_address == 0x42 +assert isotpex.identifier == 0x123 +assert isotpex.length == 8 + += Build a packet with normal addressing +pkt = CAN(identifier=0x123, data=b'\x10\xff\xde\xea\xdd\xaa\xaa') +isotpno = ISOTPHeader(bytes(pkt)) +assert isotpno.type == 1 +assert isotpno.message_size == 0xff +assert isotpno.identifier == 0x123 +assert isotpno.length == 7 + += Compare both isotp payloads +assert isotpno.data == isotpex.data +assert isotpno.message_size == isotpex.message_size + += Dissect multiple packets +frames = \ + [b'\x00\x00\x00\x00\x08\x00\x00\x00\x10(\xde\xad\xbe\xef\xde\xad', + b'\x00\x00\x00\x00\x08\x00\x00\x00!\xbe\xef\xde\xad\xbe\xef\xde', + b'\x00\x00\x00\x00\x08\x00\x00\x00"\xad\xbe\xef\xde\xad\xbe\xef', + b'\x00\x00\x00\x00\x08\x00\x00\x00#\xde\xad\xbe\xef\xde\xad\xbe', + b'\x00\x00\x00\x00\x08\x00\x00\x00$\xef\xde\xad\xbe\xef\xde\xad', + b'\x00\x00\x00\x00\x07\x00\x00\x00%\xbe\xef\xde\xad\xbe\xef'] + +isotpframes = [ISOTPHeader(x) for x in frames] + +assert isotpframes[0].type == 1 +assert isotpframes[0].message_size == 40 +assert isotpframes[0].length == 8 +assert isotpframes[1].type == 2 +assert isotpframes[1].index == 1 +assert isotpframes[1].length == 8 +assert isotpframes[2].type == 2 +assert isotpframes[2].index == 2 +assert isotpframes[2].length == 8 +assert isotpframes[3].type == 2 +assert isotpframes[3].index == 3 +assert isotpframes[3].length == 8 +assert isotpframes[4].type == 2 +assert isotpframes[4].index == 4 +assert isotpframes[4].length == 8 +assert isotpframes[5].type == 2 +assert isotpframes[5].index == 5 +assert isotpframes[5].length == 7 + += Build SF frame with constructor, check for correct length assignments +p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_SF(data=b'\xad\xbe\xad\xff'))) +assert p.length == 5 +assert p.message_size == 4 +assert len(p.data) == 4 +assert p.data == b'\xad\xbe\xad\xff' +assert p.type == 0 +assert p.identifier == 0 + += Build SF frame EA with constructor, check for correct length assignments +p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_SF(data=b'\xad\xbe\xad\xff'))) +assert p.extended_address == 0 +assert p.length == 6 +assert p.message_size == 4 +assert len(p.data) == 4 +assert p.data == b'\xad\xbe\xad\xff' +assert p.type == 0 +assert p.identifier == 0 + += Build FF frame with constructor, check for correct length assignments +p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FF(message_size=10, data=b'\xad\xbe\xad\xff'))) +assert p.length == 6 +assert p.message_size == 10 +assert len(p.data) == 4 +assert p.data == b'\xad\xbe\xad\xff' +assert p.type == 1 +assert p.identifier == 0 + += Build FF frame EA with constructor, check for correct length assignments +p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FF(message_size=10, data=b'\xad\xbe\xad\xff'))) +assert p.extended_address == 0 +assert p.length == 7 +assert p.message_size == 10 +assert len(p.data) == 4 +assert p.data == b'\xad\xbe\xad\xff' +assert p.type == 1 +assert p.identifier == 0 + += Build FF frame EA, extended size, with constructor, check for correct length assignments +p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FF_FD(message_size=2000, data=b'\xad'))) +assert p.extended_address == 0 +assert p.length == 8 +assert p.message_size == 2000 +assert len(p.data) == 1 +assert p.data == b'\xad' +assert p.type == 1 +assert p.identifier == 0 + += Build FF frame, extended size, with constructor, check for correct length assignments +p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FF_FD(message_size=2000, data=b'\xad'))) +assert p.length == 7 +assert p.message_size == 2000 +assert len(p.data) == 1 +assert p.data == b'\xad' +assert p.type == 1 +assert p.identifier == 0 + += Build CF frame with constructor, check for correct length assignments +p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_CF(data=b'\xad'))) +assert p.length == 2 +assert p.index == 0 +assert len(p.data) == 1 +assert p.data == b'\xad' +assert p.type == 2 +assert p.identifier == 0 + += Build CF frame EA with constructor, check for correct length assignments +p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_CF(data=b'\xad'))) +assert p.length == 3 +assert p.index == 0 +assert len(p.data) == 1 +assert p.data == b'\xad' +assert p.type == 2 +assert p.identifier == 0 + += Build FC frame EA with constructor, check for correct length assignments +p = ISOTPHeaderEA(bytes(ISOTPHeaderEA()/ISOTP_FC())) +assert p.length == 4 +assert p.block_size == 0 +assert p.separation_time == 0 +assert p.type == 3 +assert p.identifier == 0 + += Build FC frame with constructor, check for correct length assignments +p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FC())) +assert p.length == 3 +assert p.block_size == 0 +assert p.separation_time == 0 +assert p.type == 3 +assert p.identifier == 0 + += Construct some single frames +p = ISOTPHeader(identifier=0x123, length=5)/ISOTP_SF(message_size=4, data=b'abcd') +assert p.length == 5 +assert p.identifier == 0x123 +assert p.type == 0 +assert p.message_size == 4 +assert p.data == b'abcd' + += Construct some single frames EA +p = ISOTPHeaderEA(identifier=0x123, length=6, extended_address=42)/ISOTP_SF(message_size=4, data=b'abcd') +assert p.length == 6 +assert p.extended_address == 42 +assert p.identifier == 0x123 +assert p.type == 0 +assert p.message_size == 4 +assert p.data == b'abcd' + += Construct ISOTP_packet with extended can frame +p = get_isotp_packet(identifier=0x1234, extended=False, extended_can_id=True) +print(p) +assert (p.identifier == 0x1234) +assert (p.flags == "extended") + += Construct ISOTPEA_Packet with extended can frame +p = get_isotp_packet(identifier=0x1234, extended=True, extended_can_id=True) +print(p) +assert (p.identifier == 0x1234) +assert (p.flags == "extended") + ++ ISOTP fragment and defragment checks + += Fragment an empty ISOTP message +fragments = ISOTP().fragment() +assert len(fragments) == 1 +assert fragments[0].data == b"\0" + += Fragment another empty ISOTP message +fragments = ISOTP(b"").fragment() +assert len(fragments) == 1 +assert fragments[0].data == b"\0" + += Fragment a 4 bytes long ISOTP message +fragments = ISOTP(b"data", tx_id=0x241).fragment() +assert len(fragments) == 1 +assert isinstance(fragments[0], CAN) +fragment = CAN(bytes(fragments[0])) +assert fragment.data == b"\x04data" +assert fragment.flags == 0 +assert fragment.length == 5 +assert fragment.reserved == 0 + += Fragment a 4 bytes long ISOTP message extended +fragments = ISOTP(b"data", rx_id=0x1fff0000).fragment() +assert len(fragments) == 1 +assert isinstance(fragments[0], CAN) +fragment = CAN(bytes(fragments[0])) +assert fragment.data == b"\x04data" +assert fragment.length == 5 +assert fragment.reserved == 0 +assert fragment.flags == 4 + += Fragment a 8 bytes long ISOTP message extended +fragments = ISOTP(b"datadata", rx_id=0x1fff0000).fragment() +assert len(fragments) == 2 +assert isinstance(fragments[0], CAN) +fragment = CAN(bytes(fragments[0])) +assert fragment.data == b"\x10\x08datada" +assert fragment.length == 8 +assert fragment.reserved == 0 +assert fragment.flags == 4 +fragment = CAN(bytes(fragments[1])) +assert fragment.data == b"\x21ta" +assert fragment.length == 3 +assert fragment.reserved == 0 +assert fragment.flags == 4 + += Fragment a 7 bytes long ISOTP message +fragments = ISOTP(b"abcdefg").fragment() +assert len(fragments) == 1 +assert fragments[0].data == b"\x07abcdefg" + += Fragment a 8 bytes long ISOTP message +fragments = ISOTP(b"abcdefgh").fragment() +assert len(fragments) == 2 +assert fragments[0].data == b"\x10\x08abcdef" +assert fragments[1].data == b"\x21gh" + += Fragment an ISOTP message with extended addressing +isotp = ISOTP(b"abcdef", rx_ext_address=ord('A')) +fragments = isotp.fragment() +assert len(fragments) == 1 +assert fragments[0].data == b"A\x06abcdef" + += Fragment a 7 bytes ISOTP message with destination identifier +isotp = ISOTP(b"abcdefg", rx_id=0x64f) +fragments = isotp.fragment() +assert len(fragments) == 1 +assert fragments[0].data == b"\x07abcdefg" +assert fragments[0].identifier == 0x64f + += Fragment a 16 bytes ISOTP message with extended addressing +isotp = ISOTP(b"abcdefghijklmnop", rx_id=0x64f, rx_ext_address=ord('A')) +fragments = isotp.fragment() +assert len(fragments) == 3 +assert fragments[0].data == b"A\x10\x10abcde" +assert fragments[1].data == b"A\x21fghijk" +assert fragments[2].data == b"A\x22lmnop" +assert fragments[0].identifier == 0x64f +assert fragments[1].identifier == 0x64f +assert fragments[2].identifier == 0x64f + += Fragment a huge ISOTP message, 4997 bytes long +data = b"T" * 4997 +isotp = ISOTP(b"T" * 4997, rx_id=0x345) +fragments = isotp.fragment() +assert len(fragments) == 715 +assert fragments[0].data == dhex("10 00 00 00 13 85") + b"TT" +assert fragments[1].data == b"\x21TTTTTTT" +assert fragments[-2].data == b"\x29TTTTTTT" +assert fragments[-1].data == b"\x2ATTTT" + += Defragment a single-frame ISOTP message +fragments = [CAN(identifier=0x641, data=b"\x04test")] +isotp = ISOTP.defragment(fragments) +isotp.show() +assert isotp.data == b"test" +assert isotp.rx_id == 0x641 + += Defragment non ISOTP message +fragments = [CAN(identifier=0x641, data=b"\xa4test")] +isotp = ISOTP.defragment(fragments) +assert isotp is None + += Defragment ISOTP message with warning +fragments = [CAN(identifier=0x641, data=b"\x04test"), CAN(identifier=0x642, data=b"\x04test")] +isotp = ISOTP.defragment(fragments) +assert isotp.data == b"test" +assert isotp.rx_id == 0x641 + += Defragment exception +fragments = [] +ex = False +try: + isotp = ISOTP.defragment(fragments) + isotp.show() +except Scapy_Exception: + ex = True + +assert ex + += Defragment an ISOTP message composed of multiple CAN frames +fragments = [ + CAN(identifier=0x641, data=dhex("41 10 10 61 62 63 64 65")), + CAN(identifier=0x641, data=dhex("41 21 66 67 68 69 6A 6B")), + CAN(identifier=0x641, data=dhex("41 22 6C 6D 6E 6F 70 00")) +] +isotp = ISOTP.defragment(fragments) +isotp.show() +assert isotp.data == dhex("61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70") +assert isotp.rx_id == 0x641 +assert isotp.rx_ext_address == 0x41 + += Check if fragmenting a message and defragmenting it back yields the original message +isotp1 = ISOTP(b"abcdef", rx_ext_address=ord('A')) +fragments = isotp1.fragment() +isotp2 = ISOTP.defragment(fragments) +isotp2.show() +assert isotp1 == isotp2 + +isotp1 = ISOTP(b"abcdefghijklmnop") +fragments = isotp1.fragment() +isotp2 = ISOTP.defragment(fragments) +isotp2.show() +assert isotp1 == isotp2 + +isotp1 = ISOTP(b"abcdefghijklmnop", rx_ext_address=ord('A')) +fragments = isotp1.fragment() +isotp2 = ISOTP.defragment(fragments) +isotp2.show() +assert isotp1 == isotp2 + +isotp1 = ISOTP(b"T"*5000, rx_ext_address=ord('A')) +fragments = isotp1.fragment() +isotp2 = ISOTP.defragment(fragments) +isotp2.show() +assert isotp1 == isotp2 + += Defragment an ambiguous CAN frame +fragments = [CAN(identifier=0x641, data=dhex("02 01 AA"))] +isotp = ISOTP.defragment(fragments, False) +isotp.show() +assert isotp.data == dhex("01 AA") +assert isotp.rx_ext_address == None +isotpex = ISOTP.defragment(fragments, True) +isotpex.show() +assert isotpex.data == dhex("AA") +assert isotpex.rx_ext_address == 0x02 + += Build ISOTP_FF_FD + +pkt = ISOTP_FF_FD(message_size=0xffff0000) +assert bytes(pkt) == bytes.fromhex("1000ffff0000") + += Build ISOTP_SF_FD + +pkt = ISOTP_SF_FD(message_size=0xff) +assert bytes(pkt) == bytes.fromhex("00ff") + += Build ISOTP_FF_FD 2 + +pkt = ISOTPHeaderEA_FD(identifier=0x7ff, extended_address=0xaf)/ISOTP_FF_FD(message_size=0xffff0000) +assert bytes(pkt) == bytes.fromhex("000007ff 07 04 00 00 af 1000ffff0000") + += Build ISOTP_SF_FD 2 + +pkt = ISOTPHeaderEA_FD(identifier=0x7ff, extended_address=0xaf)/ISOTP_SF_FD(message_size=0xff) +assert bytes(pkt) == bytes.fromhex("000007ff 03 04 00 00 af 00ff") + += Build ISOTP_FF_FD 3 + +pkt = ISOTPHeader_FD(identifier=0x7ff)/ISOTP_FF_FD(message_size=0xffff0000) +assert bytes(pkt) == bytes.fromhex("000007ff 06 04 00 00 1000ffff0000") + += Build ISOTP_SF_FD 3 + +pkt = ISOTPHeader_FD(identifier=0x7ff)/ISOTP_SF_FD(message_size=0xff) +assert bytes(pkt) == bytes.fromhex("000007ff 02 04 00 00 00ff") + += Dissect ISOTPFD 1 +pkt = ISOTPHeaderEA_FD(bytes.fromhex("000007ff 07 04 00 00 af 1000ffff0000")) +pkt.show() +sub_pkt = pkt[ISOTP_FF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x7 +assert pkt.fd_flags == 0x4 +assert pkt.extended_address == 0xaf +assert sub_pkt.message_size == 0xffff0000 + += Dissect ISOTPFD 2 +pkt = ISOTPHeaderEA_FD(bytes.fromhex("000007ff 07 04 00 00 af 00ff00000000")) +pkt.show() +sub_pkt = pkt[ISOTP_SF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x7 +assert pkt.fd_flags == 0x4 +assert pkt.extended_address == 0xaf +assert sub_pkt.message_size == 0xff + += Dissect ISOTPFD 3 +pkt = ISOTPHeader_FD(bytes.fromhex("000007ff 06 04 00 00 1000ffff0000")) +pkt.show() +sub_pkt = pkt[ISOTP_FF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x6 +assert pkt.fd_flags == 0x4 +assert sub_pkt.message_size == 0xffff0000 + += Dissect ISOTPFD 4 +pkt = ISOTPHeader_FD(bytes.fromhex("000007ff 06 04 00 00 00ff00000000")) +pkt.show() +sub_pkt = pkt[ISOTP_SF_FD] +assert pkt.identifier == 0x7ff +assert pkt.length == 0x6 +assert pkt.fd_flags == 0x4 +assert sub_pkt.message_size == 0xff \ No newline at end of file diff --git a/test/contrib/isotp_soft_socket.uts b/test/contrib/isotp_soft_socket.uts new file mode 100644 index 00000000000..f112563d62a --- /dev/null +++ b/test/contrib/isotp_soft_socket.uts @@ -0,0 +1,1672 @@ +% Regression tests for ISOTPSoftSocket +~ automotive_comm + ++ Configuration +~ conf + += Imports +import time +from io import BytesIO +from scapy.layers.can import * +from scapy.contrib.isotp import * +from scapy.contrib.isotp.isotp_soft_socket import TimeoutScheduler +from test.testsocket import TestSocket, SlowTestSocket, cleanup_testsockets +with open(scapy_path("test/contrib/automotive/interface_mockup.py")) as f: + exec(f.read()) + += Redirect logging +import logging +from scapy.error import log_runtime + +from io import StringIO + +log_stream = StringIO() +handler = logging.StreamHandler(log_stream) +log_runtime.addHandler(handler) +log_isotp.addHandler(handler) + += Definition of utility functions + +# hexadecimal to bytes convenience function +dhex = bytes.fromhex + + ++ Test sniffer += Test sniffer with multiple frames + +test_frames = [ + (0x241, "EA 10 28 01 02 03 04 05"), + (0x641, "EA 30 03 00" ), + (0x241, "EA 21 06 07 08 09 0A 0B"), + (0x241, "EA 22 0C 0D 0E 0F 10 11"), + (0x241, "EA 23 12 13 14 15 16 17"), + (0x641, "EA 30 03 00" ), + (0x241, "EA 24 18 19 1A 1B 1C 1D"), + (0x241, "EA 25 1E 1F 20 21 22 23"), + (0x241, "EA 26 24 25 26 27 28" ), +] + +with TestSocket(CAN) as s, TestSocket(CAN) as tx_sock: + s.pair(tx_sock) + for f in test_frames: + tx_sock.send(CAN(identifier=f[0], data=dhex(f[1]))) + sniffed = sniff(opened_socket=s, session=ISOTPSession, timeout=1, count=1) + +assert sniffed[0]['ISOTP'].data == bytearray(range(1, 0x29)) +assert sniffed[0]['ISOTP'].tx_id == 0x641 +assert sniffed[0]['ISOTP'].ext_address == 0xEA +assert sniffed[0]['ISOTP'].rx_id == 0x241 +assert sniffed[0]['ISOTP'].rx_ext_address == 0xEA + ++ ISOTPSoftSocket tests + += CAN socket FD +~ not_pypy needs_root linux vcan_socket + +with ISOTPSoftSocket(iface0, tx_id=0x641, rx_id=0x241, fd=True) as s: + assert s.impl.can_socket.fd == True + += CAN socket non-FD +~ not_pypy needs_root linux vcan_socket + +with ISOTPSoftSocket(iface0, tx_id=0x641, rx_id=0x241) as s: + assert s.impl.can_socket.fd == False + += Single-frame receive + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: + cans.pair(stim) + stim.send(CAN(identifier=0x241, data=dhex("05 01 02 03 04 05"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex("01 02 03 04 05") + += Single-frame receive FD + +with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + cans.pair(stim) + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 6 + else: + data_str = "{} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 2 + stim.send(CANFD(identifier=0x241, data=dhex(data_str))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex(data_str[data_str_offset:]) + += Single-frame send + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: + cans.pair(stim) + s.send(ISOTP(dhex("01 02 03 04 05"))) + pkts = stim.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex("05 01 02 03 04 05") + += Single-frame send FD + +with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + cans.pair(stim) + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 6 + else: + data_str = "{} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 2 + s.send(ISOTP(dhex(data_str[data_str_offset:]))) + pkts = stim.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert dhex(data_str) in msg.data + += Two frame receive + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: + cans.pair(stim) + stim.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) + pkts = stim.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + c = pkts[0] + assert (c.data == dhex("30 00 00")) + stim.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex("01 02 03 04 05 06 07 08 09") + + += Two frame receive FD + +with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + cans.pair(stim) + stim.send(CANFD(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06 07 08 09 0A 0B"))) + pkts = stim.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + c = pkts[0] + assert (c.data == dhex("30 00 00")) + stim.send(CANFD(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + msg = pkts[0] + assert msg.data == dhex("01 02 03 04 05 06 07 08 09") + + += 20000 bytes receive + +def test(): + with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: + cans.pair(stim) + data = dhex("01 02 03 04 05") * 4000 + cf = ISOTP(data, rx_id=0x241).fragment() + ff = cf.pop(0) + cs = stim.sniff(count=1, timeout=3, started_callback=lambda: stim.send(ff)) + assert len(cs) + c = cs[0] + assert (c.data == dhex("30 00 00")) + for f in cf: + _ = stim.send(f) + msgs = s.sniff(count=1, timeout=30) + print(msgs) + msg = msgs[0] + assert msg.data == data + +test() + += 20000 bytes send + +def test(): + with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: + cans.pair(stim) + data = dhex("01 02 03 04 05")*4000 + msg = ISOTP(data, rx_id=0x641) + fragments = msg.fragment() + ack = CAN(identifier=0x241, data=dhex("30 00 00")) + ff = stim.sniff(timeout=1, count=1, + started_callback=lambda:s.send(msg)) + assert len(ff) == 1 + cfs = stim.sniff(timeout=20, count=len(fragments) - 1, + started_callback=lambda: stim.send(ack)) + for fragment, cf in zip(fragments, ff + cfs): + assert (bytes(fragment) == bytes(cf)) + +test() + += 20000 bytes send FD + +def testfd(): + with TestSocket(CANFD) as cans, TestSocket(CANFD) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241, fd=True) as s: + cans.pair(stim) + data = dhex("01 02 03 04 05")*4006 + msg = ISOTP(data, rx_id=0x641) + fragments = msg.fragment(fd=True) + ack = CANFD(identifier=0x241, data=dhex("30 00 00")) + ff = stim.sniff(timeout=1, count=1, + started_callback=lambda:s.send(msg)) + assert len(ff) == 1 + cfs = stim.sniff(timeout=20, count=len(fragments) - 1, + started_callback=lambda: stim.send(ack)) + for fragment, cf in zip(fragments, ff + cfs): + print(bytes(fragment), bytes(cf)) + assert (bytes(fragment) in bytes(cf)) + +testfd() + += Close ISOTPSoftSocket + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: + cans.pair(stim) + s.close() + s = None + += Test on_recv function with single frame +with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241) as s: + s.ins.on_recv(CAN(identifier=0x241, data=dhex("05 01 02 03 04 05"))) + msg, ts = s.ins.rx_queue.recv() + assert msg == dhex("01 02 03 04 05") + += Test on_recv function with single frame FD +with ISOTPSoftSocket(TestSocket(CANFD), tx_id=0x641, rx_id=0x241, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 6 + else: + data_str = "{} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 2 + s.ins.on_recv(CANFD(identifier=0x241, data=dhex(data_str))) + msg, ts = s.ins.rx_queue.recv() + assert msg == dhex(data_str[data_str_offset:]) + += Test on_recv function with empty frame +with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241) as s: + s.ins.on_recv(CAN(identifier=0x241, data=b"")) + assert s.ins.rx_queue.empty() + += Test on_recv function with single frame and extended addressing +with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241, rx_ext_address=0xea) as s: + cf = CAN(identifier=0x241, data=dhex("EA 05 01 02 03 04 05")) + s.ins.on_recv(cf) + msg, ts = s.ins.rx_queue.recv() + assert msg == dhex("01 02 03 04 05") + assert ts == cf.time + + += Test on_recv function with single frame and extended addressing FD +with ISOTPSoftSocket(TestSocket(CANFD), tx_id=0x641, rx_id=0x241, rx_ext_address=0xea, fd=True) as s: + pl_sizes_testings = [1, 5, 7, 8, 15, 20, 35, 40, 46, 62] + data_str = "" + data_str_offset = 0 + for size_to_send in pl_sizes_testings: + if size_to_send > 7: + data_str = "EA 00 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 8 + else: + data_str = "EA {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(size_to_send)])) + data_str_offset = 5 + cf = CANFD(identifier=0x241, data=dhex(data_str)) + s.ins.on_recv(cf) + msg, ts = s.ins.rx_queue.recv() + assert msg == dhex(data_str[data_str_offset:]) + assert ts == cf.time + += CF is sent when first frame is received +cans = TestSocket(CAN) +can_out = TestSocket(CAN) +cans.pair(can_out) +with ISOTPSoftSocket(cans, tx_id=0x641, rx_id=0x241) as s: + s.ins.on_recv(CAN(identifier=0x241, data=dhex("10 20 01 02 03 04 05 06"))) + can = can_out.sniff(timeout=1, count=1)[0] + assert can.identifier == 0x641 + assert can.data == dhex("30 00 00") + +cans.close() +can_out.close() + ++ Testing ISOTPSoftSocket with an actual CAN socket + += Verify that packets are not lost if they arrive before the sniff() is called +with TestSocket(CAN) as ss, TestSocket(CAN) as sr: + ss.pair(sr) + tx_func = lambda: ss.send(CAN(identifier=0x111, data=b"\x01\x23\x45\x67")) + p = sr.sniff(count=1, timeout=0.2, started_callback=tx_func) + assert len(p)==1 + tx_func = lambda: ss.send(CAN(identifier=0x111, data=b"\x89\xab\xcd\xef")) + p = sr.sniff(count=1, timeout=0.2, started_callback=tx_func) + assert len(p)==1 + += Send single frame ISOTP message, using send +with TestSocket(CAN) as isocan, \ + ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, \ + TestSocket(CAN) as cans: + cans.pair(isocan) + can = cans.sniff(timeout=2, count=1, started_callback=lambda: s.send(ISOTP(data=dhex("01 02 03 04 05")))) + assert can[0].identifier == 0x641 + assert can[0].data == dhex("05 01 02 03 04 05") + += Send many single frame ISOTP messages, using send + +with TestSocket(CAN) as isocan, \ + ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, \ + TestSocket(CAN) as cans: + cans.pair(isocan) + for i in range(100): + data = dhex("01 02 03 04 05") + struct.pack("B", i) + expected = struct.pack("B", len(data)) + data + can = cans.sniff(timeout=4, count=1, started_callback=lambda: s.send(ISOTP(data=data))) + assert can[0].identifier == 0x641 + print(can[0].data, data) + assert can[0].data == expected + + += Send two-frame ISOTP message, using send +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + can = cans.sniff(timeout=1, count=1, started_callback=lambda: s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08")))) + assert can[0].identifier == 0x641 + assert can[0].data == dhex("10 08 01 02 03 04 05 06") + can = cans.sniff(timeout=1, count=1, started_callback=lambda: cans.send(CAN(identifier = 0x241, data=dhex("30 00 00")))) + assert can[0].identifier == 0x641 + assert can[0].data == dhex("21 07 08") + += Send two-frame ISOTP message, using send FD +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 100 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + can = cans.sniff(timeout=1, count=1, started_callback=lambda: s.send(dhex(data_str))) + assert can[0].identifier == 0x641 + assert can[0].data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + can = cans.sniff(timeout=1, count=1, started_callback=lambda: cans.send(CANFD(identifier = 0x241, data=dhex("30 00 00")))) + assert can[0].identifier == 0x641 + assert dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) in can[0].data + += Send single frame ISOTP message +with TestSocket(CAN) as cans, TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s: + cans.pair(isocan) + s.send(ISOTP(data=dhex("01 02 03 04 05"))) + can = cans.sniff(timeout=1, count=1) + assert can[0].identifier == 0x641 + assert can[0].data == dhex("05 01 02 03 04 05") + + += Send two-frame ISOTP message + +acks = TestSocket(CAN) + +acker_ready = threading.Event() +def acker(): + acker_ready.set() + can_pkt = acks.sniff(timeout=1, count=1) + can = can_pkt[0] + acks.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 08 01 02 03 04 05 06") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 00") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 07 08") + +thread.join(15) +acks.close() +assert not thread.is_alive() + += Send two-frame ISOTP message FD + +acks = TestSocket(CANFD) + +acker_ready = threading.Event() +def acker(): + acker_ready.set() + can_pkt = acks.sniff(timeout=1, count=1) + can = can_pkt[0] + acks.send(CANFD(identifier = 0x241, data=dhex("30 00 00"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 123 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(dhex(data_str)) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 00") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) in can.data + +thread.join(15) +acks.close() +assert not thread.is_alive() + += Send two-frame ISOTP message with bs + +acks = TestSocket(CAN) +acker_ready = threading.Event() +def acker(): + acker_ready.set() + can_pkt = acks.sniff(timeout=1, count=1) + acks.send(CAN(identifier = 0x241, data=dhex("30 20 00"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 08 01 02 03 04 05 06") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 20 00") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 07 08") + +thread.join(15) +acks.close() +assert not thread.is_alive() + += Send two-frame ISOTP message with bs FD + +acks = TestSocket(CANFD) +acker_ready = threading.Event() +def acker(): + acker_ready.set() + can_pkt = acks.sniff(timeout=1, count=1) + acks.send(CANFD(identifier = 0x241, data=dhex("30 20 00"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 124 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(ISOTP(data=dhex(data_str))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 20 00") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) in can.data + +thread.join(15) +acks.close() +assert not thread.is_alive() + += Send two-frame ISOTP message with ST +acks = TestSocket(CAN) +acker_ready = threading.Event() +def acker(): + acker_ready.set() + acks.sniff(timeout=1, count=1) + acks.send(CAN(identifier = 0x241, data=dhex("30 00 10"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 08 01 02 03 04 05 06") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 10") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("21 07 08") + +thread.join(15) +acks.close() +assert not thread.is_alive() + += Send two-frame ISOTP message with ST FD +acks = TestSocket(CANFD) +acker_ready = threading.Event() +def acker(): + acker_ready.set() + acks.sniff(timeout=1, count=1) + acks.send(CANFD(identifier = 0x241, data=dhex("30 00 10"))) + +thread = Thread(target=acker) +thread.start() +acker_ready.wait(timeout=5) +with TestSocket(CANFD) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, fd=True) as s, TestSocket(CANFD) as cans: + size_to_send = 124 + max_pl_size = 62 + data_str = "{}".format(" ".join(["%02X" % x for x in range(size_to_send)])) + cans.pair(isocan) + cans.pair(acks) + isocan.pair(acks) + s.send(dhex(data_str)) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert can.data == dhex("10 {} {}".format("%02X" % size_to_send, " ".join(["%02X" % x for x in range(max_pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x241 + assert can.data == dhex("30 00 10") + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + can = pkts[0] + assert can.identifier == 0x641 + assert dhex("21 {}".format(" ".join(["%02X" % x for x in range(max_pl_size, size_to_send)]))) in can.data + +thread.join(15) +acks.close() +assert not thread.is_alive() + += Receive a single frame ISOTP message + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier = 0x241, data = dhex("05 01 02 03 04 05"))) + pkts = s.sniff(count=1, timeout=2) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + isotp = pkts[0] + assert isotp.data == dhex("01 02 03 04 05") + assert isotp.tx_id == 0x641 + assert isotp.rx_id == 0x241 + assert isotp.ext_address == None + assert isotp.rx_ext_address == None + + += Receive a single frame ISOTP message, with extended addressing + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, ext_address=0xc0, rx_ext_address=0xea) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier = 0x241, data = dhex("EA 05 01 02 03 04 05"))) + pkts = s.sniff(count=1, timeout=2) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + isotp = pkts[0] + assert isotp.data == dhex("01 02 03 04 05") + assert isotp.tx_id == 0x641 + assert isotp.rx_id == 0x241 + assert isotp.ext_address == 0xc0 + assert isotp.rx_ext_address == 0xea + + += Receive frames from CandumpReader +candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA''') + +with ISOTPSoftSocket(CandumpReader(candump_fd), tx_id=0x241, rx_id=0x541, listen_only=True) as s: + pkts = s.sniff(timeout=2, count=6) + assert len(pkts) == 6 + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + isotp = pkts[0] + print(repr(isotp)) + print(hex(isotp.tx_id)) + print(hex(isotp.rx_id)) + assert isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA") + assert isotp.tx_id == 0x241 + assert isotp.rx_id == 0x541 + += Receive frames from CandumpReader with ISOTPSniffer without extended addressing +candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA''') + +pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession(use_ext_address=False), timeout=1) +assert len(pkts) == 6 + +if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + +isotp = pkts[0] +assert isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA") +assert (isotp.rx_id == 0x541) + += Receive frames from CandumpReader with ISOTPSniffer +* all flow control frames are detected as single frame with extended address + +candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA''') + +pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession, timeout=1) +if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + +assert len(pkts) == 12 +isotp = pkts[1] +assert isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA") +assert (isotp.rx_id == 0x541) +isotp = pkts[0] +assert isotp.data == dhex("") +assert (isotp.rx_id == 0x241) + += Receive frames from CandumpReader with ISOTPSniffer and count +* all flow control frames are detected as single frame with extended address + +candump_fd = BytesIO(b''' vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA + vcan0 541 [8] 10 0A DE AD BE EF AA AA + vcan0 241 [3] 30 00 00 + vcan0 541 [5] 21 AA AA AA AA''') + +pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession, timeout=1, count=2) +if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + +assert len(pkts) == 2 +isotp = pkts[1] +assert isotp.data == dhex("DE AD BE EF AA AA AA AA AA AA") +assert (isotp.rx_id == 0x541) +isotp = pkts[0] +assert isotp.data == dhex("") +assert (isotp.rx_id == 0x241) + += Receive a two-frame ISOTP message + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier = 0x241, data = dhex("10 0B 01 02 03 04 05 06"))) + cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 10 11"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + isotp = pkts[0] + assert isotp.data == dhex("01 02 03 04 05 06 07 08 09 10 11") + += Check what happens when a CAN frame with wrong identifier gets received + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier = 0x141, data = dhex("05 01 02 03 04 05"))) + assert s.ins.rx_queue.empty() + ++ Testing ISOTPSoftSocket timeouts + += Check if not sending the last CF will make the socket timeout +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier = 0x241, data = dhex("10 11 01 02 03 04 05 06"))) + cans.send(CAN(identifier = 0x241, data = dhex("21 07 08 09 0A 0B 0C 0D"))) + isotp = s.sniff(timeout=0.1) + +assert len(isotp) == 0 + += Check if not sending the first CF will make the socket timeout + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier = 0x241, data = dhex("10 11 01 02 03 04 05 06"))) + isotp = s.sniff(timeout=0.1) + +assert len(isotp) == 0 + += Check if not sending the first FC will make the socket timeout + +# drain log_stream +log_stream.getvalue() + +isotp = ISOTP(data=dhex("01 02 03 04 05 06 07 08 09 0A")) + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s, TestSocket(CAN) as cans: + cans.pair(isocan) + s.send(isotp) + time.sleep(1.3) + +assert "TX state was reset due to timeout" in log_stream.getvalue() + += Check if not sending the second FC will make the socket timeout + +# drain log_stream +log_stream.getvalue() + +isotp = ISOTP(data=b"\xa5" * 120) +cans = TestSocket(CAN) +isocan = TestSocket(CAN) +cans.pair(isocan) + +acker = AsyncSniffer(store=False, opened_socket=cans, + prn=lambda x: cans.send(CAN(identifier = 0x241, data=dhex("30 04 00"))), + count=1, timeout=1) +acker.start() +with ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s: + s.send(isotp) + time.sleep(1.3) + +acker.join(timeout=5) +cans.close() +isocan.close() + +assert "TX state was reset due to timeout" in log_stream.getvalue() + += Check if reception of an overflow FC will make a send fail + +log_stream.getvalue() +isotp = ISOTP(data=b"\xa5" * 120) +cans = TestSocket(CAN) +isocan = TestSocket(CAN) +cans.pair(isocan) + +acker = AsyncSniffer(store=False, opened_socket=cans, + prn=lambda x: cans.send( + CAN(identifier = 0x241, data=dhex("32 00 00"))), + count=1, timeout=1) +acker.start() + +with ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241) as s: + s.send(isotp) + time.sleep(1.3) + +acker.join(timeout=5) +cans.close() +isocan.close() + +assert "Overflow happened at the receiver side" in log_stream.getvalue() + ++ More complex operations + += ISOTPSoftSocket sr1 +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x321, 0x123) as sock_rx: + isocan_rx.pair(isocan_tx) + sniffer = AsyncSniffer(opened_socket=sock_rx, timeout=1, count=1, prn=lambda x: sock_rx.send(msg)) + sniffer.start() + rx2 = sock_tx.sr1(msg, timeout=3, verbose=True) + sniffer.join(timeout=1) + rx = sniffer.results[0] + +assert rx == msg +assert rx2 is not None +assert rx2 == msg + += ISOTPSoftSocket sr1 timeout +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x321, 0x123) as sock_rx: + isocan_rx.pair(isocan_tx) + rx2 = sock_tx.sr1(msg, timeout=1, verbose=True) + +assert rx2 is None + += ISOTPSoftSocket select returns control ObjectPipe + +from scapy.automaton import ObjectPipe as _ObjectPipe + +close_pipe = _ObjectPipe("control_socket") +close_pipe.send(None) + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, 0x123, 0x321) as sock: + result = ISOTPSoftSocket.select([sock, close_pipe], remain=0) + +assert close_pipe in result + +close_pipe.close() + += ISOTPSoftSocket select returns control ObjectPipe alongside ready rx_queue + +from scapy.automaton import ObjectPipe as _ObjectPipe + +close_pipe = _ObjectPipe("control_socket") +close_pipe.send(None) + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, 0x641, 0x241) as sock: + sock.impl.rx_queue.send((b'\x62\xF1\x90\x41\x42\x43', 0.0)) + result = ISOTPSoftSocket.select([sock, close_pipe], remain=0) + +assert close_pipe in result +assert sock in result + +close_pipe.close() + += ISOTPSoftSocket sr1 SF request with MF response threaded + +from threading import Thread + +request = ISOTP(b'\x22\xF1\x90') +response_data = b'\x62\xF1\x90' + b'\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50' +response_msg = ISOTP(response_data) + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x641, 0x241) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x241, 0x641) as sock_rx: + isocan_rx.pair(isocan_tx) + def responder(): + sniffed = sock_rx.sniff(count=1, timeout=5) + if sniffed: + sock_rx.send(response_msg) + resp_thread = Thread(target=responder, daemon=True) + resp_thread.start() + time.sleep(0.1) + rx = sock_tx.sr1(request, timeout=5, verbose=False, threaded=True) + resp_thread.join(timeout=5) + assert not resp_thread.is_alive(), "resp_thread still alive" + # Stop TimeoutScheduler while sockets are still open to avoid + # callbacks crashing on closed sockets and writing to stderr. + _ts = TimeoutScheduler._thread + TimeoutScheduler.clear() + if _ts is not None: + _ts.join(timeout=5) + +assert rx is not None +assert rx.data == response_data + += ISOTPSoftSocket sr1 timeout with threaded=True + +from threading import Thread, Event +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x321, 0x123) as sock_rx: + isocan_rx.pair(isocan_tx) + start = time.time() + rx2 = sock_tx.sr1(msg, timeout=3, verbose=False, threaded=True) + elapsed = time.time() - start + # Stop TimeoutScheduler while sockets are still open. + _ts = TimeoutScheduler._thread + TimeoutScheduler.clear() + if _ts is not None: + _ts.join(timeout=5) + +assert rx2 is None +assert elapsed < 5 + += ISOTPSoftSocket sr1 timeout with threaded=True and background traffic + +from threading import Thread, Event +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x321, 0x123) as sock_rx: + isocan_rx.pair(isocan_tx) + stop_traffic = Event() + def bg_traffic(): + while not stop_traffic.is_set(): + try: + isocan_rx.send(CAN(identifier=0x456, data=dhex("01 02 03"))) + except Exception: + break + time.sleep(0.01) + traffic_thread = Thread(target=bg_traffic, daemon=True) + traffic_thread.start() + start = time.time() + rx2 = sock_tx.sr1(msg, timeout=3, verbose=False, threaded=True) + elapsed = time.time() - start + stop_traffic.set() + traffic_thread.join(timeout=5) + assert not traffic_thread.is_alive(), "traffic_thread still alive" + # Stop TimeoutScheduler while sockets are still open. + _ts = TimeoutScheduler._thread + TimeoutScheduler.clear() + if _ts is not None: + _ts.join(timeout=5) + +assert rx2 is None +assert elapsed < 5 + += ISOTPSoftSocket sr1 SF request with MF response threaded and background traffic on slow interface + +from threading import Thread, Event + +response_data = b'\x62\xF1\x90' + b'\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50' + +stim = TestSocket(CAN) +isocan = TestSocket(CAN) +stim.pair(isocan) + +bg_frame = CAN(identifier=0x456, data=dhex("01 02 03")) +ff_frame = CAN(identifier=0x241, data=dhex("10 13 62 F1 90 41 42 43")) +cf1_frame = CAN(identifier=0x241, data=dhex("21 44 45 46 47 48 49 4A")) +cf2_frame = CAN(identifier=0x241, data=dhex("22 4B 4C 4D 4E 4F 50 00")) + +bg_count = 2000 # Large number of frames to stress the ISOTPSoftSocket implementation + +for _ in range(100): + _ = stim.send(bg_frame) + +stim.send(ff_frame) + +for _ in range(bg_count): + _ = stim.send(bg_frame) + +stim.send(cf1_frame) +stim.send(cf2_frame) + +with isocan, stim, ISOTPSoftSocket(isocan, 0x641, 0x241) as sock: + pkts = sock.sniff(count=1, timeout=10) + # Stop TimeoutScheduler while sockets are still open. + _ts = TimeoutScheduler._thread + TimeoutScheduler.clear() + if _ts is not None: + _ts.join(timeout=5) + +assert len(pkts) == 1, "MF response not received due to background traffic" +assert pkts[0].data == response_data + += ISOTPSoftSocket MF response with delayed CFs and background traffic + +from threading import Thread, Event + +response_data = b'\x62\xF1\x90' + b'\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50' + +with TestSocket(CAN) as stim, TestSocket(CAN) as isocan, \ + ISOTPSoftSocket(isocan, 0x641, 0x241) as sock: + stim.pair(isocan) + stop_traffic = Event() + def bg_traffic(): + bg_frame = CAN(identifier=0x456, data=dhex("01 02 03")) + while not stop_traffic.is_set(): + try: + stim.send(bg_frame) + except Exception: + break + time.sleep(0.001) + def delayed_response(): + time.sleep(0.05) + sock.impl.rx_tx_poll_rate = 10 + stim.send(CAN(identifier=0x241, data=dhex("10 13 62 F1 90 41 42 43"))) + time.sleep(0.01) + stim.send(CAN(identifier=0x241, data=dhex("21 44 45 46 47 48 49 4A"))) + time.sleep(0.01) + stim.send(CAN(identifier=0x241, data=dhex("22 4B 4C 4D 4E 4F 50 00"))) + traffic_thread = Thread(target=bg_traffic) + traffic_thread.start() + resp_thread = Thread(target=delayed_response) + resp_thread.start() + pkts = sock.sniff(count=1, timeout=5) + stop_traffic.set() + traffic_thread.join(timeout=5) + resp_thread.join(timeout=5) + assert not traffic_thread.is_alive(), "traffic_thread still alive" + assert not resp_thread.is_alive(), "resp_thread still alive" + # Stop TimeoutScheduler while sockets are still open. + _ts = TimeoutScheduler._thread + TimeoutScheduler.clear() + if _ts is not None: + _ts.join(timeout=5) + +assert len(pkts) == 1, "MF response not received with delayed CFs and slow poll rate" +assert pkts[0].data == response_data + += ISOTPSoftSocket sniff + +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') +with TestSocket(CAN) as isocan1, ISOTPSoftSocket(isocan1, 0x123, 0x321) as sock, \ + TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, 0x321, 0x123) as rx_sock: + isocan1.pair(isocan) + msg.data += b'0' + sock.send(msg) + msg.data += b'1' + sock.send(msg) + msg.data += b'2' + sock.send(msg) + msg.data += b'3' + sock.send(msg) + msg.data += b'4' + sock.send(msg) + rx = rx_sock.sniff(count=5, timeout=5) + +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') +msg.data += b'0' +assert rx[0] == msg +msg.data += b'1' +assert rx[1] == msg +msg.data += b'2' +assert rx[2] == msg +msg.data += b'3' +assert rx[3] == msg +msg.data += b'4' +assert rx[4] == msg + ++ ISOTPSoftSocket MITM attack tests + += bridge and sniff with isotp soft sockets set up vcan0 and vcan1 for package forwarding vcan1 +succ = False + +with TestSocket(CAN) as can0_0, \ + TestSocket(CAN) as can0_1, \ + TestSocket(CAN) as can1_0, \ + TestSocket(CAN) as can1_1, \ + ISOTPSoftSocket(can0_0, tx_id=0x241, rx_id=0x641) as isoTpSocket0, \ + ISOTPSoftSocket(can1_0, tx_id=0x541, rx_id=0x141) as isoTpSocket1, \ + ISOTPSoftSocket(can0_1, tx_id=0x641, rx_id=0x241) as bSocket0, \ + ISOTPSoftSocket(can1_1, tx_id=0x141, rx_id=0x141) as bSocket1: + can0_0.pair(can0_1) + can1_1.pair(can1_0) + evt = threading.Event() + def forwarding(pkt): + global forwarded + forwarded += 1 + return pkt + def bridge(): + global forwarded, succ + forwarded = 0 + bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=1.5, + started_callback=evt.set, count=1) + succ = True + threadBridge = threading.Thread(target=bridge) + threadBridge.start() + evt.wait(timeout=5) + packetsVCan1 = isoTpSocket1.sniff(timeout=1.5, count=1, started_callback=lambda: isoTpSocket0.send(ISOTP(b'Request'))) + threadBridge.join(timeout=5) + assert not threadBridge.is_alive() + +assert forwarded == 1 +assert len(packetsVCan1) == 1 +assert succ + += bridge and sniff with isotp soft sockets and multiple long packets + +N = 3 +T = 3 + +succ = False +with TestSocket(CAN) as can0_0, \ + TestSocket(CAN) as can0_1, \ + TestSocket(CAN) as can1_0, \ + TestSocket(CAN) as can1_1, \ + ISOTPSoftSocket(can0_0, tx_id=0x241, rx_id=0x641) as isoTpSocket0, \ + ISOTPSoftSocket(can1_0, tx_id=0x541, rx_id=0x141) as isoTpSocket1, \ + ISOTPSoftSocket(can0_1, tx_id=0x641, rx_id=0x241) as bSocket0, \ + ISOTPSoftSocket(can1_1, tx_id=0x141, rx_id=0x541) as bSocket1: + can0_0.pair(can0_1) + can1_1.pair(can1_0) + evt = threading.Event() + def forwarding(pkt): + global forwarded + forwarded += 1 + return pkt + def bridge(): + global forwarded, succ + forwarded = 0 + bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, + timeout=T, count=N, started_callback=evt.set) + succ = True + threadBridge = threading.Thread(target=bridge) + threadBridge.start() + evt.wait(timeout=5) + for _ in range(N): + isoTpSocket0.send(ISOTP(b'RequestASDF1234567890')) + packetsVCan1 = isoTpSocket1.sniff(timeout=T, count=N) + threadBridge.join(timeout=5) + +assert not threadBridge.is_alive() + +assert forwarded == N +assert len(packetsVCan1) == N +assert succ + += bridge and sniff with isotp soft sockets set up vcan0 and vcan1 for package change vcan1 + +succ = False +with TestSocket(CAN) as can0_0, \ + TestSocket(CAN) as can0_1, \ + TestSocket(CAN) as can1_0, \ + TestSocket(CAN) as can1_1, \ + ISOTPSoftSocket(can0_0, tx_id=0x241, rx_id=0x641) as isoTpSocket0, \ + ISOTPSoftSocket(can1_0, tx_id=0x641, rx_id=0x241) as isoTpSocket1, \ + ISOTPSoftSocket(can0_1, tx_id=0x641, rx_id=0x241) as bSocket0, \ + ISOTPSoftSocket(can1_1, tx_id=0x241, rx_id=0x641) as bSocket1: + can0_0.pair(can0_1) + can1_1.pair(can1_0) + evt = threading.Event() + def forwarding(pkt): + pkt.data = 'changed' + return pkt + def bridge(): + global succ + bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=3, + started_callback=evt.set, count=1) + succ = True + threadBridge = threading.Thread(target=bridge) + threadBridge.start() + evt.wait(timeout=5) + packetsVCan1 = isoTpSocket1.sniff(timeout=2, count=1, started_callback=lambda: isoTpSocket0.send(ISOTP(b'Request'))) + threadBridge.join(timeout=5) + assert not threadBridge.is_alive() + +assert len(packetsVCan1) == 1 +assert packetsVCan1[0].data == b'changed' +assert succ + += Two ISOTPSoftSockets at the same time, sending and receiving + +with TestSocket(CAN) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241) as s1, \ + TestSocket(CAN) as cs2, ISOTPSoftSocket(cs2, tx_id=0x241, rx_id=0x641) as s2: + cs1.pair(cs2) + isotp = ISOTP(data=b"\x10\x25" * 43) + s2.send(isotp) + result = s1.sniff(count=1, timeout=5) + +assert len(result) == 1 +assert result[0].data == isotp.data + + += Two ISOTPSoftSockets at the same time, sending and receiving with tx_gap + +with TestSocket(CAN) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241, stmin=1) as s1, \ + TestSocket(CAN) as cs2, ISOTPSoftSocket(cs2, tx_id=0x241, rx_id=0x641) as s2: + cs1.pair(cs2) + isotp = ISOTP(data=b"\x10\x25" * 43) + s2.send(isotp) + result = s1.sniff(count=1, timeout=5) + +assert len(result) == 1 +assert result[0].data == isotp.data + + += Two ISOTPSoftSockets at the same time, multiple sends/receives +def test(): + with TestSocket(CAN) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241) as s1, \ + TestSocket(CAN) as cs2, ISOTPSoftSocket(cs2, tx_id=0x241, rx_id=0x641) as s2: + cs1.pair(cs2) + for i in range(1, 40, 5): + isotp = ISOTP(data=bytearray(range(i, i * 2))) + s2.send(isotp) + result = s1.sniff(count=8, timeout=5) + print(result) + for p in result: + print(repr(p)) + assert len(result) == 8 + +test() + += Send a single frame ISOTP message with padding + +with TestSocket(CAN) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241, padding=True) as s: + with TestSocket(CAN) as cans: + cs1.pair(cans) + s.send(ISOTP(data=dhex("01"))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + assert res.length == 8 + += Send a single frame ISOTP message with padding FD + +with TestSocket(CANFD) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241, padding=True, fd=True) as s: + with TestSocket(CANFD) as cans: + cs1.pair(cans) + pl_sizes_testings = [1, 5, 7, 8, 9, 12, 15, 17, 20, 21, 27, 35, 40, 46, 50, 62] + pl_sizes_expected = [8, 8, 8, 12, 12, 16, 20, 20, 24, 24, 32, 48, 48, 48, 64, 64] + for i, pl_size in enumerate(pl_sizes_testings): + s.send(dhex(" ".join(["%02X" % x for x in range(pl_size)]))) + pkts = cans.sniff(timeout=1, count=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + assert res.length == pl_sizes_expected[i] + + += Send a two-frame ISOTP message with padding + +acks = TestSocket(CAN) +cans = TestSocket(CAN) +acks.pair(cans) + +def send_ack(x): + acks.send(CAN(identifier = 0x241, data=dhex("30 00 00"))) + +acker = AsyncSniffer(opened_socket=acks, store=False, prn=send_ack, timeout=1, count=1) +acker.start() + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, padding=True) as s: + acks.pair(isocan) + cans.pair(isocan) + s.send(ISOTP(data=dhex("01 02 03 04 05 06 07 08"))) + canpks = cans.sniff(timeout=1, count=3) + +acker.join(timeout=5) +canpks.sort(key=lambda x:x.identifier) +assert canpks[1].identifier == 0x641 +assert canpks[1].data == dhex("10 08 01 02 03 04 05 06") +assert canpks[0].identifier == 0x241 +assert canpks[0].data == dhex("30 00 00") +assert canpks[2].identifier == 0x641 +assert canpks[2].data == dhex("21 07 08 CC CC CC CC CC") + + += Receive a padded single frame ISOTP message with padding disabled +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, padding=False) as s: + with TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + assert res.data == dhex("05 06") + + += Receive a padded single frame ISOTP message with padding enabled +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, padding=True) as s: + with TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier=0x241, data=dhex("02 05 06 00 00 00 00 00"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + assert res.data == dhex("05 06") + + += Receive a non-padded single frame ISOTP message with padding enabled +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, padding=True) as s: + with TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier=0x241, data=dhex("02 05 06"))) + pkts = s.sniff(count=1, timeout=2) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + assert res.data == dhex("05 06") + + += Receive a padded two-frame ISOTP message with padding enabled +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, padding=True) as s: + with TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) + cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + assert res.data == dhex("01 02 03 04 05 06 07 08 09") + += Receive a padded two-frame ISOTP message with padding disabled +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241, padding=False) as s: + with TestSocket(CAN) as cans: + cans.pair(isocan) + cans.send(CAN(identifier=0x241, data=dhex("10 09 01 02 03 04 05 06"))) + cans.send(CAN(identifier=0x241, data=dhex("21 07 08 09 00 00 00 00"))) + pkts = s.sniff(count=1, timeout=1) + if not len(pkts): + s.failure_analysis() + raise Scapy_Exception("ERROR") + res = pkts[0] + res.show() + assert res.data == dhex("01 02 03 04 05 06 07 08 09") + + ++ MF response via sr1() cartesian product tests +# Background traffic from pcap: 3 periodic IDs (0x062, 0x024, 0x039) every +# 10ms, plus a burst of 9 additional IDs every 100ms. +# ECU response latency: ~0.6ms after SF request (from pcap frame 385). +# CF timing after FC: CF1 +8ms, CF2 +10ms, CF3 +10ms (from pcap). +# Expected ISOTP data: "62 00 01 flag{UDS_DATA_READ}" (22 bytes). +# +# Cartesian product dimensions: +# threaded: {False, True} - sr1() threading mode +# can_filters: {[0x7eb], None} - per-socket filtering vs. no filtering +# adapter: {limited (slcan-like), unlimited (candle-like)} +# +# slcan model parameters (from real hardware testing): +# frame_delay=0.0025: ~2.5ms per serial read at 115200 baud +# serial_timeout=0.1: python-can slcan Serial(timeout=0.1) blocks 100ms +# when serial buffer is empty +# read_time_limit=0.02: SocketMapper.READ_BUS_TIME_LIMIT = 20ms caps +# total read time per mux call +# prefill_frames=200: OS serial buffer backlog from busy CAN bus +# +# All tests use retry=0, timeout=1.0. All should PASS with the fix +# (can_filters stripped from raw Bus, per-socket filtering in mux, +# read_bus time-limited to avoid TimeoutScheduler thread starvation). + += MF response helper setup for cartesian product tests + +from threading import Thread, Event + +def run_mf_response_test(frame_delay, mux_throttle, filters_kwarg, threaded, + prefill_frames=0, serial_timeout=0.0, + read_time_limit=0.0, interface_name="slcan"): + import time as _time + from threading import Thread as _Thread, Event as _Event + from scapy.layers.can import CAN as _CAN + from scapy.contrib.isotp import ISOTP as _ISOTP + from scapy.contrib.isotp.isotp_soft_socket import ISOTPSoftSocket as _ISOTPSoftSocket + from scapy.contrib.isotp.isotp_soft_socket import TimeoutScheduler as _TimeoutScheduler + from test.testsocket import TestSocket as _TestSocket, SlowTestSocket as _SlowTestSocket + _dhex = bytes.fromhex + response_data = _dhex("620001666c61677b5544535f444154415f524541447d") + bg_periodic = [0x062, 0x024, 0x039] + bg_burst = [0x1d3, 0x024, 0x039, 0x077, 0x098, 0x150, 0x1a7, 0x1b8, 0x1bb] + if frame_delay > 0: + sock_cls = _SlowTestSocket + sock_kwargs = dict(frame_delay=frame_delay, mux_throttle=mux_throttle, + serial_timeout=serial_timeout, + read_time_limit=read_time_limit, + interface_name=interface_name, + **filters_kwarg) + else: + sock_cls = _TestSocket + sock_kwargs = {} + with sock_cls(_CAN, **sock_kwargs) as isocan, \ + _TestSocket(_CAN) as ecu_mon, \ + _ISOTPSoftSocket(isocan, tx_id=0x7e3, rx_id=0x7eb) as sock: + with _TestSocket(_CAN) as stim: + stim.pair(isocan) + isocan.pair(ecu_mon) + # Pre-fill the serial buffer with background frames to + # simulate a real slcan adapter that has been connected to + # a busy CAN bus. On real hardware the OS serial buffer + # accumulates hundreds of frames before the ISOTP exchange. + for _ in range(prefill_frames): + bid = bg_periodic[_ % len(bg_periodic)] + stim.send(_CAN(identifier=bid, data=bytes(8))) + fc_received = _Event() + stop = _Event() + bg_cycle = [0] + def bg_generator(): + while not stop.is_set(): + for bid in bg_periodic: + if stop.is_set(): + return + stim.send(_CAN(identifier=bid, data=bytes(8))) + bg_cycle[0] += 1 + if bg_cycle[0] % 10 == 0: + for bid in bg_burst: + if stop.is_set(): + return + stim.send(_CAN(identifier=bid, data=bytes(8))) + _time.sleep(0.010) + def ecu_simulation(): + _time.sleep(0.05) + stim.send(_CAN(identifier=0x7eb, data=_dhex("1016620001666c61"))) + fc_received.wait(timeout=10.0) + if not fc_received.is_set(): + return + _time.sleep(0.008) + stim.send(_CAN(identifier=0x7eb, data=_dhex("21677b5544535f44"))) + _time.sleep(0.010) + stim.send(_CAN(identifier=0x7eb, data=_dhex("224154415f524541"))) + _time.sleep(0.010) + stim.send(_CAN(identifier=0x7eb, data=_dhex("23447d"))) + def fc_watcher(): + while not stop.is_set(): + if _TestSocket.select([ecu_mon], 0.1): + pkt = ecu_mon.recv() + if pkt is not None and pkt.identifier == 0x7e3 and \ + len(pkt.data) >= 1 and bytes(pkt.data)[0] == 0x30: + fc_received.set() + return + bg_thread = _Thread(target=bg_generator) + ecu_thread = _Thread(target=ecu_simulation) + fc_thread = _Thread(target=fc_watcher) + bg_thread.start() + ecu_thread.start() + fc_thread.start() + result = sock.sr1(_ISOTP(data=_dhex("220001")), + retry=0, timeout=10.0, + threaded=threaded, verbose=0) + stop.set() + fc_received.set() + bg_thread.join(timeout=5) + ecu_thread.join(timeout=5) + fc_thread.join(timeout=5) + assert not bg_thread.is_alive(), "bg_thread still alive" + assert not ecu_thread.is_alive(), "ecu_thread still alive" + assert not fc_thread.is_alive(), "fc_thread still alive" + # Stop TimeoutScheduler while sockets are still open to + # avoid callbacks crashing on closed sockets and writing + # to stderr (causes fatal error on Python 3.13 Windows). + _ts_thread = _TimeoutScheduler._thread + _TimeoutScheduler.clear() + if _ts_thread is not None: + _ts_thread.join(timeout=5) + return result, response_data + += MF response: candle-like unlimited, no can_filters, threaded=False + +result, expected = run_mf_response_test( + frame_delay=0, mux_throttle=0, + filters_kwarg={}, threaded=False, interface_name="candle") +assert result is not None, "MF response not received (candle, no filters, threaded=False)" +assert result.data == expected + += MF response: candle-like unlimited, no can_filters, threaded=True + +result, expected = run_mf_response_test( + frame_delay=0, mux_throttle=0, + filters_kwarg={}, threaded=True, interface_name="candle") +assert result is not None, "MF response not received (candle, no filters, threaded=True)" +assert result.data == expected + += MF response: candle-like unlimited, can_filters=[0x7eb], threaded=False + +result, expected = run_mf_response_test( + frame_delay=0, mux_throttle=0, + filters_kwarg=dict(can_filters=[0x7eb]), threaded=False, + interface_name="candle") +assert result is not None, "MF response not received (candle, can_filters, threaded=False)" +assert result.data == expected + += MF response: candle-like unlimited, can_filters=[0x7eb], threaded=True + +result, expected = run_mf_response_test( + frame_delay=0, mux_throttle=0, + filters_kwarg=dict(can_filters=[0x7eb]), threaded=True, + interface_name="candle") +assert result is not None, "MF response not received (candle, can_filters, threaded=True)" +assert result.data == expected + += MF response: slcan-like limited, no can_filters, threaded=False + +result, expected = run_mf_response_test( + frame_delay=0.0025, mux_throttle=0.001, serial_timeout=0.1, + read_time_limit=0.02, filters_kwarg={}, threaded=False, + prefill_frames=200) +assert result is not None, "MF response not received (slcan, no filters, threaded=False)" +assert result.data == expected + += MF response: slcan-like limited, no can_filters, threaded=True + +result, expected = run_mf_response_test( + frame_delay=0.0025, mux_throttle=0.001, serial_timeout=0.1, + read_time_limit=0.02, filters_kwarg={}, threaded=True, + prefill_frames=200) +assert result is not None, "MF response not received (slcan, no filters, threaded=True)" +assert result.data == expected + += MF response: slcan-like limited, can_filters=[0x7eb], threaded=False + +result, expected = run_mf_response_test( + frame_delay=0.0025, mux_throttle=0.001, serial_timeout=0.1, + read_time_limit=0.02, filters_kwarg=dict(can_filters=[0x7eb]), + threaded=False, prefill_frames=200) +assert result is not None, "MF response not received (slcan, can_filters, threaded=False)" +assert result.data == expected + += MF response: slcan-like limited, can_filters=[0x7eb], threaded=True + +result, expected = run_mf_response_test( + frame_delay=0.0025, mux_throttle=0.001, serial_timeout=0.1, + read_time_limit=0.02, filters_kwarg=dict(can_filters=[0x7eb]), + threaded=True, prefill_frames=200) +assert result is not None, "MF response not received (slcan, can_filters, threaded=True)" +assert result.data == expected + + ++ Cleanup + += Delete testsockets + +cleanup_testsockets() + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + +log_runtime.removeHandler(handler) diff --git a/test/contrib/isotpscan.uts b/test/contrib/isotpscan.uts index 1badb172d78..85b8eae4759 100644 --- a/test/contrib/isotpscan.uts +++ b/test/contrib/isotpscan.uts @@ -1,140 +1,25 @@ -% Regression tests for ISOTPScan +% Regression tests for isotp_scan +~ scanner + Configuration ~ conf = Imports -load_layer("can") -conf.contribs['CAN']['swap-bytes'] = False -import os, threading, six, subprocess, sys -from subprocess import call -from scapy.contrib.isotp import send_multiple_ext, filter_periodic_packets, scan, scan_extended -from scapy.consts import LINUX - -= Definition of constants, utility functions - -iface0 = "vcan0" -iface1 = "vcan1" - -# function to exit when the can-isotp kernel module is not available -ISOTP_KERNEL_MODULE_AVAILABLE = False -def exit_if_no_isotp_module(): - if not ISOTP_KERNEL_MODULE_AVAILABLE: - sys.stderr.write("TEST SKIPPED: can-isotp not available" + os.linesep) - warning("Can't test ISOTP native socket because kernel module is not loaded") - exit(0) - - -= Initialize a virtual CAN interface -~ vcan_socket needs_root linux -if 0 != call(["cansend", iface0, "000#"]): - # vcan0 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface0, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface0) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface0, "up"]): - raise Exception("could not bring up %s" % iface0) - -if 0 != call(["cansend", iface0, "000#"]): - raise Exception("cansend doesn't work") - -if 0 != call(["cansend", iface1, "000#"]): - # vcan1 is not enabled - if 0 != call(["sudo", "modprobe", "vcan"]): - raise Exception("modprobe vcan failed") - if 0 != call(["sudo", "ip", "link", "add", "name", iface1, "type", "vcan"]): - print("add %s failed: Maybe it was already up?" % iface1) - if 0 != call(["sudo", "ip", "link", "set", "dev", iface1, "up"]): - raise Exception("could not bring up %s" % iface1) - -if 0 != call(["cansend", iface1, "000#"]): - raise Exception("cansend doesn't work") - -print("CAN should work now") - -= Import CANSocket - -from scapy.contrib.cansocket_python_can import * - -new_can_socket = lambda iface: CANSocket(bustype='virtual', channel=iface) -new_can_socket0 = lambda: CANSocket(bustype='virtual', channel=iface0, timeout=0.01) -new_can_socket1 = lambda: CANSocket(bustype='virtual', channel=iface1, timeout=0.01) - -# utility function for draining a can interface, asserting that no packets are there -def drain_bus(iface=iface0, assert_empty=True): - with new_can_socket0() as s: - pkts = s.sniff(timeout=0.1) - if assert_empty: - assert len(pkts) == 0 - -print("CAN sockets should work now") - -= Overwrite definition for vcan_socket systems native sockets -~ vcan_socket not_pypy needs_root linux - -if six.PY3 and LINUX: - from scapy.contrib.cansocket_native import * - new_can_socket = lambda iface: CANSocket(iface) - new_can_socket0 = lambda: CANSocket(iface0) - new_can_socket1 = lambda: CANSocket(iface1) - - -= Overwrite definition for vcan_socket systems python-can sockets -~ vcan_socket needs_root linux -if "python_can" in CANSocket.__module__: - new_can_socket = lambda iface: CANSocket(bustype='socketcan', channel=iface, timeout=0.01) - new_can_socket0 = lambda: CANSocket(bustype='socketcan', channel=iface0, timeout=0.01) - new_can_socket1 = lambda: CANSocket(bustype='socketcan', channel=iface1, timeout=0.01) - -= Verify that a CAN socket can be created and closed -s = new_can_socket(iface0) -s.close() - - -= Check if can-isotp and can-utils are installed on this system -~ linux -p1 = subprocess.Popen(['lsmod'], stdout = subprocess.PIPE) -p2 = subprocess.Popen(['grep', '^can_isotp'], stdout = subprocess.PIPE, stdin=p1.stdout) -p1.stdout.close() -if p1.wait() == 0 and p2.wait() == 0 and b"can_isotp" in p2.stdout.read(): - p = subprocess.Popen(["isotpsend", "-s1", "-d0", iface0], stdin = subprocess.PIPE) - p.communicate(b"01") - if p.returncode == 0: - ISOTP_KERNEL_MODULE_AVAILABLE = True - - -+ Syntax check - -= Import isotp -conf.contribs['ISOTP'] = {'use-can-isotp-kernel-module': ISOTP_KERNEL_MODULE_AVAILABLE} -load_contrib("isotp") - -if six.PY3 and ISOTP_KERNEL_MODULE_AVAILABLE: - from scapy.contrib.isotp import ISOTPNativeSocket - ISOTPSocket = ISOTPNativeSocket - assert ISOTPSocket == ISOTPNativeSocket -else: - from scapy.contrib.isotp import ISOTPSoftSocket - ISOTPSocket = ISOTPSoftSocket - assert ISOTPSocket == ISOTPSoftSocket +from scapy.contrib.isotp.isotp_scanner import send_multiple_ext, filter_periodic_packets, scan_extended, scan +from test.testsocket import TestSocket +with open(scapy_path("test/contrib/automotive/interface_mockup.py")) as f: + exec(f.read()) = Test send_multiple_ext() pkt = ISOTPHeaderEA(identifier=0x100, extended_address=1)/ISOTP_FF(message_size=100, data=b'\x00\x00\x00\x00\x00') number_of_packets = 100 -def sender(): - with new_can_socket0() as sock1: - send_multiple_ext(sock1, 0, pkt, number_of_packets) - -thread = threading.Thread(target=sender) +with new_can_socket0() as sock1, new_can_socket0() as sock: + send_multiple_ext(sock1, 0, pkt, number_of_packets) + pkts = sock.sniff(timeout=4, count=number_of_packets) -with new_can_socket0() as sock: - pkts = sock.sniff(timeout=4, count=number_of_packets, started_callback=thread.start) - -thread.join(timeout=10) assert len(pkts) == number_of_packets = Test filter_periodic_packets() with periodic packets @@ -165,7 +50,6 @@ received_packets[40] = (outlier, outlier.identifier) filter_periodic_packets(received_packets) assert len(received_packets) == 1 - = Test filter_periodic_packets() with nonperiodic packets pkt = CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') received_packets = dict() @@ -177,616 +61,362 @@ for i in range(40): filter_periodic_packets(received_packets) assert len(received_packets) == 40 -= Define helper function for dynamic sniff time tests += define helper function -def test_dynamic(f): - for t in [1, 2, 4, 10]: - try: - drain_bus(iface0) - f(0.02 * t) - return True - except AssertionError as e: - if t < 10: - sys.stderr.write("Test failed. Automatically increase sniff time and retry." + os.linesep) - else: - raise e - return False +def make_noise(p, t): + for _ in range(20): + sock_noise.send(p) + time.sleep(t) -= Define test functions += test scan -def make_noise(p, t): - with new_can_socket0() as s: - for _ in range(40): - s.send(p) - time.sleep(t) - - -def test_scan(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(idx): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x700 + idx, did=0x600 + idx) as sock: - sock.sniff(timeout=sniff_time * 1500, count=1, - started_callback=semaphore.release) - listen_sockets = list() - for i in range(1, 4): - listen_sockets.append( - threading.Thread(target=isotpserver, args=(int(i),))) - listen_sockets[-1].start() - for _ in range(len(listen_sockets)): - semaphore.acquire() - with new_can_socket0() as scansock: - found_packets = scan(scansock, range(0x5ff, 0x604), noise_ids=[0x701], - sniff_time=sniff_time, verbose=True) - with new_can_socket0() as cans: - for _ in range(5): - cans.send(CAN(identifier=0x601, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x602, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x01\xaa')) - time.sleep(0) - print(len(listen_sockets)) - for thread in listen_sockets: - thread.join(timeout=1) - print(len(found_packets)) - assert len(found_packets) == 2 - - -def test_scan_extended(sniff_time=0.02): - recvpacket = CAN(flags=0, identifier=0x700, length=4, - data=b'\xaa0\x00\x00') - semaphore = threading.Semaphore(0) - def isotpserver(): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x700, did=0x601, - extended_addr=0xaa, extended_rx_addr=0xbb) as s: - s.sniff(timeout=1500 * sniff_time, count=1, - started_callback=semaphore.release) - thread = threading.Thread(target=isotpserver) - thread.start() - semaphore.acquire() - with new_can_socket0() as scansock: - found_packets = scan_extended(scansock, [0x600, 0x601], - extended_scan_range=range(0xb0, 0xc0), - sniff_time=sniff_time) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x601, data=b'\xbb\x01\xaa')) - thread.join(timeout=10) - fpkt = found_packets[list(found_packets.keys())[0]][0] - rpkt = recvpacket - assert fpkt.length == rpkt.length - assert fpkt.data == rpkt.data - assert fpkt.identifier == rpkt.identifier - - -def test_isotpscan_text(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x700 + i, did=0x600 + i) as isotpsock: - isotpsock.sniff(timeout=1500 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread_noise.start() - thread1 = threading.Thread(target=isotpserver, args=(2,)) - thread2 = threading.Thread(target=isotpserver, args=(3,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x5ff, 0x604 + 1), - output_format="text", - noise_listen_time=sniff_time * 6, - sniff_time=sniff_time, - verbose=True) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x601, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x602, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x01\xaa')) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - text = "\nFound 2 ISOTP-FlowControl Packet(s):" - assert text in result - assert "0x602" in result - assert "0x603" in result - assert "0x702" in result - assert "0x703" in result - assert "No Padding" in result - - -def test_isotpscan_text_extended_can_id(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, - sid=0x1ffff700 + i, - did=0x1ffff600 + i) as isotpsock1: - isotpsock1.sniff(timeout=1500 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x1ffff701, flags="extended", length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread_noise.start() - thread1 = threading.Thread(target=isotpserver, args=(2,)) - thread2 = threading.Thread(target=isotpserver, args=(3,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x1ffff5ff, 0x1ffff604 + 1), - output_format="text", - noise_listen_time=sniff_time * 6, - sniff_time=sniff_time, - extended_can_id=True, - verbose=True) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x1ffff601, flags="extended", - data=b'\x01\xaa')) - cans.send(CAN(identifier=0x1ffff602, flags="extended", - data=b'\x01\xaa')) - cans.send(CAN(identifier=0x1ffff603, flags="extended", - data=b'\x01\xaa')) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - print(result) - text = "\nFound 2 ISOTP-FlowControl Packet(s):" - assert text in result - assert "0x1ffff602" in result - assert "0x1ffff603" in result - assert "0x1ffff702" in result - assert "0x1ffff703" in result - assert "No Padding" in result - - -def test_isotpscan_code(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x700 + i, did=0x600 + i) as isotpsock: - isotpsock.sniff(timeout=1500 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread_noise.start() - thread1 = threading.Thread(target=isotpserver, args=(2,)) - thread2 = threading.Thread(target=isotpserver, args=(3,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x5ff, 0x603 + 1), - output_format="code", - noise_listen_time=sniff_time * 6, - sniff_time=sniff_time, - can_interface="can0", - verbose=True) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x601, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x602, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x01\xaa')) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - s1 = "ISOTPSocket(can0, sid=0x602, did=0x702, " \ - "padding=False, basecls=ISOTP)\n" - s2 = "ISOTPSocket(can0, sid=0x603, did=0x703, " \ - "padding=False, basecls=ISOTP)\n" - print(result) - assert s1 in result - assert s2 in result - - -def test_extended_isotpscan_code(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x700 + i, did=0x600 + i, - extended_addr=0x11, extended_rx_addr=0x22) as s: - s.sniff(timeout=1500 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread1 = threading.Thread(target=isotpserver, args=(2,)) - thread2 = threading.Thread(target=isotpserver, args=(3,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - thread_noise.start() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x5ff, 0x603 + 1), - extended_scan_range=range(0x20, 0x30), - extended_addressing=True, sniff_time=sniff_time, - noise_listen_time=sniff_time * 6, - output_format="code", - can_interface="can0", verbose=True) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x602, data=b'\x22\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x22\x01\xaa')) - time.sleep(0) - cans.send(CAN(identifier=0x602, data=b'\x22\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x22\x01\xaa')) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - s1 = "ISOTPSocket(can0, sid=0x602, did=0x702, padding=False, " \ - "extended_addr=0x22, extended_rx_addr=0x11, basecls=ISOTP)" - s2 = "ISOTPSocket(can0, sid=0x603, did=0x703, padding=False, " \ - "extended_addr=0x22, extended_rx_addr=0x11, basecls=ISOTP)" - print(result) - assert s1 in result - assert s2 in result - - -def test_extended_isotpscan_code_extended_can_id(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x1ffff700 + i, did=0x1ffff600 + i, - extended_addr=0x11, extended_rx_addr=0x22) as s: - s.sniff(timeout=1500 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x1ffff701, flags="extended", length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread1 = threading.Thread(target=isotpserver, args=(2,)) - thread2 = threading.Thread(target=isotpserver, args=(3,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - thread_noise.start() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x1ffff5ff, 0x1ffff604 + 1), - extended_can_id=True, - extended_scan_range=range(0x20, 0x30), - extended_addressing=True, - sniff_time=sniff_time, - noise_listen_time=sniff_time * 6, - output_format="code", - can_interface="can0", - verbose=True) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x1ffff602, flags="extended", - data=b'\x22\x01\xaa')) - cans.send(CAN(identifier=0x1ffff603, flags="extended", - data=b'\x22\x01\xaa')) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - s1 = "ISOTPSocket(can0, sid=0x1ffff602, did=0x1ffff702, padding=False, " \ - "extended_addr=0x22, extended_rx_addr=0x11, basecls=ISOTP)" - s2 = "ISOTPSocket(can0, sid=0x1ffff603, did=0x1ffff703, padding=False, " \ - "extended_addr=0x22, extended_rx_addr=0x11, basecls=ISOTP)" - print(result) - assert s1 in result - assert s2 in result - - -def test_isotpscan_none(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x700 + i, did=0x600 + i) as s: - s.sniff(timeout=1500 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread1 = threading.Thread(target=isotpserver, args=(2,)) - thread2 = threading.Thread(target=isotpserver, args=(3,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - with new_can_socket0() as socks_interface: - thread_noise.start() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x5ff, 0x603 + 1), - can_interface=socks_interface, - sniff_time=sniff_time, - noise_listen_time=sniff_time * 6, - verbose=True) - result = sorted(result, key=lambda x: x.src) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x601, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x602, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x01\xaa')) - for s in result: - # This helps to close ISOTPSoftSockets - cans.send(CAN(identifier=0x702, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x703, data=b'\x01\xaa')) - s.close() - cans.send(CAN(identifier=0x702, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x703, data=b'\x01\xaa')) - time.sleep(0) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - assert len(result) == 2 - assert 0x602 == result[0].src - assert 0x702 == result[0].dst - assert 0x603 == result[1].src - assert 0x703 == result[1].dst - for s in result: - del s - - -def test_isotpscan_none_2(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, ISOTPSocket(isocan, sid=0x700 + i, - did=0x600 + i) as s: - s.sniff(timeout=1000 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread1 = threading.Thread(target=isotpserver, args=(9,)) - thread2 = threading.Thread(target=isotpserver, args=(8,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - thread_noise.start() - with new_can_socket0() as socks_interface: - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x607, 0x60A), - can_interface=socks_interface, - sniff_time=sniff_time, - noise_listen_time=sniff_time * 6, - verbose=True) - result = sorted(result, key=lambda x: x.src) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x609, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x608, data=b'\x01\xaa')) - for s in result: - # This helps to close ISOTPSoftSockets - cans.send(CAN(identifier=0x709, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x708, data=b'\x01\xaa')) - s.close() - time.sleep(0) - cans.send(CAN(identifier=0x709, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x708, data=b'\x01\xaa')) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - assert len(result) == 2 - assert 0x608 == result[0].src - assert 0x708 == result[0].dst - assert 0x609 == result[1].src - assert 0x709 == result[1].dst - for s in result: - del s - - -def test_extended_isotpscan_none(sniff_time=0.02): - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x700 + i, did=0x600 + i, - extended_addr=0x11, extended_rx_addr=0x22) as s: - s.sniff(timeout=500 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - thread1 = threading.Thread(target=isotpserver, args=(2,)) - thread2 = threading.Thread(target=isotpserver, args=(3,)) - thread1.start() - thread2.start() - semaphore.acquire() - semaphore.acquire() - with new_can_socket0() as socks_interface: - thread_noise.start() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x5ff, 0x603 + 1), - extended_scan_range=range(0x20, 0x30), - extended_addressing=True, - can_interface=socks_interface, - sniff_time=sniff_time, - noise_listen_time=sniff_time * 6, - verbose=True) - result = sorted(result, key=lambda x: x.src) - with new_can_socket0() as cans: - cans.send(CAN(identifier=0x602, data=b'\x22\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x22\x01\xaa')) - time.sleep(0.00) - cans.send(CAN(identifier=0x602, data=b'\x22\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x22\x01\xaa')) - time.sleep(0.00) - cans.send(CAN(identifier=0x602, data=b'\x22\x01\xaa')) - cans.send(CAN(identifier=0x603, data=b'\x22\x01\xaa')) - for s in result: - # This helps to close ISOTPSoftSockets - cans.send(CAN(identifier=0x702, data=b'\x11\x01\xaa')) - cans.send(CAN(identifier=0x703, data=b'\x11\x01\xaa')) - s.close() - time.sleep(0) - cans.send(CAN(identifier=0x702, data=b'\x11\x01\xaa')) - cans.send(CAN(identifier=0x703, data=b'\x11\x01\xaa')) - thread1.join(timeout=10) - thread2.join(timeout=10) - thread_noise.join(timeout=10) - assert len(result) == 2 - assert 0x602 == result[0].src - assert 0x702 == result[0].dst - assert 0x22 == result[0].exsrc - assert 0x11 == result[0].exdst - assert 0x603 == result[1].src - assert 0x703 == result[1].dst - assert 0x22 == result[1].exsrc - assert 0x11 == result[1].exdst - for s in result: - del s - - -def test_isotpscan_none_random_ids(sniff_time=0.02): - rnd = RandNum(0x1, 0x50) - ids = set(rnd._fix() for _ in range(10)) - print(ids) - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x100 + i, did=i) as s: - s.sniff(timeout=700 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - threads = [threading.Thread(target=isotpserver, args=(x,)) for x in ids] - [t.start() for t in threads] - for _ in range(len(threads)): - semaphore.acquire() - with new_can_socket0() as socks_interface: - thread_noise.start() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x001, 0x51), - can_interface=socks_interface, - noise_listen_time=sniff_time * 6, - sniff_time=sniff_time, - verbose=True) - result = sorted(result, key=lambda x: x.src) - with new_can_socket0() as cans: - for i in ids: - # This helps to close ISOTPSoftSockets - cans.send(CAN(identifier=i, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x100 + i, data=b'\x01\xaa')) - time.sleep(0) - cans.send(CAN(identifier=i, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x100 + i, data=b'\x01\xaa')) - for s in result: - # This helps to close ISOTPSoftSockets - cans.send(CAN(identifier=s.dst, data=b'\x01\xaa')) - cans.send(CAN(identifier=s.src, data=b'\x01\xaa')) - s.close() - time.sleep(0) - cans.send(CAN(identifier=s.dst, data=b'\x01\xaa')) - cans.send(CAN(identifier=s.src, data=b'\x01\xaa')) - [t.join(timeout=10) for t in threads] - thread_noise.join(timeout=10) - assert len(result) == len(ids) - ids = sorted(ids) - for i, s in zip(ids, result): - assert i == s.src - assert i + 0x100 == s.dst - for s in result: - del s - - -def test_isotpscan_none_random_ids_padding(sniff_time=0.02): - rnd = RandNum(0x1, 0x50) - ids = set(rnd._fix() for _ in range(10)) - semaphore = threading.Semaphore(0) - def isotpserver(i): - with new_can_socket0() as isocan, \ - ISOTPSocket(isocan, sid=0x100 + i, did=i, padding=True) as s: - s.sniff(timeout=700 * sniff_time, count=1, - started_callback=semaphore.release) - pkt = CAN(identifier=0x701, length=8, - data=b'\x01\x02\x03\x04\x05\x06\x07\x08') - thread_noise = threading.Thread(target=make_noise, args=(pkt, sniff_time,)) - threads = [threading.Thread(target=isotpserver, args=(x,)) for x in ids] - [t.start() for t in threads] - for _ in range(len(threads)): - semaphore.acquire() - with new_can_socket0() as socks_interface: - thread_noise.start() - with new_can_socket0() as scansock: - result = ISOTPScan(scansock, range(0x001, 0x51), - can_interface=socks_interface, - noise_listen_time=sniff_time * 6, - sniff_time=sniff_time, - verbose=True) - result = sorted(result, key=lambda x: x.src) - with new_can_socket0() as cans: - for i in ids: - # This helps to close ISOTPSoftSockets - cans.send(CAN(identifier=i, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x100 + i, data=b'\x01\xaa')) - time.sleep(0) - cans.send(CAN(identifier=i, data=b'\x01\xaa')) - cans.send(CAN(identifier=0x100 + i, data=b'\x01\xaa')) - for s in result: - # This helps to close ISOTPSoftSockets - cans.send(CAN(identifier=s.dst, data=b'\x01\xaa')) - cans.send(CAN(identifier=s.src, data=b'\x01\xaa')) - s.close() - cans.send(CAN(identifier=s.dst, data=b'\x01\xaa')) - cans.send(CAN(identifier=s.src, data=b'\x01\xaa')) - time.sleep(0) - [t.join(timeout=10) for t in threads] - thread_noise.join(timeout=10) - assert len(result) == len(ids) - ids = sorted(ids) - for i, s in zip(ids, result): - assert i == s.src - assert i + 0x100 == s.dst - if isinstance(s, ISOTPSoftSocket): - assert s.impl.padding is True - for s in result: - del s - -= Test scan() - -test_dynamic(test_scan) - -= Test scan_extended() - -test_dynamic(test_scan_extended) - -= Test ISOTPScan(output_format=text) - -test_dynamic(test_isotpscan_text) - -= Test ISOTPScan(output_format=text) extended_can_id - -test_dynamic(test_isotpscan_text_extended_can_id) - -= Test ISOTPScan(output_format=code) - -test_dynamic(test_isotpscan_code) - -= Test extended ISOTPScan(output_format=code) - -test_dynamic(test_extended_isotpscan_code) - -= Test extended ISOTPScan(output_format=code) extended_can_id - -test_dynamic(test_extended_isotpscan_code_extended_can_id) - -= Test ISOTPScan(output_format=None) - -test_dynamic(test_isotpscan_none) - -= Test ISOTPScan(output_format=None) 2 - -test_dynamic(test_isotpscan_none_2) - -= Test extended ISOTPScan(output_format=None) +sock_sender = TestSocket(CAN) -test_dynamic(test_extended_isotpscan_none) +sockets = list() +for idx in range(1, 4): + sock_recv = TestSocket(CAN) + sock_sender.pair(sock_recv) + sockets.append(ISOTPSoftSocket(sock_recv, tx_id=0x700 + idx, rx_id=0x600 + idx)) -= Test ISOTPScan(output_format=None) random IDs +found_packets = scan(sock_sender, range(0x5ff, 0x604), + noise_ids=[0x701], sniff_time=0.1) -test_dynamic(test_isotpscan_none_random_ids) +for s in sockets: + s.close() -= Test ISOTPScan(output_format=None) random IDs padding +assert len(found_packets) == 2 +assert found_packets[0x602][0].identifier == 0x702 +assert found_packets[0x603][0].identifier == 0x703 -test_dynamic(test_isotpscan_none_random_ids_padding) += test scan extended + +sock_sender = TestSocket(CAN) +sock_recv = TestSocket(CAN) +sock_sender.pair(sock_recv) + +with ISOTPSoftSocket(sock_recv, tx_id=0x700, rx_id=0x601, ext_address=0xaa, rx_ext_address=0xbb): + found_packets = scan_extended(sock_sender, [0x600, 0x601], + extended_scan_range=range(0xb0, 0xc0), + sniff_time=0.1) + +fpkt = found_packets[list(found_packets.keys())[0]][0] +rpkt = CAN(flags=0, identifier=0x700, length=4, data=b'\xaa0\x00\x00') +assert fpkt.length == rpkt.length +assert fpkt.data == rpkt.data +assert fpkt.identifier == rpkt.identifier + += scan with text output + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + output_format="text", + noise_listen_time=0.1, + sniff_time=0.02, + verbose=False) + +text = "\nFound 2 ISOTP-FlowControl Packet(s):" +assert text in result +assert "0x602" in result +assert "0x603" in result +assert "0x702" in result +assert "0x703" in result +assert "No Padding" in result + += scan with text output padding + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602, padding=True), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603, padding=True): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + output_format="text", + noise_listen_time=0.1, + sniff_time=0.02, + verbose=False) + +text = "\nFound 2 ISOTP-FlowControl Packet(s):" +assert text in result +assert "0x602" in result +assert "0x603" in result +assert "0x702" in result +assert "0x703" in result +assert "Padding enabled" in result + += scan with text output extended_can id + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x1ffff702, rx_id=0x1ffff602), ISOTPSoftSocket(sock_recv2, tx_id=0x1ffff703, rx_id=0x1ffff603): + pkt = CAN(identifier=0x1ffff701, flags="extended", length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x1ffff5ff, 0x1ffff604 + 1), + output_format="text", + noise_listen_time=0.1, + sniff_time=0.02, + extended_can_id=True, + verbose=False) + +text = "\nFound 2 ISOTP-FlowControl Packet(s):" +assert text in result +assert "0x1ffff602" in result +assert "0x1ffff603" in result +assert "0x1ffff702" in result +assert "0x1ffff703" in result +assert "No Padding" in result + += scan with code output + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + output_format="code", + noise_listen_time=0.1, + sniff_time=0.02, + can_interface="can0", + verbose=False) + +s1 = "ISOTPSocket(can0, tx_id=0x602, rx_id=0x702, " \ + "padding=False, fd=False, basecls=ISOTP)\n" +s2 = "ISOTPSocket(can0, tx_id=0x603, rx_id=0x703, " \ + "padding=False, fd=False, basecls=ISOTP)\n" +assert s1 in result +assert s2 in result + += scan with json output + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + output_format="json", + noise_listen_time=0.1, + sniff_time=0.02, + can_interface="can0", + verbose=False) + +s1 = "\"iface\": \"can0\", \"tx_id\": 1538, \"rx_id\": 1794, " \ + "\"padding\": false, \"fd\": false, \"basecls\": \"ISOTP\"" +s2 = "\"iface\": \"can0\", \"tx_id\": 1539, \"rx_id\": 1795, " \ + "\"padding\": false, \"fd\": false, \"basecls\": \"ISOTP\"" +print(result) +assert s1 in result +assert s2 in result + += scan with code output noise + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603): + pkt = CAN(identifier=0x702, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + output_format="code", + noise_listen_time=0.1, + sniff_time=0.02, + can_interface="can0", + verbose=False) + +s1 = "ISOTPSocket(can0, tx_id=0x602, rx_id=0x702, " \ + "padding=False, fd=False, basecls=ISOTP)\n" +s2 = "ISOTPSocket(can0, tx_id=0x603, rx_id=0x703, " \ + "padding=False, fd=False, basecls=ISOTP)\n" +assert s1 not in result +assert s2 in result + += scan with code output extended_isotp + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602, ext_address=0x11, rx_ext_address=0x22), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603, ext_address=0x11, rx_ext_address=0x22): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + output_format="code", + noise_listen_time=0.1, + sniff_time=0.05, + extended_scan_range=range(0x20, 0x30), + extended_addressing=True, + can_interface="can0", + verbose=False) + +s1 = "ISOTPSocket(can0, tx_id=0x602, rx_id=0x702, padding=False, " \ + "ext_address=0x22, rx_ext_address=0x11, fd=False, basecls=ISOTP)" +s2 = "ISOTPSocket(can0, tx_id=0x603, rx_id=0x703, padding=False, " \ + "ext_address=0x22, rx_ext_address=0x11, fd=False, basecls=ISOTP)" +assert s1 in result +assert s2 in result + += scan with code output extended_isotp extended can id + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x1ffff702, rx_id=0x1ffff602, ext_address=0x11, rx_ext_address=0x22), ISOTPSoftSocket(sock_recv2, tx_id=0x1ffff703, rx_id=0x1ffff603, ext_address=0x11, rx_ext_address=0x22): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x1ffff5ff, 0x1ffff604 + 1), + output_format="code", + noise_listen_time=0.1, + sniff_time=0.05, + extended_scan_range=range(0x20, 0x30), + extended_addressing=True, + can_interface="can0", + verbose=False) + +s1 = "ISOTPSocket(can0, tx_id=0x1ffff602, rx_id=0x1ffff702, padding=False, " \ + "ext_address=0x22, rx_ext_address=0x11, fd=False, basecls=ISOTP)" +s2 = "ISOTPSocket(can0, tx_id=0x1ffff603, rx_id=0x1ffff703, padding=False, " \ + "ext_address=0x22, rx_ext_address=0x11, fd=False, basecls=ISOTP)" +print(result) +assert s1 in result +assert s2 in result + += scan default output + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + noise_listen_time=0.1, + sniff_time=0.02, + can_interface=new_can_socket0(), + verbose=False) + +assert 0x602 == result[0].tx_id +assert 0x702 == result[0].rx_id +assert 0x603 == result[1].tx_id +assert 0x703 == result[1].rx_id + +for s in result: + s.close() + del s + += scan default output extended + +sock_sender = TestSocket(CAN) +sock_recv1 = TestSocket(CAN) +sock_sender.pair(sock_recv1) +sock_recv2 = TestSocket(CAN) +sock_sender.pair(sock_recv2) +sock_noise = TestSocket(CAN) +sock_sender.pair(sock_noise) + +with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602, ext_address=0x11, rx_ext_address=0x22), ISOTPSoftSocket(sock_recv2, tx_id=0x703, rx_id=0x603, ext_address=0x11, rx_ext_address=0x22): + pkt = CAN(identifier=0x701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') + make_noise(pkt, 0.01) + result = isotp_scan(sock_sender, range(0x5ff, 0x604 + 1), + noise_listen_time=0.1, + sniff_time=0.02, + extended_scan_range=range(0x20, 0x30), + extended_addressing=True, + can_interface=new_can_socket0(), + verbose=False) + +assert 0x602 == result[0].tx_id +assert 0x702 == result[0].rx_id +assert 0x22 == result[0].ext_address +assert 0x11 == result[0].rx_ext_address +assert 0x603 == result[1].tx_id +assert 0x703 == result[1].rx_id +assert 0x22 == result[1].ext_address +assert 0x11 == result[1].rx_ext_address + +for s in result: + s.close() + del s + ++ Cleanup = Delete vcan interfaces -~ vcan_socket needs_root linux -if 0 != call(["sudo", "ip", "link", "delete", iface0]): - raise Exception("%s could not be deleted" % iface0) +assert cleanup_interfaces() + ++ Coverage stability tests + += empty tests + +from scapy.contrib.isotp.isotp_scanner import generate_code_output, generate_text_output + +assert generate_code_output("", None) == "" +assert generate_text_output("") == "No packets found." + += get_isotp_fc + +from scapy.contrib.isotp.isotp_scanner import get_isotp_fc -if 0 != call(["sudo", "ip", "link", "delete", iface1]): - raise Exception("%s could not be deleted" % iface1) +# to trigger "noise_ids.append(packet.identifier)" +a = [] +get_isotp_fc( + 1, [], a, False, + Bunch( + flags="extended", + identifier=1, + data=b"\x00" + ) +) +assert 1 in a diff --git a/test/contrib/knx.uts b/test/contrib/knx.uts new file mode 100644 index 00000000000..c7ab8abf7ee --- /dev/null +++ b/test/contrib/knx.uts @@ -0,0 +1,75 @@ +% knx layer test campaign + ++ Syntax check += Import the knx layer +from scapy.contrib.knx import * + ++ Test KNX Header += Header default values +pkt = KNX() +assert raw(pkt) == b'\x06\x10\x00\x00\x00\x06' + += KNX Header payload length calculation +pkt = KNX(service_identifier=0x0203)/KNXDescriptionRequest() +assert raw(pkt)[4:6] == b'\x00\x0e' + += KNX Header Guess Payload KNXSearchRequest +p = KNX(b'\x06\x10\x02\x01\x00\x0e\x08\x01\x00\x00\x00\x00\x00\x00') +assert isinstance(p.payload, KNXSearchRequest) + += KNX Header Guess Payload KNXSearchResponse +p = KNX(b'\x06\x10\x02\x02\x00F\x08\x01\x00\x00\x00\x00\x00\x006\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02') +assert isinstance(p.payload, KNXSearchResponse) + += KNX Header Guess Payload KNXDescriptionRequest +p = KNX(b'\x06\x10\x02\x03\x00\x0e\x08\x01\x00\x00\x00\x00\x00\x00') +assert isinstance(p.payload, KNXDescriptionRequest) + += KNX Header Guess Payload KNXDescriptionResponse +p = KNX(b'\x06\x10\x02\x04\x00>6\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02') +assert isinstance(p.payload, KNXDescriptionResponse) + += KNX Header Guess Payload KNXConnectRequest +p = KNX(b'\x06\x10\x02\x05\x00\x18\x08\x01\x00\x00\x00\x00\x00\x00\x08\x01\x00\x00\x00\x00\x00\x00\x02\x03') +assert isinstance(p.payload, KNXConnectRequest) + += KNX Header Guess Payload KNXConnectResponse +p = KNX(b'\x06\x10\x02\x06\x00\x12\x00\x00\x08\x01\x00\x00\x00\x00\x00\x00\x02\x03') +assert isinstance(p.payload, KNXConnectResponse) + += KNX Header Guess Payload KNXConnectionstateRequest +p = KNX(b'\x06\x10\x02\x07\x00\x10\x00\x00\x08\x01\x00\x00\x00\x00\x00\x00') +assert isinstance(p.payload, KNXConnectionstateRequest) + += KNX Header Guess Payload KNXConnectionstateResponse +p = KNX(b'\x06\x10\x02\x08\x00\x08\x00\x00') +assert isinstance(p.payload, KNXConnectionstateResponse) + += KNX Header Guess Payload KNXDisconnectRequest +p = KNX(b'\x06\x10\x02\t\x00\x10\x01\x00\x08\x01\x00\x00\x00\x00\x00\x00') +assert isinstance(p.payload, KNXDisconnectRequest) + += KNX Header Guess Payload KNXDisconnectResponse +p = KNX(b'\x06\x10\x02\n\x00\x08\x00\x00') +assert isinstance(p.payload, KNXDisconnectResponse) + += KNX Header Guess Payload KNXConfigurationRequest +p = KNX(b'\x06\x10\x03\x10\x00\x15\x04\x01\x00\x00\x00\x00\xbc\xe0\x00\x00\n\x03\x01\x00\x80') +assert isinstance(p.payload, KNXConfigurationRequest) + += KNX Header Guess Payload KNXConfigurationACK +p = KNX(b'\x06\x10\x03\x11\x00\n\x04\x01\x00\x00') +assert isinstance(p.payload, KNXConfigurationACK) + += KNX Header Guess Payload KNXTunnelingRequest +p = KNX(b'\x06\x10\x04 \x00\x15\x04\x01\x00\x00\x00\x00\xbc\xe0\x00\x00\n\x03\x01\x00\x80') +assert isinstance(p.payload, KNXTunnelingRequest) + += KNX Header Guess Payload KNXTunnelingACK +p = KNX(b'\x06\x10\x04!\x00\n\x04\x01\x00\x00') +assert isinstance(p.payload, KNXTunnelingACK) + ++ Test layer binding += Destination port + + diff --git a/test/contrib/lacp.uts b/test/contrib/lacp.uts index 63ef27d066a..3c778e890d1 100644 --- a/test/contrib/lacp.uts +++ b/test/contrib/lacp.uts @@ -13,13 +13,13 @@ params = dict( actor_system='00:13:c4:12:0f:00', actor_key=13, actor_port_priority=32768, - actor_port_numer=22, + actor_port_number=22, actor_state=0x85, partner_system_priority=32768, partner_system='00:0e:83:16:f5:00', partner_key=13, partner_port_priority=32768, - partner_port_numer=25, + partner_port_number=25, partner_state=0x36, collector_max_delay=32768, ) @@ -32,11 +32,11 @@ raw_pkt = b'\x01\x80\xc2\x00\x00\x02\x00\x13\xc4\x12\x0f\x0d\x88\x09\x01\x01\x01 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -assert(s == raw_pkt) +assert s == raw_pkt p = Ether(s) -assert(SlowProtocol in p and LACP in p) -assert(raw(p) == raw_pkt) +assert SlowProtocol in p and LACP in p +assert raw(p) == raw_pkt = Marker sanity @@ -44,5 +44,5 @@ pkt = Ether(src="00:13:c4:12:0f:0d") / SlowProtocol() / MarkerProtocol() pkt.show() s = raw(pkt) p = Ether(s) -assert(SlowProtocol in p and MarkerProtocol in p) -assert(raw(p) == s) +assert SlowProtocol in p and MarkerProtocol in p +assert raw(p) == s diff --git a/test/contrib/ldp.uts b/test/contrib/ldp.uts index 5e184bcdd7d..af6bcbb17d5 100644 --- a/test/contrib/ldp.uts +++ b/test/contrib/ldp.uts @@ -44,5 +44,6 @@ assert pkti.params == [180, 0, 0, 0, 0, '1.1.2.2', 0] pkta = LDPAddress(address=['1.1.2.2', '172.16.2.1'])/LDPLabelMM(fec=[('172.16.2.0', 31)])/LDPLabelMM(fec=[('1.1.2.2', 32)])/LDPLabelMM(fec=[('1.1.2.1', 32)]) = Advanced dissection - complex LDP +load_contrib("mpls") pkt = Ether(b"\xcc\x04\x04\xdc\x00\x10\xcc\x03\x04\xdc\x00\x10\x88G\x00\x01-\xfeE\xc0\x014\xfe\x84\x00\x00\xff\x06\xb5z\x01\x01\x02\x02\x01\x01\x02\x01\xe4\xe4\x02\x86\xbf\xfb'\xe4\xb9\xb3\xe4GP\x10\x0e\xb6v\x9f\x00\x00\x00\x01\x01\x08\x01\x01\x02\x02\x00\x00\x03\x00\x00\x12\x00\x00\x00\x0e\x01\x01\x00\n\x00\x01\x01\x01\x02\x02\xac\x10\x02\x01\x04\x00\x00\x18\x00\x00\x00\x0f\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x02\x00\x02\x00\x00\x04\x00\x00\x00\x03\x04\x00\x00\x18\x00\x00\x00\x10\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x02\x02\x02\x00\x00\x04\x00\x00\x00\x03\x04\x00\x00\x18\x00\x00\x00\x11\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x02\x01\x02\x00\x00\x04\x00\x00\x00\x12\x04\x00\x00\x18\x00\x00\x00\x12\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x01\x02\x02\x00\x00\x04\x00\x00\x00\x13\x04\x00\x00\x18\x00\x00\x00\x13\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x01\x01\x02\x00\x00\x04\x00\x00\x00\x14\x04\x00\x00\x18\x00\x00\x00\x14\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x01\x00\x02\x00\x00\x04\x00\x00\x00\x15\x04\x00\x00\x18\x00\x00\x00\x15\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x00\x00\x02\x00\x00\x04\x00\x00\x00\x16\x04\x00\x00$\x00\x00\x00\x16\x01\x00\x00\x14\x80\x80\x05\x0c\x00\x00\x00\x00\x00\x00\x00\n\x01\x04\x05\xdc\x0c\x04\x03\x02\x02\x00\x00\x04\x00\x00\x00\x10") assert pkt.getlayer(LDPLabelMM, 8).fec == [('0.0.0.0', 12), ('0.0.0.0', 0), ('5.0.0.0', 4), ('2.0.0.0', 3)] diff --git a/test/contrib/lldp.uts b/test/contrib/lldp.uts index f77f22dc426..bc9ed43b1d9 100644 --- a/test/contrib/lldp.uts +++ b/test/contrib/lldp.uts @@ -1,4 +1,3 @@ -from enum import test % LLDP test campaign # @@ -26,6 +25,16 @@ frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/ \ frm = frm.build() frm = Ether(frm) += build: check length calculation (#GH3107) + +frame = Ether(src='aa:bb:cc:dd:ee:ff', dst='11:22:33:44:55:66') / \ + LLDPDUChassisID(subtype=0x04, id='aa:bb:cc:dd:ee:ff') / \ + LLDPDUPortID(subtype=0x05, id='1') / \ + LLDPDUTimeToLive(ttl=5) / \ + LLDPDUManagementAddress(management_address_subtype=0x01, management_address=socket.inet_aton('192.168.0.10')) +data = b'\x11"3DUf\xaa\xbb\xcc\xdd\xee\xff\x88\xcc\x02\x07\x04\xaa\xbb\xcc\xdd\xee\xff\x04\x02\x051\x06\x02\x00\x05\x10\x0c\x05\x01\xc0\xa8\x00\n\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +assert bytes(frame) == data + = add padding if required conf.contribs['LLDP'].strict_mode_disable() @@ -34,8 +43,8 @@ frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC) / \ LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id='06:05:04:03:02:01') / \ LLDPDUTimeToLive() / \ LLDPDUEndOfLLDPDU() -assert(len(raw(frm)) == 60) -assert(len(raw(Ether(raw(frm))[Padding])) == 24) +assert len(raw(frm)) == 60 +assert len(raw(Ether(raw(frm))[Padding])) == 24 frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC) / \ LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_INTERFACE_NAME, id='eth012345678901234567890123') / \ @@ -74,6 +83,65 @@ assert pkt[LLDPDUPortID].fields_desc[2].i2s == LLDPDUPortID.LLDP_PORT_ID_TLV_SUB assert pkt[LLDPDUChassisID]._length == 7 assert pkt[LLDPDUPortID]._length == 7 += Network families / addresses in IDs + +# IPv4 + +pkt = Ether()/LLDPDUChassisID(subtype=0x05, family=1, id="1.1.1.1")/LLDPDUPortID(subtype=0x04, family=1, id="2.2.2.2")/LLDPDUTimeToLive()/LLDPDUEndOfLLDPDU() +pkt = Ether(raw(pkt)) +assert pkt[LLDPDUChassisID].id == "1.1.1.1" +assert pkt[LLDPDUPortID].id == "2.2.2.2" + +pkt = Ether(hex_bytes(b'ffffffffffff0242ac11000288cc02060501010101010406040102020202060200140000')) +assert pkt[LLDPDUChassisID].id == "1.1.1.1" +assert pkt[LLDPDUPortID].id == "2.2.2.2" + +try: + pkt = Ether()/LLDPDUChassisID(subtype=0x05, family=1, id="2001::abcd")/LLDPDUPortID()/LLDPDUTimeToLive()/LLDPDUEndOfLLDPDU() + assert False +except (socket.gaierror, AssertionError): + pass + +try: + pkt = Ether()/LLDPDUChassisID()/LLDPDUPortID(subtype=0x04, family=1, id="2001::abcd")/LLDPDUTimeToLive()/LLDPDUEndOfLLDPDU() + assert False +except (socket.gaierror, AssertionError): + pass + +# IPv6 + +pkt = Ether()/LLDPDUChassisID(subtype=0x05, family=2, id="1111::2222")/LLDPDUPortID(subtype=0x04, family=2, id="2001::abcd")/LLDPDUTimeToLive()/LLDPDUEndOfLLDPDU() +pkt = Ether(raw(pkt)) +assert pkt[LLDPDUChassisID].id == "1111::2222" +assert pkt[LLDPDUPortID].id == "2001::abcd" + +pkt = Ether(hex_bytes(b'ffffffffffff0242ac11000288cc0212050211110000000000000000000000002222041204022001000000000000000000000000abcd060200140000')) +assert pkt[LLDPDUChassisID].id == "1111::2222" +assert pkt[LLDPDUPortID].id == "2001::abcd" + +try: + pkt = Ether()/LLDPDUChassisID(subtype=0x05, family=2, id="1.1.1.1")/LLDPDUPortID()/LLDPDUTimeToLive()/LLDPDUEndOfLLDPDU() + assert False +except (socket.gaierror, AssertionError): + pass + +try: + pkt = Ether()/LLDPDUChassisID()/LLDPDUPortID(subtype=0x04, family=2, id="1.1.1.1")/LLDPDUTimeToLive()/LLDPDUEndOfLLDPDU() + assert False +except (socket.gaierror, AssertionError): + pass + +# Other + +pkt = Ether()/LLDPDUChassisID(subtype=0x05, id=b"\x00\x07\xab")/LLDPDUPortID(subtype=0x04, id=b"\x07\xaa\xbb\xcc")/LLDPDUTimeToLive()/LLDPDUEndOfLLDPDU() +pkt = Ether(raw(pkt)) +assert pkt[LLDPDUChassisID].id == b"\x00\x07\xab" +assert pkt[LLDPDUPortID].id == b"\x07\xaa\xbb\xcc" + +pkt = Ether(hex_bytes(b'ffffffffffff0242ac11000288cc020505000007ab0406040007aabbcc060200140000')) +assert pkt[LLDPDUChassisID].id == b"\x00\x07\xab" +assert pkt[LLDPDUPortID].id == b"\x07\xaa\xbb\xcc" + + strict mode handling - build = basic frame structure @@ -259,14 +327,14 @@ three_b_enum_field = ThreeBytesEnumField('test', 0x00, 0x112233: '1#2#3' }) -assert(three_b_enum_field.i2repr(None, 0) == 'zero') -assert(three_b_enum_field.i2repr(None, 0x0e) == 'fourteen') -assert(three_b_enum_field.i2repr(None, 0x5566) == 'five-six') -assert(three_b_enum_field.i2repr(None, 0x112233) == '1#2#3') -assert(three_b_enum_field.i2repr(None, 0x0e0000) == 'fourteen-zero-zero') -assert(three_b_enum_field.i2repr(None, 0x0e0100) == 'fourteen-one-zero') -assert(three_b_enum_field.i2repr(None, 0x01) == '1') -assert(three_b_enum_field.i2repr(None, 0x49763) == '300899') +assert three_b_enum_field.i2repr(None, 0) == 'zero' +assert three_b_enum_field.i2repr(None, 0x0e) == 'fourteen' +assert three_b_enum_field.i2repr(None, 0x5566) == 'five-six' +assert three_b_enum_field.i2repr(None, 0x112233) == '1#2#3' +assert three_b_enum_field.i2repr(None, 0x0e0000) == 'fourteen-zero-zero' +assert three_b_enum_field.i2repr(None, 0x0e0100) == 'fourteen-one-zero' +assert three_b_enum_field.i2repr(None, 0x01) == '1' +assert three_b_enum_field.i2repr(None, 0x49763) == '300899' = LLDPDUGenericOrganisationSpecific tests @@ -283,11 +351,11 @@ frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ frm = frm.build() frm = Ether(frm) org_spec_layer = frm[LLDPDUGenericOrganisationSpecific] -assert(org_spec_layer) -assert(org_spec_layer._type == 127) -assert(org_spec_layer.org_code == LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO) -assert(org_spec_layer.subtype == 0x42) -assert(org_spec_layer._length == 34) +assert org_spec_layer +assert org_spec_layer._type == 127 +assert org_spec_layer.org_code == LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO +assert org_spec_layer.subtype == 0x42 +assert org_spec_layer._length == 34 l="A" * 24 c=LLDPDUChassisID.SUBTYPE_CHASSIS_COMPONENT @@ -302,3 +370,490 @@ try: frm = frm.build() except: assert False + ++ Power via MDI +~ tshark + += Define check_tshark function + +def check_tshark(pkt, frame_type, selector): + import tempfile, os + fd, pcapfilename = tempfile.mkstemp() + wrpcap(pcapfilename, pkt) + rv = tcpdump(pcapfilename, prog=conf.prog.tshark, getfd=True, + args=['-Y', frame_type, '-T', 'fields', '-e', selector], dump=True, wait=True) + os.close(fd) + os.unlink(pcapfilename) + return rv.decode("utf8").strip() + += Power via MDI tests + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + )/\ + LLDPDUPowerViaMDI(MDI_power_support='PSE MDI power enabled+PSE MDI power supported', + PSE_power_pair='alt B', + power_class='class 3')/\ + LLDPDUEndOfLLDPDU() + +frm = frm.build() +frm = Ether(frm) +poe_layer = frm[LLDPDUPowerViaMDI] +# Legacy PoE TLV is not supported by WireShark +assert poe_layer +assert poe_layer._type == 127 +assert int(check_tshark(frm, "lldp", "lldp.tlv.type").split(',')[-2], 0) == 127 +assert poe_layer.org_code == LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3 +assert int(check_tshark(frm, "lldp", "lldp.orgtlv.oui").split(',')[-1], 0) == 4623 +assert poe_layer.subtype == 2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.subtype"), 0) == 0x02 +assert poe_layer._length == 7 +assert int(check_tshark(frm, "lldp", "lldp.tlv.len").split(',')[-2], 0) == 7 +assert poe_layer.MDI_power_support == 6 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_support"), 0) == 6 +assert poe_layer.PSE_power_pair == 2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_pse_pair"), 0) == 2 +assert poe_layer.power_class == 4 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_class"), 0) == 4 + + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + ) +# invalid length +try: + Ether((frm/ + LLDPDUPowerViaMDI(_length=8)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidLengthField: + pass + += Power via MDI with DDL classification extension tests + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + )/\ + LLDPDUPowerViaMDIDDL(MDI_power_support='PSE pairs controlled+PSE MDI power enabled', + PSE_power_pair='alt A', + power_class='class 4 and above', + power_type_no='type 2', + power_type_dir='PSE', + power_source='backup source', + power_prio='high', + PD_requested_power=2.21111, + PSE_allocated_power=1.521212121)/\ + LLDPDUEndOfLLDPDU() + +frm = frm.build() +frm = Ether(frm) +poe_layer = frm[LLDPDUPowerViaMDIDDL] +assert poe_layer +assert poe_layer._type == 127 +assert int(check_tshark(frm, "lldp", "lldp.tlv.type").split(',')[-2], 0) == 127 +assert poe_layer.org_code == LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3 +assert int(check_tshark(frm, "lldp", "lldp.orgtlv.oui").split(',')[-1], 0) == 4623 +assert poe_layer.subtype == 2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.subtype"), 0) == 0x02 +assert poe_layer._length == 12 +assert int(check_tshark(frm, "lldp", "lldp.tlv.len").split(',')[-2], 0) == 12 +assert poe_layer.MDI_power_support == 12 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_support"), 0) == 12 +assert poe_layer.PSE_power_pair == 1 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_pse_pair"), 0) == 1 +# NOTE: wireshark mixes power_prio and PD_4PID fields. Result will be incerrect if PD_4PID==1 +assert poe_layer.power_class == 5 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_class"), 0) == 5 +assert poe_layer.power_type_no == 0 +assert poe_layer.power_type_dir == 0 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_type"), 0) == 0 +assert poe_layer.power_source == 0b10 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_source"), 0) == 0b10 +assert poe_layer.power_prio == 0b10 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_priority"), 0) == 0b10 +assert poe_layer.PD_requested_power == 2.2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_pde_requested"), 0) == 22 +assert poe_layer.PSE_allocated_power == 1.5 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_pse_allocated"), 0) == 15 + + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + ) +# invalid length +try: + Ether((frm/ + LLDPDUPowerViaMDIDDL(_length=8)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidLengthField: + pass + +# invalid power +try: + Ether((frm/ + LLDPDUPowerViaMDIDDL(PD_requested_power=100)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIDDL(PSE_allocated_power=100)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + += Power via MDI with DDL classification and Type 3 and 4 extensions tests + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + )/\ + LLDPDUPowerViaMDIType34(MDI_power_support='port class PSE+PSE pairs controlled+PSE MDI power enabled', + PSE_power_pair='alt B', + power_class='class 2', + power_type_no='type 1', + power_type_dir='PD', + power_source='PSE and local', + PD_4PID='not supported', + power_prio='low', + PD_requested_power=12.21111, + PSE_allocated_power=11.521212121, + PD_requested_power_mode_A=2.3, + PD_requested_power_mode_B=3.3, + PD_allocated_power_alt_A=3.1, + PD_allocated_power_alt_B=0.5, + PSE_powering_status='4-pair powering single-signature PD', + PD_powered_status='powered single-signature PD', + PD_power_pair_ext='both alts', + dual_signature_class_mode_A='class 4', + dual_signature_class_mode_B='class 2', + power_class_ext='dual-signature pd', + power_type_ext='type 4 single-signature PD', + PD_load='dual-signature and electrically isolated', + PSE_max_available_power=33.333, + autoclass='autoclass completed+autoclass request', + power_down_req='power down', + power_down_time=123)/\ + LLDPDUEndOfLLDPDU() + +frm = frm.build() +frm = Ether(frm) +poe_layer = frm[LLDPDUPowerViaMDIType34] +assert poe_layer +assert poe_layer._type == 127 +assert int(check_tshark(frm, "lldp", "lldp.tlv.type").split(',')[-2], 0) == 127 +assert poe_layer.org_code == LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3 +assert int(check_tshark(frm, "lldp", "lldp.orgtlv.oui").split(',')[-1], 0) == 4623 +assert poe_layer.subtype == 2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.subtype"), 0) == 0x02 +assert poe_layer._length == 29 +assert int(check_tshark(frm, "lldp", "lldp.tlv.len").split(',')[-2], 0) == 29 +assert poe_layer.MDI_power_support == 13 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_support"), 0) == 13 +assert poe_layer.PSE_power_pair == 2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_pse_pair"), 0) == 2 +# NOTE: wireshark mixes power_prio and PD_4PID fields. Result will be incerrect if PD_4PID==1 +assert poe_layer.PD_4PID == 0 +assert poe_layer.power_class == 3 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_class"), 0) == 3 +assert poe_layer.power_type_no == 1 +assert poe_layer.power_type_dir == 1 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_type"), 0) == 3 +assert poe_layer.power_source == 0b11 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_source"), 0) == 0b11 +assert poe_layer.power_prio == 0b11 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_power_priority"), 0) == 0b11 +assert poe_layer.PD_requested_power == 12.2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_pde_requested"), 0) == 122 +assert poe_layer.PSE_allocated_power == 11.5 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.mdi_pse_allocated"), 0) == 115 +assert poe_layer.PD_requested_power_mode_A == 2.3 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_ds_pd_requested_power_value_mode_a"), 0) == 23 +assert poe_layer.PD_requested_power_mode_B == 3.3 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_ds_pd_requested_power_value_mode_b"), 0) == 33 +assert poe_layer.PD_allocated_power_alt_A == 3.1 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_ds_pse_allocated_power_value_alt_a"), 0) == 31 +assert poe_layer.PD_allocated_power_alt_B == 0.5 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_ds_pse_allocated_power_value_alt_b"), 0) == 5 +assert poe_layer.PSE_powering_status == 2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_pse_powering_status"), 0) == 2 +assert poe_layer.PD_powered_status == 1 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_pd_powered_status"), 0) == 1 +assert poe_layer.PD_power_pair_ext == 3 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_pse_power_pairs_ext"), 0) == 3 +assert poe_layer.dual_signature_class_mode_A == 4 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_ds_pwr_class_ext_a"), 0) == 4 +assert poe_layer.dual_signature_class_mode_B == 2 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_ds_pwr_class_ext_b"), 0) == 2 +assert poe_layer.power_class_ext == 15 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_pwr_class_ext_"), 0) == 15 +assert poe_layer.power_type_ext == 4 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_power_type_ext"), 0) == 4 +assert poe_layer.PD_load == 1 +assert poe_layer.PSE_max_available_power == 33.3 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_pse_maximum_available_power_value"), 0) == 333 +assert poe_layer.autoclass == 3 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_autoclass"), 0) == 3 +assert poe_layer.power_down_req == 0x1d +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_power_down_request"), 0) == 0x1d +assert poe_layer.power_down_time == 123 +assert int(check_tshark(frm, "lldp", "lldp.ieee.802_3.bt_power_down_time"), 0) == 123 + + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + ) +# invalid length +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(_length=8)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidLengthField: + pass + +# invalid power +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(PD_requested_power=100)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(PSE_allocated_power=100)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(PD_requested_power_mode_A=50)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(PD_requested_power_mode_B=50)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(PD_allocated_power_alt_A=50)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(PD_allocated_power_alt_B=50)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(PSE_max_available_power=100)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +# invalid time +try: + Ether((frm/ + LLDPDUPowerViaMDIType34(power_down_time=(1<<18))/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + += Power via MDI measurements tests + +import struct + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + )/\ + LLDPDUPowerViaMDIMeasure(support='power+current', + source='mode B', + request='energy+voltage+current', + valid='power', + voltage_uncertainty=52.25, + current_uncertainty=3.211, + power_uncertainty=140, + energy_uncertainty=2600, + voltage_measurement=22.123, + current_measurement=3.2121, + power_measurement=123.12, + energy_measurement=21123400, + power_price_index='not available')/\ + LLDPDUEndOfLLDPDU() + +frm = frm.build() +frm = Ether(frm) +poe_layer = frm[LLDPDUPowerViaMDIMeasure] +poe_layer_raw = raw(poe_layer) + +# PoE measure TLV is not supported by WireShark + +assert poe_layer +assert poe_layer._type == 127 +assert poe_layer.org_code == LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_IEEE_802_3 +assert poe_layer.subtype == 8 +assert poe_layer._length == 26 +assert poe_layer.support == 0b0110 +assert poe_layer.source == 0b10 +assert poe_layer.request == 0b1101 +assert poe_layer.valid == 0b0010 +assert poe_layer.voltage_uncertainty == 52.25 +assert struct.unpack(">H", poe_layer_raw[8:10])[0] == 52250 +assert poe_layer.current_uncertainty == 3.211 +assert struct.unpack(">H", poe_layer_raw[10:12])[0] == 32110 +assert poe_layer.power_uncertainty == 140 +assert struct.unpack(">H", poe_layer_raw[12:14])[0] == 14000 +assert poe_layer.energy_uncertainty == 2600 +assert struct.unpack(">H", poe_layer_raw[14:16])[0] == 26 +assert poe_layer.voltage_measurement == 22.123 +assert struct.unpack(">H", poe_layer_raw[16:18])[0] == 22123 +assert poe_layer.current_measurement == 3.2121 +assert struct.unpack(">H", poe_layer_raw[18:20])[0] == 32121 +assert poe_layer.power_measurement == 123.12 +assert struct.unpack(">H", poe_layer_raw[20:22])[0] == 12312 +assert poe_layer.energy_measurement == 21123400 +assert struct.unpack(">I", poe_layer_raw[22:26])[0] == 211234 +assert poe_layer.power_price_index == 0xffff + +frm = Ether(src='01:01:01:01:01:01', dst=LLDP_NEAREST_BRIDGE_MAC)/\ + LLDPDUChassisID(subtype=LLDPDUChassisID.SUBTYPE_MAC_ADDRESS, id=b'\x06\x05\x04\x03\x02\x01')/\ + LLDPDUPortID(subtype=LLDPDUPortID.SUBTYPE_MAC_ADDRESS, id=b'\x01\x02\x03\x04\x05\x06')/\ + LLDPDUTimeToLive()/\ + LLDPDUGenericOrganisationSpecific(org_code=LLDPDUGenericOrganisationSpecific.ORG_UNIQUE_CODE_PNO, + subtype=0x42, + data=b'FooBar'*5 + ) +# invalid length +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(_length=8)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidLengthField: + pass + +# invalid voltage +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(voltage_uncertainty=500)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(voltage_measurement=500)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +# invalid current +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(current_uncertainty=500)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(current_measurement=500)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +# invalid energy +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(energy_uncertainty=66000000)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +# invalid power +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(power_uncertainty=5000)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(power_measurement=5000)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass + +# invalid power price index +try: + Ether((frm/ + LLDPDUPowerViaMDIMeasure(power_price_index=150)/ + LLDPDUEndOfLLDPDU()).build()) + assert False +except LLDPInvalidFieldValue: + pass diff --git a/test/contrib/loraphy2wan.uts b/test/contrib/loraphy2wan.uts new file mode 100644 index 00000000000..739a4a16a82 --- /dev/null +++ b/test/contrib/loraphy2wan.uts @@ -0,0 +1,65 @@ +% Regression tests for Scapy + ++Syntax check += Import the loraphy2wan layer + +from scapy.contrib.loraphy2wan import * +from scapy.compat import raw + +# LoRa PHY to WAN + +############ +############ ++ Basic tests + +* Those test are here mainly to check nothing has been broken + += Packet decoding +~ field + +p = b'\x00\x00\x00\x00lovecafemeeetoo\x00iiS\x02LI' +pkt = LoRa(p) +assert pkt.Join_Request_Field[0].DevEUI == b'meeetoo\x00' +assert pkt.Join_Request_Field[0].DevNonce == 26985 + +p = b'\x0f0P@\xad\x15\x00`\x80\x06\x00\t\xca\xfe\x0c\x1d\x8d\x04\\\xb5' +pkt = LoRa(p) +assert pkt.MType == 2 +assert pkt.DataPayload == b'\xca\xfe' +assert pkt.FCnt == 6 +assert pkt.FPort == 9 +assert pkt.FCtrl[0].ADR == 1 +assert pkt.DevAddr[0].NwkID == 0xad +assert pkt.DevAddr[0].NwkAddr == 0x600015 + +p = b'\x0f0P\x80\xad\x15\x00`\x00\x01\x00\t\xca\xfe:\x98\x89|\x8f\xd4' +pkt = LoRa(p) +assert pkt.MType == 4 + += Decoding an encrypted JA packet + +LoRa.encrypted = True +p = b'\x00\x00\x00 \x086\xe2\x87\xa9\x80\\\xb7\xee\x9e_\xff|\x9e\xe9z' +pkt = LoRa(p) +assert pkt.Join_Accept_Encrypted == b'\x086\xe2\x87\xa9\x80\\\xb7\xee\x9e_\xff|\x9e\xe9z' + += Packet crafting: generating an unencrypted JA frame + +ja = Join_Accept() +ja.JoinAppNonce=0x6fe14a +ja.NetID = 0x10203 +ja.DevAddr = 0x68e8cb1 +assert raw(ja) == b'J\xe1o\x03\x02\x01\xb1\x8c\x8e\x06\x00\x00' + += Generating an unencrypted LoRa JA packet + +LoRa.encrypted = False +pkt = LoRa(MType=0b001) +pkt.Join_Accept_Field = [ja] +assert raw(pkt) == b'\x00\x00\x00 J\xe1o\x03\x02\x01\xb1\x8c\x8e\x06\x00\x00\x00\x00\x00\x00' + += Parsing Piggy back commands + +p = b'\r0\xc0\x80\xad\x15\x00`\x01\x01\x00\x02\xc0\xe3N\xb7\xc7\xae' +pkt = LoRa(p) +assert pkt.FOpts_up[0].CID == 2 diff --git a/test/contrib/mac_control.uts b/test/contrib/mac_control.uts index 79a7d95a3a1..ee559a579d9 100644 --- a/test/contrib/mac_control.uts +++ b/test/contrib/mac_control.uts @@ -15,8 +15,8 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC)/\ frm = Ether(frm.do_build()) pause_layer = frm[MACControlPause] -assert(pause_layer.pause_time == 0x1234) -assert(pause_layer.get_pause_time(ETHER_SPEED_MBIT_10) == 0.238592) +assert pause_layer.pause_time == 0x1234 +assert pause_layer.get_pause_time(ETHER_SPEED_MBIT_10) == 0.238592 = gate frame @@ -25,7 +25,7 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC)/\ frm = Ether(frm.do_build()) gate_layer = frm[MACControlGate] -assert(gate_layer.timestamp == 0x12345678) +assert gate_layer.timestamp == 0x12345678 = report frame @@ -36,8 +36,8 @@ frm = Ether(frm.do_build()) report_layer = frm[MACControlReport] -assert(report_layer.timestamp == 0x12345678) -assert(report_layer.pending_grants == 0x23) +assert report_layer.timestamp == 0x12345678 +assert report_layer.pending_grants == 0x23 = report frame flags (generic for all other register frame types) @@ -46,7 +46,7 @@ for flag in MACControl.REGISTER_FLAGS: MACControlReport(timestamp=0x12345678, flags=flag) frm = Ether(frm.do_build()) report_layer = frm[MACControlReport] - assert(report_layer.flags == flag) + assert report_layer.flags == flag = register_req frame @@ -59,7 +59,7 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC)/\ frm = Ether(frm.do_build()) register_req_layer = frm[MACControlRegisterReq] -assert(register_req_layer.timestamp == 0x87654321) +assert register_req_layer.timestamp == 0x87654321 assert (register_req_layer.echoed_pending_grants == 0x12) assert (register_req_layer.sync_time == 0x3344) assert (register_req_layer.assigned_port == 0x7766) @@ -73,9 +73,9 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC)/\ frm = Ether(frm.do_build()) register_layer = frm[MACControlRegister] -assert(register_layer.timestamp == 0x11223344) -assert(register_layer.echoed_assigned_port == 0x2277) -assert(register_layer.echoed_sync_time == 0x3399) +assert register_layer.timestamp == 0x11223344 +assert register_layer.echoed_assigned_port == 0x2277 +assert register_layer.echoed_sync_time == 0x3399 = register_ack frame @@ -86,9 +86,9 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC)/\ frm = Ether(frm.do_build()) register_ack_layer = frm[MACControlRegisterAck] -assert(register_ack_layer.timestamp == 0x11223344) -assert(register_ack_layer.echoed_assigned_port == 0x2277) -assert(register_ack_layer.echoed_sync_time == 0x3399) +assert register_ack_layer.timestamp == 0x11223344 +assert register_ack_layer.echoed_assigned_port == 0x2277 +assert register_ack_layer.echoed_sync_time == 0x3399 = class based flow control frame @@ -98,15 +98,15 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC)/ \ frm = Ether(frm.do_build()) cbfc_layer = frm[MACControlClassBasedFlowControl] -assert(cbfc_layer.c0_enabled) -assert(cbfc_layer.c0_pause_time == 0x4321) -assert(cbfc_layer.c5_enabled) -assert(cbfc_layer.c5_pause_time == 0x1234) -assert(not cbfc_layer.c1_enabled) -assert(cbfc_layer.c1_pause_time == 0) -assert(not cbfc_layer.c7_enabled) -assert(cbfc_layer.c7_pause_time == 0) -assert(cbfc_layer._reserved == 0) +assert cbfc_layer.c0_enabled +assert cbfc_layer.c0_pause_time == 0x4321 +assert cbfc_layer.c5_enabled +assert cbfc_layer.c5_pause_time == 0x1234 +assert not cbfc_layer.c1_enabled +assert cbfc_layer.c1_pause_time == 0 +assert not cbfc_layer.c7_enabled +assert cbfc_layer.c7_pause_time == 0 +assert cbfc_layer._reserved == 0 + test padding @@ -116,7 +116,7 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC) / \ MACControlRegisterAck(timestamp=0x12345678) frm = frm.do_build() -assert(len(frm) == 60) +assert len(frm) == 60 = single vlan tag @@ -125,7 +125,7 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC) / \ MACControlRegisterAck(timestamp=0x12345678) frm = frm.do_build() -assert(len(frm) == 60) +assert len(frm) == 60 = QinQ @@ -135,7 +135,7 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC) / \ MACControlRegisterAck(timestamp=0x12345678) frm = frm.do_build() -assert(len(frm) == 60) +assert len(frm) == 60 = hand craftet payload (disabled auto padding) @@ -145,5 +145,5 @@ frm = Ether(src='00:01:01:01:01:01', dst=MACControl.DEFAULT_DST_MAC) / \ frm = Ether(frm.do_build()) raw_layer = frm[Raw] -assert(raw_layer.load == b'may pass devices') -assert(len(frm) < 64) +assert raw_layer.load == b'may pass devices' +assert len(frm) < 64 diff --git a/test/contrib/macsec.uts b/test/contrib/macsec.uts index 046833d3e84..218373e1fad 100755 --- a/test/contrib/macsec.uts +++ b/test/contrib/macsec.uts @@ -11,32 +11,33 @@ sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=100, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/IP(src='192.168.0.1', dst='192.168.0.2')/ICMP(type='echo-request')/"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" m = sa.encap(p) -assert(m.type == ETH_P_MACSEC) -assert(m[MACsec].type == ETH_P_IP) -assert(len(m) == len(p) + 16) -assert(m[MACsec].an == 0) -assert(m[MACsec].pn == 100) -assert(m[MACsec].shortlen == 0) -assert(m[MACsec].SC) -assert(m[MACsec].E) -assert(m[MACsec].C) -assert(m[MACsec].sci == b'\x52\x54\x00\x13\x01\x56\x00\x01') +assert m.type == ETH_P_MACSEC +assert m[MACsec].type == ETH_P_IP +assert len(m) == len(p) + 16 +assert m[MACsec].AN == 0 +assert m[MACsec].PN == 100 +assert m[MACsec].SL == 0 +assert m[MACsec].SC +assert m[MACsec].E +assert m[MACsec].C +assert m[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' +assert m[MACsec].mysummary() == r"AN=0, PN=100, SCI=b'RT\x00\x13\x01V\x00\x01', IPv4" = MACsec - basic encryption - encrypted sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=100, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/IP(src='192.168.0.1', dst='192.168.0.2')/ICMP(type='echo-request')/"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" m = sa.encap(p) e = sa.encrypt(m) -assert(e.type == ETH_P_MACSEC) -assert(e[MACsec].type == None) -assert(len(e) == len(p) + 16 + 16) -assert(e[MACsec].an == 0) -assert(e[MACsec].pn == 100) -assert(e[MACsec].shortlen == 0) -assert(e[MACsec].SC) -assert(e[MACsec].E) -assert(e[MACsec].C) -assert(e[MACsec].sci == b'\x52\x54\x00\x13\x01\x56\x00\x01') +assert e.type == ETH_P_MACSEC +assert e[MACsec].type == None +assert len(e) == len(p) + 16 + 16 +assert e[MACsec].AN == 0 +assert e[MACsec].PN == 100 +assert e[MACsec].SL == 0 +assert e[MACsec].SC +assert e[MACsec].E +assert e[MACsec].C +assert e[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' = MACsec - basic decryption - encrypted sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=100, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) @@ -44,17 +45,17 @@ p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/IP(src='192.168.0.1' m = sa.encap(p) e = sa.encrypt(m) d = sa.decrypt(e) -assert(d.type == ETH_P_MACSEC) -assert(d[MACsec].type == ETH_P_IP) -assert(len(d) == len(m)) -assert(d[MACsec].an == 0) -assert(d[MACsec].pn == 100) -assert(d[MACsec].shortlen == 0) -assert(d[MACsec].SC) -assert(d[MACsec].E) -assert(d[MACsec].C) -assert(d[MACsec].sci == b'\x52\x54\x00\x13\x01\x56\x00\x01') -assert(raw(d) == raw(m)) +assert d.type == ETH_P_MACSEC +assert d[MACsec].type == ETH_P_IP +assert len(d) == len(m) +assert d[MACsec].AN == 0 +assert d[MACsec].PN == 100 +assert d[MACsec].SL == 0 +assert d[MACsec].SC +assert d[MACsec].E +assert d[MACsec].C +assert d[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' +assert raw(d) == raw(m) = MACsec - basic decap - decrypted sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=100, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) @@ -63,7 +64,7 @@ m = sa.encap(p) e = sa.encrypt(m) d = sa.decrypt(e) r = sa.decap(d) -assert(raw(r) == raw(p)) +assert raw(r) == raw(p) @@ -71,33 +72,33 @@ assert(raw(r) == raw(p)) sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/IP(src='192.168.0.1', dst='192.168.0.2')/ICMP(type='echo-request')/"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" m = sa.encap(p) -assert(m.type == ETH_P_MACSEC) -assert(m[MACsec].type == ETH_P_IP) -assert(len(m) == len(p) + 16) -assert(m[MACsec].an == 0) -assert(m[MACsec].pn == 200) -assert(m[MACsec].shortlen == 0) -assert(m[MACsec].SC) -assert(not m[MACsec].E) -assert(not m[MACsec].C) -assert(m[MACsec].sci == b'\x52\x54\x00\x13\x01\x56\x00\x01') +assert m.type == ETH_P_MACSEC +assert m[MACsec].type == ETH_P_IP +assert len(m) == len(p) + 16 +assert m[MACsec].AN == 0 +assert m[MACsec].PN == 200 +assert m[MACsec].SL == 0 +assert m[MACsec].SC +assert not m[MACsec].E +assert not m[MACsec].C +assert m[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' = MACsec - basic encryption - integrity only sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/IP(src='192.168.0.1', dst='192.168.0.2')/ICMP(type='echo-request')/"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" m = sa.encap(p) e = sa.encrypt(m) -assert(m.type == ETH_P_MACSEC) -assert(e[MACsec].type == None) -assert(len(e) == len(p) + 16 + 16) -assert(e[MACsec].an == 0) -assert(e[MACsec].pn == 200) -assert(e[MACsec].shortlen == 0) -assert(e[MACsec].SC) -assert(not e[MACsec].E) -assert(not e[MACsec].C) -assert(e[MACsec].sci == b'\x52\x54\x00\x13\x01\x56\x00\x01') -assert(raw(e)[:-16] == raw(m)) +assert m.type == ETH_P_MACSEC +assert e[MACsec].type == None +assert len(e) == len(p) + 16 + 16 +assert e[MACsec].AN == 0 +assert e[MACsec].PN == 200 +assert e[MACsec].SL == 0 +assert e[MACsec].SC +assert not e[MACsec].E +assert not e[MACsec].C +assert e[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' +assert raw(e)[:-16] == raw(m) = MACsec - basic decryption - integrity only sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) @@ -105,17 +106,17 @@ p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/IP(src='192.168.0.1' m = sa.encap(p) e = sa.encrypt(m) d = sa.decrypt(e) -assert(d.type == ETH_P_MACSEC) -assert(d[MACsec].type == ETH_P_IP) -assert(len(d) == len(m)) -assert(d[MACsec].an == 0) -assert(d[MACsec].pn == 200) -assert(d[MACsec].shortlen == 0) -assert(d[MACsec].SC) -assert(not d[MACsec].E) -assert(not d[MACsec].C) -assert(d[MACsec].sci == b'\x52\x54\x00\x13\x01\x56\x00\x01') -assert(raw(d) == raw(m)) +assert d.type == ETH_P_MACSEC +assert d[MACsec].type == ETH_P_IP +assert len(d) == len(m) +assert d[MACsec].AN == 0 +assert d[MACsec].PN == 200 +assert d[MACsec].SL == 0 +assert d[MACsec].SC +assert not d[MACsec].E +assert not d[MACsec].C +assert d[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' +assert raw(d) == raw(m) = MACsec - basic decap - integrity only sa = MACsecSA(sci=b'\x52\x54\x00\x13\x01\x56\x00\x01', an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) @@ -124,77 +125,77 @@ m = sa.encap(p) e = sa.encrypt(m) d = sa.decrypt(e) r = sa.decap(d) -assert(raw(r) == raw(p)) +assert raw(r) == raw(p) = MACsec - encap - shortlen 2 sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd') m = sa.encap(p) -assert(m[MACsec].shortlen == 2) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 2 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 10 sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 8) m = sa.encap(p) -assert(m[MACsec].shortlen == 10) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 10 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 18 sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 16) m = sa.encap(p) -assert(m[MACsec].shortlen == 18) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 18 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 32 sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 30) m = sa.encap(p) -assert(m[MACsec].shortlen == 32) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 32 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 40 sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 38) m = sa.encap(p) -assert(m[MACsec].shortlen == 40) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 40 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 47 sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 45) m = sa.encap(p) -assert(m[MACsec].shortlen == 47) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 47 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 0 (48) sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=1) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 45 + "y") m = sa.encap(p) -assert(m[MACsec].shortlen == 0) +assert m[MACsec].SL == 0 = MACsec - encap - shortlen 2/nosci sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=0) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd') m = sa.encap(p) -assert(m[MACsec].shortlen == 2) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 2 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 32/nosci sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=0) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 30) m = sa.encap(p) -assert(m[MACsec].shortlen == 32) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 32 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - encap - shortlen 47/nosci sa = MACsecSA(sci=0x5254001301560001, an=0, pn=200, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=0, send_sci=0) p = Ether(src='aa:aa:aa:bb:bb:bb', dst='cc:cc:cc:dd:dd:dd')/Raw("x" * 45) m = sa.encap(p) -assert(m[MACsec].shortlen == 47) -assert(len(m) == m[MACsec].shortlen + 20 + (8 if sa.send_sci else 0)) +assert m[MACsec].SL == 47 +assert len(m) == m[MACsec].SL + 20 + (8 if sa.send_sci else 0) = MACsec - authenticate @@ -206,8 +207,8 @@ txdec = txsa.decap(txsa.decrypt(tx)) rxdec = rxsa.decap(rxsa.decrypt(rx)) txref = b"RT\x00\x12\x01V.\xbc\x84\xd5\xca\x13\x08\x00E\x00\x00T\x11:@\x00@\x01\xa6\x1b\xc0\xa8\x01\x01\xc0\xa8\x01\x02\x08\x00a\xeaG+\x00\x01\xc0~RY\x00\x00\x00\x00w>\x06\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37" rxref = b".\xbc\x84\xd5\xca\x13RT\x00\x12\x01V\x08\x00E\x00\x00T\xd4\x1a\x00\x00@\x01#;\xc0\xa8\x01\x02\xc0\xa8\x01\x01\x00\x00i\xeaG+\x00\x01\xc0~RY\x00\x00\x00\x00w>\x06\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37" -assert(raw(txdec) == raw(txref)) -assert(raw(rxdec) == raw(rxref)) +assert raw(txdec) == raw(txref) +assert raw(rxdec) == raw(rxref) @@ -216,7 +217,7 @@ rx = Ether(b".\xbc\x84\xd5\xca\x13RT\x00\x12\x01V\x88\xe5 \x00\x00\x00\x00#RT\x0 txsa = MACsecSA(sci=0x5254001301560001, an=0, pn=31, key=b'\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61', icvlen=16, encrypt=0, send_sci=1) try: rxdec = rxsa.decap(rxsa.decrypt(rx)) - assert(not "This packet shouldn't have been authenticated as correct") + assert not "This packet shouldn't have been authenticated as correct" except InvalidTag: pass @@ -231,8 +232,8 @@ txdec = txsa.decap(txsa.decrypt(tx)) rxdec = rxsa.decap(rxsa.decrypt(rx)) txref = b"RT\x00\x12\x01V.\xbc\x84\xd5\xca\x13\x08\x00E\x00\x00\x80#D@\x00@\x01\x93\xe5\xc0\xa8\x01\x01\xc0\xa8\x01\x02\x08\x00E\xd5\x0f\xb3\x00\x01SrSY\x00\x00\x00\x00\x8b\x1d\r\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abc" rxref = b".\xbc\x84\xd5\xca\x13RT\x00\x12\x01V\x08\x00E\x00\x00\x80\x05\xab\x00\x00@\x01\xf1~\xc0\xa8\x01\x02\xc0\xa8\x01\x01\x00\x00M\xd5\x0f\xb3\x00\x01SrSY\x00\x00\x00\x00\x8b\x1d\r\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abc" -assert(raw(txdec) == raw(txref)) -assert(raw(rxdec) == raw(rxref)) +assert raw(txdec) == raw(txref) +assert raw(rxdec) == raw(rxref) @@ -241,7 +242,7 @@ rx = Ether(b".\xbc\x84\xd5\xca\x13RT\x00\x12\x01V\x88\xe5,\x00\x00\x00\x005RT\x0 rxsa = MACsecSA(sci=0x5254001201560001, an=0, pn=31, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) try: rxdec = rxsa.decap(rxsa.decrypt(rx)) - assert(not "This packet shouldn't have been decrypted correctly") + assert not "This packet shouldn't have been decrypted correctly" except InvalidTag: pass @@ -252,21 +253,21 @@ rxsa = MACsecSA(sci=0x5254001201560001, an=0, pn=31, key=b'aaaaaaaaaaaaaaaa', ic try: rxsa.decap(IP()) except TypeError as e: - assert(str(e) == "cannot decapsulate MACsec packet, must be Ethernet/MACsec") + assert str(e) == "cannot decapsulate MACsec packet, must be Ethernet/MACsec" = MACsec - decap - non-MACsec rxsa = MACsecSA(sci=0x5254001201560001, an=0, pn=31, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) try: rxsa.decap(Ether()/IP()) except TypeError as e: - assert(str(e) == "cannot decapsulate MACsec packet, must be Ethernet/MACsec") + assert str(e) == "cannot decapsulate MACsec packet, must be Ethernet/MACsec" = MACsec - encap - non-Ethernet txsa = MACsecSA(sci=0x5254001201560001, an=0, pn=31, key=b'aaaaaaaaaaaaaaaa', icvlen=16, encrypt=1, send_sci=1) try: txsa.encap(IP()) except TypeError as e: - assert(str(e) == "cannot encapsulate packet in MACsec, must be Ethernet") + assert str(e) == "cannot encapsulate packet in MACsec, must be Ethernet" # Reference packets tests from the MACsec specification document (IEEE Std 802.1AEbw-2013, Annex C). @@ -274,100 +275,100 @@ except TypeError as e: = MACsec - Standard Test Vectors - C.1.1 GCM-AES-128 (54-octet frame integrity protection) sa = MACsecSA(sci=b'\x12\x15\x35\x24\xC0\x89\x5E\x81', an=2, pn=0xB2C28465, key=b'\xAD\x7A\x2B\xD0\x3E\xAC\x83\x5A\x6F\x62\x0F\xDC\xB5\x06\xB3\x45', icvlen=16, encrypt=0, send_sci=1) -p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001"))) +p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001")) m = sa.encap(p) iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\x12\x15\x35\x24\xC0\x89\x5E\x81\xB2\xC2\x84\x65')) +assert raw(iv) == raw(b'\x12\x15\x35\x24\xC0\x89\x5E\x81\xB2\xC2\x84\x65') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001F09478A9B09007D06F46E9B6A1DA25DD"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001F09478A9B09007D06F46E9B6A1DA25DD")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) = MACsec - Standard Test Vectors - C.1.2 GCM-AES-256 (54-octet frame integrity protection) sa = MACsecSA(sci=b'\x12\x15\x35\x24\xC0\x89\x5E\x81', an=2, pn=0xB2C28465, key=b'\xE3\xC0\x8A\x8F\x06\xC6\xE3\xAD\x95\xA7\x05\x57\xB2\x3F\x75\x48\x3C\xE3\x30\x21\xA9\xC7\x2B\x70\x25\x66\x62\x04\xC6\x9C\x0B\x72', icvlen=16, encrypt=0, send_sci=1) -p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001"))) +p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001")) m = sa.encap(p) iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\x12\x15\x35\x24\xC0\x89\x5E\x81\xB2\xC2\x84\x65')) +assert raw(iv) == raw(b'\x12\x15\x35\x24\xC0\x89\x5E\x81\xB2\xC2\x84\x65') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400012F0BC5AF409E06D609EA8B7D0FA5EA50"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400012F0BC5AF409E06D609EA8B7D0FA5EA50")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) = MACsec - Standard Test Vectors - C.1.3 GCM-AES-XPN-128 (54-octet frame integrity protection) sa = MACsecSA(sci=b'\x12\x15\x35\x24\xC0\x89\x5E\x81', an=2, pn=0xB0DF459CB2C28465, key=b'\xAD\x7A\x2B\xD0\x3E\xAC\x83\x5A\x6F\x62\x0F\xDC\xB5\x06\xB3\x45', icvlen=16, encrypt=0, send_sci=1, xpn_en = True, ssci = 0x7A30C118, salt = b'\xE6\x30\xE8\x1A\x48\xDE\x86\xA2\x1C\x66\xFA\x6D') -p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001"))) +p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001")) m = sa.encap(p) iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\xAE\xA4\x7E\x08')) +assert raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\xAE\xA4\x7E\x08') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334000117FE1981EBDD4AFC5062697E8BAA0C23"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334000117FE1981EBDD4AFC5062697E8BAA0C23")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) = MACsec - Standard Test Vectors - C.1.4 GCM-AES-XPN-256 (54-octet frame integrity protection) sa = MACsecSA(sci=b'\x12\x15\x35\x24\xC0\x89\x5E\x81', an=2, pn=0xB0DF459CB2C28465, key=b'\xE3\xC0\x8A\x8F\x06\xC6\xE3\xAD\x95\xA7\x05\x57\xB2\x3F\x75\x48\x3C\xE3\x30\x21\xA9\xC7\x2B\x70\x25\x66\x62\x04\xC6\x9C\x0B\x72', icvlen=16, encrypt=0, send_sci=1, xpn_en = True, ssci = 0x7A30C118, salt = b'\xE6\x30\xE8\x1A\x48\xDE\x86\xA2\x1C\x66\xFA\x6D') -p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001"))) +p = Ether(src='7A:0D:46:DF:99:8D', dst='D6:09:B1:F0:56:63', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001")) m = sa.encap(p) iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\xAE\xA4\x7E\x08')) +assert raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\xAE\xA4\x7E\x08') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400014DBD2F6A754A6CF728CC129BA6931577"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400014DBD2F6A754A6CF728CC129BA6931577")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) = MACsec - Standard Test Vectors - C.5.1 GCM-AES-128 (54-octet frame confidentiality protection) sa = MACsecSA(sci=b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01', an=0, pn=0x76D457ED, key=b'\x07\x1B\x11\x3B\x0C\xA7\x43\xFE\xCC\xCF\x3D\x05\x1F\x73\x73\x82', icvlen=16, encrypt=1, send_sci=0) -p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004"))) +p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004")) m = sa.encap(p) m[MACsec].ES = 1 iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01\x76\xD4\x57\xED')) +assert raw(iv) == raw(b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01\x76\xD4\x57\xED') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED13B4C72B389DC5018E72A171DD85A5D3752274D3A019FBCAED09A425CD9B2E1C9B72EEE7C9DE7D52B3F3D6A5284F4A6D3FE22A5D6C2B960494C3"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED13B4C72B389DC5018E72A171DD85A5D3752274D3A019FBCAED09A425CD9B2E1C9B72EEE7C9DE7D52B3F3D6A5284F4A6D3FE22A5D6C2B960494C3")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) = MACsec - Standard Test Vectors - C.5.2 GCM-AES-256 (54-octet frame confidentiality protection) sa = MACsecSA(sci=b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01', an=0, pn=0x76D457ED, key=b'\x69\x1D\x3E\xE9\x09\xD7\xF5\x41\x67\xFD\x1C\xA0\xB5\xD7\x69\x08\x1F\x2B\xDE\x1A\xEE\x65\x5F\xDB\xAB\x80\xBD\x52\x95\xAE\x6B\xE7', icvlen=16, encrypt=1, send_sci=0) -p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004"))) +p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004")) m = sa.encap(p) m[MACsec].ES = 1 iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01\x76\xD4\x57\xED')) +assert raw(iv) == raw(b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01\x76\xD4\x57\xED') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457EDC1623F55730C93533097ADDAD25664966125352B43ADACBD61C5EF3AC90B5BEE929CE4630EA79F6CE51912AF39C2D1FDC2051F8B7B3C9D397EF2"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457EDC1623F55730C93533097ADDAD25664966125352B43ADACBD61C5EF3AC90B5BEE929CE4630EA79F6CE51912AF39C2D1FDC2051F8B7B3C9D397EF2")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) = MACsec - Standard Test Vectors - C.5.3 GCM-AES-XPN-128 (54-octet frame confidentiality protection) sa = MACsecSA(sci=b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01', an=0, pn=0xB0DF459C76D457ED, key=b'\x07\x1B\x11\x3B\x0C\xA7\x43\xFE\xCC\xCF\x3D\x05\x1F\x73\x73\x82', icvlen=16, encrypt=1, send_sci=0, xpn_en = True, ssci = 0x7A30C118, salt = b'\xE6\x30\xE8\x1A\x48\xDE\x86\xA2\x1C\x66\xFA\x6D') -p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004"))) +p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004")) m = sa.encap(p) m[MACsec].ES = 1 iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\x6A\xB2\xAD\x80')) +assert raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\x6A\xB2\xAD\x80') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED9CA46984430203ED416EBDC2FE2622BA3E5EAB6961C36383009E187E9B0C88564653B9ABD216441C6AB6F0A232E9E44C978CF7CD84D43484D101"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED9CA46984430203ED416EBDC2FE2622BA3E5EAB6961C36383009E187E9B0C88564653B9ABD216441C6AB6F0A232E9E44C978CF7CD84D43484D101")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) = MACsec - Standard Test Vectors - C.5.4 GCM-AES-XPN-256 (54-octet frame confidentiality protection) sa = MACsecSA(sci=b'\xF0\x76\x1E\x8D\xCD\x3D\x00\x01', an=0, pn=0xB0DF459C76D457ED, key=b'\x69\x1D\x3E\xE9\x09\xD7\xF5\x41\x67\xFD\x1C\xA0\xB5\xD7\x69\x08\x1F\x2B\xDE\x1A\xEE\x65\x5F\xDB\xAB\x80\xBD\x52\x95\xAE\x6B\xE7', icvlen=16, encrypt=1, send_sci=0, xpn_en = True, ssci = 0x7A30C118, salt = b'\xE6\x30\xE8\x1A\x48\xDE\x86\xA2\x1C\x66\xFA\x6D') -p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes(bytearray.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004"))) +p = Ether(src='F0:76:1E:8D:CD:3D', dst='E2:01:06:D7:CD:0D', type=0x0800)/Raw(bytes.fromhex("0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340004")) m = sa.encap(p) m[MACsec].ES = 1 iv = sa.make_iv(m) -assert(raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\x6A\xB2\xAD\x80')) +assert raw(iv) == raw(b'\x9C\x00\x29\x02\xF8\x01\xC3\x3E\x6A\xB2\xAD\x80') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED88D9F7D1F1578EE34BA7B1ABC89893EF1D3398C9F1DD3E47FBD8553E0FF786EF5699EB01EA10420D0EBD39A0E273C4C7F95ED843207D7A497DFA"))) -assert(raw(e) == raw(ref)) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED88D9F7D1F1578EE34BA7B1ABC89893EF1D3398C9F1DD3E47FBD8553E0FF786EF5699EB01EA10420D0EBD39A0E273C4C7F95ED843207D7A497DFA")) +assert raw(e) == raw(ref) dt = sa.decrypt(e) -assert(raw(dt) == raw(m)) +assert raw(dt) == raw(m) diff --git a/test/contrib/metawatch.uts b/test/contrib/metawatch.uts new file mode 100644 index 00000000000..2793a35dbab --- /dev/null +++ b/test/contrib/metawatch.uts @@ -0,0 +1,19 @@ +# Arista Metawatch unit tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('metawatch')" -t test/contrib/metawatch.uts + ++ Metawatch + += MetawatchEther, basic instantiation + +m = MetawatchEther() +assert m.type == 0x9000 + += MetawatchEther, build & dissect + +r = raw(MetawatchEther(dst="00:01:02:03:04:05", src="06:07:08:09:10:11")) +assert r == b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + +m = MetawatchEther(r) +assert m.dst == "00:01:02:03:04:05" and m.src == "06:07:08:09:10:11" and m.type == 0x9000 diff --git a/test/contrib/modbus.uts b/test/contrib/modbus.uts index f901576e83e..ed9f89564b8 100644 --- a/test/contrib/modbus.uts +++ b/test/contrib/modbus.uts @@ -13,33 +13,213 @@ raw(ModbusADURequest() / b'\x00\x01\x02') == b'\x00\x00\x00\x00\x00\x04\xff\x00\ = MBAP Guess Payload ModbusPDU01ReadCoilsRequest (simple case) p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x06\xff\x01\x00\x00\x00\x01') -assert(isinstance(p.payload, ModbusPDU01ReadCoilsRequest)) +assert isinstance(p.payload, ModbusPDU01ReadCoilsRequest) = MBAP Guess Payload ModbusPDU01ReadCoilsResponse p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x04\xff\x01\x01\x01') -assert(isinstance(p.payload, ModbusPDU01ReadCoilsResponse)) +assert isinstance(p.payload, ModbusPDU01ReadCoilsResponse) = MBAP Guess Payload ModbusPDU01ReadCoilsError p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x81\x02') -assert(isinstance(p.payload, ModbusPDU01ReadCoilsError)) +assert isinstance(p.payload, ModbusPDU01ReadCoilsError) + += MBAP Guess Payload ModbusPDU02ReadDiscreteInputsRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x06\xff\x02\x00\x00\x00\x01') +assert isinstance(p.payload, ModbusPDU02ReadDiscreteInputsRequest) += MBAP Guess Payload ModbusPDU02ReadDiscreteInputsResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x04\xff\x02\x01\x00') +assert isinstance(p.payload, ModbusPDU02ReadDiscreteInputsResponse) += MBAP Guess Payload ModbusPDU02ReadDiscreteInputsError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x82\x01') +assert isinstance(p.payload, ModbusPDU02ReadDiscreteInputsError) + += MBAP Guess Payload ModbusPDU03ReadHoldingRegistersRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x00\x00\x01') +assert isinstance(p.payload, ModbusPDU03ReadHoldingRegistersRequest) += MBAP Guess Payload ModbusPDU03ReadHoldingRegistersResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x05\xff\x03\x02\x00\x00') +assert isinstance(p.payload, ModbusPDU03ReadHoldingRegistersResponse) += MBAP Guess Payload ModbusPDU03ReadHoldingRegistersError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x83\x01') +assert isinstance(p.payload, ModbusPDU03ReadHoldingRegistersError) + += MBAP Guess Payload ModbusPDU04ReadInputRegistersRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x06\xff\x04\x00\x00\x00\x01') +assert isinstance(p.payload, ModbusPDU04ReadInputRegistersRequest) += MBAP Guess Payload ModbusPDU04ReadInputRegistersResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x05\xff\x04\x02\x00\x00') +assert isinstance(p.payload, ModbusPDU04ReadInputRegistersResponse) += MBAP Guess Payload ModbusPDU04ReadInputRegistersError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x84\x01') +assert isinstance(p.payload, ModbusPDU04ReadInputRegistersError) + += MBAP Guess Payload ModbusPDU05WriteSingleCoilRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x06\xff\x05\x00\x00\x00\x00') +assert isinstance(p.payload, ModbusPDU05WriteSingleCoilRequest) += MBAP Guess Payload ModbusPDU05WriteSingleCoilResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x06\xff\x05\x00\x00\x00\x00') +assert isinstance(p.payload, ModbusPDU05WriteSingleCoilResponse) += MBAP Guess Payload ModbusPDU05WriteSingleCoilError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x85\x01') +assert isinstance(p.payload, ModbusPDU05WriteSingleCoilError) + += MBAP Guess Payload ModbusPDU06WriteSingleRegisterRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x06\xff\x06\x00\x00\x00\x00') +assert isinstance(p.payload, ModbusPDU06WriteSingleRegisterRequest) += MBAP Guess Payload ModbusPDU06WriteSingleRegisterResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x06\xff\x06\x00\x00\x00\x00') +assert isinstance(p.payload, ModbusPDU06WriteSingleRegisterResponse) += MBAP Guess Payload ModbusPDU06WriteSingleRegisterError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x86\x01') +assert isinstance(p.payload, ModbusPDU06WriteSingleRegisterError) + += MBAP Guess Payload ModbusPDU07ReadExceptionStatusRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x02\xff\x07') +assert isinstance(p.payload, ModbusPDU07ReadExceptionStatusRequest) += MBAP Guess Payload ModbusPDU07ReadExceptionStatusResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x07\x00') +assert isinstance(p.payload, ModbusPDU07ReadExceptionStatusResponse) += MBAP Guess Payload ModbusPDU07ReadExceptionStatusError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x87\x01') +assert isinstance(p.payload, ModbusPDU07ReadExceptionStatusError) + += MBAP Guess Payload ModbusPDU08DiagnosticsRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x06\xff\x08\x00\x00\x00\x00') +assert isinstance(p.payload, ModbusPDU08DiagnosticsRequest) += MBAP Guess Payload ModbusPDU08DiagnosticsResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x06\xff\x08\x00\x00\x00\x00') +assert isinstance(p.payload, ModbusPDU08DiagnosticsResponse) += MBAP Guess Payload ModbusPDU08DiagnosticsError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x88\x01') +assert isinstance(p.payload, ModbusPDU08DiagnosticsError) + += MBAP Guess Payload ModbusPDU0BGetCommEventCounterRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x02\xff\x0b') +assert isinstance(p.payload, ModbusPDU0BGetCommEventCounterRequest) += MBAP Guess Payload ModbusPDU0BGetCommEventCounterResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x06\xff\x0b\x00\x00\xff\xff') +assert isinstance(p.payload, ModbusPDU0BGetCommEventCounterResponse) += MBAP Guess Payload ModbusPDU0BGetCommEventCounterError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x8b\x01') +assert isinstance(p.payload, ModbusPDU0BGetCommEventCounterError) + += MBAP Guess Payload ModbusPDU0CGetCommEventLogRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x02\xff\x0c') +assert isinstance(p.payload, ModbusPDU0CGetCommEventLogRequest) += MBAP Guess Payload ModbusPDU0CGetCommEventLogResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x02\xff\x0c\x00\x00\x00\x00\x00\x00\x00') +assert isinstance(p.payload, ModbusPDU0CGetCommEventLogResponse) += MBAP Guess Payload ModbusPDU0CGetCommEventLogError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x8c\x01') +assert isinstance(p.payload, ModbusPDU0CGetCommEventLogError) + += MBAP Guess Payload ModbusPDU0FWriteMultipleCoilsRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x08\xff\x0f\x00\x00\x00\x01\x01\x00') +assert isinstance(p.payload, ModbusPDU0FWriteMultipleCoilsRequest) += MBAP Guess Payload ModbusPDU0FWriteMultipleCoilsResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x06\xff\x0f\x00\x00\x00\x01') +assert isinstance(p.payload, ModbusPDU0FWriteMultipleCoilsResponse) += MBAP Guess Payload ModbusPDU0FWriteMultipleCoilsError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x8f\x01') +assert isinstance(p.payload, ModbusPDU0FWriteMultipleCoilsError) + += MBAP Guess Payload ModbusPDU10WriteMultipleRegistersRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\t\xff\x10\x00\x00\x00\x01\x02\x00\x00') +assert isinstance(p.payload, ModbusPDU10WriteMultipleRegistersRequest) += MBAP Guess Payload ModbusPDU10WriteMultipleRegistersResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x06\xff\x10\x00\x00\x00\x01') +assert isinstance(p.payload, ModbusPDU10WriteMultipleRegistersResponse) += MBAP Guess Payload ModbusPDU10WriteMultipleRegistersError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x90\x01') +assert isinstance(p.payload, ModbusPDU10WriteMultipleRegistersError) + += MBAP Guess Payload ModbusPDU11ReportSlaveIdRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x02\xff\x11') +assert isinstance(p.payload, ModbusPDU11ReportSlaveIdRequest) += MBAP Guess Payload ModbusPDU11ReportSlaveIdResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x11\x00') +assert isinstance(p.payload, ModbusPDU11ReportSlaveIdResponse) += MBAP Guess Payload ModbusPDU11ReportSlaveIdError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x91\x01') +assert isinstance(p.payload, ModbusPDU11ReportSlaveIdError) + += MBAP Guess Payload ModbusPDU14ReadFileRecordRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x03\xff\x14\x00') +assert isinstance(p.payload, ModbusPDU14ReadFileRecordRequest) += MBAP Guess Payload ModbusPDU14ReadFileRecordResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x14\x00') +assert isinstance(p.payload, ModbusPDU14ReadFileRecordResponse) += MBAP Guess Payload ModbusPDU14ReadFileRecordError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x91\x01') +assert isinstance(p.payload, ModbusPDU11ReportSlaveIdError) + += MBAP Guess Payload ModbusPDU15WriteFileRecordRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x03\xff\x15\x00') +assert isinstance(p.payload, ModbusPDU15WriteFileRecordRequest) += MBAP Guess Payload ModbusPDU15WriteFileRecordResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x15\x00') +assert isinstance(p.payload, ModbusPDU15WriteFileRecordResponse) += MBAP Guess Payload ModbusPDU15WriteFileRecordError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x95\x01') +assert isinstance(p.payload, ModbusPDU15WriteFileRecordError) + += MBAP Guess Payload ModbusPDU16MaskWriteRegisterRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x08\xff\x16\x00\x00\xff\xff\x00\x00') +assert isinstance(p.payload, ModbusPDU16MaskWriteRegisterRequest) += MBAP Guess Payload ModbusPDU16MaskWriteRegisterResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x08\xff\x16\x00\x00\xff\xff\x00\x00') +assert isinstance(p.payload, ModbusPDU16MaskWriteRegisterResponse) += MBAP Guess Payload ModbusPDU16MaskWriteRegisterError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x96\x01') +assert isinstance(p.payload, ModbusPDU16MaskWriteRegisterError) + += MBAP Guess Payload ModbusPDU16MaskWriteRegisterRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x08\xff\x16\x00\x00\xff\xff\x00\x00') +assert isinstance(p.payload, ModbusPDU16MaskWriteRegisterRequest) += MBAP Guess Payload ModbusPDU16MaskWriteRegisterResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x08\xff\x16\x00\x00\xff\xff\x00\x00') +assert isinstance(p.payload, ModbusPDU16MaskWriteRegisterResponse) += MBAP Guess Payload ModbusPDU16MaskWriteRegisterError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x96\x01') +assert isinstance(p.payload, ModbusPDU16MaskWriteRegisterError) + += MBAP Guess Payload ModbusPDU17ReadWriteMultipleRegistersRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\r\xff\x17\x00\x00\x00\x01\x00\x00\x00\x01\x02\x00\x00') +assert isinstance(p.payload, ModbusPDU17ReadWriteMultipleRegistersRequest) += MBAP Guess Payload ModbusPDU17ReadWriteMultipleRegistersResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x05\xff\x17\x02\x00\x00') +assert isinstance(p.payload, ModbusPDU17ReadWriteMultipleRegistersResponse) += MBAP Guess Payload ModbusPDU17ReadWriteMultipleRegistersError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x97\x01') +assert isinstance(p.payload, ModbusPDU17ReadWriteMultipleRegistersError) + += MBAP Guess Payload ModbusPDU18ReadFIFOQueueRequest +p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x04\xff\x18\x00\x00') +assert isinstance(p.payload, ModbusPDU18ReadFIFOQueueRequest) += MBAP Guess Payload ModbusPDU18ReadFIFOQueueResponse +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x06\xff\x18\x00\x02\x00\x00') +assert isinstance(p.payload, ModbusPDU18ReadFIFOQueueResponse) += MBAP Guess Payload ModbusPDU18ReadFIFOQueueError +p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\x98\x01') +assert isinstance(p.payload, ModbusPDU18ReadFIFOQueueError) = MBAP Guess Payload ModbusPDU2B0EReadDeviceIdentificationRequest (2 level test) p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x04\xff+\x0e\x01\x00') -assert(isinstance(p.payload, ModbusPDU2B0EReadDeviceIdentificationRequest)) +assert isinstance(p.payload, ModbusPDU2B0EReadDeviceIdentificationRequest) = MBAP Guess Payload ModbusPDU2B0EReadDeviceIdentificationResponse p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x1b\xff+\x0e\x01\x83\x00\x00\x03\x00\x08Pymodbus\x01\x02PM\x02\x031.0') -assert(isinstance(p.payload, ModbusPDU2B0EReadDeviceIdentificationResponse)) +assert isinstance(p.payload, ModbusPDU2B0EReadDeviceIdentificationResponse) = MBAP Guess Payload ModbusPDU2B0EReadDeviceIdentificationError p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x03\xff\xab\x01') -assert(isinstance(p.payload, ModbusPDU2B0EReadDeviceIdentificationError)) +assert isinstance(p.payload, ModbusPDU2B0EReadDeviceIdentificationError) = MBAP Guess Payload Reserved Function Request (Invalid payload) p = ModbusADURequest(b'\x00\x00\x00\x00\x00\x02\xff\x5b') -assert(isinstance(p.payload,ModbusPDUReservedFunctionCodeRequest)) +assert isinstance(p.payload,ModbusPDUReservedFunctionCodeRequest) = MBAP Guess Payload Reserved Function Response (Invalid payload) p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x02\xff\x7e') -assert(isinstance(p.payload, ModbusPDUReservedFunctionCodeResponse)) +assert isinstance(p.payload, ModbusPDUReservedFunctionCodeResponse) = MBAP Guess Payload Reserved Function Error (Invalid payload) p = ModbusADUResponse(b'\x00\x00\x00\x00\x00\x02\xff\x8a') -assert(isinstance(p.payload, ModbusPDUReservedFunctionCodeError)) +assert isinstance(p.payload, ModbusPDUReservedFunctionCodeError) = MBAP Guess Payload ModbusPDU02ReadDiscreteInputsResponse assert raw(ModbusPDU02ReadDiscreteInputsResponse()) == b'\x02\x01\x00' @@ -80,8 +260,8 @@ raw(ModbusPDU01ReadCoilsRequest()) == b'\x01\x00\x00\x00\x01' raw(ModbusPDU01ReadCoilsRequest(startAddr=16, quantity=2)) == b'\x01\x00\x10\x00\x02' = ModbusPDU01ReadCoilsRequest dissection p = ModbusPDU01ReadCoilsRequest(b'\x01\x00\x10\x00\x02') -assert(p.startAddr == 16) -assert(p.quantity == 2) +assert p.startAddr == 16 +assert p.quantity == 2 = ModbusPDU01ReadCoilsResponse raw(ModbusPDU01ReadCoilsResponse()) == b'\x01\x01\x00' @@ -89,8 +269,8 @@ raw(ModbusPDU01ReadCoilsResponse()) == b'\x01\x01\x00' raw(ModbusPDU01ReadCoilsResponse(coilStatus=[0x10]*3)) == b'\x01\x03\x10\x10\x10' = ModbusPDU01ReadCoilsResponse dissection p = ModbusPDU01ReadCoilsResponse(b'\x01\x03\x10\x10\x10') -assert(p.coilStatus == [16, 16, 16]) -assert(p.byteCount == 3) +assert p.coilStatus == [16, 16, 16] +assert p.byteCount == 3 = ModbusPDU01ReadCoilsError raw(ModbusPDU01ReadCoilsError()) == b'\x81\x01' @@ -98,8 +278,8 @@ raw(ModbusPDU01ReadCoilsError()) == b'\x81\x01' raw(ModbusPDU01ReadCoilsError(exceptCode=2)) == b'\x81\x02' = ModbusPDU81ReadCoilsError dissection p = ModbusPDU01ReadCoilsError(b'\x81\x02') -assert(p.funcCode == 0x81) -assert(p.exceptCode == 2) +assert p.funcCode == 0x81 +assert p.exceptCode == 2 # 0x02/0x82 Read Discrete Inputs Registers ------------------------------------------ = ModbusPDU02ReadDiscreteInputsRequest @@ -113,8 +293,8 @@ raw(ModbusPDU02ReadDiscreteInputsResponse()) == b'\x02\x01\x00' raw(ModbusPDU02ReadDiscreteInputsResponse(inputStatus=[0x02, 0x01])) == b'\x02\x02\x02\x01' = ModbusPDU02ReadDiscreteInputsRequest dissection p = ModbusPDU02ReadDiscreteInputsResponse(b'\x02\x02\x02\x01') -assert(p.byteCount == 2) -assert(p.inputStatus == [0x02, 0x01]) +assert p.byteCount == 2 +assert p.inputStatus == [0x02, 0x01] = ModbusPDU02ReadDiscreteInputsError raw(ModbusPDU02ReadDiscreteInputsError()) == b'\x82\x01' @@ -131,8 +311,8 @@ raw(ModbusPDU03ReadHoldingRegistersResponse()) == b'\x03\x02\x00\x00' 1==1 = ModbusPDU03ReadHoldingRegistersResponse dissection p = ModbusPDU03ReadHoldingRegistersResponse(b'\x03\x06\x02+\x00\x00\x00d') -assert(p.byteCount == 6) -assert(p.registerVal == [555, 0, 100]) +assert p.byteCount == 6 +assert p.registerVal == [555, 0, 100] = ModbusPDU03ReadHoldingRegistersError raw(ModbusPDU03ReadHoldingRegistersError()) == b'\x83\x01' @@ -250,21 +430,21 @@ raw(ModbusPDU11ReportSlaveIdError()) == b'\x91\x01' # 0x14/944 Read File Record --------------------------------------------------------- = ModbusPDU14ReadFileRecordRequest len parameters p = raw(ModbusPDU14ReadFileRecordRequest()/ModbusReadFileSubRequest()/ModbusReadFileSubRequest()) -assert(p == b'\x14\x0e\x06\x00\x01\x00\x00\x00\x01\x06\x00\x01\x00\x00\x00\x01') +assert p == b'\x14\x0e\x06\x00\x01\x00\x00\x00\x01\x06\x00\x01\x00\x00\x00\x01' = ModbusPDU14ReadFileRecordRequest minimal parameters p = raw(ModbusPDU14ReadFileRecordRequest()/ModbusReadFileSubRequest(fileNumber=4, recordNumber=1, recordLength=2)/ModbusReadFileSubRequest(fileNumber=3, recordNumber=9, recordLength=2)) -assert(p == b'\x14\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\t\x00\x02') +assert p == b'\x14\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\t\x00\x02' = ModbusPDU14ReadFileRecordRequest dissection p = ModbusPDU14ReadFileRecordRequest(b'\x14\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\t\x00\x02') -assert(isinstance(p.payload, ModbusReadFileSubRequest)) -assert(isinstance(p.payload.payload, ModbusReadFileSubRequest)) +assert isinstance(p.payload, ModbusReadFileSubRequest) +assert isinstance(p.payload.payload, ModbusReadFileSubRequest) = ModbusPDU14ReadFileRecordResponse minimal parameters raw(ModbusPDU14ReadFileRecordResponse()/ModbusReadFileSubResponse(recData=[0x0dfe, 0x0020])/ModbusReadFileSubResponse(recData=[0x33cd, 0x0040])) == b'\x14\x0c\x05\x06\r\xfe\x00 \x05\x063\xcd\x00@' = ModbusPDU14ReadFileRecordResponse dissection p = ModbusPDU14ReadFileRecordResponse(b'\x14\x0c\x05\x06\r\xfe\x00 \x05\x063\xcd\x00@') -assert(isinstance(p.payload, ModbusReadFileSubResponse)) -assert(isinstance(p.payload.payload, ModbusReadFileSubResponse)) +assert isinstance(p.payload, ModbusReadFileSubResponse) +assert isinstance(p.payload.payload, ModbusReadFileSubResponse) = ModbusPDU14ReadFileRecordError raw(ModbusPDU14ReadFileRecordError()) == b'\x94\x01' @@ -274,15 +454,15 @@ raw(ModbusPDU14ReadFileRecordError()) == b'\x94\x01' raw(ModbusPDU15WriteFileRecordRequest()/ModbusWriteFileSubRequest(fileNumber=4, recordNumber=7, recordData=[0x06af, 0x04be, 0x100d])) == b'\x15\r\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\r' = ModbusPDU15WriteFileRecordRequest dissection p = ModbusPDU15WriteFileRecordRequest(b'\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\r') -assert(isinstance(p.payload, ModbusWriteFileSubRequest)) -assert(p.payload.recordLength == 3) +assert isinstance(p.payload, ModbusWriteFileSubRequest) +assert p.payload.recordLength == 3 = ModbusPDU15WriteFileRecordResponse minimal parameters raw(ModbusPDU15WriteFileRecordResponse()/ModbusWriteFileSubResponse(fileNumber=4, recordNumber=7, recordData=[0x06af, 0x04be, 0x100d])) == b'\x15\r\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\r' = ModbusPDU15WriteFileRecordResponse dissection p = ModbusPDU15WriteFileRecordResponse(b'\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\r') -assert(isinstance(p.payload, ModbusWriteFileSubResponse)) -assert(p.payload.recordLength == 3) +assert isinstance(p.payload, ModbusWriteFileSubResponse) +assert p.payload.recordLength == 3 = ModbusPDU15WriteFileRecordError raw(ModbusPDU15WriteFileRecordError()) == b'\x95\x01' @@ -304,8 +484,8 @@ raw(ModbusPDU17ReadWriteMultipleRegistersRequest()) == b'\x17\x00\x00\x00\x01\x0 raw(ModbusPDU17ReadWriteMultipleRegistersRequest(writeRegistersValue=[0x0001, 0x0002])) == b'\x17\x00\x00\x00\x01\x00\x00\x00\x02\x04\x00\x01\x00\x02' = ModbusPDU17ReadWriteMultipleRegistersRequest dissection p = ModbusPDU17ReadWriteMultipleRegistersRequest(b'\x17\x00\x00\x00\x01\x00\x00\x00\x02\x04\x00\x01\x00\x02') -assert(p.byteCount == 4) -assert(p.writeQuantityRegisters == 2) +assert p.byteCount == 4 +assert p.writeQuantityRegisters == 2 = ModbusPDU17ReadWriteMultipleRegistersResponse raw(ModbusPDU17ReadWriteMultipleRegistersResponse()) == b'\x17\x02\x00\x00' @@ -328,8 +508,8 @@ raw(ModbusPDU18ReadFIFOQueueResponse()) == b'\x18\x00\x02\x00\x00' raw(ModbusPDU18ReadFIFOQueueResponse(FIFOVal=[0x0001, 0x0002, 0x0003])) == b'\x18\x00\x08\x00\x03\x00\x01\x00\x02\x00\x03' = ModbusPDU18ReadFIFOQueueResponse dissection p = ModbusPDU18ReadFIFOQueueResponse(b'\x18\x00\x08\x00\x03\x00\x01\x00\x02\x00\x03') -assert(p.byteCount == 8) -assert(p.FIFOCount == 3) +assert p.byteCount == 8 +assert p.FIFOCount == 3 = ModbusPDU18ReadFIFOQueueError raw(ModbusPDU18ReadFIFOQueueError()) == b'\x98\x01' @@ -345,12 +525,19 @@ raw(ModbusPDU2B0EReadDeviceIdentificationRequest()) == b'+\x0e\x01\x00' raw(ModbusPDU2B0EReadDeviceIdentificationResponse()) == b'+\x0e\x04\x01\x00\x00\x00' = ModbusPDU2B0EReadDeviceIdentificationResponse complete response p = raw(ModbusPDU2B0EReadDeviceIdentificationResponse(objCount=2)/ModbusObjectId(id=0, value="Obj1")/ModbusObjectId(id=1, value="Obj2")) -assert(p == b'+\x0e\x04\x01\x00\x00\x02\x00\x04Obj1\x01\x04Obj2') +assert p == b'+\x0e\x04\x01\x00\x00\x02\x00\x04Obj1\x01\x04Obj2' = ModbusPDU2B0EReadDeviceIdentificationResponse dissection p = ModbusPDU2B0EReadDeviceIdentificationResponse(b'+\x0e\x01\x83\x00\x00\x03\x00\x08Pymodbus\x01\x02PM\x02\x031.0') -assert(p.payload.payload.payload.id == 2) -assert(p.payload.payload.id == 1) -assert(p.payload.id == 0) +assert p.payload.payload.payload.id == 2 +assert p.payload.payload.id == 1 +assert p.payload.id == 0 = ModbusPDU2B0EReadDeviceIdentificationError raw(ModbusPDU2B0EReadDeviceIdentificationError()) == b'\xab\x01' + += Modbus test for payload subfield +# GH4112 +pkt = ModbusPDUUserDefinedFunctionCodeRequest(b'M\x00\x05\x00\n') +pkt = next(iter(pkt)) +assert pkt.mb_payload == b'\x00\x05\x00\n' + diff --git a/test/contrib/mount.uts b/test/contrib/mount.uts index d363aff7f07..ea08f4db342 100644 --- a/test/contrib/mount.uts +++ b/test/contrib/mount.uts @@ -24,20 +24,20 @@ MOUNT_Reply(status=1) = Layer Bindings for Mount Calls from scapy.contrib.oncrpc import * pkt = RPC()/RPC_Call()/NULL_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100005, 3, 0)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100005, 3, 0) pkt = RPC()/RPC_Call()/MOUNT_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100005, 3, 1)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100005, 3, 1) pkt = RPC()/RPC_Call()/UNMOUNT_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100005, 3, 3)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100005, 3, 3) = Layer Bindings for Mount Replies from scapy.contrib.oncrpc import * pkt = RPC()/RPC_Reply()/NULL_Reply() -assert(pkt.mtype == 1) +assert pkt.mtype == 1 pkt = RPC()/RPC_Reply()/MOUNT_Reply() -assert(pkt.mtype == 1) +assert pkt.mtype == 1 pkt = RPC()/RPC_Reply()/UNMOUNT_Reply() -assert(pkt.mtype == 1) +assert pkt.mtype == 1 + Test Built Packets vs Raw Strings diff --git a/test/contrib/mpls.uts b/test/contrib/mpls.uts index 1a38c746925..06d2d56d0fb 100644 --- a/test/contrib/mpls.uts +++ b/test/contrib/mpls.uts @@ -6,22 +6,20 @@ + MPLS = Build & dissect - IPv4 -if WINDOWS: - route_add_loopback() s = raw(Ether(src="00:01:02:04:05")/MPLS()/IP()) -assert(s == b'\xff\xff\xff\xff\xff\xff\x00\x01\x02\x04\x05\x00\x88G\x00\x00\x01\x00E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01') +assert s == b'\xff\xff\xff\xff\xff\xff\x00\x01\x02\x04\x05\x00\x88G\x00\x00\x01\x00E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01' p = Ether(s) -assert(MPLS in p and IP in p) +assert MPLS in p and IP in p = Build & dissect - IPv6 s = raw(Ether(src="00:01:02:04:05")/MPLS(s=0)/MPLS()/IPv6()) -assert(s == b'\xff\xff\xff\xff\xff\xff\x00\x01\x02\x04\x05\x00\x88G\x00\x000\x00\x00\x00!\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') +assert s == b'\xff\xff\xff\xff\xff\xff\x00\x01\x02\x04\x05\x00\x88G\x00\x000\x00\x00\x00!\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' p = Ether(s) -assert(IPv6 in p and isinstance(p[MPLS].payload, MPLS)) +assert IPv6 in p and isinstance(p[MPLS].payload, MPLS) = Association on IP and IPv6 p = IP()/MPLS() diff --git a/test/contrib/mqtt.uts b/test/contrib/mqtt.uts index 9371d020ec9..ad444a05104 100644 --- a/test/contrib/mqtt.uts +++ b/test/contrib/mqtt.uts @@ -13,119 +13,177 @@ from scapy.contrib.mqtt import * = MQTTPublish, packet instantiation p = MQTT()/MQTTPublish(topic='test1',value='test2') -assert(p.type == 3) -assert(p.topic == b'test1') -assert(p.value == b'test2') -assert(p.len == None) -assert(p.length == None) +assert p.type == 3 +assert p.topic == b'test1' +assert p.value == b'test2' +assert p.len == None +assert p.length == None = Fixed header and MQTTPublish, packet dissection s = b'0\n\x00\x04testtest' publish = MQTT(s) -assert(publish.type == 3) -assert(publish.QOS == 0) -assert(publish.DUP == 0) -assert(publish.RETAIN == 0) -assert(publish.len == 10) -assert(publish[MQTTPublish].length == 4) -assert(publish[MQTTPublish].topic == b'test') -assert(publish[MQTTPublish].value == b'test') - +assert publish.type == 3 +assert publish.QOS == 0 +assert publish.DUP == 0 +assert publish.RETAIN == 0 +assert publish.len == 10 +assert publish[MQTTPublish].length == 4 +assert publish[MQTTPublish].topic == b'test' +assert publish[MQTTPublish].value == b'test' + += MQTTPublish + +topicC = "testtopic/command" + +p1 = MQTT( + QOS=1 + ) / MQTTPublish( + topic=topicC, + msgid=1234, + value="msg1" + ) +p2 = MQTT( + QOS=1 + ) / MQTTPublish( + topic=topicC, + msgid=1235, + value="msg2" + ) + +p = MQTT(raw(p1 / p2)) +assert p[1].msgid == 1234 = MQTTConnect, packet instantiation c = MQTT()/MQTTConnect(clientIdlen=5, clientId='newid') -assert(c.type == 1) -assert(c.clientId == b'newid') -assert(c.clientIdlen == 5) +assert c.type == 1 +assert c.clientId == b'newid' +assert c.clientIdlen == 5 = MQTTConnect, packet dissection s = b'\x10\x1f\x00\x06MQIsdp\x03\x02\x00<\x00\x11mosqpub/1440-kali' connect = MQTT(s) -assert(connect.length == 6) -assert(connect.protoname == b'MQIsdp') -assert(connect.protolevel == 3) -assert(connect.usernameflag == 0) -assert(connect.passwordflag == 0) -assert(connect.willretainflag == 0) -assert(connect.willQOSflag == 0) -assert(connect.willflag == 0) -assert(connect.cleansess == 1) -assert(connect.reserved == 0) -assert(connect.klive == 60) -assert(connect.clientIdlen == 17) -assert(connect.clientId == b'mosqpub/1440-kali') - +assert connect.length == 6 +assert connect.protoname == b'MQIsdp' +assert connect.protolevel == 3 +assert connect.usernameflag == 0 +assert connect.passwordflag == 0 +assert connect.willretainflag == 0 +assert connect.willQOSflag == 0 +assert connect.willflag == 0 +assert connect.cleansess == 1 +assert connect.reserved == 0 +assert connect.klive == 60 +assert connect.clientIdlen == 17 +assert connect.clientId == b'mosqpub/1440-kali' + += MQTTDisconnect +mr = raw(MQTT()/MQTTDisconnect()) +dc= MQTT(mr) +assert dc.type == 14 =MQTTConnack, packet instantiation ck = MQTT()/MQTTConnack(sessPresentFlag=1,retcode=0) -assert(ck.type == 2) -assert(ck.sessPresentFlag == 1) -assert(ck.retcode == 0) +assert ck.type == 2 +assert ck.sessPresentFlag == 1 +assert ck.retcode == 0 = MQTTConnack, packet dissection s = b' \x02\x00\x00' connack = MQTT(s) -assert(connack.sessPresentFlag == 0) -assert(connack.retcode == 0) +assert connack.sessPresentFlag == 0 +assert connack.retcode == 0 = MQTTSubscribe, packet instantiation -sb = MQTT()/MQTTSubscribe(msgid=1,topic='newtopic',QOS=0,length=0) -assert(sb.type == 8) -assert(sb.msgid == 1) -assert(sb.topic == b'newtopic') -assert(sb.length == 0) -assert(sb[MQTTSubscribe].QOS == 0) +sb = MQTT()/MQTTSubscribe(msgid=1, topics=[MQTTTopicQOS(topic='newtopic', QOS=1, length=0)]) +assert sb.type == 8 +assert sb.msgid == 1 +assert sb.topics[0].topic == b'newtopic' +assert sb.topics[0].length == 0 +assert sb[MQTTSubscribe][MQTTTopicQOS].QOS == 1 = MQTTSubscribe, packet dissection -s = b'\x82\t\x00\x01\x00\x04test\x00' +s = b'\x82\t\x00\x01\x00\x04test\x01' subscribe = MQTT(s) -assert(subscribe.msgid == 1) -assert(subscribe.length == 4) -assert(subscribe.topic == b'test') -assert(subscribe.QOS == 1) +assert subscribe.msgid == 1 +assert subscribe.topics[0].length == 4 +assert subscribe.topics[0].topic == b'test' +assert subscribe.topics[0].QOS == 1 = MQTTSuback, packet instantiation -sk = MQTT()/MQTTSuback(msgid=1, retcode=0) -assert(sk.type == 9) -assert(sk.msgid == 1) -assert(sk.retcode == 0) +sk = MQTT()/MQTTSuback(msgid=1, retcodes=[0]) +assert sk.type == 9 +assert sk.msgid == 1 +assert sk.retcodes == [0] = MQTTSuback, packet dissection s = b'\x90\x03\x00\x01\x00' suback = MQTT(s) -assert(suback.msgid == 1) -assert(suback.retcode == 0) +assert suback.msgid == 1 +assert suback.retcodes == [0] +s = b'\x90\x03\x00\x01\x00\x01' +suback = MQTT(s) +assert suback.msgid == 1 +assert suback.retcodes == [0, 1] + += MQTTUnsubscribe, packet instantiation +unsb = MQTT()/MQTTUnsubscribe(msgid=1, topics=[MQTTTopic(topic='newtopic',length=0)]) +assert unsb.type == 10 +assert unsb.msgid == 1 +assert unsb.topics[0].topic == b'newtopic' +assert unsb.topics[0].length == 0 + += MQTTUnsubscribe, packet dissection +u = b'\xA2\x09\x00\x01\x00\x03\x61\x2F\x62' +unsubscribe = MQTT(u) +assert unsubscribe.msgid == 1 +assert unsubscribe.topics[0].length == 3 +assert unsubscribe.topics[0].topic == b'a/b' + += MQTTUnsuback, packet instantiation +unsk = MQTT()/MQTTUnsuback(msgid=1) +assert unsk.type == 11 +assert unsk.msgid == 1 + += MQTTUnsuback, packet dissection +u = b'\xb0\x02\x00\x01' +unsuback = MQTT(u) +assert unsuback.type == 11 +assert unsuback.msgid == 1 = MQTTPubrec, packet instantiation pc = MQTT()/MQTTPubrec(msgid=1) -assert(pc.type == 5) -assert(pc.msgid == 1) +assert pc.type == 5 +assert pc.msgid == 1 = MQTTPubrec packet dissection s = b'P\x02\x00\x01' pubrec = MQTT(s) -assert(pubrec.msgid == 1) +assert pubrec.msgid == 1 = MQTTPublish, long value p = MQTT()/MQTTPublish(topic='test1',value='a'*200) -assert(bytes(p)) -assert(p.type == 3) -assert(p.topic == b'test1') -assert(p.value == b'a'*200) -assert(p.len == None) -assert(p.length == None) +assert bytes(p) +assert p.type == 3 +assert p.topic == b'test1' +assert p.value == b'a'*200 +assert p.len == None +assert p.length == None = MQTT without payload p = MQTT() -assert(bytes(p) == b'\x10\x00') +assert bytes(p) == b'\x10\x00' = MQTT RandVariableFieldLen -assert(type(MQTT().fieldtype['len'].randval()) == RandVariableFieldLen) -assert(type(MQTT().fieldtype['len'].randval() + 0) == int) +assert type(MQTT().fieldtype['len'].randval()) == RandVariableFieldLen +assert type(MQTT().fieldtype['len'].randval() + 0) == int = MQTTUnsubscribe u = MQTT(b'\xA2\x0C\x00\x01\x00\x03\x61\x2F\x62\x00\x03\x63\x2F\x64') assert MQTTUnsubscribe in u and len(u.topics) == 2 and u.topics[1].topic == b"c/d" + += MQTTSubscribe +u = MQTT(b'\x82\x10\x00\x01\x00\x03\x61\x2F\x62\x02\x00\x03\x63\x2F\x64\x00') +assert MQTTSubscribe in u and len(u.topics) == 2 and u.topics[1].topic == b"c/d" diff --git a/test/contrib/mqttsn.uts b/test/contrib/mqttsn.uts index f0e34ce928d..564e4ff6a1a 100644 --- a/test/contrib/mqttsn.uts +++ b/test/contrib/mqttsn.uts @@ -757,41 +757,41 @@ assert p.payload.payload.data == 1115 * b"X" = MQTTSN without payload p = MQTTSN() -assert(bytes(p) == b"\x02\x00") +assert bytes(p) == b"\x02\x00" = MQTTSN without payload -- invalid lengths p = MQTTSN(len=1) try: bytes(p) # expect Scapy_Exception - assert(false) + assert false except Scapy_Exception: pass p = MQTTSN(len=0x10000) try: bytes(p) # expect Scapy_Exception - assert(false) + assert false except Scapy_Exception: pass b = '\x01' try: p = MQTTSN(b) # expect Scapy_Exception - assert(false) + assert false except Scapy_Exception: pass b = '\x01\x02' try: p = MQTTSN(b) # expect Scapy_Exception - assert(false) + assert false except Scapy_Exception: pass = MQTT-SN RandVariableFieldLen -assert(type(MQTTSN().fieldtype["len"].randval()) == RandVariableFieldLen) -assert(type(MQTTSN().fieldtype["len"].randval() + 0) == int) +assert type(MQTTSN().fieldtype["len"].randval()) == RandVariableFieldLen +assert type(MQTTSN().fieldtype["len"].randval() + 0) == int = Disect full IPv6 packages ~ dport == 1883 (0x75b) diff --git a/test/contrib/nfs.uts b/test/contrib/nfs.uts index e1100a57cbb..fd981929f23 100644 --- a/test/contrib/nfs.uts +++ b/test/contrib/nfs.uts @@ -85,92 +85,92 @@ COMMIT_Reply(status=1) = Layer Bindings for NFS Calls from scapy.contrib.oncrpc import * pkt = RPC()/RPC_Call()/NULL_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 0)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 0) pkt = RPC()/RPC_Call()/GETATTR_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 1)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 1) pkt = RPC()/RPC_Call()/SETATTR_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 2)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 2) pkt = RPC()/RPC_Call()/LOOKUP_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 3)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 3) pkt = RPC()/RPC_Call()/ACCESS_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 4)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 4) pkt = RPC()/RPC_Call()/READLINK_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 5)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 5) pkt = RPC()/RPC_Call()/READ_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 6)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 6) pkt = RPC()/RPC_Call()/WRITE_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 7)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 7) pkt = RPC()/RPC_Call()/CREATE_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 8)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 8) pkt = RPC()/RPC_Call()/MKDIR_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 9)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 9) pkt = RPC()/RPC_Call()/SYMLINK_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 10)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 10) pkt = RPC()/RPC_Call()/REMOVE_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 12)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 12) pkt = RPC()/RPC_Call()/RMDIR_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 13)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 13) pkt = RPC()/RPC_Call()/RENAME_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 14)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 14) pkt = RPC()/RPC_Call()/LINK_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 15)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 15) pkt = RPC()/RPC_Call()/READDIR_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 16)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 16) pkt = RPC()/RPC_Call()/READDIRPLUS_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 17)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 17) pkt = RPC()/RPC_Call()/FSSTAT_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 18)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 18) pkt = RPC()/RPC_Call()/FSINFO_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 19)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 19) pkt = RPC()/RPC_Call()/PATHCONF_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 20)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 20) pkt = RPC()/RPC_Call()/COMMIT_Call() -assert((pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 21)) +assert (pkt.mtype, pkt.program, pkt.pversion, pkt.procedure) == (0, 100003, 3, 21) = Layer Bindings for NFS Replies from scapy.contrib.oncrpc import * pkt = RPC()/RPC_Reply()/NULL_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/GETATTR_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/SETATTR_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/LOOKUP_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/ACCESS_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/READLINK_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/READ_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/WRITE_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/CREATE_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/MKDIR_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/SYMLINK_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/REMOVE_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/RMDIR_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/RENAME_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/LINK_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/READDIR_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/READDIRPLUS_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/FSSTAT_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/FSINFO_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/PATHCONF_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 pkt = RPC()/RPC_Reply()/COMMIT_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 + Test Built Packets Against Raw Strings @@ -601,104 +601,32 @@ pkt = ACCESS_Reply( ) assert bytes(pkt) == b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x01\xed\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x04\x00\x00\x00\x05\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xdd\xdd\xdd\xdd\xee\xee\xee\xee\xff\xff\xff\xff\x11\x11\x11\x11""""3333\x00\x00\x00\n' -pkt = READDIRPLUS_Reply( - status=0, - attributes_follow=1, - attributes=Fattr3( - type='NF3DIR', - mode=0o755, - nlink=1, - uid=2, - gid=3, - size=0xffffffffffffffff, - used=0xaaaaaaaaaaaaaaaa, - rdev=[4, 5], - fsid=0xbbbbbbbbbbbbbbbb, - fileid=0xcccccccccccccccc, - atime_s=0xdddddddd, - atime_ns=0xeeeeeeee, - mtime_s=0xffffffff, - mtime_ns=0x11111111, - ctime_s=0x22222222, - ctime_ns=0x33333333 - ), - verifier=0xa, - value_follows=1, - files=[ - File_From_Dir_Plus( - fileid=0xa, - filename=Object_Name( - length=5, - _name='file1', - fill='\x00\x00\x00' - ), - cookie=0xb, - attributes_follow=1, - attributes=Fattr3( - type='NF3REG', - mode=0o755, - nlink=1, - uid=2, - gid=3, - size=0xffffffffffffffff, - used=0xaaaaaaaaaaaaaaaa, - rdev=[4, 5], - fsid=0xbbbbbbbbbbbbbbbb, - fileid=0xcccccccccccccccc, - atime_s=0xdddddddd, - atime_ns=0xeeeeeeee, - mtime_s=0xffffffff, - mtime_ns=0x11111111, - ctime_s=0x22222222, - ctime_ns=0x33333333 - ), - handle_follows=1, - filehandle=File_Object( - length=3, - fh='fh1', - fill='\x00' - ), - value_follows=1 - ), - - File_From_Dir_Plus( - fileid=0xb, - filename=Object_Name( - length=5, - _name='file2', - fill='\x00\x00\x00' - ), - cookie=0xc, - attributes_follow=1, - attributes=Fattr3( - type='NF3REG', - mode=0o755, - nlink=1, - uid=2, - gid=3, - size=0xffffffffffffffff, - used=0xaaaaaaaaaaaaaaaa, - rdev=[4, 5], - fsid=0xbbbbbbbbbbbbbbbb, - fileid=0xcccccccccccccccc, - atime_s=0xdddddddd, - atime_ns=0xeeeeeeee, - mtime_s=0xffffffff, - mtime_ns=0x11111111, - ctime_s=0x22222222, - ctime_ns=0x33333333 - ), - handle_follows=1, - filehandle=File_Object( - length=3, - fh='fh2', - fill='\x00' - ), - value_follows=0 - ) - ], - eof=1 -) +pkt = READDIRPLUS_Reply(status=0, attributes_follow=1, + attributes=Fattr3(type='NF3DIR', mode=0o755, nlink=1, uid=2, + gid=3, size=0xffffffffffffffff, used=0xaaaaaaaaaaaaaaaa, + rdev=[4, 5], fsid=0xbbbbbbbbbbbbbbbb, fileid=0xcccccccccccccccc, + atime_s=0xdddddddd, atime_ns=0xeeeeeeee, mtime_s=0xffffffff, + mtime_ns=0x11111111, ctime_s=0x22222222, ctime_ns=0x33333333), + verifier=0xa, value_follows=1, + files=[File_From_Dir_Plus(fileid=0xa, + filename=Object_Name(length=5, _name='file1', fill='\x00\x00\x00'), + cookie=0xb, attributes_follow=1, + attributes=Fattr3(type='NF3REG', mode=0o755, nlink=1, uid=2, gid=3, + size=0xffffffffffffffff, used=0xaaaaaaaaaaaaaaaa, + rdev=[4, 5], fsid=0xbbbbbbbbbbbbbbbb, + fileid=0xcccccccccccccccc, atime_s=0xdddddddd, + atime_ns=0xeeeeeeee, mtime_s=0xffffffff, + mtime_ns=0x11111111, ctime_s=0x22222222, + ctime_ns=0x33333333), + handle_follows=1, filehandle=File_Object(length=3, fh='fh1', fill='\x00'), + value_follows=1), + File_From_Dir_Plus(fileid=0xb, filename=Object_Name(length=5, _name='file2', fill='\x00\x00\x00'), + cookie=0xc, attributes_follow=1, attributes=Fattr3(type='NF3REG', mode=0o755, nlink=1, uid=2, + gid=3, size=0xffffffffffffffff, used=0xaaaaaaaaaaaaaaaa, rdev=[4, 5], fsid=0xbbbbbbbbbbbbbbbb, + fileid=0xcccccccccccccccc, atime_s=0xdddddddd, atime_ns=0xeeeeeeee, mtime_s=0xffffffff, + mtime_ns=0x11111111, ctime_s=0x22222222, ctime_ns=0x33333333), handle_follows=1, + filehandle=File_Object(length=3, fh='fh2', fill='\x00'), value_follows=0) + ], eof=1) assert bytes(pkt) == b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x01\xed\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x04\x00\x00\x00\x05\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xdd\xdd\xdd\xdd\xee\xee\xee\xee\xff\xff\xff\xff\x11\x11\x11\x11""""3333\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x05file1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x01\xed\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x04\x00\x00\x00\x05\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xdd\xdd\xdd\xdd\xee\xee\xee\xee\xff\xff\xff\xff\x11\x11\x11\x11""""3333\x00\x00\x00\x01\x00\x00\x00\x03fh1\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x05file2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x01\xed\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x04\x00\x00\x00\x05\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xdd\xdd\xdd\xdd\xee\xee\xee\xee\xff\xff\xff\xff\x11\x11\x11\x11""""3333\x00\x00\x00\x01\x00\x00\x00\x03fh2\x00\x00\x00\x00\x00\x00\x00\x00\x01' pkt = WRITE_Reply( diff --git a/test/contrib/nsh.uts b/test/contrib/nsh.uts index 2c7e91f7b21..0751edd1338 100644 --- a/test/contrib/nsh.uts +++ b/test/contrib/nsh.uts @@ -1,7 +1,7 @@ + Basic Layer Tests = Build a NSH over NSH packet with SPI=42, and SI=1 -raw(NSH(spi=42, si=1)/NSH()) == b'\x0f\xc6\x01\x03\x00\x00*\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xc6\x01\x03\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +raw(NSH(spi=42, si=1)/NSH()) == b'\x0f\xc6\x01\x04\x00\x00*\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xc6\x01\x03\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' = Build a NSH with Fixed context headers raw(NSH(ttl=25, spi=55, si=34, context_header=b"\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\xff\xff\xff\xff")) == b'\x06F\x01\x03\x00\x007"\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\xff\xff\xff\xff' @@ -14,3 +14,7 @@ raw(Ether(src="00:00:00:00:00:01", dst="00:00:00:00:00:02")/IP(src="1.1.1.1", ds = 0 length variable length context header NSH raw(NSH(mdtype=2, spi=0xF0F0F0, si=0xFF)) == b'\x0f\xc2\x02\x03\xf0\xf0\xf0\xff' + += Build a NSH over VXLAN packet and verify bindings +raw(Ether(dst='0c:42:a1:5f:fb:e0', src='b8:59:9f:cd:de:3e')/IPv6(src='::1', dst='::2')/UDP(sport=10, dport=8472)/VXLAN(NextProtocol=4, vni=4660)/NSH()/NSH()/Ether(dst='0c:42:a1:5f:fb:e4', src='b8:59:9f:cd:de:33')/IP(src='10.200.100.10', dst='2.2.2.3')/TCP(sport=123, dport=333)) == b'\x0cB\xa1_\xfb\xe0\xb8Y\x9f\xcd\xde>\x86\xdd`\x00\x00\x00\x00v\x11@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\n!\x18\x00v\x05F\x0c\x00\x00\x04\x00\x124\x00\x0f\xc6\x01\x04\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xc6\x01\x03\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0cB\xa1_\xfb\xe4\xb8Y\x9f\xcd\xde3\x08\x00E\x00\x00(\x00\x01\x00\x00@\x06\x07\xf9\n\xc8d\n\x02\x02\x02\x03\x00{\x01M\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x1bD\x00\x00' + diff --git a/test/contrib/oam.uts b/test/contrib/oam.uts new file mode 100644 index 00000000000..b2b85bcd1b1 --- /dev/null +++ b/test/contrib/oam.uts @@ -0,0 +1,670 @@ +# OAM unit tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('oam')" -t test/contrib/oam.uts + ++ TLV + += Generic TLV + +pkt = OAM_TLV(raw(OAM_TLV()/Raw(b'123'))) +assert pkt.type == 1 +assert pkt.length == 3 + += Data TLV + +pkt = OAM_DATA_TLV(raw(OAM_DATA_TLV()/Raw(b'123'))) +assert pkt.type == 3 +assert pkt.length == 3 + += Test TLV + +from binascii import crc32 + +pkt = OAM_TEST_TLV(raw(OAM_TEST_TLV(pat_type="Null signal without CRC-32")/Raw(b'123'))) +assert pkt.type == 32 +assert pkt.length == 4 +assert raw(pkt.payload) == b'123' +pkt = OAM_TEST_TLV(raw(OAM_TEST_TLV(pat_type="Null signal with CRC-32")/Raw(b'123'))) +assert pkt.type == 32 +assert pkt.length == 8 +assert pkt.crc == crc32(raw(pkt)[:-4]) % (1 << 32) +assert pkt.crc == 0xad147086 +assert raw(pkt.payload) == b'123' +pkt = OAM_TEST_TLV(raw(OAM_TEST_TLV(pat_type="PRBS 2^-31 - 1 without CRC-32")/Raw(b'123'))) +assert pkt.type == 32 +assert pkt.length == 4 +assert raw(pkt.payload) == b'123' +pkt = OAM_TEST_TLV(raw(OAM_TEST_TLV(pat_type="PRBS 2^-31 - 1 with CRC-32")/Raw(b'123'))) +assert pkt.type == 32 +assert pkt.length == 8 +assert pkt.crc == crc32(raw(pkt)[:-4]) % (1 << 32) +assert pkt.crc == 0x71db80d +assert raw(pkt.payload) == b'123' + += LTM TLV + +pkt = OAM_LTM_TLV(raw(OAM_LTM_TLV(egress_id=3)/Raw(b'123'))) +assert pkt.type == 7 +assert pkt.length == 8 +assert pkt.egress_id == 3 + += LTR TLV + +pkt = OAM_LTR_TLV(raw(OAM_LTR_TLV(last_egress_id=2, next_egress_id=4)/Raw(b'123'))) +assert pkt.type == 8 +assert pkt.length == 16 +assert pkt.last_egress_id == 2 +assert pkt.next_egress_id == 4 + += LTR IG TLV + +pkt = OAM_LTR_IG_TLV(raw(OAM_LTR_IG_TLV(ingress_act=2, ingress_mac="00:11:22:33:44:55")/Raw(b'123'))) +assert pkt.type == 5 +assert pkt.length == 7 +assert pkt.ingress_act == 2 +assert pkt.ingress_mac == "00:11:22:33:44:55" + += LTR EG TLV + +pkt = OAM_LTR_EG_TLV(raw(OAM_LTR_EG_TLV(egress_act=2, egress_mac="00:11:22:33:44:55")/Raw(b'123'))) +assert pkt.type == 6 +assert pkt.length == 7 +assert pkt.egress_act == 2 +assert pkt.egress_mac == "00:11:22:33:44:55" + += TEST ID TLV + +pkt = OAM_TEST_ID_TLV(raw(OAM_TEST_ID_TLV(test_id=1)/Raw(b'123'))) +assert pkt.type == 36 +assert pkt.length == 32 +assert pkt.test_id == 1 + += PTP TIMESTAMP + +pkt = PTP_TIMESTAMP(raw(PTP_TIMESTAMP(seconds=5, nanoseconds=10)/Raw(b'123'))) +assert pkt.seconds == 5 +assert pkt.nanoseconds == 10 + += APS + +pkt = APS(raw(APS(req_st="Wait-to-restore (WTR)", + prot_type="D+A", + req_sig="Normal traffic", + br_sig="Normal traffic", + br_type="T")/Raw(b'123'))) +assert pkt.req_st == 0b0101 +assert pkt.prot_type == 0b1010 +assert pkt.req_sig == 1 +assert pkt.br_sig == 1 +assert pkt.br_type == 0b10000000 + += RAPS + +pkt = RAPS(raw(RAPS(req_st="Signal fail(SF)", + status="RB+BPR", + node_id="00:11:22:33:44:55")/Raw(b'123'))) +assert pkt.req_st == 0b1011 +assert pkt.sub_code == 0b0000 +assert pkt.status == 0b10100000 +assert pkt.node_id == "00:11:22:33:44:55" + ++ MEG ID + += MEG ID + +pkt = MegId(raw(MegId(format=1, + values=int(0xdeadbeef)))) +assert pkt.format == 1 +# FIXME: make compatible with python2 +# assert pkt.values.to_bytes(45, "little")[-4:] == b"\xde\xad\xbe\xef" +assert pkt.length == 45 +assert len(raw(pkt)) == 48 + += MEG ICC ID + +pkt = MegId(raw(MegId(format=32, + values=list(range(13))))) + +assert pkt.format == 32 +assert pkt.values == list(range(13)) +assert pkt.length == 13 +assert len(raw(pkt)) == 48 + += MEG ICC and CC ID + +pkt = MegId(raw(MegId(format=33, + values=list(range(15))))) + +assert pkt.format == 33 +assert pkt.values == list(range(15)) +assert pkt.length == 15 +assert len(raw(pkt)) == 48 + ++ OAM +~ tshark + += Define check_tshark function + +def check_tshark(pkt, string): + import tempfile, os + fd, pcapfilename = tempfile.mkstemp() + wrpcap(pcapfilename, pkt) + rv = tcpdump(pcapfilename, prog=conf.prog.tshark, getfd=True, args=['-Y', 'cfm'], dump=True, wait=True) + assert string in rv.decode("utf8") + os.close(fd) + os.unlink(pcapfilename) + += CCM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Continuity Check Message (CCM)", + flags="RDI", + period="Trans Int 10s", + mep_id=0xffff, + meg_id=MegId(format=32, + values=list(range(13))), + txfcf=1, + rxfcb=2, + txfcb=3))) + +assert pkt[OAM].opcode == 1 +assert pkt[OAM].period == 5 +assert pkt[OAM].tlv_offset == 70 +assert pkt[OAM].flags.RDI == True +assert pkt[OAM].flags == 1<<4 +assert pkt[OAM].mep_id == 0xffff +assert pkt[OAM].meg_id.format == 32 +assert pkt[OAM].meg_id.length == 13 +assert pkt[OAM].meg_id.values == list(range(13)) +assert pkt[OAM].txfcf == 1 +assert pkt[OAM].rxfcb == 2 +assert pkt[OAM].txfcb == 3 + +check_tshark(pkt, "(CCM)") + += LBM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Loopback Message (LBM)", + seq_num=33, + tlvs=[OAM_DATA_TLV()/Raw(b'123'), + OAM_DATA_TLV()/Raw(b'456'), + OAM_DATA_TLV()/Raw(b'789')]))) + +assert pkt[OAM].opcode == 3 +assert pkt[OAM].tlv_offset == 4 +assert pkt[OAM].seq_num == 33 +for i in range(3): + assert pkt[OAM].tlvs[i].type == 3 + assert pkt[OAM].tlvs[i].length == 3 + +assert raw(pkt[OAM].tlvs[0].payload) == b'123' +assert raw(pkt[OAM].tlvs[1].payload) == b'456' +assert raw(pkt[OAM].tlvs[2].payload) == b'789' + +check_tshark(pkt, "(LBM)") + += LTM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Linktrace Message (LTM)", + trans_id=12, + ttl=21, + flags="HWonly", + orig_mac="12:34:56:78:90:11", + targ_mac="12:34:56:78:90:22", + tlvs=[OAM_LTM_TLV(egress_id=12)]))) + +assert pkt[OAM].opcode == 5 +assert pkt[OAM].tlv_offset == 17 +assert pkt[OAM].ttl == 21 +assert pkt[OAM].flags.HWonly == True +assert pkt[OAM].flags == 1<<7 +assert pkt[OAM].orig_mac == "12:34:56:78:90:11" +assert pkt[OAM].targ_mac == "12:34:56:78:90:22" +assert pkt[OAM].tlvs[0].type == 7 +assert pkt[OAM].tlvs[0].length == 8 +assert pkt[OAM].tlvs[0].egress_id == 12 + +check_tshark(pkt, "(LTM)") + += LTR + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Linktrace Reply (LTR)", + trans_id=21, + ttl=12, + flags="HWonly+TerminalMEP", + relay_act=8, + tlvs=[OAM_LTR_TLV(last_egress_id=1, next_egress_id=2), + OAM_LTR_TLV(last_egress_id=3, next_egress_id=4), + OAM_LTR_IG_TLV(ingress_act=1, ingress_mac="12:34:56:78:90:11"), + OAM_LTR_IG_TLV(ingress_act=6, ingress_mac="12:34:56:78:90:22"), + OAM_LTR_EG_TLV(egress_act=2, egress_mac="12:34:56:78:90:33"), + OAM_LTR_EG_TLV(egress_act=3, egress_mac="12:34:56:78:90:44")]))) + +assert pkt[OAM].opcode == 4 +assert pkt[OAM].tlv_offset == 6 +assert pkt[OAM].ttl == 12 +assert pkt[OAM].flags.HWonly == True +assert pkt[OAM].flags.FwdYes == False +assert pkt[OAM].flags.TerminalMEP == True +assert pkt[OAM].flags == (1<<7) | (1<<5) +assert pkt[OAM].relay_act == 8 +assert pkt[OAM].tlvs[0].type == 8 +assert pkt[OAM].tlvs[0].length == 16 +assert pkt[OAM].tlvs[0].last_egress_id == 1 +assert pkt[OAM].tlvs[0].next_egress_id == 2 +assert pkt[OAM].tlvs[1].type == 8 +assert pkt[OAM].tlvs[1].length == 16 +assert pkt[OAM].tlvs[1].last_egress_id == 3 +assert pkt[OAM].tlvs[1].next_egress_id == 4 +assert pkt[OAM].tlvs[2].type == 5 +assert pkt[OAM].tlvs[2].length == 7 +assert pkt[OAM].tlvs[2].ingress_act == 1 +assert pkt[OAM].tlvs[2].ingress_mac == "12:34:56:78:90:11" +assert pkt[OAM].tlvs[3].type == 5 +assert pkt[OAM].tlvs[3].length == 7 +assert pkt[OAM].tlvs[3].ingress_act == 6 +assert pkt[OAM].tlvs[3].ingress_mac == "12:34:56:78:90:22" +assert pkt[OAM].tlvs[4].type == 6 +assert pkt[OAM].tlvs[4].length == 7 +assert pkt[OAM].tlvs[4].egress_act == 2 +assert pkt[OAM].tlvs[4].egress_mac == "12:34:56:78:90:33" +assert pkt[OAM].tlvs[5].type == 6 +assert pkt[OAM].tlvs[5].length == 7 +assert pkt[OAM].tlvs[5].egress_act == 3 +assert pkt[OAM].tlvs[5].egress_mac == "12:34:56:78:90:44" + +check_tshark(pkt, "(LTR)") + += AIS + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Alarm Indication Signal (AIS)", + period="1 frame per second"))) + +assert pkt[OAM].opcode == 33 +assert pkt[OAM].tlv_offset == 0 +assert pkt[OAM].period == 0b100 + +check_tshark(pkt, "(AIS)") + += LCK + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Lock Signal (LCK)", + period="1 frame per second"))) + +assert pkt[OAM].opcode == 35 +assert pkt[OAM].tlv_offset == 0 +assert pkt[OAM].period == 0b100 + +check_tshark(pkt, "(LCK)") + += TST + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Test Signal (TST)", + seq_num=15, + tlvs=[OAM_TEST_TLV(pat_type="Null signal without CRC-32")/Raw(b'123'), + OAM_TEST_TLV(pat_type="Null signal without CRC-32")/Raw(b'23456'), + OAM_TEST_TLV(pat_type="Null signal with CRC-32")/Raw(b'123'), + OAM_TEST_TLV(pat_type="Null signal with CRC-32")/Raw(b'23456'), + OAM_TEST_TLV(pat_type="PRBS 2^-31 - 1 without CRC-32")/Raw(b'123'), + OAM_TEST_TLV(pat_type="PRBS 2^-31 - 1 without CRC-32")/Raw(b'23456'), + OAM_TEST_TLV(pat_type="PRBS 2^-31 - 1 with CRC-32")/Raw(b'123'), + OAM_TEST_TLV(pat_type="PRBS 2^-31 - 1 with CRC-32")/Raw(b'23456')]))) + +assert pkt[OAM].opcode == 37 +assert pkt[OAM].tlv_offset == 4 +assert pkt[OAM].seq_num == 15 + +assert pkt[OAM].tlvs[0].type == 32 +assert pkt[OAM].tlvs[0].length == 4 +assert pkt[OAM].tlvs[0].pat_type == 0 +assert raw(pkt[OAM].tlvs[0].payload) == b'123' +assert pkt[OAM].tlvs[1].type == 32 +assert pkt[OAM].tlvs[1].length == 6 +assert pkt[OAM].tlvs[1].pat_type == 0 +assert raw(pkt[OAM].tlvs[1].payload) == b'23456' +assert pkt[OAM].tlvs[2].type == 32 +assert pkt[OAM].tlvs[2].length == 8 +assert pkt[OAM].tlvs[2].pat_type == 1 +assert raw(pkt[OAM].tlvs[2].payload) == b'123' +assert pkt[OAM].tlvs[2].crc == crc32(raw(pkt[OAM].tlvs[2])[:-4]) % (1 << 32) +assert pkt[OAM].tlvs[3].type == 32 +assert pkt[OAM].tlvs[3].length == 10 +assert pkt[OAM].tlvs[3].pat_type == 1 +assert raw(pkt[OAM].tlvs[3].payload) == b'23456' +assert pkt[OAM].tlvs[3].crc == crc32(raw(pkt[OAM].tlvs[3])[:-4]) % (1 << 32) +assert pkt[OAM].tlvs[4].type == 32 +assert pkt[OAM].tlvs[4].length == 4 +assert pkt[OAM].tlvs[4].pat_type == 2 +assert raw(pkt[OAM].tlvs[4].payload) == b'123' +assert pkt[OAM].tlvs[5].type == 32 +assert pkt[OAM].tlvs[5].length == 6 +assert pkt[OAM].tlvs[5].pat_type == 2 +assert raw(pkt[OAM].tlvs[5].payload) == b'23456' +assert pkt[OAM].tlvs[6].type == 32 +assert pkt[OAM].tlvs[6].length == 8 +assert pkt[OAM].tlvs[6].pat_type == 3 +assert raw(pkt[OAM].tlvs[6].payload) == b'123' +assert pkt[OAM].tlvs[6].crc == crc32(raw(pkt[OAM].tlvs[6])[:-4]) % (1 << 32) +assert pkt[OAM].tlvs[7].type == 32 +assert pkt[OAM].tlvs[7].length == 10 +assert pkt[OAM].tlvs[7].pat_type == 3 +assert raw(pkt[OAM].tlvs[7].payload) == b'23456' +assert pkt[OAM].tlvs[7].crc == crc32(raw(pkt[OAM].tlvs[7])[:-4]) % (1 << 32) + +check_tshark(pkt, "(TST)") + += APS + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Automatic Protection Switching (APS)", + aps=APS(req_st="Forced switch (FS)", + prot_type="A+B+R", + req_sig="Normal traffic", + br_sig="Null signal", + br_type="T")))) + +assert pkt[OAM].opcode == 39 +assert pkt[APS].req_st == 0b1101 +assert pkt[APS].prot_type.A == True +assert pkt[APS].prot_type.B == True +assert pkt[APS].prot_type.R == True +assert pkt[APS].prot_type == 0b1101 +assert pkt[APS].req_sig == 1 +assert pkt[APS].br_sig == 0 +assert pkt[APS].br_type.T == True +assert pkt[APS].br_type == (1 << 7) + +check_tshark(pkt, "(APS)") + += RAPS + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Ring-Automatic Protection Switching (R-APS)", + raps=RAPS(req_st="Event", + sub_code="Flush", + status="RB+BPR", + node_id="12:12:12:23:23:23")))) + +assert pkt[OAM].opcode == 40 +assert pkt[RAPS].req_st == 0b1110 +assert pkt[RAPS].sub_code == 0b0000 +assert pkt[RAPS].status.RB == True +assert pkt[RAPS].status.DNF == False +assert pkt[RAPS].status.BPR == True +assert pkt[RAPS].status == (1 << 7) | (1 << 5) +assert pkt[RAPS].node_id == "12:12:12:23:23:23" + +check_tshark(pkt, "(R-APS)") + += MCC + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Maintenance Communication Channel (MCC)", + oui=12, + subopcode=2))) + +assert pkt[OAM].opcode == 41 +assert pkt[OAM].oui == 12 +assert pkt[OAM].subopcode == 2 + +check_tshark(pkt, "(MCC)") + += LMM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Loss Measurement Message (LMM)", + flags="Proactive", + txfcf=1, + rxfcf=2, + txfcb=3))) + +assert pkt[OAM].opcode == 43 +assert pkt[OAM].version == 1 +assert pkt[OAM].tlv_offset == 12 +assert pkt[OAM].flags == 1 +assert pkt[OAM].flags.Proactive == True +assert pkt[OAM].txfcf == 1 +assert pkt[OAM].rxfcf == 2 +assert pkt[OAM].txfcb == 3 + +check_tshark(pkt, "(LMM)") + += LMR + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Loss Measurement Reply (LMR)", + txfcf=1, + rxfcf=2, + txfcb=3))) + +assert pkt[OAM].opcode == 42 +assert pkt[OAM].txfcf == 1 +assert pkt[OAM].rxfcf == 2 +assert pkt[OAM].txfcb == 3 + +check_tshark(pkt, "(LMR)") + += 1DM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="One Way Delay Measurement (1DM)", + txtsf=PTP_TIMESTAMP(seconds=1, nanoseconds=2), + rxtsf=PTP_TIMESTAMP(seconds=3, nanoseconds=4), + tlvs=[OAM_DATA_TLV()/Raw(b'123'), + OAM_DATA_TLV()/Raw(b'456789'), + OAM_TEST_ID_TLV(test_id=5)]))) + +assert pkt[OAM].opcode == 45 +assert pkt[OAM].version == 1 +assert pkt[OAM].tlv_offset == 16 +assert pkt[OAM].txtsf.seconds == 1 +assert pkt[OAM].txtsf.nanoseconds == 2 +assert pkt[OAM].rxtsf.seconds == 3 +assert pkt[OAM].rxtsf.nanoseconds == 4 +assert pkt[OAM].tlvs[0].type == 3 +assert pkt[OAM].tlvs[0].length == 3 +assert raw(pkt[OAM].tlvs[0].payload) == b'123' +assert pkt[OAM].tlvs[1].type == 3 +assert pkt[OAM].tlvs[1].length == 6 +assert raw(pkt[OAM].tlvs[1].payload) == b'456789' +assert pkt[OAM].tlvs[2].type == 36 +assert pkt[OAM].tlvs[2].length == 32 +assert pkt[OAM].tlvs[2].test_id == 5 + +# FIXME: for some reason wireshark does not like OAM_TEST_ID_TLV here +check_tshark(pkt, "(1DM)") + += DMM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Delay Measurement Message (DMM)", + txtsf=PTP_TIMESTAMP(seconds=1, nanoseconds=2), + txtsb=PTP_TIMESTAMP(seconds=2, nanoseconds=1), + rxtsf=PTP_TIMESTAMP(seconds=3, nanoseconds=4), + rxtsb=PTP_TIMESTAMP(seconds=6, nanoseconds=5), + tlvs=[OAM_DATA_TLV()/Raw(b'123'), + OAM_DATA_TLV()/Raw(b'456789'), + OAM_TEST_ID_TLV(test_id=5)]))) + +assert pkt[OAM].opcode == 47 +assert pkt[OAM].version == 1 +assert pkt[OAM].tlv_offset == 32 +assert pkt[OAM].txtsf.seconds == 1 +assert pkt[OAM].txtsf.nanoseconds == 2 +assert pkt[OAM].rxtsf.seconds == 3 +assert pkt[OAM].rxtsf.nanoseconds == 4 +assert pkt[OAM].txtsb.seconds == 2 +assert pkt[OAM].txtsb.nanoseconds == 1 +assert pkt[OAM].rxtsb.seconds == 6 +assert pkt[OAM].rxtsb.nanoseconds == 5 +assert pkt[OAM].tlvs[0].type == 3 +assert pkt[OAM].tlvs[0].length == 3 +assert raw(pkt[OAM].tlvs[0].payload) == b'123' +assert pkt[OAM].tlvs[1].type == 3 +assert pkt[OAM].tlvs[1].length == 6 +assert raw(pkt[OAM].tlvs[1].payload) == b'456789' +assert pkt[OAM].tlvs[2].type == 36 +assert pkt[OAM].tlvs[2].length == 32 +assert pkt[OAM].tlvs[2].test_id == 5 + +# FIXME: for some reason wireshark does not like OAM_TEST_ID_TLV here +check_tshark(pkt, "(DMM)") + += EXM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Experimental OAM Message (EXM)", + oui=123, + subopcode=33))) + +assert pkt[OAM].opcode == 49 +assert pkt[OAM].oui == 123 +assert pkt[OAM].subopcode == 33 + +check_tshark(pkt, "(EXM)") + += EXR + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Experimental OAM Reply (EXR)", + oui=123, + subopcode=33))) + +assert pkt[OAM].opcode == 48 +assert pkt[OAM].oui == 123 +assert pkt[OAM].subopcode == 33 + +check_tshark(pkt, "(EXR)") + += VSM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Vendor Specific Message (VSM)", + oui=123, + subopcode=33))) + +assert pkt[OAM].opcode == 51 +assert pkt[OAM].oui == 123 +assert pkt[OAM].subopcode == 33 + +check_tshark(pkt, "(VSM)") + += CSF + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Client Signal Fail (CSF)", + flags="RDI", + period="1 frame per minute"))) + +assert pkt[OAM].opcode == 52 +assert pkt[OAM].tlv_offset == 0 +assert pkt[OAM].flags == 0b010 +assert pkt[OAM].period == 0b110 + +check_tshark(pkt, "(CSF)") + += SLM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Synthetic Loss Message (SLM)", + test_id=11, + src_mep_id=12, + rcv_mep_id=34, + txfcf=3, + txfcb=9, + tlvs=[OAM_DATA_TLV()/Raw(b'123'), + OAM_DATA_TLV()/Raw(b'456789')]))) + +assert pkt[OAM].opcode == 55 +assert pkt[OAM].tlv_offset == 16 +assert pkt[OAM].test_id == 11 +assert pkt[OAM].src_mep_id == 12 +assert pkt[OAM].rcv_mep_id == 34 +assert pkt[OAM].txfcf == 3 +assert pkt[OAM].txfcb == 9 +assert pkt[OAM].tlvs[0].type == 3 +assert pkt[OAM].tlvs[0].length == 3 +assert raw(pkt[OAM].tlvs[0].payload) == b'123' +assert pkt[OAM].tlvs[1].type == 3 +assert pkt[OAM].tlvs[1].length == 6 +assert raw(pkt[OAM].tlvs[1].payload) == b'456789' + +check_tshark(pkt, "(SLM)") + += SLR + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Synthetic Loss Reply (SLR)", + test_id=11, + src_mep_id=12, + rcv_mep_id=34, + txfcf=3, + txfcb=9, + tlvs=[OAM_DATA_TLV()/Raw(b'123'), + OAM_DATA_TLV()/Raw(b'456789')]))) + +assert pkt[OAM].opcode == 54 +assert pkt[OAM].tlv_offset == 16 +assert pkt[OAM].test_id == 11 +assert pkt[OAM].src_mep_id == 12 +assert pkt[OAM].rcv_mep_id == 34 +assert pkt[OAM].txfcf == 3 +assert pkt[OAM].txfcb == 9 +assert pkt[OAM].tlvs[0].type == 3 +assert pkt[OAM].tlvs[0].length == 3 +assert raw(pkt[OAM].tlvs[0].payload) == b'123' +assert pkt[OAM].tlvs[1].type == 3 +assert pkt[OAM].tlvs[1].length == 6 +assert raw(pkt[OAM].tlvs[1].payload) == b'456789' + +check_tshark(pkt, "(SLR)") + += 1SL + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="One Way Synthetic Loss Measurement (1SL)", + test_id=11, + src_mep_id=12, + txfcf=3, + tlvs=[OAM_DATA_TLV()/Raw(b'123'), + OAM_DATA_TLV()/Raw(b'456789')]))) + +assert pkt[OAM].opcode == 53 +assert pkt[OAM].tlv_offset == 16 +assert pkt[OAM].test_id == 11 +assert pkt[OAM].src_mep_id == 12 +assert pkt[OAM].txfcf == 3 +assert pkt[OAM].tlvs[0].type == 3 +assert pkt[OAM].tlvs[0].length == 3 +assert raw(pkt[OAM].tlvs[0].payload) == b'123' +assert pkt[OAM].tlvs[1].type == 3 +assert pkt[OAM].tlvs[1].length == 6 +assert raw(pkt[OAM].tlvs[1].payload) == b'456789' + +check_tshark(pkt, "(1SL)") + += GNM + +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55")/Dot1Q()/ + OAM(opcode="Generic Notification Message (GNM)", + period="1 frame per minute", + nom_bdw=1, + curr_bdw=2, + port_id=3))) + +assert pkt[OAM].opcode == 32 +assert pkt[OAM].tlv_offset == 13 +assert pkt[OAM].period == 0b110 +assert pkt[OAM].subopcode == 1 +assert pkt[OAM].nom_bdw == 1 +assert pkt[OAM].curr_bdw == 2 +assert pkt[OAM].port_id == 3 + +check_tshark(pkt, "(GNM)") diff --git a/test/contrib/oncrpc.uts b/test/contrib/oncrpc.uts index 1e0a5893c58..aabe0c1a89b 100644 --- a/test/contrib/oncrpc.uts +++ b/test/contrib/oncrpc.uts @@ -6,6 +6,8 @@ = Create subpackets Object_Name() Auth_Unix() +Auth_RPCSEC_GSS() +Verifier_RPCSEC_GSS() = Create ONC RPC Packets RM_Header() @@ -17,9 +19,9 @@ RPC_Reply() = RPC Message type pkt = RPC()/RPC_Call() -assert(pkt.mtype==0) +assert pkt.mtype==0 pkt = RPC()/RPC_Reply() -assert(pkt.mtype==1) +assert pkt.mtype==1 + Test Built Packets vs Raw Strings @@ -69,6 +71,28 @@ pkt = RPC_Call( ) assert bytes(pkt) == b'\x00\x00\x00\x02\x00\x01\x86\xa5\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00 \xff\xff\xff\xff\x00\x00\x00\x05MNAME\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x05MNAME\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00' +pkt = RPC_Call( + version=2, + program=100021, + pversion=4, + procedure=20, + aflavor='RPCSEC_GSS', + a_rpcsec_gss=Auth_RPCSEC_GSS( + gss_version=1, + gss_procedure=0, + gss_seq_num=10, + gss_service=1, + gss_context=Object_Name( + length=4, + _name='AAAA', + fill='' + ), + ), + vflavor=6, + v_rpcsec_gss=Verifier_RPCSEC_GSS(b"\x00\x00\x00\x04\x41\x41\x41\x41") +) +assert bytes(pkt) == b'\x00\x00\x00\x02\x00\x01\x86\xb5\x00\x00\x00\x04\x00\x00\x00\x14\x00\x00\x00\x06\x00\x00\x00\x18\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x01\x00\x00\x00\x04\x41\x41\x41\x41\x00\x00\x00\x06\x00\x00\x00\x04\x41\x41\x41\x41' + pkt = RPC_Reply( reply_stat=1, flavor=1, diff --git a/test/contrib/opc_da.uts b/test/contrib/opc_da.uts index 89c0916482e..c664a6ede74 100644 --- a/test/contrib/opc_da.uts +++ b/test/contrib/opc_da.uts @@ -9,13 +9,13 @@ opcdaRequestPacket_Build = OpcDaMessage(OpcDaMessage= \ OpcDaHeaderMessage (versionMajor=5,versionMinor=0,pduType=0, \ pfc_flags = 131,integerRepresentation='bigEndian',\ characterRepresentation='ascii',floatingPointRepresentation='ieee',\ - reservedForFutur=0)/ OpcDaHeaderN(fragLenght=100,authLenght=0,callID=21)\ + res=0)/ OpcDaHeaderN(fragLength=100,authLength=0,callID=21)\ / OpcDaRequest(allocHint=60,contextId=6,opNum=5,\ - uuid=b'0000c41d-0a9c-0000-d702-8c761299f7bf',subData=RequestSubData(\ - versionMajor=0,versionMinor=0,subdata=''))) + uuid=b'0000c41d-0a9c-0000-d702-8c761299f7bf',stubData=RequestStubData(\ + versionMajor=0,versionMinor=0,stubdata=''))) elem2 = raw(opcdaRequestPacket_Build) -assert( elem1 == elem2 ) +assert elem1 == elem2 = OpcDaRequestLE opcdaRequestLEPacket_Dissect = hex_bytes(b'050000831000000064000000150000003c000000060005001dc400009c0a0000d7028c761299f7bf000000000000000000000000512d4e34ab431449a2cf7784b21b3ea1') @@ -25,14 +25,14 @@ opcdaRequestLEPacket_Build = OpcDaMessage(OpcDaMessage= \ OpcDaHeaderMessage (versionMajor=5,versionMinor=0,pduType=0, \ pfc_flags = 131,integerRepresentation='littleEndian',\ characterRepresentation='ascii',floatingPointRepresentation='ieee',\ - reservedForFutur=0)/ OpcDaHeaderNLE(fragLenght=100,authLenght=0,callID=21)\ + res=0)/ OpcDaHeaderNLE(fragLength=100,authLength=0,callID=21)\ / OpcDaRequestLE (allocHint=60,contextId=6,opNum=5,\ uuid=b'0000c41d-0a9c-0000-d702-8c761299f7bf',\ - subData=RequestSubDataLE(versionMajor=0,versionMinor=0,flags=0,reserved=0,\ - subUuid=b'344e2d51-43ab-4914-a2cf-7784b21b3ea1',subdata=''))) + stubData=RequestStubDataLE(versionMajor=0,versionMinor=0,\ + stubdata=b'\x00\x00\x00\x00\x00\x00\x00\x00Q-N4\xabC\x14I\xa2\xcfw\x84\xb2\x1b>\xa1'))) elem2 = raw(opcdaRequestLEPacket_Build) -assert( elem1 == elem2 ) +assert elem1 == elem2 + Test Ping Packet @@ -44,11 +44,11 @@ opcdaPingPacket_Build = OpcDaMessage(OpcDaMessage= \ OpcDaHeaderMessage (versionMajor=5,versionMinor=0,pduType=1, \ pfc_flags = 3,integerRepresentation='littleEndian',\ characterRepresentation='ascii',floatingPointRepresentation='ieee',\ - reservedForFutur=0)/ OpcDaHeaderNLE(fragLenght=100,authLenght=0,callID=21)\ + res=0)/ OpcDaHeaderNLE(fragLength=100,authLength=0,callID=21)\ / OpcDaPing()) / '\x00' elem2 = raw(opcdaPingPacket_Build) -assert( elem1 == elem2 ) +assert elem1 == elem2 + Test Response Packets @@ -60,12 +60,12 @@ opcDaResponsePacket_Build = OpcDaMessage(OpcDaMessage= \ OpcDaHeaderMessage (versionMajor=5,versionMinor=0,pduType=2, \ pfc_flags = 3,integerRepresentation='bigEndian',\ characterRepresentation='ascii',floatingPointRepresentation='ieee',\ - reservedForFutur=0)/ OpcDaHeaderN(fragLenght=212,authLenght=0,callID=21)\ + res=0)/ OpcDaHeaderN(fragLength=212,authLength=0,callID=21)\ / OpcDaResponse(allocHint=188,contextId=6,cancelCount=0,reserved=0,\ - subData=b'0'*(212-32))) + stubData=b'0'*(212-32))) elem2 = raw(opcDaResponsePacket_Build) -assert( elem1 == elem2 ) +assert elem1 == elem2 = OpcDaResponseLE opcDaResponseLEPacket_Dissect = hex_bytes(b'0500020310000000d400000015000000bc00000006000000303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030') @@ -75,12 +75,12 @@ opcDaResponseLEPacket_Build = OpcDaMessage(OpcDaMessage= \ OpcDaHeaderMessage (versionMajor=5,versionMinor=0,pduType=2, \ pfc_flags = 3,integerRepresentation='littleEndian',\ characterRepresentation='ascii',floatingPointRepresentation='ieee',\ - reservedForFutur=0)/ OpcDaHeaderNLE(fragLenght=212,authLenght=0,callID=21)\ + res=0)/ OpcDaHeaderNLE(fragLength=212,authLength=0,callID=21)\ / OpcDaResponseLE(allocHint=188,contextId=6,cancelCount=0,reserved=0,\ - subData=b'0'*(212-32))) + stubData=b'0'*(212-32))) elem2 = raw(opcDaResponseLEPacket_Build) -assert( elem1 == elem2 ) +assert elem1 == elem2 # + Test Fault Packet # No example yet @@ -140,7 +140,7 @@ ocDaAlter_contextPacket_Build = OpcDaMessage(OpcDaMessage= \ OpcDaHeaderMessage (versionMajor=5,versionMinor=0,pduType=14, \ pfc_flags = 3,integerRepresentation='bigEndian',\ characterRepresentation='ascii',floatingPointRepresentation='ieee',\ - reservedForFutur=0)/ OpcDaHeaderN(fragLenght=72,authLenght=0,callID=23)\ + res=0)/ OpcDaHeaderN(fragLength=72,authLength=0,callID=23)\ / OpcDaAlter_context(maxXmitFrag=5840,maxRecvtFrag=5840,\ assocGroupId=534853)) \ / '\x00\x00\x00\x01\x07\x00\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\xc0'\ @@ -156,7 +156,7 @@ ocDaAlter_contextLEPacket_Build = OpcDaMessage(OpcDaMessage= \ OpcDaHeaderMessage (versionMajor=5,versionMinor=0,pduType=14, \ pfc_flags = 3,integerRepresentation='littleEndian',\ characterRepresentation='ascii',floatingPointRepresentation='ieee',\ - reservedForFutur=0)/ OpcDaHeaderNLE(fragLenght=72,authLenght=0,callID=23)\ + res=0)/ OpcDaHeaderNLE(fragLength=72,authLength=0,callID=23)\ / OpcDaAlter_contextLE(maxXmitFrag=5840,maxRecvtFrag=5840,\ assocGroupId=534853)) \ / '\x01\x00\x00\x00\x07\x00\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\xc0'\ diff --git a/test/contrib/openflow.uts b/test/contrib/openflow.uts index 6d236344c19..56c50d6f772 100755 --- a/test/contrib/openflow.uts +++ b/test/contrib/openflow.uts @@ -17,7 +17,7 @@ raw(ofm) == b'\x01\x02\x00\x08\x00\x00\x00\x00' = OFPMatch(), check wildcard completion ofm = OFPMatch(in_port=1, nw_tos=8) ofm = OFPMatch(raw(ofm)) -assert(ofm.wildcards1 == 0x1) +assert ofm.wildcards1 == 0x1 ofm.wildcards2 == 0xee = OpenFlow(), generic method test with OFPTEchoRequest() @@ -27,9 +27,9 @@ isinstance(OpenFlow(s), OFPTEchoRequest) = OFPTFlowMod(), check codes and defaults values ofm = OFPTFlowMod(cmd='OFPFC_DELETE', out_port='CONTROLLER', flags='CHECK_OVERLAP+EMERG') -assert(ofm.cmd == 3) -assert(ofm.buffer_id == 0xffffffff) -assert(ofm.out_port == 0xfffd) +assert ofm.cmd == 3 +assert ofm.buffer_id == 0xffffffff +assert ofm.out_port == 0xfffd ofm.flags == 6 + Complex OFv1.0 messages @@ -55,8 +55,8 @@ raw(ofm) == s ofm = OFPTPacketIn(data=Ether()/IP()/ICMP()) p = OFPTPacketIn(raw(ofm)) dat = p.data -assert(isinstance(dat, Ether)) -assert(isinstance(dat.payload, IP)) +assert isinstance(dat, Ether) +assert isinstance(dat.payload, IP) isinstance(dat.payload.payload, ICMP) = OFPTStatsReplyFlow() @@ -93,7 +93,7 @@ isinstance(p[TCP].payload, OFPTHello) = complete Ether()/IP()/TCP()/OFPTFeaturesRequest() ofm = Ether(src='00:11:22:33:44:55',dst='01:23:45:67:89:ab')/IP(src='10.0.0.7',dst='192.168.0.42')/TCP(sport=6633, dport=6633)/OFPTFeaturesRequest(xid=23) s = b'\x01#Eg\x89\xab\x00\x11"3DU\x08\x00E\x00\x000\x00\x01\x00\x00@\x06\xaf\xee\n\x00\x00\x07\xc0\xa8\x00*\x19\xe9\x19\xe9\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x90\x0b\x00\x00\x01\x05\x00\x08\x00\x00\x00\x17' -assert(raw(ofm) == s) +assert raw(ofm) == s e = Ether(s) e.show2() e[OFPTFeaturesRequest].xid == 23 diff --git a/test/contrib/openflow3.uts b/test/contrib/openflow3.uts index ce18b3dbf39..d423af65e18 100755 --- a/test/contrib/openflow3.uts +++ b/test/contrib/openflow3.uts @@ -16,7 +16,7 @@ raw(ofm) == b'\x04\x02\x00\x08\x00\x00\x00\x00' = OFPMatch(), check padding ofm = OFPMatch(oxm_fields=OFBEthType(eth_type=0x86dd)) -assert(len(raw(ofm))%8 == 0) +assert len(raw(ofm))%8 == 0 raw(ofm) == b'\x00\x01\x00\x0a\x80\x00\x0a\x02\x86\xdd\x00\x00\x00\x00\x00\x00' = OpenFlow3(), generic method test with OFPTEchoRequest() @@ -26,13 +26,13 @@ isinstance(OpenFlow3(s), OFPTEchoRequest) = OFPTFlowMod(), check codes and defaults values ofm = OFPTFlowMod(cmd='OFPFC_DELETE', out_group='ALL', flags='CHECK_OVERLAP+NO_PKT_COUNTS') -assert(ofm.cmd == 3) -assert(ofm.out_port == 0xffffffff) -assert(ofm.out_group == 0xfffffffc) +assert ofm.cmd == 3 +assert ofm.out_port == 0xffffffff +assert ofm.out_group == 0xfffffffc ofm.flags == 10 = OFBIPv6ExtHdrHMID(), check creation of last OXM classes -assert(hasattr(OFBIPv6ExtHdr(), 'ipv6_ext_hdr_flags')) +assert hasattr(OFBIPv6ExtHdr(), 'ipv6_ext_hdr_flags') OFBIPv6ExtHdrHMID().field == 39 + Complex OFv1.3 messages @@ -74,12 +74,15 @@ assert len(fpti) == 16 assert fpti.instruction_ids[0].type == 1 assert fpti.instruction_ids[1].type == 5 +fpti.instruction_ids[0] = OFPITGotoTableID() +assert bytes(fpti) == b'\x00\x00\x00\x0c\x00\x01\x00\x04\x00\x05\x00\x04\x00\x00\x00\x00' + = OFPTPacketIn() containing an Ethernet frame ofm = OFPTPacketIn(data=Ether()/IP()/ICMP()) p = OFPTPacketIn(raw(ofm)) dat = p.data -assert(isinstance(dat, Ether)) -assert(isinstance(dat.payload, IP)) +assert isinstance(dat, Ether) +assert isinstance(dat.payload, IP) isinstance(dat.payload.payload, ICMP) = OFPTGroupMod() @@ -153,7 +156,7 @@ assert pkt[OFPTHello].elements[0].len == 8 = complete Ether()/IP()/TCP()/OFPTFeaturesRequest() ofm = Ether(src='00:11:22:33:44:55',dst='01:23:45:67:89:ab')/IP(src='10.0.0.7',dst='192.168.0.42')/TCP(sport=6633)/OFPTFeaturesRequest(xid=23) s = b'\x01#Eg\x89\xab\x00\x11"3DU\x08\x00E\x00\x000\x00\x01\x00\x00@\x06\xaf\xee\n\x00\x00\x07\xc0\xa8\x00*\x19\xe9\x19\xfd\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x8c\xf7\x00\x00\x04\x05\x00\x08\x00\x00\x00\x17' -assert(raw(ofm) == s) +assert raw(ofm) == s e = Ether(s) e.show2() e[OFPTFeaturesRequest].xid == 23 @@ -162,7 +165,7 @@ e[OFPTFeaturesRequest].xid == 23 pkt = TCP()/OFPMPRequestTableFeatures(table_features=[OFPTableFeatures(properties=[OFPTFPTMatch(oxm_ids=[OFBUDPSrcID()])])]) assert raw(pkt) == b'\x19\xfd\x19\xfd\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x00\x00\x00\x00\x04\x12\x00X\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x08\x80\x00\x1e\x02' pkt = TCP(raw(pkt)) -assert pkt.table_features[0].properties[0].oxm_ids[0].fields == {'class': 32768, 'field': 15, 'hasmask': 0, 'len': 2} +assert pkt.table_features[0].properties[0].oxm_ids[0].fields == {'class_': 32768, 'field': 15, 'hasmask': 0, 'len': 2} = Test OFBTCPSrc Autocompletion diff --git a/test/contrib/ospf.uts b/test/contrib/ospf.uts index 0c5a4763b4f..9b76bc8e0c9 100644 --- a/test/contrib/ospf.uts +++ b/test/contrib/ospf.uts @@ -47,3 +47,44 @@ assert a.answers(b) pkt = Ether(dst="01:00:5e:00:00:05", src="ca:11:09:b3:00:1c")/IPv6(dst="::1", src="fe80::160c:12aa:fe7e:cd28")/OSPFv3_Hdr(src="75.1.3.1")/\ OSPFv3_Hello(options=0x12, router="10.75.0.254", backup="10.75.0.1", neighbors=["75.1.0.1"]) assert raw(pkt) == b'\x01\x00^\x00\x00\x05\xca\x11\t\xb3\x00\x1c\x86\xdd`\x00\x00\x00\x00(Y@\xfe\x80\x00\x00\x00\x00\x00\x00\x16\x0c\x12\xaa\xfe~\xcd(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x01\x00(K\x01\x03\x01\x00\x00\x00\x00Y\x98\x00\x00\x00\x00\x00\x00\x01\x00\x00\x12\x00\n\x00(\nK\x00\xfe\nK\x00\x01K\x01\x00\x01' + += OSPFv2 Opaque lsa + +data = b'\x01\x00^\x00\x00\x05\x00\x90\x92\x9d\x94\x01\x08\x00E\xc0\x00\xb4?\x99\x00\x00\x01Y\xc6\x91\xd2\x00\x00\x01\xe0\x00\x00\x05\x02\x04\x00\xa0\x11\x03\x03\x03\x00\x00\x00d9\x9f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01 \n\x01\x00\x00\x00\x11\x03\x03\x03\x80\x00\x00\x1f\xab\xd9\x00\x84\x00\x01\x00\x04\x11\x03\x03\x03\x00\x02\x00d\x00\x01\x00\x01\x02\x00\x00\x00\x00\x02\x00\x04\xd2\x00\x00\x02\x00\x03\x00\x04\xd2\x00\x00\x01\x00\x04\x00\x04\xd2\x00\x00\x02\x00\x05\x00\x04\x00\x00\x03\xe8\x00\x06\x00\x04I\x98\x96\x80\x00\x07\x00\x04I\x98\x96\x80\x00\x08\x00 I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80\x00\t\x00\x04\x00\x00\x00\x00\x92\xe6\xb6:' + +p = Ether(data) + +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].age == 1) +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].type == 10) +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].id == '1.0.0.0') +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].adrouter == '17.3.3.3') +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].seq == 0x8000001f) +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].chksum == 0xabd9) +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].len == 132) +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].opaqueid() == 0) +assert (p[OSPF_LSUpd][OSPF_Area_Scope_Opaque_LSA].opaquetype() == 1) + +opaque_data=b'\x00\x01\x00\x04\x11\x03\x03\x03\x00\x02\x00d\x00\x01\x00\x01\x02\x00\x00\x00\x00\x02\x00\x04\xd2\x00\x00\x02\x00\x03\x00\x04\xd2\x00\x00\x01\x00\x04\x00\x04\xd2\x00\x00\x02\x00\x05\x00\x04\x00\x00\x03\xe8\x00\x06\x00\x04I\x98\x96\x80\x00\x07\x00\x04I\x98\x96\x80\x00\x08\x00 I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80I\x18\x96\x80\x00\t\x00\x04\x00\x00\x00\x00' + +p = OSPF_Link_Scope_Opaque_LSA(seq=0x80000003,data=opaque_data) +assert (p.type == 9) +assert (p.seq == 0x80000003) +assert (len(p) == 132) + +p = OSPF_Area_Scope_Opaque_LSA(seq=0x80000004,data=opaque_data) +assert (p.type == 10) +assert (p.seq == 0x80000004) +assert (len(p) == 132) + +p = OSPF_AS_Scope_Opaque_LSA(seq=0x80000005,data=opaque_data) +assert (p.type == 11) +assert (p.seq == 0x80000005) +assert (len(p) == 132) + += OSPF - build/dissect without header + +OSPF_DBDesc().show2() +OSPF_LSReq().show2() +OSPF_LSUpd().show2() +OSPF_LSAck().show2() + diff --git a/test/contrib/pcom.uts b/test/contrib/pcom.uts index 39e83eea109..b5ad57e8586 100755 --- a/test/contrib/pcom.uts +++ b/test/contrib/pcom.uts @@ -16,14 +16,10 @@ r = b'\x65\x00\x04\x00\x00\x00\x00\x00' raw(PCOMResponse() / b'\x00\x00\x00\x00')[2:] == r = PCOM/TCP Guess Payload Class -assert(isinstance(PCOMRequest(b'\x00\x00\x65\x00\x01\x00\x00\x00').payload, -PCOMAsciiRequest)) -assert(isinstance(PCOMResponse(b'\x00\x00\x65\x00\x01\x00\x00\x00').payload, -PCOMAsciiResponse)) -assert(isinstance(PCOMRequest(b'\x00\x00\x66\x00\x01\x00\x00\x00').payload, -PCOMBinaryRequest)) -assert(isinstance(PCOMResponse(b'\x00\x00\x66\x00\x01\x00\x00\x00').payload, -PCOMBinaryResponse)) +assert isinstance(PCOMRequest(b'\x00\x00\x65\x00\x01\x00\x00\x00\x00\x00\x00\x00').payload, PCOMAsciiRequest) +assert isinstance(PCOMResponse(b'\x00\x00\x65\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00').payload, PCOMAsciiResponse) +assert isinstance(PCOMRequest(b'\x00\x00\x66\x00\x01\x00\x00\x00' + b'\x00' * 25).payload, PCOMBinaryRequest) +assert isinstance(PCOMResponse(b'\x00\x00\x66\x00\x01\x00\x00\x00' + b'\x00' * 25).payload, PCOMBinaryResponse) + Test PCOM/Ascii = PCOM/ASCII Default values @@ -40,8 +36,8 @@ raw(PCOMResponse() / PCOMAsciiResponse(unitId='00',command='ID'))[2:] == r = PCOM/ASCII Known Codes f = PCOMAsciiCommandField('command', '', length_from= None) -assert(f.i2repr(None, 'CCS') == 'Send Stop Command \'CCS\'') -assert(f.i2repr(None, 'CC') == 'Reply of Admin Commands (CC*) \'CC\'') +assert f.i2repr(None, 'CCS') == 'Send Stop Command \'CCS\'' +assert f.i2repr(None, 'CC') == 'Reply of Admin Commands (CC*) \'CC\'' + Test PCOM/Binary = PCOM/Binary Default values @@ -65,4 +61,4 @@ commandSpecific='\x00\x00\x00\x00\x00\x01', len=4, data= data))[2:] == r = PCOM/Binary Known Codes f = PCOMBinaryCommandField('command', None) -assert(f.i2repr(None, 0x4d) == 'Read Operands Request - 0x4d') +assert f.i2repr(None, 0x4d) == 'Read Operands Request - 0x4d' diff --git a/test/contrib/pfcp.uts b/test/contrib/pfcp.uts new file mode 100644 index 00000000000..df2d581b2b6 --- /dev/null +++ b/test/contrib/pfcp.uts @@ -0,0 +1,791 @@ +% PFCP tests + +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('pfcp')" -t test/contrib/pfcp.uts + ++ Build packets & dissect + += Verify IEs + +import scapy.contrib.pfcp as pfcp_mod + +skip_IEs = [ + IE_Base, + IE_Compound +] + +for name, cls in pfcp_mod.__dict__.items(): + if name.startswith("IE_") and type(cls) == Packet_metaclass and cls not in skip_IEs: + print("testing %s" % name) + pkt = cls() + bs = bytes(pkt) + restored = cls(bs) + assert bytes(restored) == bs + # TODO: also test packet field equality + += Verify PCAPs + +~ pcaps + +# the following can be useful while adding more IE types +# (e.g. updating for a newer version of the spec) + +def command(pkt): + f = [] + for fn, fv in sorted(pkt.fields.items(), key=lambda item: item[0]): + if fn in ("length", "message_type"): + continue + if fn == "ietype" and not isinstance(pkt, IE_EnterpriseSpecific) and \ + not isinstance(pkt, IE_NotImplemented): + continue + if fn.startswith("num_") or fn.endswith("_length"): + continue + if fv is None: + continue + fld = pkt.get_field(fn) + if isinstance(fld, ConditionalField) and not fld._evalcond(pkt): + continue + # if fv == fld.default: + # continue + if isinstance(fv, (list, dict, set)) and len(fv) == 0: + continue + if isinstance(fv, Packet): + fv = command(fv) + elif fld.islist and fld.holds_packets and isinstance(fv, list): + fv = "[%s]" % ",".join(map(command, fv)) + elif isinstance(fld, FlagsField): + fv = int(fv) + else: + fv = repr(fv) + f.append("%s=%s" % (fn, fv)) + c = "%s(%s)" % (pkt.__class__.__name__, ", ".join(f)) + if not isinstance(pkt.payload, NoPayload): + pc = command(pkt.payload) + if pc: + c += "/" + pc + return c + +broken_ies = set([]) + +broken_ie_types = set([ + cls.ie_type for cls in broken_ies +]) + +ignore = set([]) + +def find_raw_or_not_implemented(pkt, prefix=""): + if prefix in ignore: + return False, False + if hasattr(pkt, "IE_list"): + prev = None + found_any = False + for n, ie in enumerate(pkt.IE_list, 1): + if type(ie) in broken_ies: + return False, False + name = "%s-%d-%s" % (prefix, n, type(ie).__name__) + found, leaf = find_raw_or_not_implemented(ie, prefix=name) + if found: + found_any = True + if found and leaf: + print("gotcha: %s %r" % (prefix, ie)) + bs = b"" + if prev is not None: + bs = bytes(prev) + bs += bytes(ie) + if prev is not None: + prev.show2() + ie.show2() + print("%s -- bad val: %s" % (prefix, bytes_hex(bs).decode())) + if len(bs) > 4: + l = bs[2] * 256 + bs[3] + if len(bs) >= l + 4: + print("bad val (length-limited): %s" % bytes_hex(bs[:l + 4]).decode()) + print("bad val (short): %s" % bytes_hex(bytes(ie)).decode()) + prev = ie + return found_any, False + if isinstance(pkt, Raw): + bs = bytes(pkt) + if len(bs) > 4: + ie_type = bs[0] * 256 + bs[1] + if ie_type in broken_ie_types: + return False, True + return True, True + if isinstance(pkt, Padding) or isinstance(pkt, IE_NotImplemented): + return True, True + return False, True + +def find_mismatching_command(pkt, prefix=""): + c = command(pkt) + if hasattr(pkt, "IE_list"): + for n, ie in enumerate(pkt.IE_list, 1): + name = "%s-%d-%s" % (prefix, n, type(ie).__name__) + find_mismatching_command(ie, prefix=name) + if bytes(eval(c)) != bytes(pkt): + print(prefix) + print("ORIG: %s" % bytes_hex(bytes(pkt))) + print("EVAL: %s" % bytes_hex(bytes(eval(c)))) + raise AssertionError("bad command: %s" % c) + +for n, pkt in enumerate(rdpcap("test/pcaps/pfcp.pcap"), 1): + if PFCP in pkt: + # if IE_DLBufferingSuggestedPacketCount in pkt: + # continue + pkt0 = pkt[PFCP] + if IE_NotImplemented in pkt0 or Raw in pkt0 or IE_NotImplemented in pkt0 or Padding in pkt0: + found, leaf = find_raw_or_not_implemented(pkt, prefix=str(n)) + if not found: + # ignored + continue + pkt0.show2() + raise AssertionError("IE_NotImplemented / Raw / Padding detected") + bs = bytes(pkt0) + pkt1 = PFCP(bs) + # TODO: diff show2() result + c0 = command(pkt0) + c1 = command(pkt1) + pkt2 = eval(c1) + c2 = command(pkt2) + if bytes(pkt2) != bs: + find_mismatching_command(pkt0, prefix=str(n)) + print(bytes_hex(bytes(pkt2))) + print(bytes_hex(bs)) + raise AssertionError("bytes(pkt2) != bs") + if bs != pkt0.original: + print(bytes_hex(bs)) + print(bytes_hex(pkt0.original)) + raise AssertionError("bs != pkt0.original") + if bytes(pkt1) != bs: + print(bytes_hex(bytes(pkt1))) + print(bytes_hex(bs)) + raise AssertionError("bytes(pkt1) != bs") + if c0 != c1: + print("COMMAND MISMATCH:\n----\n%s\n----\n%s\n\n" % (c0, c1)) + pkt0.show2() + pkt1.show2() + print(bytes_hex(bytes(pkt0))) + print("packet index: %d\n" % n) + raise AssertionError("c0 != c1") + if c0 != c2: + print("EVAL COMMAND MISMATCH:\n----\n%s\n----\n%s\n\n" % (c0, c2)) + pkt0.show2() + pkt2.show2() + print(bytes_hex(bytes(pkt0))) + print("packet index: %d\n" % n) + raise AssertionError("c0 != c2") + += Build and dissect PFCP Association Setup Request + +pfcpASReqBytes = hex_bytes("200500160000010000600004e1a47d08003c0006020465726777") + +pfcpASReq = PFCP(version=1, S=0, seq=1) / \ + PFCPAssociationSetupRequest(IE_list=[ + IE_RecoveryTimeStamp(timestamp=3785653512), + IE_NodeId(id_type="FQDN", id="ergw") + ]) + +# print("%r" % bytes(pfcpASReq)) +# print("%r" % pfcpASReqBytes) +assert bytes(pfcpASReq) == pfcpASReqBytes + +pfcpASReq = PFCP(pfcpASReqBytes) +assert pfcpASReq.version == 1 +assert pfcpASReq.MP == 0 +assert pfcpASReq.S == 0 +assert pfcpASReq.message_type == 5 +assert pfcpASReq.length == 22 +ies = pfcpASReq[PFCPAssociationSetupRequest].IE_list +assert isinstance(ies[0], IE_RecoveryTimeStamp) +assert ies[0].ietype == 96 +assert ies[0].length == 4 +assert ies[0].timestamp == 3785653512 +assert isinstance(ies[1], IE_NodeId) +assert ies[1].ietype == 60 +assert ies[1].length == 6 +assert ies[1].id_type == 2 +assert ies[1].id == b"ergw" + += Build and dissect PFCP Association Setup Response + +pfcpASRespBytes = hex_bytes("2006008c00000100001300010100600004e1a47af9002b00020001007400092980ac1201020263708002006448f9767070207631392e30382e312d3339377e673465333431343066612d6469727479206275696c7420627920726f6f74206f6e206275696c646b697473616e64626f7820617420576564204465632031312031353a30323a3535205554432032303139") + +pfcpASResp = PFCP(version=1, S=0, seq=1) / \ + PFCPAssociationSetupResponse(IE_list=[ + IE_Cause(cause="Request accepted"), + IE_RecoveryTimeStamp(timestamp=3785652985), + IE_UPFunctionFeatures( + TREU=0, HEEU=0, PFDM=0, FTUP=0, TRST=0, DLBD=0, DDND=0, BUCP=0, + spare=0, PFDE=0, FRRT=0, TRACE=0, QUOAC=0, UDBC=0, PDIU=0, EMPU=1), + IE_UserPlaneIPResourceInformation( + ASSOSI=0, ASSONI=1, TEIDRI=2, V6=0, V4=1, teid_range=0x80, + ipv4="172.18.1.2", network_instance="cp"), + IE_EnterpriseSpecific( + ietype=32770, + enterprise_id=18681, + data="vpp v19.08.1-397~g4e34140fa-dirty built by root on buildkitsandbox at Wed Dec 11 15:02:55 UTC 2019") + ]) + + +pfcpASResp.show2() +assert bytes(pfcpASResp) == pfcpASRespBytes + +pfcpASResp = PFCP(pfcpASRespBytes) +assert pfcpASResp.version == 1 +assert pfcpASResp.MP == 0 +assert pfcpASResp.S == 0 +assert pfcpASResp.message_type == 6 +assert pfcpASResp.length == 140 + +ies = pfcpASResp[PFCPAssociationSetupResponse].IE_list +assert isinstance(ies[0], IE_Cause) +assert ies[0].ietype == 19 +assert ies[0].length == 1 +assert ies[0].cause == 1 +assert isinstance(ies[1], IE_RecoveryTimeStamp) +assert ies[1].ietype == 96 +assert ies[1].length == 4 +assert ies[1].timestamp == 3785652985 +assert isinstance(ies[2], IE_UPFunctionFeatures) +assert ies[2].ietype == 43 +assert ies[2].length == 2 +assert ies[2].TREU == 0 +assert ies[2].HEEU == 0 +assert ies[2].PFDM == 0 +assert ies[2].FTUP == 0 +assert ies[2].TRST == 0 +assert ies[2].DLBD == 0 +assert ies[2].DDND == 0 +assert ies[2].BUCP == 0 +assert ies[2].spare == 0 +assert ies[2].PFDE == 0 +assert ies[2].FRRT == 0 +assert ies[2].TRACE == 0 +assert ies[2].QUOAC == 0 +assert ies[2].UDBC == 0 +assert ies[2].PDIU == 0 +assert ies[2].EMPU == 1 +assert isinstance(ies[3], IE_UserPlaneIPResourceInformation) +assert ies[3].ASSOSI == 0 +assert ies[3].ASSONI == 1 +assert ies[3].TEIDRI == 2 +assert ies[3].V6 == 0 +assert ies[3].V4 == 1 +assert ies[3].teid_range == 0x80 +assert ies[3].ipv4 == "172.18.1.2" +assert ies[3].network_instance == b"cp" +assert isinstance(ies[4], IE_EnterpriseSpecific) +assert ies[4].ietype == 32770 +assert ies[4].enterprise_id == 18681 +assert ies[4].data == b"vpp v19.08.1-397~g4e34140fa-dirty built by root on buildkitsandbox at Wed Dec 11 15:02:55 UTC 2019" + +assert pfcpASResp.answers(pfcpASReq) + +# = Build and dissect PFCP Session Establishment Request + +pfcpSEReq1Bytes = hex_bytes("2132011300000000000000000000020000030021002c000102006c00040000000200040010002a00010000160007066163636573730003000d002c000101006c00040000000100010038006c000400000002005f000100000200190015000901104c9033ac120102001600030263700014000103003800020002001d00040000006400010057006c000400000001000200350016000706616363657373001700210100001d7065726d6974206f75742069702066726f6d20616e7920746f20616e790014000100003800020001001d00040000fde800510004000000010006001b003e000104002500021000004a00040000003c00510004000000010039000d02ffde7210bf97810aac120101003c0006020465726777") + +pfcpSEReq1 = PFCP(version=1, S=1, seq=2, seid=0, spare_oct=0) / \ + PFCPSessionEstablishmentRequest(IE_list=[ + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=2), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="Access"), + IE_NetworkInstance(instance="access"), + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(DROP=1), + IE_FAR_Id(id=1) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=2), + IE_OuterHeaderRemoval(header="GTP-U/UDP/IPv4"), + IE_PDI(IE_list=[ + IE_FTEID(V4=1, TEID=0x104c9033, ipv4="172.18.1.2"), + IE_NetworkInstance(instance="cp"), + IE_SourceInterface(interface="CP-function"), + ]), + IE_PDR_Id(id=2), + IE_Precedence(precedence=100) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=1), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="access"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from any to any"), + IE_SourceInterface(interface="Access"), + ]), + IE_PDR_Id(id=1), + IE_Precedence(precedence=65000), + IE_URR_Id(id=1) + ]), + IE_CreateURR(IE_list=[ + IE_MeasurementMethod(EVENT=1), + IE_ReportingTriggers(start_of_traffic=1), + IE_TimeQuota(quota=60), + IE_URR_Id(id=1) + ]), + IE_FSEID(v4=1, seid=0xffde7210bf97810a, ipv4="172.18.1.1"), + IE_NodeId(id_type="FQDN", id="ergw") + ]) + +assert bytes(pfcpSEReq1) == pfcpSEReq1Bytes +assert bytes(PFCP(pfcpSEReq1Bytes)) == pfcpSEReq1Bytes + +pfcpSEReq2Bytes = hex_bytes("213202ba00000000000000000000080000030037002c000102006c00040000000400040026002a000102001600040373676900260015020012687474703a2f2f6578616d706c652e636f6d0003001e002c000102006c0004000000020004000d002a000102001600040373676900030021002c000102006c00040000000300040010002a000100001600070661636365737300030021002c000102006c00040000000100040010002a00010000160007066163636573730001006d006c0004000000040002004b00160007066163636573730017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3420746f2061737369676e65640014000100005d0005020ac00000003800020004001d00040000006400510004000000020001006d006c0004000000020002004b00160007066163636573730017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3220746f2061737369676e65640014000100005d0005020ac00000003800020002001d0004000000c800510004000000010001006a006c0004000000030002004800160004037367690017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3420746f2061737369676e65640014000102005d0005060ac00000003800020003001d00040000006400510004000000020001006a006c0004000000010002004800160004037367690017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3220746f2061737369676e65640014000102005d0005060ac00000003800020001001d0004000000c8005100040000000100060013003e000102002500020000005100040000000200060013003e00010200250002000000510004000000010039000d02ffde7210d971c146ac120101003c0006020465726777") + +pfcpSEReq2 = PFCP(seq=8) / PFCPSessionEstablishmentRequest(IE_list=[ + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=4), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="SGi-LAN/N6-LAN"), + IE_NetworkInstance(instance="sgi"), + IE_RedirectInformation(type="URL", address="http://example.com"), + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=2), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="SGi-LAN/N6-LAN"), + IE_NetworkInstance(instance="sgi"), + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=3), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="Access"), + IE_NetworkInstance(instance="access") + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=1), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="Access"), + IE_NetworkInstance(instance="access") + ]) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=4), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="access"), + IE_SDF_Filter( + FD=1, flow_description="permit out ip from 198.19.65.4 to assigned"), + IE_SourceInterface(interface="Access"), + IE_UE_IP_Address(ipv4="10.192.0.0", V4=1) + ]), + IE_PDR_Id(id=4), + IE_Precedence(precedence=100), + IE_URR_Id(id=2) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=2), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="access"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from 198.19.65.2 to assigned"), + IE_SourceInterface(interface="Access"), + IE_UE_IP_Address(ipv4="10.192.0.0", V4=1) + ]), + IE_PDR_Id(id=2), + IE_Precedence(precedence=200), + IE_URR_Id(id=1) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=3), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="sgi"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from 198.19.65.4 to assigned"), + IE_SourceInterface(interface="SGi-LAN/N6-LAN"), + IE_UE_IP_Address(ipv4="10.192.0.0", SD=1, V4=1) + ]), + IE_PDR_Id(id=3), + IE_Precedence(precedence=100), + IE_URR_Id(id=2) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=1), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="sgi"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from 198.19.65.2 to assigned"), + IE_SourceInterface(interface="SGi-LAN/N6-LAN"), + IE_UE_IP_Address(ipv4="10.192.0.0", SD=1, V4=1) + ]), + IE_PDR_Id(id=1), + IE_Precedence(precedence=200), + IE_URR_Id(id=1) + ]), + IE_CreateURR(IE_list=[ + IE_MeasurementMethod(VOLUM=1), + IE_ReportingTriggers(), + IE_URR_Id(id=2) + ]), + IE_CreateURR(IE_list=[ + IE_MeasurementMethod(VOLUM=1), + IE_ReportingTriggers(), + IE_URR_Id(id=1) + ]), + IE_FSEID(ipv4="172.18.1.1", v4=1, seid=0xffde7210d971c146), + IE_NodeId(id_type="FQDN", id="ergw")]) + +assert bytes(pfcpSEReq2) == pfcpSEReq2Bytes +assert bytes(PFCP(pfcpSEReq2Bytes)) == pfcpSEReq2Bytes + +pfcpSEReq3Bytes = hex_bytes("213203a10000000000000000000003000003001e002c000102006c0004000000060004000d002a000102001600040373676900030037002c000102006c00040000000400040026002a000102001600040373676900260015020012687474703a2f2f6578616d706c652e636f6d0003001e002c000102006c0004000000020004000d002a000102001600040373676900030021002c000102006c00040000000500040010002a000100001600070661636365737300030021002c000102006c00040000000300040010002a000100001600070661636365737300030021002c000102006c00040000000100040010002a000100001600070661636365737300010042006c000400000006000200200018000354535400160007066163636573730014000100005d0005020ac00000003800020006001d00040000009600510004000000030001006d006c0004000000040002004b00160007066163636573730017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3420746f2061737369676e65640014000100005d0005020ac00000003800020004001d00040000006400510004000000020001006d006c0004000000020002004b00160007066163636573730017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3220746f2061737369676e65640014000100005d0005020ac00000003800020002001d0004000000c800510004000000010001003f006c0004000000050002001d0018000354535400160004037367690014000102005d0005060ac00000003800020005001d00040000009600510004000000030001006a006c0004000000030002004800160004037367690017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3420746f2061737369676e65640014000102005d0005060ac00000003800020003001d00040000006400510004000000020001006a006c0004000000010002004800160004037367690017002e0100002a7065726d6974206f75742069702066726f6d203139382e31392e36352e3220746f2061737369676e65640014000102005d0005060ac00000003800020001001d0004000000c8005100040000000100060013003e000102002500020000005100040000000200060013003e000103002500020000005100040000000300060013003e00010200250002000000510004000000010039000d02ffde7211a5ab800aac120101003c0006020465726777") + +pfcpSEReq3 = PFCP(seq=3) / \ + PFCPSessionEstablishmentRequest(IE_list=[ + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=6), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="SGi-LAN/N6-LAN"), + IE_NetworkInstance(instance="sgi") + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=4), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="SGi-LAN/N6-LAN"), + IE_NetworkInstance(instance="sgi"), + IE_RedirectInformation(type="URL", address="http://example.com") + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=2), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="SGi-LAN/N6-LAN"), + IE_NetworkInstance(instance="sgi") + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=5), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="Access"), + IE_NetworkInstance(instance="access") + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=3), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="Access"), + IE_NetworkInstance(instance="access") + ]) + ]), + IE_CreateFAR(IE_list=[ + IE_ApplyAction(FORW=1), + IE_FAR_Id(id=1), + IE_ForwardingParameters(IE_list=[ + IE_DestinationInterface(interface="Access"), + IE_NetworkInstance(instance="access") + ]) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=6), + IE_PDI(IE_list=[ + IE_ApplicationId(id="TST"), + IE_NetworkInstance(instance="access"), + IE_SourceInterface(interface="Access"), + IE_UE_IP_Address(ipv4='10.192.0.0', V4=1) + ]), + IE_PDR_Id(id=6), + IE_Precedence(precedence=150), + IE_URR_Id(id=3) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=4), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="access"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from 198.19.65.4 to assigned"), + IE_SourceInterface(interface="Access"), + IE_UE_IP_Address(ipv4='10.192.0.0', V4=1) + ]), + IE_PDR_Id(id=4), + IE_Precedence(precedence=100), + IE_URR_Id(id=2) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=2), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="access"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from 198.19.65.2 to assigned"), + IE_SourceInterface(interface="Access"), + IE_UE_IP_Address(ipv4='10.192.0.0', V4=1) + ]), + IE_PDR_Id(id=2), + IE_Precedence(precedence=200), + IE_URR_Id(id=1) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=5), + IE_PDI(IE_list=[ + IE_ApplicationId(id="TST"), + IE_NetworkInstance(instance="sgi"), + IE_SourceInterface(interface="SGi-LAN/N6-LAN"), + IE_UE_IP_Address(ipv4='10.192.0.0', SD=1, V4=1) + ]), + IE_PDR_Id(id=5), + IE_Precedence(precedence=150), + IE_URR_Id(id=3) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=3), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="sgi"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from 198.19.65.4 to assigned"), + IE_SourceInterface(interface="SGi-LAN/N6-LAN"), + IE_UE_IP_Address(ipv4='10.192.0.0', SD=1, V4=1) + ]), + IE_PDR_Id(id=3), + IE_Precedence(precedence=100), + IE_URR_Id(id=2) + ]), + IE_CreatePDR(IE_list=[ + IE_FAR_Id(id=1), + IE_PDI(IE_list=[ + IE_NetworkInstance(instance="sgi"), + IE_SDF_Filter(FD=1, flow_description="permit out ip from 198.19.65.2 to assigned"), + IE_SourceInterface(interface="SGi-LAN/N6-LAN"), + IE_UE_IP_Address(ipv4='10.192.0.0', SD=1, V4=1) + ]), + IE_PDR_Id(id=1), + IE_Precedence(precedence=200), + IE_URR_Id(id=1) + ]), + IE_CreateURR(IE_list=[ + IE_MeasurementMethod(VOLUM=1), + IE_ReportingTriggers(), + IE_URR_Id(id=2) + ]), + IE_CreateURR(IE_list=[ + IE_MeasurementMethod(VOLUM=1, DURAT=1), + IE_ReportingTriggers(), + IE_URR_Id(id=3) + ]), + IE_CreateURR(IE_list=[ + IE_MeasurementMethod(VOLUM=1), + IE_ReportingTriggers(), + IE_URR_Id(id=1) + ]), + IE_FSEID(ipv4='172.18.1.1', v4=1, seid=0xffde7211a5ab800a), + IE_NodeId(id_type="FQDN", id="ergw") + ]) + +assert bytes(pfcpSEReq3) == pfcpSEReq3Bytes +assert bytes(PFCP(pfcpSEReq3Bytes)) == pfcpSEReq3Bytes + += Build and dissect PFCP Session Establishment Response + +pfcpSERespBytes = hex_bytes("21330022ffde7210bf97810a0000020000130001010039000d02ffde7210bf97810aac120102") + +pfcpSEResp = PFCP(version=1, S=1, seq=2, seid=0xffde7210bf97810a) / \ + PFCPSessionEstablishmentResponse(IE_list=[ + IE_Cause(cause="Request accepted"), + IE_FSEID(ipv4="172.18.1.2", v4=1, seid=0xffde7210bf97810a), + ]) + +assert bytes(pfcpSEResp) == pfcpSERespBytes +assert bytes(PFCP(pfcpSERespBytes)) == pfcpSERespBytes +assert pfcpSEResp.answers(pfcpSEReq1) + += Build and dissect PFCP Heartbeat Request + +pfcpHReqBytes = hex_bytes("2001000c0000030000600004e1a47d08") + +pfcpHReq = PFCP(version=1, S=0, seq=3) / \ + PFCPHeartbeatRequest(IE_list=[ + IE_RecoveryTimeStamp(timestamp=3785653512) + ]) + +assert bytes(pfcpHReq) == pfcpHReqBytes +assert bytes(PFCP(pfcpHReqBytes)) == pfcpHReqBytes + +# = Build and dissect PFCP Heartbeat Response + +pfcpHRespBytes = hex_bytes("2002000c0000030000600004e1a47af9") + +pfcpHResp = PFCP(version=1, S=0, seq=3) / \ + PFCPHeartbeatResponse(IE_list=[ + IE_RecoveryTimeStamp(timestamp=3785652985) + ]) + +assert bytes(pfcpHResp) == pfcpHRespBytes +assert bytes(PFCP(pfcpHRespBytes)) == pfcpHRespBytes +assert pfcpHResp.answers(pfcpHReq) + +# = Build and dissect PFCP Session Report Request + +pfcpSRReq1Bytes = hex_bytes("21380034ffde7210bf99c00300006b0000270001020050001f00510004000000010068000400000001003f00021000005d0005020ac00001") + +pfcpSRReq1 = PFCP(seq=107, version=1, S=1, seid=18437299340760956931) / \ + PFCPSessionReportRequest(IE_list=[ + IE_ReportType(USAR=1), + IE_UsageReport_SRR(IE_list=[ + IE_URR_Id(id=1), + IE_UR_SEQN(number=1), + IE_UsageReportTrigger(START=1), + IE_UE_IP_Address(ipv4="10.192.0.1", V4=1) + ]) + ]) + +assert bytes(pfcpSRReq1) == pfcpSRReq1Bytes +assert bytes(PFCP(pfcpSRReq1Bytes)) == pfcpSRReq1Bytes + +pfcpSRReq2Bytes = hex_bytes("2138008a0ffde7210bf940000000310000270001020050007500510004000000030068000400000018003f00020100004b0004e1b44787004c0004e1b447910042001907000000000000000000000000000000000000000000000000004300040000000a8003000a48f9e1b4479137cbd8008004000a48f9e1b4478737cbd8008005000a48f9e1b4479137cbd800") + +pfcpSRReq2 = PFCP(seq=49, seid=1152331208797536256) / \ + PFCPSessionReportRequest(IE_list=[ + IE_ReportType(USAR=1), + IE_UsageReport_SRR(IE_list=[ + IE_URR_Id(id=3), + IE_UR_SEQN(number=24), + IE_UsageReportTrigger(PERIO=1), + IE_StartTime(timestamp=3786688391), + IE_EndTime(timestamp=3786688401), + IE_VolumeMeasurement( + DLVOL=1, ULVOL=1, TOVOL=1, total=0, uplink=0, downlink=0), + IE_DurationMeasurement(duration=10), + IE_EnterpriseSpecific( + ietype=32771, + enterprise_id=18681, + data=b'\xe1\xb4G\x917\xcb\xd8\x00'), + IE_EnterpriseSpecific( + ietype=32772, + enterprise_id=18681, + data=b'\xe1\xb4G\x877\xcb\xd8\x00'), + IE_EnterpriseSpecific( + ietype=32773, + enterprise_id=18681, + data=b'\xe1\xb4G\x917\xcb\xd8\x00') + ]) + ]) + +assert bytes(pfcpSRReq2) == pfcpSRReq2Bytes +assert bytes(PFCP(pfcpSRReq2Bytes)) == pfcpSRReq2Bytes + +pfcpSRReq3Bytes = hex_bytes("21380035a2a2aa9ad7f316fd0000010000270001020050002000510004000000010068000400000000003f0003100000005d000502ac100202") + +pfcpSRReq3 = PFCP(seq=1, seid=11719116762396169981) / \ + PFCPSessionReportRequest(IE_list=[ + IE_ReportType(USAR=1), + IE_UsageReport_SRR(IE_list=[ + IE_URR_Id(id=1), + IE_UR_SEQN(number=0), + IE_UsageReportTrigger(START=1, extra_data=b'\x00'), + IE_UE_IP_Address(ipv4='172.16.2.2', V4=1) + ]) + ]) + +assert bytes(pfcpSRReq3) == pfcpSRReq3Bytes +assert bytes(PFCP(pfcpSRReq3Bytes)) == pfcpSRReq3Bytes + += Build and dissect PFCP Session Report Response + +pfcpSRRespBytes = hex_bytes("21390011ffde7210bf99c00300006b000013000101") + +pfcpSRResp = PFCP(version=1, S=1, seq=107, seid=0xffde7210bf99c003) / \ + PFCPSessionReportResponse(IE_list=[ + IE_Cause(cause="Request accepted") + ]) + +assert bytes(pfcpSRResp) == pfcpSRRespBytes +assert bytes(PFCP(pfcpSRRespBytes)) == pfcpSRRespBytes +assert pfcpSRResp.answers(pfcpSRReq1) + += Build and dissect PFCP Session Modification Request + +pfcpSMReqBytes = hex_bytes("21340018ffde72125aeb00a300000600004d00080051000400000001") +pfcpSMReq = PFCP(pfcpSMReqBytes) + +pfcpSMReq = PFCP(version=1, seq=6, seid=0xffde72125aeb00a3) / \ + PFCPSessionModificationRequest(IE_list=[ + IE_QueryURR(IE_list=[IE_URR_Id(id=1)]) + ]) +assert bytes(pfcpSMReq) == pfcpSMReqBytes +assert bytes(PFCP(pfcpSMReqBytes)) == pfcpSMReqBytes + += Build and dissect PFCP Session Modification Response + +pfcpSMRespBytes = hex_bytes("2135008affde72125aeb00a3000006000013000101004e007500510004000000010068000400000000003f00028000004b0004e16e7efa004c0004e16e7efa004200190700000000000000000000000000000000000000000000000000430004000000008003000a48f9e16e7efa05566c008004000a48f9e16e7efa027f08008005000a48f9e16e7efa027f0800") + +pfcpSMResp = PFCP(version=1, seq=6, seid=0xffde72125aeb00a3) / \ + PFCPSessionModificationResponse(IE_list=[ + IE_Cause(cause=1), + IE_UsageReport_SMR(IE_list=[ + IE_URR_Id(id=1), + IE_UR_SEQN(number=0), + IE_UsageReportTrigger(IMMER=1), + IE_StartTime(timestamp=3782115066), + IE_EndTime(timestamp=3782115066), + IE_VolumeMeasurement(DLVOL=1, ULVOL=1, TOVOL=1), + IE_DurationMeasurement(), + IE_EnterpriseSpecific(ietype=32771, enterprise_id=18681, data=b'\xe1n~\xfa\x05Vl\x00'), + IE_EnterpriseSpecific(ietype=32772, enterprise_id=18681, data=b'\xe1n~\xfa\x02\x7f\x08\x00'), + IE_EnterpriseSpecific(ietype=32773, enterprise_id=18681, data=b'\xe1n~\xfa\x02\x7f\x08\x00') + ]) + ]) + +assert bytes(pfcpSMResp) == pfcpSMRespBytes +assert bytes(PFCP(pfcpSMRespBytes)) == pfcpSMRespBytes +assert pfcpSMResp.answers(pfcpSMReq) + += Verify IEs + +from difflib import unified_diff +cases = [ + dict( + hex="0054000a0100010000000a177645", + expect=IE_OuterHeaderCreation(GTPUUDPIPV4=1, TEID=0x01000000, ipv4="10.23.118.69")), + dict( + hex="002900050461626364", + expect=IE_ForwardingPolicy(policy_identifier="abcd")), + dict( + hex="002e0001ae", + expect=IE_DownlinkDataNotificationDelay(delay=174)), + dict( + hex="003d00020000", + expect=IE_PFDContents()), + dict( + hex="005e00070300205903e95d", + expect=IE_PacketRate(ULPR=1, DLPR=1, + ul_time_unit="minute", ul_max_packet_rate=8281, + dl_time_unit="day", dl_max_packet_rate=59741)), + dict( + hex="00850007010906638dccd5", + expect=IE_MACAddress(SOUR=1, source_mac="09:06:63:8d:cc:d5")), + dict( + hex="00540014080017d0bd69dceb747a1e036c0f9c8d4af115d0", + expect=IE_OuterHeaderCreation(UDPIPV6=1, + ipv6="17d0:bd69:dceb:747a:1e03:6c0f:9c8d:4af1", + port=5584)), + dict( + hex="006700050280df69b2", + expect=IE_RemoteGTP_U_Peer(V4=1, ipv4="128.223.105.178")), +] + +for case in cases: + bs = hex_bytes(case["hex"]) + exp = case["expect"] + dissected = type(exp)(bs) + exp_text = exp.show2(dump=True) + dissected_text = dissected.show2(dump=True) + if exp_text != dissected_text: + print("---\n%s\n---\n%s\n" % (exp_text, dissected_text)) + for line in unified_diff(exp_text.split("\n"), dissected_text.split("\n"), + fromfile="expected", tofile="dissected"): + print(line) + raise AssertionError("text mismatch") + assert bytes(dissected) == bs + assert bytes(exp) == bs + +# from difflib import unified_diff +# expected = PFCP(pfcpSRReq2Bytes).show2(dump=True).split("\n") +# actual = pfcpSRReq2.show2(dump=True).split("\n") +# for line in unified_diff(expected, actual, fromfile="expected", tofile="actual"): +# print(line) diff --git a/test/contrib/pim.uts b/test/contrib/pim.uts new file mode 100644 index 00000000000..497e0d97958 --- /dev/null +++ b/test/contrib/pim.uts @@ -0,0 +1,275 @@ +# PIM Related regression tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('pim')" -t test/contrib/pim.uts + ++ pim + += PIMv2 Hello - instantiation + +hello_data = b'\x01\x00^\x00\x00\r\x00\xd0\xcb\x00\xba\xe4\x08\x00E\xc0\x00BY\xf9\x00\x00\x01gTe\x15\x15\x15\x15\xe0\x00\x00\r \x00\xa55\x00\x01\x00\x02\x00i\x00\x13\x00\x04\x00\x00\x00\x00\x00\x02\x00\x04\x01\xf4\t\xc4\x00\x14\x00\x04\x00\x00\x00\x00' + +hello_pkt = Ether(hello_data) + +assert (hello_pkt[PIMv2Hdr].version == 2) +assert (hello_pkt[PIMv2Hdr].type == 0) +assert (len(hello_pkt[PIMv2Hello].option) == 4) +assert (hello_pkt[PIMv2Hello].option[0][PIMv2HelloHoldtime].type == 1) +assert (hello_pkt[PIMv2Hello].option[0][PIMv2HelloHoldtime].holdtime == 105) +assert (hello_pkt[PIMv2Hello].option[1][PIMv2HelloDRPriority].type == 19) +assert (hello_pkt[PIMv2Hello].option[1][PIMv2HelloDRPriority].dr_priority == 0) +assert (hello_pkt[PIMv2Hello].option[2][PIMv2HelloLANPruneDelay].type == 2) +assert (hello_pkt[PIMv2Hello].option[2][PIMv2HelloLANPruneDelay].value[0][PIMv2HelloLANPruneDelayValue].t == 0) +assert (hello_pkt[PIMv2Hello].option[2][PIMv2HelloLANPruneDelay].value[0][PIMv2HelloLANPruneDelayValue].propagation_delay == 500) +assert (hello_pkt[PIMv2Hello].option[2][PIMv2HelloLANPruneDelay].value[0][PIMv2HelloLANPruneDelayValue].override_interval == 2500) +assert (hello_pkt[PIMv2Hello].option[3][PIMv2HelloGenerationID].type == 20) + +repr(PIMv2HelloLANPruneDelayValue(t=1)) + += PIMv2 Join/Prune - instantiation + +jp_data = b'\x01\x00^\x00\x00\r\x00\xd0\xcb\x00\xba\xe4\x08\x00E\xc0\x00rY\xfb\x00\x00\x01gT3\x15\x15\x15\x15\xe0\x00\x00\r#\x00\x1b\x18\x01\x00\x15\x15\x15\x16\x00\x04\x00\xd2\x01\x00\x00 \xef\x01\x01\x0b\x00\x01\x00\x00\x01\x00\x07 \x16\x16\x16\x15\x01\x00\x00 \xef\x01\x01\x0c\x00\x01\x00\x00\x01\x00\x07 \x16\x16\x16\x15\x01\x00\x00 \xef\x01\x01\x0b\x00\x00\x00\x01\x01\x00\x07 \x16\x16\x16\x15\x01\x00\x00 \xef\x01\x01\x0c\x00\x00\x00\x01\x01\x00\x07 \x16\x16\x16\x15' + +jp_pkt = Ether(jp_data) + +assert (jp_pkt[PIMv2Hdr].version == 2) +assert (jp_pkt[PIMv2Hdr].type == 3) +assert (jp_pkt[PIMv2JoinPrune].up_addr_family == 1) +assert (jp_pkt[PIMv2JoinPrune].up_encoding_type == 0) +assert (jp_pkt[PIMv2JoinPrune].up_neighbor_ip == "21.21.21.22") +assert (jp_pkt[PIMv2JoinPrune].reserved == 0) +assert (jp_pkt[PIMv2JoinPrune].num_group == 4) +assert (jp_pkt[PIMv2JoinPrune].holdtime == 210) +assert (jp_pkt[PIMv2JoinPrune].num_group == len(jp_pkt[PIMv2JoinPrune].jp_ips)) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].addr_family == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].encoding_type == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].bidirection == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].reserved == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].admin_scope_zone == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].mask_len == 32) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].gaddr == "239.1.1.11") +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].num_joins == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].num_joins == len(jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips)) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].addr_family == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].encoding_type == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].rsrvd == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].sparse == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].wildcard == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].rpt == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].mask_len == 32) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].src_ip == "22.22.22.21") +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].num_prunes == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[0].num_prunes == len(jp_pkt[PIMv2JoinPrune].jp_ips[0].prune_ips)) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].addr_family == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].encoding_type == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].bidirection == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].reserved == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].admin_scope_zone == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].mask_len == 32) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].gaddr == "239.1.1.11") +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].num_joins == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].num_joins == len(jp_pkt[PIMv2JoinPrune].jp_ips[2].join_ips)) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].num_prunes == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].num_prunes == len(jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips)) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].addr_family == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].encoding_type == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].rsrvd == 0) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].sparse == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].wildcard == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].rpt == 1) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].mask_len == 32) +assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].src_ip == "22.22.22.21") + += PIMv2 Hello - build + +hello_delay_pkt = Ether(dst="01:00:5e:00:00:0d", src="00:d0:cb:00:ba:e4")/IP(version=4, ihl=5, tos=0xc0, id=23037, ttl=1, proto=103, src="21.21.21.21", dst="224.0.0.13")/\ + PIMv2Hdr(version=2, type=0, reserved=0)/\ + PIMv2Hello(option=[PIMv2HelloHoldtime(type=1, holdtime=105), PIMv2HelloDRPriority(type=19, dr_priority=0), + PIMv2HelloLANPruneDelay(type=2, value=[PIMv2HelloLANPruneDelayValue(t=0, propagation_delay=500, override_interval=2500)]), + PIMv2HelloGenerationID(type=20, generation_id=459007194)]) + +assert raw(hello_delay_pkt) == b'\x01\x00^\x00\x00\r\x00\xd0\xcb\x00\xba\xe4\x08\x00E\xc0\x006Y\xfd\x00\x00\x01gTm\x15\x15\x15\x15\xe0\x00\x00\r \x00\xd3p\x00\x01\x00\x02\x00i\x00\x13\x00\x04\x00\x00\x00\x00\x00\x02\x00\x04\x01\xf4\t\xc4\x00\x14\x00\x04\x1b[\xe4\xda' + +hello_refresh_pkt = Ether(dst="01:00:5e:00:00:0d", src="c2:01:52:72:00:00")/IP(version=4, ihl=5, tos=0xc0, id=121, ttl=1, proto=103, src="10.0.0.1", dst="224.0.0.13")/\ + PIMv2Hdr(version=2, type=0, reserved=0)/\ + PIMv2Hello(option=[PIMv2HelloHoldtime(type=1, holdtime=105), PIMv2HelloGenerationID(type=20, generation_id=3613938422), + PIMv2HelloDRPriority(type=19, dr_priority=1), + PIMv2HelloStateRefresh(type=21, value=[PIMv2HelloStateRefreshValue(version=1, interval=0, reserved=0)])]) + +assert raw(hello_refresh_pkt) == b'\x01\x00^\x00\x00\r\xc2\x01Rr\x00\x00\x08\x00E\xc0\x006\x00y\x00\x00\x01g\xce\x1a\n\x00\x00\x01\xe0\x00\x00\r \x00\xb3\xeb\x00\x01\x00\x02\x00i\x00\x14\x00\x04\xd7hR\xf6\x00\x13\x00\x04\x00\x00\x00\x01\x00\x15\x00\x04\x01\x00\x00\x00' + += PIMv2 Join/Prune - build + +join_pkt = Ether(dst="01:00:5e:00:00:0d", src="c2:02:3d:80:00:01")/IP(version=4, ihl=5, tos=0xc0, id=139, ttl=1, proto=103, src="10.0.0.14", dst="224.0.0.13")/\ + PIMv2Hdr(version=2, type=3, reserved=0)/\ + PIMv2JoinPrune(up_addr_family=1, up_encoding_type=0, up_neighbor_ip="10.0.0.13", reserved=0, num_group=1, holdtime=210, + jp_ips=[PIMv2GroupAddrs(addr_family=1, encoding_type=0, bidirection=0, reserved=0, admin_scope_zone=0, + mask_len=32, gaddr="239.123.123.123", + join_ips=[PIMv2JoinAddrs(addr_family=1, encoding_type=0, rsrvd=0, sparse=1, wildcard=1, + rpt=1, mask_len=32, src_ip="1.1.1.1")], + prune_ips=[]) + ] ) + + +assert raw(join_pkt) == b'\x01\x00^\x00\x00\r\xc2\x02=\x80\x00\x01\x08\x00E\xc0\x006\x00\x8b\x00\x00\x01g\xcd\xfb\n\x00\x00\x0e\xe0\x00\x00\r#\x00Z\xe5\x01\x00\n\x00\x00\r\x00\x01\x00\xd2\x01\x00\x00 \xef{{{\x00\x01\x00\x00\x01\x00\x07 \x01\x01\x01\x01' + + + +prune_pkt = Ether(dst="01:00:5e:00:00:0d", src="c2:02:3d:80:00:01")/IP(version=4, ihl=5, tos=0xc0, id=139, ttl=1, proto=103, src="10.0.0.2", dst="224.0.0.13")/\ + PIMv2Hdr(version=2, type=3, reserved=0)/\ + PIMv2JoinPrune(up_addr_family=1, up_encoding_type=0, up_neighbor_ip="10.0.0.1", reserved=0, num_group=1, holdtime=210, + jp_ips=[PIMv2GroupAddrs(addr_family=1, encoding_type=0, bidirection=0, reserved=0, admin_scope_zone=0, + mask_len=32, gaddr="239.123.123.123", + prune_ips=[PIMv2PruneAddrs(addr_family=1, encoding_type=0, rsrvd=0, sparse=0, wildcard=0, rpt=0, + mask_len=32, src_ip="172.16.40.10")]) + ] + ) + +assert raw(prune_pkt) == b'\x01\x00^\x00\x00\r\xc2\x02=\x80\x00\x01\x08\x00E\xc0\x006\x00\x8b\x00\x00\x01g\xce\x07\n\x00\x00\x02\xe0\x00\x00\r#\x00\x8f\xd8\x01\x00\n\x00\x00\x01\x00\x01\x00\xd2\x01\x00\x00 \xef{{{\x00\x00\x00\x01\x01\x00\x00 \xac\x10(\n' + + + + + +#################################################################################### +# IPv6 added +#################################################################################### + + += IPv6 PIMv2 Hello - instantiation + +hello_data6 = b'33\x00\x00\x00\r\x02\x00\x00\x00\x00\x01\x86\xddk\x80\x00\x00\x008g\x01\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r \x00\xe4G\x00\x01\x00\x02\x00i\x00\x02\x00\x04\x01\xf4\t\xc4\x00\x13\x00\x04\x00\x00\x00\x01\x00\x14\x00\x04:I\x8b\xa3\x00\x18\x00\x12\x02\x00 \x01\xa7\xff@\n"\t\x00\x00\x00\x00\x00\x00\x00\x02' + +hello_pkt6 = Ether(hello_data6) + +assert (hello_pkt6[PIMv2Hdr].version == 2) +assert (hello_pkt6[PIMv2Hdr].type == 0) +assert (len(hello_pkt6[PIMv2Hello].option) == 5) +assert (hello_pkt6[PIMv2Hello].option[0][PIMv2HelloHoldtime].type == 1) +assert (hello_pkt6[PIMv2Hello].option[0][PIMv2HelloHoldtime].holdtime == 105) +assert (hello_pkt6[PIMv2Hello].option[1][PIMv2HelloLANPruneDelay].type == 2) +assert (hello_pkt6[PIMv2Hello].option[1][PIMv2HelloLANPruneDelay].value[0][PIMv2HelloLANPruneDelayValue].t == 0) +assert (hello_pkt6[PIMv2Hello].option[1][PIMv2HelloLANPruneDelay].value[0][PIMv2HelloLANPruneDelayValue].propagation_delay == 500) +assert (hello_pkt6[PIMv2Hello].option[1][PIMv2HelloLANPruneDelay].value[0][PIMv2HelloLANPruneDelayValue].override_interval == 2500) +assert (hello_pkt6[PIMv2Hello].option[2][PIMv2HelloDRPriority].type == 19) +assert (hello_pkt6[PIMv2Hello].option[2][PIMv2HelloDRPriority].dr_priority == 1) +assert (hello_pkt6[PIMv2Hello].option[3][PIMv2HelloGenerationID].type == 20) + +repr(PIMv2HelloLANPruneDelayValue(t=1)) + += IPv6 PIMv2 Join/Prune - instantiation + +jp_data6join = b'33\x00\x00\x00\r\x02\x00\x00\x00\x00\x01\x86\xddk\x80\x00\x00\x00Fg\x01\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r#\x00\xc6X\x02\x00\xfe\x80\x00\x00\x00\x00\x00\x00\xfc\x87\xff\xff\xfe\x00\x01A\x00\x01\x00\xd2\x02\x00\x00\x80\xff>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x01\x00\x01\x00\x00\x02\x00\x04\x80$\x04\x80\x00\x00\x01\xf0\x01\x00\x00\x00\x00\x00\x00\x00\x01' + +jp_pkt6 = Ether(jp_data6join) + +assert (jp_pkt6[PIMv2Hdr].version == 2) +assert (jp_pkt6[PIMv2Hdr].type == 3) +assert (jp_pkt6[PIMv2JoinPrune].up_addr_family == 2) +assert (jp_pkt6[PIMv2JoinPrune].up_encoding_type == 0) +assert (jp_pkt6[PIMv2JoinPrune].up_neighbor_ip == 'fe80::fc87:ffff:fe00:141') +assert (jp_pkt6[PIMv2JoinPrune].reserved == 0) +assert (jp_pkt6[PIMv2JoinPrune].num_group == 1) +assert (jp_pkt6[PIMv2JoinPrune].holdtime == 210) +assert (jp_pkt6[PIMv2JoinPrune].num_group == len(jp_pkt6[PIMv2JoinPrune].jp_ips)) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].addr_family == 2) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].encoding_type == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].bidirection == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].reserved == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].admin_scope_zone == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].mask_len == 128) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].gaddr == 'ff3e::8000:1') +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_joins == 1) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_joins == len(jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips)) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].addr_family == 2) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].encoding_type == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].rsrvd == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].sparse == 1) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].wildcard == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].rpt == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].mask_len == 128) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips[0][PIMv2JoinAddrs].src_ip == '2404:8000:1:f001::1') +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_prunes == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_prunes == len(jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips)) + + + +jp_data6prune = b'33\x00\x00\x00\r\x02\x00\x00\x00\x00\x01\x86\xddk\x80\x00\x00\x00Fg\x01\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r#\x00\xc6X\x02\x00\xfe\x80\x00\x00\x00\x00\x00\x00\xfc\x87\xff\xff\xfe\x00\x01A\x00\x01\x00\xd2\x02\x00\x00\x80\xff>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x01\x00\x00\x00\x01\x02\x00\x04\x80$\x04\x80\x00\x00\x01\xf0\x01\x00\x00\x00\x00\x00\x00\x00\x01' + +jp_pkt6 = Ether(jp_data6prune) + +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].addr_family == 2) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].encoding_type == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].bidirection == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].reserved == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].admin_scope_zone == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].mask_len == 128) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].gaddr == 'ff3e::8000:1') +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_joins == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_joins == len(jp_pkt6[PIMv2JoinPrune].jp_ips[0].join_ips)) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_prunes == 1) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].num_prunes == len(jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips)) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].addr_family == 2) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].encoding_type == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].rsrvd == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].sparse == 1) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].wildcard == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].rpt == 0) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].mask_len == 128) +assert (jp_pkt6[PIMv2JoinPrune].jp_ips[0].prune_ips[0][PIMv2PruneAddrs].src_ip == '2404:8000:1:f001::1') + + + + += IPv6 PIMv2 Hello - build + +hello_delay_pkt6 = Ether(dst='33:33:00:00:00:0d', src='02:00:00:00:00:01')/ \ + IPv6(tc=0xb8, nh=103, hlim=1, src='fe80::ff:fe00:1', dst='ff02::d')/ \ + PIMv2Hdr()/ \ + PIMv2Hello(option=[ \ + PIMv2HelloHoldtime(holdtime=105), + PIMv2HelloLANPruneDelay(value=[PIMv2HelloLANPruneDelayValue(propagation_delay=500, override_interval=2500)]), + PIMv2HelloDRPriority(dr_priority=1), + PIMv2HelloGenerationID(generation_id=977898403), + PIMv2HelloAddrList(value=[PIMv2HelloAddrListValue(addr_family=2,prefix='2001:a7ff:400a:2209::2')]), + ]) + + +assert raw(hello_delay_pkt6) == hello_data6 + + + + + += IPv6 PIMv2 Join/Prune - build + + +join_pkt6 = Ether(dst='33:33:00:00:00:0d', src='02:00:00:00:00:01')/\ + IPv6(tc=184, nh=103, hlim=1, src='fe80::ff:fe00:1', dst='ff02::d')/ \ + PIMv2Hdr(version=2, type=3, reserved=0)/ \ + PIMv2JoinPrune(jp_ips=[ \ + PIMv2GroupAddrs(join_ips=[ + PIMv2JoinAddrs(addr_family=2, sparse=1, wildcard=0, rpt=0, mask_len=128, src_ip='2404:8000:1:f001::1')], + addr_family=2, admin_scope_zone=0, mask_len=128, gaddr='ff3e::8000:1', + num_joins=1, num_prunes=0)], + up_addr_family=2, up_neighbor_ip='fe80::fc87:ffff:fe00:141', num_group=1, holdtime=210) + + +assert raw(join_pkt6) == jp_data6join + + + +prune_pkt6 = Ether(dst='33:33:00:00:00:0d', src='02:00:00:00:00:01')/ \ + IPv6(tc=184, nh=103, hlim=1, src='fe80::ff:fe00:1', dst='ff02::d')/ \ + PIMv2Hdr()/ \ + PIMv2JoinPrune(jp_ips=[ \ + PIMv2GroupAddrs(prune_ips=[ \ + PIMv2PruneAddrs(addr_family=2, sparse=1, wildcard=0, rpt=0, mask_len=128, src_ip='2404:8000:1:f001::1')], + addr_family=2, mask_len=128, gaddr='ff3e::8000:1', + num_joins=0, num_prunes=1)], + up_addr_family=2, up_neighbor_ip='fe80::fc87:ffff:fe00:141', num_group=1, holdtime=210) + + +assert raw(prune_pkt6) == jp_data6prune + + diff --git a/test/contrib/pnio.uts b/test/contrib/pnio.uts index d783374b708..9528e6b0d58 100644 --- a/test/contrib/pnio.uts +++ b/test/contrib/pnio.uts @@ -37,24 +37,24 @@ isinstance(p.payload, ProfinetIO) and p.frameID == 0x0102 = ProfinetIO PNIORealTime_IOxS parsing of a single status p = PNIORealTime_IOxS(b'\x80') -assert(p.dataState == 1) -assert(p.instance == 0) -assert(p.reserved == 0) -assert(p.extension == 0) +assert p.dataState == 1 +assert p.instance == 0 +assert p.reserved == 0 +assert p.extension == 0 p = PNIORealTime_IOxS(b'\xe1') -assert(p.dataState == 1) -assert(p.instance == 3) -assert(p.reserved == 0) -assert(p.extension == 1) +assert p.dataState == 1 +assert p.instance == 3 +assert p.reserved == 0 +assert p.extension == 1 True = ProfinetIO PNIORealTime_IOxS building of a single status p = PNIORealTime_IOxS(dataState = 'good', instance='subslot', extension=0) -assert(raw(p) == b'\x80') +assert raw(p) == b'\x80' p = PNIORealTime_IOxS(dataState = 'bad', instance='device', extension=1) -assert(raw(p) == b'\x41') +assert raw(p) == b'\x41' True = ProfinetIO PNIORealTime_IOxS parsing with multiple statuses @@ -70,38 +70,38 @@ TestPacket = type( ) p = TestPacket(b'\x81\xe1\x01\x80') -assert(len(p.data) == 4) -assert(p.data[0].dataState == 1) -assert(p.data[0].instance == 0) -assert(p.data[0].reserved == 0) -assert(p.data[0].extension == 1) -assert(p.data[1].dataState == 1) -assert(p.data[1].instance == 3) -assert(p.data[1].reserved == 0) -assert(p.data[1].extension == 1) -assert(p.data[2].dataState == 0) -assert(p.data[2].instance == 0) -assert(p.data[2].reserved == 0) -assert(p.data[2].extension == 1) -assert(p.data[3].dataState == 1) -assert(p.data[3].instance == 0) -assert(p.data[3].reserved == 0) -assert(p.data[3].extension == 0) +assert len(p.data) == 4 +assert p.data[0].dataState == 1 +assert p.data[0].instance == 0 +assert p.data[0].reserved == 0 +assert p.data[0].extension == 1 +assert p.data[1].dataState == 1 +assert p.data[1].instance == 3 +assert p.data[1].reserved == 0 +assert p.data[1].extension == 1 +assert p.data[2].dataState == 0 +assert p.data[2].instance == 0 +assert p.data[2].reserved == 0 +assert p.data[2].extension == 1 +assert p.data[3].dataState == 1 +assert p.data[3].instance == 0 +assert p.data[3].reserved == 0 +assert p.data[3].extension == 0 = ProfinetIO RTC PDU parsing without configuration p = Ether(b'\x00\x02\x04\x06\x08\x0a\x01\x03\x05\x07\x09\x0B\x88\x92\x80\x00\x01\x02\x03\x04\xf0\x00\x35\x00') -assert(p[Ether].dst == '00:02:04:06:08:0a') -assert(p[Ether].src == '01:03:05:07:09:0b') -assert(p[Ether].type == 0x8892) -assert(p[ProfinetIO].frameID == 0x8000) -assert(isinstance(p[ProfinetIO].payload, PNIORealTimeCyclicPDU)) -assert(len(p[PNIORealTimeCyclicPDU].data) == 1) -assert(isinstance(p[PNIORealTimeCyclicPDU].data[0], PNIORealTimeCyclicDefaultRawData)) -assert(p[PNIORealTimeCyclicDefaultRawData].data == b'\x01\x02\x03\x04') -assert(p[PNIORealTimeCyclicPDU].padding == b'') -assert(p[PNIORealTimeCyclicPDU].cycleCounter == 0xf000) -assert(p[PNIORealTimeCyclicPDU].dataStatus == 0x35) -assert(p[PNIORealTimeCyclicPDU].transferStatus == 0) +assert p[Ether].dst == '00:02:04:06:08:0a' +assert p[Ether].src == '01:03:05:07:09:0b' +assert p[Ether].type == 0x8892 +assert p[ProfinetIO].frameID == 0x8000 +assert isinstance(p[ProfinetIO].payload, PNIORealTimeCyclicPDU) +assert len(p[PNIORealTimeCyclicPDU].data) == 1 +assert isinstance(p[PNIORealTimeCyclicPDU].data[0], PNIORealTimeCyclicDefaultRawData) +assert p[PNIORealTimeCyclicDefaultRawData].data == b'\x01\x02\x03\x04' +assert p[PNIORealTimeCyclicPDU].padding == b'' +assert p[PNIORealTimeCyclicPDU].cycleCounter == 0xf000 +assert p[PNIORealTimeCyclicPDU].dataStatus == 0x35 +assert p[PNIORealTimeCyclicPDU].transferStatus == 0 True = ProfinetIO RTC PDU building @@ -149,80 +149,80 @@ p = Ether( b'\x00' ) -assert(p[Ether].dst == '00:02:04:06:08:0a') -assert(p[Ether].src == '01:03:05:07:09:0b') -assert(p[Ether].type == 0x8892) -assert(p[ProfinetIO].frameID == 0x8010) -assert(isinstance(p[ProfinetIO].payload, PNIORealTimeCyclicPDU)) -assert(len(p[PNIORealTimeCyclicPDU].data) == 3) -assert(isinstance(p[PNIORealTimeCyclicPDU].data[0], scapy.config.conf.raw_layer)) -assert(p[PNIORealTimeCyclicPDU].data[0].data == b'\x01\x02\x03\x04\x05') -assert(isinstance(p[PNIORealTimeCyclicPDU].data[1], scapy.config.conf.raw_layer)) -assert(p[PNIORealTimeCyclicPDU].data[1].data == b'\x01\x02\x03') -assert(isinstance(p[PNIORealTimeCyclicPDU].data[2], scapy.config.conf.raw_layer)) -assert(p[PNIORealTimeCyclicPDU].data[2].data == b'\x01\x02') -assert(p[PNIORealTimeCyclicPDU].padding == b'\x00' * 2) -assert(p[PNIORealTimeCyclicPDU].cycleCounter == 0xf000) -assert(p[PNIORealTimeCyclicPDU].dataStatus == 0x35) -assert(p[PNIORealTimeCyclicPDU].transferStatus == 0) +assert p[Ether].dst == '00:02:04:06:08:0a' +assert p[Ether].src == '01:03:05:07:09:0b' +assert p[Ether].type == 0x8892 +assert p[ProfinetIO].frameID == 0x8010 +assert isinstance(p[ProfinetIO].payload, PNIORealTimeCyclicPDU) +assert len(p[PNIORealTimeCyclicPDU].data) == 3 +assert isinstance(p[PNIORealTimeCyclicPDU].data[0], scapy.config.conf.raw_layer) +assert p[PNIORealTimeCyclicPDU].data[0].data == b'\x01\x02\x03\x04\x05' +assert isinstance(p[PNIORealTimeCyclicPDU].data[1], scapy.config.conf.raw_layer) +assert p[PNIORealTimeCyclicPDU].data[1].data == b'\x01\x02\x03' +assert isinstance(p[PNIORealTimeCyclicPDU].data[2], scapy.config.conf.raw_layer) +assert p[PNIORealTimeCyclicPDU].data[2].data == b'\x01\x02' +assert p[PNIORealTimeCyclicPDU].padding == b'\x00' * 2 +assert p[PNIORealTimeCyclicPDU].cycleCounter == 0xf000 +assert p[PNIORealTimeCyclicPDU].dataStatus == 0x35 +assert p[PNIORealTimeCyclicPDU].transferStatus == 0 p = Ether(b'\x00\x02\x04\x06\x08\x0a\x01\x03\x05\x07\x09\x0B\x88\x92\x80\x00\x01\x02\x03\x04\xf0\x00\x35\x00') -assert(p[Ether].dst == '00:02:04:06:08:0a') -assert(p[Ether].src == '01:03:05:07:09:0b') -assert(p[Ether].type == 0x8892) -assert(p[ProfinetIO].frameID == 0x8000) -assert(isinstance(p[ProfinetIO].payload, PNIORealTimeCyclicPDU)) -assert(len(p[PNIORealTimeCyclicPDU].data) == 1) -assert(isinstance(p[PNIORealTimeCyclicPDU].data[0], PNIORealTimeCyclicDefaultRawData)) -assert(p[PNIORealTimeCyclicDefaultRawData].data == b'\x01\x02\x03\x04') -assert(p[PNIORealTimeCyclicPDU].padding == b'') -assert(p[PNIORealTimeCyclicPDU].cycleCounter == 0xf000) -assert(p[PNIORealTimeCyclicPDU].dataStatus == 0x35) -assert(p[PNIORealTimeCyclicPDU].transferStatus == 0) +assert p[Ether].dst == '00:02:04:06:08:0a' +assert p[Ether].src == '01:03:05:07:09:0b' +assert p[Ether].type == 0x8892 +assert p[ProfinetIO].frameID == 0x8000 +assert isinstance(p[ProfinetIO].payload, PNIORealTimeCyclicPDU) +assert len(p[PNIORealTimeCyclicPDU].data) == 1 +assert isinstance(p[PNIORealTimeCyclicPDU].data[0], PNIORealTimeCyclicDefaultRawData) +assert p[PNIORealTimeCyclicDefaultRawData].data == b'\x01\x02\x03\x04' +assert p[PNIORealTimeCyclicPDU].padding == b'' +assert p[PNIORealTimeCyclicPDU].cycleCounter == 0xf000 +assert p[PNIORealTimeCyclicPDU].dataStatus == 0x35 +assert p[PNIORealTimeCyclicPDU].transferStatus == 0 True = PROFIsafe parsing (query with F_CRC_SEED=0) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeControl, 2)(b'\x80\x80\x40\x01\x02\x03') -assert(p.data == b'\x80\x80') -assert(p.control == 0x40) -assert(p.crc == 0x010203) +assert p.data == b'\x80\x80' +assert p.control == 0x40 +assert p.crc == 0x010203 True = PROFIsafe parsing (query with F_CRC_SEED=1) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeControlCRCSeed, 2)(b'\x80\x80\x40\x01\x02\x03\x04') -assert(p.data == b'\x80\x80') -assert(p.control == 0x40) -assert(p.crc == 0x01020304) +assert p.data == b'\x80\x80' +assert p.control == 0x40 +assert p.crc == 0x01020304 True = PROFIsafe parsing (response with F_CRC_SEED=0) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeStatus, 1)(b'\x80\x40\x01\x02\x03') -assert(p.data == b'\x80') -assert(p.status == 0x40) -assert(p.crc == 0x010203) +assert p.data == b'\x80' +assert p.status == 0x40 +assert p.crc == 0x010203 True = PROFIsafe parsing (response with F_CRC_SEED=1) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeStatusCRCSeed, 1)(b'\x80\x40\x01\x02\x03\x04') -assert(p.data == b'\x80') -assert(p.status == 0x40) -assert(p.crc == 0x01020304) +assert p.data == b'\x80' +assert p.status == 0x40 +assert p.crc == 0x01020304 True = PROFIsafe building (query with F_CRC_SEED=0) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeControl, 2)(data = b'\x81\x80', control=0x40, crc=0x040506) -assert(raw(p) == b'\x81\x80\x40\x04\x05\x06') +assert raw(p) == b'\x81\x80\x40\x04\x05\x06' = PROFIsafe building (query with F_CRC_SEED=1) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeControlCRCSeed, 2)(data = b'\x81\x80', control=0x02, crc=0x04050607) -assert(raw(p) == b'\x81\x80\x02\x04\x05\x06\x07') +assert raw(p) == b'\x81\x80\x02\x04\x05\x06\x07' = PROFIsafe building (response with F_CRC_SEED=0) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeStatus, 3)(data = b'\x01\x81\x00', status=0x01, crc=0x040506) -assert(raw(p) == b'\x01\x81\x00\x01\x04\x05\x06') +assert raw(p) == b'\x01\x81\x00\x01\x04\x05\x06' = PROFIsafe building (response with F_CRC_SEED=1) p = PROFIsafe.build_PROFIsafe_class(PROFIsafeStatusCRCSeed, 3)(data = b'\x01\x81\x80', status=0x01, crc=0x04050607) -assert(raw(p) == b'\x01\x81\x80\x01\x04\x05\x06\x07') +assert raw(p) == b'\x01\x81\x80\x01\x04\x05\x06\x07' conf.debug_dissector = old_conf_dissector diff --git a/test/contrib/pnio_dcp.uts b/test/contrib/pnio_dcp.uts index a48ef7c688a..b018db92356 100644 --- a/test/contrib/pnio_dcp.uts +++ b/test/contrib/pnio_dcp.uts @@ -20,18 +20,18 @@ p = Ether(b'\x01\x0e\xcf\x00\x00\x00\x01\x23\x45\x67\x89\xab\x88\x92\xfe\xfe' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -assert(p[Ether].dst == '01:0e:cf:00:00:00') -assert(p[Ether].src == '01:23:45:67:89:ab') -assert(p[Ether].type == 0x8892) -assert(p[ProfinetIO].frameID == 0xfefe) -assert(p[ProfinetDCP].service_id == 0x05) -assert(p[ProfinetDCP].service_type == 0x00) -assert(p[ProfinetDCP].xid == 0x1000001) -assert(p[ProfinetDCP].reserved == 0x01) -assert(p[ProfinetDCP].dcp_data_length == 0x04) -assert(p[ProfinetDCP].option == 0xff) -assert(p[ProfinetDCP].sub_option == 0xff) -assert(p[ProfinetDCP].dcp_block_length == 0x00) +assert p[Ether].dst == '01:0e:cf:00:00:00' +assert p[Ether].src == '01:23:45:67:89:ab' +assert p[Ether].type == 0x8892 +assert p[ProfinetIO].frameID == 0xfefe +assert p[ProfinetDCP].service_id == 0x05 +assert p[ProfinetDCP].service_type == 0x00 +assert p[ProfinetDCP].xid == 0x1000001 +assert p[ProfinetDCP].reserved == 0x01 +assert p[ProfinetDCP].dcp_data_length == 0x04 +assert p[ProfinetDCP].option == 0xff +assert p[ProfinetDCP].sub_option == 0xff +assert p[ProfinetDCP].dcp_block_length == 0x00 = DCP Set Request parsing @@ -41,19 +41,19 @@ p = Ether(b'\x01\x23\x45\x67\x89\xac\x01\x23\x45\x67\x89\xab\x88\x92\xfe\xfd' \ b'\x64\x65\x76\x69\x63\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -assert(p[Ether].dst == '01:23:45:67:89:ac') -assert(p[Ether].src == '01:23:45:67:89:ab') -assert(p[Ether].type == 0x8892) -assert(p[ProfinetIO].frameID == 0xfefd) -assert(p[ProfinetDCP].service_id == 0x04) -assert(p[ProfinetDCP].service_type == 0x00) -assert(p[ProfinetDCP].xid == 0x0000001) -assert(p[ProfinetDCP].reserved == 0x00) -assert(p[ProfinetDCP].dcp_data_length == 0x0c) -assert(p[ProfinetDCP].option == 0x02) -assert(p[ProfinetDCP].sub_option == 0x02) -assert(p[ProfinetDCP].dcp_block_length == 0x08) -assert(p[ProfinetDCP].block_qualifier == 0x01) +assert p[Ether].dst == '01:23:45:67:89:ac' +assert p[Ether].src == '01:23:45:67:89:ab' +assert p[Ether].type == 0x8892 +assert p[ProfinetIO].frameID == 0xfefd +assert p[ProfinetDCP].service_id == 0x04 +assert p[ProfinetDCP].service_type == 0x00 +assert p[ProfinetDCP].xid == 0x0000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 0x0c +assert p[ProfinetDCP].option == 0x02 +assert p[ProfinetDCP].sub_option == 0x02 +assert p[ProfinetDCP].dcp_block_length == 0x08 +assert p[ProfinetDCP].block_qualifier == 0x01 = DCP Identify Response parsing @@ -67,69 +67,122 @@ p = Ether(b'\x94\x65\x9c\x51\x90\x7d\xac\x64\x17\x21\x35\xcf\x81\x00\x00\x00' \ b'\xc0\xa8\x01\x0e\xff\xff\xff\x00\xc0\xa8\x01\x0e') # General -assert(p[ProfinetIO].frameID == 0xfeff) -assert(p[ProfinetDCP].service_id == 0x05) -assert(p[ProfinetDCP].service_type == 0x01) -assert(p[ProfinetDCP].xid == 0x1000001) -assert(p[ProfinetDCP].reserved == 0x00) -assert(p[ProfinetDCP].dcp_data_length == 0x4e) +assert p[ProfinetIO].frameID == 0xfeff +assert p[ProfinetDCP].service_id == 0x05 +assert p[ProfinetDCP].service_type == 0x01 +assert p[ProfinetDCP].xid == 0x1000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 0x4e # - DCPDeviceOptionsBlock -assert(p[DCPDeviceOptionsBlock].option == 0x02) -assert(p[DCPDeviceOptionsBlock].sub_option == 0x05) -assert(p[DCPDeviceOptionsBlock].dcp_block_length == 0x04) -assert(p[DCPDeviceOptionsBlock].block_info == 0x00) +assert p[DCPDeviceOptionsBlock].option == 0x02 +assert p[DCPDeviceOptionsBlock].sub_option == 0x05 +assert p[DCPDeviceOptionsBlock].dcp_block_length == 0x04 +assert p[DCPDeviceOptionsBlock].block_info == 0x00 # -- DeviceOption -assert(p[DeviceOption].option == 0x02) -assert(p[DeviceOption].sub_option == 0x07) +assert p[DeviceOption].option == 0x02 +assert p[DeviceOption].sub_option == 0x07 # - DCPManufacturerSpecificBlock -assert(p[DCPManufacturerSpecificBlock].option == 0x02) -assert(p[DCPManufacturerSpecificBlock].sub_option == 0x01) -assert(p[DCPManufacturerSpecificBlock].dcp_block_length == 0x09) -assert(p[DCPManufacturerSpecificBlock].block_info == 0x00) -assert(p[DCPManufacturerSpecificBlock].device_vendor_value == b'ET200SP') +assert p[DCPManufacturerSpecificBlock].option == 0x02 +assert p[DCPManufacturerSpecificBlock].sub_option == 0x01 +assert p[DCPManufacturerSpecificBlock].dcp_block_length == 0x09 +assert p[DCPManufacturerSpecificBlock].block_info == 0x00 +assert p[DCPManufacturerSpecificBlock].device_vendor_value == b'ET200SP' # - DCPNameOfStationBlock -assert(p[DCPNameOfStationBlock].option == 0x02) -assert(p[DCPNameOfStationBlock].sub_option == 0x02) -assert(p[DCPNameOfStationBlock].dcp_block_length == 0x08) -assert(p[DCPNameOfStationBlock].block_info == 0x00) -assert(p[DCPNameOfStationBlock].name_of_station == b'device') +assert p[DCPNameOfStationBlock].option == 0x02 +assert p[DCPNameOfStationBlock].sub_option == 0x02 +assert p[DCPNameOfStationBlock].dcp_block_length == 0x08 +assert p[DCPNameOfStationBlock].block_info == 0x00 +assert p[DCPNameOfStationBlock].name_of_station == b'device' # - DCPDeviceIDBlock -assert(p[DCPDeviceIDBlock].option == 0x02) -assert(p[DCPDeviceIDBlock].sub_option == 0x03) -assert(p[DCPDeviceIDBlock].dcp_block_length == 0x06) -assert(p[DCPDeviceIDBlock].block_info == 0x00) -assert(p[DCPDeviceIDBlock].vendor_id == 0x002a) -assert(p[DCPDeviceIDBlock].device_id == 0x0313) +assert p[DCPDeviceIDBlock].option == 0x02 +assert p[DCPDeviceIDBlock].sub_option == 0x03 +assert p[DCPDeviceIDBlock].dcp_block_length == 0x06 +assert p[DCPDeviceIDBlock].block_info == 0x00 +assert p[DCPDeviceIDBlock].vendor_id == 0x002a +assert p[DCPDeviceIDBlock].device_id == 0x0313 # - DCPDeviceRoleBlock -assert(p[DCPDeviceRoleBlock].option == 0x02) -assert(p[DCPDeviceRoleBlock].sub_option == 0x04) -assert(p[DCPDeviceRoleBlock].dcp_block_length == 0x04) -assert(p[DCPDeviceRoleBlock].block_info == 0x00) -assert(p[DCPDeviceRoleBlock].device_role_details == 0x01) +assert p[DCPDeviceRoleBlock].option == 0x02 +assert p[DCPDeviceRoleBlock].sub_option == 0x04 +assert p[DCPDeviceRoleBlock].dcp_block_length == 0x04 +assert p[DCPDeviceRoleBlock].block_info == 0x00 +assert p[DCPDeviceRoleBlock].device_role_details == 0x01 # - DCPDeviceInstanceBlock -assert(p[DCPDeviceInstanceBlock].option == 0x02) -assert(p[DCPDeviceInstanceBlock].sub_option == 0x07) -assert(p[DCPDeviceInstanceBlock].dcp_block_length == 0x04) -assert(p[DCPDeviceInstanceBlock].block_info == 0x00) -assert(p[DCPDeviceInstanceBlock].device_instance_high == 0x00) -assert(p[DCPDeviceInstanceBlock].device_instance_low == 0x01) +assert p[DCPDeviceInstanceBlock].option == 0x02 +assert p[DCPDeviceInstanceBlock].sub_option == 0x07 +assert p[DCPDeviceInstanceBlock].dcp_block_length == 0x04 +assert p[DCPDeviceInstanceBlock].block_info == 0x00 +assert p[DCPDeviceInstanceBlock].device_instance_high == 0x00 +assert p[DCPDeviceInstanceBlock].device_instance_low == 0x01 # - DCPIPBlock -assert(p[DCPIPBlock].option == 0x01) -assert(p[DCPIPBlock].sub_option == 0x02) -assert(p[DCPIPBlock].dcp_block_length == 0x0e) -assert(p[DCPIPBlock].block_info == 0x01) -assert(p[DCPIPBlock].ip == "192.168.1.14") -assert(p[DCPIPBlock].netmask == "255.255.255.0") -assert(p[DCPIPBlock].gateway == "192.168.1.14") +assert p[DCPIPBlock].option == 0x01 +assert p[DCPIPBlock].sub_option == 0x02 +assert p[DCPIPBlock].dcp_block_length == 0x0e +assert p[DCPIPBlock].block_info == 0x01 +assert p[DCPIPBlock].ip == "192.168.1.14" +assert p[DCPIPBlock].netmask == "255.255.255.0" +assert p[DCPIPBlock].gateway == "192.168.1.14" + += DCP Identify Response parsing with new DCP packages (DCPOEMIDBlock, DCPDeviceInitiativeBlock) + +p = Ether(b'\x01\x0e\xcf\x00\x00\x00\x01\x23\x45\x67\x89\xab\x88\x92' \ + b'\xfe\xff\x05\x01\x01\x00\x00\x01\x00\x00\x00\x7a\x02\x02\x00\x02\x00' \ + b'\x00\x01\x02\x00\x0e\x00\x01\xc0\xa8\x01\x0b\xff\xff\xff\x00\x00\x00' \ + b'\x00\x00\x02\x03\x00\x06\x00\x00\x01\x6a\x04\x00\x02\x05\x00\x16\x00' \ + b'\x00\x01\x01\x01\x02\x02\x01\x02\x02\x02\x03\x02\x04\x02\x05\x02\x07' \ + b'\x02\x08\x06\x01\x02\x04\x00\x04\x00\x00\x01\x00\x06\x01\x00\x04\x00' \ + b'\x00\x00\x00\x02\x01\x00\x18\x00\x00\x31\x32\x33\x34\x20\x44\x44\x44' + b'\x20\x33\x58\x58\x32\x2d\x31\x32\x31\x2d\x30\x46\x44\x44\x02\x07\x00' \ + b'\x04\x00\x00\x00\x01\x02\x08\x00\x06\x00\x00\x01\x1e\xff\xff') + +# - General +assert p[Ether].dst == '01:0e:cf:00:00:00' +assert p[Ether].src == '01:23:45:67:89:ab' +assert p[Ether].type == 0x8892 +assert p[ProfinetIO].frameID == 0xFEFF +assert p[ProfinetDCP].service_id == 0x05 +assert p[ProfinetDCP].service_type == 0x01 +assert p[ProfinetDCP].xid == 0x1000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 122 +assert list(map(lambda x: type(x), p[ProfinetDCP].dcp_blocks)) == [DCPNameOfStationBlock, DCPIPBlock, DCPDeviceIDBlock, DCPDeviceOptionsBlock, DCPDeviceRoleBlock, DCPDeviceInitiativeBlock, DCPManufacturerSpecificBlock, DCPDeviceInstanceBlock, DCPOEMIDBlock] +# - DCPNameOfStationBlock +assert p[DCPNameOfStationBlock].option == 0x02 +assert p[DCPNameOfStationBlock].sub_option == 0x02 + +# - DCPIPBlock +assert p[DCPIPBlock].option == 0x01 +assert p[DCPIPBlock].sub_option == 0x02 +assert p[DCPIPBlock].dcp_block_length == 0x0E +assert p[DCPIPBlock].ip == '192.168.1.11' +assert p[DCPIPBlock].netmask == '255.255.255.0' +assert p[DCPIPBlock].gateway == '0.0.0.0' + +# - DCPDeviceInitiativeBlock +assert p[DCPDeviceInitiativeBlock].option == 0x06 +assert p[DCPDeviceInitiativeBlock].sub_option == 0x01 +assert p[DCPDeviceInitiativeBlock].dcp_block_length == 0x04 +assert p[DCPDeviceInitiativeBlock].device_initiative == 0x0000 + +# - DCPManufacturerSpecificBlock +assert p[DCPManufacturerSpecificBlock].option == 0x02 +assert p[DCPManufacturerSpecificBlock].sub_option == 0x01 +assert p[DCPManufacturerSpecificBlock].device_vendor_value == b'1234 DDD 3XX2-121-0FDD' + +# - DCPOEMIDBlock +assert p[DCPOEMIDBlock].option == 0x02 +assert p[DCPOEMIDBlock].sub_option == 0x08 +assert p[DCPOEMIDBlock].dcp_block_length == 0x06 +assert p[DCPOEMIDBlock].vendor_id == 0x011e +assert p[DCPOEMIDBlock].device_id == 0xffff = DCP Set Request parsing @@ -138,19 +191,44 @@ p = Ether(b'\x94\x65\x9c\x51\x90\x7d\xac\x64\x17\x21\x35\xcf\x81\x00\x00\x00' \ b'\x00\x03\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -assert(p[ProfinetIO].frameID == 0xfefd) -assert(p[ProfinetDCP].service_id == 0x04) -assert(p[ProfinetDCP].service_type == 0x01) -assert(p[ProfinetDCP].xid == 0x0000001) -assert(p[ProfinetDCP].reserved == 0x00) -assert(p[ProfinetDCP].dcp_data_length == 0x08) - -assert(p[DCPControlBlock].option == 0x05) -assert(p[DCPControlBlock].sub_option == 0x04) -assert(p[DCPControlBlock].dcp_block_length == 0x03) -assert(p[DCPControlBlock].response == 0x02) -assert(p[DCPControlBlock].response_sub_option == 0x02) -assert(p[DCPControlBlock].block_error == 0x00) +assert p[ProfinetIO].frameID == 0xfefd +assert p[ProfinetDCP].service_id == 0x04 +assert p[ProfinetDCP].service_type == 0x01 +assert p[ProfinetDCP].xid == 0x0000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 0x08 + +assert p[DCPControlBlock].option == 0x05 +assert p[DCPControlBlock].sub_option == 0x04 +assert p[DCPControlBlock].dcp_block_length == 0x03 +assert p[DCPControlBlock].response == 0x02 +assert p[DCPControlBlock].response_sub_option == 0x02 +assert p[DCPControlBlock].block_error == 0x00 + + += DCP Set Full IP Suite Request parsing + +p = Ether(b'\x12\x34\x00\x78\x90\xab\xc8\x5b\x76\xe6\x89\xdf' \ + b'\x88\x92\xfe\xfd\x04\x00\x00\x00\x00\x04\x00\x00' \ + b'\x00\x28\x01\x03\x00\x1e\x00\x00\xc0\xa8\x01\xab' \ + b'\xff\xff\xff\x00\xc0\xa8\x01\x01\x01\x02\x03\x04' \ + b'\x05\x06\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00') + +assert p[ProfinetIO].frameID == 0xfefd +assert p[ProfinetDCP].service_id == 0x04 +assert p[ProfinetDCP].service_type == 0x00 +assert p[ProfinetDCP].xid == 0x0000004 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 40 +assert p[ProfinetDCP].option == 0x01 +assert p[ProfinetDCP].sub_option == 0x03 +assert p[ProfinetDCP].ip == "192.168.1.171" +assert p[ProfinetDCP].netmask == "255.255.255.0" +assert p[ProfinetDCP].gateway == "192.168.1.1" +assert p[ProfinetDCP].dnsaddr[0] == "1.2.3.4" +assert p[ProfinetDCP].dnsaddr[1] == "5.6.7.8" +assert p[ProfinetDCP].dnsaddr[2] == "0.0.0.0" +assert p[ProfinetDCP].dnsaddr[3] == "0.0.0.0" = DCP Identify All Request crafting @@ -158,49 +236,67 @@ assert(p[DCPControlBlock].block_error == 0x00) # dcp_data_length cannot be calculated automatically at this time p = ProfinetIO(frameID=DCP_IDENTIFY_REQUEST_FRAME_ID) / ProfinetDCP(service_id=DCP_SERVICE_ID_IDENTIFY, service_type=DCP_REQUEST, option=255, sub_option=255, dcp_data_length=4) -assert(p[ProfinetIO].frameID == 0xfefe) -assert(p[ProfinetDCP].service_id == 0x05) -assert(p[ProfinetDCP].service_type == 0x00) -assert(p[ProfinetDCP].xid == 0x1000001) -assert(p[ProfinetDCP].reserved == 0x00) -assert(p[ProfinetDCP].dcp_data_length == 0x04) -assert(p[ProfinetDCP].option == 0xff) -assert(p[ProfinetDCP].sub_option == 0xff) -assert(p[ProfinetDCP].dcp_block_length == 0x00) +assert p[ProfinetIO].frameID == 0xfefe +assert p[ProfinetDCP].service_id == 0x05 +assert p[ProfinetDCP].service_type == 0x00 +assert p[ProfinetDCP].xid == 0x1000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 0x04 +assert p[ProfinetDCP].option == 0xff +assert p[ProfinetDCP].sub_option == 0xff +assert p[ProfinetDCP].dcp_block_length == 0x00 = DCP Set Name Request with specified name crafting p = ProfinetIO(frameID=DCP_GET_SET_FRAME_ID) / ProfinetDCP ( service_id=DCP_SERVICE_ID_SET, service_type=DCP_REQUEST, option=2, sub_option=2, name_of_station="device", dcp_block_length=8, dcp_data_length=12) -assert(p[ProfinetIO].frameID == 0xfefd) -assert(p[ProfinetDCP].service_id == 0x04) -assert(p[ProfinetDCP].service_type == 0x00) -assert(p[ProfinetDCP].xid == 0x1000001) -assert(p[ProfinetDCP].reserved == 0x00) -assert(p[ProfinetDCP].dcp_data_length == 0x0c) -assert(p[ProfinetDCP].option == 0x02) -assert(p[ProfinetDCP].sub_option == 0x02) -assert(p[ProfinetDCP].dcp_block_length == 0x08) -assert(p[ProfinetDCP].block_qualifier == 0x0001) -assert(p[ProfinetDCP].name_of_station == b'device') +assert p[ProfinetIO].frameID == 0xfefd +assert p[ProfinetDCP].service_id == 0x04 +assert p[ProfinetDCP].service_type == 0x00 +assert p[ProfinetDCP].xid == 0x1000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 0x0c +assert p[ProfinetDCP].option == 0x02 +assert p[ProfinetDCP].sub_option == 0x02 +assert p[ProfinetDCP].dcp_block_length == 0x08 +assert p[ProfinetDCP].block_qualifier == 0x0001 = DCP Identify Response crafting p = ProfinetIO(frameID=DCP_IDENTIFY_RESPONSE_FRAME_ID) / ProfinetDCP(service_id=DCP_SERVICE_ID_IDENTIFY, service_type=DCP_RESPONSE, dcp_data_length=12) / DCPNameOfStationBlock(name_of_station="device", dcp_block_length=8) -assert(p[ProfinetIO].frameID == 0xfeff) -assert(p[ProfinetDCP].service_id == 0x05) -assert(p[ProfinetDCP].service_type == 0x01) -assert(p[ProfinetDCP].xid == 0x1000001) -assert(p[ProfinetDCP].reserved == 0x00) -assert(p[ProfinetDCP].dcp_data_length == 0x0c) -assert(p[DCPNameOfStationBlock].option == 0x02) -assert(p[DCPNameOfStationBlock].sub_option == 0x02) -assert(p[DCPNameOfStationBlock].dcp_block_length == 0x08) -assert(p[DCPNameOfStationBlock].block_info == 0x00) -assert(p[DCPNameOfStationBlock].name_of_station == b'device') +assert p[ProfinetIO].frameID == 0xfeff +assert p[ProfinetDCP].service_id == 0x05 +assert p[ProfinetDCP].service_type == 0x01 +assert p[ProfinetDCP].xid == 0x1000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 0x0c +assert p[DCPNameOfStationBlock].option == 0x02 +assert p[DCPNameOfStationBlock].sub_option == 0x02 +assert p[DCPNameOfStationBlock].dcp_block_length == 0x08 +assert p[DCPNameOfStationBlock].block_info == 0x00 +assert p[DCPNameOfStationBlock].name_of_station == b'device' + + += DCP Set Full IP Suite Request crafting + +p = ProfinetIO(frameID=DCP_GET_SET_FRAME_ID) / ProfinetDCP(service_id=DCP_SERVICE_ID_SET, service_type=DCP_REQUEST, option=1, sub_option=3, ip='192.168.1.171', netmask='255.255.255.0', gateway='192.168.1.1', dnsaddr=['1.2.3.4', '5.6.7.8'], dcp_data_length=40, dcp_block_length=30) + +assert p[ProfinetIO].frameID == 0xfefd +assert p[ProfinetDCP].service_id == 0x04 +assert p[ProfinetDCP].service_type == 0x00 +assert p[ProfinetDCP].xid == 0x1000001 +assert p[ProfinetDCP].reserved == 0x00 +assert p[ProfinetDCP].dcp_data_length == 40 +assert p[ProfinetDCP].option == 0x01 +assert p[ProfinetDCP].sub_option == 0x03 +assert p[ProfinetDCP].ip == "192.168.1.171" +assert p[ProfinetDCP].netmask == "255.255.255.0" +assert p[ProfinetDCP].gateway == "192.168.1.1" +assert p[ProfinetDCP].dnsaddr[0] == "1.2.3.4" +assert p[ProfinetDCP].dnsaddr[1] == "5.6.7.8" conf.debug_dissector = old_conf_dissector diff --git a/test/contrib/pnio_rpc.uts b/test/contrib/pnio_rpc.uts index 38884be0db3..62e433d534e 100644 --- a/test/contrib/pnio_rpc.uts +++ b/test/contrib/pnio_rpc.uts @@ -2,14 +2,14 @@ + Syntax check = Import the PNIO RPC layer -from scapy.contrib.dce_rpc import * +from scapy.layers.dcerpc import * +from scapy.contrib.pnio import * from scapy.contrib.pnio_rpc import * -from scapy.modules.six import itervalues = Check that we have UUIDs -for v in itervalues(RPC_INTERFACE_UUID): - assert(isinstance(v, UUID)) +for v in RPC_INTERFACE_UUID.values(): + assert isinstance(v, UUID) + Check Block @@ -204,6 +204,76 @@ p == IODWriteMultipleRes(ARUUID='01234567-89ab-cdef-0123-456789abcdef', recordDa ]) / conf.padding_layer(b'\xef') +#################################################################### + ++ Check IODReadReq + += IODReadReq default values +bytes(IODReadReq()) == bytearray.fromhex('0009003c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') + += IODReadReq basic example +bytes(IODReadReq( + ARUUID='01234567-89ab-cdef-0123-456789abcdef', seqNum=1, API=1, slotNumber=2, subslotNumber=3, + index=0x4321) + ) == bytearray.fromhex('0009003c010000010123456789abcdef0123456789abcdef00000001000200030000432100000000000000000000000000000000000000000000000000000000') + += IODReadReq dissection +p = IODReadReq(bytearray.fromhex('0009003c010000010123456789abcdef0123456789abcdef00000001000200030000432100000002000000000000000000000000000000000000000000000000abcdef')) +p == IODReadReq(ARUUID='01234567-89ab-cdef-0123-456789abcdef', seqNum=1, API=1, slotNumber=2, subslotNumber=3, + index=0x4321, block_length=60, recordDataLength=2, padding='\0\0', RWPadding=b'\0'*24 + ) / b'\xab\xcd' / conf.padding_layer(b'\xef') + += IODReadReq response +p = p.get_response() +p == IODReadRes(ARUUID='01234567-89ab-cdef-0123-456789abcdef', seqNum=1, API=1, slotNumber=2, subslotNumber=3, + index=0x4321) + + +#################################################################### + ++ Check IODReadRes + += IODReadRes default values +bytes(IODReadRes()) == bytearray.fromhex('8009003c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') + += IODReadRes basic example +bytes(IODReadRes( + ARUUID='01234567-89ab-cdef-0123-456789abcdef', seqNum=1, API=1, slotNumber=2, subslotNumber=3, + index=0x4321)) == bytearray.fromhex('8009003c010000010123456789abcdef0123456789abcdef00000001000200030000432100000000000000000000000000000000000000000000000000000000') + += IODReadRes dissection + +p = IODReadRes(bytearray.fromhex('8009003c010000010123456789abcdef0123456789abcdef00000001000200030000432100000000000000000000000000000000000000000000000000000000ef')) +p == IODReadRes( + ARUUID='01234567-89ab-cdef-0123-456789abcdef', seqNum=1, API=1, slotNumber=2, subslotNumber=3, + index=0x4321, recordDataLength=0, block_length=60, padding=b'\0\0', RWPadding=b'\0'*20 + ) / conf.padding_layer(b'\xef') + + +#################################################################### +#################################################################### + ++ Check I&M + += IM0Block default values +raw(IM0Block()) == bytearray.fromhex('002000380100000000000000000000000000000000000000000000000000000000000000000000000000000000005600000000000000000001010000') + += IM0Block basic example +raw(IM0Block(OrderID='foobar', IMSerialNumber='ABCDEF1234567890')) == bytearray.fromhex('0020003801000000666f6f62617200000000000000000000000000004142434445463132333435363738393000005600000000000000000001010000') + += IM1Block default values +raw(IM1Block()) == bytearray.fromhex('002100380100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') + += IM2Block default values +raw(IM2Block()) == bytearray.fromhex('00220012010000000000000000000000000000000000') + += IM3Block default values +raw(IM3Block()) == bytearray.fromhex('002300380100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') + += IM4Block default values +raw(IM4Block()) == bytearray.fromhex('002400380100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') + + #################################################################### #################################################################### @@ -427,6 +497,38 @@ p == ExpectedSubmoduleBlockReq(block_length=38, ] ) / conf.padding_layer(b'\xef') + +#################################################################### +#################################################################### + ++ Check AlarmCRBlockReq + += AlarmCRBlockReq default values +bytes(AlarmCRBlockReq()) == bytearray.fromhex('010300160100000188920000000000010003000300c8c000a000') + += AlarmCRBlockReq with transport +bytes(AlarmCRBlockReq(AlarmCRProperties_Transport=1)) == bytearray.fromhex('010300160100000108004000000000010003000300c8c000a000') + += AlarmCRBlockReq dissection +p = AlarmCRBlockReq(bytearray.fromhex('010300160100000188920000000000010003000300c8c000a000')) +p[AlarmCRBlockReq].AlarmCRType == 0x0001 +p[AlarmCRBlockReq].LocalAlarmReference == 0x0003 + += AlarmCRBlockReq response +p = p.get_response() +p == AlarmCRBlockRes(AlarmCRType=0x0001, LocalAlarmReference=0x0003) + ++ Check AlarmCRBlockRes + += AlarmCRBlockRes default values +bytes(AlarmCRBlockRes()) == bytearray.fromhex('810300080100000100000000') + += AlarmCRBlockRes dissection +p = AlarmCRBlockRes(bytearray.fromhex('810300080100000100030000')) +p[AlarmCRBlockRes].AlarmCRType == 0x0001 +p[AlarmCRBlockRes].LocalAlarmReference == 0x0003 + + #################################################################### #################################################################### @@ -434,18 +536,15 @@ p == ExpectedSubmoduleBlockReq(block_length=38, = PNIOServiceReqPDU basic example * PNIOServiceReqPDU must always be placed above a DCE/RPC layer as it requires the endianness field -p = DceRpc() / PNIOServiceReqPDU(blocks=[ +p = DceRpc4() / PNIOServiceReqPDU(blocks=[ Block(load=b'\x01\x02') ]) s = bytes(p) # Remove the random UUID part before comparison -s[:18] + s[24:] == \ - bytearray.fromhex('0400000000000000dea000006c9711d18271' + 'dea000016c9711d1827100a02442df7d000000000000000000000000000000000000000000000001000000000000ffffffff001c00000000' + \ - '0000000800000008000000080000000000000008' + - '0000000401000102') +assert s[:18] + s[24:] == b'\x04\x00\x00\x00\x10\x00\x00\x00\x00\x00\xa0\xde\x97l\xd1\x11\x82q\x01\x00\xa0\xde\x97l\xd1\x11\x82q\x00\xa0$B\xdf}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x1c\x00\x00\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x04\x01\x00\x01\x02' = PNIOServiceReqPDU dissection -p = DceRpc( +p = DceRpc4( bytes(bytearray.fromhex('0400000000000000dea000006c9711d18271010203040506dea000016c9711d1827100a02442df7d000000000000000000000000000000000000000000000001000000000000ffffffff001c00000000' + \ '0000000f000000080000000f0000000000000008' + \ '0000000401000102ef') @@ -464,18 +563,15 @@ bytes(p.payload) == bytes(PNIOServiceReqPDU(args_length=8, args_max=15, max_coun = PNIOServiceResPDU basic example * PNIOServiceResPDU must always be placed above a DCE/RPC layer as it requires the endianness field -p = DceRpc() / PNIOServiceResPDU(blocks=[ +p = DceRpc4() / PNIOServiceResPDU(blocks=[ Block(load=b'\x01\x02') ]) s = bytes(p) # Remove the random UUID part before comparison -s[:18] + s[24:] == \ - bytearray.fromhex('0402000000000000dea000006c9711d18271' + 'dea000026c9711d1827100a02442df7d000000000000000000000000000000000000000000000001000000000000ffffffff001c00000000' + \ - '0000000000000008000000080000000000000008' + \ - '0000000401000102') +assert s[:18] + s[24:] == b'\x04\x02\x00\x00\x10\x00\x00\x00\x00\x00\xa0\xde\x97l\xd1\x11\x82q\x02\x00\xa0\xde\x97l\xd1\x11\x82q\x00\xa0$B\xdf}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x04\x01\x00\x01\x02' = PNIOServiceResPDU dissection -p = DceRpc( +p = DceRpc4( bytes(bytearray.fromhex('0402000000000000dea000006c9711d18271010203040506dea000026c9711d1827100a02442df7d000000000000000000000000000000000000000000000001000000000000ffffffff001c00000000' + \ '00001234000000080000000f0000000000000008' + \ '0000000401000102ef') @@ -494,10 +590,10 @@ bytes(p.payload) == bytes(PNIOServiceResPDU(status=0x1234, args_length=8, max_co #### Connect Request = A PNIO RPC Connect Request -p = DceRpc( - endianness='little', opnum=0, sequence_num=0, - object_uuid='dea00000-6c97-11d1-8271-010203040506', - activity='01234567-89ab-cdef-0123-456789abcdef' +p = DceRpc4( + endian='little', opnum=0, seqnum=0, + object='dea00000-6c97-11d1-8271-010203040506', + act_id='01234567-89ab-cdef-0123-456789abcdef' ) / PNIOServiceReqPDU( blocks=[ # AR block @@ -603,10 +699,10 @@ bytes(p) == bytearray.fromhex( #### Write Request = A PNIO RPC Write Request -p = DceRpc( - endianness='little', opnum=2, sequence_num=1, - object_uuid='dea00000-6c97-11d1-8271-010203040506', - activity='01234567-89ab-cdef-0123-456789abcdef' +p = DceRpc4( + endian='little', opnum=2, seqnum=1, + object='dea00000-6c97-11d1-8271-010203040506', + act_id='01234567-89ab-cdef-0123-456789abcdef' ) / PNIOServiceReqPDU( blocks=[ IODWriteMultipleReq( @@ -638,10 +734,10 @@ bytes(p) == bytearray.fromhex( #### PrmEnd control Request = A PNIO RPC PrmEnd Control Request -p = DceRpc( - endianness='little', opnum=0, sequence_num=2, - object_uuid='dea00000-6c97-11d1-8271-010203040506', - activity='01234567-89ab-cdef-0123-456789abcdef' +p = DceRpc4( + endian='little', opnum=0, seqnum=2, + object='dea00000-6c97-11d1-8271-010203040506', + act_id='01234567-89ab-cdef-0123-456789abcdef' ) / PNIOServiceReqPDU( blocks=[ IODControlReq(ARUUID='fedcba98-7654-3210-fedc-ba9876543210', SessionKey=0, ControlCommand_PrmEnd=1) @@ -654,11 +750,11 @@ bytes(p) == bytearray.fromhex( #### ApplicationReady control Request = A PNIO RPC PrmEnd Control Request -p = DceRpc( - endianness='little', opnum=0, sequence_num=0, - object_uuid='dea00000-6c97-11d1-8271-060504030201', - activity='01020304-0506-0708-090a-0b0c0d0e0f00', - interface_uuid=RPC_INTERFACE_UUID['UUID_IO_ControllerInterface'], +p = DceRpc4( + endian='little', opnum=0, seqnum=0, + object='dea00000-6c97-11d1-8271-060504030201', + act_id='01020304-0506-0708-090a-0b0c0d0e0f00', + if_id=RPC_INTERFACE_UUID['UUID_IO_ControllerInterface'], ) / PNIOServiceReqPDU( blocks=[ IODControlReq(ARUUID='fedcba98-7654-3210-fedc-ba9876543210', SessionKey=0, ControlCommand_ApplicationReady=1) @@ -668,3 +764,81 @@ bytes(p) == bytearray.fromhex( '04000000100000000000a0de976cd11182710605040302010200a0de976cd111827100a02442df7d0403020106050807090a0b0c0d0e0f000000000001000000000000000000ffffffff340000000000' + \ '2000000020000000200000000000000020000000' + \ '0112001c01000000fedcba9876543210fedcba98765432100000000000020000') + + +### PNIO Alarms + += PNIO Alarm decoding (Alarm_Low) + +p = Ether(b'\x00\x11\x22\x33\x44\x55' \ + b'\x00\x66\x77\x88\x99\xaa' \ + b'\x81\x00\xa0\x00' \ + b'\x88\x92' \ + b'\xfe\x01' \ + b'\x00\x03\x00\x03\x11\x11\xff\xff\xff\xfe\x00\x36' \ + b'\x00\x02\x00\x32\x01\x00\x00\x01\x00\x00\x00\x00\x00' \ + b'\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00' \ + b'\x81\x00\x0f\x00\x00\x08\x01\x00\x00\x00\x00\x00\x00\x02' \ + b'\x80\x02\x00\x0f\x2c\x00\x00\x05\x80\x00\x00\x00\x00\x22') +assert p[ProfinetIO].frameID == 0xfe01 +assert isinstance(p[ProfinetIO].payload, Alarm_Low) +assert p[AlarmNotification_Low].block_type == 0x0002 +assert isinstance(p[AlarmNotification_Low].AlarmPayload[0], MaintenanceItem) +assert p[MaintenanceItem].UserStructureIdentifier == 0x8100 +assert isinstance(p[AlarmNotification_Low].AlarmPayload[1], DiagnosisItem) +assert p[DiagnosisItem].UserStructureIdentifier == 0x8002 + += PNIO Alarm decoding (Alarm_High) +p = Ether(b'\x00\x11\x22\x33\x44\x55' \ + b'\x00\x66\x77\x88\x99\xaa' \ + b'\x81\x00\xa0\x00' \ + b'\x88\x92' \ + b'\xfc\x01' \ + b'\x00\x03\x00\x03\x11\x11\xff\xff\xff\xfe\x00\x36' \ + b'\x00\x02\x00\x32\x01\x00\x00\x01\x00\x00\x00\x00\x00' \ + b'\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00' \ + b'\x81\x00\x0f\x00\x00\x08\x01\x00\x00\x00\x00\x00\x00\x02' \ + b'\x80\x02\x00\x0f\x2c\x00\x00\x05\x80\x00\x00\x00\x00\x22') +assert p[ProfinetIO].frameID == 0xfc01 +assert isinstance(p[ProfinetIO].payload, Alarm_High) +assert p[AlarmNotification_High].block_type == 0x0002 +assert isinstance(p[AlarmNotification_High].AlarmPayload[0], MaintenanceItem) +assert p[MaintenanceItem].UserStructureIdentifier == 0x8100 +assert isinstance(p[AlarmNotification_High].AlarmPayload[1], DiagnosisItem) +assert p[DiagnosisItem].UserStructureIdentifier == 0x8002 + += PNIO Alarm DiagnosisItem + +p = AlarmNotification_Low(AlarmPayload=[MaintenanceItem(), DiagnosisItem()]) +assert raw(p) == bytearray.fromhex('0002002c0100000000000000000000000000000000000000000000000000000001000000000000000000000000000000') + += PNIO Alarm UploadRetrievalItem + +p = AlarmNotification_Low(AlarmPayload=[MaintenanceItem(), UploadRetrievalItem()]) +assert raw(p) == bytearray.fromhex('00020036010000000000000000000000000000000000000000000000000000000100000000000000000000000000010000000000000000000000') + += PNIO Alarm iParameterItem +p = AlarmNotification_Low(AlarmPayload=[MaintenanceItem(), iParameterItem()]) +assert raw(p) == bytearray.fromhex('0002003e0100000000000000000000000000000000000000000000000000000001000000000000000000000000000100000000000000000000000000000000000000') + += PNIO Alarm RS_AlarmItem +p = AlarmNotification_Low(AlarmPayload=[MaintenanceItem(), RS_AlarmItem()]) +assert raw(p) == bytearray.fromhex('0002002801000000000000000000000000000000000000000000000000000000010000000000000000000000') + += PNIO Alarm PRAL_AlarmItem +p = AlarmNotification_Low(AlarmPayload=[MaintenanceItem(), PRAL_AlarmItem()]) +assert raw(p) == bytearray.fromhex('0002002e01000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000') + += PNIO PDPortDataAdjust Decoding +raw = bytearray.fromhex('0402280000000000dea000006c9711d182710001000305f9dea000016' \ + 'c9711d1827100a02442df7d0777bc51ddaa4d07addb7075183fc28b00' \ + '00000000000001000000000002ffffffff007c0000000000000000000' \ + '000680000004000000000000000688009003c0100000002ba501cd47e' \ + '40d3a0b545fd4ac70eb900000000000080020000802f0000002800000' \ + '000000000000000000000000000000000000000000002020024010000' \ + '00000080020224000c010000000000000100000000021b00080100000' \ + '000010000') +p = DceRpc4(raw) +assert p[PDPortDataAdjust].subslotNumber == 0x8002 +assert p[AdjustPeerToPeerBoundary].peerToPeerBoundary == 0x1 +assert LINKSTATE_LINK[p[AdjustLinkState].LinkState] == 'Up' diff --git a/test/contrib/portmap.uts b/test/contrib/portmap.uts index dd8a26a2abc..4e58c1ecb1e 100644 --- a/test/contrib/portmap.uts +++ b/test/contrib/portmap.uts @@ -51,24 +51,9 @@ pkt = GETPORT_Reply( ) assert bytes(pkt) == b'\x00\x00\x08\x01' -pkt = DUMP_Reply( - value_follows=1, - mappings=[ - Map_Entry( - prog=1, - vers=2, - prot=3, - port=4, - value_follows=1 - ), - - Map_Entry( - prog=5, - vers=6, - prot=7, - port=8, - value_follows=0 - ) - ] -) +pkt = DUMP_Reply(value_follows=1, + mappings=[Map_Entry(prog=1, vers=2, prot=3, port=4, value_follows=1), + Map_Entry(prog=5, vers=6, prot=7, port=8, value_follows=0), + ] + ) assert bytes(pkt) == b'\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x07\x00\x00\x00\x08\x00\x00\x00\x00' diff --git a/test/contrib/postgres.uts b/test/contrib/postgres.uts new file mode 100644 index 00000000000..f6e0aae55f7 --- /dev/null +++ b/test/contrib/postgres.uts @@ -0,0 +1,307 @@ +# Postgres Related regression tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('postgres')" -t test/contrib/postgres.uts + ++ postgres + += postgres initialization + +from scapy.contrib.postgres import * + +ssl_request = "\x00\x00\x00\x08\x04\xd2\x16\x2f" + +startup = Startup( + b"\x00\x00\x00\x57\x00\x03\x00\x00\x75\x73\x65\x72\x00\x70\x6f\x73" + b"\x74\x67\x72\x65\x73\x00\x64\x61\x74\x61\x62\x61\x73\x65\x00\x70" + b"\x6f\x73\x74\x67\x72\x65\x73\x00\x61\x70\x70\x6c\x69\x63\x61\x74" + b"\x69\x6f\x6e\x5f\x6e\x61\x6d\x65\x00\x70\x73\x71\x6c\x00\x63\x6c" + b"\x69\x65\x6e\x74\x5f\x65\x6e\x63\x6f\x64\x69\x6e\x67\x00\x57\x49" + b"\x4e\x31\x32\x35\x32\x00\x00" +) + +assert startup.len == 87 +assert startup.protocol_version_major == 3 +assert startup.protocol_version_minor == 0 +assert ( + startup.options + == b"user\x00postgres\x00database\x00postgres\x00application_name\x00psql\x00client_encoding\x00WIN1252\x00\x00" +) + +init_packet = ( + b"\x52\x00\x00\x00\x08\x00\x00\x00\x00\x53\x00\x00\x00\x1a\x61\x70" + b"\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x5f\x6e\x61\x6d\x65\x00\x70" + b"\x73\x71\x6c\x00\x53\x00\x00\x00\x1c\x63\x6c\x69\x65\x6e\x74\x5f" + b"\x65\x6e\x63\x6f\x64\x69\x6e\x67\x00\x57\x49\x4e\x31\x32\x35\x32" + b"\x00\x53\x00\x00\x00\x17\x44\x61\x74\x65\x53\x74\x79\x6c\x65\x00" + b"\x49\x53\x4f\x2c\x20\x4d\x44\x59\x00\x53\x00\x00\x00\x26\x64\x65" + b"\x66\x61\x75\x6c\x74\x5f\x74\x72\x61\x6e\x73\x61\x63\x74\x69\x6f" + b"\x6e\x5f\x72\x65\x61\x64\x5f\x6f\x6e\x6c\x79\x00\x6f\x66\x66\x00" + b"\x53\x00\x00\x00\x17\x69\x6e\x5f\x68\x6f\x74\x5f\x73\x74\x61\x6e" + b"\x64\x62\x79\x00\x6f\x66\x66\x00\x53\x00\x00\x00\x19\x69\x6e\x74" + b"\x65\x67\x65\x72\x5f\x64\x61\x74\x65\x74\x69\x6d\x65\x73\x00\x6f" + b"\x6e\x00\x53\x00\x00\x00\x1b\x49\x6e\x74\x65\x72\x76\x61\x6c\x53" + b"\x74\x79\x6c\x65\x00\x70\x6f\x73\x74\x67\x72\x65\x73\x00\x53\x00" + b"\x00\x00\x14\x69\x73\x5f\x73\x75\x70\x65\x72\x75\x73\x65\x72\x00" + b"\x6f\x6e\x00\x53\x00\x00\x00\x19\x73\x65\x72\x76\x65\x72\x5f\x65" + b"\x6e\x63\x6f\x64\x69\x6e\x67\x00\x55\x54\x46\x38\x00\x53\x00\x00" + b"\x00\x32\x73\x65\x72\x76\x65\x72\x5f\x76\x65\x72\x73\x69\x6f\x6e" + b"\x00\x31\x34\x2e\x32\x20\x28\x44\x65\x62\x69\x61\x6e\x20\x31\x34" + b"\x2e\x32\x2d\x31\x2e\x70\x67\x64\x67\x31\x31\x30\x2b\x31\x29\x00" + b"\x53\x00\x00\x00\x23\x73\x65\x73\x73\x69\x6f\x6e\x5f\x61\x75\x74" + b"\x68\x6f\x72\x69\x7a\x61\x74\x69\x6f\x6e\x00\x70\x6f\x73\x74\x67" + b"\x72\x65\x73\x00\x53\x00\x00\x00\x23\x73\x74\x61\x6e\x64\x61\x72" + b"\x64\x5f\x63\x6f\x6e\x66\x6f\x72\x6d\x69\x6e\x67\x5f\x73\x74\x72" + b"\x69\x6e\x67\x73\x00\x6f\x6e\x00\x53\x00\x00\x00\x15\x54\x69\x6d" + b"\x65\x5a\x6f\x6e\x65\x00\x45\x74\x63\x2f\x55\x54\x43\x00\x4b\x00" + b"\x00\x00\x0c\x00\x00\x01\x7f\x43\x4c\x36\xa5\x5a\x00\x00\x00\x05\x49" +) + += postgres backend sequence + +init = PostgresBackend(init_packet) + +assert isinstance(init.contents[0], Authentication) +assert init.contents[0].len == 8 +assert init.contents[0].method == 0 +assert len(init.contents) == 16 +assert isinstance(init.contents[1], ParameterStatus) +assert init.contents[1].len == 26 +assert init.contents[1].parameter == b"application_name" +assert init.contents[1].value == b"psql" + += simple queries + +simple_query_packet = ( + b"\x51\x00\x00\x00\x15\x53\x45\x4c\x45\x43\x54\x20\x56\x45\x52\x53" + b"\x49\x4f\x4e\x28\x29\x00" +) +simple_query = PostgresFrontend(simple_query_packet) + +assert isinstance(simple_query.contents[0], Query) +assert simple_query.contents[0].len == 21 +assert simple_query.contents[0].query == b"SELECT VERSION()" + +pair = SignedIntStrPair(b"\x00\x00\x00\x04\x01\x02\x03\x04") + +assert pair.len == 4 +assert pair.data == b"\x01\x02\x03\x04" + +command_response_packet = ( + b"\x54\x00\x00\x00\x20\x00\x01\x76\x65\x72\x73\x69\x6f\x6e\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x19\xff\xff\xff\xff\xff\xff\x00" + b"\x00\x44\x00\x00\x00\x85\x00\x01\x00\x00\x00\x7b\x50\x6f\x73\x74" + b"\x67\x72\x65\x53\x51\x4c\x20\x31\x34\x2e\x32\x20\x28\x44\x65\x62" + b"\x69\x61\x6e\x20\x31\x34\x2e\x32\x2d\x31\x2e\x70\x67\x64\x67\x31" + b"\x31\x30\x2b\x31\x29\x20\x6f\x6e\x20\x78\x38\x36\x5f\x36\x34\x2d" + b"\x70\x63\x2d\x6c\x69\x6e\x75\x78\x2d\x67\x6e\x75\x2c\x20\x63\x6f" + b"\x6d\x70\x69\x6c\x65\x64\x20\x62\x79\x20\x67\x63\x63\x20\x28\x44" + b"\x65\x62\x69\x61\x6e\x20\x31\x30\x2e\x32\x2e\x31\x2d\x36\x29\x20" + b"\x31\x30\x2e\x32\x2e\x31\x20\x32\x30\x32\x31\x30\x31\x31\x30\x2c" + b"\x20\x36\x34\x2d\x62\x69\x74\x43\x00\x00\x00\x0d\x53\x45\x4c\x45" + b"\x43\x54\x20\x31\x00\x5a\x00\x00\x00\x05\x49" +) + += row data response + +command_response = PostgresBackend(command_response_packet) + +assert len(command_response.contents) == 4 +assert isinstance(command_response.contents[0], RowDescription) +rd = command_response.contents[0] +assert rd.len == 32 +assert rd.numfields == 1 +assert rd.cols[0].col == b"version" +assert rd.cols[0].tableoid == 0 +assert rd.cols[0].colno == 0 +assert rd.cols[0].typeoid == 25 +assert rd.cols[0].typelen == -1 +assert rd.cols[0].format == 0 +assert rd.cols[0].typemod == -1 + +assert isinstance(command_response.contents[1], DataRow) +assert command_response.contents[1].len == 133 +assert command_response.contents[1].numfields == 1 +assert len(command_response.contents[1].data) == 1 +assert isinstance(command_response.contents[1].data[0], SignedIntStrPair) +assert command_response.contents[1].data[0].len == 123 +assert ( + command_response.contents[1].data[0].data + == b"PostgreSQL 14.2 (Debian 14.2-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit" +) + +assert isinstance(command_response.contents[2], CommandComplete) +assert isinstance(command_response.contents[3], ReadyForQuery) + +three_col_rd = RowDescription( + b"\x54\x00\x00\x00\x55\x00\x03\x6e\x61\x6d\x65\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x19\xff\xff\xff\xff\xff\xff\x00\x00\x73\x65" + b"\x74\x74\x69\x6e\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19" + b"\xff\xff\xff\xff\xff\xff\x00\x00\x64\x65\x73\x63\x72\x69\x70\x74" + b"\x69\x6f\x6e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\xff\xff" + b"\xff\xff\xff\xff\x00\x00" +) +assert three_col_rd.len == 85 +assert three_col_rd.numfields == 3 +assert len(three_col_rd.cols) == 3 + +three_col_dr = DataRow( + b"\x44\x00\x00\x00\x63\x00\x03\x00\x00\x00\x17\x61\x6c\x6c\x6f\x77" + b"\x5f\x73\x79\x73\x74\x65\x6d\x5f\x74\x61\x62\x6c\x65\x5f\x6d\x6f" + b"\x64\x73\x00\x00\x00\x03\x6f\x66\x66\x00\x00\x00\x37\x41\x6c\x6c" + b"\x6f\x77\x73\x20\x6d\x6f\x64\x69\x66\x69\x63\x61\x74\x69\x6f\x6e" + b"\x73\x20\x6f\x66\x20\x74\x68\x65\x20\x73\x74\x72\x75\x63\x74\x75" + b"\x72\x65\x20\x6f\x66\x20\x73\x79\x73\x74\x65\x6d\x20\x74\x61\x62" + b"\x6c\x65\x73\x2e" +) + +assert three_col_dr.numfields == 3 +assert len(three_col_dr.data) == 3 +assert three_col_dr.data[0].len == 23 +assert three_col_dr.data[0].data == b"allow_system_table_mods" +assert three_col_dr.data[1].len == 3 +assert three_col_dr.data[1].data == b"off" +assert three_col_dr.data[2].len == 55 +assert ( + three_col_dr.data[2].data + == b"Allows modifications of the structure of system tables." +) + += errors + +error_response = ErrorResponse( + b"\x45\x00\x00\x00\x69\x53\x45\x52\x52\x4f\x52\x00\x56\x45\x52\x52" + b"\x4f\x52\x00\x43\x34\x32\x50\x30\x31\x00\x4d\x72\x65\x6c\x61\x74" + b"\x69\x6f\x6e\x20\x22\x66\x6f\x6f\x62\x61\x72\x22\x20\x64\x6f\x65" + b"\x73\x20\x6e\x6f\x74\x20\x65\x78\x69\x73\x74\x00\x50\x31\x35\x00" + b"\x46\x70\x61\x72\x73\x65\x5f\x72\x65\x6c\x61\x74\x69\x6f\x6e\x2e" + b"\x63\x00\x4c\x31\x33\x38\x31\x00\x52\x70\x61\x72\x73\x65\x72\x4f" + b"\x70\x65\x6e\x54\x61\x62\x6c\x65\x00\x00" +) + +assert len(error_response.error_fields) == 8 +assert error_response.error_fields[0] == ("Severity", b"ERROR") +assert error_response.error_fields[7] == ("Routine", b"parserOpenTable") + += copy data response and request + +copyin_response = CopyInResponse(b"\x47\x00\x00\x00\x0f\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00") + +assert copyin_response.len == 15 +assert copyin_response.format == 0 # Text +assert len(copyin_response.cols) == 4 +assert copyin_response.ncols == 4 +assert copyin_response.cols[0] == 0 # Text +assert copyin_response.cols[1] == 0 # Text +assert copyin_response.cols[2] == 0 # Text +assert copyin_response.cols[3] == 0 # Text + +copydata_in = PostgresFrontend(b"\x64\x00\x00\x00\x10\x31\x2c\x42\x6f\x62\x2c\x32\x33\x2c\x31\x0d" \ +b"\x0a\x64\x00\x00\x00\x12\x32\x2c\x53\x61\x6c\x6c\x79\x2c\x34\x33" \ +b"\x2c\x32\x0d\x0a\x64\x00\x00\x00\x14\x33\x2c\x50\x61\x72\x64\x65" \ +b"\x65\x70\x2c\x35\x34\x2c\x33\x0d\x0a\x64\x00\x00\x00\x0f\x34\x2c" \ +b"\x53\x75\x2c\x33\x32\x2c\x34\x0d\x0a\x64\x00\x00\x00\x0f\x35\x2c" \ +b"\x58\x69\x2c\x34\x33\x2c\x35\x0d\x0a\x64\x00\x00\x00\x0e\x36\x2c" \ +b"\x50\x69\x70\x2c\x36\x36\x2c\x36\x63\x00\x00\x00\x04" +) + +copyout_response = CopyOutResponse(b"\x48\x00\x00\x00\x0f\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00") +assert copyout_response.len == 15 + +# Combined message +copydata_out = PostgresBackend(b"\x48\x00\x00\x00\x0f\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00" \ +b"\x64\x00\x00\x00\x0f\x31\x09\x42\x6f\x62\x09\x32\x33\x09\x31\x0a" \ +b"\x64\x00\x00\x00\x11\x32\x09\x53\x61\x6c\x6c\x79\x09\x34\x33\x09" \ +b"\x32\x0a\x64\x00\x00\x00\x13\x33\x09\x50\x61\x72\x64\x65\x65\x70" \ +b"\x09\x35\x34\x09\x33\x0a\x64\x00\x00\x00\x0e\x34\x09\x53\x75\x09" \ +b"\x33\x32\x09\x34\x0a\x64\x00\x00\x00\x0e\x35\x09\x58\x69\x09\x34" \ +b"\x33\x09\x35\x0a\x64\x00\x00\x00\x0f\x36\x09\x50\x69\x70\x09\x36" \ +b"\x36\x09\x36\x0a\x63\x00\x00\x00\x04\x43\x00\x00\x00\x0b\x43\x4f" \ +b"\x50\x59\x20\x36\x00\x5a\x00\x00\x00\x05\x49") + +assert len(copydata_out.contents) == 10 +assert copydata_out.contents[0].len == 15 +assert isinstance(copydata_out.contents[0], CopyOutResponse) +assert isinstance(copydata_out.contents[1], CopyData) +assert copydata_out.contents[1].len == 15 +assert copydata_out.contents[1].data == b'1\tBob\t23\t1\n' +assert isinstance(copydata_out.contents[2], CopyData) +assert copydata_out.contents[2].data == b'2\tSally\t43\t2\n' +assert isinstance(copydata_out.contents[3], CopyData) +assert copydata_out.contents[3].data == b'3\tPardeep\t54\t3\n' +assert isinstance(copydata_out.contents[4], CopyData) +assert copydata_out.contents[4].data == b'4\tSu\t32\t4\n' +assert isinstance(copydata_out.contents[5], CopyData) +assert copydata_out.contents[5].data == b'5\tXi\t43\t5\n' +assert isinstance(copydata_out.contents[6], CopyData) +assert copydata_out.contents[6].data == b'6\tPip\t66\t6\n' +assert isinstance(copydata_out.contents[7], CopyDone) +assert isinstance(copydata_out.contents[8], CommandComplete) +assert isinstance(copydata_out.contents[9], ReadyForQuery) + += Check example request packet + +request = PostgresFrontend( + b"\x50\x00\x00\x00\x64\x00\x53\x45\x4c\x45\x43\x54\x20\x44\x5f\x4e" + b"\x45\x58\x54\x5f\x4f\x5f\x49\x44\x2c\x20\x44\x5f\x54\x41\x58\x20" + b"\x20\x20\x46\x52\x4f\x4d\x20\x64\x69\x73\x74\x72\x69\x63\x74\x20" + b"\x57\x48\x45\x52\x45\x20\x44\x5f\x57\x5f\x49\x44\x20\x3d\x20\x24" + b"\x31\x20\x41\x4e\x44\x20\x44\x5f\x49\x44\x20\x3d\x20\x24\x32\x20" + b"\x46\x4f\x52\x20\x55\x50\x44\x41\x54\x45\x00\x00\x02\x00\x00\x00" + b"\x17\x00\x00\x00\x17\x42\x00\x00\x00\x20\x00\x00\x00\x02\x00\x01" + b"\x00\x01\x00\x02\x00\x00\x00\x04\x00\x00\x00\x14\x00\x00\x00\x04" + b"\x00\x00\x00\x0a\x00\x00\x44\x00\x00\x00\x06\x50\x00\x45\x00\x00" + b"\x00\x09\x00\x00\x00\x00\x00\x53\x00\x00\x00\x04" +) + +assert len(request.contents) == 5 +assert isinstance(request.contents[0], Parse) +assert isinstance(request.contents[1], Bind) +assert isinstance(request.contents[2], Describe) +assert isinstance(request.contents[3], Execute) +assert isinstance(request.contents[4], Sync) + += Check parse decoding + +parse_msg = request.contents[0] +assert parse_msg.len == 100 +assert parse_msg.destination == b"" +assert parse_msg.query == b"SELECT D_NEXT_O_ID, D_TAX FROM district WHERE D_W_ID = $1 AND D_ID = $2 FOR UPDATE" +assert parse_msg.num_param_dtypes == 2 +assert parse_msg.params[0] == 23 +assert parse_msg.params[1] == 23 + += Check bind decoding + +bind_msg = request.contents[1] +assert bind_msg.len == 32 +assert bind_msg.destination == b"" +assert bind_msg.statement == b"" +assert bind_msg.codes_count == 2 +assert bind_msg.codes[0] == 1 +assert bind_msg.codes[1] == 1 +assert bind_msg.values_count == 2 +assert bind_msg.values[0].len == 4 +assert bind_msg.values[0].data == b"\x00\x00\x00\x14" +assert bind_msg.values[1].len == 4 +assert bind_msg.values[1].data == b"\x00\x00\x00\x0a" +assert bind_msg.results_count == 0 + += Check describe decoding + +describe_msg = request.contents[2] +assert describe_msg.len == 6 +assert describe_msg.close_type == b"P" +assert describe_msg.statement == b"" + += Check execute decoding + +exec_msg = request.contents[3] +assert exec_msg.len == 9 +assert exec_msg.portal == b"" +assert exec_msg.rows == 0 + += Check sync decoding + +sync_msg = request.contents[4] +assert sync_msg.len == 4 diff --git a/test/contrib/ppi_geotag.uts b/test/contrib/ppi_geotag.uts index e25e757b8be..b406823b95e 100644 --- a/test/contrib/ppi_geotag.uts +++ b/test/contrib/ppi_geotag.uts @@ -28,35 +28,3 @@ assert raw(PPI_Hdr()/PPI_Geotag_Antenna()) == b'5u\x08\x00\x02\x00\x08\x00\x00\x assert GPSTime_Field("GPSTime", None).delta == 0.0 -= Test UTCTimeField with time values - -# Always use ``time.gmtime`` and ``calendar.timegm``, not ``time.localtime`` -# and ``time.mktime``. - -local_time = time.gmtime() -utc_time = UTCTimeField("Test", None, epoch=local_time) -assert time.gmtime(utc_time.epoch) == local_time -assert calendar.timegm(time.gmtime(utc_time.delta)) == calendar.timegm(local_time) -strft_time = time.strftime("%a, %d %b %Y %H:%M:%S %z", local_time) - -# Backup: also test summer time bug -expected = "{} ({:d})".format(strft_time, utc_time.delta) -result = utc_time.i2repr(None, None) -result -expected -assert result == expected - -= Test LETimeField with time values - -local_time = time.gmtime() -lme_time = LETimeField("Test", None, epoch=local_time) -assert time.gmtime(lme_time.epoch) == local_time -assert calendar.timegm(time.gmtime(lme_time.delta)) == calendar.timegm(local_time) -strft_time = time.strftime("%a, %d %b %Y %H:%M:%S %z", local_time) - -# Backup: also test summer time bug -expected = "{} ({:d})".format(strft_time, lme_time.delta) -result = lme_time.i2repr(None, None) -result -expected -assert result == expected diff --git a/test/contrib/psp.uts b/test/contrib/psp.uts new file mode 100644 index 00000000000..8d28cd73936 --- /dev/null +++ b/test/contrib/psp.uts @@ -0,0 +1,86 @@ +# PSP unit tests +# run with: +# test/run_tests -P "load_contrib('psp')" -t test/contrib/psp.uts -F + +% Regression tests for the PSP layer + +############### +##### PSP ##### +############### + ++ PSP tests + += PSP layer + +example_plain_packet = import_hexcap('''\ +0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........ +0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|..... +0020 7F 00 00 01 04 D2 16 2E 00 11 A0 C4 41 41 41 41 ............AAAA +0030 41 41 41 41 41 AAAAA +''') +psp_packet = PSP(example_plain_packet) +assert psp_packet.nexthdr == 4 +assert psp_packet.hdrextlen == 1 +assert psp_packet.cryptoffset == 5 +assert psp_packet.version == 0 +assert psp_packet.spi == 0x11223344 +assert psp_packet.iv == b'\x01\x02\x03\x04\x05\x06\x07\x08' + +payload = IP(psp_packet.data) +assert payload[UDP].sport == 1234 +assert payload[UDP].dport == 5678 +assert bytes(payload[Raw]) == b"A" * 9 + += PSP Usage Example + +payload = IP() / UDP(sport=1234, dport=5678) / Raw("A" * 9) +iv = b'\x01\x02\x03\x04\x05\x06\x07\x08' +spi = 0x11223344 +key = b'\xFF\xEE\xDD\xCC\xBB\xAA\x99\x88\x77\x66\x55\x44\x33\x22\x11\x00' +psp_packet = PSP(nexthdr=4, cryptoffset=5, spi=spi, iv=iv, data=payload) +hexdump(psp_packet) +expected_orig_packet = import_hexcap(r'''\ +0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........ +0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|..... +0020 7F 00 00 01 04 D2 16 2E 00 11 A0 C4 41 41 41 41 ............AAAA +0030 41 41 41 41 41 AAAAA +''') +assert bytes(psp_packet) == bytes(expected_orig_packet) +# Now let's encrypt it +psp_packet.encrypt(key) +hexdump(psp_packet) +assert bytes(psp_packet) == import_hexcap(r'''\ +0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........ +0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|..... +0020 7F 00 00 01 8E 3E 2B 13 45 C7 6B F9 5C DA C3 9B .....>+.E.k.\... +0030 86 17 62 A0 CF DF FB BE BB C6 31 3A 2B 9D E0 64 ..b.......1:+..d +0040 75 9C DD 71 C9 u..q. +''') +# Now let's decrypt it back +psp_packet.decrypt(key) +hexdump(psp_packet) +assert bytes(psp_packet) == bytes(expected_orig_packet) + += PSP RFC Test - Version 0, no VC +key_128 = b'\x39\x46\xDA\x25\x54\xEA\xE4\x6A\xD1\xEF\x77\xA6\x43\x72\xED\xC4' +spi = 0x9A345678 +IV = b'\x00\x00\x00\x00\x00\x00\x00\x01' +plaintext_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_cleartext.pcap.gz"))[0] +encrypted_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_encrypt_transport_crypt_off_128.pcap.gz"))[0] +psp_packet = PSP(nexthdr=0x11, cryptoffset=1, spi=spi, iv=IV, data=plaintext_packet[UDP]) +psp_packet.encrypt(key_128) +assert bytes(psp_packet) == bytes(encrypted_packet[PSP]) + += PSP RFC Test - Version 1, no VC +key_256 = b'\xFA\x00\xF6\x09\xDF\x60\x20\x28\x9A\x1C\x93\xD6\x02\x70\x81\xA6\x37\xAD\x45\xB2\x4A\x55\x76\xB3\x6E\x6F\x49\xDD\x43\x11\x4D\x80' +# SPI and IV are the same as before +encrypted_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_encrypt_transport_crypt_off_256.pcap.gz"))[0] +psp_packet = PSP(nexthdr=0x11, cryptoffset=1, version=1, spi=spi, iv=IV, data=plaintext_packet[UDP]) +psp_packet.encrypt(key_256) +assert bytes(psp_packet) == bytes(encrypted_packet[PSP]) + += PSP RFC Test - Version 0, with VC +encrypted_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_encrypt_transport_crypt_off_128_vc.pcap.gz"))[0] +psp_packet = PSP(nexthdr=0x11, hdrextlen=2, cryptoffset=3, is_virt=1, spi=spi, iv=IV, data=plaintext_packet[UDP]) +psp_packet.encrypt(key_128) +assert bytes(psp_packet) == bytes(encrypted_packet[PSP]) diff --git a/test/contrib/ptp_v2.uts b/test/contrib/ptp_v2.uts new file mode 100644 index 00000000000..f06fbfb82d7 --- /dev/null +++ b/test/contrib/ptp_v2.uts @@ -0,0 +1,99 @@ +% PTP regression tests for Scapy + +# +# Type the following command to launch the tests: +# $ test/run_tests -P "load_contrib('ptp_v2')" -t test/contrib/ptp_v2.uts + ++ Basic tests + += specific haslayer and getlayer implementations for PTP +~ haslayer getlayer PTP +pkt = IP() / UDP() / PTP() +assert PTP in pkt +assert pkt.haslayer(PTP) +assert isinstance(pkt[PTP], PTP) +assert isinstance(pkt.getlayer(PTP), PTP) + ++ Packet dissection tests + += Sync packet dissection +s = b'\x10\x02\x00\x2c\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x00\x00\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0' +pkt = PTP(s) +assert pkt.transportSpecific == 1 +assert pkt.messageType == 0 +assert pkt.reserved1 == 0 +assert pkt.version == 2 +assert pkt.messageLength == 44 +assert pkt.domainNumber == 123 +assert pkt.reserved2 == 0 +assert pkt.flags == None +assert pkt.correctionField == 0 +assert pkt.reserved3 == 0 +assert pkt.clockIdentity == 0x8063ffff0009ba +assert pkt.portNumber == 1 +assert pkt.sequenceId == 116 +assert pkt.controlField == 0 +assert pkt.logMessageInterval == 0 +assert pkt.originTimestamp_seconds == 1169232218 +assert pkt.originTimestamp_nanoseconds == 174389936 + += Delay_Req packet dissection +s= b'\x11\x02\x00\x2c\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x01\x00\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0' +pkt = PTP(s) +assert pkt.messageType == 0x1 +assert pkt.controlField == 0x1 + += Pdelay_Req packet dissection +s= b'\x12\x02\x00\x2c\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x05\x00\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0' +pkt = PTP(s) +assert pkt.messageType == 0x2 +assert pkt.controlField == 0x5 + += Pdelay_Resp packet dissection +s= b'\x13\x02\x00\x2c\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x05\x00\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0' +pkt = PTP(s) +assert pkt.messageType == 0x3 +assert pkt.controlField == 0x5 +assert pkt.requestReceiptTimestamp_seconds == 1169232218 +assert pkt.requestReceiptTimestamp_nanoseconds == 174389936 + += Follow_Up packet dissection +s= b'\x18\x02\x00\x2c\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x02\x00\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0' +pkt = PTP(s) +assert pkt.messageType == 0x8 +assert pkt.controlField == 0x2 +assert pkt.preciseOriginTimestamp_seconds == 1169232218 +assert pkt.preciseOriginTimestamp_nanoseconds == 174389936 + += Delay_Resp packet dissection +s= b'\x19\x02\x00\x2c\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x03\x00\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0' +pkt = PTP(s) +assert pkt.messageType == 0x9 +assert pkt.controlField == 0x3 +assert pkt.receiveTimestamp_seconds == 1169232218 +assert pkt.receiveTimestamp_nanoseconds == 174389936 + += Pdelay_Resp_Follow packet dissection +s= b'\x1A\x02\x00\x2c\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x05\x00\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0' +pkt = PTP(s) +assert pkt.messageType == 0xA +assert pkt.controlField == 0x5 +assert pkt.responseOriginTimestamp_seconds == 1169232218 +assert pkt.responseOriginTimestamp_nanoseconds == 174389936 + += Announce packet dissection +s= b'\x1b\x02\x00\x40\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\x00\x01\x00\x74\x05\x01\x00\x00\x45\xb1\x11\x5a\x0a\x64\xfa\xb0\x00\x00\x00\x60\x00\x00\x00\x80\x63\xff\xff\x00\x09\xba\xf8\x21\x00\x00\x80\x80' +pkt = PTP(s) +assert pkt.messageType == 0xB +assert pkt.messageLength == 64 +assert pkt.controlField == 0x5 +assert pkt.currentUtcOffset == 0 +assert pkt.reserved4 == 0 +assert pkt.grandmasterPriority1 == 96 +assert pkt.grandmasterClockClass == 0 +assert pkt.grandmasterClockAccuracy == 0x0 +assert pkt.grandmasterClockVariance == 128 +assert pkt.grandmasterPriority2 == 99 +assert pkt.grandmasterIdentity == 0xffff0009baf82100 +assert pkt.stepsRemoved == 128 +assert pkt.timeSource == 0x80 diff --git a/test/contrib/roce.uts b/test/contrib/roce.uts new file mode 100644 index 00000000000..ff19affec3c --- /dev/null +++ b/test/contrib/roce.uts @@ -0,0 +1,147 @@ +# RoCE unit tests +# run with: +# test/run_tests -P "load_contrib('roce')" -t test/contrib/roce.uts -F + +% Regression tests for the RoCE layer + +################ +##### RoCE ##### +################ + ++ RoCE tests + += RoCE layer + +# an example UC packet +pkt = Ether(dst='24:8a:07:a8:fa:22', src='24:8a:07:a8:fa:22')/ \ + IP(version=4, ihl=5, tos=0x1, id=1144, flags='DF', frag=0, \ + ttl=64, src='192.168.0.7', dst='192.168.0.7', len=64)/ \ + UDP(sport=49152, dport=4791, len=44)/ \ + BTH(opcode='UC_SEND_ONLY', migreq=1, padcount=2, pkey=0xffff, dqpn=211, psn=13571856)/ \ + Raw(b'F0\x81\x8b\xe2\x895\xd9\x0e\x9a\x95PT\x01\xbe\x88^P\x00\x00') + +# include ICRC placeholder +pkt = Ether(pkt.build() + b'\x00' * 4) + +assert IP in pkt.layers() +print(hex(pkt[IP].chksum)) +assert pkt[IP].chksum == 0xb4d5 +assert UDP in pkt.layers() +print(hex(pkt[UDP].chksum)) +assert pkt[UDP].chksum == 0xaca2 +assert BTH in pkt.layers() +assert pkt[BTH].icrc == 0x78f353f3 + += RoCE CNP packet + +# based on this example packet: +# https://community.mellanox.com/s/article/rocev2-cnp-packet-format-example + +pkt = Ether()/IP(src='22.22.22.8', dst='22.22.22.7', id=0x98c6, flags='DF', + ttl=0x20, tos=0x89)/ \ + UDP(sport=56238, dport=4791, chksum=0)/ \ + cnp(dqpn=0xd2) +pkt = Ether(pkt.build()) + +assert pkt[IP].len == 60 +assert pkt[UDP].len == 40 +assert pkt[BTH].opcode == 0x81 +assert pkt[BTH].becn +assert not pkt[BTH].fecn +assert pkt[BTH].resv6 == 0 +assert pkt[BTH].resv7 == 0 +assert pkt[BTH].dqpn == 0xd2 +assert pkt[BTH].version == 0 +assert not pkt[BTH].solicited +assert not pkt[BTH].migreq +assert pkt[BTH].padcount == 0 +assert pkt[BTH].pkey == 0xffff +assert not pkt[BTH].ackreq +assert pkt[BTH].psn == 0 +assert pkt[CNPPadding].reserved1 == 0 +assert pkt[CNPPadding].reserved2 == 0 +# assert pkt[BTH].icrc == 0xe42dad81 TODO - does not match example + += RoCE CNP captured on ConnectX-4 Lx + +pkt = Ether(import_hexcap('''0x0000: e41d 2dab 2bc2 7cfe 9064 3b32 0800 45c2 +0x0010: 003c 718c 4000 4011 9161 0a00 1101 0a00 +0x0020: 1201 0000 12b7 0028 0000 8100 ffff 4000 +0x0030: 0118 0000 0000 0000 0000 0000 0000 0000 +0x0040: 0000 0000 0000 82fd 002a +''')) + +assert BTH in pkt.layers() +assert pkt.opcode == CNP_OPCODE +del pkt.icrc +pkt = Ether(pkt.build()) +assert pkt.icrc == 0x82fd002a + += RoCE v1 RC RDMA WRITE ONLY + +pkt = Ether(import_hexcap('''\ +0x0000 7c fe 90 75 3c d8 7c fe 90 75 3c d8 89 15 60 20 +0x0010 00 00 00 28 1b 40 00 00 00 00 00 00 00 00 00 00 +0x0020 ff ff 0f 00 00 02 00 00 00 00 00 00 00 00 00 00 +0x0030 ff ff 0f 00 00 02 0a 70 ff ff 00 00 01 0a 80 a7 +0x0040 88 bc 00 00 55 d4 c0 72 60 00 00 00 47 b3 00 00 +0x0050 00 05 00 00 00 00 01 00 00 00 e3 d8 56 bb +''')) + +assert GRH in pkt.layers() +assert BTH in pkt.layers() +assert pkt[GRH].ipver == 6 +assert pkt[GRH].tclass == 2 +assert pkt[GRH].flowlabel == 0 +assert pkt[GRH].paylen == 40 +assert pkt[BTH].opcode == 0xa +assert pkt[BTH].padcount == 3 +assert pkt[BTH].dqpn == 0x10a +assert pkt[BTH].ackreq +assert pkt.icrc == 0xe3d856bb + += RoCE v1 RC ACKNOWLEDGE + +pkt = Ether(import_hexcap('''\ +0000 7c fe 90 75 3c d8 7c fe 90 75 3c d8 89 15 60 20 +0010 00 00 00 14 1b 40 00 00 00 00 00 00 00 00 00 00 +0020 ff ff 0f 00 00 02 00 00 00 00 00 00 00 00 00 00 +0030 ff ff 0f 00 00 02 11 40 ff ff 00 00 01 09 00 a7 +0040 88 c0 00 00 00 05 25 f0 c0 38 +''')) + +assert GRH in pkt.layers() +assert BTH in pkt.layers() +assert AETH in pkt.layers() +assert pkt[GRH].ipver == 6 +assert pkt[GRH].tclass == 2 +assert pkt[GRH].flowlabel == 0 +assert pkt[GRH].paylen == 20 +assert pkt[BTH].opcode == 0x11 +assert pkt[BTH].padcount == 0 +assert pkt[BTH].dqpn == 0x109 +assert not pkt[BTH].ackreq +assert pkt[AETH].syndrome == 0 +assert pkt[AETH].msn == 5 +assert pkt.icrc == 0x25f0c038 + += RoCE over IPv6 + +# an example UC packet +pkt = Ether(dst='24:8a:07:a8:fa:22', src='24:8a:07:a8:fa:22')/ \ + IPv6(nh=17,src='2022::1023', dst='2023::1024', \ + version=6,hlim=255,plen=44,fl=0x1face,tc=226)/ \ + UDP(sport=49152, dport=4791, len=44)/ \ + BTH(opcode='UC_SEND_ONLY', migreq=1, padcount=2, pkey=0xffff, dqpn=211, psn=13571856)/ \ + Raw(b'F0\x81\x8b\xe2\x895\xd9\x0e\x9a\x95PT\x01\xbe\x88^P\x00\x00') + +# include ICRC placeholder +pkt = Ether(pkt.build() + b'\x00' * 4) + +assert IPv6 in pkt.layers() +assert UDP in pkt.layers() +print(hex(pkt[UDP].chksum)) +assert pkt[UDP].chksum == 0xe7c5 +assert BTH in pkt.layers() +print(hex(pkt[BTH].icrc)) +assert pkt[BTH].icrc == 0x3e5b743b diff --git a/test/contrib/rpl.uts b/test/contrib/rpl.uts new file mode 100644 index 00000000000..e2d678dfdf8 --- /dev/null +++ b/test/contrib/rpl.uts @@ -0,0 +1,114 @@ +% RPL layer test campaign + ++ Syntax check += Import the RPL layer +load_contrib("rpl") +load_contrib("rpl_metrics") + ++ Test RPL Control Messages += RPL Base Objects construction +assert raw(ICMPv6RPL()/RPLDIS()) == b'\x9b\x00\x00\x00\x00\x00' +assert raw(ICMPv6RPL()/RPLDIO()) == b'\x9b\x01\x00\x00\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' +assert raw(ICMPv6RPL()/RPLDAO()) == b'\x9b\x02\x00\x00\x32\x00\x00\x01' +assert raw(ICMPv6RPL()/RPLDAOACK()) == b'\x9b\x03\x00\x00\x32\x00\x01\x00' +assert raw(ICMPv6RPL()/RPLDCO()) == b'\x9b\x07\x00\x00\x32\x00\x00\x01' +assert raw(ICMPv6RPL()/RPLDCOACK()) == b'\x9b\x08\x00\x00\x32\x00\x01\x00' +p=raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDCOACK()/RPLOptPadN(optdata='0'*10)) +assert p == b'\x60\x00\x00\x00\x00\x14\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x08\x42\x0f\x32\x00\x01\x00\x01\x0a\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDCO()/RPLOptTgt(prefix="fd00::1", plen=128)) +assert p == b'\x60\x00\x00\x00\x00\x1c\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x07\x32\x6e\x32\x00\x00\x01\x05\x12\x00\x80\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptRIO(plen=64, prefix="fd00::1")) +assert p == b'\x60\x00\x00\x00\x00\x34\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\x6b\xe6\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x16\x40\x00\xff\xff\xff\xff\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO(dodagid="aaaa::1")/RPLOptDODAGConfig()/RPLOptDAGMC()/RPLDAGMCLinkETX()) +assert p == b'\x60\x00\x00\x00\x00\x34\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xef\x1e\x32\x00\x00\x01\x88\xf0\x00\x00\xaa\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x04\x0e\x00\x14\x03\x0a\x00\x00\x01\x00\x00\x01\x00\xff\xff\xff\x02\x06\x07\x00\x00\x02\x00\x01' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO(dodagid="aaaa::1")/RPLOptPIO(plen=64, prefix="fd00::1")) +assert p == b'\x60\x00\x00\x00\x00\x3c\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xbc\x2b\x32\x00\x00\x01\x88\xf0\x00\x00\xaa\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x08\x1e\x40\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' + +p = raw(IPv6(src="fe80::1", dst="fe80::2")/ICMPv6RPL()/RPLDAO()/RPLOptTgtDesc()) +assert p == b'\x60\x00\x00\x00\x00\x0e\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x9b\x02\x2c\xab\x32\x00\x00\x01\x09\x04\x00\x00\x00\x00' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCNSA()) +assert p == b'\x60\x00\x00\x00\x00\x24\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xa9\x06\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x06\x01\x00\x00\x02\x00\x00' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCNodeEnergy()) +assert p == b'\x60\x00\x00\x00\x00\x24\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xa8\x06\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x06\x02\x00\x00\x02\x00\x00' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCHopCount()) +assert p == b'\x60\x00\x00\x00\x00\x24\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xa7\x05\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x06\x03\x00\x00\x02\x00\x01' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCLinkThroughput()) +assert p == b'\x60\x00\x00\x00\x00\x26\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xa5\xff\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x08\x04\x00\x00\x04\x00\x00\x00\x01' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCLinkColor()) +assert p == b'\x60\x00\x00\x00\x00\x25\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\x61\x03\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x07\x08\x00\x00\x03\x00\x00\x41' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCLinkLatency()) +assert p == b'\x60\x00\x00\x00\x00\x26\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xa4\xff\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x08\x05\x00\x00\x04\x00\x00\x00\x01' + +p = raw(IPv6(src="fe80::1")/ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCLinkQualityLevel()) +assert p == b'\x60\x00\x00\x00\x00\x24\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xa4\x06\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x06\x06\x00\x00\x02\x00\x00' + + += RPL Base Objects dissection +# Test DIS dissection +p = ICMPv6RPL(b'\x9b\x00\x00\x00\x00\x00') +assert p.code == 0 + +# Test DIO dissection +p = ICMPv6RPL(b'\x9b\x01\x00\x00\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') +assert p.code == 1 +assert p.RPLInstanceID == 50 +assert p.ver == 0 +assert p.rank == 1 +assert p.G == 1 +assert p.mop == 1 +assert p.dtsn == 240 +assert p.dodagid == "::1" + +# Test DAO dissection +p = ICMPv6RPL(b'\x9b\x02\x00\x00\x32\x00\x00\x01') +assert p.code == 2 + ++ Test RPL Control Message Options += RPL Control Options construction +# DIS +assert raw(ICMPv6RPL()/RPLDIS()/RPLOptPad1()) == b'\x9b\x00\x00\x00\x00\x00\x00' + +# DIS with solicited info option +assert raw(ICMPv6RPL()/RPLDIS()/RPLOptSolInfo()) == b'\x9b\x00\x00\x00\x00\x00\x07\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00' + +# DIO with DAG MC option with link ETX metric +assert raw(ICMPv6RPL()/RPLDIO()/RPLOptDAGMC()/RPLDAGMCLinkETX()) == b'\x9b\x01\x00\x00\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x06\x07\x00\x00\x02\x00\x01' + +# Normal DAO message with single target, since transit +assert raw(IPv6(src="fe80::1", dst="fe80::2")/\ + ICMPv6RPL()/RPLDAO()/\ + RPLOptTgt(plen=128,prefix="fd00::1")/\ + RPLOptTIO()) == \ + b'\x60\x00\x00\x00\x00\x22\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x9b\x02\x2c\x04\x32\x00\x00\x01\x05\x12\x00\x80\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x04\x00\x00\x00\xff' + +assert raw(ICMPv6RPL()/RPLDAO(D=1, dodagid="fd00::1")/RPLOptDAGMC()) == \ + b'\x9b\x02\x00\x00\x32\x40\x00\x01\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00' + +p=IPv6(b'\x60\x00\x00\x00\x00\x1c\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x0f\x86\xcc\x88\xaf\xfa\xbe\x25\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x07\xe8\x3f\x32\x00\x00\x01\x05\x12\x00\x80\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') +assert p.payload.code == 7 # Its a DCO + +p=IPv6(b'\x60\x00\x00\x00\x00\x2c\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x9b\x02\x35\xbb\x32\x40\x00\x01\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x05\x12\x00\x80\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') +p.show() +assert p.payload.code == 2 # Its a DAO + +p=IPv6(b'\x60\x00\x00\x00\x00\x24\x3a\x40\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x9b\x01\xa3\x05\x32\x00\x00\x01\x88\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x06\x07\x00\x00\x02\x00\x01') +#p.show() +rpl=p.payload +assert rpl.code == 1 +dio=rpl.payload +assert dio.RPLInstanceID == 50 +assert dio.dtsn == 240 +dagmc=dio.payload +assert dagmc.len == 6 +mc=dagmc.options[0] +assert mc.ETX == 1 diff --git a/test/contrib/rtcp.uts b/test/contrib/rtcp.uts new file mode 100644 index 00000000000..0686016edf1 --- /dev/null +++ b/test/contrib/rtcp.uts @@ -0,0 +1,98 @@ +# RTCP unit tests +# run with: +# test/run_tests -P "load_contrib('rtcp')" -t test/contrib/rtcp.uts -F + +% RTCP regression tests for Scapy + +############ +# RTCP +############ + ++ RTCP Sender Report tests + += test sender report parse + +raw = b'\x80\xc8\x00\x06\x9c\xe9\xc6\x48\xe5\x61\xe4\x4b\x63\x8a\x19\xc9\x98\x64\xea\x2e\x00\x00\x00\x49\x00\x00\x09\x69' +parsed = RTCP(raw) +assert parsed.version == 2 +assert parsed.padding == 0 +assert parsed.count == 0 +assert parsed.packet_type == 200 +assert parsed.length == 6 +assert parsed.sourcesync == 0x9ce9c648 +assert parsed.sender_info.ntp_timestamp == 0xe561e44b638a19c9 +assert parsed.sender_info.rtp_timestamp == 2556750382 +assert parsed.sender_info.sender_packet_count == 73 +assert parsed.sender_info.sender_octet_count == 2409 + ++ RTCP Receiver Report tests + += test receiver report parse + +raw = b'\x81\xc9\x00\x07\xa2\xdf\x02\x72\x49\x6e\x93\xbd\x00\xff\xff\xff\x00\x00\x59\x47\x00\x00\x00\x00\xe4\x8f\xb9\x3a\x00\x03\x3f\x1b' +parsed = RTCP(raw) +assert parsed.version == 2 +assert parsed.padding == 0 +assert parsed.count == 1 +assert parsed.packet_type == 201 +assert parsed.length == 7 +assert parsed.sourcesync == 0xa2df0272 +assert parsed.report_blocks[0].sourcesync == 0x496e93bd +assert parsed.report_blocks[0].fraction_lost == 0 +assert parsed.report_blocks[0].cumulative_lost == 0xffffff +assert parsed.report_blocks[0].highest_seqnum_recv == 22855 +assert parsed.report_blocks[0].interarrival_jitter == 0 +assert parsed.report_blocks[0].last_SR_timestamp == 0xe48fb93a +assert parsed.report_blocks[0].delay_since_last_SR == 212763 + ++ RTCP Source Description tests + += test source description report parse + +raw = b"\x81\xca\x00\x0c\xa2\xdf\x02\x72\x01\x1c\x75\x73\x65\x72\x31\x35" \ + b"\x30\x33\x34\x38\x38\x39\x30\x31\x40\x68\x6f\x73\x74\x2d\x65\x37" \ + b"\x32\x64\x62\x34\x33\x64\x06\x09\x47\x53\x74\x72\x65\x61\x6d\x65" \ + b"\x72\x00\x00\x00" +parsed = RTCP(raw) +assert parsed.version == 2 +assert parsed.padding == 0 +assert parsed.count == 1 +assert parsed.packet_type == 202 +assert parsed.length == 12 +assert parsed.sdes_chunks[0].sourcesync == 0xa2df0272 +assert parsed.sdes_chunks[0].items[0].chunk_type == 1 +assert parsed.sdes_chunks[0].items[0].length == 28 +assert parsed.sdes_chunks[0].items[0].value == b'user1503488901@host-e72db43d' +assert parsed.sdes_chunks[0].items[1].chunk_type == 6 +assert parsed.sdes_chunks[0].items[1].length == 9 +assert parsed.sdes_chunks[0].items[1].value == b'GStreamer' + ++ RTCP parsing tests + += test parse SR and SDES stacked +raw = b"\x81\xc9\x00\x07\xa2\xdf\x02\x72\x49\x6e\x93\xbd\x00\xff\xff\xff" \ + b"\x00\x00\x59\x47\x00\x00\x00\x00\xe4\x8f\xb9\x3a\x00\x03\x3f\x1b" \ + b"\x81\xca\x00\x0c\xa2\xdf\x02\x72\x01\x1c\x75\x73\x65\x72\x31\x35" \ + b"\x30\x33\x34\x38\x38\x39\x30\x31\x40\x68\x6f\x73\x74\x2d\x65\x37" \ + b"\x32\x64\x62\x34\x33\x64\x06\x09\x47\x53\x74\x72\x65\x61\x6d\x65" \ + b"\x72\x00\x00\x00" + += format SR + 2xRR and parse back + +rtcp = RTCP() +rtcp.packet_type = 200 +rtcp.sourcesync = 0x01010101 +rtcp.sender_info.rtp_timestamp = 0x03030303 +rtcp.count = 2 +rtcp.report_blocks.append(ReceptionReport(sourcesync=0x04040404)) +rtcp.report_blocks.append(ReceptionReport(sourcesync=0x05050505)) +b = bytes(rtcp) +rtcp2 = RTCP(b) +assert rtcp2.count == 2 +assert rtcp2.length == 18 +assert rtcp2.sourcesync == 0x01010101 +assert rtcp2.sender_info.rtp_timestamp == 0x03030303 +assert len(rtcp2.sender_info.payload) == 0 +assert rtcp2.report_blocks[0].sourcesync == 0x04040404 +assert len(rtcp2.report_blocks[0].payload) == 0 +assert rtcp2.report_blocks[1].sourcesync == 0x05050505 diff --git a/test/contrib/rtps.uts b/test/contrib/rtps.uts new file mode 100644 index 00000000000..806a5c64132 --- /dev/null +++ b/test/contrib/rtps.uts @@ -0,0 +1,580 @@ +% Real-Time Publish-Subscribe Protocol (RTPS) dissection +% +% Copyright (C) 2021 Trend Micro Incorporated +% Copyright (C) 2021 Alias Robotics S.L. +% +% This program is free software; you can redistribute it and/or modify it under +% the terms of the GNU General Public License as published by the Free Software +% Foundation; either version 2 of the License, or (at your option) any later +% version. +% +% This program is distributed in the hope that it will be useful, but WITHOUT ANY +% WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +% PARTICULAR PURPOSE. See the GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License along with +% this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +% Street, Fifth Floor, Boston, MA 02110-1301, USA. + +% RTPS layer test campaign + ++ Syntax check += Import the RTPS layer +from scapy.contrib.rtps import * +pkt = b"\x52\x54\x50\x53\x02\x01\x01\x10\x57\x63\x10\x01\xd6\xab\x40\x7f" \ + b"\x5b\xd9\xbb\x1c\x0e\x01\x0c\x00\x88\x2a\x10\x01\x5d\x8c\x97\x40" \ + b"\x78\xb6\x2d\xc2\x09\x01\x08\x00\xf4\x50\x81\x60\x51\xdd\x5c\x1c" \ + b"\x15\x05\x10\x01\x00\x00\x10\x00\x00\x01\x00\xc7\x00\x01\x00\xc2" \ + b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x03\x00\x00\x15\x00\x04\x00" \ + b"\x02\x01\x00\x00\x16\x00\x04\x00\x01\x10\x00\x00\x02\x00\x08\x00" \ + b"\x0a\x00\x00\x00\x00\x00\x00\x00\x50\x00\x10\x00\x57\x63\x10\x01" \ + b"\xd6\xab\x40\x7f\x5b\xd9\xbb\x1c\x00\x00\x01\xc1\x58\x00\x04\x00" \ + b"\x3f\x0c\x00\x00\x0f\x00\x04\x00\x00\x00\x00\x00\x31\x00\x18\x00" \ + b"\x01\x00\x00\x00\xbd\xeb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xac\x11\x00\x02\x48\x00\x18\x00\x01\x00\x00\x00" \ + b"\xe9\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + b"\xef\xff\x00\x01\x32\x00\x18\x00\x01\x00\x00\x00\xbd\xeb\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xac\x11\x00\x02" \ + b"\x33\x00\x18\x00\x01\x00\x00\x00\xe8\x1c\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\xef\xff\x00\x01\x07\x80\x38\x00" \ + b"\x00\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\x1d\x00\x00\x00\x74\x65\x73\x74\x2e\x6c\x6f\x63" \ + b"\x61\x6c\x2f\x30\x2e\x38\x2e\x30\x2f\x4c\x69\x6e\x75\x78\x2f\x4c" \ + b"\x69\x6e\x75\x78\x00\x00\x00\x00\x19\x80\x04\x00\x00\x80\x06\x00" \ + b"\x01\x00\x00\x00" + ++ Test endianness += PID_BUILTIN_ENDPOINT_QOS endianness +assert raw(PID_BUILTIN_ENDPOINT_QOS(parameterId=119, parameterLength=0, parameterData=b"")) == b'w\x00\x00\x00' + + ++ Test RTPS += RTPS default header values +pkt2 = RTPS()/RTPSMessage(submessages=[ + RTPSSubMessage_HEARTBEAT(), + RTPSSubMessage_INFO_TS(), + RTPSSubMessage_DATA(), +]) +assert bytes(RTPS()) == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + += RTPS packet declaration +pkt3 = RTPS( + protocolVersion=ProtocolVersionPacket(major=2, minor=1), + vendorId=VendorIdPacket(vendor_id=0x0110), + guidPrefix=GUIDPrefixPacket( + hostId=1466109953, appId=3601547391, instanceId=1540995868 + ), + magic=b"RTPS", +) / RTPSMessage( + submessages=[ + RTPSSubMessage_INFO_DST( + submessageId=14, + submessageFlags=1, + octetsToNextHeader=12, + guidPrefix=GUIDPrefixPacket( + hostId=2284457985, appId=1569494848, instanceId=2025205186 + ), + ), + RTPSSubMessage_INFO_TS( + submessageId=9, + submessageFlags=1, + octetsToNextHeader=8, + ts_seconds=1619087604, + ts_fraction=475848017, + ), + RTPSSubMessage_DATA( + submessageId=21, + submessageFlags=5, + octetsToNextHeader=272, + extraFlags=0, + octetsToInlineQoS=16, + readerEntityIdKey=256, + readerEntityIdKind=199, + writerEntityIdKey=256, + writerEntityIdKind=194, + writerSeqNumHi=0, + writerSeqNumLow=1, + data=DataPacket( + encapsulationKind=3, + encapsulationOptions=0, + parameterList=ParameterListPacket( + parameterValues=[ + PID_PROTOCOL_VERSION( + parameterId=21, + parameterLength=4, + protocolVersion=ProtocolVersionPacket(major=2, minor=1), + padding=b"\x00\x00", + ), + PID_VENDOR_ID( + parameterId=22, + parameterLength=4, + vendorId=VendorIdPacket(vendor_id=0x0110), + padding=b"\x00\x00", + ), + PID_PARTICIPANT_LEASE_DURATION( + parameterId=2, + parameterLength=8, + parameterData=b"\n\x00\x00\x00\x00\x00\x00\x00", + ), + PID_PARTICIPANT_GUID( + parameterId=80, + parameterLength=16, + guid=GUIDPacket( + hostId=1466109953, + appId=3601547391, + instanceId=1540995868, + entityId=449, + ), + ), + PID_BUILTIN_ENDPOINT_SET( + parameterId=88, + parameterLength=4, + parameterData=b"?\x0c\x00\x00", + ), + PID_DOMAIN_ID( + parameterId=15, + parameterLength=4, + parameterData=b"\x00\x00\x00\x00", + ), + PID_DEFAULT_UNICAST_LOCATOR( + parameterId=49, + parameterLength=24, + locator=LocatorPacket( + locatorKind=1, port=60349, address="172.17.0.2" + ), + ), + PID_DEFAULT_MULTICAST_LOCATOR( + parameterId=72, + parameterLength=24, + locator=LocatorPacket( + locatorKind=1, port=7401, address="239.255.0.1" + ), + ), + PID_METATRAFFIC_UNICAST_LOCATOR( + parameterId=50, + parameterLength=24, + locator=LocatorPacket( + locatorKind=1, port=60349, address="172.17.0.2" + ), + ), + PID_METATRAFFIC_MULTICAST_LOCATOR( + parameterId=51, + parameterLength=24, + locator=LocatorPacket( + locatorKind=1, port=7400, address="239.255.0.1" + ), + ), + PID_UNKNOWN( + parameterId=32775, + parameterLength=56, + parameterData=b"\x00\x00\x00\x00,\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00test.local/0.8.0/Linux/Linux\x00\x00\x00\x00", + ), + PID_UNKNOWN( + parameterId=32793, + parameterLength=4, + parameterData=b"\x00\x80\x06\x00", + ), + ], + sentinel=PID_SENTINEL(parameterId=1, parameterLength=0), + ), + ), + ), + ] +) + += RTPS header dissect +assert pkt3.build() == pkt + ++ Test RTI RTPS += Test dissection +d = b"\x52\x54\x50\x53\x02\x03\x01\x01\x01\x01\x30\xba\xa8\x7b\x1d\xce" \ + b"\xb3\x29\x1e\x43\x09\x01\x08\x00\xd6\x64\xa8\x61\x16\x09\x34\x7c" \ + b"\x15\x05\xdc\x02\x00\x00\x10\x00\x00\x00\x00\x00\x00\x01\x00\xc2" \ + b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x03\x00\x00\x50\x00\x10\x00" \ + b"\x01\x01\x30\xba\xa8\x7b\x1d\xce\xb3\x29\x1e\x43\x00\x00\x01\xc1" \ + b"\x58\x00\x04\x00\x3f\x0c\x00\x00\x77\x00\x04\x00\x01\x00\x00\x00" \ + b"\x15\x00\x04\x00\x02\x03\x00\x00\x16\x00\x04\x00\x01\x01\x00\x00" \ + b"\x00\x80\x04\x00\x06\x00\x01\x00\x59\x00\x78\x01\x06\x00\x00\x00" \ + b"\x16\x00\x00\x00\x64\x64\x73\x2e\x73\x79\x73\x5f\x69\x6e\x66\x6f" \ + b"\x2e\x68\x6f\x73\x74\x6e\x61\x6d\x65\x00\x00\x00\x0d\x00\x00\x00" \ + b"\x64\x66\x30\x62\x36\x64\x38\x33\x61\x62\x34\x36\x00\x00\x00\x00" \ + b"\x18\x00\x00\x00\x64\x64\x73\x2e\x73\x79\x73\x5f\x69\x6e\x66\x6f" \ + b"\x2e\x70\x72\x6f\x63\x65\x73\x73\x5f\x69\x64\x00\x05\x00\x00\x00" \ + b"\x34\x38\x33\x30\x00\x00\x00\x00\x21\x00\x00\x00\x64\x64\x73\x2e" \ + b"\x73\x79\x73\x5f\x69\x6e\x66\x6f\x2e\x65\x78\x65\x63\x75\x74\x61" \ + b"\x62\x6c\x65\x5f\x66\x69\x6c\x65\x70\x61\x74\x68\x00\x00\x00\x00" \ + b"\x42\x00\x00\x00\x2f\x75\x73\x72\x2f\x6c\x6f\x63\x61\x6c\x2f\x73" \ + b"\x72\x63\x2f\x72\x74\x69\x2f\x72\x65\x73\x6f\x75\x72\x63\x65\x2f" \ + b"\x61\x70\x70\x2f\x62\x69\x6e\x2f\x78\x36\x34\x4c\x69\x6e\x75\x78" \ + b"\x32\x2e\x36\x67\x63\x63\x34\x2e\x34\x2e\x35\x2f\x72\x74\x69\x64" \ + b"\x64\x73\x73\x70\x79\x00\x00\x00\x14\x00\x00\x00\x64\x64\x73\x2e" \ + b"\x73\x79\x73\x5f\x69\x6e\x66\x6f\x2e\x74\x61\x72\x67\x65\x74\x00" \ + b"\x14\x00\x00\x00\x78\x36\x34\x4c\x69\x6e\x75\x78\x32\x2e\x36\x67" \ + b"\x63\x63\x34\x2e\x34\x2e\x35\x00\x20\x00\x00\x00\x64\x64\x73\x2e" \ + b"\x73\x79\x73\x5f\x69\x6e\x66\x6f\x2e\x63\x72\x65\x61\x74\x69\x6f" \ + b"\x6e\x5f\x74\x69\x6d\x65\x73\x74\x61\x6d\x70\x00\x14\x00\x00\x00" \ + b"\x32\x30\x32\x31\x2d\x30\x36\x2d\x37\x20\x30\x34\x3a\x30\x39\x3a" \ + b"\x30\x32\x5a\x00\x21\x00\x00\x00\x64\x64\x73\x2e\x73\x79\x73\x5f" \ + b"\x69\x6e\x66\x6f\x2e\x65\x78\x65\x63\x75\x74\x69\x6f\x6e\x5f\x74" \ + b"\x69\x6d\x65\x73\x74\x61\x6d\x70\x00\x00\x00\x00\x14\x00\x00\x00" \ + b"\x32\x30\x32\x31\x2d\x31\x32\x2d\x31\x20\x30\x39\x3a\x31\x35\x3a" \ + b"\x32\x39\x5a\x00\x31\x00\x18\x00\x01\x00\x00\x00\xf3\x1c\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + b"\x31\x00\x18\x00\x00\x00\x00\x01\xf3\x1c\x00\x00\x61\xab\xd9\x79" \ + b"\xb5\x7c\x13\xa5\x29\x49\x2c\xa3\x00\x00\x00\x00\x32\x00\x18\x00" \ + b"\x01\x00\x00\x00\xf2\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\x32\x00\x18\x00\x00\x00\x00\x01" \ + b"\xf2\x1c\x00\x00\x61\xab\xd9\x79\xb5\x7c\x13\xa5\x29\x49\x2c\xa3" \ + b"\x00\x00\x00\x00\x33\x00\x18\x00\x01\x00\x00\x00\xe8\x1c\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xef\xff\x00\x01" \ + b"\x02\x00\x08\x00\x06\x00\x00\x00\xff\xff\xff\x7f\x01\x80\x04\x00" \ + b"\xff\xff\x00\x00\x62\x00\x28\x00\x22\x00\x00\x00\x52\x54\x49\x20" \ + b"\x44\x61\x74\x61\x20\x44\x69\x73\x74\x72\x69\x62\x75\x74\x69\x6f" \ + b"\x6e\x20\x53\x65\x72\x76\x69\x63\x65\x20\x53\x70\x79\x00\x00\x00" \ + b"\x0f\x00\x04\x00\x00\x00\x00\x00\x0f\x80\x04\x00\x00\x00\x00\x00" \ + b"\x10\x80\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\xe3\xff\x00\x00" \ + b"\x00\x00\x00\x01\x00\x00\x01\x00\x16\x80\x08\x00\x10\x00\x00\x00" \ + b"\x00\x00\x00\x00\x17\x80\x04\x00\x03\x00\x00\x00\x01\x00\x00\x00" + +p0 = RTPS(d) +p1 = RTPS( + protocolVersion=ProtocolVersionPacket(major=2, minor=3), + vendorId=VendorIdPacket(vendor_id=0x0101), + guidPrefix=GUIDPrefixPacket( + hostId=16855226, appId=2826640846, instanceId=3005816387 + ), + magic=b"RTPS", +) / RTPSMessage( + submessages=[ + RTPSSubMessage_INFO_TS( + submessageId=9, + submessageFlags=1, + octetsToNextHeader=8, + ts_seconds=1638425814, + ts_fraction=2083784982, + ), + RTPSSubMessage_DATA( + submessageId=21, + submessageFlags=5, + octetsToNextHeader=732, + extraFlags=0, + octetsToInlineQoS=16, + readerEntityIdKey=0, + readerEntityIdKind=0, + writerEntityIdKey=256, + writerEntityIdKind=194, + writerSeqNumHi=0, + writerSeqNumLow=1, + data=DataPacket( + encapsulationKind=3, + encapsulationOptions=0, + parameterList=ParameterListPacket( + parameterValues=[ + PID_PARTICIPANT_GUID( + parameterId=80, + parameterLength=16, + guid=GUIDPacket( + hostId=16855226, + appId=2826640846, + instanceId=3005816387, + entityId=449, + ), + ), + PID_BUILTIN_ENDPOINT_SET( + parameterId=88, + parameterLength=4, + parameterData=b"?\x0c\x00\x00", + ), + PID_BUILTIN_ENDPOINT_QOS( + parameterId=119, + parameterLength=4, + parameterData=b"\x01\x00\x00\x00", + ), + PID_PROTOCOL_VERSION( + parameterId=21, + parameterLength=4, + protocolVersion=ProtocolVersionPacket(major=2, minor=3), + padding=b"\x00\x00", + ), + PID_VENDOR_ID( + parameterId=22, + parameterLength=4, + vendorId=VendorIdPacket(vendor_id=0x0101), + padding=b"\x00\x00", + ), + PID_PRODUCT_VERSION( + parameterId=32768, + parameterLength=4, + productVersion=ProductVersionPacket( + major=6, minor=0, release=1, revision=0 + ), + ), + PID_PROPERTY_LIST( + parameterId=89, + parameterLength=376, + parameterData=b"\x06\x00\x00\x00\x16\x00\x00\x00dds.sys_info.hostname\x00\x00\x00\r\x00\x00\x00df0b6d83ab46\x00\x00\x00\x00\x18\x00\x00\x00dds.sys_info.process_id\x00\x05\x00\x00\x004830\x00\x00\x00\x00!\x00\x00\x00dds.sys_info.executable_filepath\x00\x00\x00\x00B\x00\x00\x00/usr/local/src/rti/resource/app/bin/x64Linux2.6gcc4.4.5/rtiddsspy\x00\x00\x00\x14\x00\x00\x00dds.sys_info.target\x00\x14\x00\x00\x00x64Linux2.6gcc4.4.5\x00 \x00\x00\x00dds.sys_info.creation_timestamp\x00\x14\x00\x00\x002021-06-7 04:09:02Z\x00!\x00\x00\x00dds.sys_info.execution_timestamp\x00\x00\x00\x00\x14\x00\x00\x002021-12-1 09:15:29Z\x00", + ), + PID_DEFAULT_UNICAST_LOCATOR( + parameterId=49, + parameterLength=24, + locator=LocatorPacket( + locatorKind=1, port=7411, address="0.0.0.0" + ), + ), + PID_DEFAULT_UNICAST_LOCATOR( + parameterId=49, + parameterLength=24, + locator=LocatorPacket( + locatorKind=16777216, + port=7411, + hostId=b"a\xab\xd9y\xb5|\x13\xa5)I,\xa3\x00\x00\x00\x00", + ), + ), + PID_METATRAFFIC_UNICAST_LOCATOR( + parameterId=50, + parameterLength=24, + locator=LocatorPacket( + locatorKind=1, port=7410, address="0.0.0.0" + ), + ), + PID_METATRAFFIC_UNICAST_LOCATOR( + parameterId=50, + parameterLength=24, + locator=LocatorPacket( + locatorKind=16777216, + port=7410, + hostId=b"a\xab\xd9y\xb5|\x13\xa5)I,\xa3\x00\x00\x00\x00", + ), + ), + PID_METATRAFFIC_MULTICAST_LOCATOR( + parameterId=51, + parameterLength=24, + locator=LocatorPacket( + locatorKind=1, port=7400, address="239.255.0.1" + ), + ), + PID_PARTICIPANT_LEASE_DURATION( + parameterId=2, + parameterLength=8, + parameterData=b"\x06\x00\x00\x00\xff\xff\xff\x7f", + ), + PID_PLUGIN_PROMISCUITY_KIND( + parameterId=32769, parameterLength=4, promiscuityKind=65535 + ), + PID_ENTITY_NAME( + parameterId=98, + parameterLength=40, + parameterData=b'"\x00\x00\x00RTI Data Distribution Service Spy\x00\x00\x00', + ), + PID_DOMAIN_ID( + parameterId=15, + parameterLength=4, + parameterData=b"\x00\x00\x00\x00", + ), + PID_RTI_DOMAIN_ID( + parameterId=32783, parameterLength=4, domainId=0 + ), + PID_TRANSPORT_INFO_LIST( + transportInfo=[ + TransportInfoPacket(classID=1, messageSizeMax=65507), + TransportInfoPacket( + classID=16777216, messageSizeMax=65536 + ), + ], + parameterId=32784, + parameterLength=20, + padding=b"\x02\x00\x00\x00", + ), + PID_REACHABILITY_LEASE_DURATION( + parameterId=32790, + parameterLength=8, + lease_duration=LeaseDurationPacket( + seconds=268435456, fraction=0 + ), + ), + PID_VENDOR_BUILTIN_ENDPOINT_SET( + parameterId=32791, parameterLength=4, flags=3 + ), + ], + sentinel=PID_SENTINEL(parameterId=1, parameterLength=0), + ), + ), + ), + ] +) +assert p0.build() == d +assert p1.build() == d + ++ Test for pr #3914 += RTPS Heartbeat SequenceNumber_t packing and dissection + +d = b"\x52\x54\x50\x53\x02\x02\x01\x0f\x01\x0f\x45\xd2\xb3\xf5\x58\xb9" \ + b"\x01\x00\x00\x00\x07\x01\x1c\x00\x00\x00\x03\xc7\x00\x00\x03\xc2" \ + b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00" \ + b"\x01\x00\x00\x00" + +p0 = RTPS(d) + +p1 = RTPS( + protocolVersion=ProtocolVersionPacket(major=2, minor=2), + vendorId=VendorIdPacket(vendor_id=0x010f), + guidPrefix=GUIDPrefixPacket( + hostId=0x010f45d2, appId=0xb3f558b9, instanceId=0x01000000 + ), + magic=b"RTPS", +) / RTPSMessage( + submessages=[ + RTPSSubMessage_HEARTBEAT( + submessageId=0x07, + submessageFlags=0x01, + octetsToNextHeader=28, + reader_id=b"\x00\x00\x03\xc7", + writer_id=b"\x00\x00\x03\xc2", + firstAvailableSeqNumHi=0, + firstAvailableSeqNumLow=1, + lastSeqNumHi=0, + lastSeqNumLow=1, + count=1 + ) + ] +) + +assert p0.build() == d +assert p1.build() == d +assert p0 == p1 + ++ Test for pr #3915 += RTPS ACKNACK count packing and dissection + +d = b"\x52\x54\x50\x53\x02\x02\x01\x0f\x01\x0f\x45\xd2\xb3\xf5\x58\xb9" \ + b"\x01\x00\x00\x00\x06\x03\x18\x00\x00\x00\x03\xc7\x00\x00\x03\xc2" \ + b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00" +p0 = RTPS(d) + +p1 = RTPS( + protocolVersion=ProtocolVersionPacket(major=2, minor=2), + vendorId=VendorIdPacket(vendor_id=0x010f), + guidPrefix=GUIDPrefixPacket( + hostId=0x010f45d2, appId=0xb3f558b9, instanceId=0x01000000 + ), + magic=b"RTPS", +) / RTPSMessage( + submessages=[ + RTPSSubMessage_ACKNACK( + submessageId=6, + submessageFlags=3, + octetsToNextHeader=0x18, + reader_id=b'\x00\x00\x03\xc7', + writer_id=b'\x00\x00\x03\xc2', + readerSNState=b'\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00', + count=1 + ) + ] +) + +assert p0.build() == d +assert p1.build() == d +assert p0 == p1 + ++ Test for #PR4545 += RTPS length computation with inlineQos + +p0 = RTPS( + protocolVersion=ProtocolVersionPacket(major=2, minor=2), + vendorId=VendorIdPacket(vendor_id=0x010f), + guidPrefix=GUIDPrefixPacket( + hostId=0x010f45d2, appId=0xb3f558b9, instanceId=0x01000000 + ),magic=b"RTPS" + )/RTPSMessage(submessages=[ + RTPSSubMessage_INFO_TS( + submessageId=9, + submessageFlags=1, + octetsToNextHeader=8, + ts_seconds=1638425814, + ts_fraction=2083784982, + ), + RTPSSubMessage_DATA( + submessageId= 0x15, + submessageFlags= 0x7, + octetsToNextHeader= 54, + extraFlags= 0x0, + octetsToInlineQoS= 16, + readerEntityIdKey= 0x0, + readerEntityIdKind= 0x0, + writerEntityIdKey= 0x0, + writerEntityIdKind= 0x0, + writerSeqNumHi= 0, + writerSeqNumLow= 4, + inlineQoS= InlineQoSPacket( + parameters= [ + PID_UNKNOWN( + parameterId= 0x801e, + parameterLength= 4, + parameterData= b'\x00\x00\x00\x00', + ), + ], + sentinel= PID_SENTINEL( + parameterId= 0x1, + parameterLength= 0, + parameterData= b'', + ), + ), + data= DataPacket( + encapsulationKind= 0x1, + encapsulationOptions= 0x3, + serializedData= b'=\x00\x00\x00abcdefghij\x00\x00\x00\x00', + ), + ), + RTPSSubMessage_INFO_TS( + submessageId=9, + submessageFlags=1, + octetsToNextHeader=8, + ts_seconds=1638425814, + ts_fraction=2083784982, + ), + RTPSSubMessage_DATA( + submessageId= 0x15, + submessageFlags= 0x7, + octetsToNextHeader= 54, + extraFlags= 0x0, + octetsToInlineQoS= 16, + readerEntityIdKey= 0x0, + readerEntityIdKind= 0x0, + writerEntityIdKey= 0x0, + writerEntityIdKind= 0x0, + writerSeqNumHi= 0, + writerSeqNumLow= 4, + inlineQoS= InlineQoSPacket( + parameters= [ + PID_UNKNOWN( + parameterId= 0x801e, + parameterLength= 4, + parameterData= b'\x00\x00\x00\x00', + ), + ], + sentinel= PID_SENTINEL( + parameterId= 0x1, + parameterLength= 0, + parameterData= b'', + ), + ), + data= DataPacket( + encapsulationKind= 0x1, + encapsulationOptions= 0x3, + serializedData= b'=\x00\x00\x00abcdefghij\x00\x00\x00\x00', + ), + ), +]) + +d = b"\x52\x54\x50\x53\x02\x02\x01\x0f\x01\x0f\x45\xd2\xb3\xf5\x58\xb9" \ + b"\x01\x00\x00\x00\x09\x01\x08\x00\xd6\x64\xa8\x61\x16\x09\x34\x7c" \ + b"\x15\x07\x36\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\x04\x00\x00\x00\x1e\x80\x04\x00\x00\x00\x00\x00" \ + b"\x01\x00\x00\x00\x00\x01\x00\x03\x3d\x00\x00\x00\x61\x62\x63\x64" \ + b"\x65\x66\x67\x68\x69\x6a\x00\x00\x00\x00\x09\x01\x08\x00\xd6\x64" \ + b"\xa8\x61\x16\x09\x34\x7c\x15\x07\x36\x00\x00\x00\x10\x00\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1e\x80" \ + b"\x04\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x01\x00\x03\x3d\x00" \ + b"\x00\x00\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x00\x00\x00\x00" + +assert RTPS(d) == p0 diff --git a/test/contrib/rtr.uts b/test/contrib/rtr.uts index 2a7b78ffd00..0ec0d4e4812 100644 --- a/test/contrib/rtr.uts +++ b/test/contrib/rtr.uts @@ -1,9 +1,5 @@ + RTR Serial Notify -from scapy.consts import WINDOWS -if WINDOWS: - route_add_loopback() - = default instantiation pkt = IP()/TCP(dport=323)/RTRSerialNotify() @@ -234,9 +230,9 @@ RTRErrorReport in pkt and pkt.error_code == 0 and pkt.erroneous_PDU == b'' and p = filled values build pkt = IP()/TCP(dport=323)/RTRErrorReport(error_code=1, error_text='Internal Error') -RTRErrorReport in pkt and pkt.error_code == 1and pkt.error_text == b'Internal Error' +RTRErrorReport in pkt and pkt.error_code == 1 and pkt.error_text == b'Internal Error' = dissection pkt = IP(b'E\x00\x00F\x00\x01\x00\x00@\x06|\xaf\x7f\x00\x00\x01\x7f\x00\x00\x01 Z\x01C\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xdc\x15\x00\x00\x00\n\x00\x01\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x0eInternal Error') -RTRErrorReport in pkt and pkt.error_code == 1and pkt.error_text == b'Internal Error' +RTRErrorReport in pkt and pkt.error_code == 1 and pkt.error_text == b'Internal Error' diff --git a/test/contrib/rtsp.uts b/test/contrib/rtsp.uts new file mode 100644 index 00000000000..9f1b5430ac4 --- /dev/null +++ b/test/contrib/rtsp.uts @@ -0,0 +1,32 @@ +% RTSP tests + ++ RTSP - Dissection and Build tests + += RTSP request - dissection + +pkt = Ether(b'\xbc\xdf \x00\x02\x00\x00\x00\x02\x00\x00\x00\x08\x00E\x00\x01\xde\x16\xca@\x00\x80\x06\xf9\xb8Q\x83\xe7CR\xd3\\\xfd\x0fU\x02*\xbf\xd4\xcb\xa4~\n\x19DP\x18"8\x86n\x00\x00DESCRIBE rtsp://EMAP1.planetwideradio.com/tfm RTSP/1.0\r\nUser-Agent: WMPlayer/10.0.0.380 guid/7405E143-26AC-4B37-9802-A35EE8C6CFA7\r\nAccept: application/sdp\r\nAccept-Charset: UTF-8, *;q=0.1\r\nX-Accept-Authentication: Negotiate, NTLM, Digest, Basic\r\nAccept-Language: en-GB, *;q=0.1\r\nCSeq: 1\r\nSupported: com.microsoft.wm.srvppair, com.microsoft.wm.sswitch, com.microsoft.wm.eosmsg, com.microsoft.wm.predstrm, com.microsoft.wm.startupprofile\r\n\r\n') +assert RTSPRequest in pkt +assert pkt.Method == b"DESCRIBE" +assert pkt.Request_Uri == b"rtsp://EMAP1.planetwideradio.com/tfm" +assert pkt.Version == b"RTSP/1.0" +assert pkt.Accept == b"application/sdp" +assert pkt.User_Agent == b"WMPlayer/10.0.0.380 guid/7405E143-26AC-4B37-9802-A35EE8C6CFA7" + += RTSP request - build + +rebuild = RTSP() / RTSPRequest(Accept=b'application/sdp', Accept_Language=b'en-GB, *;q=0.1', User_Agent=b'WMPlayer/10.0.0.380 guid/7405E143-26AC-4B37-9802-A35EE8C6CFA7', Unknown_Headers={b'Accept-Charset': b'UTF-8, *;q=0.1', b'X-Accept-Authentication': b'Negotiate, NTLM, Digest, Basic', b'CSeq': b'1', b'Supported': b'com.microsoft.wm.srvppair, com.microsoft.wm.sswitch, com.microsoft.wm.eosmsg, com.microsoft.wm.predstrm, com.microsoft.wm.startupprofile'}, Method=b'DESCRIBE', Request_Uri=b'rtsp://EMAP1.planetwideradio.com/tfm', Version=b'RTSP/1.0') +assert bytes(rebuild) == b'DESCRIBE rtsp://EMAP1.planetwideradio.com/tfm RTSP/1.0\r\nAccept: application/sdp\r\nAccept-Language: en-GB, *;q=0.1\r\nUser-Agent: WMPlayer/10.0.0.380 guid/7405E143-26AC-4B37-9802-A35EE8C6CFA7\r\nAccept-Charset: UTF-8, *;q=0.1\r\nX-Accept-Authentication: Negotiate, NTLM, Digest, Basic\r\nCSeq: 1\r\nSupported: com.microsoft.wm.srvppair, com.microsoft.wm.sswitch, com.microsoft.wm.eosmsg, com.microsoft.wm.predstrm, com.microsoft.wm.startupprofile\r\n\r\n' + += RTSP response - dissection + +pkt = Ether(b'\x00\x02\xb3L\xf6\xb2\x00 \x9cR\x93`\x08\x00E\x80\x02cY\x13@\x00p\x06\xa9\x91\xd8@\xbe=\n\xc9d)\x02*\t\x9d\xf7p\xe8O\x10\xfcz\x9fP\x18\xfc\xc0\x91L\x00\x00RTSP/1.0 200 OK\r\nTransport: RTP/AVP/UDP;unicast;server_port=5004-5005;client_port=2462-2463;ssrc=927717de;mode=PLAY\r\nDate: Sun, 06 Nov 2005 12:19:47 GMT\r\nCSeq: 2\r\nSession: 17555940012607716235;timeout=60\r\nServer: WMServer/9.1.1.3814\r\nSupported: com.microsoft.wm.srvppair, com.microsoft.wm.sswitch, com.microsoft.wm.eosmsg, com.microsoft.wm.fastcache, com.microsoft.wm.packetpairssrc, com.microsoft.wm.startupprofile\r\nLast-Modified: Thu, 20 Oct 2005 16:30:11 GMT\r\nCache-Control: x-wms-content-size=84457, max-age=86398, must-revalidate, proxy-revalidate\r\nEtag: "84457"\r\n\r\n') +assert RTSPResponse in pkt +assert pkt.Version == b"RTSP/1.0" +assert pkt.Status_Code == b"200" +assert pkt.Reason_Phrase == b"OK" +assert pkt.Server == b"WMServer/9.1.1.3814" + += RTSP response - build + +rebuild = RTSP() / RTSPResponse(Server=b'WMServer/9.1.1.3814', Unknown_Headers={b'Transport': b'RTP/AVP/UDP;unicast;server_port=5004-5005;client_port=2462-2463;ssrc=927717de;mode=PLAY', b'Date': b'Sun, 06 Nov 2005 12:19:47 GMT', b'CSeq': b'2', b'Session': b'17555940012607716235;timeout=60', b'Supported': b'com.microsoft.wm.srvppair, com.microsoft.wm.sswitch, com.microsoft.wm.eosmsg, com.microsoft.wm.fastcache, com.microsoft.wm.packetpairssrc, com.microsoft.wm.startupprofile', b'Last-Modified': b'Thu, 20 Oct 2005 16:30:11 GMT', b'Cache-Control': b'x-wms-content-size=84457, max-age=86398, must-revalidate, proxy-revalidate', b'Etag': b'"84457"'}, Version=b'RTSP/1.0', Status_Code=b'200', Reason_Phrase=b'OK') +assert bytes(rebuild) == b'RTSP/1.0 200 OK\r\nServer: WMServer/9.1.1.3814\r\nTransport: RTP/AVP/UDP;unicast;server_port=5004-5005;client_port=2462-2463;ssrc=927717de;mode=PLAY\r\nDate: Sun, 06 Nov 2005 12:19:47 GMT\r\nCSeq: 2\r\nSession: 17555940012607716235;timeout=60\r\nSupported: com.microsoft.wm.srvppair, com.microsoft.wm.sswitch, com.microsoft.wm.eosmsg, com.microsoft.wm.fastcache, com.microsoft.wm.packetpairssrc, com.microsoft.wm.startupprofile\r\nLast-Modified: Thu, 20 Oct 2005 16:30:11 GMT\r\nCache-Control: x-wms-content-size=84457, max-age=86398, must-revalidate, proxy-revalidate\r\nEtag: "84457"\r\n\r\n' diff --git a/test/contrib/sebek.uts b/test/contrib/sebek.uts index f83eb1c1c3c..39ef69c3480 100644 --- a/test/contrib/sebek.uts +++ b/test/contrib/sebek.uts @@ -8,7 +8,7 @@ = Layer binding 1 pkt = IP() / UDP() / SebekHead() / SebekV1(cmd="diepotato") assert pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 1 -assert pkt.summary() == "IP / UDP / SebekHead / Sebek v1 read ('diepotato')" +assert pkt.summary() == "IP / UDP / SebekHead / Sebek v1 read (b'diepotato')" = Packet dissection 1 pkt = IP(raw(pkt)) @@ -17,7 +17,7 @@ pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 1 = Layer binding 2 pkt = IP() / UDP() / SebekHead() / SebekV2Sock(cmd="diepotato") assert pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 2 and pkt[SebekHead].type ==2 -assert pkt.summary() == "IP / UDP / SebekHead / Sebek v2 socket ('diepotato')" +assert pkt.summary() == "IP / UDP / SebekHead / Sebek v2 socket (b'diepotato')" = Packet dissection 2 pkt = IP(raw(pkt)) @@ -26,7 +26,7 @@ pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 2 and pkt[SebekHead = Layer binding 3 pkt = IPv6()/UDP()/SebekHead()/SebekV3() assert pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 3 -assert pkt.summary() == "IPv6 / UDP / SebekHead / Sebek v3 read ('')" +assert pkt.summary() == "IPv6 / UDP / SebekHead / Sebek v3 read (b'')" = Packet dissection 3 pkt = IPv6(raw(pkt)) @@ -35,12 +35,12 @@ pkt.sport == pkt.dport == 1101 and pkt[SebekHead].version == 3 = Nonsense summaries assert SebekHead(version=2).summary() == "Sebek Header v2 read" -assert SebekV1(cmd="diepotato").summary() == "Sebek v1 ('diepotato')" -assert SebekV2(cmd="diepotato").summary() == "Sebek v2 ('diepotato')" -assert (SebekHead()/SebekV2(cmd="nottoday")).summary() == "SebekHead / Sebek v2 read ('nottoday')" -assert SebekV3(cmd="diepotato").summary() == "Sebek v3 ('diepotato')" -assert (SebekHead()/SebekV3(cmd="nottoday")).summary() == "SebekHead / Sebek v3 read ('nottoday')" -assert SebekV3Sock(cmd="diepotato").summary() == "Sebek v3 socket ('diepotato')" -assert (SebekHead()/SebekV3Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v3 socket ('nottoday')" -assert SebekV2Sock(cmd="diepotato").summary() == "Sebek v2 socket ('diepotato')" -assert (SebekHead()/SebekV2Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v2 socket ('nottoday')" +assert SebekV1(cmd="diepotato").summary() == "Sebek v1 (b'diepotato')" +assert SebekV2(cmd="diepotato").summary() == "Sebek v2 (b'diepotato')" +assert (SebekHead()/SebekV2(cmd="nottoday")).summary() == "SebekHead / Sebek v2 read (b'nottoday')" +assert SebekV3(cmd="diepotato").summary() == "Sebek v3 (b'diepotato')" +assert (SebekHead()/SebekV3(cmd="nottoday")).summary() == "SebekHead / Sebek v3 read (b'nottoday')" +assert SebekV3Sock(cmd="diepotato").summary() == "Sebek v3 socket (b'diepotato')" +assert (SebekHead()/SebekV3Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v3 socket (b'nottoday')" +assert SebekV2Sock(cmd="diepotato").summary() == "Sebek v2 socket (b'diepotato')" +assert (SebekHead()/SebekV2Sock(cmd="nottoday")).summary() == "SebekHead / Sebek v2 socket (b'nottoday')" diff --git a/test/contrib/send.uts b/test/contrib/send.uts index 80d892c0f02..c4ab0ee09cd 100644 --- a/test/contrib/send.uts +++ b/test/contrib/send.uts @@ -10,7 +10,7 @@ assert pkt[ICMPv6NDOptRsaSig].signature_pad == b"\x01" * 12 = ICMPv6NDOptCGA build and dissection -pkt = Ether()/IPv6()/ICMPv6ND_NS()/ICMPv6NDOptCGA(CGA_PARAMS=CGA_Params()) +pkt = Ether()/IPv6()/ICMPv6ND_NS()/ICMPv6NDOptCGA(CGA_PARAMS=CGA_Params(pubkey=X509_SubjectPublicKeyInfo(signatureAlgorithm=X509_AlgorithmIdentifier(parameters=0)))) pkt = Ether(raw(pkt)) assert ICMPv6NDOptCGA in pkt diff --git a/test/contrib/spbm.uts b/test/contrib/spbm.uts deleted file mode 100644 index fb135e5dede..00000000000 --- a/test/contrib/spbm.uts +++ /dev/null @@ -1,18 +0,0 @@ -% Regression tests for the spbm module - -+ Basic SPBM test - -= Test build and dissection - -backboneEther = Ether(dst='00:bb:00:00:90:00', src='00:bb:00:00:40:00', type=0x8100) -backboneDot1Q = Dot1Q(vlan=4051,type=0x88e7) -backboneServiceID = SPBM(prio=1,isid=20011) -customerEther = Ether(dst='00:1b:4f:5e:ca:00',src='00:00:00:00:00:01',type=0x8100) -customerDot1Q = Dot1Q(prio=1,vlan=11,type=0x0800) -customerIP = IP(src='10.100.11.10',dst='10.100.12.10',id=0x0629,len=106) -customerUDP = UDP(sport=1024,dport=1025,chksum=0,len=86) - -pkt = backboneEther/backboneDot1Q/backboneServiceID/customerEther/customerDot1Q/customerIP/customerUDP/"Payload" -pkt = Ether(raw(pkt)) -assert SPBM in pkt -assert pkt[SPBM].payload.payload.payload.src == '10.100.11.10' diff --git a/test/contrib/stamp.uts b/test/contrib/stamp.uts new file mode 100644 index 00000000000..b6b6b71f38e --- /dev/null +++ b/test/contrib/stamp.uts @@ -0,0 +1,88 @@ +% STAMP regression tests for Scapy + +# More information at http://www.secdev.org/projects/UTscapy/ + +# Type the following command to launch start the tests: +# $ test/run_tests -t test/contrib/stamp.uts + +############ +# STAMP +############ + ++ STAMP tests + += Load module + +load_contrib("stamp") + += Test STAMP Session-Sender Test (Unauthenticated) +~ stamp-session-sender-test + +created = STAMPSessionSenderTestUnauthenticated( + seq=0x1234, + ts=1234.5678, + err_estimate=ErrorEstimate( + S=1, + Z=0, + scale=0x12, + multiplier=0x34 + ), + ssid=1357 +) +assert raw(created) == b'\x00\x00\x12\x34\x00\x00\x04\xD2\x91\x5B\x57\x3E\x92\x34\x05\x4D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +parsed = STAMPSessionSenderTestUnauthenticated(raw(created)) +assert parsed.seq == 0x1234 +assert parsed.ts == 1234.5678 +assert parsed.err_estimate.S == 1 +assert parsed.err_estimate.Z == 0 +assert parsed.err_estimate.scale == 0x12 +assert parsed.err_estimate.multiplier == 0x34 +assert parsed.ssid == 1357 +assert parsed.mbz == 0 +assert not parsed.tlv_objects + += Test STAMP Session-Reflector Test (Unauthenticated) +~ stamp-session-reflector-test + +created = STAMPSessionReflectorTestUnauthenticated( + seq=0x1234, + ts=1234.5678, + err_estimate=ErrorEstimate( + S=1, + Z=0, + scale=0x12, + multiplier=0x34 + ), + ssid=1357, + ts_rx=4321.8765, + seq_sender=0x4321, + ts_sender=2143.6587, + err_estimate_sender=ErrorEstimate( + S=0, + Z=0, + scale=0x21, + multiplier=0x43 + ), + ttl_sender=111 +) +assert raw(created) == b'\x00\x00\x12\x34\x00\x00\x04\xD2\x91\x5B\x57\x3E\x92\x34\x05\x4D\x00\x00\x10\xE1\xE0\x62\x4D\xD2\x00\x00\x43\x21\x00\x00\x08\x5F\xA8\xA0\x90\x2D\x21\x43\x00\x00\x6F\x00\x00\x00' +parsed = STAMPSessionReflectorTestUnauthenticated(raw(created)) +assert parsed.seq == 0x1234 +assert parsed.ts == 1234.5678 +assert parsed.err_estimate.S == 1 +assert parsed.err_estimate.Z == 0 +assert parsed.err_estimate.scale == 0x12 +assert parsed.err_estimate.multiplier == 0x34 +assert parsed.ssid == 1357 +assert parsed.ts_rx == 4321.8765 +assert parsed.seq_sender == 0x4321 +assert parsed.ts_sender == 2143.6587 +assert parsed.err_estimate_sender.S == 0 +assert parsed.err_estimate_sender.Z == 0 +assert parsed.err_estimate_sender.scale == 0x21 +assert parsed.err_estimate_sender.multiplier == 0x43 +assert parsed.mbz1 == 0 +assert parsed.ttl_sender == 111 +assert parsed.mbz2 == 0 +assert not parsed.tlv_objects + diff --git a/test/contrib/stun.uts b/test/contrib/stun.uts new file mode 100644 index 00000000000..f79d43ecf2e --- /dev/null +++ b/test/contrib/stun.uts @@ -0,0 +1,242 @@ +# STUN unit tests +# run with: +# test/run_tests -P "load_contrib('stun')" -t test/contrib/stun.uts -F + +% STUN regression tests for Scapy + +############ +# STUN +############ + ++ STUN Binding messages + += test STUN binding request 1 + +raw = b"\x00\x01\x00\x64\x21\x12\xa4\x42\xcf\xac\xb2\xa4\x3a\xa2\xde\x5a" \ + b"\x9d\x56\xd8\x5a\x00\x25\x00\x00\x00\x24\x00\x04\x6e\x20\x00\xff" \ + b"\x80\x2a\x00\x08\x1b\x0a\xb9\x8b\x6e\x8e\xff\xa6\x00\x06\x00\x25" \ + b"\x6f\x4e\x70\x68\x3a\x48\x74\x31\x31\x4d\x61\x52\x5a\x48\x63\x34" \ + b"\x47\x4f\x4c\x4a\x55\x73\x62\x75\x31\x52\x33\x59\x43\x73\x37\x32" \ + b"\x48\x59\x4e\x32\x35\x20\x20\x20\x00\x08\x00\x14\xfc\xbc\x47\x21" \ + b"\x68\x1f\xdb\x59\x91\x33\x42\xbe\x96\x19\x9e\x7f\x3e\xf0\xe7\x77" \ + b"\x80\x28\x00\x04\x87\x18\xc3\xa4" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding request" +assert parsed.length == 100 +assert parsed.magic_cookie == 0x2112A442 +assert parsed.transaction_id == 0xcfacb2a43aa2de5a9d56d85a, parsed.transaction_id +assert parsed.attributes == [ + STUNUseCandidate(), + STUNPriority(priority=1847591167), + STUNIceControlling(tie_breaker=0x1b0ab98b6e8effa6), + STUNUsername(length=37, username="oNph:Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25"), + STUNMessageIntegrity(hmac_sha1=0xfcbc4721681fdb59913342be96199e7f3ef0e777), + STUNFingerprint(crc_32=0x8718c3a4) +] + += test STUN binding request 2 + +raw = b"\x00\x01\x00\x6c\x21\x12\xa4\x42\x34\x79\x47\x65\x34\x63\x59\x36" \ + b"\x31\x6a\x79\x6a\x00\x06\x00\x25\x48\x74\x31\x31\x4d\x61\x52\x5a" \ + b"\x48\x63\x34\x47\x4f\x4c\x4a\x55\x73\x62\x75\x31\x52\x33\x59\x43" \ + b"\x73\x37\x32\x48\x59\x4e\x32\x35\x3a\x6f\x4e\x70\x68\x00\x00\x00" \ + b"\xc0\x57\x00\x04\x00\x00\x03\xe7\x80\x2a\x00\x08\xa6\x96\x81\x9e" \ + b"\x91\xc9\x37\xda\x00\x25\x00\x00\x00\x24\x00\x04\x6e\x00\x1e\xff" \ + b"\x00\x08\x00\x14\xc1\x87\xaa\xfa\xb1\xe0\xf3\x12\x31\x43\x3a\xb1" \ + b"\x4d\x67\x6b\xc7\xb9\x89\xbd\x5f\x80\x28\x00\x04\xc9\x56\x6c\xfc" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding request" +assert parsed.length == 108 +assert parsed.magic_cookie == 0x2112A442 +assert parsed.transaction_id == 0x3479476534635936316a796a +assert parsed.attributes == [ + STUNUsername(length=37, username='Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25:oNph'), + STUNGoogNetworkInfo(), + STUNIceControlling(tie_breaker=0xa696819e91c937da), + STUNUseCandidate(), + STUNPriority(priority=1845501695), + STUNMessageIntegrity(hmac_sha1=0xc187aafab1e0f31231433ab14d676bc7b989bd5f), + STUNFingerprint(crc_32=0xc9566cfc) + +] + += test STUN binding success response 1 + +raw = b"\x01\x01\x00\x2c\x21\x12\xa4\x42\xcf\xac\xb2\xa4\x3a\xa2\xde\x5a" \ + b"\x9d\x56\xd8\x5a\x00\x20\x00\x08\x00\x01\xbf\x32\x8d\x06\xa4\x68" \ + b"\x00\x08\x00\x14\xb7\x1f\xc9\x23\x58\x97\xc8\x02\xe3\xff\xf8\xe3" \ + b"\xd8\x89\xfa\x41\x42\x8d\x96\x7d\x80\x28\x00\x04\xea\x9b\x65\x59" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding success response" +assert parsed.length == 44 +assert parsed.magic_cookie == 0x2112A442 +assert parsed.transaction_id == 0xcfacb2a43aa2de5a9d56d85a, parsed.transaction_id +assert parsed.attributes == [ + STUNXorMappedAddress(length=8, xport=40480, xip="172.20.0.42"), + STUNMessageIntegrity(hmac_sha1=0xb71fc9235897c802e3fff8e3d889fa41428d967d), + STUNFingerprint(crc_32=0xea9b6559) +] + += test STUN binding success response 2 + +raw = b"\x01\x01\x00\x58\x21\x12\xa4\x42\x34\x79\x47\x65\x34\x63\x59\x36" \ + b"\x31\x6a\x79\x6a\x00\x20\x00\x08\x00\x01\x40\xba\x8d\x06\xa4\x8a" \ + b"\x00\x06\x00\x25\x48\x74\x31\x31\x4d\x61\x52\x5a\x48\x63\x34\x47" \ + b"\x4f\x4c\x4a\x55\x73\x62\x75\x31\x52\x33\x59\x43\x73\x37\x32\x48" \ + b"\x59\x4e\x32\x35\x3a\x6f\x4e\x70\x68\x20\x20\x20\x00\x08\x00\x14" \ + b"\x4b\x67\x03\x6d\xfb\x65\xca\x84\xd6\x3b\xca\xc8\x6c\x8d\x59\x81" \ + b"\xdf\x65\x70\x31\x80\x28\x00\x04\x40\x41\xe9\xc3" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding success response" +assert parsed.length == 88 +assert parsed.magic_cookie == 0x2112A442 +assert parsed.transaction_id == 0x3479476534635936316a796a, parsed.transaction_id +assert parsed.attributes[0] == STUNXorMappedAddress(length=8, xport=25000, xip="172.20.0.200"), parsed.attributes +assert parsed.attributes == [ + STUNXorMappedAddress(length=8, xport=25000, xip="172.20.0.200"), + STUNUsername(length=37, username="Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25:oNph"), + STUNMessageIntegrity(hmac_sha1=0x4b67036dfb65ca84d63bcac86c8d5981df657031), + STUNFingerprint(crc_32=0x4041e9c3) +] + += test STUN binding success response IPv6 + +raw = b"\x01\x01\x00\x18\x21\x12\xa4\x42\x91\x1b\x25\x32\x99\x8d\xa0\x1c" \ + b"\xf9\xd0\x53\xd9\x00\x20\x00\x14\x00\x02\x3c\xd7\x21\x12\xa4\x42" \ + b"\x91\x1b\x25\x32\x99\x8d\xa0\x1c\xf9\xd0\x53\xd8" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding success response" +assert parsed.length == 24 +assert parsed.magic_cookie == 0x2112A442 +assert parsed.transaction_id == 0x911b2532998da01cf9d053d9, parsed.transaction_id +assert len(parsed.attributes) == 1, len(parsed.attributes) +assert parsed.attributes[0].type == 0x0020, parsed.attributes[0].type +assert parsed.attributes[0].length == 20, parsed.attributes[0].length +assert parsed.attributes[0].address_family == 0x02, parsed.attributes[0].address_family +assert parsed.attributes[0].xport == 7621, parsed.attributes[0].xport +assert parsed.attributes[0].xip == "::1", parsed.attributes[0].xip + += test STUN classic binding success response + +raw = b"\x01\x01\x00\x0c\x37\x06\xd1\x4d\x38\x3a\xd6\xc8\x40\x5e\x17\x9a" \ + b"\x93\x92\xea\xa8\x00\x01\x00\x08\x00\x01\x0d\x14\xc0\xa8\x00\x05" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding success response" +assert parsed.length == 12 +assert parsed.magic_cookie == 0x3706d14d +assert parsed.transaction_id == 0x383ad6c8405e179a9392eaa8, parsed.transaction_id +assert len(parsed.attributes) == 1, len(parsed.attributes) +assert parsed.attributes[0].type == 0x0001, parsed.attributes[0].type +assert parsed.attributes[0].length == 8, parsed.attributes[0].length +assert parsed.attributes[0].address_family == 0x01, parsed.attributes[0].address_family +assert parsed.attributes[0].port == 3348, parsed.attributes[0].port +assert parsed.attributes[0].ip == "192.168.0.5", parsed.attributes[0].ip + += test STUN binding indication 1 + +raw = b"\x00\x11\x00\x08\x21\x12\xa4\x42\x29\x3d\x68\x7b\x0f\xbc\x44\x7c" \ + b"\x01\xb5\x8d\x2e\x80\x28\x00\x04\xc8\x84\xfe\x99" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding indication" +assert parsed.length == 8 +assert parsed.magic_cookie == 0x2112A442 +assert parsed.transaction_id == 0x293d687b0fbc447c01b58d2e, parsed.transaction_id +assert parsed.attributes == [ + STUNFingerprint(crc_32=0xc884fe99) +] + += test STUN binding indication 2 + +raw = b"\x00\x11\x00\x08\x21\x12\xa4\x42\x1d\x93\x57\xa1\xe9\x4a\x20\x51" \ + b"\x27\x19\x96\xd9\x80\x28\x00\x04\x53\x80\x0d\x81" + +parsed = STUN(raw) +assert parsed.RESERVED == 0x00, parsed.RESERVED +assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding indication" +assert parsed.length == 8 +assert parsed.magic_cookie == 0x2112A442 +assert parsed.transaction_id == 0x1d9357a1e94a2051271996d9, parsed.transaction_id +assert parsed.attributes == [ + STUNFingerprint(crc_32=0x53800d81) +] + += test STUN packet build +stun = STUN( + stun_message_type="Binding request", + transaction_id=0x7664047a24772b5748c0f173 +) +built = stun.build() +parsed = STUN(built) + +assert parsed.build() == built + += test STUN packet build with attributes +stun = STUN( + stun_message_type="Binding success response", + transaction_id=0x3479476534635936316a796a, + attributes=[ + STUNXorMappedAddress(xport=25000, xip="172.20.0.200"), + STUNUsername(length=37, username="Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25:oNph"), + STUNMessageIntegrity(hmac_sha1=0x4b67036dfb65ca84d63bcac86c8d5981df657031), + STUNFingerprint(crc_32=0x4041e9c3) + ] +) + +built = stun.build() +parsed = STUN(built) + +assert parsed.build() == built + += test STUN packet build IPv6 + +stun = STUN( + stun_message_type="Binding success response", + transaction_id=0x911b2532998da01cf9d053d9, + attributes=[ + STUNXorMappedAddress(xport=7621, address_family="IPv6", xip="::1") + ] +) +built = stun.build() +parsed = STUN(built) + +assert parsed.build() == built +assert parsed.attributes[0].length == 20 + += test STUN bottom up binding 1 + +udp = UDP(sport=62049, dport=3478) / STUN() +built = udp.build() +parsed = UDP(built) + +assert type(parsed.payload) == STUN, parsed.show(dump=True) + += test STUN bottom up binding 2 + +udp = UDP(sport=3478, dport=62049) / STUN(stun_message_type="Binding error response") +built = udp.build() +parsed = UDP(built) + +assert type(parsed.payload) == STUN, parsed.show(dump=True) + += test STUN top down binding + +udp = UDP() / STUN() +built = udp.build() +parsed = UDP(built) + +assert parsed.sport == 3478, parsed.sport +assert parsed.dport == 3478, parsed.dport diff --git a/test/contrib/tacacs.uts b/test/contrib/tacacs.uts index 4c4fd0ca803..94f07ad6a42 100644 --- a/test/contrib/tacacs.uts +++ b/test/contrib/tacacs.uts @@ -1,11 +1,12 @@ +# TACACS+ related regression tests +# +# Type the following command to launch the tests: +# $ test/run_tests -P "load_contrib('tacacs')" -t test/contrib/tacacs.uts + + Tacacs+ header = default instantiation -from scapy.consts import WINDOWS -if WINDOWS: - route_add_loopback() - pkt = IP()/TCP(dport=49)/TacacsHeader() raw(pkt) == b'E\x00\x004\x00\x01\x00\x00@\x06|\xc1\x7f\x00\x00\x01\x7f\x00\x00\x01\x001\x001\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xd0p\x00\x00\xc0\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -192,3 +193,14 @@ scapy.contrib.tacacs.SECRET = 'foobar' pkt = Ether(b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x009\x00\x01\x00\x00@\x06|\xbc\x7f\x00\x00\x01\x7f\x00\x00\x01\x001\x001\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00/,\x00\x00\xc0\x03\x02\x00\x1aM\x05\r\x00\x00\x00\x05S)\x9b\xb4\x92') pkt.status == 1 ++ Unencrypted Authentication + += instantiation + +pkt = IP()/TCP(dport=49)/TacacsHeader(seq=1, flags=1, session_id=2424164486, length=28)/TacacsAuthenticationStart(user_len=5, port_len=4, rem_addr_len=11, data_len=0, user='scapy', port='tty2', rem_addr='172.10.10.1') +raw(pkt) == b"E\x00\x00P\x00\x01\x00\x00@\x06|\xa5\x7f\x00\x00\x01\x7f\x00\x00\x01\x001\x001\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00sG\x00\x00\xc0\x01\x01\x01\x90}\xd0\x86\x00\x00\x00\x1c\x01\x01\x01\x01\x05\x04\x0b\x00scapytty2172.10.10.1" + += dissection + +pkt = Ether(b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00P\x00\x01\x00\x00@\x06|\xa5\x7f\x00\x00\x01\x7f\x00\x00\x01\x001\x001\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00sG\x00\x00\xc0\x01\x01\x01\x90}\xd0\x86\x00\x00\x00\x1c\x01\x01\x01\x01\x05\x04\x0b\x00scapytty2172.10.10.1') +pkt.user == b'scapy' and pkt.port == b'tty2' diff --git a/test/contrib/tcpao.uts b/test/contrib/tcpao.uts new file mode 100644 index 00000000000..50a39208003 --- /dev/null +++ b/test/contrib/tcpao.uts @@ -0,0 +1,374 @@ +% Tests for RFC5925 TCP Authentication Option (TCP-AO) +~ tcp tcpao +# Some data from https://datatracker.ietf.org/doc/html/draft-touch-tcpm-ao-test-vectors-02 + ++ Test Utilities += Test Utilities + +# Tolerate all whitespace like py37+ bytes.fromhex +def fromhex(hex): + return bytes(bytearray.fromhex(hex.replace(" ", "").replace("\n", ""))) + ++ TCP-AO Test Vectors += TCP-AO Test Vectors Utilities +from scapy.contrib import tcpao +master_key = b"testvector" +client_keyid = 61 +server_keyid = 84 + +def check( + packet_hex, + traffic_key_hex, + mac_hex, + src_isn, + dst_isn, + include_options=True, + alg_name="HMAC-SHA-1-96", + sne=0, + ): + packet_bytes = fromhex(packet_hex) + # sanity check for ip version + ipv = orb(packet_bytes[0]) >> 4 + if ipv == 4: + p = IP(fromhex(packet_hex)) + assert p[IP].proto == socket.IPPROTO_TCP + elif ipv == 6: + p = IPv6(fromhex(packet_hex)) + assert p[IPv6].nh == socket.IPPROTO_TCP + else: + raise ValueError("bad ipv={}".format(ipv)) + # sanity check for seq/ack in SYN/ACK packets + if p[TCP].flags.S and p[TCP].flags.A is False: + assert p[TCP].seq == src_isn + assert p[TCP].ack == 0 + if p[TCP].flags.S and p[TCP].flags.A: + assert p[TCP].seq == src_isn + assert p[TCP].ack == dst_isn + 1 + # check option bytes in header + opt = get_tcpao(p[TCP]) + assert opt is not None + assert opt.keyid in [client_keyid, server_keyid] + assert opt.rnextkeyid in [client_keyid, server_keyid] + assert opt.mac == fromhex(mac_hex), "match parsed mac" + # check traffic key + alg = get_alg(alg_name) + context_bytes = tcpao.build_context_from_packet(p, src_isn, dst_isn) + traffic_key = alg.kdf(master_key, context_bytes) + assert traffic_key == fromhex(traffic_key_hex), "match traffic key" + # check mac + message_bytes = tcpao.build_message_from_packet( + p, include_options=include_options, sne=sne + ) + mac = alg.mac(traffic_key, message_bytes) + assert mac == fromhex(mac_hex), "match computed mac" + += TCP-AO Test Vector 4.1.1: SHA-1 Send Syn +client_isn_41x = 0xFBFBAB5A +server_isn_41x = 0x11C14261 + +check( + """ + 45 e0 00 4c dd 0f 40 00 ff 06 bf 6b 0a 0b 0c 0d + ac 1b 1c 1d e9 d7 00 b3 fb fb ab 5a 00 00 00 00 + e0 02 ff ff ca c4 00 00 02 04 05 b4 01 03 03 08 + 04 02 08 0a 00 15 5a b7 00 00 00 00 1d 10 3d 54 + 2e e4 37 c6 f8 ed e6 d7 c4 d6 02 e7 + """, + "6d 63 ef 1b 02 fe 15 09 d4 b1 40 27 07 fd 7b 04 16 ab b7 4f", + "2e e4 37 c6 f8 ed e6 d7 c4 d6 02 e7", + client_isn_41x, + 0, +) + += TCP-AO Test Vector 4.1.2 SHA-1 Recv Syn-Ack +check( + """ + 45 e0 00 4c 65 06 40 00 ff 06 37 75 ac 1b 1c 1d + 0a 0b 0c 0d 00 b3 e9 d7 11 c1 42 61 fb fb ab 5b + e0 12 ff ff 37 76 00 00 02 04 05 b4 01 03 03 08 + 04 02 08 0a 84 a5 0b eb 00 15 5a b7 1d 10 54 3d + ee ab 0f e2 4c 30 10 81 51 16 b3 be + """, + "d9 e2 17 e4 83 4a 80 ca 2f 3f d8 de 2e 41 b8 e6 79 7f ea 96", + "ee ab 0f e2 4c 30 10 81 51 16 b3 be", + server_isn_41x, + client_isn_41x, +) + += TCP-AO Test Vector 4.1.3 SHA-1 Send Other +check( + """ + 45 e0 00 87 36 a1 40 00 ff 06 65 9f 0a 0b 0c 0d + ac 1b 1c 1d e9 d7 00 b3 fb fb ab 5b 11 c1 42 62 + c0 18 01 04 a1 62 00 00 01 01 08 0a 00 15 5a c1 + 84 a5 0b eb 1d 10 3d 54 70 64 cf 99 8c c6 c3 15 + c2 c2 e2 bf ff ff ff ff ff ff ff ff ff ff ff ff + ff ff ff ff 00 43 01 04 da bf 00 b4 0a 0b 0c 0d + 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02 + 00 02 02 42 00 02 06 41 04 00 00 da bf 02 08 40 + 06 00 64 00 01 01 00 + """, + "d2 e5 9c 65 ff c7 b1 a3 93 47 65 64 63 b7 0e dc 24 a1 3d 71", + "70 64 cf 99 8c c6 c3 15 c2 c2 e2 bf", + client_isn_41x, + server_isn_41x, +) + += TCP-AO Test Vector 4.1.4 SHA-1 Recv Other +check( + """ + 45 e0 00 87 1f a9 40 00 ff 06 7c 97 ac 1b 1c 1d + 0a 0b 0c 0d 00 b3 e9 d7 11 c1 42 62 fb fb ab 9e + c0 18 01 00 40 0c 00 00 01 01 08 0a 84 a5 0b f5 + 00 15 5a c1 1d 10 54 3d a6 3f 0e cb bb 2e 63 5c + 95 4d ea c7 ff ff ff ff ff ff ff ff ff ff ff ff + ff ff ff ff 00 43 01 04 da c0 00 b4 ac 1b 1c 1d + 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02 + 00 02 02 42 00 02 06 41 04 00 00 da c0 02 08 40 + 06 00 64 00 01 01 00 + """, + "d9 e2 17 e4 83 4a 80 ca 2f 3f d8 de 2e 41 b8 e6 79 7f ea 96", + "a6 3f 0e cb bb 2e 63 5c 95 4d ea c7", + server_isn_41x, + client_isn_41x, +) + += TCP-AO Test Vector 4.2.1 +client_isn_42x = 0xCB0EFBEE +server_isn_42x = 0xACD5B5E1 +check( + """ + 45 e0 00 4c 53 99 40 00 ff 06 48 e2 0a 0b 0c 0d + ac 1b 1c 1d ff 12 00 b3 cb 0e fb ee 00 00 00 00 + e0 02 ff ff 54 1f 00 00 02 04 05 b4 01 03 03 08 + 04 02 08 0a 00 02 4c ce 00 00 00 00 1d 10 3d 54 + 80 af 3c fe b8 53 68 93 7b 8f 9e c2 + """, + "30 ea a1 56 0c f0 be 57 da b5 c0 45 22 9f b1 0a 42 3c d7 ea", + "80 af 3c fe b8 53 68 93 7b 8f 9e c2", + client_isn_42x, + 0, + include_options=False, +) + += TCP-AO Test Vector 4.2.2 +check( + """ + 45 e0 00 4c 32 84 40 00 ff 06 69 f7 ac 1b 1c 1d + 0a 0b 0c 0d 00 b3 ff 12 ac d5 b5 e1 cb 0e fb ef + e0 12 ff ff 38 8e 00 00 02 04 05 b4 01 03 03 08 + 04 02 08 0a 57 67 72 f3 00 02 4c ce 1d 10 54 3d + 09 30 6f 9a ce a6 3a 8c 68 cb 9a 70 + """, + "b5 b2 89 6b b3 66 4e 81 76 b0 ed c6 e7 99 52 41 01 a8 30 7f", + "09 30 6f 9a ce a6 3a 8c 68 cb 9a 70", + server_isn_42x, + client_isn_42x, + include_options=False, +) + += TCP-AO Test Vector 4.2.3 +check( + """ + 45 e0 00 87 a8 f5 40 00 ff 06 f3 4a 0a 0b 0c 0d + ac 1b 1c 1d ff 12 00 b3 cb 0e fb ef ac d5 b5 e2 + c0 18 01 04 6c 45 00 00 01 01 08 0a 00 02 4c ce + 57 67 72 f3 1d 10 3d 54 71 06 08 cc 69 6c 03 a2 + 71 c9 3a a5 ff ff ff ff ff ff ff ff ff ff ff ff + ff ff ff ff 00 43 01 04 da bf 00 b4 0a 0b 0c 0d + 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02 + 00 02 02 42 00 02 06 41 04 00 00 da bf 02 08 40 + 06 00 64 00 01 01 00 + """, + "f3 db 17 93 d7 91 0e cd 80 6c 34 f1 55 ea 1f 00 34 59 53 e3", + "71 06 08 cc 69 6c 03 a2 71 c9 3a a5", + client_isn_42x, + server_isn_42x, + include_options=False, +) + += TCP-AO Test Vector 4.2.4 +check( + """ + 45 e0 00 87 54 37 40 00 ff 06 48 09 ac 1b 1c 1d + 0a 0b 0c 0d 00 b3 ff 12 ac d5 b5 e2 cb 0e fc 32 + c0 18 01 00 46 b6 00 00 01 01 08 0a 57 67 72 f3 + 00 02 4c ce 1d 10 54 3d 97 76 6e 48 ac 26 2d e9 + ae 61 b4 f9 ff ff ff ff ff ff ff ff ff ff ff ff + ff ff ff ff 00 43 01 04 da c0 00 b4 ac 1b 1c 1d + 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02 + 00 02 02 42 00 02 06 41 04 00 00 da c0 02 08 40 + 06 00 64 00 01 01 00 + """, + "b5 b2 89 6b b3 66 4e 81 76 b0 ed c6 e7 99 52 41 01 a8 30 7f", + "97 76 6e 48 ac 26 2d e9 ae 61 b4 f9", + server_isn_42x, + client_isn_42x, + include_options=False, +) + += TCP-AO Test Vector 5.1.1 +check( + """ + 45 e0 00 4c 7b 9f 40 00 ff 06 20 dc 0a 0b 0c 0d + ac 1b 1c 1d c4 fa 00 b3 78 7a 1d df 00 00 00 00 + e0 02 ff ff 5a 0f 00 00 02 04 05 b4 01 03 03 08 + 04 02 08 0a 00 01 7e d0 00 00 00 00 1d 10 3d 54 + e4 77 e9 9c 80 40 76 54 98 e5 50 91 + """, + "f5 b8 b3 d5 f3 4f db b6 eb 8d 4a b9 66 0e 60 e3", + "e4 77 e9 9c 80 40 76 54 98 e5 50 91", + 0x787A1DDF, + 0, + include_options=True, + alg_name="AES-128-CMAC-96", +) + += TCP-AO Test Vector 6.1.1 +client_isn_61x = 0x176A833F +server_isn_61x = 0x3F51994B +check( + """ + 6e 08 91 dc 00 38 06 40 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 01 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 02 f7 e4 00 b3 17 6a 83 3f + 00 00 00 00 e0 02 ff ff 47 21 00 00 02 04 05 a0 + 01 03 03 08 04 02 08 0a 00 41 d0 87 00 00 00 00 + 1d 10 3d 54 90 33 ec 3d 73 34 b6 4c 5e dd 03 9f + """, + "62 5e c0 9d 57 58 36 ed c9 b6 42 84 18 bb f0 69 89 a3 61 bb", + "90 33 ec 3d 73 34 b6 4c 5e dd 03 9f", + client_isn_61x, + 0, + include_options=True, +) + += TCP-AO Test Vector 6.1.2 +check( + """ + 6e 01 00 9e 00 38 06 40 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 01 00 b3 f7 e4 3f 51 99 4b + 17 6a 83 40 e0 12 ff ff bf ec 00 00 02 04 05 a0 + 01 03 03 08 04 02 08 0a bd 33 12 9b 00 41 d0 87 + 1d 10 54 3d f1 cb a3 46 c3 52 61 63 f7 1f 1f 55 + """, + "e4 a3 7a da 2a 0a fc a8 71 14 34 91 3f e1 38 c7 71 eb cb 4a", + "f1 cb a3 46 c3 52 61 63 f7 1f 1f 55", + server_isn_61x, + client_isn_61x, + include_options=True, +) + += TCP-AO Test Vector 6.2.2 +client_isn_62x = 0x020C1E69 +server_isn_62x = 0xEBA3734D +check( + """ + 6e 0a 7e 1f 00 38 06 40 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 01 00 b3 c6 cd eb a3 73 4d + 02 0c 1e 6a e0 12 ff ff 77 4d 00 00 02 04 05 a0 + 01 03 03 08 04 02 08 0a 5e c9 9b 70 00 9d b9 5b + 1d 10 54 3d 3c 54 6b ad 97 43 f1 2d f8 b8 01 0d + """, + "40 51 08 94 7f 99 65 75 e7 bd bc 26 d4 02 16 a2 c7 fa 91 bd", + "3c 54 6b ad 97 43 f1 2d f8 b8 01 0d", + server_isn_62x, + client_isn_62x, + include_options=False, +) + += TCP-AO Test Vector 6.2.4 +check( + """ + 6e 0a 7e 1f 00 73 06 40 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 01 00 b3 c6 cd eb a3 73 4e + 02 0c 1e ad c0 18 01 00 71 6a 00 00 01 01 08 0a + 5e c9 9b 7a 00 9d b9 65 1d 10 54 3d 55 9a 81 94 + 45 b4 fd e9 8d 9e 13 17 ff ff ff ff ff ff ff ff + ff ff ff ff ff ff ff ff 00 43 01 04 fd e8 00 b4 + 01 01 01 7a 26 02 06 01 04 00 01 00 01 02 02 80 + 00 02 02 02 00 02 02 42 00 02 06 41 04 00 00 fd + e8 02 08 40 06 00 64 00 01 01 00 + """, + "40 51 08 94 7f 99 65 75 e7 bd bc 26 d4 02 16 a2 c7 fa 91 bd", + "55 9a 81 94 45 b4 fd e9 8d 9e 13 17", + server_isn_62x, + client_isn_62x, + include_options=False, +) + += TCP-AO Test Vector 7.1.2 +server_isn_71x = 0xA6744ECB +client_isn_71x = 0x193CCCEC +check( + """ + 6e 06 15 20 00 38 06 40 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 01 00 b3 f8 5a a6 74 4e cb + 19 3c cc ed e0 12 ff ff ea bb 00 00 02 04 05 a0 + 01 03 03 08 04 02 08 0a 71 da ab c8 13 e4 ab 99 + 1d 10 54 3d dc 28 43 a8 4e 78 a6 bc fd c5 ed 80 + """, + "cf 1b 1e 22 5e 06 a6 36 16 76 4a 06 7b 46 f4 b1", + "dc 28 43 a8 4e 78 a6 bc fd c5 ed 80", + server_isn_71x, + client_isn_71x, + alg_name="AES-128-CMAC-96", + include_options=True, +) + += TCP-AO Test Vector 7.1.4 +check( + """ + 6e 06 15 20 00 73 06 40 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 01 00 b3 f8 5a a6 74 4e cc + 19 3c cd 30 c0 18 01 00 52 f4 00 00 01 01 08 0a + 71 da ab d3 13 e4 ab a3 1d 10 54 3d c1 06 9b 7d + fd 3d 69 3a 6d f3 f2 89 ff ff ff ff ff ff ff ff + ff ff ff ff ff ff ff ff 00 43 01 04 fd e8 00 b4 + 01 01 01 7a 26 02 06 01 04 00 01 00 01 02 02 80 + 00 02 02 02 00 02 02 42 00 02 06 41 04 00 00 fd + e8 02 08 40 06 00 64 00 01 01 00 + """, + "cf 1b 1e 22 5e 06 a6 36 16 76 4a 06 7b 46 f4 b1", + "c1 06 9b 7d fd 3d 69 3a 6d f3 f2 89", + server_isn_71x, + client_isn_71x, + alg_name="AES-128-CMAC-96", + include_options=True, +) + ++ TCP-AO Signature API += TCP-AO sign SYN packet build from scratch +master_key = b"hello" +alg = TCPAOAlg_HMAC_SHA1() +keyid = 12 +rnextkeyid = 34 + +p = IP() / TCP() +p[TCP].flags == "S" +sisn = p[TCP].seq +disn = 0 + +# sign +traffic_key = calc_tcpao_traffic_key(p, alg, master_key, sisn, disn) +sign_tcpao(p, alg, traffic_key, keyid, rnextkeyid) +mac = calc_tcpao_mac(p, alg, traffic_key) + +# parse +p2 = IP(raw(p)) +ao = get_tcpao(p2[TCP]) +ao is not None +ao.keyid == keyid +ao.rnextkeyid == rnextkeyid +ao.mac == mac + +# calculate signature again on parsed packet +traffic_key2 = calc_tcpao_traffic_key(p2, alg, master_key, p2[TCP].seq, 0) +traffic_key == traffic_key2 +mac2 = calc_tcpao_mac(p2, alg, traffic_key2) +mac == mac2 diff --git a/test/contrib/tcpros.uts b/test/contrib/tcpros.uts new file mode 100644 index 00000000000..04468b6bb70 --- /dev/null +++ b/test/contrib/tcpros.uts @@ -0,0 +1,58 @@ +% TCPROS transport layer for ROS Melodic Morenia 1.14.5 dissection +% +% Copyright (C) Víctor Mayoral-Vilches +% +% This program is free software; you can redistribute it and/or modify it under +% the terms of the GNU General Public License as published by the Free Software +% Foundation; either version 2 of the License, or (at your option) any later +% version. +% +% This program is distributed in the hope that it will be useful, but WITHOUT ANY +% WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +% PARTICULAR PURPOSE. See the GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License along with +% this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +% Street, Fifth Floor, Boston, MA 02110-1301, USA. + +% TCPROS layer test campaign + ++ Syntax check += Import the RTPS layer +from scapy.contrib.tcpros import * + +bind_layers(TCP, TCPROS, sport=11311) +bind_layers(HTTPRequest, XMLRPC) +bind_layers(HTTPResponse, XMLRPC) + +pkt = b"POST /RPC2 HTTP/1.1\r\nAccept-Encoding: gzip\r\nContent-Length: " \ + b"227\r\nContent-Type: text/xml\r\nHost: 12.0.0.2:11311\r\nUser-Agent:" \ + b"xmlrpclib.py/1.0.1 (by www.pythonware.com)\r\n\r\n\n\nshutdown\n" \ + b"\n\n/rosparam-92418\n" \ + b"\n\nBOOM" \ + b"\n\n\n\n" + +p = TCPROS(pkt) + ++ Test TCPROS += Test basic package composition +assert(HTTP in p) +assert(HTTPRequest in p) +assert(XMLRPC in p) +assert(XMLRPCCall in p) + += Test HTTPRequest within TCPROS +assert(p[HTTPRequest].Content_Length == b'227') +assert(p[HTTPRequest].Content_Type == b'text/xml') +assert(p[HTTPRequest].Host == b'12.0.0.2:11311') +assert(p[HTTPRequest].User_Agent == b'xmlrpclib.py/1.0.1 (by www.pythonware.com)') +assert(p[HTTPRequest].Method == b'POST') +assert(p[HTTPRequest].Path == b'/RPC2') +assert(p[HTTPRequest].Http_Version == b'HTTP/1.1') + += Test XMLRPCCall within TCPROS +assert(p[XMLRPCCall].version == b"\n") +assert(p[XMLRPCCall].methodcall_opentag == b'\n') +assert(p[XMLRPCCall].methodname == b'shutdown') +assert(p[XMLRPCCall].params == b'\n/rosparam-92418\n\n\nBOOM\n\n') diff --git a/test/contrib/tzsp.uts b/test/contrib/tzsp.uts index 965565e0d84..45ea280282c 100644 --- a/test/contrib/tzsp.uts +++ b/test/contrib/tzsp.uts @@ -21,8 +21,8 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02')/ \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_KEEPALIVE) -assert(not tzsp_lyr.payload) +assert tzsp_lyr.type == TZSP.TYPE_KEEPALIVE +assert not tzsp_lyr.payload == basic TZSP header - keepalive + ignored end tag @@ -37,8 +37,8 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02')/ \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_KEEPALIVE) -assert(tzsp_lyr.guess_payload_class(tzsp_lyr.payload) is scapy.packet.Raw) +assert tzsp_lyr.type == TZSP.TYPE_KEEPALIVE +assert tzsp_lyr.guess_payload_class(tzsp_lyr.payload) is scapy.packet.Raw == basic TZSP header with RX Packet and EndTag @@ -55,14 +55,14 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_end = tzsp_lyr.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 encapsulated_payload = tzsp_lyr.get_encapsulated_payload() encapsulated_ether_lyr = encapsulated_payload.getlayer(Ether) -assert(encapsulated_ether_lyr.src == '00:03:03:03:03:03') +assert encapsulated_ether_lyr.src == '00:03:03:03:03:03' == basic TZSP header with RX Packet and Padding @@ -80,13 +80,13 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_padding = tzsp_lyr.payload -assert(tzsp_tag_padding.type == 0) +assert tzsp_tag_padding.type == 0 tzsp_tag_end = tzsp_tag_padding.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and RAWRSSI (byte, short) @@ -105,18 +105,18 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_raw_rssi_byte = tzsp_lyr.payload -assert(tzsp_tag_raw_rssi_byte.type == 10) -assert(tzsp_tag_raw_rssi_byte.raw_rssi == 42) +assert tzsp_tag_raw_rssi_byte.type == 10 +assert tzsp_tag_raw_rssi_byte.raw_rssi == 42 tzsp_tag_raw_rssi_short = tzsp_tag_raw_rssi_byte.payload -assert(tzsp_tag_raw_rssi_short.type == 10) -assert(tzsp_tag_raw_rssi_short.raw_rssi == 12345) +assert tzsp_tag_raw_rssi_short.type == 10 +assert tzsp_tag_raw_rssi_short.raw_rssi == 12345 tzsp_tag_end = tzsp_tag_raw_rssi_short.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and SNR (byte, short) @@ -135,20 +135,20 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_snr_byte = tzsp_lyr.payload -assert(tzsp_tag_snr_byte.type == 11) -assert(tzsp_tag_snr_byte.len == 1) -assert(tzsp_tag_snr_byte.snr == 23) +assert tzsp_tag_snr_byte.type == 11 +assert tzsp_tag_snr_byte.len == 1 +assert tzsp_tag_snr_byte.snr == 23 tzsp_tag_snr_short = tzsp_tag_snr_byte.payload -assert(tzsp_tag_snr_short.type == 11) -assert(tzsp_tag_snr_short.len == 2) -assert(tzsp_tag_snr_short.snr == 54321) +assert tzsp_tag_snr_short.type == 11 +assert tzsp_tag_snr_short.len == 2 +assert tzsp_tag_snr_short.snr == 54321 tzsp_tag_end = tzsp_tag_snr_short.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and DATA Rate @@ -166,15 +166,15 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_data_rate = tzsp_lyr.payload -assert(tzsp_tag_data_rate.type == 12) -assert(tzsp_tag_data_rate.len == 1) -assert(tzsp_tag_data_rate.data_rate == TZSPTagDataRate.DATA_RATE_33) +assert tzsp_tag_data_rate.type == 12 +assert tzsp_tag_data_rate.len == 1 +assert tzsp_tag_data_rate.data_rate == TZSPTagDataRate.DATA_RATE_33 tzsp_tag_end = tzsp_tag_data_rate.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and Timestamp @@ -192,15 +192,15 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_timestamp = tzsp_lyr.payload -assert(tzsp_tag_timestamp.type == 13) -assert(tzsp_tag_timestamp.len == 4) -assert(tzsp_tag_timestamp.timestamp == 0x11223344) +assert tzsp_tag_timestamp.type == 13 +assert tzsp_tag_timestamp.len == 4 +assert tzsp_tag_timestamp.timestamp == 0x11223344 tzsp_tag_end = tzsp_tag_timestamp.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and ContentionFree @@ -219,20 +219,20 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_contention_free_no = tzsp_lyr.payload -assert(tzsp_tag_contention_free_no.type == 15) -assert(tzsp_tag_contention_free_no.len == 1) -assert(tzsp_tag_contention_free_no.contention_free == TZSPTagContentionFree.NO) +assert tzsp_tag_contention_free_no.type == 15 +assert tzsp_tag_contention_free_no.len == 1 +assert tzsp_tag_contention_free_no.contention_free == TZSPTagContentionFree.NO tzsp_tag_contention_free_yes = tzsp_tag_contention_free_no.payload -assert(tzsp_tag_contention_free_yes.type == 15) -assert(tzsp_tag_contention_free_yes.len == 1) -assert(tzsp_tag_contention_free_yes.contention_free == TZSPTagContentionFree.YES) +assert tzsp_tag_contention_free_yes.type == 15 +assert tzsp_tag_contention_free_yes.len == 1 +assert tzsp_tag_contention_free_yes.contention_free == TZSPTagContentionFree.YES tzsp_tag_end = tzsp_tag_contention_free_yes.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and Decrypted @@ -251,20 +251,20 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_decrypted_no = tzsp_lyr.payload -assert(tzsp_tag_decrypted_no.type == 16) -assert(tzsp_tag_decrypted_no.len == 1) -assert(tzsp_tag_decrypted_no.decrypted == TZSPTagDecrypted.NO) +assert tzsp_tag_decrypted_no.type == 16 +assert tzsp_tag_decrypted_no.len == 1 +assert tzsp_tag_decrypted_no.decrypted == TZSPTagDecrypted.NO tzsp_tag_decrypted_yes= tzsp_tag_decrypted_no.payload -assert(tzsp_tag_decrypted_yes.type == 16) -assert(tzsp_tag_decrypted_yes.len == 1) -assert(tzsp_tag_decrypted_yes.decrypted == TZSPTagDecrypted.YES) +assert tzsp_tag_decrypted_yes.type == 16 +assert tzsp_tag_decrypted_yes.len == 1 +assert tzsp_tag_decrypted_yes.decrypted == TZSPTagDecrypted.YES tzsp_tag_end = tzsp_tag_decrypted_yes.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and FCS error @@ -283,20 +283,20 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_error_no = tzsp_lyr.payload -assert(tzsp_tag_error_no.type == 17) -assert(tzsp_tag_error_no.len == 1) -assert(tzsp_tag_error_no.fcs_error == TZSPTagError.NO) +assert tzsp_tag_error_no.type == 17 +assert tzsp_tag_error_no.len == 1 +assert tzsp_tag_error_no.fcs_error == TZSPTagError.NO tzsp_tag_error_yes = tzsp_tag_error_no.payload -assert(tzsp_tag_error_yes.type == 17) -assert(tzsp_tag_error_yes.len == 1) -assert(tzsp_tag_error_yes.fcs_error == TZSPTagError.YES) +assert tzsp_tag_error_yes.type == 17 +assert tzsp_tag_error_yes.len == 1 +assert tzsp_tag_error_yes.fcs_error == TZSPTagError.YES tzsp_tag_end = tzsp_tag_error_yes.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and RXChannel @@ -314,15 +314,15 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_rx_channel = tzsp_lyr.payload -assert(tzsp_tag_rx_channel.type == 18) -assert(tzsp_tag_rx_channel.len == 1) -assert(tzsp_tag_rx_channel.rx_channel == 123) +assert tzsp_tag_rx_channel.type == 18 +assert tzsp_tag_rx_channel.len == 1 +assert tzsp_tag_rx_channel.rx_channel == 123 tzsp_tag_end = tzsp_tag_rx_channel.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and Packet count @@ -340,15 +340,15 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_packet_count = tzsp_lyr.payload -assert(tzsp_tag_packet_count.type == 40) -assert(tzsp_tag_packet_count.len == 4) -assert(tzsp_tag_packet_count.packet_count == 0x44332211) +assert tzsp_tag_packet_count.type == 40 +assert tzsp_tag_packet_count.len == 4 +assert tzsp_tag_packet_count.packet_count == 0x44332211 tzsp_tag_end = tzsp_tag_packet_count.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and RXFrameLength @@ -366,15 +366,15 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_frame_length = tzsp_lyr.payload -assert(tzsp_tag_frame_length.type == 41) -assert(tzsp_tag_frame_length.len == 2) -assert(tzsp_tag_frame_length.rx_frame_length == 0xbad0) +assert tzsp_tag_frame_length.type == 41 +assert tzsp_tag_frame_length.len == 2 +assert tzsp_tag_frame_length.rx_frame_length == 0xbad0 tzsp_tag_end = tzsp_tag_frame_length.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == basic TZSP header with RX Packet and WLAN RADIO HDR SERIAL @@ -394,15 +394,15 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02') / \ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr.type == TZSP.TYPE_RX_PACKET) +assert tzsp_lyr.type == TZSP.TYPE_RX_PACKET tzsp_tag_sensor_id = tzsp_lyr.payload -assert(tzsp_tag_sensor_id.type == 60) -assert(tzsp_tag_sensor_id.len == len(SENSOR_ID)) -assert(tzsp_tag_sensor_id.sensor_id == SENSOR_ID) +assert tzsp_tag_sensor_id.type == 60 +assert tzsp_tag_sensor_id.len == len(SENSOR_ID) +assert tzsp_tag_sensor_id.sensor_id == SENSOR_ID tzsp_tag_end = tzsp_tag_sensor_id.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 == handling of unknown tag @@ -424,9 +424,9 @@ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr) +assert tzsp_lyr tzsp_tag_unknown = tzsp_lyr.payload -assert(type(tzsp_tag_unknown) is TZSPTagUnknown) +assert type(tzsp_tag_unknown) is TZSPTagUnknown = all layers stacked @@ -463,62 +463,62 @@ frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) tzsp_raw_rssi_byte_lyr = tzsp_lyr.payload -assert(tzsp_raw_rssi_byte_lyr.type == 10) +assert tzsp_raw_rssi_byte_lyr.type == 10 tzsp_tag_raw_rssi_short = tzsp_raw_rssi_byte_lyr.payload -assert(tzsp_tag_raw_rssi_short.type == 10) +assert tzsp_tag_raw_rssi_short.type == 10 tzsp_tag_snr_byte = tzsp_tag_raw_rssi_short.payload -assert(tzsp_tag_snr_byte.type == 11) +assert tzsp_tag_snr_byte.type == 11 tzsp_tag_snr_short = tzsp_tag_snr_byte.payload -assert(tzsp_tag_snr_short.type == 11) +assert tzsp_tag_snr_short.type == 11 tzsp_tag_data_rate = tzsp_tag_snr_short.payload -assert(tzsp_tag_data_rate.type == 12) +assert tzsp_tag_data_rate.type == 12 tzsp_tag_timestamp = tzsp_tag_data_rate.payload -assert(tzsp_tag_timestamp.type == 13) +assert tzsp_tag_timestamp.type == 13 tzsp_tag_contention_free_no = tzsp_tag_timestamp.payload -assert(tzsp_tag_contention_free_no.type == 15) +assert tzsp_tag_contention_free_no.type == 15 tzsp_tag_contention_free_yes = tzsp_tag_contention_free_no.payload -assert(tzsp_tag_contention_free_yes.type == 15) +assert tzsp_tag_contention_free_yes.type == 15 tzsp_tag_decrypted_no = tzsp_tag_contention_free_yes.payload -assert(tzsp_tag_decrypted_no.type == 16) +assert tzsp_tag_decrypted_no.type == 16 tzsp_tag_decrypted_yes = tzsp_tag_decrypted_no.payload -assert(tzsp_tag_decrypted_yes.type == 16) +assert tzsp_tag_decrypted_yes.type == 16 tzsp_tag_error_yes = tzsp_tag_decrypted_yes.payload -assert(tzsp_tag_error_yes.type == 17) +assert tzsp_tag_error_yes.type == 17 tzsp_tag_error_no = tzsp_tag_error_yes.payload -assert(tzsp_tag_error_no.type == 17) +assert tzsp_tag_error_no.type == 17 tzsp_tag_rx_channel = tzsp_tag_error_no.payload -assert(tzsp_tag_rx_channel.type == 18) +assert tzsp_tag_rx_channel.type == 18 tzsp_tag_packet_count = tzsp_tag_rx_channel.payload -assert(tzsp_tag_packet_count.type == 40) +assert tzsp_tag_packet_count.type == 40 tzsp_tag_frame_length = tzsp_tag_packet_count.payload -assert(tzsp_tag_frame_length.type == 41) +assert tzsp_tag_frame_length.type == 41 tzsp_tag_sensor_id = tzsp_tag_frame_length.payload -assert(tzsp_tag_sensor_id.type == 60) +assert tzsp_tag_sensor_id.type == 60 tzsp_tag_padding = tzsp_tag_sensor_id.payload -assert(tzsp_tag_padding.type == 0) +assert tzsp_tag_padding.type == 0 tzsp_tag_end = tzsp_tag_padding.payload -assert(tzsp_tag_end.type == 1) +assert tzsp_tag_end.type == 1 encapsulated_payload = tzsp_tag_end.payload encapsulated_ether_lyr = encapsulated_payload.getlayer(Ether) -assert(encapsulated_ether_lyr.src == '00:03:03:03:03:03') +assert encapsulated_ether_lyr.src == '00:03:03:03:03:03' + corner cases @@ -538,11 +538,11 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02')/ \ frm = frm.build() frm = Ether(frm) tzsp_tag_contention_free = frm.getlayer(TZSPTagContentionFree) -assert(tzsp_tag_contention_free) +assert tzsp_tag_contention_free tzsp_tag_contention_free_attr = tzsp_tag_contention_free.get_field('contention_free') -assert(tzsp_tag_contention_free_attr) +assert tzsp_tag_contention_free_attr symb_str = tzsp_tag_contention_free_attr.i2repr(tzsp_tag_contention_free, tzsp_tag_contention_free.contention_free) -assert(symb_str == 'yes') +assert symb_str == 'yes' == TZSPTagError @@ -558,11 +558,11 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02')/ \ frm = frm.build() frm = Ether(frm) tzsp_tag_error = frm.getlayer(TZSPTagError) -assert(tzsp_tag_error) +assert tzsp_tag_error tzsp_tag_error_attr = tzsp_tag_error.get_field('fcs_error') -assert(tzsp_tag_error_attr) +assert tzsp_tag_error_attr symb_str = tzsp_tag_error_attr.i2repr(tzsp_tag_error, tzsp_tag_error.fcs_error) -assert(symb_str == 'no') +assert symb_str == 'no' frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02')/ \ IP(src='1.1.1.1', dst='2.2.2.2')/ \ @@ -574,11 +574,11 @@ frm = Ether(src='00:01:01:01:01:01', dst='00:02:02:02:02:02')/ \ frm = frm.build() frm = Ether(frm) tzsp_tag_error = frm.getlayer(TZSPTagError) -assert(tzsp_tag_error) +assert tzsp_tag_error tzsp_tag_error_attr = tzsp_tag_error.get_field('fcs_error') -assert(tzsp_tag_error_attr) +assert tzsp_tag_error_attr symb_str = tzsp_tag_error_attr.i2repr(tzsp_tag_error, tzsp_tag_error.fcs_error) -assert(symb_str == 'reserved') +assert symb_str == 'reserved' == missing TZSP header before end tag @@ -608,7 +608,7 @@ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(type(tzsp_lyr.payload) is Raw ) +assert type(tzsp_lyr.payload) is Raw == handling of unknown tag - payload to short @@ -626,10 +626,10 @@ frm = frm.build() frm = Ether(frm) frm.show() tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr) +assert tzsp_lyr raw_lyr = tzsp_lyr.payload -assert(type(raw_lyr) is Raw) -assert(raw_lyr.load == b'\xff\x0a\x01\x02\x03\x04\x05') +assert type(raw_lyr) is Raw +assert raw_lyr.load == b'\xff\x0a\x01\x02\x03\x04\x05' == handling of unknown tag - no payload after tag type @@ -647,7 +647,7 @@ frm = frm.build() frm = Ether(frm) tzsp_lyr = frm.getlayer(TZSP) -assert(tzsp_lyr) +assert tzsp_lyr raw_lyr = tzsp_lyr.payload -assert(type(raw_lyr) is Raw) -assert(raw_lyr.load == b'\xff') +assert type(raw_lyr) is Raw +assert raw_lyr.load == b'\xff' diff --git a/test/contrib/vqp.uts b/test/contrib/vqp.uts new file mode 100644 index 00000000000..c4559452ff2 --- /dev/null +++ b/test/contrib/vqp.uts @@ -0,0 +1,21 @@ +% VQP tests + ++ Basic VQP tests + += Build VQP + +pkt = UDP()/VQP(type=2, + seq=15)/VQPEntry(datatype=3073,data="1.2.3.4")/VQPEntry(datatype=3078, + data="AA:AA:AA:AA:AA:AA") + +assert bytes(pkt) == b'\x065\x065\x00&\x00\x00\x01\x02\x00\x02\x00\x00\x00\x0f\x00\x00\x0c\x01\x00\x04\x01\x02\x03\x04\x00\x00\x0c\x06\x00\x06\xaa\xaa\xaa\xaa\xaa\xaa' + += Dissect VQP + +pkt = UDP(b'\x065\x065\x00&\x00\x00\x01\x02\x00\x02\x00\x00\x00\x0f\x00\x00\x0c\x01\x00\x04\x01\x02\x03\x04\x00\x00\x0c\x06\x00\x06\xaa\xaa\xaa\xaa\xaa\xaa') + +assert pkt[VQP].sprintf("%type%") == "responseVLAN" +assert pkt.getlayer(VQPEntry, 1).len == 4 +assert pkt.getlayer(VQPEntry, 1).sprintf("%datatype%") == "clientIPAddress" +assert pkt.getlayer(VQPEntry, 2).len == 6 +assert pkt.getlayer(VQPEntry, 2).sprintf("%datatype%") == "ReqMACAddress" diff --git a/test/dot15d4.uts b/test/dot15d4.uts deleted file mode 100644 index 518b35271a2..00000000000 --- a/test/dot15d4.uts +++ /dev/null @@ -1,451 +0,0 @@ -% Regression tests for the Dot15D4, SixLoWPAN and Zigbee layers - -################### -##### Dot15D4 ##### -################### - -+ Dot15D4 tests - -= Dot15D4 layers - -# a crazy packet with all classes in it! -pkt = Dot15d4()/Dot15d4Ack()/Dot15d4AuxSecurityHeader()/Dot15d4Beacon()/Dot15d4Cmd()/Dot15d4CmdAssocReq()/Dot15d4CmdAssocResp()/Dot15d4CmdCoordRealign()/Dot15d4CmdDisassociation()/Dot15d4CmdGTSReq()/Dot15d4Data()/Dot15d4FCS() -assert Dot15d4 in pkt.layers() -assert Dot15d4Ack in pkt.layers() -assert Dot15d4AuxSecurityHeader in pkt.layers() -assert Dot15d4Beacon in pkt.layers() -assert Dot15d4Cmd in pkt.layers() -assert Dot15d4CmdAssocReq in pkt.layers() -assert Dot15d4CmdAssocResp in pkt.layers() -assert Dot15d4CmdCoordRealign in pkt.layers() -assert Dot15d4CmdDisassociation in pkt.layers() -assert Dot15d4CmdGTSReq in pkt.layers() -assert Dot15d4Data in pkt.layers() -assert Dot15d4FCS in pkt.layers() - -= Dot15d4FCS parent matching - -pkt = Ether()/IP()/Dot15d4FCS() -assert pkt[Dot15d4] - -################### -#### SixLoWPAN #### -################### - -+ SixLoWPAN tests - -= Set SixLoWPAN - -conf.dot15d4_protocol = "sixlowpan" - -= SixLoWPAN layers - -# a crazy packet with all classes in it! -pkt = SixLoWPAN()/LoWPANFragmentationFirst()/LoWPANFragmentationSubsequent()/LoWPANMesh()/LoWPANUncompressedIPv6() -assert SixLoWPAN in pkt.layers() -assert LoWPANFragmentationFirst in pkt.layers() -assert LoWPANFragmentationSubsequent in pkt.layers() -assert LoWPANMesh in pkt.layers() -assert LoWPANUncompressedIPv6 in pkt.layers() - -= Default dissection - -# some sample packet extracted - -lowpan_frag_first = b'\xc29\x00\x17`\x00\x00\x00\x00\x00\x00\x00 \x02\r\xb8\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x01 \x02\r\xb8\x00\x00\x00\x00\x00\x11"\xff\xfe3DU\xc4\xf9\x00Pw\x9b\x18\x9d\x00\x00\x01\xa2P\x18\x13X\x08\x10\x00\x00GET / HTTP/1.1\r\nHost: [aaaa::11:22ff' -lowpan_frag_first_packet = SixLoWPAN(lowpan_frag_first) - -assert lowpan_frag_first_packet.load == b'`\x00\x00\x00\x00\x00\x00\x00 \x02\r\xb8\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x01 \x02\r\xb8\x00\x00\x00\x00\x00\x11"\xff\xfe3DU\xc4\xf9\x00Pw\x9b\x18\x9d\x00\x00\x01\xa2P\x18\x13X\x08\x10\x00\x00GET / HTTP/1.1\r\nHost: [aaaa::11:22ff' - -= Frag second dissection - -lowpan_frag_second = b'\xe29\x00\x17\x0c`\x00\x00\x00\x00\x00\x00\x00 \x02\r\xb8\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x01 \x02\r\xb8\x00\x00\x00\x00\x00\x11"\xff\xfe3DUerer: http://[aaaa::11:22ff:fe33:4455]/sensor.shtml\r\nUse' -lowpan_frag_sec_packet = SixLoWPAN(lowpan_frag_second) - -assert LoWPANFragmentationSubsequent in lowpan_frag_sec_packet -assert lowpan_frag_sec_packet.datagramSize == 569 -assert lowpan_frag_sec_packet.datagramTag == 0x17 - -= LoWPAN_IPHC dissections - -lowpan_iphc = b"\x78\xf6\x00\x06\x80\x00\x01\x00\x50\xc4\xf9\x00\x00\x02\x12\x77\x9b\x1a\x9a\x50\x18\x04\xc4\x12\xd5\x00\x00\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x48\x54\x4d\x4c\x20\x50\x55\x42\x4c\x49\x43\x20\x22\x2d\x2f\x2f\x57\x33\x43\x2f\x2f\x44\x54\x44\x20\x48\x54\x4d\x4c\x20\x34\x2e\x30\x31\x20\x54\x72\x61\x6e\x73\x69\x74\x69\x6f\x6e\x61\x6c\x2f\x2f\x45\x4e\x22\x20\x22\x68\x74\x74\x70" -lowpan_frag_iphc = LoWPAN_IPHC(lowpan_iphc) - -assert IPv6 in lowpan_frag_iphc -assert lowpan_frag_iphc.load == b'\x0c\x00\x00@\x11\xe1\x95\xac\x10\x01\x90\xac\x10\x014EZEZ\x00S\xda\x93EX\x02\x01\x03\x00Y\x01\xff\x00\x02\xab\xa2\x81\xba\xc2\xdf\x00\x00<\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+A\x88U\xaa\x1b\xff\xfffU{;:\x1a\x9b\x01uE\x00\xf1\x03Z\x8b\xf0\x00\x00\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x11"lF' -ack_frame = b'\x00"\x19\x100\xe5\x00\x1c\xda\x00\x10\x04\x08\x00E\x00\x00A>\x0e\x00\x00@\x11\xe1\xb9\xac\x10\x01\x90\xac\x10\x014EZEZ\x00-d7EX\x02\x01\x03\x00Y\x01\xff\x00\x02\xab\xa8\x84\xcb\x07\xd0\x00\x00<\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x02\x00[\xeeY' -router_adv = b'\x00"\x19\x100\xe5\x00\x1c\xda\x00\x10\x04\x08\x00E\x00\x00\xab>F\x00\x00@\x11\xe1\x17\xac\x10\x01\x90\xac\x10\x014EZEZ\x00\x97\x81\xb0EX\x02\x01\x03\x00Y\x01\xff\x00\x02\xab\xe8E\xce\xbf\xec\x00\x00') -assert(rf.i2repr_one(None, RandNum(0, 10)) == '') -assert(lf.i2repr_one(None, RandNum(0, 10)) == '') -assert(fcb.i2repr_one(None, RandNum(0, 10)) == '') +assert f.i2repr_one(None, RandNum(0, 10)) == '' +assert rf.i2repr_one(None, RandNum(0, 10)) == '' +assert lf.i2repr_one(None, RandNum(0, 10)) == '' +assert fcb.i2repr_one(None, RandNum(0, 10)) == '' True = EnumField.i2repr ~ field enumfield -assert(f.i2repr(None, 0) == 'Foo') -assert(f.i2repr(None, 1) == 'Bar') -expect_exception(KeyError, 'f.i2repr(None, 2)') -assert(f.i2repr(None, [0, 1]) == ['Foo', 'Bar']) +assert f.i2repr(None, 0) == 'Foo' +assert f.i2repr(None, 1) == 'Bar' +assert f.i2repr(None, 2) == '2' +assert f.i2repr(None, [0, 1]) == ['Foo', 'Bar'] -assert(rf.i2repr(None, 0) == 'Foo') -assert(rf.i2repr(None, 1) == 'Bar') -expect_exception(KeyError, 'rf.i2repr(None, 2)') -assert(rf.i2repr(None, [0, 1]) == ['Foo', 'Bar']) +assert rf.i2repr(None, 0) == 'Foo' +assert rf.i2repr(None, 1) == 'Bar' +assert rf.i2repr(None, 2) == '2' +assert rf.i2repr(None, [0, 1]) == ['Foo', 'Bar'] -assert(lf.i2repr(None, 0) == 'Foo') -assert(lf.i2repr(None, 1) == 'Bar') -expect_exception(KeyError, 'lf.i2repr(None, 2)') -assert(lf.i2repr(None, [0, 1]) == ['Foo', 'Bar']) +assert lf.i2repr(None, 0) == 'Foo' +assert lf.i2repr(None, 1) == 'Bar' +assert lf.i2repr(None, 2) == '2' +assert lf.i2repr(None, [0, 1]) == ['Foo', 'Bar'] -assert(fcb.i2repr(None, 0) == 'Foo') -assert(fcb.i2repr(None, 1) == 'Bar') -assert(fcb.i2repr(None, 5) == 'Bar') -assert(fcb.i2repr(None, 11) == repr(11)) -assert(fcb.i2repr(None, [0, 1, 5, 11]) == ['Foo', 'Bar', 'Bar', repr(11)]) +assert fcb.i2repr(None, 0) == 'Foo' +assert fcb.i2repr(None, 1) == 'Bar' +assert fcb.i2repr(None, 5) == 'Bar' +assert fcb.i2repr(None, 11) == repr(11) +assert fcb.i2repr(None, [0, 1, 5, 11]) == ['Foo', 'Bar', 'Bar', repr(11)] conf.noenum.add(f, rf, lf, fcb) -assert(f.i2repr(None, 0) == repr(0)) -assert(f.i2repr(None, 1) == repr(1)) -assert(f.i2repr(None, 2) == repr(2)) -assert(f.i2repr(None, [0, 1, 2]) == [repr(0), repr(1), repr(2)]) +assert f.i2repr(None, 0) == repr(0) +assert f.i2repr(None, 1) == repr(1) +assert f.i2repr(None, 2) == repr(2) +assert f.i2repr(None, [0, 1, 2]) == [repr(0), repr(1), repr(2)] -assert(rf.i2repr(None, 0) == repr(0)) -assert(rf.i2repr(None, 1) == repr(1)) -assert(rf.i2repr(None, 2) == repr(2)) -assert(rf.i2repr(None, [0, 1, 2]) == [repr(0), repr(1), repr(2)]) +assert rf.i2repr(None, 0) == repr(0) +assert rf.i2repr(None, 1) == repr(1) +assert rf.i2repr(None, 2) == repr(2) +assert rf.i2repr(None, [0, 1, 2]) == [repr(0), repr(1), repr(2)] -assert(lf.i2repr(None, 0) == repr(0)) -assert(lf.i2repr(None, 1) == repr(1)) -assert(lf.i2repr(None, 2) == repr(2)) -assert(lf.i2repr(None, [0, 1, 2]) == [repr(0), repr(1), repr(2)]) +assert lf.i2repr(None, 0) == repr(0) +assert lf.i2repr(None, 1) == repr(1) +assert lf.i2repr(None, 2) == repr(2) +assert lf.i2repr(None, [0, 1, 2]) == [repr(0), repr(1), repr(2)] -assert(fcb.i2repr(None, 0) == repr(0)) -assert(fcb.i2repr(None, 1) == repr(1)) -assert(fcb.i2repr(None, 5) == repr(5)) -assert(fcb.i2repr(None, 11) == repr(11)) -assert(fcb.i2repr(None, [0, 1, 5, 11]) == [repr(0), repr(1), repr(5), repr(11)]) +assert fcb.i2repr(None, 0) == repr(0) +assert fcb.i2repr(None, 1) == repr(1) +assert fcb.i2repr(None, 5) == repr(5) +assert fcb.i2repr(None, 11) == repr(11) +assert fcb.i2repr(None, [0, 1, 5, 11]) == [repr(0), repr(1), repr(5), repr(11)] conf.noenum.remove(f, rf, lf, fcb) -assert(f.i2repr_one(None, RandNum(0, 10)) == '') -assert(rf.i2repr_one(None, RandNum(0, 10)) == '') -assert(lf.i2repr_one(None, RandNum(0, 10)) == '') -assert(fcb.i2repr_one(None, RandNum(0, 10)) == '') +assert f.i2repr_one(None, RandNum(0, 10)) == '' +assert rf.i2repr_one(None, RandNum(0, 10)) == '' +assert lf.i2repr_one(None, RandNum(0, 10)) == '' +assert fcb.i2repr_one(None, RandNum(0, 10)) == '' True += EnumField with Enum +from enum import Enum + +class JUICE(Enum): + APPLE = 0 + ORANGE = 1 + PINEAPPLE = 2 + + +class Breakfast(Packet): + fields_desc = [EnumField("juice", 1, JUICE, fmt="H")] + + +assert raw(Breakfast(juice="ORANGE")) == b"\x00\x01" + += LE3BytesEnumField +~ field le3bytesenumfield + +f = LE3BytesEnumField('test', 0, {0: 'Foo', 1: 'Bar'}) + += LE3BytesEnumField.i2repr_one +~ field le3bytesenumfield + +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 2) == '2' + += XLE3BytesEnumField + +assert XLE3BytesEnumField("a", 0, {0: "test"}).i2repr_one(None, 0) == "test" +assert XLE3BytesEnumField("a", 0, {0: "test"}).i2repr_one(None, 1) == "0x1" + ############ ############ + CharEnumField tests @@ -972,9 +1322,9 @@ True def expect_exception(e, c): try: eval(c) - return False + assert False except e: - return True + assert True = CharEnumField tests initialization @@ -991,13 +1341,13 @@ True = CharEnumField.any2i_one ~ field charenumfield -assert(fc.any2i_one(None, 'Foo') == 'f') -assert(fc.any2i_one(None, 'Bar') == 'b') +assert fc.any2i_one(None, 'Foo') == 'f' +assert fc.any2i_one(None, 'Bar') == 'b' expect_exception(KeyError, 'fc.any2i_one(None, "Baz")') -assert(fcb.any2i_one(None, 'Foo') == 'a') -assert(fcb.any2i_one(None, 'Bar') == 'b') -assert(fcb.any2i_one(None, 'Baz') == '') +assert fcb.any2i_one(None, 'Foo') == 'a' +assert fcb.any2i_one(None, 'Bar') == 'b' +assert fcb.any2i_one(None, 'Baz') == '' True @@ -1011,9 +1361,9 @@ True def expect_exception(e, c): try: eval(c) - return False + assert False except e: - return True + assert True = XByteEnumField tests initialization @@ -1030,13 +1380,13 @@ True = XByteEnumField.i2repr_one ~ field xbyteenumfield -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 0xff) == '0xff' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 0xff) == '0xff' True @@ -1054,28 +1404,28 @@ True = XByteEnumField.i2repr_one with update ~ field xbyteenumfield -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 2) == '0x2') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 2) == '0x2' +assert f.i2repr_one(None, 0xff) == '0xff' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 2) == '0x2') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 2) == '0x2' +assert f.i2repr_one(None, 0xff) == '0xff' del enum[1] enum[2] = 'Baz' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == '0x1') -assert(f.i2repr_one(None, 2) == 'Baz') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == '0x1' +assert f.i2repr_one(None, 2) == 'Baz' +assert f.i2repr_one(None, 0xff) == '0xff' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == '0x1') -assert(f.i2repr_one(None, 2) == 'Baz') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == '0x1' +assert f.i2repr_one(None, 2) == 'Baz' +assert f.i2repr_one(None, 0xff) == '0xff' True @@ -1089,9 +1439,9 @@ True def expect_exception(e, c): try: eval(c) - return False + assert False except e: - return True + assert True = XShortEnumField tests initialization @@ -1108,13 +1458,13 @@ True = XShortEnumField.i2repr_one ~ field xshortenumfield -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 0xff) == '0xff' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 0xff) == '0xff' True @@ -1132,28 +1482,28 @@ True = XShortEnumField.i2repr_one with update ~ field xshortenumfield -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 2) == '0x2') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 2) == '0x2' +assert f.i2repr_one(None, 0xff) == '0xff' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == 'Bar') -assert(f.i2repr_one(None, 2) == '0x2') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == 'Bar' +assert f.i2repr_one(None, 2) == '0x2' +assert f.i2repr_one(None, 0xff) == '0xff' del enum[1] enum[2] = 'Baz' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == '0x1') -assert(f.i2repr_one(None, 2) == 'Baz') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == '0x1' +assert f.i2repr_one(None, 2) == 'Baz' +assert f.i2repr_one(None, 0xff) == '0xff' -assert(f.i2repr_one(None, 0) == 'Foo') -assert(f.i2repr_one(None, 1) == '0x1') -assert(f.i2repr_one(None, 2) == 'Baz') -assert(f.i2repr_one(None, 0xff) == '0xff') +assert f.i2repr_one(None, 0) == 'Foo' +assert f.i2repr_one(None, 1) == '0x1' +assert f.i2repr_one(None, 2) == 'Baz' +assert f.i2repr_one(None, 0xff) == '0xff' True @@ -1164,11 +1514,11 @@ True = Raise exception - test data dnsf = DNSStrField("test", "") -assert(dnsf.getfield(None, b"\x01x\x00") == (b"", b"x.")) +assert dnsf.getfield(None, b"\x01x\x00") == (b"", b"x.") try: dnsf.getfield(None, b"\xc0\xff") - assert(False) + assert False except (Scapy_Exception, IndexError): pass @@ -1177,47 +1527,47 @@ except (Scapy_Exception, IndexError): = default usage yn_bf = YesNoByteField('test', 0x00) -assert(yn_bf.i2repr(None, 0x00) == 'no') -assert(yn_bf.i2repr(None, 0x01) == 'yes') -assert(yn_bf.i2repr(None, 0x02) == 'yes') -assert(yn_bf.i2repr(None, 0xff) == 'yes') +assert yn_bf.i2repr(None, 0x00) == 'no' +assert yn_bf.i2repr(None, 0x01) == 'yes' +assert yn_bf.i2repr(None, 0x02) == 'yes' +assert yn_bf.i2repr(None, 0xff) == 'yes' = inverted yes - no (scalar config) yn_bf = YesNoByteField('test', 0x00, config={'yes': 0x00, 'no': 0x01}) -assert(yn_bf.i2repr(None, 0x00) == 'yes') -assert(yn_bf.i2repr(None, 0x01) == 'no') -assert(yn_bf.i2repr(None, 0x02) == 2) -assert(yn_bf.i2repr(None, 0xff) == 255) +assert yn_bf.i2repr(None, 0x00) == 'yes' +assert yn_bf.i2repr(None, 0x01) == 'no' +assert yn_bf.i2repr(None, 0x02) == 2 +assert yn_bf.i2repr(None, 0xff) == 255 = inverted yes - no (range config) yn_bf = YesNoByteField('test', 0x00, config={'yes': 0x00, 'no': (0x01, 0xff)}) -assert(yn_bf.i2repr(None, 0x00) == 'yes') -assert(yn_bf.i2repr(None, 0x01) == 'no') -assert(yn_bf.i2repr(None, 0x02) == 'no') -assert(yn_bf.i2repr(None, 0xff) == 'no') +assert yn_bf.i2repr(None, 0x00) == 'yes' +assert yn_bf.i2repr(None, 0x01) == 'no' +assert yn_bf.i2repr(None, 0x02) == 'no' +assert yn_bf.i2repr(None, 0xff) == 'no' = yes - no (using sets) yn_bf = YesNoByteField('test', 0x00, config={'yes': [0x00, 0x02], 'no': [0x01, 0x04, 0xff]}) -assert(yn_bf.i2repr(None, 0x00) == 'yes') -assert(yn_bf.i2repr(None, 0x01) == 'no') -assert(yn_bf.i2repr(None, 0x02) == 'yes') -assert(yn_bf.i2repr(None, 0x03) == 3) -assert(yn_bf.i2repr(None, 0x04) == 'no') -assert(yn_bf.i2repr(None, 0x05) == 5) -assert(yn_bf.i2repr(None, 0xff) == 'no') +assert yn_bf.i2repr(None, 0x00) == 'yes' +assert yn_bf.i2repr(None, 0x01) == 'no' +assert yn_bf.i2repr(None, 0x02) == 'yes' +assert yn_bf.i2repr(None, 0x03) == 3 +assert yn_bf.i2repr(None, 0x04) == 'no' +assert yn_bf.i2repr(None, 0x05) == 5 +assert yn_bf.i2repr(None, 0xff) == 'no' = yes, no and invalid yn_bf = YesNoByteField('test', 0x00, config={'no': 0x00, 'yes': 0x01, 'invalid': (0x02, 0xff)}) -assert(yn_bf.i2repr(None, 0x00) == 'no') -assert(yn_bf.i2repr(None, 0x01) == 'yes') -assert(yn_bf.i2repr(None, 0x02) == 'invalid') -assert(yn_bf.i2repr(None, 0xff) == 'invalid') +assert yn_bf.i2repr(None, 0x00) == 'no' +assert yn_bf.i2repr(None, 0x01) == 'yes' +assert yn_bf.i2repr(None, 0x02) == 'invalid' +assert yn_bf.i2repr(None, 0xff) == 'invalid' = invalid scalar spec try: YesNoByteField('test', 0x00, config={'no': 0x00, 'yes': 256}) - assert(False) + assert False except FieldValueRangeException: pass @@ -1225,7 +1575,7 @@ except FieldValueRangeException: try: YesNoByteField('test', 0x00, config={'no': 0x00, 'yes': (0x00, 0x02, 0x02)}) - assert(False) + assert False except FieldAttributeException: pass @@ -1233,13 +1583,13 @@ except FieldAttributeException: try: YesNoByteField('test', 0x00, config={'no': 0x00, 'yes': (0x100, 0x01)}) - assert(False) + assert False except FieldValueRangeException: pass try: YesNoByteField('test', 0x00, config={'no': 0x00, 'yes': (0x00, 0x100)}) - assert(False) + assert False except FieldValueRangeException: pass @@ -1247,7 +1597,7 @@ except FieldValueRangeException: try: YesNoByteField('test', 0x00, config={'no': 0x00, 'yes': [0x01, 0x101]}) - assert(False) + assert False except FieldValueRangeException: pass @@ -1319,6 +1669,25 @@ assert o.subfield == 0xDEAD o.switch = 2 assert o.subfield == 0xBEEFBEEF +o = SweetPacket(switch=1, subfield=0x88) +assert o.subfield == 0x88 + += MultipleTypeField - syntax error + +import warnings + +with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + class MTFPacket(Packet): + fields_desc = [ByteField("a", 0), + MultipleTypeField([ + (ByteField("b", 0), lambda pkt: pkt.a == 0), + (ShortField("not_b", 0), lambda: pkt.a != 0), + ], IntField("b", 0))] + assert len(w) == 1 + assert issubclass(w[-1].category, SyntaxWarning) + + ######## ######## + FlagsField @@ -1419,6 +1788,49 @@ assert "f2" in flags assert "f4" in flags assert "f0" in flags += FlagsField with str + +class TCPTest(Packet): + fields_desc = [ + BitField("reserved", 0, 7), + FlagsField("flags", 0x2, 9, "FSRPAUECN") + ] + +a = TCPTest(flags=3) +assert a.flags.F +assert a.flags.S +assert a.sprintf("%flags%") == "FS" + += FlagsField with dict + +class FlagsTest2(Packet): + fields_desc = [ + FlagsField("flags", 0x2, 16, { + 0x0001: "A", + 0x0008: "B", + 0x1000: "C", + }) + ] + +a = FlagsTest2(flags=9) +a.sprintf("%flags%") +assert a.flags.A +assert a.flags.B +assert a.sprintf("%flags%") == "A+B" + +b = FlagsTest2(flags="B+C") +assert b.flags == 0x1000 | 0x0008 + += Conditional FlagsField command + +class CondFlagsTest(Packet): + fields_desc = [ + ByteField("b", 0), + ConditionalField(FlagsField("f", 0, 8, ""), lambda p: p.b == 0) + ] + +p = CondFlagsTest(b"\x00\x0f") +assert p == eval(p.command()) ######## ######## @@ -1700,120 +2112,27 @@ assert bytes(y) != bytes(y) ############ ############ -+ BitExtendedField - -= BitExtendedField: simple test - -class DebugPacket(Packet): - fields_desc = [ - BitExtendedField("val", None, extension_bit=0) - ] += LSBExtendedField +* Test addfield and getfield -a = DebugPacket(val=1234) -assert(a.val == 1234) - -= BitExtendedField i2m: corner values -* 7 bits of data = 0 -import codecs -for i in range(8): - m = BitExtendedField("foo", None, extension_bit=i) - r = m.i2m(None, 0) - r = int(codecs.encode(r, 'hex'), 16) - assert(r == 0) - -* 7 bits of data = 1 -for i in range(8): - m = BitExtendedField("foo", None, extension_bit=i) - r = m.i2m(None, 0b1111111) - r = int(codecs.encode(r, 'hex'), 16) - assert(r == 0xff - 2**i) - -= BitExtendedField i2m: field expansion -* If there is 8 bits of data, we need to add a byte -m = BitExtendedField("foo", None, extension_bit=0) -r = m.i2m(None, 0b10000000) -r = int(codecs.encode(r, 'hex'), 16) -assert(r == 0x0300) - -= BitExtendedField i2m: test all FX bit positions -* Data is 0b10000001 (129) and all str values are precomputed -data_129 = { - "extended": 129, - "int_with_fx": [770, 769, 1281, 2305, 4353, 8449, 16641, 33025], - "str_with_fx" : [] -} -for i in range(8): - m = BitExtendedField("foo", None, extension_bit=i) - r = m.i2m(None, data_129["extended"]) - data_129["str_with_fx"].append(r) - r = int(codecs.encode(r, 'hex'), 16) - assert(r == data_129["int_with_fx"][i]) - -= BitExtendedField m2i: test all FX bit positions -* Data is 0b10000001 (129) and all str values are precomputed -for i in range(8): - m = BitExtendedField("foo", None, extension_bit=i) - r = m.m2i(None, data_129["str_with_fx"][i]) - assert(r == data_129["extended"]) - -= BitExtendedField m2i: stop at FX zero -* 1 byte of zeroes (FX stop) then 1 byte of ones : ignore 2nd byte -for i in range(8): - m = BitExtendedField("foo", None, extension_bit=i) - r = m.m2i(None, b'\x00\xff') - assert(r == 0) - -= BitExtendedField m2i: multiple bytes -* 0b00000011 0b11111110 --> 0xff -data_254 = { - "extended": 0xff, - "str_with_fx" : [b'\x03\xfe', b'\x03\xfd', b'\x05\xfb', b'\x09\xf7', b'\x11\xef', b'\x21\xdf', b'\x41\xbf', b'\x81\x7f'] -} -for i in range(len(data_254['str_with_fx'])): - m = BitExtendedField("foo", None, extension_bit=i) - r = m.m2i(None, data_254["str_with_fx"][i]) - assert(r == data_254['extended']) - -= BitExtendedField m2i: invalid field with no stopping bit -* 1 byte of one (no FX stop) shall return an error -for i in range(8): - m = BitExtendedField("foo", None, extension_bit=i) - r = m.m2i(None, b'\xff') - assert(r == None) +f = LSBExtendedField("a", 0) -= LSBExtendedField -* Test i2m and m2i -data_129 = { - "extended": 129, - "int_with_fx": 770, - "str_with_fx" : None -} -m = LSBExtendedField("foo", None) -r = m.i2m(None, data_129["extended"]) -data_129["str_with_fx"] = r -r = int(codecs.encode(r, 'hex'), 16) -assert(r == data_129["int_with_fx"]) - -m = LSBExtendedField("foo", None) -r = m.m2i(None, data_129["str_with_fx"]) -assert(r == data_129["extended"]) +assert f.addfield(None, b"", 1) == b"\x02" +assert f.addfield(None, b"", 127) == b"\xfe" +assert f.addfield(None, b"", 128) == b"\x01\x02" +assert f.addfield(None, b"", 536) == b"1\x08" +assert f.addfield(None, b"", 16383) == b"\xff\xfe" = MSBExtendedField * Test i2m and m2i -data_129 = { - "extended": 129, - "int_with_fx": 33025, - "str_with_fx" : None -} -m = MSBExtendedField("foo", None) -r = m.i2m(None, data_129["extended"]) -data_129["str_with_fx"] = r -r = int(codecs.encode(r, 'hex'), 16) -assert(r == data_129["int_with_fx"]) - -m = MSBExtendedField("foo", None) -r = m.m2i(None, data_129["str_with_fx"]) -assert(r == data_129["extended"]) + +f = MSBExtendedField("a", 0) + +assert f.addfield(None, b"", 1) == b"\x01" +assert f.addfield(None, b"", 127) == b"\x7f" +assert f.addfield(None, b"", 128) == b"\x80\x01" +assert f.addfield(None, b"", 536) == b"\x98\x04" +assert f.addfield(None, b"", 16383) == b"\xff\x7f" ############ @@ -1878,6 +2197,32 @@ pkt = TestPacket(raw(pkt)) assert pkt.fcs == 0xbeef += FCSField: multiple + +class TestPacket2(Packet): + fields_desc = [ + ByteField("a", 0), + LEShortField("b", 15), + FCSField("fcs1", None), + LEIntField("c", 7), + FCSField("fcs2", None), + IntField("bottom", 0), + ] + +bind_layers(TestPacket2, Ether) + +pkt = TestPacket2(a=12, fcs1=0xbeef, fcs2=0xfeed, bottom=123)/Ether(src="aa:aa:aa:aa:aa:aa", dst="bb:bb:bb:bb:bb:bb")/IP(src="127.0.0.1", dst="127.0.0.1") + +assert raw(pkt) == b'\x0c\x0f\x00\x07\x00\x00\x00\x00\x00\x00{\xbb\xbb\xbb\xbb\xbb\xbb\xaa\xaa\xaa\xaa\xaa\xaa\x08\x00E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01\xfe\xed\xbe\xef' +assert raw(pkt) == b'\x0c\x0f\x00\x07\x00\x00\x00\x00\x00\x00{\xbb\xbb\xbb\xbb\xbb\xbb\xaa\xaa\xaa\xaa\xaa\xaa\x08\x00E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01\xfe\xed\xbe\xef' + +pkt = TestPacket2(raw(pkt)) +assert pkt.fcs1 == 0xbeef +assert pkt.fcs2 == 0xfeed +assert pkt.bottom == 123 +assert pkt.a == 12 + + ############ ############ + PacketField @@ -1920,3 +2265,83 @@ assert isinstance(p.packet.short1, RandShort) assert isinstance(p.packet.byte, RandByte) assert isinstance(p.packet.long, RandLong) assert isinstance(p.short2, RandShort) + += Test parent reference in guess_payload_class + +class TestGuessInner(Packet): + name="test guess inner" + fields_desc=[ ByteField("foo", 0) ] + def guess_payload_class(self, payload): + self.parentflag = True + if self.parent is None: + # all exceptions are caught, so have to use flag + self.parentflag = False + return super(TestGuessInner, self).guess_payload_class(payload) + +class TestGuess(Packet): + name="test guess" + fields_desc=[ PacketField("pf", None, TestGuessInner) ] + +x=TestGuess(pf=TestGuessInner()/Raw(b'123')) +p=TestGuess(raw(x)) +assert p[TestGuessInner].parentflag +assert p[TestGuessInner].parent == p + +############ +############ ++ XStr(*)Field tests + += i2repr +~ field xstrfield + +from collections import namedtuple +MockPacket = namedtuple('MockPacket', ['type']) + +mp = MockPacket(0) +f = XStrField('test', None) +x = f.i2repr(mp, RandBin()) +assert x == '' + +############ +############ ++ Raw() tests + += unaligned data + +p = Raw(b"abc") +p + +offsetdata = bytes.fromhex("0" + p.load.hex() + "0") + +p = Raw((offsetdata, 4)) +p + +############ +############ ++ PacketListField() tests + += unaligned data + +class PInner(Packet): + name = "PInner" + fields_desc = [ + BitField("x", 0, 8), + ] + def extract_padding(self, s): + return '', s + +class POuter(Packet): + name = "POuter" + fields_desc = [ + BitField("indent", 0, 4), + BitFieldLenField("pcount", None, 8, count_of="plist"), + PacketListField("plist", None, PInner, + count_from=lambda pkt: pkt.pcount), + ] + +p = POuter(b"\xf0\x44\x14\x24\x34\x40") +p + +assert p.indent == 0xf +assert p.pcount == 4 +assert [p.x for p in p.plist] == [0x41, 0x42, 0x43, 0x44] diff --git a/test/import_tester b/test/import_tester deleted file mode 100644 index eebaba60135..00000000000 --- a/test/import_tester +++ /dev/null @@ -1,3 +0,0 @@ -#! /bin/bash -cd "$(dirname $0)/.." -find scapy -name '*.py' | sed -e 's#/#.#g' -e 's/\(\.__init__\)\?\.py$//' | while read a; do echo "######### $a"; python -c "import $a"; done diff --git a/test/imports.uts b/test/imports.uts new file mode 100644 index 00000000000..ad6ca83598e --- /dev/null +++ b/test/imports.uts @@ -0,0 +1,104 @@ +% Import tests +~ not_pypy + ++ Import tests +~ imports + += Prepare importing all scapy files + +import os +import glob +import subprocess +import re +import time +import sys +from scapy.consts import WINDOWS, OPENBSD + +# DEV: to add your file to this list, make sure you have +# a GREAT reason. +EXCEPTIONS = [ + "scapy.__main__", + "scapy.all", + "scapy.contrib.automotive*", + "scapy.contrib.cansocket*", + "scapy.contrib.isotp*", + "scapy.contrib.scada*", + "scapy.layers.all", + "scapy.main", +] + +if WINDOWS: + EXCEPTIONS.append("scapy.layers.tuntap") + +EXCEPTION_PACKAGES = [ + "arch", + "libs", + "modules", + "tools", +] + +ALL_FILES = [ + "scapy." + re.match(".*scapy\\" + os.path.sep + "(.*)\\.py$", x).group(1).replace(os.path.sep, ".") + for x in glob.iglob(scapy_path('/scapy/**/*.py'), recursive=True) +] +ALL_FILES = [ + x for x in ALL_FILES if + not any(x == y if y[-1] != "*" else x.startswith(y[:-1]) for y in EXCEPTIONS) and + x.split(".")[1] not in EXCEPTION_PACKAGES +] + +NB_PROC = 1 if WINDOWS or OPENBSD else 4 + +def append_processes(processes, filename): + processes.append( + (subprocess.Popen( + [sys.executable, "-c", "import %s" % filename], + stderr=subprocess.PIPE, encoding="utf8"), + time.time(), + filename)) + +def check_processes(processes): + for i, tup in enumerate(processes): + proc, start_ts, file = tup + errs = "" + try: + _, errs = proc.communicate(timeout=0.5) + except subprocess.TimeoutExpired: + if time.time() - start_ts > 30: + proc.kill() + errs = "Timed out (>30s)!" + if proc.returncode is None: + continue + else: + print("Finished %s with %d after %f sec" % + (file, proc.returncode, time.time() - start_ts)) + if proc.returncode != 0: + for p in processes: + p[0].kill() + raise Exception( + "Importing the file '%s' failed !\\n%s" % (file, errs)) + del processes[i] + return + + +def import_all(FILES): + processes = list() + while len(processes) == NB_PROC: + check_processes(processes) + for filename in FILES: + check_processes(processes) + if len(processes) < NB_PROC: + append_processes(processes, filename) + + += Try importing all core separately + +import_all(x for x in ALL_FILES if "layers" not in x and "contrib" not in x) + += Try importing all layers separately + +import_all(x for x in ALL_FILES if "layers" in x) + += Try importing all contribs separately + +import_all(x for x in ALL_FILES if "contrib" in x) diff --git a/test/linux.uts b/test/linux.uts index a62a996f924..d651fccee3f 100644 --- a/test/linux.uts +++ b/test/linux.uts @@ -9,19 +9,14 @@ + Linux only test = L3RawSocket -~ netaccess IP ICMP linux needs_root - -old_l3socket = conf.L3socket -old_debug_dissector = conf.debug_dissector -conf.debug_dissector = False -conf.L3socket = L3RawSocket -x = sr1(IP(dst="www.google.com")/ICMP(),timeout=3) -conf.debug_dissector = old_debug_dissector -conf.L3socket = old_l3socket +~ netaccess IP TCP linux needs_root + +with no_debug_dissector(): + x = sr1(IP(dst="www.google.com")/TCP(sport=RandShort(), dport=80, flags="S"),timeout=3) + x assert x[IP].ottl() in [32, 64, 128, 255] assert 0 <= x[IP].hops() <= 126 -x is not None and ICMP in x and x[ICMP].type == 0 # TODO: fix this test (randomly stuck) # ex: https://travis-ci.org/secdev/scapy/jobs/247473497 @@ -49,9 +44,9 @@ if exit_status == 0: print(get_if_list()) conf.route.resync() print(conf.route.routes) - assert(conf.route.route("198.51.100.254") == ("scapy0", "198.51.100.1", "0.0.0.0")) + assert conf.route.route("198.51.100.254") == ("scapy0", "198.51.100.1", "0.0.0.0") route_alias = (3325256704, 4294967040, "0.0.0.0", "scapy0", "198.51.100.1", 0) - assert(route_alias in conf.route.routes) + assert route_alias in conf.route.routes exit_status = os.system("ip link add link scapy0 name scapy0.42 type vlan id 42") exit_status = os.system("ip addr add 203.0.113.42/24 dev scapy0.42") exit_status = os.system("ip link set scapy0.42 up") @@ -59,95 +54,148 @@ if exit_status == 0: print(get_if_list()) conf.route.resync() print(conf.route.routes) - assert(conf.route.route("192.0.2.43") == ("scapy0.42", "203.0.113.42", "203.0.113.41")) - route_specific = (3221226027, 4294967295, "203.0.113.41", "scapy0.42", "203.0.113.42", 0) - assert(route_specific in conf.route.routes) + assert conf.route.route("192.0.2.43") == ("scapy0.42", "203.0.113.42", "203.0.113.41") + route_specific = (3221226027, 4294967295, "203.0.113.41", "scapy0.42", "0.0.0.0", 0) + assert route_specific in conf.route.routes + assert conf.route.route("203.0.113.42") == ('scapy0.42', '203.0.113.42', '0.0.0.0') + assert conf.route.route("203.0.113.43") == ('scapy0.42', '203.0.113.42', '0.0.0.0') exit_status = os.system("ip link del name dev scapy0") else: assert True -= catch loopback device missing + += Test scoped interface addresses ~ linux needs_root -from mock import patch +import os +exit_status = os.system("ip link add name scapy0 type dummy") +exit_status = os.system("ip link add name scapy1 type dummy") +exit_status |= os.system("ip addr add 192.0.2.1/24 dev scapy0") +exit_status |= os.system("ip addr add 192.0.3.1/24 dev scapy1") +exit_status |= os.system("ip link set scapy0 address 00:01:02:03:04:05 multicast on up") +exit_status |= os.system("ip link set scapy1 address 06:07:08:09:10:11 multicast on up") +assert exit_status == 0 + +conf.ifaces.reload() +conf.route.resync() +conf.route6.resync() -# can't remove the lo device (or its address without causing trouble) - use some pseudo dummy instead +conf.route6 -with patch('scapy.consts.LOOPBACK_NAME', 'scapy_lo_x'): - routes = read_routes() +try: + # IPv4 + a = Ether()/IP(dst="224.0.0.1%scapy0") + assert a[Ether].src == "00:01:02:03:04:05" + assert a[IP].src == "192.0.2.1" + b = Ether()/IP(dst="224.0.0.1%scapy1") + assert b[Ether].src == "06:07:08:09:10:11" + assert b[IP].src == "192.0.3.1" + c = Ether()/IP(dst="224.0.0.1/24%scapy1") + assert c[Ether].src == "06:07:08:09:10:11" + assert c[IP].src == "192.0.3.1" + # IPv6 + a = Ether()/IPv6(dst="ff02::fb%scapy0") + assert a[Ether].src == "00:01:02:03:04:05" + assert a[IPv6].src == "fe80::201:2ff:fe03:405" + b = Ether()/IPv6(dst="ff02::fb%scapy1") + assert b[Ether].src == "06:07:08:09:10:11" + assert b[IPv6].src == "fe80::407:8ff:fe09:1011" + c = Ether()/IPv6(dst="ff02::fb/30%scapy1") + assert c[Ether].src == "06:07:08:09:10:11" + assert c[IPv6].src == "fe80::407:8ff:fe09:1011" +finally: + exit_status = os.system("ip link del scapy0") + exit_status = os.system("ip link del scapy1") + conf.ifaces.reload() + conf.route.resync() + conf.route6.resync() -= catch loopback device no address assigned += catch loopback device missing ~ linux needs_root -import os, socket -from mock import patch - -exit_status = os.system("ip link add name scapy_lo type dummy") -assert(exit_status == 0) -exit_status = os.system("ip link set dev scapy_lo up") -assert(exit_status == 0) - -with patch('scapy.consts.LOOPBACK_NAME', 'scapy_lo'): - routes = read_routes() +from unittest.mock import patch -exit_status = os.system("ip addr add dev scapy_lo 10.10.0.1/24") -assert(exit_status == 0) +# can't remove the lo device (or its address without causing trouble) - use some pseudo dummy instead -with patch('scapy.consts.LOOPBACK_NAME', 'scapy_lo'): +with patch('scapy.arch.linux.conf.loopback_name', 'scapy_lo_x'): routes = read_routes() -got_lo_device = False -for route in routes: - dst_int, msk_int, gw_str, if_name, if_addr, metric = route - if if_name == 'scapy_lo': - got_lo_device = True - assert(if_addr == '10.10.0.1') - dst_addr = socket.inet_ntoa(struct.pack("!I", dst_int)) - assert(dst_addr == '10.10.0.0') - msk = socket.inet_ntoa(struct.pack("!I", msk_int)) - assert (msk == '255.255.255.0') - break += catch loopback device no address assigned +~ linux needs_root -assert(got_lo_device) +import os, socket +from unittest.mock import patch -exit_status = os.system("ip link del dev scapy_lo") -assert(exit_status == 0) +try: + exit_status = os.system("ip link add name scapy_lo type dummy") + assert exit_status == 0 + exit_status = os.system("ip link set dev scapy_lo up") + assert exit_status == 0 + + with patch('scapy.arch.linux.conf.loopback_name', 'scapy_lo'): + routes = read_routes() + + exit_status = os.system("ip addr add dev scapy_lo 10.10.0.1/24") + assert exit_status == 0 + + with patch('scapy.arch.linux.conf.loopback_name', 'scapy_lo'): + routes = read_routes() + + lo_routes = [ + (ltoa(dst_int), ltoa(msk_int), gw_str, if_name, if_addr, metric) + for dst_int, msk_int, gw_str, if_name, if_addr, metric in routes + if if_name == "scapy_lo" + ] + lo_routes.sort(key=lambda x: x[0]) + + expected_routes = [ + (168427520, 4294967040, '0.0.0.0', 'scapy_lo', '10.10.0.1', 0), + (168427521, 4294967295, '0.0.0.0', 'scapy_lo', '10.10.0.1', 0), + (168427775, 4294967295, '0.0.0.0', 'scapy_lo', '10.10.0.1', 0), + ] + print(lo_routes) + print(expected_routes) +finally: + exit_status = os.system("ip link del dev scapy_lo") + assert exit_status == 0 = IPv6 link-local address selection -from mock import patch +conf.ifaces._add_fake_iface("scapy0", 'e2:39:91:79:19:10') + +from unittest.mock import patch conf.route6.routes = [('fe80::', 64, '::', 'scapy0', ['fe80::e039:91ff:fe79:1910'], 256)] conf.route6.ipv6_ifaces = set(['scapy0']) bck_conf_iface = conf.iface conf.iface = "scapy0" -with patch("scapy.layers.l2.get_if_hwaddr") as mgih: - mgih.return_value = 'e2:39:91:79:19:10' - p = Ether()/IPv6(dst="ff02::1")/ICMPv6NIQueryName(data="ff02::1") - print(p.sprintf("%Ether.src% > %Ether.dst%\n%IPv6.src% > %IPv6.dst%")) - ip6_ll_address = 'fe80::e039:91ff:fe79:1910' - print(p[IPv6].src, ip6_ll_address) - assert p[IPv6].src == ip6_ll_address - mac_address = 'e2:39:91:79:19:10' - print(p[Ether].src, mac_address) - assert p[Ether].src == mac_address + +p = Ether()/IPv6(dst="ff02::1")/ICMPv6NIQueryName(data="ff02::1") +print(p.sprintf("%Ether.src% > %Ether.dst%\n%IPv6.src% > %IPv6.dst%")) +ip6_ll_address = 'fe80::e039:91ff:fe79:1910' +print(p[IPv6].src, ip6_ll_address) +assert p[IPv6].src == ip6_ll_address +mac_address = 'e2:39:91:79:19:10' +print(p[Ether].src, mac_address) +assert p[Ether].src == mac_address conf.iface = bck_conf_iface conf.route6.resync() -= IPv6 -~ linux += IPv6 - check OS routes +~ linux ipv6 addrs = in6_getifaddr() -if len(addrs) == 0: - assert True -else: - assert all([in6_isvalid(addr[0]) for addr in in6_getifaddr()]) - assert set([addr[2] for addr in in6_getifaddr()]) == conf.route6.ipv6_ifaces +if addrs: + assert all(in6_isvalid(addr[0]) for addr in in6_getifaddr()), 'invalid ipv6 address' + ifaces6 = [addr[2] for addr in in6_getifaddr()] + assert all(iface in ifaces6 for iface in conf.route6.ipv6_ifaces), 'ipv6 interface has route but no real' = veth interface error handling ~ linux needs_root veth +from scapy.arch.linux import VEthPair + try: veth = VEthPair('this_IF_name_is_to_long_and_will_cause_an_error', 'veth_scapy_1') veth.setup() @@ -184,17 +232,18 @@ try: store=True, count=2, lfilter=lambda p: Ether in p and p[Ether].type == 0xbeef, - started_callback=_sniffer_started) + started_callback=_sniffer_started, + timeout=3) global frm_count frm_count = 2 - t_sniffer = Thread(target=_sniffer) + t_sniffer = Thread(target=_sniffer, name="linux.uts sniff veth_scapy_1") t_sniffer.start() cond_started.wait() sendp(Ether(type=0xbeef)/Raw(b'0123456789'), iface='veth_scapy_0', count=2) t_sniffer.join(1) - assert(frm_count == 2) + assert frm_count == 2 if_list = get_if_list() assert ('veth_scapy_0' not in if_list) @@ -230,6 +279,7 @@ except subprocess.CalledProcessError: except Exception: assert False +conf.ifaces.reload() if_list = get_if_list() assert ('veth_scapy_0' in if_list) assert ('veth_scapy_1' in if_list) @@ -240,23 +290,25 @@ def _sniffer(): store=True, count=2, lfilter=lambda p: Ether in p and p[Ether].type == 0xbeef, - started_callback=_sniffer_started) + started_callback=_sniffer_started, + timeout=3) global frm_count frm_count = 2 -t_sniffer = Thread(target=_sniffer) +t_sniffer = Thread(target=_sniffer, name="linux.uts sniff veth_scapy_1 2") t_sniffer.start() cond_started.wait() sendp(Ether(type=0xbeef)/Raw(b'0123456789'), iface='veth_scapy_0', count=2) t_sniffer.join(1) -assert(frm_count == 2) +assert frm_count == 2 try: veth.down() veth.destroy() + conf.ifaces.reload() if_list = get_if_list() assert ('veth_scapy_0' not in if_list) assert ('veth_scapy_1' not in if_list) @@ -269,7 +321,7 @@ except Exception: = Routing table, interface with no names ~ linux -from mock import patch +from unittest.mock import patch @patch("scapy.arch.linux.ioctl") def test_read_routes(mock_ioctl): @@ -286,71 +338,79 @@ test_read_routes() = L3PacketSocket sendto exception ~ linux needs_root -if six.PY3: - import mock, six, socket - @mock.patch("scapy.arch.linux.socket.socket.sendto") - def test_L3PacketSocket_sendto_python3(mock_sendto): - mock_sendto.side_effect = OSError(22, 2807) - l3ps = L3PacketSocket() - l3ps.send(IP(dst="8.8.8.8")/ICMP()) - return True - assert test_L3PacketSocket_sendto_python3() +from scapy.arch.linux import L3PacketSocket + +import socket +from unittest import mock + +@mock.patch("scapy.arch.linux.socket.socket.sendto") +def test_L3PacketSocket_sendto_python3(mock_sendto): + mock_sendto.side_effect = OSError(22, 2807) + l3ps = L3PacketSocket() + l3ps.send(IP(dst="8.8.8.8")/ICMP()) + return True + +assert test_L3PacketSocket_sendto_python3() = Test _interface_selection ~ netaccess linux needs_root import os from scapy.sendrecv import _interface_selection -assert _interface_selection(None, IP(dst="8.8.8.8")/UDP()) == conf.iface +assert _interface_selection(IP(dst="8.8.8.8")/UDP()) == (conf.iface, False) exit_status = os.system("ip link add name scapy0 type dummy") exit_status = os.system("ip addr add 192.0.2.1/24 dev scapy0") +exit_status = os.system("ip addr add fc00::/24 dev scapy0") exit_status = os.system("ip link set scapy0 up") -assert _interface_selection(None, IP(dst="192.0.2.42")/UDP()) == "scapy0" +conf.ifaces.reload() +conf.route.resync() +conf.route6.resync() +assert _interface_selection(IP(dst="192.0.2.42")/UDP()) == ("scapy0", False) +assert _interface_selection(IPv6(dst="fc00::ae0d")/UDP()) == ("scapy0", True) exit_status = os.system("ip link del name dev scapy0") +conf.ifaces.reload() +conf.route.resync() +conf.route6.resync() -= Test 802.Q sniffing -~ linux needs_root python3_only += Test 802.1Q sniffing +~ linux needs_root veth +from scapy.arch.linux import VEthPair from threading import Thread, Condition -veth = VEthPair("left0", "right0") -veth.setup() -veth.up() -exit_status = os.system("ip link add link right0 name vlanright0 type vlan id 42") -exit_status = os.system("ip link add link left0 name vlanleft0 type vlan id 42") -exit_status = os.system("ip link set vlanright0 up") -exit_status = os.system("ip link set vlanleft0 up") -exit_status = os.system("ip addr add 198.51.100.1/24 dev vlanleft0") -exit_status = os.system("ip addr add 198.51.100.2/24 dev vlanright0") - -cond_started = Condition() - -def _sniffer_started(): - - global cond_started - cond_started.acquire() - cond_started.notify() - cond_started.release() - -cond_started.acquire() - -dot1q_count = 0 - -def _sniffer(): - sniffed = sniff(iface="right0", - lfilter=lambda p: Dot1Q in p, - count=2, - timeout=5, - started_callback=_sniffer_started) - global dot1q_count - dot1q_count = len(sniffed) - -t_sniffer = Thread(target=_sniffer) -t_sniffer.start() -cond_started.wait() -sendp(Ether()/IP(dst="198.51.100.2")/ICMP(), iface='vlanleft0', count=2) - -t_sniffer.join(1) -assert(dot1q_count == 2) - -veth.destroy() +def _send(): + sendp(Ether()/IP(dst="198.51.100.2")/ICMP(), iface='vlanleft0', count=2) + + +with VEthPair("left0", "right0") as veth: + exit_status = os.system("ip link add link right0 name vlanright0 type vlan id 42") + exit_status = os.system("ip link add link left0 name vlanleft0 type vlan id 42") + exit_status = os.system("ip link set vlanright0 up") + exit_status = os.system("ip link set vlanleft0 up") + exit_status = os.system("ip addr add 198.51.100.1/24 dev vlanleft0") + exit_status = os.system("ip addr add 198.51.100.2/24 dev vlanright0") + sniffer = AsyncSniffer( + iface="right0", + lfilter=lambda p: Dot1Q in p, + count=2, + timeout=5, + started_callback=_send, + ) + sniffer.start() + sniffer.join(1) + if sniffer.running: + sniffer.stop() + raise Scapy_Exception("Sniffer did not stop !") + else: + results = sniffer.results + + +assert len(results) == 2 +assert all(Dot1Q in x for x in results) + + += Reload interfaces & routes + +conf.ifaces.reload() +conf.route.resync() +conf.route6.resync() diff --git a/test/nmap.uts b/test/nmap.uts index 45cb0faeee8..abea7137c9a 100644 --- a/test/nmap.uts +++ b/test/nmap.uts @@ -20,54 +20,63 @@ assert len(d) == 5 = Fetch database ~ netaccess -from __future__ import print_function try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen -for i in range(10): - try: - open('nmap-os-fingerprints', 'wb').write(urlopen('https://raw.githubusercontent.com/nmap/nmap/9efe1892/nmap-os-fingerprints').read()) - break - except: - pass +filename = 'nmap-os-fingerprints' + str(RandString(6)) -conf.nmap_base = 'nmap-os-fingerprints' +def _test(): + with open(filename, 'wb') as fd: + fd.write(urlopen('https://raw.githubusercontent.com/nmap/nmap/9efe1892/nmap-os-fingerprints').read()) + +retry_test(_test) + +conf.nmap_base = filename = Database loading ~ netaccess -assert len(nmap_kdb.get_base()) > 100 +print(conf.nmap_kdb.base, conf.nmap_kdb.filename, len(conf.nmap_kdb.get_base())) +assert len(conf.nmap_kdb.get_base()) > 100 = fingerprint test: www.secdev.org -~ netaccess -score, fprint = nmap_fp('www.secdev.org') +~ netaccess needs_root +with no_debug_dissector(): + score, fprint = nmap_fp('www.secdev.org') + print(score, fprint) assert score > 0.5 assert fprint = fingerprint test: gateway -~ netaccess -score, fprint = nmap_fp(conf.route.route('0.0.0.0')[2]) +~ netaccess needs_root +with no_debug_dissector(): + score, fprint = nmap_fp(conf.route.route('0.0.0.0')[2]) + print(score, fprint) assert score > 0.5 assert fprint = fingerprint test: to text -~ netaccess +~ netaccess needs_root import re as re_ -a = nmap_sig("www.secdev.org", 80, 81) +with no_debug_dissector(): + a = nmap_sig("www.secdev.org", 80, 81) + a for x in nmap_sig2txt(a).split("\n"): assert re_.match(r"\w{2,4}\(.*\)", x) = nmap_udppacket_sig test: www.google.com -~ netaccess +~ netaccess needs_root + +with no_debug_dissector(): + a = nmap_sig("www.google.com", ucport=80) -a = nmap_sig("www.google.com", ucport=80) assert len(a) > 3 assert len(a["PU"]) > 0 @@ -75,13 +84,13 @@ assert len(a["PU"]) > 0 = Nmap base not available -nmap_kdb.filename = "invalid" -nmap_kdb.reload() -assert nmap_kdb.filename == None +conf.nmap_kdb.filename = "invalid" +conf.nmap_kdb.reload() +assert conf.nmap_kdb.filename == None = Clear temp files try: - os.remove('nmap-os-fingerprints') + os.remove(filename) except: pass diff --git a/test/p0f.uts b/test/p0f.uts index 628163f4258..9b389d523f7 100644 --- a/test/p0f.uts +++ b/test/p0f.uts @@ -2,9 +2,6 @@ ~ p0f - -############ -############ + Basic p0f module tests = Module loading @@ -13,99 +10,113 @@ load_module('p0f') = Fetch database ~ netaccess -from __future__ import print_function try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen -def _load_database(file): - for i in range(10): - try: - open(file, 'wb').write(urlopen('https://raw.githubusercontent.com/p0f/p0f/4b4d1f384abebbb9b1b25b8f3c6df5ad7ab365f7/' + file).read()) - break - except: - raise - pass +for i in range(10): + try: + open("p0f.fp", 'wb').write(urlopen('https://raw.githubusercontent.com/p0f/p0f/e8b924ae7fa099a3a5fe7def0ce3e397fd9a7137/p0f.fp').read()) + break + except: + raise -_load_database("p0f.fp") conf.p0f_base = "p0f.fp" -_load_database("p0fa.fp") -conf.p0fa_base = "p0fa.fp" -_load_database("p0fr.fp") -conf.p0fr_base = "p0fr.fp" -_load_database("p0fo.fp") -conf.p0fo_base = "p0fo.fp" - -p0f_load_knowledgebases() +p0fdb.reload(conf.p0f_base) -############ -############ + Default tests -= Test p0f += Test TCP p0f, SYN - Windows +~ netaccess + +pkt = IP(b'E\x00\x004Se@\x00\x80\x06\x93?\n\x00\x00\x14\n\x00\x00\x0c\xc3\x08\x01\xbb\xcf\xb4\xbb\\\x00\x00\x00\x00\x80\x02 \x00\xeb\x1b\x00\x00\x02\x04\x05\xb4\x01\x03\x03\x08\x01\x01\x04\x02') +assert p0f(pkt) == (('s', 'win', 'Windows', '7 or 8'), 0, False) + += Test TCP p0f, SYN - Linux +~ netaccess + +pkt = IP(b"E\x10\x00 40.77.226.249:https (S) (distance 0)\n' +pkt = IP(b"E\x00\x05\xdc'\xde@\x00\xfb\x06\x0b\xc1\xae\x8f\xd5\xb8\xc0\xa8\x01\x8c\x00P\xe1N\xc7R\x9d\x89\x8eP\x19\x88\x80\x10\x00lS\xc4\x00\x00\x01\x01\x08\n1\xc7\xbaT\x00!\xd2_HTTP/1.1 200 OK\r\nServer: nginx/0.8.53\r\nDate: Tue, 01 Mar 2011 20:45:16 GMT\r\nContent-Type: image/png\r\nContent-Length: 21684\r\nLast-Modified: Fri, 21 Jan 2011 03:41:14 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=20\r\nExpires: Wed, 29 Feb 2012 20:45:16 GMT\r\nCache-Control: max-age=31536000\r\nCache-Control: public\r\nVary: Accept-Encoding\r\nAccept-Ranges: bytes\r\n\r\n") +assert p0f(pkt) == (('s', '!', 'nginx', '1.x', ('@unix',)), False) + += Test MTU p0f +~ netaccess + +pkt = IP(b'E\x00\x004Se@\x00\x80\x06\x93?\n\x00\x00\x14\n\x00\x00\x0c\xc3\x08\x01\xbb\xcf\xb4\xbb\\\x00\x00\x00\x00\x80\x02 \x00\xeb\x1b\x00\x00\x02\x04\x05\xb4\x01\x03\x03\x08\x01\x01\x04\x02') +assert fingerprint_mtu(pkt) == "Ethernet or modem" + -############ -############ + Tests for p0f_impersonate -# XXX: a lot of pieces of p0f_impersonate don't have tests yet. += Check that the impersonated packet is properly detected by p0f +~ netaccess + +pkt = p0f_impersonate(IP()/TCP(), osgenre="Linux", osdetails="3.11 and newer") +assert p0f(pkt) == (("s", "unix", "Linux", "3.11 and newer"), 0, False) + += Check incidence of MSS value on linux version detection +~ netaccess + +pkt = IP(ttl=64, flags=2)/TCP(options=[('MSS', 14), ('SAckOK', ''), ('Timestamp', (2638474259, 0)), ('NOP', None), ('WScale', 7)], window=280, seq=3964706621, flags=2) +assert p0f(pkt) == (('g', 'unix', 'Linux', '2.2.x-3.x'), 0, False) + +pkt[TCP].options = [('MSS', 100), ('SAckOK', ''), ('Timestamp', (2638474259, 0)), ('NOP', None), ('WScale', 7)] +pkt[TCP].window = 100*20 +assert p0f(pkt) == (("s", "unix", "Linux", "3.11 and newer"), 0, False) = Impersonate when window size must be multiple of some integer -sig = ('%467', 64, 1, 60, 'M*,W*', '.', 'Phony Sys', '1.0') +sig = "*:64:0:1460:%8192,0:mss,nop,ws::0" pkt = p0f_impersonate(IP()/TCP(), signature=sig) -assert pkt.payload.window % 467 == 0 +assert pkt[TCP].window % 8192 == 0 -= Handle unusual flags ("F") quirk -sig = ('1024', 64, 0, 60, 'W*', 'F', 'Phony Sys', '1.0') += Impersonate when window size must be multiple of mss +sig = "*:64:0:1024:mss*4,0:mss::0" pkt = p0f_impersonate(IP()/TCP(), signature=sig) -assert (pkt.payload.flags & 40) in (8, 32, 40) +assert (pkt[TCP].window // 4) == 1024 -= Use valid option values from original packet -sig = ('S4', 64, 1, 60, 'M*,W*,T', '.', 'Phony Sys', '1.0') -opts = [('MSS', 1400), ('WScale', 3), ('Timestamp', (97256, 0))] -pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) -assert pkt.payload.options == opts += Impersonate when the following quirks are present: seq-,ack-,pushf+,urgf+ +sig = "*:64:0:1460:8192,0:mss:seq-,ack-,pushf+,urgf+:0" +pkt = p0f_impersonate(IP()/TCP(seq=1, ack=1, flags="S"), signature=sig) +tcp = pkt[TCP] +assert pkt[TCP].seq == pkt[TCP].ack == 0 +assert pkt[TCP].flags.A and pkt[TCP].flags.P and pkt[TCP].flags.U -= Use valid option values when multiples required -sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0') -opts = [('MSS', 37*15), ('WScale', 19*12)] -pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) -assert pkt.payload.options == opts - -= Discard non-multiple option values when multiples required -sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0') -opts = [('MSS', 37*15 + 1), ('WScale', 19*12 + 1)] += Use valid option values from original packet +sig = "*:64:0:*:8192,*:mss,ws,ts::0" +opts = [("MSS", 1400), ("WScale", 3), ("Timestamp", (97256, 0))] pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) -assert pkt.payload.options[0][1] % 37 == 0 -assert pkt.payload.options[1][1] % 19 == 0 +assert pkt[TCP].options == opts -= Discard bad timestamp values -sig = ('S4', 64, 1, 60, 'M*,T', '.', 'Phony Sys', '1.0') -opts = [('Timestamp', (0, 1000))] -pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) -# since option is "T" and not "T0": -assert pkt.payload.options[1][1][0] > 0 -# since T quirk is not present: -assert pkt.payload.options[1][1][1] == 0 - -= Discard 2nd timestamp of 0 if "T" quirk is present -sig = ('S4', 64, 1, 60, 'M*,T', 'T', 'Phony Sys', '1.0') -opts = [('Timestamp', (54321, 0))] += Discard invalid options values +sig = "*:64:0:1000:8192,5:mss,ws::0" +opts = [("MSS", 1400), ("WScale", 3)] pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) -assert pkt.payload.options[1][1][1] > 0 +assert pkt[TCP].options[0][1] == 1000 +assert pkt[TCP].options[1][1] == 5 + Clear temp files @@ -117,6 +128,3 @@ def _rem(f): pass _rem("p0f.fp") -_rem("p0fa.fp") -_rem("p0fr.fp") -_rem("p0fo.fp") \ No newline at end of file diff --git a/test/p0fv2.uts b/test/p0fv2.uts new file mode 100644 index 00000000000..3933f5ecfd1 --- /dev/null +++ b/test/p0fv2.uts @@ -0,0 +1,122 @@ +% Tests for Scapy's p0fv2 module. + +~ p0f + + +############ +############ ++ Basic p0f module tests + += Module loading +load_module('p0fv2') + += Fetch database +~ netaccess + +try: + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +def _load_database(file): + for i in range(10): + try: + with open(file, 'wb') as fd: + fd.write(urlopen('https://raw.githubusercontent.com/p0f/p0f/4b4d1f384abebbb9b1b25b8f3c6df5ad7ab365f7/' + file).read()) + break + except: + raise + pass + +_load_database("p0f.fp") +conf.p0f_base = "p0f.fp" +_load_database("p0fa.fp") +conf.p0fa_base = "p0fa.fp" +_load_database("p0fr.fp") +conf.p0fr_base = "p0fr.fp" +_load_database("p0fo.fp") +conf.p0fo_base = "p0fo.fp" + +p0f_load_knowledgebases() + +############ +############ ++ Default tests + += Test p0f +~ netaccess + +pkt = Ether(b'\x14\x0cv\x8f\xfe(\xd0P\x99V\xdd\xf9\x08\x00E\x00\x0045+@\x00\x80\x06\x00\x00\xc0\xa8\x00w(M\xe2\xf9\xda\xcb\x01\xbbcc\xdd\x1e\x00\x00\x00\x00\x80\x02\xfa\xf0\xcc\x8c\x00\x00\x02\x04\x05\xb4\x01\x03\x03\x08\x01\x01\x04\x02') + +assert p0f(pkt) == [('@Windows', 'XP/2000 (RFC1323+, w+, tstamp-)', 0)] + += Test prnp0f +~ netaccess + +with ContextManagerCaptureOutput() as cmco: + prnp0f(pkt) + assert cmco.get_output() == '192.168.0.119:56011 - @Windows XP/2000 (RFC1323+, w+, tstamp-)\n -> 40.77.226.249:https (S) (distance 0)\n' + +############ +############ ++ Tests for p0f_impersonate + +# XXX: a lot of pieces of p0f_impersonate don't have tests yet. + += Impersonate when window size must be multiple of some integer +sig = ('%467', 64, 1, 60, 'M*,W*', '.', 'Phony Sys', '1.0') +pkt = p0f_impersonate(IP()/TCP(), signature=sig) +assert pkt.payload.window % 467 == 0 + += Handle unusual flags ("F") quirk +sig = ('1024', 64, 0, 60, 'W*', 'F', 'Phony Sys', '1.0') +pkt = p0f_impersonate(IP()/TCP(), signature=sig) +assert (pkt.payload.flags & 40) in (8, 32, 40) + += Use valid option values from original packet +sig = ('S4', 64, 1, 60, 'M*,W*,T', '.', 'Phony Sys', '1.0') +opts = [('MSS', 1400), ('WScale', 3), ('Timestamp', (97256, 0))] +pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) +assert pkt.payload.options == opts + += Use valid option values when multiples required +sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0') +opts = [('MSS', 37*15), ('WScale', 19*12)] +pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) +assert pkt.payload.options == opts + += Discard non-multiple option values when multiples required +sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0') +opts = [('MSS', 37*15 + 1), ('WScale', 19*12 + 1)] +pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) +assert pkt.payload.options[0][1] % 37 == 0 +assert pkt.payload.options[1][1] % 19 == 0 + += Discard bad timestamp values +sig = ('S4', 64, 1, 60, 'M*,T', '.', 'Phony Sys', '1.0') +opts = [('Timestamp', (0, 1000))] +pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) +# since option is "T" and not "T0": +assert pkt.payload.options[1][1][0] > 0 +# since T quirk is not present: +assert pkt.payload.options[1][1][1] == 0 + += Discard 2nd timestamp of 0 if "T" quirk is present +sig = ('S4', 64, 1, 60, 'M*,T', 'T', 'Phony Sys', '1.0') +opts = [('Timestamp', (54321, 0))] +pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig) +assert pkt.payload.options[1][1][1] > 0 + ++ Clear temp files + += Remove fp files +def _rem(f): + try: + os.remove(f) + except: + pass + +_rem("p0f.fp") +_rem("p0fa.fp") +_rem("p0fr.fp") +_rem("p0fo.fp") diff --git a/test/packet.uts b/test/packet.uts deleted file mode 100644 index cd6c944d647..00000000000 --- a/test/packet.uts +++ /dev/null @@ -1,299 +0,0 @@ -% Regression tests for Scapy packets - -+ Test packet conversion (convert_to/convert_packet) - -= Setup packet conversion - -def expect_exception(e, c): - try: - c() - return False - except e: - return True - -def no_theme(c): - old_theme = conf.color_theme - conf.color_theme = NoTheme() - try: - return c() - finally: - conf.color_theme = old_theme - -# PacketA declares no conversions, but will have conversions declared by -# PacketB. -class PacketA(Packet): - name = 'PacketA' - fields_desc = [LEShortField('foo', None)] - -# PacketB declares conversions for PacketA -class PacketB(Packet): - name = 'PacketB' - fields_desc = [ShortField('bar', None)] - def convert_to(self, other_cls, **kwargs): - if other_cls is PacketA: - return PacketA(foo=self.bar) - return Packet.convert_to(self, other_cls, **kwargs) - @classmethod - def convert_packet(cls, pkt, **kwargs): - if isinstance(pkt, PacketA): - return cls(bar=pkt.foo) - return Packet.convert_packet(pkt, **kwargs) - -# PacketC has no defined conversions, either way. -# Converting to or from it should fail. -class PacketC(Packet): - name = 'PacketC' - fields_desc = [IntField('x', None),] - -# Packet D stacks Packet C under it. -class PacketD(Packet): - name = 'PacketD' - fields_desc = [ShortField('version', 12),] - -bind_layers(PacketD, PacketC) - -= Check formatting is expected. - -# This isn't strictly relevant for the test, but it ensures that we have the -# environment we expected. - -p = PacketA(b'\xd2\x04') -assert p.foo == 1234 - -p = PacketB(b'\x04\xd2') -assert p.bar == 1234 - -p = PacketC(b'\0\0\x04\xd2') -assert p.x == 1234 - -p = PacketD(b'\0\x0c\0\0\x04\xd2') -assert p.version == 12 -assert p.haslayer(PacketC) -assert p.x == 1234 - -p = PacketA(foo=1234) -assert raw(p) == b'\xd2\x04' - -p = PacketB(raw(p)) -assert p.bar == 53764 - -p = PacketB(bar=1234) -assert raw(p) == b'\x04\xd2' - -p = PacketA(raw(p)) -assert p.foo == 53764 - -p = PacketC(x=1234) -assert raw(p) == b'\0\0\x04\xd2' - -p = PacketD()/PacketC(x=1234) -assert raw(p) == b'\0\x0c\0\0\x04\xd2' - -= convert_to A -> B - -p1 = PacketA(foo=1234) -p2 = p1.convert_to(PacketB) - -assert p1.foo == 1234 -assert p2.bar == 1234 - -= convert_to A -> C (fails) - -p1 = PacketA(foo=1234) -expect_exception(TypeError, lambda: p1.convert_to(PacketC)) - -assert p1.foo == 1234 - -= convert_to A -> Raw - -p1 = PacketA(foo=1234) -p2 = p1.convert_to(Raw) - -assert p1.foo == 1234 -assert isinstance(p2, Raw) -assert p2.load == b'\xd2\x04' - -= convert_to B -> A - -p1 = PacketB(bar=1234) -p2 = p1.convert_to(PacketA) - -assert p1.bar == 1234 -assert p2.foo == 1234 - -= convert_to B -> C (fails) - -p1 = PacketB(bar=1234) -expect_exception(TypeError, lambda: p1.convert_to(PacketC)) - -assert p1.bar == 1234 - -= convert_to B -> Raw - -p1 = PacketB(bar=1234) -p2 = p1.convert_to(Raw) - -assert p1.bar == 1234 -assert isinstance(p2, Raw) -assert p2.load == b'\x04\xd2' - -= convert_to D -> C (fails) - -p1 = PacketD()/PacketC(x=1234) -expect_exception(TypeError, lambda: p1.convert_to(PacketC)) - -assert p1.x == 1234 - -= convert_to invalid type - -try: - PacketA().convert_to(3) -except TypeError: - pass -else: - assert False - -= convert_packet A -> B - -p1 = PacketA(foo=1234) -p2 = PacketB.convert_packet(p1) - -assert p1.foo == 1234 -assert p2.bar == 1234 - -= convert_packet A -> C (fails) - -p1 = PacketA(foo=1234) -expect_exception(TypeError, lambda: PacketC.convert_packet(p1)) - -assert p1.foo == 1234 - -= convert_packet A -> Raw - -p1 = PacketA(foo=1234) -p2 = Raw.convert_packet(p1) - -assert p1.foo == 1234 -assert isinstance(p2, Raw) -assert p2.load == b'\xd2\x04' - -= convert_packet B -> A - -p1 = PacketB(bar=1234) -p2 = PacketA.convert_packet(p1) - -assert p1.bar == 1234 -assert p2.foo == 1234 - -= convert_packet B -> C (fails) - -p1 = PacketB(bar=1234) -expect_exception(TypeError, lambda: PacketC.convert_packet(p1)) - -assert p1.bar == 1234 - -= convert_packet B -> Raw - -p1 = PacketB(bar=1234) -p2 = Raw.convert_packet(p1) - -assert p1.bar == 1234 -assert isinstance(p2, Raw) -assert p2.load == b'\x04\xd2' - -= convert_packet D -> C (fails) - -p1 = PacketD()/PacketC(x=1234) -expect_exception(TypeError, lambda: PacketC.convert_packet(p1)) - -assert p1.x == 1234 - -= convert_packets A -> B - -p1 = [PacketA(foo=x) for x in range(100)] -p2 = list(PacketB.convert_packets(p1)) - -for x in range(100): - assert p1[x].foo == x - assert p2[x].bar == x - -= convert_packets B -> A - -p1 = [PacketB(bar=x) for x in range(100)] -p2 = list(PacketA.convert_packets(p1)) - -for x in range(100): - assert p1[x].bar == x - assert p2[x].foo == x - -= convert_packet invalid type - -try: - PacketA.convert_packet(3) -except TypeError: - pass -else: - assert False - -= PacketList.convert_to A -> B - -pl1 = PacketList([PacketA(foo=x) for x in range(10)]) -pl2 = pl1.convert_to(PacketB) - -for i, p2 in enumerate(pl2): - assert p2.bar == i - -assert 'PacketB' in pl2.listname - -= PacketList.convert_to custom name / stats - -pl1 = PacketList( - [PacketA(foo=x) for x in range(10)], name='my old list', stats=[PacketA]) -assert pl1.listname == 'my old list' - -pl2 = pl1.convert_to(PacketB, name='my new list', stats=[PacketB, PacketC]) - -for i, p2 in enumerate(pl2): - assert p2.bar == i - -assert pl2.listname == 'my new list' - -assert no_theme(lambda: 'PacketA' not in str(pl2)) -assert no_theme(lambda: 'PacketB:10' in str(pl2)) - -= PacketList.getlayer - -pl1 = PacketList([PacketC(x=x)/PacketD(version=x*2) for x in range(10)]) -pl2 = pl1.getlayer(PacketD) - -for i, p2 in enumerate(pl2): - assert p2.version == i * 2 - -assert 'PacketD' in pl2.listname - -= PacketList.getlayer custom name / stats - -pl1 = PacketList( - [PacketC(x=x)/PacketD(version=x*2) for x in range(10)], - name='old list', stats=[PacketC]) -pl2 = pl1.getlayer(PacketD, name='new list', stats=[PacketD]) - -for i, p2 in enumerate(pl2): - assert p2.version == i * 2 - -assert pl2.listname == 'new list' -assert no_theme(lambda: 'PacketC' not in str(pl2)) -assert no_theme(lambda: 'PacketD:10' in str(pl2)) - -= PacketList.getlayer filtering - -pl1 = PacketList([PacketC(x=x)/PacketD(version=x*2) for x in range(10)]) -pl2 = pl1.getlayer(PacketD, nb=1, flt={'version': 6}) - -assert len(pl2) == 1 -assert pl2[0].version == 6 - -= PacketList.getlayer filtering with 0 results - -pl2 = pl1.getlayer(PacketD, nb=1, flt={'version': 1}) -assert len(pl2) == 0 diff --git a/test/pcaps/bad_rsn_parsing_overrides_ssid.pcap b/test/pcaps/bad_rsn_parsing_overrides_ssid.pcap new file mode 100644 index 00000000000..ce764004a46 Binary files /dev/null and b/test/pcaps/bad_rsn_parsing_overrides_ssid.pcap differ diff --git a/test/pcaps/bgp_fragmented.pcap.gz b/test/pcaps/bgp_fragmented.pcap.gz new file mode 100644 index 00000000000..71d84784e31 Binary files /dev/null and b/test/pcaps/bgp_fragmented.pcap.gz differ diff --git a/test/pcaps/candump_gmlan_scanner.pcap.gz b/test/pcaps/candump_gmlan_scanner.pcap.gz new file mode 100644 index 00000000000..c996c15f329 Binary files /dev/null and b/test/pcaps/candump_gmlan_scanner.pcap.gz differ diff --git a/test/pcaps/candump_uds_scanner.pcap.gz b/test/pcaps/candump_uds_scanner.pcap.gz new file mode 100644 index 00000000000..7976dfad2e8 Binary files /dev/null and b/test/pcaps/candump_uds_scanner.pcap.gz differ diff --git a/test/pcaps/canfd.pcap.gz b/test/pcaps/canfd.pcap.gz new file mode 100644 index 00000000000..f4349b4f9f9 Binary files /dev/null and b/test/pcaps/canfd.pcap.gz differ diff --git a/test/pcaps/dcerpc_msdrsr_cracknames.pcapng.gz b/test/pcaps/dcerpc_msdrsr_cracknames.pcapng.gz new file mode 100644 index 00000000000..20cbb1b9c06 Binary files /dev/null and b/test/pcaps/dcerpc_msdrsr_cracknames.pcapng.gz differ diff --git a/test/pcaps/dcerpc_msnrpc.pcapng.gz b/test/pcaps/dcerpc_msnrpc.pcapng.gz new file mode 100644 index 00000000000..cd8b45a4bb1 Binary files /dev/null and b/test/pcaps/dcerpc_msnrpc.pcapng.gz differ diff --git a/test/pcaps/dcerpc_privacy_krb.pcapng.gz b/test/pcaps/dcerpc_privacy_krb.pcapng.gz new file mode 100644 index 00000000000..0d17553efdf Binary files /dev/null and b/test/pcaps/dcerpc_privacy_krb.pcapng.gz differ diff --git a/test/pcaps/dcerpc_privacy_ntlm.pcapng.gz b/test/pcaps/dcerpc_privacy_ntlm.pcapng.gz new file mode 100644 index 00000000000..1592d551f85 Binary files /dev/null and b/test/pcaps/dcerpc_privacy_ntlm.pcapng.gz differ diff --git a/test/pcaps/doip.pcap.gz b/test/pcaps/doip.pcap.gz new file mode 100644 index 00000000000..eb664db3048 Binary files /dev/null and b/test/pcaps/doip.pcap.gz differ diff --git a/test/pcaps/doip_ack.pcap b/test/pcaps/doip_ack.pcap new file mode 100644 index 00000000000..9c07e43cfdc Binary files /dev/null and b/test/pcaps/doip_ack.pcap differ diff --git a/test/pcaps/doip_functional_request.pcap.gz b/test/pcaps/doip_functional_request.pcap.gz new file mode 100644 index 00000000000..c2b9e9cf35f Binary files /dev/null and b/test/pcaps/doip_functional_request.pcap.gz differ diff --git a/test/pcaps/ecu_trace.pcap.gz b/test/pcaps/ecu_trace.pcap.gz new file mode 100644 index 00000000000..261d649372c Binary files /dev/null and b/test/pcaps/ecu_trace.pcap.gz differ diff --git a/test/pcaps/gmlan_trace.pcap.gz b/test/pcaps/gmlan_trace.pcap.gz new file mode 100644 index 00000000000..cacfee19f78 Binary files /dev/null and b/test/pcaps/gmlan_trace.pcap.gz differ diff --git a/test/pcaps/gvrp.pcapng.gz b/test/pcaps/gvrp.pcapng.gz new file mode 100644 index 00000000000..8e040394718 Binary files /dev/null and b/test/pcaps/gvrp.pcapng.gz differ diff --git a/test/pcaps/http_compressed-zstd.pcap b/test/pcaps/http_compressed-zstd.pcap new file mode 100644 index 00000000000..0f2bdd64e8f Binary files /dev/null and b/test/pcaps/http_compressed-zstd.pcap differ diff --git a/test/pcaps/http_head.pcapng.gz b/test/pcaps/http_head.pcapng.gz new file mode 100644 index 00000000000..86626f135cd Binary files /dev/null and b/test/pcaps/http_head.pcapng.gz differ diff --git a/test/pcaps/http_tcp_psh.pcap.gz b/test/pcaps/http_tcp_psh.pcap.gz new file mode 100644 index 00000000000..6b8b170a357 Binary files /dev/null and b/test/pcaps/http_tcp_psh.pcap.gz differ diff --git a/test/pcaps/ikev2_nat_t.pcapng b/test/pcaps/ikev2_nat_t.pcapng new file mode 100644 index 00000000000..8492f15196b Binary files /dev/null and b/test/pcaps/ikev2_nat_t.pcapng differ diff --git a/test/pcaps/ikev2_notify_redirect.pcap b/test/pcaps/ikev2_notify_redirect.pcap new file mode 100644 index 00000000000..454753f0add Binary files /dev/null and b/test/pcaps/ikev2_notify_redirect.pcap differ diff --git a/test/pcaps/macos.pcapng.gz b/test/pcaps/macos.pcapng.gz new file mode 100644 index 00000000000..491e13d8b2d Binary files /dev/null and b/test/pcaps/macos.pcapng.gz differ diff --git a/test/pcaps/multiple_doip_layers.pcap.gz b/test/pcaps/multiple_doip_layers.pcap.gz new file mode 100644 index 00000000000..79302b2bc76 Binary files /dev/null and b/test/pcaps/multiple_doip_layers.pcap.gz differ diff --git a/test/pcaps/pfcp.pcap b/test/pcaps/pfcp.pcap new file mode 100644 index 00000000000..89fb650a9bb Binary files /dev/null and b/test/pcaps/pfcp.pcap differ diff --git a/test/pcaps/psp_v4_cleartext.pcap.gz b/test/pcaps/psp_v4_cleartext.pcap.gz new file mode 100644 index 00000000000..c1ea14c2827 Binary files /dev/null and b/test/pcaps/psp_v4_cleartext.pcap.gz differ diff --git a/test/pcaps/psp_v4_encrypt_transport_crypt_off_128.pcap.gz b/test/pcaps/psp_v4_encrypt_transport_crypt_off_128.pcap.gz new file mode 100644 index 00000000000..88b6e527141 Binary files /dev/null and b/test/pcaps/psp_v4_encrypt_transport_crypt_off_128.pcap.gz differ diff --git a/test/pcaps/psp_v4_encrypt_transport_crypt_off_128_vc.pcap.gz b/test/pcaps/psp_v4_encrypt_transport_crypt_off_128_vc.pcap.gz new file mode 100644 index 00000000000..648ee4630ce Binary files /dev/null and b/test/pcaps/psp_v4_encrypt_transport_crypt_off_128_vc.pcap.gz differ diff --git a/test/pcaps/psp_v4_encrypt_transport_crypt_off_256.pcap.gz b/test/pcaps/psp_v4_encrypt_transport_crypt_off_256.pcap.gz new file mode 100644 index 00000000000..0661915d5c8 Binary files /dev/null and b/test/pcaps/psp_v4_encrypt_transport_crypt_off_256.pcap.gz differ diff --git a/test/pcaps/ssh_ed25519.pcap b/test/pcaps/ssh_ed25519.pcap new file mode 100644 index 00000000000..8d10143541a Binary files /dev/null and b/test/pcaps/ssh_ed25519.pcap differ diff --git a/test/pcaps/tls_tcp_frag.pcap.gz b/test/pcaps/tls_tcp_frag.pcap.gz new file mode 100644 index 00000000000..166fca06cf6 Binary files /dev/null and b/test/pcaps/tls_tcp_frag.pcap.gz differ diff --git a/test/pcaps/tls_tcp_frag_withnss.pcap.gz b/test/pcaps/tls_tcp_frag_withnss.pcap.gz new file mode 100644 index 00000000000..a9c19fcc770 Binary files /dev/null and b/test/pcaps/tls_tcp_frag_withnss.pcap.gz differ diff --git a/test/pipetool.uts b/test/pipetool.uts index 0acb71b7bb9..f622f385eec 100644 --- a/test/pipetool.uts +++ b/test/pipetool.uts @@ -85,7 +85,7 @@ assert len(b.high_sources) == 1 a b -a = AutoSource() +a = Sink() b = AutoSource() a << b assert len(a.high_sinks) == 0 @@ -95,16 +95,16 @@ assert len(b.high_sources) == 0 a b -a = AutoSource() -b = AutoSource() -a == b +a = Sink() +b = Sink() +a % b assert len(a.sinks) == 1 assert len(a.sources) == 1 assert len(b.sinks) == 1 assert len(b.sources) == 1 -a = AutoSource() -b = AutoSource() +a = Sink() +b = Sink() a//b assert len(a.high_sinks) == 1 assert len(a.high_sources) == 1 @@ -112,7 +112,7 @@ assert len(b.high_sinks) == 1 assert len(b.high_sources) == 1 a = AutoSource() -b = AutoSource() +b = Sink() a^b assert len(b.trigger_sources) == 1 assert len(a.trigger_sinks) == 1 @@ -216,18 +216,25 @@ p.start() p.wait_and_stop() assert test_val == "hello" += Test PeriodicSource exhaustion + +s = PeriodicSource("", 1) +s.msg = [] +p = PipeEngine(s) +p.start() +p.wait_and_stop() + + Advanced ScapyPipes pipetools tests = Test SniffSource -import mock -r, w = os.pipe() -os.write(w, b"X") - -mocked_l2listen = mock.patch("scapy.config.conf.L2listen", return_value=Bunch(close=lambda *args: None, fileno=lambda: r, recv=lambda *args: Raw("data"))) -mocked_l2listen.start() +from unittest import mock +fd = ObjectPipe("sniffsource") +fd.write("test") -try: +@mock.patch("scapy.scapypipes.conf.L2listen") +def _test(l2listen): + l2listen.return_value=Bunch(close=lambda *args: None, fileno=lambda: fd.fileno(), recv=lambda *args: Raw("data")) p = PipeEngine() s = SniffSource() assert s.s is None @@ -236,18 +243,20 @@ try: s > d1 > c p.add(s) p.start() - assert c.q.get(2) + x = c.q.get(2) + assert bytes(x) == b"data" assert s.s is not None p.stop() + +try: + _test() finally: - mocked_l2listen.stop() - os.close(r) - os.close(w) + fd.close() = Test SniffSource with socket -r, w = os.pipe() -os.write(w, b"X") +fd = ObjectPipe("sniffsource_socket") +fd.write("test") class FakeSocket(object): def __init__(self): @@ -258,7 +267,7 @@ class FakeSocket(object): self.times += 1 return Raw(b'hello') def fileno(self): - return r + return fd.fileno() try: p = PipeEngine() @@ -272,8 +281,7 @@ try: p.stop() assert raw(msg) == b'hello' finally: - os.close(r) - os.close(w) + fd.close() = Test SniffSource with invalid args @@ -287,7 +295,7 @@ else: = Test exhausted AutoSource and SniffSource -import mock +from unittest import mock from scapy.error import Scapy_Exception def _fail(): @@ -295,7 +303,7 @@ def _fail(): a = AutoSource() a._send = mock.MagicMock(side_effect=_fail) -a._wake_up() +a.send("x") try: a.deliver() except: @@ -310,89 +318,68 @@ except: pass = Test WiresharkSink +~ wiresharksink -from io import BytesIO +q = ObjectPipe("wiresharksink") +pkt = Ether(dst="aa:aa:aa:aa:aa:aa", src="bb:bb:bb:bb:bb:bb")/IP(dst="127.0.0.1", src="127.0.0.1")/ICMP() -f = BytesIO() -pkt = Ether()/IP()/ICMP() - -with mock.patch("subprocess.Popen", return_value=Bunch(stdin=f)) as popen: - p = PipeEngine() - src = CLIFeeder() +from unittest import mock +with mock.patch("scapy.scapypipes.subprocess.Popen", return_value=Bunch(stdin=q)) as popen: sink = WiresharkSink() - p.add(src > sink) - p.start() - src.send(pkt) - time.sleep(3) - # Prevent stop from closing the BytesIO - with mock.patch.object(f, 'close'): - p.stop() + sink.start() + +sink.push(pkt) + +q.recv() +q.recv() +assert raw(pkt) in q.recv() popen.assert_called_once_with( - [conf.prog.wireshark, '-ki', '-'], stdin=subprocess.PIPE, stdout=None, + [conf.prog.wireshark, '-Slki', '-'], stdin=subprocess.PIPE, stdout=None, stderr=None) -bytes_hex(f.getvalue()) -bytes_hex(raw(pkt)) -assert raw(pkt) in f.getvalue() = Test WiresharkSink with linktype +~ wiresharksink -f = BytesIO() -pkt = Ether()/IP()/ICMP() linktype = scapy.data.DLT_EN3MB +q = ObjectPipe("wiresharksink_linktype") +pkt = Ether(dst="aa:aa:aa:aa:aa:aa", src="bb:bb:bb:bb:bb:bb")/IP(dst="127.0.0.1", src="127.0.0.1")/ICMP() -with mock.patch("subprocess.Popen", return_value=Bunch(stdin=f)) as popen: - p = PipeEngine() - src = CLIFeeder() +from unittest import mock +with mock.patch("scapy.scapypipes.subprocess.Popen", return_value=Bunch(stdin=q)) as popen: sink = WiresharkSink(linktype=linktype) - p.add(src > sink) - p.start() - src.send(pkt) - time.sleep(3) - # Prevent stop from closing the BytesIO - with mock.patch.object(f, 'close'): - p.stop() - -popen.assert_called_once_with( - [conf.prog.wireshark, '-ki', '-'], - stdin=subprocess.PIPE, stdout=None, stderr=None) + sink.start() -bytes_hex(f.getvalue()) -bytes_hex(raw(pkt)) -assert raw(pkt) in f.getvalue() +sink.push(pkt) -# Check that the linktype was also correct -f.seek(0) or None -r = PcapReader(f) -assert r.linktype == DLT_EN3MB +chb(linktype) in q.recv() +q.recv() +assert raw(pkt) in q.recv() = Test WiresharkSink with args +~ wiresharksink -f = BytesIO() -pkt = Ether()/IP()/ICMP() +linktype = scapy.data.DLT_EN3MB +q = ObjectPipe("wiresharksink_args") +pkt = Ether(dst="aa:aa:aa:aa:aa:aa", src="bb:bb:bb:bb:bb:bb")/IP(dst="127.0.0.1", src="127.0.0.1")/ICMP() -with mock.patch("subprocess.Popen", return_value=Bunch(stdin=f)) as popen: - p = PipeEngine() - src = CLIFeeder() +from unittest import mock +with mock.patch("scapy.scapypipes.subprocess.Popen", return_value=Bunch(stdin=q)) as popen: sink = WiresharkSink(args=['-c', '1']) - p.add(src > sink) - p.start() - src.send(pkt) - time.sleep(3) - # Prevent stop from closing the BytesIO - with mock.patch.object(f, 'close'): - p.stop() + sink.start() + +sink.push(pkt) popen.assert_called_once_with( - [conf.prog.wireshark, '-ki', '-', '-c', '1'], + [conf.prog.wireshark, '-Slki', '-', '-c', '1'], stdin=subprocess.PIPE, stdout=None, stderr=None) = Test RdpcapSource and WrpcapSink dname = get_temp_dir() -req = Ether()/IP()/ICMP() -rpy = Ether()/IP(b'E\x00\x00\x1c\x00\x00\x00\x004\x01\x1d\x04\xd8:\xd0\x83\xc0\xa8\x00w\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +req = Ether(b'E\x00\x00\x1c\x00\x00\x00\x004\x01\x1d\x04\xd8:\xd0\x83\xc0\xa8\x00w\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +rpy = Ether(b'\x8c\xf8\x13C5P\xdcS`\xeb\x80H\x08\x00E\x00\x00\x1c\x00\x00\x00\x004\x01\x1d\x04\xd8:\xd0\x83\xc0\xa8\x00w\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') wrpcap(os.path.join(dname, "t.pcap"), [req, rpy]) @@ -400,24 +387,24 @@ p = PipeEngine() s = RdpcapSource(os.path.join(dname, "t.pcap")) d1 = Drain(name="d1") -c = WrpcapSink(os.path.join(dname, "t2.pcap"), name="c") +c = WrpcapSink(os.path.join(dname, "t2.pcap.gz"), name="c", gz=1) s > d1 > c p.add(s) p.start() p.wait_and_stop() -results = rdpcap(os.path.join(dname, "t2.pcap")) +results = rdpcap(os.path.join(dname, "t2.pcap.gz")) assert raw(results[0]) == raw(req) assert raw(results[1]) == raw(rpy) os.unlink(os.path.join(dname, "t.pcap")) -os.unlink(os.path.join(dname, "t2.pcap")) +os.unlink(os.path.join(dname, "t2.pcap.gz")) = Test InjectSink and Inject3Sink ~ needs_root -import mock +from unittest import mock a = IP(dst="192.168.0.1")/ICMP() msgs = [] @@ -666,110 +653,73 @@ p.add(s) p.add(s2) p.start() +pkt = DNS() + s.send(IP(src="127.0.0.1")/UDP()/DNS()) -s2.send(DNS()) +s2.send(pkt) res = [c.q.get(timeout=2), c.q.get(timeout=2)] -assert b'\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' in res -res.remove(b'\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00') +assert raw(pkt) in res +res.remove(raw(pkt)) assert DNS in res[0] and res[0][UDP].sport == 1234 p.stop() -= FDSourceSink on a Bunch object += FDSourceSink on a ObjectPipe object -fd = Bunch(write=lambda x: None, read=lambda: "hello", fileno=lambda: None) +obj = ObjectPipe("fdsourcesink") +obj.send("hello") -s = FDSourceSink(fd) +s = FDSourceSink(obj) d = Drain() c = QueueSink() s > d > c -assert s.fileno() == None s.push("data") s.deliver() assert c.q.get(timeout=1) == "hello" -= TCPConnectPipe networking test += UDPClientPipe and UDPServerPipe ~ networking needs_root p = PipeEngine() s = CLIFeeder() -d1 = TCPConnectPipe(addr="www.secdev.org", port=80) -c = QueueSink() +srv = UDPServerPipe(name="srv", port=10000) +cli = UDPClientPipe(name="cli", addr="127.0.0.1", port=10000) +c = QueueSink(name="c") -s > d1 > c +s > cli +srv > c -p.add(s) +p.add(s, c) p.start() -s.send(b"GET / HTTP/1.1\nHost: www.secdev.org\n\n") -result = c.q.get(timeout=10) +s.send(b"hello") +p.start() +assert c.recv() == b"hello" p.stop() +srv.stop() -assert result.startswith(b"HTTP/1.1 200 OK") - -= Packet conversion (ConvertPipe) - -class PacketA(Packet): - fields_desc = [LEShortField('foo', None)] - -class PacketB(Packet): - fields_desc = [ShortField('bar', None)] - def convert_to(self, other_cls, **kwargs): - if other_cls is PacketA: - return PacketA(foo=self.bar) - return Packet.convert_to(self, other_cls, **kwargs) - @classmethod - def convert_packet(cls, pkt, **kwargs): - if isinstance(pkt, PacketA): - return cls(bar=pkt.foo) - return Packet.convert_packet(pkt, **kwargs) += TCPConnectPipe networking test +~ networking needs_root p = PipeEngine() + s = CLIFeeder() -sh = CLIHighFeeder() -c = ConvertPipe(low_type=PacketA) -d = QueueSink() +d1 = TCPConnectPipe(addr="www.google.com", port=80) +c = QueueSink() -s > c > d -sh >> c >> d +s > d1 > c p.add(s) p.start() -# QueueSink puts all packets in the same queue, and this can race on Windows -s.send(PacketB(bar=1234)) -r0 = d.q.get(timeout=5) - -sh.send(PacketB(bar=1234)) -r1 = d.q.get(timeout=5) +from scapy.layers.http import HTTPRequest, HTTP +s.send(bytes(HTTP()/HTTPRequest(Host="www.google.com"))) +result = c.q.get(timeout=10) p.stop() -# Debug info -r0, raw(r0) -r1, raw(r1) - -assert raw(r0) == b'\xd2\x04' -assert raw(r1) == b'\x04\xd2' -assert isinstance(r0, PacketA) -assert isinstance(r1, PacketB) - -# Try converting on high -c.high_type = PacketB -c.low_type = None - -p.start() -s.send(PacketA(foo=1234)) -r0 = d.q.get(timeout=5) -sh.send(PacketA(foo=1234)) -r1 = d.q.get(timeout=5) -p.stop() +result +assert result.startswith(b"HTTP/1.1 200 OK") or result.startswith(b"HTTP/1.1 302 Found") -r0, raw(r0) -r1, raw(r1) -assert raw(r0) == b'\xd2\x04' -assert raw(r1) == b'\x04\xd2' -assert isinstance(r0, PacketA) -assert isinstance(r1, PacketB) diff --git a/test/random.uts b/test/random.uts new file mode 100644 index 00000000000..c0cec76c5f0 --- /dev/null +++ b/test/random.uts @@ -0,0 +1,135 @@ +% Regression tests for Scapy random objects + +############ +############ ++ Random objects + += RandomEnumeration + +ren = RandomEnumeration(0, 7, seed=0x2807, forever=False) +[x for x in ren] == [5, 0, 2, 7, 6, 3, 1, 4] + += RandIP6 + +random.seed(0x2807) +r6 = RandIP6() +assert r6 == "240b:238f:b53f:b727:d0f9:bfc4:2007:e265" +assert r6.command() == "RandIP6()" + +random.seed(0x2807) +r6 = RandIP6("2001:db8::-") +assert r6 == "2001:0db8::b53f" +assert r6.command() == "RandIP6(ip6template='2001:db8::-')" + +r6 = RandIP6("2001:db8::*") +assert r6 == "2001:0db8::bfc4" +assert r6.command() == "RandIP6(ip6template='2001:db8::*')" + += RandMAC + +random.seed(0x2807) +rm = RandMAC() +assert rm == "24:23:b5:b7:d0:bf" +assert rm.command() == "RandMAC()" + +rm = RandMAC("00:01:02:03:04:0-7") +assert rm == "00:01:02:03:04:01" +assert rm.command() == "RandMAC(template='00:01:02:03:04:0-7')" + + += RandOID + +random.seed(0x2807) +rand_obj = RandOID() +assert rand_obj == "7.222.44.194.276.116.320.6.84.97.31.5.25.20.13.84.104.18" +assert rand_obj.command() == "RandOID()" + +rand_obj = RandOID("1.2.3.*") +assert rand_obj == "1.2.3.41" +assert rand_obj.command() == "RandOID(fmt='1.2.3.*')" + +rand_obj = RandOID("1.2.3.0-28") +assert rand_obj == "1.2.3.12" +assert rand_obj.command() == "RandOID(fmt='1.2.3.0-28')" + +rand_obj = RandOID("1.2.3.0-28", depth=RandNumExpo(0.2), idnum=RandNumExpo(0.02)) +assert rand_obj.command() == "RandOID(fmt='1.2.3.0-28', depth=RandNumExpo(lambd=0.2), idnum=RandNumExpo(lambd=0.02))" + += RandRegExp +~ not_pyannotate + +random.seed(0x2807) +rex = RandRegExp("[g-v]* @? [0-9]{3} . (g|v)") +bytes(rex) == b'irrtv @ 517 \xc2\xb8 v' +assert rex.command() == "RandRegExp(regexp='[g-v]* @? [0-9]{3} . (g|v)')" + +rex = RandRegExp("[:digit:][:space:][:word:]") +assert re.match(b"\\d\\s\\w", bytes(rex)) + += Corrupted(Bytes|Bits) + +random.seed(0x2807) +cb = CorruptedBytes("ABCDE", p=0.5) +assert cb.command() == "CorruptedBytes(s='ABCDE', p=0.5)" +assert sane(raw(cb)) in [".BCD)", "&BCDW"] + +cb = CorruptedBits("ABCDE", p=0.2) +assert cb.command() == "CorruptedBits(s='ABCDE', p=0.2)" +assert sane(raw(cb)) in ["ECk@Y", "QB.P."] + += RandEnumKeys +random.seed(0x2807) +rek = RandEnumKeys({'a': 1, 'b': 2, 'c': 3}, seed=0x2807) +rek.enum.sort() +assert rek.command() == "RandEnumKeys(enum=['a', 'b', 'c'], seed=10247)" +r = str(rek) +assert r == 'a' + += RandSingNum +random.seed(0x2807) +rs = RandSingNum(-28, 7) +assert rs._fix() in [2, 3] +assert rs.command() == "RandSingNum(mn=-28, mx=7)" + += Rand* +random.seed(0x2807) +rss = RandSingString() +assert rss == "foo.exe:" +assert rss.command() == "RandSingString()" + +random.seed(0x2807) +rts = RandTermString(4, "scapy") +assert sane(raw(rts)) in ["...Zscapy", "$#..scapy"] +assert rts.command() == "RandTermString(size=4, term=b'scapy')" + += RandInt (test __bool__) +a = "True" if RandNum(False, True) else "False" +assert a in ["True", "False"] + += Various volatiles + +random.seed(0x2807) +rng = RandNumGamma(1, 42) +assert rng._fix() in (8, 73) +assert rng.command() == "RandNumGamma(alpha=1, beta=42)" + +random.seed(0x2807) +rng = RandNumGauss(1, 42) +assert rng._fix() == 8 +assert rng.command() == "RandNumGauss(mu=1, sigma=42)" + +renum = RandEnum(1, 42, seed=0x2807) +assert renum == 37 +assert renum.command() == "RandEnum(min=1, max=42, seed=10247)" + +rp = RandPool((IncrementalValue(), 42), (IncrementalValue(), 0)) +assert rp == 0 +assert rp.command() == "RandPool((IncrementalValue(), 42), (IncrementalValue(), 0))" + +de = DelayedEval("3 + 1") +assert de == 4 +assert de.command() == "DelayedEval(expr='3 + 1')" + +v = IncrementalValue(restart=2) +assert v == 0 and v == 1 and v == 2 and v == 0 +assert v.command() == "IncrementalValue(restart=2)" diff --git a/test/regression.uts b/test/regression.uts index f637e195c34..1f34aa8e2ad 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -20,7 +20,7 @@ def expect_exception(e, c): conf IP().src -scapy.consts.LOOPBACK_INTERFACE +conf.loopback_name = Test module version detection ~ conf @@ -32,7 +32,7 @@ class FakeModule2(object): __version__ = "5.143.3.12" class FakeModule3(object): - __version__ = "v2.4.2.dev42" + __version__ = "v2.4.2.post42" from scapy.config import _version_checker @@ -44,6 +44,32 @@ assert not _version_checker(FakeModule2, (5, 143, 4)) assert _version_checker(FakeModule3, (2, 4, 2)) += Check Scapy version + +from unittest import mock + +import scapy +from scapy import _parse_tag, _version_from_git_describe +from scapy.config import _version_checker + +b = Bunch(returncode=0, communicate=lambda *args, **kargs: (b"v2.4.5rc1-261-g44b98e14", None)) +with mock.patch('scapy.subprocess.Popen', return_value=b): + with mock.patch('scapy.os.path.isdir', return_value=True): + class GitModuleScapy(object): + __version__ = _version_from_git_describe() + +# GH3847 +with mock.patch('scapy.subprocess.Popen', return_value=b): + with mock.patch('scapy.os.path.isdir', return_value=False): + try: + _version_from_git_describe() + assert False + except ValueError: + pass + +assert GitModuleScapy.__version__ == '2.4.5rc1.post261' +assert _version_checker(GitModuleScapy, (2, 4, 5)) + = List layers ~ conf command ls() @@ -58,6 +84,17 @@ with ContextManagerCaptureOutput() as cmco: assert all("IP" in x for x in result_ls if x.strip()) assert len(result_ls) >= 3 += List packet fields - ls +~ command + +with ContextManagerCaptureOutput() as cmco: + ls(ARP(hwsrc="aa:aa:aa:aa:aa:aa", psrc="1.1.1.1")) + result_ls = cmco.get_output().split("\n") + +result_ls +assert result_ls[5] == "hwsrc : MultipleTypeField (SourceMACField, StrFixedLenField) = 'aa:aa:aa:aa:aa:aa' ('None')" +assert result_ls[6] == "psrc : MultipleTypeField (SourceIPField, SourceIP6Field, StrFixedLenField) = '1.1.1.1' ('None')" + = List commands ~ conf command lsc() @@ -68,12 +105,29 @@ def test_list_contrib(): with ContextManagerCaptureOutput() as cmco: list_contrib() result_list_contrib = cmco.get_output() - assert("http2 : HTTP/2 (RFC 7540, RFC 7541) status=loads" in result_list_contrib) - assert(len(result_list_contrib.split('\n')) > 40) + assert "http2 : HTTP/2 (RFC 7540, RFC 7541) status=loads" in result_list_contrib + assert len(result_list_contrib.split('\n')) > 40 test_list_contrib() -= Test automatic doc generation += Test packet show() on LatexTheme +% with LatexTheme + +class SmallPacket(Packet): + fields_desc = [ByteField("a", 0)] + +conf_color_theme = conf.color_theme +conf.color_theme = LatexTheme() +pkt = SmallPacket() +with ContextManagerCaptureOutput() as cmco: + pkt.show() + result = cmco.get_output().strip() + +assert result == '\\#\\#\\#[ \\textcolor{red}{\\bf SmallPacket} ]\\#\\#\\#\n \\textcolor{blue}{a} = \\textcolor{purple}{0}' +conf.color_theme = conf_color_theme + + += Test rfc() ~ command dat = rfc(IP, ret=True).split("\n") @@ -108,6 +162,82 @@ result = [x.strip() for x in result.split("\n")] output = [x.strip() for x in rfc(IP, ret=True).strip().split("\n")] assert result == output +result = """ + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | CODE | ID | LEN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | TYPE |L|M|S|RES|VERSI| MESSAGE LEN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | DATA | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fig. EAP_TTLS +""".strip() +result = [x.strip() for x in result.split("\n")] +output = [x.strip() for x in rfc(EAP_TTLS, ret=True).strip().split("\n")] +assert result == output + + +result = """ + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |VERSION| TC | FL | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | PLEN | NH | HLIM | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SRC | + + + + | | + + + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | DST | + + + + | | + + + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fig. IPv6 +""".strip() +result = [x.strip() for x in result.split("\n")] +output = [x.strip() for x in rfc(IPv6, ret=True).strip().split("\n")] +assert result == output + + +class TestPad(Packet): + fields_desc = [ShortField("f0", 0), + ShortField("f1", 0), + PadField(ByteField("f2", 1), 8), + PadField(ShortField("f3", 0), 4)] + + +result = """ + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | F0 | F1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | F2 | padding | + +-+-+-+-+-+-+-+-+ + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | F3 | padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fig. TestPad +""".strip() +result = [x.strip() for x in result.split("\n")] +output = [x.strip() for x in rfc(TestPad, ret=True).strip().split("\n")] +assert result == output + = Check that all contrib modules are well-configured ~ command list_contrib(_debug=True) @@ -138,6 +268,32 @@ except: assert not conf.use_bpf += Configuration conf.use_pcap +~ linux libpcap + +if not conf.use_pcap: + assert not conf.iface.provider.libpcap + conf.use_pcap = True + assert conf.iface.provider.libpcap + for iface in conf.ifaces.values(): + assert iface.provider.libpcap or iface.is_valid() == False + conf.use_pcap = False + assert not conf.iface.provider.libpcap + += Test layer filtering +~ filter + +pkt = NetflowHeader()/NetflowHeaderV5()/NetflowRecordV5() + +conf.layers.filter([NetflowHeader, NetflowHeaderV5]) +assert NetflowRecordV5 not in NetflowHeader(bytes(pkt)) + +# Conf.ifaces.reload() should still work (arch/* is exempt) +conf.ifaces.reload() + +conf.layers.unfilter() +assert NetflowRecordV5 in NetflowHeader(bytes(pkt)) + ########### ########### @@ -154,30 +310,19 @@ assert p == "127.0.0.1" = Interface related functions +from unittest import mock + conf.iface -get_if_raw_hwaddr(conf.iface) +get_if_addr(conf.iface) +get_if_hwaddr(conf.iface) bytes_hex(get_if_raw_addr(conf.iface)) def get_dummy_interface(): """Returns a dummy network interface""" - if WINDOWS: - data = {} - data["name"] = "dummy0" - data["description"] = "Does not exist" - data["win_index"] = -1 - data["guid"] = "{1XX00000-X000-0X0X-X00X-00XXXX000XXX}" - data["invalid"] = True - data["ipv4_metric"] = 1 - data["ipv6_metric"] = 1 - data["mac"] = "00:00:00:00:00:00" - data["ips"] = ["127.0.0.1", "::1"] - dummy_int = NetworkInterface(data) - dummy_int.pcap_name = "\\Device\\NPF_" + data["guid"] - return dummy_int - else: - return "dummy0" + conf.ifaces._add_fake_iface("dummy0") + return "dummy0" get_if_raw_addr(get_dummy_interface()) @@ -185,13 +330,154 @@ get_if_list() get_working_if() -get_if_raw_addr6(conf.iface6) +get_if_raw_addr6(conf.iface) + += More Interfaces related functions + +# Test name resolution +old = conf.iface +conf.iface = conf.iface.name +assert conf.iface == old + +assert isinstance(conf.iface, NetworkInterface) +assert conf.iface.is_valid() + +from unittest import mock +@mock.patch("scapy.interfaces.conf.route.routes", []) +@mock.patch("scapy.interfaces.conf.ifaces.values") +def _test_get_working_if(rou): + rou.side_effect = lambda: [] + assert get_working_if() is None + +assert conf.iface + "a" # left + +assert "hey! are you, ready to go ? %s" % conf.iface # format +assert "cuz you know the way to go" + conf.iface # right + + +_test_get_working_if() + += Test conf.ifaces + +conf.iface +conf.ifaces + +assert conf.iface in conf.ifaces.values() +assert conf.ifaces.dev_from_index(conf.iface.index) == conf.iface +assert conf.ifaces.dev_from_networkname(conf.iface.network_name) == conf.iface + +conf.ifaces.data = {'a': NetworkInterface(InterfaceProvider(), {"name": 'a', "network_name": 'a', "description": 'a', "ips": ["127.0.0.1", "::1", "::2", "127.0.0.2"], "mac": 'aa:aa:aa:aa:aa:aa'})} + +with ContextManagerCaptureOutput() as cmco: + conf.ifaces.show() + output = cmco.get_output() + +data = """ +Source Index Name MAC IPv4 IPv6 +Unknown 0 a aa:aa:aa:aa:aa:aa 127.0.0.1 ::1 + 127.0.0.2 ::2 +""".strip() + +output = [x.strip() for x in output.strip().split("\n")] +data = [x.strip() for x in data.strip().split("\n")] + +assert output == data + +conf.ifaces.reload() + += Test extcap detection in conf.ifaces +~ linux extcap + +import os +from scapy.libs.extcap import load_extcap + +_bkp_extcap = conf.prog.extcap_folders +_bkp_providers = conf.ifaces.providers.copy() + +conf.ifaces.providers.clear() + +# Create some sort of extcap parody program +extcapfld = get_temp_dir() +extcapprog = os.path.join(extcapfld, "runner.sh") +data = """#!/usr/bin/env python3 + +import struct +import argparse +parser = argparse.ArgumentParser() +parser.add_argument('--extcap-interfaces', action='store_true') +parser.add_argument('--capture', action='store_true') +parser.add_argument('--extcap-config', action='store_true') +parser.add_argument('--scan-follow-rsp', action='store_true') +parser.add_argument('--scan-follow-aux', action='store_true') +parser.add_argument('--extcap-interface', type=str) +parser.add_argument('--fifo', type=str) + +args = parser.parse_args() +if args.extcap_interfaces: + # List interfaces + print(bytes.fromhex("0a657874636170207b76657273696f6e3d342e312e317d7b646973706c61793d6e524620536e696666657220666f7220426c7565746f6f7468204c457d7b68656c703d68747470733a2f2f7777772e6e6f7264696373656d692e636f6d2f536f6674776172652d616e642d546f6f6c732f446576656c6f706d656e742d546f6f6c732f6e52462d536e69666665722d666f722d426c7565746f6f74682d4c457d0a696e74657266616365207b76616c75653d2f6465762f747479555342352d4e6f6e657d7b646973706c61793d6e524620536e696666657220666f7220426c7565746f6f7468204c457d0a636f6e74726f6c207b6e756d6265723d307d7b747970653d73656c6563746f727d7b646973706c61793d4465766963657d7b746f6f6c7469703d446576696365206c6973747d0a636f6e74726f6c207b6e756d6265723d317d7b747970653d73656c6563746f727d7b646973706c61793d4b65797d7b746f6f6c7469703d7d0a636f6e74726f6c207b6e756d6265723d327d7b747970653d737472696e677d7b646973706c61793d56616c75657d7b746f6f6c7469703d3620646967697420706173736b6579206f72203136206f7220333220627974657320656e6372797074696f6e206b657920696e2068657861646563696d616c207374617274696e67207769746820273078272c2062696720656e6469616e20666f726d61742e49662074686520656e7465726564206b65792069732073686f72746572207468616e203136206f722033322062797465732c2069742077696c6c206265207a65726f2d70616464656420696e2066726f6e74277d7b76616c69646174696f6e3d5c625e28285b302d395d7b367d297c2830785b302d39612d66412d465d7b312c36347d297c285b302d39412d46612d665d7b327d5b3a2d5d297b357d285b302d39412d46612d665d7b327d2920287075626c69637c72616e646f6d2929245c627d0a636f6e74726f6c207b6e756d6265723d337d7b747970653d737472696e677d7b646973706c61793d41647620486f707d7b64656661756c743d33372c33382c33397d7b746f6f6c7469703d4164766572746973696e67206368616e6e656c20686f702073657175656e63652e204368616e676520746865206f7264657220696e2077686963682074686520736e6966666572207377697463686573206164766572746973696e67206368616e6e656c732e2056616c6964206368616e6e656c73206172652033372c20333820616e642033392073657061726174656420627920636f6d6d612e7d7b76616c69646174696f6e3d5e5c732a282833377c33387c3339295c732a2c5c732a297b302c327d2833377c33387c3339297b317d5c732a247d7b72657175697265643d747275657d0a636f6e74726f6c207b6e756d6265723d377d7b747970653d627574746f6e7d7b646973706c61793d436c6561727d7b746f6f6c746f703d436c656172206f722072656d6f7665206465766963652066726f6d20446576696365206c6973747d0a636f6e74726f6c207b6e756d6265723d347d7b747970653d627574746f6e7d7b726f6c653d68656c707d7b646973706c61793d48656c707d7b746f6f6c7469703d416363657373207573657220677569646520286c61756e636865732062726f77736572297d0a636f6e74726f6c207b6e756d6265723d357d7b747970653d627574746f6e7d7b726f6c653d726573746f72657d7b646973706c61793d44656661756c74737d7b746f6f6c7469703d52657365747320746865207573657220696e7465726661636520616e6420636c6561727320746865206c6f672066696c657d0a636f6e74726f6c207b6e756d6265723d367d7b747970653d627574746f6e7d7b726f6c653d6c6f676765727d7b646973706c61793d4c6f677d7b746f6f6c7469703d4c6f672070657220696e746572666163657d0a76616c7565207b636f6e74726f6c3d307d7b76616c75653d207d7b646973706c61793d416c6c206164766572746973696e6720646576696365737d7b64656661756c743d747275657d0a76616c7565207b636f6e74726f6c3d307d7b76616c75653d5b30302c30302c30302c30302c30302c30302c305d7d7b646973706c61793d466f6c6c6f772049524b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d307d7b646973706c61793d4c656761637920506173736b65797d7b64656661756c743d747275657d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d317d7b646973706c61793d4c6567616379204f4f4220646174617d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d327d7b646973706c61793d4c6567616379204c544b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d337d7b646973706c61793d5343204c544b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d347d7b646973706c61793d53432050726976617465204b65797d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d357d7b646973706c61793d49524b7d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d367d7b646973706c61793d416464204c4520616464726573737d0a76616c7565207b636f6e74726f6c3d317d7b76616c75653d377d7b646973706c61793d466f6c6c6f77204c4520616464726573737d").decode()) +elif args.extcap_interface and args.extcap_config: + # List config + print(bytes.fromhex("617267207b6e756d6265723d307d7b63616c6c3d2d2d6f6e6c792d6164766572746973696e677d7b646973706c61793d4f6e6c79206164766572746973696e67207061636b6574737d7b746f6f6c7469703d54686520736e69666665722077696c6c206f6e6c792063617074757265206164766572746973696e67207061636b6574732066726f6d207468652073656c6563746564206465766963657d7b747970653d626f6f6c666c61677d7b736176653d747275657d0a617267207b6e756d6265723d317d7b63616c6c3d2d2d6f6e6c792d6c65676163792d6164766572746973696e677d7b646973706c61793d4f6e6c79206c6567616379206164766572746973696e67207061636b6574737d7b746f6f6c7469703d54686520736e69666665722077696c6c206f6e6c792063617074757265206c6567616379206164766572746973696e67207061636b6574732066726f6d207468652073656c6563746564206465766963657d7b747970653d626f6f6c666c61677d7b736176653d747275657d0a617267207b6e756d6265723d327d7b63616c6c3d2d2d7363616e2d666f6c6c6f772d7273707d7b646973706c61793d46696e64207363616e20726573706f6e736520646174617d7b746f6f6c7469703d54686520736e69666665722077696c6c20666f6c6c6f77207363616e20726571756573747320616e64207363616e20726573706f6e73657320696e207363616e206d6f64657d7b747970653d626f6f6c666c61677d7b64656661756c743d747275657d7b736176653d747275657d0a617267207b6e756d6265723d337d7b63616c6c3d2d2d7363616e2d666f6c6c6f772d6175787d7b646973706c61793d46696e6420617578696c6961727920706f696e74657220646174617d7b746f6f6c7469703d54686520736e69666665722077696c6c20666f6c6c6f772061757820706f696e7465727320696e207363616e206d6f64657d7b747970653d626f6f6c666c61677d7b64656661756c743d747275657d7b736176653d747275657d0a617267207b6e756d6265723d337d7b63616c6c3d2d2d636f6465647d7b646973706c61793d5363616e20616e6420666f6c6c6f772064657669636573206f6e204c4520436f646564205048597d7b746f6f6c7469703d5363616e20666f72206465766963657320616e6420666f6c6c6f772061647665727469736572206f6e204c4520436f646564205048597d7b747970653d626f6f6c666c61677d7b64656661756c743d66616c73657d7b736176653d747275657d").decode()) +elif args.capture and args.extcap_interface and args.fifo: + # Capture + pkts = [ + bytes.fromhex("ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172") + ] + with open(args.fifo, "wb", 0) as fd: + # header + fd.write( + struct.pack( + "IHHIIII", + 0xa1b2c3d4, + 2, 4, 0, 0, 65535, 1 + ) + ) + for pkt in pkts: + fd.write(struct.pack("IIII", 0, 0, len(pkt), len(pkt))) + fd.write(bytes(pkt)) +else: + raise ValueError("Bad arguments") +""".strip() +with open(extcapprog, "w") as fd: + fd.write(data) + +print(data) + +os.chmod(extcapprog, 0o777) + +# Inject and load provider +conf.prog.extcap_folders = [extcapfld] +load_extcap() +print(conf.ifaces.providers) +conf.ifaces.reload() + +# Now do the tests +iface = conf.ifaces.dev_from_networkname('/dev/ttyUSB5-None') +assert iface.name == "nRF Sniffer for Bluetooth LE" +sock = iface.l2listen()(iface=iface) +pkts = sock.sniff(timeout=2) +sock.close() +assert UDP in pkts[0] + +config = iface.get_extcap_config() +assert config["arg"] == [ + ('0', '--only-advertising', 'Only advertising packets', '', ''), + ('1', '--only-legacy-advertising', 'Only legacy advertising packets', '', ''), + ('2', '--scan-follow-rsp', 'Find scan response data', 'true', ''), + ('3', '--scan-follow-aux', 'Find auxiliary pointer data', 'true', ''), + ('3', '--coded', 'Scan and follow devices on LE Coded PHY', 'false', '') +] + +# Restore +conf.prog.extcap_folders = _bkp_extcap +conf.ifaces.providers = _bkp_providers +conf.ifaces.reload() = Test read_routes6() - default output routes6 = read_routes6() if WINDOWS: - route_add_loopback(routes6, True) + from scapy.arch.windows import _route_add_loopback + _route_add_loopback(routes6, True) routes6 @@ -204,10 +490,11 @@ routes6 if routes6: iflist = get_if_list() if WINDOWS: - route_add_loopback(ipv6=True, iflist=iflist) + from scapy.arch.windows import _route_add_loopback + _route_add_loopback(ipv6=True, iflist=iflist) if OPENBSD: len(routes6) >= 2 - elif iflist == [LOOPBACK_NAME]: + elif iflist == [conf.loopback_name]: len(routes6) == 1 elif len(iflist) >= 2: len(routes6) >= 1 @@ -215,27 +502,50 @@ if routes6: False else: # IPv6 seems disabled. Force a route to ::1 - conf.route6.routes.append(("::1", 128, "::", LOOPBACK_NAME, ["::1"], 1)) - conf.route6.ipv6_ifaces = set([LOOPBACK_NAME]) + conf.route6.routes.append(("::1", 128, "::", conf.loopback_name, ["::1"], 1)) + conf.route6.ipv6_ifaces = set([conf.loopback_name]) True += Build HBHOptUnknown for IPv6ExtHdrHopByHop with disabled autopad +~ ipv6 hbh opt +* Build the HBHOptUnknown of IPv6ExtHdrHopByHop with autopad=0 +v6Opt = HBHOptUnknown(otype=3, optlen=7, optdata="Beijing") +pkt = Ether()/IPv6()/IPv6ExtHdrHopByHop(autopad=0, options=[v6Opt, ]) +pkt.build() + += Build HBHOptUnknown for IPv6ExtHdrDestOpt with disabled autopad +~ ipv6 hbh opt +* Build the HBHOptUnknown of IPv6ExtHdrDestOpt with autopad=0 +v6Opt = HBHOptUnknown(otype=3, optlen=6, optdata="Haikou") +pkt = Ether()/IPv6()/IPv6ExtHdrDestOpt(autopad=0, options=[v6Opt, ]) +pkt.build() + + = Test read_routes6() - check mandatory routes -if len(routes6) > 1 and not WINDOWS: - assert(sum(1 for r in routes6 if r[0] == "::1" and r[4] == ["::1"]) >= 1) - if not OPENBSD and len(iflist) >= 2: - assert sum(1 for r in routes6 if r[0] == "fe80::" and r[1] == 64) >= 1 +import re +ll_route = re.compile(r"fe80:\d{0,2}:") +# match fe80::, fe80:5:, etc. (if scoped) + +conf.route6 + +if len(routes6) > 2 and not WINDOWS: + # Identify routes to fe80::/64 + assert sum(1 for r in routes6 if r[0] == "::1" and r[4] == ["::1"]) >= 1 + if len(iflist) >= 2: + assert sum(1 for r in routes6 if ll_route.match(r[0])) >= 1 try: - assert sum(1 for r in routes6 if in6_islladdr(r[0]) and r[1] == 128 and r[4] == ["::1"]) >= 1 + # Identify a route to a node IPv6 link-local address + assert sum(1 for r in routes6 if in6_islladdr(r[0]) and r[1] == 128) >= 1 except: # IPv6 is not available, but we still check the loopback - assert conf.route6.route("::/0") == (scapy.consts.LOOPBACK_NAME, "::", "::") + assert conf.route6.route("::/0") == (conf.loopback_name, "::", "::") assert sum(1 for r in routes6 if r[1] == 128 and r[4] == ["::1"]) >= 1 else: True = Test ifchange() -conf.route6.ifchange(LOOPBACK_NAME, "::1/128") +conf.route6.ifchange(conf.loopback_name, "::1/128") if WINDOWS: conf.netcache.in6_neighbor["::1"] = "ff:ff:ff:ff:ff:ff" # Restore fake cache @@ -246,14 +556,33 @@ assert (Ether() / ARP()).route()[0] is not None assert (Ether() / ARP()).payload.route()[0] is not None assert (ARP(ptype=0, pdst="hello. this isn't a valid IP")).route()[0] is None += utils/in4_is* + +assert in4_ismaddr("224.0.0.1") +assert not in4_ismaddr("192.168.0.1") +assert in4_ismaddr("239.0.0.255") + +assert in4_ismlladdr("224.0.0.1") +assert in4_ismlladdr("224.0.0.255") +assert not in4_ismlladdr("224.0.1.255") + +assert in4_ismgladdr("235.0.0.1") +assert not in4_ismgladdr("224.0.0.1") +assert not in4_ismgladdr("239.0.0.1") + +assert in4_ismlsaddr("239.0.0.1") +assert not in4_ismlsaddr("224.0.0.1") + +assert in4_isaddrllallnodes("224.0.0.1") +assert not in4_isaddrllallnodes("224.0.0.3") + +assert in4_getnsmac(b'\xe0\x00\x00\x01') == '01:00:5e:00:00:01' +assert getmacbyip("224.0.0.1") == '01:00:5e:00:00:01' = plain_str test -if not six.PY2: - # Only Python 3 has to deal with Str/Bytes conversion, - # as we don't use Python 2's unicode - data = b"\xffsweet\xef celestia\xab" - assert plain_str(data) == "sweet celestia" +data = b"\xffsweet\xef celestia\xab" +assert plain_str(data) == "\\xffsweet\\xef celestia\\xab" ############ ############ @@ -266,16 +595,10 @@ hex_data = bytes_hex(monty_data) assert hex_data == b'53746f70212057686f20617070726f61636865732074686520427269646765206f66204465617468206d75737420616e73776572206d65207468657365207175657374696f6e732074687265652c202765726520746865206f746865722073696465206865207365652e' assert hex_bytes(hex_data) == monty_data -= test gzip_decompress/gzip_compress - -from scapy.compat import gzip_compress, gzip_decompress - -gziped_data = b"\x1f\x8b\x08\x00N\xf5\xd7\\\x02\xff\x1d\x8b9\x0e\x800\x0c\x04\xbf\xb2T4\x88G ~@A\x1d\x91\x85\xa4H\x1cl#\xbe\xcf\xd1\xac4\x9a\xd9\xc5\xa5uX\x93 \xb4\xa6\x12\xb6D\x83'b\xd2\x1c\x0fBv\xcc\x0c\x9eP.s\x84j7\x15\x85_b\xc4y\xd1>> IP().src\n'127.0.0.1'\n", '127.0.0.1')) +ret +assert ret == (">>> IP().src\n'127.0.0.1'\n", '127.0.0.1') ret = autorun_get_html_interactive_session("IP().src") -assert(ret == (">>> IP().src\n'127.0.0.1'\n", '127.0.0.1')) +ret +assert ret == (">>> IP().src\n'127.0.0.1'\n", '127.0.0.1') ret = autorun_get_latex_interactive_session("IP().src") -assert(ret == ("\\textcolor{blue}{{\\tt\\char62}{\\tt\\char62}{\\tt\\char62} }IP().src\n'127.0.0.1'\n", '127.0.0.1')) +ret +assert ret == ("\\textcolor{blue}{{\\tt\\char62}{\\tt\\char62}{\\tt\\char62} }IP().src\n'127.0.0.1'\n", '127.0.0.1') + +ret = autorun_get_text_interactive_session("scapy_undefined") +assert "NameError" in ret[0] + += Test autorun with logging + +cmds = """log_runtime.info(hex_bytes("446166742050756e6b"))\n""" +ret = autorun_get_text_interactive_session(cmds) +ret +assert "Daft Punk" in ret[0] = Test utility TEX functions @@ -698,44 +1154,56 @@ os.write(fd, b"conf.verb = 42\n") os.close(fd) from scapy.main import _read_config_file _read_config_file(fname, globals(), locals()) -assert(conf.verb == 42) +assert conf.verb == 42 conf.verb = saved_conf_verb += Test config file functions failures + +from scapy.main import _read_config_file, _probe_config_folder +assert _read_config_file(_probe_config_folder("filethatdoesnotexistnorwillever.tsppajfsrdrr")) is None + = Test CacheInstance repr conf.netcache = Test pyx detection functions -from scapy.extlib import _test_pyx -assert _test_pyx() == False +from unittest.mock import patch + +def _r(*args, **kwargs): + raise OSError + +with patch("scapy.libs.test_pyx.subprocess.check_call", _r): + from scapy.libs.test_pyx import _test_pyx + assert _test_pyx() == False = Test matplotlib detection functions -from mock import MagicMock, patch +from unittest.mock import MagicMock, patch -bck_scapy_ext_lib = sys.modules.get("scapy.extlib", None) -del(sys.modules["scapy.extlib"]) +bck_scapy_libs_matplot = sys.modules.get("scapy.libs.matplot", None) +if bck_scapy_libs_matplot: + del sys.modules["scapy.libs.matplot"] mock_matplotlib = MagicMock() mock_matplotlib.get_backend.return_value = "inline" mock_matplotlib.pyplot = MagicMock() mock_matplotlib.pyplot.plt = None with patch.dict("sys.modules", **{ "matplotlib": mock_matplotlib, "matplotlib.lines": mock_matplotlib}): - from scapy.extlib import MATPLOTLIB, MATPLOTLIB_INLINED, MATPLOTLIB_DEFAULT_PLOT_KARGS, Line2D + from scapy.libs.matplot import MATPLOTLIB, MATPLOTLIB_INLINED, MATPLOTLIB_DEFAULT_PLOT_KARGS, Line2D assert MATPLOTLIB == 1 assert MATPLOTLIB_INLINED == 1 assert "marker" in MATPLOTLIB_DEFAULT_PLOT_KARGS mock_matplotlib.get_backend.return_value = "ko" with patch.dict("sys.modules", **{ "matplotlib": mock_matplotlib, "matplotlib.lines": mock_matplotlib}): - from scapy.extlib import MATPLOTLIB, MATPLOTLIB_INLINED, MATPLOTLIB_DEFAULT_PLOT_KARGS + from scapy.libs.matplot import MATPLOTLIB, MATPLOTLIB_INLINED, MATPLOTLIB_DEFAULT_PLOT_KARGS assert MATPLOTLIB == 1 assert MATPLOTLIB_INLINED == 0 assert "marker" in MATPLOTLIB_DEFAULT_PLOT_KARGS -if bck_scapy_ext_lib: - sys.modules["scapy.extlib"] = bck_scapy_ext_lib +if bck_scapy_libs_matplot: + sys.modules["scapy.libs.matplot"] = bck_scapy_libs_matplot ############ @@ -748,30 +1216,30 @@ if bck_scapy_ext_lib: = Packet class methods p = IP()/ICMP() ret = p.do_build_ps() -assert(ret[0] == b"@\x00\x00\x00\x00\x01\x00\x00@\x01\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x00\x00\x00\x00\x00\x00\x00") -assert(len(ret[1]) == 2) +assert ret[0] == b"@\x00\x00\x00\x00\x01\x00\x00@\x01\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x00\x00\x00\x00\x00\x00\x00" +assert len(ret[1]) == 2 -assert(p[ICMP].firstlayer() == p) +assert p[ICMP].firstlayer() == p -assert(p.command() == "IP()/ICMP()") +assert p.command() == "IP()/ICMP()" p.decode_payload_as(UDP) -assert(p.sport == 2048 and p.dport == 63487) +assert p.sport == 2048 and p.dport == 63487 = hide_defaults conf_color_theme = conf.color_theme conf.color_theme = BlackAndWhite() p = IP(ttl=64)/ICMP() -assert(repr(p) in [">", ">"]) +assert repr(p) in [">", ">"] p.hide_defaults() -assert(repr(p) in [">", ">"]) +assert repr(p) in [">", ">"] conf.color_theme = conf_color_theme = split_layers p = IP()/ICMP() s = raw(p) split_layers(IP, ICMP, proto=1) -assert(Raw in IP(s)) +assert Raw in IP(s) bind_layers(IP, ICMP, frag=0, proto=1) = fuzz @@ -811,15 +1279,32 @@ IP().summary() a=IP(ttl=4)/TCP() a.ttl a.ttl=10 -del(a.ttl) +del a.ttl a.ttl TCP in a a[TCP] a[TCP].dport=[80,443] a -assert(a.copy().time == a.time) +assert a.copy().time == a.time a=3 += Bind string array as payload +~ basic +assert bytes(Raw("sca")/"py") == b"scapy" +assert bytes(Raw("sca")/b"py") == b"scapy" +assert bytes(Raw("sca")/bytearray(b"py")) == b"scapy" +assert bytes("sca"/Raw("py")) == b"scapy" +assert bytes(b"sca"/Raw("py")) == b"scapy" +assert bytes(bytearray(b"sca")/Raw("py")) == b"scapy" +a=Raw("sca") +a.add_payload("py") +assert bytes(a) == b"scapy" +a=Raw("sca") +a.add_payload(b"py") +assert bytes(a) == b"scapy" +a=Raw("sca") +a.add_payload(bytearray(b"py")) +assert bytes(a) == b"scapy" = Checking overloads ~ basic IP TCP Ether @@ -839,7 +1324,7 @@ r in ['0x800 64 0x07b 4', 'IPv4 64 0x07b 4'] = sprintf() function ~ basic sprintf IP TCP SNAP LLC Dot11 -* This test is on the conditionnal substring feature of sprintf() +* This test is on the conditional substring feature of sprintf() a=Dot11()/LLC()/SNAP()/IP()/TCP() r = a.sprintf("{IP:{TCP:flags=%TCP.flags%}{UDP:port=%UDP.ports%} %IP.src%}") r @@ -896,14 +1381,6 @@ assert pkt[IP::{"ttl":3}].ttl == 3 assert pkt.getlayer(IP, ttl=3).ttl == 3 assert IPv6ExtHdrHopByHop(options=[HBHOptUnknown()]).getlayer(HBHOptUnknown, otype=42) is None -= specific haslayer and getlayer implementations for NTP -~ haslayer getlayer NTP -pkt = IP() / UDP() / NTPHeader() -assert NTP in pkt -assert pkt.haslayer(NTP) -assert isinstance(pkt[NTP], NTPHeader) -assert isinstance(pkt.getlayer(NTP), NTPHeader) - = specific haslayer and getlayer implementations for EAP ~ haslayer getlayer EAP pkt = Ether() / EAPOL() / EAP_MD5() @@ -997,6 +1474,48 @@ assert answer.answers(query) conf.checkIPsrc = conf_checkIPsrc +############ +############ ++ command() / json() tests +~ command + += Test command() with normal packet + +pkt = IP(dst="127.0.0.1", src="127.0.0.1") / UDP(dport=12345, sport=654) +assert pkt.command() == "IP(src='127.0.0.1', dst='127.0.0.1')/UDP(sport=654, dport=12345)" + += Test json() with normal packet + +assert pkt.json() == '{"version": 4, "ihl": null, "tos": 0, "len": null, "id": 1, "flags": 0, "frag": 0, "ttl": 64, "proto": 17, "chksum": null, "src": "127.0.0.1", "dst": "127.0.0.1", "payload": {"sport": 654, "dport": 12345, "len": null, "chksum": null}}' + += Test command() with nested packet + +pkt = DNS(qd=[DNSQR(qtype="A", qname="google.com")]) +assert pkt.command() == "DNS(qd=[DNSQR(qname=b'google.com.', qtype=1)])" + += Test json() with nested packet + +assert pkt.json() == '{"length": null, "id": 0, "qr": 0, "opcode": 0, "aa": 0, "tc": 0, "rd": 1, "ra": 0, "z": 0, "ad": 0, "cd": 0, "rcode": 0, "qdcount": null, "ancount": null, "nscount": null, "arcount": null, "qd": [{"qname": "google.com.", "qtype": 1, "unicastresponse": 0, "qclass": 1}]}' + += Test command() with ASN.1 packet + +pkt = KRB_AP_REP(bytes(KRB_AP_REP(encPart=EncryptedData()))) +assert pkt.command() == "KRB_AP_REP(pvno=ASN1_INTEGER(5), msgType=ASN1_INTEGER(15), encPart=EncryptedData(etype=ASN1_INTEGER(23), kvno=None, cipher=ASN1_STRING(b'')))" + += Test json(à with ASN.1 packet + +assert pkt.json() == '{"pvno": {"type": "ASN1_INTEGER", "value": "5"}, "msgType": {"type": "ASN1_INTEGER", "value": "15"}, "encPart": {"etype": {"type": "ASN1_INTEGER", "value": "23"}, "kvno": null, "cipher": {"type": "ASN1_STRING", "value": ""}}}' + += Test command() with meaningless payload + +pkt = PPTPStartControlConnectionReply() / IP(dst="127.0.0.1", src="127.0.0.1") +assert pkt.command() == "PPTPStartControlConnectionReply()/IP(src='127.0.0.1', dst='127.0.0.1')" + += Test json() with meaningless payload + +assert pkt.json() == '{"len": 156, "type": 1, "magic_cookie": 439041101, "ctrl_msg_type": 2, "reserved_0": 0, "protocol_version": 256, "result_code": 1, "error_code": 0, "framing_capabilities": 0, "bearer_capabilities": 0, "maximum_channels": 65535, "firmware_revision": 256, "host_name": "linux", "vendor_string": "", "payload": {"version": 4, "ihl": null, "tos": 0, "len": null, "id": 1, "flags": 0, "frag": 0, "ttl": 64, "proto": 0, "chksum": null, "src": "127.0.0.1", "dst": "127.0.0.1"}}' + + ############ ############ + Tests on padding @@ -1004,35 +1523,36 @@ conf.checkIPsrc = conf_checkIPsrc = Padding assembly r = raw(Padding("abc")) r -assert(r == b"abc" ) +assert r == b"abc" r = raw(Padding("abc")/Padding("def")) r -assert(r == b"abcdef" ) +assert r == b"abcdef" r = raw(Raw("ABC")/Padding("abc")/Padding("def")) r -assert(r == b"ABCabcdef" ) +assert r == b"ABCabcdef" r = raw(Raw("ABC")/Padding("abc")/Raw("DEF")/Padding("def")) r -assert(r == b"ABCDEFabcdef") +assert r == b"ABCDEFabcdef" = Padding and length computation p = IP(raw(IP()/Padding("abc"))) p -assert(p.len == 20 and len(p) == 23) +assert p.len == 20 and len(p) == 23 p = IP(raw(IP()/Raw("ABC")/Padding("abc"))) p -assert(p.len == 23 and len(p) == 26) +assert p.len == 23 and len(p) == 26 p = IP(raw(IP()/Raw("ABC")/Padding("abc")/Padding("def"))) p -assert(p.len == 23 and len(p) == 29) +assert p.len == 23 and len(p) == 29 = PadField test ~ PadField padding class TestPad(Packet): - fields_desc = [ PadField(StrNullField("st", b""),4), StrField("id", b"")] + fields_desc = [ PadField(StrNullField("st", b""), 6, padwith=b"\xff"), StrField("id", b"")] -TestPad() == TestPad(raw(TestPad())) +assert TestPad() == TestPad(raw(TestPad())) +assert raw(TestPad(st=b"st", id=b"id")) == b'st\x00\xff\xff\xffid' = ReversePadField ~ PadField padding @@ -1057,243 +1577,14 @@ class IPv3(IP): a = IPv3() v,t = a.version, a.ttl v,t -assert((v,t) == (3,32)) +assert (v,t) == (3,32) r = raw(a) r -assert(r == b'5\x00\x00\x14\x00\x01\x00\x00 \x00\xac\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01') - - -############ -############ -+ ISAKMP transforms test - -= ISAKMP creation -~ IP UDP ISAKMP -p=IP(src='192.168.8.14',dst='10.0.0.1')/UDP()/ISAKMP()/ISAKMP_payload_SA(prop=ISAKMP_payload_Proposal(trans=ISAKMP_payload_Transform(transforms=[('Encryption', 'AES-CBC'), ('Hash', 'MD5'), ('Authentication', 'PSK'), ('GroupDesc', '1536MODPgr'), ('KeyLength', 256), ('LifeType', 'Seconds'), ('LifeDuration', 86400)])/ISAKMP_payload_Transform(res2=12345,transforms=[('Encryption', '3DES-CBC'), ('Hash', 'SHA'), ('Authentication', 'PSK'), ('GroupDesc', '1024MODPgr'), ('LifeType', 'Seconds'), ('LifeDuration', 86400)]))) -p.show() -p - - -= ISAKMP manipulation -~ ISAKMP -r = p[ISAKMP_payload_Transform:2] -r -r.res2 == 12345 - -= ISAKMP assembly -~ ISAKMP -hexdump(p) -raw(p) == b"E\x00\x00\x96\x00\x01\x00\x00@\x11\xa7\x9f\xc0\xa8\x08\x0e\n\x00\x00\x01\x01\xf4\x01\xf4\x00\x82\xbf\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00z\x00\x00\x00^\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00R\x01\x01\x00\x00\x03\x00\x00'\x00\x01\x00\x00\x80\x01\x00\x07\x80\x02\x00\x01\x80\x03\x00\x01\x80\x04\x00\x05\x80\x0e\x01\x00\x80\x0b\x00\x01\x00\x0c\x00\x03\x01Q\x80\x00\x00\x00#\x00\x0109\x80\x01\x00\x05\x80\x02\x00\x02\x80\x03\x00\x01\x80\x04\x00\x02\x80\x0b\x00\x01\x00\x0c\x00\x03\x01Q\x80" - - -= ISAKMP disassembly -~ ISAKMP -q=IP(raw(p)) -q.show() -r = q[ISAKMP_payload_Transform:2] -r -r.res2 == 12345 - - -############ -############ -+ TFTP tests - -= TFTP Options -x=IP()/UDP(sport=12345)/TFTP()/TFTP_RRQ(filename="fname")/TFTP_Options(options=[TFTP_Option(oname="blksize", value="8192"),TFTP_Option(oname="other", value="othervalue")]) -assert( raw(x) == b'E\x00\x00H\x00\x01\x00\x00@\x11|\xa2\x7f\x00\x00\x01\x7f\x00\x00\x0109\x00E\x004B6\x00\x01fname\x00octet\x00blksize\x008192\x00other\x00othervalue\x00' ) -y=IP(raw(x)) -y[TFTP_Option].oname -y[TFTP_Option:2].oname -assert(len(y[TFTP_Options].options) == 2 and y[TFTP_Option].oname == b"blksize") - - -############ -############ -+ Dot11 tests - -= Dot11FCS parent matching - -pkt = Ether()/IP()/Dot11FCS() -assert pkt[Dot11] - -= Dot11FCS - test FCS with FCSField - -data = b'\x00\x00 \x00\xae@\x00\xa0 \x08\x00\xa0 \x08\x00\x00\x10\x02\x85\t\xa0\x00\xe2\x00d\x00\x00\x00\x00\x00\x00\x01\xa0@:\x01\x00\xc0\xca\xa4}PLfA\xac\xe4\xb3\x00\xc0\xca\xa4}P\x00\x03\x00 \x08 \x00\x00\x00\x00\x0f)\x1d\xd4\xd49\x1f>4\xeb' -pkt = RadioTap(data) -w_payload = hex_bytes('00002000ae4000a0200800a02008000010028509a000e2006400000000000001a0403a0100c0caa47d504c6641ace4b300c0caa47d50000300200820000000000f291dd4d4391f3e34eb') -assert raw(pkt) == w_payload - -= WEP tests -~ wifi crypto Dot11 LLC SNAP IP TCP -conf.wepkey = "" -bck_conf_crypto_valid = conf.crypto_valid -p = Dot11WEP(b'\x00\x00\x00\x00\xe3OjYLw\xc3x_%\xd0\xcf\xdeu-\xc3pH#\x1eK\xae\xf5\xde\xe7\xb8\x1d,\xa1\xfe\xe83\xca\xe1\xfe\xbd\xfe\xec\x00)T`\xde.\x93Td\x95C\x0f\x07\xdd') -assert isinstance(p, Dot11WEP) -conf.crypto_valid = bck_conf_crypto_valid - -conf.wepkey = "Fobar" -r = raw(Dot11WEP()/LLC()/SNAP()/IP()/TCP(seq=12345678)) -r -assert(r == b'\x00\x00\x00\x00\xe3OjYLw\xc3x_%\xd0\xcf\xdeu-\xc3pH#\x1eK\xae\xf5\xde\xe7\xb8\x1d,\xa1\xfe\xe83\xca\xe1\xfe\xbd\xfe\xec\x00)T`\xde.\x93Td\x95C\x0f\x07\xdd') -p = Dot11WEP(r) -p -assert(TCP in p and p[TCP].seq == 12345678) - -= RadioTap - dissection & build -data = b'\x00\x008\x00k\x084\x00oo\x0f\x98\x00\x00\x00\x00\x10\x00\x99\x16@\x01\xc5\xa1\x01\x00\x00\x00@\x01\x02\x00\x99\x16\x9d"\x05\x0b\x00\x00\x00\x00\x00\x00\xff\x01\x16\x01\x82\x00\x00\x00\x01\x00\x00\x00\x88\x020\x00\xb8\xe8VB_\xb2\x82*\xa8Uq\x15\xf0\x9f\xc2\x11\x16dP\xb0\x00\x00\xaa\xaa\x03\x00\x00\x00\x08\x00E\x00\x00GC\xad@\x007\x11\x97;\xd0C\xde{\xac\x10\r\xee\x005\xed\xec\x003\xd5/\xfc\\\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00\tlocalhost\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\t:\x80\x00\x04\x7f\x00\x00\x01\xcdj\x88]' -r = RadioTap(data) -r = RadioTap(raw(r)) -assert r.dBm_AntSignal == -59 -assert r.ChannelFrequency == 5785 -assert r.ChannelPlusFrequency == 5785 -assert r.present == 3410027 -assert r.A_MPDU_ref == 2821 -assert r.KnownVHT == 511 -assert r.PresentVHT == 22 -assert r.notdecoded == b'' - -= RadioTap Big-Small endian dissection -data = b'\x00\x00\x1a\x00/H\x00\x00\xe1\xd3\xcb\x05\x00\x00\x00\x00@0x\x14@\x01\xac\x00\x00\x00' -r = RadioTap(data) -r.show() -assert r.present == 18479 - -= RadioTap MCS dissection -data = b"\x00\x00\x0b\x00\x00\x00\x08\x00?,\x05" -r = RadioTap(data) -r.show() -assert r.present.MCS -assert r.knownMCS.MCS_bandwidth -assert r.knownMCS.MCS_index -assert r.knownMCS.guard_interval -assert r.knownMCS.HT_format -assert r.knownMCS.FEC_type -assert r.knownMCS.STBC_streams -assert not r.knownMCS.Ness -assert not r.knownMCS.Ness_MSB -assert r.MCS_bandwidth == 0 -assert r.guard_interval == 1 -assert r.HT_format == 1 -assert r.FEC_type == 0 -assert r.STBC_streams == 1 -assert r.MCS_index == 5 -assert r.Ness_LSB == 0 - -= RadioTap RX/TX Flags dissection -data = b'\x00\x00\x0c\x00\x00\xc0\x00\x00\x02\x00\x1f\x00' -r = RadioTap(data) -r.show() -assert r.present.TXFlags -assert r.TXFlags.TX_FAIL -assert r.TXFlags.CTS -assert r.TXFlags.RTS -assert r.TXFlags.NOACK -assert r.TXFlags.NOSEQ -assert r.present.RXFlags -assert r.RXFlags.BAD_PLCP - -= RadioTap, other fields - -data = b'\x00\x00 \x00\xae@\x00\xa0 \x08\x00\xa0 \x08\x00\x00\x10\x02\x85\t\xa0\x00\xe2\x00d\x00\x00\x00\x00\x00\x00\x01\xa0@:\x01\x00\xc0\xca\xa4}PLfA\xac\xe4\xb3\x00\xc04\xeb\xca\xa4}P\x00 \x08 \x00\x00\x00\x00\x0f)\x1d\xd4\xd49\x00\x03\x1f>' -r = RadioTap(data) -assert Dot11TKIP in r -assert r[Dot11] -assert r.dBm_AntSignal == -30 -assert r.Lock_Quality == 100 -assert r.RXFlags == 0 - -= RadioTap - Dissection - guess_payload_class() test -data = b'\x00\x00\r\x00\x04\x80\x02\x00\x02\x00\x00\x00\x00@\x00\x00\x00\xff\xff\xff\xff\xff\xff\xe8\x94\xf6\x1c\xdf\x8b\xff\xff\xff\xff\xff\xff\xa0\x01\x00\x10ciscosb-wpa2-eap\x01\x08\x02\x04\x0b\x16\x0c\x12\x18$2\x040H`l\x03\x01\x01-\x1an\x11\x1b\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -radiotap = RadioTap(data) -assert radiotap.present.Rate -assert radiotap.present.TXFlags -assert radiotap.present.b18 -assert radiotap.present == 163844 -assert radiotap.guess_payload_class("") == Dot11 - -= RadioTap - Dissection with Extended presence mask -data = b"\x00\x00 \x00\xae@\x00\xa0 \x08\x00\xa0 \x08\x00\x00\x10\x02\x9e\t\xa0\x00\xa2\x00d\x00\x00\x00\x00\x00\x00\x01\x80\x00\x00\x00\xff\xff\xff\xff\xff\xff\x94S0\xe8\x93\xb2\x94S0\xe8\x93\xb2\xf0u\x85\xe1H\x9c\x08\x00\x00\x00d\x00\x11\x14\x00\x08Why Fye?\x01\x08\x82\x84\x8b\x96$0Hl\x03\x01\x0b\x05\x04\x00\x01\x00\x00*\x01\x04/\x01\x040\x14\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x02\x0c\x002\x04\x0c\x12\x18`\x0b\x05\x07\x00;\x00\x00-\x1a\xad\x19\x17\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00=\x16\x0b\x08\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x08\x04\x00\x08\x00\x00\x00\x00@\xdd1\x00P\xf2\x04\x10J\x00\x01\x10\x10D\x00\x01\x02\x10G\x00\x10\xef\xda]\xd2#\xe8\xa7\xf0\xb2/\xa4\x98\xbf\x0cv\xe7\x10<\x00\x01\x03\x10I\x00\x06\x007*\x00\x01 \xdd\t\x00\x10\x18\x02\x07\x00\x1c\x00\x00\xdd\x18\x00P\xf2\x02\x01\x01\x80\x00\x03\xa4\x00\x00'\xa4\x00\x00BC^\x00b2/\x00F\x05r\x08\x01\x00\x00\xdd\x1e\x00\x90L\x04\x08\xbf\x0c\xb2Y\x82\x0f\xea\xff\x00\x00\xea\xff\x00\x00\xc0\x05\x00\x0b\x00\x00\x00\xc3\x02\x00\x02\x08I\xc0\xdb" -radiotap = RadioTap(data) - -assert radiotap.present.Ext -assert len(radiotap.Ext) == 2 -assert radiotap.Ext[0].present.b5 -assert radiotap.Ext[0].present.b11 -assert radiotap.Ext[0].present.b29 -assert radiotap.Ext[0].present.Ext -assert radiotap.Ext[1].present.b37 -assert radiotap.Ext[1].present.b43 -assert not radiotap.Ext[1].present.Ext - -assert radiotap.present.Flags -assert radiotap.Flags.FCS -assert Dot11FCS in radiotap -assert radiotap.fcs == 0xdbc04908 - -assert Dot11EltRates in radiotap -assert radiotap[Dot11EltRates].rates == [0x82, 0x84, 0x8b, 0x96, 0x24, 0x30, 0x48, 0x6c] - -= RadioTap - Build with Extended presence mask - -a = RadioTapExtendedPresenceMask(present="b0+b12+b29+Ext") -b = RadioTapExtendedPresenceMask(index=1, present="b32+b45+b59+b62") -pkt = RadioTap(present="Ext", Ext=[a, b]) -assert raw(pkt) == b'\x00\x00\x10\x00\x00\x00\x00\x80\x01\x10\x00\xa0\x01 \x00H' - -= fuzz() calls for Dot11Elt() -for i in range(10): - assert isinstance(raw(fuzz(Dot11Elt())), bytes) - +assert r == b'5\x00\x00\x14\x00\x01\x00\x00 \x00\xac\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01' ############ ############ -+ SNMP tests - -= SNMP assembling -~ SNMP ASN1 -r = raw(SNMP()) -r -assert(r == b'0\x18\x02\x01\x01\x04\x06public\xa0\x0b\x02\x01\x00\x02\x01\x00\x02\x01\x000\x00') -p = SNMP(version="v2c", community="ABC", PDU=SNMPbulk(id=4,varbindlist=[SNMPvarbind(oid="1.2.3.4",value=ASN1_INTEGER(7)),SNMPvarbind(oid="4.3.2.1.2.3",value=ASN1_IA5_STRING("testing123"))])) -p -r = raw(p) -r -assert(r == b'05\x02\x01\x01\x04\x03ABC\xa5+\x02\x01\x04\x02\x01\x00\x02\x01\x000 0\x08\x06\x03*\x03\x04\x02\x01\x070\x14\x06\x06\x81#\x02\x01\x02\x03\x16\ntesting123') - -= SNMP disassembling -~ SNMP ASN1 -x=SNMP(b'0y\x02\x01\x00\x04\x06public\xa2l\x02\x01)\x02\x01\x00\x02\x01\x000a0!\x06\x12+\x06\x01\x04\x01\x81}\x08@\x04\x02\x01\x07\n\x86\xde\xb78\x04\x0b172.31.19.20#\x06\x12+\x06\x01\x04\x01\x81}\x08@\x04\x02\x01\x07\n\x86\xde\xb76\x04\r255.255.255.00\x17\x06\x12+\x06\x01\x04\x01\x81}\x08@\x04\x02\x01\x05\n\x86\xde\xb9`\x02\x01\x01') -x.show() -assert(x.community==b"public" and x.version == 0) -assert(x.PDU.id == 41 and len(x.PDU.varbindlist) == 3) -assert(x.PDU.varbindlist[0].oid == "1.3.6.1.4.1.253.8.64.4.2.1.7.10.14130104") -assert(x.PDU.varbindlist[0].value == b"172.31.19.2") -assert(x.PDU.varbindlist[2].oid == "1.3.6.1.4.1.253.8.64.4.2.1.5.10.14130400") -assert(x.PDU.varbindlist[2].value == 1) - -= Basic UDP/SNMP bindings -~ SNMP ASN1 -z = UDP()/x -z = UDP(raw(z)) -assert SNMP in z - -x = UDP()/SNMP() -assert x.sport == x.dport == 161 - -= Basic SNMPvarbind build -~ SNMP ASN1 -x = SNMPvarbind(oid=ASN1_OID("1.3.6.1.2.1.1.4.0"), value=RandBin()) -x = SNMPvarbind(raw(x)) -assert isinstance(x.value, ASN1_STRING) - -= Failing SNMPvarbind dissection -~ SNMP ASN1 -try: - SNMP('0a\x02\x01\x00\x04\x06public\xa3T\x02\x02D\xd0\x02\x01\x00\x02\x01\x000H0F\x06\x08+\x06\x01\x02\x01\x01\x05\x00\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D\x00\x03\x01\x02D') - assert False -except BER_Decoding_Error: - pass ++ ASN.1 tests = ASN1 - ASN1_Object assert ASN1_Object(1) == ASN1_Object(1) @@ -1310,11 +1601,9 @@ random.seed(0x2807) o = bytes(a) o assert o in [ - b'\x02\x02\xfe\x92', - b'A\x02\x07q', - b'\x15\x10E55WW2a7yrh9XEck', - b'C\x02\xfe\x92', - b'\x1e\x023V' + b'\x1e\x023V', # PyPy 2.7 + b'A\x02\x07q', # Python 2.7 + b'F\x02\xfe\x92', # python 3.7-3.9 ] = ASN1 - ASN1_BIT_STRING @@ -1337,7 +1626,7 @@ a = ASN1_DECODING_ERROR("error", exc=OSError(1)) assert repr(a) == "" b = ASN1_DECODING_ERROR("error", exc=OSError(ASN1_BIT_STRING("0"))) assert repr(b) in ["}}>", - "}}>"] + "}}>"] = ASN1 - ASN1_INTEGER a = ASN1_INTEGER(int("1"*23)) @@ -1363,6 +1652,37 @@ raw(RandASN1Object()) random.seed(1873503288) raw(RandASN1Object()) += SSID is parsed properly even with the presence of RSN Information +~ SSID RSN Information +# A regression test for https://github.com/secdev/scapy/pull/2685. +# https://github.com/secdev/scapy/issues/2683 describes a packet with +# RSN Information that isn't parsed properly, +# causing the SSID to be overridden. +# This test checks the SSID is parsed properly. +filename = scapy_path("/test/pcaps/bad_rsn_parsing_overrides_ssid.pcap") +frame = rdpcap(filename)[0] +beacon = frame.getlayer(5) +ssid = beacon.network_stats()['ssid'] +assert ssid == "ROUTE-821E295" + += SSID is parsed properly even when the Country Information Tag Element has an odd length (not complying with the standard) and a missing pad byte +~ Missing Pad Byte in Country Info +# A regression test for https://github.com/secdev/scapy/pull/2685. +# https://github.com/secdev/scapy/issues/4132 describes a packet with +# a Country Information element tag that has an odd length, even though it's against the standard. +# The transmitter should have added a padding byte to make the length even, but it didn't. +# The effect on scapy used to be improper parsing of the next tag elements, causing the SSID to be overridden. +# This test checks the SSID is parsed properly. +from io import BytesIO +pcapfile = BytesIO(b'\n\r\r\n\x80\x00\x00\x00M<+\x1a\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x03\x00\x10\x00Linux 6.1.21-v8+\x04\x00E\x00Dumpcap (Wireshark) 3.4.10 (Git v3.4.10 packaged as 3.4.10-0+deb11u1)\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00\x7f\x00\x00\x00\x00\x04\x00\x00\x02\x00\x05\x00wifi2\x00\x00\x00\t\x00\x01\x00\t\x00\x00\x00\x0c\x00\x10\x00Linux 6.1.21-v8+\x00\x00\x00\x00@\x00\x00\x00\x06\x00\x00\x00\xb0\x01\x00\x00\x00\x00\x00\x00c\xd3\x87\x17\xe3c5\x82\x90\x01\x00\x00\x90\x01\x00\x00\x00\x00 \x00\xae@\x00\xa0 \x08\x00\xa0 \x08\x00\x00\x10\x0cd\x14@\x01\xa9\x00\x0c\x00\x00\x00\xa6\x00\xa8\x01\x80\x00\x00\x00\xff\xff\xff\xff\xff\xff\x02\xbf\xaf\x9f\xf8\x07\x02\xbf\xaf\x9f\xf8\x070\x96[p\xdcM\x06\x00\x00\x00d\x00\x11\x00\x00\x00\x01\x08\x8c\x12\x98$\xb0H`l\x03\x01,\x05\x04\x00\x01\x00\x00\x07QUS \x01\r\x80$\x01\x80(\x01\x80,\x01\x800\x01\x804\x01\x808\x01\x80<\x01\x80@\x01\x80d\x01\x80h\x01\x80l\x01\x80p\x01\x80t\x01\x80x\x01\x80|\x01\x80\x80\x01\x80\x84\x01\x80\x88\x01\x80\x8c\x01\x80\x90\x01\x80\x95\x01\x80\x99\x01\x80\x9d\x01\x80\xa1\x01\x80\xa5\x01\x800\x14\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x02\x0c\x00;\x02s\x00-\x1a,\t\x13\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00,\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00=\x16,\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdd\x18\x00P\xf2\x02\x01\x01\x81\x00\x03\xa4\x00\x00\'\xa4\x00\x00BC]\x00a\x11.\x00\xdd;\x00P\xf2\x04\x10J\x00\x01\x10\x10D\x00\x01\x02\x10I\x00\x06\x007*\x00\x01 \x10\x11\x00\x1358" Hisense Roku TV\x10T\x00\x08\x00\x07\x00P\xf2\x04\x00\x01\xdd\x16\xc8:k\x01\x01\x1048<@dhlptx|\x80\x84\x88\x8c\x90\xdd\x12Po\x9a\t\x02\x02\x00!\x0b\x03\x06\x00\x02\xbf\xaf\x9f\xf8\x07\xdd\rPo\x9a\n\x00\x00\x06\x01\x11\x1cD\x002\xf5N\xfbh\xb0\x01\x00\x00') +pktpcap = rdpcap(pcapfile) +frame = pktpcap[0] +beacon = frame.getlayer(4) +stats = beacon.network_stats() +ssid = stats['ssid'] +assert ssid == "" +country = stats['country'] +assert country == 'US' ############ ############ @@ -1371,99 +1691,157 @@ raw(RandASN1Object()) * Those tests need network access = Sending and receiving an ICMP -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP icmp_firewall def _test(): - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - x = sr1(IP(dst="www.google.com")/ICMP(),timeout=3) - conf.debug_dissector = old_debug_dissector + with no_debug_dissector(): + x = sr1(IP(dst="www.google.com")/ICMP(),timeout=3) + assert x is not None x assert x[IP].ottl() in [32, 64, 128, 255] assert 0 <= x[IP].hops() <= 126 - x is not None and ICMP in x and x[ICMP].type == 0 + assert ICMP in x and x[ICMP].type == 0 retry_test(_test) -= Sending an ICMP message at layer 2 and layer 3 -~ netaccess IP ICMP += Sending a TCP syn message at layer 2 and layer 3 +~ netaccess needs_root IP def _test(): - tmp = send(IP(dst="8.8.8.8")/ICMP(), return_packets=True, realtime=True) - assert(len(tmp) == 1) + tmp = send(IP(dst="8.8.8.8")/TCP(sport=RandShort(), dport=53, flags="S"), return_packets=True, realtime=True) + assert len(tmp) == 1 - tmp = sendp(Ether()/IP(dst="8.8.8.8")/ICMP(), return_packets=True, realtime=True) - assert(len(tmp) == 1) + tmp = sendp(Ether()/IP(dst="8.8.8.8")/TCP(sport=RandShort(), dport=53, flags="S"), return_packets=True, realtime=True) + assert len(tmp) == 1 - p = Ether()/IP(dst="8.8.8.8")/ICMP() + p = Ether()/IP(dst="8.8.8.8")/TCP(sport=RandShort(), dport=53, flags="S") from decimal import Decimal p.time = Decimal(p.time) tmp = sendp(p, return_packets=True, realtime=True) - assert(len(tmp) == 1) + assert len(tmp) == 1 retry_test(_test) = Latency check: localhost ICMP -~ netaccess linux latency +~ netaccess needs_root linux latency +# Note: still needs to enforce L3RawSocket as this won't work otherwise with libpcap sock = conf.L3socket conf.L3socket = L3RawSocket def _test(): req = IP(dst="127.0.0.1")/ICMP() - ans = sr1(req) + ans = sr1(req, timeout=3) assert (ans.time - req.sent_time) >= 0 assert (ans.time - req.sent_time) <= 1e-3 -conf.L3socket = sock - -= Sending an ICMP message 'forever' at layer 2 and layer 3 -~ netaccess IP ICMP +try: + retry_test(_test) +finally: + conf.L3socket = sock + += Test sniffing on multiple sockets +~ netaccess needs_root sniff + +# This test sniffs on the same interface twice at the same time, to +# simulate sniffing on multiple interfaces. + +def _test(): + iface = conf.route.route(str(Net("www.google.com")))[0] + port = int(RandShort()) + pkt = IP(dst="www.google.com")/TCP(sport=port, dport=80, flags="S") + def cb(): + sr1(pkt, timeout=3) + sniffer = AsyncSniffer(started_callback=cb, + iface=[iface, iface], + lfilter=lambda x: TCP in x and x[TCP].dport == port, + prn=lambda x: x.summary(), + count=2) + sniffer.start() + sniffer.join(timeout=3) + assert len(sniffer.results) == 2 + for pkt in sniffer.results: + assert pkt.sniffed_on == iface + +retry_test(_test) + += Test sniffing with AsyncSniffer on failed + +try: + sniffer = AsyncSniffer(iface="this_interface_does_not_exists") + sniffer.start() + sniffer.join() + assert False, "Should have errored by now" +except ValueError: + assert True + +try: + sniffer = AsyncSniffer(iface="this_interface_does_not_exists") + sniffer.start() + sniffer.thread.join() + sniffer.stop() + assert False, "Should have errored by now" +except ValueError: + assert True + += Sending a TCP syn 'forever' at layer 2 and layer 3 +~ netaccess needs_root IP def _test(): - tmp = srloop(IP(dst="8.8.8.8")/ICMP(), count=1) - assert(type(tmp) == tuple and len(tmp[0]) == 1) + tmp = srloop(IP(dst="8.8.8.8")/TCP(sport=RandShort(), dport=53, flags="S"), count=1, timeout=3) + assert type(tmp) == tuple and len(tmp[0]) == 1 - tmp = srploop(Ether()/IP(dst="8.8.8.8")/ICMP(), count=1) - assert(type(tmp) == tuple and len(tmp[0]) == 1) + tmp = srploop(Ether()/IP(dst="8.8.8.8")/TCP(sport=RandShort(), dport=53, flags="S"), count=1, timeout=3) + assert type(tmp) == tuple and len(tmp[0]) == 1 retry_test(_test) -= Sending and receiving an ICMP with flooding methods -~ netaccess IP ICMP += Sending and receiving an TCP syn with flooding methods +~ netaccess needs_root IP flood from functools import partial # flooding methods do not support timeout. Packing the test for security -def _test_flood(flood_function, add_ether=False): - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - p = IP(dst="www.google.com")/ICMP() - if add_ether: - p = Ether()/p - x = flood_function(p, timeout=2) - conf.debug_dissector = old_debug_dissector +def _test_flood(ip, flood_function, add_ether=False): + with no_debug_dissector(): + p = IP(dst=ip)/TCP(sport=RandShort(), dport=80, flags="S") + if add_ether: + p = Ether()/p + p.show2() + x = flood_function(p, timeout=0.5, maxretries=10) if type(x) == tuple: x = x[0][0][1] x assert x[IP].ottl() in [32, 64, 128, 255] assert 0 <= x[IP].hops() <= 126 - x is not None and ICMP in x and x[ICMP].type == 0 -_test_srflood = partial(_test_flood, srflood) +_test_srflood = partial(_test_flood, "www.google.com", srflood) retry_test(_test_srflood) -_test_sr1flood = partial(_test_flood, sr1flood) +_test_sr1flood = partial(_test_flood, "www.google.fr", sr1flood) retry_test(_test_sr1flood) -_test_srpflood = partial(_test_flood, srpflood, True) +_test_srpflood = partial(_test_flood, "www.google.net", srpflood, True) retry_test(_test_srpflood) -_test_srp1flood = partial(_test_flood, srp1flood, True) +_test_srp1flood = partial(_test_flood, "www.google.co.uk", srp1flood, True) retry_test(_test_srp1flood) += test chainEX +~ netaccess + +import socket +sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +ssck = StreamSocket(sck) + +try: + r = ssck.sr1(ICMP(type='echo-request'), timeout=0.1, chainEX=True, threaded=False) + assert False +except Exception: + assert True +finally: + sck.close() + = Sending and receiving an ICMPv6EchoRequest -~ netaccess ipv6 +~ netaccess ipv6 needs_root def _test(): - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - x = sr1(IPv6(dst="www.google.com")/ICMPv6EchoRequest(),timeout=3) - conf.debug_dissector = old_debug_dissector + with no_debug_dissector(): + x = sr1(IPv6(dst="www.google.com")/ICMPv6EchoRequest(),timeout=3) x assert x[IPv6].ottl() in [32, 64, 128, 255] assert 0 <= x[IPv6].hops() <= 126 @@ -1471,22 +1849,8 @@ def _test(): retry_test(_test) -= DNS request -~ netaccess IP UDP DNS -* A possible cause of failure could be that the open DNS (resolver1.opendns.com) -* is not reachable or down. -def _test(): - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - dns_ans = sr1(IP(dst="resolver1.opendns.com")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.slashdot.com")),timeout=5) - conf.debug_dissector = old_debug_dissector - DNS in dns_ans - return dns_ans - -dns_ans = retry_test(_test) - = Whois request -~ netaccess IP +~ netaccess IP as_resolvers * This test retries on failure because it often fails def _test(): IP(src="8.8.8.8").whois() @@ -1494,20 +1858,20 @@ def _test(): retry_test(_test) = AS resolvers -~ netaccess IP +~ netaccess IP as_resolvers * This test retries on failure because it often fails def _test(): ret = conf.AS_resolver.resolve("8.8.8.8", "8.8.4.4") assert (len(ret) == 2) - all(x[1] == "AS15169" for x in ret) + assert any(x[1] == "AS15169" for x in ret) retry_test(_test) -def _test(): - ret = AS_resolver_riswhois().resolve("8.8.8.8") - assert (len(ret) == 1) - assert all(x[1] == "AS15169" for x in ret) +riswhois_data = b"route: 8.8.8.0/24\ndescr: Google\norigin: AS15169\nnotify: radb-contact@google.com\nmnt-by: MAINT-AS15169\nchanged: radb-contact@google.com 20150728\nsource: RADB\n\nroute: 8.0.0.0/9\ndescr: Proxy-registered route object\norigin: AS3356\nremarks: auto-generated route object\nremarks: this next line gives the robot something to recognize\nremarks: L'enfer, c'est les autres\nremarks: \nremarks: This route object is for a Level 3 customer route\nremarks: which is being exported under this origin AS.\nremarks: \nremarks: This route object was created because no existing\nremarks: route object with the same origin was found, and\nremarks: since some Level 3 peers filter based on these objects\nremarks: this route may be rejected if this object is not created.\nremarks: \nremarks: Please contact routing@Level3.net if you have any\nremarks: questions regarding this object.\nmnt-by: LEVEL3-MNT\nchanged: roy@Level3.net 20060203\nsource: LEVEL3\n\n\n" + +ret = AS_resolver_riswhois()._parse_whois(riswhois_data) +assert ret == ('AS15169', 'Google') retry_test(_test) @@ -1526,18 +1890,18 @@ Bulk mode; whois.cymru.com [2017-10-03 08:38:08 +0000] 26496 | 68.178.213.61 | AS-26496-GO-DADDY-COM-LLC - GoDaddy.com, LLC, US """ tmp = AS_resolver_cymru().parse(cymru_bulk_data) -assert(len(tmp) == 3) -assert([l[1] for l in tmp] == ['AS24776', 'AS36459', 'AS26496']) +assert len(tmp) == 3 +assert [l[1] for l in tmp] == ['AS24776', 'AS36459', 'AS26496'] = AS resolver - IPv6 -~ netaccess IP +~ netaccess IP as_resolvers * This test retries on failure because it often fails def _test(): as_resolver6 = AS_resolver6() ret = as_resolver6.resolve("2001:4860:4860::8888", "2001:4860:4860::4444") assert (len(ret) == 2) - assert all(x[1] == 15169 for x in ret) + assert any(x[1] == 15169 for x in ret) retry_test(_test) @@ -1553,6 +1917,7 @@ asrm = AS_resolver_multi(MockAS_resolver()) assert len(asrm.resolve(["8.8.8.8", "8.8.4.4"])) == 0 = sendpfast +~ tcpreplay old_interactive = conf.interactive conf.interactive = False @@ -1586,13 +1951,12 @@ l = [p for p in a] len(l) == 7 = Implicit logic 3 - -# In case there's a single option: __iter__ should return self +# In case there's a single option: __iter__ should not return self a = Ether()/IP(src="127.0.0.1", dst="127.0.0.1")/ICMP() for i in a: i.sent_time = 1 -assert a.sent_time == 1 +assert a.sent_time is None # In case they are several, self should never be returned a = Ether()/IP(src="127.0.0.1", dst="127.0.0.1")/ICMP(seq=(0, 5)) @@ -1607,16 +1971,10 @@ assert a.sent_time is None + Real usages = Port scan -~ netaccess IP TCP +~ netaccess needs_root IP TCP def _test(): - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - ans,unans=sr(IP(dst="www.google.com/30")/TCP(dport=[80,443]),timeout=2) - conf.debug_dissector = old_debug_dissector - - # Backward compatibility: Python 2 only - if six.PY2: - exec("""ans.make_table(lambda (s, r): (s.dst, s.dport, r.sprintf("{TCP:%TCP.flags%}{ICMP:%ICMP.code%}")))""") + with no_debug_dissector(): + ans,unans=sr(IP(dst="www.google.com/30")/TCP(dport=[80,443]), timeout=2) # New format: all Python versions ans.make_table(lambda s, r: (s.dst, s.dport, r.sprintf("{TCP:%TCP.flags%}{ICMP:%ICMP.code%}"))) @@ -1624,136 +1982,110 @@ def _test(): retry_test(_test) = Send & receive with debug_match -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP def _test(): old_debug_match = conf.debug_match conf.debug_match = True - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - ans, unans = sr(IP(dst="www.google.fr") / ICMP(), timeout=2) + with no_debug_dissector(): + ans, unans = sr(IP(dst="www.google.fr") / TCP(sport=RandShort(), dport=80, flags="S"), timeout=2) + assert ans[0].query == ans[0][0] + assert ans[0].answer == ans[0][1] conf.debug_match = old_debug_match - conf.debug_dissector = old_debug_dissector assert ans and not unans retry_test(_test) = Send & receive with retry -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP def _test(): - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - ans, unans = sr(IP(dst=["8.8.8.8", "1.2.3.4"]) / ICMP(), timeout=2, retry=1) - conf.debug_dissector = old_debug_dissector - len(ans) == 1 and len(unans) == 1 + with no_debug_dissector(): + ans, unans = sr(IP(dst=["8.8.8.8", "1.2.3.4"]) / TCP(sport=RandShort(), dport=53, flags="S"), timeout=2, retry=1) + assert len(ans) == 1 and len(unans) == 1 retry_test(_test) = Send & receive with multi -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP def _test(): - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - ans, unans = sr(IP(dst=["8.8.8.8", "1.2.3.4"]) / ICMP(), timeout=2, multi=1) - conf.debug_dissector = old_debug_dissector - len(ans) == 1 and len(unans) == 1 + with no_debug_dissector(): + ans, unans = sr(IP(dst=["8.8.8.8", "1.2.3.4"]) / TCP(sport=RandShort(), dport=53, flags="S"), timeout=2, multi=1) + assert len(ans) >= 1 and len(unans) == 1 retry_test(_test) = Traceroute function -~ netaccess tcpdump +~ netaccess needs_root tcpdump * Let's test traceroute -ans, unans = traceroute("www.slashdot.org") -ans.nsummary() -s,r=ans[0] -s.show() -s.show(2) - -= DNS packet manipulation -~ netaccess IP UDP DNS -dns_ans.show() -dns_ans.show2() -dns_ans[DNS].an.show() -dns_ans2 = IP(raw(dns_ans)) -DNS in dns_ans2 -assert(raw(dns_ans2) == raw(dns_ans)) -dns_ans2.qd.qname = "www.secdev.org." -* We need to recalculate these values -del(dns_ans2[IP].len) -del(dns_ans2[IP].chksum) -del(dns_ans2[UDP].len) -del(dns_ans2[UDP].chksum) -assert(b"\x03www\x06secdev\x03org\x00" in raw(dns_ans2)) -assert(DNS in IP(raw(dns_ans2))) -assert raw(DNSRR(type='A', rdata='1.2.3.4')) == b'\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x01\x02\x03\x04' - -= Arping -~ netaccess tcpdump -* This test assumes the local network is a /24. This is bad. def _test(): - ip_address = conf.route.route("0.0.0.0")[2] - ip_address - arping(ip_address+"/24") + with no_debug_dissector(): + ans, unans = traceroute("www.slashdot.org") + ans.nsummary() + s,r=ans[0] + s.show() + s.show(2) retry_test(_test) = send() and sniff() -~ netaccess tcpdump -import time -import os +~ netaccess needs_root -from scapy.modules.six.moves.queue import Queue - -def _send_or_sniff(pkt, timeout, flt, pid, fork, t_other=None, opened_socket=None): - assert pid != -1 - if pid == 0: - time.sleep(1) - (sendp if isinstance(pkt, (Ether, Dot3)) else send)(pkt) - if fork: - os._exit(0) - else: - return - else: - spkt = raw(pkt) - # We do not want to crash when a packet cannot be parsed - old_debug_dissector = conf.debug_dissector - conf.debug_dissector = False - pkts = sniff( - timeout=timeout, filter=flt, opened_socket=opened_socket, - stop_filter=lambda p: pkt.__class__ in p and raw(p[pkt.__class__]) == spkt - ) - conf.debug_dissector = old_debug_dissector - if fork: - os.waitpid(pid, 0) - else: - t_other.join() - assert raw(pkt) in (raw(p[pkt.__class__]) for p in pkts if pkt.__class__ in p) - -def send_and_sniff(pkt, timeout=2, flt=None, opened_socket=None): - """Send a packet, sniff, and check the packet has been seen""" - if hasattr(os, "fork"): - _send_or_sniff(pkt, timeout, flt, os.fork(), True) - else: - from threading import Thread - def run_function(pkt, timeout, flt, pid, thread, results, opened_socket): - _send_or_sniff(pkt, timeout, flt, pid, False, t_other=thread, opened_socket=opened_socket) - results.put(True) - results = Queue() - t_parent = Thread(target=run_function, args=(pkt, timeout, flt, 0, None, results, None)) - t_child = Thread(target=run_function, args=(pkt, timeout, flt, 1, t_parent, results, opened_socket)) - t_parent.start() - t_child.start() - t_parent.join() - t_child.join() - assert results.qsize() >= 2 - while not results.empty(): - assert results.get() - -retry_test(lambda: send_and_sniff(IP(dst="secdev.org")/ICMP())) -retry_test(lambda: send_and_sniff(IP(dst="secdev.org")/ICMP(), flt="icmp")) -retry_test(lambda: send_and_sniff(Ether()/IP(dst="secdev.org")/ICMP())) +def _test(): + sendp(Ether()/IP(src="9.0.0.0")/UDP(), count=3, iface=conf.iface) + +r = sniff(timeout=3, count=1, + lfilter=lambda x: IP in x and x[IP].src == "9.0.0.0", + iface=conf.iface, + started_callback=_test) + +assert r + += sniff() with socket failure +* GH issue 3631 + +REFPACKET = Ether()/IP()/UDP() + +# A socket that fails after 10 packets +class OOPipe(ObjectPipe): + def recv(self, x=MTU): + self.i = getattr(self, "i", 0) + 1 + if self.i == 11: + self.close() + raise OSError("Giant failure") + pkt = super(OOPipe, self).recv(x) + self.send(REFPACKET) + return pkt + +o = OOPipe() +o.send(REFPACKET) + +pkts = sniff(opened_socket=[o], timeout=3) +assert len(pkts) == 10 + += GH issue 3306 +~ netaccess needs_root + +send(fuzz(ARP())) + += Test SuperSocket.select +~ select + +from unittest import mock + +@mock.patch("scapy.supersocket.select") +def _test_select(select): + def f(a, b, c, d): + raise IOError(0) + select.side_effect = f + try: + SuperSocket.select([]) + return False + except: + return True + +assert _test_select() = Test L2ListenTcpdump socket -~ netaccess FIXME_py3 +~ netaccess # Needs to be fixed. Fails randomly #import time @@ -1775,7 +2107,7 @@ retry_test(lambda: send_and_sniff(Ether()/IP(dst="secdev.org")/ICMP())) True = Test set of sent_time by sr -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP def _test(): packet = IP(dst="8.8.8.8")/ICMP() r = sr(packet, timeout=2) @@ -1784,7 +2116,7 @@ def _test(): retry_test(_test) = Test set of sent_time by sr (multiple packets) -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP def _test(): packet1 = IP(dst="8.8.8.8")/ICMP() packet2 = IP(dst="8.8.4.4")/ICMP() @@ -1795,20 +2127,20 @@ def _test(): retry_test(_test) = Test set of sent_time by srflood -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP def _test(): packet = IP(dst="8.8.8.8")/ICMP() - r = srflood(packet, timeout=2) + r = srflood(packet, timeout=0.5) assert packet.sent_time is not None retry_test(_test) = Test set of sent_time by srflood (multiple packets) -~ netaccess IP ICMP +~ netaccess needs_root IP ICMP def _test(): packet1 = IP(dst="8.8.8.8")/ICMP() packet2 = IP(dst="8.8.4.4")/ICMP() - r = srflood([packet1, packet2], timeout=2) + r = srflood([packet1, packet2], timeout=0.5) assert packet1.sent_time is not None assert packet2.sent_time is not None @@ -1822,5064 +2154,42 @@ retry_test(_test) if conf.manufdb: len(conf.manufdb) -else: - True - -= check _resolve_MAC - -if conf.manufdb: - assert conf.manufdb._resolve_MAC("00:00:17") == "Oracle" -else: - True - -############ -############ -+ Automaton tests - -= Simple automaton -~ automaton -class ATMT1(Automaton): - def parse_args(self, init, *args, **kargs): - Automaton.parse_args(self, *args, **kargs) - self.init = init - @ATMT.state(initial=1) - def BEGIN(self): - raise self.MAIN(self.init) - @ATMT.state() - def MAIN(self, s): - return s - @ATMT.condition(MAIN, prio=-1) - def go_to_END(self, s): - if len(s) > 20: - raise self.END(s).action_parameters(s) - @ATMT.condition(MAIN) - def trA(self, s): - if s.endswith("b"): - raise self.MAIN(s+"a") - @ATMT.condition(MAIN) - def trB(self, s): - if s.endswith("a"): - raise self.MAIN(s*2+"b") - @ATMT.state(final=1) - def END(self, s): - return s - @ATMT.action(go_to_END) - def action_test(self, s): - self.result = s - -= Simple automaton Tests -~ automaton - -a=ATMT1(init="a", ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'aabaaababaaabaaababab') -r = a.result -r -assert(r == 'aabaaababaaabaaababab') -a = ATMT1(init="b", ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'babababababababababababababab') -r = a.result -assert(r == 'babababababababababababababab') - -= Simple automaton stuck test -~ automaton - -try: - ATMT1(init="", ll=lambda: None, recvsock=lambda: None).run() -except Automaton.Stuck: - True -else: - False - - -= Automaton state overloading -~ automaton -class ATMT2(ATMT1): - @ATMT.state() - def MAIN(self, s): - return "c"+ATMT1.MAIN(self, s).run() - -a=ATMT2(init="a", ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'ccccccacabacccacababacccccacabacccacababab') - - -r = a.result -r -assert(r == 'ccccccacabacccacababacccccacabacccacababab') -a=ATMT2(init="b", ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'cccccbaccbabaccccbaccbabab') -r = a.result -r -assert(r == 'cccccbaccbabaccccbaccbabab') - - -= Automaton condition overloading -~ automaton -class ATMT3(ATMT2): - @ATMT.condition(ATMT1.MAIN) - def trA(self, s): - if s.endswith("b"): - raise self.MAIN(s+"da") - - -a=ATMT3(init="a", debug=2, ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'cccccacabdacccacabdabda') -r = a.result -r -assert(r == 'cccccacabdacccacabdabda') -a=ATMT3(init="b", ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'cccccbdaccbdabdaccccbdaccbdabdab') - -r = a.result -r -assert(r == 'cccccbdaccbdabdaccccbdaccbdabdab') - - -= Automaton action overloading -~ automaton -class ATMT4(ATMT3): - @ATMT.action(ATMT1.go_to_END) - def action_test(self, s): - self.result = "e"+s+"e" - -a=ATMT4(init="a", ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'cccccacabdacccacabdabda') -r = a.result -r -assert(r == 'ecccccacabdacccacabdabdae') -a=ATMT4(init="b", ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'cccccbdaccbdabdaccccbdaccbdabdab') -r = a.result -r -assert(r == 'ecccccbdaccbdabdaccccbdaccbdabdabe') - - -= Automaton priorities -~ automaton -class ATMT5(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - self.res = "J" - @ATMT.condition(BEGIN, prio=1) - def tr1(self): - self.res += "i" - raise self.END() - @ATMT.condition(BEGIN) - def tr2(self): - self.res += "p" - @ATMT.condition(BEGIN, prio=-1) - def tr3(self): - self.res += "u" - - @ATMT.action(tr1) - def ac1(self): - self.res += "e" - @ATMT.action(tr1, prio=-1) - def ac2(self): - self.res += "t" - @ATMT.action(tr1, prio=1) - def ac3(self): - self.res += "r" - - @ATMT.state(final=1) - def END(self): - return self.res - -a=ATMT5(ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == 'Jupiter') - -= Automaton test same action for many conditions -~ automaton -class ATMT6(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - self.res="M" - @ATMT.condition(BEGIN) - def tr1(self): - raise self.MIDDLE() - @ATMT.action(tr1) # default prio=0 - def add_e(self): - self.res += "e" - @ATMT.action(tr1, prio=2) - def add_c(self): - self.res += "c" - @ATMT.state() - def MIDDLE(self): - self.res += "u" - @ATMT.condition(MIDDLE) - def tr2(self): - raise self.END() - @ATMT.action(tr2, prio=2) - def add_y(self): - self.res += "y" - @ATMT.action(tr1, prio=1) - @ATMT.action(tr2) - def add_r(self): - self.res += "r" - @ATMT.state(final=1) - def END(self): - return self.res - -a=ATMT6(ll=lambda: None, recvsock=lambda: None) -r = a.run() -assert(r == 'Mercury') - -a.restart() -r = a.run() -r -assert(r == 'Mercury') - -= Automaton test io event -~ automaton - -class ATMT7(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - self.res = "S" - @ATMT.ioevent(BEGIN, name="tst") - def tr1(self, fd): - self.res += fd.recv() - raise self.NEXT_STATE() - @ATMT.state() - def NEXT_STATE(self): - self.oi.tst.send("ur") - @ATMT.ioevent(NEXT_STATE, name="tst") - def tr2(self, fd): - self.res += fd.recv() - raise self.END() - @ATMT.state(final=1) - def END(self): - self.res += "n" - return self.res - -a=ATMT7(ll=lambda: None, recvsock=lambda: None) -a.run(wait=False) -a.io.tst.send("at") -r = a.io.tst.recv() -r -a.io.tst.send(r) -r = a.run() -r -assert(r == "Saturn") - -a.restart() -a.run(wait=False) -a.io.tst.send("at") -r = a.io.tst.recv() -r -a.io.tst.send(r) -r = a.run() -r -assert(r == "Saturn") - -= Automaton test io event from external fd -~ automaton -import os - -class ATMT8(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - self.res = b"U" - @ATMT.ioevent(BEGIN, name="extfd") - def tr1(self, fd): - self.res += fd.read(2) - raise self.NEXT_STATE() - @ATMT.state() - def NEXT_STATE(self): - pass - @ATMT.ioevent(NEXT_STATE, name="extfd") - def tr2(self, fd): - self.res += fd.read(2) - raise self.END() - @ATMT.state(final=1) - def END(self): - self.res += b"s" - return self.res - -if WINDOWS: - r = w = ObjectPipe() -else: - r,w = os.pipe() - -def writeOn(w, msg): - if WINDOWS: - w.write(msg) - else: - os.write(w, msg) - -a=ATMT8(external_fd={"extfd":r}, ll=lambda: None, recvsock=lambda: None) -a.run(wait=False) -writeOn(w, b"ra") -writeOn(w, b"nu") - -r = a.run() -r -assert(r == b"Uranus") - -a.restart() -a.run(wait=False) -writeOn(w, b"ra") -writeOn(w, b"nu") -r = a.run() -r -assert(r == b"Uranus") - -= Automaton test interception_points, and restart -~ automaton -class ATMT9(Automaton): - def my_send(self, x): - self.io.loop.send(x) - @ATMT.state(initial=1) - def BEGIN(self): - self.res = "V" - self.send(Raw("ENU")) - @ATMT.ioevent(BEGIN, name="loop") - def received_sth(self, fd): - self.res += plain_str(fd.recv().load) - raise self.END() - @ATMT.state(final=1) - def END(self): - self.res += "s" - return self.res - -a=ATMT9(debug=5, ll=lambda: None, recvsock=lambda: None) -r = a.run() -r -assert(r == "VENUs") - -a.restart() -r = a.run() -r -assert(r == "VENUs") - -a.restart() -a.BEGIN.intercepts() -while True: - try: - x = a.run() - except Automaton.InterceptionPoint as p: - a.accept_packet(Raw(p.packet.load.lower()), wait=False) - else: - break - -r = x -r -assert(r == "Venus") - -= Automaton graph -~ automaton - -class HelloWorld(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - pass - @ATMT.condition(BEGIN) - def wait_for_nothing(self): - raise self.END() - @ATMT.action(wait_for_nothing) - def on_nothing(self): - pass - @ATMT.state(final=1) - def END(self): - pass - -graph = HelloWorld.build_graph() -assert graph.startswith("digraph") -assert '"BEGIN" -> "END"' in graph - -= TCP_client automaton -~ automaton netaccess needs_root -* This test retries on failure because it may fail quite easily - -# This test doesn't pass on Travis BSD, and will be skipped - -SECDEV_IP4 = "203.178.141.194" - -if LINUX: - import os - IPTABLE_RULE = "iptables -%c INPUT -s %s -p tcp --sport 80 -j DROP" - # Drop packets from SECDEV_IP4 - assert(os.system(IPTABLE_RULE % ('A', SECDEV_IP4)) == 0) - -load_layer("http") - -def _tcp_client_test(): - req = HTTP()/HTTPRequest( - Accept_Encoding=b'gzip, deflate', - Cache_Control=b'no-cache', - Pragma=b'no-cache', - Connection=b'keep-alive', - Host=b'www.kame.net', - ) - t = TCP_client.tcplink(HTTP, SECDEV_IP4, 80) - response = t.sr1(req, timeout=3) - t.close() - assert response.Http_Version == b'HTTP/1.1' - assert response.Status_Code == b'200' - assert response.Reason_Phrase == b'OK' - -def _http_request_test(): - response = http_request("www.kame.net", path="/", iptables=LINUX) - assert response.Http_Version == b'HTTP/1.1' - assert response.Status_Code == b'200' - assert response.Reason_Phrase == b'OK' - -# This test doesn't pass on Travis BSD -if not BSD: - try: - retry_test(_tcp_client_test) - finally: - if LINUX: - # Remove the iptables rule - assert(os.system(IPTABLE_RULE % ('D', SECDEV_IP4)) == 0) - retry_test(_http_request_test) - -############ -############ -+ Test IP options - -= IP options individual assembly -~ IP options -r = raw(IPOption()) -r -assert(r == b'\x00\x02') -r = raw(IPOption_NOP()) -r -assert(r == b'\x01') -r = raw(IPOption_EOL()) -r -assert(r == b'\x00') -r = raw(IPOption_LSRR(routers=["1.2.3.4","5.6.7.8"])) -r -assert(r == b'\x83\x0b\x04\x01\x02\x03\x04\x05\x06\x07\x08') - -= IP options individual dissection -~ IP options -io = IPOption(b"\x00") -io -assert(io.option == 0 and isinstance(io, IPOption_EOL)) -io = IPOption(b"\x01") -io -assert(io.option == 1 and isinstance(io, IPOption_NOP)) -lsrr=b'\x83\x0b\x04\x01\x02\x03\x04\x05\x06\x07\x08' -p=IPOption_LSRR(lsrr) -p -q=IPOption(lsrr) -q -assert(p == q) - -= IP assembly and dissection with options -~ IP options -p = IP(src="9.10.11.12",dst="13.14.15.16",options=IPOption_SDBM(addresses=["1.2.3.4","5.6.7.8"]))/TCP() -r = raw(p) -r -assert(r == b'H\x00\x004\x00\x01\x00\x00@\x06\xa2q\t\n\x0b\x0c\r\x0e\x0f\x10\x95\n\x01\x02\x03\x04\x05\x06\x07\x08\x00\x00\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00_K\x00\x00') -q=IP(r) -q -assert( isinstance(q.options[0],IPOption_SDBM) ) -assert( q[IPOption_SDBM].addresses[1] == "5.6.7.8" ) -p.options[0].addresses[0] = '5.6.7.8' -assert( IP(raw(p)).options[0].addresses[0] == '5.6.7.8' ) -p = IP(src="9.10.11.12", dst="13.14.15.16", options=[IPOption_NOP(),IPOption_LSRR(routers=["1.2.3.4","5.6.7.8"]),IPOption_Security(transmission_control_code="XYZ")])/TCP() -p -r = raw(p) -r -assert(r == b'K\x00\x00@\x00\x01\x00\x00@\x06\xf3\x83\t\n\x0b\x0c\r\x0e\x0f\x10\x01\x83\x0b\x04\x01\x02\x03\x04\x05\x06\x07\x08\x82\x0b\x00\x00\x00\x00\x00\x00XYZ\x00\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00_K\x00\x00') -q = IP(r) -q -assert(q[IPOption_LSRR].get_current_router() == "1.2.3.4") -assert(q[IPOption_Security].transmission_control_code == b"XYZ") -assert(q[TCP].flags == 2) - - -############ -############ -+ Test PPP - -= PPPoE -~ ppp pppoe -p=Ether(b'\xff\xff\xff\xff\xff\xff\x08\x00\x27\xf3<5\x88c\x11\x09\x00\x00\x00\x0c\x01\x01\x00\x00\x01\x03\x00\x04\x01\x02\x03\x04\x00\x00\x00\x00') -p -assert(p[Ether].type==0x8863) -assert(PPPoED in p) -assert(p[PPPoED].version==1) -assert(p[PPPoED].type==1) -assert(p[PPPoED].code==0x09) -assert(PPPoED_Tags in p) -q=p[PPPoED_Tags] -assert(q.tag_list is not None) -r=q.tag_list -assert(r[0].tag_type==0x0101) -assert(r[1].tag_type==0x0103) -assert(r[1].tag_len==4) -assert(r[1].tag_value==b'\x01\x02\x03\x04') -assert(r[2].tag_type==0x0000) - - -= PPP/HDLC -~ ppp hdlc -p = HDLC()/PPP()/PPP_IPCP() -p -s = raw(p) -s -assert(s == b'\xff\x03\x80!\x01\x00\x00\x04') -p = PPP(s) -p -assert(HDLC in p) -assert(p[HDLC].control==3) -assert(p[PPP].proto==0x8021) -q = PPP(s[2:]) -q -assert(HDLC not in q) -assert(q[PPP].proto==0x8021) - - -= PPP IPCP -~ ppp ipcp -p = PPP(b'\x80!\x01\x01\x00\x10\x03\x06\xc0\xa8\x01\x01\x02\x06\x00-\x0f\x01') -p -assert(p[PPP_IPCP].code == 1) -assert(p[PPP_IPCP_Option_IPAddress].data=="192.168.1.1") -assert(p[PPP_IPCP_Option].data == b'\x00-\x0f\x01') -p=PPP()/PPP_IPCP(options=[PPP_IPCP_Option_DNS1(data="1.2.3.4"),PPP_IPCP_Option_DNS2(data="5.6.7.8"),PPP_IPCP_Option_NBNS2(data="9.10.11.12")]) -r = raw(p) -r -assert(r == b'\x80!\x01\x00\x00\x16\x81\x06\x01\x02\x03\x04\x83\x06\x05\x06\x07\x08\x84\x06\t\n\x0b\x0c') -q = PPP(r) -q -assert(raw(p) == raw(q)) -assert(PPP(raw(q))==q) -p = PPP()/PPP_IPCP(options=[PPP_IPCP_Option_DNS1(data="1.2.3.4"),PPP_IPCP_Option_DNS2(data="5.6.7.8"),PPP_IPCP_Option(type=123,data="ABCDEFG"),PPP_IPCP_Option_NBNS2(data="9.10.11.12")]) -p -r = raw(p) -r -assert(r == b'\x80!\x01\x00\x00\x1f\x81\x06\x01\x02\x03\x04\x83\x06\x05\x06\x07\x08{\tABCDEFG\x84\x06\t\n\x0b\x0c') -q = PPP(r) -q -assert( q[PPP_IPCP_Option].type == 123 ) -assert( q[PPP_IPCP_Option].data == b"ABCDEFG" ) -assert( q[PPP_IPCP_Option_NBNS2].data == '9.10.11.12' ) - - -= PPP ECP -~ ppp ecp - -p = PPP()/PPP_ECP(options=[PPP_ECP_Option_OUI(oui="XYZ")]) -p -r = raw(p) -r -assert(r == b'\x80S\x01\x00\x00\n\x00\x06XYZ\x00') -q = PPP(r) -q -assert(raw(p) == raw(q)) -p = PPP()/PPP_ECP(options=[PPP_ECP_Option_OUI(oui="XYZ"),PPP_ECP_Option(type=1,data="ABCDEFG")]) -p -r = raw(p) -r -assert(r == b'\x80S\x01\x00\x00\x13\x00\x06XYZ\x00\x01\tABCDEFG') -q = PPP(r) -q -assert( raw(p) == raw(q) ) -assert( q[PPP_ECP_Option].data == b"ABCDEFG" ) - - -= PPP with only one byte for protocol -~ ppp - -assert(len(raw(PPP() / IP())) == 21) - -p = PPP(b'!E\x00\x00<\x00\x00@\x008\x06\xa5\xce\x85wP)\xc0\xa8Va\x01\xbbd\x8a\xe2}r\xb8O\x95\xb5\x84\xa0\x12q \xc8\x08\x00\x00\x02\x04\x02\x18\x04\x02\x08\nQ\xdf\xd6\xb0\x00\x07LH\x01\x03\x03\x07Ao') -assert(IP in p) -assert(TCP in p) - -# Scapy6 Regression Test Campaign - -############ -############ -+ Test IPv6 Class -= IPv6 Class basic Instantiation -a=IPv6() - -= IPv6 Class basic build (default values) -raw(IPv6()) == b'`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= IPv6 Class basic dissection (default values) -a=IPv6(raw(IPv6())) -a.version == 6 and a.tc == 0 and a.fl == 0 and a.plen == 0 and a.nh == 59 and a.hlim ==64 and a.src == "::1" and a.dst == "::1" - -= IPv6 Class with basic TCP stacked - build -raw(IPv6()/TCP()) == b'`\x00\x00\x00\x00\x14\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x8f}\x00\x00' - -= IPv6 Class with basic TCP stacked - dissection -a=IPv6(raw(IPv6()/TCP())) -a.nh == 6 and a.plen == 20 and isinstance(a.payload, TCP) and a.payload.chksum == 0x8f7d - -= IPv6 Class with TCP and TCP data - build -raw(IPv6()/TCP()/Raw(load="somedata")) == b'`\x00\x00\x00\x00\x1c\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xd5\xdd\x00\x00somedata' - -= IPv6 Class with TCP and TCP data - dissection -a=IPv6(raw(IPv6()/TCP(dport=1234, sport=1234)/Raw(load="somedata"))) -a.nh == 6 and a.plen == 28 and isinstance(a.payload, TCP) and a.payload.chksum == 0xcc9d and isinstance(a.payload.payload, Raw) and a[Raw].load == b"somedata" - -= IPv6 Class binding with Ethernet - build -raw(Ether(src="00:00:00:00:00:00", dst="ff:ff:ff:ff:ff:ff")/IPv6()/TCP()) == b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x86\xdd`\x00\x00\x00\x00\x14\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x8f}\x00\x00' - -= IPv6 Class binding with Ethernet - dissection -a=Ether(raw(Ether()/IPv6()/TCP())) -a.type == 0x86dd - -= IPv6 Class - summary -a = Ether(src="aa:aa:aa:aa:aa:aa", dst="bb:bb:bb:bb:bb:bb")/IPv6(src='c266:a92d:0ed8:dc54:7d6f:9667:3743:a32f', dst='6406:c31f:d0b5:72fc:1700:2081:62e7:fae9') -assert a.summary() == 'Ether / c266:a92d:ed8:dc54:7d6f:9667:3743:a32f > 6406:c31f:d0b5:72fc:1700:2081:62e7:fae9 (59)' - -= IPv6 Class binding with GRE - build -s = raw(IP(src="127.0.0.1")/GRE()/Ether(dst="ff:ff:ff:ff:ff:ff", src="00:00:00:00:00:00")/IP()/GRE()/IPv6(src="::1")) -s == b'E\x00\x00f\x00\x01\x00\x00@/|f\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00eX\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00@\x00\x01\x00\x00@/|\x8c\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00\x86\xdd`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= IPv6 Class binding with GRE - dissection -p = IP(s) -GRE in p and p[GRE:1].proto == 0x6558 and p[GRE:2].proto == 0x86DD and IPv6 in p - -= IPv6 ma_addr coverage on hashret -IPv6(dst="ff00::1:ff28:9c5a", src="::").hashret() == b';' - -########### IPv6ExtHdrRouting Class ########################### - -= IPv6ExtHdrRouting Class - No address - build -raw(IPv6(src="2048::deca", dst="2047::cafe")/IPv6ExtHdrRouting(addresses=[])/TCP(dport=80)) ==b'`\x00\x00\x00\x00\x1c+@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x06\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xa5&\x00\x00' - -= IPv6ExtHdrRouting Class - One address - build -raw(IPv6(src="2048::deca", dst="2047::cafe")/IPv6ExtHdrRouting(addresses=["2022::deca"])/TCP(dport=80)) == b'`\x00\x00\x00\x00,+@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x06\x02\x00\x01\x00\x00\x00\x00 "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x91\x7f\x00\x00' - -= IPv6ExtHdrRouting Class - Multiple Addresses - build -raw(IPv6(src="2048::deca", dst="2047::cafe")/IPv6ExtHdrRouting(addresses=["2001::deca", "2022::deca"])/TCP(dport=80)) == b'`\x00\x00\x00\x00<+@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x06\x04\x00\x02\x00\x00\x00\x00 \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x91\x7f\x00\x00' - -= IPv6ExtHdrRouting Class - Specific segleft (2->1) - build -raw(IPv6(src="2048::deca", dst="2047::cafe")/IPv6ExtHdrRouting(addresses=["2001::deca", "2022::deca"], segleft=1)/TCP(dport=80)) == b'`\x00\x00\x00\x00<+@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x06\x04\x00\x01\x00\x00\x00\x00 \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x91\x7f\x00\x00' - -= IPv6ExtHdrRouting Class - Specific segleft (2->0) - build -raw(IPv6(src="2048::deca", dst="2047::cafe")/IPv6ExtHdrRouting(addresses=["2001::deca", "2022::deca"], segleft=0)/TCP(dport=80)) == b'`\x00\x00\x00\x00<+@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x06\x04\x00\x00\x00\x00\x00\x00 \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xa5&\x00\x00' - -########### IPv6ExtHdrSegmentRouting Class ########################### - -= IPv6ExtHdrSegmentRouting Class - default - build & dissect -s = raw(IPv6()/IPv6ExtHdrSegmentRouting()/UDP()) -assert(s == b'`\x00\x00\x00\x00 +@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x11\x02\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x005\x005\x00\x08\xffr') - -p = IPv6(s) -assert(UDP in p and IPv6ExtHdrSegmentRouting in p) -assert(p[IPv6ExtHdrSegmentRouting].lastentry == 0 and len(p[IPv6ExtHdrSegmentRouting].addresses) == 1 and len(p[IPv6ExtHdrSegmentRouting].tlv_objects) == 0) - -= IPv6ExtHdrSegmentRouting Class - addresses list - build & dissect - -s = raw(IPv6()/IPv6ExtHdrSegmentRouting(addresses=["::1", "::2", "::3"])/UDP()) -assert(s == b'`\x00\x00\x00\x00@+@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x11\x06\x04\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x005\x005\x00\x08\xffr') - -p = IPv6(s) -assert(UDP in p and IPv6ExtHdrSegmentRouting in p) -assert(p[IPv6ExtHdrSegmentRouting].lastentry == 2 and len(p[IPv6ExtHdrSegmentRouting].addresses) == 3 and len(p[IPv6ExtHdrSegmentRouting].tlv_objects) == 0) - -= IPv6ExtHdrSegmentRouting Class - TLVs list - build & dissect - -s = raw(IPv6()/IPv6ExtHdrSegmentRouting(tlv_objects=[IPv6ExtHdrSegmentRoutingTLV()])/TCP()) -assert(s == b'`\x00\x00\x00\x004+@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x04\x02\x00\x00\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x8f}\x00\x00') - -p = IPv6(s) -assert(TCP in p and IPv6ExtHdrSegmentRouting in p) -assert(p[IPv6ExtHdrSegmentRouting].lastentry == 0) -assert(len(p[IPv6ExtHdrSegmentRouting].addresses) == 1 and len(p[IPv6ExtHdrSegmentRouting].tlv_objects) == 2) -assert(isinstance(p[IPv6ExtHdrSegmentRouting].tlv_objects[1], IPv6ExtHdrSegmentRoutingTLVPadding)) - -= IPv6ExtHdrSegmentRouting Class - both lists - build & dissect - -s = raw(IPv6()/IPv6ExtHdrSegmentRouting(addresses=["::1", "::2", "::3"], tlv_objects=[IPv6ExtHdrSegmentRoutingTLVIngressNode(),IPv6ExtHdrSegmentRoutingTLVEgressNode()])/ICMPv6EchoRequest()) -assert(s == b'`\x00\x00\x00\x00h+@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01:\x0b\x04\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x01\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x80\x00\x7f\xbb\x00\x00\x00\x00') - -p = IPv6(s) -assert(p[IPv6ExtHdrSegmentRouting].lastentry == 2) -assert(ICMPv6EchoRequest in p and IPv6ExtHdrSegmentRouting in p) -assert(len(p[IPv6ExtHdrSegmentRouting].addresses) == 3 and len(p[IPv6ExtHdrSegmentRouting].tlv_objects) == 2) - -= IPv6ExtHdrSegmentRouting Class - UDP pseudo-header checksum - build & dissect - -s= raw(IPv6(src="fc00::1", dst="fd00::42")/IPv6ExtHdrSegmentRouting(addresses=["fd00::42", "fc13::1337"][::-1], segleft=1, lastentry=1) / UDP(sport=11000, dport=4242) / Raw('foobar')) -assert(s == b'`\x00\x00\x00\x006+@\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B\x11\x04\x04\x01\x01\x00\x00\x00\xfc\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x137\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B*\xf8\x10\x92\x00\x0e\x81\xb7foobar') - - -############ -############ -+ Test in6_get6to4Prefix() - -= Test in6_get6to4Prefix() - 0.0.0.0 address -in6_get6to4Prefix("0.0.0.0") == "2002::" - -= Test in6_get6to4Prefix() - 255.255.255.255 address -in6_get6to4Prefix("255.255.255.255") == "2002:ffff:ffff::" - -= Test in6_get6to4Prefix() - 1.1.1.1 address -in6_get6to4Prefix("1.1.1.1") == "2002:101:101::" - -= Test in6_get6to4Prefix() - invalid address -in6_get6to4Prefix("somebadrawing") is None - - -############ -############ -+ Test in6_6to4ExtractAddr() - -= Test in6_6to4ExtractAddr() - 2002:: address -in6_6to4ExtractAddr("2002::") == "0.0.0.0" - -= Test in6_6to4ExtractAddr() - 255.255.255.255 address -in6_6to4ExtractAddr("2002:ffff:ffff::") == "255.255.255.255" - -= Test in6_6to4ExtractAddr() - "2002:101:101::" address -in6_6to4ExtractAddr("2002:101:101::") == "1.1.1.1" - -= Test in6_6to4ExtractAddr() - invalid address -in6_6to4ExtractAddr("somebadrawing") is None - - -########### RFC 4489 - Link-Scoped IPv6 Multicast address ########### - -= in6_getLinkScopedMcastAddr() : default generation -a = in6_getLinkScopedMcastAddr(addr="FE80::") -a == 'ff32:ff::' - -= in6_getLinkScopedMcastAddr() : different valid scope values -a = in6_getLinkScopedMcastAddr(addr="FE80::", scope=0) -b = in6_getLinkScopedMcastAddr(addr="FE80::", scope=1) -c = in6_getLinkScopedMcastAddr(addr="FE80::", scope=2) -d = in6_getLinkScopedMcastAddr(addr="FE80::", scope=3) -a == 'ff30:ff::' and b == 'ff31:ff::' and c == 'ff32:ff::' and d is None - -= in6_getLinkScopedMcastAddr() : grpid in different formats -a = in6_getLinkScopedMcastAddr(addr="FE80::A12:34FF:FE56:7890", grpid=b"\x12\x34\x56\x78") -b = in6_getLinkScopedMcastAddr(addr="FE80::A12:34FF:FE56:7890", grpid="12345678") -c = in6_getLinkScopedMcastAddr(addr="FE80::A12:34FF:FE56:7890", grpid=305419896) -a == b and b == c - - -########### ethernet address to iface ID conversion ################# - -= in6_mactoifaceid() conversion function (test 1) -in6_mactoifaceid("FD:00:00:00:00:00", ulbit=0) == 'FD00:00FF:FE00:0000' - -= in6_mactoifaceid() conversion function (test 2) -in6_mactoifaceid("FD:00:00:00:00:00", ulbit=1) == 'FF00:00FF:FE00:0000' - -= in6_mactoifaceid() conversion function (test 3) -in6_mactoifaceid("FD:00:00:00:00:00") == 'FF00:00FF:FE00:0000' - -= in6_mactoifaceid() conversion function (test 4) -in6_mactoifaceid("FF:00:00:00:00:00") == 'FD00:00FF:FE00:0000' - -= in6_mactoifaceid() conversion function (test 5) -in6_mactoifaceid("FF:00:00:00:00:00", ulbit=1) == 'FF00:00FF:FE00:0000' - -= in6_mactoifaceid() conversion function (test 6) -in6_mactoifaceid("FF:00:00:00:00:00", ulbit=0) == 'FD00:00FF:FE00:0000' - -########### iface ID conversion ################# - -= in6_mactoifaceid() conversion function (test 1) -in6_ifaceidtomac(in6_mactoifaceid("FD:00:00:00:00:00", ulbit=0)) == 'ff:00:00:00:00:00' - -= in6_mactoifaceid() conversion function (test 2) -in6_ifaceidtomac(in6_mactoifaceid("FD:00:00:00:00:00", ulbit=1)) == 'fd:00:00:00:00:00' - -= in6_mactoifaceid() conversion function (test 3) -in6_ifaceidtomac(in6_mactoifaceid("FD:00:00:00:00:00")) == 'fd:00:00:00:00:00' - -= in6_mactoifaceid() conversion function (test 4) -in6_ifaceidtomac(in6_mactoifaceid("FF:00:00:00:00:00")) == 'ff:00:00:00:00:00' - -= in6_mactoifaceid() conversion function (test 5) -in6_ifaceidtomac(in6_mactoifaceid("FF:00:00:00:00:00", ulbit=1)) == 'fd:00:00:00:00:00' - -= in6_mactoifaceid() conversion function (test 6) -in6_ifaceidtomac(in6_mactoifaceid("FF:00:00:00:00:00", ulbit=0)) == 'ff:00:00:00:00:00' - - -= in6_addrtomac() conversion function (test 1) -in6_addrtomac("FE80::" + in6_mactoifaceid("FD:00:00:00:00:00", ulbit=0)) == 'ff:00:00:00:00:00' - -= in6_addrtomac() conversion function (test 2) -in6_addrtomac("FE80::" + in6_mactoifaceid("FD:00:00:00:00:00", ulbit=1)) == 'fd:00:00:00:00:00' - -= in6_addrtomac() conversion function (test 3) -in6_addrtomac("FE80::" + in6_mactoifaceid("FD:00:00:00:00:00")) == 'fd:00:00:00:00:00' - -= in6_addrtomac() conversion function (test 4) -in6_addrtomac("FE80::" + in6_mactoifaceid("FF:00:00:00:00:00")) == 'ff:00:00:00:00:00' - -= in6_addrtomac() conversion function (test 5) -in6_addrtomac("FE80::" + in6_mactoifaceid("FF:00:00:00:00:00", ulbit=1)) == 'fd:00:00:00:00:00' - -= in6_addrtomac() conversion function (test 6) -in6_addrtomac("FE80::" + in6_mactoifaceid("FF:00:00:00:00:00", ulbit=0)) == 'ff:00:00:00:00:00' - -########### RFC 3041 related function ############################### -= Test in6_getRandomizedIfaceId -import socket - -res=True -for a in six.moves.range(10): - s1,s2 = in6_getRandomizedIfaceId('20b:93ff:feeb:2d3') - inet_pton(socket.AF_INET6, '::'+s1) - tmp2 = inet_pton(socket.AF_INET6, '::'+s2) - res = res and ((orb(s1[0]) & 0x04) == 0x04) - s1,s2 = in6_getRandomizedIfaceId('20b:93ff:feeb:2d3', previous=tmp2) - tmp = inet_pton(socket.AF_INET6, '::'+s1) - inet_pton(socket.AF_INET6, '::'+s2) - res = res and ((orb(s1[0]) & 0x04) == 0x04) - -########### RFC 1924 related function ############################### -= Test RFC 1924 function - in6_ctop() basic test -in6_ctop("4)+k&C#VzJ4br>0wv%Yp") == '1080::8:800:200c:417a' - -= Test RFC 1924 function - in6_ctop() with character outside charset -in6_ctop("4)+k&C#VzJ4br>0wv%Y'") == None - -= Test RFC 1924 function - in6_ctop() with bad length address -in6_ctop("4)+k&C#VzJ4br>0wv%Y") == None - -= Test RFC 1924 function - in6_ptoc() basic test -in6_ptoc('1080::8:800:200c:417a') == '4)+k&C#VzJ4br>0wv%Yp' - -= Test RFC 1924 function - in6_ptoc() basic test -in6_ptoc('1080::8:800:200c:417a') == '4)+k&C#VzJ4br>0wv%Yp' - -= Test RFC 1924 function - in6_ptoc() with bad input -in6_ptoc('1080:::8:800:200c:417a') == None - -########### in6_getAddrType ######################################### - -= in6_getAddrType - 6to4 addresses -in6_getAddrType("2002::1") == (IPV6_ADDR_UNICAST | IPV6_ADDR_GLOBAL | IPV6_ADDR_6TO4) - -= in6_getAddrType - Assignable Unicast global address -in6_getAddrType("2001:db8::1") == (IPV6_ADDR_UNICAST | IPV6_ADDR_GLOBAL) - -= in6_getAddrType - Multicast global address -in6_getAddrType("FF0E::1") == (IPV6_ADDR_GLOBAL | IPV6_ADDR_MULTICAST) - -= in6_getAddrType - Multicast local address -in6_getAddrType("FF02::1") == (IPV6_ADDR_LINKLOCAL | IPV6_ADDR_MULTICAST) - -= in6_getAddrType - Unicast Link-Local address -in6_getAddrType("FE80::") == (IPV6_ADDR_UNICAST | IPV6_ADDR_LINKLOCAL) - -= in6_getAddrType - Loopback address -in6_getAddrType("::1") == IPV6_ADDR_LOOPBACK - -= in6_getAddrType - Unspecified address -in6_getAddrType("::") == IPV6_ADDR_UNSPECIFIED - -= in6_getAddrType - Unassigned Global Unicast address -in6_getAddrType("4000::") == (IPV6_ADDR_GLOBAL | IPV6_ADDR_UNICAST) - -= in6_getAddrType - Weird address (FE::1) -in6_getAddrType("FE::") == (IPV6_ADDR_GLOBAL | IPV6_ADDR_UNICAST) - -= in6_getAddrType - Weird address (FE8::1) -in6_getAddrType("FE8::1") == (IPV6_ADDR_GLOBAL | IPV6_ADDR_UNICAST) - -= in6_getAddrType - Weird address (1::1) -in6_getAddrType("1::1") == (IPV6_ADDR_GLOBAL | IPV6_ADDR_UNICAST) - -= in6_getAddrType - Weird address (1000::1) -in6_getAddrType("1000::1") == (IPV6_ADDR_GLOBAL | IPV6_ADDR_UNICAST) - -########### ICMPv6DestUnreach Class ################################# - -= ICMPv6DestUnreach Class - Basic Build (no argument) -raw(ICMPv6DestUnreach()) == b'\x01\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6DestUnreach Class - Basic Build over IPv6 (for cksum and overload) -raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6DestUnreach()) == b'`\x00\x00\x00\x00\x08:@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x01\x00\x14e\x00\x00\x00\x00' - -= ICMPv6DestUnreach Class - Basic Build over IPv6 with some payload -raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6DestUnreach()/IPv6(src="2047::cafe", dst="2048::deca")) == b'`\x00\x00\x00\x000:@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x01\x00\x8e\xa3\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@ G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca' - -= ICMPv6DestUnreach Class - Dissection with default values and some payload -a = IPv6(raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6DestUnreach()/IPv6(src="2047::cafe", dst="2048::deca"))) -a.plen == 48 and a.nh == 58 and ICMPv6DestUnreach in a and a[ICMPv6DestUnreach].type == 1 and a[ICMPv6DestUnreach].code == 0 and a[ICMPv6DestUnreach].cksum == 0x8ea3 and a[ICMPv6DestUnreach].unused == 0 and IPerror6 in a - -= ICMPv6DestUnreach Class - Dissection with specific values -a=IPv6(raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6DestUnreach(code=1, cksum=0x6666, unused=0x7777)/IPv6(src="2047::cafe", dst="2048::deca"))) -a.plen == 48 and a.nh == 58 and ICMPv6DestUnreach in a and a[ICMPv6DestUnreach].type == 1 and a[ICMPv6DestUnreach].cksum == 0x6666 and a[ICMPv6DestUnreach].unused == 0x7777 and IPerror6 in a[ICMPv6DestUnreach] - -= ICMPv6DestUnreach Class - checksum computation related stuff -a=IPv6(raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6DestUnreach(code=1, cksum=0x6666, unused=0x7777)/IPv6(src="2047::cafe", dst="2048::deca")/TCP())) -b=IPv6(raw(IPv6(src="2047::cafe", dst="2048::deca")/TCP())) -a[ICMPv6DestUnreach][TCPerror].chksum == b.chksum - - -########### ICMPv6PacketTooBig Class ################################ - -= ICMPv6PacketTooBig Class - Basic Build (no argument) -raw(ICMPv6PacketTooBig()) == b'\x02\x00\x00\x00\x00\x00\x05\x00' - -= ICMPv6PacketTooBig Class - Basic Build over IPv6 (for cksum and overload) -raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6PacketTooBig()) == b'`\x00\x00\x00\x00\x08:@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x02\x00\x0ee\x00\x00\x05\x00' - -= ICMPv6PacketTooBig Class - Basic Build over IPv6 with some payload -raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6PacketTooBig()/IPv6(src="2047::cafe", dst="2048::deca")) == b'`\x00\x00\x00\x000:@ H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x02\x00\x88\xa3\x00\x00\x05\x00`\x00\x00\x00\x00\x00;@ G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca' - -= ICMPv6PacketTooBig Class - Dissection with default values and some payload -a = IPv6(raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6PacketTooBig()/IPv6(src="2047::cafe", dst="2048::deca"))) -a.plen == 48 and a.nh == 58 and ICMPv6PacketTooBig in a and a[ICMPv6PacketTooBig].type == 2 and a[ICMPv6PacketTooBig].code == 0 and a[ICMPv6PacketTooBig].cksum == 0x88a3 and a[ICMPv6PacketTooBig].mtu == 1280 and IPerror6 in a -True - -= ICMPv6PacketTooBig Class - Dissection with specific values -a=IPv6(raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6PacketTooBig(code=2, cksum=0x6666, mtu=1460)/IPv6(src="2047::cafe", dst="2048::deca"))) -a.plen == 48 and a.nh == 58 and ICMPv6PacketTooBig in a and a[ICMPv6PacketTooBig].type == 2 and a[ICMPv6PacketTooBig].code == 2 and a[ICMPv6PacketTooBig].cksum == 0x6666 and a[ICMPv6PacketTooBig].mtu == 1460 and IPerror6 in a - -= ICMPv6PacketTooBig Class - checksum computation related stuff -a=IPv6(raw(IPv6(src="2048::deca", dst="2047::cafe")/ICMPv6PacketTooBig(code=1, cksum=0x6666, mtu=0x7777)/IPv6(src="2047::cafe", dst="2048::deca")/TCP())) -b=IPv6(raw(IPv6(src="2047::cafe", dst="2048::deca")/TCP())) -a[ICMPv6PacketTooBig][TCPerror].chksum == b.chksum - - -########### ICMPv6TimeExceeded Class ################################ -# To be done but not critical. Same mechanisms and format as -# previous ones. - -########### ICMPv6ParamProblem Class ################################ -# See previous note - -############ -############ -+ Test ICMPv6EchoRequest Class - -= ICMPv6EchoRequest - Basic Instantiation -raw(ICMPv6EchoRequest()) == b'\x80\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6EchoRequest - Instantiation with specific values -raw(ICMPv6EchoRequest(code=0xff, cksum=0x1111, id=0x2222, seq=0x3333, data="thisissomestring")) == b'\x80\xff\x11\x11""33thisissomestring' - -= ICMPv6EchoRequest - Basic dissection -a=ICMPv6EchoRequest(b'\x80\x00\x00\x00\x00\x00\x00\x00') -a.type == 128 and a.code == 0 and a.cksum == 0 and a.id == 0 and a.seq == 0 and a.data == b"" - -= ICMPv6EchoRequest - Dissection with specific values -a=ICMPv6EchoRequest(b'\x80\xff\x11\x11""33thisissomerawing') -a.type == 128 and a.code == 0xff and a.cksum == 0x1111 and a.id == 0x2222 and a.seq == 0x3333 and a.data == b"thisissomerawing" - -= ICMPv6EchoRequest - Automatic checksum computation and field overloading (build) -raw(IPv6(dst="2001::cafe", src="2001::deca", hlim=64)/ICMPv6EchoRequest()) == b'`\x00\x00\x00\x00\x08:@ \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x80\x00\x95\xf1\x00\x00\x00\x00' - -= ICMPv6EchoRequest - Automatic checksum computation and field overloading (dissection) -a=IPv6(b'`\x00\x00\x00\x00\x08:@ \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x80\x00\x95\xf1\x00\x00\x00\x00') -isinstance(a, IPv6) and a.nh == 58 and isinstance(a.payload, ICMPv6EchoRequest) and a.payload.cksum == 0x95f1 - - -############ -############ -+ Test ICMPv6EchoReply Class - -= ICMPv6EchoReply - Basic Instantiation -raw(ICMPv6EchoReply()) == b'\x81\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6EchoReply - Instantiation with specific values -raw(ICMPv6EchoReply(code=0xff, cksum=0x1111, id=0x2222, seq=0x3333, data="thisissomestring")) == b'\x81\xff\x11\x11""33thisissomestring' - -= ICMPv6EchoReply - Basic dissection -a=ICMPv6EchoReply(b'\x80\x00\x00\x00\x00\x00\x00\x00') -a.type == 128 and a.code == 0 and a.cksum == 0 and a.id == 0 and a.seq == 0 and a.data == b"" - -= ICMPv6EchoReply - Dissection with specific values -a=ICMPv6EchoReply(b'\x80\xff\x11\x11""33thisissomerawing') -a.type == 128 and a.code == 0xff and a.cksum == 0x1111 and a.id == 0x2222 and a.seq == 0x3333 and a.data == b"thisissomerawing" - -= ICMPv6EchoReply - Automatic checksum computation and field overloading (build) -raw(IPv6(dst="2001::cafe", src="2001::deca", hlim=64)/ICMPv6EchoReply()) == b'`\x00\x00\x00\x00\x08:@ \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x81\x00\x94\xf1\x00\x00\x00\x00' - -= ICMPv6EchoReply - Automatic checksum computation and field overloading (dissection) -a=IPv6(b'`\x00\x00\x00\x00\x08:@ \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xca \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe\x80\x00\x95\xf1\x00\x00\x00\x00') -isinstance(a, IPv6) and a.nh == 58 and isinstance(a.payload, ICMPv6EchoRequest) and a.payload.cksum == 0x95f1 - -########### ICMPv6EchoReply/Request answers() and hashret() ######### - -= ICMPv6EchoRequest and ICMPv6EchoReply - hashret() test 1 -b=IPv6(src="2047::deca", dst="2048::cafe")/ICMPv6EchoReply(data="somedata") -a=IPv6(src="2048::cafe", dst="2047::deca")/ICMPv6EchoRequest(data="somedata") -b.hashret() == a.hashret() - -# data are not taken into account for hashret -= ICMPv6EchoRequest and ICMPv6EchoReply - hashret() test 2 -b=IPv6(src="2047::deca", dst="2048::cafe")/ICMPv6EchoReply(data="somedata") -a=IPv6(src="2048::cafe", dst="2047::deca")/ICMPv6EchoRequest(data="otherdata") -b.hashret() == a.hashret() - -= ICMPv6EchoRequest and ICMPv6EchoReply - hashret() test 3 -b=IPv6(src="2047::deca", dst="2048::cafe")/ICMPv6EchoReply(id=0x6666, seq=0x7777,data="somedata") -a=IPv6(src="2048::cafe", dst="2047::deca")/ICMPv6EchoRequest(id=0x6666, seq=0x8888, data="somedata") -b.hashret() != a.hashret() - -= ICMPv6EchoRequest and ICMPv6EchoReply - hashret() test 4 -b=IPv6(src="2047::deca", dst="2048::cafe")/ICMPv6EchoReply(id=0x6666, seq=0x7777,data="somedata") -a=IPv6(src="2048::cafe", dst="2047::deca")/ICMPv6EchoRequest(id=0x8888, seq=0x7777, data="somedata") -b.hashret() != a.hashret() - -= ICMPv6EchoRequest and ICMPv6EchoReply - answers() test 5 -b=IPv6(src="2047::deca", dst="2048::cafe")/ICMPv6EchoReply(data="somedata") -a=IPv6(src="2048::cafe", dst="2047::deca")/ICMPv6EchoRequest(data="somedata") -(a > b) == True - -= ICMPv6EchoRequest and ICMPv6EchoReply - answers() test 6 -b=IPv6(src="2047::deca", dst="2048::cafe")/ICMPv6EchoReply(id=0x6666, seq=0x7777, data="somedata") -a=IPv6(src="2048::cafe", dst="2047::deca")/ICMPv6EchoRequest(id=0x6666, seq=0x7777, data="somedata") -(a > b) == True - -= ICMPv6EchoRequest and ICMPv6EchoReply - live answers() use Net6 -~ netaccess ipv6 - -a = IPv6(dst="www.google.com")/ICMPv6EchoRequest() -b = IPv6(src="www.google.com", dst=a.src)/ICMPv6EchoReply() -assert b.answers(a) -assert (a > b) - - -########### ICMPv6MRD* Classes ###################################### - -= ICMPv6MRD_Advertisement - Basic instantiation -raw(ICMPv6MRD_Advertisement()) == b'\x97\x14\x00\x00\x00\x00\x00\x00' - -= ICMPv6MRD_Advertisement - Instantiation with specific values -raw(ICMPv6MRD_Advertisement(advinter=0xdd, queryint=0xeeee, robustness=0xffff)) == b'\x97\xdd\x00\x00\xee\xee\xff\xff' - -= ICMPv6MRD_Advertisement - Basic Dissection and overloading mechanisms -a=Ether(raw(Ether()/IPv6()/ICMPv6MRD_Advertisement())) -a.dst == "33:33:00:00:00:02" and IPv6 in a and a[IPv6].plen == 8 and a[IPv6].nh == 58 and a[IPv6].hlim == 1 and a[IPv6].dst == "ff02::2" and ICMPv6MRD_Advertisement in a and a[ICMPv6MRD_Advertisement].type == 151 and a[ICMPv6MRD_Advertisement].advinter == 20 and a[ICMPv6MRD_Advertisement].queryint == 0 and a[ICMPv6MRD_Advertisement].robustness == 0 - - -= ICMPv6MRD_Solicitation - Basic dissection -raw(ICMPv6MRD_Solicitation()) == b'\x98\x00\x00\x00' - -= ICMPv6MRD_Solicitation - Instantiation with specific values -raw(ICMPv6MRD_Solicitation(res=0xbb)) == b'\x98\xbb\x00\x00' - -= ICMPv6MRD_Solicitation - Basic Dissection and overloading mechanisms -a=Ether(raw(Ether()/IPv6()/ICMPv6MRD_Solicitation())) -a.dst == "33:33:00:00:00:02" and IPv6 in a and a[IPv6].plen == 4 and a[IPv6].nh == 58 and a[IPv6].hlim == 1 and a[IPv6].dst == "ff02::2" and ICMPv6MRD_Solicitation in a and a[ICMPv6MRD_Solicitation].type == 152 and a[ICMPv6MRD_Solicitation].res == 0 - - -= ICMPv6MRD_Termination Basic instantiation -raw(ICMPv6MRD_Termination()) == b'\x99\x00\x00\x00' - -= ICMPv6MRD_Termination - Instantiation with specific values -raw(ICMPv6MRD_Termination(res=0xbb)) == b'\x99\xbb\x00\x00' - -= ICMPv6MRD_Termination - Basic Dissection and overloading mechanisms -a=Ether(raw(Ether()/IPv6()/ICMPv6MRD_Termination())) -a.dst == "33:33:00:00:00:6a" and IPv6 in a and a[IPv6].plen == 4 and a[IPv6].nh == 58 and a[IPv6].hlim == 1 and a[IPv6].dst == "ff02::6a" and ICMPv6MRD_Termination in a and a[ICMPv6MRD_Termination].type == 153 and a[ICMPv6MRD_Termination].res == 0 - - -############ -############ -+ Test HBHOptUnknown Class - -= HBHOptUnknown - Basic Instantiation -raw(HBHOptUnknown()) == b'\x01\x00' - -= HBHOptUnknown - Basic Dissection -a=HBHOptUnknown(b'\x01\x00') -a.otype == 0x01 and a.optlen == 0 and a.optdata == b"" - -= HBHOptUnknown - Automatic optlen computation -raw(HBHOptUnknown(optdata="B"*10)) == b'\x01\nBBBBBBBBBB' - -= HBHOptUnknown - Instantiation with specific values -raw(HBHOptUnknown(optlen=9, optdata="B"*10)) == b'\x01\tBBBBBBBBBB' - -= HBHOptUnknown - Dissection with specific values -a=HBHOptUnknown(b'\x01\tBBBBBBBBBB') -a.otype == 0x01 and a.optlen == 9 and a.optdata == b"B"*9 and isinstance(a.payload, Raw) and a.payload.load == b"B" - - -############ -############ -+ Test Pad1 Class - -= Pad1 - Basic Instantiation -raw(Pad1()) == b'\x00' - -= Pad1 - Basic Dissection -raw(Pad1(b'\x00')) == b'\x00' - - -############ -############ -+ Test PadN Class - -= PadN - Basic Instantiation -raw(PadN()) == b'\x01\x00' - -= PadN - Optlen Automatic computation -raw(PadN(optdata="B"*10)) == b'\x01\nBBBBBBBBBB' - -= PadN - Basic Dissection -a=PadN(b'\x01\x00') -a.otype == 1 and a.optlen == 0 and a.optdata == b"" - -= PadN - Dissection with specific values -a=PadN(b'\x01\x0cBBBBBBBBBB') -a.otype == 1 and a.optlen == 12 and a.optdata == b'BBBBBBBBBB' - -= PadN - Instantiation with forced optlen -raw(PadN(optdata="B"*10, optlen=9)) == b'\x01\x09BBBBBBBBBB' - - -############ -############ -+ Test RouterAlert Class (RFC 2711) - -= RouterAlert - Basic Instantiation -raw(RouterAlert()) == b'\x05\x02\x00\x00' - -= RouterAlert - Basic Dissection -a=RouterAlert(b'\x05\x02\x00\x00') -a.otype == 0x05 and a.optlen == 2 and a.value == 00 - -= RouterAlert - Instantiation with specific values -raw(RouterAlert(optlen=3, value=0xffff)) == b'\x05\x03\xff\xff' - -= RouterAlert - Instantiation with specific values -a=RouterAlert(b'\x05\x03\xff\xff') -a.otype == 0x05 and a.optlen == 3 and a.value == 0xffff - - -############ -############ -+ Test Jumbo Class (RFC 2675) - -= Jumbo - Basic Instantiation -raw(Jumbo()) == b'\xc2\x04\x00\x00\x00\x00' - -= Jumbo - Basic Dissection -a=Jumbo(b'\xc2\x04\x00\x00\x00\x00') -a.otype == 0xC2 and a.optlen == 4 and a.jumboplen == 0 - -= Jumbo - Instantiation with specific values -raw(Jumbo(optlen=6, jumboplen=0xffffffff)) == b'\xc2\x06\xff\xff\xff\xff' - -= Jumbo - Dissection with specific values -a=Jumbo(b'\xc2\x06\xff\xff\xff\xff') -a.otype == 0xc2 and a.optlen == 6 and a.jumboplen == 0xffffffff - - -############ -############ -+ Test HAO Class (RFC 3775) - -= HAO - Basic Instantiation -raw(HAO()) == b'\xc9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= HAO - Basic Dissection -a=HAO(b'\xc9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.otype == 0xC9 and a.optlen == 16 and a.hoa == "::" - -= HAO - Instantiation with specific values -raw(HAO(optlen=9, hoa="2001::ffff")) == b'\xc9\t \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff' - -= HAO - Dissection with specific values -a=HAO(b'\xc9\t \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff') -a.otype == 0xC9 and a.optlen == 9 and a.hoa == "2001::ffff" - -= HAO - hashret - -p = IPv6()/IPv6ExtHdrDestOpt(options=HAO(hoa="2001:db8::1"))/ICMPv6EchoRequest() -p.hashret() == b' \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00:\x00\x00\x00\x00' - - -############ -############ -+ Test IPv6ExtHdrHopByHop() - -= IPv6ExtHdrHopByHop - Basic Instantiation -raw(IPv6ExtHdrHopByHop()) == b';\x00\x01\x04\x00\x00\x00\x00' - -= IPv6ExtHdrHopByHop - Instantiation with HAO option -raw(IPv6ExtHdrHopByHop(options=[HAO()])) == b';\x02\x01\x02\x00\x00\xc9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= IPv6ExtHdrHopByHop - Instantiation with RouterAlert option -raw(IPv6ExtHdrHopByHop(options=[RouterAlert()])) == b';\x00\x05\x02\x00\x00\x01\x00' - -= IPv6ExtHdrHopByHop - Instantiation with Jumbo option -raw(IPv6ExtHdrHopByHop(options=[Jumbo()])) == b';\x00\xc2\x04\x00\x00\x00\x00' - -= IPv6ExtHdrHopByHop - Complete dissection with Jumbo option -s = b'`\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01:\x00\xc2\x04\x00\x00\x00\x10\x80\x00\x7f\xbb\x00\x00\x00\x00' -p = IPv6(s) -assert IPv6ExtHdrHopByHop in p and Jumbo in p and ICMPv6EchoRequest in p - -s = b'`\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01:\x01\x01\x06\x00\x00\x00\x00\x00\x00\xc2\x04\x00\x00\x00\x18\x80\x00\x7f\xbb\x00\x00\x00\x00' -p = IPv6(s) -assert IPv6ExtHdrHopByHop in p and PadN in p and Jumbo in p and ICMPv6EchoRequest in p - -= IPv6ExtHdrHopByHop - Instantiation with Pad1 option -raw(IPv6ExtHdrHopByHop(options=[Pad1()])) == b';\x00\x00\x01\x03\x00\x00\x00' - -= IPv6ExtHdrHopByHop - Instantiation with PadN option -raw(IPv6ExtHdrHopByHop(options=[Pad1()])) == b';\x00\x00\x01\x03\x00\x00\x00' - -= IPv6ExtHdrHopByHop - Instantiation with Jumbo, RouterAlert, HAO -raw(IPv6ExtHdrHopByHop(options=[Jumbo(), RouterAlert(), HAO()])) == b';\x03\xc2\x04\x00\x00\x00\x00\x05\x02\x00\x00\x01\x00\xc9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= IPv6ExtHdrHopByHop - Instantiation with HAO, Jumbo, RouterAlert -raw(IPv6ExtHdrHopByHop(options=[HAO(), Jumbo(), RouterAlert()])) == b';\x04\x01\x02\x00\x00\xc9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\xc2\x04\x00\x00\x00\x00\x05\x02\x00\x00\x01\x02\x00\x00' - -= IPv6ExtHdrHopByHop - Instantiation with RouterAlert, HAO, Jumbo -raw(IPv6ExtHdrHopByHop(options=[RouterAlert(), HAO(), Jumbo()])) == b';\x03\x05\x02\x00\x00\xc9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\xc2\x04\x00\x00\x00\x00' - -= IPv6ExtHdrHopByHop - Basic Dissection -a=IPv6ExtHdrHopByHop(b';\x00\x01\x04\x00\x00\x00\x00') -a.nh == 59 and a.len == 0 and len(a.options) == 1 and isinstance(a.options[0], PadN) and a.options[0].otype == 1 and a.options[0].optlen == 4 and a.options[0].optdata == b'\x00'*4 - -#= IPv6ExtHdrHopByHop - Automatic length computation -#raw(IPv6ExtHdrHopByHop(options=["toto"])) == b'\x00\x00toto' -#= IPv6ExtHdrHopByHop - Automatic length computation -#raw(IPv6ExtHdrHopByHop(options=["toto"])) == b'\x00\x00tototo' - - -############ -############ -+ Test ICMPv6ND_RS() class - ICMPv6 Type 133 Code 0 - -= ICMPv6ND_RS - Basic instantiation -raw(ICMPv6ND_RS()) == b'\x85\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6ND_RS - Basic instantiation with empty dst in IPv6 underlayer -raw(IPv6(src="2001:db8::1")/ICMPv6ND_RS()) == b'`\x00\x00\x00\x00\x08:\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x85\x00M\xfe\x00\x00\x00\x00' - -= ICMPv6ND_RS - Basic dissection -a=ICMPv6ND_RS(b'\x85\x00\x00\x00\x00\x00\x00\x00') -a.type == 133 and a.code == 0 and a.cksum == 0 and a.res == 0 - -= ICMPv6ND_RS - Basic instantiation with empty dst in IPv6 underlayer -a=IPv6(b'`\x00\x00\x00\x00\x08:\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x85\x00M\xfe\x00\x00\x00\x00') -isinstance(a, IPv6) and a.nh == 58 and a.hlim == 255 and isinstance(a.payload, ICMPv6ND_RS) and a.payload.type == 133 and a.payload.code == 0 and a.payload.cksum == 0x4dfe and a.payload.res == 0 - - -############ -############ -+ Test ICMPv6ND_RA() class - ICMPv6 Type 134 Code 0 - -= ICMPv6ND_RA - Basic Instantiation -raw(ICMPv6ND_RA()) == b'\x86\x00\x00\x00\x00\x08\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6ND_RA - Basic instantiation with empty dst in IPv6 underlayer -raw(IPv6(src="2001:db8::1")/ICMPv6ND_RA()) == b'`\x00\x00\x00\x00\x10:\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x86\x00E\xe7\x00\x08\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6ND_RA - Basic dissection -a=ICMPv6ND_RA(b'\x86\x00\x00\x00\x00\x08\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 134 and a.code == 0 and a.cksum == 0 and a.chlim == 0 and a.M == 0 and a.O == 0 and a.H == 0 and a.prf == 1 and a.res == 0 and a.routerlifetime == 1800 and a.reachabletime == 0 and a.retranstimer == 0 - -= ICMPv6ND_RA - Basic instantiation with empty dst in IPv6 underlayer -a=IPv6(b'`\x00\x00\x00\x00\x10:\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x86\x00E\xe7\x00\x08\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00') -isinstance(a, IPv6) and a.nh == 58 and a.hlim == 255 and isinstance(a.payload, ICMPv6ND_RA) and a.payload.type == 134 and a.code == 0 and a.cksum == 0x45e7 and a.chlim == 0 and a.M == 0 and a.O == 0 and a.H == 0 and a.prf == 1 and a.res == 0 and a.routerlifetime == 1800 and a.reachabletime == 0 and a.retranstimer == 0 - -= ICMPv6ND_RA - Answers -assert ICMPv6ND_RA().answers(ICMPv6ND_RS()) -a=IPv6(b'`\x00\x00\x00\x00\x10:\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x86\x00E\xe7\x00\x08\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00') -b = IPv6(b"`\x00\x00\x00\x00\x10:\xff\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x85\x00M\xff\x00\x00\x00\x00") -assert a.answers(b) - -= ICMPv6ND_RA - Summary Output -ICMPv6ND_RA(chlim=42, M=0, O=1, H=0, prf=1, P=0, routerlifetime=300).mysummary() == "ICMPv6 Neighbor Discovery - Router Advertisement Lifetime 300 Hop Limit 42 Preference High Managed 0 Other 1 Home 0" - -############ -############ -+ ICMPv6ND_NS Class Test - -= ICMPv6ND_NS - Basic Instantiation -raw(ICMPv6ND_NS()) == b'\x87\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6ND_NS - Instantiation with specific values -raw(ICMPv6ND_NS(code=0x11, res=3758096385, tgt="ffff::1111")) == b'\x87\x11\x00\x00\xe0\x00\x00\x01\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6ND_NS - Basic Dissection -a=ICMPv6ND_NS(b'\x87\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.code==0 and a.res==0 and a.tgt=="::" - -= ICMPv6ND_NS - Dissection with specific values -a=ICMPv6ND_NS(b'\x87\x11\x00\x00\xe0\x00\x00\x01\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -a.code==0x11 and a.res==3758096385 and a.tgt=="ffff::1111" - -= ICMPv6ND_NS - IPv6 layer fields overloading -a=IPv6(raw(IPv6()/ICMPv6ND_NS())) -a.nh == 58 and a.dst=="ff02::1" and a.hlim==255 - -############ -############ -+ ICMPv6ND_NA Class Test - -= ICMPv6ND_NA - Basic Instantiation -raw(ICMPv6ND_NA()) == b'\x88\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6ND_NA - Instantiation with specific values -raw(ICMPv6ND_NA(code=0x11, R=0, S=1, O=0, res=1, tgt="ffff::1111")) == b'\x88\x11\x00\x00@\x00\x00\x01\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6ND_NA - Basic Dissection -a=ICMPv6ND_NA(b'\x88\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.code==0 and a.R==0 and a.S==0 and a.O==0 and a.res==0 and a.tgt=="::" - -= ICMPv6ND_NA - Dissection with specific values -a=ICMPv6ND_NA(b'\x88\x11\x00\x00@\x00\x00\x01\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -a.code==0x11 and a.R==0 and a.S==1 and a.O==0 and a.res==1 and a.tgt=="ffff::1111" -assert a.hashret() == b'ffff::1111' - -= ICMPv6ND_NS - IPv6 layer fields overloading -a=IPv6(raw(IPv6()/ICMPv6ND_NS())) -a.nh == 58 and a.dst=="ff02::1" and a.hlim==255 - - -############ -############ -+ ICMPv6ND_ND/ICMPv6ND_ND matching test - -= ICMPv6ND_ND/ICMPv6ND_ND matching - test 1 -# Sent NS -a=IPv6(b'`\x00\x00\x00\x00\x18:\xff\xfe\x80\x00\x00\x00\x00\x00\x00\x02\x0f\x1f\xff\xfe\xcaFP\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x87\x00UC\x00\x00\x00\x00\xfe\x80\x00\x00\x00\x00\x00\x00\x02\x0f4\xff\xfe\x8a\x8a\xa1') -# Received NA -b=IPv6(b'n\x00\x00\x00\x00 :\xff\xfe\x80\x00\x00\x00\x00\x00\x00\x02\x0f4\xff\xfe\x8a\x8a\xa1\xfe\x80\x00\x00\x00\x00\x00\x00\x02\x0f\x1f\xff\xfe\xcaFP\x88\x00\xf3F\xe0\x00\x00\x00\xfe\x80\x00\x00\x00\x00\x00\x00\x02\x0f4\xff\xfe\x8a\x8a\xa1\x02\x01\x00\x0f4\x8a\x8a\xa1') -b.answers(a) - - -############ -############ -+ ICMPv6NDOptUnknown Class Test - -= ICMPv6NDOptUnknown - Basic Instantiation -raw(ICMPv6NDOptUnknown()) == b'\x00\x02' - -= ICMPv6NDOptUnknown - Instantiation with specific values -raw(ICMPv6NDOptUnknown(len=4, data="somestring")) == b'\x00\x04somestring' - -= ICMPv6NDOptUnknown - Basic Dissection -a=ICMPv6NDOptUnknown(b'\x00\x02') -a.type == 0 and a.len == 2 - -= ICMPv6NDOptUnknown - Dissection with specific values -a=ICMPv6NDOptUnknown(b'\x00\x04somerawing') -a.type == 0 and a.len==4 and a.data == b"so" and isinstance(a.payload, Raw) and a.payload.load == b"merawing" - - -############ -############ -+ ICMPv6NDOptSrcLLAddr Class Test - -= ICMPv6NDOptSrcLLAddr - Basic Instantiation -raw(ICMPv6NDOptSrcLLAddr()) == b'\x01\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptSrcLLAddr - Instantiation with specific values -raw(ICMPv6NDOptSrcLLAddr(len=2, lladdr="11:11:11:11:11:11")) == b'\x01\x02\x11\x11\x11\x11\x11\x11' - -= ICMPv6NDOptSrcLLAddr - Basic Dissection -a=ICMPv6NDOptSrcLLAddr(b'\x01\x01\x00\x00\x00\x00\x00\x00') -a.type == 1 and a.len == 1 and a.lladdr == "00:00:00:00:00:00" - -= ICMPv6NDOptSrcLLAddr - Instantiation with specific values -a=ICMPv6NDOptSrcLLAddr(b'\x01\x02\x11\x11\x11\x11\x11\x11') -a.type == 1 and a.len == 2 and a.lladdr == "11:11:11:11:11:11" - - -############ -############ -+ ICMPv6NDOptDstLLAddr Class Test - -= ICMPv6NDOptDstLLAddr - Basic Instantiation -raw(ICMPv6NDOptDstLLAddr()) == b'\x02\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptDstLLAddr - Instantiation with specific values -raw(ICMPv6NDOptDstLLAddr(len=2, lladdr="11:11:11:11:11:11")) == b'\x02\x02\x11\x11\x11\x11\x11\x11' - -= ICMPv6NDOptDstLLAddr - Basic Dissection -a=ICMPv6NDOptDstLLAddr(b'\x02\x01\x00\x00\x00\x00\x00\x00') -a.type == 2 and a.len == 1 and a.lladdr == "00:00:00:00:00:00" - -= ICMPv6NDOptDstLLAddr - Instantiation with specific values -a=ICMPv6NDOptDstLLAddr(b'\x02\x02\x11\x11\x11\x11\x11\x11') -a.type == 2 and a.len == 2 and a.lladdr == "11:11:11:11:11:11" - - -############ -############ -+ ICMPv6NDOptPrefixInfo Class Test - -= ICMPv6NDOptPrefixInfo - Basic Instantiation -raw(ICMPv6NDOptPrefixInfo()) == b'\x03\x04\x00\xc0\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptPrefixInfo - Instantiation with specific values -raw(ICMPv6NDOptPrefixInfo(len=5, prefixlen=64, L=0, A=0, R=1, res1=1, validlifetime=0x11111111, preferredlifetime=0x22222222, res2=0x33333333, prefix="2001:db8::1")) == b'\x03\x05@!\x11\x11\x11\x11""""3333 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= ICMPv6NDOptPrefixInfo - Basic Dissection -a=ICMPv6NDOptPrefixInfo(b'\x03\x04\x00\xc0\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 3 and a.len == 4 and a.prefixlen == 0 and a.L == 1 and a.A == 1 and a.R == 0 and a.res1 == 0 and a.validlifetime == 0xffffffff and a.preferredlifetime == 0xffffffff and a.res2 == 0 and a.prefix == "::" - -= ICMPv6NDOptPrefixInfo - Instantiation with specific values -a=ICMPv6NDOptPrefixInfo(b'\x03\x05@!\x11\x11\x11\x11""""3333 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.type == 3 and a.len == 5 and a.prefixlen == 64 and a.L == 0 and a.A == 0 and a.R == 1 and a.res1 == 1 and a.validlifetime == 0x11111111 and a.preferredlifetime == 0x22222222 and a.res2 == 0x33333333 and a.prefix == "2001:db8::1" - - -############ -############ -+ ICMPv6NDOptRedirectedHdr Class Test - -= ICMPv6NDOptRedirectedHdr - Basic Instantiation -~ ICMPv6NDOptRedirectedHdr -raw(ICMPv6NDOptRedirectedHdr()) == b'\x04\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptRedirectedHdr - Instantiation with specific values -~ ICMPv6NDOptRedirectedHdr -raw(ICMPv6NDOptRedirectedHdr(len=0xff, res="abcdef", pkt="somestringthatisnotanipv6packet")) == b'\x04\xffabcdefsomestringthatisnotanipv' - -= ICMPv6NDOptRedirectedHdr - Instantiation with simple IPv6 packet (no upper layer) -~ ICMPv6NDOptRedirectedHdr -raw(ICMPv6NDOptRedirectedHdr(pkt=IPv6())) == b'\x04\x06\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= ICMPv6NDOptRedirectedHdr - Basic Dissection -~ ICMPv6NDOptRedirectedHdr -a=ICMPv6NDOptRedirectedHdr(b'\x04\x00\x00\x00') -assert(a.type == 4) -assert(a.len == 0) -assert(a.res == b"\x00\x00") -assert(a.pkt == b"") - -= ICMPv6NDOptRedirectedHdr - Disssection with specific values -~ ICMPv6NDOptRedirectedHdr -a=ICMPv6NDOptRedirectedHdr(b'\x04\xff\x11\x11\x00\x00\x00\x00somerawingthatisnotanipv6pac') -a.type == 4 and a.len == 255 and a.res == b'\x11\x11\x00\x00\x00\x00' and isinstance(a.pkt, Raw) and a.pkt.load == b"somerawingthatisnotanipv6pac" - -= ICMPv6NDOptRedirectedHdr - Dissection with cut IPv6 Header -~ ICMPv6NDOptRedirectedHdr -a=ICMPv6NDOptRedirectedHdr(b'\x04\x06\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 4 and a.len == 6 and a.res == b"\x00\x00\x00\x00\x00\x00" and isinstance(a.pkt, Raw) and a.pkt.load == b'`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptRedirectedHdr - Complete dissection -~ ICMPv6NDOptRedirectedHdr -x=ICMPv6NDOptRedirectedHdr(b'\x04\x06\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x00\x00;@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -y=x.copy() -del(y.len) -x == ICMPv6NDOptRedirectedHdr(raw(y)) - -# Add more tests - - -############ -############ -+ ICMPv6NDOptMTU Class Test - -= ICMPv6NDOptMTU - Basic Instantiation -raw(ICMPv6NDOptMTU()) == b'\x05\x01\x00\x00\x00\x00\x05\x00' - -= ICMPv6NDOptMTU - Instantiation with specific values -raw(ICMPv6NDOptMTU(len=2, res=0x1111, mtu=1500)) == b'\x05\x02\x11\x11\x00\x00\x05\xdc' - -= ICMPv6NDOptMTU - Basic dissection -a=ICMPv6NDOptMTU(b'\x05\x01\x00\x00\x00\x00\x05\x00') -a.type == 5 and a.len == 1 and a.res == 0 and a.mtu == 1280 - -= ICMPv6NDOptMTU - Dissection with specific values -a=ICMPv6NDOptMTU(b'\x05\x02\x11\x11\x00\x00\x05\xdc') -a.type == 5 and a.len == 2 and a.res == 0x1111 and a.mtu == 1500 - -= ICMPv6NDOptMTU - Summary Output -ICMPv6NDOptMTU(b'\x05\x02\x11\x11\x00\x00\x05\xdc').mysummary() == "ICMPv6 Neighbor Discovery Option - MTU 1500" - - -############ -############ -+ ICMPv6NDOptShortcutLimit Class Test (RFC2491) - -= ICMPv6NDOptShortcutLimit - Basic Instantiation -raw(ICMPv6NDOptShortcutLimit()) == b'\x06\x01(\x00\x00\x00\x00\x00' - -= ICMPv6NDOptShortcutLimit - Instantiation with specific values -raw(ICMPv6NDOptShortcutLimit(len=2, shortcutlim=0x11, res1=0xee, res2=0xaaaaaaaa)) == b'\x06\x02\x11\xee\xaa\xaa\xaa\xaa' - -= ICMPv6NDOptShortcutLimit - Basic Dissection -a=ICMPv6NDOptShortcutLimit(b'\x06\x01(\x00\x00\x00\x00\x00') -a.type == 6 and a.len == 1 and a.shortcutlim == 40 and a.res1 == 0 and a.res2 == 0 - -= ICMPv6NDOptShortcutLimit - Dissection with specific values -a=ICMPv6NDOptShortcutLimit(b'\x06\x02\x11\xee\xaa\xaa\xaa\xaa') -a.len==2 and a.shortcutlim==0x11 and a.res1==0xee and a.res2==0xaaaaaaaa - - -############ -############ -+ ICMPv6NDOptAdvInterval Class Test - -= ICMPv6NDOptAdvInterval - Basic Instantiation -raw(ICMPv6NDOptAdvInterval()) == b'\x07\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptAdvInterval - Instantiation with specific values -raw(ICMPv6NDOptAdvInterval(len=2, res=0x1111, advint=0xffffffff)) == b'\x07\x02\x11\x11\xff\xff\xff\xff' - -= ICMPv6NDOptAdvInterval - Basic dissection -a=ICMPv6NDOptAdvInterval(b'\x07\x01\x00\x00\x00\x00\x00\x00') -a.type == 7 and a.len == 1 and a.res == 0 and a.advint == 0 - -= ICMPv6NDOptAdvInterval - Dissection with specific values -a=ICMPv6NDOptAdvInterval(b'\x07\x02\x11\x11\xff\xff\xff\xff') -a.type == 7 and a.len == 2 and a.res == 0x1111 and a.advint == 0xffffffff - - -############ -############ -+ ICMPv6NDOptHAInfo Class Test - -= ICMPv6NDOptHAInfo - Basic Instantiation -raw(ICMPv6NDOptHAInfo()) == b'\x08\x01\x00\x00\x00\x00\x00\x01' - -= ICMPv6NDOptHAInfo - Instantiation with specific values -raw(ICMPv6NDOptHAInfo(len=2, res=0x1111, pref=0x2222, lifetime=0x3333)) == b'\x08\x02\x11\x11""33' - -= ICMPv6NDOptHAInfo - Basic dissection -a=ICMPv6NDOptHAInfo(b'\x08\x01\x00\x00\x00\x00\x00\x01') -a.type == 8 and a.len == 1 and a.res == 0 and a.pref == 0 and a.lifetime == 1 - -= ICMPv6NDOptHAInfo - Dissection with specific values -a=ICMPv6NDOptHAInfo(b'\x08\x02\x11\x11""33') -a.type == 8 and a.len == 2 and a.res == 0x1111 and a.pref == 0x2222 and a.lifetime == 0x3333 - - -############ -############ -+ ICMPv6NDOptSrcAddrList Class Test - -= ICMPv6NDOptSrcAddrList - Basic Instantiation -raw(ICMPv6NDOptSrcAddrList()) == b'\t\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptSrcAddrList - Instantiation with specific values (auto len) -raw(ICMPv6NDOptSrcAddrList(res="BBBBBB", addrlist=["ffff::ffff", "1111::1111"])) == b'\t\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6NDOptSrcAddrList - Instantiation with specific values -raw(ICMPv6NDOptSrcAddrList(len=3, res="BBBBBB", addrlist=["ffff::ffff", "1111::1111"])) == b'\t\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6NDOptSrcAddrList - Basic Dissection -a=ICMPv6NDOptSrcAddrList(b'\t\x01\x00\x00\x00\x00\x00\x00') -a.type == 9 and a.len == 1 and a.res == b'\x00\x00\x00\x00\x00\x00' and not a.addrlist - -= ICMPv6NDOptSrcAddrList - Dissection with specific values (auto len) -a=ICMPv6NDOptSrcAddrList(b'\t\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -a.type == 9 and a.len == 5 and a.res == b'BBBBBB' and len(a.addrlist) == 2 and a.addrlist[0] == "ffff::ffff" and a.addrlist[1] == "1111::1111" - -= ICMPv6NDOptSrcAddrList - Dissection with specific values -conf.debug_dissector = False -a=ICMPv6NDOptSrcAddrList(b'\t\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -conf.debug_dissector = True -a.type == 9 and a.len == 3 and a.res == b'BBBBBB' and len(a.addrlist) == 1 and a.addrlist[0] == "ffff::ffff" and isinstance(a.payload, Raw) and a.payload.load == b'\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - - -############ -############ -+ ICMPv6NDOptTgtAddrList Class Test - -= ICMPv6NDOptTgtAddrList - Basic Instantiation -raw(ICMPv6NDOptTgtAddrList()) == b'\n\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptTgtAddrList - Instantiation with specific values (auto len) -raw(ICMPv6NDOptTgtAddrList(res="BBBBBB", addrlist=["ffff::ffff", "1111::1111"])) == b'\n\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6NDOptTgtAddrList - Instantiation with specific values -raw(ICMPv6NDOptTgtAddrList(len=3, res="BBBBBB", addrlist=["ffff::ffff", "1111::1111"])) == b'\n\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6NDOptTgtAddrList - Basic Dissection -a=ICMPv6NDOptTgtAddrList(b'\n\x01\x00\x00\x00\x00\x00\x00') -a.type == 10 and a.len == 1 and a.res == b'\x00\x00\x00\x00\x00\x00' and not a.addrlist - -= ICMPv6NDOptTgtAddrList - Dissection with specific values (auto len) -a=ICMPv6NDOptTgtAddrList(b'\n\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -a.type == 10 and a.len == 5 and a.res == b'BBBBBB' and len(a.addrlist) == 2 and a.addrlist[0] == "ffff::ffff" and a.addrlist[1] == "1111::1111" - -= ICMPv6NDOptTgtAddrList - Instantiation with specific values -conf.debug_dissector = False -a=ICMPv6NDOptTgtAddrList(b'\n\x03BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -conf.debug_dissector = True -a.type == 10 and a.len == 3 and a.res == b'BBBBBB' and len(a.addrlist) == 1 and a.addrlist[0] == "ffff::ffff" and isinstance(a.payload, Raw) and a.payload.load == b'\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - - -############ -############ -+ ICMPv6NDOptIPAddr Class Test (RFC 4068) - -= ICMPv6NDOptIPAddr - Basic Instantiation -raw(ICMPv6NDOptIPAddr()) == b'\x11\x03\x01@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptIPAddr - Instantiation with specific values -raw(ICMPv6NDOptIPAddr(len=5, optcode=0xff, plen=40, res=0xeeeeeeee, addr="ffff::1111")) == b'\x11\x05\xff(\xee\xee\xee\xee\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6NDOptIPAddr - Basic Dissection -a=ICMPv6NDOptIPAddr(b'\x11\x03\x01@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 17 and a.len == 3 and a.optcode == 1 and a.plen == 64 and a.res == 0 and a.addr == "::" - -= ICMPv6NDOptIPAddr - Dissection with specific values -a=ICMPv6NDOptIPAddr(b'\x11\x05\xff(\xee\xee\xee\xee\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -a.type == 17 and a.len == 5 and a.optcode == 0xff and a.plen == 40 and a.res == 0xeeeeeeee and a.addr == "ffff::1111" - - -############ -############ -+ ICMPv6NDOptNewRtrPrefix Class Test (RFC 4068) - -= ICMPv6NDOptNewRtrPrefix - Basic Instantiation -raw(ICMPv6NDOptNewRtrPrefix()) == b'\x12\x03\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptNewRtrPrefix - Instantiation with specific values -raw(ICMPv6NDOptNewRtrPrefix(len=5, optcode=0xff, plen=40, res=0xeeeeeeee, prefix="ffff::1111")) == b'\x12\x05\xff(\xee\xee\xee\xee\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6NDOptNewRtrPrefix - Basic Dissection -a=ICMPv6NDOptNewRtrPrefix(b'\x12\x03\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 18 and a.len == 3 and a.optcode == 0 and a.plen == 64 and a.res == 0 and a.prefix == "::" - -= ICMPv6NDOptNewRtrPrefix - Dissection with specific values -a=ICMPv6NDOptNewRtrPrefix(b'\x12\x05\xff(\xee\xee\xee\xee\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -a.type == 18 and a.len == 5 and a.optcode == 0xff and a.plen == 40 and a.res == 0xeeeeeeee and a.prefix == "ffff::1111" - - -############ -############ -+ ICMPv6NDOptLLA Class Test (RFC 4068) - -= ICMPv6NDOptLLA - Basic Instantiation -raw(ICMPv6NDOptLLA()) == b'\x13\x01\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptLLA - Instantiation with specific values -raw(ICMPv6NDOptLLA(len=2, optcode=3, lla="ff:11:ff:11:ff:11")) == b'\x13\x02\x03\xff\x11\xff\x11\xff\x11' - -= ICMPv6NDOptLLA - Basic Dissection -a=ICMPv6NDOptLLA(b'\x13\x01\x00\x00\x00\x00\x00\x00\x00') -a.type == 19 and a.len == 1 and a.optcode == 0 and a.lla == "00:00:00:00:00:00" - -= ICMPv6NDOptLLA - Dissection with specific values -a=ICMPv6NDOptLLA(b'\x13\x02\x03\xff\x11\xff\x11\xff\x11') -a.type == 19 and a.len == 2 and a.optcode == 3 and a.lla == "ff:11:ff:11:ff:11" - - -############ -############ -+ ICMPv6NDOptRouteInfo Class Test - -= ICMPv6NDOptRouteInfo - Basic Instantiation -raw(ICMPv6NDOptRouteInfo()) == b'\x18\x01\x00\x00\xff\xff\xff\xff' - -= ICMPv6NDOptRouteInfo - Instantiation with forced prefix but no length -raw(ICMPv6NDOptRouteInfo(prefix="2001:db8:1:1:1:1:1:1")) == b'\x18\x03\x00\x00\xff\xff\xff\xff \x01\r\xb8\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01' - -= ICMPv6NDOptRouteInfo - Instantiation with forced length values (1/4) -raw(ICMPv6NDOptRouteInfo(len=1, prefix="2001:db8:1:1:1:1:1:1")) == b'\x18\x01\x00\x00\xff\xff\xff\xff' - -= ICMPv6NDOptRouteInfo - Instantiation with forced length values (2/4) -raw(ICMPv6NDOptRouteInfo(len=2, prefix="2001:db8:1:1:1:1:1:1")) == b'\x18\x02\x00\x00\xff\xff\xff\xff \x01\r\xb8\x00\x01\x00\x01' - -= ICMPv6NDOptRouteInfo - Instantiation with forced length values (3/4) -raw(ICMPv6NDOptRouteInfo(len=3, prefix="2001:db8:1:1:1:1:1:1")) == b'\x18\x03\x00\x00\xff\xff\xff\xff \x01\r\xb8\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01' - -= ICMPv6NDOptRouteInfo - Instantiation with forced length values (4/4) -raw(ICMPv6NDOptRouteInfo(len=4, prefix="2001:db8:1:1:1:1:1:1")) == b'\x18\x04\x00\x00\xff\xff\xff\xff \x01\r\xb8\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptRouteInfo - Instantiation with specific values -raw(ICMPv6NDOptRouteInfo(len=6, plen=0x11, res1=1, prf=3, res2=1, rtlifetime=0x22222222, prefix="2001:db8::1")) == b'\x18\x06\x119"""" \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptRouteInfo - Basic dissection -a=ICMPv6NDOptRouteInfo(b'\x18\x03\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 24 and a.len == 3 and a.plen == 0 and a.res1 == 0 and a.prf == 0 and a.res2 == 0 and a.rtlifetime == 0xffffffff and a. prefix == "::" - -= ICMPv6NDOptRouteInfo - Dissection with specific values -a=ICMPv6NDOptRouteInfo(b'\x18\x04\x119"""" \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.plen == 0x11 and a.res1 == 1 and a.prf == 3 and a.res2 == 1 and a.rtlifetime == 0x22222222 and a.prefix == "2001:db8::1" - -= ICMPv6NDOptRouteInfo - Summary Output -ICMPv6NDOptRouteInfo(b'\x18\x04\x119"""" \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01').mysummary() == "ICMPv6 Neighbor Discovery Option - Route Information Option 2001:db8::1/17 Preference Low" - - -############ -############ -+ ICMPv6NDOptMAP Class Test - -= ICMPv6NDOptMAP - Basic Instantiation -raw(ICMPv6NDOptMAP()) == b'\x17\x03\x1f\x80\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptMAP - Instantiation with specific values -raw(ICMPv6NDOptMAP(len=5, dist=3, pref=10, R=0, res=1, validlifetime=0x11111111, addr="ffff::1111")) == b'\x17\x05:\x01\x11\x11\x11\x11\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11' - -= ICMPv6NDOptMAP - Basic Dissection -a=ICMPv6NDOptMAP(b'\x17\x03\x1f\x80\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type==23 and a.len==3 and a.dist==1 and a.pref==15 and a.R==1 and a.res==0 and a.validlifetime==0xffffffff and a.addr=="::" - -= ICMPv6NDOptMAP - Dissection with specific values -a=ICMPv6NDOptMAP(b'\x17\x05:\x01\x11\x11\x11\x11\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11') -a.type==23 and a.len==5 and a.dist==3 and a.pref==10 and a.R==0 and a.res==1 and a.validlifetime==0x11111111 and a.addr=="ffff::1111" - - -############ -############ -+ ICMPv6NDOptRDNSS Class Test - -= ICMPv6NDOptRDNSS - Basic Instantiation -raw(ICMPv6NDOptRDNSS()) == b'\x19\x01\x00\x00\xff\xff\xff\xff' - -= ICMPv6NDOptRDNSS - Basic instantiation with 1 DNS address -raw(ICMPv6NDOptRDNSS(dns=["2001:db8::1"])) == b'\x19\x03\x00\x00\xff\xff\xff\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= ICMPv6NDOptRDNSS - Basic instantiation with 2 DNS addresses -raw(ICMPv6NDOptRDNSS(dns=["2001:db8::1", "2001:db8::2"])) == b'\x19\x05\x00\x00\xff\xff\xff\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= ICMPv6NDOptRDNSS - Instantiation with specific values -raw(ICMPv6NDOptRDNSS(len=43, res=0xaaee, lifetime=0x11111111, dns=["2001:db8::2"])) == b'\x19+\xaa\xee\x11\x11\x11\x11 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= ICMPv6NDOptRDNSS - Basic Dissection -a=ICMPv6NDOptRDNSS(b'\x19\x01\x00\x00\xff\xff\xff\xff') -a.type==25 and a.len==1 and a.res == 0 and a.dns==[] - -= ICMPv6NDOptRDNSS - Dissection (with 1 DNS address) -a=ICMPv6NDOptRDNSS(b'\x19\x03\x00\x00\xff\xff\xff\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.type==25 and a.len==3 and a.res ==0 and len(a.dns) == 1 and a.dns[0] == "2001:db8::1" - -= ICMPv6NDOptRDNSS - Dissection (with 2 DNS addresses) -a=ICMPv6NDOptRDNSS(b'\x19\x05\xaa\xee\xff\xff\xff\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.type==25 and a.len==5 and a.res == 0xaaee and len(a.dns) == 2 and a.dns[0] == "2001:db8::1" and a.dns[1] == "2001:db8::2" - -= ICMPv6NDOptRDNSS - Summary Output -a=ICMPv6NDOptRDNSS(b'\x19\x05\xaa\xee\xff\xff\xff\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.mysummary() == "ICMPv6 Neighbor Discovery Option - Recursive DNS Server Option 2001:db8::1, 2001:db8::2" - - -############ -############ -+ ICMPv6NDOptDNSSL Class Test - -= ICMPv6NDOptDNSSL - Basic Instantiation -raw(ICMPv6NDOptDNSSL()) == b'\x1f\x01\x00\x00\xff\xff\xff\xff' - -= ICMPv6NDOptDNSSL - Instantiation with 1 search domain, as seen in the wild -raw(ICMPv6NDOptDNSSL(lifetime=60, searchlist=["home."])) == b'\x1f\x02\x00\x00\x00\x00\x00<\x04home\x00\x00\x00' - -= ICMPv6NDOptDNSSL - Basic instantiation with 2 search domains -raw(ICMPv6NDOptDNSSL(searchlist=["home.", "office."])) == b'\x1f\x03\x00\x00\xff\xff\xff\xff\x04home\x00\x06office\x00\x00\x00' - -= ICMPv6NDOptDNSSL - Basic instantiation with 3 search domains -raw(ICMPv6NDOptDNSSL(searchlist=["home.", "office.", "here.there."])) == b'\x1f\x05\x00\x00\xff\xff\xff\xff\x04home\x00\x06office\x00\x04here\x05there\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptDNSSL - Basic Dissection -p = ICMPv6NDOptDNSSL(b'\x1f\x01\x00\x00\xff\xff\xff\xff') -p.type == 31 and p.len == 1 and p.res == 0 and p.searchlist == [] - -= ICMPv6NDOptDNSSL - Basic Dissection with specific values -p = ICMPv6NDOptDNSSL(b'\x1f\x02\x00\x00\x00\x00\x00<\x04home\x00\x00\x00') -p.type == 31 and p.len == 2 and p.res == 0 and p.lifetime == 60 and p.searchlist == ["home."] - -= ICMPv6NDOptDNSSL - Summary Output -ICMPv6NDOptDNSSL(searchlist=["home.", "office."]).mysummary() == "ICMPv6 Neighbor Discovery Option - DNS Search List Option home., office." - - -############ -############ -+ ICMPv6NDOptEFA Class Test - -= ICMPv6NDOptEFA - Basic Instantiation -raw(ICMPv6NDOptEFA()) == b'\x1a\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NDOptEFA - Basic Dissection -a=ICMPv6NDOptEFA(b'\x1a\x01\x00\x00\x00\x00\x00\x00') -a.type==26 and a.len==1 and a.res == 0 - - -############ -############ -+ Test Node Information Query - ICMPv6NIQueryNOOP - -= ICMPv6NIQueryNOOP - Basic Instantiation -raw(ICMPv6NIQueryNOOP(nonce=b"\x00"*8)) == b'\x8b\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NIQueryNOOP - Basic Dissection -a = ICMPv6NIQueryNOOP(b'\x8b\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 139 and a.code == 1 and a.cksum == 0 and a.qtype == 0 and a.unused == 0 and a.flags == 0 and a.nonce == b"\x00"*8 and a.data == b"" - - -############ -############ -+ Test Node Information Query - ICMPv6NIQueryName - -= ICMPv6NIQueryName - single label DNS name (internal) -a=ICMPv6NIQueryName(data="abricot").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 1 and a[1] == b'\x07abricot\x00\x00' - -= ICMPv6NIQueryName - single label DNS name -ICMPv6NIQueryName(data="abricot").data == b"abricot" - -= ICMPv6NIQueryName - fqdn (internal) -a=ICMPv6NIQueryName(data="n.d.org").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 1 and a[1] == b'\x01n\x01d\x03org\x00' - -= ICMPv6NIQueryName - fqdn -ICMPv6NIQueryName(data="n.d.org").data == b"n.d.org" - -= ICMPv6NIQueryName - IPv6 address (internal) -a=ICMPv6NIQueryName(data="2001:db8::1").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 0 and a[1] == '2001:db8::1' - -= ICMPv6NIQueryName - IPv6 address -ICMPv6NIQueryName(data="2001:db8::1").data == "2001:db8::1" - -= ICMPv6NIQueryName - IPv4 address (internal) -a=ICMPv6NIQueryName(data="169.254.253.252").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 2 and a[1] == '169.254.253.252' - -= ICMPv6NIQueryName - IPv4 address -ICMPv6NIQueryName(data="169.254.253.252").data == '169.254.253.252' - -= ICMPv6NIQueryName - build & dissection -s = raw(IPv6()/ICMPv6NIQueryName(data="n.d.org")) -p = IPv6(s) -ICMPv6NIQueryName in p and p[ICMPv6NIQueryName].data == b"n.d.org" - -= ICMPv6NIQueryName - dissection -s = b'\x8b\x00z^\x00\x02\x00\x00\x00\x03g\x90\xc7\xa3\xdd[\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' -p = ICMPv6NIQueryName(s) -p.show() -assert ICMPv6NIQueryName in p and p.data == "ff02::1" - - -############ -############ -+ Test Node Information Query - ICMPv6NIQueryIPv6 - -= ICMPv6NIQueryIPv6 - single label DNS name (internal) -a = ICMPv6NIQueryIPv6(data="abricot") -ls(a) -a = a.getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 1 and a[1] == b'\x07abricot\x00\x00' - -= ICMPv6NIQueryIPv6 - single label DNS name -ICMPv6NIQueryIPv6(data="abricot").data == b"abricot" - -= ICMPv6NIQueryIPv6 - fqdn (internal) -a=ICMPv6NIQueryIPv6(data="n.d.org").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 1 and a[1] == b'\x01n\x01d\x03org\x00' - -= ICMPv6NIQueryIPv6 - fqdn -ICMPv6NIQueryIPv6(data="n.d.org").data == b"n.d.org" - -= ICMPv6NIQueryIPv6 - IPv6 address (internal) -a=ICMPv6NIQueryIPv6(data="2001:db8::1").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 0 and a[1] == '2001:db8::1' - -= ICMPv6NIQueryIPv6 - IPv6 address -ICMPv6NIQueryIPv6(data="2001:db8::1").data == "2001:db8::1" - -= ICMPv6NIQueryIPv6 - IPv4 address (internal) -a=ICMPv6NIQueryIPv6(data="169.254.253.252").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 2 and a[1] == '169.254.253.252' - -= ICMPv6NIQueryIPv6 - IPv4 address -ICMPv6NIQueryIPv6(data="169.254.253.252").data == '169.254.253.252' - - -############ -############ -+ Test Node Information Query - ICMPv6NIQueryIPv4 - -= ICMPv6NIQueryIPv4 - single label DNS name (internal) -a=ICMPv6NIQueryIPv4(data="abricot").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 1 and a[1] == b'\x07abricot\x00\x00' - -= ICMPv6NIQueryIPv4 - single label DNS name -ICMPv6NIQueryIPv4(data="abricot").data == b"abricot" - -= ICMPv6NIQueryIPv4 - fqdn (internal) -a=ICMPv6NIQueryIPv4(data="n.d.org").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 1 and a[1] == b'\x01n\x01d\x03org\x00' - -= ICMPv6NIQueryIPv4 - fqdn -ICMPv6NIQueryIPv4(data="n.d.org").data == b"n.d.org" - -= ICMPv6NIQueryIPv4 - IPv6 address (internal) -a=ICMPv6NIQueryIPv4(data="2001:db8::1").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 0 and a[1] == '2001:db8::1' - -= ICMPv6NIQueryIPv4 - IPv6 address -ICMPv6NIQueryIPv4(data="2001:db8::1").data == "2001:db8::1" - -= ICMPv6NIQueryIPv4 - IPv4 address (internal) -a=ICMPv6NIQueryIPv4(data="169.254.253.252").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 2 and a[1] == '169.254.253.252' - -= ICMPv6NIQueryIPv4 - IPv4 address -ICMPv6NIQueryIPv4(data="169.254.253.252").data == '169.254.253.252' - -= ICMPv6NIQueryIPv4 - dissection -s = b'\x8b\x01\x00\x00\x00\x04\x00\x00\xc2\xb9\xc2\x96\xc3\xa1.H\x07freebsd\x00\x00' -p = ICMPv6NIQueryIPv4(s) -p.show() -assert ICMPv6NIQueryIPv4 in p and p.data == b"freebsd" - -= ICMPv6NIQueryIPv4 - hashret() - -random.seed(0x2807) -p = IPv6(src="::", dst="::")/ICMPv6NIQueryIPv4(data="freebsd") -h = p.hashret() -h -assert h in [ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00:g\x02f1\xbd?\xb3\xc4', - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00:\x88\xccb\x19~\x9e\xe3a', - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00:$#\xb5\xb7\xd0\xbf \xe2' -] - - -############ -############ -+ Test Node Information Query - Flags tests - -= ICMPv6NIQuery* - flags handling (Test 1) -t = ICMPv6NIQueryIPv6(flags="T") -a = ICMPv6NIQueryIPv6(flags="A") -c = ICMPv6NIQueryIPv6(flags="C") -l = ICMPv6NIQueryIPv6(flags="L") -s = ICMPv6NIQueryIPv6(flags="S") -g = ICMPv6NIQueryIPv6(flags="G") -allflags = ICMPv6NIQueryIPv6(flags="TALCLSG") -t.flags == 1 and a.flags == 2 and c.flags == 4 and l.flags == 8 and s.flags == 16 and g.flags == 32 and allflags.flags == 63 - - -= ICMPv6NIQuery* - flags handling (Test 2) -t = raw(ICMPv6NIQueryNOOP(flags="T", nonce="A"*8))[6:8] -a = raw(ICMPv6NIQueryNOOP(flags="A", nonce="A"*8))[6:8] -c = raw(ICMPv6NIQueryNOOP(flags="C", nonce="A"*8))[6:8] -l = raw(ICMPv6NIQueryNOOP(flags="L", nonce="A"*8))[6:8] -s = raw(ICMPv6NIQueryNOOP(flags="S", nonce="A"*8))[6:8] -g = raw(ICMPv6NIQueryNOOP(flags="G", nonce="A"*8))[6:8] -allflags = raw(ICMPv6NIQueryNOOP(flags="TALCLSG", nonce="A"*8))[6:8] -t == b'\x00\x01' and a == b'\x00\x02' and c == b'\x00\x04' and l == b'\x00\x08' and s == b'\x00\x10' and g == b'\x00\x20' and allflags == b'\x00\x3F' - - -= ICMPv6NIReply* - flags handling (Test 1) -t = ICMPv6NIReplyIPv6(flags="T") -a = ICMPv6NIReplyIPv6(flags="A") -c = ICMPv6NIReplyIPv6(flags="C") -l = ICMPv6NIReplyIPv6(flags="L") -s = ICMPv6NIReplyIPv6(flags="S") -g = ICMPv6NIReplyIPv6(flags="G") -allflags = ICMPv6NIReplyIPv6(flags="TALCLSG") -t.flags == 1 and a.flags == 2 and c.flags == 4 and l.flags == 8 and s.flags == 16 and g.flags == 32 and allflags.flags == 63 - - -= ICMPv6NIReply* - flags handling (Test 2) -t = raw(ICMPv6NIReplyNOOP(flags="T", nonce="A"*8))[6:8] -a = raw(ICMPv6NIReplyNOOP(flags="A", nonce="A"*8))[6:8] -c = raw(ICMPv6NIReplyNOOP(flags="C", nonce="A"*8))[6:8] -l = raw(ICMPv6NIReplyNOOP(flags="L", nonce="A"*8))[6:8] -s = raw(ICMPv6NIReplyNOOP(flags="S", nonce="A"*8))[6:8] -g = raw(ICMPv6NIReplyNOOP(flags="G", nonce="A"*8))[6:8] -allflags = raw(ICMPv6NIReplyNOOP(flags="TALCLSG", nonce="A"*8))[6:8] -t == b'\x00\x01' and a == b'\x00\x02' and c == b'\x00\x04' and l == b'\x00\x08' and s == b'\x00\x10' and g == b'\x00\x20' and allflags == b'\x00\x3F' - - -= ICMPv6NIQuery* - Flags Default values -a = ICMPv6NIQueryNOOP() -b = ICMPv6NIQueryName() -c = ICMPv6NIQueryIPv4() -d = ICMPv6NIQueryIPv6() -a.flags == 0 and b.flags == 0 and c.flags == 0 and d.flags == 62 - -= ICMPv6NIReply* - Flags Default values -a = ICMPv6NIReplyIPv6() -b = ICMPv6NIReplyName() -c = ICMPv6NIReplyIPv6() -d = ICMPv6NIReplyIPv4() -e = ICMPv6NIReplyRefuse() -f = ICMPv6NIReplyUnknown() -a.flags == 0 and b.flags == 0 and c.flags == 0 and d.flags == 0 and e.flags == 0 and f.flags == 0 - - - -# Nonces -# hashret and answers -# payload guess -# automatic destination address computation when integrated in scapy6 -# at least computeNIGroupAddr - - -############ -############ -+ Test Node Information Query - Dispatching - -= ICMPv6NIQueryIPv6 - dispatch with nothing in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv6()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv6) - -= ICMPv6NIQueryIPv6 - dispatch with IPv6 address in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv6(data="2001::db8::1")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv6) - -= ICMPv6NIQueryIPv6 - dispatch with IPv4 address in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv6(data="192.168.0.1")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv6) - -= ICMPv6NIQueryIPv6 - dispatch with name in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv6(data="alfred")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv6) - -= ICMPv6NIQueryName - dispatch with nothing in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryName()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryName) - -= ICMPv6NIQueryName - dispatch with IPv6 address in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryName(data="2001:db8::1")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryName) - -= ICMPv6NIQueryName - dispatch with IPv4 address in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryName(data="192.168.0.1")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryName) - -= ICMPv6NIQueryName - dispatch with name in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryName(data="alfred")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryName) - -= ICMPv6NIQueryIPv4 - dispatch with nothing in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv4()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv4) - -= ICMPv6NIQueryIPv4 - dispatch with IPv6 address in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv4(data="2001:db8::1")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv4) - -= ICMPv6NIQueryIPv4 - dispatch with IPv6 address in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv4(data="192.168.0.1")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv4) - -= ICMPv6NIQueryIPv4 - dispatch with name in data -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIQueryIPv4(data="alfred")) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIQueryIPv4) - -= ICMPv6NIReplyName - dispatch -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIReplyName()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIReplyName) - -= ICMPv6NIReplyIPv6 - dispatch -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIReplyIPv6()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIReplyIPv6) - -= ICMPv6NIReplyIPv4 - dispatch -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIReplyIPv4()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIReplyIPv4) - -= ICMPv6NIReplyRefuse - dispatch -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIReplyRefuse()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIReplyRefuse) - -= ICMPv6NIReplyUnknown - dispatch -s = raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/ICMPv6NIReplyUnknown()) -p = IPv6(s) -isinstance(p.payload, ICMPv6NIReplyUnknown) - - -############ -############ -+ Test Node Information Query - ICMPv6NIReplyNOOP - -= ICMPv6NIReplyNOOP - single DNS name without hint => understood as string (internal) -a=ICMPv6NIReplyNOOP(data="abricot").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 0 and a[1] == b"abricot" - -= ICMPv6NIReplyNOOP - single DNS name without hint => understood as string -ICMPv6NIReplyNOOP(data="abricot").data == b"abricot" - -= ICMPv6NIReplyNOOP - fqdn without hint => understood as string (internal) -a=ICMPv6NIReplyNOOP(data="n.d.tld").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 0 and a[1] == b"n.d.tld" - -= ICMPv6NIReplyNOOP - fqdn without hint => understood as string -ICMPv6NIReplyNOOP(data="n.d.tld").data == b"n.d.tld" - -= ICMPv6NIReplyNOOP - IPv6 address without hint => understood as string (internal) -a=ICMPv6NIReplyNOOP(data="2001:0db8::1").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 0 and a[1] == b"2001:0db8::1" - -= ICMPv6NIReplyNOOP - IPv6 address without hint => understood as string -ICMPv6NIReplyNOOP(data="2001:0db8::1").data == b"2001:0db8::1" - -= ICMPv6NIReplyNOOP - IPv4 address without hint => understood as string (internal) -a=ICMPv6NIReplyNOOP(data="169.254.253.010").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 0 and a[1] == b"169.254.253.010" - -= ICMPv6NIReplyNOOP - IPv4 address without hint => understood as string -ICMPv6NIReplyNOOP(data="169.254.253.010").data == b"169.254.253.010" - - -############ -############ -+ Test Node Information Query - ICMPv6NIReplyName - -= ICMPv6NIReplyName - single label DNS name as a rawing (without ttl) (internal) -a=ICMPv6NIReplyName(data="abricot").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 2 and type(a[1]) is list and len(a[1]) == 2 and a[1][0] == 0 and a[1][1] == b'\x07abricot\x00\x00' - -= ICMPv6NIReplyName - single label DNS name as a rawing (without ttl) -ICMPv6NIReplyName(data="abricot").data == [0, b"abricot"] - -= ICMPv6NIReplyName - fqdn name as a rawing (without ttl) (internal) -a=ICMPv6NIReplyName(data="n.d.tld").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 2 and type(a[1]) is list and len(a[1]) == 2 and a[1][0] == 0 and a[1][1] == b'\x01n\x01d\x03tld\x00' - -= ICMPv6NIReplyName - fqdn name as a rawing (without ttl) -ICMPv6NIReplyName(data="n.d.tld").data == [0, b'n.d.tld'] - -= ICMPv6NIReplyName - list of 2 single label DNS names (without ttl) (internal) -a=ICMPv6NIReplyName(data=["abricot", "poire"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 2 and type(a[1]) is list and len(a[1]) == 2 and a[1][0] == 0 and a[1][1] == b'\x07abricot\x00\x00\x05poire\x00\x00' - -= ICMPv6NIReplyName - list of 2 single label DNS names (without ttl) -ICMPv6NIReplyName(data=["abricot", "poire"]).data == [0, b"abricot", b"poire"] - -= ICMPv6NIReplyName - [ttl, single-label, single-label, fqdn] (internal) -a=ICMPv6NIReplyName(data=[42, "abricot", "poire", "n.d.tld"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 2 and type(a[1]) is list and len(a[1]) == 2 and a[1][0] == 42 and a[1][1] == b'\x07abricot\x00\x00\x05poire\x00\x00\x01n\x01d\x03tld\x00' - -= ICMPv6NIReplyName - [ttl, single-label, single-label, fqdn] -ICMPv6NIReplyName(data=[42, "abricot", "poire", "n.d.tld"]).data == [42, b"abricot", b"poire", b"n.d.tld"] - -= ICMPv6NIReplyName - dissection - -s = b'\x8c\x00\xd1\x0f\x00\x02\x00\x00\x00\x00\xd9$\x94\x8d\xc6%\x00\x00\x00\x00\x07freebsd\x00\x00' -p = ICMPv6NIReplyName(s) -p.show() -assert ICMPv6NIReplyName in p and p.data == [0, b'freebsd'] - - -############ -############ -+ Test Node Information Query - ICMPv6NIReplyIPv6 - -= ICMPv6NIReplyIPv6 - one IPv6 address without TTL (internal) -a=ICMPv6NIReplyIPv6(data="2001:db8::1").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 3 and type(a[1]) is list and len(a[1]) == 1 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "2001:db8::1" - -= ICMPv6NIReplyIPv6 - one IPv6 address without TTL -ICMPv6NIReplyIPv6(data="2001:db8::1").data == [(0, '2001:db8::1')] - -= ICMPv6NIReplyIPv6 - one IPv6 address without TTL (as a list) (internal) -a=ICMPv6NIReplyIPv6(data=["2001:db8::1"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 3 and type(a[1]) is list and len(a[1]) == 1 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "2001:db8::1" - -= ICMPv6NIReplyIPv6 - one IPv6 address without TTL (as a list) -ICMPv6NIReplyIPv6(data=["2001:db8::1"]).data == [(0, '2001:db8::1')] - -= ICMPv6NIReplyIPv6 - one IPv6 address with TTL (internal) -a=ICMPv6NIReplyIPv6(data=[(0, "2001:db8::1")]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 3 and type(a[1]) is list and len(a[1]) == 1 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "2001:db8::1" - -= ICMPv6NIReplyIPv6 - one IPv6 address with TTL -ICMPv6NIReplyIPv6(data=[(0, "2001:db8::1")]).data == [(0, '2001:db8::1')] - -= ICMPv6NIReplyIPv6 - two IPv6 addresses as a list of rawings (without TTL) (internal) -a=ICMPv6NIReplyIPv6(data=["2001:db8::1", "2001:db8::2"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 3 and type(a[1]) is list and len(a[1]) == 2 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "2001:db8::1" and len(a[1][1]) == 2 and a[1][1][0] == 0 and a[1][1][1] == "2001:db8::2" - -= ICMPv6NIReplyIPv6 - two IPv6 addresses as a list of rawings (without TTL) -ICMPv6NIReplyIPv6(data=["2001:db8::1", "2001:db8::2"]).data == [(0, '2001:db8::1'), (0, '2001:db8::2')] - -= ICMPv6NIReplyIPv6 - two IPv6 addresses as a list (first with ttl, second without) (internal) -a=ICMPv6NIReplyIPv6(data=[(42, "2001:db8::1"), "2001:db8::2"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 3 and type(a[1]) is list and len(a[1]) == 2 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 42 and a[1][0][1] == "2001:db8::1" and len(a[1][1]) == 2 and a[1][1][0] == 0 and a[1][1][1] == "2001:db8::2" - -= ICMPv6NIReplyIPv6 - two IPv6 addresses as a list (first with ttl, second without) -ICMPv6NIReplyIPv6(data=[(42, "2001:db8::1"), "2001:db8::2"]).data == [(42, "2001:db8::1"), (0, "2001:db8::2")] - -= ICMPv6NIReplyIPv6 - build & dissection - -s = raw(IPv6()/ICMPv6NIReplyIPv6(data="2001:db8::1")) -p = IPv6(s) -ICMPv6NIReplyIPv6 in p and p.data == [(0, '2001:db8::1')] - -############ -############ -+ Test Node Information Query - ICMPv6NIReplyIPv4 - -= ICMPv6NIReplyIPv4 - one IPv4 address without TTL (internal) -a=ICMPv6NIReplyIPv4(data="169.254.253.252").getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 4 and type(a[1]) is list and len(a[1]) == 1 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "169.254.253.252" - -= ICMPv6NIReplyIPv4 - one IPv4 address without TTL -ICMPv6NIReplyIPv4(data="169.254.253.252").data == [(0, '169.254.253.252')] - -= ICMPv6NIReplyIPv4 - one IPv4 address without TTL (as a list) (internal) -a=ICMPv6NIReplyIPv4(data=["169.254.253.252"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 4 and type(a[1]) is list and len(a[1]) == 1 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "169.254.253.252" - -= ICMPv6NIReplyIPv4 - one IPv4 address without TTL (as a list) -ICMPv6NIReplyIPv4(data=["169.254.253.252"]).data == [(0, '169.254.253.252')] - -= ICMPv6NIReplyIPv4 - one IPv4 address with TTL (internal) -a=ICMPv6NIReplyIPv4(data=[(0, "169.254.253.252")]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 4 and type(a[1]) is list and len(a[1]) == 1 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "169.254.253.252" - -= ICMPv6NIReplyIPv4 - one IPv4 address with TTL (internal) -ICMPv6NIReplyIPv4(data=[(0, "169.254.253.252")]).data == [(0, '169.254.253.252')] - -= ICMPv6NIReplyIPv4 - two IPv4 addresses as a list of rawings (without TTL) -a=ICMPv6NIReplyIPv4(data=["169.254.253.252", "169.254.253.253"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 4 and type(a[1]) is list and len(a[1]) == 2 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 0 and a[1][0][1] == "169.254.253.252" and len(a[1][1]) == 2 and a[1][1][0] == 0 and a[1][1][1] == "169.254.253.253" - -= ICMPv6NIReplyIPv4 - two IPv4 addresses as a list of rawings (without TTL) (internal) -ICMPv6NIReplyIPv4(data=["169.254.253.252", "169.254.253.253"]).data == [(0, '169.254.253.252'), (0, '169.254.253.253')] - -= ICMPv6NIReplyIPv4 - two IPv4 addresses as a list (first with ttl, second without) -a=ICMPv6NIReplyIPv4(data=[(42, "169.254.253.252"), "169.254.253.253"]).getfieldval("data") -type(a) is tuple and len(a) == 2 and a[0] == 4 and type(a[1]) is list and len(a[1]) == 2 and type(a[1][0]) is tuple and len(a[1][0]) == 2 and a[1][0][0] == 42 and a[1][0][1] == "169.254.253.252" and len(a[1][1]) == 2 and a[1][1][0] == 0 and a[1][1][1] == "169.254.253.253" - -= ICMPv6NIReplyIPv4 - two IPv4 addresses as a list (first with ttl, second without) (internal) -ICMPv6NIReplyIPv4(data=[(42, "169.254.253.252"), "169.254.253.253"]).data == [(42, "169.254.253.252"), (0, "169.254.253.253")] - -= ICMPv6NIReplyIPv4 - build & dissection - -s = raw(IPv6()/ICMPv6NIReplyIPv4(data="192.168.0.1")) -p = IPv6(s) -ICMPv6NIReplyIPv4 in p and p.data == [(0, '192.168.0.1')] - -s = raw(IPv6()/ICMPv6NIReplyIPv4(data=[(2807, "192.168.0.1")])) -p = IPv6(s) -ICMPv6NIReplyIPv4 in p and p.data == [(2807, "192.168.0.1")] - - -############ -############ -+ Test Node Information Query - ICMPv6NIReplyRefuse -= ICMPv6NIReplyRefuse - basic instantiation -raw(ICMPv6NIReplyRefuse())[:8] == b'\x8c\x01\x00\x00\x00\x00\x00\x00' - -= ICMPv6NIReplyRefuse - basic dissection -a=ICMPv6NIReplyRefuse(b'\x8c\x01\x00\x00\x00\x00\x00\x00\xf1\xe9\xab\xc9\x8c\x0by\x18') -a.type == 140 and a.code == 1 and a.cksum == 0 and a.unused == 0 and a.flags == 0 and a.nonce == b'\xf1\xe9\xab\xc9\x8c\x0by\x18' and a.data == None - - -############ -############ -+ Test Node Information Query - ICMPv6NIReplyUnknown - -= ICMPv6NIReplyUnknown - basic instantiation -raw(ICMPv6NIReplyUnknown(nonce=b'\x00'*8)) == b'\x8c\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= ICMPv6NIReplyRefuse - basic dissection -a=ICMPv6NIReplyRefuse(b'\x8c\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.type == 140 and a.code == 2 and a.cksum == 0 and a.unused == 0 and a.flags == 0 and a.nonce == b'\x00'*8 and a.data == None - - -############ -############ -+ Test Node Information Query - utilities - -= computeNIGroupAddr -computeNIGroupAddr("scapy") == "ff02::2:f886:2f66" - - -############ -############ -+ IPv6ExtHdrFragment Class Test - -= IPv6ExtHdrFragment - Basic Instantiation -raw(IPv6ExtHdrFragment()) == b';\x00\x00\x00\x00\x00\x00\x00' - -= IPv6ExtHdrFragment - Instantiation with specific values -raw(IPv6ExtHdrFragment(nh=0xff, res1=0xee, offset=0x1fff, res2=1, m=1, id=0x11111111)) == b'\xff\xee\xff\xfb\x11\x11\x11\x11' - -= IPv6ExtHdrFragment - Basic Dissection -a=IPv6ExtHdrFragment(b';\x00\x00\x00\x00\x00\x00\x00') -a.nh == 59 and a.res1 == 0 and a.offset == 0 and a.res2 == 0 and a.m == 0 and a.id == 0 - -= IPv6ExtHdrFragment - Instantiation with specific values -a=IPv6ExtHdrFragment(b'\xff\xee\xff\xfb\x11\x11\x11\x11') -a.nh == 0xff and a.res1 == 0xee and a.offset==0x1fff and a.res2==1 and a.m == 1 and a.id == 0x11111111 - - -############ -############ -+ Test fragment6 function - -= fragment6 - test against a long TCP packet with a 1280 MTU -l=fragment6(IPv6()/IPv6ExtHdrFragment()/TCP()/Raw(load="A"*40000), 1280) -len(l) == 33 and len(raw(l[-1])) == 644 - - -############ -############ -+ Test defragment6 function - -= defragment6 - test against a long TCP packet fragmented with a 1280 MTU -l=fragment6(IPv6()/IPv6ExtHdrFragment()/TCP()/Raw(load="A"*40000), 1280) -raw(defragment6(l)) == (b'`\x00\x00\x00\x9cT\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xe92\x00\x00' + b'A'*40000) - - -= defragment6 - test against a large TCP packet fragmented with a 1280 bytes MTU and missing fragments -l=fragment6(IPv6()/IPv6ExtHdrFragment()/TCP()/Raw(load="A"*40000), 1280) -del(l[2]) -del(l[4]) -del(l[12]) -del(l[18]) -raw(defragment6(l)) == (b'`\x00\x00\x00\x9cT\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xe92\x00\x00' + 2444*b'A' + 1232*b'X' + 2464*b'A' + 1232*b'X' + 9856*b'A' + 1232*b'X' + 7392*b'A' + 1232*b'X' + 12916*b'A') - - -= defragment6 - test against a TCP packet fragmented with a 800 bytes MTU and missing fragments -l=fragment6(IPv6()/IPv6ExtHdrFragment()/TCP()/Raw(load="A"*4000), 800) -del(l[4]) -del(l[2]) -raw(defragment6(l)) == b'`\x00\x00\x00\x0f\xb4\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xb2\x0f\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - -= defragment6 - test the packet length -pkts = fragment6(IPv6()/IPv6ExtHdrFragment()/UDP(dport=42, sport=42)/Raw(load="A"*1500), 1280) -pkts = [IPv6(raw(p)) for p in pkts] -assert defragment6(pkts).plen == 1508 - - -############ -############ -+ Test Route6 class - -= Route6 - Route6 flushing -conf_iface = conf.iface -conf.iface = "eth0" -conf.route6.routes=[ -( '::1', 128, '::', 'lo', ['::1'], 1), -( 'fe80::20f:1fff:feca:4650', 128, '::', 'lo', ['::1'], 1)] -conf.route6.flush() -not conf.route6.routes - -= Route6 - Route6.route -conf.route6.flush() -conf.route6.ipv6_ifaces = set(['lo', 'eth0']) -conf.route6.routes=[ -( '::1', 128, '::', 'lo', ['::1'], 1), -( 'fe80::20f:1fff:feca:4650', 128, '::', 'lo', ['::1'], 1), -( 'fe80::', 64, '::', 'eth0', ['fe80::20f:1fff:feca:4650'], 1), -('2001:db8:0:4444:20f:1fff:feca:4650', 128, '::', 'lo', ['::1'], 1), -( '2001:db8:0:4444::', 64, '::', 'eth0', ['2001:db8:0:4444:20f:1fff:feca:4650'], 1), -( '::', 0, 'fe80::20f:34ff:fe8a:8aa1', 'eth0', ['2001:db8:0:4444:20f:1fff:feca:4650', '2002:db8:0:4444:20f:1fff:feca:4650'], 1) -] -conf.route6.route("2002::1") == ('eth0', '2002:db8:0:4444:20f:1fff:feca:4650', 'fe80::20f:34ff:fe8a:8aa1') and conf.route6.route("2001::1") == ('eth0', '2001:db8:0:4444:20f:1fff:feca:4650', 'fe80::20f:34ff:fe8a:8aa1') and conf.route6.route("fe80::20f:1fff:feab:4870") == ('eth0', 'fe80::20f:1fff:feca:4650', '::') and conf.route6.route("::1") == ('lo', '::1', '::') and conf.route6.route("::") == ('eth0', '2001:db8:0:4444:20f:1fff:feca:4650', 'fe80::20f:34ff:fe8a:8aa1') and conf.route6.route('ff00::') == ('eth0', '2001:db8:0:4444:20f:1fff:feca:4650', 'fe80::20f:34ff:fe8a:8aa1') -conf.iface = conf_iface -conf.route6.resync() -if not len(conf.route6.routes): - # IPv6 seems disabled. Force a route to ::1 - conf.route6.routes.append(("::1", 128, "::", LOOPBACK_NAME, ["::1"], 1)) - True - -= Route6 - Route6.make_route - -r6 = Route6() -r6.make_route("2001:db8::1", dev=LOOPBACK_NAME) == ("2001:db8::1", 128, "::", LOOPBACK_NAME, [], 1) -len_r6 = len(r6.routes) - -= Route6 - Route6.add & Route6.delt - -r6.add(dst="2001:db8:cafe:f000::/64", gw="2001:db8:cafe::1", dev="eth0") -assert(len(r6.routes) == len_r6 + 1) -r6.delt(dst="2001:db8:cafe:f000::/64", gw="2001:db8:cafe::1") -assert(len(r6.routes) == len_r6) - -= Route6 - Route6.ifadd & Route6.ifdel -r6.ifadd("scapy0", "2001:bd8:cafe:1::1/64") -r6.ifdel("scapy0") - -= IPv6 - utils - -@mock.patch("scapy.layers.inet6.get_if_hwaddr") -@mock.patch("scapy.layers.inet6.srp1") -def test_neighsol(mock_srp1, mock_get_if_hwaddr): - mock_srp1.return_value = Ether()/IPv6()/ICMPv6ND_NA()/ICMPv6NDOptDstLLAddr(lladdr="05:04:03:02:01:00") - mock_get_if_hwaddr.return_value = "00:01:02:03:04:05" - return neighsol("fe80::f6ce:46ff:fea9:e04b", "fe80::f6ce:46ff:fea9:e04b", "scapy0") - -p = test_neighsol() -ICMPv6NDOptDstLLAddr in p and p[ICMPv6NDOptDstLLAddr].lladdr == "05:04:03:02:01:00" - - -@mock.patch("scapy.layers.inet6.neighsol") -@mock.patch("scapy.layers.inet6.conf.route6.route") -def test_getmacbyip6(mock_route6, mock_neighsol): - mock_route6.return_value = ("scapy0", "fe80::baca:3aff:fe72:b08b", "::") - mock_neighsol.return_value = test_neighsol() - return getmacbyip6("fe80::704:3ff:fe2:100") - -test_getmacbyip6() == "05:04:03:02:01:00" - -= IPv6 - IPerror6 & UDPerror & _ICMPv6Error - -query = IPv6(dst="2001:db8::1", src="2001:db8::2", hlim=1)/UDP()/DNS() -answer = IPv6(dst="2001:db8::2", src="2001:db8::1", hlim=1)/ICMPv6TimeExceeded()/IPerror6(dst="2001:db8::1", src="2001:db8::2", hlim=0)/UDPerror()/DNS() -answer.answers(query) == True - -# Test _ICMPv6Error -from scapy.layers.inet6 import _ICMPv6Error -assert _ICMPv6Error().guess_payload_class(None) == IPerror6 - -= Windows: reset routes properly - -if WINDOWS: - route_add_loopback() - -############ -############ -+ ICMPv6ML - -= ICMPv6MLQuery - build & dissection -s = raw(IPv6(src="fe80::1")/ICMPv6MLQuery()) -assert s == b"`\x00\x00\x00\x00\x18:\x01\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x82\x00Y\x17'\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - -p = IPv6(s) -assert ICMPv6MLQuery in p and p[IPv6].dst == "ff02::1" - -= Check answers - -q = IPv6()/IPv6ExtHdrHopByHop(options=[RouterAlert()])/ICMPv6MLQuery() -a = IPv6()/IPv6ExtHdrHopByHop(options=[RouterAlert()])/ICMPv6MLReport() - -assert a.answers(q) - -############ -############ -+ ICMPv6MLv2 - -= ICMPv6MLQuery2 - build & dissection -p = IPv6()/IPv6ExtHdrHopByHop(options=[RouterAlert()])/ICMPv6MLQuery2(sources=["::1"]) -s = raw(p) -assert s == b"`\x00\x00\x00\x004\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01:\x00\x05\x02\x00\x00\x01\x00\x82\x00V\x85'\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" - -p = IPv6(s) -assert ICMPv6MLQuery2 in p and p.sources_number == 1 - -= ICMPv6MLReport2 - build & dissection -p = IPv6()/IPv6ExtHdrHopByHop(options=[RouterAlert()])/ICMPv6MLReport2(records=[ICMPv6MLDMultAddrRec(), ICMPv6MLDMultAddrRec(sources=["::1"], auxdata="scapy")]) -s = raw(p) -assert s == b'`\x00\x00\x00\x00M\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01:\x00\x05\x02\x00\x00\x01\x00\x8f\x00\x1a\xa1\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01scapy' - -p = IPv6(s) -assert ICMPv6MLReport2 in p and p.records_number == 2 - -= ICMPv6MLReport2 and ICMPv6MLDMultAddrRec - dissection - -z = b'33\x00\x00\x00\x16\xd0P\x99V\xdd\xf9\x86\xdd`\x00\x00\x00\x00\x1c:\x01\xfe\x80\x00\x00\x00\x00\x00\x00q eX\x98\x86\xfa\x88\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x8f\x00\x13\x4d\x00\x00\x00\x01\x04\x00\x00\x00\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xffR\xf3\xe1' -w = Ether(z) - -assert len(w.records) == 1 -assert isinstance(w.records[0], ICMPv6MLDMultAddrRec) -assert w.records[0].dst == "ff02::1:ff52:f3e1" - -= Check answers - -q = IPv6()/IPv6ExtHdrHopByHop(options=[RouterAlert()])/ICMPv6MLQuery2() -a = IPv6()/IPv6ExtHdrHopByHop(options=[RouterAlert()])/ICMPv6MLReport2() - -assert a.answers(q) - -############ -############ -+ Ether tests with IPv6 - -= Ether IPv6 checking for dst -~ netaccess ipv6 - -p = Ether()/IPv6(dst="www.google.com")/TCP() -assert p.dst != p[IPv6].dst -p.show() - -############ -############ -+ TracerouteResult6 - -= get_trace() -ip6_hlim = [("2001:db8::%d" % i, i) for i in six.moves.range(1, 12)] - -tr6_packets = [ (IPv6(dst="2001:db8::1", src="2001:db8::254", hlim=hlim)/UDP()/"scapy", - IPv6(dst="2001:db8::254", src=ip)/ICMPv6TimeExceeded()/IPerror6(dst="2001:db8::1", src="2001:db8::254", hlim=0)/UDPerror()/"scapy") - for (ip, hlim) in ip6_hlim ] - -tr6 = TracerouteResult6(tr6_packets) -tr6.get_trace() == {'2001:db8::1': {1: ('2001:db8::1', False), 2: ('2001:db8::2', False), 3: ('2001:db8::3', False), 4: ('2001:db8::4', False), 5: ('2001:db8::5', False), 6: ('2001:db8::6', False), 7: ('2001:db8::7', False), 8: ('2001:db8::8', False), 9: ('2001:db8::9', False), 10: ('2001:db8::10', False), 11: ('2001:db8::11', False)}} - -= show() -def test_show(): - with ContextManagerCaptureOutput() as cmco: - tr6 = TracerouteResult6(tr6_packets) - tr6.show() - result = cmco.get_output() - expected = " 2001:db8::1 :udpdomain \n" - expected += "1 2001:db8::1 3 \n" - expected += "2 2001:db8::2 3 \n" - expected += "3 2001:db8::3 3 \n" - expected += "4 2001:db8::4 3 \n" - expected += "5 2001:db8::5 3 \n" - expected += "6 2001:db8::6 3 \n" - expected += "7 2001:db8::7 3 \n" - expected += "8 2001:db8::8 3 \n" - expected += "9 2001:db8::9 3 \n" - expected += "10 2001:db8::10 3 \n" - expected += "11 2001:db8::11 3 \n" - index_result = result.index("\n1") - index_expected = expected.index("\n1") - assert(result[index_result:] == expected[index_expected:]) - -test_show() - -= graph() -saved_AS_resolver = conf.AS_resolver -conf.AS_resolver = None -tr6.make_graph() -assert len(tr6.graphdef) == 530 -assert tr6.graphdef.startswith("digraph trace {") -'"2001:db8::1 53/udp";' in tr6.graphdef -conf.AS_resolver = saved_AS_resolver - -############ -############ -+ IPv6 attacks - -= Define test utilities - -import mock - -@mock.patch("scapy.layers.inet6.sniff") -@mock.patch("scapy.layers.inet6.sendp") -def test_attack(function, pktlist, sendp_mock, sniff_mock, options=()): - pktlist = [Ether(raw(x)) for x in pktlist] - ret_list = [] - def _fake_sniff(lfilter=None, prn=None, **kwargs): - for p in pktlist: - if lfilter and lfilter(p) and prn: - prn(p) - sniff_mock.side_effect = _fake_sniff - def _fake_sendp(pkt, *args, **kwargs): - ret_list.append(Ether(raw(pkt))) - sendp_mock.side_effect = _fake_sendp - function(*options) - return ret_list - -= Test NDP_Attack_DAD_DoS_via_NS - -data = [Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ff:00:11:11')/IPv6(src="::", dst="ff02::1:ff00:1111")/ICMPv6ND_NS(tgt="ffff::1111", code=17, res=3758096385), - Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ff:5d:c3:53')/IPv6(src="::", dst="ff02::1:ff5d:c353")/ICMPv6ND_NS(tgt="b643:44c3:f659:f8e6:31c0:6437:825d:c353"), - Ether()/IP()/ICMP()] -results = test_attack(NDP_Attack_DAD_DoS_via_NS, data) -assert len(results) == 2 - -a = results[0][IPv6] -assert a[IPv6].src == "::" -assert a[IPv6].dst == "ff02::1:ff00:1111" -assert a[IPv6].hlim == 255 -assert a[ICMPv6ND_NS].tgt == "ffff::1111" - -b = results[1][IPv6] -assert b[IPv6].src == "::" -assert b[IPv6].dst == "ff02::1:ff5d:c353" -assert b[IPv6].hlim == 255 -assert b[ICMPv6ND_NS].tgt == "b643:44c3:f659:f8e6:31c0:6437:825d:c353" - -= Test NDP_Attack_DAD_DoS_via_NA - -data = [Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ff:00:11:11')/IPv6(src="::", dst="ff02::1:ff00:1111")/ICMPv6ND_NS(tgt="ffff::1111", code=17, res=3758096385), - Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ff:5d:c3:53')/IPv6(src="::", dst="ff02::1:ff5d:c353")/ICMPv6ND_NS(tgt="b643:44c3:f659:f8e6:31c0:6437:825d:c353"), - Ether()/IP()/ICMP()] -results = test_attack(NDP_Attack_DAD_DoS_via_NA, data, options=(None, None, None, "ab:ab:ab:ab:ab:ab")) -assert len(results) == 2 -results[0].dst = "ff:ff:ff:ff:ff:ff" -results[1].dst = "ff:ff:ff:ff:ff:ff" - -a = results[0] -assert a[Ether].dst == "ff:ff:ff:ff:ff:ff" -assert a[Ether].src == "ab:ab:ab:ab:ab:ab" -assert a[IPv6].src == "ffff::1111" -assert a[IPv6].dst == "ff02::1:ff00:1111" -assert a[IPv6].hlim == 255 -assert a[ICMPv6ND_NA].tgt == "ffff::1111" -assert a[ICMPv6NDOptDstLLAddr].lladdr == "ab:ab:ab:ab:ab:ab" - -b = results[1] -assert b[Ether].dst == "ff:ff:ff:ff:ff:ff" -assert b[Ether].src == "ab:ab:ab:ab:ab:ab" -assert b[IPv6].src == "b643:44c3:f659:f8e6:31c0:6437:825d:c353" -assert b[IPv6].dst == "ff02::1:ff5d:c353" -assert b[IPv6].hlim == 255 -assert b[ICMPv6ND_NA].tgt == "b643:44c3:f659:f8e6:31c0:6437:825d:c353" -assert b[ICMPv6NDOptDstLLAddr].lladdr == "ab:ab:ab:ab:ab:ab" - -= Test NDP_Attack_NA_Spoofing - -data = [Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ff:d4:e5:f6')/IPv6(src="753a:727c:97b5:f71d:51ea:3901:ab52:e110", dst="ff02::1:ffd4:e5f6")/ICMPv6ND_NS(tgt="ff02::1:ffd4:e5f6", code=171, res=3758096), - Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:e4:68:c9:4f')/IPv6(src="753a:727c:97b5:f71d:51ea:3901:ab52:e110", dst="fe9c:98b0:52b5:7033:5db0:394f:e468:c94f")/ICMPv6ND_NS(), - Ether()/IP()/ICMP()] -results = test_attack(NDP_Attack_NA_Spoofing, data, options=(None, None, None, "ff:ff:ff:ff:ff:ff", None)) -assert len(results) == 2 - -a = results[0] -assert a[Ether].dst == "aa:aa:aa:aa:aa:aa" -assert a[Ether].src == "ff:ff:ff:ff:ff:ff" -assert a[IPv6].src == "ff02::1:ffd4:e5f6" -assert a[IPv6].dst == "753a:727c:97b5:f71d:51ea:3901:ab52:e110" -assert a[IPv6].hlim == 255 -assert a[ICMPv6ND_NA].R == 0 -assert a[ICMPv6ND_NA].S == 1 -assert a[ICMPv6ND_NA].O == 1 -assert a[ICMPv6ND_NA].tgt == "ff02::1:ffd4:e5f6" -assert a[ICMPv6NDOptDstLLAddr].lladdr == "ff:ff:ff:ff:ff:ff" - -b = results[1] -assert b[Ether].dst == "aa:aa:aa:aa:aa:aa" -assert b[Ether].src == "ff:ff:ff:ff:ff:ff" -assert b[IPv6].src == "::" -assert b[IPv6].dst == "753a:727c:97b5:f71d:51ea:3901:ab52:e110" -assert b[IPv6].hlim == 255 -assert b[ICMPv6ND_NA].R == 0 -assert b[ICMPv6ND_NA].S == 1 -assert b[ICMPv6ND_NA].O == 1 -assert b[ICMPv6ND_NA].tgt == "::" -assert b[ICMPv6NDOptDstLLAddr].lladdr == "ff:ff:ff:ff:ff:ff" - -= Test NDP_Attack_Kill_Default_Router - -data = [Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ff:d4:e5:f6')/IPv6(src="753a:727c:97b5:f71d:51ea:3901:ab52:e110", dst="ff02::1:ffd4:e5f6")/ICMPv6ND_RA(routerlifetime=1), - Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ab:52:e1:10')/IPv6(src="fe9c:98b0:52b5:7033:5db0:394f:e468:c94f", dst="753a:727c:97b5:f71d:51ea:3901:ab52:e110")/ICMPv6ND_RA(routerlifetime=1), - Ether()/IP()/"RANDOM"] -results = test_attack(NDP_Attack_Kill_Default_Router, data) -assert len(results) == 2 - -a = results[0][IPv6] -assert a[IPv6].src == "753a:727c:97b5:f71d:51ea:3901:ab52:e110" -assert a[IPv6].dst == "ff02::1" -assert a[IPv6].hlim == 255 -assert a[ICMPv6ND_RA].M == 0 -assert a[ICMPv6ND_RA].O == 0 -assert a[ICMPv6ND_RA].H == 0 -assert a[ICMPv6ND_RA].P == 0 -assert a[ICMPv6ND_RA].routerlifetime == 0 -assert a[ICMPv6ND_RA].reachabletime == 0 -assert a[ICMPv6ND_RA].retranstimer == 0 -assert a[ICMPv6NDOptSrcLLAddr].lladdr == "aa:aa:aa:aa:aa:aa" - -b = results[1][IPv6] -assert b[IPv6].src == "fe9c:98b0:52b5:7033:5db0:394f:e468:c94f" -assert b[IPv6].dst == "ff02::1" -assert b[IPv6].hlim == 255 -assert b[ICMPv6ND_RA].M == 0 -assert b[ICMPv6ND_RA].O == 0 -assert b[ICMPv6ND_RA].H == 0 -assert b[ICMPv6ND_RA].P == 0 -assert b[ICMPv6ND_RA].routerlifetime == 0 -assert b[ICMPv6ND_RA].reachabletime == 0 -assert b[ICMPv6ND_RA].retranstimer == 0 -assert b[ICMPv6NDOptSrcLLAddr].lladdr == "aa:aa:aa:aa:aa:aa" - -= Test NDP_Attack_Fake_Router - -ra = Ether()/IPv6()/ICMPv6ND_RA() -ra /= ICMPv6NDOptPrefixInfo(prefix="2001:db8:1::", prefixlen=64) -ra /= ICMPv6NDOptPrefixInfo(prefix="2001:db8:2::", prefixlen=64) -ra /= ICMPv6NDOptSrcLLAddr(lladdr="00:11:22:33:44:55") - -rad = Ether(raw(ra)) - -data = [Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ab:52:e1:10')/IPv6(src="753a:727c:97b5:f71d:51ea:3901:ab52:e110", dst="ff02::1:ffd4:e5f6")/ICMPv6ND_RS(code=11, res=3758096), - Ether(src='aa:aa:aa:aa:aa:aa', dst='33:33:ab:52:e1:10')/IPv6(src="753a:727c:97b5:f71d:51ea:3901:ab52:e110", dst="fe9c:98b0:52b5:7033:5db0:394f:e468:c94f")/ICMPv6ND_RS(), - Ether()/IP()/ICMP()] -results = test_attack(NDP_Attack_Fake_Router, data, options=(ra,)) -assert len(results) == 2 - -assert results[0] == rad -assert results[1] == rad - -= Test NDP_Attack_NS_Spoofing - -r = test_attack(NDP_Attack_NS_Spoofing, [], options=("aa:aa:aa:aa:aa:aa", "753a:727c:97b5:f71d:51ea:3901:ab52:e110", "2001:db8::1", 'e4a0:654b:1a24:1b15:761d:2e5d:245d:ba83', "cc:cc:cc:cc:cc:cc", "dd:dd:dd:dd:dd:dd"))[0] - -assert r[Ether].dst == "dd:dd:dd:dd:dd:dd" -assert r[Ether].src == "cc:cc:cc:cc:cc:cc" -assert r[IPv6].hlim == 255 -assert r[IPv6].src == "753a:727c:97b5:f71d:51ea:3901:ab52:e110" -assert r[IPv6].dst == "e4a0:654b:1a24:1b15:761d:2e5d:245d:ba83" -assert r[ICMPv6ND_NS].tgt == "2001:db8::1" -assert r[ICMPv6NDOptSrcLLAddr].lladdr == "aa:aa:aa:aa:aa:aa" - -# Below is our Homework : here is the mountain ... -# - -########### ICMPv6MLReport Class #################################### -########### ICMPv6MLDone Class ###################################### -########### ICMPv6ND_Redirect Class ################################# -########### ICMPv6NDOptSrcAddrList Class ############################ -########### ICMPv6NDOptTgtAddrList Class ############################ -########### ICMPv6ND_INDSol Class ################################### -########### ICMPv6ND_INDAdv Class ################################### - - - - - -##################################################################### -##################################################################### -########################## DHCPv6 ########################## -##################################################################### -##################################################################### - - -############ -############ -+ Test DHCP6 DUID_LLT - -= DUID_LLT basic instantiation -a=DUID_LLT() - -= DUID_LLT basic build -raw(DUID_LLT()) == b'\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DUID_LLT build with specific values -raw(DUID_LLT(lladdr="ff:ff:ff:ff:ff:ff", timeval=0x11111111, hwtype=0x2222)) == b'\x00\x01""\x11\x11\x11\x11\xff\xff\xff\xff\xff\xff' - -= DUID_LLT basic dissection -a=DUID_LLT(raw(DUID_LLT())) -a.type == 1 and a.hwtype == 1 and a.timeval == 0 and a.lladdr == "00:00:00:00:00:00" - -= DUID_LLT dissection with specific values -a=DUID_LLT(b'\x00\x01""\x11\x11\x11\x11\xff\xff\xff\xff\xff\xff') -a.type == 1 and a.hwtype == 0x2222 and a.timeval == 0x11111111 and a.lladdr == "ff:ff:ff:ff:ff:ff" - - -############ -############ -+ Test DHCP6 DUID_EN - -= DUID_EN basic instantiation -a=DUID_EN() - -= DUID_EN basic build -raw(DUID_EN()) == b'\x00\x02\x00\x00\x017' - -= DUID_EN build with specific values -raw(DUID_EN(enterprisenum=0x11111111, id="iamastring")) == b'\x00\x02\x11\x11\x11\x11iamastring' - -= DUID_EN basic dissection -a=DUID_EN(b'\x00\x02\x00\x00\x017') -a.type == 2 and a.enterprisenum == 311 - -= DUID_EN dissection with specific values -a=DUID_EN(b'\x00\x02\x11\x11\x11\x11iamarawing') -a.type == 2 and a.enterprisenum == 0x11111111 and a.id == b"iamarawing" - - -############ -############ -+ Test DHCP6 DUID_LL - -= DUID_LL basic instantiation -a=DUID_LL() - -= DUID_LL basic build -raw(DUID_LL()) == b'\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00' - -= DUID_LL build with specific values -raw(DUID_LL(hwtype=1, lladdr="ff:ff:ff:ff:ff:ff")) == b'\x00\x03\x00\x01\xff\xff\xff\xff\xff\xff' - -= DUID_LL basic dissection -a=DUID_LL(raw(DUID_LL())) -a.type == 3 and a.hwtype == 1 and a.lladdr == "00:00:00:00:00:00" - -= DUID_LL with specific values -a=DUID_LL(b'\x00\x03\x00\x01\xff\xff\xff\xff\xff\xff') -a.hwtype == 1 and a.lladdr == "ff:ff:ff:ff:ff:ff" - - -############ -############ -+ Test DHCP6 DUID_UUID - -= DUID_UUID basic instantiation -a=DUID_UUID() - -= DUID_UUID basic build -raw(DUID_UUID()) == b"\0\x04\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - -= DUID_UUID build with specific values -raw(DUID_UUID(uuid="272adcca-138c-4e8d-b3f4-634e953128cf")) == \ - b"\x00\x04'*\xdc\xca\x13\x8cN\x8d\xb3\xf4cN\x951(\xcf" - -= DUID_UUID basic dissection -a=DUID_UUID(raw(DUID_UUID())) -a.type == 4 and str(a.uuid) == "00000000-0000-0000-0000-000000000000" - -= DUID_UUID with specific values -a=DUID_UUID(b"\x00\x04'*\xdc\xca\x13\x8cN\x8d\xb3\xf4cN\x951(\xcf") -a.type == 4 and str(a.uuid) == "272adcca-138c-4e8d-b3f4-634e953128cf" - - -############ -############ -+ Test DHCP6 Opt Unknown - -= DHCP6 Opt Unknown basic instantiation -a=DHCP6OptUnknown() - -= DHCP6 Opt Unknown basic build (default values) -raw(DHCP6OptUnknown()) == b'\x00\x00\x00\x00' - -= DHCP6 Opt Unknown - len computation test -raw(DHCP6OptUnknown(data="shouldbe9")) == b'\x00\x00\x00\tshouldbe9' - - -############ -############ -+ Test DHCP6 Client Identifier option - -= DHCP6OptClientId basic instantiation -a=DHCP6OptClientId() - -= DHCP6OptClientId basic build -raw(DHCP6OptClientId()) == b'\x00\x01\x00\x00' - -= DHCP6OptClientId instantiation with specific values -raw(DHCP6OptClientId(duid="toto")) == b'\x00\x01\x00\x04toto' - -= DHCP6OptClientId instantiation with DUID_LL -raw(DHCP6OptClientId(duid=DUID_LL())) == b'\x00\x01\x00\n\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00' - -= DHCP6OptClientId instantiation with DUID_LLT -raw(DHCP6OptClientId(duid=DUID_LLT())) == b'\x00\x01\x00\x0e\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptClientId instantiation with DUID_EN -raw(DHCP6OptClientId(duid=DUID_EN())) == b'\x00\x01\x00\x06\x00\x02\x00\x00\x017' - -= DHCP6OptClientId instantiation with specified length -raw(DHCP6OptClientId(optlen=80, duid="somestring")) == b'\x00\x01\x00Psomestring' - -= DHCP6OptClientId basic dissection -a=DHCP6OptClientId(b'\x00\x01\x00\x00') -a.optcode == 1 and a.optlen == 0 - -= DHCP6OptClientId instantiation with specified length -raw(DHCP6OptClientId(optlen=80, duid="somestring")) == b'\x00\x01\x00Psomestring' - -= DHCP6OptClientId basic dissection -a=DHCP6OptClientId(b'\x00\x01\x00\x00') -a.optcode == 1 and a.optlen == 0 - -= DHCP6OptClientId dissection with specific duid value -a=DHCP6OptClientId(b'\x00\x01\x00\x04somerawing') -a.optcode == 1 and a.optlen == 4 and isinstance(a.duid, Raw) and a.duid.load == b'some' and isinstance(a.payload, DHCP6OptUnknown) - -= DHCP6OptClientId dissection with specific DUID_LL as duid value -a=DHCP6OptClientId(b'\x00\x01\x00\n\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00') -a.optcode == 1 and a.optlen == 10 and isinstance(a.duid, DUID_LL) and a.duid.type == 3 and a.duid.hwtype == 1 and a.duid.lladdr == "00:00:00:00:00:00" - -= DHCP6OptClientId dissection with specific DUID_LLT as duid value -a=DHCP6OptClientId(b'\x00\x01\x00\x0e\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.optcode == 1 and a.optlen == 14 and isinstance(a.duid, DUID_LLT) and a.duid.type == 1 and a.duid.hwtype == 1 and a.duid.timeval == 0 and a.duid.lladdr == "00:00:00:00:00:00" - -= DHCP6OptClientId dissection with specific DUID_EN as duid value -a=DHCP6OptClientId(b'\x00\x01\x00\x06\x00\x02\x00\x00\x017') -a.optcode == 1 and a.optlen == 6 and isinstance(a.duid, DUID_EN) and a.duid.type == 2 and a.duid.enterprisenum == 311 and a.duid.id == b"" - - -############ -############ -+ Test DHCP6 Server Identifier option - -= DHCP6OptServerId basic instantiation -a=DHCP6OptServerId() - -= DHCP6OptServerId basic build -raw(DHCP6OptServerId()) == b'\x00\x02\x00\x00' - -= DHCP6OptServerId basic build with specific values -raw(DHCP6OptServerId(duid="toto")) == b'\x00\x02\x00\x04toto' - -= DHCP6OptServerId instantiation with DUID_LL -raw(DHCP6OptServerId(duid=DUID_LL())) == b'\x00\x02\x00\n\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00' - -= DHCP6OptServerId instantiation with DUID_LLT -raw(DHCP6OptServerId(duid=DUID_LLT())) == b'\x00\x02\x00\x0e\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptServerId instantiation with DUID_EN -raw(DHCP6OptServerId(duid=DUID_EN())) == b'\x00\x02\x00\x06\x00\x02\x00\x00\x017' - -= DHCP6OptServerId instantiation with specified length -raw(DHCP6OptServerId(optlen=80, duid="somestring")) == b'\x00\x02\x00Psomestring' - -= DHCP6OptServerId basic dissection -a=DHCP6OptServerId(b'\x00\x02\x00\x00') -a.optcode == 2 and a.optlen == 0 - -= DHCP6OptServerId dissection with specific duid value -a=DHCP6OptServerId(b'\x00\x02\x00\x04somerawing') -a.optcode == 2 and a.optlen == 4 and isinstance(a.duid, Raw) and a.duid.load == b'some' and isinstance(a.payload, DHCP6OptUnknown) - -= DHCP6OptServerId dissection with specific DUID_LL as duid value -a=DHCP6OptServerId(b'\x00\x02\x00\n\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00') -a.optcode == 2 and a.optlen == 10 and isinstance(a.duid, DUID_LL) and a.duid.type == 3 and a.duid.hwtype == 1 and a.duid.lladdr == "00:00:00:00:00:00" - -= DHCP6OptServerId dissection with specific DUID_LLT as duid value -a=DHCP6OptServerId(b'\x00\x02\x00\x0e\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.optcode == 2 and a.optlen == 14 and isinstance(a.duid, DUID_LLT) and a.duid.type == 1 and a.duid.hwtype == 1 and a.duid.timeval == 0 and a.duid.lladdr == "00:00:00:00:00:00" - -= DHCP6OptServerId dissection with specific DUID_EN as duid value -a=DHCP6OptServerId(b'\x00\x02\x00\x06\x00\x02\x00\x00\x017') -a.optcode == 2 and a.optlen == 6 and isinstance(a.duid, DUID_EN) and a.duid.type == 2 and a.duid.enterprisenum == 311 and a.duid.id == b"" - - -############ -############ -+ Test DHCP6 IA Address Option (IA_TA or IA_NA suboption) - -= DHCP6OptIAAddress - Basic Instantiation -raw(DHCP6OptIAAddress()) == b'\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptIAAddress - Basic Dissection -a = DHCP6OptIAAddress(b'\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.optcode == 5 and a.optlen == 24 and a.addr == "::" and a.preflft == 0 and a. validlft == 0 and a.iaaddropts == b"" - -= DHCP6OptIAAddress - Instantiation with specific values -raw(DHCP6OptIAAddress(optlen=0x1111, addr="2222:3333::5555", preflft=0x66666666, validlft=0x77777777, iaaddropts="somestring")) == b'\x00\x05\x11\x11""33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00UUffffwwwwsomestring' - -= DHCP6OptIAAddress - Instantiation with specific values (default optlen computation) -raw(DHCP6OptIAAddress(addr="2222:3333::5555", preflft=0x66666666, validlft=0x77777777, iaaddropts="somestring")) == b'\x00\x05\x00"""33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00UUffffwwwwsomestring' - -= DHCP6OptIAAddress - Dissection with specific values -a = DHCP6OptIAAddress(b'\x00\x05\x00"""33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00UUffffwwwwsomerawing') -a.optcode == 5 and a.optlen == 34 and a.addr == "2222:3333::5555" and a.preflft == 0x66666666 and a. validlft == 0x77777777 and a.iaaddropts == b"somerawing" - - -############ -############ -+ Test DHCP6 Identity Association for Non-temporary Addresses Option - -= DHCP6OptIA_NA - Basic Instantiation -raw(DHCP6OptIA_NA()) == b'\x00\x03\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptIA_NA - Basic Dissection -a = DHCP6OptIA_NA(b'\x00\x03\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.optcode == 3 and a.optlen == 12 and a.iaid == 0 and a.T1 == 0 and a.T2==0 and a.ianaopts == [] - -= DHCP6OptIA_NA - Instantiation with specific values (keep automatic length computation) -raw(DHCP6OptIA_NA(iaid=0x22222222, T1=0x33333333, T2=0x44444444)) == b'\x00\x03\x00\x0c""""3333DDDD' - -= DHCP6OptIA_NA - Instantiation with specific values (forced optlen) -raw(DHCP6OptIA_NA(optlen=0x1111, iaid=0x22222222, T1=0x33333333, T2=0x44444444)) == b'\x00\x03\x11\x11""""3333DDDD' - -= DHCP6OptIA_NA - Instantiation with a list of IA Addresses (optlen automatic computation) -raw(DHCP6OptIA_NA(iaid=0x22222222, T1=0x33333333, T2=0x44444444, ianaopts=[DHCP6OptIAAddress(), DHCP6OptIAAddress()])) == b'\x00\x03\x00D""""3333DDDD\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptIA_NA - Dissection with specific values -a = DHCP6OptIA_NA(b'\x00\x03\x00L""""3333DDDD\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.optcode == 3 and a.optlen == 76 and a.iaid == 0x22222222 and a.T1 == 0x33333333 and a.T2==0x44444444 and len(a.ianaopts) == 2 and isinstance(a.ianaopts[0], DHCP6OptIAAddress) and isinstance(a.ianaopts[1], DHCP6OptIAAddress) - -= DHCP6OptIA_NA - Instantiation with a list of different opts: IA Address and Status Code (optlen automatic computation) -raw(DHCP6OptIA_NA(iaid=0x22222222, T1=0x33333333, T2=0x44444444, ianaopts=[DHCP6OptIAAddress(), DHCP6OptStatusCode(statuscode=0xff, statusmsg="Hello")])) == b'\x00\x03\x003""""3333DDDD\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\x00\x07\x00\xffHello' - - -############ -############ -+ Test DHCP6 Identity Association for Temporary Addresses Option - -= DHCP6OptIA_TA - Basic Instantiation -raw(DHCP6OptIA_TA()) == b'\x00\x04\x00\x04\x00\x00\x00\x00' - -= DHCP6OptIA_TA - Basic Dissection -a = DHCP6OptIA_TA(b'\x00\x04\x00\x04\x00\x00\x00\x00') -a.optcode == 4 and a.optlen == 4 and a.iaid == 0 and a.iataopts == [] - -= DHCP6OptIA_TA - Instantiation with specific values -raw(DHCP6OptIA_TA(optlen=0x1111, iaid=0x22222222, iataopts=[DHCP6OptIAAddress(), DHCP6OptIAAddress()])) == b'\x00\x04\x11\x11""""\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptIA_TA - Dissection with specific values -a = DHCP6OptIA_TA(b'\x00\x04\x11\x11""""\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.optcode == 4 and a.optlen == 0x1111 and a.iaid == 0x22222222 and len(a.iataopts) == 2 and isinstance(a.iataopts[0], DHCP6OptIAAddress) and isinstance(a.iataopts[1], DHCP6OptIAAddress) - -= DHCP6OptIA_TA - Instantiation with a list of different opts: IA Address and Status Code (optlen automatic computation) -raw(DHCP6OptIA_TA(iaid=0x22222222, iataopts=[DHCP6OptIAAddress(), DHCP6OptStatusCode(statuscode=0xff, statusmsg="Hello")])) == b'\x00\x04\x00+""""\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\x00\x07\x00\xffHello' - - -############ -############ -+ Test DHCP6 Option Request Option - -= DHCP6OptOptReq - Basic Instantiation -raw(DHCP6OptOptReq()) == b'\x00\x06\x00\x04\x00\x17\x00\x18' - -= DHCP6OptOptReq - optlen field computation -raw(DHCP6OptOptReq(reqopts=[1,2,3,4])) == b'\x00\x06\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04' - -= DHCP6OptOptReq - instantiation with empty list -raw(DHCP6OptOptReq(reqopts=[])) == b'\x00\x06\x00\x00' - -= DHCP6OptOptReq - Basic dissection -a=DHCP6OptOptReq(b'\x00\x06\x00\x00') -a.optcode == 6 and a.optlen == 0 and a.reqopts == [23,24] - -= DHCP6OptOptReq - Dissection with specific value -a=DHCP6OptOptReq(b'\x00\x06\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04') -a.optcode == 6 and a.optlen == 8 and a.reqopts == [1,2,3,4] - -= DHCP6OptOptReq - repr -a.show() - - -############ -############ -+ Test DHCP6 Option - Preference option - -= DHCP6OptPref - Basic instantiation -raw(DHCP6OptPref()) == b'\x00\x07\x00\x01\xff' - -= DHCP6OptPref - Instantiation with specific values -raw(DHCP6OptPref(optlen=0xffff, prefval= 0x11)) == b'\x00\x07\xff\xff\x11' - -= DHCP6OptPref - Basic Dissection -a=DHCP6OptPref(b'\x00\x07\x00\x01\xff') -a.optcode == 7 and a.optlen == 1 and a.prefval == 255 - -= DHCP6OptPref - Dissection with specific values -a=DHCP6OptPref(b'\x00\x07\xff\xff\x11') -a.optcode == 7 and a.optlen == 0xffff and a.prefval == 0x11 - - -############ -############ -+ Test DHCP6 Option - Elapsed Time - -= DHCP6OptElapsedTime - Basic Instantiation -raw(DHCP6OptElapsedTime()) == b'\x00\x08\x00\x02\x00\x00' - -= DHCP6OptElapsedTime - Instantiation with specific elapsedtime value -raw(DHCP6OptElapsedTime(elapsedtime=421)) == b'\x00\x08\x00\x02\x01\xa5' - -= DHCP6OptElapsedTime - Basic Dissection -a=DHCP6OptElapsedTime(b'\x00\x08\x00\x02\x00\x00') -a.optcode == 8 and a.optlen == 2 and a.elapsedtime == 0 - -= DHCP6OptElapsedTime - Dissection with specific values -a=DHCP6OptElapsedTime(b'\x00\x08\x00\x02\x01\xa5') -a.optcode == 8 and a.optlen == 2 and a.elapsedtime == 421 - -= DHCP6OptElapsedTime - Repr -a.show() - - -############ -############ -+ Test DHCP6 Option - Server Unicast Address - -= DHCP6OptServerUnicast - Basic Instantiation -raw(DHCP6OptServerUnicast()) == b'\x00\x0c\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptServerUnicast - Instantiation with specific values (test 1) -raw(DHCP6OptServerUnicast(srvaddr="2001::1")) == b'\x00\x0c\x00\x10 \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptServerUnicast - Instantiation with specific values (test 2) -raw(DHCP6OptServerUnicast(srvaddr="2001::1", optlen=42)) == b'\x00\x0c\x00* \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptServerUnicast - Dissection with default values -a=DHCP6OptServerUnicast(b'\x00\x0c\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.optcode == 12 and a.optlen == 16 and a.srvaddr == "::" - -= DHCP6OptServerUnicast - Dissection with specific values (test 1) -a=DHCP6OptServerUnicast(b'\x00\x0c\x00\x10 \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 12 and a.optlen == 16 and a.srvaddr == "2001::1" - -= DHCP6OptServerUnicast - Dissection with specific values (test 2) -a=DHCP6OptServerUnicast(b'\x00\x0c\x00* \x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 12 and a.optlen == 42 and a.srvaddr == "2001::1" - - -############ -############ -+ Test DHCP6 Option - Status Code - -= DHCP6OptStatusCode - Basic Instantiation -raw(DHCP6OptStatusCode()) == b'\x00\r\x00\x02\x00\x00' - -= DHCP6OptStatusCode - Instantiation with specific values -raw(DHCP6OptStatusCode(optlen=42, statuscode=0xff, statusmsg="Hello")) == b'\x00\r\x00*\x00\xffHello' - -= DHCP6OptStatusCode - Automatic Length computation -raw(DHCP6OptStatusCode(statuscode=0xff, statusmsg="Hello")) == b'\x00\r\x00\x07\x00\xffHello' - -# Add tests to verify Unicode behavior - - -############ -############ -+ Test DHCP6 Option - Rapid Commit - -= DHCP6OptRapidCommit - Basic Instantiation -raw(DHCP6OptRapidCommit()) == b'\x00\x0e\x00\x00' - -= DHCP6OptRapidCommit - Basic Dissection -a=DHCP6OptRapidCommit(b'\x00\x0e\x00\x00') -a.optcode == 14 and a.optlen == 0 - - -############ -############ -+ Test DHCP6 Option - User class - -= DHCP6OptUserClass - Basic Instantiation -raw(DHCP6OptUserClass()) == b'\x00\x0f\x00\x00' - -= DHCP6OptUserClass - Basic Dissection -a = DHCP6OptUserClass(b'\x00\x0f\x00\x00') -a.optcode == 15 and a.optlen == 0 and a.userclassdata == [] - -= DHCP6OptUserClass - Instantiation with one user class data rawucture -raw(DHCP6OptUserClass(userclassdata=[USER_CLASS_DATA(data="something")])) == b'\x00\x0f\x00\x0b\x00\tsomething' - -= DHCP6OptUserClass - Dissection with one user class data rawucture -a = DHCP6OptUserClass(b'\x00\x0f\x00\x0b\x00\tsomething') -a.optcode == 15 and a.optlen == 11 and len(a.userclassdata) == 1 and isinstance(a.userclassdata[0], USER_CLASS_DATA) and a.userclassdata[0].len == 9 and a.userclassdata[0].data == b'something' - -= DHCP6OptUserClass - Instantiation with two user class data rawuctures -raw(DHCP6OptUserClass(userclassdata=[USER_CLASS_DATA(data="something"), USER_CLASS_DATA(data="somethingelse")])) == b'\x00\x0f\x00\x1a\x00\tsomething\x00\rsomethingelse' - -= DHCP6OptUserClass - Dissection with two user class data rawuctures -a = DHCP6OptUserClass(b'\x00\x0f\x00\x1a\x00\tsomething\x00\rsomethingelse') -a.optcode == 15 and a.optlen == 26 and len(a.userclassdata) == 2 and isinstance(a.userclassdata[0], USER_CLASS_DATA) and isinstance(a.userclassdata[1], USER_CLASS_DATA) and a.userclassdata[0].len == 9 and a.userclassdata[0].data == b'something' and a.userclassdata[1].len == 13 and a.userclassdata[1].data == b'somethingelse' - - -############ -############ -+ Test DHCP6 Option - Vendor class - -= DHCP6OptVendorClass - Basic Instantiation -raw(DHCP6OptVendorClass()) == b'\x00\x10\x00\x04\x00\x00\x00\x00' - -= DHCP6OptVendorClass - Basic Dissection -a = DHCP6OptVendorClass(b'\x00\x10\x00\x04\x00\x00\x00\x00') -a.optcode == 16 and a.optlen == 4 and a.enterprisenum == 0 and a.vcdata == [] - -= DHCP6OptVendorClass - Instantiation with one vendor class data rawucture -raw(DHCP6OptVendorClass(vcdata=[VENDOR_CLASS_DATA(data="something")])) == b'\x00\x10\x00\x0f\x00\x00\x00\x00\x00\tsomething' - -= DHCP6OptVendorClass - Dissection with one vendor class data rawucture -a = DHCP6OptVendorClass(b'\x00\x10\x00\x0f\x00\x00\x00\x00\x00\tsomething') -a.optcode == 16 and a.optlen == 15 and a.enterprisenum == 0 and len(a.vcdata) == 1 and isinstance(a.vcdata[0], VENDOR_CLASS_DATA) and a.vcdata[0].len == 9 and a.vcdata[0].data == b'something' - -= DHCP6OptVendorClass - Instantiation with two vendor class data rawuctures -raw(DHCP6OptVendorClass(vcdata=[VENDOR_CLASS_DATA(data="something"), VENDOR_CLASS_DATA(data="somethingelse")])) == b'\x00\x10\x00\x1e\x00\x00\x00\x00\x00\tsomething\x00\rsomethingelse' - -= DHCP6OptVendorClass - Dissection with two vendor class data rawuctures -a = DHCP6OptVendorClass(b'\x00\x10\x00\x1e\x00\x00\x00\x00\x00\tsomething\x00\rsomethingelse') -a.optcode == 16 and a.optlen == 30 and a.enterprisenum == 0 and len(a.vcdata) == 2 and isinstance(a.vcdata[0], VENDOR_CLASS_DATA) and isinstance(a.vcdata[1], VENDOR_CLASS_DATA) and a.vcdata[0].len == 9 and a.vcdata[0].data == b'something' and a.vcdata[1].len == 13 and a.vcdata[1].data == b'somethingelse' - - -############ -############ -+ Test DHCP6 Option - Vendor-specific information - -= DHCP6OptVendorSpecificInfo - Basic Instantiation -raw(DHCP6OptVendorSpecificInfo()) == b'\x00\x11\x00\x04\x00\x00\x00\x00' - -= DHCP6OptVendorSpecificInfo - Basic Dissection -a = DHCP6OptVendorSpecificInfo(b'\x00\x11\x00\x04\x00\x00\x00\x00') -a.optcode == 17 and a.optlen == 4 and a.enterprisenum == 0 - -= DHCP6OptVendorSpecificInfo - Instantiation with specific values (one option) -raw(DHCP6OptVendorSpecificInfo(enterprisenum=0xeeeeeeee, vso=[VENDOR_SPECIFIC_OPTION(optcode=43, optdata="something")])) == b'\x00\x11\x00\x11\xee\xee\xee\xee\x00+\x00\tsomething' - -= DHCP6OptVendorSpecificInfo - Dissection with with specific values (one option) -a = DHCP6OptVendorSpecificInfo(b'\x00\x11\x00\x11\xee\xee\xee\xee\x00+\x00\tsomething') -a.optcode == 17 and a.optlen == 17 and a.enterprisenum == 0xeeeeeeee and len(a.vso) == 1 and isinstance(a.vso[0], VENDOR_SPECIFIC_OPTION) and a.vso[0].optlen == 9 and a.vso[0].optdata == b'something' - -= DHCP6OptVendorSpecificInfo - Instantiation with specific values (two options) -raw(DHCP6OptVendorSpecificInfo(enterprisenum=0xeeeeeeee, vso=[VENDOR_SPECIFIC_OPTION(optcode=43, optdata="something"), VENDOR_SPECIFIC_OPTION(optcode=42, optdata="somethingelse")])) == b'\x00\x11\x00"\xee\xee\xee\xee\x00+\x00\tsomething\x00*\x00\rsomethingelse' - -= DHCP6OptVendorSpecificInfo - Dissection with with specific values (two options) -a = DHCP6OptVendorSpecificInfo(b'\x00\x11\x00"\xee\xee\xee\xee\x00+\x00\tsomething\x00*\x00\rsomethingelse') -a.optcode == 17 and a.optlen == 34 and a.enterprisenum == 0xeeeeeeee and len(a.vso) == 2 and isinstance(a.vso[0], VENDOR_SPECIFIC_OPTION) and isinstance(a.vso[1], VENDOR_SPECIFIC_OPTION) and a.vso[0].optlen == 9 and a.vso[0].optdata == b'something' and a.vso[1].optlen == 13 and a.vso[1].optdata == b'somethingelse' - - -############ -############ -+ Test DHCP6 Option - Interface-Id - -= DHCP6OptIfaceId - Basic Instantiation -raw(DHCP6OptIfaceId()) == b'\x00\x12\x00\x00' - -= DHCP6OptIfaceId - Basic Dissection -a = DHCP6OptIfaceId(b'\x00\x12\x00\x00') -a.optcode == 18 and a.optlen == 0 - -= DHCP6OptIfaceId - Instantiation with specific value -raw(DHCP6OptIfaceId(ifaceid="something")) == b'\x00\x12\x00\x09something' - -= DHCP6OptIfaceId - Dissection with specific value -a = DHCP6OptIfaceId(b'\x00\x12\x00\x09something') -a.optcode == 18 and a.optlen == 9 and a.ifaceid == b"something" - - -############ -############ -+ Test DHCP6 Option - Reconfigure Message - -= DHCP6OptReconfMsg - Basic Instantiation -raw(DHCP6OptReconfMsg()) == b'\x00\x13\x00\x01\x0b' - -= DHCP6OptReconfMsg - Basic Dissection -a = DHCP6OptReconfMsg(b'\x00\x13\x00\x01\x0b') -a.optcode == 19 and a.optlen == 1 and a.msgtype == 11 - -= DHCP6OptReconfMsg - Instantiation with specific values -raw(DHCP6OptReconfMsg(optlen=4, msgtype=5)) == b'\x00\x13\x00\x04\x05' - -= DHCP6OptReconfMsg - Dissection with specific values -a = DHCP6OptReconfMsg(b'\x00\x13\x00\x04\x05') -a.optcode == 19 and a.optlen == 4 and a.msgtype == 5 - - -############ -############ -+ Test DHCP6 Option - Reconfigure Accept - -= DHCP6OptReconfAccept - Basic Instantiation -raw(DHCP6OptReconfAccept()) == b'\x00\x14\x00\x00' - -= DHCP6OptReconfAccept - Basic Dissection -a = DHCP6OptReconfAccept(b'\x00\x14\x00\x00') -a.optcode == 20 and a.optlen == 0 - -= DHCP6OptReconfAccept - Instantiation with specific values -raw(DHCP6OptReconfAccept(optlen=23)) == b'\x00\x14\x00\x17' - -= DHCP6OptReconfAccept - Dssection with specific values -a = DHCP6OptReconfAccept(b'\x00\x14\x00\x17') -a.optcode == 20 and a.optlen == 23 - - -############ -############ -+ Test DHCP6 Option - SIP Servers Domain Name List - -= DHCP6OptSIPDomains - Basic Instantiation -raw(DHCP6OptSIPDomains()) == b'\x00\x15\x00\x00' - -= DHCP6OptSIPDomains - Basic Dissection -a = DHCP6OptSIPDomains(b'\x00\x15\x00\x00') -a.optcode == 21 and a.optlen == 0 and a.sipdomains == [] - -= DHCP6OptSIPDomains - Instantiation with one domain -raw(DHCP6OptSIPDomains(sipdomains=["toto.example.org"])) == b'\x00\x15\x00\x12\x04toto\x07example\x03org\x00' - -= DHCP6OptSIPDomains - Dissection with one domain -a = DHCP6OptSIPDomains(b'\x00\x15\x00\x12\x04toto\x07example\x03org\x00') -a.optcode == 21 and a.optlen == 18 and len(a.sipdomains) == 1 and a.sipdomains[0] == "toto.example.org." - -= DHCP6OptSIPDomains - Instantiation with two domains -raw(DHCP6OptSIPDomains(sipdomains=["toto.example.org", "titi.example.org"])) == b'\x00\x15\x00$\x04toto\x07example\x03org\x00\x04titi\x07example\x03org\x00' - -= DHCP6OptSIPDomains - Dissection with two domains -a = DHCP6OptSIPDomains(b'\x00\x15\x00$\x04toto\x07example\x03org\x00\x04TITI\x07example\x03org\x00') -a.optcode == 21 and a.optlen == 36 and len(a.sipdomains) == 2 and a.sipdomains[0] == "toto.example.org." and a.sipdomains[1] == "TITI.example.org." - -= DHCP6OptSIPDomains - Enforcing only one dot at end of domain -raw(DHCP6OptSIPDomains(sipdomains=["toto.example.org."])) == b'\x00\x15\x00\x12\x04toto\x07example\x03org\x00' - - -############ -############ -+ Test DHCP6 Option - SIP Servers IPv6 Address List - -= DHCP6OptSIPServers - Basic Instantiation -raw(DHCP6OptSIPServers()) == b'\x00\x16\x00\x00' - -= DHCP6OptSIPServers - Basic Dissection -a = DHCP6OptSIPServers(b'\x00\x16\x00\x00') -a.optcode == 22 and a. optlen == 0 and a.sipservers == [] - -= DHCP6OptSIPServers - Instantiation with specific values (1 address) -raw(DHCP6OptSIPServers(sipservers = ["2001:db8::1"] )) == b'\x00\x16\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptSIPServers - Dissection with specific values (1 address) -a = DHCP6OptSIPServers(b'\x00\x16\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 22 and a.optlen == 16 and len(a.sipservers) == 1 and a.sipservers[0] == "2001:db8::1" - -= DHCP6OptSIPServers - Instantiation with specific values (2 addresses) -raw(DHCP6OptSIPServers(sipservers = ["2001:db8::1", "2001:db8::2"] )) == b'\x00\x16\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptSIPServers - Dissection with specific values (2 addresses) -a = DHCP6OptSIPServers(b'\x00\x16\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 22 and a.optlen == 32 and len(a.sipservers) == 2 and a.sipservers[0] == "2001:db8::1" and a.sipservers[1] == "2001:db8::2" - - -############ -############ -+ Test DHCP6 Option - DNS Recursive Name Server - -= DHCP6OptDNSServers - Basic Instantiation -raw(DHCP6OptDNSServers()) == b'\x00\x17\x00\x00' - -= DHCP6OptDNSServers - Basic Dissection -a = DHCP6OptDNSServers(b'\x00\x17\x00\x00') -a.optcode == 23 and a. optlen == 0 and a.dnsservers == [] - -= DHCP6OptDNSServers - Instantiation with specific values (1 address) -raw(DHCP6OptDNSServers(dnsservers = ["2001:db8::1"] )) == b'\x00\x17\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptDNSServers - Dissection with specific values (1 address) -a = DHCP6OptDNSServers(b'\x00\x17\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 23 and a.optlen == 16 and len(a.dnsservers) == 1 and a.dnsservers[0] == "2001:db8::1" - -= DHCP6OptDNSServers - Instantiation with specific values (2 addresses) -raw(DHCP6OptDNSServers(dnsservers = ["2001:db8::1", "2001:db8::2"] )) == b'\x00\x17\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptDNSServers - Dissection with specific values (2 addresses) -a = DHCP6OptDNSServers(b'\x00\x17\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 23 and a.optlen == 32 and len(a.dnsservers) == 2 and a.dnsservers[0] == "2001:db8::1" and a.dnsservers[1] == "2001:db8::2" - - -############ -############ -+ Test DHCP6 Option - DNS Domain Search List Option - -= DHCP6OptDNSDomains - Basic Instantiation -raw(DHCP6OptDNSDomains()) == b'\x00\x18\x00\x00' - -= DHCP6OptDNSDomains - Basic Dissection -a = DHCP6OptDNSDomains(b'\x00\x18\x00\x00') -a.optcode == 24 and a.optlen == 0 and a.dnsdomains == [] - -= DHCP6OptDNSDomains - Instantiation with specific values (1 domain) -raw(DHCP6OptDNSDomains(dnsdomains=["toto.example.com."])) == b'\x00\x18\x00\x12\x04toto\x07example\x03com\x00' - -= DHCP6OptDNSDomains - Dissection with specific values (1 domain) -a = DHCP6OptDNSDomains(b'\x00\x18\x00\x12\x04toto\x07example\x03com\x00') -a.optcode == 24 and a.optlen == 18 and len(a.dnsdomains) == 1 and a.dnsdomains[0] == "toto.example.com." - -= DHCP6OptDNSDomains - Instantiation with specific values (2 domains) -raw(DHCP6OptDNSDomains(dnsdomains=["toto.example.com.", "titi.example.com."])) == b'\x00\x18\x00$\x04toto\x07example\x03com\x00\x04titi\x07example\x03com\x00' - -= DHCP6OptDNSDomains - Dissection with specific values (2 domains) -a = DHCP6OptDNSDomains(b'\x00\x18\x00$\x04toto\x07example\x03com\x00\x04titi\x07example\x03com\x00') -a.optcode == 24 and a.optlen == 36 and len(a.dnsdomains) == 2 and a.dnsdomains[0] == "toto.example.com." and a.dnsdomains[1] == "titi.example.com." - - -############ -############ -+ Test DHCP6 Option - IA_PD Prefix Option - -= DHCP6OptIAPrefix - Basic Instantiation -raw(DHCP6OptIAPrefix()) == b'\x00\x1a\x00\x19\x00\x00\x00\x00\x00\x00\x00\x000 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -#TODO : finish me - - -############ -############ -+ Test DHCP6 Option - Identity Association for Prefix Delegation - -= DHCP6OptIA_PD - Basic Instantiation -raw(DHCP6OptIA_PD()) == b'\x00\x19\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6OptIA_PD - Instantiation with a list of different opts: IA Address and Status Code (optlen automatic computation) -raw(DHCP6OptIA_PD(iaid=0x22222222, T1=0x33333333, T2=0x44444444, iapdopt=[DHCP6OptIAAddress(), DHCP6OptStatusCode(statuscode=0xff, statusmsg="Hello")])) == b'\x00\x19\x003""""3333DDDD\x00\x05\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\x00\x07\x00\xffHello' - -#TODO : finish me - - -############ -############ -+ Test DHCP6 Option - NIS Servers - -= DHCP6OptNISServers - Basic Instantiation -raw(DHCP6OptNISServers()) == b'\x00\x1b\x00\x00' - -= DHCP6OptNISServers - Basic Dissection -a = DHCP6OptNISServers(b'\x00\x1b\x00\x00') -a.optcode == 27 and a. optlen == 0 and a.nisservers == [] - -= DHCP6OptNISServers - Instantiation with specific values (1 address) -raw(DHCP6OptNISServers(nisservers = ["2001:db8::1"] )) == b'\x00\x1b\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptNISServers - Dissection with specific values (1 address) -a = DHCP6OptNISServers(b'\x00\x1b\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 27 and a.optlen == 16 and len(a.nisservers) == 1 and a.nisservers[0] == "2001:db8::1" - -= DHCP6OptNISServers - Instantiation with specific values (2 addresses) -raw(DHCP6OptNISServers(nisservers = ["2001:db8::1", "2001:db8::2"] )) == b'\x00\x1b\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptNISServers - Dissection with specific values (2 addresses) -a = DHCP6OptNISServers(b'\x00\x1b\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 27 and a.optlen == 32 and len(a.nisservers) == 2 and a.nisservers[0] == "2001:db8::1" and a.nisservers[1] == "2001:db8::2" - - -############ -############ -+ Test DHCP6 Option - NIS+ Servers - -= DHCP6OptNISPServers - Basic Instantiation -raw(DHCP6OptNISPServers()) == b'\x00\x1c\x00\x00' - -= DHCP6OptNISPServers - Basic Dissection -a = DHCP6OptNISPServers(b'\x00\x1c\x00\x00') -a.optcode == 28 and a. optlen == 0 and a.nispservers == [] - -= DHCP6OptNISPServers - Instantiation with specific values (1 address) -raw(DHCP6OptNISPServers(nispservers = ["2001:db8::1"] )) == b'\x00\x1c\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptNISPServers - Dissection with specific values (1 address) -a = DHCP6OptNISPServers(b'\x00\x1c\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 28 and a.optlen == 16 and len(a.nispservers) == 1 and a.nispservers[0] == "2001:db8::1" - -= DHCP6OptNISPServers - Instantiation with specific values (2 addresses) -raw(DHCP6OptNISPServers(nispservers = ["2001:db8::1", "2001:db8::2"] )) == b'\x00\x1c\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptNISPServers - Dissection with specific values (2 addresses) -a = DHCP6OptNISPServers(b'\x00\x1c\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 28 and a.optlen == 32 and len(a.nispservers) == 2 and a.nispservers[0] == "2001:db8::1" and a.nispservers[1] == "2001:db8::2" - - -############ -############ -+ Test DHCP6 Option - NIS Domain Name - -= DHCP6OptNISDomain - Basic Instantiation -raw(DHCP6OptNISDomain()) == b'\x00\x1d\x00\x00' - -= DHCP6OptNISDomain - Basic Dissection -a = DHCP6OptNISDomain(b'\x00\x1d\x00\x00') -a.optcode == 29 and a.optlen == 0 and a.nisdomain == b"" - -= DHCP6OptNISDomain - Instantiation with one domain name -raw(DHCP6OptNISDomain(nisdomain="toto.example.org")) == b'\x00\x1d\x00\x11\x04toto\x07example\x03org' - -= DHCP6OptNISDomain - Dissection with one domain name -a = DHCP6OptNISDomain(b'\x00\x1d\x00\x11\x04toto\x07example\x03org\x00') -a.optcode == 29 and a.optlen == 17 and a.nisdomain == b"toto.example.org" - -= DHCP6OptNISDomain - Instantiation with one domain with trailing dot -raw(DHCP6OptNISDomain(nisdomain="toto.example.org.")) == b'\x00\x1d\x00\x12\x04toto\x07example\x03org\x00' - - -############ -############ -+ Test DHCP6 Option - NIS+ Domain Name - -= DHCP6OptNISPDomain - Basic Instantiation -raw(DHCP6OptNISPDomain()) == b'\x00\x1e\x00\x00' - -= DHCP6OptNISPDomain - Basic Dissection -a = DHCP6OptNISPDomain(b'\x00\x1e\x00\x00') -a.optcode == 30 and a.optlen == 0 and a.nispdomain == b"" - -= DHCP6OptNISPDomain - Instantiation with one domain name -raw(DHCP6OptNISPDomain(nispdomain="toto.example.org")) == b'\x00\x1e\x00\x11\x04toto\x07example\x03org' - -= DHCP6OptNISPDomain - Dissection with one domain name -a = DHCP6OptNISPDomain(b'\x00\x1e\x00\x11\x04toto\x07example\x03org\x00') -a.optcode == 30 and a.optlen == 17 and a.nispdomain == b"toto.example.org" - -= DHCP6OptNISPDomain - Instantiation with one domain with trailing dot -raw(DHCP6OptNISPDomain(nispdomain="toto.example.org.")) == b'\x00\x1e\x00\x12\x04toto\x07example\x03org\x00' - - -############ -############ -+ Test DHCP6 Option - SNTP Servers - -= DHCP6OptSNTPServers - Basic Instantiation -raw(DHCP6OptSNTPServers()) == b'\x00\x1f\x00\x00' - -= DHCP6OptSNTPServers - Basic Dissection -a = DHCP6OptSNTPServers(b'\x00\x1f\x00\x00') -a.optcode == 31 and a. optlen == 0 and a.sntpservers == [] - -= DHCP6OptSNTPServers - Instantiation with specific values (1 address) -raw(DHCP6OptSNTPServers(sntpservers = ["2001:db8::1"] )) == b'\x00\x1f\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptSNTPServers - Dissection with specific values (1 address) -a = DHCP6OptSNTPServers(b'\x00\x1f\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 31 and a.optlen == 16 and len(a.sntpservers) == 1 and a.sntpservers[0] == "2001:db8::1" - -= DHCP6OptSNTPServers - Instantiation with specific values (2 addresses) -raw(DHCP6OptSNTPServers(sntpservers = ["2001:db8::1", "2001:db8::2"] )) == b'\x00\x1f\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptSNTPServers - Dissection with specific values (2 addresses) -a = DHCP6OptSNTPServers(b'\x00\x1f\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 31 and a.optlen == 32 and len(a.sntpservers) == 2 and a.sntpservers[0] == "2001:db8::1" and a.sntpservers[1] == "2001:db8::2" - -############ -############ -+ Test DHCP6 Option - Information Refresh Time - -= DHCP6OptInfoRefreshTime - Basic Instantiation -raw(DHCP6OptInfoRefreshTime()) == b'\x00 \x00\x04\x00\x01Q\x80' - -= DHCP6OptInfoRefreshTime - Basic Dissction -a = DHCP6OptInfoRefreshTime(b'\x00 \x00\x04\x00\x01Q\x80') -a.optcode == 32 and a.optlen == 4 and a.reftime == 86400 - -= DHCP6OptInfoRefreshTime - Instantiation with specific values -raw(DHCP6OptInfoRefreshTime(optlen=7, reftime=42)) == b'\x00 \x00\x07\x00\x00\x00*' - -############ -############ -+ Test DHCP6 Option - BCMCS Servers - -= DHCP6OptBCMCSServers - Basic Instantiation -raw(DHCP6OptBCMCSServers()) == b'\x00"\x00\x00' - -= DHCP6OptBCMCSServers - Basic Dissection -a = DHCP6OptBCMCSServers(b'\x00"\x00\x00') -a.optcode == 34 and a. optlen == 0 and a.bcmcsservers == [] - -= DHCP6OptBCMCSServers - Instantiation with specific values (1 address) -raw(DHCP6OptBCMCSServers(bcmcsservers = ["2001:db8::1"] )) == b'\x00"\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptBCMCSServers - Dissection with specific values (1 address) -a = DHCP6OptBCMCSServers(b'\x00"\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 34 and a.optlen == 16 and len(a.bcmcsservers) == 1 and a.bcmcsservers[0] == "2001:db8::1" - -= DHCP6OptBCMCSServers - Instantiation with specific values (2 addresses) -raw(DHCP6OptBCMCSServers(bcmcsservers = ["2001:db8::1", "2001:db8::2"] )) == b'\x00"\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptBCMCSServers - Dissection with specific values (2 addresses) -a = DHCP6OptBCMCSServers(b'\x00"\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 34 and a.optlen == 32 and len(a.bcmcsservers) == 2 and a.bcmcsservers[0] == "2001:db8::1" and a.bcmcsservers[1] == "2001:db8::2" - - -############ -############ -+ Test DHCP6 Option - BCMCS Domains - -= DHCP6OptBCMCSDomains - Basic Instantiation -raw(DHCP6OptBCMCSDomains()) == b'\x00!\x00\x00' - -= DHCP6OptBCMCSDomains - Basic Dissection -a = DHCP6OptBCMCSDomains(b'\x00!\x00\x00') -a.optcode == 33 and a.optlen == 0 and a.bcmcsdomains == [] - -= DHCP6OptBCMCSDomains - Instantiation with specific values (1 domain) -raw(DHCP6OptBCMCSDomains(bcmcsdomains=["toto.example.com."])) == b'\x00!\x00\x12\x04toto\x07example\x03com\x00' - -= DHCP6OptBCMCSDomains - Dissection with specific values (1 domain) -a = DHCP6OptBCMCSDomains(b'\x00!\x00\x12\x04toto\x07example\x03com\x00') -a.optcode == 33 and a.optlen == 18 and len(a.bcmcsdomains) == 1 and a.bcmcsdomains[0] == "toto.example.com." - -= DHCP6OptBCMCSDomains - Instantiation with specific values (2 domains) -raw(DHCP6OptBCMCSDomains(bcmcsdomains=["toto.example.com.", "titi.example.com."])) == b'\x00!\x00$\x04toto\x07example\x03com\x00\x04titi\x07example\x03com\x00' - -= DHCP6OptBCMCSDomains - Dissection with specific values (2 domains) -a = DHCP6OptBCMCSDomains(b'\x00!\x00$\x04toto\x07example\x03com\x00\x04titi\x07example\x03com\x00') -a.optcode == 33 and a.optlen == 36 and len(a.bcmcsdomains) == 2 and a.bcmcsdomains[0] == "toto.example.com." and a.bcmcsdomains[1] == "titi.example.com." - - -############ -############ -+ Test DHCP6 Option - Relay Agent Remote-ID - -= DHCP6OptRemoteID - Basic Instantiation -raw(DHCP6OptRemoteID()) == b'\x00%\x00\x04\x00\x00\x00\x00' - -= DHCP6OptRemoteID - Basic Dissection -a = DHCP6OptRemoteID(b'\x00%\x00\x04\x00\x00\x00\x00') -a.optcode == 37 and a.optlen == 4 and a.enterprisenum == 0 and a.remoteid == b"" - -= DHCP6OptRemoteID - Instantiation with specific values -raw(DHCP6OptRemoteID(enterprisenum=0xeeeeeeee, remoteid="someid")) == b'\x00%\x00\n\xee\xee\xee\xeesomeid' - -= DHCP6OptRemoteID - Dissection with specific values -a = DHCP6OptRemoteID(b'\x00%\x00\n\xee\xee\xee\xeesomeid') -a.optcode == 37 and a.optlen == 10 and a.enterprisenum == 0xeeeeeeee and a.remoteid == b"someid" - - -############ -############ -+ Test DHCP6 Option - Subscriber ID - -= DHCP6OptSubscriberID - Basic Instantiation -raw(DHCP6OptSubscriberID()) == b'\x00&\x00\x00' - -= DHCP6OptSubscriberID - Basic Dissection -a = DHCP6OptSubscriberID(b'\x00&\x00\x00') -a.optcode == 38 and a.optlen == 0 and a.subscriberid == b"" - -= DHCP6OptSubscriberID - Instantiation with specific values -raw(DHCP6OptSubscriberID(subscriberid="someid")) == b'\x00&\x00\x06someid' - -= DHCP6OptSubscriberID - Dissection with specific values -a = DHCP6OptSubscriberID(b'\x00&\x00\x06someid') -a.optcode == 38 and a.optlen == 6 and a.subscriberid == b"someid" - - -############ -############ -+ Test DHCP6 Option - Client FQDN - -= DHCP6OptClientFQDN - Basic Instantiation -raw(DHCP6OptClientFQDN()) == b"\x00'\x00\x01\x00" - -= DHCP6OptClientFQDN - Basic Dissection -a = DHCP6OptClientFQDN(b"\x00'\x00\x01\x00") -a.optcode == 39 and a.optlen == 1 and a.res == 0 and a.flags == 0 and a.fqdn == b"" - -= DHCP6OptClientFQDN - Instantiation with various flags combinations -raw(DHCP6OptClientFQDN(flags="S")) == b"\x00'\x00\x01\x01" and raw(DHCP6OptClientFQDN(flags="O")) == b"\x00'\x00\x01\x02" and raw(DHCP6OptClientFQDN(flags="N")) == b"\x00'\x00\x01\x04" and raw(DHCP6OptClientFQDN(flags="SON")) == b"\x00'\x00\x01\x07" and raw(DHCP6OptClientFQDN(flags="ON")) == b"\x00'\x00\x01\x06" - -= DHCP6OptClientFQDN - Instantiation with one fqdn -raw(DHCP6OptClientFQDN(fqdn="toto.example.org")) == b"\x00'\x00\x12\x00\x04toto\x07example\x03org" - -= DHCP6OptClientFQDN - Dissection with one fqdn -a = DHCP6OptClientFQDN(b"\x00'\x00\x12\x00\x04toto\x07example\x03org\x00") -a.optcode == 39 and a.optlen == 18 and a.res == 0 and a.flags == 0 and a.fqdn == b"toto.example.org" - - -############ -############ -+ Test DHCP6 Option PANA Auth Agent - -= DHCP6OptPanaAuthAgent - Basic Instantiation -raw(DHCP6OptPanaAuthAgent()) == b'\x00(\x00\x00' - -= DHCP6OptPanaAuthAgent - Basic Dissection -a = DHCP6OptPanaAuthAgent(b"\x00(\x00\x00") -a.optcode == 40 and a.optlen == 0 and a.paaaddr == [] - -= DHCP6OptPanaAuthAgent - Instantiation with specific values (1 address) -raw(DHCP6OptPanaAuthAgent(paaaddr=["2001:db8::1"])) == b'\x00(\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptPanaAuthAgent - Dissection with specific values (1 address) -a = DHCP6OptPanaAuthAgent(b'\x00(\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 40 and a.optlen == 16 and len(a.paaaddr) == 1 and a.paaaddr[0] == "2001:db8::1" - -= DHCP6OptPanaAuthAgent - Instantiation with specific values (2 addresses) -raw(DHCP6OptPanaAuthAgent(paaaddr=["2001:db8::1", "2001:db8::2"])) == b'\x00(\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptPanaAuthAgent - Dissection with specific values (2 addresses) -a = DHCP6OptPanaAuthAgent(b'\x00(\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 40 and a.optlen == 32 and len(a.paaaddr) == 2 and a.paaaddr[0] == "2001:db8::1" and a.paaaddr[1] == "2001:db8::2" - - -############ -############ -+ Test DHCP6 Option - New POSIX Time Zone - -= DHCP6OptNewPOSIXTimeZone - Basic Instantiation -raw(DHCP6OptNewPOSIXTimeZone()) == b'\x00)\x00\x00' - -= DHCP6OptNewPOSIXTimeZone - Basic Dissection -a = DHCP6OptNewPOSIXTimeZone(b'\x00)\x00\x00') -a.optcode == 41 and a.optlen == 0 and a.optdata == b"" - -= DHCP6OptNewPOSIXTimeZone - Instantiation with specific values -raw(DHCP6OptNewPOSIXTimeZone(optdata="EST5EDT4,M3.2.0/02:00,M11.1.0/02:00")) == b'\x00)\x00#EST5EDT4,M3.2.0/02:00,M11.1.0/02:00' - -= DHCP6OptNewPOSIXTimeZone - Dissection with specific values -a = DHCP6OptNewPOSIXTimeZone(b'\x00)\x00#EST5EDT4,M3.2.0/02:00,M11.1.0/02:00') -a.optcode == 41 and a.optlen == 35 and a.optdata == b"EST5EDT4,M3.2.0/02:00,M11.1.0/02:00" - - -############ -############ -+ Test DHCP6 Option - New TZDB Time Zone - -= DHCP6OptNewTZDBTimeZone - Basic Instantiation -raw(DHCP6OptNewTZDBTimeZone()) == b'\x00*\x00\x00' - -= DHCP6OptNewTZDBTimeZone - Basic Dissection -a = DHCP6OptNewTZDBTimeZone(b'\x00*\x00\x00') -a.optcode == 42 and a.optlen == 0 and a.optdata == b"" - -= DHCP6OptNewTZDBTimeZone - Instantiation with specific values -raw(DHCP6OptNewTZDBTimeZone(optdata="Europe/Zurich")) == b'\x00*\x00\rEurope/Zurich' - -= DHCP6OptNewTZDBTimeZone - Dissection with specific values -a = DHCP6OptNewTZDBTimeZone(b'\x00*\x00\rEurope/Zurich') -a.optcode == 42 and a.optlen == 13 and a.optdata == b"Europe/Zurich" - - -############ -############ -+ Test DHCP6 Option Relay Agent Echo Request Option - -= DHCP6OptRelayAgentERO - Basic Instantiation -raw(DHCP6OptRelayAgentERO()) == b'\x00+\x00\x04\x00\x17\x00\x18' - -= DHCP6OptRelayAgentERO - optlen field computation -raw(DHCP6OptRelayAgentERO(reqopts=[1,2,3,4])) == b'\x00+\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04' - -= DHCP6OptRelayAgentERO - instantiation with empty list -raw(DHCP6OptRelayAgentERO(reqopts=[])) == b'\x00+\x00\x00' - -= DHCP6OptRelayAgentERO - Basic dissection -a=DHCP6OptRelayAgentERO(b'\x00+\x00\x00') -a.optcode == 43 and a.optlen == 0 and a.reqopts == [23,24] - -= DHCP6OptRelayAgentERO - Dissection with specific value -a=DHCP6OptRelayAgentERO(b'\x00+\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04') -a.optcode == 43 and a.optlen == 8 and a.reqopts == [1,2,3,4] - - -############ -############ -+ Test DHCP6 Option LQ Client Link - -= DHCP6OptLQClientLink - Basic Instantiation -raw(DHCP6OptLQClientLink()) == b'\x000\x00\x00' - -= DHCP6OptLQClientLink - Basic Dissection -a = DHCP6OptLQClientLink(b"\x000\x00\x00") -a.optcode == 48 and a.optlen == 0 and a.linkaddress == [] - -= DHCP6OptLQClientLink - Instantiation with specific values (1 address) -raw(DHCP6OptLQClientLink(linkaddress=["2001:db8::1"])) == b'\x000\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' - -= DHCP6OptLQClientLink - Dissection with specific values (1 address) -a = DHCP6OptLQClientLink(b'\x000\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') -a.optcode == 48 and a.optlen == 16 and len(a.linkaddress) == 1 and a.linkaddress[0] == "2001:db8::1" - -= DHCP6OptLQClientLink - Instantiation with specific values (2 addresses) -raw(DHCP6OptLQClientLink(linkaddress=["2001:db8::1", "2001:db8::2"])) == b'\x000\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' - -= DHCP6OptLQClientLink - Dissection with specific values (2 addresses) -a = DHCP6OptLQClientLink(b'\x000\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02') -a.optcode == 48 and a.optlen == 32 and len(a.linkaddress) == 2 and a.linkaddress[0] == "2001:db8::1" and a.linkaddress[1] == "2001:db8::2" - -############ -############ -+ Test DHCP6 Option - Boot File URL - -= DHCP6OptBootFileUrl - Basic Instantiation -raw(DHCP6OptBootFileUrl()) == b'\x00;\x00\x00' - -= DHCP6OptBootFileUrl - Basic Dissection -a = DHCP6OptBootFileUrl(b'\x00;\x00\x00') -a.optcode == 59 and a.optlen == 0 and a.optdata == b"" - -= DHCP6OptBootFileUrl - Instantiation with specific values -raw(DHCP6OptBootFileUrl(optdata="http://wp.pl/file")) == b'\x00;\x00\x11http://wp.pl/file' - -= DHCP6OptBootFileUrl - Dissection with specific values -a = DHCP6OptBootFileUrl(b'\x00;\x00\x11http://wp.pl/file') -a.optcode == 59 and a.optlen == 17 and a.optdata == b"http://wp.pl/file" - - -############ -############ -+ Test DHCP6 Option - Client Arch Type - -= DHCP6OptClientArchType - Basic Instantiation -raw(DHCP6OptClientArchType()) -raw(DHCP6OptClientArchType()) == b'\x00=\x00\x00' - -= DHCP6OptClientArchType - Basic Dissection -a = DHCP6OptClientArchType(b'\x00=\x00\x00') -a.optcode == 61 and a.optlen == 0 and a.archtypes == [] - -= DHCP6OptClientArchType - Instantiation with specific value as just int -raw(DHCP6OptClientArchType(archtypes=7)) == b'\x00=\x00\x02\x00\x07' - -= DHCP6OptClientArchType - Instantiation with specific value as single item list of int -raw(DHCP6OptClientArchType(archtypes=[7])) == b'\x00=\x00\x02\x00\x07' - -= DHCP6OptClientArchType - Dissection with specific 1 value list -a = DHCP6OptClientArchType(b'\x00=\x00\x02\x00\x07') -a.optcode == 61 and a.optlen == 2 and a.archtypes == [7] - -= DHCP6OptClientArchType - Instantiation with specific value as 2 item list of int -raw(DHCP6OptClientArchType(archtypes=[7, 9])) == b'\x00=\x00\x04\x00\x07\x00\x09' - -= DHCP6OptClientArchType - Dissection with specific 2 values list -a = DHCP6OptClientArchType(b'\x00=\x00\x04\x00\x07\x00\x09') -a.optcode == 61 and a.optlen == 4 and a.archtypes == [7, 9] - - -############ -############ -+ Test DHCP6 Option - Client Network Inter Id - -= DHCP6OptClientNetworkInterId - Basic Instantiation -raw(DHCP6OptClientNetworkInterId()) -raw(DHCP6OptClientNetworkInterId()) == b'\x00>\x00\x03\x00\x00\x00' - -= DHCP6OptClientNetworkInterId - Basic Dissection -a = DHCP6OptClientNetworkInterId(b'\x00>\x00\x03\x00\x00\x00') -a.optcode == 62 and a.optlen == 3 and a.iitype == 0 and a.iimajor == 0 and a.iiminor == 0 - -= DHCP6OptClientNetworkInterId - Instantiation with specific values -raw(DHCP6OptClientNetworkInterId(iitype=1, iimajor=2, iiminor=3)) == b'\x00>\x00\x03\x01\x02\x03' - -= DHCP6OptClientNetworkInterId - Dissection with specific values -a = DHCP6OptClientNetworkInterId(b'\x00>\x00\x03\x01\x02\x03') -a.optcode == 62 and a.optlen == 3 and a.iitype == 1 and a.iimajor == 2 and a.iiminor == 3 - - -############ -############ -+ Test DHCP6 Option - ERP Domain - -= DHCP6OptERPDomain - Basic Instantiation -raw(DHCP6OptERPDomain()) == b'\x00A\x00\x00' - -= DHCP6OptERPDomain - Basic Dissection -a = DHCP6OptERPDomain(b'\x00A\x00\x00') -a.optcode == 65 and a.optlen == 0 and a.erpdomain == [] - -= DHCP6OptERPDomain - Instantiation with specific values (1 domain) -raw(DHCP6OptERPDomain(erpdomain=["toto.example.com."])) == b'\x00A\x00\x12\x04toto\x07example\x03com\x00' - -= DHCP6OptERPDomain - Dissection with specific values (1 domain) -a = DHCP6OptERPDomain(b'\x00A\x00\x12\x04toto\x07example\x03com\x00') -a.optcode == 65 and a.optlen == 18 and len(a.erpdomain) == 1 and a.erpdomain[0] == "toto.example.com." - -= DHCP6OptERPDomain - Instantiation with specific values (2 domains) -raw(DHCP6OptERPDomain(erpdomain=["toto.example.com.", "titi.example.com."])) == b'\x00A\x00$\x04toto\x07example\x03com\x00\x04titi\x07example\x03com\x00' - -= DHCP6OptERPDomain - Dissection with specific values (2 domains) -a = DHCP6OptERPDomain(b'\x00A\x00$\x04toto\x07example\x03com\x00\x04titi\x07example\x03com\x00') -a.optcode == 65 and a.optlen == 36 and len(a.erpdomain) == 2 and a.erpdomain[0] == "toto.example.com." and a.erpdomain[1] == "titi.example.com." - - -############ -############ -+ Test DHCP6 Option - Relay Supplied Option - -= DHCP6OptRelaySuppliedOpt - Basic Instantiation -raw(DHCP6OptRelaySuppliedOpt()) == b'\x00B\x00\x00' - -= DHCP6OptRelaySuppliedOpt - Basic Dissection -a = DHCP6OptRelaySuppliedOpt(b'\x00B\x00\x00') -a.optcode == 66 and a.optlen == 0 and a.relaysupplied == [] - -= DHCP6OptRelaySuppliedOpt - Instantiation with specific values -raw(DHCP6OptRelaySuppliedOpt(relaysupplied=DHCP6OptERPDomain(erpdomain=["toto.example.com."]))) == b'\x00B\x00\x16\x00A\x00\x12\x04toto\x07example\x03com\x00' - -= DHCP6OptRelaySuppliedOpt - Dissection with specific values -a = DHCP6OptRelaySuppliedOpt(b'\x00B\x00\x16\x00A\x00\x12\x04toto\x07example\x03com\x00') -a.optcode == 66 and a.optlen == 22 and len(a.relaysupplied) == 1 and isinstance(a.relaysupplied[0], DHCP6OptERPDomain) and a.relaysupplied[0].erpdomain[0] == "toto.example.com." - - -############ -############ -+ Test DHCP6 Option Client Link Layer address - -= Basic build & dissect -s = raw(DHCP6OptClientLinkLayerAddr()) -assert(s == b"\x00O\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00") - -p = DHCP6OptClientLinkLayerAddr(s) -assert(p.clladdr == "00:00:00:00:00:00") - -r = b"\x00O\x00\x08\x00\x01\x00\x01\x02\x03\x04\x05" -p = DHCP6OptClientLinkLayerAddr(r) -assert(p.clladdr == "00:01:02:03:04:05") - - -############ -############ -+ Test DHCP6 Option Virtual Subnet Selection - -= Basic build & dissect -s = raw(DHCP6OptVSS()) -assert(s == b"\x00D\x00\x01\xff") - -p = DHCP6OptVSS(s) -assert(p.type == 255) - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Solicit - -= DHCP6_Solicit - Basic Instantiation -raw(DHCP6_Solicit()) == b'\x01\x00\x00\x00' - -= DHCP6_Solicit - Basic Dissection -a = DHCP6_Solicit(b'\x01\x00\x00\x00') -a.msgtype == 1 and a.trid == 0 - -= DHCP6_Solicit - Basic test of DHCP6_solicit.hashret() -DHCP6_Solicit().hashret() == b'\x00\x00\x00' - -= DHCP6_Solicit - Test of DHCP6_solicit.hashret() with specific values -DHCP6_Solicit(trid=0xbbccdd).hashret() == b'\xbb\xcc\xdd' - -= DHCP6_Solicit - UDP ports overload -a=UDP()/DHCP6_Solicit() -a.sport == 546 and a.dport == 547 - -= DHCP6_Solicit - Dispatch based on UDP port -a=UDP(raw(UDP()/DHCP6_Solicit())) -isinstance(a.payload, DHCP6_Solicit) - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Advertise - -= DHCP6_Advertise - Basic Instantiation -raw(DHCP6_Advertise()) == b'\x02\x00\x00\x00' - -= DHCP6_Advertise - Basic test of DHCP6_solicit.hashret() -DHCP6_Advertise().hashret() == b'\x00\x00\x00' - -= DHCP6_Advertise - Test of DHCP6_Advertise.hashret() with specific values -DHCP6_Advertise(trid=0xbbccdd).hashret() == b'\xbb\xcc\xdd' - -= DHCP6_Advertise - Basic test of answers() with solicit message -a = DHCP6_Solicit() -b = DHCP6_Advertise() -a > b - -= DHCP6_Advertise - Test of answers() with solicit message -a = DHCP6_Solicit(trid=0xbbccdd) -b = DHCP6_Advertise(trid=0xbbccdd) -a > b - -= DHCP6_Advertise - UDP ports overload -a=UDP()/DHCP6_Advertise() -a.sport == 547 and a.dport == 546 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Request - -= DHCP6_Request - Basic Instantiation -raw(DHCP6_Request()) == b'\x03\x00\x00\x00' - -= DHCP6_Request - Basic Dissection -a=DHCP6_Request(b'\x03\x00\x00\x00') -a.msgtype == 3 and a.trid == 0 - -= DHCP6_Request - UDP ports overload -a=UDP()/DHCP6_Request() -a.sport == 546 and a.dport == 547 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Confirm - -= DHCP6_Confirm - Basic Instantiation -raw(DHCP6_Confirm()) == b'\x04\x00\x00\x00' - -= DHCP6_Confirm - Basic Dissection -a=DHCP6_Confirm(b'\x04\x00\x00\x00') -a.msgtype == 4 and a.trid == 0 - -= DHCP6_Confirm - UDP ports overload -a=UDP()/DHCP6_Confirm() -a.sport == 546 and a.dport == 547 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Renew - -= DHCP6_Renew - Basic Instantiation -raw(DHCP6_Renew()) == b'\x05\x00\x00\x00' - -= DHCP6_Renew - Basic Dissection -a=DHCP6_Renew(b'\x05\x00\x00\x00') -a.msgtype == 5 and a.trid == 0 - -= DHCP6_Renew - UDP ports overload -a=UDP()/DHCP6_Renew() -a.sport == 546 and a.dport == 547 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Rebind - -= DHCP6_Rebind - Basic Instantiation -raw(DHCP6_Rebind()) == b'\x06\x00\x00\x00' - -= DHCP6_Rebind - Basic Dissection -a=DHCP6_Rebind(b'\x06\x00\x00\x00') -a.msgtype == 6 and a.trid == 0 - -= DHCP6_Rebind - UDP ports overload -a=UDP()/DHCP6_Rebind() -a.sport == 546 and a.dport == 547 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Reply - -= DHCP6_Reply - Basic Instantiation -raw(DHCP6_Reply()) == b'\x07\x00\x00\x00' - -= DHCP6_Reply - Basic Dissection -a=DHCP6_Reply(b'\x07\x00\x00\x00') -a.msgtype == 7 and a.trid == 0 - -= DHCP6_Reply - UDP ports overload -a=UDP()/DHCP6_Reply() -a.sport == 547 and a.dport == 546 - -= DHCP6_Reply - Answers - -assert not DHCP6_Reply(trid=0).answers(DHCP6_Request(trid=1)) -assert DHCP6_Reply(trid=1).answers(DHCP6_Request(trid=1)) - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Release - -= DHCP6_Release - Basic Instantiation -raw(DHCP6_Release()) == b'\x08\x00\x00\x00' - -= DHCP6_Release - Basic Dissection -a=DHCP6_Release(b'\x08\x00\x00\x00') -a.msgtype == 8 and a.trid == 0 - -= DHCP6_Release - UDP ports overload -a=UDP()/DHCP6_Release() -a.sport == 546 and a.dport == 547 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Decline - -= DHCP6_Decline - Basic Instantiation -raw(DHCP6_Decline()) == b'\x09\x00\x00\x00' - -= DHCP6_Confirm - Basic Dissection -a=DHCP6_Confirm(b'\x09\x00\x00\x00') -a.msgtype == 9 and a.trid == 0 - -= DHCP6_Decline - UDP ports overload -a=UDP()/DHCP6_Decline() -a.sport == 546 and a.dport == 547 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_Reconf - -= DHCP6_Reconf - Basic Instantiation -raw(DHCP6_Reconf()) == b'\x0A\x00\x00\x00' - -= DHCP6_Reconf - Basic Dissection -a=DHCP6_Reconf(b'\x0A\x00\x00\x00') -a.msgtype == 10 and a.trid == 0 - -= DHCP6_Reconf - UDP ports overload -a=UDP()/DHCP6_Reconf() -a.sport == 547 and a.dport == 546 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_InfoRequest - -= DHCP6_InfoRequest - Basic Instantiation -raw(DHCP6_InfoRequest()) == b'\x0B\x00\x00\x00' - -= DHCP6_InfoRequest - Basic Dissection -a=DHCP6_InfoRequest(b'\x0B\x00\x00\x00') -a.msgtype == 11 and a.trid == 0 - -= DHCP6_InfoRequest - UDP ports overload -a=UDP()/DHCP6_InfoRequest() -a.sport == 546 and a.dport == 547 - - -############ -############ -+ Test DHCP6 Messages - DHCP6_RelayForward - -= DHCP6_RelayForward - Basic Instantiation -raw(DHCP6_RelayForward()) == b'\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6_RelayForward - Basic Dissection -a=DHCP6_RelayForward(b'\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.msgtype == 12 and a.hopcount == 0 and a.linkaddr == "::" and a.peeraddr == "::" - -= DHCP6_RelayForward - Dissection with options -a = DHCP6_RelayForward(b'\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x04\x03\x01\x00\x00') -a.msgtype == 12 and DHCP6OptRelayMsg in a and isinstance(a.message, DHCP6_Request) - -= DHCP6_RelayForward - Advanced dissection -s = b'`\x00\x00\x00\x002\x11@\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x02#\x02#\x002\xf0\xaf\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x04\x01\x00\x00\x00' -p = IPv6(s) -assert DHCP6OptRelayMsg in p and isinstance(p.message, DHCP6_Solicit) - - -############ -############ -+ Test DHCP6 Messages - DHCP6OptRelayMsg - -= DHCP6OptRelayMsg - Basic Instantiation -raw(DHCP6OptRelayMsg(optcode=37)) == b'\x00%\x00\x04\x00\x00\x00\x00' - -= DHCP6OptRelayMsg - Basic Dissection -a = DHCP6OptRelayMsg(b'\x00\r\x00\x00') -a.optcode == 13 and a.optlen == 0 and isinstance(a.message, DHCP6) - -= DHCP6OptRelayMsg - Embedded DHCP6 packet Instantiation -raw(DHCP6OptRelayMsg(message=DHCP6_Solicit())) == b'\x00\t\x00\x04\x01\x00\x00\x00' - -= DHCP6OptRelayMsg - Embedded DHCP6 packet Dissection -p = DHCP6OptRelayMsg(b'\x00\t\x00\x04\x01\x00\x00\x00') -isinstance(p.message, DHCP6_Solicit) - - -############ -############ -+ Test DHCP6 Messages - DHCP6_RelayReply - -= DHCP6_RelayReply - Basic Instantiation -raw(DHCP6_RelayReply()) == b'\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= DHCP6_RelayReply - Basic Dissection -a=DHCP6_RelayReply(b'\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -a.msgtype == 13 and a.hopcount == 0 and a.linkaddr == "::" and a.peeraddr == "::" - - -############ -############ -+ Home Agent Address Discovery - -= in6_getha() -in6_getha('2001:db8::') == '2001:db8::fdff:ffff:ffff:fffe' - -= ICMPv6HAADRequest - build/dissection -p = IPv6(raw(IPv6(dst=in6_getha('2001:db8::'), src='2001:db8::1')/ICMPv6HAADRequest(id=42))) -p.cksum == 0x9620 and p.dst == '2001:db8::fdff:ffff:ffff:fffe' and p.R == 1 - -= ICMPv6HAADReply - build/dissection -p = IPv6(raw(IPv6(dst='2001:db8::1', src='2001:db8::42')/ICMPv6HAADReply(id=42, addresses=['2001:db8::2', '2001:db8::3']))) -p.cksum = 0x3747 and p.addresses == [ '2001:db8::2', '2001:db8::3' ] - -= ICMPv6HAADRequest / ICMPv6HAADReply - build/dissection -a=ICMPv6HAADRequest(id=42) -b=ICMPv6HAADReply(id=42) -not a < b and a > b - - -############ -############ -+ Mobile Prefix Solicitation/Advertisement - -= ICMPv6MPSol - build (default values) - -s = b'`\x00\x00\x00\x00\x08:@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x92\x00m\xbb\x00\x00\x00\x00' -raw(IPv6()/ICMPv6MPSol()) == s - -= ICMPv6MPSol - dissection (default values) -p = IPv6(s) -p[ICMPv6MPSol].type == 146 and p[ICMPv6MPSol].cksum == 0x6dbb and p[ICMPv6MPSol].id == 0 - -= ICMPv6MPSol - build -s = b'`\x00\x00\x00\x00\x08:@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x92\x00(\x08\x00\x08\x00\x00' -raw(IPv6()/ICMPv6MPSol(cksum=0x2808, id=8)) == s - -= ICMPv6MPSol - dissection -p = IPv6(s) -p[ICMPv6MPSol].cksum == 0x2808 and p[ICMPv6MPSol].id == 8 - -= ICMPv6MPAdv - build (default values) -s = b'`\x00\x00\x00\x00(:@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x93\x00\xe8\xd6\x00\x00\x80\x00\x03\x04\x00\xc0\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -raw(IPv6()/ICMPv6MPAdv()/ICMPv6NDOptPrefixInfo()) == s - -= ICMPv6MPAdv - dissection (default values) -p = IPv6(s) -p[ICMPv6MPAdv].type == 147 and p[ICMPv6MPAdv].cksum == 0xe8d6 and p[ICMPv6NDOptPrefixInfo].prefix == '::' - -= ICMPv6MPAdv - build -s = b'`\x00\x00\x00\x00(:@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x93\x00(\x07\x00*@\x00\x03\x04\x00@\xff\xff\xff\xff\x00\x00\x00\x0c\x00\x00\x00\x00 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' -raw(IPv6()/ICMPv6MPAdv(cksum=0x2807, flags=1, id=42)/ICMPv6NDOptPrefixInfo(prefix='2001:db8::1', L=0, preferredlifetime=12)) == s - -= ICMPv6MPAdv - dissection -p = IPv6(s) -p[ICMPv6MPAdv].cksum == 0x2807 and p[ICMPv6MPAdv].flags == 1 and p[ICMPv6MPAdv].id == 42 and p[ICMPv6NDOptPrefixInfo].prefix == '2001:db8::1' and p[ICMPv6NDOptPrefixInfo].preferredlifetime == 12 - - -############ -############ -+ Type 2 Routing Header - -= IPv6ExtHdrRouting - type 2 - build/dissection -p = IPv6(raw(IPv6(dst='2001:db8::1', src='2001:db8::2')/IPv6ExtHdrRouting(type=2, addresses=['2001:db8::3'])/ICMPv6EchoRequest())) -p.type == 2 and len(p.addresses) == 1 and p.cksum == 0x2446 - -= IPv6ExtHdrRouting - type 2 - hashret - -p = IPv6()/IPv6ExtHdrRouting(addresses=["2001:db8::1", "2001:db8::2"])/ICMPv6EchoRequest() -p.hashret() == b" \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00:\x00\x00\x00\x00" - - -############ -############ -+ Mobility Options - Binding Refresh Advice - -= MIP6OptBRAdvice - build (default values) -s = b'\x02\x02\x00\x00' -raw(MIP6OptBRAdvice()) == s - -= MIP6OptBRAdvice - dissection (default values) -p = MIP6OptBRAdvice(s) -p.otype == 2 and p.olen == 2 and p.rinter == 0 - -= MIP6OptBRAdvice - build -s = b'\x03*\n\xf7' -raw(MIP6OptBRAdvice(otype=3, olen=42, rinter=2807)) == s - -= MIP6OptBRAdvice - dissection -p = MIP6OptBRAdvice(s) -p.otype == 3 and p.olen == 42 and p.rinter == 2807 - - -############ -############ -+ Mobility Options - Alternate Care-of Address - -= MIP6OptAltCoA - build (default values) -s = b'\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -raw(MIP6OptAltCoA()) == s - -= MIP6OptAltCoA - dissection (default values) -p = MIP6OptAltCoA(s) -p.otype == 3 and p.olen == 16 and p.acoa == '::' - -= MIP6OptAltCoA - build -s = b'*\x08 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' -raw(MIP6OptAltCoA(otype=42, olen=8, acoa='2001:db8::1')) == s - -= MIP6OptAltCoA - dissection -p = MIP6OptAltCoA(s) -p.otype == 42 and p.olen == 8 and p.acoa == '2001:db8::1' - - -############ -############ -+ Mobility Options - Nonce Indices - -= MIP6OptNonceIndices - build (default values) -s = b'\x04\x10\x00\x00\x00\x00' -raw(MIP6OptNonceIndices()) == s - -= MIP6OptNonceIndices - dissection (default values) -p = MIP6OptNonceIndices(s) -p.otype == 4 and p.olen == 16 and p.hni == 0 and p.coni == 0 - -= MIP6OptNonceIndices - build -s = b'\x04\x12\x00\x13\x00\x14' -raw(MIP6OptNonceIndices(olen=18, hni=19, coni=20)) == s - -= MIP6OptNonceIndices - dissection -p = MIP6OptNonceIndices(s) -p.hni == 19 and p.coni == 20 - - -############ -############ -+ Mobility Options - Binding Authentication Data - -= MIP6OptBindingAuthData - build (default values) -s = b'\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -raw(MIP6OptBindingAuthData()) == s - -= MIP6OptBindingAuthData - dissection (default values) -p = MIP6OptBindingAuthData(s) -p.otype == 5 and p.olen == 16 and p.authenticator == 0 - -= MIP6OptBindingAuthData - build -s = b'\x05*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\xf7' -raw(MIP6OptBindingAuthData(olen=42, authenticator=2807)) == s - -= MIP6OptBindingAuthData - dissection -p = MIP6OptBindingAuthData(s) -p.otype == 5 and p.olen == 42 and p.authenticator == 2807 - - -############ -############ -+ Mobility Options - Mobile Network Prefix - -= MIP6OptMobNetPrefix - build (default values) -s = b'\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -raw(MIP6OptMobNetPrefix()) == s - -= MIP6OptMobNetPrefix - dissection (default values) -p = MIP6OptMobNetPrefix(s) -p.otype == 6 and p.olen == 18 and p.plen == 64 and p.prefix == '::' - -= MIP6OptMobNetPrefix - build -s = b'\x06*\x02 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -raw(MIP6OptMobNetPrefix(olen=42, reserved=2, plen=32, prefix='2001:db8::')) == s - -= MIP6OptMobNetPrefix - dissection -p = MIP6OptMobNetPrefix(s) -p.olen == 42 and p.reserved == 2 and p.plen == 32 and p.prefix == '2001:db8::' - - -############ -############ -+ Mobility Options - Link-Layer Address (MH-LLA) - -= MIP6OptLLAddr - basic build -raw(MIP6OptLLAddr()) == b'\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00' - -= MIP6OptLLAddr - basic dissection -p = MIP6OptLLAddr(b'\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00') -p.otype == 7 and p.olen == 7 and p.ocode == 2 and p.pad == 0 and p.lla == "00:00:00:00:00:00" - -= MIP6OptLLAddr - build with specific values -raw(MIP6OptLLAddr(olen=42, ocode=4, pad=0xff, lla='EE:EE:EE:EE:EE:EE')) == b'\x07*\x04\xff\xee\xee\xee\xee\xee\xee' - -= MIP6OptLLAddr - dissection with specific values -p = MIP6OptLLAddr(b'\x07*\x04\xff\xee\xee\xee\xee\xee\xee') - -raw(MIP6OptLLAddr(olen=42, ocode=4, pad=0xff, lla='EE:EE:EE:EE:EE:EE')) -p.otype == 7 and p.olen == 42 and p.ocode == 4 and p.pad == 0xff and p.lla == "ee:ee:ee:ee:ee:ee" - - -############ -############ -+ Mobility Options - Mobile Node Identifier - -= MIP6OptMNID - basic build -raw(MIP6OptMNID()) == b'\x08\x01\x01' - -= MIP6OptMNID - basic dissection -p = MIP6OptMNID(b'\x08\x01\x01') -p.otype == 8 and p.olen == 1 and p.subtype == 1 and p.id == b"" - -= MIP6OptMNID - build with specific values -raw(MIP6OptMNID(subtype=42, id="someid")) == b'\x08\x07*someid' - -= MIP6OptMNID - dissection with specific values -p = MIP6OptMNID(b'\x08\x07*someid') -p.otype == 8 and p.olen == 7 and p.subtype == 42 and p.id == b"someid" - - - -############ -############ -+ Mobility Options - Message Authentication - -= MIP6OptMsgAuth - basic build -raw(MIP6OptMsgAuth()) == b'\x09\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA' - -= MIP6OptMsgAuth - basic dissection -p = MIP6OptMsgAuth(b'\x09\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA') -p.otype == 9 and p.olen == 17 and p.subtype == 1 and p.mspi == 0 and p.authdata == b"A"*12 - -= MIP6OptMsgAuth - build with specific values -raw(MIP6OptMsgAuth(authdata="B"*16, mspi=0xeeeeeeee, subtype=0xff)) == b'\t\x15\xff\xee\xee\xee\xeeBBBBBBBBBBBBBBBB' - -= MIP6OptMsgAuth - dissection with specific values -p = MIP6OptMsgAuth(b'\t\x15\xff\xee\xee\xee\xeeBBBBBBBBBBBBBBBB') -p.otype == 9 and p.olen == 21 and p.subtype == 255 and p.mspi == 0xeeeeeeee and p.authdata == b"B"*16 - - -############ -############ -+ Mobility Options - Replay Protection - -= MIP6OptReplayProtection - basic build -raw(MIP6OptReplayProtection()) == b'\n\x08\x00\x00\x00\x00\x00\x00\x00\x00' - -= MIP6OptReplayProtection - basic dissection -p = MIP6OptReplayProtection(b'\n\x08\x00\x00\x00\x00\x00\x00\x00\x00') -p.otype == 10 and p.olen == 8 and p.timestamp == 0 - -= MIP6OptReplayProtection - build with specific values -s = raw(MIP6OptReplayProtection(olen=42, timestamp=(72*31536000)<<32)) -s == b'\n*\x87V|\x00\x00\x00\x00\x00' - -= MIP6OptReplayProtection - dissection with specific values -p = MIP6OptReplayProtection(s) -p.otype == 10 and p.olen == 42 and p.timestamp == 9752118382559232000 -p.fields_desc[-1].i2repr("", p.timestamp) == 'Mon, 13 Dec 1971 23:50:39 +0000 (9752118382559232000)' - - -############ -############ -+ Mobility Options - CGA Parameters -= MIP6OptCGAParams - - -############ -############ -+ Mobility Options - Signature -= MIP6OptSignature - - -############ -############ -+ Mobility Options - Permanent Home Keygen Token -= MIP6OptHomeKeygenToken - - -############ -############ -+ Mobility Options - Care-of Test Init -= MIP6OptCareOfTestInit - - -############ -############ -+ Mobility Options - Care-of Test -= MIP6OptCareOfTest - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptBRAdvice -= Mobility Options - Automatic Padding - MIP6OptBRAdvice -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptBRAdvice()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x02\x02\x00\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x00\x02\x02\x00\x00\x01\x04\x00\x00\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x02\x02\x00\x00\x01\x04\x00\x00\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x00\x02\x02\x00\x00\x01\x02\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x02\x02\x00\x00\x01\x02\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x00\x02\x02\x00\x00\x01\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x02\x02\x00\x00\x01\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x00\x02\x02\x00\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptBRAdvice()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x02\x02\x00\x00' -a and b and c and d and e and g and h and i and j - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptAltCoA -= Mobility Options - Automatic Padding - MIP6OptAltCoA -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptAltCoA()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptAltCoA()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptAltCoA()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptAltCoA()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x01\x05\x00\x00\x00\x00\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptAltCoA()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x01\x04\x00\x00\x00\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptAltCoA()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x01\x03\x00\x00\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptAltCoA()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x01\x02\x00\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptAltCoA()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x01\x01\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptAltCoA()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x01\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptNonceIndices -= Mobility Options - Automatic Padding - MIP6OptNonceIndices -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptNonceIndices()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x04\x10\x00\x00\x00\x00\x01\x04\x00\x00\x00\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptNonceIndices()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x00\x04\x10\x00\x00\x00\x00\x01\x02\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptNonceIndices()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x04\x10\x00\x00\x00\x00\x01\x02\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptNonceIndices()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x00\x04\x10\x00\x00\x00\x00\x01\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptNonceIndices()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x04\x10\x00\x00\x00\x00\x01\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptNonceIndices()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x00\x04\x10\x00\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptNonceIndices()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x04\x10\x00\x00\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptNonceIndices()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x00\x04\x10\x00\x00\x00\x00\x01\x04\x00\x00\x00\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptNonceIndices()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x04\x10\x00\x00\x00\x00\x01\x04\x00\x00\x00\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptBindingAuthData -= Mobility Options - Automatic Padding - MIP6OptBindingAuthData -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptBindingAuthData()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptBindingAuthData()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x01\x03\x00\x00\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptBindingAuthData()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x01\x02\x00\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptBindingAuthData()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x01\x01\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptBindingAuthData()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x01\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptBindingAuthData()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptBindingAuthData()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptBindingAuthData()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x01\x05\x00\x00\x00\x00\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptBindingAuthData()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x01\x04\x00\x00\x00\x00\x05\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptMobNetPrefix -= Mobility Options - Automatic Padding - MIP6OptMobNetPrefix -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptMobNetPrefix()])) == b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x01\x05\x00\x00\x00\x00\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x01\x04\x00\x00\x00\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x01\x03\x00\x00\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x01\x02\x00\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x01\x01\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x01\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptMobNetPrefix()])) == b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptLLAddr -= Mobility Options - Automatic Padding - MIP6OptLLAddr -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptLLAddr()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptLLAddr()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptLLAddr()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptLLAddr()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x05\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptLLAddr()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x04\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptLLAddr()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptLLAddr()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptLLAddr()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptLLAddr()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptMNID -= Mobility Options - Automatic Padding - MIP6OptMNID -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptMNID()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x08\x01\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptMNID()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x08\x01\x01' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptMNID()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x08\x01\x01\x01\x05\x00\x00\x00\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptMNID()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x08\x01\x01\x01\x04\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptMNID()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x08\x01\x01\x01\x03\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptMNID()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x08\x01\x01\x01\x02\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptMNID()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x08\x01\x01\x01\x01\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptMNID()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x08\x01\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptMNID()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x08\x01\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptMsgAuth -= Mobility Options - Automatic Padding - MIP6OptMsgAuth -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptMsgAuth()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptMsgAuth()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptMsgAuth()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x01\x01\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA\x01\x02\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptMsgAuth()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x01\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA\x01\x02\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptMsgAuth()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA\x01\x02\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptMsgAuth()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA\x01\x02\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptMsgAuth()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x01\x01\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptMsgAuth()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x01\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptMsgAuth()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x00\t\x11\x01\x00\x00\x00\x00AAAAAAAAAAAA' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptReplayProtection -= Mobility Options - Automatic Padding - MIP6OptReplayProtection -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptReplayProtection()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptReplayProtection()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x01\x03\x00\x00\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptReplayProtection()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x01\x02\x00\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptReplayProtection()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x01\x01\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptReplayProtection()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x01\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptReplayProtection()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptReplayProtection()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptReplayProtection()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x01\x05\x00\x00\x00\x00\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptReplayProtection()])) ==b';\x04\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x01\x04\x00\x00\x00\x00\n\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptCGAParamsReq -= Mobility Options - Automatic Padding - MIP6OptCGAParamsReq -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptCGAParamsReq()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x0b\x00\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptCGAParamsReq()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x0b\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptCGAParamsReq()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x0b\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptCGAParamsReq()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x0b\x00\x01\x05\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptCGAParamsReq()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x0b\x00\x01\x04\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptCGAParamsReq()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x0b\x00\x01\x03\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptCGAParamsReq()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x0b\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptCGAParamsReq()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x0b\x00\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptCGAParamsReq()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x0b\x00\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptCGAParams -= Mobility Options - Automatic Padding - MIP6OptCGAParams -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptCGAParams()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x0c\x00\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptCGAParams()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x0c\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptCGAParams()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x0c\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptCGAParams()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x0c\x00\x01\x05\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptCGAParams()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x0c\x00\x01\x04\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptCGAParams()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x0c\x00\x01\x03\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptCGAParams()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x0c\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptCGAParams()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x0c\x00\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptCGAParams()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x0c\x00\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptSignature -= Mobility Options - Automatic Padding - MIP6OptSignature -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptSignature()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\r\x00\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptSignature()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\r\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptSignature()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\r\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptSignature()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\r\x00\x01\x05\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptSignature()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\r\x00\x01\x04\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptSignature()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\r\x00\x01\x03\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptSignature()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\r\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptSignature()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\r\x00\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptSignature()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\r\x00\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptHomeKeygenToken -= Mobility Options - Automatic Padding - MIP6OptHomeKeygenToken -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptHomeKeygenToken()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x0e\x00\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptHomeKeygenToken()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x0e\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptHomeKeygenToken()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x0e\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptHomeKeygenToken()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x0e\x00\x01\x05\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptHomeKeygenToken()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x0e\x00\x01\x04\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptHomeKeygenToken()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x0e\x00\x01\x03\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptHomeKeygenToken()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x0e\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptHomeKeygenToken()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x0e\x00\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptHomeKeygenToken()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x0e\x00\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptCareOfTestInit -= Mobility Options - Automatic Padding - MIP6OptCareOfTestInit -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptCareOfTestInit()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x0f\x00\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptCareOfTestInit()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x0f\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptCareOfTestInit()])) ==b';\x01\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x0f\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptCareOfTestInit()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x0f\x00\x01\x05\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptCareOfTestInit()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x0f\x00\x01\x04\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptCareOfTestInit()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x0f\x00\x01\x03\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptCareOfTestInit()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x0f\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptCareOfTestInit()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x0f\x00\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptCareOfTestInit()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x0f\x00\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Mobility Options - Automatic Padding - MIP6OptCareOfTest -= Mobility Options - Automatic Padding - MIP6OptCareOfTest -a = raw(MIP6MH_BU(seq=0x4242, options=[MIP6OptCareOfTest()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00' -b = raw(MIP6MH_BU(seq=0x4242, options=[Pad1(),MIP6OptCareOfTest()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00' -c = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*0),MIP6OptCareOfTest()])) ==b';\x02\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00' -d = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*1),MIP6OptCareOfTest()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x01\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x05\x00\x00\x00\x00\x00' -e = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*2),MIP6OptCareOfTest()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x02\x00\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x04\x00\x00\x00\x00' -g = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*3),MIP6OptCareOfTest()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x03\x00\x00\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00' -h = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*4),MIP6OptCareOfTest()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x04\x00\x00\x00\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00' -i = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*5),MIP6OptCareOfTest()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x05\x00\x00\x00\x00\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00' -j = raw(MIP6MH_BU(seq=0x4242, options=[PadN(optdata=b'\x00'*6),MIP6OptCareOfTest()])) ==b';\x03\x05\x00\x00\x00BB\xd0\x00\x00\x03\x01\x06\x00\x00\x00\x00\x00\x00\x10\x08\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00' -a and b and c and d and e and g and h and i and j - - -############ -############ -+ Binding Refresh Request Message -= MIP6MH_BRR - Build (default values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_BRR()) == b'`\x00\x00\x00\x00\x08\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x00\x00\x00h\xfb\x00\x00' - -= MIP6MH_BRR - Build with specific values -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_BRR(nh=0xff, res=0xee, res2=0xaaaa, options=[MIP6OptLLAddr(), MIP6OptAltCoA()])) == b'`\x00\x00\x00\x00(\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\x04\x00\xee\xec$\xaa\xaa\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= MIP6MH_BRR - Basic dissection -a=IPv6(b'`\x00\x00\x00\x00\x08\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x00\x00\x00h\xfb\x00\x00') -b=a.payload -a.nh == 135 and isinstance(b, MIP6MH_BRR) and b.nh == 59 and b.len == 0 and b.mhtype == 0 and b.res == 0 and b.cksum == 0x68fb and b.res2 == 0 and b.options == [] - -= MIP6MH_BRR - Dissection with specific values -a=IPv6(b'`\x00\x00\x00\x00(\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\x04\x00\xee\xec$\xaa\xaa\x07\x07\x02\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -b=a.payload -a.nh == 135 and isinstance(b, MIP6MH_BRR) and b.nh == 0xff and b.len == 4 and b.mhtype == 0 and b.res == 238 and b.cksum == 0xec24 and b.res2 == 43690 and len(b.options) == 3 and isinstance(b.options[0], MIP6OptLLAddr) and isinstance(b.options[1], PadN) and isinstance(b.options[2], MIP6OptAltCoA) - -= MIP6MH_BRR / MIP6MH_BU / MIP6MH_BA hashret() and answers() -hoa="2001:db8:9999::1" -coa="2001:db8:7777::1" -cn="2001:db8:8888::1" -ha="2001db8:6666::1" -a=IPv6(raw(IPv6(src=cn, dst=hoa)/MIP6MH_BRR())) -b=IPv6(raw(IPv6(src=coa, dst=cn)/IPv6ExtHdrDestOpt(options=HAO(hoa=hoa))/MIP6MH_BU(flags=0x01))) -b2=IPv6(raw(IPv6(src=coa, dst=cn)/IPv6ExtHdrDestOpt(options=HAO(hoa=hoa))/MIP6MH_BU(flags=~0x01))) -c=IPv6(raw(IPv6(src=cn, dst=coa)/IPv6ExtHdrRouting(type=2, addresses=[hoa])/MIP6MH_BA())) -b.answers(a) and not a.answers(b) and c.answers(b) and not b.answers(c) and not c.answers(b2) - -len(b[IPv6ExtHdrDestOpt].options) == 2 - - -############ -############ -+ Home Test Init Message - -= MIP6MH_HoTI - Build (default values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_HoTI()) == b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x01\x00g\xf2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= MIP6MH_HoTI - Dissection (default values) -a=IPv6(b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x01\x00g\xf2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -b = a.payload -a.nh == 135 and isinstance(b, MIP6MH_HoTI) and b.nh==59 and b.mhtype == 1 and b.len== 1 and b.res == 0 and b.cksum == 0x67f2 and b.cookie == b'\x00'*8 - - -= MIP6MH_HoTI - Build (specific values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_HoTI(res=0x77, cksum=0x8899, cookie=b"\xAA"*8)) == b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x01w\x88\x99\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa' - -= MIP6MH_HoTI - Dissection (specific values) -a=IPv6(b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x01w\x88\x99\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa') -b=a.payload -a.nh == 135 and isinstance(b, MIP6MH_HoTI) and b.nh==59 and b.mhtype == 1 and b.len == 1 and b.res == 0x77 and b.cksum == 0x8899 and b.cookie == b'\xAA'*8 - - -############ -############ -+ Care-of Test Init Message - -= MIP6MH_CoTI - Build (default values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_CoTI()) == b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x02\x00f\xf2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= MIP6MH_CoTI - Dissection (default values) -a=IPv6(b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x02\x00f\xf2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -b = a.payload -a.nh == 135 and isinstance(b, MIP6MH_CoTI) and b.nh==59 and b.mhtype == 2 and b.len== 1 and b.res == 0 and b.cksum == 0x66f2 and b.cookie == b'\x00'*8 - -= MIP6MH_CoTI - Build (specific values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_CoTI(res=0x77, cksum=0x8899, cookie=b"\xAA"*8)) == b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x02w\x88\x99\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa' - -= MIP6MH_CoTI - Dissection (specific values) -a=IPv6(b'`\x00\x00\x00\x00\x10\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x01\x02w\x88\x99\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa') -b=a.payload -a.nh == 135 and isinstance(b, MIP6MH_CoTI) and b.nh==59 and b.mhtype == 2 and b.len == 1 and b.res == 0x77 and b.cksum == 0x8899 and b.cookie == b'\xAA'*8 - - -############ -############ -+ Home Test Message - -= MIP6MH_HoT - Build (default values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_HoT()) == b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x03\x00e\xe9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= MIP6MH_HoT - Dissection (default values) -a=IPv6(b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x03\x00e\xe9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -b = a.payload -a.nh == 135 and isinstance(b, MIP6MH_HoT) and b.nh==59 and b.mhtype == 3 and b.len== 2 and b.res == 0 and b.cksum == 0x65e9 and b.index == 0 and b.cookie == b'\x00'*8 and b.token == b'\x00'*8 - -= MIP6MH_HoT - Build (specific values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_HoT(res=0x77, cksum=0x8899, cookie=b"\xAA"*8, index=0xAABB, token=b'\xCC'*8)) == b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x03w\x88\x99\xaa\xbb\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc' - -= MIP6MH_HoT - Dissection (specific values) -a=IPv6(b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x03w\x88\x99\xaa\xbb\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc') -b = a.payload -a.nh == 135 and isinstance(b, MIP6MH_HoT) and b.nh==59 and b.mhtype == 3 and b.len== 2 and b.res == 0x77 and b.cksum == 0x8899 and b.index == 0xAABB and b.cookie == b'\xAA'*8 and b.token == b'\xCC'*8 - -= MIP6MH_HoT answers -a1, a2 = "2001:db8::1", "2001:db8::2" -cookie = RandString(8)._fix() -p1 = IPv6(src=a1, dst=a2)/MIP6MH_HoTI(cookie=cookie) -p2 = IPv6(src=a2, dst=a1)/MIP6MH_HoT(cookie=cookie) -p2_ko = IPv6(src=a2, dst=a1)/MIP6MH_HoT(cookie="".join(chr((orb(b'\xff') + 1) % 256))) -assert p1.hashret() == p2.hashret() and p2.answers(p1) and not p1.answers(p2) -assert p1.hashret() != p2_ko.hashret() and not p2_ko.answers(p1) and not p1.answers(p2_ko) - - -############ -############ -+ Care-of Test Message - -= MIP6MH_CoT - Build (default values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_CoT()) == b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x04\x00d\xe9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= MIP6MH_CoT - Dissection (default values) -a=IPv6(b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x04\x00d\xe9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -b = a.payload -a.nh == 135 and isinstance(b, MIP6MH_HoT) and b.nh==59 and b.mhtype == 4 and b.len== 2 and b.res == 0 and b.cksum == 0x64e9 and b.index == 0 and b.cookie == b'\x00'*8 and b.token == b'\x00'*8 - -= MIP6MH_CoT - Build (specific values) -raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/MIP6MH_CoT(res=0x77, cksum=0x8899, cookie=b"\xAA"*8, index=0xAABB, token=b'\xCC'*8)) == b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x04w\x88\x99\xaa\xbb\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc' - -= MIP6MH_CoT - Dissection (specific values) -a=IPv6(b'`\x00\x00\x00\x00\x18\x87@ \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02;\x02\x04w\x88\x99\xaa\xbb\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc') -b = a.payload -a.nh == 135 and isinstance(b, MIP6MH_CoT) and b.nh==59 and b.mhtype == 4 and b.len== 2 and b.res == 0x77 and b.cksum == 0x8899 and b.index == 0xAABB and b.cookie == b'\xAA'*8 and b.token == b'\xCC'*8 - - -############ -############ -+ Binding Update Message - -= MIP6MH_BU - build (default values) -s= b'`\x00\x00\x00\x00(<@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x87\x02\x01\x02\x00\x00\xc9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\x01\x05\x00\xee`\x00\x00\xd0\x00\x00\x03\x01\x02\x00\x00' -raw(IPv6()/IPv6ExtHdrDestOpt(options=[HAO()])/MIP6MH_BU()) == s - -= MIP6MH_BU - dissection (default values) -p = IPv6(s) -p[MIP6MH_BU].len == 1 - -= MIP6MH_BU - build -s = b'`\x00\x00\x00\x00P<@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x87\x02\x01\x02\x00\x00\xc9\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xca\xfe;\x06\x05\x00\xea\xf2\x00\x00\xd0\x00\x00*\x01\x00\x03\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x06\x12\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -raw(IPv6()/IPv6ExtHdrDestOpt(options=[HAO(hoa='2001:db8::cafe')])/MIP6MH_BU(mhtime=42, options=[MIP6OptAltCoA(),MIP6OptMobNetPrefix()])) == s - -= MIP6MH_BU - dissection -p = IPv6(s) -p[MIP6MH_BU].cksum == 0xeaf2 and p[MIP6MH_BU].len == 6 and len(p[MIP6MH_BU].options) == 4 and p[MIP6MH_BU].mhtime == 42 - - -############ -############ -+ Binding ACK Message - -= MIP6MH_BA - build -s = b'`\x00\x00\x00\x00\x10\x87@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01;\x01\x06\x00\xbc\xb9\x00\x80\x00\x00\x00*\x01\x02\x00\x00' -raw(IPv6()/MIP6MH_BA(mhtime=42)) == s - -= MIP6MH_BA - dissection -p = IPv6(s) -p[MIP6MH_BA].cksum == 0xbcb9 and p[MIP6MH_BA].len == 1 and len(p[MIP6MH_BA].options) == 1 and p[MIP6MH_BA].mhtime == 42 - - -############ -############ -+ Binding ERR Message - -= MIP6MH_BE - build -s = b'`\x00\x00\x00\x00\x18\x87@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01;\x02\x07\x00\xbbY\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' -raw(IPv6()/MIP6MH_BE(status=2, ha='1::2')) == s - -= MIP6MH_BE - dissection -p = IPv6(s) -p[MIP6MH_BE].cksum=0xba10 and p[MIP6MH_BE].len == 1 and len(p[MIP6MH_BE].options) == 1 - - -############ -############ -+ Netflow v5 -~ netflow - -= NetflowHeaderV5 - basic building - -raw(NetflowHeader()/NetflowHeaderV5()) == b'\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -raw(NetflowHeaderV5(engineID=42)) == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x00\x00' - -raw(NetflowRecordV5(dst="192.168.0.1")) == b'\x7f\x00\x00\x01\xc0\xa8\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -raw(NetflowHeader()/NetflowHeaderV5(count=1)/NetflowRecordV5(dst="192.168.0.1")) == b'\x00\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x01\xc0\xa8\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00' - -= NetflowHeaderV5 - UDP bindings - -s = raw(IP(src="127.0.0.1")/UDP()/NetflowHeader()/NetflowHeaderV5()) -assert s == b'E\x00\x004\x00\x01\x00\x00@\x11|\xb6\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x07\x08\x07\x00 \xf1\x98\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -pkt = IP(s) -assert NetflowHeaderV5 in pkt - -= NetflowHeaderV5 - basic dissection - -nf5 = NetflowHeader(b'\x00\x05\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00') -nf5.version == 5 and nf5[NetflowHeaderV5].count == 2 and isinstance(nf5[NetflowRecordV5].payload, NetflowRecordV5) - -############ -############ -+ Netflow v9 -~ netflow - -= NetflowV9 - advanced dissection - -import os -tmp = "/test/pcaps/netflowv9.pcap" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename -a = rdpcap(filename) -a = netflowv9_defragment(a) - -nfv9_fl = a[0] -assert NetflowFlowsetV9 in nfv9_fl -assert len(nfv9_fl.templates[0].template_fields) == 21 -assert nfv9_fl.templates[0].template_fields[1].fieldType == 12 - -nfv9_ds = a[3] -assert NetflowDataflowsetV9 in nfv9_ds -assert len(nfv9_ds[NetflowDataflowsetV9].records) == 24 -assert nfv9_ds[NetflowDataflowsetV9].records[21].IP_PROTOCOL_VERSION == 4 -assert nfv9_ds.records[21].IPV4_SRC_ADDR == '20.0.0.248' -assert nfv9_ds.records[21].IPV4_DST_ADDR == '30.0.0.248' - -nfv9_options_fl = a[1] -assert NetflowOptionsFlowsetV9 in nfv9_options_fl -assert isinstance(nfv9_options_fl[NetflowOptionsFlowsetV9].scopes[0], NetflowOptionsFlowsetScopeV9) -assert isinstance(nfv9_options_fl[NetflowOptionsFlowsetV9].options[0], NetflowOptionsFlowsetOptionV9) -assert nfv9_options_fl[NetflowOptionsFlowsetV9].options[0].optionFieldType == 36 - -nfv9_options_ds = a[4] -assert NetflowDataflowsetV9 in nfv9_options_ds -assert isinstance(nfv9_options_ds.records[0], NetflowOptionsRecordScopeV9) -assert nfv9_options_ds.records[0].IN_BYTES == b'\x01\x00\x00\x00' -assert nfv9_options_ds.records[1].SAMPLING_INTERVAL == 12 -assert nfv9_options_ds.records[1].SAMPLING_ALGORITHM == 0x2 - -= NetflowV9 - Multiple FlowSets in one packet - -nfv9_multiple_flowsets = NetflowHeader(b'\x00\t\x00\x03\x00\x00K [F\x17\x97\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00H\x04\x00\x00\x10\x00\x08\x00\x04\x00\x0c\x00\x04\x00\x15\x00\x04\x00\x16\x00\x04\x00\x01\x00\x08\x00\x02\x00\x08\x00\n\x00\x04\x00\x0e\x00\x04\x00\x07\x00\x02\x00\x0b\x00\x02\x00\x04\x00\x01\x00\x06\x00\x01\x00<\x00\x01\x00\x05\x00\x01\x00 \x00\x02\x00:\x00\x02\x00\x00\x00L\x08\x00\x00\x11\x00\x1b\x00\x10\x00\x1c\x00\x10\x00\x1f\x00\x04\x00\x15\x00\x04\x00\x16\x00\x04\x00\x01\x00\x08\x00\x02\x00\x08\x00\n\x00\x04\x00\x0e\x00\x04\x00\x07\x00\x02\x00\x0b\x00\x02\x00\x04\x00\x01\x00\x06\x00\x01\x00<\x00\x01\x00\x05\x00\x01\x00 \x00\x02\x00:\x00\x02\x04\x00\x008\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x00\x10\xac\x00\x00\x10\x83\x00\x00\x00\x00\x00\x00\x0b\xb8\x00\x00\x00\x00\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00\x01\x005\x005\x11\x00\x04\x00\x00\x00\x00e') -assert nfv9_multiple_flowsets.haslayer(NetflowFlowsetV9) -assert nfv9_multiple_flowsets.haslayer(NetflowDataflowsetV9) -nfv9_defrag = netflowv9_defragment(list(nfv9_multiple_flowsets)) -flowset1 = nfv9_defrag[0].getlayer(NetflowFlowsetV9, 1) -assert flowset1.templates[0].template_fields[0].fieldType == 8 -assert flowset1.templates[0].template_fields[0].fieldLength == 4 -assert flowset1.templates[0].template_fields[5].fieldType == 2 -assert flowset1.templates[0].template_fields[5].fieldLength == 8 -flowset2 = nfv9_defrag[0].getlayer(NetflowFlowsetV9, 2) -assert flowset2.templates[0].template_fields[0].fieldType == 27 -assert flowset2.templates[0].template_fields[0].fieldLength == 16 -assert flowset2.templates[0].template_fields[5].fieldType == 1 -assert flowset2.templates[0].template_fields[5].fieldLength == 8 -assert nfv9_defrag[0].getlayer(NetflowFlowsetV9, 2) -assert nfv9_defrag[0].records[0].IP_PROTOCOL_VERSION == 4 -assert nfv9_defrag[0].records[0].PROTOCOL == 17 -assert nfv9_defrag[0].records[0].IPV4_SRC_ADDR == "127.0.0.1" - -= NetflowV9 - build and dissection -~ netflow - -header = Ether()/IP()/UDP() -netflow_header = NetflowHeader()/NetflowHeaderV9() - -flowset = NetflowFlowsetV9( - templates=[NetflowTemplateV9( - template_fields=[ - NetflowTemplateFieldV9(fieldType=1, fieldLength=1), # IN_BYTES - NetflowTemplateFieldV9(fieldType=2, fieldLength=4), # IN_PKTS - NetflowTemplateFieldV9(fieldType=4), # PROTOCOL - NetflowTemplateFieldV9(fieldType=8), # IPV4_SRC_ADDR - NetflowTemplateFieldV9(fieldType=12), # IPV4_DST_ADDR - ], - templateID=256, - fieldCount=5) - ], - flowSetID=0 -) -recordClass = GetNetflowRecordV9(flowset) -dataFS = NetflowDataflowsetV9( - templateID=256, - records=[ # Some random data. - recordClass( - IN_BYTES=b"\x12", - IN_PKTS=b"\0\0\0\0", - PROTOCOL=6, - IPV4_SRC_ADDR="192.168.0.10", - IPV4_DST_ADDR="192.168.0.11" - ), - ], -) -pkt = header / netflow_header / flowset / dataFS -pkt = netflowv9_defragment(Ether(raw(pkt)))[0] - -assert NetflowDataflowsetV9 in pkt -assert len(pkt[NetflowDataflowsetV9].records) == 1 -assert pkt[NetflowDataflowsetV9].records[0].IPV4_DST_ADDR == "192.168.0.11" - -= NetflowV9 - advanced build -~ netflow - -atm_time = 1547927349.328283 - -header = Ether(src="00:00:00:00:00:00", dst="aa:aa:aa:aa:aa:aa")/IP(dst="127.0.0.1", src="127.0.0.1")/UDP()/NetflowHeader()/NetflowHeaderV9(unixSecs=atm_time) -flowset = NetflowFlowsetV9(templates=[NetflowTemplateV9(template_fields=[NetflowTemplateFieldV9(fieldType=8, fieldLength=4),NetflowTemplateFieldV9(fieldType=12, fieldLength=4),NetflowTemplateFieldV9(fieldType=5, fieldLength=1),NetflowTemplateFieldV9(fieldType=4, fieldLength=1),NetflowTemplateFieldV9(fieldType=7, fieldLength=2),NetflowTemplateFieldV9(fieldType=11, fieldLength=2),NetflowTemplateFieldV9(fieldType=32, fieldLength=2),NetflowTemplateFieldV9(fieldType=10, fieldLength=4),NetflowTemplateFieldV9(fieldType=16, fieldLength=4),NetflowTemplateFieldV9(fieldType=17, fieldLength=4),NetflowTemplateFieldV9(fieldType=18, fieldLength=4),NetflowTemplateFieldV9(fieldType=14, fieldLength=4),NetflowTemplateFieldV9(fieldType=1, fieldLength=4),NetflowTemplateFieldV9(fieldType=2, fieldLength=4),NetflowTemplateFieldV9(fieldType=22, fieldLength=4),NetflowTemplateFieldV9(fieldType=21, fieldLength=4),NetflowTemplateFieldV9(fieldType=15, fieldLength=4),NetflowTemplateFieldV9(fieldType=9, fieldLength=1),NetflowTemplateFieldV9(fieldType=13, fieldLength=1),NetflowTemplateFieldV9(fieldType=6, fieldLength=1),NetflowTemplateFieldV9(fieldType=60, fieldLength=1)], templateID=424, fieldCount=21)], flowSetID=0, length=92) -dataflowset = NetflowDataflowsetV9(records=[NetflowRecordV9(fieldValue=b'\x14\x00\x00\xfd\x1e\x00\x00\xfd\x00\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x03 \x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x02\xfb\x00\x15a|\x00\x00\x07\x0f$\x95x\xed$\x99\x91<\ndg\x01 \x00\x04')], templateID=424) - -pkt = netflowv9_defragment(list(header/flowset/dataflowset))[0] -assert pkt.records[0].IPV4_NEXT_HOP == "10.100.103.1" -assert pkt.records[0].OUTPUT_SNMP == b'\x00\x00\x02\xfb' - -assert raw(pkt) == b'\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00\xcc\x00\x01\x00\x00@\x11|\x1e\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x07\x08\x07\x00\xb8\x86\xe7\x00\t\x00\x02\x00\x00\x00\x00\\C\x7f5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\\x01\xa8\x00\x15\x00\x08\x00\x04\x00\x0c\x00\x04\x00\x05\x00\x01\x00\x04\x00\x01\x00\x07\x00\x02\x00\x0b\x00\x02\x00 \x00\x02\x00\n\x00\x04\x00\x10\x00\x04\x00\x11\x00\x04\x00\x12\x00\x04\x00\x0e\x00\x04\x00\x01\x00\x04\x00\x02\x00\x04\x00\x16\x00\x04\x00\x15\x00\x04\x00\x0f\x00\x04\x00\t\x00\x01\x00\r\x00\x01\x00\x06\x00\x01\x00<\x00\x01\x01\xa8\x00@\x14\x00\x00\xfd\x1e\x00\x00\xfd\x00\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x03 \x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x02\xfb\x00\x15a|\x00\x00\x07\x0f$\x95x\xed$\x99\x91<\ndg\x01 \x00\x04' +else: + True -= NetflowV9 - padding #GH2257 += check _resolve_MAC -dat = hex_bytes("fb200807007840a10009000277efe9c450c843f900362202000000000001001801000004000800010000002a00040029000400000101004477ef819077ef81900000003c00000001009300930ac900640ac9033b060009ee0b3500000ac9033b131302000000000000260bdc69aa6480996649a000000000") -pkt = UDP(dat) -assert pkt[NetflowOptionsFlowsetV9].pad == b"\x00\x00" -pkt[NetflowOptionsFlowsetV9].pad = None -assert raw(pkt) == dat +if conf.manufdb: + assert conf.manufdb._resolve_MAC("00:00:17") == "Oracle" +else: + True ############ ############ -+ Netflow v10 (aka IPFix) -~ netflow ++ Ether tests with IPv6 -= IPFix dissection += Ether IPv6 checking for dst +~ netaccess ipv6 + +p = Ether()/IPv6(dst="www.google.com")/TCP() +assert p.dst != p[IPv6].dst +p.show() -import os -tmp = "/test/pcaps/ipfix.pcap" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename -a = sniff(offline=filename, session=NetflowSession) - -# Templates -pkt1 = a[0] -assert NetflowHeaderV10 in pkt1 -assert len(pkt1[NetflowFlowsetV9].templates) == 1 -assert len(pkt1[NetflowFlowsetV9].templates[0].template_fields) == 23 -flds = pkt1[NetflowFlowsetV9].templates[0].template_fields -assert (flds[0].fieldType == 8 and flds[0].fieldLength == 4) -assert (flds[4].fieldType == 7 and flds[4].fieldLength == 2) - -# Data -pkt2 = a[2] -assert NetflowHeaderV10 in pkt2 -assert len(pkt2.records) == 1 -assert pkt2.records[0].IPV4_SRC_ADDR == "70.1.115.1" -assert pkt2.records[0].flowStartMilliseconds == 1480449931519 - -# Options -pkt3 = a[1] -assert NetflowOptionsFlowset10 in pkt3 -assert pkt3.scope_field_count == 1 -assert pkt3.field_count == 3 -assert len(pkt3[NetflowOptionsFlowset10].scopes) == 1 -assert len(pkt3[NetflowOptionsFlowset10].options) == 2 -assert pkt3.scopes[0].scopeFieldType == 5 -assert pkt3.scopes[0].scopeFieldlength == 2 -assert pkt3[NetflowOptionsFlowset10].options[0].optionFieldType == 36 - -# Templates with enterprise-specific Information Elements. -s=b'\x01\x07\x00\x12\x01\n\x00\x04\x84\x0c\x00\x02\x00\x00\x00\t\x01\n\x00&\x00\x0b\x00\x02\x00\x07\x00\x02\x00\x04\x00\x01\x00\x0c\x00\x04\x00\x08\x00\x04\x00\xea\x00\x02\x01\n\x00\x01\x84\x10\x00\x06\x00\x00\x00\t\x84\x0e\x00\x06\x00\x00\x00\t\x84\x0f\x00\x06\x00\x00\x00\t\x00\x01\x00\x04\x00\x02\x00\x04\x00\xf3\x00\x02\x00\x06\x00\x01\x01\n\x00#' -pkt4 = NetflowTemplateV9(s) -assert len(pkt4.template_fields) == pkt4.fieldCount -assert sum([template.fieldLength for template in pkt4.template_fields]) == 124 ############ ############ + pcap / pcapng format support +~ pcap = Variable creations from io import BytesIO +import base64 pcapfile = BytesIO(b'\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00e\x00\x00\x00\xcf\xc5\xacVo*\n\x00(\x00\x00\x00(\x00\x00\x00E\x00\x00(\x00\x01\x00\x00@\x06|\xcd\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x91|\x00\x00\xcf\xc5\xacV_-\n\x00\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x11|\xce\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x00\x08\x01r\xcf\xc5\xacV\xf90\n\x00\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x01|\xde\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x00\xf7\xff\x00\x00\x00\x00') pcapngfile = BytesIO(b'\n\r\r\n\\\x00\x00\x00M<+\x1a\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00,\x00File created by merging: \nFile1: test.pcap \n\x04\x00\x08\x00mergecap\x00\x00\x00\x00\\\x00\x00\x00\x01\x00\x00\x00\\\x00\x00\x00e\x00\x00\x00\xff\xff\x00\x00\x02\x006\x00Unknown/not available in original file format(libpcap)\x00\x00\t\x00\x01\x00\x06\x00\x00\x00\x00\x00\x00\x00\\\x00\x00\x00\x06\x00\x00\x00H\x00\x00\x00\x00\x00\x00\x00\x8d*\x05\x00/\xfc[\xcd(\x00\x00\x00(\x00\x00\x00E\x00\x00(\x00\x01\x00\x00@\x06|\xcd\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x91|\x00\x00H\x00\x00\x00\x06\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x8d*\x05\x00\x1f\xff[\xcd\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x11|\xce\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x00\x08\x01r<\x00\x00\x00\x06\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x8d*\x05\x00\xb9\x02\\\xcd\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x01|\xde\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x00\xf7\xff\x00\x00\x00\x00<\x00\x00\x00') pcapnanofile = BytesIO(b"M<\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00e\x00\x00\x00\xcf\xc5\xacV\xc9\xc1\xb5'(\x00\x00\x00(\x00\x00\x00E\x00\x00(\x00\x01\x00\x00@\x06|\xcd\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x91|\x00\x00\xcf\xc5\xacV-;\xc1'\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x11|\xce\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x00\x08\x01r\xcf\xc5\xacV\x9aL\xcf'\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x01|\xde\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x00\xf7\xff\x00\x00\x00\x00") pcapwirelenfile = BytesIO(b'\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x01\x00\x00\x00}\x87pZ.\xa2\x08\x00\x0f\x00\x00\x00\x10\x00\x00\x00\xff\xff\xff\xff\xff\xff GG\xee\xdd\xa8\x90\x00a') -pcapngdefaults = BytesIO(base64_bytes(b'Cg0NChwAAABNPCsaAQAAAP//////////HAAAAAEAAAAgAAAAEgEAAP//AAAJAAEACUeZiQAAAAAgAAAAAQAAACAAAAASAQAA//8AAAkAAQAJAAAAAAAAACAAAAABAAAAIAAAABIBAAD//wAACQABAAkAAAAAAAAAIAAAAAEAAAAgAAAAEgEAAP//AAAJAAEACQAAAAAAAAAgAAAABgAAAIQBAAADAAAApO/bFdgJaeBiAQAAYgEAAFVVVVVVVVXV////////IMbr4D7PCABFAAFIlQkAAEAR5JwAAAAA/////wBEAEMBNJDsAQEGAFSpVwIACoAAAAAAAAAAAAAAAAAAAAAAACDG6+A+zwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjglNjNQEB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsOs+bAAAhAEAAAYAAACAAQAAAwAAAKTv2xXIDYznYAEAAGABAABVVVVVVVVV1QEAXn//+iDG6+A+zwgARQABRgGPAAAEEal3qf5wqO////rhbgdsATJi0U5PVElGWSAqIEhUVFAvMS4xDQpIT1NUOiAyMzkuMjU1LjI1NS4yNTA6MTkwMA0KQ0FDSEUtQ09OVFJPTDogbWF4LWFnZT0xODAwDQpMT0NBVElPTjogaHR0cDovLzE2OS4yNTQuMTEyLjE2ODo1NTAwMC9ucmMvZGRkLnhtbA0KTlQ6IHV1aWQ6NEQ0NTQ5MzAtMDIwMC0xMDAwLTgwMDEtMjBDNkVCRTAzRUNGDQpOVFM6IHNzZHA6YWxpdmUNClNFUlZFUjogRnJlZUJTRC84LjAgVVBuUC8xLjAgUGFuYXNvbmljLU1JTC1ETE5BLVNWLzEuMA0KVVNOOiB1dWlkOjRENDU0OTMwLTAyMDAtMTAwMC04MDAxLTIwQzZFQkUwM0VDRg0KDQpcQcvWgAEAAAYAAAC4AQAAAwAAAKTv2xV4Ao3nlQEAAJUBAABVVVVVVVVV1QEAXn//+iDG6+A+zwgARQABewGQAAAEEalBqf5wqO////rhbgdsAWfu+k5PVElGWSAqIEhUVFAvMS4xDQpIT1NUOiAyMzkuMjU1LjI1NS4yNTA6MTkwMA0KQ0FDSEUtQ09OVFJPTDogbWF4LWFnZT0xODAwDQpMT0NBVElPTjogaHR0cDovLzE2OS4yNTQuMTEyLjE2ODo1NTAwMC9ucmMvZGRkLnhtbA0KTlQ6IHVybjpwYW5hc29uaWMtY29tOmRldmljZTpwMDBSZW1vdGVDb250cm9sbGVyOjENCk5UUzogc3NkcDphbGl2ZQ0KU0VSVkVSOiBGcmVlQlNELzguMCBVUG5QLzEuMCBQYW5hc29uaWMtTUlMLURMTkEtU1YvMS4wDQpVU046IHV1aWQ6NEQ0NTQ5MzAtMDIwMC0xMDAwLTgwMDEtMjBDNkVCRTAzRUNGOjp1cm46cGFuYXNvbmljLWNvbTpkZXZpY2U6cDAwUmVtb3RlQ29udHJvbGxlcjoxDQoNCrLVKmoAAAC4AQAABgAAAHgBAAADAAAApO/bFVjbjedXAQAAVwEAAFVVVVVVVVXVAQBef//6IMbr4D7PCABFAAE9AZEAAAQRqX6p/nCo7///+uFuB2wBKaZATk9USUZZICogSFRUUC8xLjENCkhPU1Q6IDIzOS4yNTUuMjU1LjI1MDoxOTAwDQpDQUNIRS1DT05UUk9MOiBtYXgtYWdlPTE4MDANCkxPQ0FUSU9OOiBodHRwOi8vMTY5LjI1NC4xMTIuMTY4OjU1MDAwL25yYy9kZGQueG1sDQpOVDogdXBucDpyb290ZGV2aWNlDQpOVFM6IHNzZHA6YWxpdmUNClNFUlZFUjogRnJlZUJTRC84LjAgVVBuUC8xLjAgUGFuYXNvbmljLU1JTC1ETE5BLVNWLzEuMA0KVVNOOiB1dWlkOjRENDU0OTMwLTAyMDAtMTAwMC04MDAxLTIwQzZFQkUwM0VDRjo6dXBucDpyb290ZGV2aWNlDQoNCjagXoUAeAEAAAYAAAC0AQAAAwAAAKTv2xXYw47nkwEAAJMBAABVVVVVVVVV1QEAXn//+iDG6+A+zwgARQABeQGSAAAEEalBqf5wqO////rhbgdsAWWV4E5PVElGWSAqIEhUVFAvMS4xDQpIT1NUOiAyMzkuMjU1LjI1NS4yNTA6MTkwMA0KQ0FDSEUtQ09OVFJPTDogbWF4LWFnZT0xODAwDQpMT0NBVElPTjogaHR0cDovLzE2OS4yNTQuMTEyLjE2ODo1NTAwMC9ucmMvZGRkLnhtbA0KTlQ6IHVybjpwYW5hc29uaWMtY29tOnNlcnZpY2U6cDAwTmV0d29ya0NvbnRyb2w6MQ0KTlRTOiBzc2RwOmFsaXZlDQpTRVJWRVI6IEZyZWVCU0QvOC4wIFVQblAvMS4wIFBhbmFzb25pYy1NSUwtRExOQS1TVi8xLjANClVTTjogdXVpZDo0RDQ1NDkzMC0wMjAwLTEwMDAtODAwMS0yMEM2RUJFMDNFQ0Y6OnVybjpwYW5hc29uaWMtY29tOnNlcnZpY2U6cDAwTmV0d29ya0NvbnRyb2w6MQ0KDQovXKFrALQBAAAGAAAAqAEAAAMAAACk79sVuJKP54cBAACHAQAAVVVVVVVVVdUBAF5///ogxuvgPs8IAEUAAW0BkwAABBGpTKn+cKjv///64W4HbAFZRNJOT1RJRlkgKiBIVFRQLzEuMQ0KSE9TVDogMjM5LjI1NS4yNTUuMjUwOjE5MDANCkNBQ0hFLUNPTlRST0w6IG1heC1hZ2U9MTgwMA0KTE9DQVRJT046IGh0dHA6Ly8xNjkuMjU0LjExMi4xNjg6NTUwMDAvbnJjL2RkZC54bWwNCk5UOiB1cm46ZGlhbC1tdWx0aXNjcmVlbi1vcmc6c2VydmljZTpkaWFsOjENCk5UUzogc3NkcDphbGl2ZQ0KU0VSVkVSOiBGcmVlQlNELzguMCBVUG5QLzEuMCBQYW5hc29uaWMtTUlMLURMTkEtU1YvMS4wDQpVU046IHV1aWQ6NEQ0NTQ5MzAtMDIwMC0xMDAwLTgwMDEtMjBDNkVCRTAzRUNGOjp1cm46ZGlhbC1tdWx0aXNjcmVlbi1vcmc6c2VydmljZTpkaWFsOjENCg0KLn5A6QCoAQAA')) +pcapngdefaults = BytesIO(base64.b64decode(b'Cg0NChwAAABNPCsaAQAAAP//////////HAAAAAEAAAAgAAAAEgEAAP//AAAJAAEACUeZiQAAAAAgAAAAAQAAACAAAAASAQAA//8AAAkAAQAJAAAAAAAAACAAAAABAAAAIAAAABIBAAD//wAACQABAAkAAAAAAAAAIAAAAAEAAAAgAAAAEgEAAP//AAAJAAEACQAAAAAAAAAgAAAABgAAAIQBAAADAAAApO/bFdgJaeBiAQAAYgEAAFVVVVVVVVXV////////IMbr4D7PCABFAAFIlQkAAEAR5JwAAAAA/////wBEAEMBNJDsAQEGAFSpVwIACoAAAAAAAAAAAAAAAAAAAAAAACDG6+A+zwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjglNjNQEB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsOs+bAAAhAEAAAYAAACAAQAAAwAAAKTv2xXIDYznYAEAAGABAABVVVVVVVVV1QEAXn//+iDG6+A+zwgARQABRgGPAAAEEal3qf5wqO////rhbgdsATJi0U5PVElGWSAqIEhUVFAvMS4xDQpIT1NUOiAyMzkuMjU1LjI1NS4yNTA6MTkwMA0KQ0FDSEUtQ09OVFJPTDogbWF4LWFnZT0xODAwDQpMT0NBVElPTjogaHR0cDovLzE2OS4yNTQuMTEyLjE2ODo1NTAwMC9ucmMvZGRkLnhtbA0KTlQ6IHV1aWQ6NEQ0NTQ5MzAtMDIwMC0xMDAwLTgwMDEtMjBDNkVCRTAzRUNGDQpOVFM6IHNzZHA6YWxpdmUNClNFUlZFUjogRnJlZUJTRC84LjAgVVBuUC8xLjAgUGFuYXNvbmljLU1JTC1ETE5BLVNWLzEuMA0KVVNOOiB1dWlkOjRENDU0OTMwLTAyMDAtMTAwMC04MDAxLTIwQzZFQkUwM0VDRg0KDQpcQcvWgAEAAAYAAAC4AQAAAwAAAKTv2xV4Ao3nlQEAAJUBAABVVVVVVVVV1QEAXn//+iDG6+A+zwgARQABewGQAAAEEalBqf5wqO////rhbgdsAWfu+k5PVElGWSAqIEhUVFAvMS4xDQpIT1NUOiAyMzkuMjU1LjI1NS4yNTA6MTkwMA0KQ0FDSEUtQ09OVFJPTDogbWF4LWFnZT0xODAwDQpMT0NBVElPTjogaHR0cDovLzE2OS4yNTQuMTEyLjE2ODo1NTAwMC9ucmMvZGRkLnhtbA0KTlQ6IHVybjpwYW5hc29uaWMtY29tOmRldmljZTpwMDBSZW1vdGVDb250cm9sbGVyOjENCk5UUzogc3NkcDphbGl2ZQ0KU0VSVkVSOiBGcmVlQlNELzguMCBVUG5QLzEuMCBQYW5hc29uaWMtTUlMLURMTkEtU1YvMS4wDQpVU046IHV1aWQ6NEQ0NTQ5MzAtMDIwMC0xMDAwLTgwMDEtMjBDNkVCRTAzRUNGOjp1cm46cGFuYXNvbmljLWNvbTpkZXZpY2U6cDAwUmVtb3RlQ29udHJvbGxlcjoxDQoNCrLVKmoAAAC4AQAABgAAAHgBAAADAAAApO/bFVjbjedXAQAAVwEAAFVVVVVVVVXVAQBef//6IMbr4D7PCABFAAE9AZEAAAQRqX6p/nCo7///+uFuB2wBKaZATk9USUZZICogSFRUUC8xLjENCkhPU1Q6IDIzOS4yNTUuMjU1LjI1MDoxOTAwDQpDQUNIRS1DT05UUk9MOiBtYXgtYWdlPTE4MDANCkxPQ0FUSU9OOiBodHRwOi8vMTY5LjI1NC4xMTIuMTY4OjU1MDAwL25yYy9kZGQueG1sDQpOVDogdXBucDpyb290ZGV2aWNlDQpOVFM6IHNzZHA6YWxpdmUNClNFUlZFUjogRnJlZUJTRC84LjAgVVBuUC8xLjAgUGFuYXNvbmljLU1JTC1ETE5BLVNWLzEuMA0KVVNOOiB1dWlkOjRENDU0OTMwLTAyMDAtMTAwMC04MDAxLTIwQzZFQkUwM0VDRjo6dXBucDpyb290ZGV2aWNlDQoNCjagXoUAeAEAAAYAAAC0AQAAAwAAAKTv2xXYw47nkwEAAJMBAABVVVVVVVVV1QEAXn//+iDG6+A+zwgARQABeQGSAAAEEalBqf5wqO////rhbgdsAWWV4E5PVElGWSAqIEhUVFAvMS4xDQpIT1NUOiAyMzkuMjU1LjI1NS4yNTA6MTkwMA0KQ0FDSEUtQ09OVFJPTDogbWF4LWFnZT0xODAwDQpMT0NBVElPTjogaHR0cDovLzE2OS4yNTQuMTEyLjE2ODo1NTAwMC9ucmMvZGRkLnhtbA0KTlQ6IHVybjpwYW5hc29uaWMtY29tOnNlcnZpY2U6cDAwTmV0d29ya0NvbnRyb2w6MQ0KTlRTOiBzc2RwOmFsaXZlDQpTRVJWRVI6IEZyZWVCU0QvOC4wIFVQblAvMS4wIFBhbmFzb25pYy1NSUwtRExOQS1TVi8xLjANClVTTjogdXVpZDo0RDQ1NDkzMC0wMjAwLTEwMDAtODAwMS0yMEM2RUJFMDNFQ0Y6OnVybjpwYW5hc29uaWMtY29tOnNlcnZpY2U6cDAwTmV0d29ya0NvbnRyb2w6MQ0KDQovXKFrALQBAAAGAAAAqAEAAAMAAACk79sVuJKP54cBAACHAQAAVVVVVVVVVdUBAF5///ogxuvgPs8IAEUAAW0BkwAABBGpTKn+cKjv///64W4HbAFZRNJOT1RJRlkgKiBIVFRQLzEuMQ0KSE9TVDogMjM5LjI1NS4yNTUuMjUwOjE5MDANCkNBQ0hFLUNPTlRST0w6IG1heC1hZ2U9MTgwMA0KTE9DQVRJT046IGh0dHA6Ly8xNjkuMjU0LjExMi4xNjg6NTUwMDAvbnJjL2RkZC54bWwNCk5UOiB1cm46ZGlhbC1tdWx0aXNjcmVlbi1vcmc6c2VydmljZTpkaWFsOjENCk5UUzogc3NkcDphbGl2ZQ0KU0VSVkVSOiBGcmVlQlNELzguMCBVUG5QLzEuMCBQYW5hc29uaWMtTUlMLURMTkEtU1YvMS4wDQpVU046IHV1aWQ6NEQ0NTQ5MzAtMDIwMC0xMDAwLTgwMDEtMjBDNkVCRTAzRUNGOjp1cm46ZGlhbC1tdWx0aXNjcmVlbi1vcmc6c2VydmljZTpkaWFsOjENCg0KLn5A6QCoAQAA')) = Read a pcap file pktpcap = rdpcap(pcapfile) @@ -6897,6 +2207,157 @@ pktpcapngdefaults = rdpcap(pcapngdefaults) assert pktpcapngdefaults[0].time == 1575115986.114775512 assert Ether in pktpcapngdefaults[0] += Read a pcapng with little-endian SHB +pktcapng = sniff(offline=scapy_path("/test/pcaps/macos.pcapng.gz")) +assert len(pktcapng) != 0 + += Write a pcapng + +tmpfile = get_temp_file(autoext=".pcapng") +r = RawPcapNgWriter(tmpfile) +r._write_block_shb() +r._write_block_idb(linktype=DLT_EN10MB) +ts = 1632568366.384185 +r._write_block_epb(raw(Ether()/"Hello Scapy!!!"), ifid=0, timestamp=ts) +r.f.close() + +assert os.stat(tmpfile).st_size == 108 + +l = rdpcap(tmpfile) +assert b"Scapy" in l[0][Raw].load +assert l[0].time == ts + += Check wrpcapng() + +tmpfile = get_temp_file(autoext=".pcapng") +p = Ether()/"Hello Scapy!!!" +p.time = 1632568366.384185 +wrpcapng(tmpfile, p) + +assert os.stat(tmpfile).st_size == 108 + +l = rdpcap(tmpfile) +assert b"Scapy" in l[0][Raw].load +assert l[0].time == ts + +p = Ether() / IPv6() / TCP() +p.comment = b"Hello Scapy!" +wrpcapng(tmpfile, p) +l = rdpcap(tmpfile) +assert l[0].comment == p.comment + +p = Ether() / IPv6() / TCP() +p.comments = [b"Hello!", b"Scapy!", b"Pcapng!"] +wrpcapng(tmpfile, p) +l = rdpcap(tmpfile) +assert l[0].comments == p.comments + += rdpcap on fifo +~ linux +f = get_temp_file() +os.unlink(f) +os.mkfifo(f) +p = Ether(bytes(Ether(dst="ff:ff:ff:ff:ff:ff")/"Hello Scapy!!!")) +s = AsyncSniffer(offline=f) +s.start() +wrpcap(f, p) +s.join(timeout=1) +assert s.results[0] == p + += Check multiple packets with different combination of linktype,comment,direction,sniffed_on fields. test both wrpcap() and wrpcapng() +import random,string +random.seed(0x2807) +plist = [] +ptypes = [] +ptypes.append(Ether((Ether() / IPv6() / TCP()).build())) +ptypes.append(IP((IP() / IPv6() / TCP()).build())) +ifaces=[None,'','i','int0',''.join(random.choices(string.printable,k=20))] +comments=[None,'','a','abcd',''.join(random.choices(string.printable,k=20))] +directions=[None,0,1,2,3] + +for iface in ifaces: + for comment in comments: + if comment is not None: + comment=comment.encode('utf-8') + for direction in directions: + for p in ptypes: + if iface is not None and type(ptypes[ifaces.index(iface) % len(ptypes)]) != type(p): + continue + pnew = p.copy() + pnew.time = 1632568366.384185 + pnew.sniffed_on = iface + pnew.direction = direction + pnew.comment = comment + plist.append(pnew) + +random.shuffle(plist) +tmpfile = get_temp_file(autoext=".pcapng") +wrpcapng(tmpfile, plist) +plist_check = rdpcap(tmpfile) +assert len(plist_check) == len(plist) +for i in range(len(plist)): + assert plist_check[i].comment == plist[i].comment + assert plist_check[i].direction == plist[i].direction + assert plist_check[i].sniffed_on == plist[i].sniffed_on + assert plist_check[i].time == plist[i].time + #if interface is unknown, verify pkt bytes integrity and that linktype was set to first packet + if plist[i].sniffed_on is None: + assert bytes(plist_check[i]) == bytes(plist[i]) + assert type(plist_check[i]) == type(plist[0]) + else: + assert plist_check[i] == plist[i] + +tmpfile = get_temp_file(autoext=".pcap") +wrpcap(tmpfile, plist) +plist_check = rdpcap(tmpfile) +for i in range(len(plist)): + assert plist_check[i].time == plist[i].time + assert type(plist_check[i]) == type(plist[0]) + assert bytes(plist_check[i]) == bytes(plist[i]) + += PcapNg - Process Information Block + +pib_pcapng_file = BytesIO(b'\n\r\r\n\xbc\x00\x00\x00M<+\x1a\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x02\x00\x05\x00arm64\x00\x00\x00\x03\x00f\x00Darwin Kernel Version 23.3.0: Thu Dec 21 02:29:41 PST 2023; root:xnu-10002.81.5~11/RELEASE_ARM64_T8122\x00\x00\x04\x00 \x00tcpdump (libpcap version 1.10.1)\x00\x00\x00\x00\xbc\x00\x00\x00\x01\x00\x00\x00 \x00\x00\x00\x01\x00\x00\x00\x00\x00\x08\x00\x02\x00\x03\x00en0\x00\x00\x00\x00\x00 \x00\x00\x00\x01\x00\x00\x80 \x00\x00\x00$\'\x00\x00\x02\x00\x06\x00trustd\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x01\x00\x00\x80$\x00\x00\x00")\x00\x00\x02\x00\x0c\x00mobileassetd\x00\x00\x00\x00$\x00\x00\x00\x06\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\xfb\x18\x06\x00EcqdB\x00\x00\x00B\x00\x00\x00\xe8\x9f\x80\xfa\x8c\xc6P\xa6\xd8\xd5\x83v\x08\x00E\x00\x004\x00\x00@\x00@\x06\x90T\nh\x01\xc3\xc0\xe5\xdd_\xf4\xb8\x00P\x95\xc3\xcb\x01\xcb\xeb\x11\xe8\x80\x11\x08\x00\x0c\xe6\x00\x00\x01\x01\x08\n\xbe\xb8\xd4\xb3\xbb\x9b4\xbc\x00\x00\x01\x80\x04\x00\x00\x00\x00\x00\x03\x80\x04\x00\x01\x00\x00\x00\x02\x00\x04\x00\x02\x00\x00\x00\x02\x80\x04\x00\x00\x00\x00\x00\x04\x80\x04\x00\x10\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00') + +l = rdpcap(pib_pcapng_file) +assert(len(l) == 1) +assert(TCP in l[0]) +assert(len(l[0].process_information) == 2) +assert(l[0].process_information["proc"]["name"] == "trustd") + += OSS-Fuzz Findings + +from io import BytesIO +# Issue 68352 +file = BytesIO(b"\n\r\r\n\x1c\x00\x00\x00M<+\x1a\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x1c\x00\x00\x00\x01\x00\x00\x00\x14\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x04\x00\x14\x00\x00\x00\x01\x00\x00\x00(\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x04\x00\x02\x00\t\x00b'ens16\xb0'\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06\x00\x00\x004\x00\x00\x00\x01\x00\x00\x00}\x17\x06\x00\xb5t\x1d\x85\x14\x00\x00\x00\x14\x00\x00\x00E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x014\x00\x00\x00") +rdpcap(file) + +# Issue 68354 +file = BytesIO(b'\n\r\r\n\xff\xfe\xfe\xffM<+\x1a') +try: + rdpcap(file) +except Scapy_Exception: + pass + +# Issue #70115 +file = BytesIO(b"\n\r\r\n\x00\x00\x008\x1a+ Raise +try: + invalid_pcapngfile_1 = BytesIO(b'\n\r\r\n\r\x00\x00\x00M<+\x1a\xb2<\xb2\xa1\x01\x00\x00\x00\r\x00\x00\x00M<+\x1a\x80\xaa\xb2\x02') + rdpcap(invalid_pcapngfile_1) + assert False +except Scapy_Exception: + pass + +# Invalid Packet in PCAPNG -> return +invalid_pcapngfile_2 = BytesIO(b'\n\r\r\n\x00\x00\x00\x1c\x1a+ raise EOFError +try: + invalid_pcapngfile_3 = BytesIO(b'\n\n\n\x14\x00\x00\x00M<+\x1a \x14\x00\x00\x00\x03\x00\x00\x00\x14\x00\x00\x00 \x14\x00\x00\x00') + rdpcap(invalid_pcapngfile_3) + assert False +except Scapy_Exception: + pass + +# Invalid SPB in PCAPNG -> raise EOFError +try: + invalid_pcapngfile_4 = BytesIO(b'\n\n\n\x14\x00\x00\x00M<+\x1a \x14\x00\x00\x00\x01\x00\x00\x00\x14\x00\x00\x00 \x14\x00\x00\x00\x03\x00\x00\x00\x0c\x00\x00\x00\x0c\x00\x00\x00') + rdpcap(invalid_pcapngfile_4) + assert False +except Scapy_Exception: + pass + = Check PcapWriter on null write f = BytesIO() @@ -7071,6 +2620,42 @@ assert r.linktype == DLT_EN10MB l = [ p for p in RawPcapReader(f) ] assert len(l) == 1 += Check RawPcapReader on pcap +~ pcap + +fd = get_temp_file() +wrpcap(fd, [Ether()/IP()/ICMP()]) +assert len([p for p in RawPcapReader(fd)]) == 1 + +for (x, y) in RawPcapReader(fd): + pass + += Check RawPcapReader with a Context Manager +~ pcap + +filename = get_temp_file(fd=False) +wrpcap(filename, [IP()/TCP(), IP()/UDP()]) + +try: + with RawPcapReader(filename) as reader: + packet = next(reader, None) + assert True +except TypeError: + assert False + += Check RawPcapWriter +~ pcap + +# GH3256 +fd = get_temp_file() +with RawPcapWriter(fd, linktype=1) as w: + w.write(b"test") + +fd = get_temp_file() +with RawPcapWriter(fd) as w: + w.write(b"test") + assert w.linktype == 1 + = Check tcpdump() ~ tcpdump from io import BytesIO @@ -7085,7 +2670,7 @@ assert b'127.0.0.1 > 127.0.0.1:' in data[2] * Non existing tcpdump binary -import mock +from unittest import mock conf_prog_tcpdump = conf.prog.tcpdump conf.prog.tcpdump = "tcpdump_fake" @@ -7116,8 +2701,8 @@ data = tcpdump([Ether()/IP()/ICMP()], dump=True, args=['-nn']).split(b'\n') print(data) assert b'127.0.0.1 > 127.0.0.1: ICMP' in data[0].upper() -= Check tcpdump() command with linktype -~ tcpdump += Check tcpdump() command with linktype +~ tcpdump libpcap f = BytesIO() pkt = Ether()/IP()/ICMP() @@ -7126,10 +2711,14 @@ with mock.patch('subprocess.Popen', return_value=Bunch( stdin=f, wait=lambda: None)) as popen: # Prevent closing the BytesIO with mock.patch.object(f, 'close'): - tcpdump([pkt], linktype="DLT_EN3MB", use_tempfile=False) + tcpdump([pkt], linktype="DLT_EN10MB", use_tempfile=False) + +expected_command = [conf.prog.tcpdump, '-y', 'EN10MB', '-U', '-r', '-'] +if OPENBSD: + expected_command = [conf.prog.tcpdump, '-y', 'EN10MB', '-r', '-'] popen.assert_called_once_with( - [conf.prog.tcpdump, '-y', 'EN3MB', '-r', '-'], + expected_command, stdin=subprocess.PIPE, stdout=None, stderr=None) print(bytes_hex(f.getvalue())) @@ -7138,7 +2727,7 @@ f.close() del f, pkt = Check tcpdump() command with linktype and args -~ tcpdump +~ tcpdump libpcap f = BytesIO() pkt = Ether()/IP()/ICMP() @@ -7149,8 +2738,12 @@ with mock.patch('subprocess.Popen', return_value=Bunch( with mock.patch.object(f, 'close'): tcpdump([pkt], linktype=scapy.data.DLT_EN10MB, use_tempfile=False) +expected_command = [conf.prog.tcpdump, '-y', 'EN10MB', '-U', '-r', '-'] +if OPENBSD: + expected_command = [conf.prog.tcpdump, '-y', 'EN10MB', '-r', '-'] + popen.assert_called_once_with( - [conf.prog.tcpdump, '-y', 'EN10MB', '-r', '-'], + expected_command, stdin=subprocess.PIPE, stdout=None, stderr=None) print(bytes_hex(f.getvalue())) @@ -7158,6 +2751,14 @@ assert raw(pkt) in f.getvalue() f.close() del f, pkt += Check sniff() offline with linktype & 802.11 filter +~ tcpdump linux + +fd = get_temp_file() +wrpcap(fd, [RadioTap()/Dot11()/Dot11ProbeReq(), RadioTap()/Dot11()]) +lst = sniff(offline=fd, filter="subtype probe-req") +assert len(lst) == 1 + = Check tcpdump() command rejects non-string input for prog pkt = Ether()/IP()/ICMP() @@ -7219,1128 +2820,498 @@ r = tdecode(pkts, dump=True, linktype=DLT_EN10MB) assert b'Src: 192.0.2.1' in r assert b'Src: 192.0.2.2' in r assert b'Dst: 192.0.2.2' in r -assert b'Dst: 192.0.2.1' in r -assert b'Echo (ping) request' in r -assert b'Echo (ping) reply' in r -assert b'ICMP' in r -assert len(conf.temp_files) == tempfile_count - - -= Run scapy's tshark command -~ netaccess -tshark(count=1, timeout=3) - -= Check wireshark() - -f = BytesIO() -pkt = Ether()/IP()/ICMP() - -with mock.patch('subprocess.Popen', return_value=Bunch(stdin=f)) as popen: - # Prevent closing the BytesIO - with mock.patch.object(f, 'close'): - wireshark([pkt]) - -popen.assert_called_once_with( - [conf.prog.wireshark, '-ki', '-'], - stdin=subprocess.PIPE, stdout=None, stderr=None) - -print(bytes_hex(f.getvalue())) -assert raw(pkt) in f.getvalue() -f.close() -del f, pkt - -= Check Raw IP pcap files - -import tempfile -filename = tempfile.mktemp(suffix=".pcap") -wrpcap(filename, [IP()/UDP(), IPv6()/UDP()], linktype=DLT_RAW) -packets = rdpcap(filename) -assert(isinstance(packets[0], IP) and isinstance(packets[1], IPv6)) - -= Check wrpcap() with no packet - -import tempfile -filename = tempfile.mktemp(suffix=".pcap") -wrpcap(filename, []) -fstat = os.stat(filename) -assert fstat.st_size != 0 -os.remove(filename) - -= Check wrpcap() with SndRcvList - -import tempfile -filename = tempfile.mktemp(suffix=".pcap") -wrpcap(filename, SndRcvList(res=[(Ether()/IP(), Ether()/IP())])) -assert len(rdpcap(filename)) == 2 -os.remove(filename) - -= Check wrpcap() with different packets types - -import mock -import os -import tempfile - -with mock.patch("scapy.utils.warning") as warning: - filename = tempfile.mktemp() - wrpcap(filename, [IP(), Ether(), IP(), IP()]) - os.remove(filename) - assert any("Inconsistent" in arg for arg in warning.call_args[0]) - -############ -############ -+ Sessions - -= IPSession - dissect fragmented IP packets on-the-flow -packet = IP()/("data"*1000) -frags = fragment(packet) -tmp_file = get_temp_file() -wrpcap(tmp_file, frags) - -dissected_packets = [] -def callback(pkt): - dissected_packets.append(pkt) - -sniff(offline=tmp_file, session=IPSession, prn=callback) -assert len(dissected_packets) == 1 -assert raw(dissected_packets[0]) == raw(packet) - -= NetflowSession - dissect packet NetflowV9 packets on-the-flow - -import os -tmp = "/test/pcaps/netflowv9.pcap" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename - -dissected_packets = [] -def callback(pkt): - dissected_packets.append(pkt) - -sniff(offline=filename, session=NetflowSession, prn=callback) -records = dissected_packets[3][NetflowDataflowsetV9].records -assert len(records) == 24 -assert records[0].IPV4_SRC_ADDR == '20.0.1.174' -assert records[0].IPV4_NEXT_HOP == '10.100.103.1' - -= StringBuffer - -buffer = StringBuffer() -assert not buffer - -buffer.append(b"kie", 5) -buffer.append(b"e", 11) -buffer.append(b"pi", 2) -buffer.append(b"pi", 9) -buffer.append(b"n", 4) - -assert bytes_hex(bytes(buffer)) == b'0070696e6b696500706965' -assert len(buffer) == 11 -assert buffer - -= TCPSession - dissect HTTP 1.0 chunked image -~ http - -load_layer("http") - -import os - -tmp = "/test/pcaps/http_chunk.pcap.gz" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename - -a = sniff(offline=filename, session=TCPSession) - -a[2].show() -assert HTTPRequest in a[2] -assert a[2].Path == b'/httpgallery/chunked/chunkedimage.aspx?0.2911017199439567' -assert a[2].Accept_Encoding == b'gzip, deflate' -assert a[2].Accept == b'image/webp,image/apng,image/*,*/*;q=0.8' -assert a[2].Http_Version == b'HTTP/1.1' -assert a[2].Referer == b'http://www.httpwatch.com/httpgallery/chunked/' - -a[29].show() -assert HTTPResponse in a[29] -assert a[29].Transfer_Encoding == b"chunked" -assert a[29].Content_Type == b'image/jpeg; charset=utf-8' -assert a[29].Http_Version == b'HTTP/1.1' -assert a[29].Status_Code == b"200" -assert a[29].Reason_Phrase == b"OK" -assert len(a[29].load) == 33653 -# According to wireshark: -wireshark_data = b'/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNBCUAAAAAABAAAAAAAAAAAAAAAAAAAAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgCtwKdAwERAAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAAAQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPBUtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZqbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEyobHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq84/OfzmdJ0caLZycdQ1JT6rKaGO3rRj/z0+yPaubTszTccuM8o/e8/wBva/wsfhx+qf2D9vL5pF5T8z6jPpFvcQXTo6jhMgaq802NV+zv1+nOV7VxT0uplGJIidx7j+rk9n2DqYa3RwnMAzHpl7x+sUfiyq2876pFQTpHcDuSODfeu34Zjw7TyDnRc7J2VjPIkJva+edMkoLiOS3buftr943/AAzMh2njPMEOFk7KyD6SCnFpq+mXdBb3MbseiVo3/AmhzMx6jHPkQ4OTTZIfVEovLml2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KoTV9Vs9J0y51K9fhbWqGSQ99ugHux2Hvk8eMzkIjmWrPmjigZy5B8r+Y9evNe1q61W7P724eqpWoRBsiL7Ku2ddhxDHARHR811WplmyGcuZTvyBqXpXktg7fBcDnED/Og3A+a/qznPajR8eEZRzhz9x/a9d7EdoeHqJYCdsg2/rD9Yv5BnZzgn1VrAlrFUba61qtpT0LqRAOik8l/4FqjLoanJDkS0ZNLjn9UQm9r581KOguIY51HcVRj9IqPwzMh2pMfUAXCydk4z9JI+1ObXzzpEtBOslu3csOS/etT+GZsO08Z52HBydlZRyqSc2up6ddgfVrmOUn9lWHL/geuZkM0J/SQXByYJw+oEInLWp2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvD/zu86fW75fLdm9ba0YPfMp+1PT4U27IDv/AJXyzf8AZem4R4h5nl7njfaHX8UvBjyjz9/d8Pv9zyrNu80r2d1LaXcVzEaSQuHX3oehp2OV5sUckDCXKQpt0+eWHJHJH6okEfB67b3EdzbRXERrHKgdD7MKjPJNRgliyShLnE0+/aTUxz4o5I/TMAr8oclrFXHAlrFWjilb3wKmFp5g1m0oIbuQKOiMea/c1cyMeryQ5SLjZNHinziE4tPzAv0oLq3jmX+ZCY2/42H4Zm4+1Zj6gD9jhZOx4H6SR9qdWnnnRJqCUyWzf5a1X715ZmY+08UudhwMnZWWPKpJ1a6hY3QrbXEc3sjAn6QN8zYZYT+kguDkwzh9QIV8sa3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWN/mB5ti8seXZr0EG9l/c2MZ3rKw+1TwQfEfu75laPT+LOunV1/aetGnxGX8R2Hv/Y+YJZpZpXmlcySyMXkdjVmZjUkk9yc6sChQfOZSJNnmtwobxQz7yFqXrafJYufjtm5R/wDGNzX8Gr9+cL7U6PhyRyjlLY+8frH3PqPsN2jx4ZaeR3huP6p5/I/7plGcm941irjgS1irRxStwK0cVaOKWjgS4Eggg0I6EYqmVp5m121oI7t2UfsyfvB/w1cycetyx5S/S4uTQ4Z84j7k6tPzDu1oLu1SQd2jJQ/ceWZuPtaQ+oW4GTsaJ+mVe9O7PzxoNxQSSPbMe0qmn3ryH35m4+08UuZr3uDk7KzR5Di9yc217aXS8raeOZe5jYN+rM2GSMvpILgTxSh9QIVsmwdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirTMqqWYhVUVJOwAGKkvmj8zfOLeZvMUkkLE6ZZ1hsV7EA/FJ83P4UzqdDpvChv9R5vnva2u/MZbH0R2H6/ixEZmuqbxVvFCaeW9S/R+sQTMaROfSmJNBwfapPsaH6M13auj/MaeUP4uY94/FO47B7Q/KauGQ/TdS/qnn8ufwepZ5U+6NYpccCWsVaOKVuBWjirRxS0cCXYFWnFLRxV2BLau6MGRirDowNCPuxBI5IIB5ppZ+bNftaBLtpFH7MtJB97b/jmXj1+aP8V+/dxMnZ+GfONe7ZO7T8x5hQXloreLxMV/4VuX68zcfa5/ij8nAydij+GXzTuz87eX7igaZrdj+zMpH/AAw5L+OZ2PtLDLrXvcDJ2Xmj0v3J1Bc21wnO3lSZP5o2DD7xmbGcZCwbcCcJRNSFKmSYuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV5p+dfnP9F6QNCs5KX2pKfXI6pbVof+RhHH5Vza9mabjlxnlH73n+39f4ePw4/VPn7v2/reCZ0LxLhihvFW8UN4q9Q8sal9f0eGRmrNEPSm3qeSdz8xQ55l29o/A1Mq+mXqHx5/a+1+y/aH5nRxJPrh6T8OXzFfFNM0z0TjgS1irRxStwK0cVaOKWjgS7Aq04paOKuwJaOKtYEtYq0cCtxyyxOHido3HRlJB+8YRIg2FMQRR3Tiz85eYbWgFyZkH7MwD/APDH4vxzMx9o5o9b97hZOzcE/wCGvdt+xPLP8yTsL2z+bwt/xq3/ADVmdj7Y/nR+Tr8vYn8yXz/H6E8svOnl66oPrPoOf2ZgU/4b7P45nY+0cMute9wMvZmeHS/d+LTmKaGZA8MiyIejIQw+8ZmRkCLBtwZRMTRFL8kxdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVQWs6vZaPpdzqd63C2tUMjnuadFX3Y7D3yeLGZyERzLTnzRxQM5cg+VvMOuXmu6zdareH99cvy41qEUbIg9lUAZ1+HEMcREdHzbVaiWbIZy5lL8saHDFDeKt4obxVk3kTUvQ1J7Nz+7ul+H2dASPvFfwznPabR+Jg4x9WP7jz/AEF7H2L7R8HVHEfpyiv84bj9I+IZ9nnj6444EtYq0cUrcCtHFWjilo4EuwKtOKWjirsCWjirWBLWKtHArRxS0cUtYFawJXw3FxbvzgleJ/5kYqfvGSjMx3BpjKEZCiLTqz88eYragM4uEH7Myhv+GHFvxzNx9p5o9b97g5eysE+le5PbL8zIjQXtmy+LwsG/4Vqf8SzOx9sj+KPydfl7DP8ABL5p9ZecfLt3QLdrE5/YmrHT6W+H8cz8faGGf8Ve/Z12Xs3PD+G/dum8ckciB42DoejKQQfpGZgIO4cIxINFdhQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8L/O/wA6G91FfLlnJ/oti3K9ZTs89Nk+UY/4b5Z0HZem4Y8Z5nl7njfaDX8c/Cj9Mefv/Z97yzNs823irhihvFW8UN4qqQTSQTRzRHjJEwdD1oVNRkckBOJieR2Z4ssscxOJqUTY94etWN3HeWcN1H9iZA4HhXqDTuOmeSazTHBlljP8J/s+x9+7P1kdTghljymL/WPgdlY5iua1irRxStwK0cVaOKWjgS7Aq04paOKuwJaOKtYEtYq0cCtHFLRxS1gVrAlo4paxVrAlrFVa2vby1fnbTyQN4xsV/UclDJKJuJIYTxRmKkAU7s/P3mK2oHlS5QfszKK/8EvE/fmdj7VzR5ni97gZeyMEuQ4fcn1l+Z1o1Be2bx+LxMHHzo3Gn35n4+2on6o17nXZew5D6JA+9P7Lzb5dvKCO9RGP7EtYzXw+Og+7M/Hr8M+Uh8dnXZezs8OcT8N/uTZWVlDKQyncEbg5lg24ZFN4UOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVjH5ieb4/LHlya7Ug389YbCM71lYfaI8EHxH7u+Zej0/izrp1dd2nrRp8Rl/Edh7/2PmCSSSWRpZGLyOxZ3Y1JYmpJJ7nOrAp88kSTZaxYt4q4YobxVvFDeKuxQzjyFqXqW02nufihPqRD/ACGPxAfJv15xPtXo6lHMOvpP6Px5PpnsJ2jcZ6eX8Pqj7uv218yyw5xz6G1irRxStwK0cVaOKWjgS7Aq04paOKuwJaOKtYEtYq0cCtHFLRxS1gVrAlo4paxVrAlrFXYFWnFLRwK7FKvaalqFm1bW5kgP/FblQfmAcnjzTh9JIasmGE/qAKfWX5ieYbegmaO6Uf78WjU+acfxzPx9r5o86l73X5exsMuVx937U/sfzP096Le2skB/mjIkX6a8D+vNhi7agfqiR9rrsvYUx9Egffsn9j5p8v3tBBfR8j0Rz6bfc/Gv0Zn4tdhnykPu+912XQZsfOJ+/wC5NQQRUbg9DmW4bsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVad0RGd2CooJZiaAAbkknEBBNPmP8AMrzk/mfzFJNEx/RtpWGwTxQH4pPnId/lQds6rRabwoV/Eeb592rrvzGWx9A2H6/ixQZmOsbxQ3irhihvFW8UN4q7FCYaFqJ0/VILmtIw3GXr9htm6eHXMLtHSDUYJY+pG3v6Oy7H150mqhl6A7/1Tsfs+16pWoqOmeTEEGi+9xkCLHJrAyaOKVuBWjirRxS0cCXYFWnFLRxV2BLRxVrAlrFWjgVo4paOKWsCtYEtHFLWKtYEtYq7Aq04paOBXYpawKtxS7ArRxSirLV9UsSDaXUsIH7KOQv0r0OW49Rkh9MiGnLp8eT6ogsgsvzJ1+CguBFdL3Lrwb70oPwzPxdsZo86k67L2Jhl9Nx/HmyCy/M/SZaC8t5bZj1ZaSIPp+FvwzY4u2sZ+oEfa63L2FkH0kS+xkFj5k0G+p9WvomY9EZuD/8AAvxb8M2GLWYp/TIOty6LNj+qJTLMlxXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8x/O3zp+jdKXQLOSl7qK1uip3S26Ef89Dt8q5tey9NxS4zyj9/wCx57t7XeHDwo/VLn7v2vBM6F4tsYq3ihvFXDFDeKt4obxV2KG8Vek+UtR+uaNEG/vbb9y/yUfCf+Bpnm3tFo/B1JkPpn6vj1+3f4vs3sh2h+Y0Yifqxek+7+H7NvgnOaF6lo4pW4FaOKtHFLRwJdgVacUtHFXYEtHFWsCWsVaOBWjilo4pawK1gS0cUtYq1gS1irsCrTilo4FdilrAq3FLsCtHFLWBLWKtHArRxSjrHXtZsKC0vJYlHRAxKf8AAGq/hl+LVZMf0yIcfLpMWT6ogsgsfzO1yGguoorte5p6b/evw/8AC5sMXbWWP1AS+z8fJ1uXsLFL6SY/b+PmyGx/M/Q5qLdRS2rHq1BIg+lfi/4XNji7axH6gY/b+Pk63L2Flj9JEvs/HzZFY6/ot/QWl7DKx6IGAf8A4A0b8M2GLVYp/TIF1mXSZcf1RIR+ZDjuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KoHXNZstF0m61S9bjb2qF28WPRVX3ZqAZZixmchEcy06jPHFAzlyD5U1/W73XNYutVvDWe6csV6hV6Ki+yrQDOuw4hjiIjo+b6nUSzZDOXMpfljQ2MVbxQ3irhihvFW8UN4q7FDeKsh8laiLXVfQc0iuxw3oBzXdP4j6c0HtHo/G0xkPqx7/Dr+v4PV+x3aP5fWCBPoy+n4/w/bt8XoOebvsjRxStwK0cVaOKWjgS7Aq04paOKuwJaOKtYEtYq0cCtHFLRxS1gVrAlo4paxVrAlrFXYFWnFLRwK7FLWBVuKXYFaOKWsCWsVaOBWjilrArRxVrFLRwJTKx8y69YUFrfSoq9Iy3NB/sH5L+GZOLWZcf0yLi5dDhyfVEMk0z8ztaDrFdW0V1/lLWJvpI5L/wubLB21lupAS+x0uu7JwYoHJxGIHx/HzelWtzFc20VxEaxzIHQ+zCudLCYlESHIvOSjwmlTJMXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8H/O/wA6fpDVF8vWclbPT25XZXo9xSnH5Rg0/wBavhnQdl6bhjxnmeXueN7f13HPwo/THn7/ANjy3Ns863ihsYq3ihvFXDFDeKt4obxV2KG8VXRyPHIskbFXQhkYdQQagjAQCKKYyMSCNiHq2m3yX1hBdpsJVBYDsw2YfQds8l7Q0h0+eWPuO3u6fY++9k68avTQzD+Ib+/kftRJzDdktwK0cVaOKWjgS7Aq04paOKuwJaOKtYEtYq0cCtHFLRxS1gVrAlo4paxVrAlrFXYFWnFLRwK7FLWBVuKXYFaOKWsCWsVaOBWjilrArRxVrFLRwJdiqY2EHBPUYfE/T2GZeCFC3hfaLtDxMnhR+mHPzl+z9b0nyBqXrWEli5+O2blH/wAY3Nfwav3jOj7MzXEwPT7j+11+OXFjB7tj8OX2fcyrNol2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVi35j+cI/K/lyW5jYfpC5rDYIf8AfhG708EG/wBw75l6LTeLOug5uu7U1o0+IkfUdh+PJ8wPI8kjSSMXdyWdiakk7kk51YFPnpN7lbihvFDYxVvFDeKuGKG8VbxQ3irsUN4q7FWZ+Q9Rqs+nud1/ewg16HZx99D9+cd7V6OxHMP6p/R+l9F9g+0aM9NI/wBKP3S/Qfmy45xL6WtwK0cVaOKWjgS7Aq04paOKuwJaOKtYEtYq0cCtHFLRxS1gVrAlo4paxVrAlrFXYFWnFLRwK7FLWBVuKXYFaOKWsCWsVaOBWjilrArRxVrFLRwJVrSD1Zd/sLu39MsxQ4i6ntjtD8thJH1y2H6/gmozPfOLTXy3qX6O1iCdm4wsfSnPQcH2JPspo30ZkaXL4eQS6dfd+N3L0cvUY/zvv6fq+L1TOncl2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KtSSJGjSSMERAWdiaAAbkk4gWgmhZfL/5kecZPNHmOW5jY/o62rDYIa09MHd6eMh3+VB2zq9FpvChX8R5vn3amtOoykj6RsPx5sWzLda7FW8UNjFW8UN4q4YobxVvFDeKuxQ3irsVRmk3zWGowXQ3EbfGPFTsw/wCBOY2t0wz4ZYz/ABD+z7XN7N1p0uohmH8B+zqPiHqisroGUgqwqpG4IOeRzgYyMTzD9AYskZxEom4yFj3FrIM2jirRxS0cCXYFWnFLRxV2BLRxVrAlrFWjgVo4paOKWsCtYEtHFLWKtYEtYq7Aq04paOBXYpawKtxS7ArRxS1gS1irRwK0cUtYFaOKtYpdQk0G5PQYolIAWeSa28IiiC9+rH3zOxw4Q+a9qa46nMZfwjaPu/aqjLHWrsUg1u9Q8q6l9f0WF2NZof3Mx6nkgFCf9ZaHOk0Wbjxi+Y2Lt5HiqQ/i3/X9qb5lsHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXlv54edP0fpa+XrOSl5qC8rsqd0t604/OQin+rXxzbdl6bilxnkOXved7f13BDwo/VLn7v2vBs6B41vFXYq3ihsYq3ihvFXDFDeKt4obxV2KG8VdireFD0LyfqP1rSFidqy2p9Miorw6oafLb6M869ptH4Wo4x9OTf49f1/F9h9i+0fH0nhyPqxGv83+H9I+CeZzb2DRxVo4paOBLsCrTilo4q7Alo4q1gS1irRwK0cUtHFLWBWsCWjilrFWsCWsVdgVacUtHArsUtYFW4pdgVo4pawJaxVo4FaOKWsCtHFWsUouxgq3qsNhsvz8cvwws28v7R9ocEfBid5fV7u74/d70dmU8U2MKrsUsm8ial9X1NrN2pHdr8NenqJuPvFfwzY9nZeHJw9Jfe5+llcTHu3H6f0fa9BzfNrsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVS/wAwa3ZaHo91qt4aQWqFyvQs3RUX3ZqAZZhxHJIRHVo1OeOHGZy5B8p67rV7rer3WqXrcri6cuwHRR0VF9lWgGddixCEREcg+c6jPLLMzlzKAyxobxV2Kt4obGKt4obxVwxQ3ireKG8VdihvFXYq3hQnnlDUfqmrJGxpFdfumG9OR+wafPb6c0nb+j8fTGvqh6h8Of2PS+yfaP5bWxs+jJ6T8eX2/Zb0LPMX2xo4q0cUtHAl2BVpxS0cVdgS0cVawJaxVo4FaOKWjilrArWBLRxS1irWBLWKuwKtOKWjgV2KWsCrcUuwK0cUtYEtYq0cCtHFLWBWjirccZkcKO/fDEWaaNXqY4MZyS5D8UmqKFUKNgNhmfEUKfL8+eWWZnLnJdhamxhVdilUgmkgmjmiPGWJg6HwZTUfjkgSDY5hsw5OCYl+K6/Y9csLyK9sobuL7EyBwOtCeoPuDtnU4sgnESHV2U40aV8sYuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvA/zu86fpLVl0CzkrZac1boqdnuehH/ADzG3zrnQ9l6bhjxnmfueM7e13iT8KP0x5+/9jzDNq8+7FDeKuxVvFDYxVvFDeKuGKG8VbxQ3irsUN4q7FW8KFyMVYMpIYGoI6g4Ct09Q0i+F9p0F1+06/GOlHGzfiM8o7V0f5fUSh05j3Hl+p977C7Q/N6SGX+Kql/WGx/X8UWc1ztmjilo4EuwKtOKWjirsCWjirWBLWKtHArRxS0cUtYFawJaOKWsVawJaxV2BVpxS0cCuxS1gVbil2BWjilrAlrFWjgVo4pawK0cVR1nDwTmftN+AzKwwoW8L7QdoeLk8OP0Q+0/s5fNEjL3nW8VbGFV2KWxhVnH5f6lzt59Oc/FEfVhH+QxowHybf6c3HZmXYwPTcfp/Hm7PFLixg9Y7fq/V8GXZtkuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVif5l+ck8r+XJJ4mH6Suqw2CHrzI+KSnhGN/nQd8zNFpvFnX8I5ut7U1v5fESPqOw/X8HzCzu7s7sWdiSzE1JJ3JJOdUA+fE21irsUN4q7FW8UNjFW8UN4q4YobxVvFDeKuxQ3irsVbwobGBWWeRdQ4yz2DnZ/3sXT7Q2YeO4p92cl7V6PixxzDnHY+48vt+97/2D7R4MstPI7T9UfeOfzH+5Zgc4R9SaOKWjgS7Aq04paOKuwJaOKtYEtYq0cCtHFLRxS1gVrAlo4paxVrAlrFXYFWnFLRwK7FLWBVuKXYFaOKWsCWsVaOBWjilrAqrbRepJv8AZXc5PHCy6ntntD8th2+uWw/X8EwGZr5y2MKt4q2MKrsUtjCqP0TUTp2qW92T+7RqTDxjbZvuG+XYMvhzEu77nK0c6nw/ztv1fb9j1cEEVHTOocp2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVbLJHFG8srBI4wWd2NAFAqSSewwgWgkAWXy7+YvnCTzR5kmu1JFhBWGwjO1IlP2iP5nPxH7u2dVo9P4UK69Xz3tPWnUZTL+EbD3ftYuMy3Xt4q7FDeKuxVvFDYxVvFDeKuGKG8VbxQ3irsUN4q7FW8KGxgVE6feSWd7DdJ9qJg1OlR3H0jbKdTgjmxyxy5SFOTotXLT5o5Y84EH9nx5PUY5EljSSMhkcBkYdCCKg55DlxSxzMJc4mn6DwZo5ccZx3jIAj4tnK25o4EuwKtOKWjirsCWjirWBLWKtHArRxS0cUtYFawJaOKWsVawJaxV2BVpxS0cCuxS1gVbil2BWjilrAlrFWjgVo4paAJNB1PTFjKQiCTyCYwxiOML37n3zLxxoPmvamuOpzGX8PIe5Uyx1zYwpbxVsYVXYpbGFW8KvSvJ2pfXNFjRjWa1/cv8lHwH/gafTnQaDLx4wOsdv1O2lLjAn/O+/r+v4p3maxdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiryr88vOv1HTl8uWclLu+Xnesp3S3rsnzkI/wCB+ebbsvTcUuM8hy97zvb2u4IeFHnLn7v2vCM6B45wxVvFXYobxV2Kt4obGKt4obxVwxQ3ireKG8VdihvFXYq3hQ2MCtjChnnk3UDcaabdzWS1biOv2G3Xr9Izz72p0fBmGUcp/eP2V9r637Ddo+LpjhkfViO39U/qN/Ynxzl3uGjgS7Aq04paOKuwJaOKtYEtYq0cCtHFLRxS1gVrAlo4paxVrAlrFXYFWnFLRwK7FLWBVuKXYFaOKWsCWsVaOBWjilEWkW/qH5Ll2KPV5b2j7Q4Y+DHmfq93d8fxzRYzIeLbwq2MKW8VbGFV2KWxhVvCrIPJOpfVNXEDmkV4PTP+uN0P61+nM3QZeDJXSW36vx5udpJWDD4j9P2b/B6LnQNzsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqXeYtdstB0W61W8P7m2TlxrQux2RB7sxAy3DiOSQiOrRqdRHDjM5cg+UNb1i91nVbnU71+dzdOXc9h2CrX9lRQD2zrcWMQiIjkHznPmllmZy5lB5Y0uGKt4q7FDeKuxVvFDYxVvFDeKuGKG8VbxQ3irsUN4q7FW8KGxgVsYUJv5Yv8A6nq8RY0jm/cyf7I7H/gqZqe2tH+Y00oj6h6h7x+sbO+9me0fymthI/RL0y9x/UaL0M55Y+6tHAl2BVpxS0cVdgS0cVawJaxVo4FaOKWjilrArWBLRxS1irWBLWKuwKtOKWjgV2KWsCrcUuwK0cUtYEtYq0cCtohdgo79ThAs04+s1UcGI5JdPt8keqhQAOg6ZmAU+YZ80sszOXOS4YWpvCrYwpbxVsYVXYpbGFW8Kro3dHV0Yq6EMjDqCDUH6Dj7mzFkMJCQ6PWdKv01DToLtaD1UBZR2cbMv0MCM6jBl8SAl3uznEA7cunu6IrLWDsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVfP/AOd3nX9KawNBs5K2GmsfrBB2kuaUP0Rg8fnXOh7M03BHjPOX3PGdu67xJ+HH6Y8/f+z9bzLNq6BvFDhireKuxQ3irsVbxQ2MVbxQ3irhihvFW8UN4q7FDeKuxVvChsYFbGFDeKHpWh6h9f0yGcmstOM3SvNdiTTx655X21o/y+plEfSdx7j+rk+7+zfaP5vRwmT64+mXvH6xR+KOOal3zsCrTilo4q7Alo4q1gS1irRwK0cUtHFLWBWsCWjilrFWsCWsVdgVacUtHArsUtYFW4pdgVo4pawJaxVo4FRdvHxXkftN+rMjFGt3g/aDtDxcnhxPoh9p/ZyVsuefbGKt4VbGFLeKtjCq7FLYwq3hVsYqzLyBqW9xpzn/AIvh/BXH6j9+bbszLuYH3j9LssMuLH5x2+B5fp+xmWbdk7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqxD8z/Oi+V/LkkkDD9J3lYbBe4Yj4paeEYNfnTMzQ6bxZ7/AEjm6ztXXfl8Vj65bD9fwfMLMzMWYlmY1ZjuST3OdS8CS7ChvFDhireKuxQ3irsVbxQ2MVbxQ3irhihvFW8UN4q7FDeKuxVvChsYFbGFDeKGTeSdQ9O6lsXPwzDnEK7c1G4A91/VnLe1Oj48IyjnDn7j+39L3XsL2j4WolgkfTlG39YfrF/IMyOefPrbsCrTilo4q7Alo4q1gS1irRwK0cUtHFLWBWsCWjilrFWsCWsVdgVacUtHArsUtYFW4pdgVo4pawJaxVfDHzep6DrkoRsuo7Z7Q/L4dvrlsP0n4fejBmU+dN4VbGKt4VbGFLeKtjCq7FLYwq3hVsYqi9LvnsNQgu1r+6cFwO6HZh9Kk5ZiyGEhLucnSzEZ0eUtvx7jResI6OiuhDIwBVh0IO4OdQDYsOYQQaLeFDsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiq2aaKGF5pnEcUSl5JGNFVVFSST2AwgWaCJEAWeT5Z/MPzhL5p8yT3oJFjF+5sIztSJT9qn8zn4j93bOr0mn8KFder592lrDqMpl/CNh7mM5lOvbxVvFDhireKuxQ3irsVbxQ2MVbxQ3irhihvFW8UN4q7FDeKuxVvChsYFbGFDeKFW0uZLa5juIz8cTBl60NOxp2OVZsUckDCXKQpv02olhyRyQ+qJBHweoQTRzwxzRmscqh0PswqM8g1OCWHJKEucTT9C6PVR1GGOWP0zAK/KHJWnFLRxV2BLRxVrAlrFWjgVo4paOKWsCtYEtHFLWKtYEtYq7Aq04paOBXYpawKtxS7ArRxS1gS4Ak0HU4sZzEQSdgEXGgRaffmTCNB807S1x1OYz/h5D3Lxk3Abwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCr0PyVqP1rSBbuay2Z9M77+md0Pyp8P0ZvezsvFj4esfu6fq+DtuLjiJ9/P3jn+v4sgzPYuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvJ/z087fU7BfLVlJS5vFD37Kd0g/ZTbvIev+T882/Zem4j4h5Dl73nO3tdwx8KPOXP3fteE5v3kXYobxVvFDhireKuxQ3irsVbxQ2MVbxQ3irhihvFW8UN4q7FDeKuxVvChsYFbGFDeKHDArNvJd+JbF7Nj8duap0+w5r9NGrnB+1ej4ckcw5S2PvH7PufVfYLtHjwy055wPEP6p5/I/7pkWci+gLTilo4q7Alo4q1gS1irRwK0cUtHFLWBWsCWjilrFWsCWsVdgVacUtHArsUtYFW4pdgVo4pawJVoE/bP0ZZjj1eU9o+0OEeDHmd5e7oFfMh41sYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCqd+UdS+pazGrGkN1+5k8KsfgP/BbfTmXosvBkHcdv1fb97naSV3D4j3j9l/IPSM6FudirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqWeZdfsvL+iXWrXh/dWyVVK0LudkRfdm2y3DiOSYiOrRqtRHDjM5dHyfrGrXur6pc6nevzurqQySHtv0A9lGw9s67HjEIiI5B86zZpZJmcuZQmTanYobxVvFDhireKuxQ3irsVbxQ2MVbxQ3irhihvFW8UN4q7FDeKuxVvChsYFbGFDeKHDAqZ+X7/wCo6pDKTxic+nMTQDi3cn2NDmu7W0f5jTyh/FzHvH4p3PYHaP5PWQyH6bqX9U8/lz+D0XPJ33xacCWjirsCWjirWBLWKtHArRxS0cUtYFawJaOKWsVawJaxV2BVpxS0cCuxS1gVbil2BWjilyqWan34gWXG1mqjgxHJLp9pRQAAoOgzJAp8wzZpZJmcvqK7JNbYxS3hVsYq3hVsYUt4q2MKrsUtjCreFWxireFWxXsaHsR1xbMczGQkOYeqaHqI1DS7e6JHqMtJQNqSL8LbfMbe2dLpsviYxLr+l2cwLscjuEdl7B2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV89/nb52/S+tDQ7OSun6WxExB2kuejH/AJ5/ZHvXOi7M03BHjPOX3PGdua7xMnhx+mH3/seaDNo6FvFXYobxVvFDhireKuxQ3irsVbxQ2MVbxQ3irhihvFW8UN4q7FDeKuxVvChsYFbGFDeKHDArYxV6L5ev/rulQyMSZYx6UpNSeS9yT1qKHPMO39H4GplX0z9Q+PP7X3H2U7R/NaKNn14/Qfhy+Yr42mBzSPStHFXYEtHFWsCWsVaOBWjilo4pawK1gS0cUtYq1gS1irsCrTilo4FdilrAq3FLsCtHFKvEnEVPU5djjTwXb/aHjZeCP0Q+09f1KmWOgbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYq3hVsYUss8hajwuJ9Pc/DMPVhH+Woow+lafdmy7Ny1Iw79/x+OjsMEuLHXWP3H9R+9m2blm7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWG/mp50Hljy25t3pql9ygsR3U0+OX/YA7e5GZuh03iz3+kc3Wdq63wMW31y2H6/g+YSzMxZiSxNSTuSTnUPBFsYUN4q7FDeKt4ocMVbxV2KG8VdireKGxireKG8VcMUN4q3ihvFXYobxV2Kt4UNjArYwobxQ4YFbGKsi8mXxiv3tGPwXC1X/AF03/wCI1/DOb9p9H4un4x9WPf4Hn+t7P2I7R8HV+ET6cor/ADhy/SPkzM55y+xtHFXYEtHFWsCWsVaOBWjilo4pawK1gS0cUtYq1gS1irsCrTilo4FdilrAq3FLsCro1qanoMlGNl0/bXaH5fDUfrlsP0lXGXvnjeFW8KtjFLeFWxireFWxhS3irYwquxS2MKt4VbGKt4VbGFKIsruSzvIbuPd4HDgeIHVf9kKjJQmYyEhzDfpsgjMXyOx937Ob1eGaOeGOaI8o5VDo3irCoOdPGQkARyLmyiQaPRfkkOxV2KuxV2KuxV2KuxV2KuxV2KuxVZPPDbwSTzuI4YlLyyMaBVUVYk+AGEAk0ESkALPIPlb8wfN83mnzJPf1Is4/3NhEduMKnYkfzP8AaOdXpNOMUAOvV8+7R1h1GUy/h6e5jYzKcFsYobxV2KG8VbxQ4Yq3irsUN4q7FW8UNjFW8UN4q4YobxVvFDeKuxQ3irsVbwobGBWxhQ3ihwwK2MVVIJXhmjmjNHjYOp67qajIzgJRMTyOzPFlljmJx2lE2PeHplrcx3VtFcR/YlUMBttXsadxnkOt0xwZpYz/AAn+z7H6G7O1sdVp4Zo8pxv49R8DsqHMVzXYEtHFWsCWsVaOBWjilo4pawK1gS0cUtYq1gS1irsCrTilo4FdilrAq3FLqVNMDGcxGJkdgFZRQUy+IoPmnaOtOpymZ5dPcvGScFvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWe+RtR9fTXs3NZLRvh/wCMb1K/caj5UzddnZbgY/zfuLtBLjgJfA/D9lfG2SZsUOxV2KuxV2KuxV2KuxV2KuxV2KuxV5F+e3nf6rZr5Ysn/f3SiTUWU7rDWqR/NyKn2+ebjsvTWfEPTk8529ruGPhR5nn7u74vC83zyTYxVsYobxV2KG8VbxQ4Yq3irsUN4q7FW8UNjFW8UN4q4YobxVvFDeKuxQ3irsVbwobGBWxhQ3ihwwK2MVbxQzDyZf8AO2ksnPxQnnGO/FuoHyb9ecR7WaOjHMOvpP6P0vqHsB2jcZ6aXT1R938X20fiyM5xj6O7Alo4q1gS1irRwK0cUtHFLWBWsCWjilrFWsCWsVdgVacUtHArsUtYFW4pVI1/a+7JwHV5T2j7QoeBHrvL9A/Svy145cMUt4Vbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYq3hVsYUt4qm3ljUPqOsQSMaRSn0Zf9VyKH6GoflmTpcvBkB6Hb5/tczRy3MP533j8EfF6XnRN7sVdirsVdirsVdirsVdirsVdiqVeaPMNl5e0K61a7PwW6VSOtDJIdkQe7N/XLcGE5JiI6uPqtRHDjM5dHydq+q3uranc6lev6l1dSGSVvc9h4ADYDwzrseMQiIjkHzzNllkmZS5lCZNqbGKtjFDeKuxQ3ireKHDFW8VdihvFXYq3ihsYq3ihvFXDFDeKt4obxV2KG8VdireFDYwK2MKG8UOGBWxireKEfot99S1GGcmkYPGXr9htjsOtOuYXaOkGowSx9429/T7Xa9i686TVQy9Inf8AqnY/Y9EzyOUSDR5v0DGQkLHIuyLJo4q1gS1irRwK0cUtHFLWBWsCWjilrFWsCWsVdgVacUtHArsUtYFaUVNMQLcXW6uOnxGZ6faVUZeA+ZZssskjKXMt4WtcMUt4Vbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYq3hVsYUt4q3QEEHoeuGkxkYkEcw9O8ual+kNIgmZuUyD05/HmmxJ/1hRvpzodJl48YJ58j+PtdrOj6hylv+PcdkyzJYOxV2KuxV2KuxV2KuxV2KuxV87/nZ52/TOtjRrOTlpulsVkKnaS56O3yT7I+nxzo+zdNwQ4j9UvueM7b13i5OCP0w+/8AY81zZuibxVsYq2MUN4q7FDeKt4ocMVbxV2KG8VdireKGxireKG8VcMUN4q3ihvFXYobxV2Kt4UNjArYwobxQ4YFbGKt4oXrgLKLPfLl79a0uPkayQ/un/wBiPhP/AANM819o9H4OpMh9OTf49f1/F9r9j+0fzGjESfXi9Pw/h+zb4JnnPvVtHFWsCWsVaOBWjilo4pawK1gS0cUtYq1gS1irsCrTilo4FdilrAq9RQe+WRDwXb3aHjZeCP0Q+09f1NjJuhbwquGKW8Kt4VbGKW8KtjFW8KtjClvFWxhVdilsYVbwq2MVbwq2MKW8VbGFWT+RtR9G/ksXPwXQ5Rj/AIsQV2/1k/Vmw7Oy8M+H+d94/Z9zsNNLigR/N3+B5/bXzLOc3TN2KuxV2KuxV2KuxV2KuxVhX5sedR5Z8tuLZ+Oq6hyhsqdUFP3kv+wB2/yiMztBpvFnv9I5ur7W1vgYtvrlsP1vmOpO53J6nOoeEaxQ3irYxVsYobxV2KG8VbxQ4Yq3irsUN4q7FW8UNjFW8UN4q4YobxVvFDeKuxQ3irsVbwobGBWxhQ3ihwwK2MVbxQvXAWUU+8q3ogv/AEWNEuAF7AcxuvX6R9Oc/wC0ej8bTGQ+qHq+HX7N/g9j7Hdofl9YIk+nL6fj/D9u3xZlnmr7K0cVawJaxVo4FaOKWjilrArWBLRxS1irWBLWKuwKtOKWjgV2KXKN64Yi3Tdt9ofl8VR+uew/SV4y189cMVbwquGKW8Kt4VbGKW8KtjFW8KtjClvFWxhVdilsYVbwq2MVbwq2MKW8VbGFVW3nlt547iI0lhYOnYVU1ofY98IkYmxzDdp8nBME8uvu6vVrS5iurWK5iNY5kDrXrRhXf3zpscxOIkORc+ceEkKuTYuxV2KuxV2KuxV2KqdxcQW1vLcXDiKCFWklkY0VVUVYk+wwgEmgxlIRFnkHyp5/83T+afMlxqJqtqv7qxiP7EKk8ajxb7R9znWaTTjFAR69Xz/tDVnPlMunT3MczJcJ2KG8VbGKtjFDeKuxQ3ireKHDFW8VdihvFXYq3ihsYq3ihvFXDFDeKt4obxV2KG8VdireFDYwK2MKG8UOGBWxireKF64CyirRO8bK6Eq6kMrDqCNwchIAii5GORiQRzD0e1aWewt7wxMkdwgZWIPEnoQCQK0IIzybtHRnT5pQ6A7e7o++dk68arTwy9ZR39/X7VxzBdk1gS1irRwK0cUtHFLWBWsCWjilrFWsCWsVdgVacUtHArsWM5iETKWwC4ZYBT5p2hrDqMpmeXTyDYyThOGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwquxS2MKt4VbGKt4VbGFLeKtjCreKs38iah6lpNYOfit25xf6khqR9DV+8Zt+zctgw7v0/t+92cJccAeo2P6Ps2+DKM2auxV2KuxV2KuxV2KvH/z487m3t08rWUlJrgCXUmU7rH1SLb+f7Te1PHNx2XprPiH4PN9va2h4UeZ5/qeGZvnlG8VdihvFWxirYxQ3irsUN4q3ihwxVvFXYobxV2Kt4obGKt4obxVwxQ3ireKG8VdihvFXYq3hQ2MCtjChvFDhgVsYq3iheuAsop/5N8tT+Ytdt9OjqsJ+O6lH7EK/aPzPQe5zG1WcYoGTs+ztIdRkEBy6+59KW9na21pHaQxqltCgjjiA+EIooBTOUmeIkne30bHEQAEdgEBeeV9Bu6mS0RGP7cX7s/8LQH6cw8mhxT5x+WznY9fmhyl890jvPy5tmqbO7eM9klAcfevH9WYWTsiP8Mvm5+PtqX8Ufkkd55H1+3qUiW4Ud4mBP8AwLcTmDk7NzR6X7nYY+1cMuZ4feklzaXVs3C4heFvCRSp/HMGeOUeYpzoZIy3iQVE5BsaOKWsCtYEtHFLWKtYEtYq7Aq04paOBXDJRDyntH2hQ8CJ85foH6fk2Mm8euGFLhireFVwxS3hVvCrYxS3hVsYq3hVsYUt4q2MKrsUtjCreFWxireFWxhS3irYwq3iqYaFqH6P1WC5Y0irwnPb032JP+rs30Zdgy+HMS6dfd+N3M0cvVw/zvv6fq+L0/Okb3Yq7FXYq7FXYqlPmvzHZ+XNButWut1gX91HWhklbZEHzP3DfLsGE5JiIcfV6mOHGZno+TNU1O81TUrjUb1/UurqRpZX92NaDwA6AeGdbCAjERHIPnmXLLJIylzKFybW3irsUN4q2MVbGKG8VdihvFW8UOGKt4q7FDeKuxVvFDYxVvFDeKuGKG8VbxQ3irsUN4q7FW8KGxgVsYUN4ocMCtjFW8UL1wFlF9D/AJXeUf0BoKzXKcdSvwstxXqiU+CP6Aan3Ocxr9T4k6H0h9D7F0HgYrl9ctz+gMyzBdw7FXYq7FVskccilJFDoeqsAQfoOAgHYpBI3CUXnlDy/dVLWqxOf2oSY/wHw/hmJk7Pwy/hr3Obj7RzQ/iv37pHe/ltGamyvCvgky1/4Zaf8RzAydjj+GXzdhi7bP8AHH5JDe+SfMNtUiAXCD9qFg3/AApo34Zg5Ozc0el+52GLtTBPrXvSWe3uIH4TxPE/8rqVP3HMGUDE0RTnRnGQsG1I5FsaxVrAlrFXYFWnFLsXE12rjp8Rmfh5lrLA+Z5MkpyMpGyWxi1rhhS4Yq3hVcMUt4Vbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYq3hVsYUt4q2MKt4q31wpBp6P5W1E32jxFzWaD9zL41QChPzUg5vtFl48YvmNvx8HazPFUh/Fv+v7U3zLYOxV2KuxV2KvnP86vO/6c139E2cnLTNLYqSOklx0d/cL9lfp8c6Ps3TcEOI/VL7ni+2td4uTgj9MfvecZsnSuwobxV2KG8VbGKtjFDeKuxQ3ireKHDFW8VdihvFXYq3ihsYq3ihvFXDFDeKt4obxV2KG8VdireFDYwK2MKG8UOGBWxireKGfflH5POta2NRukrpunEOajaSbqifR9pvo8c1vaWp8OHCPql9zv+wOz/Gy8cvoh9p6D9L37Obe+dirsVdirsVdirsVdirsVWTQQzoY5o1lQ9UcBh9xyMoiQoi2UZmJsGkmvfJXl26qfq3oOf2oSU/4XdfwzDydm4ZdK9znYu1M8Ot+/8WkN5+WjbmyvQfBJlp/wy/8ANOYGTsb+bL5uwxdufz4/L8fpSC98meYrWpNqZkH7UJElf9iPi/DMDJ2dmj/Dfu3dji7TwT/ir37fsSaWKWJykqNG46qwKkfQcwpRI2LnxkCLG6zIpWnFLR8MkA8B272h4+Xgj9EPtPUuyTo2xiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwquxS2MKt4VbGKt4VbGFLeKtjCreKrhhSyDyXqH1fVDbOaRXa8R/wAZEqV+VRyH3Zm6DLw5K6S+/wDFudpZXAx7tx+n9HyLPc3jY7FXYq7FWD/m352/w15baO1k46tqPKG0ofiRafvJf9iDQe5GZ2g03iz3+kOq7W1vgYqH1y5frfMedO8M3irsKG8VdihvFWxirYxQ3irsUN4q3ihwxVvFXYobxV2Kt4obGKt4obxVwxQ3ireKG8VdihvFXYq3hQ2MCtjChvFDhgVsYqidOsLrUL6CxtE9S5uXEcSDuzGn3ZGcxEEnkGeLFLJMRjzL6f8AK/l618v6HbaXb7+ktZpaUMkrbu5+Z6e22cjqMxyzMi+naLSR0+IYx0+0prlLluxV2KuxV2KuxV2KuxV2KuxV2KuxV2KqVxaWtynC4hSZP5ZFDD7jkJ44yFSFs4ZJRNxJCSXvkTy7dVKwtbOf2oWI/wCFbkv4ZhZOy8Mule5z8Xa2eHXi97CfNnli20MRGO89Z5ieEDJRgo6sSD/DNLrdDHDVSu+jLWdvy8IxAqcutscGYLyTeFWxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwquxS2MKt4VbGKt4VbGFLeKtjCreKrhhSujkkjdJIzxkjYPG3gymqn7xhsjcc23Dk4JCX4rr9j1PT7yO9soLqPZZkDcfA91PuDtnSYsgnESHVz5xo0iMsYuxV2KvlT8ydc1fW/M9xfahbTWkX91Y286MhSBD8OzDq1eR9znUaAYxjAgRLvo3u8F2nlyZMplOJj3AitmLDM11zeKuwobxV2KG8VbGKtjFDeKuxQ3ireKHDFW8VdihvFXYq3ihsYq3ihvFXDFDeKt4obxV2KG8VdireFDYwK2MKG8UOGBWxir2X8k/J/pQv5lvE/eShotOVuydJJf9l9ke1fHNH2rqbPhj4vYeznZ9Dx5ddo/pP6HrGaV6x2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVTuLiG3gknmYJFEpd2PYKKnIykIgk8ggmhbxrXtYm1fVJrySoVjxhT+WMfZH9ffOR1Oc5ZmRdVknxG0vGY7BvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWxhVvFVwwpbwqzDyJqFY59Pc7p++hH+STRx9DUP05tOzcvOHxH6fx5uyxy4sYPWO36v0j4MszaK7FXYqxvVLKH15YJY1khf4hG4DKVbtSlNjUZz+sgcWW47Xu7DCROFHdimp/lt5Nv6s+npbyHo9sTDT/Yr8H/AAuZGDtzVY+U+If0t/2/a4GfsHSZP4OE/wBHb7Bt9jE9T/JCE1bS9SZfCK5QNX/Zpx/4hm5we1h/ykP9L+o/rdLqPZIf5Of+mH6R+pimp/ld5xsAzC0F5GvV7Vg/3IeMh/4HN1p/aDSZNuLhP9Lb7eX2uk1Hs9q8W/DxD+jv9nP7GM3VneWkphu4JLeUdY5UZGH0MAc2+PLGYuJEh5bunyYpQNSBifPZRyxrbxVsYq2MUN4q7FDeKt4ocMVbxV2KG8VdireKGxireKG8VcMUN4q3ihvFXYobxV2Kt4UNjArYwobxQ4YFT/yT5Xn8ya/Bp6VW3H7y7lH7EK/aPzP2R7nMfVagYoGXXo53Z2iOpzCA5dfc+m7a2gtbeK2t0EUEKiOKNdgqqKAD5DOSlIk2eb6ZCAiBEbAKmBk7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqwL8x/MH2dGt28HvCPvRP8AjY/Rmj7W1X+THx/U4WqyfwhgWaNwmxilvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWxhVvFVwwpbwqjNKvzYahBd/sxt+9G+8bbP09jUe+WYsnBIS7vu6uVpJVPh6S2/V9v2PUAQRUbg9DnSOQ7FXYql+swc4FlHWI79fstt296Zgdo4uLHfWLkaadSrvSfNA7Fo4FawJUrm1trmJobmJJ4W+1HIodT8w1RkoZJQNxJB8mGTHGY4ZAEdx3YzqX5ZeTr7k31L6rI3+7LZjHT5JvH/wubbB7QavH/FxD+lv9vP7XUaj2e0mX+HhP9Hb7OX2MV1L8k2+JtM1IH+WK5Sn3yJ/zRm60/taP8pD4xP6D+t0mo9kDzxT+Eh+kfqYrqX5becLCpNibmMf7stiJa/JR8f8AwubvT9v6TL/Hwn+lt9vL7XR6j2f1eL+DiH9Hf7Of2Mdnt7i3kMVxE8Mo6pIpVh9BzbwnGQuJBHk6ieOUDUgQfNZkmDsUN4q3ihwxVvFXYobxV2Kt4obGKt4obxVwxQ3ireKG8VdihvFXYq3hQ2MCtjChvFDYwK+i/wArvJ/+HvL6yXKcdTv6S3VR8SLT4Iv9iDv7k5y/aGp8We30h9D7F7P/AC+G5fXLc/oDMswXcuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kpfr+sQ6Rpc15JQso4wp/NIfsj+vtmPqc4xQMi15J8MbeMXFxNcTyTzMXllYu7HuWNTnISkZEk8y6omzazAhsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCrYwpbxVsYVbxVcMKW8KtjFXoHlDUPrWkJExrLaH0W/wBUfYP/AAO3zGbvQZeLHXWO36naylxAT/nff1/X8U7zNYOxVqRFkRkYVVgVYex2wEWKKsakjeKRo3+0hIO1K07/AE5y2bHwTMe522OXEAVhypm1gS7FWsirWKXYFQ93Y2V5H6V3bx3MX++5UV1+5gRlmLNPGbgTE+Rpry4YZBU4iQ8xbGdS/K/yheglLZrOQ7l7Zyv/AArc0+5c3Gn9o9Xj5y4x/SH6RR+102o9m9Jl5RMD/RP6DY+xi2o/kvcrybTdRST+WK4Qof8Ag05V/wCBzd6f2uif7yBH9U39hr73R6j2PkN8UwfKQr7Rf3MX1H8v/N1hUyae8yD9u3pMD70SrfeM3en7d0mXlMA/0tvv2dFqOwdZi5wJH9H1fdv9iQSRSROY5EKSLsyMCCD7g5tYyEhY3DqZRMTRFFaMkxbxV2KG8VdireKGxireKG8VcMUN4q3ihvFXYobxV2Kt4UNjArYwobxQ9B/KDyd+mda/Sl2ldO01gwB6ST9UX3C/aP0eOaztLU8EOEfVL7nf9gdn+Nl8SX0Q+0/jd77nNveuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV5R558wfpTVDBC1bO0JSOnRn/af+A/tzl+0tV4k6H0xdZqMvEaHIMbzXNDeFWxilvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWxhVvFVwwpbwq2MVTvyjqH1TV1jY0iux6TeHPrGfvqv8Assy9Fl4Mg7pbfq/V8XO0srBh8R+n7N/g9AzetjsVdiqUazBxlScCgk+Fug+IdPnUfqzT9p4uU/g5mlnzCWnNQ5rWBLsVayKtYpdgVrAl2KtYFccCULe6bp18nC9tYrlOwlRXp8uQOW4dTkxG4SMfcaac2nx5RU4iQ8xbGdS/K3ynd1aKGSyc94HNK/6r8x91M3Wn9p9Xj5kTH9Ifqp0mo9mNJk5AwP8ARP6DbF9S/Ju/Sradfxzj+SdTGflyXmD+GbzT+1+M/wB5Ax92/wCr9LotT7HZB/dTEv6233X+hi+o+R/NWngmfTpGQf7shpKtPH92Wp9ObzT9t6TL9OQX57fe6LUdh6vF9WMkeXq+5JGVlYqwKsDQg7EHNoDe4dURWxawobxQ2MVbxQ3irhihvFW8UN4q7FDeKuxVvChsYFbGFCK03TrvUtQt7C0T1Lm5cRxL7sep8AOpOQyTEImR5Bsw4pZJiEeZfUPljy/aaBoltpdtusK/vJO7yNu7n5n8Ns5HPmOSZkX03R6WODEMcen2lNMpcp2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVjPnvzB+jNM+rQNS8vAVQjqqdGb+A/szW9parw4UPqk4+oy8Iocy8pGcw61vFW8KtjFLeFWxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwquxS2MKt4VbGKt4VbGFLeKtjCreKrhhS3hVsYq2CwIKsVYbqw6gjcEfLFsxZDCQkOj0/Sb9b/ToLsbGRfjUdA4+Fx9DA50eDL4kBJ2M4gHbl09x5IvLWDsVUb2D17Z4x9qlU7fENxvlWfFxwMWcJcJBY5Wu+csRWztgbayKXYq1kVaxS7ArWBLsVawK44EtYFccCtYpaOBUHfaRpd+vG9tIbkdjKisR8iRUZfg1eXD/AHcpR9xcfPpMWb+8jGXvFsZ1D8q/K9zU26y2Tncek/Ja/wCrJz/AjN5p/arVw+rhmPMfqp0eo9lNJk+nigfI/rtjOoflBqsdWsLyK4XssoMTfLbmv4jN5p/bDDLbJCUfduP0F0Oo9js0f7ucZe/b9f6GM6h5P8zaeT9Z0+XgOskY9VKePKPkB9Ob7TdsaXN9GSN9x2PyNOh1PYurw/Vjl8Nx9lpQQQaHYjqM2Tq3Yq4YobxVvFDeKuxQ3irsVbwobGBWxhQ9o/JPycYLdvMt4lJZwY9PVhusfR5P9l0HtXxzQ9q6mz4Y6c3sfZzs/hHjS5n6fd3/AB/HN6vmmeqdirsVdirsVdirsVdirsVdirsVdirsVdirsVU7m5htreS4nYJDEpd2PYAVORnMRBJ5BBNCy8W17WJtX1Oa9l2DGkSfyxj7K/1984/U5zlmZF1OSfFK0AMoYN4q3hVsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCrYwpbxVsYVbxVcMKW8KtjFW8KWV+RdQpJPp7nZv38PzFFcf8AET9+bHs7LRMPj+v9H2uwwy4sfnHb4H9t/Yy/Nsl2KuxVINTgMN29PsyfGvXv1FfnnPdoYuDJfSTsdNO413ITMByXYq1kVaxS7ArWBLsVawK44EtYFccCtYpaOBWsCuwJaxVrAlA6hoej6gP9NsoZ27O6AsPk32h9+ZWn1+fD/dzlH47fLk4mo0GDN/eQjL3jf5sZv/yr8uXFTatNZt2CNzT6Q/Jv+Gze6f2t1UPrEZj3Ufs2+x0Oo9kdJP6OKHuNj7bP2sbv/wAptZhqbO6hul8GrE5+g8l/4bN7p/bDTy/vIyh/sh+g/Y6HUexuoj/dyjP/AGJ/SPtY1f8AlfzDYVN1p8yKOrqvNB/s05L+Ob7T9raXN9GSJ8ro/I7ug1HZGqw/XjkPhY+YsJZmwda3irsUN4q7FW8KGxgVkPkbytN5l8wwWAqtsv728lH7MKkcqe7fZHucxtXqBigZdejndm6I6nMIfw8z7n05bwQ28EdvAgjhiUJFGuwVVFAB8hnJkkmy+lRiIgAcgvwMnYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXn/5keYeTLo1u2wo94R49UT/jY/Rmh7W1X+THx/U4Oqy/whgWaNwmxhS3ireFWxilvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWxhVvFVwwpbwq2MVbwpRFhePZXsF2m5gcMQOpXo4+lSRkoTMJCQ6fj7nI0s+GdHlLY/jyNF6f68PofWOY9Dj6nqV+HhSvKvhTOj4hV9HL4DddV+SYuxVA6xb+pbeoB8URr/sTs39fozC1+Ljx31G7fp58Mvekec47N2KtZFWsUuwK1gS7FWsCuOBLWBXHArWKWjgVrArsCWsVawJdgVrAlo4q7AlLr/y/omoVN5YwzOeshUB/+DFG/HM3T9p6jB/dzkB3Xt8uTg6ns3T5/wC8hGR763+fNjeoflXoM9WtJZrRuy19RPub4v8Ahs32m9sNTD+8EZ/Yfs2+x0Gp9jtLP6DKH2j7d/tY3qH5Wa7AC1pLFdqOi19Nz9DfD/w2b7Te2GmntkEofaPs3+x0Gp9jdTDfHKM/9ift2+1jt/5e1uwqbuxmiVRUyFSU/wCDWq/jm/03aWnz/wB3OMj3Xv8ALm8/qey9Tg/vMcgO+tvmNkuzNcBvChtQSaDcnoMCvpD8sPJ48ueXlNwnHU77jNeV6rt8EX+wB39yc5fX6nxZ7fSOT6H2NoPy+Hf65bn9A+H3swzBdu7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmHWotH0qW8ehcfDBGf2pG+yP4n2zH1WoGKBkfh72vLk4Y28XmnluJ5J5mLyysXkc9SzGpOcfKRkbPMupJs2syKGxhS3ireFWxilvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWxhVvFVwwpbwq2MVbwpbUEmgFSegxVn36Lvf8Kfo/mfrPo8abdK19PpSnH4M3XgS8Dg61+B+h2XiS+r+Kvtrn7+vvTvM1DsVaZVZSrCqkUIPQg4qxqeIwzPE25Q0rtuOoO3iN85XUYvDmYu2xT4ogqeUtjWRVrFLsCtYEuxVrArjgS1gVxwK1ilo4FawK7AlrFWsCXYFawJaOKuwJawK7ArWBXYEpXf+WtAv6/WrGF2OxkC8H/4NaN+ObHTdr6rD9GSQ8rsfI2HXansnS5/rxxJ76o/Mbscv/ys0eWps7iW2Y9FakiD6Dxb/hs3+n9s9RH+8jGf+xP6R9joNT7Gaee+OUof7Ifr+1f5L/LmLTfMsN9q9xHNZWv72BVDVaYH4Oa02C/a6nfNpk9rsGXHw1KEj38vs/U4Gk9kcuHMJyMZwjuO+/d+17PDd20/91Kr+wIr92UYtTjyfTIF388co8wq5cwdirsVdirsVdirsVdirsVdirsVdirsVdirsVeSeefMP6V1UxQNWytKpFTozftP9PQe2cr2jqvFnQ+mLrNRl4pbcgxwZr3HbxVsYUt4q3hVsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCrYwpbxVsYVbxVcMKW8KtjFW8KWR+T9I+s3RvZVrDbn4AejSdv+B65n6HBxS4jyDdhhZtm+bhy3Yq7FXYqlOtwUaOcd/gb9Y/jmp7Uw2BMe5zNJPekrzSuc1kVaxS7ArWBLsVawK44EtYFccCtYpaOBWsCuwJaxVrAl2BWsCWjirsCWsCuwK1gV2BLRwK1gVrAl2KomDVL+D+7nan8rfEPuNcy8XaGfH9Mj9/3tU9PCXMI+HzPcLQTRK48Vqp/jmxxdv5B9cQfdt+txZ9nxPI0mEHmLTpNnLRH/KFR94rmzxdt4Jc7j7/ANjjT0OQct0fDc28wrFIsn+qQc2WLPDJ9Mgfc40sco8xSplrB2KuxV2KuxV2KuxV2KuxV2KsW8/eYf0bpn1SBqXl4Cop1SPozfT0H9maztPVeHDhH1S+5xtTl4RQ5l5TnMOtbGKt4q2MKW8Vbwq2MUt4VbGKrhhS4Yq3hVcMUt4Vbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYq3hVsYUt4q2MKt4quGFLeFWxiqvZ2k13dR20IrJIaD28Sflk8cDIgBlEWaem2FlDZWkdtEPgjFK9ye5PzOdFjxiEQA58Y0KV8ml2KuxV2KqV1AJ7d4j1YfCT0BG4O3vleXGJxMT1ZQlRtjRBBoQQR1B2IzlJRINF24Ni2sglrFLsCtYEuxVrArjgS1gVxwK1ilo4FawK7AlrFWsCXYFawJaOKuwJawK7ArWBXYEtHArWBWsCXYq1gS0cCtYEtgkGoNCOhwA0qKg1jUoaBZ2IHZ/iH41zNxdp6jHykfjv97RPS45cwmEHmmYbTwq3+UhK/ga5s8XtDIfXEH3bfrcWfZw/hKYQeYtNl2Z2iPg4/iKjNnh7b08+ZMff+xxp6HIPNMIp4JhWKRZB4qQf1Zs8eaExcSD7nFlAx5il+WMXYq7FXYq7FVK7uoLS2luZ2CQwqXkY9gBXIzmIxMjyCJEAWXimuavPq2pzXstRzNI0/kQfZX/PvnG6nOcszIuoyTMpWgMpYNjFW8VbGFLeKt4VbGKW8KtjFVwwpcMVbwquGKW8Kt4VbGKW8KtjFW8KtjClvFWxhVdilsYVbwq2MVbwq2MKW8VbGFW8VXDClvCrYxVm3k3SPRtzqEq/vZhSEHsnj/sv1Zt9BgocR5ly8MKFslzYt7sVdirsVdirsVSHVbf0rssBRJfjHhX9r8d/pzQdpYeGfF0k7HSzuNdyCzWOS1il2BWsCXYq1gVxwJawK44FaxS0cCtYFdgS1irWBLsCtYEtHFXYEtYFdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpcrMrBlJVh0I2OESINhBFo2DW9Th6TFx4P8AF+J3zPxdrajHylfv3ceekxy6JjB5rcbTwA+LIafga/rzaYfaM/xw+X6v2uLPs0fwlMIPMGly0rIYmPaQU/EVH45tMPbemn/Fwnz/ABTiz0WSPS0wjlilXlG6uvipBH4Zs4ZIzFxII8nGlEjmKXZNi87/ADK8w85F0a3b4Uo92R3bqqfR1P0ZoO1tVZ8MfH9TgavL/CGBjNG4TeFLYxVvFWxhS3ireFWxilvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWxhVvFVwwpbwqmegaU2pX6REH0E+Odv8kdvmemZGmw+JKunVsxw4i9IVVVQqgBVFAB0AGdAA5zeKuxV2KuxV2KuxVBatB6lozj7UXxj5D7X4b5h67Dx4z3jduwT4ZJDnMu0axS7ArWBLsVawK44EtYFccCtYpaOBWsCuwJaxVrAl2BWsCWjirsCWsCuwK1gV2BLRwK1gVrAl2KtYEtHArWBLsCtYEuwK1kUtYq7Iq1gS7ArkkkjbkjFG8VJB/DJRnKJuJooMQeaOg1/VYRQTcx4SDl+PX8c2OHtnU4/4uIee/7XGnosculPOLw3Bu5jcMXnLsZWPUsTufpyzj4/V3vEZoGMzGXMFSGLW3hS2MVbxVsYUt4q3hVsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCrYwpbxVsYVbxVcMKW1BJAAqTsAMIV6T5d0kabp6ow/wBIk+Oc+56L/sc3+lw+HCurnY4cITPMlsdirsVdirsVdirsVdirGbqAwXDxdlPw9fsncbn2zltXh8PIR0dthnxRBUcxm12BWsCXYq1gVxwJawK44FaxS0cCtYFdgS1irWBLsCtYEtHFXYEtYFdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpaxV2RVrAl2BVuBXYEsb8x2nC5W4UfDKKN/rL/AGZn6Wdiu55XtzT8OQTHKX3hJxmU6NvClsYq3irYwpbxVvCrYxS3hVsYquGFLhireFVwxS3hVvCrYxS3hVsYq3hVsYUt4q2MKrsUtjCreFWxireFWxhS3irYwq3iq4YUsl8m6P8AWLo30y1htz+7B/ak/wCbc2GgwcUuI8g34IWbZxm5ct2KuxV2KuxV2KuxV2KuxVK9bt6qlwo3HwP8uoP0H9eartTDcRPucvSTo13pPmidg7ArWBLsVawK44EtYFccCtYpaOBWsCuwJaxVrAl2BWsCWjirsCWsCuwK1gV2BLRwK1gVrAl2KtYEtHArWBLsCtYEuwK1kUtYq7Iq1gS7Aq3ArsCUHqtr9ZsZIwKuByT/AFl/r0yzDPhkC4XaGn8XCY9eY94YeM2rwzeFLYxVvFWxhS3ireFWxilvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVXYpbGFW8KtjFW8KtjClvFWxhVvFURY2c15dx20IrJKaDwA7k/IZZjgZyAHVlGNmnqFjZw2VpHbQiiRig8Se5PzOdHjxiEQA7CMaFK+TS7FXYq7FXYq7FXYq7FXYqp3EKzQPE3RxStK0PY/QchkgJRMT1TE0bYwysrFWFGUkMOtCNiM5KcDEkHo7mMrFtZBLWBLsVawK44EtYFccCtYpaOBWsCuwJaxVrAl2BWsCWjirsCWsCuwK1gV2BLRwK1gVrAl2KtYEtHArWBLsCtYEuwK1kUtYq7Iq1gS7Aq3ArsCWsCsR1e1+rX0igUR/jT5H+3NpgnxReJ7T0/hZiOh3CDy9wGxireKtjClvFW8KtjFLeFWxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwquxS2MKt4VbGKt4VbGFLeKtjCreKs58l6P6Fsb+Zf304pED2j8f9l+rNzoMHCOI8y5mCFC2TZsW92KuxV2KuxV2KuxV2KuxV2KuxVItZg9O6Eg+zMK/7Jdj/DND2phqYkOrsNJOxXcgM1TltYEuxVrArjgS1gVxwK1ilo4FawK7AlrFWsCXYFawJaOKuwJawK7ArWBXYEtHArWBWsCXYq1gS0cCtYEuwK1gS7ArWRS1irsirWBLsCrcCuwJawKlPmG19S1Eyj4oTv8A6rbHMnSzqVd7pu29Px4uMc4/cxvNk8m2MVbxVsYUt4q3hVsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCrYwpbxVsYVTXy7pDanqCxsD9Xj+Odv8n+X/ZZk6XB4k66dWzFDiL0tVCqFUUUCgA6ADOhDnuxV2KuxV2KuxV2KuxV2KuxV2KuxVC6pbmazcAVdPjUCvUdRQddq5i6zD4mMjq24Z8MgWO5yztmsCXYq1gVxwJawK44FaxS0cCtYFdgS1irWBLsCtYEtHFXYEtYFdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpaxV2RVrAl2BVuBXYEtYFWyIskbIwqrAqw9jiDRtjOAlExPIsLuIGgnkhbqhI/tzcQlxAF4HPiOOZgehWDJtTeKtjClvFW8KtjFLeFWxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwquxS2MKt4VbGKt4VbGFLeKrkVmIVRViaADqScIV6Z5d0hdM05I2H+kSfHO3+Ue3+x6Z0OlweHCuvVz8cOEJnmS2OxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ksavrf6vdPGBROqf6p6U+XTOX12Hw8hHQ7u108+KKHzDb3Yq1gVxwJawK44FaxS0cCtYFdgS1irWBLsCtYEtHFXYEtYFdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpaxV2RVrAl2BVuBXYEtYFdgSx/zHa8ZUuVGz/A/zHT8Mz9JPYxeZ7d09SGQddj+PxySYZmvPt4q2MKW8Vbwq2MUt4VbGKrhhS4Yq3hVcMUt4Vbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYq3hVsYUt4qyjyVo3r3J1CZf3MBpCD3k8f9j+vNloMHEeM8g5GCFm2c5uXLdirsVdirsVdirsVdirsVdirsVdirsVdiqWa5b8olnHVDxfp9lun4/rzWdqYeKHEOcXK0s6lXekuc87J2KtYFccCWsCuOBWsUtHArWBXYEtYq1gS7ArWBLRxV2BLWBXYFawK7Alo4FawK1gS7FWsCWjgVrAl2BWsCXYFayKWsVdkVawJdgVbgV2BLWBXYEobULYXNpJF+0RVP9YbjJ4p8MgXF1un8XEY9envYfQgkHYjNy8IQ3ihsYUt4q3hVsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVvCrYwpRNhZTXt5Fawj45TSvYDuT8hlmLGZyER1ZRjZp6nZWcNnaRW0IpHEvEe/iT8zvnSY4CEQB0dhGNClbJpdirsVdirsVdirsVdirsVdirsVdirsVdiq2WNZYnjb7Lgqadd9sjOIkCDyKQaNsWdGjdkf7SEq1OlQaZyOXGYSMT0dzCXEAVuVsmsCuOBLWBXHArWKWjgVrArsCWsVawJdgVrAlo4q7AlrArsCtYFdgS0cCtYFawJdirWBLRwK1gS7ArWBLsCtZFLWKuyKtYEuwKtwK7AlrArsCWsBVi2s2voXzECiS/GvzPX8c2umycUPc8Z2tp/DzGuUt/1oHMh1jYwpbxVvCrYxS3hVsYquGFLhireFVwxS3hVvCrYxS3hVsYq3hVsYUt4q2MKrsUtjCreFWxireFWxhSzzyVo31a1N/MtJrgfugeqx/wDN2brs/Bwx4jzP3OZghQtk2bFvdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqSa3b8LhZgPhlFGP+Uu34j9WaLtXDUhPv2c/Rz2MUtzUOa1gVxwJawK44FaxS0cCtYFdgS1irWBLsCtYEtHFXYEtYFdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpaxV2RVrAl2BVuBXYEtYFdgS1gKpbrtt6tn6gHxwnl/se/wDXMjSZOGVd7qe2dPx4eIc4b/DqxrNq8e2MKW8Vbwq2MUt4VbGKrhhS4Yq3hVcMUt4Vbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYq3hVN/LWjnU9RVGH+jRUec+3Zf8AZZlaTB4k/Ic23FDiL0wAKAAKAbADoBnROe7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqhtRtvrFo6AVcfFH0ryHhXx6Zj6rD4mMxbMU+GQLGs5Mu4dgVxwJawK44FaxS0cCtYFdgS1irWBLsCtYEtHFXYEtYFdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpaxV2RVrAl2BVuBXYEtYFdgS1gKrWUMpVhVSKEexwXSJRBFFh93bm3uZIT+wdj4jqPwzd458UQXgtVgOLIYHoVMZY0N4q3hVsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCrYxVciszBVBZmNFA3JJwgWl6f5e0hdM05ISB67/ABzt/lHt8h0zo9Lg8OFdern44cITPMlsdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirG9St/QvHUfZb41+Tf21zmO0MPBlPcd3aaafFH3IXMFyHHAlrArjgVrFLRwK1gV2BLWKtYEuwK1gS0cVdgS1gV2BWsCuwJaOBWsCtYEuxVrAlo4FawJdgVrAl2BWsilrFXZFWsCXYFW4FdgS1gV2BLWAq1gSkfmG2/u7lR/kP+sZn6LJzi8529p+WQe4/oSYZsHnG8Vbwq2MUt4VbGKrhhS4Yq3hVcMUt4Vbwq2MUt4VbGKt4VbGFLeKtjCq7FLYwq3hVsYqyvyRovr3B1GZf3UBpAD3k8f9j+vNn2fp7PGeQ5OTghe7Oc3TluxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KpdrduXt1lHWI79fstsenvTNd2nh48d9YuTpZ1Ku9Is5t2bjgS1gVxwK1ilo4FawK7AlrFWsCXYFawJaOKuwJawK7ArWBXYEtHArWBWsCXYq1gS0cCtYEuwK1gS7ArWRS1irsirWBLsCrcCuwJawK7AlrAVawJULu3FxbSQn9obHwPUfjksc+GQLRqsAy4zA9WJFSpKkUI2I983oLwJBBouxQ3hVsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWxhVvCqJ02xmvr2K1h+3KaV7AdST8hlmLGZyER1ZRjZp6tZWkNnaxW0IpHEoVfE+JPuc6bHAQiAOjsYxoUrZNLsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirToroyMKqwIYeIOxwEAiioLFZomhleJvtISCelff6euchnxHHMx7nc458UQVhylsawK44FaxS0cCtYFdgS1irWBLsCtYEtHFXYEtYFdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpaxV2RVrAl2BVuBXYEtYFdgS1gKtYEtYFY5rdt6V4ZAPgmHIfPvm20eTihXc8f2zp/DzcQ5T3+PVL8ynUt4VbGKW8KtjFVwwpcMVbwquGKW8Kt4VbGKW8KtjFW8KtjClvFWxhVdilsYVbwqz/AMk6L9VszfTLSe5H7sHqsXUf8F1+7N52fp+GPEeZ+5zcEKFsmzYt7sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqS67b8ZUuANn+B+n2huPfcfqzSdrYeUx7i52jnzilZzSOe1gVxwK1ilo4FawK7AlrFWsCXYFawJaOKuwJawK7ArWBXYEtHArWBWsCXYq1gS0cCtYEuwK1gS7ArWRS1irsirWBLsCrcCuwJawK7AlrAVawJawKgdYtvWs2IHxxfGPkOv4ZkaXJwz97rO1tP4mEkc47/AK2NZuHjG8KtjFLeFWxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwquxS2MKpx5X0Y6nqKq4/0WGjznxHZf8AZZl6PT+JPfkObZihxF6cAAKDYDoM6N2DsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVUL23+sWskX7RFU7fENxlOoxeJAx72eOfDIFjBzkCK2LuQbayKXHArWKWjgVrArsCWsVawJdgVrAlo4q7AlrArsCtYFdgS0cCtYFawJdirWBLRwK1gS7ArWBLsCtZFLWKuyKtYEuwKtwK7AlrArsCWsBVrAlrArRAIoemBaYpe25t7qSL9kGq/wCqdxm8w5OOILwet0/g5ZR6dPco5c4rYxS3hVsYquGFLhireFVwxS3hVvCrYxS3hVsYq3hVsYUt4q2MKrsUro0d3VEBZ2ICqNySdgBkgLV6l5e0hNL01ICB67/HOw7ue3yHTOl0uDw4V16uwxw4RSZ5kNjsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirHdWtzDeMQPgk+NTv1P2hU++c12nh4MljlJ2mlnca7kFmtclxwK1ilo4FawK7AlrFWsCXYFawJaOKuwJawK7ArWBXYEtHArWBWsCXYq1gS0cCtYEuwK1gS7ArWRS1irsirWBLsCrcCuwJawK7AlrAVawJawK7AqT6/bVRLgDdfhf5Hp+OZ+hybmLoO3dPcRkHTY/o/HmkubN5hsYpbwq2MVXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq2MKW8VbGFV2KWW+RdF9ac6nMv7uE8bcHu/dv9j+v5ZteztPZ4z05OTgx2bZ1m6ct2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KoDWrf1LT1APjh+Kv+Sftf1+jMDtHDx4jXOO7kaafDP3sfzl3auOBWsUtHArWBXYEtYq1gS7ArWBLRxV2BLWBXYFawK7Alo4FawK1gS7FWsCWjgVrAl2BWsCXYFayKWsVdkVawJdgVbgV2BLWBXYEtYCrWBLWBXYFUriFZoXibo4phhPhkD3NefCMkDA9QxN0ZHZGFGUkEe4zoImxYeAnAxJieYcMLFvCrYxVcMKXDFW8KrhilvCreFWxilvCrYxVvCrYwpbxVsYVRemafNqF9FaQ/akNC3ZV7sfkMtw4jOQiGcI2aesWdpDaWsVtAOMUShVH8T7nOnxwEYiI5B2MRQpWyaXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq5lDKVYVUihB6EHEhWK3MBguJITvwNAe9OoJp7Zx+qw+HkMXc4p8UQVI5jtjWKWjgVrArsCWsVawJdgVrAlo4q7AlrArsCtYFdgS0cCtYFawJdirWBLRwK1gS7ArWBLsCtZFLWKuyKtYEuwKtwK7AlrArsCWsBVrAlrArsCtZEpSDW7f07kSgfDKN/wDWHXNvoclxrueS7b0/Bl4xyl96XDM10zeFWxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3irYwq9E8k6J9Tsvrsy0uLofCD1WPqP+C6/dm+7P0/BHiPM/c52DHQtkubFvdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqT69b7x3A/4xv+tf45pu18NgTHTYubo57mKUHNA7BrFLRwK1gV2BLWKtYEuwK1gS0cVdgS1gV2BWsCuwJaOBWsCtYEuxVrAlo4FawJdgVrAl2BWsilrFXZFWsCXYFW4FdgS1gV2BLWAq1gS1gV2BWsiUoPVLf17NwBV0+NfmP7Mv0uTgmO4uv7T0/i4SBzG4Y2M3rxLeFWxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhS3iqd+VNFOp6kokFbWCjznsf5U/wBl+rMzRafxJ7/SObbhhxHyengACg6Z0jsHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FVK7gFxbyQn9obE9iNwdvfK82MTgYnqyhLhILFWDAkMCrDYqeoOcbKJiSD0d1E2LayLJo4FawK7AlrFWsCXYFawJaOKuwJawK7ArWBXYEtHArWBWsCXYq1gS0cCtYEuwK1gS7ArWRS1irsirWBLsCrcCuwJawK7AlrAVawJawK7ArWRKWsCsZ1C39C7dAKKfiT5HN9psnHAF4btDT+FmMenMe4ofMhwmxiq4YUuGKt4VXDFLeFW8KtjFLeFWxireFWxhSvjjeSRY41LO5Cqo6knYAYQCTQUPVvL2jppWmx2+xmb452Hdz1+gdBnT6XB4cK69XY44cIpMsyGx2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kse1m39K7LgUSYch0Hxftf1+nOb7Vw8OTi6SdlpJ3Gu5AZq3MaOBWsCuwJaxVrAl2BWsCWjirsCWsCuwK1gV2BLRwK1gVrAl2KtYEtHArWBLsCtYEuwK1kUtYq7Iq1gS7Aq3ArsCWsCuwJawFWsCWsCuwK1kSlrAqWa5b8oVmHWM0b5H+3M/QZKkY97o+3NPxQGQfw/cUkzbvKtjFVwwpcMVbwquGKW8Kt4VbGKW8KtjFW8KtjClmPkPRPVmbVJ1/dxErbg937t/sen+1m17N09njPTk5Onx9WdZu3MdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVQWsW/rWbMB8cXxj5D7X4Zha/B4mI943b9PPhmGOZyjt2jgVrArsCWsVawJdgVrAlo4q7AlrArsCtYFdgS0cCtYFawJdirWBLRwK1gS7ArWBLsCtZFLWKuyKtYEuwKtwK7AlrArsCWsBVrAlrArsCtZEpawKtljWSNo2+ywIP04YSMSCOjDLjE4mJ5EMWkjaORo2+0pIP0Z0cJCQBHV4DLjMJGJ5grRkmtcMKXDFW8KrhilvCreFWxilvCrYxVvCqM0rTptRv4rSH7Uh+JuyqN2Y/IZbhxHJIRDOEeI09btLWG0toraBeMUShVHy/ic6mEBGIA5B2URQpVyaXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqxu60u6hkfhGXiqeDL8Xw9q985fVaDJGZ4Y3Hydrh1ESBZ3QTAgkEUI6g5riK5uSGsCuwJaxVrAl2BWsCWjirsCWsCuwK1gV2BLRwK1gVrAl2KtYEtHArWBLsCtYEuwK1kUtYq7Iq1gS7Aq3ArsCWsCuwJawFWsCWsCuwK1kSlrArsCUk1u34zLMBs4o3zH9mbfs/LcTHueV7c0/DMZB/F94/YlozYOiXDClwxVvCq4Ypbwq3hVsYpbwq2MVbwq9H8kaH9SsPrky0uboAivVY+qj/ZdT9GdB2fp+CPEecvuc/BjoX3slzYt7sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVWSQwyikiK4/wAoA5CeKM/qALKMyORQcuiWL/ZBjP8Akn+BrmBk7Kwy5en3N8dXMeaCm8vTDeKVW9mHE/xzAydjSH0yB97kR1o6hBTabfRfahYjxX4h+Ga/Loc0OcT97kxzwlyKEIIND1zELc7ArWBLRxV2BLWBXYFawK7Alo4FawK1gS7FWsCWjgVrAl2BWsCXYFayKWsVdkVawJdgVbgV2BLWBXYEtYCrWBLWBXYFayJS1gV2BKGv7f17V0AqwHJPmMu02XgmC4XaGn8XCY9eY97GxnQvDLhhVwxVvCq4Ypbwq3hVsYpbwq2MVT3ylon6U1IGRa2lvR5/A/yp/sv1ZnaHT+JPf6RzbsOPiPk9QzpHYOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KqctvBMP3sav7kAnKsmGE/qALOM5R5FBTaDZP9jlEfY1H41zAy9kYZcrj+PNyI6yY57oGby7cLvFIrjwPwn+Oa/L2LMfSQfsciOuieYpAzadew/bhag7gch94rmuy6LNDnEuTDPCXIobMVtawJdgVrArsCWjgVrArWBLsVawJaOBWsCXYFawJdgVrIpaxV2RVrAl2BVuBXYEtYFdgS1gKtYEtYFdgVrIlLWBXYEtHArHtRt/Ru3A+y/wAS/T/bm/0mXjgO8bPE9qafwsxHQ7hDDMp17hireFVwxS3hVvCrYxS3hVfDFJLIkUal5HYKijqSTQDDEEmgmreteX9Hj0rTY7YUMp+Odx3c9foHQZ1OlwDFADr1djjhwikxzIbHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FVKa0tZv72JWPiRv8Af1ynJpsc/qiCzjllHkUDN5fsn3jLRHwBqPx3/HNdl7GxS+m4uTDWzHPdATeXbtf7p1kHh9k/jt+Oa7L2LkH0kS+z8fNyYa6J5ikBNYXkP97CyjxpUfeNs12XSZcf1RIcmGaEuRUMxW1o4FawK1gS7FWsCWjgVrAl2BWsCXYFayKWsVdkVawJdgVbgV2BLWBXYEtYCrWBLWBXYFayJS1gV2BLRwKl+sQc7cSAfFGd/keuZ3Z+Xhnw97pu29Px4uMc4/ckozdvJOGKt4VXDFLeFW8KtjFLeFWZ+QND9SRtVnX4IyUtge7dGb6Ogzb9maaz4h+DlafH/EzvN25jsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVUJrGzm/vYVYnvSh+8b5jZdJiyfVEFshmnHkUBN5ctH3idoz4faH47/jmuy9h4j9JMft/HzcmGukOYtATeXb1N4yso7AHifx2/HNbl7EzR+mpfZ+Pm5UNdA89kvns7uCvqxMg8SNvv6ZrculyY/qiQ5MMsZcio5jtjWBLRwK1gS7ArWBLsCtZFLWKuyKtYEuwKtwK7AlrArsCWsBVrAlrArsCtZEpawK7Alo4FWuqupVhVWFCPY4iRBsMZwEgQeRY1NE0UrxnqppnS45icRIdXgc+E45mB6FYMsaW8KrhilvCreFWxilG6Rpk+pahFZw9ZD8bdlUfaY/IZdgwnJMRDOEeI09dtLWG1to7aBeMUShUHsM6uEBEADkHZAUKVckl2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KoafTbGf+8hUk/tAcT94pmJl0OHJ9UR933N0M848igJ/LNs28MjRnwPxD+BzW5ewcZ+mRj9rkw18hzFpfP5c1CPePjKP8k0P3GmazN2Jnj9NS/Hm5UNdA89kumtbmA/vYmT3YED781mXT5Mf1RIcqGSMuRtSyhm1gS7ArWRS1irsirWBLsCrcCuwJawK7AlrAVawJawK7ArWRKWsCuwJaOBWsBVKdZgo6zAbN8LfMdM23ZuWwYvNdu6epDIOux/QlgzaPPN4VXDFLeFW8KtjFL0ryRof1DT/rcy0ursBqHqsfVV+nqc6Ls7TcEOI/VL7nPwY6F97Jc2Le7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXEAih6YqhJ9J06b7cCgnuvwn/haZhZezsGTnEfDb7m+GpyR5FL5/K0DVMEzJ7MAw/CmavL7PwP0SI9+/6nKh2jLqEun8u6lHUoqyj/ACDv9xpmrzdiaiHICXu/a5UNdjPPZL5re4hNJo2jP+UCP15q8uCeM1IEe9yozjLkbUsqZuyKtYEuwKtwK7AlrArsCWsBVrAlrArsCtZEpawK7Alo4FawFVG7hE0Dx9yPh+Y6Zbp8vBMScbWafxcRj16e9jtCDQ9c6V4Ih2FVwxS3hVvCqfeT9D/SepBpVraW1Hmr0Y/sp9P6szdBpvEnv9Ib8OPiPk9SzpnYOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuKhgQwBB6g4CARRUFBT6Lps32oFU+KfD+rMDN2Vp8nOIHu2+5yIarJHql0/lWI1ME5XwVwD+IpmqzezsT9EiPe5UO0T/EEun8u6nFUhBKPFDX8DQ5qs3YmohyHF7nLhrsZ60l8sM0TcZUZG8GBH681eTFOBqQIPm5UZiXI2pZUydgS1gV2BLWAq1gS1gV2BWsiUtYFdgS0cCtYCrWRSkepweldEgfDJ8Q+ffOg0OXjxjvGzxna+n8PMSOUt/wBaEzNdWuGKW8Kr4YpJpUiiUvJIwVFHUkmgGSjEk0EgW9d0DSI9K0yK1Whk+1O4/ac9T/AZ1WmwDFAR+bs8cOEUmOZDN2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVp0R14uoZT1BFRkZREhRFhIJHJA3GhaXNuYQjeMfw/gNvwzXZux9Nk/ho+W37HJhrMket+9LbjykOtvPTwWQV/Ef0zU5vZof5Ofz/AFj9TlQ7S/nD5JbceXtUh39L1FHeM8vw6/hmpz9i6nH/AA8Q8t/2/Y5kNbjl1r3pfJFJG3GRCjeDAg/jmryY5RNSBB83JjIHksyssmsCWsCuwK1kSlrArsCWjgVrAVayKUHqkHqWxYfaj+L6O+Z3Z+XhyV0k6ntnT+Jh4hzhv8OqSZv3jlwxS3hVm35faFzdtWnX4UqlqD3boz/R0H05uey9Nf7w/By9Nj/iLO83bmOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVbJFHIvGRA6/wArAEfjkJ44zFSAI80xkRyS+48u6VNU+l6bHvGafhuPwzWZuxNNk/h4fdt+z7HKhrsset+9Lbjyg25t7gHwWQU/4Yf0zT5/Zg/5Ofz/AFj9Tlw7T/nD5JXcaBqsNawF1H7UfxfgN/wzUZ+xdTj/AIbHlv8AtcyGtxS6170A6OjFXUqw6gihzVzgYmiKLlAg8luQKWsCuwJaOBWsBVrIpaIBBB3B6jG6NoIBFFj1xCYZnjP7J2+XbOowZOOAl3vA6rAcWSUO4rBlrQjtF0ubVNRis4tuZrI/8qD7TZfp8JyTEQzxw4jT1+1tobW3jt4V4xRKERfYZ1kICIAHIOzAoUq5JLsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVWSwQTLxljWRfBgD+vK8mGGQVICXvFsozMeRpLbjyzpU1SsZhY94zT8DUZqc/YGmycgYnyP67Dlw1+WPW/ellx5PlFTb3Ct4K4K/iK5p8/svIf3cwfft+ty4dqD+IJZcaFqsFS1uzL/Mnx/wDEanNNn7H1WPnAn3b/AHOZDWYpcigGUqSGFCOoOawgjYuUCtyJVrIpdgKpZrEH2Jh/qt/DNt2Xm5wPvec7e0/LIPcf0JaM3Dzj07yPoX6P0761MtLu7AY16rH1Vfp6nOk7O03hw4j9Uvuc/T4+EX1LJM2LkOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KqU9rbTik0SSD/AClB/XlObT48gqcRL3hnDJKPI0ltx5W0qWpRWhb/ACDt9zVzT5/ZzTT5Aw9x/Xblw7Ryx57pXc+Trlam3nWQeDgqfw5Zps/srkH93MS9+363Nx9qRP1CkqudE1S3qZLdyo/aT4x/wtc0mo7I1OL6oGvLf7nMx6vHLlJLrmESxPE21RT5HMLDkOOYl3J1OEZcZh3j+xryboB1LVOc6/6LaENMD0Zq/Cn9fbO47O0/iyv+EPD4sJMqPR6lnTuwdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqDvl0hvhvfQqenqlQfoJ3zX6yOlO2bg/zq/S34TlH0cXwdpUOlRQOummMw+oxkMTBx6hpWpqd+mXaSGKMKxVweRv7WmRuRJ53v70ZmUh2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kv/9k=' -assert a[29].load == base64_bytes(wireshark_data) - -# This a valid JPEG image: try it out -# open("image.jpg", "wb").write(a[29].load) - -= TCPSession - dissect HTTP 1.0 html page with Content_Length -~ http - -load_layer("http") - -import os - -# Packet from -# https://community.cisco.com/t5/networking-documents/http-packet-captures/ta-p/3121453 -tmp = "/test/pcaps/http_content_length.pcap" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename - -expected_data = b"""Google

A faster way to browse the web



 
  Advanced Search
  Language Tools


Advertising Programs - Business Solutions - About Google

©2010 - Privacy

""" - - -conf.contribs["http"]["auto_compression"] = False -a = sniff(offline=filename, session=TCPSession) -pkt = a[7] -assert HTTP in pkt -assert HTTPResponse in pkt -assert pkt[HTTP].Content_Length == b'5012' -assert len(pkt[Raw].load) == 5012 - -conf.contribs["http"]["auto_compression"] = True -a = sniff(offline=filename, session=TCPSession) -pkt = a[7] -assert HTTP in pkt -assert HTTPResponse in pkt -print(pkt[Raw].load, expected_data) -assert pkt[Raw].load == expected_data - -############ -############ -+ HTTP 1.0 -~ http - -= HTTP decompression (gzip) - -conf.debug_dissector = True -load_layer("http") - -import os -import gzip - -tmp = "/test/pcaps/http_compressed.pcap" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename - -# First without auto decompression - -conf.contribs["http"]["auto_compression"] = False -pkts = sniff(offline=filename, session=TCPSession) - -data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xffEQ]o\xdb0\x0c\xfc+\x9a\x1f\x92\xa7\x9a\x96?\xe4\xb8\x892`I\x81\r\xe8\xda\xa2p1\xec\xa9P-\xd5\x16*[\x86\xa5\xd8K\x7f\xfd\xa8\x14E\x1f\x8e:R\x07\xf2D\xed\xbe\x1d\xef\x0f\xf5\xdf\x87\x1b\xd2\xf9\xde\x90\x87\xa7\x1f\xb7\xbf\x0e$\xba\x02\xf8\x93\x1d\x00\x8e\xf5\x91\xfc\xac\x7f\xdf\x92\xe5?9\x89QV\x01\x02\x00\x00' - -pkts[2].show() -assert HTTPResponse in pkts[2] -assert pkts[2].Expires == b'Mon, 22 Apr 2019 15:23:19 GMT' -assert pkts[2].Content_Type == b'text/html; charset=UTF-8' -assert pkts[2].load == data - -# Now with auto decompression - -conf.contribs["http"]["auto_compression"] = True -pkts = sniff(offline=filename, session=TCPSession) - -pkts[2].show() -assert HTTPResponse in pkts[2] -assert pkts[2].load == b'' - -= HTTP decompression (brotli) - -conf.debug_dissector = True -load_layer("http") - -import os -import brotli - -tmp = "/test/pcaps/http_compressed-brotli.pcap" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename - -# First without auto decompression - -conf.contribs["http"]["auto_compression"] = False -pkts = sniff(offline=filename, session=TCPSession) - -data = b'\x1f\x41\x00\xe0\xc5\x6d\xec\x77\x56\xf7\xb5\x8b\x1c\x52\x10\x48\xe0\x90\x03\xf6\x6f\x97\x30\xd0\x40\x24\xb8\x01\x9b\xdb\xa0\xf4\x5c\x92\x4c\xc4\x6f\x89\x58\xf7\x4b\xf7\x4b\x6f\x8c\x2e\x2c\x28\x64\x06\x1d\x03' - -pkts[0].show() -assert HTTPResponse in pkts[0] -assert pkts[0].Content_Encoding == b'br' -assert pkts[0].Content_Type == b'text/plain' -assert pkts[0].load == data - -# Now with auto decompression - -conf.contribs["http"]["auto_compression"] = True -pkts = sniff(offline=filename, session=TCPSession) - -pkts[0].show() -assert HTTPResponse in pkts[0] -assert pkts[0].load == b'This is a test file for testing brotli decompression in Wireshark\n' - -= HTTP build - -pkt = TCP()/HTTP()/HTTPRequest(Method=b'GET', Path=b'/download', Http_Version=b'HTTP/1.1', Accept=b'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', Accept_Encoding=b'gzip, deflate', Accept_Language=b'en-US,en;q=0.5', Cache_Control=b'max-age=0', Connection=b'keep-alive', Host=b'scapy.net', User_Agent=b'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0') -raw_pkt = raw(pkt) -raw_pkt -assert raw_pkt == b'\x00P\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x00\x00\x00\x00GET /download HTTP/1.1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: en-US,en;q=0.5\r\nCache-Control: max-age=0\r\nConnection: keep-alive\r\nHost: scapy.net\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0\r\n\r\n' - -= HTTP 1.1 -> HTTP 2.0 Upgrade (h2c) -~ Test h2c - -conf.debug_dissector = True -load_layer("http") -from scapy.contrib.http2 import H2Frame - -import os - -tmp = "/test/pcaps/http2_h2c.pcap" -filename = os.path.abspath(os.path.join(os.path.dirname(__file__),"../")) + tmp -filename = os.getenv("SCAPY_ROOT_DIR")+tmp if not os.path.exists(filename) else filename - -pkts = sniff(offline=filename, session=TCPSession) - -assert HTTPResponse in pkts[1] -assert pkts[1].Connection == b"Upgrade" -assert H2Frame in pkts[1] -assert pkts[1][H2Frame].settings[0].id == 3 - -for i in range(3, 10): - assert HTTP not in pkts[i] - assert H2Frame in pkts[i] - -############ -############ -+ LLMNR protocol - -= Simple packet dissection -pkt = Ether(b'\x11\x11\x11\x11\x11\x11\x99\x99\x99\x99\x99\x99\x08\x00E\x00\x00(\x00\x01\x00\x00@\x11:\xa4\xc0\xa8\x00w\x7f\x00\x00\x01\x14\xeb\x14\xeb\x00\x14\x95\xcf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -assert pkt.sport == 5355 -assert pkt.dport == 5355 -assert pkt[LLMNRQuery].opcode == 0 - -= Packet build / dissection -pkt = UDP(raw(UDP()/LLMNRResponse())) -assert LLMNRResponse in pkt -assert pkt.qr == 1 -assert pkt.c == 0 -assert pkt.tc == 0 -assert pkt.z == 0 -assert pkt.rcode == 0 -assert pkt.qdcount == 0 -assert pkt.arcount == 0 -assert pkt.nscount == 0 -assert pkt.ancount == 0 - -= Answers - building -a = UDP()/LLMNRResponse(id=12) -b = UDP()/LLMNRQuery(id=12) -assert a.answers(b) -assert not b.answers(a) -assert b.hashret() == b'\x00\x0c' - -= Answers - dissecting -a = Ether(b'\xd0P\x99V\xdd\xf9\x14\x0cv\x8f\xfe(\x08\x00E\x00\x00(\x00\x01\x00\x00@\x11:\xa4\x7f\x00\x00\x01\xc0\xa8\x00w\x14\xeb\x14\xeb\x00\x14\x95\xcf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -b = Ether(b'\x14\x0cv\x8f\xfe(\xd0P\x99V\xdd\xf9\x08\x00E\x00\x00(\x00\x01\x00\x00@\x11:\xa4\xc0\xa8\x00w\x7f\x00\x00\x01\x14\xeb\x14\xeb\x00\x14\x15\xcf\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00') -assert b.answers(a) -assert not a.answers(b) - -############ -############ -+ LLTD protocol - -= Simple packet dissection -pkt = Ether(b'\xff\xff\xff\xff\xff\xff\x86\x14\xf0\xc7[.\x88\xd9\x01\x00\x00\x01\xff\xff\xff\xff\xff\xff\x86\x14\xf0\xc7[.\x00\x00\xfe\xe9[\xa9\xaf\xc1\x0bS[\xa9\xaf\xc1\x0bS\x01\x06}[G\x8f\xec.\x02\x04p\x00\x00\x00\x03\x04\x00\x00\x00\x06\x07\x04\xac\x19\x88\xe4\t\x02\x00l\n\x08\x00\x00\x00\x00\x00\x0fB@\x0c\x04\x00\x08=`\x0e\x00\x0f\x0eT\x00E\x00S\x00T\x00-\x00A\x00P\x00\x12\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x04\x00\x00\x00\x00\x15\x01\x02\x18\x00\x19\x02\x04\x00\x1a\x00\x00') -assert pkt.dst == pkt.real_dst -assert pkt.src == pkt.real_src -assert pkt.current_mapper_address == pkt.apparent_mapper_address -assert pkt.mac == '7d:5b:47:8f:ec:2e' -assert pkt.hostname == "TEST-AP" -assert isinstance(pkt[LLTDAttributeEOP].payload, NoPayload) - -= Packet build / dissection -pkt = Ether(raw(Ether(dst=ETHER_BROADCAST, src=RandMAC()) / LLTD(tos=0, function=0))) -assert LLTD in pkt -assert pkt.dst == pkt.real_dst -assert pkt.src == pkt.real_src -assert pkt.tos == 0 -assert pkt.function == 0 - -= Attribute build / dissection -assert isinstance(LLTDAttribute(), LLTDAttribute) -assert isinstance(LLTDAttribute(raw(LLTDAttribute())), LLTDAttribute) -assert all(isinstance(LLTDAttribute(type=i), LLTDAttribute) for i in six.moves.range(256)) -assert all(isinstance(LLTDAttribute(raw(LLTDAttribute(type=i))), LLTDAttribute) for i in six.moves.range(256)) - -= Large TLV -m1, m2, seq = RandMAC()._fix(), RandMAC()._fix(), 123 -preqbase = Ether(src=m1, dst=m2) / LLTD() / \ - LLTDQueryLargeTlv(type="Detailed Icon Image") -prespbase = Ether(src=m2, dst=m1) / LLTD() / \ - LLTDQueryLargeTlvResp() -plist = [] -pkt = preqbase.copy() -pkt.seq = seq -plist.append(Ether(raw(pkt))) -pkt = prespbase.copy() -pkt.seq = seq -pkt.flags = "M" -pkt.value = "abcd" -plist.append(Ether(raw(pkt))) -pkt = preqbase.copy() -pkt.seq = seq + 1 -pkt.offset = 4 -plist.append(Ether(raw(pkt))) -pkt = prespbase.copy() -pkt.seq = seq + 1 -pkt.value = "efg" -plist.append(Ether(raw(pkt))) -builder = LargeTlvBuilder() -builder.parse(plist) -data = builder.get_data() -assert len(data) == 1 -key, value = data.popitem() -assert key.endswith(' [Detailed Icon Image]') -assert value == 'abcdefg' - +assert b'Dst: 192.0.2.1' in r +assert b'Echo (ping) request' in r +assert b'Echo (ping) reply' in r +assert b'ICMP' in r +assert len(conf.temp_files) == tempfile_count -############ -############ -+ Test fragment() / defragment() functions - -= fragment() -payloadlen, fragsize = 100, 8 -assert fragsize % 8 == 0 -fragcount = (payloadlen // fragsize) + bool(payloadlen % fragsize) -* create the packet -pkt = IP() / ("X" * payloadlen) -* create the fragments -frags = fragment(pkt, fragsize) -* count the fragments -assert len(frags) == fragcount -* each fragment except the last one should have MF set -assert all(p.flags == 1 for p in frags[:-1]) -assert frags[-1].flags == 0 -* each fragment except the last one should have a payload of fragsize bytes -assert all(len(p.payload) == 8 for p in frags[:-1]) -assert len(frags[-1].payload) == ((payloadlen % fragsize) or fragsize) - -= fragment() and overloaded_fields -pkt1 = Ether() / IP() / UDP() -pkt2 = fragment(pkt1)[0] -pkt3 = pkt2.__class__(raw(pkt2)) -assert pkt1[IP].proto == pkt2[IP].proto == pkt3[IP].proto - -= fragment() already fragmented packets -payloadlen = 1480 * 3 -ffrags = fragment(IP() / ("X" * payloadlen), 1480) -ffrags = fragment(ffrags, 1400) -len(ffrags) == 6 -* each fragment except the last one should have MF set -assert all(p.flags == 1 for p in ffrags[:-1]) -assert ffrags[-1].flags == 0 -* fragment offset should be well computed -plen = 0 -for p in ffrags: - assert p.frag == plen // 8 - plen += len(p.payload) - -assert plen == payloadlen - -= defrag() -nonfrag, unfrag, badfrag = defrag(frags) -assert not nonfrag -assert not badfrag -assert len(unfrag) == 1 - -= defragment() -defrags = defragment(frags) -* we should have one single packet -assert len(defrags) == 1 -* which should be the same as pkt reconstructed -assert defrags[0] == IP(raw(pkt)) - -= defragment() uses timestamp of last fragment -payloadlen, fragsize = 100, 8 -assert fragsize % 8 == 0 -packet = Ether()/IP()/("X" * payloadlen) -frags = fragment(packet, fragsize) -for i,fragment in enumerate(frags): - fragment.time -= 100 + i - -last_time = max(frag.time for frag in frags) -defrags = defragment(frags) -assert defrags[0].time == last_time -nonfrag, defrags, badfrag = defrag(frags) -assert defrags[0].time == last_time - - -= defrag() / defragment() - Real DNS packets -import base64 += Run scapy's tshark command +~ needs_root +tshark(count=1, timeout=3) -a = base64.b64decode('bnmYJ63mREVTUwEACABFAAV0U8UgADIR+u0EAgIECv0DxAA1sRIL83Z7gbCBgAABAB0AAAANA255YwNnb3YAAP8AAcAMAAYAAQAAA4QAKgZ2d2FsbDDADApob3N0bWFzdGVywAx4Og5wAAA4QAAADhAAJOoAAAACWMAMAC4AAQAAA4QAmwAGCAIAAAOEWWm9jVlgdP0mfQNueWMDZ292AHjCDBL0C1rEKUjsuG6Zg3+Rs6gj6llTABm9UZnWk+rRu6nPqW4N7AEllTYqNK+r6uFJ2KhfG3MDPS1F/M5QCVR8qkcbgrqPVRBJAG67/ZqpGORppQV6ib5qqo4ST5KyrgKpa8R1fWH8Fyp881NWLOZekM3TQyczcLFrvw9FFjdRwAwAAQABAAADhAAEobkenMAMAC4AAQAAA4QAmwABCAIAAAOEWWm9jVlgdP0mfQNueWMDZ292ABW8t5tEv9zTLdB6UsoTtZIF6Kx/c4ukIud8UIGM0XdXnJYx0ZDyPDyLVy2rfwmXdEph3KBWAi5dpRT16nthlMmWPQxD1ecg9rc8jcaTGo8z833fYJjzPT8MpMTxhapu4ANSBVbv3LRBnce2abu9QaoCdlHPFHdNphp6JznCLt4jwAwAMAABAAADhAEIAQEDCAMBAAF77useCfI+6T+m6Tsf2ami8/q5XDtgS0Ae7F0jUZ0cpyYxy/28DLFjJaS57YiwAYaabkkugxsoSv9roqBNZjD+gjoUB+MK8fmfaqqkSOgQuIQLZJeOORWD0gAj8mekw+S84DECylbKyYEGf8CB3/59IfV+YkTcHhXBYrMNxhMK1Eiypz4cgYxXiYUSz7jbOmqE3hU2GinhRmNW4Trt4ImUruSO+iQbTTj6LtCtIsScOF4vn4gcLJURLHOs+mf1NU9Yqq9mPC9wlYZk+8rwqcjVIiRpDmmv83huv4be1x1kkz2YqTFwtc33Fzt6SZk96Qtk2wCgg8ZQqLKGx5uwIIyrwAwAMAABAAADhAEIAQEDCAMBAAGYc7SWbSinSc3u8ZcYlO0+yZcJD1vqC5JARxZjKNzszHxc9dpabBtR9covySVu1YaBVrlxNBzfyFd4PKyjvPcBER5sQImoCikC+flD5NwXJbnrO1SG0Kzp8XXDCZpBASxuBF0vjUSU9yMqp0FywCrIfrbfCcOGAFIVP0M2u8dVuoI4nWbkRFc0hiRefoxc1O2IdpR22GAp2OYeeN2/tnFBz/ZMQitU2IZIKBMybKmWLC96tPcqVdWJX6+M1an1ox0+NqBZuPjsCx0/lZbuB/rLHppJOmkRc7q2Fw/tpHOyWHV+ulCfXem9Up/sbrMcP7uumFz0FeNhBPtg3u5kA5OVwAwAMAABAAADhACIAQADCAMBAAF5mlzmmq8cs6Hff0qZLlGKYCGPlG23HZw2qAd7N2FmrLRqPQ0R/hbnw54MYiIs18zyfm2J+ZmzUvGd+gjHGx3ooRRffQQ4RFLq6g6oxaLTbtvqPFbWt4Kr2GwX3UslgZCzH5mXLNpPI2QoetIcQCNRdcxn5QpWxPppCVXbKdNvvcAMADAAAQAAA4QAiAEAAwgDAQABqeGHtNFc0Yh6Pp/aM+ntlDW1fLwuAWToGQhmnQFBTiIUZlH7QMjwh5oMExNp5/ABUb3qBsyk9CLanRfateRgFJCYCNYofrI4S2yqT5X9vvtCXeIoG/QqMSl3PJk4ClYufIKjMPgl5IyN6yBIMNmmsATlMMu5TxM68a/CLCh92L3ADAAuAAEAAAOEAJsAMAgCAAADhFlpvY1ZYHT9Jn0DbnljA2dvdgAViVpFoYwy9dMUbOPDHTKt/LOtoicvtQbHeXiUSQeBkGWTLyiPc/NTW9ZC4WK5AuSj/0+V') -b = base64.b64decode('bnmYJ63mREVTUwEACABFAAV0U8UgrDIR+kEEAgIECv0DxApz1F5olFRytjhNlG/JbdW0NSAFeUUF4rBRqsly/h6nFWKoQfih35Lm+BFLE0FoMaikWCjGJQIuf0CXiElMSQifiDM+KTeecNkCgTXADAAuAAEAAAOEARsAMAgCAAADhFlpvY1ZYHT9VwUDbnljA2dvdgAdRZxvC6VlbYUVarYjan0/PlP70gSz1SiYCDZyw5dsGo9vrZd+lMcAm5GFjtKYDXeCb5gVuegzHSNzxDQOa5lVKLQZfXgVHsl3jguCpYwKAygRR3mLBGtnhPrbYcPGMOzIxO6/UE5Hltx9SDqKNe2+rtVeZs5FyHQE5pTVGVjNED9iaauEW9UF3bwEP3K+wLgxWeVycjNry/l4vt9Z0fyTU15kogCZG8MXyStJlzIgdzVZRB96gTJbGBDRFQJfbE2Af+INl0HRY4p+bqQYwFomWg6Tzs30LcqAnkptknb5peUNmQTBI/MU00A6NeVJxkKK3+lf2EuuiJl+nFpfWiKpwAwAMwABAAADhAAJAQAADASqu8zdwAwALgABAAADhACbADMIAgAAA4RZab2NWWB0/SZ9A255YwNnb3YAVhcqgSl33lqjLLFR8pQ2cNhdX7dKZ2gRy0vUHOa+980Nljcj4I36rfjEVJCLKodpbseQl0OeTsbfNfqOmi1VrsypDl+YffyPMtHferm02xBK0agcTMdP/glpuKzdKHTiHTlnSOuBpPnEpgxYPNeBGx8yzMvIaU5rOCxuO49Sh/PADAACAAEAAAOEAAoHdndhbGw0YcAMwAwAAgABAAADhAAKB3Z3YWxsMmHADMAMAAIAAQAAA4QACgd2d2FsbDNhwAzADAACAAEAAAOEAAoHdndhbGwxYcAMwAwALgABAAADhACbAAIIAgAAA4RZab2NWWB0/SZ9A255YwNnb3YANn7LVY7YsKLtpH7LKhUz0SVsM/Gk3T/V8I9wIEZ4vEklM9hI92D2aYe+9EKxOts+/py6itZfANXU197pCufktASDxlH5eWSc9S2uqrRnUNnMUe4p3Jy9ZCGhiHDemgFphKGWYTNZUJoML2+SDzbv9tXo4sSbZiKJCDkNdzSv2lfADAAQAAEAAAOEAEVEZ29vZ2xlLXNpdGUtdmVyaWZpY2F0aW9uPWMycnhTa2VPZUxpSG5iY24tSXhZZm5mQjJQcTQzU3lpeEVka2k2ODZlNDTADAAQAAEAAAOEADc2dj1zcGYxIGlwNDoxNjEuMTg1LjIuMC8yNSBpcDQ6MTY3LjE1My4xMzIuMC8yNSBteCAtYWxswAwALgABAAADhACbABAIAgAAA4RZab2NWWB0/SZ9A255YwNnb3YAjzLOj5HUtVGhi/emNG90g2zK80hrI6gh2d+twgVLYgWebPeTI2D2ylobevXeq5rK5RQgbg2iG1UiTBnlKPgLPYt8ZL+bi+/v5NTaqHfyHFYdKzZeL0dhrmebRbYzG7tzOllcAOOqieeO29Yr4gz1rpiU6g75vkz6yQoHNfmNVMXADAAPAAEAAAOEAAsAZAZ2d2FsbDLADMAMAA8AAQAAA4QACwBkBnZ3YWxsNMAMwAwADwABAAADhAALAAoGdndhbGwzwAzADAAPAAEAAAOEAAsACgZ2d2FsbDXADMAMAA8AAQAAA4QACwAKBnZ3YWxsNsAMwAwADwABAAADhAALAAoGdndhbGw3wAzADAAPAAEAAAOEAAsACgZ2d2FsbDjADMAMAA8AAQAAA4QACwBkBnZ3YWxsMcAMwAwALgABAAADhACbAA8IAgAAA4RZab2NWWB0/SZ9A255YwNnb3YAooXBSj6PfsdBd8sEN/2AA4cvOl2bcioO') -c = base64.b64decode('bnmYJ63mREVTUwEACABFAAFHU8UBWDIRHcMEAgIECv0DxDtlufeCT1zQktat4aEVA8MF0FO1sNbpEQtqfu5Al//OJISaRvtaArR/tLUj2CoZjS7uEnl7QpP/Ui/gR0YtyLurk9yTw7Vei0lSz4cnaOJqDiTGAKYwzVxjnoR1F3n8lplgQaOalVsHx9UAAQABAAADLAAEobkBA8epAAEAAQAAAywABKG5AQzHvwABAAEAAAMsAASnmYIMx5MAAQABAAADLAAEp5mCDcn9AAEAAQAAAqUABKeZhAvKFAABAAEAAAOEAAShuQIfyisAAQABAAADhAAEobkCKcpCAAEAAQAAA4QABKG5AjPKWQABAAEAAAOEAAShuQI9ynAAAQABAAADhAAEobkCC8nPAAEAAQAAA4QABKG5AgzJ5gABAAEAAAOEAASnmYQMAAApIAAAAAAAAAA=') -d = base64.b64decode('////////REVTUwEACABFAABOawsAAIARtGoK/QExCv0D/wCJAIkAOry/3wsBEAABAAAAAAAAIEVKRkRFQkZFRUJGQUNBQ0FDQUNBQ0FDQUNBQ0FDQUFBAAAgAAEAABYP/WUAAB6N4XIAAB6E4XsAAACR/24AADyEw3sAABfu6BEAAAkx9s4AABXB6j4AAANe/KEAAAAT/+wAAB7z4QwAAEuXtGgAAB304gsAABTB6z4AAAdv+JAAACCu31EAADm+xkEAABR064sAABl85oMAACTw2w8AADrKxTUAABVk6psAABnF5joAABpA5b8AABjP5zAAAAqV9WoAAAUW+ukAACGS3m0AAAEP/vAAABoa5eUAABYP6fAAABX/6gAAABUq6tUAADXIyjcAABpy5Y0AABzb4yQAABqi5V0AAFXaqiUAAEmRtm4AACrL1TQAAESzu0wAAAzs8xMAAI7LcTQAABxN47IAAAbo+RcAABLr7RQAAB3Q4i8AAAck+NsAABbi6R0AAEdruJQAAJl+ZoEAABDH7zgAACOA3H8AAAB5/4YAABQk69sAAEo6tcUAABJU7asAADO/zEAAABGA7n8AAQ9L8LMAAD1DwrwAAB8F4PoAABbG6TkAACmC1n0AAlHErjkAABG97kIAAELBvT4AAEo0tcsAABtC5L0AAA9u8JEAACBU36sAAAAl/9oAABBO77EAAA9M8LMAAA8r8NQAAAp39YgAABB874MAAEDxvw4AAEgyt80AAGwsk9MAAB1O4rEAAAxL87QAADtmxJkAAATo+xcAAAM8/MMAABl55oYAACKh3V4AACGj3lwAAE5ssZMAAC1x0o4AAAO+/EEAABNy7I0AACYp2dYAACb+2QEAABB974IAABc36MgAAA1c8qMAAAf++AEAABDo7xcAACLq3RUAAA8L8PQAAAAV/+oAACNU3KsAABBv75AAABFI7rcAABuH5HgAABAe7+EAAB++4EEAACBl35oAAB7c4SMAADgJx/YAADeVyGoAACKN3XIAAA/C8D0AAASq+1UAAOHPHjAAABRI67cAAABw/48=') - -old_debug_dissector = conf.debug_dissector -conf.debug_dissector = 0 -plist = PacketList([Ether(x) for x in [a, b, c, d]]) -conf.debug_dissector = old_debug_dissector - -left, defragmented, errored = defrag(plist) -assert len(left) == 1 -assert left[0] == Ether(d) -assert len(defragmented) == 1 -assert len(defragmented[0]) == 3093 -assert defragmented[0][DNSRR].rrname == b'nyc.gov.' -assert len(errored) == 0 - -plist_def = defragment(plist) -assert len(plist_def) == 2 -assert len(plist_def[0]) == 3093 -assert plist_def[0][DNSRR].rrname == b'nyc.gov.' - -= Packet().fragment() -payloadlen, fragsize = 100, 8 -assert fragsize % 8 == 0 -fragcount = (payloadlen // fragsize) + bool(payloadlen % fragsize) -* create the packet -pkt = IP() / ("X" * payloadlen) -* create the fragments -frags = pkt.fragment(fragsize) -* count the fragments -assert len(frags) == fragcount -* each fragment except the last one should have MF set -assert all(p.flags == 1 for p in frags[:-1]) -assert frags[-1].flags == 0 -* each fragment except the last one should have a payload of fragsize bytes -assert all(len(p.payload) == 8 for p in frags[:-1]) -assert len(frags[-1].payload) == ((payloadlen % fragsize) or fragsize) - -= Packet().fragment() and overloaded_fields -pkt1 = Ether() / IP() / UDP() -pkt2 = pkt1.fragment()[0] -pkt3 = pkt2.__class__(raw(pkt2)) -assert pkt1[IP].proto == pkt2[IP].proto == pkt3[IP].proto - -= Packet().fragment() already fragmented packets -payloadlen = 1480 * 3 -ffrags = (IP() / ("X" * payloadlen)).fragment(1480) -ffrags = reduce(lambda x, y: x + y, (pkt.fragment(1400) for pkt in ffrags)) -len(ffrags) == 6 -* each fragment except the last one should have MF set -assert all(p.flags == 1 for p in ffrags[:-1]) -assert ffrags[-1].flags == 0 -* fragment offset should be well computed -plen = 0 -for p in ffrags: - assert p.frag == plen / 8 - plen += len(p.payload) - -assert plen == payloadlen += Check wireshark() +~ wireshark +f = BytesIO() +pkt = Ether()/IP()/ICMP() -############ -############ -+ TCP/IP tests -~ tcp +with mock.patch('subprocess.Popen', return_value=Bunch(stdin=f)) as popen: + # Prevent closing the BytesIO + with mock.patch.object(f, 'close'): + wireshark([pkt]) -= TCP options: UTO - basic build -raw(TCP(options=[("UTO", 0xffff)])) == b"\x00\x14\x00\x50\x00\x00\x00\x00\x00\x00\x00\x00\x60\x02\x20\x00\x00\x00\x00\x00\x1c\x04\xff\xff" +popen.assert_called_once_with( + [conf.prog.wireshark, '-ki', '-'], + stdin=subprocess.PIPE, stdout=None, stderr=None) -= TCP options: UTO - basic dissection -uto = TCP(b"\x00\x14\x00\x50\x00\x00\x00\x00\x00\x00\x00\x00\x60\x02\x20\x00\x00\x00\x00\x00\x1c\x04\xff\xff") -uto[TCP].options[0][0] == "UTO" and uto[TCP].options[0][1] == 0xffff +print(bytes_hex(f.getvalue())) +assert raw(pkt) in f.getvalue() +f.close() +del f, pkt -= TCP options: SAck - basic build -raw(TCP(options=[(5, b"abcdefgh")])) == b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x80\x02 \x00\x00\x00\x00\x00\x05\nabcdefgh\x00\x00" += Check Raw IP pcap files -= TCP options: SAck - basic dissection -sack = TCP(b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x80\x02 \x00\x00\x00\x00\x00\x05\nabcdefgh\x00\x00") -sack[TCP].options[0][0] == "SAck" and sack[TCP].options[0][1] == (1633837924, 1701209960) +import tempfile +filename = tempfile.mktemp(suffix=".pcap") +wrpcap(filename, [IP()/UDP(), IPv6()/UDP()], linktype=DLT_RAW) +packets = rdpcap(filename) +assert isinstance(packets[0], IP) and isinstance(packets[1], IPv6) -= TCP options: SAckOK - basic build -raw(TCP(options=[('SAckOK', b'')])) == b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00`\x02 \x00\x00\x00\x00\x00\x04\x02\x00\x00" += Check wrpcap() with no packet -= TCP options: SAckOK - basic dissection -sackok = TCP(b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00`\x02 \x00\x00\x00\x00\x00\x04\x02\x00\x00") -sackok[TCP].options[0][0] == "SAckOK" and sackok[TCP].options[0][1] == b'' +import tempfile +filename = tempfile.mktemp(suffix=".pcap") +wrpcap(filename, []) +fstat = os.stat(filename) +assert fstat.st_size != 0 +os.remove(filename) -= TCP options: EOL - basic build -raw(TCP(options=[(0, '')])) == b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00`\x02 \x00\x00\x00\x00\x00\x00\x00\x00\x00" += Check wrpcap() with SndRcvList -= TCP options: EOL - basic dissection -eol = TCP(b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00`\x02 \x00\x00\x00\x00\x00\x00\x02\x00\x00") -eol[TCP].options[0][0] == "EOL" and eol[TCP].options[0][1] == None +import tempfile +filename = tempfile.mktemp(suffix=".pcap") +wrpcap(filename, SndRcvList(res=[(Ether()/IP(), Ether()/IP())])) +assert len(rdpcap(filename)) == 2 +os.remove(filename) -= TCP options: malformed - build -raw(TCP(options=[('unknown', b'')])) == raw(TCP()) += Check wrpcap() with different packets types -= TCP options: malformed - dissection -raw(TCP(b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00`\x02 \x00\x00\x00\x00\x00\x03\x00\x00\x00")) == b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00`\x02 \x00\x00\x00\x00\x00\x03\x00\x00\x00" +from unittest import mock +import os +import tempfile -= TCP options: wrong offset -TCP(raw(TCP(dataofs=11)/b"o")) +with mock.patch("scapy.utils.warning") as warning: + filename = tempfile.mktemp() + wrpcap(filename, [IP(), Ether(), IP(), IP()]) + os.remove(filename) + assert any("Inconsistent" in arg for arg in warning.call_args[0]) -= TCP options: MPTCP - basic build using bytes -raw(TCP(options=[(30, b"\x10\x03\xc1\x1c\x95\x9b\x81R_1")])) == b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x80\x02 \x00\x00\x00\x00\x00\x1e\x0c\x10\x03\xc1\x1c\x95\x9b\x81R_1" += Check wrpcap() with the Loopback layer +~ tshark -= TCP options: invalid data offset -data = b'\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x80\x02 \x00\x1b\xb8\x00\x00\x02\x04\x05\xb4\x04\x02\x08\x06\xf7\xa26C\x00\x00\x00\x00\x01\x03\x03\x07' -p = TCP(data) -assert TCP in p and Raw in p and len(p.options) == 3 +for cls in [Loopback, LoopbackOpenBSD]: + filename = tempfile.mktemp(suffix=".pcap") + wrpcap(filename, [cls()/IP()/ICMP()]) + return_value = b"".join(line for line in tcpdump(filename, prog=conf.prog.tshark, getfd=True)) + assert b"Echo (ping) request" in return_value -= TCP options: build oversized packet +############ +############ ++ ERF Ethernet format support -raw(TCP(options=[('TFO', (1607681672, 2269173587)), ('AltChkSum', (81, 27688)), ('TFO', (253281879, 1218258937)), ('Timestamp', (1613741359, 4215831072)), ('Timestamp', (3856332598, 1434258666))])) += Variable creations +erffile = BytesIO(b'3;!E_9\x92_\x02\x04\x00p\x00\x00\x00P\x00\x00\x00\x0fS?\xca\xc0\x1cjz\x18\x90\xed\x81\x00\x01:\x08\x00E\x00\x00(\xdf\xab@\x00;\x06\xb3s\n\x01]\xdb\n\xfb9\xda\xc3v\x84\xecD\x16\xb9\xab\xda\xa1b\xf9P\x10f\x98\x18\xcb\x00\x00\x00\x00\x90\x9e\xd7\xd2_\x929_\x0f\x9e\xcd\x1f\x01\x88\xb9\x15[/s<\x01\x88\xb9\x15[/\xcd\x1f\x01\x88\xb9\x15[/0\xcd"E_9\x92_\x02\x04\x00p\x00\x00\x00P\x00\x00\x1cjz\x18\x90\xed\x00\x0fS?\xca\xc0\x08\x00E\x00\x00(\xa2\xdd@\x00@\x06\xebA\n\xfb9\xda\n\x01]\xdb\x84\xec\xc3v\xda\xa1b\xf9D\x16\xb9\xacP\x10\x9a\xf0\xe4q\x00\x00\x00\x00\x00\x00\x00\x00o\xbc\xe2{_\x929_\x0f\x9f+3\x01\x88\xb9\x15u\x1e(^\x01\x88\xb9\x15u\x1e+3\x01\x88\xb9\x15u\x1e') +erffilewithheader = BytesIO(b'4;!E_9\x92_\x82\x00\x00x\x00\x00\x00P\x00\x00\x1a+